ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PyPTO Pro SIMD API:transpose 转置指令的完整用法与源码级解析

PyPTO Pro SIMD API:transpose 转置指令的完整用法与源码级解析
  • 人工智能
  • 编译器
  • 模型编译
  • 深度学习
  • 高性能计算
  • CANN
  • Ascend

【免费下载链接】pypto

PyPTO(发音: pai p-t-o):Parallel Tensor/Tile Operation编程范式。

项目地址:https://gitcode.com/cann/pypto
点击查看免费下载

PyPTO(PyPTO Pro,plDSL)面向 Ascend 950PR/950DT 系列提供的pl.transpose指令,可在片上 UB(Unified Buffer)内交换二维 Tile 的两个轴,直接完成矩阵转置,免去经 GM 中转的额外拷贝。本文基于仓库 API 文档 transpose.md,完整覆盖产品支持、参数约束、可运行调用示例与实测输出,并结合 语言层声明、IR 构建器、代码生成 与 单元测试、系统测试 逐层印证其实现细节,读完后可在 kernel 中正确使用、排错并理解其底层调用链。

产品支持情况

pl.transpose属于 PyPTO Pro SIMD(向量)API 中的 Tile 计算类指令,产品支持情况如下(来自 transpose.md):

产品系列支持情况
Ascend 950PR & 950DT 系列产品支持
Atlas A3 系列产品不支持
Atlas A2 系列产品不支持

因此在使用前需确认目标设备为 950 系列;仓库中对应的系统测试也使用@pytest.mark.soc("950")标记并做了设备检查(见 test_transpose_scalar_access.py 中_require_a5(device)的调用)。

功能说明与函数原型

pl.transpose交换二维 Tile 的两个轴,实现矩阵转置:若src的 shape 为[64, 128],则out的 shape 为[128, 64],且满足out[i, j] == src[j, i]。

函数原型:

pypto_pro.language.transpose(out: Tile, src: Tile) -> None

需要说明的是,这些pl.xxx声明并不在 Python 运行时真正执行。语言层声明文件 的模块 docstring 明确写道:这些声明的目的是让 IDE 跳转定义、拼写检查与类型检查可用;在 PyPTO kernel 中,AST parser 会在 Python 执行前拦截每一次pl.xxx调用并转入 IR 构建流程;在 kernel 之外直接调用声明函数会抛出RuntimeError。

transpose的声明本体位于 _api.py 的 B7 矩阵运算区块:

@_api_decl def transpose(out: Tile, src: Tile) -> None: """Transpose Tile by swapping the last two dimensions. Args: out: Destination Tile src: Source Tile """

从源码结构看,它被归入 "B7. Matrix ops"(矩阵运算)分组,与matmul、matmul_acc、matmul_mx等 Cube/UB 矩阵类指令相邻,体现了它作为"矩阵重排类"指令的定位。

参数说明与约束

参数输入/输出说明
out输出目的操作数,Tile 类型,存放转置结果。dtype 与src一致;shape 为src转置后的结果,如src为[64, 128]时out为[128, 64];不可与src为同一 Tile。
src输入源操作数,Tile 类型,二维 UB。dtype 支持DT_INT8、DT_UINT8、DT_INT16、DT_UINT16、DT_FP16、DT_BF16、DT_INT32、DT_UINT32、DT_FP32,不支持 64 bit 元素。源、目标主维长度乘元素字节数须 32 字节对齐。

上述约束并非只存在于文档中,IR 构建器_ir_transpose在解析阶段就会强制检查:

def _ir_transpose(out: Expr, src: Expr, *, span: Span | None = None) -> Expr: dt = getattr(out.type, "dtype", None) if dt is not None and dt.get_bit() not in (8, 16, 32): raise InvalidType(f"transpose: unsupported dtype {dt}, supported: b8/b16/b32") src_dt = getattr(src.type, "dtype", None) if src_dt is not None and dt is not None and src_dt.get_bit() != dt.get_bit(): raise InvalidType(f"transpose: dtype size mismatch between dst ({dt}) and src ({src_dt})") return _ir_core.create_op_call( block_ir_op("transpose"), [out, src], {}, span or _span(), )

由此可以确认两条与文档一致的硬性约束:

  1. 位宽限制:out的 dtype 必须是 8/16/32 bit,64 bit 元素(如 FP64/INT64)在解析期即被拒绝,报错信息为transpose: unsupported dtype ...;
  2. 源/目标位宽一致:src与out的 dtype 位宽必须相同,否则抛出dtype size mismatch错误。

另外,操作数角色在 操作流水线注册表 中登记为"transpose": ["W", "R"],即第一个参数out是写(Write)角色,第二个参数src是读(Read)角色。该表用于多核/自动互斥场景下推导 Tile 的读写角色并插入跨核同步,也解释了文档中"不可与src为同一 Tile"的原因——原地转置(同一 Tile 既是写又是读)不在支持范围内,必须准备独立的目的地 Tile(通常分配在不同的 UB 地址段)。

调用示例:完整的转置 kernel

文档给出的基本用法完整可运行。整体流程为:创建两个 UB Tile Group(源与目的使用不同地址、不同互斥 ID),进入向量 section,load从 GM 读入源 Tile,执行transpose,最后store回 GM:

import pypto_pro.language as pl @pl.jit(auto_mutex=True) def transpose_kernel( a: pl.Tensor[[64, 64], pl.DT_FP16], out: pl.Tensor[[64, 64], pl.DT_FP16], ): tt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec) tile_a = pl.make_tile_group(type=tt, addrs=0x0000, mutex_ids=[0]) tile_out = pl.make_tile_group(type=tt, addrs=0x4000, mutex_ids=[1]) with pl.section_vector(): cur_a = tile_a.current() cur_out = tile_out.current() pl.load(cur_a, a, [0, 0]) pl.transpose(cur_out, cur_a) pl.store(out, cur_out, [0, 0])

几个关键点的解读(结合 _api.py 中 load/store 声明):

  • pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec):声明一个位于 UB(Vec 内存空间)的 64×64 FP16 Tile 类型。pl.transpose要求操作数是"二维 UB",即target_memory为MemorySpace.Vec的 Tile。
  • pl.make_tile_group(type=tt, addrs=0x0000, mutex_ids=[0]):以 Tile Group(多缓冲区组)形式分配 UB 地址,源 Tile 在0x0000、目的 Tile 在0x4000(64×64×2B=8KB,地址不重叠),并分别绑定互斥 ID 0 和 1,配合@pl.jit(auto_mutex=True)让框架自动推导加锁/同步逻辑。
  • pl.load(cur_a, a, [0, 0]):load(dst_tile, src_tensor, /, offsets, *, order=None),按元素坐标从 GM Tensor 读入片上 Tile,offsets为每轴元素级偏移;示例中从(0, 0)开始读取整个 64×64 块。
  • pl.transpose(cur_out, cur_a):在 UB 内完成转置,无需 GM 往返。
  • pl.store(out, cur_out, [0, 0]):store(dst_tensor, src_tile, /, offsets, ...),将转置结果写回 GM。

文档同时给出了实测结果示例,输入是一个元素按行号*16 + 列偏移*0.25规律递增的 64×64 矩阵,输出矩阵各行变为输入矩阵的对应列:

输入数据a:[[1 1.25 1.5 1.75 2 2.25 2.5 2.75 ...], [17 17.25 17.5 17.75 18 18.25 18.5 18.75 ...], [33 33.25 33.5 33.75 34 34.25 34.5 34.75 ...], [49 49.25 49.5 49.75 50 50.25 50.5 50.75 ...], ...] 输出数据out:[[1 17 33 49 65 81 97 113 ...], [1.25 17.25 33.25 49.25 65.25 81.25 97.25 113.25 ...], [1.5 17.5 33.5 49.5 65.5 81.5 97.5 113.5 ...], [1.75 17.75 33.75 49.75 65.75 81.75 97.75 113.75 ...], ...]

可以看到第 0 行输出[1, 17, 33, 49, ...]正好是输入矩阵第 0 列的首元素序列,符合out[i, j] = src[j, i]的语义。该示例对应的可执行验证即 test_transpose_scalar_access.py 中的transpose_kernel与test_transpose用例:用torch.rand(64, 64, fp16)作为输入,运行后以torch.testing.assert_close(out, a.t().contiguous(), rtol=1e-2, atol=1e-2)断言结果与 PyTorch 的a.t()一致。

编译与执行流程:从pl.transpose到 TileOp 调用

结合仓库源码,一次pl.transpose调用会经历如下链路:

  1. AST 拦截与角色登记:parser 在 kernel 内拦截pl.transpose(out, src),查 _op_pipeline.py 的_BLOCK_OP_TILE_ROLES得到参数角色["W", "R"],供自动互斥(auto_mutex)推导同步关系。
  2. IR 构建与类型检查:分发到register_table中注册的"transpose": OpSpec(builder=_ir_transpose),由_ir_transpose完成 8/16/32 bit 位宽检查与源/目标 dtype 位宽一致性检查,然后通过_ir_core.create_op_call(block_ir_op("transpose"), [out, src], {}, span)生成 IR op 调用。单元测试test_manual_transpose验证了这一点:对 shape[16, 32]的src与[32, 16]的dst调用pl.transpose(dst, src)后,解析出的程序 IR 文本中包含block.transpose节点。
  3. 向量化代码生成:在 NPU codegen 阶段,codegen_vector.cpp读取 op 属性中的shape(transpose 的轴描述)并输出对应 TileOp 模板调用:
std::vector<int64_t> transposeAxis = AnyCast<std::vector<int64_t>>(opAttrs.at(OP_ATTR_PREFIX + "shape")); oss << tileOpName << "<" << (transposeAxis[0] + correctionAxis) << ", " << (transposeAxis[1] + correctionAxis)

从这段代码结构看,生成的 CCE 代码形如transpose<axis0, axis1><...>的 TileOp 模板实例化,转置轴由 IR 属性携带并加上维度校正量后输出。这印证了"交换二维 Tile 的两个轴"在底层对应一条带轴参数的向量 TileOp 指令。 4.运行时执行:kernel 由@pl.jit编译并在向量流水(V 流水)上执行 UB 内转置,load/store则经由 DMA(MTE)完成 GM 与 UB 之间的数据搬运,与转置指令本身解耦。

相关用法:与 load 的order转置搬运的区别

在使用pl.transpose时值得注意一个替代路径:数据搬运指令pl.load支持order参数——升序(如[0, 1])为不转置加载,降序(如[1, 0])则直接以转置方式加载(DN 排布):

pl.load(l1, x, [b, s0, h, 0, k0], order=[1, 3, 4]) # NZ [S_tile*G, D_tile] pl.load(l1, x, [b, s0, h, 0, k0], order=[4, 3, 1]) # ZN [D_tile, S_tile*G]

从源码注释看,load的order降序形式(DN layout)与pl.transpose的适用场景不同:前者在 GM→L1/UB 搬运阶段由 DMA 一次完成排布变化(对 matmul 的 ZN 操作数尤其高效,"same bytes, costs nothing"),后者是 UB 内两个已有 Tile 之间的计算转置。若转置只发生在"从 GM 取出时",优先考虑load/store的order降序写法;若转置发生在片上计算中途(如 attention 中 QK^T 结果的 ND/DN 排布互换),则使用pl.transpose。仓库中 test_matmul_softmax.py 即在 attention 流程中多次调用pl.transpose(cur_qk_dn, cur_qk_nd)等完成片上排布转换,属于典型用例。

验证方式与测试参考

仓库中与该指令相关的可查证测试包括:

类型路径验证内容
单元测试(IR)test_block_ops.pytest_manual_transpose:[16,32]→[32,16]转置后程序 IR 含block.transpose节点
系统测试(端到端)test_transpose_scalar_access.py64×64 FP16 转置 kernel,与torch的a.t()数值比对
前端组合测试test_abs_addc_and.pypl.transpose与 abs/addc/and 等指令的组合调用链
代码生成测试test_cce_tensor_order_variants.py含转置语义的 load/order 变体在 CCE 代码生成上的正确性

执行这些测试的前提是具备 950 系列设备(stest 通过@pytest.mark.soc("950")过滤),UT 层面的 IR 断言则不依赖硬件。

小结

  • pl.transpose(out, src)是 PyPTO Pro SIMD API 中面向 Ascend 950PR/950DT 的 UB 内二维 Tile 转置指令,交换两个轴,outshape 为src的转置结果,两者不可为同一 Tile;
  • 仅支持 8/16/32 bit dtype(DT_INT8/UINT8/INT16/UINT16/FP16/BF16/INT32/UINT32/FP32),源/目标位宽必须一致,且主维长度×元素字节数须 32 字节对齐;这些约束在_ir_transpose中于解析期强制执行;
  • 典型用法是load → transpose → store三步,用make_tile_group为源/目的 Tile 分配不同 UB 地址与互斥 ID;
  • 编译链路上,pl.transpose经角色表(["W", "R"])与 IR 注册表落到block.transposeop,最终由codegen_vector.cpp生成带轴参数的 TileOp 模板调用;
  • 若转置可合并进 GM 搬运阶段,可优先考虑pl.load/pl.store的order降序参数以避免额外的 UB 转置与地址开销。

参考文档入口:SIMD-API 总目录、转置与元素读写目录、transpose 指令文档。

  • 人工智能
  • 编译器
  • 模型编译
  • 深度学习
  • 高性能计算
  • CANN
  • Ascend

【免费下载链接】pypto

PyPTO(发音: pai p-t-o):Parallel Tensor/Tile Operation编程范式。

项目地址:https://gitcode.com/cann/pypto
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进