vLLM 上游 (NVIDIA / AMD) vs vLLM-Ascend 插件 — 源码级深度对比
同一个 Kimi K3 模型,在三个硬件平台上需要不同的底层算子实现。vLLM 的设计是:
model.py 2155行 完整模型
mla.py MLA注意力
kda.py 1058行 KDA注意力
latent_moe_runner.py
dspark_mla.py
mtp.py
ops/
attn_res.py Triton kernel
latent_moe_tail.py
cute_dsl/ CUDA底层
third_party/kda/ KDA chunk算子
linear.py ~900行 完整实现
model.py 249行 多模态封装
kda.py 619行 KDA注意力
mla.py MLA注意力
latent_moe_runner.py
mtp.py
ops/
attn_res.py Triton kernel
kda_decode.py
third_party/kda/ KDA算子
models/kimi_k3.py 879行 适配层
ops/kimi_kda.py 752行 NPU KDA
ops/triton/kimi_k3/
attention_residual.py NPU attn_res
vllm/models/kimi_k3/__init__.py上游通过检测当前平台,自动路由到对应的实现:
# vllm/models/kimi_k3/__init__.py
from vllm.platforms import current_platform
if TYPE_CHECKING:
from .nvidia.model import KimiK3ForConditionalGeneration
elif current_platform.is_rocm():
from .amd.model import KimiK3ForConditionalGeneration # AMD
elif current_platform.device_type == "tpu":
pass # TPU插件自己注册,不在此加载
else:
from .nvidia.model import KimiK3ForConditionalGeneration # NVIDIA
vllm_ascend/models/kimi_k3.pyAscend 插件绕过上述路由,直接继承上游 AMD 版本的类,并替换关键组件:
# vllm_ascend/models/kimi_k3.py 的导入链
from vllm.models.kimi_k3.amd.linear import KimiDecoderLayer as UpstreamKimiDecoderLayer
from vllm.models.kimi_k3.amd.linear import KimiMLAAttention as UpstreamKimiMLAAttention
from vllm.models.kimi_k3.amd.model import KimiK3ForConditionalGeneration as UpstreamKimiK3ForConditionalGeneration
from vllm_ascend.ops.kimi_kda import AscendKimiK3DeltaAttention # NPU KDA后端
from vllm_ascend.ops.triton.kimi_k3.attention_residual import apply_attn_res # NPU attn_res
| 维度 | NVIDIA | AMD | Ascend |
|---|---|---|---|
| 类定义 | 独立完整实现 | KimiMLAAttention独立完整实现 |
AscendKimiMLAAttention继承 UpstreamKimiMLAAttention |
| KDA 层 | nvidia/kda.py1058行, Triton+CUDA |
amd/kda.py619行, ROCm Triton |
ops/kimi_kda.py752行, AscendC+Triton |
| MLA 层 | 自建 CUDA MLA wrapper | amd/mla.pyROCm MLA wrapper |
复用上游 MLA wrapper 仅配置 RoPE + DSpark |
| 底层算子 | CUDA / Triton kernel | ROCm / Triton kernel | AscendC / NPU Triton kernel |
| forward 逻辑 | 完整实现 | 完整实现 | 委托给 self.mla_attn()上游已构建好 |
| 维度 | NVIDIA | AMD | Ascend |
|---|---|---|---|
| 类定义 | 内联在 model.py 中 | KimiMoE 独立类 |
AscendKimiMoE 独立类 |
| Gate | GateLinear |
GateLinear |
GateLinear ✓ 相同 |
| Shared Experts | KimiMLP |
KimiMLP |
KimiMLP ✓ 从上游导入 |
| Routed Experts | FusedMoEFactory |
FusedMoEFactory+ ROCmLatentMoERunner |
FusedMoEFactory+ 序列并行支持 |
| Latent MoE | 内建降维→专家→升维 | ROCmLatentMoERunner |
复用上游 KimiRoutedOutputTransform量化配置走 Ascend 量化 |
| 关键差异 | — | TP padding 处理 | Ascend 量化配置透传 + 序列并行开关 |
KimiMoE.__init__self.experts = FusedMoEFactory( # ↳ 专家容器来自上游 FusedMoE 体系
...
runner_cls=ROCmLatentMoERunner # ↳ AMD 特有:潜空间 MoE 专用 Runner
if self.use_latent_moe else None,
)
if self.padded_moe_intermediate_size
!= moe_intermediate_size:
# AMD 特有的 TP padding 处理
w13_weight.data.zero_() # ↳ AMD 特有:TP 切分 pad 需要先清零
w2_weight.data.zero_() # (昇腾无此处理)
AscendKimiMoE.__init__latent_quant_config = (
quant_config if quant_config is not None
and quant_config.get_name() == "ascend"
else None # ⚡ 只有昇腾量化配置才透传给潜空间专家
)
self.experts = FusedMoEFactory(
...
is_sequence_parallel=use_sequence_parallel, # ⚡ 昇腾新增:序列并行开关
# ↳ 无需 runner_cls,复用上游默认 Runner
)
Attention Residual 是 Kimi K3 独有的机制:将传统的加法残差替换为学习式 softmax 加权混合。三平台的数学逻辑完全相同,但算子实现不同。
实现在 nvidia/ops/attn_res.py
@triton.jit
def _attn_res_kernel(
prefix_ptr, delta_ptr, # 累积残差 / 本块增量
blocks_ptr, norm_weight_ptr, # 块级残差 / RMSNorm 权重
qk_weight_ptr, ... # 打分投影权重
):
# ↳ NVIDIA 路径:Triton GPU kernel(CUDA 后端编译)
# ⚡ 对应昇腾的 _apply_attn_res_kernel,数学逻辑相同
实现在 amd/ops/attn_res.py
@triton.jit
def _attn_res_kernel(
prefix_ptr, delta_ptr, # 与 NVIDIA 版同参同名
blocks_ptr, norm_weight_ptr,
qk_weight_ptr, ...
):
# ↳ AMD 路径:同一份 Triton 源码,改由 ROCm 后端编译
# → 昇腾版本见 9.3 源码外的 apply_attn_res(triton-ascend 编译)
实现在 models/kimi_k3.py:92
def _apply_ascend_attn_res(...):
if apply_attn_res is not None
and prefix_sum.device.type=="npu":
return apply_attn_res(...) # NPU Triton
# 回退:纯PyTorch实现
values = torch.cat(...)
scores = (normalized * weight).sum(-1)
return softmax @ values
# ─── AMD 版 (amd/linear.py:141) ─── 调用 ROCm Triton kernel
def _apply_attn_res(prefix_sum, block_residual, proj, norm, num_valid_blocks, ...):
return attn_res(prefix_sum, delta, block_residual,
norm.weight, proj.weight.squeeze(0), ...)
# ─── Ascend 版 (kimi_k3.py:92) ─── 优先 NPU Triton,回退纯 PyTorch
def _apply_ascend_attn_res(prefix_sum, block_residual, proj, norm, num_valid_blocks):
if apply_attn_res is not None and prefix_sum.device.type == "npu":
return apply_attn_res(...) # ← NPU 加速路径
# 纯 PyTorch 回退(数学等价)
values = torch.cat((block_residual[:,:num_valid_blocks,:], prefix_sum.unsqueeze(1)), dim=1)
values_fp32 = values.float()
inverse_rms = torch.rsqrt(values_fp32.square().mean(-1, keepdim=True) + norm.variance_epsilon)
normalized_without_gamma = values_fp32 * inverse_rms
score_weight = norm.weight.float() * proj.weight.squeeze(0).float()
scores = (normalized_without_gamma * score_weight).sum(-1)
probabilities = scores.softmax(-1).unsqueeze(1)
return torch.matmul(probabilities, values_fp32).squeeze(1).to(values.dtype)
Decoder Layer 是差异最大的组件。三平台的核心分歧在于 Attention 后端的选择 和 残差路径的处理。
KimiDecoderLayer.__init__if config.is_kda_layer(layer_idx):
kda_config = config.linear_attn_config
if kda_config.get("use_full_rank_gate", False):
self.self_attn = KimiK3DeltaAttention(...) # ↳ AMD KDA(ROCm Triton)
else:
self.self_attn = KimiLinearGatedDeltaNetAttention(...)
else:
self.self_attn = KimiMLAAttention(...) # ↳ AMD MLA wrapper
# ⚡ 昇腾版把上面两个分支整体换成 Ascend 实现(见右侧 Ascend 版对比)
if self.is_moe_layer:
self.block_sparse_moe = KimiMoE(...)
self.mlp = self.block_sparse_moe
else:
self.mlp = KimiMLP(...)
AscendKimiDecoderLayer.__init__if config.is_kda_layer(layer_idx):
self.self_attn = AscendKimiK3DeltaAttention(...)
# ↑ NPU 专用 KDA 后端
else:
self.self_attn = AscendKimiMLAAttention(...
use_rope=False, # 无RoPE
)
if self.is_moe_layer:
self.block_sparse_moe = AscendKimiMoE(...
use_sequence_parallel=use_sequence_parallel,
)
self.mlp = self.block_sparse_moe
else:
self.mlp = KimiMLP(...)
_run_self_attndef _run_self_attn(self, positions, hidden_states):
attn_output = torch.empty_like(hidden_states)
self.self_attn(
hidden_states=hidden_states,
positions=positions,
output=attn_output, # 写入预分配缓冲
)
return attn_output
_run_self_attndef _run_self_attn(self, positions, hidden_states):
return self.self_attn(
positions=positions,
hidden_states=hidden_states,
# ↑ Ascend attention 直接返回结果
)
forward_attn_residualdef forward_attn_residual(self, positions, hidden_states, block_residual):
prefix_sum = hidden_states
hidden_states = _apply_attn_res(
prefix_sum, block_residual,
self.self_attention_res_proj,
self.self_attention_res_norm,
self.prev_valid_blocks,
output_norm=self.input_layernorm,
block_write_idx=(self.block_write_idx
if self.is_block_write_layer else -1),
)
if self.is_block_write_layer:
prefix_sum = None
hidden_states = self._run_self_attn(positions, hidden_states)
if prefix_sum is None:
prefix_sum = hidden_states
prefix_delta = None
else:
prefix_delta = hidden_states
hidden_states = _apply_attn_res(
..., delta=prefix_delta,
output_norm=self.post_attention_layernorm,
)
hidden_states = self.mlp(hidden_states)
prefix_sum = prefix_sum + hidden_states
return prefix_sum, block_residual
forward_attn_residualdef forward_attn_residual(self, positions, hidden_states, block_residual):
prefix_sum = hidden_states
hidden_states = _apply_ascend_attn_res(
prefix_sum, block_residual,
self.self_attention_res_proj,
self.self_attention_res_norm,
self.prev_valid_blocks,
)
if self.is_block_write_layer:
block_residual[:, self.block_write_idx, :].copy_(prefix_sum)
prefix_sum = None
hidden_states = self.input_layernorm(hidden_states)
if self.use_sequence_parallel:
hidden_states = sp_all_gather(hidden_states)
hidden_states = hidden_states[:positions.shape[0]]
hidden_states = self.self_attn(...)
if self.use_sequence_parallel:
hidden_states = sp_reduce_scatter(hidden_states)
prefix_sum = hidden_states if prefix_sum is None else prefix_sum + hidden_states
hidden_states = _apply_ascend_attn_res(...)
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = prefix_sum + hidden_states
return hidden_states, block_residual
规律:越靠近底层(算子级),越需要替换;越靠近上层(模型结构、权重加载),越可以继承复用。
不是每个平台"都写一遍"同一份代码,而是:
vllm/models/kimi_k3/ascend/ 下的一套完整实现,届时代码量会大幅增加图9-1 把一次推理分成了六步,其中第④步「模型逐层计算」占 95% 以上的计算量。 接下来两节只聚焦它:9.2 放大看 30 层里一层的实际调用时序(顺便看清哪些代码来自 AMD 基类、 哪些是昇腾新写的);9.3 顺着时序钻进源码。
先快速过一遍全流程(上一版已有,紫色是本章主角):
第④步 = 30 次循环,每次调 AscendKimiDecoderLayer.forward_attn_residual
(models/kimi_k3.py:478,这个类继承自 AMD 的 KimiDecoderLayer,同名方法被覆盖重写)。
下面是一次层计算的完整时序——注意三列参与者的来源:中间是昇腾写的,右列是 NPU 底层算子,
方法名旁标 ↳ 的行为/组件直接来自 AMD 基类。
# 机制一:整类继承 —— 类体是空的或只加参数,方法全部来自 AMD 基类
class AscendKimiMLAAttention(UpstreamKimiMLAAttention): # Upstream = vllm/models/kimi_k3/amd/linear.py
def forward(self, positions, hidden_states):
return self.mla_attn(positions, hidden_states) # 只改一个委托点
# 机制二:继承 + 覆盖 —— 方法签名沿用 AMD 基类,实现重写
class AscendKimiDecoderLayer(UpstreamKimiDecoderLayer):
def forward_attn_residual(self, ...): ... # 同名同序,换掉残差函数/注意力分派
# 机制三:整组件替换 —— 上游类彻底不用,接口对齐
self.self_attn = AscendKimiK3DeltaAttention(...) # AMD 的 KimiK3DeltaAttention 被丢弃
一句话:AMD 提供的是"层的组织方式"(方法顺序、组件接线、权重加载),昇腾提供的是"每个动作怎么算"。 时序图里的编号 ①→⑤ 就是 AMD 基类定下的顺序;每个编号算什么,红色算子说了算。
时序图 alt 分支里 KDA 是最特殊的一条(上游没有、AMD 用 ROCm Triton、昇腾用自研 AscendC 算子)。
下面三段源码就是时序图 消息 4 → 6 → 7 的真身(截自 ops/kimi_kda.py,
⚡ 昇腾新逻辑 / ↳ 与 AMD·上游的关系 / →NPU 底层算子,见行内注释)。
def forward( # ↳ 接口与上游 KimiK3DeltaAttention 一致(整组件替换,机制三)
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
) -> torch.Tensor:
if self.uses_mixed_projection:
num_tokens = hidden_states.size(0)
mixed_qkv = self.in_proj_qkvgfab(hidden_states) # ⚡ 融合投影:一次出 q/k/v/门控多路[0]
projected_gfab = self.in_proj_gfab(hidden_states)[0]
split_sizes = [
self.local_projection_size,
self.head_dim,
self.local_num_heads,
]
if self.in_proj_padding:
split_sizes.append(self.in_proj_padding)
def _forward(
self,
mixed_qkv: torch.Tensor,
g1: torch.Tensor,
g2: torch.Tensor,
beta: torch.Tensor,
core_attn_out: torch.Tensor,
) -> None:
"""Dispatch speculative, prefill, and decode tokens through KDA kernels."""
forward_context = get_forward_context()
attn_metadata_raw = forward_context.attn_metadata
if attn_metadata_raw is None:
core_attn_out.zero_()
return
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata, GDNAttentionMetadata)
num_actual_tokens = attn_metadata.num_actual_tokens
mixed_qkv = mixed_qkv[:num_actual_tokens]
g1 = g1[:, :num_actual_tokens]
def _run_causal_conv1d(
mixed_qkv: torch.Tensor,
conv_weights_t: torch.Tensor,
conv_state: torch.Tensor,
query_start_loc: torch.Tensor,
cache_indices: torch.Tensor,
initial_state_mode: torch.Tensor | None,
*,
run_mode: int,
num_accepted_tokens: torch.Tensor | None = None,
) -> torch.Tensor:
output = torch.empty_like(mixed_qkv)
# Consume the operator's declared output alias. Returning ``output``
# independently would let graph functionalization treat the custom-op
# result as dead and expose the uninitialized allocation instead.
return torch.ops._C_ascend.npu_causal_conv1d_custom( # →NPU:自研 AscendC 算子,CANN 调度到 AI Core
output,
mixed_qkv,
conv_weights_t,
conv_state=conv_state,
bias_opt=None,
query_start_loc_opt=query_start_loc,
cache_indices_opt=cache_indices,
initial_state_mode_opt=initial_state_mode,
读完这三段,时序图右边一列(NPU 底层算子)就有了实体:
torch.ops._C_ascend.* 是 vllm-ascend 自己编译的 C++ 算子库(AscendC 语言编写、随包发布),
调用之后编译与调度全部交给 CANN 落到 AI Core。MLA 分支同理,走的是第二个通道 torch_npu(第 10 章)。
第 9 章调用链里出现了三种"红色"底层调用:torch.ops._C_ascend.*、torch_npu.npu_*、
@triton.jit。它们就是 vllm-ascend 与"Ascend 底层"的三个接口。这一章把它们与 CANN、硬件串成一条线。
| 通道 | 是什么 | Kimi K3 里在哪用 | 为什么选它 |
|---|---|---|---|
torch.ops._C_ascend.* |
vllm-ascend 自己编译的 C++ 扩展 vllm_ascend_C(utils.enable_custom_op 惰性注册),内部算子用华为 AscendC 语言写,随包发布 |
KDA 全部核心计算:chunk_kda_fwd(prefill 分块)、recurrent_kda(decode 递归)、npu_causal_conv1d_custom(因果卷积) |
上游没有的算子(KDA 是 Kimi K3 新结构),必须自己写设备代码 |
torch_npu.npu_* |
华为官方 torch_npu 库提供的 aclnn 算子(CANN 自带,随固件/驱动发布) | MLA 的融合推理注意力(FIA)、MoE 的 npu_grouped_matmul_swiglu_quant、各类基础算子 |
通用大算子华为已高度优化,直接调用即可,无需自研 |
@triton.jit(triton-ascend) |
Triton 语言的 Ascend 后端,Python 写 kernel、运行时编译到 NPU | 注意力残差 _apply_attn_res_kernel(ops/triton/kimi_k3/attention_residual.py) |
与 NVIDIA/AMD 共享同一份 Triton 源码风格,移植成本低;无环境时回退纯 PyTorch |
# ① Python 层:ops/kimi_kda.py(文档里能打开的源码)
def _run_prefill(self, ...):
...准备 q/k/v/beta 并按 AscendC 契约整理(_prepare_beta)...
return torch.ops._C_ascend.chunk_kda_fwd(q, k, v, g, beta, ...) # ← 跨过 Python/USA 边界
# ② C++ 层:vllm_ascend_C 扩展(随包发布的 .so,源码在仓库 vllm_ascend/C 下)
# chunk_kda_fwd 的 C++ 实现负责:参数校验 → 取 NPU Stream → launch AscendC kernel
# ③ CANN 层:AscendC kernel(设备侧 C++/类 CUDA 语法)编译产物在 AI Core 上执行
# Host 侧由 libascendcl(acl)管理设备上下文、内存、Stream 任务下发
# ④ 硬件层:AI Core 的 Cube 单元做矩阵乘(q·k、状态更新),Vector 单元做激活/门控
torch.ops._C_ascend(自研 AscendC)、torch_npu(华为 aclnn)、
triton-ascend 三个入口,再由 CANN 完成编译与调度落到 AI Core(本章图 10)。