请添加图片描述
个人主页:

之前帮同事调一个 Transformer 推理性能问题,模型在 PyTorch 上跑得好好的,转到昇腾 NPU 上之后吞吐直接掉了 40%。打开 Graph Compiler 的编译日志一看——整个计算图被切成了一百多个小算子,每个算子都要做数据搬运、格式转换、内存分配,NPU 的计算单元大部分时间在等数据。

这个问题本质上不是算子写得不好,而是编译链路没有全局视野——逐算子编译只看局部,不知道整个计算图里哪些数据可以复用、哪些算子可以融合、哪些内存可以静态分配。

CANN 的解决思路是在 Graph Compiler 和底层硬件指令之间插了一层虚拟指令集 PTO(PTO Instruction Set Architecture)。Graph Compiler 不直接生成 NPU 的底层指令,而是先生成 PTO 虚拟指令,再由后端的指令映射模块把 PTO 翻译成真正能在昇腾 NPU 上执行的机器码。

这篇文章拆开这条编译链路:从为什么需要虚拟 ISA、PTO 长什么样、Graph Compiler 怎么生成 PTO、昇腾 NPU 怎么执行底层指令,到 Transformer 推理的完整编译流程。


为什么 PTO 存在:编译系统的"普通话"

直接说结论:PTO 存在的核心价值是解耦前端编译和后端指令生成

没有 PTO 的时候,Graph Compiler 需要直接面对不同代际的昇腾 NPU 硬件——Ascend 910、Ascend 950PR、Ascend 950DT 的底层指令集都不一样,每适配一款新硬件就要改一遍 Graph Compiler 的指令生成逻辑。硬件迭代一次,编译器跟着改一次,维护成本线性增长。

有了 PTO 之后,编译流程变成:

PyTorch/TensorFlow 计算图
        ↓  (前端:图解析 + 图优化)
Graph Compiler 中间表示(HIR/MIR/LIR)
        ↓  (指令选择 + Tile 调度)
PTO 虚拟指令(90+ 标准 Tile 级操作)
        ↓  (指令映射 + 寄存器分配)
昇腾 NPU 底层机器码(达芬奇架构指令)

前端只需要生成一份 PTO,后端针对每款硬件写一份 PTO→机器码的映射规则。新增硬件?只改后端映射,前端不动。新增算子?只改前端生成逻辑,后端映射自动复用。

这个思路和 LLVM IR、NVPTX 本质上是一样的——虚拟 ISA 是编译系统的"普通话",让前后端用同一套语言对话

PTO 的全称是 PTO Instruction Set Architecture,它是 CANN 五层架构中第 3 层(编译层)的核心组件,和 Graph Compiler 配合工作。pto-isa 仓库定义了这套虚拟指令集的规范:90+ 标准 Tile 级操作,覆盖矩阵乘、向量运算、数据搬运、内存管理这些算子开发的基础操作。它的定位是跨平台算子开发的中间表示,不是底层硬件指令集。

仓库地址:https://atomgit.com/cann/pto-isa


为什么 AI 编译需要虚拟 ISA:三个绕不开的工程问题

问题 1:硬件指令集碎片化

昇腾 NPU 的达芬奇架构指令集是和硬件微架构绑定的。Ascend 910 的 Cube 单元指令和 Ascend 950 的 Cube 单元指令不一样,更不用说 Vector 单元、Scalar 单元的差异。如果 Graph Compiler 直接生成硬件指令,每款硬件要维护一套独立的指令生成逻辑——CANN 要支持的硬件组合一多,这套逻辑就崩了。

问题 2:算子融合的优化空间被硬件指令集限制

算子融合是 AI 编译最核心的优化手段之一。FlashAttention 把 Attention 的多个中间步骤融合成一个算子,省掉四次 HBM 读写。但如果 Graph Compiler 直接操作硬件指令,融合策略会被硬件约束绑架——某些融合模式在 Ascend 910 上能生成高效指令,在 Ascend 950 上反而性能下降,因为两款硬件的 Cube/Vector 并行度不一样。

PTO 把融合优化和指令生成解耦:融合在 PTO 层做,指令生成在后端的映射阶段做。前端只管生成语义正确的 PTO 指令序列,后端根据目标硬件的微架构特点做指令调度和优化。

问题 3:跨框架的计算图表示不统一

PyTorch 的 Dynamo 导出的计算图、TensorFlow 的 XLA 生成的 HLO、ONNX 的算子原型——它们的计算图表示都不一样。Graph Compiler 要做统一编译,先得把这些异构计算图都 lowering 到同一套中间表示,再生成统一的 PTO 指令。

如果跳过 PTO 直接生成硬件指令,等于把"统一中间表示"和"硬件指令生成"绑死在一起——换一个输入框架就要改一遍指令生成逻辑。


Graph Compiler 如何生成 PTO:从计算图到虚拟指令的完整流水线

Graph Compiler 的生成流程可以拆成四个阶段。每个阶段的输入输出、核心数据结构、关键变换,下面逐一拆解。

阶段 1:计算图解析与 HIR 生成

Graph Compiler 拿到的是框架导出的计算图(PyTorch 的 TorchScript、TensorFlow 的 GraphDef、ONNX 的 ModelProto)。第一步是把这些异构计算图统一 lowering 到 Graph Compiler 的 HIR(High-Level IR)。

HIR 是算子树状表示,每个节点对应一个算子(MatMul、Softmax、LayerNorm 等),边表示数据流依赖。这一步主要做三件事:

  1. 算子原型对齐:把不同框架的算子映射到 CANN 的算子原型。比如 PyTorch 的 torch.nn.functional.scaled_dot_product_attention 会映射到 ops-transformer 仓库的 FlashAttentionScore 算子原型。
  2. 形状推导:根据输入张量的形状信息,推导每个算子输出的形状、dtype、layout。
  3. 数据类型统一:把框架特有的数据类型(比如 PyTorch 的 bfloat16)映射到 CANN 的通用数据类型表示。
// HIR 阶段的算子节点表示(伪代码,展示数据结构设计)
struct HIRNode {
    std::string op_type;           // 算子类型:"MatMul", "Softmax", ...
    std::vector<Tensor*> inputs;   // 输入张量(含形状/dtype信息)
    std::vector<Tensor*> outputs;  // 输出张量
    std::map<std::string, Attr> attrs;  // 算子属性(axis, alpha, ...)
    std::vector<HIRNode*> consumers;     // 消费本节点输出的下游节点
};

阶段 2:图优化与 MIR 生成

HIR 生成之后,Graph Compiler 做图优化。优化后的计算图 lowering 到 MIR(Middle-Level IR),这一步开始引入 Tile 化的计算模型。

核心优化手段:

算子融合。把相邻算子合并成一个融合算子,减少中间结果的 HBM 读写。典型融合模式:MatMul + BiasAdd + GELU、Softmax + MatMul(即 FlashAttention 的核心思路)。融合决策在 MIR 层做——分析算子的数据访问模式、Tile 大小、寄存器压力,决定是否融合、怎么融合。

内存复用。分析张量的生命周期,让不同算子的输出张量复用同一块显存。比如 Transformer 的 Attention 输出和 FFN 的输入之间如果没有算子依赖,这两块显存可以复用。

死代码消除。去掉计算图中对最终输出没有贡献的节点。训练计算图转推理计算图时,Dropout、某些归一化算子会被消除。

MIR 的每个节点不再是一个完整算子,而是一个 Tile 级的计算任务。一个 MatMul 会被拆成若干个 Tile,每个 Tile 的计算对应一次 Cube 单元调用。

// MIR 阶段的 Tile 任务表示(伪代码)
struct MIRTile {
    std::string tile_type;   // Tile 类型:"matmul_tile", "vector_tile", ...
    TileShape shape;         // Tile 形状:(M, N, K)  for MatMul
    MemoryRegion* input_A;   // 输入 A 的内存区域(可能在 SRAM 或 HBM)
    MemoryRegion* input_B;   // 输入 B
    MemoryRegion* output_C;   // 输出 C
    int parallel_strategy;    // 并行策略:row_split / col_split / batch_split
};

阶段 3:指令选择与 LIR 生成

MIR 的 Tile 任务确定之后,下一步是指令选择——给每个 Tile 任务分配对应的 PTO 虚拟指令序列。这一阶段的输出是 LIR(Low-Level IR),它是 PTO 指令的线性序列。

指令选择的核心数据结构是一张指令映射表

Tile 任务类型 所选 PTO 指令 备注
MatMul Tile (Cube) PTO_CUBE_MM 矩阵乘,调用 Cube 单元
Vector Add Tile PTO_VECTOR_ADD 逐元素加法,调用 Vector 单元
Softmax Tile PTO_VECTOR_SOFTMAX Softmax,Vector 单元 + 临时寄存器
Transpose Tile PTO_DMA_TRANSPOSE 数据重排,调用 DMA 引擎
Reduce Tile PTO_VECTOR_REDUCE 归约操作(sum/max/…)

每个 PTO 指令是一个 Tile 级操作,它的操作数不是单个数值,而是一块连续内存区域(Tile)。这和传统 CPU 指令集的标量操作完全不一样——PTO 的一条指令可能对应 Cube 单元的数千次并行乘加。

// LIR 阶段的 PTO 指令表示(伪代码)
struct PTOInstruction {
    PTOOpcode opcode;        // 操作码:PTO_CUBE_MM / PTO_VECTOR_SOFTMAX / ...
    TileOperand dst;         // 目标 Tile(输出)
    std::vector<TileOperand> src;  // 源 Tile(输入,可能有多个)
    InstructionAttr attr;    // 指令属性:dtype, layout, ...
};

// 生成 PTO 指令序列的伪代码
std::vector<PTOInstruction> lower_tile_to_pto(const MIRTile& tile) {
    if (tile.tile_type == "matmul_tile") {
        return {PTOInstruction{
            .opcode = PTO_CUBE_MM,
            .dst = tile.output_C,
            .src = {tile.input_A, tile.input_B},
            .attr = {.dtype = tile.shape.dtype, .layout = "FRACTAL_NZ"}
        }};
    }
    // ... 其他 Tile 类型
}

阶段 4:PTO 指令调度与寄存器分配

LIR 生成之后,最后一步是指令调度和寄存器分配。这一步决定:每条 PTO 指令在什么时间点执行、用哪些寄存器存放中间结果、Cube/Vector/Scalar 单元怎么并行。

调度策略考虑三个约束:

  1. 数据依赖约束:如果指令 B 读指令 A 的产出 Tile,B 必须在 A 完成后才能发射。
  2. 资源约束:Cube 单元和 Vector 单元可以并行,但同一类单元的内部算术逻辑要串行。
  3. 显存层次约束:SRAM 的大小有限,要合理安排 Tile 的加载/驱逐策略,尽量减少 HBM 访问。

调度完成后,PTO 指令序列和调度信息一起输出,作为后端指令映射阶段的输入。


昇腾 NPU 如何执行底层指令:从 PTO 到机器码的完整映射

PTO 是虚拟指令,不能直接在硬件上执行。昇腾 NPU 的执行引擎需要把 PTO 指令映射成达芬奇架构的底层机器码。

指令映射的核心:PTO opcode → 达芬奇微操作码

达芬奇架构的计算单元分三类:Cube(矩阵乘)、Vector(向量运算)、Scalar(标量运算)。PTO 的 90+ 标准操作会根据操作类型被映射到不同单元的微操作码。

映射规则示例(以 Ascend 910 为例):

PTO 指令 目标硬件单元 达芬奇微操作码 说明
PTO_CUBE_MM Cube CUBE_MM 矩阵乘,FP16/INT8 输入
PTO_VECTOR_ADD Vector V_ADD 逐元素加法
PTO_VECTOR_SOFTMAX Vector V_SOFTMAX + V_REDUCE Softmax 拆成两步走
PTO_DMA_LOAD DMA DMA_L1TO_L0 数据从 L1 缓冲区搬到 L0 缓冲区
PTO_SINC Scalar SCALAR_ADD 标量加法(用于循环计数等)

映射阶段还会做寄存器分配:把 PTO 指令的 Tile 操作数绑定到具体的物理寄存器(URG、L0A、L0B、L0C 等)。这个分配结果会影响指令的发射延迟——如果两个相邻指令竞争同一个寄存器文件,就得插入等待周期。

Kernel 生成:从机器码到可执行 Task

指令映射完成后,生成的是线性机器码序列。但这还不是一个可以提交给 NPU 执行的完整 Kernel。

Kernel 生成的最后一步是把机器码序列封装成 Task 描述符,包含:

  • 机器码本身的二进制数据
  • Task 的类型(Vector Task / Cube Task / DMA Task)
  • 依赖关系(这个 Task 需要等哪些 Task 完成)
  • 块/线程配置(Grid/Block 维度,对应 Launch 参数)

Runtime 运行时读取 Task 描述符,把机器码加载到 NPU 的指令缓存,触发执行。

// Kernel 生成伪代码(展示 Task 描述符结构)
struct TaskDescriptor {
    TaskType type;               // VECTOR_TASK / CUBE_TASK / DMA_TASK
    void* machine_code;           // 机器码指针
    size_t machine_code_size;     // 机器码字节数
    std::vector<int> depend_ids;  // 依赖的 Task ID 列表
    Dim3 grid_dim;               // Grid 维度
    Dim3 block_dim;              // Block 维度
};

// Runtime 提交 Task 的伪代码
void submit_task(const TaskDescriptor& task) {
    // 1. 把机器码加载到 NPU 指令缓存
    load_machine_code_to_icache(task.machine_code, task.machine_code_size);
    // 2. 设置 Grid/Block 配置
    configure_grid_block(task.grid_dim, task.block_dim);
    // 3. 等待依赖的 Task 完成
    wait_dependencies(task.depend_ids);
    // 4. 触发执行
    trigger_execution();
}

Transformer 推理中的编译链路:从输入到输出的完整路径

把上面所有阶段串起来,看一个 Transformer 推理请求在 CANN 上的完整编译和执行链路。

编译期(只需执行一次)

假设要部署的模型是 Qwen3-32B,推理框架用的是 PyTorch + TorchAir 图模式。

步骤 1:计算图导出。PyTorch 模型通过 TorchAir 导出成统一的计算图表示(绕过 PyTorch 的 eager 执行,直接拿到静态计算图)。

步骤 2:Graph Compiler 前端处理。计算图被 lowering 到 HIR,完成算子原型对齐和形状推导。这一步会把 nn.MultiHeadAttention 映射到 ops-transformer 仓库的 FlashAttentionScore + GEMM 算子组合。

步骤 3:图优化。Graph Compiler 做算子融合——把 MatMul + GELU 融合成一个算子,把 Softmax + MatMul 融合成 FlashAttention 模式。同时做内存复用分析,让 KV Cache 的显存块在整个推理过程中静态分配。

步骤 4:PTO 生成。优化后的计算图被 lowering 到 MIR(Tile 化),再生成 LIR(PTO 指令序列),最后做指令调度和寄存器分配。输出是一份完整的 PTO 指令序列 + 调度信息。

步骤 5:指令映射 + Kernel 生成。后端把 PTO 指令映射成 Ascend 910 的机器码,封装成 Task 描述符。所有 Task 的依赖关系和并行策略都在这一步确定。

步骤 6:序列化。编译完成的模型(包含 Task 描述符、权重、KV Cache 配置)被序列化成离线模型文件,部署时直接加载,不需要重新编译。

推理期(每个请求执行一次)

步骤 7:模型加载。Runtime 运行时把离线模型加载到 NPU 显存,初始化 KV Cache 缓冲区。

步骤 8:推理执行。每个 token 的生成经历 Prefill 阶段(处理输入序列)+ Decode 阶段(逐 token 生成)。每个阶段都按照编译期确定的 Task 调度顺序执行——FlashAttention 的 PTO 指令先跑,GELU 的 PTO 指令等 MatMul 完成后再跑,Cube 和 Vector 单元的并行在指令映射阶段已经安排好。

步骤 9:KV Cache 管理。Transformer 推理的核心瓶颈之一是 KV Cache 的显存占用。编译期已经通过 PTO 指令的调度分析确定了 KV Cache 的显存布局(Paged Attention 模式下的分块管理),推理期只需要按调度好的地址做 DMA 搬运,不需要动态显存分配。

# Transformer 推理编译链路的伪代码(展示关键调用)
import torch
import torch_npu
from torchair import TorchAirConfig

# 编译期:导出计算图 + Graph Compiler 编译
model = load_qwen3_32b()
config = TorchAirConfig(enable_graph_mode=True, enable_fusion=True)
with torchair.inference_session(config) as session:
    # 这一步触发 Graph Compiler 的完整编译流水线
    # HIR → MIR → LIR(PTO) → 机器码 → Task 描述符
    session.compile(model, example_inputs)

# 推理期:加载编译结果 + 执行
runtime = torch_npu.npu.runtime.Runtime(session.get_compiled_model())
kv_cache = runtime.allocate_kv_cache(max_seq_len=8192)

for token in input_tokens:
    # 每个 token 按照编译好的 Task 调度顺序执行
    output = runtime.forward(token, kv_cache)

从 PTO 看 CANN 编译层的设计取舍

PTO 这套虚拟指令集的设计,背后有几个关键的工程取舍:

Tile 级指令 vs 算子级指令。PTO 选择 Tile 级而不是算子级作为虚拟指令的粒度。好处是后端的指令映射有更大的调度空间——可以把多个小 Tile 合并成一次大的硬件调用,也可以把一个大 Tile 拆成多次小调用以适配 SRAM 大小。如果 PTO 是算子级的,这些调度决策就被前端固定死了。

90+ 标准操作 vs 开放扩展。pto-isa 仓库定义了 90+ 标准 Tile 级操作,覆盖大部分算子开发的需求。但 AI 算子在不停演进——MoE、GroupedQueryAttention、MLA 这些新结构不一定能直接用标准操作组合出来。PTO 的设计是标准操作 + 自定义 Tile 操作并行——常用算子走标准路径,新算子可以定义自定义 PTO 操作,只要后端的指令映射能处理。

跨平台 vs 硬件特化。PTO 是虚拟 ISA,理论上可以映射到不同的硬件架构。但 PTO 的 Tile 操作集本身是针对达芬奇架构的计算特点设计的(Cube/Vector/Scalar 三分法、SRAM/HBM 两级存储、DMA 数据搬运)。如果要把 PTO 映射到其他厂商的 NPU,指令映射层的改动量不小。所以 PTO 的"跨平台"目前主要在昇腾系列硬件内跨代兼容,不是通用虚拟 ISA。


继续深入:Graph Compiler 与 pto-isa 仓库

这篇文章拆了 Graph Compiler 生成 PTO 的完整流水线——从计算图解析、图优化、Tile 调度、PTO 指令生成、到指令映射和 Kernel 生成。这条链路是 CANN 编译层的核心,理解了它,就理解了昇腾 NPU 上 AI 模型为什么能跑、怎么跑得快。

如果想深入掌握这套编译系统,建议按这个顺序继续学习:

  1. 先读 pto-isa 仓库的指令集规范。90+ 标准 Tile 级操作的定义、每个操作的输入输出语义、Tile 的内存布局约定。仓库地址:https://atomgit.com/cann/pto-isa

  2. 再读 Graph Compiler 的图优化实现。GE 图引擎的源码里可以看到算子融合、内存复用、多流并行的具体实现。它和 Graph Compiler 的配合方式是理解编译链路的关键。仓库地址:https://atomgit.com/cann/ge

  3. 最后看一个具体的算子如何从 Python 到 PTO 到机器码。ops-transformer 仓库里的 FlashAttentionScore 算子是个好例子——它有完整的 Ascend C 实现、Graph Compiler 的融合注册逻辑、PTO 指令生成路径。跑一遍 debug 流程,编译链路的每个环节就都串起来了。

CANN 的开源仓库都在 https://atomgit.com/cann/ ,直接 clone 下来看源码比读文档有效得多。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐