关于 ops-transformer 和它背后那套系统,几个我见过最常见的误解
关于 ops-transformer 和它背后那套系统,几个我见过最常见的误解
在社区里解答问题的时候,有几类问题每隔一段时间就会出现,每次都有人踩。问题本身不难,但背后反映的是对整个架构设计逻辑的误解。把这几个认知误区拆开来说清楚,比零散地讲知识点更有用。
误解一:GE 是编译阶段,Runtime 是运行阶段,二者先后的
很多人以为 GE 和 Runtime 是流水线上的两个站,先 GE 编译完,再 Runtime 执行。这在逻辑上听起来没问题,但跟实际情况差得很远。
GE 不是在执行之前把事情全部做完的编译器,而是一个跟 Runtime 深度协作的图优化层。
GE 在模型加载阶段做一轮图优化——算子融合、常量折叠、内存规划——这些确实发生在 Runtime 启动之前。但 GE 还负责在线的融合决策、动态 shape 推理、以及跟 Runtime 的实时交互。Runtime 每跑完一个算子,GE 可能已经在准备下一个子图的融合策略了。
ops-transformer 的 FlashAttention 算子从被 GE 识别到最终在 NPU 上执行,中间经过了 GE 的融合匹配 → Runtime 的任务拆分 → 数据从 HBM 到 UB 的搬运 → Cube/Vector 单元执行 → 结果写回。这整条链路不是"GE做完→Runtime接上"的串行关系,而是多层协同的并行关系。
把这个关系理解错了,调优的时候就会盯着错误的地方看——你以为问题出在 Runtime 调度,但你可能需要去 GE 层看融合规则有没有匹配上。
误解二:ops-transformer 算子可以像调用 CUDA kernel 那样直接调用
这是 CUDA 开发者转到昇腾 NPU 时最容易踩的坑。
在 CUDA 环境里,你可以写一个.cu文件,定义一个__global__的 kernel,然后从 CUDA runtime API 直接 launch 这个 kernel。整个过程你控制得很细。
ops-transformer 不是这样工作的。ops-transformer 的算子不是被直接调用的,是被 GE 的融合引擎匹配之后才能发挥作用的。
你写的 PyTorch 代码经过 Framework Adaptor 注册到系统里,GE 在图编译阶段扫描整个计算图,找有没有可以匹配的融合模式。如果你的 FlashAttention 前后没有符合融合规则的算子序列,GE 可能根本不会把它识别为融合目标——它会按单个算子一条一条地执行,性能差距是好几倍。
这不是 ops-transformer 的问题,是整个设计思路的差异。CUDA 是细粒度控制,昇腾 NPU 是图级别优化。ops-transformer 的性能收益大部分来自 GE 的融合决策,不来自算子本身的实现。不理解这一点,你永远调不出这个仓库的上限。
误解三:GE 就是一个图编译器,跟 gcc 差不多
把 GE 等同于传统编译器,是一个看起来合理但完全错误的类比。
gcc 做的事是:拿到源代码,生成机器码,编译完就结束了。你拿到的是一段静态的二进制,执行的时候跟编译器没有关系了。
GE 不一样。GE 的输出不是一段静态的二进制,而是一个动态的执行计划——这个计划在 Runtime 阶段还会被持续调整。
举一个具体的例子。GE 做内存规划的时候,会估算每个算子的中间结果需要多少显存。但有些模型的 shape 是动态的(比如变长序列的生成任务),GE 会在运行时根据实际 shape 重新分配显存。这就是 GE 在 Runtime 阶段的在线调整能力。
另一个例子是 GE 的自适应融合策略。当 Runtime 检测到某个算子的输入 shape 发生了变化,GE 可以触发一次轻量级的重新融合,不需要重新跑完整的图编译流程。这种能力在传统编译器里根本不存在。
所以当你说"我想优化 GE 的融合规则"的时候,你其实是在向一个运行时协同优化的图引擎里写规则——这个过程跟写 CUDA kernel 完全不同,你需要理解 GE 的融合 pass 是怎么注册、怎么触发的、跟 Runtime 的数据平面是怎么交互的。
误解四:算子融合就是把几个算子拼在一起
很多人以为融合算子就是"把 MatMul 和 Softmax 写到一个 kernel 里,减少一次 HBM 读写"。这个理解只对了一半。
真实的算子融合远比"拼接"复杂。GE 的融合引擎做的是完整的数据流分析——它需要保证融合前和融合后的数值结果完全等价,同时还要考虑寄存器压力、分块策略、以及不同计算单元之间的负载均衡。
拿 ops-transformer 的 FlashAttention 融合来说。融合前是三条独立的算子链:MatMul(Q, K^T) → Softmax → MatMul(Attn, V)。GE 的融合 pass 会把这三条算子合并,但合并的方式取决于输入数据的 shape 和 dtype。如果 dtype 是 float16,GE 可能选择 BF16 的融合路径;如果 shape 是能被 16 整除的短序列,它可能选择一种 tile 大小的融合策略;如果序列特别长,它可能把融合边界切开,分段执行以避免 UB 溢出。
这些决策全都是在融合阶段自动做出的,不是在 ops-transformer 的算子代码里写死的。你在 ops-transformer 里看到的 FlashAttention 实现,只是这个融合策略的其中一种执行路径——它能被 GE 以多种方式调度,取决于编译期的分析和运行期的数据。
误解五:Runtime 只负责调度,调度完就没关系了
Runtime 的调度能力只是它职责的一部分。很多人只看 Runtime 的任务分配功能,忽略了它在内存管理、数据搬运同步、以及异常恢复上的关键角色。
Runtime 实际上在管理一条从 Host 到 Device 的数据高速公路——它决定数据什么时候搬、搬多少、搬完之后计算单元什么时候启动、启动完了之后显存什么时候释放。
ops-transformer 的 FlashAttention 在 UB 上做 tile 级计算,每一个 tile 完成后,Runtime 负责把这个 tile 的结果从 UB 写回 HBM,同时触发下一个 tile 的数据预加载。这个预加载和计算的 overlap,就是 Runtime 的异步调度能力在发挥作用。
如果你发现 FlashAttention 的计算效率和理论值差得远,先别去看 ops-transformer 的算子实现——去看 Runtime 的日志,看数据搬运有没有和计算真正 overlap 起来。很多时候性能瓶颈不是算子实现的问题,是 Runtime 没有把数据和计算的调度排好序。
这套理解体系为什么重要
说这些认知误区不是为了让人记住知识点,是为了改变看问题的角度。
当你遇到性能问题的时候,你不再只盯着 ops-transformer 的源码。你会先想:GE 的融合有没有生效?Runtime 的调度有没有 overlap?数据排布符不符合融合规则的要求?这些问题的答案在源码里找不到——你得去理解整个系统是怎么协同工作的。
分层架构的核心价值不是让你每层都学,而是让你知道出了问题该往哪层找。GE 管融合,Runtime 管调度,ops-transformer 管执行。三个层次各司其职,出了问题才不会乱翻。
理解了这一点,你再去翻 cann-learning-hub 里关于 GE 图引擎调优和 Runtime 性能调优的内容,就不再是看陌生的名词——你是在验证你已有的理解,顺着这条线把每个层级的能力摸透。
相关仓库:
https://atomgit.com/cann/ops-transformer
https://atomgit.com/cann/ge
https://atomgit.com/cann/cann-learning-hub
