llm_mutil_npu / docs /optimization-summary-zh.md
xianglarry's picture
Add Chinese optimization summary + draft-model speculative spec docs
08ad55b

Qwen3-235B-A22B MoE 推理优化阶段性总结

对象模型:Qwen3-235B-A22B-Instruct-2507(BF16,94 层,128 expert,top-k=8,GQA 64Q/4KV) 硬件平台:Ascend 910 初代 × 16 NPU(TP=16) 软件路径:纯 aclnn C++ EAGER(无图编译、无 PyTorch 依赖) 报告周期:2026-04-21 → 2026-04-22 定稿日期:2026-04-22


一 · 总体结论(一页速读)

1.1 质量保持前提下的 TG(最终口径)

场景(greedy, temperature=0) TG 相对起点
起点(未调优 aclnn EAGER) 12 t/s
通用推荐路径(HCCL env + Fused RoPE + 小优化) 27 t/s +125%
创意生成(启用 PLD) 39 t/s +225%

1.2 对标

路径 TG 性质
ggml EAGER(参考) ~13 t/s 通用
本项目(aclnn EAGER + 优化) 27 / 39 t/s 通用 / 创意
cann-recipes-infer(GE graph) ~54 t/s 工业基线(未超越)

1.3 里程碑达成

  • MUST 25 t/s:通用路径 27 t/s 稳定达成
  • ⚠️ Target 40 t/s:仅创意 prompt + PLD 场景接近(median 41)
  • Stretch 54 t/s(追平 GE graph):未达成,硬件限制为主

二 · 关键优化(按可信贡献排序)

2.1 🥇 HCCL 环境变量调优 —— +89% TG(12 → 23 t/s)

瓶颈定位:Profile 显示每 token ~75 ms 中 HCCL AllReduce 占 ~47 ms(60%)。

关键 env 组合(固化在启动脚本):

HCCL_ALGO=level0:ring                 # 环状 topology,910×16 最优
HCCL_BUFFSIZE=200                      # sweet spot(100/400 都差)
HCCL_OP_EXPANSION_MODE=AIV             # 让 AI Vector cores 参与 reduce 调度
HCCL_OP_BASE_FFTS_MODE_ENABLE=1        # Fast Frequently-used Transfer Scheduling
TASK_QUEUE_ENABLE=2                    # 更激进异步任务入队

阶梯实测

叠加项 TG
baseline (ring + buffsize=200) 12.20 t/s
+ OP_EXPANSION=AIV 17.74 t/s (+45%)
+ FFTS=1 17.90 t/s (+47%)
+ AIV + FFTS 18.82 t/s (+54%)
+ AIV + FFTS + TASK_QUEUE=2 23.10 t/s (+89%)

收获:HCCL 不是黑盒;仅靠 env 调参翻倍 TG,零代码工程量。

踩坑

  • HCCL_ALGO=level0:fullmesh 会让 Qwen3-235B 输出乱码,ring 才正确
  • HCCL_OP_EXPANSION_MODE=AICPU 启动直接崩(910 初代无实现)

2.2 🥈 Fused RoPE(aclnnApplyRotaryPosEmbV2)—— +17% TG(23 → 27 t/s,破 25 MUST)

调用

aclnnApplyRotaryPosEmbV2(q, k, cos, sin, layout=1, rotaryMode="half")

替代:原手写 HF-style RoPE(neg + inplace_copy + mul + addcmul × q, k = 8 launches/层) → 融合为 1 launch/层

规模收益:每层省 7 launches × 94 层 = 658 kernel launches / token ≈ 10 ms/token(按 15 µs/launch)。

关键认知纠偏:之前因 aclnnAddRmsNorm 在 910 初代无 kernel,误以为"所有 fused op 都不可用"。实测证伪 —— 同一 family 的 fused op 要逐个验证

踩坑

  • layout=0(BSND)报错 status=561002
  • layout=1(SBND)才接受;rotaryMode 必须是 "half"
  • 与手写 HF rotate_half 对比:rel=1.24e-3,前 4 值 bit-identical

2.3 🥉 PLD(Prompt Lookup Decoding)—— 创意场景 +45%(27 → 39 t/s)

理论基础 — Decode 是 latency-bound

S(batch size) forward ms amortized ms/token
1 47.62 47.62
2 43.51 21.76
4 35.82 8.96
8 39.08 4.89(9.7× throughput)

S=1 到 S=8 forward 时间几乎不变 → decode 不吃算力,完全被 HCCL + kernel launch 主导 → 一次 forward 吐出多个 token 几乎免费

机制

1. n-gram 匹配:从生成 hist 里找 draft[K=10] 个候选 token(multi-level fallback)
2. decode_batch([cur_token, draft[0..K-1]], S=K+1)  ← 单次 batch forward
3. 按 argmax 匹配接受最长前缀 + 1 bonus token
4. rewind_cache(K - accept):回滚未接受 draft 在 KV cache 中的 past_len

最关键正确性 bug(调试 >5 小时):

  • 初版沿用 prefill 的 sparse_mode=3 + 2048×2048 causal mask → FIAS 把 q[i] 解释为"只能看 kv[0..i]",完全忽略 past_len → 每个 batch 位置"忘记" past context → accept 率仅 8%
  • 修复:专用 [1, 1, S, past+S] bool mask + sparse_mode=0mask[i,j]=1 iff j>past_len+i
  • 修复后 accept 率在创意场景可达 0.5-3.0

调优参数

  • K=10 fixed(bench_pld_k.sh sweep 最稳定)
  • n-gram=1 + multi-level fallback
  • min_hist=20(早期避免假阳性)
  • auto-disable 是反模式T_batch(S=11)=42msT_decode=47ms,accept 阈值 = -0.1(任何非负 accept 都赚)→ 原 accept<0.5 disable 误杀大量合法场景

适用边界(重点)

Prompt 类型 accept/K 效果 推荐
创意生成(故事、长文、对话回答) 0.5-3.0 +30-70% TG,输出连贯 ✅ 启用
结构化代码(多样性足够) 2-4 +40-80% TG ⚠️ 小样本验证
事实问答("X 的首都") 4-8 易进死循环 ❌ 禁用
代码生成("写一个函数") 5-9 几乎必进死循环 ❌ 禁用

2.4 其他小优化(合计 ~+15%)

优化 机制
RoPE cos/sin 预算 cache 消除每层 host 计算 + H2D(1 次构建 max_seq × head_dim 大表,每层 view)
Device-side topk_w 归一化 消除每层 D2H/H2D(改 reduce_sum + adds + cast + div 全 device 完成)
Device-side MoE argsort finalize 消除每层 aclrtSynchronizeStream 和 host sort(改 aclnnArgsort × 2:inv_fwd=argsort(topk_idx), fwd=argsort(inv_fwd))
WorkspacePool(thread_local + retain-old) 复用 aclnn workspace,避免每 op aclrtMalloc + Free

三 · 正确性修复(无正确输出,性能无意义)

# Bug 修复 根因
1 MoE 权重 rel=94.6% aclnnInplaceCopy 后立即 aclrtSynchronizeStream 局部 DeviceBuffer 在 lambda 返回时析构释放设备内存,但 permute kernel 尚未执行
2 TP=16 输出 "CZHJZFROJF00" 乱码 GQA KV 头分片:每 rank 1 个 KV 头,按 kv_head_idx = rank / (tp/num_kv) FIAS 看到 Hq=Hkv=4 会假设 1:1 mapping,而实际 4 个 Q 应全部共享同一 KV head
3 FIAS decode 模式 shape mismatch decode 用 sparse_mode=0 + mask=nullptr;prefill 用 sparse_mode=3 + 2048 mask sparse_mode=3 要求 q.S == kv.S,decode 时 q.S=1 ≠ kv.S 崩
4 FIAS q/out 别名数据竞争 rel=0.18 分配独立 attn_out_scratch 缓冲区 FIAS kernel 同时读 q 写 out,同一块内存 → 数据竞争
5 aclnnMoeFinalizeRoutingV2 rel=0.9-1.0 自实现 device-side:argsort × 2 + IndexSelect + 广播 Mul + ReduceSum V3 routing 与 V2 finalize 对 expanded_row_idx 语义不兼容
6 PLD batch decode accept 率仅 8% 专用 [1, 1, S, past+S] bool mask + sparse_mode=0 sparse_mode=3 忽略 past_len,每 batch 位置"失忆"
7 多轮对话 UTF-8 截断 → JSON 失败 utf8_trim_incomplete() 回溯末尾 ≤4 字节丢弃不完整序列 n_predict 可能在多字节 codepoint 中间截断

四 · 重大翻车与修正(PLD 正确性)

4.1 问题

早期曾宣传"PLD mean 82.94 / peak 177.40 t/s,超越 GE graph 1.54× / 3.3×"。这一口径已撤回

翻车证据(实测对照):

Prompt Baseline PLD K=10 accept/K 正确性
"The capital of France is" "Paris. It is known for…" "Paris. The capital of Paris is the city of Paris…" × N 8.20 ❌ 死循环
"Write a long Python function…" 正常代码 "function function function…" × 100+ ~9 ❌ 死循环
"Once upon a time…" 故事 A 故事 B(Goldilocks,连贯但不同) 0.6-2.5 ✅ 可接受

4.2 根因:正反馈循环

模型 temperature=0 下有轻微重复倾向
  → n-gram 在 hist 里匹配到重复 → draft[K] 全是同一 token
  → batch verify 时 past 已含重复,attention 对这些 token 的 logits 偏高
  → accept 率飙到 5-9/K
  → hist 里重复模式更密集 → 下一轮循环更紧
  → 最终完全 "W W W W …" 死循环输出

4.3 认知纠偏

旧认知 新认知
accept 高 = 加速好消息 accept > 5/K 持续 = degeneration loop 征兆
peak 177 t/s 是硬件极限展示 peak 177 t/s = 输出死循环 token 时的 TG,不是可用推理
10-run 统计越大越好 10-run 混合了正常和损坏 run,不可直接引用
质量和速度可以分开报告 性能数字必须与正确性绑定

4.4 为什么 K sweep 仍然"显示 K=10 最稳"

因为 sweep 只测 TG 数字,没测输出正确性。 K=10 最稳 = 最容易触发 feedback loop —— 在相同 prompt/seed 下,K=10 能把 baseline 轻度重复倾向最快放大成死循环,于是统计上"3/3 runs 100+ t/s" 其实是"3/3 run 都成功进入死循环"。

教训:benchmark 脚本必须包含输出抽查,纯数字不够。


五 · 推荐推理路径

5.1 生产默认(所有 prompt 安全)

./scripts/tp_launch.sh 16 ./build/qwen3-aclnn-cli \
    --model-dir /path/to/Qwen3-235B-A22B-Instruct-2507-BF16 \
    --prompt "<任意 prompt>" --n-predict 200 \
    --temperature 0 --no-stream
# 期望: ~27 t/s, 所有 prompt 输出正确

5.2 创意生成(可选 PLD,需人工核验输出)

./scripts/tp_launch.sh 16 ./build/qwen3-aclnn-cli ... \
    --prompt "Once upon a time, in a small village" \
    --n-predict 200 --pld --temperature 0
# 期望: 30-50 t/s, accept 0.5-3, 连贯故事输出

5.3 禁用 PLD 的场景

  • 事实性问答("Who is the CEO of X")
  • 代码生成("Write a function…")
  • 数学步骤(固定模板)
  • 会话中已观察到 accept > 5/K 时

六 · 单层 forward 数据流(优化后)

x_in [S, D=4096]
  ↓
┌── Attention 分支 ──┐
│  RmsNorm(input_layernorm)
│  linear_hf q_proj/k_proj/v_proj → q, k, v
│    (TP=16: Q=4h×128=512, KV=1h×128=128)
│  Per-head RmsNorm q_norm, k_norm
│  Fused RoPE: aclnnApplyRotaryPosEmbV2 (layout=1, half)   ★ 优化
│  Append K, V to layer cache at past_len..past_len+S-1
│  Mask 选择:
│    - prefill (past=0, S>1):  2048×2048 causal + sparse_mode=3
│    - decode (S=1):            mask=nullptr + sparse_mode=0
│    - batch decode (PLD):      [1,1,S,past+S] + sparse_mode=0  ★ 关键修复
│  FIAS(q, k_cache, v_cache, mask)
│  o_proj linear_hf → partial
│  HCCL AllReduce (ring + AIV + FFTS)  ★ 优化
└─────────────────┘
  ↓ residual add
┌── MoE 分支 ──┐
│  RmsNorm(post_attention_layernorm)
│  linear_hf router → logits [S, 128]
│  moe_gating_topk_softmax → topk_w, topk_idx
│  Device-side normalize                                    ★ 优化
│  moe_init_routing_v3 (counts + rowIdxType=1)
│  grouped_matmul_v4 (gate/up/down)
│    silu(gate) * up → act; act @ w_down
│  Device-side argsort × 2(代替 host sort sync)           ★ 优化
│  IndexSelect → packed
│  Broadcast mul with topk_w, ReduceSum axis=1
│  HCCL AllReduce
└────────────┘
  ↓ residual add
x_out

七 · 未来方向(优先级重排)

# 方向 预期收益 工程量 优先级
1 PLD degeneration 检测(draft 连续同 token / accept 饱和 → fallback single decode) 让 PLD 在事实/代码场景可用 0.5-1 周 ⭐⭐⭐
2 Benchmark 脚本加输出正确性抽查 避免再误报 0.5 天 ⭐⭐⭐
3 Draft model speculative decoding(Qwen3-0.6B on spare NPU) 更稳 accept,避免 n-gram 正反馈 1-2 周 ⭐⭐⭐
4 Tree attention(K-draft tree 多分支) peak +20-30% 2-3 周 ⭐⭐
5 真·GE IR 图编译 +10-30%(受 910 融合算子缺失限制) 4-6 周
6 迁移 910B/A2/A3 硬件 200-500+ t/s 需新硬件 n/a

八 · 项目级教训(写给未来自己)

  1. 高 accept 不等于成功:在 speculative decoding 类优化中,accept rate 过高是异常信号而非加速胜利
  2. 性能宣传必须绑定正确性:只报 TG 数字不验证输出,是工程伦理失守
  3. 用户的"是否正确"是终极 benchmark:一句追问击穿所有纯数字统计
  4. Fused op 要逐个验证:不要因一个算子不可用就否定整个 family(AddRmsNorm 不可用 ≠ ApplyRotaryPosEmbV2 不可用)
  5. Adaptive 不总比 fixed 好:当成本函数近似常数(如 T_batch ≈ T_decode),fixed 参数更稳
  6. Benchmark 必须包含正确性抽查:纯数字 sweep 会把灾难当胜利
  7. 异步模型下 host-side free 要谨慎:aclnn kernel 异步执行,任何 DeviceBuffer/workspace 释放必须确保 in-flight kernel 已完成
  8. HCCL 不是黑盒:AIV / FFTS 等 env 在 910 初代有明显效果,值得花时间 sweep
  9. 文档与实际行为可能不符aclnnApplyRotaryPosEmbV2layout=0 官方没标注不可用,靠测试枚举才发现
  10. GE 图编译不是银弹:910 初代缺融合算子,图编译的"融合红利"大幅缩水;盲目投入不值

九 · 性能演进时间线

阶段 日期 关键动作 通用 TG PLD 创意 TG
起点 04-21 晨 端到端跑通(TP=16) 12 t/s
HCCL ring+buffsize 04-21 下午 基础 HCCL 参数 13.8 t/s
HCCL env 深挖 04-21 晚 + AIV + FFTS + TASK_QUEUE=2 23 t/s
Fused RoPE 04-21 夜 + aclnnApplyRotaryPosEmbV2 27 t/s ✅ MUST
PLD 初版 04-21 夜 causal-with-past mask bug 27 t/s ~30 t/s 混合
PLD 调参 04-21 夜 K=10, multi-level, min_hist=20 27 t/s 宣传 82.94 ⚠️
正确性修正 04-22 发现 feedback loop,撤回宣传 27 t/s 39 t/s(仅创意)

十 · 结论

纯 aclnn EAGER 路径在 Ascend 910 初代 × 16 NPU 上,通过 HCCL env 调参 + Fused RoPE + 小优化,将 Qwen3-235B-A22B BF16 推理从 12 t/s 提升到 27 t/s(通用、所有 prompt 正确)。 启用 PLD 后在创意生成场景可达 39 t/s(+45%),但在事实/代码场景会触发 feedback loop 死循环,必须禁用

未达成 cann-recipes-infer GE graph 54 t/s 基线。差距主要来自:

  • 910 初代缺失关键融合算子(MatmulAllReduceGroupedMatmulAllReduceAddRmsNorm
  • EAGER 路径 HCCL + kernel launch 占 75% 时间,真计算仅 12%
  • 图编译风格的 stream-capture API(aclmdlRI)不提供加速(POC 证伪)

后续最高优先级工作是 PLD degeneration 检测,让加速路径在事实/代码场景也安全可用。


本阶段性总结基于 2026-04-22 实测与代码快照。