| # 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 组合**(固化在启动脚本): |
|
|
| ```bash |
| 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) |
|
|
| **调用**: |
| ```cpp |
| 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=0`;`mask[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)=42ms` ≈ `T_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 安全) |
|
|
| ```bash |
| ./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,需人工核验输出) |
|
|
| ```bash |
| ./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. **文档与实际行为可能不符**:`aclnnApplyRotaryPosEmbV2` 的 `layout=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 初代缺失关键融合算子(`MatmulAllReduce`、`GroupedMatmulAllReduce`、`AddRmsNorm`) |
| - EAGER 路径 HCCL + kernel launch 占 75% 时间,真计算仅 12% |
| - 图编译风格的 stream-capture API(`aclmdlRI`)不提供加速(POC 证伪) |
|
|
| 后续最高优先级工作是 **PLD degeneration 检测**,让加速路径在事实/代码场景也安全可用。 |
|
|
| --- |
|
|
| *本阶段性总结基于 2026-04-22 实测与代码快照。* |
|
|