Prhub

#53396 [Kimi K3][Kernel] Support DS conv-state layout in fused KDA decode kernel

原始 PR 作者 gcanlin 合并时间 2026-08-27 14:54 文件变更 5 提交数 5 评论 19 代码增减 +213 / -106

执行摘要

KDA decode 融合内核补齐 DS 布局支持,消除性能回退

PR body 明确指出:生产 K3 服务固定使用 VLLM_SSM_CONV_STATE_LAYOUT=DS(NIXL PD transfer 要求),而此前的 fused_kda_decode 要求 SD 布局,导致 DS 部署下融合内核从未被使用,模型 69 层 KDA 全部静默回退到多 op Triton decode 链;回退链在 B200 上慢 1.16-1.58x,且每层 decode 链的 launch 次数约 3 倍(8-9 个 vs 3 个内核)。

值得精读。该 PR 是“功能可用性修复 + 性能优化”的典型样例:用一个看似微小的模板改动,消除了生产配置下静默的性能回退,并提供了完整的 microbenchmark、E2E 与 SASS 位级验证链条。对 kernel 工程师,compile-time specialization 与 enum vs NTTP 的取舍、以及用 cuobjdump 对比 SASS 证明代码等价的方法尤其值得借鉴。

讨论亮点

review 中 gau-nernst 提出两个关键设计反馈:一是用两个整型 stride 模板参数替代 ConvStateLayout 枚举,二是把 conv_state_offset helper 内联。作者 gcanlin 最初尝试 stride-NTTP 版本时测出 B200 上 18.3%(DS)/ 7.2%(SD)回归,gau-nernst 认为异常并质疑为何还不如动态 stride;随后作者借助 K3 重写,得到与枚举版本 SASS 位级一致的实现,回归被归因于此前混入的无关改动。另有对 SD 路径是否回归的疑问,特化版本实测几何平均 -0.00%,完全恢复基线。

实现拆解

  1. 内核模板特化(csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel.cu):kda_decode_fusion_many_heads_kernel 模板尾部新增 kConvStateChannelStride、kConvStateTapStride 两个 int NTTP;conv-state 读取与回写索引从 hk + w * kPackedDim 改为 hk * kConvStateChannelStride + w * kConvStateTapStride,默认参数对应 SD 布局(<1, 3 * H_local * 128>)。
  2. 启动时布局解析与实例分派:在 head-count dispatch 处根据 conv_state 张量的运行时 strides 识别 SD / DS(SD = <1, 3 * H * 128>,DS = <kConvWidth - 1, 1>),两者都被显式实例化,保证地址计算始终是编译期常量。
  3. 启用条件变更(vllm/models/kimi_k3/nvidia/kda.py):is_fused_kda_decode_supported 移除 or is_conv_state_dim_first() 判断,DS 布局不再被拒之门外,融合路径对生产布局开放。
  4. ROCm 同步(csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu):kda_decode_fusion_kernel 以相同的双 stride 模板参数替换 kPackedDim 常量寻址,保持 CUDA / ROCm 行为一致。
  5. 测试与基准配套:tests/models/kimi_k3/test_kda.py 的 test_fused_kda_decode_correctness 参数化 conv_layout(SD / DS 各 5 组),按布局构造 conv_seed,并用 as_strided 以对应 stride 校验缓存读写;benchmarks/kernels/benchmark_kimi_k3_kda_decode.py 的 Inputs 接受 conv_layout 参数、main 新增 --layout 开关,使两种布局都能在 CUDA graph replay 下被基准覆盖。
文件 模块 状态 重要度
csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel.cu 融合内核 modified 6.01
csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel_rocm.cu 兼容内核 modified 5.08
vllm/models/kimi_k3/nvidia/kda.py 模型接入 modified 5.43
tests/models/kimi_k3/test_kda.py 内核测试 modified 5.99
benchmarks/kernels/benchmark_kimi_k3_kda_decode.py 内核基准 modified 6.3

关键符号

kda_decode_fusion_many_heads_kernel kda_decode_fusion_kernel is_fused_kda_decode_supported Inputs.__init__ test_fused_kda_decode_correctness

关键源码片段

csrc/libtorch_stable/kimi_k3/fused_kda_decode_kernel.cu core-logic

融合 decode 内核核心实现:为 kda_decode_fusion_many_heads_kernel 增加 kConvStateChannelStride / kConvStateTapStride 两个整型模板参数,conv-state 读写全部改为编译期 stride 寻址,是本次性能收益的根本来源。

// conv-state 布局通过两个编译期整型 stride 模板参数特化,SD / DS 各生成一套实例:
// SD 布局(channel 最内层):kConvStateChannelStride = 1,
// kConvStateTapStride = 3 * kFixedHeads * kDimK(即每条 tap 的通道数);
// DS 布局(tap 最内层):kConvStateChannelStride = kConvWidth - 1,
// kConvStateTapStride = 1。
// 运行时仅按 conv_state 张量的 strides 选择已有特化实例,两个值始终保持编译期常量,
// 避免动态 stride 带来的整数指令与寄存器压力(实测寄存器由 64 增至 72)。
template <
    bool kApplyOnorm, bool kUseStaticDecodeLayout = false, int kFixedHeads = 0,
    int kFixedValueHeads = 0, bool kUseHeadGrid = false,
    bool kAccumulateOnormSumsq = false, bool kUseActiveQkReduction = false,
    bool kUseCacheGlobalStore = false, bool kComputeOutputBeforeStore = false,
    bool kSkipWarpSync = false, bool kPreloadOnormParams = false,
    bool kPrefetchNextStateChunk = false, bool kUseActiveOnormReduction = false,
    bool kUpdateConvState = false, bool kUseLowerBound = false,
    bool kApplyBetaSigmoid = true, int kConvStateChannelStride = 1,
    int kConvStateTapStride = 3 * kFixedHeads * kDimK>
__global__ __launch_bounds__(kThreads, 2)
void kda_decode_fusion_many_heads_kernel(/* ... */) {
    // 读取旧 tap 状态参与 1D 卷积累加。
    // 地址 = channel * 通道方向 stride + tap * tap 方向 stride,
    // 两 stride 均为模板常量,编译器直接生成立即数偏移。
    #pragma unroll
    for (int w = 0; w < kConvStateWidth; ++w) {
        const int cs_idx = hk * kConvStateChannelStride + w * kConvStateTapStride;
        const __nv_bfloat16 q_state = cs_q_for_slot[cs_idx];
        const __nv_bfloat16 k_state = cs_k_for_slot[cs_idx];
        q_acc += __bfloat162float(q_state) * conv_weight_load<kLocalDim>(w_q_t, hk, w);
        k_acc += __bfloat162float(k_state) * conv_weight_load<kLocalDim>(w_k_t, hk, w);
    }
    // 回写当前 tap 结果:shift0 / shift1 / 新值按相同编译期 stride 落位,
    // 与读取方向完全对称,确保整个 slot 内状态维护与布局无关。
    cs_q_for_slot[hk * kConvStateChannelStride] = q_shift0;
    cs_q_for_slot[hk * kConvStateChannelStride + kConvStateTapStride] = q_shift1;
    cs_q_for_slot[hk * kConvStateChannelStride + 2 * kConvStateTapStride] = q_new;
    /* k / v 通道与 onorm 归一化的后续分支与原实现一致,此处省略 */
}
tests/models/kimi_k3/test_kda.py test-coverage

test_fused_kda_decode_correctness 参数化 conv_layout,SD/DS 各 5 组,用于验证双布局下融合内核与参考实现数值一致。

@pytest.mark.parametrize(
    ("num_heads", "num_seqs", "lower_bound", "fuse_output_norm", "conv_layout"),
    [
        # SD 布局保持原有 5 组基线,DS 布局补齐同样 5 组,
        # 确保两种物理缓存布局都被融合内核覆盖。
        (12, 1, -5.0, True, "SD"), (12, 4, None, False, "SD"),
        (24, 4, None, False, "SD"), (48, 1, -5.0, True, "SD"),
        (96, 1, -5.0, True, "SD"),
        (12, 1, -5.0, True, "DS"), (12, 4, None, False, "DS"),
        (24, 4, None, False, "DS"), (48, 1, -5.0, True, "DS"),
        (96, 1, -5.0, True, "DS"),
    ],
)
@torch.inference_mode()
def test_fused_kda_decode_correctness(
    num_heads, num_seqs, lower_bound, fuse_output_norm, conv_layout,
):
    # DS cache layout:每个 slot 内 tap 最内层(stride (W-1, 1)),
    # 对应 VLLM_SSM_CONV_STATE_LAYOUT=DS 生产布局。
    if conv_layout == "DS":
        conv_seed = 0.1 * torch.randn(slots, 3 * dim, W - 1,
                                      dtype=torch.bfloat16, device=DEVICE)
    else:
        # SD cache layout:每个 slot 内 channel 最内层,tap 外层。
        conv_seed = 0.1 * torch.randn(slots, W - 1, 3 * dim,
                                      dtype=torch.bfloat16, device=DEVICE
                                      ).transpose(1, 2)
    # 同一块缓存存储按两种布局分别解释,校验内核的实际访存 stride。
    conv_actual = torch.as_strided(
        cache_storage.view(torch.bfloat16),
        size=(slots, 3 * dim, W - 1),
        stride=(
            page_bytes // torch.bfloat16.itemsize,
            (W - 1) if conv_layout == "DS" else 1,
            1 if conv_layout == "DS" else 3 * dim,
        ),
    )

评论区精华

模板参数设计:ConvStateLayout 枚举 vs 两个整型 stride 设计

gau-nernst 建议直接用两个整型 stride 模板参数替代 ConvStateLayout 枚举;gcanlin 最初尝试时测出 18.3%(DS)/ 7.2%(SD)回归,gau-nernst 质疑为何还不如动态 stride;随后 gcanlin 用 K3 重写,达到与枚举版本 SASS 位级一致。

结论:接受建议,改为 kConvStateChannelStride / kConvStateTapStride 两个 int NTTP,并内联 stride 运算,性能与枚举版一致。 · 已解决

SD 布局回归验证 性能

gau-nernst 在 issue 评论要求 microbenchmark 证明现有 SD codepath 无回归;gcanlin 先给出小 batch +1.3%~+4.8% 的初版数据,随后特化版本在 SD 上几何平均 -0.00%。

结论:特化版本完全恢复 SD 基线性能,结论写入 PR body。 · 已解决

conv_state_offset helper 内联 style

gau-nernst:'again just style but can you inline this?';gcanlin:'Sure. Done.'

结论:内联到调用点,删除 helper,生成 SASS 保持位级一致。 · 已解决

风险与影响

1)核心解码路径变更:这是 K3 每层 decode 的核心 kernel,改动决定所有 69 层 KDA 的行为,但数值正确性有 10 组参数化测试覆盖,并做了 128 个实例的 SASS 位级一致性对比,风险可控。2)模板实例增多使扩展二进制增加 392 KB(0.95%),对包体积和编译时间有轻微影响;3)ROCm 版本同步改动,作者明确请 @tjtanaa 与 @AndreasKaratzas 审阅,AMD 侧暂无性能数据,若不合适应回退;4)性能结论主要覆盖 B200 / H_local=12 / K=V=128,其他算力和头数组合的收益与回归尚未验证。

对使用 VLLM_SSM_CONV_STATE_LAYOUT=DS 的 K3 生产部署(NIXL P/D 分离)影响最大:decode 融合路径从不可用变为可用,每层 kernel 提速 1.22-1.87x,E2E 输出吞吐提升 3.84%;SD 布局用户无行为变化。对团队而言,kernel 维护面从单布局扩展到双布局特化,后续测试与基准需持续覆盖两种布局;benchmark 工具新增 --layout 维度,便于回归验证。

核心解码路径变更 DS/SD 双布局特化 ROCm 后端同步改动 模板实例膨胀

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论