Kimi K3 性能优化 PR #15416 已合入 main 作者 Dawn952 · 2026-09-04 · 改动 4 文件 +597/-52
Kimi K3 的 KDA 注意力在昇腾上采用 mixed-precision 全秩门控布局(W8A8 MXFP8 量化 + FP32 门控)。 优化前,一层的门控路径要跑一连串小算子,而且全部排在主流程串行执行:
in_proj_qkvgfab:QKV 大矩阵乘(Cube 重活)in_proj_gfab:门控侧 f_a / f_b 两个独立小投影 + beta / 输出门控_prepare_beta:对 beta 现场 float().sigmoid()(每步重复做)问题:小算子多 → 每个 kernel 都有启动开销;全串行 → QKV 大矩阵乘执行时, 门控侧的小算子在旁边干等,Cube/Vector 单元没有被喂满。一层看起来不大,30 层叠起来就是可观的时延浪费。
改动前(下面这段代码来自当前文档附带的源码,即改动前版本, 点 查看 ops/kimi_kda.py 全文): 两个投影顺序调用、beta 现场激活,全部主流串行。
def forward( # ↳ 接口与上游 KimiK3DeltaAttention 完全一致(整组件替换,机制三)
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
) -> torch.Tensor:
if self.uses_mixed_projection: # ↳ mixed-precision 全秩门控布局才走本路径;普通上游路径不受影响
num_tokens = hidden_states.size(0)
mixed_qkv = self.in_proj_qkvgfab(hidden_states)[0] # 主路:QKV 融合投影(大 GEMM)
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)
g_proj_states, f_a, beta = projected_gfab.split(split_sizes, dim=-1)[:3] # 拆出输出门控 g / F 中间 / beta 三路
beta = beta.unsqueeze(0)
g1 = self.f_b_proj(f_a)[0] # ⚡f_b/f_a 两个独立小投影(本需求将其离线组合掉)
g1 = rearrange(g1, "n (h d) -> 1 n h d", d=self.head_dim)
g2 = rearrange(g_proj_states, "... (h d) -> ... h d", d=self.head_dim)
core_attn_out = torch.empty( # 输出缓冲:形状 (1, tokens, heads, head_dim)
(1, num_tokens, self.local_num_heads, self.head_dim),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
self._forward( # ⚡分派 spec/prefill/decode 三路到 AscendC 算子(→NPU)
mixed_qkv=mixed_qkv,
g1=g1,
g2=g2,
beta=beta,
core_attn_out=core_attn_out,
)
core_attn_out = rearrange(core_attn_out, "1 n h d -> n (h d)")
return self.o_proj(core_attn_out)[0] # 输出投影回 hidden 维度
return super().forward(hidden_states, positions)
改动后把门控侧整体搬到一个辅助流,与主流的两次大矩阵乘两阶段重叠, 并用四个事件保证顺序安全(hidden_states_ready 分叉 → bfg_projection_ready 串行两块 Cube 乘 → quant_ready 门控侧的 beta 向量活等主流量化完 → bfg_ready 汇合):
PR #15416 共改 4 个文件(ops/kimi_kda.py +282 行为主,
models/kimi_k3.py 16 行接线,另有 2 个测试文件)。
核心改动三点:
| # | 改动 | 解决什么 |
|---|---|---|
| 1 | 权重预组合:checkpoint 加载后离线算 f_proj = f_b_proj @ f_a_proj(_maybe_fuse_f_proj) | 两个小投影 GEMM → 合成一个,少一次 kernel、少一次中间张量 |
| 2 | BFG 打包:beta + F 投影 + 输出门控打包成一个 FP32 的 fused_bfg_proj(_KDAFusedBFGLinear) | 门控侧多次小 GEMM → 一次 GEMM |
| 3 | 双流重叠:_run_overlapped_qkv_bfg 把门控侧搬到辅助流,与主流 DynamicQuant / QKV GEMM 两阶段重叠(四事件调度) | 主流大乘不再让门控小算子干等;图捕获时辅流严格 join,ACL graph 安全 |
def _maybe_fuse_f_proj(self) -> None: # ⚡改动①:f_a/f_b 权重加载完成后离线组合成 f_proj
if not self._f_a_loaded or not self._f_b_loaded: # 两份权重都到位才组合(各自加载器回调里触发)
return
output_dim = getattr(self.weight, "output_dim", None)
if output_dim is None:
raise ValueError("KDA fused f_proj requires an output-sharded parameter")
shard_offset = sum(self.output_sizes[:_F_PROJ_SHARD_ID]) // self.tp_size
shard_size = self.output_sizes[_F_PROJ_SHARD_ID] // self.tp_size
param_shard = self.weight.narrow(output_dim, shard_offset, shard_size)
fused_weight = torch.matmul( # 离线 f_b @ f_a:运行期两个小投影 GEMM 合成一个
self.f_b_weight.float(),
self.f_a_weight.float(),
).to(dtype=param_shard.dtype)
if fused_weight.shape != param_shard.shape:
raise ValueError(
"KDA composed f_proj shape mismatch: "
f"expected {tuple(param_shard.shape)}, got {tuple(fused_weight.shape)}"
)
param_shard.copy_(fused_weight) # 写回 fused_bfg_proj 的 TP 分片槽位
def _zero_padded_output(
def _prepare_beta(
beta: torch.Tensor,
def _run_overlapped_qkv_bfg( # ⚡改动③:门控侧整体搬到辅助流,与主流两阶段重叠
self,
hidden_states: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Fork BFG work to an auxiliary stream and join it back to main."""
main_stream = torch.npu.current_stream() # 主流:承载 DynamicQuant 与 QKV GEMM
bfg_stream = _kda_bfg_stream() # 辅助流:门控侧专用
hidden_states_ready = main_stream.record_event() # 事件①:从主流分叉
hidden_states.record_stream(bfg_stream)
with npu_stream_switch(bfg_stream):
bfg_stream.wait_event(hidden_states_ready)
fused_bfg = self._project_bfg(hidden_states) # 辅流:BFG 融合投影(一次 GEMM)
bfg_projection_ready = bfg_stream.record_event() # 事件②:BFG 与 QKV 两块 Cube 乘保持串行
quantized_qkv = self._quantize_fused_qkv(hidden_states) # 主流:MXFP8 DynamicQuant(与辅流 BFG GEMM 重叠)
quant_ready = main_stream.record_event()
# Stage 1 join: DynamicQuant on main overlaps the BFG GEMM on the
# auxiliary stream, but the two Cube matmuls remain serialized.
main_stream.wait_event(bfg_projection_ready) # Stage1 join:DynamicQuant 完成后再排 QKV GEMM
mixed_qkv = self._matmul_fused_qkv(quantized_qkv) # 主流:QKV GEMM(辅流小算子在旁重叠执行)
with npu_stream_switch(bfg_stream):
# Stage 2: after both first-stage branches complete, overlap the
# QKV Cube matmul with beta's FP32 conversion and sigmoid vector
# work. Split and reshape the F/output gates here as well so all
# BFG output handling occurs after the QKV matmul is enqueued.
bfg_stream.wait_event(quant_ready) # 事件③:辅流 beta 向量活等主流量化完成
beta, g1, g2 = self._postprocess_bfg(fused_bfg) # 辅流重叠期:B/F/G 拆分 + beta sigmoid + gate reshape
bfg_ready = bfg_stream.record_event()
for tensor in (beta, g1, g2):
tensor.record_stream(main_stream)
# bfg_ready is the auxiliary stream tail. Joining that exact event is
# required for multi-stream ACL graph capture as well as eager reuse.
main_stream.wait_event(bfg_ready) # 事件④:KDA 消费前辅流尾部全部到齐
return mixed_qkv, beta, g1, g2
def _project_bfg(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor:
return self.fused_bfg_proj(hidden_states)[0]
def _postprocess_bfg(
self,
fused_bfg: torch.Tensor,
配套小改动:_prepare_beta 增加 is_preprocessed 标记——
辅助流已做过 sigmoid,主路径只做切片;普通上游 raw beta 仍走原 FP32 sigmoid 路径,两条路互不影响。
上游普通 in_proj_qkvgfab 路径保持不变,只有 mixed-precision 全秩门控布局走新逻辑。