PyPTO 逐模块实现指南:从 DESIGN 到可验证 Kernel 的完整开发流程
【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym
本文基于 module-development.md 展开,面向在 CANN / pypto-gym 仓库中使用 PyPTO 框架开发自定义算子的开发者与 Agent。文章系统讲解单模块与多模块两种实现路径、实现前的核对清单、验证与失败定位方法、
@pypto.frontend.jit的 Kernel 配置、循环与 Tile 设计、数据读取写回方式、动态长度与状态管理以及数据类型与精度策略。读完本文,你将掌握从读取 DESIGN.md 到产出通过detailed_tensor_compare精度验证的<op>_impl.py的完整工作流,并能避开本仓库实测中反复出现的反模式。
一、模块划分:单模块与多模块两条路径
PyPTO 算子的实现以 DESIGN.md 中的模块划分为基准。模块只用于组织开发与验证,并不要求每个模块都有独立的 JIT kernel——所有模块最终会整合进同一个生产级@pypto.frontend.jit入口。
单模块路径(L0)
当设计判定module_count == 1时(DESIGN.md §0.3,复杂度预算total_complexity < 1.3),直接产出单个<op>_impl.py,用完整 golden 做一次端到端验证即可。此路径不涉及分阶段文件链。编排规则 中明确:L0 路径下 "staged chain is skipped entirely — single<op>_impl.pyis the deliverable"。
多模块路径(L1)
module_count >= 2时,每次只实现一个语义模块,验证通过后再实现下一个。编排器使用累计实现文件推进:
<op>_module1_impl.py # 仅 M1 的真实 PyPTO 逻辑 <op>_module12_impl.py # M1 + M2 <op>_module123_impl.py # M1 + M2 + M3 ... <op>_module1…N_impl.py # 全模块 = 端到端 kernel命名与调度约定遵循编排规则:后缀数字是累计的(1、12、123…),每个文件都要通过detailed_tensor_compare对所有输出的比较,才允许进入下一个阶段文件。每个文件的命名约定为<op>_module<k>_wrapper(lint 规则 OL08 强制)。
多模块路径的三条架构禁令(见编排规则):
- Prohibition A:禁止一次性把所有模块拼成一个 JIT(仅 L0 允许 one-shot);
- Prohibition B:禁止用 Python host
for循环模拟 kernel 的分块迭代逻辑; - Prohibition C:禁止"每模块一个生产 JIT"的架构——模块是语义块,不是独立 JIT 入口。
验证中间模块时,可以导出中间结果,或直接使用 golden 提供的边界张量,但被验证的计算必须与最终实现一致——不能为了通过边界检查而临时改变算法。
二、实现准备:动笔前必须核对的清单
开始写代码前,逐项核对以下内容(源自module-development.md):
| 核对项 | 说明 |
|---|---|
| API 签名 | 与 DESIGN.md /module_interfaces.yaml中的模块契约一致 |
| 输入/输出 shape、dtype | 与 golden 契约一致,动态轴显式标注 |
| 切片秩与偏移 | pypto.view的 offsets / valid_shape 落在原 Tensor 范围内 |
| 尾块有效范围 | 动态长度下最后一块可能不满 tile,必须用valid_shape标记 |
| 循环状态与 tile 配置 | 状态变量作用域、TileShape 是否在对应操作前设置 |
要点:
- 采用 DESIGN.md 中确定的Tile、显式配置和类型转换位置。设计文档中的候选值仅供后续比较,不代表已验证可用;关键值未确定时应先补全设计,而不是猜。
- 调整参数时遵守记录的限制(见执行约束),并重新验证受影响的结果。
- 使用实现模板作为强制骨架,模板覆盖 Layer G–K(G cache/bridge、H pypto_* 子内核、I kernel 实现、J JIT 入口、K host wrapper),并内嵌了 OL45 / OL46 / OL47 反模式注释;结构规范详见 pypto-kernel-design-format.md 的 Layer A–L 定义。
- 遇到具体问题再读调试手册,而不是盲目试错。
Golden 覆盖检查:实现必须覆盖 golden 中属于本模块的每一个计算,包括掩码、缩放、cast、状态更新和全部输出。编排规则第 19 条要求逐项对照 golden 头部注释中的函数清单(Golden function inventory),在 MEMORY.md 中给每个数学操作标记 ✅(对应 pypto 调用 + 行号)或 ❌(缺失);存在 ❌ 时不允许继续推进模块。
三、验证与失败定位:以证据驱动,不做口头断言
验证入口
将实现文件路径和模块输出交给验证 Agent:先检查编译与 shape/dtype,再用detailed_tensor_compare与 golden 做数值比较。编译通过或结构检查通过都不能代替精度比较。
detailed_tensor_compare位于 detailed_tensor_compare.py,核心行为:
- 支持
tuple / list / dict嵌套输出的递归比较,对每个叶子张量都生成报告(tensor_leaf_pairs会逐层展开); - shape 不匹配直接判
all_close=False; - 输出
total_elements、out_of_tolerance_count、out_of_tolerance_ratio、max_diff、mean_diff、std_diff以及超出容差元素的前 20 个明细(索引、两侧值、绝对/相对差); - 容差默认
rtol=1e-3, atol=1e-3,可通过options或旧式位置参数调整; - 对
inf处理有专门语义:两侧同为同符号inf视为匹配,否则计入超差。
调用方式(由验证 runner 统一引入,无需手动配置 PYTHONPATH):
from detailed_tensor_compare import detailed_tensor_compare result = detailed_tensor_compare(kernel_out, golden_out, tensor_name="output") assert result["all_close"], "Mismatch in output"编排规则强制:每个阶段文件、每个输出(tuple/list/dict 的每个叶子)都必须比较,禁止只挑一个输出验证。
设计检查 vs 实现测试
DESIGN.md 中的设计检查结论只说明"已核对的文档、推导或接口",不能直接当作实现测试结果。必须按其验证方案执行,并记录实际证据(编排规则第 4 条:禁止用口头 "should pass" 代替真实运行)。
失败定位策略
- 验证通过后记录该模块输出的比较结果,继续下一个模块;
- 后续失败时,先找出最早输出错误的模块,检查其输入输出及中间值,必要时二分定位;
- 不要因为后续模块失败就随意修改已验证的模块;确需修改时,重新验证该模块及所有依赖它的结果。
二分定位可参考上板二分定位法:在 kernel 函数中把检查点 tensor 作为输入参数(支持原地修改),与 golden 对应位置的中间结果比较,从单个关键计算点开始逐步二分,直到定位首个出错的 op。检查点必须与 golden 的 shape、dtype、数量、顺序完全一致;循环内的变量可在循环外创建大 tensor,循环内用view/assemble赋值。
已知问题排查路径:通用调试、精度调试、中间值比较。每个调整必须对应具体原因,不能盲目叠加精度变通参数。
收尾
全部模块完成后,验证组合及所有最终输出,并确认临时输入或检查点没有进入生产计算路径(cleanup 调度会整理出独立的<op>_impl.py)。
四、Kernel 配置与计算函数
JIT 入口与 helper 拆分
JIT 入口统一使用@pypto.frontend.jit。计算较复杂或需要复用时,把 PyPTO 计算放入普通 helper,由 JIT 入口调用:
def compute(x, output): # 根据设计完成切分、加载、计算和写回。 ... @pypto.frontend.jit def kernel(x, output): compute(x, output)注意:helper 仍在构图环境中执行,不能把它当作独立的 CPU 数值函数。另外存在一个已知陷阱——若 helper 体内使用pypto.is_loop_begin(idx)/pypto.is_loop_end(idx),parser 会在编译期抛F00002, ValueError: Not concrete value且无源行信息。两种规避方式(见 impl_template.py.tmpl 与 SKILL.md 注意点 19):
- 推荐:把整个 body 直接 inline 进
@pypto.frontend.jit入口; - 替代:给该 helper 加
@pypto.frontend.function(仅支持 tensor 参数,非 tensor 参数当前不支持)。
pass_options 与 runtime_options
根据具体实现选择,不要照搬固定取值:
| 参数 | 用途 |
|---|---|
cube_l1_reuse_setting | 配置 Cube L1 复用 |
vec_nbuffer_setting、cube_nbuffer_setting | 配置缓冲副本数,需计入资源预算(涉及 UB 容量估算) |
stitch_function_max_num | 配置子图合并数量 |
device_sched_mode | 选择目标设备支持的调度方式 |
生产级用法(见 SKILL.md)还包括:
ready_on_host_tensors: ["cu_seqlens_q", ...](runtime_options):标量索引 tensor 驻留 host,避免 device→host 同步读取;pypto.experimental.set_operation_options(combine_axis=True):轴合并优化;- 上述 setting 的 per-op keyed 形式(如
{0: 8, 1: 1}/{-2: 1, 0: 8, 1: 2})按算子索引精调,优于全局{-1: x}; sg_set_scope=N包裹 reduce / softmax 链,融合为一次向量超算子,中间量流式通过、不逐个物化 UB。
语义标签
需要识别不同计算段时,可用pypto.set_semantic_label("scores")等有含义的名称。标签只用于定位代码段,不替代正确的数据依赖和 Tile 配置——不要让标签"看起来对"就忽略了真正的依赖关系。
五、循环与 Tile 设计
循环选择与返回值遵循循环约束:
| 需要 | 表达方式 | 返回值 |
|---|---|---|
| 按运行时边界重复执行 | pypto.loop | 符号索引 |
| 按指定因子展开计算 | pypto.loop_unroll | (索引, 展开因子) |
| 少量、编译期确定的迭代 | Pythonrange | 具体整数索引 |
关键约束(C-LOOP-* 规则节选):
- C-LOOP-02:符号循环边界必须用
pypto.loop,不能交给 Pythonrange(Python 无法把运行时符号转成整数); - C-LOOP-03:符号循环索引不能用作 Python 容器下标或 Python 布尔条件;
- C-LOOP-04:循环内读取循环前尚未提交的计算结果时核对提交依赖——
submit_before_loop=True用于进入循环前提交已有任务,不代表每轮迭代之间同步; - C-LOOP-05:展开因子从单一候选开始;
loop_unroll解包索引与展开因子两个返回值; - C-LOOP-06:跨迭代保留的状态在合适的外层作用域建立,明确初始化、更新及最终写回;
- C-LOOP-08:
loop_unroll覆盖被展开轴的数据访问必须以解包的展开因子确定尺寸(如pypto.view(x, [uf, ...], [i, ...])),不得写死常量尺寸。
需要按展开因子调整每次处理的数据量时,使用loop_unroll返回的因子。注意:多个展开候选会增加编译路径——实现阶段unroll_list只能含单一值(默认[1]),照搬 DESIGN.md 「范式与设计决策」 中的选定值,多值展开(如{2, 1})仅允许在性能调优阶段使用(lint OL56 强制 FAIL)。
Tile 约束详见Tiling 约束:
- C-TILE-02:尾轴 32B 对齐——
tile_last × dtype_bytes % 32 == 0,FP32 为 8 个元素,FP16/BF16 为 16 个元素; - C-TILE-05:矩阵乘 m/k/n 各轴使用
[L0, L1]配置,满足0 < L0 <= L1且L1 % L0 == 0;Vector 配置不能替代 Cube 配置; - C-TILE-06:TileShape 必须在对应操作前设置,参数必须是编译期整数或可解析为整数的常量,不能来自运行时 shape、kernel 参数或 SymbolicScalar(lint OL48 强制);
- C-TILE-08:计算形状变化时重新核对当前 Vector TileShape。
Tile 作用域遵循 per-stage 原则(pypto-kernel-design-format.md §11c):多个pypto_*子内核需要不同 tile 时,把set_cube_tile_shapes/set_vec_tile_shapes放到各自子内核内部,而不是在_kernel_impl顶部设一个全局值(OL47 会提示)。
六、数据读取与写回
| 场景 | 表达方式 | 注意事项 |
|---|---|---|
| 连续 tile | pypto.view(tensor, shape, offsets, valid_shape=...) | 显式指定有效范围时使用 view |
| 简单连续切片 | tensor[start:stop, :] | 核对目标版本的索引及有效形状支持 |
| 分页 KV | view、索引及结果拼装 | 参考分页加载模式 |
| 稀疏索引读取 | pypto.index_select | 核对索引轴、类型(DT_INT32/DT_INT64)和边界 |
| 按模式选择元素 | pypto.gathermask | 例如 RoPE 奇偶位置,核对 mode 的含义 |
| 输出 tile | pypto.assemble(tile, offsets, output)或切片赋值 | 核对秩、偏移和输出有效区域 |
| Cache 更新 | pypto.scatter_update | 明确索引和重复写入的行为 |
几个源码级要点:
pypto.view参数类型(execution-constraints.md §5.2):shape必须全是 Python int(不接受 SymbolicScalar);offsets接受 SymbolicScalar;valid_shape接受 SymbolicScalar,用于尾块有效数据标记。Python[]切片内部会对 index 做int()转换,因此也不能用 SymbolicScalar 做切片索引。- 分页 KV 拼装(AT-17):在 loop 外分配拼装缓冲区,loop 内按
block_table逐块 view 拷贝(零搬运),尾块用valid_shape标记实际长度。禁止替换为gather_in_l1/gather_in_ub——后者是显式搬运指令,机制冲突且引入真实开销。 assemble写回:pypto.assemble(tile, offsets, output)没有返回值,直接修改out;offsets必须小于out.shape。同一 Tensor 在同一图里既被view读取、又被assemble写回会形成图成环报错(见执行约束 §4.9)。- 输出写回三选一(OL02):
out[:] = ...、out.move(...)、pypto.assemble(..., out);out = ...只会绑定局部变量,不会修改出参。 scatter_update不支持 broadcast,dim保持默认-2。
七、动态长度与状态位置
动态长度计算
length = tensor.shape[axis] tile_count = (length + tile_size - 1) // tile_size valid_len = (length - offset).min(tile_size) # 需要将复杂符号表达式标记为中间变量时,单独调用: seq_len = cu_seqlens[batch + 1] - cu_seqlens[batch] seq_len.as_variable()as_variable()原地修改符号对象、返回 None。它不负责分配 Tensor,也不自动建立循环状态——只是把运行时标量显式标记为变量,便于后续引用。
注意valid_len中的.min():在 kernel 内对 SymbolicScalar 使用s.min(other),对 Tensor 逐元素使用pypto.minimum;Python 原生min/max只能用于宿主侧逻辑(执行约束 §4.5)。
状态变量的作用域
| 状态 | 典型位置 | 原因 |
|---|---|---|
| Online Softmax 累积器 | Q tile 循环内、KV tile 循环外 | 每个 Q tile 独立累积 |
| 递推状态 | Batch 循环内、序列循环外 | 每个序列保持独立状态 |
| 单轮临时张量 | 当前循环体内 | 无跨迭代依赖 |
需要全零状态时使用目标 API 支持的分配和填充方式(如pypto.full,注意fill_value与dtype必须一致,动态图尾块无法自动推导有效范围时必须显式传valid_shape)。必须在正确作用域初始化,避免每轮重置。
循环内累加的标准模式(执行约束 §5.4):
acc = pypto.tensor([TILE, D], pypto.DT_FP32, "acc") for idx in pypto.loop(n, name="LOOP", idx_name="idx", unroll_list=[1]): # 实现阶段单一值 tile = compute_something(...) if pypto.is_loop_begin(idx): acc[:] = tile # 首次迭代:初始化 else: acc[:] = acc + tile # 后续迭代:累加 if pypto.is_loop_end(idx): result = pypto.cast(acc, pypto.DT_BF16) pypto.assemble(result, [offset, 0], output) # 最后一次:写回注意pypto.tensor()创建的是未初始化随机值,必须在is_loop_begin分支中初始化(实现阶段高频陷阱 #1:用pypto.full在循环前物化初始化会触发 F00003 对齐错误;正确做法是pypto.tensor纯声明 +is_loop_begin内 shape-matched 赋值)。
多动态轴:2D reshape + 嵌套 loop + concrete tile
当算子有 2 个及以上动态轴(如 Batch + SeqLen)时,不能直接在高维 tensor 上调受限 API(matmul 编译期需要 concrete shape,DYN 维度在编译期表现为 -1),必须采用:
4D [B, N, S, D] ↓ 在 Python wrapper 层做 reshape 2D [B*N*S, D] ↓ 进入 kernel ↓ pypto.loop(b) → pypto.loop(N) → pypto.loop(s_tiles) ↓ pypto.view([S_TILE, D], [symbolic_offset, 0], valid_shape=[actual_s, D]) 2D tile [S_TILE, D] ← shape 全是 concrete int ↓ matmul / elementwise / sum ↓ pypto.assemble(result, [symbolic_offset, 0], output_2d)关键点:shape 全 concrete、动态性只进入 offset 和 loop bound、valid_shape 处理尾块、2D matmul 编译期完全确定。此外,梯度算子多输出在不同维度累加时(如 dQ 沿 S2 累加、dK/dV 沿 S1 累加),使用两趟分离计算,避免跨 loop 的读写依赖(无需submit_before_loop=True,代价是中间结果重复计算一次)。
八、数据类型与精度
在具体计算中标明输入、累加、转换和输出四种类型:
- 常见的浮点归约采用FP32 累加后转回输出类型(API 约束 C-API-05:精度敏感的归约和跨循环累加优先使用 FP32,转换位置与参考计算的数值要求一致);
- INT8 矩阵乘可产生 INT32 累加结果再反量化;
- FP8 的缩放及转换位置取决于所选 API,不能套用统一路径(不同版本的 FP8 matmul API 对 scale 的处理方式不同,必须查对应 API 文档)。
除法可按目标 API 选择pypto.PrecisionType.HIGH_PRECISION或INTRINSIC,并与 golden 比较误差后决定。精度规则见 API 约束,量化计算见对应的 AT 卡片(patterns/atoms)。
其他 dtype 相关约束(执行约束):
matmul:显式给out_dtype;调用前必须设置set_cube_tile_shapes(...);3D/4D 场景还要设置set_vec_tile_shapes(...);cast:显式暴露CastMode和SaturationMode;浮点转整数时satmode=ON/OFF会直接改变溢出后的结果值;sum只支持DT_FP32,需要提高累加精度时显式转换为 FP32;- 标量参与计算且 dtype 不能依赖隐式映射时,使用
pypto.Element(dtype, value)(构造顺序固定)。
九、总结:逐模块开发的纪律
逐模块开发的核心纪律可以浓缩为四点:
- 一次一个模块:多模块路径下,任一时刻只有一个语义模块的 PyPTO 逻辑处于"未冻结"状态,后续模块用 stub 或 golden 边界张量占位,并在代码中显式注释
# STUB: until Mk verified; golden-fed tensor; - 证据驱动推进:每个模块的边界验证都必须有
detailed_tensor_compare的实际运行结果,并记录到 MEMORY.md 的 Per-module verification log;lint 失败不得判完成; - 失败先定位再修改:从最早出错的模块开始二分定位,不因后续失败随意回改已验证模块;
- 收尾清理:全部模块通过后,确认临时输入/检查点没有进入生产计算路径,再整理出独立的
<op>_impl.py与README.md。
遵循以上流程,配合实现模板、执行约束与调试手册,即可在 CANN / pypto-gym 仓库中稳定地完成从设计到可验证 Kernel 的算子开发闭环。
【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考