GE图引擎:昇腾NPU上的“流水线调度员“到底在干什么
图引擎(GE)是昇腾NPU架构中的关键优化层,负责将动态图转换为静态计算图并进行深度优化。其核心工作包括:1)构建计算图;2)算子融合(如合并Conv+BN+ReLU);3)内存优化(显存复用和重计算);4)数据排布优化;5)执行调度。通过全局视角的优化,GE能显著减少显存读写次数,提升计算密度。若NPU性能未达预期,可能因动态shape、自定义算子或CANN版本过老导致优化失效。开发者可通过日志
先搞清一个问题:为什么需要图引擎?
在说 GE 之前,先聊清楚一件事:为什么 PyTorch 代码在 GPU 上跑得好好的,到了昇腾 NPU 上就需要一个图引擎来管?
PyTorch 默认是动态图(Eager Execution),也就是一行代码一行代码执行。你写 output = model(input),Python 解释器就一行行地调用对应的算子,每一步的结果立刻算出来,立刻用掉。灵活、调试方便,但每次执行都要重新走一遍 Python 解释器 → 算子调用的链路。
动态图有个好处是灵活,坏处是慢。每次调用算子,Python 和 CUDA/CANN 之间都要跨越一次语言边界(Python → C++ → 底层 API),这个切换本身有开销。更关键的是,GPU/NPU 的底层执行是高度并行的,但动态图每次只能看到一条指令,没法做全局优化。
静态图就不一样了。你把整个模型的计算流程先打包成一张图(Graph),图里每个节点是一个算子,每条边是数据依赖关系。图打好之后,编译器可以站在全局视角做优化:哪些算子可以合并、哪些计算可以重排、哪段数据不需要落显存直接复用……
GE 干的就是把动态图转成静态图,然后做全局优化这件事。
GE 在整个 CANN 架构里的位置
在说 GE 具体干什么之前,先把它放在 CANN 五层架构里看一下位置:
你的 PyTorch 代码(动态图)
↓
CANN 第一层:AscendCL(对外窗口)
↓
CANN 第三层:GE(图引擎)← 这里
↓
CANN 第四层:Runtime(运行时)
↓
昇腾 NPU 硬件
GE 夹在 AscendCL 和 Runtime 中间。它的上游是各种框架(PyTorch、MindSpore、ONNX),下游是 Runtime。模型代码先经过 GE 做编译和优化,再交给 Runtime 去真正驱动硬件执行。
这个设计思路跟 NVIDIA 的 TensorRT、TensorFlow 的 XLA 是一样的——都是在框架和硬件之间插一层编译优化层。但 GE 的实现更复杂一些,因为它要适配的硬件(昇腾达芬奇架构)和场景(训练+推理+算子开发)都更多。
GE 具体干的五件事
GE 做的事情很多,但核心可以归纳成五件。按顺序说:
第一件事:图构建(Graph Building)
你的 PyTorch 代码到了 GE 这里,第一步是把它转成 GE 能理解的数据结构——一张计算图。
这一步有两种模式:
- 追踪模式(Trace):让 PyTorch 真正跑一遍 forward,把中间的所有算子调用记录下来,生成静态的计算图。这种方式简单直接,但只记录了实际执行过的路径,分支和动态 shape 处理不一定完整。
- 解析模式(Parse):直接解析模型文件(比如 ONNX),根据文件格式规范重建计算图。这种方式拿到的是完整的模型结构定义,但有时候 ONNX 里某些算子的语义和 PyTorch 里不一样,需要额外处理。
图构建完之后,GE 手里拿到的是一张有向无环图(DAG),节点是算子,边是数据流向。
第二件事:算子融合(Operator Fusion)
这是 GE 最核心、也最能提升性能的一步。
什么叫算子融合?就是把两个或多个相邻的算子合并成一个算子执行,减少中间结果的读写。
举个例子,你的 PyTorch 模型里有一个卷积层 + BatchNorm 层 + ReLU 激活:
import torch
import torch_npu
class ConvBnRelu(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv = torch.nn.Conv2d(64, 64, 3, padding=1)
self.bn = torch.nn.BatchNorm2d(64)
self.relu = torch.nn.ReLU()
def forward(self, x):
x = self.conv(x) # 算子1:卷积
x = self.bn(x) # 算子2:归一化
x = self.relu(x) # 算子3:激活
return x
model = ConvBnRelu().npu()
x = torch.randn(1, 64, 224, 224).npu()
output = model(x)
在动态图里,这是三次独立的算子调用:Conv → BN → ReLU。每次算完的结果都要先写回 HBM 显存,再读出来给下一个算子用。
HBM: [conv输出] → 写 → 读 → [bn输入] → 算 → 写 → 读 → [relu输入] → 算 → 写 → 读 → [最终输出]
三次 HBM 写 + 三次 HBM 读,加上中间 buffer 的分配和释放开销,显存带宽被吃掉了不少。
GE 在图优化阶段发现这三个算子可以融合成一个:Conv + BatchNorm 可以在数学上合并成一个等效的卷积(权重和偏置做一次线性变换),ReLU 也合并进去,最后只需要一次卷积操作就能完成三个算子的工作:
# 融合后,GE 把上面三行代码变成了这一行等效操作
output = fused_conv_bn_relu(x) # 一次计算,结果只写一次 HBM
融合之后,HBM 读写从 6 次变成 2 次(输入 + 输出),计算密度大幅提升。在实际的 ResNet-50 模型里,光是 Conv+BN+ReLU 融合这一项,就能让端到端推理性能提升 20%~30%。
GE 内置了几十种融合规则,除了 Conv+BN+ReLU,还有:MatMul+Add(矩阵乘加融合)、Softmax+Reshape、LayerNorm+FusedAdd 等等。这个融合规则库随着 CANN 版本更新在不断扩充。
第三件事:内存优化(Memory Optimization)
图融合减少的是读写次数,但还有另一个瓶颈:显存占用。
深度学习模型中间会有大量临时张量(activation)。如果每个算子的中间结果都完整保存在 HBM 里,显存峰值会非常大。GE 有几个手段来处理这个问题:
内存复用(Memory Reuse):两张量生命周期不重叠的情况下,可以复用同一块显存区域。比如算子 A 的输出到了算子 B 手里之后,算子 A 的输出张量就可以释放了,它占的显存可以让给后续算子用。GE 会在图上分析每个张量的生命周期,制定一个显存分配方案,让峰值显存尽可能低。
重计算(Recomputation):有些算子的输入很小但输出很大(比如 Softmax)。如果把这个大输出存下来会占用大量显存,但重新算一遍反而更划算。GE 会自动判断哪些算子适合重计算,在 backward 时重新算出需要的中间值,而不是预先存好。
张量虚拟化(Tensor Virtualization):对于一些大模型的中间激活,可以通过 CPU 内存做 offload,在需要的时候再搬回来。GE 会在编译时分析数据流,决定哪些激活应该留在 NPU 显存里,哪些可以 offload 到 CPU 内存。
第四件事:数据排布优化(Layout Optimization)
昇腾达芬奇架构的矩阵计算单元(Cube Unit)对输入数据的排布方式有要求。不同的算子组合,最优的数据排布可能不一样。
GE 会分析数据流和算子类型,选择最优的 layout。比如某些场景下 NCHW(通道优先)排布更优,某些场景下 NHWC(通道后置)更优。GE 会在关键节点插入 layout 转换算子,同时尽量减少不必要的转换开销。
第五件事:执行调度(Execution Scheduling)
图优化完了,GE 还要决定这些算子的执行顺序。
听起来图已经有了拓扑顺序,执行顺序应该是确定的啊?但这里有个问题:GPU/NPU 上算子之间有依赖关系,但依赖关系不是简单的串行。有些算子在等数据,有些算子在计算,理想情况下应该让等待和计算并行起来。
GE 的执行调度器会把可以并行的算子分到不同的执行流里,让计算和访存尽量重叠。比如在读取下一批数据的同时,GPU/NPU 在计算上一批数据。这个过程叫双缓冲(Double Buffering)或者流水线调度,是高性能推理引擎的标配。
怎么让 GE 不偷懒
回到开头那个问题:为什么有人装了 CANN 但性能只有理论算力的三分之一?
大概率是图融合没生效。可能有几种原因:
原因一:动态 shape 导致 GE 无法融合
如果你的模型输入 shape 不是固定的(比如变长序列),GE 在做图优化时会比较保守。很多融合规则要求 shape 固定才能做,否则融合后 shape 对不上就麻烦了。
解法:尽量用固定 shape,或者在 ATC 转换时用 --input_shape 指定固定 shape:
atc --model=model.onnx \
--framework=5 \
--output=model_npu \
--soc_version=Ascend910 \
--input_shape="actual_input_1:1,3,224,224" # 固定 shape 让 GE 充分优化
原因二:某些自定义算子没有注册到融合规则里
如果你的模型用了自定义算子(不是 CANN 标准算子库里的),GE 可能不知道怎么处理,直接跳过优化。
解法:检查你的模型里有没有自定义算子,或者看看是不是某些 PyTorch 的实现方式触发了不太好融合的算子组合。
原因三:CANN 版本太老
新版本的 CANN 会不断添加新的融合规则。如果你的 CANN 版本很老,很多优化 GE 还没学会。
解法:升 CANN 版本。新版本的 ge 仓库里会记录每个版本新增的融合规则。
怎么看到 GE 干了什么
调试 GE 的一个有用技巧是在 ATC 转换时打开详细日志:
atc --model=resnet50.onnx \
--framework=5 \
--output=resnet50_npu \
--soc_version=Ascend910 \
--input_shape="actual_input_1:1,3,224,224" \
--log=info # 打开详细日志,可以看到 GE 做了哪些优化
日志里会输出 GE 执行的每一步优化:融合了哪些算子、优化了哪些数据排布、内存分配策略是什么。仔细看一遍日志,你能对自己模型的性能瓶颈有更清晰的认识。
如果你是 PyTorch 动态图场景(不用 ATC 转离线模型),可以用 torch_npu 的 profiling 工具看 GE 的优化情况:
import torch
from torch_npu.contrib import profile
with profile(profile_dir="./ge_profile"):
output = model(input)
生成的 profiling 报告里可以看到每个算子有没有被融合、算子间的数据排布是什么、显存峰值是多少。
总结
GE 图引擎是 CANN 里最核心的一层。它的核心工作是把你的动态图模型转成一张经过深度优化的静态图,然后交给 Runtime 去执行。
优化主要靠五件事:图构建 → 算子融合 → 内存优化 → 数据排布优化 → 执行调度。其中算子融合(把 Conv+BN+ReLU 合并成一次计算)是性能提升最显著的一步,也是最容易被忽略的一步。
很多人在昇腾 NPU 上跑不出理想性能,根本原因就是 GE 的图优化没生效——可能是 shape 不固定、可能是用了不好融合的算子组合、可能是 CANN 版本太老。学会看 GE 的日志,能帮你快速定位问题出在哪一层。
理解了 GE 在干什么,你再去看 cann-recipes-infer 里的推理优化技巧,就能知其然也知其所以然。昇腾 NPU 的性能上限不低,GE 的优化做好了,120% 的算力利用率不是梦。
本篇文章涉及的相关仓库:
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐



所有评论(0)