Kestrel_VLLM - 14 · router 两个方向判否,与 decode 的真瓶颈

14 · router 两个方向判否,与 decode 的真瓶颈

一句话 :把 MoE router 量化(fp16)与并行化两条路都走了一遍,双双判否 ------router 的 7ms 是「位级一致硬约束下的延迟地板」;而细粒度 stub 拆解出一个被长期忽略的真相:decode 最大的单项其实是 attention(8ms,42%),不是 FFN 。 前置 :建议先读 第 13 篇 · Tiling 与 Tensor Core 判否、第 09 篇 · full-GPU decode。 环境:x86-64 + NVIDIA RTX 5090(sm_120)· 阶段三 30B-A3B MoE(Qwen3-30B-A3B q4:128 专家 / top-8 / dim=2048 / moe_ffn=768 / 48 层)。

一、问题与结论

方向 判定 卡点 标注
router 量化 fp16 ❌ 判否 减字节不减延迟(延迟受限非带宽受限) 实测
router 并行化 ❌ 判否 拆 i 维破坏保序 → 破坏位级一致(greedy t=27 发散) 实测
decode 真瓶颈 --- attention 8ms(42%)> FFN(router+gate/up/down 9ms) 实测

二、背景:router 为什么是 7ms

router 是 [128 专家][2048] f32 的矩阵向量乘,每 token 读 ~50MB f32(48 层 × 1MB)。

现实现是 128 线程 (1 线程 1 专家,各做 2048 步串行 点积)= 仅 4 warps 。 在 170 SM 的 RTX 5090 上用了 0.6% 的算力。50MB 串行流式读 + 4 warps = 延迟受限 (有效带宽 ~7 GB/s,峰值的 0.4%)。

「1 线程 1 专家、2048 步串行链」的现实现长这样(这也是位级档的形态):

c 复制代码
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_moe_router_gemm_serial,节选) */
__global__ void k_moe_router_gemm_serial(const float *__restrict router,
                                         const float *__restrict x,
                                         float *__restrict logits,
                                         int ne, int d) {
    const int e = blockIdx.x * blockDim.x + threadIdx.x;
    if (e >= ne) return;
    const float *rw = router + (size_t)e * d;
    float s = 0.0f;
    for (int i = 0; i < d; i++) s += rw[i] * x[i];   /* d 步串行链 */
    logits[e] = s;
}

三、方向 1:router 量化 fp16 ------ 判否

按项目纪律「指令预算推算不能替代实测」,不能只靠「字节减半 → 快一倍」的推理,落地实测:

臂 decode greedy 标注
f32 router(基线) 19 ms/tok 16/16 实测
fp16 router 19 ms/tok(51.51 tok/s) 64/64 一致 实测
  • 正确性:fp16 的 ~2⁻¹¹ 误差没有翻转任何 top-k,greedy 64/64 逐 token 一致。
  • 但吞吐一点没变 ------因为 router 是延迟受限(128 线程 × 2048 次串行加载), 字节减半不减「串行加载的次数」,延迟地板不变。

首次尝试用了自定义 vc_half2float(分支重的位操作),decode 反而 19→29ms 变慢; 换成原生 __half2float(单指令)后回到 19ms。这又是一次「实现细节决定实测」的实例。

判否:量化减字节不减延迟,对延迟受限的 router 无效。

四、方向 2:router 并行化 ------ 判否

按项目纪律「s 分块之间续接原累加器 (非部分和再合并)」:

router 点积 s = Σᵢ rw[i]·x[i] 是 2048 步串行 FMA 链。把 i 拆成两半分别累加再相加, 就是「部分和再合并」,违反保序公理。

并行的实现就是把 d 维切成若干 chunk、各自归约再树合并:

c 复制代码
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_moe_router_gemm,节选) */
__global__ void k_moe_router_gemm(const float *__restrict router, /* [ne][d] */
                                  const float *__restrict x,      /* [d] */
                                  float *__restrict logits,       /* [ne] */
                                  int ne, int d) {
    const int e  = blockIdx.x * 4 + (threadIdx.x >> 6);  /* 0..127 */
    const int c  = threadIdx.x & 63;                     /* 0..63 */
    const int chunk = d >> 6;                            /* 32 */
    const float *rw = router + (size_t)e * d + (size_t)c * chunk;
    const float *xc = x + (size_t)c * chunk;
    float s = 0.0f;
    for (int i = 0; i < chunk; i++) s += rw[i] * xc[i];
    __shared__ float lg[4][64];
    lg[threadIdx.x >> 6][c] = s;
    __syncthreads();
    for (int off = 32; off > 0; off >>= 1) {
        if (c < off) lg[threadIdx.x >> 6][c] += lg[threadIdx.x >> 6][c + off];
        __syncthreads();
    }
    if (c == 0) logits[e] = lg[threadIdx.x >> 6][0];
}

实测落地:

  • 拆两半(256 线程):greedy 在 t=27 发散(1773 → 3837),near-tie 的 top-k 被翻转。

判否:并行化必须拆 i 维,而拆 i 维破坏保序 → 破坏位级一致。router 的并行度被硬锁在 128(专家数)。

五、完整 decode 拆解(细粒度 stub)

用 VLLM_CUDA_STUB_{ROUTER,GU,ATTN,LMHEAD} 逐段 stub,把 19ms 拆到底:

组件 成本 占比 标注
attention(QKV 投影 + RoPE + KV 量化 + attention 核 + O 投影) 8 ms 42% 实测
router 7 ms 37% 实测
gate/up/down 2 ms 10% 实测
lm_head + norm + launch 2 ms 11% 实测

这推翻了「MoE FFN 是 decode 瓶颈」的直觉------FFN(router+gate/up/down)合计 9ms, 而 attention 单一项就 8ms,且之前一直被当成「小头」忽略。

六、attention 为什么慢,是下一个目标

k_attn_decode_f32q 的 pass 3 对每个输出元素 i < hd=128 各做一次 butterfly 归约:

css 复制代码
for i in 0..128:
    acc 各线程累加 → __syncthreads → butterfly(8 步 __syncthreads) → 写 outp[i]

即 128 次串行归约 × 每次 8 步 __syncthreads ≈ 1408 次 barrier / 层 × 48 层 ≈ 6.7 万次 barrier / token。 这是典型的「按输出元素逐个归约」低效结构。同一套结构在同族的 q8 变体里保留至今:

c 复制代码
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_attn_decode_q8 的 Pass 3,节选) */
/* Pass 3: weighted V sum (reduce over the token partition per output dim). */
for (int i = 0; i < hd; i++) {
    float acc = 0.0f;
    for (int t = threadIdx.x; t < seq_len; t += blockDim.x) {
        const int8_t *vt = vb + (size_t)t * kv_dim;
        const float vss = vs[(size_t)t * nkv + kh] * (1.0f / 127.0f);
        acc += (float)vt[i] * scores[t] * vss;
    }
    red[threadIdx.x] = acc;
    __syncthreads();
    for (int off = blockDim.x >> 1; off > 0; off >>= 1) {
        if (threadIdx.x < off) red[threadIdx.x] += red[threadIdx.x + off];
        __syncthreads();
    }
    if (threadIdx.x == 0) outp[i] = red[0] * invS;
    __syncthreads();
}

优化方向:把 128 次串行归约改成一次并行归约 + 转置(或按 (i,t) 二维映射), 把 barrier 次数从 O(hd·log T) 降到 O(log T)。代价是「数学等价但非位级等价」,须重新位级对拍 greedy。

七、边界与已知限制

  • 口径:router 的 7ms / 0.6% 算力 / ~7 GB/s 有效带宽是 64-token decode 下的量;fp16 与并行化的 A/B 也在此口径下。
  • 位级一致是硬约束 :本文的两次判否都以「greedy 位级一致」为前提;第 15 篇 会说明这条约束被放宽后,router 并行化反而落地并带来收益。
  • 并行化发散点:t=27 是实测的 near-tie 翻转点;换 prompt 未必同点,但机制(拆 i 维破坏保序)不变。
  • attention 的 8ms 拆解 只是定位,不是优化结论;真正的修改在 第 15 篇。
  • fp16 的 2⁻¹¹ 误差未翻转 top-k 只在本 prompt 的 64 token 上验证。

CPU 对照(迁移前基线)

  • CPU 参考 :kestrel-llm/src/model/vllm_safetensors.c(函数 st_moe_ffn_sparse_q8 / st_moe_ffn_sparse_q4 内的 router 串行点积 s += rw[i]*x_ffn[i])------ CPU 侧逐专家沿 i 维串行点积出 router logits。
  • 迁移要点:CPU 侧串行点积保序 → CUDA 侧 router 量化 fp16 判否(延迟受限而非带宽);并行化拆 i 维破坏保序 → 位级判否(greedy t=27 发散);细粒度 stub 拆出 attention 42% 才是最大项。
  • 真机验证 :未独立复现:需 30B + 细粒度 stub 探针(VLLM_CUDA_STUB_*)与 near-tie 翻转点观察。

八、小结(可复用结论)

认知 内容
先探针再排序 细粒度 stub 才拆出「attention 42% > FFN」的反直觉真相
量化/并行化对延迟受限无效 router 是延迟受限(4 warps),减字节、加线程都动不了它
位级一致是硬约束 拆 i 维(并行化)违反保序公理,实测 greedy 发散
实现细节决定实测 手写 fp16 转换 19→29ms,原生指令才回到 19ms
  1. 延迟受限 ≠ 带宽受限:router 的瓶颈是「128 线程 × 2048 次串行加载」的次数,不是字节数;量化减字节、并行化加线程,在「位级一致」约束下都不解决它。
  2. 别让直觉替探针排序:一直以为 MoE FFN 是瓶颈,stub 一拆才发现 attention 单一项就占 42%。
  3. 保序约束会把并行度钉死:router 的并行度上限是专家数(128),因为拆 i 维就变成「部分和再合并」。
  4. 实现选择能反转结论:同样的 fp16 量化,分支重的位操作实现反而更慢,原生指令才回到基线。
  5. 拆解先于优化:先把 19ms 拆成四块并定位最大项,再决定动哪个内核。

一句话(收束):router 两条路都判否,因为它的 7ms 是位级一致约束下的延迟地板; 真正的下一个大头是 attention 核的「128 次串行归约」低效结构。


相关篇目:第 13 篇 · Tiling 与 Tensor Core 判否、第 15 篇 · decode 优化四连击 源码与配套资源:本仓库 gitee.com/pei-xiaogua...; CPU 推理源码 gitee.com/pei-xiaogua...

相关推荐
石狮松松1 小时前
第 05 章 你的第一行"AI"代码:拟合一条直线
人工智能
狒狒AI1 小时前
Remotion / GSAP+Three / HyperFrames / Skia 四栈实测:代码渲染知识视频的选栈方法(附开源技能)
前端·人工智能·aigc
自养Agent1 小时前
Agent Harness 从用到会造:报错的那层,往往不是坏掉的那层
人工智能
RockChinQ1 小时前
一个 Telegram 群,三条事件路由:欢迎、群规问答与群管理各归一个 Agent
人工智能
果霸大叔1 小时前
从根上吃透AI Agent:Agent拆解系列之DeerFlow2.0深度拆解
人工智能
2025的限定小孙1 小时前
AI自己写的SQL自己跑?把执行权从模型手里拿走的三层闸
人工智能
用户427019230121 小时前
排障实录 :OpenClaw 升级失败三次,我是怎么把系统救回来的
人工智能
不加辣椒1 小时前
第 14 章 控制项选型:成本模型与决策框架
人工智能
fanjianzhihui1 小时前
对话录音质检系统的工程实践:从语音识别到LLM判定的误差控制
人工智能·语音识别