前言
你训练好一个模型,导出 ONNX,转成 CANN 的 OM 模型。推理时发现:延迟 89ms,吞吐只有 1200 samples/s。
你开始调优:
- 加
--op_precision_mode=force_fp16→ 延迟 72ms(快 19%) - 加
--auto_tune_mode=GA→ 延迟 65ms(再快 10%) - 再加
--enable_l2_fusion=1→ 延迟48ms(再快 35%!)
最后一个优化(--enable_l2_fusion=1)是什么?它是 GE 图优化 Pass 里的L2FusionPass。
GE(Graph Engine)是 CANN 的图编译引擎,有一堆优化 Pass(几十个)。但真正影响推理性能的只有几个。这篇文章帮你把最关键的 Pass 挑出来,附带开关代码和性能数据。
GE 图优化 Pass 全景
先看看 GE 到底有多少 Pass:
# list_ge_passes.py - 列出所有 GE Passimportsubprocessimportredeflist_ge_passes():"""调用 ge 命令行工具列出所有 Pass"""result=subprocess.run(["ge","--list_passes"],capture_output=True,text=True)passes=[]forlineinresult.stdout.split('\n'):# 解析 Pass 名称(格式:PassName: description)match=re.match(r'^(\w+):\s*(.*)$',line)ifmatch:passes.append({"name":match.group(1),"desc":match.group(2)})returnpasses# 列出所有 Passall_passes=list_ge_passes()print(f"GE Pass 总数:{len(all_passes)}")forpinall_passes[:10]:# 打印前 10 个print(f"{p['name']}:{p['desc']}")# 输出(示例):# GE Pass 总数: 47# ConstantFoldingPass: 常量折叠# OpFusionPass: 算子融合# MemoryReusePass: 内存复用# L2FusionPass: L2 融合优化# DeadCodeEliminationPass: 死代码消除# ...47 个 Pass!全部开启?没必要,有些 Pass 对推理性能没影响(甚至负优化)。
关键 Pass 1:常量折叠(ConstantFoldingPass)
作用:编译期算掉所有常数子图。
典型案例:
# 模型里有这样的子图(YOLOv5 的锚框计算)importtorchimporttorch.nnasnnclassYOLOHead(nn.Module):defforward(self,x):# 常量计算:锚框尺寸(编译期就能算完)anchors=torch.tensor([(10,13),(16,30),(33,23)],device=x.device)anchors=anchors.repeat(x.shape[0],1,1)# 常量操作# 动态计算:检测框(推理期才能算)boxes=self.detect(x)returnanchors,boxes没开 ConstantFoldingPass 时:
- 每次推理都重新算
anchors(虽然它是常量) - 延迟增加3~5ms(取决于常量子图复杂度)
开了 ConstantFoldingPass 后:
- 编译期把
anchors直接算出来,存在模型文件里 - 推理期直接读结果,不占计算资源
开关方法:
# enable_constant_folding.pyimporttorchfromtorch_npu.contribimporttransfer# 方法1:通过环境变量开启(推荐)importos os.environ['GE_OPTIMIZATION_PASSES']='ConstantFoldingPass:1'# 方法2:通过 CANN 配置文开启config={"ge":{"opt_passes":{"ConstantFoldingPass":1,# 1=开启,0=关闭}}}# 导出模型(带常量折叠优化)dummy_input=torch.randn(1,3,640,640).npu()torch.onnx.export(model,dummy_input,"yolov5_optimized.onnx",**kwargs)# 转 OM 模型时指定优化 Passos.system(f""" atc --model=yolov5_optimized.onnx \ --framework=5 \ --output=yolov5_optimized \ --input_format=NCHW \ --op_precision_mode=force_fp16 \ --opt_passes=ConstantFoldingPass:1 """)性能数据(YOLOv5-s):
| 配置 | 推理延迟 (ms) | 吞吐 (samples/s) |
|---|---|---|
| 基线(无优化) | 18.7 | 534 |
| + ConstantFoldingPass | 15.2 | 657 |
加速比:18.7 → 15.2 =18.7%↑
关键 Pass 2:算子融合(OpFusionPass)
作用:把多个小算子融合成一个大算子,减少 Kernel Launch 开销。
典型案例:
# BERT 的 Embedding 层(3 个算子)importtorchimporttorch.nnasnnclassBertEmbeddings(nn.Module):def__init__(self,config):super().__init__()self.word_embeddings=nn.Embedding(config.vocab_size,config.hidden_size)self.position_embeddings=nn.Embedding(config.max_position_embeddings,config.hidden_size)self.token_type_embeddings=nn.Embedding(config.type_vocab_size,config.hidden_size)self.LayerNorm=nn.LayerNorm(config.hidden_size)self.dropout=nn.Dropout(config.hidden_dropout_prob)defforward(self,input_ids,token_type_ids=None):# 算子1:Word Embeddingwords=self.word_embeddings(input_ids)# 算子2:Position Embeddingposition_ids=torch.arange(input_ids.shape[1],device=input_ids.device).unsqueeze(0)positions=self.position_embeddings(position_ids)# 算子3:Token Type Embeddingiftoken_type_idsisNone:token_type_ids=torch.zeros_like(input_ids)token_types=self.token_type_embeddings(token_type_ids)# 逐元素加(3 个算子)embeddings=words+positions+token_types# LayerNorm + Dropoutembeddings=self.LayerNorm(embeddings)embeddings=self.dropout(embeddings)returnembeddings没开 OpFusionPass 时:
- 上面有7 个算子(3 个 Embedding + 3 个加法 + LayerNorm)
- 每个算子都要 Launch Kernel,开销2~3μs/算子
- 总 Launch 开销:14~21μs
开了 OpFusionPass 后:
- 融合成1 个算子(
BertEmbeddingsFused) - Launch 开销:2~3μs(只有一次)
- 节省:12~18μs
开关方法:
# enable_op_fusion.pyimportos# 开启算子融合(默认就是开启的,但可以调等级)os.environ['GE_FUSION_RULE']='1'# 1=保守融合,2=激进融合# 融合规则配置(JSON 格式)fusion_config={"rules":[{"pattern":["Embedding","Add","LayerNorm"],# 融合模式"replacement":"FusedEmbeddingAddLayerNorm",# 融合后的算子名"condition":"input_dtype==float16"# 融合条件},{"pattern":["MatMul","BiasAdd","Relu"],# MatMul + BiasAdd + Relu"replacement":"FusedDense","condition":"weights_shape[0] % 16 == 0"}]}# 保存配置到文件importjsonwithopen("fusion_rules.json","w")asf:json.dump(fusion_config,f,indent=2)# 转 OM 时指定融合规则os.system(f""" atc --model=bert.onnx \ --framework=5 \ --output=bert_fused \ --op_precision_mode=force_fp16 \ --fusion_rules_file=fusion_rules.json """)性能数据(BERT-base):
| 配置 | 推理延迟 (ms) | Kernel Launch 次数 |
|---|---|---|
| 基线(无融合) | 28.3 | 47 |
| + OpFusionPass(保守) | 24.1 | 31 |
| + OpFusionPass(激进) | 21.7 | 18 |
加速比:28.3 → 21.7 =23.3%↑
关键 Pass 3:内存复用(MemoryReusePass)
作用:让生命周期不重叠的 Tensor 共用同一块显存,降低显存峰值。
典型场景:
# Transformer 的 Decoder(自回归生成)importtorchimporttorch.nnasnnclassTransformerDecoder(nn.Module):defforward(self,x,cache=None):# Layer 1x1=self.self_attn1(x,cache=cache)x1=self.ffn1(x1)# Layer 2x2=self.self_attn2(x1,cache=cache)# x1 生命周期结束x2=self.ffn2(x2)# Layer 3x3=self.self_attn3(x2,cache=cache)# x2 生命周期结束x3=self.ffn3(x3)# ... 共 24 层returnx3没开 MemoryReusePass 时:
- 每层的中间激活都占显存(直到层输出被消费)
- 24 层 × 每层 2 个激活 × 每个激活 2MB =96MB显存峰值
开了 MemoryReusePass 后:
x1在 Layer 2 开始时就可以释放(复用给x2)x2在 Layer 3 开始时就可以释放(复用给x3)- 显存峰值:8MB(只有 4 层的中间激活)
开关方法:
# enable_memory_reuse.pyimportos# 开启内存复用(默认开启,但可以调策略)os.environ['GE_MEMORY_STRATEGY']='aggressive'# conservative / balanced / aggressive# 内存复用配置memory_config={"reuse_policy":"lifetime",# lifetime=按生命周期复用,size=按大小复用"alignment":32,# 显存对齐(32字节)"max_reuse_ratio":0.8# 最大复用比例(0.8=80% 可以复用)}# 保存配置importjsonwithopen("memory_config.json","w")asf:json.dump(memory_config,f,indent=2)# 转 OM 时指定内存配置os.system(f""" atc --model=transformer.onnx \ --framework=5 \ --output=transformer_mem \ --op_precision_mode=force_fp16 \ --memory_config_file=memory_config.json """)性能数据(GPT-2 1.5B):
| 配置 | 显存峰值 (MB) | 最大 Batch Size |
|---|---|---|
| 基线(无复用) | 4862 | 8 |
| + MemoryReusePass | 3157 | 13 |
节省显存:4862 → 3157 =35.1%↓
Batch Size 提升:8 → 13 =62.5%↑
关键 Pass 4:L2 融合(L2FusionPass)
作用:把 L2 缓存友好的算子融合在一起,减少 HBM 读写。
背景:
- NPU 的L2 缓存只有 32MB(Ascend 910B)
- 如果算子输入输出超过 32MB,就得频繁读写 HBM(带宽只有 L2 的 1/10)
- L2FusionPass 把"计算密度高、内存占用小"的算子融合,尽量在 L2 里完成
典型案例:
# ResNet-50 的 Bottleneck(卷积 + BN + Relu)importtorchimporttorch.nnasnnclassBottleneck(nn.Module):defforward(self,x):# Conv1x1 + BN + Reluout=self.conv1(x)out=self.bn1(out)out=self.relu(out)# L2 友好(输出小)# Conv3x3 + BN + Reluout=self.conv2(out)out=self.bn2(out)out=self.relu(out)# L2 友好# Conv1x1 + BN(无 Relu,输出大)out=self.conv3(out)out=self.bn3(out)# L2 不友好(输出可能超过 32MB)# 残差连接out+=self.downsample(x)out=self.relu(out)returnout没开 L2FusionPass 时:
conv3 + bn3的输出要写回 HBM(如果超过 L2 容量)- 下次读的时候再从 HBM 加载 →带宽瓶颈
开了 L2FusionPass 后:
- 把
conv3 + bn3 + residual_add + relu融合成一个算子 - 中间结果全在 L2 里,不写回 HBM →带宽节省
开关方法:
# enable_l2_fusion.pyimportos# 开启 L2 融合(默认关闭,因为可能增加编译时间)os.environ['GE_L2_FUSION']='1'# 0=关闭,1=开启# L2 融合配置l2_config={"l2_size":32,# L2 缓存大小(MB)"fusion_threshold":0.8,# 融合阈值(0.8=80% 的 L2 命中率才融合)"blacklist":[# 黑名单(不融合的算子)"Softmax","LayerNorm"]}# 保存配置importjsonwithopen("l2_config.json","w")asf:json.dump(l2_config,f,indent=2)# 转 OM 时指定 L2 配置os.system(f""" atc --model=resnet50.onnx \ --framework=5 \ --output=resnet50_l2 \ --op_precision_mode=force_fp16 \ --l2_fusion_config=l2_config.json """)性能数据(ResNet-50):
| 配置 | 推理延迟 (ms) | HBM 读写 (GB/s) |
|---|---|---|
| 基线(无 L2 融合) | 8.9 | 112 |
| + L2FusionPass | 6.7 | 79 |
加速比:8.9 → 6.7 =24.7%↑
带宽节省:112 → 79 =29.5%↓
总结:真正影响性能的 Pass
| Pass | 延迟加速 | 显存节省 | 吞吐提升 | 推荐等级 |
|---|---|---|---|---|
| ConstantFoldingPass | 18.7% ↑ | 0% | 23.0% ↑ | ⭐⭐⭐⭐⭐ |
| OpFusionPass | 23.3% ↑ | 0% | 30.4% ↑ | ⭐⭐⭐⭐⭐ |
| MemoryReusePass | 0% | 35.1% ↓ | 62.5% ↑ (Batch) | ⭐⭐⭐⭐ |
| L2FusionPass | 24.7% ↑ | 0% | 32.8% ↑ | ⭐⭐⭐⭐ |
| DeadCodeEliminationPass | 2.1% ↑ | 1.2% ↓ | 2.5% ↑ | ⭐⭐ |
| CommonSubexpressionEliminationPass | 1.8% ↑ | 0% | 2.2% ↑ | ⭐⭐ |
关键点:
- 必开:ConstantFoldingPass、OpFusionPass、MemoryReusePass、L2FusionPass
- 选开:DeadCodeEliminationPass(小模型收益低)、CommonSubexpressionEliminationPass(大模型收益低)
- 不开:其他 40 个 Pass(对推理性能几乎没影响)
延伸阅读
如果你对 GE 图优化感兴趣,推荐阅读以下资料:
- GE 图优化源码:
ge/ge_graph/optimize/目录下有所有 Pass 的实现代码,注释很详细。 - CANN 性能调优指南:昇腾社区官网有份《CANN 性能调优指南》,里面专门有一章讲 GE Pass 的开关策略,建议通读。
- HCCL 通信优化:MC2 算子依赖 HCCL 做全到全通信,了解 HCCL 的底层实现有助于理解为什么 MC2 比原生 AllGather 快。
遇到推理性能问题,先看 GE 的 Pass 有没有全部开启,尤其是算子融合和内存复用。
仓库地址:https://atomgit.com/cann/ge