KDA 门控投影融合与双流重叠

Kimi K3 性能优化 PR #15416 已合入 main 作者 Dawn952 · 2026-09-04 · 改动 4 文件 +597/-52

一、需求背景

Kimi K3 的 KDA 注意力在昇腾上采用 mixed-precision 全秩门控布局(W8A8 MXFP8 量化 + FP32 门控)。 优化前,一层的门控路径要跑一连串小算子,而且全部排在主流程串行执行:

问题:小算子多 → 每个 kernel 都有启动开销;全串行 → QKV 大矩阵乘执行时, 门控侧的小算子在旁边干等,Cube/Vector 单元没有被喂满。一层看起来不大,30 层叠起来就是可观的时延浪费。

二、相关流程

改动前(下面这段代码来自当前文档附带的源码,即改动前版本, 点 查看 ops/kimi_kda.py 全文): 两个投影顺序调用、beta 现场激活,全部主流串行。

代码 · 改动前的 forward 门控路径(ops/kimi_kda.py:145)
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)
图1 · 改动前:门控路径全部串行在主流
sequenceDiagram participant M as 主流(一个 KDA 层内) M->>M: in_proj_qkvgfab:QKV 大矩阵乘(Cube) M->>M: in_proj_gfab:f_a / f_b 门控投影(小算子,干等 QKV 完) M->>M: beta 现场 float().sigmoid()(每步重复) M->>M: npu_causal_conv1d_custom M->>M: chunk_kda_fwd / recurrent_kda Note over M: 全串行:QKV 跑的时候门控侧闲置

改动后把门控侧整体搬到一个辅助流,与主流的两次大矩阵乘两阶段重叠, 并用四个事件保证顺序安全(hidden_states_ready 分叉 → bfg_projection_ready 串行两块 Cube 乘 → quant_ready 门控侧的 beta 向量活等主流量化完 → bfg_ready 汇合):

图2 · 改动后:双流两阶段重叠(四事件调度)
sequenceDiagram participant Main as 主流 participant Aux as 辅助流(门控侧) Main->>Main: DynamicQuant(MXFP8 量化) Main-->>Aux: ① hidden_states_ready 分叉 Aux->>Aux: fused_bfg_proj 门控大投影(一次 GEMM) Aux-->>Main: ② bfg_projection_ready(两块 Cube 乘保持串行) Main->>Main: QKV GEMM(Cube) Main-->>Aux: ③ quant_ready Aux->>Aux: B/F/G 拆分 + beta FP32 sigmoid + gate reshape(小算子重叠期) Aux-->>Main: ④ bfg_ready 汇合(KDA 消费前必须到齐) Main->>Main: conv → chunk_kda_fwd / recurrent_kda Note over Main,Aux: QKV 大乘与门控小算子并行;汇合点后无任何辅流操作

三、需求改动

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、少一次中间张量
2BFG 打包:beta + F 投影 + 输出门控打包成一个 FP32 的 fused_bfg_proj(_KDAFusedBFGLinear)门控侧多次小 GEMM → 一次 GEMM
3双流重叠:_run_overlapped_qkv_bfg 把门控侧搬到辅助流,与主流 DynamicQuant / QKV GEMM 两阶段重叠(四事件调度)主流大乘不再让门控小算子干等;图捕获时辅流严格 join,ACL graph 安全
改动①+② · 权重预组合(加载后离线融合)—— 摘自 commit 222677fc7 diff
    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,
改动③ · 双流重叠核心 _run_overlapped_qkv_bfg(新增函数,含四事件调度)
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 全秩门控布局走新逻辑。

四、相关链接

首页