Skip to content

Mosaic 布局推断

本页中的所有地址、符号、op-name 字符串和错误字符串均适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d,build libtpu_lts_20260413_b_RC00)。其他版本会有所不同。

摘要

Mosaic 的布局阶段是在应用器(apply-vector-layout)之前运行的一个三遍求解器。本页覆盖这三遍:16 阶段流水线中的 InferMemRefLayout(阶段 4)、TilingPropagation(阶段 9)和 InferVectorLayout(阶段 10)。它们共同计算物理 memref 分块,以及应用 pass 会消费的逐值 in_layout/out_layout VectorLayout 属性。VectorLayout 值类型本身,包括 (sublane, lane) 分块对、偏移、ImplicitDimtilesPerVreg/tileArrayShape,位于 Mosaic VectorLayout;本页只记录这些布局是如何被选择的。

这个模型类似带半格的数据流分析,但有一个决定一切的设计选择:InferVectorLayout 从不插入 relayout。 LLVM pass 在求解寄存器类别时可能会拆分一个值或插入拷贝,而这个 pass 会为每个值精确选择一个 VectorLayout,以操作数驱动的贪心方式进行,并用布局格上的 VectorLayout::join meet 来调和多个操作数需求。当调和确实不可能时(例如 transpose 喂给 matmul),它只会发出一个与消费者 in_layout 不匹配的生产者 out_layout,并让独立的 RelayoutInsertion pass(阶段 11)弥合差距。这就是为什么 apply pass 可以断言 in == out,并把问题归因于缺少 relayout-insertion 运行。

本页按流水线顺序组织为三个 pass:先是 InferMemRefLayout(它产生其他部分读取的 memref TiledLayoutAttr),然后是 TilingPropagation(它把该分块穿到 load/store/DMA 位置),再到 InferVectorLayout(op-TypeID 分发和逐 op 规则);随后是它们共享的 join/generalizes 格,以及一个端到端的 matmul 示例。

对于重新实现,契约是:

  • memref 分块公式。 getTilingFactor,即 sublane-tile 选择器:packing = 32/bw(32/bw)*sublanes 的 “large 2nd-minor” 乘数表、可整除性/二次幂回退,以及 inferLayout 在上一层应用的 leading_tile_rows arg-attr 覆盖(完全绕过 getTilingFactor)。这是后续 load/store 向量规则要匹配的分块的生产者。
  • 传播不动点。 propagateTiling 在 26 项 op-name→rule StringMap 上的 worklist、看穿 erase_layout 的消费者规则、memref_slice 的 tile-stride 折叠,以及延迟的 EraseLayoutOp 删除。
  • 向量 op-TypeID 分发。 inferBlock 的约 35 分支 TypeID switch(apply StringMap 的生产者侧对应物)、bitwidth-cast 预分发,以及 elementwise/extension 回退。
  • 逐 op 规则。 共享的 bitwidth → native-tiling 骨架,以及达到重新实现粒度的 matmul / elementwise / load-store / broadcast / rotate 规则主体。
  • 格。 VectorLayout::join(选择更具体兼容布局的 meet)和 generalizes(≤ 关系),以及为什么 join 失败是 relayout-insertion 信号而不是错误。
Pass 1(阶段 4)mlir::tpu::InferMemRefLayoutPass::runOnOperation @ 0x132c1820(create: 0x132c0f00
Pass 2(阶段 9)mlir::tpu::TilingPropagationPass::runOnOperation @ 0x132e0dc0(create: 0x132e0900
Pass 3(阶段 10)mlir::tpu::InferVectorLayoutPass::runOnOperation @ 0x132c3600(create: 0x132c2c20
向量分发核心(anon)::VectorLayoutInferer::inferBlock @ 0x132c3dc0(反编译约 1400 行;约 35 分支 TypeID switch)
Memref 分块核心inferLayout @ 0x132bef00getTilingFactor @ 0x132bed80
传播不动点propagateTiling @ 0x132e10a0;规则表 rules() @ 0x132e15e0(26 项)
mlir::tpu::VectorLayout::join @ 0x14a957c0generalizes(≤);源码 layout.h:320
输入/输出读取 kernel-arg/alloca memrefs + leading_tile_rows arg-attr;写入 memref TiledLayoutAttr 和逐 op 的 in_layout/out_layout ArrayAttrs
关键设计规则推断为每个值选择一个布局;它插入 tpu.relayout,这由 RelayoutInsertion(阶段 11)完成
置信度HIGH(由符号/字符串锚定),除非某行或标注另有说明

这三个 Pass 位于何处

Mosaic 布局阶段由四个 pass 组成;本页覆盖其中三个,apply-vector-layout 是第四个。数据流严格为生产者→消费者:

text
stage 4   infer-memref-layout    InferMemRefLayoutPass   ── memref args/allocas get a TiledLayoutAttr
              │   (sublane×lane physical tiling + packing tile; insert erase_layout / reinterpret_cast)

stage 9   tiling-propagation     TilingPropagationPass   ── push the tiling through erase_layout / slice /
              │   casts to every load/store/DMA; drop the EraseLayoutOps once consumed

stage 10  infer-vector-layout    InferVectorLayoutPass   ── every vector op gets in_layout / out_layout
              │   (reads the memref tiling at load/store sites; join-reconciles multi-operand demands)

stage 11  relayout-insertion     RelayoutInsertionPass   ── where producer.out_layout != consumer.in_layout,
              │   insert tpu.relayout (so the applier never reconciles)   [not this page]

stage 12  apply-vector-layout    ApplyVectorLayoutPass    ── materialize native vregs + shuffles  [not this page]
```text

重新实现者必须内化的架构事实是:阶段 4/9/10 都是*分析*,它们添加注解,从不重排数据。它们唯一会修改的 IR 是 memref 类型(阶段 4 重新给 args/allocas 定型并插入 `erase_layout`/`reinterpret_cast`)、`erase_layout` 周围的操作数连线(阶段 9 重新穿线然后擦除它),以及 `in_layout`/`out_layout` 属性(阶段 10)。所有实际的 vreg 物化和 relayout shuffle 都发生在之后的阶段 11–12。

---

## Pass 1 — InferMemRefLayout(阶段 4)

### 目的

为每个内核参数和 `alloca` memref 分配一个 `mlir::tpu::TiledLayoutAttr`:物理 sublane×lane 分块加上 tile stride,包括 sub-32-bit 的 packing tile。它精确地在向量 pass 之前运行,这样阶段 10 的 load/store 向量规则就有一个具体的 memref 分块,可用来与其 vreg 布局匹配;它们不会发明分块,而是读取这个分块。

### 入口点

```text
InferMemRefLayoutPass::runOnOperation (0x132c1820)        ── requires hardware_generation (struct +114)
  └─ inferFunc (0x132c0560)                               ── per-func; single-block required
       ├─ inferMemref (0x132bfd60)   [per arg]            ── semaphore→contiguous; else inferLayout
       │    └─ inferLayout (0x132bef00)                   ── the core tiling math
       │         └─ getTilingFactor (0x132bed80)          ── the sublane-tile chooser
       └─ inferOp (0x132c01a0)       [per body op]        ── memref.alloca / tpu.alloca_semaphore results

算法

inferFunc 将函数的 memref 参数和所有 alloca 结果重新定型为 tiled 类型,然后通过一个 erase_layout(或 reinterpret_cast)为每个值建立别名,使函数体其余部分在传播重新穿线之前仍看到 untiled 视图:

c
function inferFunc(func, gen, target_shape, flags):          // 0x132c0560
    require func.body.hasOneBlock()                          // "Functions should only have a single block"
    core_type = TPUDialect::GetCoreTypeAttr(func)
    is_arg_in_smem = (core_type == 0x100000001)              // default MemorySpace for inferMemref
    for arg in func.arguments where isa<MemRefType>(arg):
        lead_rows = consume_arg_attr(arg, "leading_tile_rows")   // i32 override; removed after read
        tiled = inferMemref(arg.type, gen, target_shape, flags,
                            is_arg=1, lead_rows, default_space=is_arg_in_smem)
        set_arg_type(arg, tiled)
        if canReinterpretToUntiledMemref(tiled):            // 2-D, 32-bit, contiguous, ...
            alias = insert tpu.reinterpret_cast(arg)        // fold tile strides into linear strides
        else:
            alias = insert tpu.erase_layout(arg)            // typed-but-untiled body alias
        replaceAllUsesExcept(arg, alias, /*except=*/alias)
    rebuild FunctionType with the new arg types
    for op in func.body: inferOp(op, gen, target_shape, flags, default_space)   // 0x132c01a0
```text

`inferOp` 处理 memref **结果**:对于 `memref.alloca` 和 `tpu.alloca_semaphore`,它会在结果 memref 上调用 `inferMemref` 并用一个 `EraseLayoutOp` 包裹它,然后递归进入嵌套 region。所有其他 op 原样返回 success。

`inferMemref` 解析 memory space,然后委托分块决策:

```c
function inferMemref(memref, gen, target_shape, flags, is_arg, lead_rows, space):  // 0x132bfd60
    if memref has a non-tpu MemorySpace attr: return memref          // already placed; leave it
    if elem is semaphore / dma-semaphore:
        return TiledLayoutAttr::getContiguous(...) in space=4        // semaphore_mem, untiled
    layout = inferLayout(memref, ctx, target_shape, flags, is_arg, lead_rows)  // 0x132bef00
    checkTiles(layout)                                               // 0x132bfac0 — validates the tiling
    return MemRefType(elem, shape, layout, resolved_MemorySpaceAttr)

inferLayout 是核心分块数学。两种模式分别是一维(沿 minor 轴一个 tile)和二维及以上(由 getTilingFactor 选择 sublane tile,lane tile = target_lane),并为 sub-32-bit 元素类型追加 packing tile:

c
function inferLayout(memref, ctx, target_shape, flags, is_arg, lead_rows):  // 0x132bef00
    require target_shape.size() == 1 or == 2                  // infer_memref_layout.cc:112
    bw = bitwidth(elem)
    if layout already isa<TiledLayoutAttr>:
        if lead_rows requested and mismatches existing sublane tiling:
            error "Trying to infer memref layout with sublane tiling <X>, but the memref"
                  " already has sublane tiling <Y>"
        return existing
    if layout isa<AffineMapAttr>: require identity else "Non-identity affine layout"
    elif layout isa<StridedLayoutAttr>: ok
    else: error "Unrecognized layout annotation"
    require isIntOrFloat(elem)                                // "Invalid element type for memref"
    if shape empty: return TiledLayoutAttr::get(ctx, 0,0,0,0)  // scalar
    lane = target_shape[-1]                                   // e.g. 128
    if rank == 1:
        tile = ((32/bw) << (gen < 4)) * lane                 // {2,...} packing descriptor 0x200000000
        if bw < 32: append packing tile (32/bw, 1)
        return single-tile layout along the minor axis
    else:                                                     // rank >= 2
        if lead_rows != 0:                                    // leading_tile_rows arg-attr overrides
            sublane = lead_rows
        else:
            sublane = getTilingFactor(shape[-2], gen, target_shape[0], flags, bw, is_arg, /*1d=*/0)
        L = TiledLayoutAttr::getContiguous(ctx, {sublane, lane}, shape)   // computes tile strides
        if bw < 32: append packing tile (32/bw, 1)
        return L
    // non-power-of-2 or bw > 32 → "Unsupported bitwidth: <bw>"
```text

`getTilingFactor` 选择 sublane tile。它的第三个参数是**目标 sublane 数**(`target_shape[0]`,通常为 8):`inferLayout` 在这里传入 `*a3`,而不是 `leading_tile_rows`。packing factor `32/bw` 是下限;逐 bitwidth 的 “large 2nd-minor” 乘数(packing factor 乘以目标 sublane,受三个 `TpuTilingFlags` 字节、`is_arg` 和 `gen` 门控)可以提高它;可整除性/大小回退会让它对实际维度保持合法。`leading_tile_rows` arg-attr 在上一层 `inferLayout` 中处理,存在时会直接设置 sublane tile,此时不会为该 arg 调用 `getTilingFactor`:

```c
function getTilingFactor(dim, gen, sublanes, flags, bw, is_arg, is_1d):   // 0x132bed80
    require isPowerOf2_32(bw)                                 // infer_memref_layout.cc:53
    require 2 <= bw <= 32                                     // :54 / :55
    packing = 32 / bw
    base    = max(packing, sublanes)
    factor  = base
    if not is_1d:                                             // "large 2nd-minor" multiplier
        switch tzcnt(bw):                                     // bw2→1, bw4→2, bw8→3, bw16→4
            case bw==2 : candidate = 16 * sublanes            // unconditional
            case bw==4 : candidate =  8 * sublanes  if flags[2]
            case bw==8 : candidate =  4 * sublanes  if flags[1]
            case bw==16: candidate =  2 * sublanes  if (flags[0] or (not is_arg and gen >= 6))
        if candidate set: factor = candidate
    if dim % factor != 0: factor = base                       // divisibility fallback
    if dim < factor:                                          // walk powers of two up
        f = packing << (gen < 4)
        while f < min(dim, base): f *= 2
        factor = f
    return factor

每个分支中的乘数都是 (32/bw) * sublanes,也就是扩大 sublane tile,使该 tile 容纳一个完整 vreg 数量的打包子元素。对于常见的 sublanes == 8:bw=2 → 128,bw=4 → 64,bw=8 → 32,bw=16 → 16;对于 f32(bw=32),switch 不会进入,tile 保持为 base = max(1, 8) = 8

GOTCHA — memref 分块依赖 generation 和 arg,而不仅依赖 bitwidth。 getTilingFactor 会读取 gen、三个 TpuTilingFlags 字节、is_arg 标志和目标 sublane 数。只从 32/bw 推导 sublane tile 的重新实现,对 f32(factor 8)会给出正确答案,但对 bf16/int8/int4 会出错;在这些类型上,当 flags 和可整除性允许时,“large 2nd-minor” 分支会扩大 tile(sublanes=8 的 bf16 → 2*8 = 16)。门控随 bitwidth 不同:bw=2 分支无条件,bw=4 需要 flags[2],bw=8 需要 flags[1],bw=16 在设置了 flags[0] memref 是 gen >= 6 上的非 arg 时采用宽 tile(即 flags[0] or (not is_arg and gen >= 6))。这些 flags 恢复为 flags[0..2];其人类可读名称是从用途推断的(MEDIUM)。

QUIRK — erase_layoutreinterpret_cast 由一个未反编译的谓词决定。canReinterpretToUntiledMemref 成立时,inferFunc 插入 tpu.reinterpret_cast(把 tile stride 折叠为线性 stride 的 untiled 视图),否则插入 tpu.erase_layout(typed-but-untiled 别名)。调用点已恢复;其函数体(2-D/32-bit/contiguous 资格测试)未反编译。可把选择视为:可 reinterpret 的 contiguous 32-bit 2-D → reinterpret_cast;其他所有情况 → erase_layout(精确谓词 LOW)。

函数表

函数地址作用
InferMemRefLayoutPass::runOnOperation0x132c1820pass 入口;需要 hardware_generation(+114);调用 inferFunc
createInferMemRefLayoutPass0x132c0f00工厂(gen、target span、TpuTilingFlags);pass 结构 0x328 B
inferFunc0x132c0560逐函数 arg 重新定型 + 插入 erase_layout/reinterpret_cast
inferOp0x132c01a0alloca/alloca_semaphore 做 memref 结果推断
inferMemref0x132bfd60semaphore→contiguous;否则 inferLayout + checkTiles
inferLayout0x132bef001-D/2-D/packing 分块数学
getTilingFactor0x132bed80sublane-tile 选择器(上面的公式)
checkTiles0x132bfac0验证已解析的分块

Pass 2 — TilingPropagation(阶段 9)

目的

InferMemRefLayout 把每个 tiled memref 放在 erase_layout/reinterpret_cast 别名后面,因此函数体仍引用一个 untiled 视图。TilingPropagation 是将物理分块向前推送到每个 load / store / DMA / memref-shape op 的不动点:把它们的 memref 操作数改写为直接引用 tiled memref(看穿 erase_layout),然后在每个 EraseLayoutOp 不再有剩余 use 后删除它。此 pass 之后,每个 memory op 都引用 tiled memref,这正是阶段 10 load/store 向量规则要读取的内容。

入口点

text
TilingPropagationPass::runOnOperation (0x132e0dc0)         ── builds a PropagationContext from options
  └─ propagateTiling(ctx, entry_block) (0x132e10a0)        ── the worklist fixpoint
       ├─ rules() (0x132e15e0)                             ── 26-entry op-name → rule StringMap
       ├─ propagate_layout_to_consumer_rule (0x132e40a0)   ── 20 consumer ops: see-through erase_layout
       └─ tpu_memref_slice_rule (0x132e1b20)               ── slice: re-thread + fold tile strides
```text

`PropagationContext` 携带 `{target_sublane, target_lane}`(结构 `+0x150`)、收集到的 `EraseLayoutOps` 向量(`+16`),以及 `sparse_core` `cl::opt` bool(结构 `+472`)。`createTilingPropagationPass({sublane,lane}, sparse_core)`(`0x132e0900`)构造一个嵌套在 `func.func` 上、大小为 `0x228` 字节的 pass。

### 算法

```c
function propagateTiling(ctx, block):                        // 0x132e10a0
    for op in block, IN ORDER:
        if isa<EraseLayoutOp>(op):
            ctx.erase_list.push(op)                          // defer removal to the end
        for region in op.regions: for b in region:
            propagateTiling(ctx, b)                          // depth-first into nested regions
        rule = rules().lookup(op.name)                       // xxh3_64 + StringMapImpl::FindKey
                                                             // miss → StringMap default-bucket entry
        if rule(ctx, op) == failure: return failure
    for erase_op in ctx.erase_list:                          // cleanup
        if erase_op.use_empty() or all_uses_in_sc_tpu(erase_op):  // "sc_t"/"up" literal test
            erase_op.erase()
        else:
            return error "Failed to propagate the layout to all operations"
    return success

“all uses in sc_tpu” 谓词是字面字符串测试 (*str ^ 0x745F6373) | (*(str+4) ^ 0x7570),也就是 op-name 前缀等于 "sc_t" 然后 "up"sc_tpu)。SparseCore op 合法地保留 untiled 别名,因此它们不会阻止擦除。

中央规则会看穿 erase_layout

c
function propagate_layout_to_consumer_rule(ctx, op):          // 0x132e40a0
    for operand in op.operands:
        def = operand.getDefiningOp()
        if isa<EraseLayoutOp>(def):
            op.setOperand(operand_index, def.getOperand(0))   // use the TILED memref directly
    return success                                            // always succeeds
```text

硬规则是 `tpu_memref_slice_rule`。它必须重新给 slice 分块,并且当它折叠一个 4 级 tiled 32-bit memref 的 2nd-minor 维时,用发出的算术把 tile stride 折叠进 slice 的 base index:

```c
function tpu_memref_slice_rule(ctx, slice):                   // 0x132e1b20
    src = slice.source
    if not isa<EraseLayoutOp>(src.getDefiningOp()): return success   // no-op
    tiled_src = src.getDefiningOp().getOperand(0)
    verifyOffsetAndSizeTileAlignment(slice)
    if collapses 2nd-minor of a 4-level-tiled 32-bit memref:
        // fold the tile strides into the physical base offset:
        new_idx = (idx / tile_size) * tile_stride + (idx % tile_size)
        // emitted via arith.constant + divui + remui + muli + addi (createOrFold)
        require isGuaranteedDivisible(off, stride, 128)
    elif contiguous & reinterpretable:
        require isGuaranteedDivisible(off, 8, 128)            // "Slice offset for 32-bit 1D
                                                             //  memrefs must be a multiple of 8"
        emit ReinterpretCastOp over an untiled TiledLayoutAttr
    new_slice = rebuild MemRefSliceOp on tiled_src
    result = wrap new_slice in a fresh EraseLayoutOp; ctx.erase_list.push(it)
    return success

另外五个 memref-shape 规则(squeeze/reshape/bitcast/reinterpret_cast/memref.cast0x132e2a60..0x132e3ee0)类似地把 tiled memref 重新穿过 cast,并重新计算结果 tile stride;其逐规则 stride 数学经过抽样,但没有逐个反编译(HIGH)。

26 项规则表

rules()0x132e15e0)是一个 op-name→rule StringMap,是 apply pass 的 49 项 rewrite 表在传播类型上的同级物。与其列出 26 行几乎相同的内容,不如说明其形状:6 个 memref-shape op 带有专门规则;另外 20 个 “consumer” op 共享 propagate_layout_to_consumer_rule

分组Op-name规则 fn地址
memref slicetpu.memref_slicetpu_memref_slice_rule0x132e1b20
memref squeezetpu.memref_squeezetpu_memref_squeeze_rule0x132e2a60
memref reshapetpu.memref_reshapetpu_memref_reshape_rule0x132e33a0
memref bitcasttpu.memref_bitcasttpu_memref_bitcast_rule0x132e37a0
reinterpret casttpu.reinterpret_casttpu_reinterpret_cast_rule0x132e3e60
memref castmemref.castmemref_cast_rule0x132e3ee0
consumers(×20)tpu.{load,store,strided_load,strided_store,vector_load,vector_store,vector_load_idx,vector_store_idx,enqueue_dma,enqueue_indirect_dma,wait_dma2,wait_indirect_dma,log_buffer,fetch_and_add_sync,sem_signal,sem_wait,sem_read}, memref.{store,load,reinterpret_cast}propagate_layout_to_consumer_rule0x132e40a0

四个 op-name 字符串 tpu.vector_storetpu.enqueue_indirect_dmatpu.wait_indirect_dmatpu.fetch_and_add_sync 是从 0x869EF8B / 0x879F7AE / 0x879F798 / 0x8730A53 处的 .rodata 解析出的。

NOTE — 分发机制与 apply pass 共享。 查找过程是 xxh3_64bits + StringMapImpl::FindKey,miss 时落到 default-bucket fallback,与 apply pass 的 applyLayoutOp StringMap 完全相同。重新实现者可以为两个生产者侧 map 使用同一个 StringMap 实现。

函数表

函数地址作用
TilingPropagationPass::runOnOperation0x132e0dc0pass 入口;构造 PropagationContext
createTilingPropagationPass0x132e0900工厂({sublane,lane}sparse_core);结构 0x228 B
propagateTiling0x132e10a0worklist 不动点 + 延迟的 EraseLayoutOp 删除
rules()0x132e15e026 项 op-name→rule StringMap
propagate_layout_to_consumer_rule0x132e40a0为 20 个 consumer op 看穿 erase_layout
tpu_memref_slice_rule0x132e1b20slice 重新分块 + tile-stride 折叠
tpu_memref_{squeeze,reshape,bitcast}_rule0x132e2a60/0x132e33a0/0x132e37a0重新穿过 cast + 重新计算 stride
tpu_reinterpret_cast_rule / memref_cast_rule0x132e3e60 / 0x132e3ee0重新穿过 cast + 重新计算 stride

Pass 3 — InferVectorLayout(阶段 10)

目的

为每个 op 标注 in_layout ArrayAttr(每个操作数一个 VectorLayout,标量为 kNoLayout)和 out_layout ArrayAttr(每个结果一个)。这正是 apply pass 读取的精确属性的生产者。它实现为一个 op-TypeID switch,是 apply pass 的 op-name StringMap 在生产者侧的对应物,但由于 inferer 是单个翻译单元,它使用直接的 TypeIDResolver<> 指针相等。

入口点

text
InferVectorLayoutPass::runOnOperation (0x132c3600)        ── requires hardware_generation (pass +114)
  │   constructs a VectorLayoutInferer on the stack:
  │     +0  hardware_generation     +24 target_sublane (8)   +32 target_lane (128)
  │     +TpuTilingFlags (+214/+164/+568), large-2nd-minor bool (+1056), reduce bool (+1138)
  │   requires single-block func ── "Only one block functions supported"
  └─ VectorLayoutInferer::inferBlock(block, fn::return_handler) (0x132c3dc0)   ── per-op driver
```text

### 算法

`inferBlock` 是逐 op driver。对终结符之前的每个 op,它会运行 guard,收集操作数布局,判断标量或向量,运行 bitwidth-cast 预分发,然后进入 TypeID switch,最后走 fallback:

```c
function inferBlock(block, terminator_handler):              // 0x132c3dc0
    for op in block until terminator:
        // 1. pre-attached guard
        if (op has in_layout or out_layout) and not isa<AssumeLayoutOp>(op):
            error "layout attributes already attached"
        if isa<AssumeLayoutOp>(op):
            require op has both layouts                       // "expect layout attributes in tpu::AssumeLayoutOp"
            continue                                          // pass through

        // 2. collect operand layouts (skipped for broadcast / extract_strided_slice)
        if not isa<vector::BroadcastOp, ExtractStridedSliceOp>(op):
            operand_layouts = getLayoutFromOperands(op)       // 0x132c59a0; kNoLayout for scalars
        else:
            set inferer+44 lane-replicate flag if an operand is replicated along an axis

        // 3. scalar test — no VectorType among operands OR results
        if no operand and no result is a VectorType:
            setInLayout(op, kNoLayout per operand)            // 373/374 CHECK path
            if op.getNumResults() == 0: continue

        // 4. bitwidth-cast pre-dispatch  (extsi/extf/sitofp/uitofp/trunci/truncf/fptosi/fptoui/extui)
        if isConversionOp(op):
            bin  = operand_bitwidth(op)                       // Float8EXMY counts as 8; i1 special
            bout = result_bitwidth(op)
            if   bout > bin:  return inferExt(op)              // 0x132c5be0  widening
            elif bout < bin:  return inferTrunc(op)            // 0x132c6600  narrowing
            else:             return inferElementwise(op)      // extui i1 (bout == bin)

        // 5. select / cmp guards
        if isa<arith.select, arith.cmpi, arith.cmpf>(op):
            return inferElementwise(op)                        // "Only one side of arith/cmp is a vector?"

        // 6. op-TypeID switch (≈35 cases) → infer(<Op>)        [table below]
        switch TypeID(op): ...

        // 7. fallback
        else if hasElementwiseMappableTraits(op): inferElementwise(op)
        else if extensions::canInferVectorLayout(op):          // 0x13246280
                 extensions::inferVectorLayout(op)             // 0x132462a0  out-of-tree ops
        else: error "Not implemented: Unsupported operation: <op> in infer-vector-layout pass"

        // 8. post-checks
        re-assert 373/374 CHECKs; clear inferer+44 lane-replicate flag

每个 op 会重新断言两个不变量(infer_vector_layout.cc:373/374):每个有结果的 op 都带有 out_layout;每个有操作数的 op 都带有 in_layoutsetInLayout0x14b75c60)、setOutLayout0x14b75e40)和 setLayout0x14b75fa0/0x14b75fe0/0x14b76020)是写入 ArrayAttrs 的自由函数;kNoLayout 是一个模块全局哨兵 VectorLayout,标记标量/非向量操作数槽位。

QUIRK — broadcastextract_strided_slice 会有意跳过操作数布局收集。 对这两个 op,inferBlock 不会调用 getLayoutFromOperands;相反,当某个操作数沿某个轴被 replicated/singleton 时,它会设置 inferer 的 +44 lane-replicate 标志。该标志会让后续 getLayout 读取强制为源采用 lane-replicated 布局。对这些 op 运行统一操作数收集路径的重新实现会读取错误的源布局。

QUIRK — extension 布局由 packing 方向选择,而不是由 op 名称选择。 i1arith.extuibout == bin,会路由到 inferElementwise;而 arith.extsi/tpu.extf 变宽(inferExt),arith.trunci/tpu.truncf 变窄(inferTrunc)。分发器比较操作数和结果 bitwidth,并忽略 op 名称是 ext 还是 trunc,因此 sign-extension 和 fp-cast 布局遵循 sub-32-bit packing 的变化,而不是遵循拼写。

Op-TypeID 分发表

这个 switch 有约 35 个分支。作为 apply pass 的 49 个 rewrite 规则在生产者侧的对应物,它把每个 op TypeID 映射到一个 infer(<Op>) 方法。多个 op 共享一个方法(已注明),四个 op 内联处理:

Opinfer 方法地址备注
arith.constantinfer(arith::ConstantOp)0x132c78c0
cf.assertinlinesetInLayout = kNoLayout
memref.loadinfer(memref::LoadOp)0x132c7de0不同于 tpu.load
tpu.loadinfer(LoadOp)0x132cb6c0
tpu.storeinfer(StoreOp)0x132cbb20
tpu.strided_load / tpu.strided_storeinfer(Strided{Load,Store}Op)0x132cbd80 / 0x132cc240
tpu.matmul(+ push_rhs/acc_lhs/popinfer(Matmul*Op)0x132cc740 / 0x132cccc0 / 0x132cce00 / 0x132ccf40
tpu.rotate / tpu.dynamic_rotateinfer({Rotate,DynamicRotate}Op)0x132ca600 / 0x132ca840
tpu.concatenateinfer(ConcatenateOp)0x132cacc0
tpu.erase_layoutinlinesetLayout = kNoLayout(in+out)
tpu.iotainfer(IotaOp)0x132cd0a0
tpu.gatherinfer(GatherOp)0x132cd380不同于 dynamic
tpu.dynamic_gatherinfer(DynamicGatherOp)0x132cd3e0
tpu.reduce_indexinfer(ReduceIndexOp)0x132cd6e0
tpu.bitcastinfer(BitcastOp)0x132cdb40
tpu.traceinfer(TraceOp)0x132ce020
tpu.prng_random_bitsinfer(PRNGRandomBitsOp)0x132ce160
tpu.regioninfer(RegionOp)0x132ce300携带 region
scf.if / scf.for / scf.whileinfer(scf::{If,For,While}Op)0x132c8140 / 0x132c8c40 / 0x132c9760携带 region
vector.broadcastinfer(vector::BroadcastOp)0x132ce520
vector.extractinfer(vector::ExtractOp)0x132cece0
vector.multi_reductioninfer(MultiDimReductionOp)0x132cf660
vector.shape_cast / tpu.reshapeinferReshape0x132d09a0共享
vector.extract_strided_sliceinfer(ExtractStridedSliceOp)0x132d2d80跳过操作数收集
tpu.vector_load / tpu.vector_storeinfer(Vector{Load,Store}Op)0x132cf2c0 / 0x132d2340inferLoadStoreVectorLayout
tpu.transposeinfer(TransposeOp)0x132d26c0

只有两个向量内存 op,即 tpu.vector_loadtpu.vector_store,会经过共享的 inferLoadStoreVectorLayout0x132d48c0);标量 memref.load / tpu.load / tpu.store 以及 strided_* op 各有自己的 infer(...) 主体,并不会调用它。

函数表

函数地址作用
InferVectorLayoutPass::runOnOperation0x132c3600pass 入口;构造 VectorLayoutInferer
createInferVectorLayoutPass0x132c2c20工厂(gen{sublane,lane}TpuTilingFlags、bool)
VectorLayoutInferer::inferBlock0x132c3dc0逐 op TypeID 分发(反编译约 1400 行)
getLayoutFromOperands0x132c59a0收集每个操作数的生产者 out_layout
getLayout0x132d3260读取某个值在结果索引处的 out_layout
inferExt / inferTrunc0x132c5be0 / 0x132c6600变宽 / 变窄 cast 布局
inferElementwise0x132c70e0layout-preserving elementwise 规则(使用 join
inferLoadStoreVectorLayout0x132d48c0读取 memref 分块,匹配 vreg 布局
verifyMemoryTiling0x132d3580强制合法的 memory-op 分块
infer(MatmulOp)0x132cc740MXU-packed 操作数/acc/result 布局
infer(vector::BroadcastOp)0x132ce520replicated-axis 选择
infer(RotateOp) / infer(IotaOp) / infer(TransposeOp) / inferReshape0x132ca600 / 0x132cd0a0 / 0x132d26c0 / 0x132d09a0逐 op 规则
其余约 25 个 infer(...) 主体0x132c78c0..0x132d2d80地址 + 签名已恢复;主体已抽样
setInLayout / setOutLayout / setLayout0x14b75c60 / 0x14b75e40 / 0x14b75fa0写入 in_layout/out_layout ArrayAttrs
extensions::{can,}inferVectorLayout0x13246280 / 0x132462a0out-of-tree op 回退

逐 Op 规则骨架

每个逐 op 规则都共享一个骨架。重新实现者只要内化这个骨架,就能重构约 25 个只抽样、未完全反编译主体的规则:

  • Bitwidth。 计算每个向量操作数/结果的元素 bitwidth bwFloat8EXMYType 按 8 计;i1 特判。
  • Native sublane tiling = target_sublane * 32 / bw,即每个 sublane 容纳 32/bw 个子元素的 packed tile:f32 为 8,bf16 为 16,int8/fp8 为 32,int4 为 64
  • Native lane tiling = target_lane(128)。
  • Offset = 新产生值的 {0,0}。只有当 op 在某个轴上 broadcast 或 reduce 时,该轴才会变为 replicated(offset 缺失)。
  • implicit_dim 按 rank 决定:rank-2+ → NONE(0);1-D → MINOR(1) 或 SECOND_MINOR(2),取决于该向量位于 lane 轴还是 sublane 轴;标量提升 → MINOR_AND_SECOND_MINOR(3)。

以下是四个达到重新实现粒度的规则和次要规则。

infer(MatmulOp) — 0x132cc740

matmul 规则把操作数强制为 MXU latch 期望的 packed native tiling,并把 accumulator/result 强制为 f32 (8,128) 分块。它 CHECK "Expected 32-bit acc in tpu::MatmulOp""Expected 32-bit result in tpu::MatmulOp",然后发出三个 in_layout 和一个 out_layout

c
function infer(MatmulOp op):                                 // 0x132cc740
    bw_lhs = bitwidth(op.lhs);  bw_rhs = bitwidth(op.rhs)
    require bitwidth(op.acc) == 32   // "Expected 32-bit acc in tpu::MatmulOp"
    require bitwidth(op.res) == 32   // "Expected 32-bit result in tpu::MatmulOp"
    in_layout[lhs] = { bw_lhs, {0,0}, (32*target_sublane/bw_lhs, target_lane), NONE }
    in_layout[rhs] = { bw_rhs, {0,0}, (32*target_sublane/bw_rhs, target_lane), NONE }
    in_layout[acc] = { 32,     {0,0}, (target_sublane,          target_lane), NONE }
    out_layout     = { 32,     {0,0}, (target_sublane,          target_lane), NONE }
    // 32*sublane/bw computed as 32 * inferer[+24] / bw
```text

对于 bf16 操作数,这会得到 packed 的 `16,{0,0},(16,128)` 和 f32 结果 `32,{0,0},(8,128)`,正是 [apply pass](mosaic-vectorlayout.md) 在其 bf16 matmul worked example 中消费的属性。解释操作数*为什么*必须 packed 的 MXU tile-cost 推理位于 dot/conv cost 页面。

### inferElementwise — `0x132c70e0`

Layout-preserving:所有非 `i1` 的向量/标量操作数加上结果必须共享一个 bitwidth;向量操作数布局会通过 `VectorLayout::join` 折叠为一个公共布局,该布局同时成为每个向量操作数的 `in_layout` 和 `out_layout`:

```c
function inferElementwise(op):                               // 0x132c70e0
    require op.getNumResults() == 1            // "only one result supported"
    require op.getNumOperands() > 0            // "elementwise ops with no operands unsupported"
    bw = -1
    for v in operands + [result] where v is non-i1 vector/scalar:
        if bw == -1: bw = bitwidth(v)
        else require bitwidth(v) == bw         // "Mismatched bitwidth in elementwise for non-i1 ..."
        require isa<VectorType, scalar>(v)     // "expected only vector and scalar operands"
    L = none
    for vop in vector operands:
        l = getLayout(vop)                     // "missing vector layout"
        L = (L is none) ? l : VectorLayout::join(L, l, shape)
        if join failed: L = native_layout(bw)  // re-derive a fresh native layout
    if no vector operand but result is vector:
        L = { bw, {0,0}, (32*target_sublane/bw, target_lane) }   // synthesize native
    set every vector operand's in_layout = L; out_layout = L; scalar operands → kNoLayout

join 失败在这里不是错误,它会为该操作数回退到重新推导的 fresh native layout,最终的布局不匹配稍后由 RelayoutInsertion 弥合。

inferLoadStoreVectorLayout — 0x132d48c0

load/store 规则不会发明分块,而是读取 memref 的 TiledLayoutAttr(由 InferMemRefLayout 设置、由 TilingPropagation 传播),并让 vreg 布局与它匹配:

c
function inferLoadStoreVectorLayout(memref, vec, indices, ...):  // 0x132d48c0
    require rank(memref) == rank(vec)          // "memref and vector rank mismatch"
    require rank(vec) > 0                       // "rank 0 vectors unsupported"
    bw = bitwidth(elem)                         // "Unsupported bitwidth"
    if rank == 1:
        require 1D tiling                       // "Expected 1D tiling in 1D loads/stores"
                                                // else "Unsupported tiling for 1D load/store"
        layout = { offset {0, base_offset mod (lane*packing)},
                   tiling (1, lane*packing), implicit MINOR(2) }
    else:                                       // rank >= 2
        require 2D tiling                       // "Expected 2D tiling in 2D+ loads/stores"
        if memref 2nd-minor dim <= one sublane-tile OR vec minor dim == 1:
            layout.offset = {0,0}               // sublane-replicated fast path
        else:
            layout.offset = { base_idx mod tile (sublane), base_idx mod tile (lane) }
        if bw==32 and vec minor dim==1 and canReinterpretToUntiledMemref:
            layout = sublane=1 broadcast layout
    verifyMemoryTiling(...)                      // 0x132d3580 (CHECKs below)
    if canReinterpretToUntiledMemref(memref):
        fold tile strides into offset using leading_tile_rows
```text

`verifyMemoryTiling`(`0x132d3580`)强制:"Loads of types wider than 32-bit unsupported""Only three-level tiling supported for 1D memory ops narrower than 32-bit""Invalid first-level tile in 1D memory op"

### infer(vector::BroadcastOp) — `0x132ce520`

标量源会产生一个 result native layout(rank-1 → `MINOR`(2),否则 `NONE`),并令 `in_layout = kNoLayout`。向量源会通过 `getLayout` 读取源布局,用两个方向的 `VectorLayout::generalizes` 测试等价性,然后在输出上把 broadcast 轴标为 replicated,只在源和目标 dim 大小匹配的位置保留 offset:

```c
function infer(vector::BroadcastOp op):                      // 0x132ce520
    require rank(result) > 0                    // "rank 0 vectors unsupported"
    if scalar source:
        out = native_layout(bw, implicit = rank1 ? MINOR(2) : NONE)
        in_layout = kNoLayout
    else:
        src = getLayout(op.source)              // "missing vector layout"
                                                // "unsupported broadcast source type"
        require generalizes(src, dst) or generalizes(dst, src)   // equivalence
        for axis i: keep offset[i] iff src_dim[i] == dst_dim[i]  // else replicated (absent)
        out = src with broadcast axes replicated

次要规则(rotate、iota、transpose、multi_reduction、reshape)

这些规则遵循同一个 bitwidth → native-tiling 骨架;其精确 offset/implicit-dim 选择经过抽样,但不是每个都完全反编译(HIGH):

Op地址行为
infer(RotateOp)0x132ca600仅 32-bit("not implemented: Rotate with non-32-bit data");native (sublane,lane)in == out
infer(IotaOp)0x132cd0a0native tiling;iota dim 在该轴上设为 replicated(offset 缺失)
infer(MultiDimReductionOp)0x132cf660native tiling;reduced dim 在该轴上设为 replicated
infer(TransposeOp)0x132d26c0交换 (sublane,lane) 角色 → 产生与操作数不同的 out_layout(因此由 RelayoutInsertion 弥合)
inferReshape0x132d09a0重新计算 implicit-dim;可能强制 {0,0} re-layout

NOTE — 携带 region 的 op(scf.for/scf.while/scf.if/tpu.region)未被完全追踪。 它们的入口点是 0x132c8140/0x132c8c40/0x132c9760/0x132ce300。统一 loop body 中 iter-arg 和 yield 布局的跨迭代不动点未反编译(这些主体 LOW)。重新实现必须添加一个 loop-body 布局统一步骤,这是上面的逐 op 表没有捕获的。


格:join 和 generalizes

因为 InferVectorLayout 从不插入 relayout,它会用 VectorLayout 上的半格(源码 layout.h)来调和多个操作数布局。VectorLayout 结构本身位于 Mosaic VectorLayout;这里仅说明 meet 和 ≤ 关系。

c
function VectorLayout::join(a, b, shape):                    // 0x14a957c0
    require shape.size() >= layout_rank(implicit_dim)        // layout.h:320
    if a more_general_than b:                                // a replicated where b concrete,
        return b                                             //   same bw/tiling/implicit, singleton dim
    if b more_general_than a:
        return a
    require bitwidth(a) == bitwidth(b)                        // 16-byte tiling pair XOR + ptest
    require implicit_dim(a) == implicit_dim(b)
    out = a
    for each dim:                                            // merge offsets
        if one offset replicated, other concrete: take the concrete
        elif both concrete and equal:                        take it
        else (concrete and different):                       return invalid()   // no join
    return out
```text

`generalizes(this, other, shape)` 是 ≤ 关系(同样有 `layout.h:320` CHECK):当且仅当 `this` 至少与 `other` 一样泛化时为真,也就是 `this` 的每个 concrete 轴都被 `other` 匹配,而 `this` 的每个 replicated 轴都接受 `other` 的 concrete 值。`inferElementwise` 使用 `join`;`infer(BroadcastOp)` 在把 broadcast 轴标为 replicated 之前,用两个方向的 `generalizes` 测试等价性。

`getLayout(Value)`(`0x132d3260`)读取一个值的布局:它找到定义 op,并在结果索引处读取其 `out_layout` ArrayAttr(CHECK `infer_vector_layout.cc:2152/2154/2157` — `"op"`/`"op_result"`/`"out_attrs.size() > result_index"`)。当 inferer 的 `+44` lane-replicate 标志被设置时,它会强制 lane offset 为 replicated,使 broadcast/extract 源被读取为 lane-broadcast。

> **GOTCHA — `join` 失败是功能,不是错误。** 当两个操作数确实需要不同布局时,`join` 返回一个*无效*布局(`_RDI[56] = 0` 的 “no value” 标志)。`inferElementwise` 把这视为重新推导 fresh native layout 的信号,产生的生产者/消费者不匹配随后由 `RelayoutInsertion`(阶段 11)弥合。把 join 失败当作硬错误的重新实现,会拒绝生产编译器可以正常编译的 kernel。

---

## 示例:一个 bf16 Matmul Kernel

该 kernel(`{sublane=8, lane=128}`):

```mlir
%a : memref<512x256xbf16, #tpu.memory_space<vmem>>
%b : memref<256x128xbf16, #tpu.memory_space<vmem>>
%o : memref<512x128xf32,  #tpu.memory_space<vmem>>
%va  = tpu.vector_load %a   : vector<512x256xbf16>
%vb  = tpu.vector_load %b   : vector<256x128xbf16>
%acc = tpu.matmul %va, %vb  : vector<512x128xf32>
       tpu.vector_store %acc, %o

阶段 4 — infer-memref-layout。 每个 arg memref 都获得一个 TiledLayoutAttr。没有 leading_tile_rows arg-attr 时,对于 bf16(bw=16),getTilingFactor(512, gen, sublanes=8, …, is_arg=1, 16)packing=2,并且(假设 flags[0] 已设置,因为这些是 kernel args)bw==16 的 “large 2nd-minor” 分支给出 2*sublanes = 16(可整除 512),再加上 packing tile (2,1)

text
%a  →  tiles [(16,128),(2,1)], contiguous strides          (512x256 bf16)
%b  →  tiles [(16,128),(2,1)]                               (256x128 bf16)
%o  →  f32 (bw=32): sublane tile 8 → tiles [(8,128)]        (512x128 f32)
```text

每个 arg 的 tiled type 都会通过一个 `tpu.erase_layout` 为函数体建立别名。

**阶段 9 — tiling-propagation。** 两个 `tpu.vector_load` 和一个 `tpu.vector_store` 匹配 `propagate_layout_to_consumer_rule`;它们的 memref 操作数被重新穿线到 tiled memref(看穿 `erase_layout`)。三个 `EraseLayoutOp` 现在没有 use,因此被擦除。

### 阶段 10 — infer-vector-layout。

```text
tpu.vector_load %a  → inferLoadStoreVectorLayout reads %a's tiling (16,128)+(2,1)
                      out_layout = 16,{0,0},(16,128)   (bf16 native, packed sublane 16)
                      in_layout  = kNoLayout           (memref operand)
tpu.vector_load %b  → out_layout = 16,{0,0},(16,128)
tpu.matmul %va,%vb  → infer(MatmulOp):
                        in_layout[lhs] = 16,{0,0},(16,128)
                        in_layout[rhs] = 16,{0,0},(16,128)
                        in_layout[acc] = 32,{0,0},(8,128)
                        out_layout     = 32,{0,0},(8,128)
                      ── lhs/rhs MATCH the loads' out_layouts ⇒ no relayout
tpu.vector_store    → in_layout[%acc] = 32,{0,0},(8,128)  (matches matmul out) ⇒ no relayout

阶段 11 — relayout-insertion。 每个 producer.out_layout == consumer.in_layout不会插入 tpu.relayout;apply-pass 的 in == out 不变量成立。

阶段 12 — apply-vector-layout 消费的正是这些 attrs:bf16 → vector<8x128x2xbf16> vregs;f32 → vector<8x128xf32>;matmul → MXU latch/matpush/matres。

NOTE — 改一个 op 就会出现 relayout。 如果一个 tpu.transpose 喂给 matmul lhs,infer(TransposeOp) 会发出一个无法与 matmul 所需 lhs in_layout joinout_layout;阶段 11 随后会在两者之间插入 tpu.relayout。这正是求解器和应用器分离为两个 pass 的原因:求解器记录分歧,relayout-insertion pass 解决它。


未追踪的内容

  • matmul / elementwise / load-store / broadcast / rotate / iota 之外的约 25 个逐 op infer(...) 主体:地址 + 签名已恢复,骨架已在代表性子集上验证;reduce_index / dynamic_gather / prng_random_bits / concatenate / reshape / transpose / extract_strided_slice 的精确 offset/implicit-dim 选择经过抽样,未完全反编译(HIGH)。
  • tpu_memref_slice_rule 之外的 5 个专门 memref-propagation 规则主体:已验证会重新穿线 tiled memref 并重新计算 tile stride;逐规则 stride 数学未逐个反编译(HIGH)。
  • TpuTilingFlags 字节语义:门控 getTilingFactor 的 “large 2nd-minor” 乘数的 3 个 flag 字节已恢复为 flags[0..2];其字段名从用途推断(MEDIUM)。
  • canReinterpretToUntiledMemref / canReinterpretToUntiledContiguousMemref,即选择 erase_layout 还是 reinterpret_cast 的谓词:调用点已恢复,函数体未反编译(精确谓词 LOW)。
  • scf.for/while/if + tpu.region region-layout 推断(iter-arg/yield 统一不动点):入口点已恢复,跨迭代不动点未反编译(这些主体 LOW)。
  • SparseCore 镜像 mosaic_sc::InferVectorLayoutPass0x132ed380 + 逐 op infer 方法 0x132ed7a0..0x132ee1e0):确认存在且与 TensorCore inferer 平行,主体未在此反编译。

交叉引用

  • Mosaic 概览 — 16 阶段流水线;这里的三个 pass 是其中的阶段 4、9、10。
  • Mosaic VectorLayout(sublane, lane) VectorLayout 结构、ImplicitDimtilesPerVreg/tileArrayShaperelayout driver,以及消费本页产生属性的 49 项 applyLayoutOp 表。
  • tpu MLIR 方言 — 这些 pass 会推断其布局的 ops(tpu.matmultpu.vector_load/storetpu.erase_layouttpu.memref_slice、…)。
  • MHLO → XTile → tpu Lowering — 通用 HLO 路径,以及不存在 MHLO→tpu legalizer 的证明(Mosaic 是唯一的 tpu 生产者)。
  • Layout Assignment — host-side HLO layout assignment,与这个 in-kernel Mosaic 分块求解器不同。
  • Binary: extracted/libtpu-0.0.40-cp314-cp314-manylinux_2_31_x86_64/libtpu/libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d
  • 索引项: Part V — Compiler: Lowering & Optimization Passes / MLIR lowering chain — 返回索引