理解 ops-transformer 最有效的方式,不是从单个算子的实现细节入手,而是先搞清楚这个仓库解决的是什么类型的架构问题。

大多数算子仓库的本质是"一组高性能计算实现",但 ops-transformer 不完全是这样。它的设计目标不是简单地把计算做得更快,而是解决一个分层系统中的协作问题:PyTorch 框架发过来的计算请求,如何高效地落到昇腾 NPU 的硬件上,同时充分利用 GE 的融合能力和 Runtime 的调度能力。这个目标的实现,既需要算子层面的高性能实现,也需要算子接口层面的融合匹配设计,还需要跟 CANN 五层架构的每一层做精确的对接。

把这三个层次的关系说清楚,就是这篇文章要做的事。

ops-transformer 在 CANN 架构中的分层定位

CANN 的五层架构中,每一层都有明确的职责边界和接口约束。ops-transformer 处于第二层——算子库(AOL)。它的上一层是 Framework Adaptor,负责接收 PyTorch 的计算图并将其翻译为 CANN 的中间表示;它的下一层是 GE 图引擎,负责在编译期做算子融合和内存规划;再往下是 Runtime,负责运行期的任务调度和数据搬运。

ops-transformer 的设计,必须同时满足来自上、下两层的约束。从上层看,算子必须通过 Framework Adaptor 注册到 PyTorch 的算子体系里,调用方式要跟 PyTorch 原生的 attention 接口对齐,用户才能用最少的代码改动完成迁移。从下层看,算子的接口描述必须对齐 GE 融合规则的匹配条件,shape、dtype、tiling 参数都要符合 GE 的预期格式,融合才能真正触发。

这种"同时满足上下约束"的设计要求,决定了 ops-transformer 的算子不是独立存在的。它是一个分层系统里的中间件——算子本身既要保证高性能实现,下层的接口设计又要保证 GE 能正确识别和融合。如果只关注算子实现而忽略接口设计,性能可能只能发挥到裸链路的水平;如果只关注接口设计而算子实现有缺陷,融合虽然能触发但最终结果会出现数值误差。

算子接口设计与 GE 融合规则的匹配逻辑

ops-transformer 的算子接口设计,是整个仓库最核心的技术决策之一。

GE 的融合引擎在编译期扫描 PyTorch 传来的计算图,识别符合特定模式的算子序列。融合规则以 pass 的形式存在,每条 pass 定义了一组匹配条件和一个融合后的算子实现。FlashAttention 的融合规则叫 flash_attention_fusion_pass,它的匹配条件包括:输入的 dtype 必须是 float16 或 bfloat16,序列长度必须是 2 的幂次方或接近的某个范围,Q、K、V 的 shape 必须满足特定的对齐要求。

ops-transformer 的算子在接口层暴露了所有这些约束参数。用户需要按照这些约束配置输入,融合规则才会匹配成功。如果用户的输入不满足约束,GE 会放弃融合,算子按逐个算子的方式执行,性能收益就会显著下降。

接口设计的另一个关键点是 tiling 参数的配置。FlashAttention 的分块计算策略依赖 tile 大小的选择——tile 太大,UB 会溢出;tile 太小,tile 之间的调度开销会抵消融合带来的收益。ops-transformer 的算子接口会根据输入 shape 自动推荐最优的 tile 大小,这个推荐值来自 GE 在编译期对 UB 寄存器压力的估算。用户可以覆盖这个默认值,但需要理解这个参数的物理含义才能做出正确的选择。

Runtime 与算子执行的协作机制

算子被 GE 融合之后,Runtime 负责把融合后的执行计划调度到 NPU 上。这个调度过程不是简单的顺序执行,而是一个包含数据预加载、计算 overlap 和同步管理的复杂过程。

对于 FlashAttention 这个融合算子,Runtime 的调度逻辑大致如下:Runtime 先根据输入 shape 确定需要多少个 tile,然后启动一个主循环——每个循环内先发数据搬运指令把下一个 tile 的 K、V 从 HBM 搬到 UB,同时当前 tile 在计算单元上执行,等当前 tile 计算完成后把结果写回 HBM,再启动下一个循环。数据搬运和计算几乎完全 overlap,计算单元在整个过程中很少停下来等数据。

这个 overlap 效率受几个因素影响。batch_size 越小,数据搬运占比越高,计算占比越低——batch=1 时数据搬运加上等待的时间可能比计算时间还长。seq_len 的动态程度也影响 overlap 效率:固定 seq_len 时 Runtime 可以提前规划所有 tile 的数据预加载;动态 seq_len 时 Runtime 必须等上一个 tile 完成才能确定下一个 tile 的 shape,预加载的时机被推迟,overlap 效率下降。

ops-transformer 的算子实现需要配合 Runtime 的调度模型做优化。比如 tile 大小的选择不仅要考虑 UB 容量,还要考虑跟 Runtime 数据预加载节奏的配合——tile 太小,tile 之间的调度频率上升,Runtime 的调度开销增加;tile 太大,单个 tile 的计算时间变长,数据预加载的 overlap 空间减小。

仓库设计中的性能与可维护性平衡

ops-transformer 在架构层面的一个重要设计权衡,是性能优化与代码可维护性之间的平衡。

如果只追求极致性能,最优的做法是为每一种 shape、dtype、tile_size 的组合写一个专门的 kernel 实现,这样可以对每一个维度做极致的优化。但这种做法的问题是维护成本极高——一个算子可能有几十种配置组合,每种组合都需要独立实现和测试,而且当硬件升级或者融合规则变化时,所有实现都需要同步更新。

ops-transformer 的做法是把可变的配置抽象为参数——shape、dtype、tile_size 都通过参数传递,在算子内部通过条件分支或者参数化的计算逻辑适配不同的配置。这种做法牺牲了一部分极致性能(比如某些特定 shape 下可能有更优的实现方案),但换来了代码的可维护性和配置的灵活性。GE 在编译期根据具体 shape 选择最优的参数配置,实际上是在保持代码统一性的同时,通过编译期的参数优化来弥补运行时参数化的性能损失。

理解这个权衡,对理解 ops-transformer 的设计哲学很重要。它不是一个"为某一个 shape 做到极致"的仓库,而是一个"用一个统一的实现框架,覆盖所有常见 shape,并让 GE 在编译期找到每个 shape 的最优配置"的仓库。这个设计哲学在 CANN 的整个算子生态里有代表性——GE 的融合引擎就是为了在编译期做全局优化而设计的,而 ops-transformer 的算子接口设计,就是为了给 GE 提供足够的信息去做这个全局优化。

相关仓库:

https://atomgit.com/cann/ops-transformer

https://atomgit.com/cann/ge

https://atomgit.com/cann/cann-learning-hub

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐