File size: 15,165 Bytes
08ad55b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | # 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 实测与代码快照。*
|