一个 Ascend C kernel 写好后要在昇腾 NPU 上执行需要经过两道编译第一道昇腾编译器把 Ascend C 翻译成 PTOParallel Tensor Orchestration虚拟指令第二道NPU 固件在运行时把 PTO 虚拟指令翻译成 AI Core 的具体硬件指令。PTO-ISA 定义的就是中间这一层的指令集规范。它不绑定具体的 NPU 硬件代际——Ascend 910 和 Ascend 950PR 都跑同一套 PTO 指令固件负责把 PTO 映射到各自的硬件实现。这是「写一次跨NPU代际运行」的关键。PTO 指令集的三类指令指令类数量功能对应硬件计算指令32MMAD、VMAC、VEXP、VLOG…Cube/Vector 单元数据搬运指令12LOAD、STORE、DMA_COPY、PREFETCHSDMA/L1 缓存控制指令8SYNC、BARRIER、LOOP、COND调度器一个简化版的 MatMul kernel 对应的 PTO 指令序列; PTO IR for simplified MatMul: C[M,N] A[M,K] * B[K,N] ; M256, N256, K256, tile64 LOOP k_outer, K/64: ; 外部循环K 维度分块 LOAD tile_a, A_ptr, {64, 64} ; 从 HBM 加载 A[64,64] 到 L1 LOAD tile_b, B_ptr, {64, 64} ; 从 HBM 加载 B[64,64] 到 L1 SYNC LOAD_DONE ; 等待数据到达 MMAD C_local, tile_a, tile_b ; Cube 单元计算 C A × B SYNC COMPUTE_DONE STORE C_ptr, C_local, {64, 64} ; 写回 HBM SYNC STORE_DONE ADD A_ptr, A_ptr, 64*64*2 ; 推进 A、B 的 HBM 指针 ADD B_ptr, B_ptr, 64*64*2 END_LOOP每条 PTO 指令被固件展开为 1-N 条硬件指令。MMAD在 Ascend 910 上被映射到 4 条硬指令四次 16×16 的矩阵乘-加在 Ascend 950PR 上可能映射到 1 条硬指令硬件支持 64×64 的 MMAD 了。PTO 的融合重写指令级优化编译器在生成 PTO 时会做指令级重写——把多个独立的 PTO 指令融合成一条复合 PTO 指令。这是算子性能的关键; 优化前独立的 LOAD MMAD STORE LOAD tile_a, A_ptr, {64, 64} LOAD tile_b, B_ptr, {64, 64} SYNC LOAD_DONE MMAD C_local, tile_a, tile_b SYNC COMPUTE_DONE STORE C_ptr, C_local, {64, 64} SYNC STORE_DONE ; 优化后融合成一条 FUSED_MMAD 指令 ; LOAD 用双缓冲加载下一块数据时 MMAD 在算当前块 ; STORE 也一样MMAD 在算时上一块的结果被 SDMA 搬走 FUSED_MMAD C_ptr, A_ptr, B_ptr, { tile{64, 64, 64}, double_buffertrue, async_storetrue }融合后的FUSED_MMAD一条指令覆盖了原来的 7 条指令。硬件上Cube 单元算 64×64×64 的 MMAD 时SDMA 同时在搬数据——算力 100% 跑满搬运也在同步走。延迟藏在双缓冲的 overlap 里。踩坑一LOAD 和 MMAD 的依赖分析失效PTO 编译器用数据依赖分析来判断哪些 LOAD 可以和 MMAD 融合。如果 Ascend C kernel 里的指针别名分析不够精确编译器保守地认为两个 LOAD 可能访问重叠的内存——不融合性能直接掉 40%。错误写法// Ascend C kernel两个指针被编译器认为是可能重叠的__aicore__voidkernel(GlobalTensorfloata,GlobalTensorfloatb){float*ptr1a.GetPtr();// 从参数 a 获取float*ptr2a.GetPtr();// 从同一个参数 a 获取// 编译器看到两个指针都指向 a → 可能重叠 → LOAD 不并行LocalTensorfloatt1(64);LocalTensorfloatt2(64);DataCopy(t1,ptr1,64);// LOAD 1DataCopy(t2,ptr2,64);// LOAD 2等 LOAD 1 完成保守是// PTO 生成LOAD t1 SYNC → LOAD t2 SYNC// 两条 LOAD 串行没有融合}正确写法用__restrict__告诉编译器两个指针不重叠。// 正确用 __restrict__ 声明指针不重叠__aicore__voidkernel(GlobalTensorfloat__restrict__ a,GlobalTensorfloat__restrict__ b){float*__restrict__ ptr1a.GetPtr();float*__restrict__ ptr2b.GetPtr();// 不同参数LocalTensorfloatt1(64);LocalTensorfloatt2(64);DataCopy(t1,ptr1,64);// LOAD 1DataCopy(t2,ptr2,64);// LOAD 2独立可并行// PTO 生成FUSED_LOAD t1, t2融合成一条// 两个 LOAD 并行启动SDMA 同时搬两路数据}性能差异两条 LOAD 独立并行 → 融合成 FUSED_LOAD数据搬运时间减半。在带宽敏感的 kernel 里如 matmul这是 30-40% 的性能差异。踩坑二STORE 的隐式同步点PTO 编译器在MMAD和STORE之间自动插入SYNC COMPUTE_DONE。但如果 kernel 在MMAD之后有别的不依赖结果的计算比如处理下一块的 index 更新这个隐式 SYNCC 是不必要的——它把流水线打断了。错误写法// Ascend C kernelMMAD 之后直接更新 index// PTO 编译器插入了隐式 SYNC COMPUTE_DONEMMAD(C_local,a_tile,b_tile);// ← 自动插入 SYNC// index 更新不依赖 C_local不需要等 MMAD 完成intnext_offsetcurrent_offsettile_size;// 不依赖 MMADDataCopy(C[offset],C_local,64);// 编译器又插入 SYNC等 index 更新完成正确写法先把不依赖结果的 index 更新提到 MMAD 之前。// 正确MMAD 之前算好所有 indexintnext_offsetcurrent_offsettile_size;// 不依赖上一个 MMADMMAD(C_local,a_tile,b_tile);// ← 编译器自动插入 SYNC现在只有一次不影响 indexDataCopy(C[current_offset],C_local,64);// 不需要再等 index根因PTO 编译器对MMAD后的第一个写操作包括整数变量赋值自动插SYNC COMPUTE_DONE——它保守地认为后续操作可能依赖前面的计算。但实际上 index 更新是纯整数计算完全不依赖浮点 MMAD 的结果。踩坑三虚指令的硬件退化路径PTO 的FUSED_MMAD在某些硬件代际上不被原生支持——固件会把它退化degrade成多条基本 PTO 指令。退化后的指令序列有额外的 L1 容量需求可能导致 L1 溢出。场景在 Ascend 910 上开发了用了FUSED_MMAD的 kernel。性能在 910 上很好。部署到 Ascend 950PR 时固件把FUSED_MMAD映射到一条硬件指令原生支持L1 里的中间数据排布和 910 不同。kernel 里硬编码的 L1 usage 假设被打乱了。正确做法不和底层硬件绑定——在 kernel 代码里用#ifdef或运行时查询来适配 L1 容量// 查询当前 NPU 的 L1 大小intl1_capacityGetChipL1CacheSize();// Ascend 910: 192KB// Ascend 950PR: 256KB// 按 L1 容量动态算 tile sizeintmax_tile(l1_capacity-reserve)/(3*sizeof(float));intMbmin(M,max_tile);intNbmin(N,max_tile);intKbmin(K,max_tile/2);PTO-ISA 不是面向应用开发者的接口——写 kernel 的人看不到 PTO 指令。但理解 PTO 的作用能解释为什么 kernel 性能在 910 和 950PR 上差一倍FUSED_MMAD 被退化 vs 原生支持为什么加一行__restrict__能让 matmul 快 40%LOAD 融合为什么 index 更新放在 MMAD 之后会拖慢流水线隐式 SYNC 打断。这些不是编译器 bug是编译器保守策略和硬件代际差异的合理约束。PTO-ISA 文档提供了每种指令在不同硬件上的退化路径——看一遍退化路径就知道怎么写 Ascend C kernel 能让三个代际的 NPU 都跑出峰值。
昇腾CANN pto-isa:虚拟指令集如何把 Ascend C 翻译成硬件指令
一个 Ascend C kernel 写好后要在昇腾 NPU 上执行需要经过两道编译第一道昇腾编译器把 Ascend C 翻译成 PTOParallel Tensor Orchestration虚拟指令第二道NPU 固件在运行时把 PTO 虚拟指令翻译成 AI Core 的具体硬件指令。PTO-ISA 定义的就是中间这一层的指令集规范。它不绑定具体的 NPU 硬件代际——Ascend 910 和 Ascend 950PR 都跑同一套 PTO 指令固件负责把 PTO 映射到各自的硬件实现。这是「写一次跨NPU代际运行」的关键。PTO 指令集的三类指令指令类数量功能对应硬件计算指令32MMAD、VMAC、VEXP、VLOG…Cube/Vector 单元数据搬运指令12LOAD、STORE、DMA_COPY、PREFETCHSDMA/L1 缓存控制指令8SYNC、BARRIER、LOOP、COND调度器一个简化版的 MatMul kernel 对应的 PTO 指令序列; PTO IR for simplified MatMul: C[M,N] A[M,K] * B[K,N] ; M256, N256, K256, tile64 LOOP k_outer, K/64: ; 外部循环K 维度分块 LOAD tile_a, A_ptr, {64, 64} ; 从 HBM 加载 A[64,64] 到 L1 LOAD tile_b, B_ptr, {64, 64} ; 从 HBM 加载 B[64,64] 到 L1 SYNC LOAD_DONE ; 等待数据到达 MMAD C_local, tile_a, tile_b ; Cube 单元计算 C A × B SYNC COMPUTE_DONE STORE C_ptr, C_local, {64, 64} ; 写回 HBM SYNC STORE_DONE ADD A_ptr, A_ptr, 64*64*2 ; 推进 A、B 的 HBM 指针 ADD B_ptr, B_ptr, 64*64*2 END_LOOP每条 PTO 指令被固件展开为 1-N 条硬件指令。MMAD在 Ascend 910 上被映射到 4 条硬指令四次 16×16 的矩阵乘-加在 Ascend 950PR 上可能映射到 1 条硬指令硬件支持 64×64 的 MMAD 了。PTO 的融合重写指令级优化编译器在生成 PTO 时会做指令级重写——把多个独立的 PTO 指令融合成一条复合 PTO 指令。这是算子性能的关键; 优化前独立的 LOAD MMAD STORE LOAD tile_a, A_ptr, {64, 64} LOAD tile_b, B_ptr, {64, 64} SYNC LOAD_DONE MMAD C_local, tile_a, tile_b SYNC COMPUTE_DONE STORE C_ptr, C_local, {64, 64} SYNC STORE_DONE ; 优化后融合成一条 FUSED_MMAD 指令 ; LOAD 用双缓冲加载下一块数据时 MMAD 在算当前块 ; STORE 也一样MMAD 在算时上一块的结果被 SDMA 搬走 FUSED_MMAD C_ptr, A_ptr, B_ptr, { tile{64, 64, 64}, double_buffertrue, async_storetrue }融合后的FUSED_MMAD一条指令覆盖了原来的 7 条指令。硬件上Cube 单元算 64×64×64 的 MMAD 时SDMA 同时在搬数据——算力 100% 跑满搬运也在同步走。延迟藏在双缓冲的 overlap 里。踩坑一LOAD 和 MMAD 的依赖分析失效PTO 编译器用数据依赖分析来判断哪些 LOAD 可以和 MMAD 融合。如果 Ascend C kernel 里的指针别名分析不够精确编译器保守地认为两个 LOAD 可能访问重叠的内存——不融合性能直接掉 40%。错误写法// Ascend C kernel两个指针被编译器认为是可能重叠的__aicore__voidkernel(GlobalTensorfloata,GlobalTensorfloatb){float*ptr1a.GetPtr();// 从参数 a 获取float*ptr2a.GetPtr();// 从同一个参数 a 获取// 编译器看到两个指针都指向 a → 可能重叠 → LOAD 不并行LocalTensorfloatt1(64);LocalTensorfloatt2(64);DataCopy(t1,ptr1,64);// LOAD 1DataCopy(t2,ptr2,64);// LOAD 2等 LOAD 1 完成保守是// PTO 生成LOAD t1 SYNC → LOAD t2 SYNC// 两条 LOAD 串行没有融合}正确写法用__restrict__告诉编译器两个指针不重叠。// 正确用 __restrict__ 声明指针不重叠__aicore__voidkernel(GlobalTensorfloat__restrict__ a,GlobalTensorfloat__restrict__ b){float*__restrict__ ptr1a.GetPtr();float*__restrict__ ptr2b.GetPtr();// 不同参数LocalTensorfloatt1(64);LocalTensorfloatt2(64);DataCopy(t1,ptr1,64);// LOAD 1DataCopy(t2,ptr2,64);// LOAD 2独立可并行// PTO 生成FUSED_LOAD t1, t2融合成一条// 两个 LOAD 并行启动SDMA 同时搬两路数据}性能差异两条 LOAD 独立并行 → 融合成 FUSED_LOAD数据搬运时间减半。在带宽敏感的 kernel 里如 matmul这是 30-40% 的性能差异。踩坑二STORE 的隐式同步点PTO 编译器在MMAD和STORE之间自动插入SYNC COMPUTE_DONE。但如果 kernel 在MMAD之后有别的不依赖结果的计算比如处理下一块的 index 更新这个隐式 SYNCC 是不必要的——它把流水线打断了。错误写法// Ascend C kernelMMAD 之后直接更新 index// PTO 编译器插入了隐式 SYNC COMPUTE_DONEMMAD(C_local,a_tile,b_tile);// ← 自动插入 SYNC// index 更新不依赖 C_local不需要等 MMAD 完成intnext_offsetcurrent_offsettile_size;// 不依赖 MMADDataCopy(C[offset],C_local,64);// 编译器又插入 SYNC等 index 更新完成正确写法先把不依赖结果的 index 更新提到 MMAD 之前。// 正确MMAD 之前算好所有 indexintnext_offsetcurrent_offsettile_size;// 不依赖上一个 MMADMMAD(C_local,a_tile,b_tile);// ← 编译器自动插入 SYNC现在只有一次不影响 indexDataCopy(C[current_offset],C_local,64);// 不需要再等 index根因PTO 编译器对MMAD后的第一个写操作包括整数变量赋值自动插SYNC COMPUTE_DONE——它保守地认为后续操作可能依赖前面的计算。但实际上 index 更新是纯整数计算完全不依赖浮点 MMAD 的结果。踩坑三虚指令的硬件退化路径PTO 的FUSED_MMAD在某些硬件代际上不被原生支持——固件会把它退化degrade成多条基本 PTO 指令。退化后的指令序列有额外的 L1 容量需求可能导致 L1 溢出。场景在 Ascend 910 上开发了用了FUSED_MMAD的 kernel。性能在 910 上很好。部署到 Ascend 950PR 时固件把FUSED_MMAD映射到一条硬件指令原生支持L1 里的中间数据排布和 910 不同。kernel 里硬编码的 L1 usage 假设被打乱了。正确做法不和底层硬件绑定——在 kernel 代码里用#ifdef或运行时查询来适配 L1 容量// 查询当前 NPU 的 L1 大小intl1_capacityGetChipL1CacheSize();// Ascend 910: 192KB// Ascend 950PR: 256KB// 按 L1 容量动态算 tile sizeintmax_tile(l1_capacity-reserve)/(3*sizeof(float));intMbmin(M,max_tile);intNbmin(N,max_tile);intKbmin(K,max_tile/2);PTO-ISA 不是面向应用开发者的接口——写 kernel 的人看不到 PTO 指令。但理解 PTO 的作用能解释为什么 kernel 性能在 910 和 950PR 上差一倍FUSED_MMAD 被退化 vs 原生支持为什么加一行__restrict__能让 matmul 快 40%LOAD 融合为什么 index 更新放在 MMAD 之后会拖慢流水线隐式 SYNC 打断。这些不是编译器 bug是编译器保守策略和硬件代际差异的合理约束。PTO-ISA 文档提供了每种指令在不同硬件上的退化路径——看一遍退化路径就知道怎么写 Ascend C kernel 能让三个代际的 NPU 都跑出峰值。