PyPTO-Gym 长轴 Scan/Cumulative 与超 UB 规约的分块进位(Block + Carry)调试实战
【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym
本文是 PyPTO-Gym 仓库中
pypto-general-debug技能包调试文档《Large-axis scan / cumulative & UB-exceeding reduction》(scan-and-reduction.md,对应 DEBUG_GUIDEBOOK.md §9.21)的展开版。它面向在 Ascend NPU 上基于 PyPTO 编写融合算子的开发者,解决一个高频致命问题:当pypto.cumsum/pypto.cumprod等原生 scan/cumulative 算子沿长轴执行、或某个原生规约算子的单算子 UB 占用超出预算时,编译期报F40005UB 溢出,而"逐元素循环"的错误修复又会导致宿主崩溃或超时。读完本文,你将掌握"分块扫描轴 + 跨块进位"(Block-the-axis + carry)这一标准修复范式:如何根据 UB 预算选择块长T、如何在pypto.loop中组织带跨块依赖的循环、为什么T是 view 形状而非 vec tile 尺寸,以及该模式在 pypto-kernel-design-format.md §9c 中的规范地位。
何时打开本文:症状与适用边界
本文对应的调试入口在 scan-and-reduction.md,它是pypto-general-debug技能包的路由索引 DEBUG_GUIDEBOOK.md 中的§9.21节。技能包要求:遇到卡死或不透明失败时,先通过DEBUG_GUIDEBOOK.md的 "Quick map: situation → leaf file" 表路由到唯一的叶子文件,再只读该文件;§9.21 对应的路由触发条件有两条:
- 在 scan/cumulative 或长轴规约上出现
F40005(UB 分配超限),或正打算对某个轴逐元素做 scan; - 正准备写
pypto.cumsum/pypto.cumprod(对长轴做 scan)。
该模式的适用范围有明确边界,原文给出如下Scope:
- 适用:原生 scan/cumulative 算子(
pypto.cumsum、pypto.cumprod、cumsum_reverse、masked_cumsum等)沿长轴执行;或原生规约算子的单算子 UB 占用超过预算; - 不适用:算子本身能放进 UB 预算的场景——此时不需要引入分块进位机制,直接对整轴调用原生算子即可。
症状:从编译期 F40005 到运行期崩溃
编译期:UB 分配失败
当对整条长轴一次性应用原生 scan 算子时,编译阶段直接报 UB 分配失败:
ErrCode F40005! TENSOR_MEMORY_ALLOCATION. Alloc tensor size [256000] exceeds MEM_UB size [196608]!错误信息中的256000是申请的张量字节数,196608是 MEM_UB 可用字节数(192 KB)。同类故障也可能表现为L0A/L0B/L0C/L1 size exceeded、tile 对齐失败或 tile shape 未设置,此时应转向 tile-shapes.md 处理。
运行期:宿主崩溃、挂死或超时
如果开发者用"逐元素循环"绕开编译错误(见下文反模式),编译虽然通过,但运行期会出现:
SLAB_ADD_CACHE_FAILED → segfault / process killed或者运行永不结束——例如 4 小时 benchmark 超时、缺失产物文件。
根因:原生 scan 的工作区随轴长线性增长
工作区 ≈ 轴长 × 64 字节
原生 scan 算子的内部工作区(workspace)与轴长成正比。以pypto.cumsum为例,约64 字节/轴元素(≈ 16× FP32,用于并行树形扫描的各级中间结果),因此:
- 4000 长的轴需要约 256 KB 内部工作区 > 192 KB UB 预算;
- UB 硬上限 ≈3072 个 FP32 元素(192 KB ÷ 4 字节);
- 对整轴一次性应用算子 → 编译期必然失败(
F40005)。
反模式:逐元素循环的任务爆炸
pypto.view([1, 1])逐位置循环在数值上正确,但会产生axis_len × batch个任务——例如 4000 × 128 ≈512K 个任务,直接耗尽宿主侧调度资源,表现为SLAB_ADD_CACHE_FAILED+ 段错误,或长时间超时。这与 pypto-kernel-design-format.md §9c 记录的 BAD #2 反模式完全一致。
修复范式:分块扫描轴 + 跨块进位
核心思路
把长轴切分为长度为T的块,使得算子逐块的 UB 占用落在预算内(cumsum 取T=1000时约 64 KB < 192 KB)。然后:
- 只对少数块做循环;
- 用
pypto.view切出真实的[.., T]块; - 在块上应用原生算子;
- 把运行累加器(running accumulator)跨块传递。
标准实现(原文核心代码)
# ✅ block the scan axis; native op per block; carry across blocks def _op_kernel_impl(x, out): # x, out: [B, D] (D large, e.g. 4000) B = x.shape[0] # SymbolicScalar (dynamic batch) D = 4000; T = 1000; NB = D // T # T fits per-block UB; NB small (4) for b in pypto.loop(B, name="batch"): carry = pypto.full([1, 1], 0.0, pypto.DT_FP32) # running accumulator # block loop carries a cross-block dependency -> submit_before_loop=True (only NB iters) for t in pypto.loop(NB, name="block", unroll_list=[1], submit_before_loop=True): blk = pypto.view(x, [1, T], [b, t * T]) # real [1, T] block (NOT [1,1]) scan = pypto.cumsum(blk, dim=1) # native op on the block scan[:] = scan + carry # fold in running carry ([1,1]->[1,T]) pypto.assemble(scan, [b, t * T], out) carry[:] = pypto.view(scan, [1, 1], [0, T - 1]) # block tail = new running total逐行解读:
| 代码行 | 作用 | 关键点 |
|---|---|---|
B = x.shape[0] | 动态 batch 维 | 返回SymbolicScalar,运行期才解析的实际批量 |
D = 4000; T = 1000; NB = D // T | 分块参数 | 块长T必须满足逐块 UB 预算;NB只有 4,循环次数少 |
carry = pypto.full([1, 1], 0.0, pypto.DT_FP32) | 运行累加器 | 每个 batch 行独立初始化,跨块复用 |
pypto.loop(NB, ..., submit_before_loop=True) | 块循环 | 存在跨块依赖,必须submit_before_loop=True;unroll_list=[1]保持循环不被展开 |
pypto.view(x, [1, T], [b, t * T]) | 切真实块 | 是真正的[1, T]块,不是[1, 1]标量 |
pypto.cumsum(blk, dim=1) | 块上原生算子 | 逐块 UB 已在预算内 |
scan[:] = scan + carry | 折叠进位 | [1,1]广播到[1,T] |
pypto.assemble(scan, [b, t * T], out) | 写回输出 | view 形状与 offsets 维度匹配 |
carry[:] = pypto.view(scan, [1, 1], [0, T - 1]) | 更新进位 | 块尾元素 = 新的运行累计和 |
两个必须绕开的反模式
# ❌ BAD #1 — native op on the whole axis at once → UB OOM (F40005: 256000 > 196608) scan = pypto.cumsum(x, dim=1) # 4000-long workspace blows the UB budget # ❌ BAD #2 — element-by-element loop over the large axis → task explosion / host crash / timeout for j in pypto.loop(D, submit_before_loop=True): # D=4000 iters × B = ~512K tasks x_col = pypto.view(x, [1, 1], [b, j]) # one scalar per iter (SLAB_ADD_CACHE_FAILED + segfault)BAD #1 直接对 4000 长轴调用pypto.cumsum(x, dim=1),工作区 256 KB 撑爆 192 KB UB,编译报F40005;BAD #2 把循环数推到 512K 级,宿主调度资源耗尽。
块长 T 的选取原则
- 选取能整除轴长的最大
T,且逐块 UB 在预算内; - cumsum FP32 场景:
T ≲ 3000(即 UB 硬上限 3072 个 FP32 元素的约 97% 以内),常用取值 1000 / 800 / 500; - 原文示例用
T=1000,单块约 64 KB,留足余量。
关键澄清:块长 T ≠ vec tile 尺寸
这是最容易踩坑的认知误区。原文明确强调:T是 view 形状,不是 vec tile。
- 上述核心里程碑代码在
set_vec_tile_shapes(16, 16)这样的小 vec tile 下也能通过编译——正确性来自分块结构,而非 tile 取值; - 不要把 tile 尺寸与
T耦合; - 不要改动正常的 vec tile 规则:每轴
[16, 64]、rank 匹配、单算子 UB 适配。详见 tile-shapes.md。
这与 pypto-kernel-design-format.md §9c 的结语一致:"这是一个 loop/view 结构规则,独立于 vec-tile 尺寸"。
源码级佐证:从调试叶子到规范与设计原则
调试叶子文件在技能包中的定位
scan-and-reduction.md 是pypto-general-debug技能包(SKILL.md)的聚焦叶子文件之一。技能包要求按DEBUG_GUIDEBOOK.md的路由表只读匹配的叶子,避免一次性加载整个 playbook。§9.21 在 "Quick map: situation → leaf file" 表中明确指向本文件。
规范级实现骨架
分块进位模式在算子开发技能包中拥有规范地位:pypto-kernel-design-format.md §9c "Blocked scan / cumulative (carry) pattern — for UB-exceeding scan/reduction only" 完整收录了同样的好/坏示例,并给出两条配套约束:
- JIT 图内唯一允许的循环是
pypto.loop(lint 规则 OL57,S0 级):块分解等结构性循环必须用pypto.loop,且迭代间存在依赖时加submit_before_loop=True;Pythonfor/while会在构图期展开迭代,破坏框架的 tiling/并行,并膨胀编译时间; - 批维在宿主侧折叠(§9b):把前导 batch 维折叠成 2D,让核心算子保持低秩,避免高维 matmul 的 tiling 复杂度。
设计层原则
pypto-op-design/SKILL.md §3.0 的第 4 条数据流原则(逐块处理 + 跨块进位)是本文模式的设计级依据,与 dataflow.md 中的分页、共享存储和尾块约束互相呼应。
知识库中的相关实现
pypto-pro-op-kb 提供了两个相关视角:
- vec-scan-prefix-dependent.md:向量单元上实现前缀依赖扫描的完整指南(运行极值扫描、累积 min/max/sum、
torch.cummin等)。文中明确指出pypto 没有 scan 原语、也没有寄存器到寄存器的 lane shuffle,vf.shift_left/right是位运算而非 lane 移动; - cumsum_matmul_impl.py:在 pl(PyPTO-Pro)无 scan 算子的前提下,把 cumsum 改写为
x @ U(U 为上三角全 1 矩阵)的 cube 算子实现,覆盖 FP32[8192, 128]。值得注意的是,这种三角矩阵改写只对+组合子(cumsum)成立——cumprod需要 log 域改写且要求元素严格为正,logcumsumexp根本不是 matmul。
实战检查清单
- 判断是否命中:原生 scan/规约沿长轴执行、单算子 UB 超预算、或正打算逐元素扫描 → 使用本文模式;否则直接整轴调用。
- 选
T:整除轴长的最大块长,逐块 UB < 192 KB;cumsum FP32 取T ≲ 3000(常用 1000/800/500)。 - 组织循环:batch 维
pypto.loop(B);块循环pypto.loop(NB, unroll_list=[1], submit_before_loop=True)(跨块依赖必须加 submit 标志)。 - 切块要真实:
pypto.view(x, [1, T], [b, t*T])必须是[1, T],严禁退回[1, 1]逐元素。 - 进位传播:块尾
view(scan, [1, 1], [0, T-1])写入carry,下一块先scan + carry再写回。 - 不要动 vec tile:
T与 tile 尺寸解耦,沿用[16, 64]每轴、rank 匹配、单算子 UB 适配的正常规则。 - 禁止 Python 循环:JIT 图内只用
pypto.loop(OL57),结构性块分解同样遵守。
参考与延伸阅读
- 调试入口: scan-and-reduction.md(§9.21)与路由索引 DEBUG_GUIDEBOOK.md
- 规范骨架: pypto-kernel-design-format.md §9c
- 设计原则: pypto-op-design/SKILL.md §3.0 与 dataflow.md
- 配套知识: vec-scan-prefix-dependent.md(向量单元扫描)、cumsum_matmul_impl.py(matmul 改写 cumsum)
- 相关调试: tile-shapes.md、pypto-view.md、error-codes.md
【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考