⚡ 模块八:LLM 推理优化深度(MUSA)

MUSA SDK 5.2.0 与 vLLM-MUSA FP8 MoE 推理调优实战

高级·⏱ 110分钟·📅 2026-08-10
版本评审(2026-11-14)
本课写作时 v0.24.0-dev 分支尚无 tag/release。此后 v0.24.0 已于 2026-09-04 正式发布,v0.28.0 于 2026-11-03 发布;新部署应以正式 Release 为基线,见 m5-l3 的升级边界一节。本课的 FP8 MoE 后端分析保留作原理参考。vLLM-MUSA v0.24.0 Release vLLM-MUSA v0.28.0 Release
📌
本课定位
上一课 m8-l11 回答的是"新模型开源当天怎么先跑起来",本课回答它的下一个问题:软件栈换了大版本、上游又合入了一批算子级 PR 之后,怎么把版本吃透、把后端选对、把收益量化出来。本课以 MUSA SDK 5.2.0 的组件矩阵作为版本坐标,以 vLLM-MUSA 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 Driver5.2.0矩阵要求的驱动版本,是整套栈的地基
MUSA Toolkit5.2.0编译器与运行时,决定算子能不能编出来
muDNN3.4.0数值内核库,新增 5 个后端 C API
MCCL2.4.0集合通信,本轮对 MoE 专家并行影响最大
Triton-MUSA3.6.0大量融合算子的实际编译后端
TileLang-MUSA0.1.8DSL 算子路径,Qwen3.5 GDN 等走这条线
MATE0.2.3高性能算子库,含 FlashAttention-3 实现
⚠️
发布日期:n.a.(官方未标注)
MUSA SDK 5.2.0 的发布说明页面没有给出发布日期,因此本课一律把日期字段写作 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 SDK5.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_musa2.9.1.post1+musa5.2.0s5000该 wheel 仅在摩尔线程私有 index 提供
torchada0.1.72;Qwen 路径最低 0.1.760.1.76 提供嵌套 torch/cuda.h → torch/musa.h 构建映射
MATE / flash_attn_30.2.4与 SDK 矩阵中的 MATE 0.2.3 是不同的分发口径,勿混淆
Python==3.10.*,仅 x86_64MUSA wheel 只提供 cp310 x86_64,其他版本装不上
GPUMTT S5000 × 8(隔离整机)其他型号未标定,见"失败关闭"一节
vLLM-MUSAMooreThreads/vllm-musa 的 v0.24.0-dev 分支该分支无 tag、无 release,请自行钉住 commit
🚫
驱动版本不一致,请勿盲目混搭
MUSA SDK 5.2.0 的组件矩阵要求 MT Linux Driver 5.2.0,但本课引用的 FP8 MoE 基准是在 driver 3.3.5-server 的机器上测出来的。这是两条不同的事实,本课如实并列,不代表"3.3.5-server 已被官方认证可搭配 SDK 5.2.0"。

你应当做的是:① 以你实际拿到的官方镜像 / 支持矩阵为准,而不是以本课的数字反推驱动可用性;② 升级驱动前后各跑一次完整的功能与精度回归;③ 注意官方另有已知约束——当 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=8M ≤ 13 走 GEMVcontiguous DeepGEMMupstream
DeepSeek-V4 TP8M ≤ 5 走 GEMVcontiguous DeepGEMMupstream
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):图捕获路径的边界更严格,因为捕获态下的形状假设一旦失效,代价不是变慢而是错误。这正体现了标定的谨慎程度。

💡
失败关闭(fail-closed)是这套设计的灵魂
未知设备、未标定形状、不支持的布局、不安全的捕获状态——一律回退到 upstream 默认路径,而不是"猜一个阈值试试"。这意味着你把它跑在一个没被标定过的模型上时,最坏结果是"没有收益",而不是"精度出错"。做推理平台的人应该把这条原则抄进自己的算子派发层:优化可以缺席,正确性不能赌。

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
⚠️
关于容器镜像:本课不给 tag
官方确实提供了多阶段构建的 vLLM-MUSA 容器镜像,但其公开可拉取的 tag 在本轮取证中无法确认。本课因此不给出任何 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
⚠️
这两个 Pass 没有独立基准数字,不要对外承诺加速比
本轮取证中,这两个融合 Pass 没有可引用的独立 before/after 基准。因此本课只教你"怎么确认它开着、怎么关掉做对照",不给出任何加速百分比。如果你需要在自己的场景里量化收益,请用上面的 ON/OFF 双臂各跑三轮取中位数,并在报告里注明是你自测的、不是官方数字。

另外一条硬依赖: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 落点
BS161.752 → 67.831+9.84%M=1,命中 GEMV
BS4227.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,自然没有变化——高批量下的变化幅度很小,甚至可能是轻微负值。

🎯
本课最重要的一句话
这类优化的正确用法是形状感知的针对性优化,不是"升级即变快"。如果你的线上负载是高并发批处理(例如离线批量摘要、大规模评测),这套 FP8 MoE 派发策略几乎不会给你带来任何收益;但如果你的场景是低并发、低时延的交互式对话或单用户 Agent——恰恰是 M 最小的场景——接近 10% 的解码吞吐提升与近 8% 的 TPOT 下降是实打实的体验差异。先量你的 M 分布,再决定要不要投入。

顺带说一个方法论细节:这套优化在验收时使用了"预先声明的 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——本课提到的融合算子的实现层展开
✓
本课小结
你已经掌握四件事:① MUSA SDK 5.2.0 的组件矩阵与四项推理相关变化,并知道它的发布日期是 n.a.、需要自己向官方核实;② FP8 MoE 后端标定选择的原理——M 的准确定义、小 M 走 GEMV 的道理、逐形状标定的阈值表与 fail-closed 原则;③ 一条从版本自检、双 index 安装、服务拉起、融合 Pass 对照、派发验证到 exact 4096/1024 基准复现的六步可执行路径;④ 最重要的判断力——把 +9.84% 放回它成立的条件里(BS1、M=1、Qwen3.5 TP4 E=256/topk=8、8×S5000、特定软件栈),而不是把它当成一句可以到处引用的口号。推理调优的专业性,就体现在你敢不敢把每个数字的适用边界一起说出来。

附录:2026-08-10 更新章节 —— Qwen3.5 / Qwen3.6 与 DeepSeek-V4 优化案例

🆕
本章节新增于 2026-08-10
本课正文建立的核心方法论是"形状感知":同一个优化在 BS1 上是 +9.84%、在 BS64 上可能是 −0.25%。本章节用 2026-07-27 至 08-07 之间合入 vllm-musa 的一批优化 PR,把这套方法论套在三个真实模型族上。本节所有性能数字均附出处 PR 链接,未标注出处的数字一律不写。读数字之前请先记住:每个数字都绑定着特定的模型、并行度、batch size 与输入输出长度,脱离条件引用等于造假。

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 BlockPool217 → 65 slots(3.34×)内存效率
hybrid attention page64 KV 容量134,582 → 2,236,416 tokens(16.62×)容量/并发上限
grouped BF16 DeepGEMM prefillTTFT −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
⚠️
引用性能数字的三条纪律
① 永远连同条件一起引用:模型、量化格式、TP 度、batch size、输入输出长度、硬件与软件栈版本,缺一项这个数字就失去意义。② 区分"吞吐提升"与"耗时下降":−8.64% 的耗时下降不等于 +8.64% 的吞吐提升。③ 对"×倍"类数字要看清对象:Mamba BlockPool 的 3.34× 是 slot 数量下降,page64 KV 的 16.62× 是容量上升,两者都不是端到端吞吐倍数。

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 出处,每个数字都有适用边界。