【Bug已解决】[CUDA] Qwen3.6-35B-A3B Throughput Optimization 解决方案
一、现象长什么样
把 Qwen3.6-35B-A3B(一个 35B 参数、每次激活约 3B 的 MoE 模型)导出成 ONNX,在 ONNX Runtime 的 CUDA EP 上做推理,吞吐明显低于预期(比如同样一张 H100,对比厂商优化后的参考实现只有 40%~60% 的吞吐):
import onnxruntime as ort so = ort.SessionOptions() so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess = ort.InferenceSession("qwen3.6-35b-a3b.onnx", so, providers=["CUDAExecutionProvider"]) # 默认配置下,MoE 专家计算与 GQA 注意力没充分融合,吞吐偏低最小信号:
吞吐远低于参考实现 GPU 利用率波动大,kernel 启动密集 专家路由 + 各 expert 计算没融成高效 kernel注意:结果正确,只是慢。这是针对 Qwen3.6-35B-A3B 这个具体 MoE 结构的吞吐优化问题。
二、背景
Qwen3.35B-A3B 是 MoE 结构:总参数 35B,但每层只有约 3B 参数被激活(8 个 expert 里选 2~3 个)。它还有几个对 CUDA 吞吐极关键的特征:
- MoE 路由 + 分组专家计算:token 按 router 分数被分发到不同 expert,每个 expert 是一次大矩阵乘。若没融合,路由、分组、各 expert 计算被拆成几十上百个小 kernel,launch 开销爆炸。
- GQA(分组查询注意力):KV 头远少于 Q 头。需要
GroupQueryAttention融合 kernel,且正确传num_kv_heads。 - 可能的长上下文 / 高并发:batch 大时,kernel 是否能针对常见形状特化(CUDA graph)很关键。
ONNX Runtime 的 CUDA EP 要让这个模型跑满吞吐,需要:(a) MoE 融合(把路由+专家计算合成一个高效 kernel 或紧密调度的序列);(b) GQA 融合;(c) CUDA graph 捕获固定结构;(d) 动态维度覆盖让 kernel 特化。默认配置下这些没全开,于是吞吐掉一截。
三、根因
根因是CUDA EP 的关键优化对 Qwen3.6-35B-A3B 没全激活,且模型导出形态不利于融合:
- MoE 未充分融合:导出时 router + 各 expert 是标准
MatMul/Gather/Concat,ORT 的 MoE 融合 pass 没匹配上,大量小 kernel 串行。 - GQA 融合属性/结构不匹配:
GroupQueryAttention融合要求num_kv_heads/head_size齐全且导出保留Attention节点;若拆成裸MatMul则无法融合。 - CUDA graph 没开:默认
enable_cuda_graph=false,每轮推理重新录制命令,launch 开销大。 - 动态维度未覆盖:没给
batch/seq设自由维度边界,kernel 无法特化。 - 不是结果错:融合与图捕获没开导致 kernel 碎片化、GPU 利用率低、吞吐低。
所以这不是数值错,而是融合与图捕获未激活,MoE 大模型吞吐被 launch 开销拖垮。
四、最小可运行复现
下面用 Python 模拟“MoE 融合与否对 kernel 启动次数的影响”(Qwen3 风格,每层 8 expert 选 2):
import numpy as np def run_unfused(num_tokens, num_experts=8, top_k=2): """未融合:每个 token 被选中的 expert 各一次 MatMul(极多 launch)。""" launches = 0 for _ in range(num_tokens): for _ in range(top_k): launches += 1 return launches def run_fused(num_tokens, num_experts=8, top_k=2): """融合:整批一次 fused MoE kernel。""" return 1 if __name__ == "__main__": for n in (512, 2048, 8192): unfused = run_unfused(n) fused = run_fused(n) print(f"tokens={n}: 未融合 launch={unfused}, 融合 launch={fused}, " f"差距≈{unfused/fused:.0f}x")跑出来:tokens=8192时未融合 ~16000 次 launch、融合 1 次,上万倍 launch 差距——这正是 MoE 不融合时吞吐崩塌的简化模型(实际加速没这么夸张,但量级说明问题)。
五、解决方案(第一层:最小直接修复)
最小修复:打开 CUDA 优化开关并让导出形态可被融合识别,针对 Qwen3.6-35B-A3B:
import onnxruntime as ort so = ort.SessionOptions() so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL so.add_free_dimension_override_by_name("batch_size", 1, 128) so.add_free_dimension_override_by_name("seq_len", 1, 32768) cuda_opts = { "device_id": 0, "enable_cuda_graph": True, "use_tf32": True, "cuda_graph_enable_partial": True, "max_batch_size": 128, } provider = ("CUDAExecutionProvider", cuda_opts) sess = ort.InferenceSession("qwen3.6-35b-a3b.onnx", so, providers=[provider])导出时保留 MoE 子图与Attention/GroupQueryAttention节点(用支持 MoE 导出的工具),让 ORT 的 MoE 融合 pass 能匹配。这一层立刻把吞吐抬上去。
六、解决方案(第二层:结构性改进)
把“Qwen3.6-35B-A3B 在 CUDA 上的优化配置”收口成唯一的配置对象OrtQwenThroughputPolicy,部署读它:
from dataclasses import dataclass, field from typing import Dict, Tuple @dataclass(frozen=True) class OrtQwenThroughputPolicy: """Qwen3.6-35B-A3B 在 CUDA EP 上的吞吐优化单一事实来源。""" optimization_level: str = "ORT_ENABLE_ALL" enable_cuda_graph: bool = True use_tf32: bool = True cuda_graph_partial: bool = True free_dim_overrides: Tuple[Tuple[str, int, int], ...] = ( ("batch_size", 1, 128), ("seq_len", 1, 32768), ) # MoE 融合要求保留的子图 keep_moe_subgraph: bool = True keep_gqa_node: bool = True # 模型结构提示(用于诊断) model_kind: str = "qwen3_moe_35b_a3b" def cuda_provider_options(self) -> Dict: return { "enable_cuda_graph": self.enable_cuda_graph, "use_tf32": self.use_tf32, "cuda_graph_enable_partial": self.cuda_graph_partial, } def describe(self) -> str: return "融合 MoE+GQA、开 CUDA graph、覆盖动态维度以特化 Qwen3 MoE kernel" POLICY = OrtQwenThroughputPolicy() def build_options(policy: OrtQwenThroughputPolicy = POLICY) -> dict: return { "opt": policy.optimization_level, "cuda": policy.cuda_provider_options(), "free": policy.free_dim_overrides, }所有部署读同一份POLICY,融合与图捕获配置固化,避免“忘了开某个开关又变慢”。
七、解决方案(第三层:断言 / CI 守护)
把“Qwen3 MoE 吞吐优化开关生效”做成断言。下面用 pytest 风格守护:
import pytest def test_cuda_graph_on(policy): assert policy.cuda_provider_options()["enable_cuda_graph"] is True def test_free_dims_covered(policy): names = [d[0] for d in policy.free_dim_overrides] assert "batch_size" in names and "seq_len" in names def test_moe_gqa_kept(policy): assert policy.keep_moe_subgraph is True assert policy.keep_gqa_node is True def test_opt_level_all(policy): assert policy.optimization_level == "ORT_ENABLE_ALL"这四组断言锁住:(1) CUDA graph 开;(2) 动态维度覆盖;(3) MoE/GQA 子图保留;(4) 优化等级为 ALL。CI 跑通即代表吞吐优化路径激活。
八、排查清单
遇到 Qwen3 MoE 在 CUDA 上吞吐低:
- 看融合是否生效:session 里有没有
FusedMatMul/GroupQueryAttention/MoE节点。 - 开优化等级:
ORT_ENABLE_ALL,别留ORT_DISABLE_ALL。 - 开 CUDA graph:
enable_cuda_graph=true。 - 覆盖动态维度:给
batch/seq设边界,帮助 kernel 特化。 - 检查导出形态:MoE 子图、GQA 节点有没有被展开成裸算子。
- 统一策略对象:用
OrtQwenThroughputPolicy固化。 - CI 守护:断言关键开关开启、融合子图保留。
九、小结
[CUDA] Qwen3.6-35B-A3B Throughput Optimization的根因是:CUDA EP 的关键优化(MoE 融合、GroupQueryAttention融合、CUDA graph、动态维度特化)默认没激活,且模型导出形态可能把 MoE/GQA 拆成无法被融合识别的裸算子,导致 kernel 碎片化、launch 密集、GPU 利用率低、吞吐只有预期的零头。
最小修复是打开ORT_ENABLE_ALL、启用 CUDA graph 与 TF32、覆盖动态维度,并保证导出时保留 MoE/GQA 子图;结构性改进是用唯一的OrtQwenThroughputPolicy固化配置;CI 用四组断言守护“融合开关生效、维度覆盖、优化等级为 ALL”。记住:ORT 跑 MoE 大模型,融合和图捕获要显式打开,否则就是一堆小 kernel 在空转——这和 GPT-OSS 这类 MoE 的优化思路一致,但 Qwen3 的 A3B 激活比更稀疏,融合收益更大。