PyPTO 逐模块实现指南:从 DESIGN 到可验证 Kernel 的完整开发流程
2026/9/20 4:52:57 网站建设 项目流程

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 hostfor循环模拟 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_elementsout_of_tolerance_countout_of_tolerance_ratiomax_diffmean_diffstd_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" 代替真实运行)。

失败定位策略

  1. 验证通过后记录该模块输出的比较结果,继续下一个模块;
  2. 后续失败时,先找出最早输出错误的模块,检查其输入输出及中间值,必要时二分定位;
  3. 不要因为后续模块失败就随意修改已验证的模块;确需修改时,重新验证该模块及所有依赖它的结果。

二分定位可参考上板二分定位法:在 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):

  1. 推荐:把整个 body 直接 inline 进@pypto.frontend.jit入口;
  2. 替代:给该 helper 加@pypto.frontend.function(仅支持 tensor 参数,非 tensor 参数当前不支持)。

pass_options 与 runtime_options

根据具体实现选择,不要照搬固定取值

参数用途
cube_l1_reuse_setting配置 Cube L1 复用
vec_nbuffer_settingcube_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-08loop_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 <= L1L1 % 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 会提示)。

六、数据读取与写回

场景表达方式注意事项
连续 tilepypto.view(tensor, shape, offsets, valid_shape=...)显式指定有效范围时使用 view
简单连续切片tensor[start:stop, :]核对目标版本的索引及有效形状支持
分页 KVview、索引及结果拼装参考分页加载模式
稀疏索引读取pypto.index_select核对索引轴、类型(DT_INT32/DT_INT64)和边界
按模式选择元素pypto.gathermask例如 RoPE 奇偶位置,核对 mode 的含义
输出 tilepypto.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)没有返回值,直接修改outoffsets必须小于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_valuedtype必须一致,动态图尾块无法自动推导有效范围时必须显式传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 boundvalid_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_PRECISIONINTRINSIC,并与 golden 比较误差后决定。精度规则见 API 约束,量化计算见对应的 AT 卡片(patterns/atoms)。

其他 dtype 相关约束(执行约束):

  • matmul:显式给out_dtype;调用前必须设置set_cube_tile_shapes(...);3D/4D 场景还要设置set_vec_tile_shapes(...)
  • cast:显式暴露CastModeSaturationMode;浮点转整数时satmode=ON/OFF会直接改变溢出后的结果值;
  • sum只支持DT_FP32,需要提高累加精度时显式转换为 FP32;
  • 标量参与计算且 dtype 不能依赖隐式映射时,使用pypto.Element(dtype, value)(构造顺序固定)。

九、总结:逐模块开发的纪律

逐模块开发的核心纪律可以浓缩为四点:

  1. 一次一个模块:多模块路径下,任一时刻只有一个语义模块的 PyPTO 逻辑处于"未冻结"状态,后续模块用 stub 或 golden 边界张量占位,并在代码中显式注释# STUB: until Mk verified; golden-fed tensor
  2. 证据驱动推进:每个模块的边界验证都必须有detailed_tensor_compare的实际运行结果,并记录到 MEMORY.md 的 Per-module verification log;lint 失败不得判完成;
  3. 失败先定位再修改:从最早出错的模块开始二分定位,不因后续失败随意回改已验证模块;
  4. 收尾清理:全部模块通过后,确认临时输入/检查点没有进入生产计算路径,再整理出独立的<op>_impl.pyREADME.md

遵循以上流程,配合实现模板、执行约束与调试手册,即可在 CANN / pypto-gym 仓库中稳定地完成从设计到可验证 Kernel 的算子开发闭环。

【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询