news 2026/9/19 2:28:43

PyPTO-Gym 算子设计 R0 阶段:Module 划分方法论与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyPTO-Gym 算子设计 R0 阶段:Module 划分方法论与实战指南

PyPTO-Gym 算子设计 R0 阶段:Module 划分方法论与实战指南

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

Module 划分是 PyPTO-Pro 算子 tile 级方案设计(pypto-pro-op-design)中 R0 轮的核心任务:在动手规划 Tile、循环与片上布局之前,先把整体计算流拆成逻辑清晰、可独立验证的计算模块。本文以 module_partitioning.md 为骨架,结合 PyPTO-Gym 仓库中的设计流程、脚本与模板,系统讲解如何根据数据依赖Section 边界完成 Module 划分,并产出可供下游 R1–R8 轮直接消费的 Module 契约。读完本文,你将掌握稳定 Softmax、Online Softmax、Cube+Vector 组合算子等典型场景的 Module 切分方法,并能正确填写 R0 的 Module 列表与module_interfaces.yaml

Module 是什么:逻辑划分,不是语法结构

Module 是设计文档中对计算流程的逻辑划分,不是 PyPTO Pro 的语法结构。换句话说,Module 只存在于 DESIGN.md 与module_interfaces.yaml这类设计产物中,kernel 源码里并不存在名为 "Module" 的代码块。这一点决定了划分时的两个基本视角:

  • 数据视角:计算流中哪些步骤之间存在"必须先完成 A 才能开始 B"的依赖关系;
  • 资源视角:相邻步骤合并在同一个 Module 后,UB 等片上空间能否放下同时存活的数据。

Module 划分主要看数据依赖Section 边界,同时需要评估片上资源是否足够。整个 R0 轮在 pypto-pro-op-design 的九轮迭代设计(R0–R8)中承担"分解计算流"的职责,其完整流程见 SKILL.md 的「R0:Module 划分」一节;而 R0 的产出将写入 design-template.md 的 §0「Module 划分(R0 输出)」以及机器可读的module_interfaces.yaml

Module 划分按两步进行:

  1. 根据数据依赖找出必须分开的计算步骤;
  2. 根据API 和数据通路判断 Section 边界:Cube 和 Vector 计算属于不同 Section,也要拆成不同 Module。

下面分别展开这两步。

第一步:根据数据依赖确定边界

对于相邻的两步计算 A 和 B,是否拆成两个 Module,可以按以下常见情况判断:

情形判断理由
B 必须等 A 遍历完完整的归约轴(或其他完整数据范围),并基于 A 的最终结果开始一次新的遍历或独立计算阶段通常划分为两个 ModuleB 的输入是 A 的全局结果,二者无法在同一轮循环内完成
A 处理完当前 Tile 后,B 能在同一层循环内立即消费该 Tile,并且两步属于同一个 Section通常放在同一个 Module数据可在循环内直接接力,无需跨遍历保存状态
A 和 B 之间只有少量同域计算,没有插入其他 Section 或独立计算阶段可以放在同一个 Module依赖紧、边界不明显,拆分开反而增加设计复杂度
B 只是紧接着对 A 得到的归约结果做汇总或简单处理,不需要重新遍历完整数据范围可以继续放在 A 所在的 ModuleB 不依赖对完整数据范围的重遍历

需要特别强调的是,这些是常用判断方式,不是硬性规则。有两个容易误解的点:

  • A 的结果是否需要保存,不能单独决定 Module 边界。同一 Module 内也可能暂存中间结果(例如跨 Tile 循环的累加状态),不同 Module 之间也可能通过片上空间直接传递数据,并不一定落 GM。
  • 即使 A 和 B 可以连续执行,如果合并后 UB 等片上空间放不下同时存活的数据,也需要拆分 Module。资源约束可以反向制造边界——这是"也需要注意片上资源是否足够"的落点。

案例一:沿 N 轴分块的稳定 Softmax(三遍遍历)

以沿 N 轴分块的稳定 Softmax 为例。常规实现需要三次完整遍历,后两次遍历都依赖前一次得到的最终结果,因此通常按三次遍历划分三个 Module:

Module 1:遍历全部 N Tile,得到每行的 m = max(x) Module 2:重新遍历全部 N Tile,得到每行的 s = sum(exp(x - m)) Module 3:重新遍历全部 N Tile,写出 y = exp(x - m) / s

对同一个行块,前一个 Module 遍历完全部 N Tile 后,下一个 Module 才能开始。这里的边界来自"基于最终结果重新遍历 N 轴"这一数据依赖,不是因为ms需要保存——ms作为跨 Module 的归约状态,即使在同一个 Module 内部也需要持久化,可见"是否需要保存"与"是否拆分 Module"是两个维度。

该三遍结构与 api_mapping_and_tile_planning.md 中给出的稳定 Softmax 数据依赖图一致:x → reduce_max(N) → max,随后subtract → exp → reduce_sum(N) → sum,最后exp ÷ divide → out。其中maxsum的 shape 均为[M, 1],后续计算需要沿 N 轴广播。

案例二:Online Softmax(两遍遍历)

采用 Online Softmax 时,可以在一次遍历中同时更新最大值和指数和。处理完前 k 个 N Tile 后,每行维护两个跨 Tile 状态:

  • m:当前最大值;
  • s:以当前m为基准的指数和,即sum(exp(x - m))

每次合入一个新 Tile 的更新公式为:

m_new = max(m, tile_max) s_new = s * exp(m - m_new) + sum(exp(tile - m_new))

由于最终的ms到遍历结束时才能确定,写输出时通常要重新读取各 Tile,因此典型的 Module 划分是两遍:

Module 1:在线遍历全部 N Tile,得到最终 m 和 s Module 2:重新遍历全部 N Tile,写出 exp(x - m) / s

这里有一个重要的边界细节:如果后续只对最终的ms做一次汇总或简单运算,不再重新遍历 N 轴,这部分可以留在 Module 1 中。这与"根据数据依赖确定边界"表格中第四条判断(B 只是紧接着对 A 的归约结果做汇总,可留在同一 Module)完全对应。

关于 Online Softmax 的完整递推与尾块处理,仓库中的模式文档 online-softmax-tail.md 给出了更完整的公式(含输出累加量o)与数值安全要求:状态mlo的累加应保持在 FP32,尾块 mask 必须在row_max之前生效("padding 零对 max 不是恒等元"),并且 PyPTO-Pro 中模块级float("inf")无法编译(会渲染成未声明的 C++ 标识符inff),应使用有限哨兵值NEG_LARGE = -1.0e30

案例三:Attention 的在线归一化——不能直接套用 Softmax 公式

Attention 的在线归一化与 Online Softmax 的关键区别在于:除了ms,它还要维护输出累加量m更新时,已有的输出累加量也要按新的m缩放,再合入当前 Tile。因此:

Attention 应按完整的在线更新公式划分 Module,不能直接套用上面的 Softmax 公式。

完整的 Attention 在线更新递推(见 online-softmax-tail.md 的 Complete recurrence)为:

m_chunk = row_max(s) m_new = max(m_old, m_chunk) alpha = exp((m_old - m_new) * scale) p = exp((s - m_new) * scale) l_new = l_old * alpha + row_sum(p) o_new = o_old * alpha + p @ v_chunk

最终y = o_new / l_new。其中分母更新不可分割:只乘alpha不加row_sum(p)是错误的。这条模式文档同时提示:QK/PV 交接与 cross-core 同步属于目标版本相关的内容,应从官方代码推导,而不是从递推公式照搬——这与 Module 划分后由 R6 轮(跨核同步)承接的工作边界一致。

第二步:根据 Section 边界划分

数据依赖分析完成后,再根据API 使用的执行域、内存空间和硬件通路判断相邻步骤之间是否存在 Section 边界。Section 是 PyPTO Pro 中按执行域组织的代码结构(pl.section_cube()/pl.section_vector()),不同 Section 使用不同的硬件流水,因此跨 Section 的数据交接天然形成 Module 边界。

各数据通路对应的 Section 归属如下:

计算/搬运路径所属 Section说明
L1/L0A/L0B/L0C 上的矩阵路径Cube Section包括 GM→L1、L1→L0A/L0B、矩阵乘和 L0C 写回
Vec(UB)上的数据搬运和向量计算Vector Section使用 VF 时,外层 Kernel 负责 GM 与 UB 之间的搬运,@pl.vector_function负责 UB 与寄存器之间的数据交换和寄存器计算
pl.quantpl.dequantVector Section走 V 流水,输入、scale、offset 和输出均为 Vec Tile
pl.move/pl.store对 Acc 结果做随路量化或反量化Cube Section操作走 FIX 流水;per-channel 参数使用MemorySpace.ScalingTile 时,参数先从 Mat(L1)搬到 Scaling,再由 FIX 使用
UB→L1、L0C→UB 等跨执行域搬运按当前move接口文档选择数据通路以目标版本 API 文档为准

关于量化参数缓冲有一个易踩的坑:Scaling是量化参数使用的片上缓冲区,不是独立的 Section。不能看到MemorySpace.Scaling就新建 Module;仍要根据使用它的 API 和数据通路判断属于 Cube 还是 Vector。同理,pl.quant/pl.dequant因为走 V 流水放在 Vector Section,而pl.move/pl.store对 Acc 的随路量化走 FIX 流水放在 Cube Section——同样是量化,归属完全不同。

一个 Module 只能属于一个 Section。相邻步骤分别属于 Cube 和 Vector 时,在两者之间划分 Module。R0 只记录每个 Module 属于 Cube 还是 Vector;具体使用几个 Section 代码块、多个同域 Module 是否放在同一个 Section,以及循环相对 Section 的位置,在 R4(循环与 Section 结构)确定。这符合 loop_design.md 的约定:R0 已经记录每个 Module 属于 Cube 还是 Vector,R4 据此确定源码中的 Section 结构,相邻的同域 Module 可以顺序写在同一个 Section 中。

案例四:Cube 与 Vector 组合算子

Matmul 后接 Vector 后处理是融合算子的典型形态,Section 边界同时形成 Module 边界:

Cube Module:A × B → intermediate ↓ Vector Module:intermediate → 激活、归约或归一化 → output

两个 Module 可以由同一次 Kernel 启动完成。中间数据可以写到 GM workspace,也可以走move接口支持的片上通路。R0 记录数据的写入位置、读取位置和同步方向;具体同步点和event_id在 R6 确定,规则见 cross_core_synchronization.md。

这类"整数 Cube 收缩 + FP 浮点 Vector 尾处理"的融合形态,在仓库的 cv-quant-matmul-direct-epilogue.md 模式文档中有详细约束:整数收缩在 K 全过程中保持 int32,再在同一 kernel 内进入 FP 浮点 Vector epilogue;epilogue 顺序(整数 pre-bias → FP32 scale → 可选 offset → 浮点 post-bias → 最终 cast)必须从 golden 冻结。由于 Cube 与 Vector 分属不同 Module,跨执行域的数据交接(Acc 直搬 Vec,或经 GM workspace)与 READY/FREE 信用同步必须按跨核同步规则设计。

关于 Cube 路径的数据通路,api_mapping_and_tile_planning.md 给出了完整的链路:pl.matmul(dst, lhs, rhs)要求lhs位于 Left(L0A)、rhs位于 Right(L0B)、dst位于 Acc(L0C),一次矩阵乘通常展开为GM → Mat(L1) → Left/Right(L0A/L0B) → matmul/matmul_acc → Acc(L0C) → store/move → GM 或 Vec(UB)。VF 路径则呈现GM → Vec(UB) → vf.load* → 寄存器 → vf.* → vf.store* → Vec(UB) → GM的层次。这些通路信息正是第二步判断 Section 归属时的硬件依据。

R0 输出:Module 列表与 module_interfaces.yaml

R0 的产出落在两个地方:DESIGN.md 的 §0,以及机器可读的module_interfaces.yaml契约。

Module 列表

每个 Module 记录数学目标、Section、输入、输出和前置依赖

Module数学目标Section输入输出前置依赖
Module 1...Cube/Vector.........

只有一个 Module时,必须说明数据依赖和 Section 边界为何不需要继续拆分(例如纯逐元素、单遍完成且不跨执行域的算子)。跨 Tile 状态及其循环范围在 R4 补充——即 loop_design.md 中"结果单元"与"跨 Tile 状态生命周期"的职责。

DESIGN.md 模板 §0 还要求记录归约轴容量结论:归约轴是否可能超单 tile、多 Tile 归约方案(常规多遍 / 在线统计加输出两遍 / 其他),以及 Module 级数据流示意。这些信息直接呼应 R0 的划分依据。

module_interfaces.yaml 契约

R0 阶段必须产出机器可读的module_interfaces.yaml(single source of truth),包含以下字段:

  • module_count:Module 总数;
  • is_fusion:是否为融合算子(同时含 cube 和 vec section →true)。按 R0 定义,is_fusion=true隐含module_count >= 2(Cube 和 Vector 必须划分到不同 Module);
  • has_cross_core:是否涉及 cross_core 跨核流水(信息记录用,不影响分流判据);
  • modules[]:每个 Module 的id/name/description/sectioncubevector)/golden_steps/inputs(source 为primarymodule_<j>,且j < 当前 id)/outputs/golden_stage_fn
  • final_outputs:每个 golden 返回值对应到产出 Module;
  • composition_verification:atol / rtol / seeds / shapes。

骨架可由脚本自动生成:

python ./scripts/gen_module_interfaces.py custom/<op>/<op>_golden_cpu.py \ --spec custom/<op>/SPEC.md --op <op> \ --design custom/<op>/DESIGN.md > custom/<op>/module_interfaces.yaml

脚本 gen_module_interfaces.py 会从{op}_golden_cpu.py中解析 golden 函数签名,自动填充schema_versionopprimary_inputscomposition_verification等确定性部分;modules[]边界、final_outputs接线、is_fusionhas_cross_core等标注TODO的判断部分由 architect 手工填写。

产出后必须自验:

python ./scripts/validate_module_yaml.py custom/<op>/module_interfaces.yaml --json

返回"status": "PASS"才算完成。验证器 validate_module_yaml.py 内置了完整的契约规则(rule0–rule7):文件必须是声明了op/module_count/modules的非空映射且至少一个 Module(防止生成器错误消息被重定向进 YAML 后空文件通过校验);inputs[*].source == "primary"的名称必须存在于primary_inputssource == "module_j"必须满足j < 当前 id且名称存在于module_j.outputs(禁止前向/自引用);final_outputsmodule_j必须满足j <= module_count且名称存在于对应 Module 输出;不允许 no-op Module;shape 表达式只允许+ - * //与名称/整数 token;dtype 必须在允许的词汇表内;module_count必须等于len(modules)。该脚本还支持--self-test运行全部规则的自检用例,其中记录了从旧拼写phase_<j>到规范拼写module_<j>的演进,两套拼写目前均被接受以保证历史产物继续通过校验。

Module 划分与后续轮次的衔接

Module 划分不是孤立的设计动作,它与后续轮次存在明确的上下游关系:

  • R1(API 映射):在 R0 的 Module 划分基础上,将 API 调用链细化到 Module 内部每一步操作;
  • R4(循环与 Section):R4不重新划分 Module,只负责把 R0 的 Module 落实到 Section 与循环结构;若 API 数据通路与 R0 记录的 Section 归属冲突,需回到 R0 修正 Module 划分;
  • R6(跨核同步):Cube 与 Vector 之间的数据交接使用set_cross_core/wait_cross_core,R0 记录数据的写入位置、读取位置和同步方向,R6 落实具体同步点与event_id
  • R8(综合评估):"数据依赖是否正确(Module 顺序、sync 位置)"与"跨 tile 状态是否正确初始化和持久化"均可能回溯到 R0 重新评估。

其中最关键的一条回溯链是:归约轴超单 tile 时回 R0。当 R7.5 目标测试 case 验证发现"归约轴超单 Tile 的跨 tile 状态与持久化"适配不了时,要回溯 R0 重新设计归约方案(常规多遍 / 在线统计加输出两遍),而不是删改测试 case 迁就设计。这再次印证了 R0 Module 划分质量对整条设计链的决定性影响。

小结

Module 划分的两把尺子是:数据依赖决定"必须分开"Section 边界决定"按执行域分开",同时片上空间容量可以强制制造额外的拆分边界。判断边界时始终记住:是否需要保存中间结果不构成划分依据;Scaling不是 Section;一个 Module 只能属于一个 Section。对 Softmax 类多遍遍历算子,边界来自"基于最终结果重新遍历归约轴";对 Cube+Vector 融合算子,Section 边界就是 Module 边界,跨域交接的同步细节交给 R6。最终把划分结论同时落进 DESIGN.md §0 的 Module 列表与机器可读的module_interfaces.yaml,并通过 validate_module_yaml.py 的 PASS 校验,R0 才算真正闭环。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 2:26:07

VS2019中bits/stdc++.h缺失原因与安全添加方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 2:25:11

单片机污水控制:4-20mA采集、Modbus与级联PID整定

简介&#xff1a;围绕单片机在污水处理中的自动化控制&#xff0c;这份文档资料面向自动化、环境工程及电子类专业的学生与技术人员&#xff0c;尤其适合课程设计、毕业设计或方案调研阶段参考。内容以MCS-51单片机为控制核心&#xff0c;讲解如何用流量传感器与pH值传感器采集…

作者头像 李华
网站建设 2026/9/19 2:24:25

CUDA环境搭建避坑指南:驱动、环境变量与多版本管理

换个思路搭CUDA环境&#xff1a;不再被驱动、路径、权限反复折腾最近又在三台机器上配了一遍CUDA开发环境——一台Windows 11主力机&#xff0c;一台Ubuntu 22.04工作站&#xff0c;一台不带显示器的Ubuntu 20.04服务器。说实话&#xff0c;CUDA环境搭建这事&#xff0c;光看显…

作者头像 李华
网站建设 2026/9/19 2:21:27

用WorkBuddy搭建简历筛选工作流:50份简历30分钟筛完

上周帮一个做招聘的朋友处理简历筛选&#xff0c;他在招聘平台挂了职位&#xff0c;三天收了80多份简历&#xff0c;各种格式混在一起&#xff0c;看完他差点崩溃。他花了一个下午硬读&#xff0c;最后只面了3个人&#xff0c;其中一个还不匹配。我当时就跟他讲&#xff0c;这种…

作者头像 李华