Prhub

#53053 [Kimi-K3] Extend GEMM-RS to GEMM-AR

原始 PR 作者 gau-nernst 合并时间 2026-08-21 11:44 文件变更 9 提交数 10 评论 8 代码增减 +481 / -232

执行摘要

Kimi-K3 新增 GEMM-AR 融合内核,kernel 内完成 all-reduce

PR body 明确说明这是对 #52079 的 GEMM-RS 的扩展,并取代早前的 #52687:"Extends GEMM-RS, introduced by #52079, to GEMM-AR by replacing unicast store with multimem.st"。相对 #52687 的优势是 "Any value of M is supported -> no padding/input copy is required" 与 "Flag signalling/synchronization is fully done inside the kernel -> no flag reset + entry barrier is needed"。动机是让 non-SP 部署也能吃到融合聚合 GEMM 的性能红利:微基准在 M>=128 时约 1.0-1.21x,M=32768 时达 1.21x;E2E 在并发 8 时 TTFT 降低 5.74%,吞吐提升 1.22%。

值得精读。核心看点:(1) 用 CuTe DSL 的 llvm.inline_asm 封装 multimem.st 多播存储,把 collective 语义融合进 GEMM 内核的实现手法;(2) 输出 workspace 复用带来的生命周期设计权衡与对应测试锚点;(3) 初始化失败优雅降级(NVLink 缺失时回退普通 GEMM)的容错模式。若后续计划支持非 NVLink 多播环境或消除输出 clone,应在此 PR 基础上迭代。

讨论亮点

核心讨论集中在 gemm_rs_ar.py 第 731 行的 NVLink 多播校验:wzhao18 质疑默认启用融合时,遇到无 NVLink 多播的环境(例如 2 个 B200 节点组成的 TP)不应直接 raise,建议返回 None 并记录不支持日志;随后他自查确认 model.py 的 maybe_init_gemm_rs_ar 已用 try-except 包裹 init_gemm_rs_ar,捕获 RuntimeError 后告警并降级禁用,于是撤回意见("never mind just saw the try-except handling. so it is safe."),并 APPROVE 且请 zyongye 二次复核后合入。另外由于 PR 来自 fork,Claude 自动 review 被跳过(claude[bot] 注明 automated review is disabled),只能依赖人工审核。

实现拆解

整体分四步推进,配套修改 9 个文件:

  1. 内核层:RS 泛化为 RS/AR 双模式。 vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs.py 重命名为 gemm_rs_ar.py,新增 @dsl_user_op 的 multimem_st_16B,通过 llvm.inline_asm 发射 multimem.st.relaxed.sys.global.v4.f32 多播存储指令;Sm100GemmRsBF16 泛化为 Sm100GemmRsArBF16,构造参数新增 all_reduce。kernel 内 local_M 在 AR 模式下取 partial_uc.shape[0] // num_ranks(全局行数),RS 模式仍取本 rank 的 output.shape[0];epilogue 只在 RS 模式做 output 向量切分,AR 模式由通信 warp 把部分和通过 multimem_st_16B 多播写回对称内存 workspace,从而把 all-reduce 语义融进 GEMM 内核。

  2. 特性开关与初始化降级。 vllm/envs.py 新增 VLLM_KIMI_K3_GEMM_AR;vllm/models/kimi_k3/nvidia/model.py 把 maybe_init_gemm_rs 重构为 maybe_init_gemm_rs_ar,以 all_reduce = not use_sequence_parallel 决定模式(SP 开启走 RS、关闭走 AR),保留原有 BF16/SM100 系/TP 2-16 且 128 可被 TP 整除的校验链;init_gemm_rs_ar 初始化时若 multicast_ptr == 0(典型如跨节点无 NVLink 多播)会抛出 RuntimeError,由上层 try-except 捕获后告警并降级禁用,回退到普通 o_proj 线性层。

  3. 模型调用链接入。 KimiMLP、KimiKda、KimiMla 从布尔标志 run_gemm_rs 切换到 self.gemm_rs_ar 单例引用:初始化时用 get_gemm_rs_ar().can_run(weight) 判断投影形状是否兼容,forward 时用 should_run(input) 判断 token 数是否达到 M>=128 的调度门槛,命中即返回融合内核结果,否则落入常规 o_proj。注释明确 RS 要求 sequence sharding,而 AR 作用于 replicated tokens。

  4. 测试、基准、预热与 CI 配套。 单测与 benchmark 整体重命名为 *_gemm_rs_ar.py:单测抽出 _run_mode 双模式参数化,_reference 在 AR 模式下改用 dist.all_reduce 作参考实现,并新增 AR 专属的 workspace 复用生命周期校验(首次输出快照在二次调用后仍须有效);benchmark 新增 --mode rs|ar 参数与 make_torch_ring_ll_gemm_collective、make_torch_ldmc_gemm_collective、make_fused_gemm_collective 等集体通信基线;vllm/model_executor/warmup/kernel_warmup.py 新增 _warmup_kimi_k3_gemm_rs_ar,用 sys.modules.get 探测 gemm_rs_ar 模块是否已加载,避免非 Kimi-K3 模型触发无关 JIT 编译;.buildkite/test_areas/distributed.yaml 同步更新分布式测试覆盖范围。

文件 模块 状态 重要度
vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py 融合内核 renamed 8.05
vllm/models/kimi_k3/nvidia/model.py 模型集成 modified 8.08
tests/kernels/test_kimi_k3_gemm_rs_ar.py 内核测试 renamed 7.61
vllm/models/kimi_k3/nvidia/kda.py 注意力模块 modified 6.6
vllm/model_executor/warmup/kernel_warmup.py 预热逻辑 modified 6.65
benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py 性能基准 renamed 8.39
vllm/models/kimi_k3/nvidia/mla.py 注意力模块 modified 6.2
vllm/envs.py 环境变量 modified 4.67
.buildkite/test_areas/distributed.yaml CI 配置 modified 3.61

关键符号

multimem_st_16B Sm100GemmRsArBF16.__init__ Sm100GemmRsArBF16.kernel init_gemm_rs_ar get_gemm_rs_ar warmup_gemm_rs_ar maybe_init_gemm_rs_ar KimiMLP.__init__ KimiKda.forward KimiAttention.forward _warmup_kimi_k3_gemm_rs_ar _run_mode benchmark_shape

关键源码片段

vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py core-logic

核心内核文件:新增 multimem_st_16B 多播存储 DSL 算子,Sm100GemmRsBF16 泛化为 Sm100GemmRsArBF16 并支持 all_reduce 模式,是 GEMM-AR 的实现主体

# ===== multimem_st_16B:多播存储原语 =====
# 将 16B 向量写入 NVLink 多播地址,使同一 NVLink 域内所有 rank 都读到同一份数据。
# 相比 GEMM-RS 的单播 store,这是 GEMM-AR 能实现内核内 all-reduce 的关键。
@dsl_user_op
def multimem_st_16B(dst: cute.Tensor, value: cute.Tensor, *, loc=None, ip=None) -> None:
    # 向量指令要求 dst 与 value 是连续的 BF16x8 向量,且 dst 16 字节对齐。
    assert dst.element_type == BFloat16
    assert value.element_type == BFloat16
    ptr = dst.iterator.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
    regs = cute.recast_tensor(value, Int32, loc=loc, ip=ip)
    # 内联汇编发射 PTX:multimem.st.relaxed.sys.global.v4.f32 一次写 4 个寄存器。
    llvm.inline_asm(
        None,
        [ptr] + [regs[i].ir_value(loc=loc, ip=ip) for i in range(4)],
        "multimem.st.relaxed.sys.global.v4.f32 [$0], {$1, $2, $3, $4};",
        "l,r,r,r,r",
        has_side_effects=True,
        loc=loc,
        ip=ip,
    )
​
​
# ===== Sm100GemmRsArBF16:RS/AR 双模式内核 =====
class Sm100GemmRsArBF16:
    """SM100 BF16 GEMM,内核内融合 TP 维 reduce-scatter(RS)或 all-reduce(AR)。"""
​
    def __init__(
        self,
        *,
        rank: int,
        num_ranks: int,
        BN: int = 128,
        cta_group: int = 1,
        all_reduce: bool = False,
    ) -> None:
        self.rank = rank
        self.num_ranks = num_ranks
        BM, BK = 128, 64
        self.cta_tile = (BM, BN, BK)
        self.cta_group = cta_group
        self.all_reduce = all_reduce # 决定写回路径与 local_M 语义
​
    # ---- kernel() 内与 RS/AR 相关的核心分支(节选)----
    # AR 模式下每个 rank 持有全局 partial 的部分和,local_M 取总行数再按 rank 数
    # 切分;RS 模式下 output 只是本 rank 的序列分片,local_M 直接取 output 行数。
    local_M = (
        partial_uc.shape[0] // num_ranks
        if self.all_reduce
        else output.shape[0]
    )
​
    # 通信 warp(tid 4-7 区间)负责跨 rank 归约;epilogue 仅在 RS 模式需要把
    # 结果切分写入 output,AR 模式由 multimem_st_16B 直接多播写回 workspace。
    if cutlass.const_expr(not self.all_reduce):
        output_vecs = cute.zipped_divide(output, (1, vec_width))
vllm/models/kimi_k3/nvidia/model.py integration

模型集成入口:maybe_init_gemm_rs 重构为 maybe_init_gemm_rs_ar,按 SP 拓扑选择 RS/AR 单例,并加入 NVLink 多播缺失时的优雅降级

def maybe_init_gemm_rs_ar(vllm_config: VllmConfig, use_sequence_parallel: bool) -> bool:
    # 两个特性开关可能同时打开,但 worker 的静态 SP 拓扑决定单例只能绑定一种
    # 模式:开启 sequence parallel 时运行 GEMM-RS,否则运行 GEMM-AR。
    all_reduce = not use_sequence_parallel
    mode = "GEMM-AR" if all_reduce else "GEMM-RS"
    enabled = envs.VLLM_KIMI_K3_GEMM_AR if all_reduce else envs.VLLM_KIMI_K3_GEMM_RS
    if not enabled:
        return False
​
    parallel_config = vllm_config.parallel_config
    tp_size = parallel_config.tensor_parallel_size
    # 逐项校验运行条件,任一不满足就给出禁用原因并回退到普通 GEMM 路径。
    if parallel_config.use_ubatching:
        reason = "ubatching is enabled"
    elif vllm_config.model_config.dtype != torch.bfloat16:
        reason = "the model dtype is not BF16"
    elif not current_platform.is_cuda():
        reason = "the device is not CUDA"
    elif not current_platform.is_device_capability_family(100):
        reason = "the device is not SM100-family"
    elif not 1 < tp_size <= 16:
        reason = "TP size is not in the supported range 2-16"
    elif 128 % tp_size != 0:
        reason = "TP size does not divide 128"
    else:
        reason = None
​
    if reason is not None:
        logger.warning_once("%s is disabled because %s.", mode, reason)
        return False
​
    from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import init_gemm_rs_ar
​
    config = vllm_config.model_config.hf_text_config
    try:
        init_gemm_rs_ar(
            max_M=vllm_config.scheduler_config.max_num_batched_tokens,
            N=config.hidden_size,
            all_reduce=all_reduce,
        )
    except RuntimeError as e:
        # 典型场景:TP rank 不在同一 NVLink 域(如跨 2 个 B200 节点),
        # 对称内存多播指针为 0;捕获后告警并降级,保证服务仍可用。
        logger.warning_once(
            "%s is disabled because initialization failed: %s. This may mean "
            "the TP ranks do not share one NVLink domain.",
            mode,
            e,
        )
        return False
    if all_reduce:
        logger.info_once(
            "GEMM-AR is enabled. To disable it, set VLLM_KIMI_K3_GEMM_AR=0."
        )
    else:
        logger.info_once("GEMM-RS is enabled.")
    return True
tests/kernels/test_kimi_k3_gemm_rs_ar.py test-coverage

单测从 RS-only 扩展为 RS/AR 双模式,并新增 AR 专属的 workspace 复用生命周期校验,直接锚定本 PR 的核心限制

def _reference(
    x: torch.Tensor,
    weight: torch.Tensor,
    world_size: int,
    group: dist.ProcessGroup,
    all_reduce: bool,
) -> torch.Tensor:
    # 参考实现:torch.mm 计算全局 partial,再按模式补一次集体通信。
    M = x.shape[0]
    padded_M = (M + world_size - 1) // world_size * world_size
    partial = torch.empty(
        (padded_M, weight.shape[0]),
        dtype=x.dtype,
        device=x.device,
    )
    torch.mm(x, weight.T, out=partial[:M])
    if padded_M > M:
        partial[M:].zero_()
​
    if all_reduce:
        # AR 语义:所有 rank 拿到完整的全局结果。
        dist.all_reduce(partial, group=group)
        return partial[:M]
​
    # RS 语义:reduce-scatter 到每个 rank 的序列分片。
    output = torch.empty(
        (padded_M // world_size, weight.shape[0]),
        dtype=x.dtype,
        device=x.device,
    )
    dist.reduce_scatter_single(output, partial, group=group)
    return output
​
​
def _run_mode(
    *,
    all_reduce: bool,
    device: torch.device,
    group: dist.ProcessGroup,
    rank: int,
    world_size: int,
    weights: dict[int, torch.Tensor],
) -> None:
    # cute_dsl 仅在 CUDA 上可用,所以在 GPU worker 内部再导入,避免模块级导入失败。
    from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr
​
    gemm_rs_ar = GemmRsAr(
        max_M=max(M for M, _ in _SHAPES),
        N=_N,
        all_reduce=all_reduce,
    )
    input_generator = torch.Generator(device=device)
​
    # 交替形状覆盖不同 grid 大小、CTA-group 选择与 BN=128/BN=256 分派。
    for M, K in (*_SHAPES, *_SHAPES[::-1]):
        input_generator.manual_seed(2000 + M + K)
        x = torch.randn(
            M,
            K,
            dtype=torch.bfloat16,
            device=device,
            generator=input_generator,
        )
        expected = _reference(x, weights[K], world_size, group, all_reduce)
        actual = gemm_rs_ar(x, weights[K])
        torch.accelerator.synchronize(device)
        _assert_valid_rows_close(actual, expected, M, rank, all_reduce)
​
    if all_reduce:
        # AR 输出落在对称内存多播 workspace 上,第二次调用会复用同一块内存;
        # 这里验证 workspace 复用后第一次的结果快照仍有效(对应 PR body 中
        # 提到的输出生命周期限制)。
        lifetime_M, lifetime_K = 257, 768
        input_generator.manual_seed(2500)
        lifetime_x = torch.randn(
            lifetime_M,
            lifetime_K,
            dtype=torch.bfloat16,
            device=device,
            generator=input_generator,
        )
        first_output = gemm_rs_ar(lifetime_x, weights[lifetime_K])
        first_snapshot = first_output.clone()
        second_output = gemm_rs_ar(-lifetime_x, weights[lifetime_K])
        # 后续断言:first_snapshot 必须仍与 first_output 一致,second_output
        # 必须与基于 -lifetime_x 的参考 all-reduce 结果一致。

评论区精华

无 NVLink 多播环境下初始化失败的处理方式 设计

wzhao18 在 gemm_rs_ar.py 第 731 行指出:既然默认启用融合,对于没有 NVLink 多播的配置(例如 2 个 B200 节点组成的 TP),与其 raise 错误不如返回 None 并记录不支持日志。

结论:wzhao18 随即自我确认 model.py 的 maybe_init_gemm_rs_ar 已用 try-except 包裹 init_gemm_rs_ar,RuntimeError 会被捕获并告警降级,因此保留 raise 是安全的("never mind just saw the try-except handling. so it is safe.")。 · 已解决

fork PR 自动 review 与双人复核流程 other

Claude 机器人因 PR 来自 fork 而跳过自动 review(automated review is disabled);wzhao18 APPROVE 后请 zyongye 二次过目再合入。

结论:zyongye 复核后 APPROVE 并合入,审查流程闭环。 · 已解决

风险与影响

  1. 多播 workspace 复用竞态(核心风险):multimem.st 写入的是初始化时分配的对称内存多播 workspace,而非调用方新分配缓冲区;若消费者未读完就再次调用 GEMM-AR,旧结果会被覆盖。PR 通过 M>=128 才启用分派(小 batch decode 不经过该路径)和测试中的生命周期校验来缓解,但根本消除依赖未来把输出 clone 融合进 kernel(PR body 已列为 future work)。
  2. NVLink 多播依赖:跨节点 TP(如 2 个 B200 节点)没有多播指针时 init_gemm_rs_ar 会 raise,目前依赖 maybe_init_gemm_rs_ar 的 try-except 降级;若未来有其他调用点未包裹该异常,可能导致启动失败。review 中 wzhao18 曾就这一点提出疑虑。
  3. 文件重命名连锁影响:gemm_rs.py -> gemm_rs_ar.py、single test 和 benchmark 同步改名,任何仍引用旧路径的代码会漂移;例如历史 PR #53113 修改过旧测试文件 tests/kernels/test_kimi_k3_gemm_rs.py 的 ROCm 跳过名单,需要确认 CI 侧引用已同步(本 PR 已更新 .buildkite/test_areas/distributed.yaml)。
  4. 对时序敏感的 warp 分工:kernel 注释明确 "Swapping the warpgroups can hang due to warp scheduling",epilogue 与通信 warp 的划分对调度敏感,改动底层调度器时需回归此内核。
  5. warmup 依赖模块加载状态:_warmup_kimi_k3_gemm_rs_ar 用 sys.modules.get 探测模块是否加载,若模型构造顺序变化导致模块尚未加载,预热会被跳过,首次执行可能触发 JIT 编译延迟。

影响范围限定在 Kimi-K3 模型 + SM100 系硬件(GB300 等)+ TP>1 的部署:SP 开启时走原有 GEMM-RS,SP 关闭时新启用 GEMM-AR 路径。E2E 收益以延迟为主——并发 1-32 下 TTFT 降低 2.84%-5.74%,吞吐变化在 -0.5%-+1.2% 之间;微基准大 M 收益显著(M=32768 时 1.21x),小 M(M<128)反而变慢,因此设计了 M>=128 调度门槛。新增 VLLM_KIMI_K3_GEMM_AR 环境变量用于开关。对团队而言,内核文件、测试与 benchmark 的统一重命名使 Kimi-K3 内核演进保持单一入口,也为后续消除输出 clone 的工作预留了测试锚点;普通 CUDA/ROCm 环境因 SM100、BF16、TP 多重校验不受影响。

多播 workspace 复用存在时序竞态 依赖 NVLink 多播能力,缺失时依赖降级路径 内核文件重命名波及下游 CI 引用 限定 SM100/BF16/TP 组合 AR 分派按 M>=128 门控

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论