FlattenTileNdTo2D Pass¶
将 InCore 函数中的 ND Tile 操作(3D+)展平为 2D,合并除最后一个维度外的所有维度。
概述¶
PTO-ISA 仅支持 2D Tile。ConvertTensorToTileOps 之后,Tile 可能具有超过 2 个维度(匹配张量形状)。该 Pass 通过将高维轴合并为一个维度并保持最后一个轴不变,将所有 >2D 的 Tile 操作展平为 2D。例如,Tile [2, 3, 4] 变为 [6, 4]。
对于 batch 矩阵乘法,ConvertTensorToTileOps 会先保留为
tile.batch_matmul(带累加器时为 tile.batch_matmul_acc)。随后由
FlattenTileNdTo2D 统一负责把它展开成带 broadcast 语义的逐 batch
2D tile.matmul / tile.matmul_acc。
前置条件:
- 输入 IR 必须为 SSA 形式
- 输入 IR 必须包含 Tile 操作(需先运行
ConvertTensorToTileOps) - 每个 Tile 的物理形状必须为静态(
ConstInt);Tile 的valid_shape可以是动态的,并在展平时 被保留(见动态 valid_shape) - 所有 Tile 归约操作必须沿最后一个轴归约
- 所有 Tile 内存必须是连续的
使用时机:在 ConvertTensorToTileOps 之后、ExpandMixedKernel / InitMemRef 之前运行。
API¶
| C++ | Python | 级别 |
|---|---|---|
pass::FlattenTileNdTo2D() |
passes.flatten_tile_nd_to_2d() |
函数级 |
Python 用法:
from pypto.pypto_core import passes
flatten_pass = passes.flatten_tile_nd_to_2d()
program_2d = flatten_pass(program)
算法¶
对每个 InCore 函数(InCore、AIC、AIV):
- 验证前置条件:检查静态物理形状、最后轴归约、不允许对 >2D 使用
tile.read/tile.write/tile.slice,以及不允许写入区域无法连续折叠的 >2Dtile.assemble - 变换语句:遍历函数体,将 >2D Tile 操作转换为 2D,并保留动态的
valid_shape(见动态 valid_shape) - 验证后置条件:由独立的
TileOps2D属性验证器 (property verifier) 检查改写后的 InCore IR 仅包含受支持的 Tile rank、2Dtile.assemble偏移与 codegen-ready transpose 形态。TileOps2D已列入GetVerifiedProperties(),因此只要VerificationLevel高于None,PassPipeline就会在该 Pass 之后自动运行这个验证器
按语句类型处理:
| Tile 操作 | 变换方式 |
|---|---|
tile.load(>2D) |
将结果 tile 重建为 2D。对于 natural NZ Mat load,还会在源张量上插入 shape-only 的 2D tensor.view,把 leading offsets/shapes/valid_shape 折叠到 2D 源窗口,并要求该窗口按 row-major 连续可折叠。Vec load 和 transposed Mat load 保留原始 rank>2 源窗口,只展平结果 tile |
tile.store(rank>2 张量) |
在转换后 IR 中注入张量 rank 对应的分区 shapes 作为额外的第 4 个操作数,供后端 codegen 重建 partition_view;DSL 源码不变。该窗口是目标张量所包含的 box,以 store 自身的 offsets 为原点 —— 见 store 的分区窗口。若 tile 操作数本身仍是 rank>2,pass 会先插入一个 tile.reshape 把 tile 操作数压回 2D —— 这是给手工构造 IR 的兜底,因为 tile.load 与 tile.reshape 分支现在已经展平了 DSL 能写出的每一种生产者 —— codegen 要求 tile 必须是 2D,而 tile shape 仍由 shapes 分区操作数携带 |
tile.store(2D 张量) |
直接透传 |
tile.create/tile.full(>2D) |
直接使用展平的 2D 形状重建 |
tile.assemble(>2D 目标) |
用与 tile.load 折叠 tensor-rank 偏移相同的行主序折叠,把 ND 偏移折进展平后的 (row, col) 空间(row = ((o0*d1 + o1)*d2 + o2)*… + o[k-2],col = o[k-1]);Tile 操作数本身由其定义处的算子展平。要求 source、target 与 offset 具有相同 rank,且写入区域能折叠为连续的行区间(IsRowMajorCollapseContiguous),否则在前置条件阶段报错。若不折叠,偏移会以 ND rank 残留在 2D Tile 上,而 codegen 只按位置读取 elements[0]/elements[1] 并忽略其余元素,从而静默地写到错误地址 |
tile.transpose |
pto.ttrans scratch 物化的唯一归属。进入时为 3-arg(input, axis1, axis2)。2D:创建一块 scratch tile(shape = 源页,位于输入所在 memory),产出 codegen-ready 的 4-arg tile.transpose(in, a1, a2, scratch)。>2D(末两轴交换):展开为逐 batch 的 2D transpose,每个都是 4-arg 形态,scratch 从扁平 [batch*A, B] 池中切片,再 assemble 进合并后的 2D 输出。交换 batch 轴属用户错误 |
tile.batch_matmul |
展开为逐 batch 的 2D tile.matmul,处理 batch broadcast。b_trans/a_trans 操作数以一个零拷贝 tile.transpose_view(覆盖在自然 load 之上)出现(不再 transpose-at-load、不搬数据);tile 级算子本身无 transpose 语义。每个操作数处理方式一致(见下方操作数处理)。当结果本身就是批量累加器(下游 tile.batch_matmul_acc 会继续写它)时,各页改为通过 tile.matmul_acc(window, lhs_b, rhs_b, init_cond=True) 写入同一块按列打包的 Acc tile —— 见批量累加器按列打包 |
tile.batch_matmul_acc |
展开为逐 batch 的 2D tile.matmul_acc,按 batch 索引取(已展平的)累加器的一个窗口:链按列打包时取 [M, B*N] tile 的列窗口 [0, b*N],否则取 [B*M, N] tile 的旧行窗口 [b*M, 0] —— 见批量累加器按列打包。本 pass 未直接确定的内存空间决策(行打包累加器上的 Vec/Acc 来回搬运、上游 tile.create 的可重定向生产者改写、TileView 刷新)交由 InferTileMemorySpace(pass 20)负责 —— 本 pass 不发射任何 tile.move |
tile.reshape / tile.reinterpret_view(>2D 结果) |
将字面量目标 shape 操作数改写为合并后的 2D [product(leading), last] 并重新推导类型。这两个是仅有的结果 rank 来自 shape 操作数而非某个操作数类型的 tile 算子,因此下面的通用路径无法下降它们——通用路径会用同一个 ND 元组重建调用,rank>2 的结果就此存活到 PTO codegen,被 ExtractTileTypeInfo 按前两维定型。此处的折叠严格保持语义:tile 是一段连续的行主序数据,[2, 8, 128] 与 [16, 128] 指向同一批元素、同一顺序。由此得到的 2D reshape 往往是恒等变换,随后由 FoldNoOpReshape(pass 38)消除。喂给 tile.batch_matmul 的安全 batch-only reshape 由该 lowering 剥离(见上),不会走到这个分支 |
| 其他 Tile 操作(>2D) | 替换变量,使用 2D 类型重新创建 |
| 1D/2D Tile 操作 | 不变 |
统一的操作数处理 —— 整块切片 vs 逐 batch load。 每个 batch_matmul 操作数
(lhs 或 rhs、转置与否、来自 load 或 move)处理方式完全一致。路由按操作数判定:
仅当两个操作数的整块 tile 能一起放进 Mat(L1)(BatchOperandsWholeFit 容量门)
且该操作数的整块 load 连续可塌(WholeLoadContiguous)时才保留整块,否则逐
batch 重发。
- 整块(默认):操作数整块进 Mat 一次,再按 batch 切片 —— 普通
(行批
[B*rows, cols])操作数行切,tile.transpose_view(列批[K, B*N]) 操作数列切。3D[B, N, K]张量的自然 Mat load 在此保留逻辑 ND 源语义,但本 pass 会在 load 前插入 2Dtensor.view([B*N, K]),让下游tile.loadcodegen 看到与其他消费者一致的展平源窗口。本 pass 同时把 load 的结果 tile 展平为 2D。广播操作数复用其单页。 - 逐 batch(整块会撑爆 L1,或整块 load 非连续):从底层自然
tile.load逐 batch 重发(每 batch[1, .., X, Y]窗口 → 2D[X, Y],用 load 自身的 窗口维度,故部分子 tile 也能正确重发),转置时再加逐 batchtile.transpose_view。随后丢弃死掉的整块 load/view。 - 非连续 指既切多 batch、又部分切矩阵行(中间)维的 load —— 如从
[2, K, N]切[2, K0<K, N]。展平成[2*K, N]后各 batch 间有空洞,无法做成单个 2D ND2NZ load;逐 batch 后每块是[1, K0, N](连续),可正常塌。此路由保证 codegen 的连续性守卫永不对 batch_matmul 操作数触发。
死 load 消除(仅逐 batch)。 当操作数逐 batch 重发(容量 !fit 或非连续)时,
原始整块 load/view 变为死代码并被丢弃。丢弃 pre-scan 采用与 LowerBatchMatmul
相同的按操作数路由,故非连续操作数的链在此也被识别为逐 batch。一条链
(tile.load → tile.transpose_view,会向上回溯)在其每一处使用都是
tile.batch_matmul[_acc] 操作数时才可丢弃,且仅当其所有消费 matmul 都把它判为
逐 batch 时才丢(与任一保留整块的 matmul 共享的链保持整块)。使用次数按递归统计
(含嵌套的 If/For/While/Scope 体)。容量门按后端门控(无后端 → 判 fit),
但连续性检查不门控,故非连续路由在单测里也会触发。
逐 batch 的 V2C move(move 来源且放不下 L1 的操作数)是后续待办;此类操作数目前 仍走整块切片路径,仅在被搬运的整块 tile 放得下固定跨核 ring 时正确。
批量累加器按列打包¶
批量 tile.batch_matmul_acc 的累加器是本 pass 唯一不按通用
[prod(leading), last] 规则展平的值。它的各页沿列堆叠:一块 [M, B*N] 的
Acc tile,第 b 页位于 tile.slice(acc, [M, N], [0, b*N])。
为什么不能按行打包¶
Acc(L0C)是 NZ 分块的:对 4 字节累加器元素,[M, N] tile 的第 (r_b, c_b)
块起始于 (c_b * M/16 + r_b) * 1024 字节。因此当 tile 有多个块列时,行窗口
是跨步的 —— 它的块间距是父 tile 的行数,而不是窗口自身的行数。pto-isa 的 MAD
以裸指针紧凑写出 [m, n] 目标,没有目标跨步(hw-native-sys/pto-isa#253),所以按行
打包的 [B*M, N] 形状根本没有正确的降级路径:每页只有前 16 列会落在正确位置。
这正是 CanonicalizeTileSlice(pass 19)拒绝的形状,参见
18-canonicalize_tile_slice.md。
列窗口覆盖父 tile 的整个行范围,因此窗口自身的紧凑几何与父 tile 的几何一致,
被丢弃的跨步也就无关紧要。GetSliceAccumulatorGeometry 正是给这种形状计算
NZ 精确字节偏移(参见 33-init_memref.md)。
生产者侧的变化¶
LowerBatchMatmul 过去把多 batch 结果暂存到 Vec —— 在 Acc 中汇聚各页需要
L0C→L0C 拷贝,而 ISA 没有这条指令。这会把累加器从 tile.matmul_acc 唯一接受的
内存空间里搬走。当结果是某条累加器链的根时,本 pass 现在自己分配打包后的 Acc
tile,并按下面的形式写入第 b 页:
acc_0 = tile.create([M, B*N], dtype=FP32, target_memory=Acc)
w_b = tile.slice(acc_b, [M, N], [0, b*N])
m_b = tile.matmul_acc(w_b, lhs_b, rhs_b, True) # init_cond=True
acc_b1 = tile.assemble(acc_b, m_b, [0, b*N]) # 自拷贝,codegen 直接省略
init_cond=True 不需要新算子:字面常量谓词会折叠到非累加分支,因此这里发射的
是普通的 pto.tmatmul ins(lhs, rhs) outs(window)。这正是 tile.matmul 没有目标
操作数、却仍能写入子区域的方式。tile.assemble 回写只用于串联 SSA;codegen 识别
出它是同一窗口的自拷贝并且不发射任何指令 —— 这是必需的而不只是优化,因为 Acc
目标没有合法的 tmov。因此偏移元组只构造一次,同时传给 slice 和 assemble:
codegen 通过两个 subview 发射出的 SSA 名字来匹配,重新构造一个数值相同的偏移会
发射出非法的 L0C→L0C 搬运。
对这类结果会关闭 direct-store 融合:把它融合进下一条 tile.store 会吞掉链上仍然
需要的语句。
消费者(drain)侧的变化¶
[M, B*N] tile 并不是 [B, M, N] 输出窗口的行主序折叠,所以单条整块
tile.store 会写出错误数据,而下游没有任何环节能发现。打包累加器的 tile.store
因此变成逐页一条 store,直接从 L0C 写出:
没有 Vec 暂存,也没有 tile.move:源为 Acc tile 的 store 被判定为 CUBE,因此留在
cube 通道上,不涉及 ExpandMixedKernel 的 AIC→AIV 边界。(该边界 —— Acc→Vec
搬运 —— 在所有非累加器的 tile.batch_matmul 上保持不变,它们仍经 Vec 暂存。)
何时打包一条链¶
这个决策在任何改写之前、按整个函数做一次(acc_packing.cpp),因为一条链通常跨
多个块:tile.create 在 K 循环外,tile.batch_matmul_acc 在循环内,tile.store
在循环后。改写主循环自带的预扫描是按块进行的,看不到这种结构。
一条链是同一缓冲区别名图的连通分量 —— 包括 tile.batch_matmul_acc 的原地边、
普通 SSA 别名、循环 iter_arg / yield / return_var 携带边,以及 IfStmt 汇合边。
只有当每个成员都由本 pass 能逐页改写的形式产生和消费(tile.create 或
tile.batch_matmul 根、tile.batch_matmul_acc、携带边、tile.store drain),
且几何形状是 L0C 能寻址的,才会打包:
| 条件 | 原因 |
|---|---|
M % 16 == 0 且 N % 16 == 0 |
父 tile 两个方向都必须是完整的 16×16 块,且页的列原点 b*N 必须块对齐,否则 GetSliceAccumulatorGeometry 会拒绝,InitMemRef 静默退回行主序算法 |
| 4 字节累加器元素(FP32 / INT32) | 只有每元素 4 字节时,16×16 块才正好是 kAccFractal(1024)字节 |
B*M*N*4 能放进 Acc |
从后端读取(GetMemSize(Acc)),不写死 —— L0C 在 Ascend910B 上是 128 KB,在 950 上是 256 KB |
无部分 valid_shape |
第 b 页的有效区域从 b*N 开始但只有 N_valid 宽,打包后的父 tile 没有单一有效矩形可以表达 |
batch_count > 1 |
B == 1 时两种打包都是同一块 [M, N] tile,因此现有快速路径逐字节不变 |
不满足其中任一条的链保留旧的行打包降级 —— 当每页不超过 16 列时它仍然正确,因为
此时窗口落在单个 L0C 块列内,而 CanonicalizeTileSlice 明确放行这种形状。注意
M % 16 == 0 比行打包所需的 B*M % 16 == 0 更严格,所以例如 M = 8, B = 2,
N = 16 会有意退回行打包。
另有三种情况连行打包也无法表达。前两种无论页宽多少(包括 16)都会在这里直接报错; 第三种只在超过 16 列时出现,此时行打包本就不可用:
| 情况 | 为什么行打包也救不了 |
|---|---|
链中存在多于一个分配型定义(例如 if k == 0: acc = matmul(...) else: acc = matmul_acc(acc, ...),或循环体内重新创建累加器) |
两个缓冲区在控制流汇合点相遇,合并它们需要 ISA 并不具备的 L0C→L0C 拷贝。若放行,程序会一直走到二十个 pass 之后的 MemoryReuse YieldFixup 并以内部错误崩溃 |
定义方根本无法写 Acc(例如 tile.load) |
与 batch 无关 —— 同样的累加器在 B == 1 时以完全相同的方式失败 |
N > 16 时以 tile.move 排空 |
把 move 按页拆开很容易,难的是把页聚合回去:搬出的页保留 L0C 的 col_major/1024 块布局,而 tile.assemble 无法把它写进 [B*M, N] 展平所期望的 row_major 向量 tile。这不是累加器特有的限制 —— 普通的 batch > 1 tile.batch_matmul 后接任意向量算子会在同一处检查失败(pto_ops_shared.cpp,"blayout mismatch between source and result"),所以这里不做任何打包 |
这三种情况各有自己的诊断信息和规避方式;对它们不会打印按列打包的说明,因为 它们都与页的几何无关。
其余情况 —— 更宽但无法按列打包的页,或生产者不得不经 Vec 暂存的页 —— 会在这里 直接报错,并给出 DSL 侧的规避方式,而不是发射一个地址会静默退回行主序的窗口:
tile.batch_matmul_acc: cannot lower a batch-2 accumulator of 16x24 FP32 pages,
because the page column extent N=24 is not a multiple of 16.
The pages of a batched accumulator have to be packed along COLUMNS — one 16x48
Acc (L0C) tile with page b at tile.slice(acc, [16, 24], [0, b * 24]) — because
the hardware MAD writes its destination compactly and has no destination stride
...
Workarounds: write the batch loop out in the kernel and accumulate each page
into its own 2-D tile (pl.matmul / pl.matmul_acc on 2-D operands); or keep the
accumulator at most 16 columns wide, which fits a single L0C block column and
needs no packing.
累加器逻辑行窗口¶
同一打包机制支持通过
acc[...] = pl.matmul_acc(acc[...], lhs, rhs, init_cond=...) 更新局部二维累加器。
DSL 保留逻辑行坐标;对划分为 R 行窗口的 [T*R, N] 累加器,物理分配变为
[R, T*N]:
# Logical window
win = pl.tile.slice(acc, [16, 32], [16, 0])
part = pl.tile.matmul_acc(win, a, b, init_cond=True)
acc_updated = pl.tile.assemble(acc, part, [16, 0])
# Packed window: acc's allocation changes from [32, 32] to [16, 64]
win = pl.tile.slice(acc, [16, 32], [0, 32])
part = pl.tile.matmul_acc(win, a, b, init_cond=True)
acc_updated = pl.tile.assemble(acc, part, [0, 32])
运行时 t0 对应的列偏移为 (t0 // R) * N + n0。回写保留计算的定义使用关系,
无需 L0C 到 L0C 的复制。最终对整个累加器的存储拆成逐窗口存储,目标逻辑行偏移为
t * R。K 外层、行内层的循环顺序及权重复用保持不变,行循环次数可以在运行时决定。
只有 MAD 本身无法寻址的窗口才会被打包。列数不超过 16 且完整落在同一个 16 列块内的
窗口只占一个 L0C 块列,紧凑写回不存在会被错误跨步的第二个块列,pto-isa 的
MadAccStrideCompatible 因此接受它。判据取自窗口自身的列范围而非父累加器:
ptoas 解析行窗口时保留父累加器的物理 Rows,Cols 则取自窗口,所以 [48, 32]
累加器上的 [16, 16] 窗口是可寻址的。这类链原样通过,下面的要求对它们一律不适用
—— 把硬件本就接受的链纳入打包,只会让原本可用的 kernel 落入下列拒绝条件。该豁免
条件与 CanonicalizeTileSlice 的 CheckAccWindowContiguous 保持一致,因此这里
放行的窗口不会在两个 pass 之后被拒绝。
打包要求:单个由编译器分配的缓冲区;统一的静态窗口行数且整除父累加器行数;
可证明对齐的行偏移;完整有效形状;FP32/INT32 元素;窗口行数和父累加器列数均为
16 的倍数;按目标物理行对齐后仍能放入 L0C。运行时对齐证明目前覆盖窗口高度为
2 的幂的情况。每个 assemble 必须将 matmul_acc 结果写回其读取的原窗口。
显式绑定的 MemRef,以及要求以其他布局读取整个逻辑累加器的消费者,会得到明确诊断,
不会被静默重排。
示例¶
之前:
@pl.program
class Before:
@pl.function(type=pl.FunctionType.InCore)
def main_incore_0(self, x: pl.Tensor[[2, 3, 4], pl.FP32],
out_0: pl.Out[pl.Tensor[[2, 3, 4], pl.FP32]]) -> pl.Tensor[[2, 3, 4], pl.FP32]:
x_tile: pl.Tile[[2, 3, 4], pl.FP32] = pl.load(x, [0, 0, 0], [2, 3, 4])
y_tile: pl.Tile[[2, 3, 4], pl.FP32] = pl.tile.add(x_tile, x_tile)
out_0 = pl.store(y_tile, [0, 0, 0], out_0)
return out_0
之后:
@pl.program
class After:
@pl.function(type=pl.FunctionType.InCore)
def main_incore_0(self, x: pl.Tensor[[2, 3, 4], pl.FP32],
out_0: pl.Out[pl.Tensor[[2, 3, 4], pl.FP32]]) -> pl.Tensor[[2, 3, 4], pl.FP32]:
x_tile: pl.Tile[[6, 4], pl.FP32] = pl.load(x, [0, 0, 0], [2, 3, 4])
y_tile: pl.Tile[[6, 4], pl.FP32] = pl.tile.add(x_tile, x_tile)
out_0 = pl.store(y_tile, [0, 0, 0], out_0)
return out_0
3D Tile [2, 3, 4] 被展平为 [6, 4]。tile.load 直接产生 2D tile,无需插入 tile.reshape。tile.store 接受 2D tile 并写入原始的 rank>2 张量。对于 rank>2 张量,Pass 会在转换后 IR 中将分区 shapes 注入为额外的第 4 个操作数(例如 pl.store(y_tile, [0, 0, 0], out_0, (2, 3, 4)));该操作数仅存在于转换后的 IR 中,不属于 DSL 源码。
store 的分区窗口¶
shapes 操作数是目标坐标系下的一个 box —— 每个尺寸都不超过所在轴的 extent,以 store 自身的 offsets 为原点。Codegen 把它转成 pto.partition_view,而后者只能表达 box。
当 tile 的每个维度就是它所落到的那个张量维度时,"前面补 1 + 接上 tile 各维"正好得到这个 box:张量 [B, M, N] 由 [M, N] tile 写入,得到 [1, M, N]。
一旦 tile 的前导 extent 是若干张量维度的折叠,这条规则就失效了 —— tensor.gather 的下降正是如此:[2, 3, 8] 的结果变成 [6, 8] 的 tile。补 1 会得到 [1, 6, 8],即向一个 extent 为 3 的轴索要 6。这不是目标张量所包含的 box;它此前之所以能写对字节,只是因为外层 stride 恰好连续,而 PTOAS >= 0.61 会直接拒绝:
因此窗口改为从最内侧的前导轴向外走,在行数仍然跨越该轴时将其整轴吃掉:[2, 3, 8]。
并非每个折叠 store 都存在窗口。 展平后的 store 写的是从 offsets 起、行主序连续的 rows 个位置,而 box 只有在"行数吃掉的每个轴都被整轴吃掉且从 0 开始"时才与该连续段重合。[12, 8] 的 tile 写入 [2, 2, 4, 8],存在一个 in-bounds 且行数相乘为 12 的 box [2, 2, 3, 8],但它覆盖的扁平位置是 {0,1,2, 4,5,6, 8,9,10, 12,13,14},而 store 的语义是 {0..11}。此时 Pass 选择拒绝,而不是把写入重定向到作者并未指定的内存。最内侧轴不受此约束 —— 它承载 tile 的列,部分列区间仍然是矩形,只需放得下即可。
动态 Tile 维度(issue #1578)¶
硬件 Tile 对应固定大小的片上缓冲,因此每个物理 Tile 维度都必须是编译期常量;运行时实际范围保存在
TileView.valid_shape 中。要处理动态维,用户自己写分块循环:用 pl.range 以静态 CHUNK 步进迭代
动态维,每趟把这一块 load 成静态物理 [1, CHUNK, 512] 的 tile,并在 valid_shape 里用
min(CHUNK, s - c) 夹住尾块。chunk 大小由用户决定 —— 它对性能影响显著,因此 Pass 不自动选取:
# 用户自己写:对动态 S 维分块,在 valid_shape 里夹住尾块。
for c, (o,) in pl.range(0, s_dim, CHUNK, init_values=(out,)):
valid = pl.min(CHUNK, s_dim - c)
t = pl.load(x, [b, c, 0], [1, CHUNK, 512], valid_shape=[1, valid, 512])
t = pl.cast(t, target_type=pl.FP32)
o = pl.store(t, [b, c, 0], o) # 物理静态 [1, CHUNK, 512],valid 动态
pl.yield_(o)
每趟的 tile 物理上是 [1, CHUNK, 512](静态),valid_shape 是 [1, min(CHUNK, s - c), 512](动态)。
FlattenTileNdTo2D 在这里的唯一职责,就是把这个 >2D tile 降成 [CHUNK, 512],同时保留动态的
valid_shape —— ComputeMergedValidShape 用与 ComputeMergedShape 合并物理形状相同的方式合并
valid_shape 的前导维,但允许动态项,因此运行时尾块能穿过展平活下来,而不是被重置成满物理形状。循环是
用户写的,Pass 不生成它。
chunk 必须放得下片上 Vec(UB)内存(
CHUNK * <保留维> * <存活 tile 字节数> <= UB 容量),否则AllocateMemoryAddr会以 "Vec buffer usage exceeds platform limit" 报错。选 chunk 是用户的责任。
如果一个 >2D tile 到达本 Pass 时物理形状是动态的(用户没切静态 chunk),它无法展平,Pass 会抛出可操作的
报错,指向两种修法:用 pl.range/pl.parallel 对动态维分块,或在进入 InCore(pl.at)作用域前 reshape 为 2D。
循环携带值的 valid_shape 修复¶
当 tile.batch_matmul 的左操作数带着被收窄的 valid_shape 时,展开后得到的 2D matmul
结果比它流入的累加器更窄。而累加器所经过的循环携带值只按其初值定型,因此它仍然宣称
种子那个没有任何 mad 写满过的完整盒高:
acc__tile : Tile[[64, 256], INT32] <- pl.create_tensor 种子
iter_arg : Tile[[64, 256], INT32] <- 按种子定型
yield : Tile[[64, 256], INT32, Acc, valid=[v, 256], compact] <- 循环体实际产出
return_var : Tile[[64, 256], INT32] <- 被强行拉回 iter_arg 的类型
mad 以 ceil(v/16)*16 的 N-fractal 步长把乘积写进 L0C,而相信完整盒高的读者会按物理行
步长遍历该缓冲区,从而打乱第一个之后的每一个 N-fractal(issue #2470)。因此本 pass 在返回
之前会对每个改写过的函数调用 narrow_loop_carry::NarrowAccCarries:把种子按 yield 可证明
的范围重新声明——tile.create(compact=True) 加 tile.set_validshape,与 AutoTileMatmulL0
切分 K 时构造的形式一致——并让循环体的 def-use 闭包通过算子自身的 deducer 重新定型。
在这里修复而不是留给后续 pass,正是流水线可验证性的前提:否则本 pass 产出的携带值会被
TypeCheck 诊断与 AccCompactValid 属性验证器拒绝。ConvertTensorToTileOps 出于同样的
原因调用同一个 helper——2D 种子在 tensor.matmul 变成 tile.matmul 时就已经被收窄了。
两种情况下携带值保持原样:一是缓冲区的两种读法本来就不会分歧——单 fractal 块的 [16, N]
累加器无论有效行是多少都按物理行打包;二是收窄用的表达式只在循环体内计算,重新声明的种子
在那之前根本命名不到它。
实现¶
头文件:include/pypto/ir/transforms/passes.h
实现按职责拆分:
| 阶段 | 文件 | 职责 |
|---|---|---|
| 协调 | src/ir/transforms/flatten_tile_nd_to_2d/pass.cpp |
选择 InCore 函数,按 analysis → rewrite 顺序执行,并修复被改写收窄的循环携带值 |
| 分析 (analysis) | src/ir/transforms/flatten_tile_nd_to_2d/analysis.cpp |
只读的前置条件验证 |
| 改写协调 | src/ir/transforms/flatten_tile_nd_to_2d/rewrite.cpp |
递归遍历语句并分派算子改写 |
| 改写工具 | src/ir/transforms/flatten_tile_nd_to_2d/rewrite_utils.cpp |
共享形状、索引和容量辅助逻辑 |
| 累加器打包 | src/ir/transforms/flatten_tile_nd_to_2d/acc_packing.cpp |
按整个函数决定哪些批量 Acc 累加器链按列打包 |
| 批量矩阵乘改写 | src/ir/transforms/flatten_tile_nd_to_2d/batch_matmul.cpp |
批量矩阵乘与累加算子的分页降级 |
| 转置改写 | src/ir/transforms/flatten_tile_nd_to_2d/transpose.cpp |
独立 N 维转置的降级 |
| 验证 (verification) | src/ir/transforms/flatten_tile_nd_to_2d/verification.cpp |
独立验证 TileOps2D 后置条件 |
这些阶段入口和改写组件接口仅供 transform 内部使用;公共 API 仍为 pass::FlattenTileNdTo2D()。
Python 绑定:python/bindings/modules/passes.cpp
测试:tests/ut/ir/transforms/test_flatten_tile_nd_to_2d.py、tests/ut/ir/transforms/test_narrow_loop_carry_valid_shape.py(携带值修复)、tests/st/codegen/dsl/test_flatten_dynamic_tile_3d.py(issue #1578 端到端)
Pass 属性¶
| 属性 | 值 |
|---|---|
| 所需 | SSAForm, IncoreTileOps, NormalizedStmtStructure |
| 产生 | SSAForm, TileOps2D, NormalizedStmtStructure |
| 失效 | — |
作用范围¶
| Tile 维度 | 处理方式 |
|---|---|
| 1D | 不变 |
| 2D | 不变 |
| 3D+ | 展平为 2D |
仅处理 InCore 类型函数(InCore、AIC、AIV)。Orchestration 和 Opaque 函数原样返回。