CANN pto-isa:虚拟指令集如何连接编译与执行

个人主页:ujainu

文章目录

    • 前言
    • [为什么 PTO 存在:编译器的"中间协议"](#为什么 PTO 存在:编译器的"中间协议")
    • [为什么 AI 编译需要虚拟 ISA:从指令集碎片化说起](#为什么 AI 编译需要虚拟 ISA:从指令集碎片化说起)
      • [没有虚拟 ISA 的世界](#没有虚拟 ISA 的世界)
      • [引入 PTO 虚拟 ISA 之后](#引入 PTO 虚拟 ISA 之后)
    • [PTO 指令集:90+ 标准 Tile 级操作](#PTO 指令集:90+ 标准 Tile 级操作)
      • [PTO 指令的基本格式](#PTO 指令的基本格式)
    • [Graph Compiler 如何生成 PTO:从计算图到指令序列](#Graph Compiler 如何生成 PTO:从计算图到指令序列)
    • [昇腾 NPU 如何执行底层指令:从 PTO 到硬件](#昇腾 NPU 如何执行底层指令:从 PTO 到硬件)
    • [Transformer 推理中的编译链路:完整走读](#Transformer 推理中的编译链路:完整走读)
    • [调试技巧:如何查看 PTO 指令序列](#调试技巧:如何查看 PTO 指令序列)
    • [配置文件:编译选项对 PTO 生成的影响](#配置文件:编译选项对 PTO 生成的影响)
    • [构建与运行:验证 PTO 指令生成](#构建与运行:验证 PTO 指令生成)
    • 总结

前言

第一次看 CANN 五层架构图时,很多人会漏掉第 3 层编译层和底层硬件之间的那个"灰色地带"------编译完了,指令是怎么真正跑到昇腾 NPU 上的?

答案就藏在 pto-isa 这个仓库里。

pto-isa 定义了一套 PTO(Parallel Tile Operation)虚拟指令集架构。它不直接对应昇腾达芬奇架构的底层指令,而是给 Graph Compiler 提供一个稳定的中间目标------让编译器只管生成 PTO 指令,让运行时去操心怎么映射到真实硬件。

这篇文章把这条链路拆开讲清楚:为什么需要虚拟 ISA、Graph Compiler 怎么生成 PTO、昇腾 NPU 怎么执行底层指令、以及 Transformer 推理里这条编译链路长什么样。


为什么 PTO 存在:编译器的"中间协议"

直接说结论:没有 PTO,Graph Compiler 就要直接面对每一代昇腾 NPU 的底层指令变化。

昇腾 NPU 的底层指令集是硬件相关的。Ascend 910、Ascend 950PR、Ascend 950DT 的指令编码格式不同,Cube 单元和 Vector 单元的指令格式也不同。如果 Graph Compiler 直接生成底层指令,每出一款新硬件就要改一遍编译后端------这个维护成本,没有人扛得住。

PTO 干的事就是插进去做"中间协议":

复制代码
Graph Compiler(硬件无关)
       ↓ 生成
PTO 虚拟指令(pto-isa 定义)
       ↓ 指令映射
底层 NPU 指令(硬件相关)

这个设计的核心收益有三条:

  1. 编译器与硬件解耦------Graph Compiler 只需要面向 PTO 指令集编程,不需要关心底层指令怎么编码
  2. 跨代兼容------新硬件只需要新增一套 PTO→底层指令的映射规则,编译器侧零修改
  3. 跨平台算子开发------pto-isa 定义了 90+ 标准 Tile 级操作,开发者面向 PTO 写算子,自动获得多硬件后端支持

用一句大白话讲:PTO 是编译器和硬件之间的"翻译协议",让两边各自演化,不再互相绑架。


为什么 AI 编译需要虚拟 ISA:从指令集碎片化说起

通用 CPU 世界里有 x86 和 ARM 两条指令集,大家忍忍也就过去了。AI 加速芯片的世界里,每一家的指令集都是私有的,甚至连同一家的不同代芯片都会改指令编码格式。

If you write a compiler that directly generates hardware instructions, you're signing up for a maintenance nightmare.

没有虚拟 ISA 的世界

复制代码
Graph Compiler ──→ Ascend 910 指令格式(硬编码)
                    ↑ 换硬件?重写编译器后端

这是早期 AI 编译器的做法。问题在于:

  • 昇腾 NPU 的指令是 Tile 级 的------每次操作处理一个 tile(小块数据),而不是逐元素操作。Tile 的大小、排布方式、Cube/Vector 单元协同方式,每一代都会优化调整
  • 底层指令格式是 编码级 的------opcode 占几位、寄存器索引占几位、立即数怎么编码,都是硬件细节
  • 融合算子需要 指令级协同------FlashAttention 的 tiling 逻辑需要在指令层面做流水编排,直接生成底层指令会让编译器变得极其复杂

引入 PTO 虚拟 ISA 之后

复制代码
Graph Compiler ──→ PTO 虚拟指令(pto-isa 标准定义)
                         ↓ 指令映射层(运行时负责)
                      Ascend 910 指令格式
                      Ascend 950PR 指令格式
                      ...(新增硬件只需加映射规则)

PTO 的"虚拟"体现在三件事上:

  1. 不绑定具体硬件编码------PTO 指令描述的是"做什么操作",不是"怎么编码成二进制"
  2. Tile 级语义 ------每条 PTO 指令对应一个标准 Tile 操作(如 tile_matmultile_softmax),编译器用 Tile 粒度思考,不用管底层怎么拆分
  3. 可扩展的映射规则------同一份 PTO 指令序列,通过不同的映射配置,可以生成不同硬件的底层指令

PTO 指令集:90+ 标准 Tile 级操作

pto-isa 仓库的核心产物是一份 PTO 指令集规范,定义了 90+ 条标准 Tile 级操作。

这些操作按功能可以分成几大类:

类别 代表指令 说明
矩阵运算 tile_matmultile_mmad Tile 级矩阵乘,映射到 Cube 单元
向量运算 tile_relutile_gelutile_layernorm Tile 级向量计算,映射到 Vector 单元
数据搬运 tile_loadtile_storetile_broadcast Tile 间数据搬运,映射到 MTE
归约操作 tile_reduce_sumtile_reduce_max Tile 内归约,常用于 Softmax
融合操作 tile_matmul_gelutile_attention_score 多操作融合为单条 PTO 指令

PTO 指令的基本格式

每条 PTO 指令包含:

  • 操作码(opcode) :标识指令类型(如 TILE_MATMUL
  • 操作数描述:源 Tile 索引、目标 Tile 索引、Tile 形状
  • 数据类型:FP16、BF16、FP32、INT8 等
  • 融合标志:是否与其他指令融合执行

下面是一个 PTO 指令序列的真实样子(伪代码格式):

python 复制代码
# PTO 指令序列示例:FlashAttention 的 attention score 计算
# 这段 PTO 指令由 Graph Compiler 生成,描述了一个完整的 attention score 计算

pto_program = [
    # 1. Q 和 K 做矩阵乘,得到 attention score
    PTOInstruction(
        opcode="TILE_MATMUL",
        src_tiles=[tile_q, tile_k],   # Q 和 K 各是一个 tile
        dst_tile=tile_score,
        dtype="FP16",
        fuse_next=True                # 和下一指令融合
    ),
    # 2. 对 attention score 做 Softmax(融合执行,省一次 Tile 写回)
    PTOInstruction(
        opcode="TILE_SOFTMAX",
        src_tiles=[tile_score],
        dst_tile=tile_softmax,
        dim=-1,                       # 沿最后一维做 softmax
        fuse_next=False
    ),
    # 3. attention score 和 V 做矩阵乘,得到输出
    PTOInstruction(
        opcode="TILE_MATMUL",
        src_tiles=[tile_softmax, tile_v],
        dst_tile=tile_output,
        dtype="FP16"
    )
]

这段 PTO 指令序列描述了一个完整的 attention score 计算流程。注意第 1 条指令的 fuse_next=True------这个标志告诉指令映射层:把 TILE_MATMULTILE_SOFTMAX 融合为一次底层指令发射,省掉中间 Tile 的写回开销。


Graph Compiler 如何生成 PTO:从计算图到指令序列

Graph Compiler 是 CANN 第 3 层编译层的核心组件,负责把计算图编译为可执行的指令序列。PTO 是 Graph Compiler 的直接输出目标。

生成流程拆解

复制代码
计算图(GE 输出)
    ↓ 图优化(算子融合、内存复用)
优化后的计算图
    ↓ 算子调度(Tile 切分、流水编排)
调度计划(Tile 执行顺序)
    ↓ 指令选择(每条 Tile 操作 → PTO 指令)
PTO 指令序列
    ↓ 指令映射(PTO → 底层 NPU 指令)
底层 NPU 指令流

关键步骤详解

步骤 1:图优化

Graph Compiler 接收 GE 优化后的计算图,进一步做算子融合。比如 MatMul → GELU 融合为单算子,对应的 PTO 输出就是一条融合指令 TILE_MATMUL_GELU,而不是两条独立指令。

步骤 2:Tile 切分

大模型推理的矩阵经常大过 Tile 的大小(典型 Tile 形状:128×128 或 256×256)。Graph Compiler 把大矩阵切分成多个 Tile,为每个 Tile 生成对应的 PTO 指令。

步骤 3:指令选择

每条 Tile 操作映射为 PTO 指令。这个映射是 模式匹配 的过程------Graph Compiler 内部维护了一张"Tile 操作模式 → PTO 指令"的映射表。

下面是一段 Graph Compiler 生成 PTO 指令的伪代码:

python 复制代码
# Graph Compiler 内部:将融合算子映射为 PTO 指令
# 文件:graph_compiler/fusion_mapper.py(伪代码)

def map_fusion_pattern_to_pto(pattern, tile_shape, dtype):
    """
    将算子融合模式映射为 PTO 指令序列
    
    Args:
        pattern: 融合模式,如 "matmul_gelu"、"attention_score"
        tile_shape: Tile 形状,如 (128, 128)
        dtype: 数据类型,如 "FP16"
    
    Returns:
        List[PTOInstruction]: PTO 指令序列
    """
    if pattern == "matmul_gelu":
        # MatMul + GELU 融合 → 单条 PTO 融合指令
        return [PTOInstruction(
            opcode="TILE_MATMUL_GELU",
            src_tiles=["tile_a", "tile_b"],
            dst_tile="tile_out",
            dtype=dtype,
            tile_shape=tile_shape
        )]
    
    elif pattern == "attention_score":
        # Q @ K^T + Softmax → 两条指令,标记融合执行
        return [
            PTOInstruction("TILE_MATMUL", ["tile_q", "tile_k"], 
                           "tile_score", dtype, fuse_next=True),
            PTOInstruction("TILE_SOFTMAX", ["tile_score"], 
                           "tile_softmax", dtype, fuse_next=False)
        ]
    
    else:
        # 未知模式 → 拆成多条基础指令
        return decompose_to_basic_ops(pattern, tile_shape, dtype)

步骤 4:指令调度

PTO 指令序列生成后,Graph Compiler 会对指令做调度优化:

  • Tile 间并行------独立的 Tile 计算可以并行发射
  • 流水编排------Cube 单元和 Vector 单元交替工作,隐藏延迟
  • 内存复用------相邻 Tile 复用同一块片上内存,减少 MTE 搬运

昇腾 NPU 如何执行底层指令:从 PTO 到硬件

PTO 指令序列生成后,并没有直接扔给硬件。中间还有一层 指令映射(Instruction Mapping),由 CANN 运行时(Runtime)负责。

指令映射层

复制代码
PTO 指令序列(硬件无关)
         ↓ 指令映射层(Runtime 负责)
      opcode 映射(TILE_MATMUL → 底层 matmul opcode)
      操作数映射(Tile 索引 → 物理寄存器/片上内存地址)
      数据类型映射(FP16 → 硬件支持的编码格式)
         ↓
底层 NPU 指令流(硬件相关)
         ↓ 指令发射
    昇腾 NPU 执行(Cube / Vector / MTE)

指令映射的核心数据结构是一张 映射表,描述每条 PTO 指令在不同硬件上的底层实现:

cpp 复制代码
// 指令映射配置(伪代码)
// 文件:runtime/instruction_mapper.h(伪代码)

struct PTOInstructionMapping {
    PTOOpcode pto_opcode;           // PTO 操作码
    HardwareType hw_type;            // 目标硬件类型
    
    // 映射结果
    LowLevelOpcode ll_opcode;        // 底层操作码
    ExecutionUnit exe_unit;          // 执行单元(CUBE / VECTOR / MTE)
    bool support_fusion;             // 是否支持融合执行
    int latency_cycles;             // 执行延迟(周期数)
};

// Ascend 910 上的映射表示例
PTOInstructionMapping mapping_table[] = {
    {TILE_MATMUL,      ASCEND_910,  LL_MATMUL,    CUBE,  true,  128},
    {TILE_SOFTMAX,     ASCEND_910,  LL_SOFTMAX,   VECTOR, false, 64},
    {TILE_MATMUL_GELU, ASCEND_910,  LL_MATMUL_GELU, CUBE, true,  192},
    // ... 90+ 条 PTO 指令各自的映射配置
};

底层指令执行流程

映射生成的底层指令通过以下路径到达硬件:

复制代码
Runtime 指令队列
    ↓ 指令发射(Task 下发)
任务调度器(Task Scheduler)
    ↓ 分配执行单元
Cube 单元 / Vector 单元 / MTE
    ↓ 执行
达芬奇架构 AI Core

关键设计:指令映射是延迟绑定的。Graph Compiler 编译时只生成 PTO 指令,真正的映射发生在模型加载到具体硬件时。这意味着同一份编译结果(PTO 指令序列)可以在不同代的昇腾 NPU 上运行,只要运行时支持对应的映射规则。


Transformer 推理中的编译链路:完整走读

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

端到端链路

复制代码
PyTorch 模型
    ↓ TorchAir(图模式接入)
ONNX 计算图
    ↓ GE 图引擎(图优化、算子融合)
优化后的计算图
    ↓ Graph Compiler(编译层)
PTO 指令序列                    ← pto-isa 定义的标准格式
    ↓ Runtime 指令映射
底层 NPU 指令流
    ↓ Task 下发
昇腾 NPU 执行(Attention + FFN)
    ↓
推理结果返回

FlashAttention 在链路中的具体形态

以 Transformer 中最核心的 FlashAttention 算子为例,看它在各层的具体形态:

层级 形态 格式
框架层 PyTorch 算子调用 F.scaled_dot_product_attention(Q, K, V)
GE 层 计算图节点 FlashAttentionScore 节点(已融合)
Graph Compiler 层 PTO 指令序列 TILE_MATMULTILE_SOFTMAXTILE_MATMUL
Runtime 层 底层指令流 Ascend 910 的 matmul + softmax + matmul 指令
硬件层 AI Core 执行 Cube 单元执行矩阵乘,Vector 单元执行 Softmax

性能关键点

这条链路上有三个性能关键点,都是 PTO 指令集设计直接影响的地方:

  1. 融合粒度 ------FlashAttention 的 Q@K^TSoftmax 能否融合为一次指令发射,取决于 PTO 是否定义了对应的融合指令(TILE_MATMUL_SOFTMAX)。pto-isa 的 90+ 指令里包含了常见的融合模式,减少了 Tile 中间结果的写回次数。

  2. Tile 大小匹配------PTO 指令的 Tile 形状参数需要和硬件的片上内存大小匹配。Graph Compiler 在生成 PTO 指令时会查询硬件的 Tile 配置(通过 Runtime 获取),选择合适的 Tile 形状。

  3. 指令映射延迟------PTO 到底层指令的映射需要在模型加载时完成。mapping table 的查找是 O(1) 的(哈希表实现),但融合指令的拆解(当硬件不支持某个融合 PTO 指令时,需要拆成多条底层指令)会引入额外开销。


调试技巧:如何查看 PTO 指令序列

开发过程中经常需要确认 Graph Compiler 生成的 PTO 指令是否符合预期。pto-isa 仓库提供了调试工具,可以打印 PTO 指令序列。

打印 PTO 指令

python 复制代码
# 调试技巧:打印 Graph Compiler 生成的 PTO 指令序列
# 需要设置环境变量开启 PTO dump

import os
# 开启 PTO 指令打印
os.environ["DUMP_PTO_INSTRUCTIONS"] = "1"
os.environ["PTO_DUMP_FILE"] = "/tmp/pto_dump.txt"

# 正常运行推理,PTO 指令会自动 dump 到指定文件
# 文件格式:每条指令一行,包含 opcode、操作数、Tile 形状

dump 出来的文件内容大致如下:

复制代码
# /tmp/pto_dump.txt
[PTO] op=TILE_MATMUL, src=[tile_q:128x128, tile_k:128x128], dst=tile_score:128x128, dtype=FP16, fuse=1
[PTO] op=TILE_SOFTMAX, src=[tile_score:128x128], dst=tile_softmax:128x128, dtype=FP16, fuse=0
[PTO] op=TILE_MATMUL, src=[tile_softmax:128x128, tile_v:128x128], dst=tile_out:128x128, dtype=FP16, fuse=0

性能分析方法

除了直接看 PTO 指令序列,还可以用性能分析工具确认每条 PTO 指令的底层执行效率:

python 复制代码
# 性能分析:统计每条 PTO 指令的执行周期
# 需要 CANN 运行时开启 profiling

import torch
import torch_npu

# 开启 PTO 级别 profiling
with torch.autograd.profiler.profile(
    activities=[torch.profiler.ProfilerActivity.NPU],
    with_stack=True
) as prof:
    output = model(input_ids)

# 打印 profiling 结果,可以看到每条 PTO 指令的耗时
print(prof.key_averages().table(sort_by="self_npu_time_total"))

配置文件:编译选项对 PTO 生成的影响

Graph Compiler 生成 PTO 指令时,行为受编译配置文件控制。关键配置项影响 Tile 大小、融合策略和指令映射方式。

复制代码
# ${CANN_INSTALL_PATH}/config/graph_compiler.conf

[pto_generation]
# Tile 形状:影响 PTO 指令的 src_tiles/dst_tile 形状参数
tile_m=${TILE_M:128}          # Tile M 维度大小
tile_n=${TILE_N:128}          # Tile N 维度大小
tile_k=${TILE_K:128}          # Tile K 维度大小

# 融合策略:控制是否生成融合 PTO 指令
enable_op_fusion=${ENABLE_OP_FUSION:true}
max_fusion_depth=${MAX_FUSION_DEPTH:3}   # 最多融合几个算子

# 指令映射:选择映射规则版本
instruction_mapping_version=${MAPPING_VER:ascend_910_v2}
enable_late_mapping=${LATE_MAPPING:true}  # 延迟绑定:加载模型时才做指令映射

enable_late_mapping=true 是跨代兼容的关键配置。设为 true 时,Graph Compiler 编译结果里只存 PTO 指令序列,底层指令映射推迟到模型在具体硬件上加载时执行。


构建与运行:验证 PTO 指令生成

如果要基于 pto-isa 仓库做二次开发(比如新增自定义 PTO 指令),需要构建并验证。

bash 复制代码
# 克隆 pto-isa 仓库
git clone https://atomgit.com/cann/pto-isa.git
cd pto-isa

# 构建(使用 CANN 提供的构建脚本)
bash build.sh

# 构建产物:
# - libpto_isa.so:PTO 指令集动态库
# - pto_tools/pto_dump:PTO 指令打印工具
# - include/pto_isa.h:PTO 指令集头文件(供 Graph Compiler 引用)

# 验证:用 pto_dump 工具查看示例 PTO 程序
./build/pto_tools/pto_dump ./examples/flash_attention.pto

总结

PTO 在 CANN 五层架构里的定位,用一句话说就是:编译层输出 PTO,执行层消费 PTO,中间靠指令映射衔接。

理解这一层,才能真正看懂 CANN 的编译链路为什么能做到"编译一次,多硬件运行"------虚拟 ISA 是最核心的抽象层。

如果这篇文章帮你理清了 PTO 的位置,下一步建议直接去看 Graph Compiler 的源码------看它怎么把计算图节点映射为 PTO 指令,比读十篇文档都管用。

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

相关推荐
赏金术士1 天前
第六章:UI组件与Material3主题
android·ui·kotlin·compose
ujainu1 天前
CANN pto-isa:PTO 虚拟指令集里的 90+ Tile 操作怎么设计的
ascend
TechMerger1 天前
Android 17 重磅重构!服役 20 年的 MessageQueue 迎来无锁改造,卡顿大幅优化!
android·性能优化
yuhuofei20211 天前
【Python入门】Python中字符串相关拓展
android·java·python
dalancon1 天前
Android Input Spy Window
android
dalancon1 天前
InputDispatcher派发事件,查找目标窗口
android
我命由我123451 天前
Android Framework P3 - MediaServer 进程、认识 ServiceManager 进程
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
天才少年曾牛1 天前
Android14 新增系统服务后,应用调用出现 “hidden api” 警告的原因与解决方案
android·frameworks
赏金术士1 天前
Jetpack Compose 底部导航实战教程(完整版)
android·kotlin·compose