vLLM 源码 · 2026-10-16 发布 · 参数→算子链路实战笔记
op_statistic.csv 是算子级的聚合统计(每个算子一共调用了多少次、耗时多少),kernel_details.csv 是每一次调用的明细。阅读时可以按"先看第 1 节的四种影响方式 → 再看第 3、4 节的例子 → 最后看第 5 节的反推"这个顺序,跳过第 2 节的大表也不影响理解。
同一次推理可以从三层看:配置层(启动敲进的参数)、框架层(vllm / vllm-ascend 据此组织的执行路径)、算子层(NPU 上真正跑的 kernel)。profiling(op_statistic.csv / kernel_details.csv)就是算子层的观测窗口。
所有参数对算子的影响,归纳起来就四种,每种都能在这次的真实数据里找到例子。先记住这四个分类,后面所有排查都会用到它。
| 方式 | 含义 | 真实例子(GLM-5.1) |
|---|---|---|
| ① 换实现 | 同一份计算逻辑,换不同 kernel(甚至融合成一个大算子) | enable_fused_mc2=1:MoE 的 dispatch+FFN+combine 融合成 DispatchFFNCombine 一个 kernel(占 40.5%);关掉则变成 MoeDistributeDispatchV2 + 专家 MatMul + CombineV2 三个 |
| ② 换 shape | kernel 名不变,但调用的 shape / 次数变了 | --enable-chunked-prefill + CP 切片:3.5K prompt 的 MoE 调用 shape 是 220~222×6144 的 chunk,不是整条 3.5K;num_speculative_tokens=3:decode 每 step 1~4 个 token(shape [1~4,6144]) |
| ③ 增删算子 | 开了新路径,出现原本没有的 kernel | enable_dsa_cp(DSA 稀疏注意力)→ 出现 SparseFlashAttention(9.3%)+ Indexer 相关小 kernel(IndexCheck 等);--quantization ascend → 出现 QuantBatchMatmulV3(10.9%)+ AscendQuantV2(2.6%) |
| ④ 改执行方式 | kernel 清单不变,改的是怎么调度执行 | cudagraph_mode FULL_DECODE_ONLY:decode 走图模式,kernel 名一个不变,变的是 host 侧 launch 方式(间隔更密、CPU 开销小);--async-scheduling、multi stream 同理 |
先把我们环境的 op_statistic.csv(1009 prefill 窗口,15 秒)摆出来,再做逐行归因。看不懂的可以只扫一眼第一列和最后一列,主要看谁占大头。
OP Type Core Type Count Total(ms) Avg(us) Ratio(%)
DispatchFFNCombine MIX_AIC 12007 4574.4 380.9 40.5 ← enable_fused_mc2
QuantBatchMatmulV3 MIX_AIC 54036 1378.4 25.5 12.2 ← --quantization ascend (W8A8)
SparseFlashAttention MIX_AIC 12469 1284.8 103.0 11.4 ← enable_dsa_cp (DSA)
AivKernel (vector聚合) 78382 1174.7 15.0 8.5 ← 各类 vector 小算子聚合名
TransposeBatchMatMul AI_CORE 24939 549.6 22.0 4.9 ← 注意力里的矩阵变换
allgatherAicpuKernel AI_CPU 249 467.4 1877.2 4.1 ← TP16 / SP 通信
MatMulV2 AI_CORE 17212 407.5 23.7 3.6 ← 未融合的普通矩阵乘
AscendQuantV2 AI_VECTOR_CORE 41104 356.1 8.7 3.2 ← W8A8 的激活量化
ForeachAddList AI_VECTOR_CORE 12008 213.6 17.8 1.9 ← MoE 残差加(见 4.2)
reduce_scatterAicpu AI_CPU 171 172.4 1008.1 1.5 ← TP16 / SP 通信
| 启动参数 | 落到的算子 | 本窗口占比 |
|---|---|---|
enable_fused_mc2: 1(additional_config) | DispatchFFNCombine(MC2 融合:dispatch + 专家 FFN + combine) | 40.5% |
--quantization ascend(W8A8C8 权重) | QuantBatchMatmulV3(A8×W8 矩阵乘)+ AscendQuantV2(激活量化)+ 名字里带 WeightNz(NZ 权重格式) | 12.2% + 3.2% |
enable_dsa_cp: true(DSA 稀疏注意力 + CP) | SparseFlashAttention + Indexer 系列(IndexCheck 等小 kernel,未进 top10) | 11.4% |
--tensor-parallel-size 16 + SP / --enable-expert-parallel | allgather / reduce_scatter AicpuKernel(EP 的 all-to-all 在 communication.json 里看) | 5.6% |
--enable-chunked-prefill + CP 切片 | 不新增算子,决定 FFNCombine / attention 的 Input Shapes(220~222×6144) | —(②换 shape) |
--speculative-config deepseek_mtp num=3 | decode 每 step 1+3 个 token 并行验证 → shape [1~4,6144];本窗口 FFNCombine decode 形状调用 11622 次 | —(②换 shape) |
--compilation-config cudagraph_mode FULL_DECODE_ONLY | kernel 清单不变,decode 的 host launch 走图模式 | —(④改执行) |
一个细节:kernel 名里的前缀也有信息量——aclnn 开头是经 AscendC 算子库下发的标准算子,无前缀的(DispatchFFNCombine、SparseFlashAttention)是自定义融合算子包里的,对应 CANN 自定义算子包(我们对比 staticKey 的那几个文件)。
这是最能说明"参数 = 算子清单开关"的例子。MoE 一层 FFN 前后有两套实现:拆开的 3 个算子,或者融合成的 1 个大算子。
压测输入 3.5K token,经 CP=16 切片(每卡约 219 token)+ chunked prefill 按块组 batch 后,落到每卡单次 MoE 调用就是 [220~222, 6144] 的 chunk。所以看到 FFNCombine 的 Input Shapes 首维在 220 附近,就能反推:输入约 3.5K、CP=16、chunked prefill 开着。反过来,decode 的 shape 首维 = 1 + draft 数:mtp num=3 时每 step 最多 4 个 token(1 个真实 + 3 个 draft 并行验证),窗口里看到的是 [1~3, 6144]。
kernel 之间的次数关系能暴露它们的归属路径,这次窗口里有两对精准对齐:
| 对齐关系 | 说明什么 |
|---|---|
| ForeachAddList 12008 次 ≈ DispatchFFNCombine 12007 次 | 每次 MoE 调用后跟一次残差/列表加法——ForeachAddList 是 MoE 路径的随从,FFNCombine 走融合路径,它就同步存在 |
| 0815 窗口:ScatterNdUpdate 10611 次 == SparseFlashAttention 10611 次 | 每次注意力计算后一次 KV cache 散射写入——KV 写入跟着 attention 走,次数一致说明每层每 chunk 写一次 |
方法:把候选 kernel 的 Count 列放一起比,成对出现的就在同一条执行路径上。版本升级后某个 kernel 从 top10 消失(如 1009 窗口 ScatterNdUpdate 不在了),先查它是不是被融合/改名,再看调用次数去哪了——不要直接当成"优化掉了"。
拿到一份陌生环境的 profile,按下面的三步决策树走:先认头部 kernel 认模型和路径,再看 shape 认输入和阶段,最后看通信算子认并行布局。
/home/ywx1279670/profile/1010_cc8_prefill2/**/op_statistic.csv:第一行 DispatchFFNCombine 40.5% → MoE + fused_mc2;第二行 QuantBatchMatmulV3 + 第三行 SparseFlashAttention → W8A8 量化 + DSA;去 kernel_details 按 Input Shapes 分组,首维 220~222 和 1~3 两簇 → prefill 窗口里混着 decode 调用。三个结论全是配置层事实,没用任何额外信息。改完参数不确定有没有生效?按成本从低到高,依次用这三招验证:
| 手段 | 看什么 | 本例 |
|---|---|---|
| ① 服务日志回显 | 启动日志 non-default args 段有没有新参数(最快,零成本) | enable_prefill_mc2: True 回显确认配置进去了 |
| ② kernel 清单 diff | 开关前后各采 15s,diff op_statistic 的 OP Type 清单与占比(①③ 类参数必变;② 类看 shape;④ 类不变) | fused_mc2 关/开 → DispatchFFNCombine 行消失/出现;enable_prefill_mc2 开后 TTFT 753-768 持平 → 生效但无收益 |
| ③ 端侧指标交叉 | TTFT / TPOT 方向与算子变化自洽 | FFNCombine 大 chunk +8% ↔ TTFT 差 15-20%(它占 40.5%,量级吻合) |
注意 ② 的坑:有的开关生效后 kernel 名不变(改 shape/调度),diff 清单会一无所获——先判断参数属于第 1 节的哪一类,再决定看哪张表。
姊妹篇:《Profiling 实战解读:从采集、读数到把变慢钉到单个算子》——本篇引用的 op_statistic.csv 怎么采、怎么逐行读、怎么归因,姊妹篇逐步拆过。
注:本篇所有 kernel 名、次数、占比均来自 GLM-5.1(TP16×DP2、EP32、W8A8C8、MTP=3、DSA-CP)2026-10 排查时 1009 prefill 窗口 op_statistic.csv 真实数据;0815 对照数据来自 1010_cc8_0815_prefill。不同模型/配置的算子清单会有差异,映射关系(哪个参数出哪个 kernel)以实际 profile 为准。
↑ 回到顶部