Prhub

#33630 Add kda replayssm tests

原始 PR 作者 ispobock 合并时间 2026-08-10 09:11 文件变更 5 提交数 4 评论 13 代码增减 +776 / -0

执行摘要

KDA ReplaySSM 新增 5 组 parity 测试,锁定 gate 与 ring 正确性

PR body 说明这是 #32541 的 follow-up,并阐述了测试的必要性:

The GDN side of ReplaySSM has parity tests upstream; the KDA side landed with none... The whole feature rests on one claim, that the state rebuilt by replaying raw inputs is bit-identical to what the recurrent baseline would have committed. That claim is exactly what silently breaks: an earlier version of this path recomputed the gate on the torch side with a subtly different formula, which left every output looking correct while the state drifted underneath. End-to-end accuracy does not catch that; a parity test does.

即此前 KDA 侧没有任何 parity 测试,而该功能正确性恰好依赖“重放原始输入重建的状态与基线逐位一致”,并且曾真实发生过 gate 公式(plain softplus 与 K3 safe gate)不一致导致状态悄然漂移的 bug,端到端精度无法发现,必须靠 parity 测试才能拦住。

值得精读。虽然文件全是测试,但它们展示了高质量内核 parity 测试的设计范式:

1) 从原始输入同时驱动双侧,不让任何一侧预先享有 gate 公式;
2) 用不同 shape 矩阵(GQA、非 pow2、单请求、padding)钉死索引偏移类 bug;
3) 对性能敏感的批量折叠用位级相等而非数值容差;
4) 在测试标题中明确记录“该测试在修复前代码上会红”的历史价值。建议后续维护 KDA/ReplaySSM 相关内核的开发者以此为模板补充测试,也建议阅读 test_kda_replayssm_fold.py 中 safe gate 与 softplus 的分支设计,这是捕获真实回归的关键。

讨论亮点

本 PR 没有收到任何 review 评论(review_comments_count = 0),评论区全部是 /rerun-test 命令与 CI 结果回执。从中能提炼出一个有技术含量的点:

  • 第一次把 5 个测试一起放到 1-gpu-h100 上跑时失败,原因是 test_kda_mtp_cutedsl_replayssm_ring.py 依赖 SM100 的 CuTe DSL 内核,h100 的 sm_90a 无法编译;
  • 作者随后通过提交“keep the cutedsl ring test on b200”将 CuTe 测试单独固定到 4-gpu-b200,后续 rerun 全部通过,说明该测试的硬件约束被 CI 配置正确吸收。

这同时印证了测试文件中的注释:k3 等非 SM100 平台跑这个文件是编译失败而非数值失败,因此必须选择正确的 runner_config。

实现拆解

实现分四步推进,全部落在 test/registered/kernels/ 目录下:

  1. 确立 parity 测试方法:所有测试都采用“双臂对照”结构。baseline 臂用 fused_sigmoid_gating_delta_rule_update(is_kda=True, disable_state_update=True) 让内核内部生成 gate,并把每步状态快照写入 intermediate_states_buffer;ring 臂让同一内核以 cache_ring=True 模式把 rawv/rawk/gk/beta 写入每 slot 的环形缓冲,再用 commit_kda_replayssm_spec(或 commit_kda_replayssm_spec_all_layers)把环折叠回 checkpoint。两侧输入完全相同,最后比较折叠结果与基线最后一步快照的相对误差。关键设计是“两侧都从原始 (a, b, A_log, dt_bias) 构造”,避免直接喂同一个 gk 导致 gate 公式差异被掩盖。
  2. 分文件覆盖不同风险面
    • test_kda_replayssm_fold.py:显式覆盖 K3 的 safe gate(lower_bound * sigmoid(exp(A_log) * x),lower_bound = -5.0)与普通 softplus 两个分支,直接钉死 gate 公式;
    • test_kda_replayssm_ring_fused.py:验证融合进 verify 内核的 ring 写入(CACHE_RING=True)与内核自身上一步状态一致,覆盖 GQA、非 pow2 K/V、单请求、padding 槽位;
    • test_kda_replayssm_ring_ragged.py:覆盖变长(varlen)验证布局下的 ring 写入,含 full/partial commit 与 padding 槽位;
    • test_kda_replayssm_fold_batched.py:验证 layer-batched 折叠(一次 launch)与逐层循环调用结果 torch.equal 位级相同,防层偏移/stride bug;
    • test_kda_mtp_cutedsl_replayssm_ring.py:SM100 专属,覆盖 CuTe MTP 验证内核的 CACHE_RING 模式,额外校验输出张量在两种模式下 bitwise 相等以及 CUDA graph padding 槽位不被写坏。
  3. CI 注册:使用 register_cuda_ci(est_time=..., stage="base-b-kernel-unit", runner_config=...) 将测试注册进 kernel-unit stage,fold 系列与 fused/ragged 用 1-gpu-large,CuTe MTP 因 SM100 限制用 4-gpu-b200
  4. 演进修正:提交记录显示先添加测试、再注册 CI,随后发现 CuTe 测试在 1-gpu-h100 上失败(libNVVM 拒绝为 sm_90a 生成设备 IR),通过“keep the cutedsl ring test on b200”提交将之固定到 b200 runner,最终合并 main,全部 41 个用例在 8x B300 上通过。
文件 模块 状态 重要度
test/registered/kernels/test_kda_replayssm_fold.py 折叠测试 added 7.3
test/registered/kernels/test_kda_mtp_cutedsl_replayssm_ring.py 内核测试 added 7.39
test/registered/kernels/test_kda_replayssm_ring_ragged.py 环形测试 added 7.28
test/registered/kernels/test_kda_replayssm_fold_batched.py 批量折叠 added 6.43
test/registered/kernels/test_kda_replayssm_ring_fused.py 环形融合 added 6.21

关键符号

test_cutedsl_ring_fold_parity test_cutedsl_fused_output_norm test_cutedsl_cuda_graph_padding_slot_is_safe test_ragged_full_commit test_ragged_partial_commit test_ragged_pad_slot TestKDAReplaySSMFoldParity._parity test_safe_gate test_softplus_gate test_fold_batched_matches_per_layer test_ring_fold_parity

关键源码片段

test/registered/kernels/test_kda_replayssm_fold.py test-coverage

最关键的 parity 测试:从原始输入同时驱动 verify 内核与 fold 路径,显式覆盖 K3 safe gate 与普通 softplus 两个分支,能捕获真实发生过的 gate 公式漂移 bug(body 明确提到修复前会使此测试变红)。

def _parity(self, lower_bound):
    # 双臂 parity:baseline 由 verify 内核内部生成 gate,fold 侧在 torch 里
    # 重算 gk/beta 写入 ring 再折叠。两侧输入完全一致,只有 gate 来源不同,
    # 因此 gate 公式不一致会直接表现为相对误差超限。
    dev = "cuda"
    B, T, HV, K, V, H, L = self.B, self.T, self.HV, self.K, self.V, self.H, self.L
    scale = K**-0.5
    torch.manual_seed(0)
​
    # 使用固定的随机种子构造 q/k/v/a/b/A_log/dt_bias/h0,保证可复现。
    q = torch.randn(B, T, H, K, device=dev, dtype=torch.float32)
    k = torch.randn(B, T, H, K, device=dev, dtype=torch.float32)
    v = torch.randn(B, T, HV, V, device=dev, dtype=torch.float32)
    a = torch.randn(B, T, HV, K, device=dev, dtype=torch.float32)
    b = torch.randn(B, T, HV, device=dev, dtype=torch.float32)
    A_log = torch.randn(HV, device=dev, dtype=torch.float32)
    dt_bias = torch.randn(HV, K, device=dev, dtype=torch.float32)
    h0 = torch.randn(B, HV, V, K, device=dev, dtype=torch.float32)
​
    # slot 从 1 开始(0 保留给无效槽位),与生产内存池约定一致。
    slots = torch.arange(1, B + 1, device=dev, dtype=torch.int32)
    num_slots = B + 1
​
    # baseline 臂:verify 内核内部按 KDA 规则形成 gate,
    # 并把每步状态写入 intermediate_states_buffer(禁掉最终 state 更新)。
    h0_src = torch.zeros(num_slots, HV, V, K, device=dev, dtype=torch.float32)
    for j in range(B):
        h0_src[slots[j]] = h0[j]
    inter = torch.zeros(num_slots, T, HV, V, K, device=dev, dtype=torch.float32)
    fused_sigmoid_gating_delta_rule_update(
        A_log=A_log, a=a, dt_bias=dt_bias,
        softplus_beta=1.0, softplus_threshold=20.0,
        q=q, k=k, v=v, b=b,
        initial_state_source=h0_src, initial_state_indices=slots,
        scale=scale, use_qk_l2norm_in_kernel=True,
        is_kda=True, lower_bound=lower_bound,
        disable_state_update=True,
        intermediate_states_buffer=inter, intermediate_state_indices=slots,
        cache_steps=T,
    )
    accept = T # 提交完整窗口
    base = torch.stack([inter[slots[j], accept - 1] for j in range(B)], 0)
​
    # fold 臂:在 torch 侧按两个分支显式重算 gate,避免两端共用同一个 gk
    # 而掩盖公式差异。这是本测试能抓到 softplus/safe-gate 漂移的关键。
    x = a + dt_bias.view(1, 1, HV, K)
    exp_a_log = torch.exp(A_log).view(1, 1, HV, 1)
    if lower_bound is not None:
        gk = lower_bound * torch.sigmoid(exp_a_log * x)
    else:
        gk = -exp_a_log * torch.nn.functional.softplus(x)
    beta = torch.sigmoid(b)
​
    # 把 v/k/gk/beta 转置后写入 slot 的 ring,再调用 commit 折叠回 checkpoint。
    rawv = torch.zeros(num_slots, HV, L, V, device=dev, dtype=torch.float32)
    rawk = torch.zeros(num_slots, H, L, K, device=dev, dtype=torch.float32)
    gkr = torch.zeros(num_slots, HV, L, K, device=dev, dtype=torch.float32)
    betar = torch.zeros(num_slots, HV, L, device=dev, dtype=torch.float32)
    ckpt = torch.zeros(num_slots, HV, V, K, device=dev, dtype=torch.float32)
    for j in range(B):
        s = slots[j].item()
        rawv[s, :, :T] = v[j].transpose(0, 1)
        rawk[s, :, :T] = k[j].transpose(0, 1)
        gkr[s, :, :T] = gk[j].transpose(0, 1)
        betar[s, :, :T] = beta[j].transpose(0, 1)
        ckpt[s] = h0[j]
    acc = torch.full((B,), accept, device=dev, dtype=torch.int32)
    commit_kda_replayssm_spec(
        ckpt, rawv, rawk, gkr, betar, slots, acc,
        max_cache_len=L, num_k_heads=H, use_qk_l2norm_in_kernel=True,
    )
    fold = torch.stack([ckpt[slots[j].item()] for j in range(B)], 0)
​
    # 相对误差必须小于 1e-3,fold 与 verify 内核状态应高度一致。
    rel = ((fold - base).abs().max() / base.abs().max().clamp_min(1e-6)).item()
    self.assertLess(rel, 1e-3, f"fold vs verify parity failed: rel={rel:.3e}")

评论区精华

SM100 专属 CuTe 测试的 runner 选择 测试

多条 /rerun-test 命令显示 5 个测试在 1-gpu-h100 上失败,随后通过提交 'keep the cutedsl ring test on b200' 将 CuTe MTP 测试固定到 4-gpu-b200,rerun 全部通过。

结论:CuTe DSL 内核仅支持 SM100(libNVVM 拒绝为 sm_90a 生成设备 IR),该测试必须运行在 b200,不能与其他测试混在 h100 上。 · 已解决

风险与影响

具体风险如下:

  • 硬件依赖test_kda_mtp_cutedsl_replayssm_ring.py 仅能在 SM100 上运行,并要求 runner 池中有 4-gpu-b200;若 CI 机器池变动,该测试可能无法调度或被迫跳过,降低 KDA CuTe 路径的覆盖。
  • 容差设置:CuTe 测试相对误差阈值给到 2e-2(bf16 存储所限),理论上可能放过小幅数值漂移;其余测试为 1e-3,更严格。若后续调整阈值应结合 bf16 精度理解。
  • 回归防护面:测试全部为 parity 性质,若 KDA ReplaySSM 未来的行为有意改变(如 gate 公式变更),测试会失败并需要显式更新,这是正面约束,但对不熟悉该路径的开发者可能产生困惑。
  • CI 时长:新增 5 个 GPU 内核测试(每个约 15-27 秒),虽然注册到 kernel-unit stage,但仍会占用 CI 资源;若并发 runner 不足可能拖慢合入节奏。
  • 无源码改动:本 PR 不改任何生产代码,不会直接引入回归,但它把当前 KDA ReplaySSM 行为固化为契约,任何相关源码改动都会在此显性暴露。

影响范围集中在测试与 CI 侧:

  • 对用户:无运行时影响,但 KDA ReplaySSM 路径获得可执行的正确性护栏,防止“输出正常但状态漂移”的静默回归再次出现。
  • 对系统:CI 新增 5 个 GPU 内核测试,其中 1 个要求 SM100(b200 runner),其余在 1-gpu-large 上运行,增加约 100 秒的 kernel-unit 总时长。
  • 对团队:明确了该功能的正确性契约——gate 公式必须与 K3 一致(safe gate)、ring 偏移/层折叠必须位级一致、padding 槽位必须零写入,后续开发与 code review 有了具体依据。
  • 与 GDN 侧已有 parity 测试形成对照,补齐了 KDA 侧缺失的另一半覆盖,使 ReplaySSM 两条实现路径都具备回归保障。
纯测试 PR,无源码主路径改动 SM100-only 测试依赖 b200 runner gate 公式回归是核心风险 新增 GPU CI 时长

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论