昇腾CANN ge 仓的图优化 Pass:哪些 Pass 真正影响推理性能

前言

你训练好一个模型,导出 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:

python 复制代码
# list_ge_passes.py - 列出所有 GE Pass
import subprocess
import re

def list_ge_passes():
    """调用 ge 命令行工具列出所有 Pass"""
    result = subprocess.run(
        ["ge", "--list_passes"],
        capture_output=True,
        text=True
    )
    
    passes = []
    for line in result.stdout.split('\n'):
        # 解析 Pass 名称(格式:PassName: description)
        match = re.match(r'^(\w+):\s*(.*)$', line)
        if match:
            passes.append({
                "name": match.group(1),
                "desc": match.group(2)
            })
    
    return passes

# 列出所有 Pass
all_passes = list_ge_passes()
print(f"GE Pass 总数: {len(all_passes)}")
for p in all_passes[:10]:  # 打印前 10 个
    print(f"  {p['name']}: {p['desc']}")

# 输出(示例):
# GE Pass 总数: 47
#  ConstantFoldingPass: 常量折叠
#  OpFusionPass: 算子融合
#  MemoryReusePass: 内存复用
#  L2FusionPass: L2 融合优化
#  DeadCodeEliminationPass: 死代码消除
#  ...

47 个 Pass! 全部开启?没必要,有些 Pass 对推理性能没影响(甚至负优化)。

关键 Pass 1:常量折叠(ConstantFoldingPass)

作用:编译期算掉所有常数子图。

典型案例

python 复制代码
# 模型里有这样的子图(YOLOv5 的锚框计算)
import torch
import torch.nn as nn

class YOLOHead(nn.Module):
    def forward(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)
        
        return anchors, boxes

没开 ConstantFoldingPass 时

  • 每次推理都重新算 anchors(虽然它是常量)
  • 延迟增加 3~5ms(取决于常量子图复杂度)

开了 ConstantFoldingPass 后

  • 编译期把 anchors 直接算出来,存在模型文件里
  • 推理期直接读结果,不占计算资源

开关方法

python 复制代码
# enable_constant_folding.py
import torch
from torch_npu.contrib import transfer

# 方法1:通过环境变量开启(推荐)
import os
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 模型时指定优化 Pass
os.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 开销。

典型案例

python 复制代码
# BERT 的 Embedding 层(3 个算子)
import torch
import torch.nn as nn

class BertEmbeddings(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)
    
    def forward(self, input_ids, token_type_ids=None):
        # 算子1:Word Embedding
        words = self.word_embeddings(input_ids)
        
        # 算子2:Position Embedding
        position_ids = torch.arange(input_ids.shape[1], device=input_ids.device).unsqueeze(0)
        positions = self.position_embeddings(position_ids)
        
        # 算子3:Token Type Embedding
        if token_type_ids is None:
            token_type_ids = torch.zeros_like(input_ids)
        token_types = self.token_type_embeddings(token_type_ids)
        
        # 逐元素加(3 个算子)
        embeddings = words + positions + token_types
        
        # LayerNorm + Dropout
        embeddings = self.LayerNorm(embeddings)
        embeddings = self.dropout(embeddings)
        
        return embeddings

没开 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

开关方法

python 复制代码
# enable_op_fusion.py
import os

# 开启算子融合(默认就是开启的,但可以调等级)
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"
        }
    ]
}

# 保存配置到文件
import json
with open("fusion_rules.json", "w") as f:
    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 共用同一块显存,降低显存峰值。

典型场景

python 复制代码
# Transformer 的 Decoder(自回归生成)
import torch
import torch.nn as nn

class TransformerDecoder(nn.Module):
    def forward(self, x, cache=None):
        # Layer 1
        x1 = self.self_attn1(x, cache=cache)
        x1 = self.ffn1(x1)
        
        # Layer 2
        x2 = self.self_attn2(x1, cache=cache)  # x1 生命周期结束
        x2 = self.ffn2(x2)
        
        # Layer 3
        x3 = self.self_attn3(x2, cache=cache)  # x2 生命周期结束
        x3 = self.ffn3(x3)
        
        # ... 共 24 层
        
        return x3

没开 MemoryReusePass 时

  • 每层的中间激活都占显存(直到层输出被消费)
  • 24 层 × 每层 2 个激活 × 每个激活 2MB = 96MB 显存峰值

开了 MemoryReusePass 后

  • x1 在 Layer 2 开始时就可以释放(复用给 x2
  • x2 在 Layer 3 开始时就可以释放(复用给 x3
  • 显存峰值:8MB(只有 4 层的中间激活)

开关方法

python 复制代码
# enable_memory_reuse.py
import os

# 开启内存复用(默认开启,但可以调策略)
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% 可以复用)
}

# 保存配置
import json
with open("memory_config.json", "w") as f:
    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 里完成

典型案例

python 复制代码
# ResNet-50 的 Bottleneck(卷积 + BN + Relu)
import torch
import torch.nn as nn

class Bottleneck(nn.Module):
    def forward(self, x):
        # Conv1x1 + BN + Relu
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)  # L2 友好(输出小)
        
        # Conv3x3 + BN + Relu
        out = 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)
        
        return out

没开 L2FusionPass 时

  • conv3 + bn3 的输出要写回 HBM(如果超过 L2 容量)
  • 下次读的时候再从 HBM 加载 → 带宽瓶颈

开了 L2FusionPass 后

  • conv3 + bn3 + residual_add + relu 融合成一个算子
  • 中间结果全在 L2 里,不写回 HBM → 带宽节省

开关方法

python 复制代码
# enable_l2_fusion.py
import os

# 开启 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"
    ]
}

# 保存配置
import json
with open("l2_config.json", "w") as f:
    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

相关推荐
深度先生2 小时前
Conda 全面讲解——数据科学家的标配工具
python
深度先生2 小时前
虚拟环境:别让包打架
python
L、2182 小时前
昇腾NPU性能调优Checklist——从“能跑“到“跑得快“的20步
服务器·人工智能·深度学习
漠效2 小时前
随机代理‌IP访问脚本
开发语言·python
SilentSamsara3 小时前
元类与 __init_subclass__:类是如何被“创建“出来的
开发语言·python·青少年编程
碧海银沙音频科技研究院3 小时前
恒玄bes2600WM+DSP蓝牙耳机项目
深度学习·语音识别
蓦然回首却已人去楼空3 小时前
深度学习进阶:自然语言处理|4.1.2 QA|grads 列表与省略号 [...] 详解
人工智能·深度学习·自然语言处理
手写码匠3 小时前
Android 17 适配实战指南:新特性解读、隐私变更与迁移全攻略
人工智能·深度学习·算法·aigc
隔壁大炮3 小时前
MNE-Python 第6天学习笔记:分段(Epoching)与基线校正
python·eeg·mne·脑电数据处理