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

相关推荐
-SOLO-9 小时前
备份apk 工具
android
私人珍藏库14 小时前
【Android】BotHub-多模型AI机器人聚合库-内置免费模型
android·人工智能·智能手机·app·工具·多功能
普马萨特14 小时前
Wi-Fi 扫描频率限制与 Android 演进全解析
android
张拭心15 小时前
Android 17 新特性:后台音频交互限制加强
android·前端
张拭心15 小时前
Android 17 新特性:ProfilingManager 新触发器
android·前端
张拭心15 小时前
Android 17 新特性:MessageQueue 无锁实现
android·前端
brycegao15 小时前
如何搭建标准化 Git 工具流,保障 Android 团队代码质量
android·ci/cd
AI科技星15 小时前
数术江湖·全卷合集 - 硬核江湖・数理史诗
android·人工智能·架构·概率论·学习方法
五月君_15 小时前
安卓也支持了!微信链接 Claude Code 保姆级教程
android·微信
柚鸥ASO优化15 小时前
一篇讲透安卓ASO!开发者千万别只盯着iOS了
android·ios·aso优化