v0.24.0-dev 分支上的 FP8 MoE 后端标定选择与默认融合算子集为主线,给出一条可以在 MTT S5000 上复现的调优路径。请先明确一件事:本课不是"打开一个开关就全场变快"的教程,而是要把你训练成一个具备形状感知(shape-aware)判断力的推理工程师——同一个优化,在 BS1 上是 +9.84%,在 BS64 上可能是 −0.25%。一、先建立版本坐标:MUSA SDK 5.2.0 的组件矩阵
做推理调优最常见的翻车方式,不是算法选错,而是不知道自己脚下站的是哪个版本。同一份启动命令,在 muDNN 3.3 与 3.4、在 Triton-MUSA 3.2 与 3.6 上跑出来的算子选择可能完全不同,而报出来的吞吐差异会被误当成"调优成果"。所以本课的第一步不是敲命令,而是把版本矩阵抄在手边。
根据官方 MUSA SDK 5.2.0 发布说明,5.2.0 已取代 5.1.0 成为文档站当前最新版本,其组件版本矩阵如下:
| 组件 | 版本 | 与 LLM 推理的关系 |
|---|---|---|
| MT Linux Driver | 5.2.0 | 矩阵要求的驱动版本,是整套栈的地基 |
| MUSA Toolkit | 5.2.0 | 编译器与运行时,决定算子能不能编出来 |
| muDNN | 3.4.0 | 数值内核库,新增 5 个后端 C API |
| MCCL | 2.4.0 | 集合通信,本轮对 MoE 专家并行影响最大 |
| Triton-MUSA | 3.6.0 | 大量融合算子的实际编译后端 |
| TileLang-MUSA | 0.1.8 | DSL 算子路径,Qwen3.5 GDN 等走这条线 |
| MATE | 0.2.3 | 高性能算子库,含 FlashAttention-3 实现 |
n.a. / 官方未标注,不做任何推断。可以确认的事实只有一条:该版本在 2026-07-20 之前已经在线(本轮巡检核对)。如果你需要在变更单或合规材料里填"版本发布日期",请直接向摩尔线程官方渠道索取,不要引用任何二手推断值。5.2.0 中与本课主线直接相关的四项变化:
- MUSA Graph 新增 33 个 Graph API:图捕获(capture)能力更完整。这一点在本课很关键——FP8 MoE 的后端标定策略在 eager 与 capture 两种执行模式下阈值可以不同,图捕获路径越稳定,小 M 解码窗口的收益越容易稳定复现。
- MCCL 2.4.0 支持 GPU peer direct memory access 与 kernel 内发起通信:官方称可降低 MoE 专家并行(EP)与投机解码的通信时延。对 MoE 模型而言,这条比任何单点算子优化都更有结构性意义,因为 MoE 的通信量本身就是瓶颈的一部分。
- muDNN 3.4.0 新增五个后端 C API:
mudnnGather/mudnnUnfold/mudnnScan/mudnnWeightNorm/mudnnL2Loss。Gather与Scan恰好是 MoE 路由与 top-k 采样里最常见的两类原语。 - 实验特性 MTSHMEM DeepEP(normal / ll_compatibility):面向大规模专家并行的通信原语,标注为实验特性,不要在生产环境直接依赖。
二、兼容性声明(本课必读,先看完再动手)
本课引用的所有性能数字都来自一次具体的实测环境。把这套环境完整写出来,不是形式主义,而是因为换掉其中任意一项,数字就不再成立。
| 项目 | 本课基准所用取值 | 说明 |
|---|---|---|
| MUSA SDK | 5.2.0(Toolkit 5.2.0 / muDNN 3.4.0 / MCCL 2.4.0 / Triton-MUSA 3.6.0 / TileLang-MUSA 0.1.8 / MATE 0.2.3) | 官方组件矩阵;发布日期 n.a. |
| MT Linux Driver | 矩阵要求 5.2.0;FP8 MoE 基准的实测机器为 3.3.5-server | 两者不一致,见下方红框 |
| torch / torch_musa | 2.9.1.post1+musa5.2.0s5000 | 该 wheel 仅在摩尔线程私有 index 提供 |
| torchada | 0.1.72;Qwen 路径最低 0.1.76 | 0.1.76 提供嵌套 torch/cuda.h → torch/musa.h 构建映射 |
| MATE / flash_attn_3 | 0.2.4 | 与 SDK 矩阵中的 MATE 0.2.3 是不同的分发口径,勿混淆 |
| Python | ==3.10.*,仅 x86_64 | MUSA wheel 只提供 cp310 x86_64,其他版本装不上 |
| GPU | MTT S5000 × 8(隔离整机) | 其他型号未标定,见"失败关闭"一节 |
| vLLM-MUSA | MooreThreads/vllm-musa 的 v0.24.0-dev 分支 | 该分支无 tag、无 release,请自行钉住 commit |
你应当做的是:① 以你实际拿到的官方镜像 / 支持矩阵为准,而不是以本课的数字反推驱动可用性;② 升级驱动前后各跑一次完整的功能与精度回归;③ 注意官方另有已知约束——当 mtbios 高于某一版本时,驱动、dcgm 与 mtml 需要成套升级,具体取值以发布说明为准。
另外要有心理准备:驱动文档站与开发者站 SDK 下载页的更新节奏明显滞后于 SDK 发布说明,5.2.0 目前主要通过私有渠道与容器镜像分发。在你的环境里,"文档写了什么"和"你实际装了什么"必须分别核对。
三、FP8 MoE 后端标定选择:原理与阈值
3.1 先把 M 定义清楚
整套策略只围绕一个变量转:M。它的定义是一次 MoE 调用中 hidden_states 的第 0 维长度,也就是这次调用要处理的 token 行数。请特别注意它不是什么:
- 不是 HTTP 层的并发批大小(虽然在纯 decode 稳态下两者常常数值相等);
- 不是每个专家收到的路由条数(那是 topk 分发之后的事情)。
把 M 搞混,是复现这套优化时最常见的失败原因。稳态解码阶段每个活跃序列每步只产出 1 个 token,所以 M 约等于活跃序列数;而 prefill 阶段 M 是被切分到本次调用的 token 数,会受 chunked prefill 配置影响,通常是几百到几千的量级。
3.2 为什么小 M 要换 GEMV
MoE 的专家计算本质是一批矩阵乘。当 M 很大时,这是一个标准的 GEMM 问题,DeepGEMM 这类为大矩阵设计的实现能把张量核心喂满;但当 M 只有个位数时,矩阵乘退化成"瘦长矩阵 × 权重",算术强度极低,瓶颈从算力彻底转移到访存与 kernel 启动开销。这时继续走 GEMM 内核,等于用一台重型机床去拧一颗螺丝。GEMV(矩阵-向量乘)内核针对这个区间设计,能显著压低单步解码时延,直接反映为 TPOT 下降。
问题在于:切换点在哪里?这个交叉点由模型形状(专家数 E、topk、每卡分片后的 N/K)、并行度、执行模式(eager / capture)共同决定,没有普适答案,只能逐形状标定。这正是这套"自动派发"机制的价值——把标定结果固化成策略表。
3.3 已标定的阈值表
| 每卡 FP8 MoE 形状 | 小 M 区间 | 大 M 且符合条件的 eager 场景 | 其余 |
|---|---|---|---|
| Qwen3.5 live TP4,E=256 / topk=8 | M ≤ 13 走 GEMV | contiguous DeepGEMM | upstream |
| DeepSeek-V4 TP8 | M ≤ 5 走 GEMV | contiguous DeepGEMM | upstream |
| DeepSeek-V2-Lite TP1(eager) | M ≤ 3 走 GEMV | 不适用(E=64 不满足条件) | upstream |
| DeepSeek-V2-Lite TP1(capture) | 仅 M = 1 走 GEMV | 不适用 | upstream |
注意 DSV2-Lite 在 eager 与 capture 下阈值不同(3 对 1):图捕获路径的边界更严格,因为捕获态下的形状假设一旦失效,代价不是变慢而是错误。这正体现了标定的谨慎程度。
3.4 用一段代码复刻这个判据
# fp8_moe_policy.py —— 复刻自动派发判据(教学用简化版,可直接运行)
#
# M = hidden_states.shape[0],即一次 MoE 调用的 token 行数;
# 既不是 HTTP 并发批大小,也不是每个专家收到的路由条数。
GEMV_THRESHOLDS = {
# (模型标识, 每卡形状/并行度, 执行模式): GEMV 生效的最大 M
("qwen3_5_live", "tp4_e256_topk8", "eager"): 13,
("qwen3_5_live", "tp4_e256_topk8", "capture"): 13,
("dsv4", "tp8_block32x8", "eager"): 5,
("dsv4", "tp8_block16x8", "eager"): 5,
("dsv2_lite", "tp1", "eager"): 3,
("dsv2_lite", "tp1", "capture"): 1,
}
def pick_backend(key, M, deepgemm_eligible=False):
"""返回 'gemv' / 'deepgemm' / 'upstream'。
未标定的 key 一律 fail-closed 回 upstream,绝不外推阈值。
"""
if key not in GEMV_THRESHOLDS:
return "upstream" # 未知设备或未标定形状
if M <= GEMV_THRESHOLDS[key]:
return "gemv" # 小 M 解码窗口
if deepgemm_eligible:
return "deepgemm" # 大 M 且符合条件的 eager prefill
return "upstream"
def rows_in_decode_step(num_running_seqs, tokens_per_seq=1):
"""稳态解码阶段的 M 估算:每个活跃序列每步产出 1 个 token。"""
return num_running_seqs * tokens_per_seq
if __name__ == "__main__":
key = ("qwen3_5_live", "tp4_e256_topk8", "capture")
for bs in (1, 4, 16, 64):
m = rows_in_decode_step(bs)
print("BS", str(bs).rjust(2), "-> M =", str(m).rjust(2),
"-> backend =", pick_backend(key, m))
# 预期输出:BS 1/4 命中 gemv;BS 16/64 落回 upstream。
# 这正好解释了后面基准表里"小批量涨、大批量平"的形状。
四、Step-by-Step:从环境校验到基准复现
步骤 1:把版本钉死(先跑自检,再谈调优)
# check_musa_stack.py —— 调优前的第一道闸门
# 用法: python3 check_musa_stack.py
import platform
import sys
EXPECTED = {
"python": "3.10", # wheel 仅 cp310
"machine": "x86_64", # wheel 仅 x86_64
"torch": "2.9.1.post1+musa5.2.0s5000",
"torchada_min": "0.1.76", # Qwen 路径最低要求
"mate": "0.2.4",
}
def line(name, got, want=None):
hit = want is None or str(got).startswith(str(want))
tag = "OK" if hit else "CHECK"
suffix = "" if want is None else " (期望 " + str(want) + ")"
print(tag.ljust(6), name.ljust(22), str(got) + suffix)
def main():
line("python", platform.python_version(), EXPECTED["python"])
line("machine", platform.machine(), EXPECTED["machine"])
try:
import torch
line("torch", torch.__version__, EXPECTED["torch"])
# torch_musa 构建会暴露 MUSA 运行时版本号,5.2.0 对应 50200
line("torch.version.musa", getattr(torch.version, "musa", "n.a."))
except Exception as exc:
line("torch", "import failed: " + repr(exc))
return 1
try:
import torch_musa # noqa: F401 仅为注册 musa 后端
line("torch_musa", torch_musa.__version__, EXPECTED["torch"])
line("musa.is_available", torch.musa.is_available())
line("musa.device_count", torch.musa.device_count())
except Exception as exc:
line("torch_musa", "import failed: " + repr(exc))
return 1
for mod in ("torchada", "mate", "flash_attn_3", "vllm"):
try:
line(mod, getattr(__import__(mod), "__version__", "n.a."))
except Exception:
line(mod, "not installed")
return 0
if __name__ == "__main__":
sys.exit(main())
任何一行打出 CHECK,都请先解决再往下走。尤其是 Python 版本:MUSA wheel 只有 cp310 x86_64 构建,在 3.11/3.12 环境里 pip 会直接找不到候选包,而不是给出可读的错误信息。
步骤 2:配置双 pip index(占位符必须替换)
#!/usr/bin/env bash
set -euo pipefail
# ============ 占位符 ============
# 下面尖括号里的内容是【占位符】,不是可用地址。
# torch==2.9.1.post1+musa5.2.0s5000 只在摩尔线程私有 index 提供,
# 该地址需要通过官方商务 / 支持渠道获取,本课不提供、也无法提供。
PRIVATE_INDEX="<MTHREADS_PRIVATE_INDEX_URL>"
# ================================
if [[ "$PRIVATE_INDEX" == *"<"* ]]; then
echo "请先把 PRIVATE_INDEX 替换成真实地址后再执行" >&2
exit 1
fi
python3 -c 'import sys; assert sys.version_info[:2] == (3, 10), "MUSA wheel 仅提供 cp310"'
# 双 index:公共 PyPI 取通用依赖,私有 index 取 MUSA 专用轮子
pip install \
--index-url https://pypi.org/simple \
--extra-index-url "$PRIVATE_INDEX" \
"torch==2.9.1.post1+musa5.2.0s5000" \
"torch_musa==2.9.1.post1+musa5.2.0s5000" \
"torchada>=0.1.76"
python3 check_musa_stack.py
docker pull 具体 tag——写一个猜的 tag 只会让你浪费半小时在 manifest unknown 上。请以你从官方渠道拿到的镜像清单为准,本课后续命令里凡是出现 <...> 的位置都是需要你替换的占位符。步骤 3:拉起 Qwen3.5-35B-A3B-FP8(TP4)
#!/usr/bin/env bash
set -euo pipefail
MODEL="<PATH_TO_Qwen3.5-35B-A3B-FP8>" # 占位符:换成你本地权重目录
PORT=8000
python3 -m vllm.entrypoints.openai.api_server \
--model "$MODEL" \
--served-model-name qwen35moe \
--tensor-parallel-size 4 \
--max-model-len 8192 \
--no-enable-prefix-caching \
--port "$PORT" \
2>&1 | tee vllm-server.log
三个参数值得解释:--tensor-parallel-size 4 对应标定表里的 "Qwen3.5 live TP4",改成别的 TP 就离开了标定区间;--no-enable-prefix-caching 是为了对齐基准口径——前缀缓存会让重复 prompt 的 TTFT 失真,做 A/B 时必须关掉;tee 保存日志是为了步骤 5 的验证。
步骤 4:显式声明编译期融合 Pass(用于 A/B 留痕)
Qwen serving 路径上有两个融合 Pass:dense SiLU+mul 与 group-128 FP8 量化融合、residual RMSNorm 与 group-128 FP8 量化融合。它们通过 vLLM 标准的 post-grad pass manager 接入,开关名分别是 PassConfig.fuse_act_quant 与 PassConfig.fuse_norm_quant,在该分支上默认开启,没有额外的 opt-in 环境变量。显式写出来的价值不在"打开",而在"能关掉做对照":
# 候选臂(默认行为,显式写出便于在日志中留痕)
FUSE_ON='{"pass_config": {"fuse_act_quant": true, "fuse_norm_quant": true}}'
# 对照臂(关闭两个融合 Pass)
FUSE_OFF='{"pass_config": {"fuse_act_quant": false, "fuse_norm_quant": false}}'
python3 -m vllm.entrypoints.openai.api_server \
--model "$MODEL" \
--served-model-name qwen35moe \
--tensor-parallel-size 4 \
--no-enable-prefix-caching \
--compilation-config "$FUSE_ON" \
--port 8000 2>&1 | tee vllm-fuse-on.log
另外一条硬依赖:Qwen 路径要求 torchada ≥ 0.1.76,因为该版本才提供嵌套
torch/cuda.h → torch/musa.h 的构建映射;低于这个版本,原生扩展会在编译期失败,而不是在运行期降级。步骤 5:验证 GEMV / DeepGEMM 切换点确实生效
# 从服务日志里找后端派发痕迹(关键字随构建版本而异,请按需调整)
grep -iE "moe|gemv|deepgemm|dispatch|backend" vllm-server.log | tail -60
# 用不同并发各打一发,观察 decode 阶段落在哪个区间
for BS in 1 4 16 64; do
echo "===== concurrency=$BS ====="
python3 fp8_moe_policy.py
done
判定标准很直接:BS1 / BS4 的稳态解码步应当落在 GEMV 区间(M = 1、4,均 ≤ 13),BS16 / BS64 应当落在 upstream。如果日志显示 BS16 也在走 GEMV,说明你的 M 换算口径有问题(多半是把 HTTP 批大小当成了 M);如果 BS1 也没走 GEMV,先检查 TP 是不是 4、形状是不是 E=256 / topk=8。
步骤 6:按 exact 4096 / 1024 口径复现基准
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="http://127.0.0.1:8000"
MODEL_NAME="qwen35moe"
OUT_DIR="ab_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUT_DIR"
# 对齐官方口径:精确 4096 输入 / 1024 输出、关闭前缀缓存、
# 每个批量独立跑、结果落盘后再比对。
for BS in 1 4 16 64; do
for REP in 1 2 3; do
python3 benchmarks/benchmark_serving.py \
--backend openai \
--base-url "$BASE_URL" \
--endpoint /v1/completions \
--model "$MODEL_NAME" \
--dataset-name random \
--random-input-len 4096 \
--random-output-len 1024 \
--ignore-eos \
--max-concurrency "$BS" \
--num-prompts $((BS * 4)) \
--percentile-metrics ttft,tpot,itl \
--save-result \
--result-filename "$OUT_DIR/bs$BS-rep$REP.json"
done
done
echo "结果已写入 $OUT_DIR,请取三轮中位数后再与对照臂比较"
三个必须遵守的纪律:① 输入输出长度必须精确,--ignore-eos 保证每条请求都产出满 1024 个 token,否则平均值会被短回复污染;② 对照臂与候选臂要使用各自独立的编译缓存目录,否则第一次跑的一方会背上 JIT 编译时间;③ 至少三轮取中位数,单轮结果在 1% 量级的差异上没有意义。
五、基准数据解读:为什么这不是"普适加速"
下面是在 8×MTT S5000、exact 4096 输入 / 1024 输出口径下测得的两组关键数字。
Qwen3.5-35B-A3B(TP4,E=256 / topk=8):
| 批量 | 输出吞吐(tok/s)base → PR | 相对变化 | M 落点 |
|---|---|---|---|
| BS1 | 61.752 → 67.831 | +9.84% | M=1,命中 GEMV |
| BS4 | 227.352 → 247.811 | +9.00% | M=4,命中 GEMV |
| BS16 / BS64 | 变化很小 | 约 ±0.5% 以内 | M > 13,走 upstream |
DeepSeek-V2-Lite-FP8(TP1):
| 指标 | base → PR | 相对变化 |
|---|---|---|
| BS1 输出吞吐(tok/s) | 76.584 → 83.007 | +8.39% |
| BS1 TPOT(ms) | 12.775 → 11.762 | −7.92% |
把这两张表和 3.4 节那段代码放在一起看,结论就非常清楚了:收益完全集中在 M ≤ 阈值的小批量解码窗口。BS1 的 M 是 1,BS4 的 M 是 4,都落在 13 以内;BS16 起 M 已经超过阈值,派发器直接回到 upstream,自然没有变化——高批量下的变化幅度很小,甚至可能是轻微负值。
顺带说一个方法论细节:这套优化在验收时使用了"预先声明的 3% 回归红线"——任何一格测量结果的劣化超过 3% 就要整改。把红线写在测量之前,而不是看完数据再决定标准,这是性能工作能不能被信任的分水岭。建议你把这条搬进自己的性能验收流程。
六、排错清单
- pip 找不到候选包:99% 是 Python 不是 3.10 或架构不是 x86_64。先跑步骤 1 的自检脚本。
- 原生扩展编译失败,报找不到 torch/cuda.h:torchada 低于 0.1.76,升级即可。这是编译期失败,不会静默降级。
- 开了优化但完全没变化:先确认 M 落点(步骤 5)。其次确认 TP 与形状在标定表内;不在表内的形状是设计上回退 upstream,不是 bug。
- A/B 数字忽大忽小:多半是编译缓存或前缀缓存串味。两臂必须各自独立的缓存根目录,并显式关闭前缀缓存。
- capture 模式下结果与 eager 不一致:注意 DSV2-Lite 在 capture 下阈值收紧到 M=1,这是刻意为之。捕获态形状假设失效的代价是正确性,不是性能。
- 想在非 S5000 卡上复现:标定表只覆盖 MTT S5000,其他设备会 fail-closed 回 upstream。你需要自己重做一遍交叉点标定,而不是照抄阈值。
七、延伸阅读与课程衔接
- MUSA SDK 5.2.0 发布说明:组件矩阵、新增 API 与已知问题的一手来源
- MooreThreads/vllm-musa:本课全部 PR 证据的来源仓库(工作分支
v0.24.0-dev) - vllm-musa 开放 PR 列表:本轮另有 17 个未合入的性能 PR,下轮巡检复查
- MooreThreads/torchada · MooreThreads/mate:本课依赖的两个关键组件
- m8-l11:Day-0 适配方法论——本课是它的下一站,从"当天跑起来"走到"版本感知的算子级调优"
- m8-l1 / m8-l2:TTFT / TPOT 指标口径与 KV Cache 基础,是读懂本课基准表的前置
- m6-l4 / m6-l7:TileLang-MUSA 与 MATE——本课提到的融合算子的实现层展开
附录:2026-08-10 更新章节 —— Qwen3.5 / Qwen3.6 与 DeepSeek-V4 优化案例
0. 先建立一个读数字的框架
本节会出现十几个性能数字。在看它们之前,先把这些数字分成四类,否则很容易把不同量纲的改进混为一谈。
第一类是端到端吞吐,单位通常是 tok/s。这是唯一能直接对应到"服务能接多少量"的数字,也是唯一适合向业务方汇报的数字。第二类是延迟指标,TTFT 与 TPOT,它们与吞吐并不是简单的倒数关系,在批处理场景下甚至可能同向变化。第三类是容量与资源指标,比如 KV 容量的 token 数、内存池的 slot 数。这类数字的提升倍数往往非常大(本节里有 16.62×),但它们不是吞吐倍数,而是"并发上限的天花板被抬高了多少"。第四类是微观耗时,比如某个 kernel 从 316.48 us 降到 93.08 us。它告诉你优化确实生效了,但能否传导到端到端,取决于这个 kernel 在总耗时里占多大比重。
把四类分清楚之后,你就不会再犯那个最常见的错误:看到"内存池 3.34×"就以为服务快了三倍多。实际上这两件事之间隔着好几层传导,而每一层都可能把收益衰减掉。下面逐个模型族看真实案例时,请随时对照这四分类。
1. Qwen3.5:MoE 路由与混合 Decode
PR #137 针对 Qwen3.5 的 MoE 与混合 Decode 做了一组优化,包含:fused top-k routing(把路由计算融进一个 kernel)、面向 256 专家 top-k=8 配置的专门实现、even-K 与 scalar-scale 快路径、288-thread 的 moe_align、以及 64-token 的 paged-attention 快路径。
可复现的量化结果:Qwen3.5-35B-A3B-FP8,TP1 配置下吞吐从 396.12 tok/s 提升到 436.25 tok/s(PR #137)。约合 +10.1%。
此外 PR #152 修复了 Qwen3.5 开启 MTP 时无法启动的问题,涉及 TileLang kernel 中 causal_conv1d 的 stride 修复、GDN MTP token 对齐,以及 MTP verify 改走 flash_attn_with_kvcache。如果你此前尝试过在 Qwen3.5 上开 MTP 失败,可以在这个 PR 之后重试。
2. Qwen3.6:一次教科书式的多点优化
PR #118 是本轮量化证据最完整的一个 PR,五项优化各自独立可验证:
| 优化项 | 效果 | 性质 |
|---|---|---|
| Mamba BlockPool | 217 → 65 slots(3.34×) | 内存效率 |
| hybrid attention page64 KV 容量 | 134,582 → 2,236,416 tokens(16.62×) | 容量/并发上限 |
| grouped BF16 DeepGEMM prefill | TTFT −9.54% | 首字延迟 |
移除 sampler 中的 .item() | TPS +10.30% | 同步开销 |
| fused shared-expert routing | +5.51% | kernel 融合 |
| 综合结果 | 约 666 tok/s | — |
这张表里最值得学的是第四行:移除 sampler 里的一个 .item() 调用,换来 +10.30% TPS。.item() 会把 GPU 张量取回 CPU,强制一次设备同步——在 Decode 这种每步都执行的热路径上,一次同步就足以打断流水线。这类"非算子级"的性能问题在实践中占比极高,却常常被忽视,因为它不出现在任何 kernel profile 的热点列表顶部。
PR #171 与 PR #173 继续优化 Qwen3.6-35B-A3B 的 BF16 serving:fused routed/shared gate proj +3.64%;dual-stream GDN decode projections −8.64%(耗时下降);移除 GDN buffer clear;metadata 复用;SSM slot-mapping skip 从 316.48 us 降到 93.08 us;C25 W1 tile 配置 M32/N64/K128/W8 带来 +2.17%。
小 batch 场景另有专门优化:PR #172 与 PR #175 实现了原生 BF16 MoE GEMV(适用条件 E=256/257、top_k=8/9、M≤12),结果是 BS1 的 TPOT 下降 31.2%,BS4 下降 13.9%。注意"M≤12"这个条件——它正是本课正文强调的形状感知阈值的又一个实例:超出这个范围,GEMV 路径不再优于 GEMM。
3. DeepSeek-V4:indexer decode kernel 与 FP8 融合
PR #151 为 DeepSeek-V4 的 C4 recent-indexer compressor 实现了 MUSA decode kernel,并修复了 CAR graph-memory 问题。吞吐随 batch 的变化为:BS1 48.66 tok/s,BS64 389.94 tok/s。这条曲线本身就是一堂课——从 BS1 到 BS64 吞吐提升约 8 倍而非 64 倍,说明 Decode 阶段仍受访存与同步开销主导,batch 带来的边际收益递减。
PR #156 则是一个 FP8 算子融合的范例:把 shared-expert SwiGLU 下投影路径上的 clamp + SiLU + mul + BF16 round + group absmax + FP8 quant 六个步骤融合进单个 MUSA kernel,输出直接喂给 fp8_gemm_nt。六步变一步意味着五轮中间张量的写回与读取被彻底消除,这是典型的访存受限场景下最有效的优化手段。
此外 PR #144 针对 DeepSeek-V4 no-MTP 路径做了 FlashMLA cache packing 并行化、DeepGEMM 路由、MHC prenorm 融合、split tiles W1 4×32 / W2 32×4、aux-stream overlap 等优化。PR #140 引入了 MUSA JIT custom allreduce 与 RMSNorm 融合,覆盖 TP2/TP4/TP8,并保证 graph-safe registered inputs。
4. 架构侧变化:优化契约中心化
PR #166 与 PR #167 引入了 ModelSignature / ExecutionSignature 的 provider registry 与 QWEN35_36_* 常量,把 DSV4 的优化绑定到模型契约上;PR #170 用能力分发取代了默认开启的进程开关。这组重构的意义在于:优化不再是散落各处的 if 判断,而是与"模型签名 + 执行签名"绑定的显式契约。对使用者的影响是——某个优化是否生效,越来越取决于你的模型是否匹配签名,而不是你是否打开了某个环境变量。这也意味着排查"为什么我没享受到这个优化"时,第一步应该是确认签名匹配。
5. Step-by-Step:如何复现与验证这些数字
# Step 1 — 固定基线:分支、镜像、模型、并行度全部记录在案
git -C /path/to/vllm-musa log -1 --format='%H %ad %s'
docker images | grep vllm
# Step 2 — 固定负载:exact 输入/输出长度,不要用变长真实流量做对比
BENCH="python benchmarks/benchmark_serving.py \
--model /models/Qwen3.6-35B-A3B \
--dataset-name random --random-input-len 4096 --random-output-len 1024"
# Step 3 — 逐 batch size 扫描,而不是只测一个点
for BS in 1 4 16 64; do
echo "=== batch size $BS ==="
$BENCH --max-concurrency $BS --num-prompts $((BS*8))
done | tee /tmp/bench_after.log
# Step 4 — 与优化前基线逐 BS 比对(关键:小 BS 与大 BS 分开看结论)
diff <(grep -E "Output token throughput|TPOT" /tmp/bench_before.log) \
<(grep -E "Output token throughput|TPOT" /tmp/bench_after.log)
# Step 5 — 确认优化确实生效(契约中心化之后尤其重要)
grep -iE "signature|fast path|gemv|fused" server.log | head -30
6. 从这批 PR 里能学到的三个通用优化模式
十几个 PR 看上去零散,但它们反复使用的手段其实只有三类。把这三类记住,你以后看任何推理优化 PR 都能快速归类,甚至能反过来指导自己的优化工作。
模式一:消除同步点。典型代表是移除 sampler 中的 .item()(+10.30% TPS)与 dual-stream GDN decode projections(−8.64% 耗时)。GPU 执行是异步的,任何强制 CPU 等待 GPU 的操作都会把流水线扳断。这类问题的隐蔽性在于:它不会出现在 kernel profile 的热点榜首位,因为它本身不耗 GPU 时间,只是让 GPU 空等。排查方法:看时间线上的空白间隙,而不是看占比排序的函数列表。
模式二:kernel 融合。典型代表是 DeepSeek-V4 的 SwiGLU FP8 下投影六步合一(#156)、fused top-k routing(#137)、fused shared-expert routing(+5.51%)与 RMSNorm 融合(#140)。融合的收益来源单一且可预估:减少中间张量的写回与读取。因此它在访存受限的算子上收益最大,在计算受限的算子上几乎无效。判断一个融合机会值不值得做,先算它的算存比。
模式三:形状特化快路径。典型代表是 M≤12 的 BF16 MoE GEMV(BS1 TPOT −31.2%)、even-K/scalar-scale 快路径、64-token paged-attn 快路径、288-thread moe_align、C25 W1 tile M32/N64/K128/W8。这类优化的共同特点是只在特定形状区间内有效,超出区间可能反而变慢。这正是本课正文反复强调的形状感知的根源:优化不是开关,而是带适用域的分段函数。
三类模式对应三种不同的验证策略。同步点消除类优化,在所有 batch size 上应该普遍受益,如果只在某个点上变快,要怀疑测量噪声;kernel 融合类优化,收益应该与该算子在总耗时中的占比成正比,可以用 profile 交叉验证;形状特化类优化,必须扫描整个形状区间,包括边界以外的点,确认没有回退。把这三套验证策略写进你的基准脚本,比背下任何一个具体数字都有价值。
本章节小结:三个模型族、十余个 PR,共同验证了本课正文的形状感知方法论。Qwen3.5-35B-A3B-FP8 TP1 从 396.12 到 436.25 tok/s(#137);Qwen3.6 五项优化合计约 666 tok/s,其中移除一个 .item() 就换来 +10.30% TPS(#118);Qwen3.6 BF16 serving 的 SSM slot-mapping skip 从 316.48 us 降到 93.08 us(#171/#173);小 batch BF16 MoE GEMV 在 M≤12 时 BS1 TPOT −31.2%(#172/#175);DeepSeek-V4 C4 indexer decode kernel BS1 48.66 → BS64 389.94 tok/s(#151),shared-expert SwiGLU FP8 下投影六步融合为单 kernel(#156)。每个数字都有 PR 出处,每个数字都有适用边界。