PyPTO-Tensor 让你「快速写出算子」,PyPTO Professional模式让你「进一步挖掘性能」。

1. 背景:我们为什么需要PyPTO Professional模式

先说一个几乎所有算子开发者都经历过的场景。

你接到一个融合算子需求,用PyPTO Tensor DSL 写了两百行,一天跑通,精度对齐,性能达到了0.6x的理论值,非常开心的搞定了训练和后训练。但是到了推理部署阶段,性能团队永远是希望尽可能的压榨硬件,拿到更优的性能,减少推理的成本,这就需要算子对硬件更极致的控制。

于是你开始「往下走」—— 手写指令级 Kernel。这一走,画风突变:

// 熟悉的味道:一半代码在算 offset,另一半在填指令参数
uint64_t src_offset = (b * n_head + h) * seq_len * head_dim
                    + (s_outer * TILE_S + s_inner) * head_dim + d_off;
uint16_t nBurst  = tile_rows;
uint16_t lenBurst = tile_cols * sizeof(half) / 32;
uint16_t srcGap  = (stride_row - tile_cols) * sizeof(half) / 32;
uint16_t dstGap  = 0;
copy_gm_to_cbuf(dst + dst_offset, src + src_offset, 0, nBurst, lenBurst, srcGap, dstGap, ...);

set_flag(PIPE_MTE2, PIPE_MTE1, EVENT_ID0);   // 这三行写错一个 event id
wait_flag(PIPE_MTE2, PIPE_MTE1, EVENT_ID0);  // 就是一个下午的 debug
// ... 还有 ping-pong buffer 的地址轮转、跨核 flag、preload 展开

这里有两个真实的痛点,它们是 PyPTO Professional 推出的理由:

痛点一:性能。 基于 Tensor 抽象的编程范式(PyPTO-Tensor)在表达力和开发效率上极好,但抽象层次越高,编译器需要替你做的决策就越多——多核切分、Buffer 复用、流水排布,任何一个决策不是你想要的,性能就大打折扣。同时MPMD的调度使用了AICPU,调度开销在小算子上不可忽略。而越来越多的客户场景(大模型的 FA、MLA、SFA 路由,以及各种奇形怪状的融合算子)明确提出需要更高的性能。到了极限的性能场景,「编译器帮我猜」或者「抽象的性能旋钮」不够用了,开发者必须能亲手按住每一块片上 Buffer、每一条流水。

痛点二:传统算子语言的「体感」。 能达到极致性能的路径一直存在——就是上面那段代码。但它的编码风格要求多维坐标要手工摊平成一维 offset,指令参数要按硬件手册逐个填,double buffer 要手工轮转地址,流水同步要手工 set_flag/wait_flag 配对,跨核流水还要手工展开 preload。这些工作没有创造性、极易出错、且与算法逻辑完全无关,却占掉了算子开发 70% 的时间和 90% 的 debug 精力。

结论很清楚:我们要的是在不交出硬件控制权的前提下,把那些机械、易错的活儿交给框架
这就是 PyPTO Professional。

2. PyPTO Professional模式 是什么

PyPTO Professional模式 是一门以 Python 为前端、构建在 PTO-ISA 高性能指令集之上的 DSL,与 PyPTO-Tensor 是同一个家族的两位成员:

PyPTO-Tensor PyPTO Professional
抽象载体 Tensor 图 Tile(二维数据块)+ RegTensor
谁做多核切分 框架 开发者(和算子强相关,通用算法性能有上限)
谁做 Buffer 复用 框架 开发者(和Tiling强绑定,通用算法性能有上限)
谁算 offset / 填指令参数 框架 框架
谁插核内同步 框架 框架(auto_mutex=True)
谁排核间流水 框架 框架(@pl.pipeline.stage)
定位 算法开发者快速实现 在高易用的前提下开放几乎全部硬件控制力

他两从架构上是共后端IR和PTO-ISA,前端表达上略有不同,架构图如下

PyPTO Professional 的设计哲学是立足易用性,并尽可能的提供更强的硬件控制力:

把「决定性能的决策」留给开发者,把「不决定性能但极易出错的苦力」交给框架。

多核怎么切、Tile 开多大、Buffer 摆在 UB 的哪个地址、流水开几级——这些直接决定你能不能摸到 0.9x+ 峰值,框架绝不替你猜。而 offset 换算、指令参数、event id 分配、同步配对、preload 展开——这些写对了性能不会更好、写错了直接精度出错或死锁,框架全包。

先看它长什么样。一个完整可运行的 Add 算子:

import pypto_pro.language as pl
import torch


@pl.jit
def add_kernel(
    x: pl.Tensor[[pl.DYNAMIC, pl.DYNAMIC], pl.DT_FP16],
    y: pl.Tensor[[pl.DYNAMIC, pl.DYNAMIC], pl.DT_FP16],
    z: pl.Tensor[[pl.DYNAMIC, pl.DYNAMIC], pl.DT_FP16],
):
    tile_type = pl.TileType(shape=[128, 128], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)

    a_db = pl.make_tile_group(type=tile_type, addrs=0x0000, mutex_ids=[0, 1]) # 地址手动指定, 使能double buffer
    b_db = pl.make_tile_group(type=tile_type, addrs=0x10000, mutex_ids=[2, 3])
    c_db = pl.make_tile_group(type=tile_type, addrs=0x20000, mutex_ids=[30, 31])

    with pl.section_vector():
        num_cores = pl.get_block_num()
        core_id = pl.get_block_idx()
        m_tile_num = x.shape[0] // 128
        n_tile_num = x.shape[1] // 128

        for i in pl.range(core_id, m_tile_num, num_cores):
            for j in pl.range(0, n_tile_num, 1):
                tile_a = a_db.next() # 自动选择下一块buffer,并插入对应的同步
                tile_b = b_db.next()
                tile_c = c_db.next()
                pl.load_tile(tile_a, x, [i, j]) # 根据坐标自动计算offset
                pl.load_tile(tile_b, y, [i, j])
                pl.add(tile_c, tile_a, tile_b)
                pl.store_tile(z, tile_c, [i, j])


device = "npu:0"
torch.npu.set_device(device)
m_size = 8192
n_size = 4096
num_cores = m_size // 128
torch.manual_seed(0)
dtype = torch.float16
# Host 侧就是普通 PyTorch
x = torch.rand([m_size, n_size], device=device, dtype=dtype)
y = torch.rand([m_size, n_size], device=device, dtype=dtype)
z = torch.empty([m_size, n_size], device=device, dtype=dtype)

add_kernel[None, num_cores](x, y, z) # 首次调用触发 JIT
torch.npu.synchronize()

没有 .cpp,没有 CMake,没有算子工程,没有 set_flag 一个 .py 文件里 Host 侧和 Device 侧代码并排放着,python add.py 直接跑。这是 PyPTO Professional 给人的第一印象。

3. 架构总览:从 Python 到二进制发生了什么

add_kernel(a, b, out) 这一行调用背后,是一条完整的编译流水线:

几个值得注意的设计点:

  • 前端是「解析」而不是「追踪」。 PyPTO Professional 真正解析你的 Python AST(if/elif/elsefor、函数调用、闭包变量都按 Python 语义处理),因此报错能精确到源码行列,而不是给你一堆运行时 trace 的天书。
  • Pipeline 变换在前端完成,且产物可读。 流水变换后的等价 Python 源码会被 dump 成 pipeline_generated.py 写进 build 目录——框架替你展开的 preload 和同步长什么样,你打开文件就能看,而不是只能靠猜。这一点对建立信任非常重要。
  • Cube 和 Vector 是两个独立 Program。 section_cube() / section_vector() 标记的代码段在 IR 层就被拆成两份,分别 codegen、分别编译,最后 fatobj 链接成一个 Kernel。这也是核间(Cube↔Vector)流水能被自动编排的结构基础。
  • 编译期特化取代 C++ 模板。 TilingKey 的每个取值组合会把字段折成 IR 常量,生成一份专属kernel.cpp。你不需要写 template<int TILING_KEY>,也不需要和模板实例化爆炸搏斗。

4. 一句话总结

PyPTO Professional 的赌注是:算子开发者应该把时间花在「切多大的块、开几级流水、Buffer 怎么摆」上,而不是花在「这个 offset 算对了吗、这个 event id 配对了吗」上。

前者是创造性的、决定性能上限的工作;后者是机械的、只决定你今晚能不能下班的工作。PyPTO Professional 把后者全部接管,同时一寸不让地把前者留给你——因为想摸到理论峰值 0.9x 以上,方向盘就必须在人手里。

如果你符合下面任意一条,那么欢迎使用PyPTO Professional,他会给你舒适的算子编程体验:

  • ✅ 手写过指令级 Kernel,被 set_flag/wait_flag 折磨过;
  • ✅ 被流水编排,核间同步插入折磨过(精度问题、卡死等);
  • ✅ 需要开发 Cube + Vector 融合算子(FA / MLA / SFA / 各种自定义融合);
  • ✅ 已有PyPTO-Tensor算子性能不满意,想要更细粒度的硬件控制;
  • ✅ 想要一条从「快速实验」直达「二进制发布」且不修改任何kernel源码的完整路径。
Logo

1331

更多推荐