Skip to content

布局分配

地址适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so。其他版本会有所不同。

摘要

布局分配是 XLA pass,它会在图被融合并 lowering 之前,固定模块中每个张量的物理 minor-to-major 维度顺序和 HBM tiling。在 TPU 上,这个 pass 是 xla::jellyfish::TpuLayoutAssignmentRunImpl @ 0x110ace00),它是开源 xla::LayoutAssignment 的子类。基类拥有双向约束传播工作列表;TPU 子类只覆盖后端扩展点:要种下哪些种子约束(AddBackendConstraints @ 0x110b19a0)、每个 op 如何为操作数/结果选择布局、哪些 op 可以改变布局,以及一个按张量的 fallback 选择器,用于传播永远到达不了的缓冲区(FindMemoryMinimizingLayout @ 0x1109dfe0)。它运行在 Phase 4(HloOptimizeThroughLayoutAssignment)内部,在 sharding/SPMD 之后、fusion 之前;参见 compile-phases.md

熟悉上游 XLA LayoutAssignment 的读者会认出其骨架:种下强制约束,在 BufferLayoutConstraint/OperandLayoutConstraint 记录的工作列表上运行 forward+backward fixpoint,然后提交布局,并在生产者布局无法满足消费者所需布局时插入 kCopy。TPU 特有的是约束的内容:MXU 的 128-lane / 8-sublane tile 会驱动每个 dot、convolution、reduce-window、scatter 和 collective op 走向一种布局,使其两个最 minor 的维度能干净地铺进 tile;以及包在 OSS pass 外的一层重跑循环:RunImpl 会运行 OSS 布局分配直到 fixpoint,用 ModuleLayoutIsValid0x110a6b80)验证,如果任何 TPU validator(Gather/Scatter/SelectAndScatter/ReduceWindow)拒绝,则重新播种并再次运行。

本页记录了重实现者必须复现的三件事:按张量的布局选择器 FindMemoryMinimizingLayout 及其真实成本模型(先看字节大小,再用 minor 维度填充效率比率打破平局,不是离散 penalty 的加权和)、AddBackendConstraints 按 opcode 种下的种子约束,以及这些选择如何通过 memory_space 整数和印在每个 xla::Layout 上的 tile 几何信息到达 memory-space assignment(MSA)与调度器。

对重实现而言,契约是:

  • 驱动器形态。 TPU 重跑循环下的 OSS pass,并由 TPU validator 对每次迭代把关。
  • 选择器。 FindMemoryMinimizingLayout:它的 element-type gate、rank>3 search gate,以及精确的双键成本:compact bytes,然后是 minor-dim fill ratio。
  • 种子约束。 AddBackendConstraints 中的 opcode→handler dispatch,以及每个 handler 强制执行的 MXU / packed-collective / window-stride==1 规则。
  • 下游契约。 MSA 和调度从已分配的 xla::Layout 中读取的内容(memory space、tile)。
Pass 类xla::jellyfish::TpuLayoutAssignment : xla::LayoutAssignment : HloPassInterface
驱动器TpuLayoutAssignment::RunImpl @ 0x110ace00 (0x37c4 ≈ 14 KB)
种子约束TpuLayoutAssignment::AddBackendConstraints @ 0x110b19a0 (0x546d ≈ 21 KB; ModuleLayoutIsValid @ 0x585a ≈ 22 KB 略大)
按张量选择器(anon)::FindMemoryMinimizingLayout @ 0x1109dfe0
Compact-layout 选择器Target::ChooseCompactLayoutForShape @ 0x1d61bd00
OSS 基类 RunImpl / AssignLayouts0x169bf440 / 0x169bb0e0
流水线位置Phase 4 尾部(HloOptimizeThroughLayoutAssignment),在 SPMD 之后、fusion 之前
IR 层级XLA HLO(HloModule / HloInstruction),MLIR 之前
源文件platforms/xla/service/jellyfish/tpu_layout_assignment.cc (rodata)
MXU tile(SublaneCount, LaneCount) = (8, 128) 此构建(v5+/ghostlite v6);pufferfish v4 上为 (16, 128)
置信度除非某行或 callout 另有说明,均为 CONFIRMED(字节锚定)

驱动器 — RunImpl 和重跑循环

目的

RunImpl 是该 pass 入口点的 TPU 覆盖实现。它不重新实现布局分配;它用设置逻辑、Mosaic kernel 解析步骤,以及外层 validate-and-re-run 循环包装开源 xla::LayoutAssignment::RunImpl0x169bf440)。这层包装存在的原因是 OSS 算法分配的是对任意后端合法的布局;其中有些布局对 TPU 的 gather/scatter/reduce-window 限制并不合法,而发现这一点的唯一方式是运行 OSS 到 fixpoint、验证、再为违规者重新播种。

入口点

text
TpuLayoutAssignment::RunImpl  (0x110ace00)                  ── driver, timed "XLA::JF Layout Assignment"
  ├─ GetTpuCompEnv / PartitionReplicaMapper setup           ── multi-slice (megascale) topology
  ├─ PreprocessModule (0x1109ff20)                          ── insert kCopy boundary nodes
  ├─ ParseCustomCallKernelsInParallel                       ── pin Mosaic kernel I/O layouts
  └─ loop:
       ├─ xla::LayoutAssignment::RunImpl (0x169bf440)        ── OSS: seed → propagate → assign
       │     └─ TpuLayoutAssignment::AddBackendConstraints   ── (TPU vtable hook, fires inside OSS)
       └─ ModuleLayoutIsValid (0x110a6b80)                   ── TPU validator; re-seed + repeat if false
```text

### 算法

```c
function RunImpl(module, exec_threads):              // 0x110ace00
    this->tpu_comp_env = GetTpuCompEnv(module)        // member +1472 bytes (offset 184)

    if this->target->GetMultiSliceTopology():         // megascale / ICI mesh
        CHECK(module.config().static_device_assignment().has_value())   // src:3618
        this->partition_replica_mapper =
            PartitionReplicaMapper::Create(this->target, module.device_assignment())

    ScopedLoggingTimer timer(name(), kXlaTimerTraceStats0)  // guard 0x224cdec0
    TraceMe("XLA::JF Layout Assignment")

    // Insert kCopy boundary nodes so propagation has free choices at the seams.
    PreprocessModule(module, exec_threads)            // 0x1109ff20

    // Mosaic (Pallas) kernels arrive with layouts pinned in their kernel proto.
    // Parse them and stash per-operand / per-result layouts in the proposed maps.
    ParseCustomCallKernelsInParallel(module)          // populates members +0x420 / +0x440
    for (instr, kernel) in tpu_custom_call instructions:
        operand_layouts_map[instr] = CreateTwoMinorLayout(kernel.input_dims)   // 0x110c1fa0
        result_layouts_map[instr]  = CreateTwoMinorLayout(kernel.output_dims)

    passes_run = 0
    while true:
        // VLOG(2) "Before TpuLayoutAssignment: " ...                src:3625
        LogNeighborhoodFingerprints(module, "layout", 6, 6)          // src:3626

        // Reject ops that must have been removed by earlier expanders.
        for c in module.MakeNonfusionComputationsSorted(exec_threads):
            for inst in c.MakeInstructionPostOrder():
                if inst.opcode in {batch-norm-grad/inf/training} or
                   (inst.opcode == fusion and inst.fusion_kind != kCustom):
                    return InvalidArgument(
                      "Instruction %s is not expected to be seen during layout assignment")

        // The OSS engine: AddBackendConstraints (our hook) → PropagateConstraints
        //                  → AssignLayouts → PostProcess.
        xla::LayoutAssignment::RunImpl(module, exec_threads)         // 0x169bf440 ; src:3737
        ++this->first_pass_count                                     // member +183
        if passes_run == 0: VLOG(2) "Ran first pass of layout assignment."

        if not this->ModuleLayoutIsValid(module, exec_threads):      // 0x110a6b80
            VLOG(2) "Running " passes_run " additional passes of layout assignment ..."
            // VLOG(3) the four offender lists (at-risk / scatter / scatter-updates /
            //         incompatible / expensive-collective), src:3679-3723
            ++passes_run
            continue          // re-seed: the recorded incompatibilities steer the next pass
        break

    // VLOG(2) "After TpuLayoutAssignment: " ...                     src post-loop
    return /*module_changed=*/ true

注意 — 重跑由验证失败驱动,而不是固定迭代次数。每次 OSS 运行本身都是一次完整的 forward/backward fixpoint;外层循环只会在 TPU 专属 validator 拒绝某个 OSS 引擎认为已完成的 op 时再次触发。实践中第二轮很少见:它会在某个 gather/scatter/reduce-window op 收到 TPU lowering 无法实现的布局时触发,并由记录下来的“incompatible layouts”列表引导下一轮重新播种。

函数映射

函数地址作用
TpuLayoutAssignment::RunImpl0x110ace00驱动器 + 重跑循环
TpuLayoutAssignment::PreprocessModule0x1109ff20分配前插入 kCopy 边界节点
TpuLayoutAssignment::ModuleLayoutIsValid0x110a6b80TPU 模块级 validator(循环 gate)
xla::LayoutAssignment::RunImpl (base)0x169bf440OSS seed→propagate→assign 引擎
xla::LayoutAssignment::AssignLayouts (base)0x169bb0e0将选定布局提交到模块
xla::LayoutAssignment::PropagateConstraints (base)0x169b8120Forward+backward 工作列表
(anon)::CreateTwoMinorLayout0x110c1fa0从固定的 2-minor dims 构建 Layout
GetReduceLayoutFromOperand (anon)0x110ac4e0转发 reduce 布局
MemorySpaceColorMap::UpdateFromLayout0x110411a0memory_space 读入按缓冲区的 color map(供 MSA 使用)

特性 — TpuLayoutAssignment 成员在反编译中以 *((qword*)this + N) 访问。最重要的两个是:target_ 位于 qword +181(1448 字节处),每个 handler 都通过它访问芯片描述符;allow_relayout flag 位于 qword +152(offset 0x4C0),在任何 handler 被允许插入 relayout copy 之前都会测试 *((_QWORD*)this + 152) != 0


按张量选择器 — FindMemoryMinimizingLayout

目的

当约束传播永远到达不了某个缓冲区时,仍然必须为它选择布局;这通常发生在长 elementwise 链深处的浮点中间值上。对于 rank>3 张量,这个选择由 FindMemoryMinimizingLayout0x1109dfe0)完成:它搜索 major 维度的排列,保留 compact byte size 最小的排列;如有平局,则按两个最 minor 维度填满 lane/sublane tile 的程度打破。它有两条到达路径:来自 GetUnconstrainedLayout0x110b7520,OSS “没有约束到达此缓冲区” fallback),以及 AddBackendConstraints 在 copy-into-SparseCore 路径上的直接调用。

算法

c
function FindMemoryMinimizingLayout(target, shape /*in/out*/, out_bytes, cap):  // 0x1109dfe0
    // ---- Guard 1: element-type must be in the searchable set ----
    // mask = 0x2FFF91FFE ; only proceed if primitive-type bit is set.   movabs @0x1109e014
    et = shape.element_type()
    if et > 0x21 or not bittest64(0x2FFF91FFE, et):
        return                                  // type not layout-searchable; keep incoming

    CHECK(shape.IsArray()); CHECK(LayoutUtil::HasLayout(shape))   // src:528-529

    // ---- Baseline: compact byte size of the incoming layout ----
    best_bytes = target->ShapeSizeCompact(shape)        // 0x1d61a620
    if out_bytes != null: *out_bytes = best_bytes

    // ---- Guard 2: only search when rank > 3 ----
    if shape.dimensions_size() <= 3:                    // @0x1109e0d8 (cmpq $3 ; ja)
        return                                          // low rank: keep OSS default layout

    // ---- Minor-dimension tile fill factors (used only for the tie-break) ----
    lane_chunks    = shape.dimensions_minor(0) / target->LaneCount()      // 0x1d60f400
    sublane_chunks = shape.dimensions_minor(1) / target->SublaneCount()   // 0x1d60f300

    // ---- Search candidate permutations of the (rank>>1) major dims ----
    for perm in candidate_major_permutations(shape):
        apply perm to shape.layout().minor_to_major()
        cand_bytes = target->ShapeSizeCompact(shape)    // @0x1109e504
        if cap.has_value() and cand_bytes > cap: continue

        // KEY 1 (primary): strictly fewer bytes wins.       jl @0x1109e77c (accept)
        if cand_bytes < best_bytes:
            best = perm; best_bytes = cand_bytes; continue

        // KEY 2 (tie-break, equal bytes): lower fill-waste wins.
        //   cost = (per-dim utilization fractions, built via vcvtsi2sd/vdivsd/vmulsd)
        //   @0x1109e262 and @0x1109e73a, compared @0x1109e78c (vucomisd ; jbe @0x1109e790 reject)
        if cand_bytes == best_bytes and cand_cost < best_cost:
            best = perm; best_cost = cand_cost

    commit best to shape.layout()
```text

### 成本模型 — 它是什么,以及不是什么

成本是**按优先级排列的两个 key**

| Key | 数量 | 来源 | 规则 |
|---|---|---|---|
| 1 (primary) | Compact byte size `ShapeSizeCompact(shape)` | `0x1d61a620`(target-aware:tile + dtype packing) | 字节数严格更少 ⇒ 接受 |
| 2 (tie-break) | Minor-dimension fill-efficiency ratio(各维利用率分数的乘积) | 由 `dimensions_minor(0)/LaneCount` 和 `dimensions_minor(1)/SublaneCount` 构建 | 字节数相等时,fill-waste 更低 ⇒ 接受 |

函数中唯一的数值常量是 element-type 有效性 bitmask `0x2FFF91FFE`(bit *i* 置位 ⇒ primitive type *i* 可搜索)和 lane/sublane 除数(从芯片描述符读取,不是字面量)。反编译直接确认了二者:入口处的 `if ( v10 > 0x21 || (v11 = 0x2FFF91FFELL, !_bittest64(&v11, v10)) )`,以及从两个 minor-dim chunk count 构建比率的 `vcvtsi2sd xmm0,…,r12 / vcvtsi2sd xmm1,…,r15 / vdivsd` 序列。

> **注意 —** `FindMemoryMinimizingLayout` 内部没有 copy-count feedback:成本纯粹是最小 compact bytes,再用 minor-dim fill ratio 打破平局,没有离散 transpose/copy/bitcast penalty,也没有 `adaptive_layout_map_` 项。确实存在的 copy-count adaptivity 位于别处,即 `AdaptiveHloLayoutMap::RemoveOnCopyOverhead` @ `0x110accc0`,它作用于 pass *之间* profile-loaded overrides,而不是此选择器内部。
>
> **陷阱 —** rank gate 是 `> 3`,不是 `>= 3`。Rank-1/2/3 张量永远不会被搜索;它们保留 OSS `GetDefaultLayoutForRank` 布局。若重实现从 rank 3 开始搜索,将会生成与 libtpu 不同(且更慢收敛)的布局,尤其影响非常常见的 3-D activation shapes。

### 函数映射

| 函数 | 地址 | 作用 |
|---|---|---|
| `(anon)::FindMemoryMinimizingLayout` | `0x1109dfe0` | Rank>3 无约束选择器 |
| `TpuLayoutAssignment::GetUnconstrainedLayout` | `0x110b7520` | OSS fallback hook;调用选择器后再调用 `ChooseCompactLayoutForShape` |
| `Target::ShapeSizeCompact` | `0x1d61a620` | Compact byte size(成本 key 1|
| `Target::LaneCount` / `SublaneCount` | `0x1d60f400` / `0x1d60f300` | Tile 除数(成本 key 2|
| `Target::ChooseCompactLayoutForShape` | `0x1d61bd00` | 兜底 compact/SparseCore 选择器 |

---

## 种子约束 — AddBackendConstraints

### 目的

`AddBackendConstraints`(`0x110b19a0`)是 OSS 引擎在传播之前调用的 TPU vtable hook。它种下工作列表随后展开的*种子*约束。函数主体是一系列彼此独立的 `for inst in computation->MakeInstructionPostOrder()` 循环,每个循环都由 `HloInstruction+0xc` 上的内联 opcode-byte 测试把关(已验证:`movzbl 0xc(%r12),%eax`)。每个循环通过 OSS `SetInstructionLayout` / `SetArrayOperandLayout` / `SetOperandLayout`(`0x169b0740` / `0x169b00e0` / `0x169af600`)种下布局。

### 算法 — opcode dispatch

```c
function AddBackendConstraints(constraints):              // 0x110b19a0
    target = this->target_                                 // member qword +181
    allow_relayout = (this->[qword 152] != 0)              // offset 0x4C0

    // LOOP 1 — HBM-transfer / RNG boundary ops -> compact tiled layout, tiles cleared.
    for inst where opcode in {recv(89), rng-bit-generator(100), send(110)}:
        L = target->ChooseCompactLayoutForShape(inst.operand(0).shape())   // 0x1d61bd00
        ClearTiles(L); SetInstructionLayout(inst, L); SetArrayOperandLayout(inst.operand(0), L)

    // LOOP 2 — adaptive / autofdo-proposed overrides (seeded before per-op loops).
    for inst where this->adaptive_layout_map.HasLayout(inst):              // 0x110b6e20
        SetInstructionLayout(inst, adaptive_layout_map.GetLayout(inst), pin=false, set_default=false)

    // LOOP 3 — Mosaic custom-call operand/result pinning (from RunImpl parse step).
    for inst in operand_layouts_map (members +0x420 / +0x440):
        if single-output: pin operand-0 layout = front()
        elif tuple kCall(27)/kFusion(61): pin each tuple element's layout

    // Collective ops -> packed-lane layout, replicas must agree.
    for inst where CollectiveOpsNeedPackedLayout(inst):                    // 0x1109ef40
        // switch on opcode: all-gather(6), all-gather-start(8), all-reduce(9),
        //   all-reduce-start(11), all-to-all(12), collective-broadcast(33),
        //   collective-permute(34), ragged-all-to-all(86), reduce-scatter(93)
        SetInstructionLayout(inst, compact, pin=1, allow=1, default=1)

    // LOOP 4 — the bulk: per-op layout choosers.
    GetLayoutConfig(span, &conv_decisions /*this+0x500*/, &op_decisions /*this+0x560*/)  // 0x110a5940
    for inst in computation->MakeInstructionPostOrder():
        switch inst.opcode:
          case dot(52):
          case convolution(43):
            AssignConvolutionLayout(inst, this, target, &conv_decisions, allow_relayout)  // 0x11096160
          case reshape(97):
            AssignReshapeLayout(target, inst, this, &op_decisions, allow_relayout)        // 0x1109be80
          case select-and-scatter(109):
            AssignSelectAndScatterLayout(target, inst, this)                              // 0x1109b400
          case ragged-dot(87):
            AssignRaggedDotLayout(inst, this, target)                                     // 0x1109a060
          case reduce-window(94):
            // max window dim over operand; if 2nd-minor extent >= SublaneCount() (runtime call)
            // allocate new optimal layout; window bound/stride must be 1 in 2 minor dims
          case concatenate(39):
            CreateShapeWithOptimalLayoutForConcat(target, inst); pin concat layout        // 0x110b1320
          case gather(62):
            GatherLayoutIsValid(inst, false)                                              // 0x110a2d80
            AddIndicesLayoutConstraintForScatterGather(inst, idx=dnums+0x90, this)        // 0x110b7080
          case scatter(107):
            ScatterLayoutIsValid(inst)                                                    // 0x110a12a0
            AddIndicesLayoutConstraintForScatterGather(inst, idx=dnums+0x90, this)
            pin each update operand via GetScatterUpdatesLayout                           // 0x110a2600
          case copy(44) feeding SparseCore:
            if TransferSizeUtil::HasSparseCoreLayout(target.Topology(), op.shape()):      // 0x110b7440
                FindMemoryMinimizingLayout(target, op.shape(), ...); pin                  // 0x1109dfe0

按类别划分的强制约束

上面的 handler 强制执行少量硬规则。重实现者必须复现的是这些规则;按 op 的 handler 只是机制。

类别Ops强制布局规则执行者
MXU feeddot(52), convolution(43)Contracting(input-feature)dim 作为内部 reduction 到达 MXU;output-feature/batch 通过 TwoMinorSize0x110bd3a0)铺进 lane/sublane MXU tileAssignConvolutionLayout (0x11096160)
HBM transferrecv(89), send(110), rng-bit-generator(100)Compact tiled layout,且 tiles cleared(传输边界处线性)LOOP 1 inline
Collectiveall-gather(6/8), all-reduce(9/11), all-to-all(12), collective-broadcast(33), collective-permute(34), ragged-all-to-all(86), reduce-scatter(93)Packed-lane layout;所有 replica 一致CollectiveOpsNeedPackedLayout (0x1109ef40)
Gather/Scattergather(62), scatter(107)Indices operand 固定在 index_vector_dim(packed);scatter updates 通过 GetScatterUpdatesLayout 固定Add…ForScatterGather (0x110b7080)
Windowreduce-window(94), select-and-scatter(109)Window bound 与 stride 在两个最 minor 维度中必须为 1;pass 会尝试让 window dim 外置AssignSelectAndScatterLayout (0x1109b400) + inline
Concatconcatenate(39)为所有 operands 选择单个 optimal layoutCreateShapeWithOptimalLayoutForConcat (0x110b1320)
Copy→SparseCorecopy(44)Memory-minimising layout,然后固定FindMemoryMinimizingLayout (0x1109dfe0)

陷阱 — window-op 规则(“bound and stride must be 1 in the two minor-most dims”)是运行时 fatal 的来源之一:"Encountered select-and-scatter with a window bound or window stride in one of the two minor-most dimensions that are not 1, which is not implemented for TPU. XLA tries to choose a layout such that this is not the case, but that does not always succeed." 如果重实现不把 window dim 引向外侧,就会在合法 HLO 上触发它。

特性 — 没有单独的 kDot 循环。Dot(opcode 52)和 convolution(opcode 43)共享同一个 AssignConvolutionLayout 调用点;AssignConvolutionLayout 在入口处按 0x34(dot)/0x2b(conv) 自分发。期望找到独立 matmul layout routine 的重实现者不会找到它:MXU tiling 数学是统一的。

函数映射

函数地址作用
TpuLayoutAssignment::AddBackendConstraints0x110b19a0种子约束 dispatch(按 opcode 循环)
TpuLayoutAssignment::AssignConvolutionLayout0x11096160dot+conv MXU 布局(自分发 0x34/0x2b)
TpuLayoutAssignment::AssignReshapeLayout0x1109be80通过 ImproveReshapeLayout 的 reshape 布局
TpuLayoutAssignment::AssignSelectAndScatterLayout0x1109b400Window-op 联合布局
TpuLayoutAssignment::AssignRaggedDotLayout0x1109a060RaggedDot(variable-batch matmul)布局
TpuLayoutAssignment::GatherLayoutIsValid0x110a2d80Gather validator
TpuLayoutAssignment::ScatterLayoutIsValid0x110a12a0Scatter validator
TpuLayoutAssignment::CollectiveOpsNeedPackedLayout0x1109ef40Collective packed-lane 测试(opcodes 6/8/9/11/12/33/34/86/93)
TpuLayoutAssignment::GetLayoutConfig0x110a5940将 autofdo proposed layouts 加载到 decision caches
(anon)::AddIndicesLayoutConstraintForScatterGather0x110b7080固定 indices operand layout
(anon)::CreateShapeWithOptimalLayoutForConcat0x110b1320Concat optimal layout
(anon)::TwoMinorSize0x110bd3a0Two-minor MXU tile sizing(conv 中有 8 个 call sites)

传播 Hook — 按 op 的布局选择

目的

在每个 OSS 传播步骤内部,引擎会询问子类:“给定这个操作数布局,输出布局是什么?”(forward)以及“给定这个输出布局,操作数布局是什么?”(backward)。TPU 覆盖实现会为少数 opcode 回答,其余委托给 OSS 基类。它还覆盖两个策略谓词:哪些 op 可以改变布局,以及输出布局是否总是要推送到 operands。

算法 — forward / backward routing

c
function ChooseOutputLayoutFromOperandLayout(layout, inst, opnd_idx):   // 0x110ba2c0 (forward)
    switch inst.opcode:
      case reduce(91):   return GetReduceLayoutFromOperand(layout, inst)      // 0x110ac4e0
      case gather(62):   return GetGatherOutputLayout(inst, layout, idx)      // 0x110a4be0
      case reshape(97):  return ImproveReshapeLayout(layout, shape, …, target)// 0x110b8f00
      default:           return OSS::ChooseOutputLayoutFromOperandLayout(...) // 0x169b76a0

function ChooseOperandLayoutFromOutputLayout(layout, inst, opnd_idx):   // 0x110b7de0 (backward)
    switch inst.opcode:
      case scatter(107): if opnd is the updates operand:
                           return GetScatterUpdatesLayout(target, inst, layout,
                                    contiguous_update_window_dim)             // 0x110a2600
      case reduce(91):   return <reduce-operand layout>
      case reshape(97):  return ImproveReshapeLayout(...)                     // 0x110b8f00
      case fusion(61):   return FindOperandPilots(...)   // aggressive-loop-fusion path; flag-gated
      default:           return OSS::ChooseOperandLayoutFromOutputLayout(...) // 0x169b64c0

function OutputLayoutAlwaysPropagateToOperands(inst):                   // 0x11094ae0
    if inst.opcode == 127 /*transpose*/: return true
    return OSS::OutputLayoutAlwaysPropagateToOperands(inst)                   // 0x169b7660

function InstructionCanChangeLayoutDeepsea(inst):                       // 0x110b0640
    switch inst.opcode:
      case fusion(61):       can-change iff fusion_kind == kCustom (else cmps 40/120/49)
      case custom-call(49):  look up CompilationProperties in the global custom-call
                             registry (0x10a87cc0); use its instruction_can_change_layout
      case batch-norm-grad/inf/training 21/22/23: FATAL "!IsInvalidInstructionDuringLayoutAssignment" (src:3811)
      default:               return OSS::InstructionCanChangeLayout(inst)     // 0x169c19c0
```text

> **特性 —** `OutputLayoutAlwaysPropagateToOperands` 对 **transpose(opcode 127** 返回 true,这让 transpose 可以被实现为*纯布局变化*(免费),而不是数据 copy:所需的输出布局被强制推到 transpose 的 operand 上,因此 transpose 变成对 `minor_to_major` 的重新标记。反编译非常明确:`if ( *((_BYTE *)a2 + 12) == 127 ) return 1;`,而在 `HloOpcode` map 中 127 = `transpose`(该 enum 的最后一个值)。
>
> **注意 —** `InstructionCanChangeLayoutDeepsea` 的 custom-call 分支是该 pass 的可扩展性 hook。每个 TPU custom kernel(`tpu_custom_call`、`XlaMosaic`、`TopK`、`Sharding`、…)都会注册一个 `CompilationProperties` struct,其中的 `instruction_can_change_layout` boolean 会在这里被查询。重实现者若添加 custom op,必须注册它,否则 layout assignment 会按 OSS 默认方式处理它。

### 本页使用的权威 opcode 整数

上面的按 op dispatch 是针对编入此 libtpu 的上游 `HloOpcode` enum 的 byte test(从 `HloOpcodeString` @ `0x1e5ef000` 解码)。重实现者需要的整数如下:

| Opcode | Int | Opcode | Int | Opcode | Int |
|---|---|---|---|---|---|
| convolution | 43 (`0x2b`) | reduce | 91 (`0x5b`) | reduce-scatter | 93 (`0x5d`) |
| copy | 44 (`0x2c`) | reduce-window | 94 (`0x5e`) | gather | 62 (`0x3e`) |
| dot | 52 (`0x34`) | reshape | 97 (`0x61`) | scatter | 107 (`0x6b`) |
| custom-call | 49 (`0x31`) | fusion | 61 (`0x3d`) | select-and-scatter | 109 (`0x6d`) |
| collective-permute | 34 (`0x22`) | ragged-all-to-all | 86 (`0x56`) | ragged-dot | 87 (`0x57`) |
| concatenate | 39 (`0x27`) | transpose | 127 (`0x7f`) | sort | 120 (`0x78`) |

> **注意 —** `HloOpcode` enum 结束于 127(`transpose`);没有 opcode 130。注意几个容易混淆的点:convolution 是 43(不是 4949 是 custom-call),0x78 是 sort(不是 broadcast)。对于上表以外的任何 opcode integer,都应先重新推导再使用。

### 函数映射

| 函数 | 地址 | 作用 |
|---|---|---|
| `TpuLayoutAssignment::ChooseOutputLayoutFromOperandLayout` | `0x110ba2c0` | Forward 按 op routing |
| `TpuLayoutAssignment::ChooseOperandLayoutFromOutputLayout` | `0x110b7de0` | Backward 按 op routing |
| `TpuLayoutAssignment::OutputLayoutAlwaysPropagateToOperands` | `0x11094ae0` | transpose-always-propagate 规则 |
| `TpuLayoutAssignment::InstructionCanChangeLayoutDeepsea` | `0x110b0640` | 按 op 的 can-change bitmap |
| `(anon)::ImproveReshapeLayout` | `0x110b8f00` | Reshape source↔target dim mapping(双方向) |
| `(anon)::GetScatterUpdatesLayout` | `0x110a2600` | Scatter-updates backward layout |
| `(anon)::GetGatherOutputLayout` | `0x110a4be0` | Gather forward output layout |
| `xla::LayoutAssignment::InstructionCanChangeLayout` (base) | `0x169c19c0` | OSS fallback |

---

## 深度感知的布局成本与 Tile

### Tile 几何

每个已分配的 `xla::Layout` 都携带一个物理 HBM tile,其 dims 为 `(SublaneCount, LaneCount)`。这些值从 `target->[0x3b8]` 处的运行时芯片描述符读取,而不是硬编码:

- `Target::LaneCount()` (`0x1d60f400`) = `target->[+0x3b8]->[+0x198]` — 所有世代均为 `128`。
- `Target::SublaneCount()` (`0x1d60f300`) = `target->[+0x3b8]->[+0x1a0]` — 此 v5+/v6 构建上为 `8`,jellyfish v4 上为 `16`。
- `Target::ChunksPerTile()` (`0x1d60f2c0`) = `[+0x198] / [+0x1a0]`(lane_count / sublane_count,来自同一描述符)。

因此此构建的默认 tile 是 `(8, 128)`,v4 上是 `(16, 128)`。tile 和 `memory_space` 由 `HardwareLayout::PopulateDefaultLayout`(`0x1d6da120`)/ `HardwareLayout::PopulateShape`(`0x1d6da360`)印到 leaf subshapes 上。另一个按 shape 的 fixup `(anon)::UpdateLayout(target, Layout, Shape&)`(`0x110f66a0`)会把选定的 `Layout` 复制到 shape 上,然后委托给 `Target::UpdateLayout`(`0x1d618aa0`),后者运行 `TransferSizeUtil::UpdateLayout`(`0x1d6b05a0`)以在 dtype 改变后重新计算 tile/packing。若干 `jellyfish` pass 暴露自己的 `UpdateLayout(Shape*)` shim(`TpuSpmdPartitioner` `0x127a4100`、`TpuBFloat16Propagation` `0x110128e0`、`TpuBFloat16Normalization` `0x11012980`、`TpuReduceWindowRewriter` `0x109589c0` 等),以便在 pass 改变 shape 后刷新布局元数据;这些是彼此独立的 shim,都会调用 `Target`/`TransferSizeUtil` 布局路径,而不是共享同一个 helper。

### “深度”在哪里进入

`FindMemoryMinimizingLayout` 中的成本是按张量、tile-aware 的,而不是 graph-depth-aware 的:它会在每个候选 `minor_to_major` 下计算该张量的 compact bytes,并偏好两个 minor dims 最完整填充 `(SublaneCount, LaneCount)` tile 的布局。整个 *pass* 的“depth-aware”行为来自迭代重跑循环(更深的链会暴露更多 incompatibilities,从而为下一轮重新播种),以及 `ScatterLayoutIsValid` 用来确认第 2 minor dim 能干净打包进 sublane tile 的 `Compact2ndMinorRatio`(`0x1d61a4e0`)packing 检查。

> **特性 —** `ShapeSizeCompact` 已经把 tile 和 dtype packing 折进它返回的字节数里,所以“minimise bytes”和“fill the tile”并不是彼此独立的目标:浪费 tile lanes 的布局会 pad 出更多字节。fill-ratio tie-break 只在两个排列 pad 到完全相同的 byte total、但浪费在 lane dim 与 sublane dim 上的分布不同的时候才有意义。

### 函数映射

| 函数 | 地址 | 作用 |
|---|---|---|
| `Target::LaneCount` / `SublaneCount` / `ChunksPerTile` | `0x1d60f400` / `0x1d60f300` / `0x1d60f2c0` | 来自芯片描述符的 tile dims |
| `Target::Compact2ndMinorRatio` | `0x1d61a4e0` |2 minor packing ratio(由 `ScatterLayoutIsValid` 调用) |
| `(anon)::UpdateLayout(target, Layout, Shape&)` | `0x110f66a0` | 按 shape fixup:设置 Layout,然后调用 `Target::UpdateLayout` |
| `Target::UpdateLayout` | `0x1d618aa0` | 通过 `TransferSizeUtil::UpdateLayout` 重新计算 tile/packing |
| `HardwareLayout::PopulateDefaultLayout` | `0x1d6da120` | 将 tile + memory_space 印到 leaf subshapes |
| `HardwareLayout::PopulateShape` | `0x1d6da360` | Tile-kind selection(Default/X64/X128) |

---

## 布局如何供给 MSA 和调度

### 目的

布局分配是整个后端的前提。它写入的两类元数据会被下游消费:嵌入每个 `xla::Layout` 的 `memory_space` 整数,以及 tile 几何信息。Memory-space assignment(MSA)读取前者来知道缓冲区位于哪个物理层级;调度器则按这些层级为 spill/refill 定价。

### 交接

`memory_space` 整数路由物理放置。`MemorySpaceColorMap::UpdateFromLayout`(`0x110411a0`)在布局分配后遍历模块中的每个 `Shape`,并将 `Layout::memory_space()` 读入按缓冲区的 color map;`BuildFromLayoutAndBackendConfig`(`0x11040d80`)还会合入 `frontend_attribute`/`backend_config` memory-space 字符串。这个 color map 就是 buffer allocator(MSA)消费的内容;参见 [msa-overview.md](msa-overview.md)。整数码由 `MemorySpaceToColor` 生成;命名空间包括 `kHbm`(off-chip)、`kVmem`(on-chip vector SRAM)、`kCmem`/`kSmem`(scalar/sequencer SRAM)、`kAlternate`(MSA 预留的 secondary HBM pool),以及 SparseCore/BarnaCore 空间。

Fusion(Phase 5)在布局分配*之后*运行,因为 fusion 合法性取决于物理 tile 布局;参见 [fusion-patterns.md](fusion-patterns.md)。调度器在 MSA 之后、位于 [scheduling pipeline](../sched/overview.md) 中运行,并按已分配的 memory spaces 为每个 op 以 cycles 定价;强制产生 HBM round-trip 的布局比将缓冲区保留在 VMEM 中的布局更昂贵,这正是此阶段最小化字节数(从而最小化 tile padding / HBM footprint)会影响最终 schedule 的原因。

> **注意 —** SparseCore 有自己的下游布局 pass:`xla::tpu::sparse_core::RunLayoutAssignment`(`0x12e02140`,src `sparse_core_layout_assignment.cc`),它在 post-layout pipeline 中运行于 SparseCore custom-call instructions 上(由 `sparse_core_compiler_util` 驱动,印上按 leaf 的 `HardwareLayout::TileKind`)。这里记录的 TensorCore 侧 `TpuLayoutAssignment` 会为 SparseCore 侧的 `memory_space` tags 播种(LOOP 1 / copy→SparseCore path via `HasSparseCoreLayout`),因此两个 pass 通过 layout 的 memory space 耦合。(此构建中命名的 SparseCore memory spaces 为 `kSparseCorePrivateStackHbm`、`kSparseCoreSequencerSmem`、`kSparseCoreSequencerSflag`,没有扁平的 `kSparseCoreMem` enumerator。)

---

## 相关组件

| 名称 | 关系 |
|---|---|
| `xla::LayoutAssignment` (OSS base, `0x169bf440`) | 拥有传播工作列表;TPU 类只覆盖 backend hooks |
| `TpuLayoutAssignment::PreprocessModule` (`0x1109ff20`) | Pre-pass:插入 `kCopy` 边界节点 |
| `MemorySpaceColorMap` (`0x110411a0`) | 读取为 MSA 分配的 `memory_space` |
| `xla::tpu::sparse_core::RunLayoutAssignment` (`0x12e02140`) | SparseCore 侧布局,位于下游,通过 memory space 耦合 |
| `TpuTilingRewriter::RewriteHostComputeWithLinearLayout` (`0x1112efc0`) | Post-layout:将 tiled TensorCore layouts 桥接到 host flat memory |

## 交叉引用

- [compile-phases.md](compile-phases.md) — 有序 phase 主干;布局分配是 Phase 4 尾部,在 SPMD 之后、fusion 之前
- [overview.md](overview.md) — Part V 定位;布局分配位于 XLA-then-MLIR 下降过程中的位置
- [hlo-pre-passes.md](hlo-pre-passes.md) — 必须先运行的 pre-passes(包括 BatchNormExpander);layout assignment 会对未展开的 batch-norm FATAL
- [fusion-patterns.md](fusion-patterns.md) — 主 fusion(Phase 5),它在布局之后运行,因为 fusion 合法性取决于物理 tile 布局
- [msa-overview.md](msa-overview.md) — memory-space assignment,消费这里写入的 `memory_space` color map
- [mosaic-layout-inference.md](mosaic-layout-inference.md) — Mosaic/Pallas kernel layouts,在 `RunImpl` 中解析并固定为种子约束
- [../sched/overview.md](../sched/overview.md) — 调度器会根据此 pass 分配的 memory spaces 为 op 以 cycles 定价