TileLang Layout 系统深度指南:Layout/Fragment 核心类型、CuTe 代数与布局推断
【免费下载链接】tilelangDomain-specific language designed to streamline the development of high-performance GPU/CPU/Accelerators kernels项目地址: https://gitcode.com/GitHub_Trending/ti/tilelang
本文以 TileLang 仓库中的布局系统开发文档(
.agents/skills/tilelang-layout/SKILL.md)为骨架,结合 src/layout/layout.h、tilelang/layout/ 与 maint/layout_inference/ 等源码与测试工具,系统讲解tl::Layout/tl::Fragment的数据模型、复刻(replication)语义、CuTe 布局代数及其 TileLang↔CuTe 转换、布局推断(layout inference)的三种层级与可插拔代价模型,并给出调试、验证与 Python 侧检查的完整实践路径。读完本文,你将能够理解 TileLang 中"逻辑坐标→物理坐标"的映射机制,读懂 Fragment 的打印输出与逆向打包规则,掌握tl.layout_cost_model两个策略的行为差异,并会用maint/layout_inference工具链做布局回归验证。
一、核心类型:Layout 与 Fragment
1.1tl::Layout:逻辑坐标到物理坐标的映射
tl::Layout的核心职责是把逻辑坐标映射为物理坐标,其数据模型定义在 src/layout/layout.h 的LayoutNode中,由两部分组成:
input_size_(ffi::Array<PrimExpr>):逻辑形状(logical shape),即输入各维的 extent;forward_index_(ffi::Array<PrimExpr>):前向索引,每个输出轴对应一个PrimExpr表达式。
表达式使用进程级(process-wide)占位变量书写:InputPlaceholder(i),打印时表现为_i、_j等;复刻占位变量为ReplicationPlaceholder(),打印为_rep(见 src/layout/layout.h)。
关键 API 一览(均可从 Python 侧经 FFI 调用,见 tilelang/layout/layout.py):
| API | 语义 |
|---|---|
InputShape()/OutputShape() | 输入形状 / 输出形状。注意:OutputShape()不是存储的字段,而是由分析器(Analyzer)根据各表达式的取值范围推导出来的;Python 侧对应get_input_shape()/get_output_shape() |
GetForwardIndex() | 取出前向索引表达式数组(Python:get_forward_index()/index) |
Forward(vars) | 给定输入坐标,计算物理输出坐标 |
Inverse() | 求逆映射 |
InverseWithLevel() | 带迭代映射层级信息的求逆(见下文 Fragment 的规范打包) |
Reshape() | 重设逻辑形状,支持rescale_num/rescale_den处理不同 dtype 别名视图时的元素尺寸换算 |
DetectInjective() | 校验"不同逻辑坐标 → 不同物理坐标"的单射性 |
DebugOutput() | 结构化的调试打印(Pythonrepr直接使用它) |
从 Python 构造一个Layout非常直观:Layout(shape, forward_fn),其中forward_fn接收每个维度的变量并返回前向索引表达式(详见 tilelang/layout/layout.py)。__call__与map_forward_index则用 TVM 的IndexMap完成具体坐标的求值。
1.2tl::Fragment:带线程维度的寄存器缓冲布局
tl::Fragment继承Layout,额外增加线程维度,用于描述寄存器缓冲(register buffer)中数据的物理分布(定义见 src/layout/layout.h,Python 包装见 tilelang/layout/fragment.py)。核心成员:
forward_thread_:一个PrimExpr,描述逻辑点被哪个线程持有,可能引用ReplicationPlaceholder()(_rep);replicate_size_:复刻数;thread_range_:通过BindThreadRange设置,用于 warp specialization 时偏移线程空间;前向映射本身始终保持在归一化的[0, T)范围内。
两个"推导而非存储"的量:
ThreadExtent()= 在有界定义域下max(forward_thread) + 1;OutputShape()同样由分析器推导。
复刻(replication)语义:replicate_size表示每个逻辑点由多少个线程共同持有(例如广播读场景下,每个线程都持有整份数据,即FullyReplicated)。配套操作Replicate / DeReplicate / CondenseReplicateVar用于操控复刻轴。Python 侧可分别通过replicate()、condense_rep_var()调用(见 tilelang/layout/fragment.py)。
1.3 已经踩过的三个"坑"(Gotchas)
文档明确列出了几个经过验证、极易踩中的点:
FragmentNode::GetForwardVars()会前置复刻占位变量。当replicate_size > 1时,它把ReplicationPlaceholder()前插到变量列表头部(src/layout/layout.cc),而输入占位变量始终是尾部的InputDim()条目。因此,Python 代码若把get_forward_vars()与形状逐位zip,对复刻 Fragment 而言是错误的。规范打包顺序:
(thread, slot, rep)的打包逻辑位于FragmentNode::InverseWithLevel(src/layout/layout.cc)。它将 rep 变为尾部的一个普通输入维度(extent 为ReplicateExtent(),替换占位变量),thread 作为尾部输出。于是逆映射(slot..., thread) -> (coords..., rep),rep 永远在最后。loop_partition.cc正是消费这一顺序,并在存储时生成 replica-zero 保护。需要把 Fragment 当普通多输出布局用时,请严格镜像这一打包方式。结构化打印:通过
DebugOutput()/ Pythonrepr输出,例如:
Fragment((2,) -> (2,), replicate: 256, thread: _rep, index: (_i,), thread_range: I.Range(0, 256))结构化字段从 Python 侧可直接访问:replicate_size、get_thread_size()、forward_thread、forward_index、thread_range、get_input_shape(),方便在调试与测试中断言字段值,而非匹配打印字符串。
1.4 Swizzle:共享内存的 XOR 布局
Swizzle(见 tilelang/layout/swizzle.py、src/layout/swizzle_mode.h)是共享内存中基于 XOR 的布局,用于缓解 bank conflict。它不能用 strided 布局表达;在 CuTe 侧被建模为独立的Swizzle函子(ComposedLayout),而永远不是(shape, stride)模式。
Swizzle 模式枚举定义在 src/layout/swizzle_mode.h:NONE、SWIZZLE_32B、SWIZZLE_64B、SWIZZLE_128B,其字节宽度(BBits)分别对应 0、1、2、3。此外 src/layout/layout.h 提供了MakeSwizzledLayout、MakeWgmmaSwizzledLayout、MakeTcgen05MmaSwizzledLayout、MakeFullBankSwizzleLayout等构造器,以及DetectSwizzleMode与MergeSwizzleLayouts(按更小粒度合并两个 swizzle 布局)。
二、CuTe 布局代数:TileLang 内建的全套 CuTe 实现
TileLang 为 MMA/TMA 后端维护了一套完整的 CuTe 布局代数实现,C++ 侧在namespace tvm::tl::cute(src/layout/cute_layout.h、src/layout/cute_layout.cc),Python 侧为tilelang.layout.cute(tilelang/layout/cute.py)。
2.1 基本约定
- 布局表示为
(shape, stride)的IntTuple树,列主序(column-major,第一维最快)——与 TileLang 行主序的直觉相反,是转换时最容易出错的地方; - 多输出陪域使用
ScaledBasis步长(v@axis、E<i>记法)。Python 侧E(mode)返回单位基向量,ScaledBasis(value, mode)表示value沿mode方向的缩放基。
2.2 代数操作全集
Python 侧全部以函数形式暴露(见 tilelang/layout/cute.py):
| 操作 | Python API | 说明 |
|---|---|---|
| 合并连续模式 | coalesce(layout, max_extent=None) | 支持max_extent上限 |
| 右逆 | right_inverse(layout) | 部分函数,见下方约定 |
| 左逆 | left_inverse(layout) | |
| 复合 | composition(lhs, rhs) | |
| 补 | complement(layout, cotarget) | |
| 大小 | cosize/size/coshape | 陪域大小 |
| 划分 | logical_divide/logical_product/tiled_product/blocked_product | |
| 过滤/限制 | filter/restrict | |
| 变形 | with_shape、make_layout、make_column_major_layout、make_row_major_layout、make_identity_layout | make_layout在只传 shape 时默认生成列主序布局 |
| 解析/打印 | parse/Print | 精确的 CuTe 拼写 |
2.3 转换器:probe-then-prove(先探测、后证明)
三个*FromTileLang转换器(tilelang/layout/cute.py 与Layout.from_tilelang、Layout.from_tilelang_hierarchical、ComposedLayout.from_tilelang):
LayoutFromTileLang:针对单一扁平输出。多输出的tl::Layout按行主序序列化,因此输出[thread, slot]会得到thread * slots + slot的扁平布局;LayoutFromTileLangHierarchical:按轴(per-axis)恢复基准轴上的映射;ComposedLayoutFromTileLang:恢复 swizzle。
三者都采用probe-then-prove:先在 one-hot 点上数值探测 stride,再符号化证明等价性——错误的恢复不可能漏网;失败时返回None/nullopt,而不是抛出异常。
2.4 失败约定(必须遵守)
- 只有上述三个
*FromTileLang转换器返回 Optional; - 其余所有代数操作在其前置条件不满足时直接 ICHECK 崩溃,包括:composition 的整除性、probe 中出现非常数 extent、complement 非单射、restrict 的秩不匹配等。因此输入不可信时,务必包一层调用做保护;
RightInverse是部分函数:它静默丢弃 stride-0 与非 const-stride 的模式,只对最大连续链求逆。请通过size(right_inverse(F)) == size(F)校验双射性。
2.5 转换注意事项
转换器只读取GetForwardIndex();Fragment 的forward_thread与复刻会被静默忽略,除非你先把它们打包进一个普通的多输出 Layout(使用上文 1.3 的规范打包方式,用尾部输入变量替换ReplicationPlaceholder)。如果复刻变量泄漏进探测过程,转换会优雅地返回None。
三、布局推断(Layout Inference):三级策略与代价模型
3.1 整体流程
tl.LayoutInference(实现于 src/transform/layout_inference/layout_inference.cc)为每个 fragment 缓冲与并行循环嵌套分配布局,分三个层级:
- strict(严格):来自注解与 MMA 指令强加的布局,直接采用;
- common(公共):通过共享缓冲做 BFS 传播;
- free(自由):对每个连通分量(connected component),尝试把每个成员作为推断根,保留代价最低的方案。
推断结果以 IR 注解的形式落盘:
layout_map(Buffer → Layout,挂在 SBlock 上);parallel_loop_layout(Fragment,挂在最外层并行 For 上);- 配套注解还有
parallel_loop_predicate、parallel_loop_requires_padding_guard、coalesced_width(见 src/layout/layout.h)。
ParallelLoopLayoutValidator(src/transform/layout_inference/parallel_loop_layout_validator.h)负责强制注解契约。
3.2 可插拔的"最廉价"策略
"最廉价"由可插拔策略决定,实现于 src/transform/layout_inference/layout_cost_model.h 与 layout_cost_model.cc,通过环境变量tl.layout_cost_model选择:
"register-count"(默认):仅按寄存器槽位总数排序,RegisterCountCostModel;"io-aware"(可选开启):IOAwareCostModel遍历分量内所有触碰全局内存的语句(fragment↔global 拷贝、直接访问 global 的并行循环),在尝试布局下按max(bandwidth bytes, issue bytes)计费,并在 CuTe 代数上做符号化评分(pack →LayoutFromTileLang→RightInverse→Composition;从合并后的模式读出向量宽度;按 warp/step 粒度统计 segment 数)。寄存器作为字典序 tiebreak。模型之外的语句按保守最坏情况计费——一次尝试绝不能从"不透明"中获利。
评分结构为AttemptCost{mem, regs},比较规则:mem优先,regs次之(见 layout_cost_model.h);其中mem还包含寄存器数组因线程相关索引而溢出到 local memory 的流量估算。
3.3 硬件几何参数化
- 通道宽度(lane width):
MaxVectorLoadBits(src/transform/loop_vectorize.h),与向量化器共享,保证模型对宽度的"信念"与代码生成一致; - warp 大小:取自 target 的
thread_warp_size; - segment 粒度:128B(见
BindMemoryGeometry)。
3.4 评分公式的 Python 奇偶校验
评分公式由 Python 奇偶校验守护:maint/layout_inference/run.py --cute将符号化评分器与独立的 NumPy 精确枚举 oracle 对比。修改layout_cost_model.cc时必须同步更新 maint/layout_inference/cute_model.py,保持二者一致。
四、下游消费者:布局如何影响代码生成
布局注解最终被以下关键 pass 消费:
- src/transform/loop_partition.cc:通过 fragment 逆映射按线程划分并行循环,并为复刻存储生成 replica-zero 保护;
- src/transform/loop_vectorize.cc:规划向量宽度(
GetVectorizeSize、IndicesCanVectorize),代价模型镜像其判断; - TMA/MMA 降低:src/cuda/op/tma_layout.cc 与
producer_consumer_ws.cc通过ComposedLayoutFromTileLang恢复共享缓冲的 swizzle;tcgen05/wgmma 宏生成器(tilelang/cuda/intrinsics/macro/)则通过to_tilelang/from_tilelang_hierarchical往返 TMEM 布局。
五、调试与验证工具
5.1maint/layout_inference/验证框架
该目录是布局验证工具链(详见 maint/layout_inference/README.md),每个cases/*.py构造一个已知正确答案的PrimFunc,驱动run.py在两种代价策略下分别推断,并与expected/*.json中的黄金布局快照对比。核心用法:
python run.py # 校验所有 case 与 expected/ 一致 python run.py --case NAME # 按子串过滤单个 case python run.py --show # 同时打印推断出的布局 python run.py --record # 用当前行为重写黄金快照(录完必须人工审查 diff) python run.py --anchor # 完整 lower 后,对照 VECTOR_ANCHOR 检查设备端 TIR 的每缓冲向量宽度 python run.py --cute # 符号评分器 vs 独立精确枚举 oracle 的奇偶校验各选项的含义(来自 maint/layout_inference/run.py 的 docstring 与 README):
--anchor闭环验证"模型假设的向量宽度"与"向量化器实际发出的宽度"是否一致;不一致说明模型的宽度信念与代码生成发生了漂移——这正是共享MaxVectorLoadBits策略要防止的问题;--cute中,cute_model.py按(coords..., rep) -> [thread, slot]的规范打包(FragmentNode::InverseWithLevel)把每个 fragment 经cute.Layout.from_tilelang转成单一strided 布局,用right_inverse+composition推导字节地址布局,从合并的 slot 模式读出向量宽度;oracle.py则是独立的 numpy 实现。每个黄金布局都以 load 和 store 两种方式双路径评分(含复刻门控),(V, issue, bw, segments)必须完全一致。README 记录的现状是:88/88 条语句匹配、100% 转换命中率;--record只是录制而非批准:录制后必须人工阅读expected/下的 diff,确认每个变更的布局都是预期行为;即使录制模式下,结构不变量(case 的check回调)依然强制执行。
每个 case 文件定义VARIANTS(名字 → 返回新PrimFunc的可调用对象)与可选的check(variant, model, result)断言。result中每个布局都是结构化 dict(common.layout_to_dict),例如:
{"kind": "Fragment", "input_shape": [2], "output_shape": [2], "forward_index": ["_i"], "replicate": 256, "threads": 256, "forward_thread": "_rep", "thread_range": [0, 256]}因此黄金 diff 能精确定位哪个字段发生了移动(如replicate: expected 1, got 128),check也能断言字段值而非匹配打印格式。当前 8 个 case 覆盖的场景见下表(摘自 maint/layout_inference/README.md):
| case | 固定了什么行为 |
|---|---|
elementwise_copy | 基线:两种模型必须对合并、向量化的往返布局达成一致,主等分锚点 |
fp8_copy | 1 字节 dtype:共享宽度策略宽端上的 16 元素向量宽度 |
broadcast_read | Issue #1729:两模型有意分歧——register-count 保留线程折叠的遗留病态布局(黄金记录之),io-aware 必须选择全复刻 + 非复刻合并循环(由check强制) |
transposed_store | 加载与存储把布局拉向相反方向;fp32 变体两模型选择不同布局,值得基准测试 |
mixed_dtype_chain | fp16/fp32 fragment 对共处一个分量,向量化按冲突 dtype 定宽 |
reduce_broadcast | Softmax 形行归约 + 广播消费:最常用的真实 kernel 分量,两模型一致 |
offset_region_copy | 区域偏移携带块索引("外部变量")的多块平铺拷贝:偏移区域必须与零偏移区域排序一致 |
shared_staging | global→shared→fragment→global 链:shared 侧拷贝在 io 模型之外,fragment 仅由拷出决定 |
扩展规范:每次修改推断、代价模型或转换器时,都应为该变更新增一个 case;黄金快照先--record录制,再由人工审查。
5.2 其他诊断手段
- DLOG 日志:推断与代价模型 pass 在 debug 构建下通过 DLOG 记录决策;
- 布局可视化:
tl.layout_visualization_enable可渲染布局; - Python 侧检查:在模块上运行
tl.transform.LayoutInference()并读取注解(提取惯用法见 maint/layout_inference/common.py);或调用cute.Layout.from_tilelang(...)查看布局的(shape, stride)正规形式。
六、文件地图速查
| 领域 | 文件 |
|---|---|
| 核心类型 | src/layout/layout.h、src/layout/layout.cc、tilelang/layout/layout.py、tilelang/layout/fragment.py |
| Swizzle | src/layout/swizzle_mode.h、tilelang/layout/swizzle.py |
| CuTe 代数 | src/layout/cute_layout.h、src/layout/cute_layout.cc、tilelang/layout/cute.py |
| 推断 | src/transform/layout_inference/layout_inference.cc |
| 代价模型 | src/transform/layout_inference/layout_cost_model.h、layout_cost_model.cc |
| MMA 布局 | src/layout/gemm_layouts.cc、src/layout/tcgen05_layout.h |
| 验证工具链 | maint/layout_inference/(含 README.md、run.py、cute_model.py、oracle.py) |
七、总结与最佳实践清单
- 坐标系直觉要切换:TileLang 布局是"逻辑→物理"映射,输出形状是推导值;CuTe 布局是列主序
(shape, stride),与行主序直觉相反; - 处理 Fragment 时牢记三个 Gotcha:
GetForwardVars()前置 rep 变量;规范打包为(slot..., thread) -> (coords..., rep)且 rep 在尾部;打印输出中的结构化字段可直接在 Python 断言; - 转换器与代数操作的失败语义不同:只有
*FromTileLang返回 Optional,其余操作前置条件失败即 ICHECK 崩溃,不可信输入务必包一层调用;RightInverse是部分函数,用size比对校验双射; - 自由模式搜索的代价策略:默认
register-count,io-aware按全局内存流量(带宽字节 vs issue 字节取最大)符号化计费并共享向量化器的宽度策略;修改评分公式必须同步cute_model.py并保持--cute奇偶校验通过; - 回归纪律:任何对推断、代价模型或转换器的改动,都在
maint/layout_inference下新增 case、录制黄金、人工审查后再提交。
【免费下载链接】tilelangDomain-specific language designed to streamline the development of high-performance GPU/CPU/Accelerators kernels项目地址: https://gitcode.com/GitHub_Trending/ti/tilelang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考