执行摘要
- 一句话:Kimi-K3 新增 GEMM-AR 融合内核,kernel 内完成 all-reduce
- 推荐动作:值得精读。核心看点:(1) 用 CuTe DSL 的
llvm.inline_asm 封装 multimem.st 多播存储,把 collective 语义融合进 GEMM 内核的实现手法;(2) 输出 workspace 复用带来的生命周期设计权衡与对应测试锚点;(3) 初始化失败优雅降级(NVLink 缺失时回退普通 GEMM)的容错模式。若后续计划支持非 NVLink 多播环境或消除输出 clone,应在此 PR 基础上迭代。
功能与动机
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%。
实现拆解
整体分四步推进,配套修改 9 个文件:
-
内核层: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 内核。
-
特性开关与初始化降级。 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 线性层。
-
模型调用链接入。 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。
-
测试、基准、预热与 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(模块 融合内核;类别 source;类型 core-logic;符号 multimem_st_16B, Sm100GemmRsArBF16, GemmRsAr, init_gemm_rs_ar): 核心内核文件:新增 multimem_st_16B 多播存储 DSL 算子,Sm100GemmRsBF16 泛化为 Sm100GemmRsArBF16 并支持 all_reduce 模式,是 GEMM-AR 的实现主体
vllm/models/kimi_k3/nvidia/model.py(模块 模型集成;类别 source;类型 integration;符号 maybe_init_gemm_rs_ar, maybe_init_gemm_rs, KimiMLP): 模型集成入口:maybe_init_gemm_rs 重构为 maybe_init_gemm_rs_ar,按 SP 拓扑选择 RS/AR 单例,并加入 NVLink 多播缺失时的优雅降级
tests/kernels/test_kimi_k3_gemm_rs_ar.py(模块 内核测试;类别 test;类型 test-coverage;符号 _run_mode, _reference, _assert_valid_rows_close, test_kimi_k3_gemm_rs_ar): 单测从 RS-only 扩展为 RS/AR 双模式,并新增 AR 专属的 workspace 复用生命周期校验,直接锚定本 PR 的核心限制
vllm/models/kimi_k3/nvidia/kda.py(模块 注意力模块;类别 source;类型 integration;符号 KimiKda, KimiAttention): 注意力 O-proj 调用链从 run_gemm_rs 布尔标志切换到 gemm_rs_ar 单例,forward 中通过 should_run 决定是否走融合内核
vllm/model_executor/warmup/kernel_warmup.py(模块 预热逻辑;类别 source;类型 integration;符号 _warmup_kimi_k3_gemm_rs_ar, kernel_warmup): 新增 _warmup_kimi_k3_gemm_rs_ar 预热钩子,用 sys.modules.get 探测模块加载状态,避免非 Kimi-K3 模型引入 JIT 编译开销
benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py(模块 性能基准;类别 source;类型 benchmark;符号 make_torch_ring_ll_gemm_collective, make_torch_ldmc_gemm_collective, make_fused_gemm_collective, make_ring_ll_collective): benchmark 整体重命名并新增 --mode rs|ar,补充环形/树形/torch+fused 集体通信基线,用于量化 AR 相对 torch.mm+NCCL all-reduce 的收益
vllm/models/kimi_k3/nvidia/mla.py(模块 注意力模块;类别 source;类型 integration;符号 KimiMla): MLA 模块与 kda.py 同步切换 gemm_rs_ar 单例,保持注意力输出投影路径行为一致
vllm/envs.py(模块 环境变量;类别 source;类型 configuration;符号 VLLM_KIMI_K3_GEMM_AR): 新增 VLLM_KIMI_K3_GEMM_AR 环境变量,与既有 VLLM_KIMI_K3_GEMM_RS 并列控制 AR 模式开关
.buildkite/test_areas/distributed.yaml(模块 CI 配置;类别 config;类型 configuration): 分布式测试区域配置随测试文件重命名与双模式扩展同步更新,保证 CI 覆盖
关键符号: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
核心内核文件:新增 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
模型集成入口: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
单测从 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 结果一致。
评论区精华
核心讨论集中在 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),只能依赖人工审核。
- 无 NVLink 多播环境下初始化失败的处理方式 (design): 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): zyongye 复核后 APPROVE 并合入,审查流程闭环。
风险与影响
- 风险:
- 多播 workspace 复用竞态(核心风险):
multimem.st 写入的是初始化时分配的对称内存多播 workspace,而非调用方新分配缓冲区;若消费者未读完就再次调用 GEMM-AR,旧结果会被覆盖。PR 通过 M>=128 才启用分派(小 batch decode 不经过该路径)和测试中的生命周期校验来缓解,但根本消除依赖未来把输出 clone 融合进 kernel(PR body 已列为 future work)。
- NVLink 多播依赖:跨节点 TP(如 2 个 B200 节点)没有多播指针时
init_gemm_rs_ar 会 raise,目前依赖 maybe_init_gemm_rs_ar 的 try-except 降级;若未来有其他调用点未包裹该异常,可能导致启动失败。review 中 wzhao18 曾就这一点提出疑虑。
- 文件重命名连锁影响:
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)。
- 对时序敏感的 warp 分工:kernel 注释明确 "Swapping the warpgroups can hang due to warp scheduling",epilogue 与通信 warp 的划分对调度敏感,改动底层调度器时需回归此内核。
- 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 门控
关联脉络
- PR #53113 [CI/Build][ROCm] Keep the CUDA-only kernel tests out of the ROCm run: 该 PR 修改过 tests/kernels/test_kimi_k3_gemm_rs.py 的 ROCm 跳过名单,而本 PR 将该文件重命名为 test_kimi_k3_gemm_rs_ar.py,需要 CI 引用同步,避免跳过逻辑漂移。
- PR #53040 [DSV4][Kernel] Fuse shared experts into MegaMoE: 同期也改动 vllm/models/kimi_k3/nvidia/model.py,说明该模型目录处于活跃演进期,合入时需注意同一文件上的并发修改冲突。
参与讨论