Skip to content

LLO Bundle 打包

本页中的每个地址、偏移和常量都逐字节精确地读取自 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(BuildID md5 89edbbe81c5b328a958fe628a9f2207d)。其他版本会不同。对该二进制而言,.text VMA == 文件偏移;所有地址都是虚拟地址。

摘要

Bundle 打包是原始 bundle 字节发射前最后一个调度与合法化阶段:它接收某个 region 的扁平 LloInstruction 列表,也就是 wire form 之上的最后一层 IR,并在固定宽度的 VLIW bundle 内为每条指令分配 slot,同时遵守每一代的资源预算(功能单元数量、向量源端口、立即数 slot、谓词字段)。输出是一个 vector<Bundle>,其带类型的子字段随后由每代的 Encoder<gen>::EncodeBundleInternal 序列化(MC 发射器)。本页记录 slot 填充 / 列表调度算法、逐 slot 合法性规则、决定一个 op 是否适合当前 bundle 或需要开启新 bundle 的资源冲突检查,以及 bundle 发射循环。

该算法是前向贪心列表调度,不是 ILP,也不是模调度。内层循环软件流水是独立路径(Bundle 模调度);LLO 之上的宏观异步 / 关键路径重排是另一个 pass(LatencyHidingScheduler 核心)。这里记录的 packer 是逐 region 且局部的:它按 IR 顺序遍历 LLO op,根据读后写依赖计算每个 op 最早合法 bundle,向资源计数器(SlotTracker)询问在该点或之后第一个剩余容量可容纳该 op 的 BundleRequirement 的 bundle;若没有适合的 bundle,则追加空 bundle 并提交。上层 scheduler 用来为 bundle 定价的“max”(Bundle 感知成本),在这里被镜像为硬性的合法性检查:bundle 累计的 requirement 必须逐字段保持在该 gen 的 limit vector 以内。

熟悉 LLVM 的读者应记住一个类比和一个差异。类比是:这与 LLVM 的 VLIWPacketizer / DFAPacketizer 所做的是同一件事,即在资源自动机约束下把相互独立的 machine op 打包到一个 VLIW packet 中。差异是:libtpu 为两个 IR 携带了两个 packer。子系统 (A) xla::jellyfish::BundlePacker 打包 LloInstruction*,是本文记录的规范主路径。子系统 (B) LLVM target 的 (anonymous)::BundlePacker MachineFunctionPass 在指令选择后打包 MachineInstr*,并通过 ResourceSolver 生成相同的 bundle 字节布局。两者并存,是因为 TPU 代码在通向字节的过程中有两种 IR 表示。

对于重新实现,契约如下:

  • drain 是按 topo 顺序处理 region 的 GlobalBundlePacker::Pack:先 PackPhis(收集 PHI-edge member op,即 opcode 233..236,并将每个交给 PackInstruction),再通过 PackInstruction 处理 region body;PackInstruction 自身会把 branch opcode(0x87/0x88/0xEF)重新分派给 PackBranch,然后执行 ConsumeBundles
  • BundlePacker::Feed 计算 MinSafeHoistBundleNo,它是在每个依赖下界(operand producer bundle + LatencyBetween、control source/target、register/arch-register 可行性、vmem fence)上的运行中 max,然后把 op 放在那里,或追加 NOP bundle 直到其适配。
  • SlotTracker::FindFeasibleBundleAfter 是资源冲突检查:它断言 op 的 BundleRequirement 在 empty-bundle limit 内,然后扫描逐 requirement 的 feasible-bundle bitmap,寻找第一个索引 ≥ start 且满足 CombinationIsWithin(bundle_req, requirement, limits) 的 bundle。
  • PlaceInstructionInBundle 提交Bundle::AddInstruction(instr, value_set, multi),设置 scheduled_bundleno,当 LloOpcodeIsComplement 时加入 complement primary(一个 LLO op → 两个 slot),并在 opcode 135 且 target 非空时把 bundle 标记为 terminator。
  • 逐 slot 合法性由每个 gen 的虚函数 TpuBundleRestrictions 给出SetLimits / AddXluRequirements / AddMxuRequirements / MatchScalar),在获取 limit 时以 (TpuVersion, TpuSequencerType) 为键。
顶层入口deepsea_compiler_backend::(anon)::PackBundles @ 0x10a30a20
编排器GlobalBundlePacker::Pack @ 0x10a86420(src global_bundle_packer.cc
调度一个 opBundlePacker::Feed(LloInstruction*, latency) @ 0x14021f20(src bundle_packer.cc
资源冲突检查SlotTracker::FindFeasibleBundleAfter @ 0x140340a0(src sched/slot_tracker.cc
提交BundlePacker::PlaceInstructionInBundle @ 0x1401fe80PlaceInstruction @ 0x14020a00
修改 slotBundle::AddInstruction(instr, value_set, multi) @ 0x1c455e60
逐 gen 合法性TpuBundleRestrictions::SetLimits(vtable+16),MatchScalar(vtable+80)
limit 获取BundleRequirementTracker::GetBundleLimits(Target, TpuSequencerType) @ 0x1c6232e0
收尾BundlePacker::ConsumeBundles @ 0x14026c40ValidatePacking @ 0x14026ca0
Bundle struct 步长112 字节(vector<Bundle> 元素);out-of-line block 160 字节(0xA0
置信度除非行内另有说明,否则为 CONFIRMED(byte-anchored)

算法类别:前向贪心列表调度

packer 是一次单向前扫,并带有显式的逐 bundle 资源记账。没有回溯,也没有 spill-to-next-bundle 搜索:如果资源计数器说没有现有 bundle 能接纳该 op,算法总是追加新的空 bundle 并重试;只有在那之后 Feed 仍返回错误时,编译才会失败。其形态从 GlobalBundlePacker::Pack0x10a86420)和 BundlePacker::Feed0x14021f20)恢复如下:

c
// GlobalBundlePacker::Pack()  @ 0x10a86420  (src global_bundle_packer.cc:157, 325-330)
StatusOr<vector<Bundle>> Pack() {
    ScopedLoggingTimer t("global bundle packer (...)");        // .rodata string, src 157
    GetOrCreateLastEmptyBundle();                              // seed bundles_ with one empty
    LloRegionVisitHelper visit(module);                        // topo walk of region members
    for (member in region, in IR order) {
        if (member.kind != kInstruction) continue;             // llo_region.h:161 RET_CHECK
        instr = member.instruction;
        uint16 op = instr->opcode;
        if (op - 233u >= 4u)                                   // skip opcodes 233..236 (PHI-edge members, handled by PackPhis)
            PackInstruction(instr);                            // 0x10a875a0; re-dispatches branches to PackBranch internally
    }
    Status st = ConsumeBundles();                              // 0x14026c40 — hand over vector<Bundle>
    // post-walk audit: for each bundle, has_branch_instruction == bundle.has_branch()  (src 79)
    return st;
}
```text

`PackInstruction`(`0x10a875a0`)首先重新测试 branch family `(op - 135u) < 2 || op == 239`,并对这些 opcode 尾调用 `PackBranch`;否则它读取 `packer + 0xDA0` 处的单调 pack-position 计数器(`*((_QWORD*)this + 436)` 字段,即每个 op 前进一步的调度时钟),转发给 `Feed(instr, counter)`,并在成功时递增该计数器。`PackBranch`(`0x10a877a0`)做同样的 Feed/递增,但随后追加 branch-delay-slot bundle。`Pack` 的后遍历审计中的 branch 分派测试逐字节就是 `(op - 135u) < 2 || op == 239`,即 opcode **135**(`0x87`,conditional branch)、**136**(`0x88`,jump)、**239**(`0xEF`,predicated transfer)。

> **QUIRK:branch 放置由 region walk 固定,而不是由启发式规则决定。** Branch 总是 region 发射的最后一条 LLO 指令,因此 `PackBranch` 最后运行,而 branch 的 `scheduled_bundleno` 必然是 block 中最高的。没有单独的“把 terminator 放最后”规则;topological order 保证这一点。若重新实现者在该 packer 之前重排 op,必须保留这个不变式,否则后遍历的 `has_branch_instruction == bundle.has_branch()` 审计(`global_bundle_packer.cc:79`)会 FATAL。

### 函数映射

| 函数 | 地址 | 作用 |
|---|---|---|
| `(anon)::PackBundles` | `0x10a30a20` | 顶层 driver:timer,构造 `BundlePackerOptions`,构造 `GlobalBundlePacker` |
| `GlobalBundlePacker::Pack` | `0x10a86420` | region topo walk;逐 op 分派;`ConsumeBundles` |
| `GlobalBundlePacker::PackPhis` | `0x10a87160` | 收集 opcode 233..236,经 `PackInstruction` feed 每个 op |
| `GlobalBundlePacker::PackInstruction` | `0x10a875a0` | regular op → `Feed(instr, pack_counter)` |
| `GlobalBundlePacker::PackBranch` | `0x10a877a0` | branch op → `Feed` + delay-slot padding + branch barrier |
| `BundlePacker::Feed` | `0x14021f20` | 调度一个 op:earliest-bundle + place |
| `BundlePacker::ConsumeBundles` | `0x14026c40` | 清除 alias analysis,将 `vector<Bundle>` 交给调用者 |
| `ValidatePacking` | `0x14026ca0` | pack 后正确性复查 |

---

## 调度一个 op:`BundlePacker::Feed`

### 目的

`Feed(LloInstruction*, latency)`(`0x14021f20`)是子系统 (A) 的核心:它确定该 op 可合法占据的最早 bundle,向资源 tracker 查询那里可行的 bundle,必要时用 NOP bundle 扩展 bundle vector,并提交。其反编译很大,因为它把 opcode-class 分派、冗余 move peephole、常量拒绝、依赖遍历、fence 插入和放置折叠进了一个函数。

### 算法

```c
// BundlePacker::Feed(LloInstruction* instr, int64 latency)  @ 0x14021f20  (src bundle_packer.cc)
Status Feed(LloInstruction *instr, int64 latency) {
    uint16 op = instr->opcode;

    // ---- (0) ops not yet in any bundle: special opcode classes ----
    if (!instr->in_bundle()) {                                // *((int*)instr+12) < 0
        if (op == 8) { /* drop into last empty bundle, NoteSchedulingBarrier */ ... return Ok; }
        if (RegisterTracker::IsRedundantMove(instr))          // 0x14033900  mov rN,rN peephole
            { /* co-locate with the operand's bundle; do not consume a slot */ return Ok; }
        // opcodes 219..234 with bit-test masks: barrier-into-last-empty-bundle class
        if (op == 44 /* constant */)
            return FailedPrecondition(                        // src bundle_packer.cc, near offset
                "Cannot feed constants into bundle packer. Copy them to registers first.");
    } else {
        // op already scheduled: must be the complement half of a wide-vector pair
        complement = GetInstructionComplement(instr);         // 0x1d4f62c0
        RET_CHECK(complement != nullptr);                     // src 872
        RET_CHECK(complement->in_bundle());                   // src 873
        RET_CHECK(instr->scheduled_bundleno()
                  == complement->scheduled_bundleno());       // src 875 "must be same bundle"
        return Ok;
    }

    // ---- (1) earliest legal bundle: a running MAX over every dependency floor ----
    int n = bundle_limits_min;                                // RangeSpec-driven base
    if (LloOpcodeCreatesSchedulingBarrierBeforeBundle(op)) BarrierHere();   // sync ops
    n = BundleFifoTracker::FindFeasibleBundle(instr);         // 0x14445ac0  FIFO-port floor
    for (operand in instr->non_immediate_operands()) {        // RAW from data operands
        producer = LloInstruction::FromValue(operand);
        if (producer->in_bundle()) {
            int rt = producer->scheduled_bundleno()
                   + LatencyTable::LatencyBetween(operand, instr);   // dependency latency
            n = max(n, rt);
        }
    }
    n = max(n, RegisterTracker::FindFirstFeasibleBundle(instr));          // 0x...
    n = max(n, BundleArchRegisterTracker::FindFirstFeasibleBundle(instr));
    // control-edge hazards (predicate / branch targets)
    if (OpcodeLowersToVload(op))
        n += DelayForRawHazardsFromControlSources(instr, n, ...);        // 0x14026440
    if (op == 135 && instr->target_block())
        n += DelayForRawHazardsToControlTargets(instr, n, ...);          // 0x140265e0
    n = max(n, BundleAliasAnalysis::FindMinSafeHoistBundleNo(instr, n, ...));

    // ---- (2) vector-store fence ordering ----
    BundleRequirement req = GetBundleRequirement(instr, immediates);     // op→requirement
    bool need_fence = RequiresVectorStoreFence(instr, n);                // 0x14020900
    if (need_fence) { /* synthesize CreateVectorStoreFence, fold its req into `req` */ }
    if (!need_fence)
        need_fence = !IsVmemReadPrecededByVmemStoreFence(instr, n);      // 0x14020820

    // ---- (3) grow the bundle vector to cover index n (NOP bundles) ----
    while (bundles_.size() <= n) {                            // emplace_back zero-inits 0xA0 bytes
        if (n - bundles_.size() + 1 >= 257)                   // src 1470
            LOG(WARNING) << "suspiciously large number of nops: " << ...;
        bundles_.emplace_back();                              // __emplace_back_slow_path 0x10a89620
    }

    // ---- (4) commit ----
    if (need_fence) PlaceInstruction(n, fence_instr, ...);    // fence goes first
    return PlaceInstruction(n, instr, value_set, req, is_branch);        // 0x14020a00
}

依赖下界变量在反编译中名为 MinSafeHoistBundleNo,并在每个贡献者处通过 if (n <= rt) n = rt; 提升,也就是一个 max。operand RAW 下界在 bundle_packer.cc 附近偏移的循环中逐字节精确:对每个 operand 取其 scheduled_bundleno + LatencyTable::LatencyBetween(operand, instr)LatencyBetween 与成本模型使用的是同一个逐 op-pair 依赖延迟(Bundle 感知成本)。

GOTCHA:没有 spill-to-next-bundle 搜索。 当 tracker 无法把 op 放入 n 或之后的任何现有 bundle 时,算法追加空 bundle(每个都是 112 字节清零的 Bundle,out-of-line 160 字节 block 惰性分配)并重试;它从不重试更早的索引,也不移动已经放置的 op。唯一的失败是下面五条 fatal 路径。重新实现若添加“如果放不下,就尝试前一个 bundle”的分支,就会偏离该二进制。

NOTE:Pack 中的 op - 233u >= 4u 会跳过 PHI-edge opcode 窗口;PackInstructionFeed 会重新分派。 GlobalBundlePacker::Pack 的 region 循环跳过 opcode 233..236op - 233 >= 4保留条件),因为这些 member op 由 PackPhisglobal_bundle_packer.cc:104)处理;后者通过相反测试 op - 233 <= 3 收集它们,并将每个传给 PackInstructionPackInstruction0x10a875a0)随后重新测试 branch family 并尾调用 PackBranchFeed 自身在通用依赖遍历前,会 special-case op == 8(落入最后一个空 bundle + barrier)、冗余 move peephole、一个 v5 - 219 <= 0x15 barrier window(opcode 219..240,再由两个 inline bit-test mask 裁剪),以及 op == 44(常量拒绝)。bit-test-masked barrier window 的精确逐 opcode 成员未列举(PARTIAL)。

失败路径

子系统 (A) 中有六个不同的 fatal/error site,全部锚定到反编译里的 bundle_packer.cc / global_bundle_packer.cc 行号:

条件位置返回的 status
Complement halves 位于不同 bundlebundle_packer.cc:875RET_CHECK → FailedPrecondition
Complement 为 null / 不在 bundle 中bundle_packer.cc:872/873RET_CHECK
Constant 被 feed 给 packer"Cannot feed constants…"FailedPrecondition
只允许非 constant 的位置出现 constant operandbundle_packer.cc:928RET_CHECK
Requirement 超出 empty-bundle limitslot_tracker.cc:23LogMessageFatal "requirement doesn't fit in an empty bundle!"
has_branch / instruction 不匹配(post-walk)global_bundle_packer.cc:79RET_CHECK

资源冲突检查:SlotTracker::FindFeasibleBundleAfter

目的

SlotTracker(ctor 0x14033fc0)是每个 BundlePacker 的可变资源计数器。它保存 gen 的逐 bundle limit vector(一个 BundleRequirement),并为每个现有 bundle 保存目前已消耗的 requirement。FindFeasibleBundleAfter(req, start)0x140340a0)是决定一个 op 是否适合当前 bundle 或必须开启新 bundle 的函数:它返回第一个索引 ≥ start 且剩余容量可接纳 req 的 bundle。

算法

c
// SlotTracker::FindFeasibleBundleAfter(req, start)  @ 0x140340a0  (src sched/slot_tracker.cc:20-49)
int FindFeasibleBundleAfter(const BundleRequirement &req, int start) {
    // (1) the op must fit an EMPTY bundle at all — checked field by field via
    //     four bitmap-difference masks against bundle_limits_:
    CHECK( ((limit[0] - req[0]) & 0x4925555552A84888) == 0 &&   // src 23 "requirement.IsWithin(...)"
           ((limit[1] - req[1]) & 0x555555554A508424) == 0 &&
           ((limit[2] - req[2]) & 0x2491515555088AAA) == 0 &&
           ((limit[3] - req[3]) & 0x524)               == 0 );  // else FATAL "doesn't fit empty bundle!"

    start = max(start, point_of_no_return_);                    // closed bundles excluded

    // (2) the per-requirement feasible-bundle cache (a bitmap of bundle indices)
    if (!feasible_bundles_by_requirement_.contains(req))
        PopulateFeasibleBundles(req);                           // 0x14034680 — rebuild the bitmap
    CHECK(feasible_bundles_by_requirement_.contains(req));      // src 38

    // (3) scan forward from `start` for the first set bit (feasible bundle)
    int ret = FindNextSetBitBeforeLimit(feasible_bitmap, start, num_bundles);
    if (found) {
        // (4) sanity: the chosen bundle's accumulated req + this req is within limits
        CHECK( BundleRequirement::CombinationIsWithin(                       // src 49
                   bundle_requirement(ret), req, bundle_limits_) );
        return ret;
    }
    return max(start, num_bundles + base);                      // none → index past the end
}
```text

这四个 AND mask 是 `BundleRequirement` struct 的 bitfield 布局:每个 `BasicBitmap<uint64>` word 携带若干 slot-type 子计数器,而逐 mask 的 AND 会隔离在 `req` 超过 `limit` 时产生借位的那些 bit。这四个 mask(`0x4925555552A84888`、`0x555555554A508424`、`0x2491515555088AAA`、`0x524`)是 slot-field 的“carry mask”——它们已逐字节确认,但逐字段分解(哪些 bit 是 scalar、vector-ALU、immediate、XLU、MXU)是后续工作(leaf field 边界为 PARTIAL)。

> **NOTE:“适合当前 bundle 还是开启新 bundle”就是步骤 (3) 中的 set-bit 扫描。** 从 `start` 在 `feasible_bitmap` 上执行 `FindNextSetBitBeforeLimit` 会返回该 op 可以加入的第一个 bundle;如果该 bit 是当前(最后一个)bundle,就打包在那里,否则搜索跳到后面的已有 bundle;如果不存在 set bit,`Feed` 会扩展 vector。维护 bitmap 的资源记账是 `NoteAddedToBundle`(`0x14035080`,`bundle.consumed += req`)和 `NoteRemovedFromBundle`(`0x14035780`,`-= req`);`BundleRequirement::operator+=`(`0x140237a0`)对四个 packed word 执行普通 64-bit integer **addition**(word 02 为 `*a1 += *a2`,外加 32-bit word 3),并在每次 add 后重新应用同样四个 AND mask(`0x4925555552A84888`、`0x555555554A508424`、`0x2491515555088AAA`、`0x524`)作为 carry-overflow 检查;如果任何逐字段子计数器溢出到相邻字段,就以 `"IsValidWord(i)"`(`bundle_requirement.h:629`)FATAL。

### `BundleRequirement` 字段(形态)

`BundleRequirement` 是一个由 `BasicBitmap<uint64>` 字段和 scalar counter 组成的 packed struct。精确 bit 边界尚未完全解码,但字段清单从逐 gen 的 `SetLimits` mask 写入和 `OverflowingFields` reporter(`0x1c623e00`)恢复如下:

| 字段组 | 跟踪内容 |
|---|---|
| scalar slots | SPU / sequencer slot 占用(JF/PF 为 2,V5+ 最高 3|
| vector-ALU slots | VPU lane 占用(JF 为 2,V5+ 最高 23|
| immediate slots | immediate-field 占用(JF 为 6;逐 gen) |
| vector source ports | vs0..vs2(JF 为 3,V5+4|
| XLU resource | transcendental-unit demand(JF/PF 为 1,V5+2|
| MXU resource | matrix-unit demand(JF 为 1,V5+ 最高 24|
| ttu / branch / vmem-load / vmem-store | TTU(JF)、terminator flag、fence-position counter |

> **GOTCHA:limit vector 是逐 `(version, sequencer-type)` 的,而不是逐 chip 的。** `BundleRequirementTracker::GetBundleLimits(Target, TpuSequencerType)`(`0x1c6232e0`)索引 `trackers[*(int*)(target+920)]`(即 `TpuVersion`),然后按 sequencer type 偏移:`TensorCore`(type 0)为 **`+32`**,type 1**`+128`**,type 2**`+224`**;type **3, 4, 5** 在该 build 中以 `"Unsupported sequencer type"` FATAL。若重新实现假设每个 chip 只有一个 limit vector,就会错误估计 BarnaCore / SparseCore sub-bundle 的大小。更高 sequencer type 到 SparseCore/TAC/TEC limit offset 的原始映射在 `0.0.40` 中**不存在**(LOW confidence;只有 type 02 被连线)。

---

## 逐 slot 合法性:`TpuBundleRestrictions`

逐 generation 的 slot 规则封装在多态 `TpuBundleRestrictions` subclass 中,每个 silicon generation 一个;它们静态注册(`TpuBundleRestrictionsRegisterer`,`0x1c457880`)并在 tracker 构造时按 codename 查找。`0.0.40` 中存在四个具体 subclass:`JellyfishBundleRestrictions`、`PufferfishBundleRestrictions`、`ViperfishBundleRestrictions` 和 `GhostliteBundleRestrictions`(位于抽象基类 `TpuBundleRestrictions` 之上)。每个都实现同一套虚接口;逐 gen 差异完全体现在这些虚函数携带的常量和逐 opcode 表中。

### 虚接口

| vtable slot | 方法 | 决定内容 |
|---|---|---|
| +16 | `SetLimits(tc, sparse, misc)` | 通过 masked AND/OR 写入逐 sequencer-type slot-limit vector |
| +24 | `AddXluRequirements(instr, req)` | 逐 opcode XLU(transcendental)slot demand |
| +32 | `AddMxuRequirements(instr, req)` | 逐 opcode MXU(matmul)slot demand |
| +40 | `HasVectorSlotRestrictions()` | gen 是否禁止某些 VPU slot 配对 |
| +48 | `HasEupRestrictions()` | 是否要求 EUP-modulo 对齐 |
| +56/+64 | `HasVectorOdd/EvenSlotRestrictions()` | shuffle/reduce 的奇/偶 VPU-slot 约束 |
| +72 | `LaneBroadcastSpecifierIsImplicitImmediate(int)` | lane-broadcast 字段是否编码为 implicit immediate(节省一个 slot) |
| +80 | `MatchScalar(op_idx, ImmediateOperandType)` | 哪个 scalar/immediate slot 可以容纳该 immediate |

`JellyfishBundleRestrictions::SetLimits`(`0x1c457a40`)是最清楚的锚点:它使用向量化的 `vandps`/`vorps`/`vpblendw`,结合逐 gen 常量 blob 来填充三个 limit vector(`tc`、`sparse`、`misc`),其中包括已记录的 `0xFE07FF8000007000` mask 以及 `0xAA9428404` / `0xC7FE607FFFFFFFFF` immediate-field 常量。每个 masked word 都设置一个 sequencer type 的 slot ceiling。

### 逐 gen slot 清单

下面的每代 slot count 从 `SetLimits` 常量、`BundleRequirement` struct size 增长(JF 上 `0x80` → V5+ 上 `0xe0`),以及 [bundle-model slot 清单](../isa/bundle-model-overview.md#slot-taxonomy) 重建。字节宽度(41/51/64)在 bundle-model 页上为 CONFIRMED;逐 slot count 是增长的*形态*

该 build 中只有四个带具体 `*BundleRestrictions` subclass 的 codename;`dragonfish`(v3)共享 `JellyfishBundleRestrictions` class(未注册单独 subclass)。Version label 遵循 SM/codename map:jellyfish = v2,dragonfish = v3,pufferfish = v4,viperfish = v5,ghostlite = v6 lite。

| Restriction class | Codename / ver | Bundle B | scalar | vec-ALU | imm | vs | XLU | MXU | ttu |
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|
| `JellyfishBundleRestrictions` | jellyfish (v2);也服务 dragonfish (v3) | 41 | 2 | 2 | 6 | 3 | 1 | 1 | 1 |
| `PufferfishBundleRestrictions` | pufferfish (v4) | 51 | 2 | 2 | 6 | 3 | 1 | 1 ||
| `ViperfishBundleRestrictions` | viperfish (v5) | 64 | 2 | 2 | 67 | 4 | 2 | 12 ||
| `GhostliteBundleRestrictions` | ghostlite (v6 lite) | 64 | 23 | 2 | 67 | 4 | 2 | 2 ||

> **NOTE:`MatchScalar` 是 immediate-slot legalizer。** 当 op 携带 immediate operand 时,`(anon)::RequiredImmSlotCount`(`0x1c625ea0`)和 `ImmediateSlotsRequired`(`0x1c626680`)会统计 immediate-slot 贡献,而逐 gen 的 `MatchScalar`(vtable +80)选择哪个物理 immediate/scalar slot 可以满足每个 immediate。一个 bundle 中的多个 immediate 会竞争;`SlotTracker` 跟踪消耗。对任何 slot 都过宽的 constant 会在该 packer **上游** spill 到 constant pool;`Feed` 的 `op == 44` 拒绝确认了 packer 假定逐 instruction immediate 已经适配。参见 [Immediate Slot](../isa/slot-immediate.md)。

---

## 提交:`PlaceInstructionInBundle`

`PlaceInstructionInBundle`(`0x1401fe80`)是 mutation 步骤:一旦 `PlaceInstruction`(`0x14020a00`)应用了 control-hazard delay 和 fence 插入,该函数就把 op 写入选中的 `Bundle`。

```c
// BundlePacker::PlaceInstructionInBundle(bundle, bno, instr, value_set, req)  @ 0x1401fe80
//   (src bundle_packer.cc:659-689)
void PlaceInstructionInBundle(Bundle *b, int bno, LloInstruction *instr,
                              const LloValueSet &vs, const BundleRequirement &req) {
    bool multi = vptest(req) /* any requirement bit set → wide-vector pair */;
    Bundle::AddInstruction(b, instr, vs, multi);              // 0x1c455e60 — append InstructionRecord
    instr->scheduled_bundleno = bno;                         // *((int*)instr + 12) = bno

    if (LloOpcodeIsComplement(instr->opcode)) {              // 0x1d60c960 → opcode == 355 || == 36
        LloInstruction *primary = GetInstructionPrimary(instr);   // 0x1d4f6520
        Bundle::AddInstruction(b, primary, {}, /*multi=*/true);   // one LLO op fills two slots
        primary->scheduled_bundleno = bno;                   // *((int*)primary + 12) = bno
    }

    if (instr->opcode == 135 && instr->target_block()) {     // conditional branch with target
        b->ensure_branch_metadata();                         // alloc 0xA0-byte block at bundle[+104] (qw13)
        b->branch_meta->has_branch = 1;                      // byte +28 within that block
    }
    // NoteAddedToBundle(req, bno) bookkeeping happens on the SlotTracker
}

Bundle::AddInstruction0x1c455e60)把一个 InstructionRecord {LloInstruction*, LloValueSet, bool multi} 追加到 bundle 的 inlined-vector(inline capacity 2,溢出时走 EmplaceBackSlow)。multi flag 由对 BundleRequirementvptest 计算(非零 ⇒ set),它告诉 encoder 该 record 驱动两个物理 slot(wide-vector / complement pair)。

QUIRK:一个 LLO op 可以填充两个 slot。 LloOpcodeIsComplement 只对 opcode 3550x163)和 360x24)返回 true;对这些 opcode,PlaceInstructionInBundle 会把 complement 和它的 GetInstructionPrimary 加入同一个 bundle,并各自使用 multi=1,同时给二者打上同一个 scheduled_bundleno。这种 co-location 正是 Feed 的 complement RET_CHECKbundle_packer.cc:875)在第二次 feed 的半边上强制要求的。若重新实现把每个 LLO op 都当成一个 slot,就会错误打包 wide-vector pair,并触发审计。


Branch 处理:Terminator Bundle 与 Delay Slot

GlobalBundlePacker::PackBranch0x10a877a0)处理 opcode 0x87 / 0x88 / 0xEF(由 LloOpcodeIsScalarBranchglobal_bundle_packer.cc:112 断言)。

c
// GlobalBundlePacker::PackBranch(instr)  @ 0x10a877a0  (src global_bundle_packer.cc:112-133)
Status PackBranch(LloInstruction *instr) {
    int branch_delay = subtarget->branch_delay_slots;       // read at subtarget + 2324 (0x914)
    RET_CHECK(LloOpcodeIsScalarBranch(instr->opcode()));     // src 112
    Feed(instr, pack_counter);                               // normal placement
    int bno = instr->scheduled_bundleno();
    bundles_[bno].ensure_branch_metadata()->has_branch = 1;  // byte +28: terminator
    // append `branch_delay` empty bundles AFTER bno as delay slots
    for (int i = 0; i < branch_delay; ++i) {
        int slot = bno + 1 + i;
        while (bundles_.size() <= slot) bundles_.emplace_back();
        bundles_[slot].ensure_branch_metadata()->is_branch_delay_slot = branch_delay; // byte +29
    }
    ++pack_counter;
    EstablishBranchBarrier(bno, instr);                      // 0x14026880
    return Ok;
}
```text

`branch_delay` 从 `TpuSubtarget + 2324`(= `0x914`,与字段偏移完全匹配)读取。delay-slot bundle 用 branch-metadata block 偏移 `+29` 处的字节(`is_branch_delay_slot` 字段)标记。随后 `EstablishBranchBarrier`(`0x14026880`):

1. 调用 `BundleAliasAnalysis::NoteSchedulingBarrier(bno)`(`0x1402c780`)——污染 branch 之后的 memory aliasing。
2. 调用 `SlotTracker::UpdatePointOfNoReturn(bno)`(`0x14035b40`)——因此后续 `Feed` 不能把 op 放在 `bno` 或之前(`FindFeasibleBundleAfter` 中的 `start = max(start, point_of_no_return_)` clamp)。
3. 当 branch 是 predicated 时记录一个 predication override(`packer + 3032`/`+3040` 处的 `latest_branch_barrier_` pair),反转 predicate,使 delay-slot op 可以被 predicated off。

> **NOTE:delay slot 在这里保持 NOP。** 该 packer **不会**用 hoisted op 填充 `branch_delay` bundle;它们保持为空(NOP)bundle。任何 delay-slot scheduling 都是独立的 pre-pass。`point_of_no_return_` clamp 专门阻止 `Feed` 把后续 op 滑入 delay window。

### Barrier

| Primitive | 地址 | 效果 |
|---|---|---|
| `EstablishBarrier(bno)` | `0x14021d00` | 对 sync/unknown-effect op 执行 `NoteSchedulingBarrier(bno)` + `UpdatePointOfNoReturn(bno)` |
| `EstablishBranchBarrier(bno, instr)` | `0x14026880` | 同上,外加 branch 的 predication-override 记账 |
| `BarrierHere()` | `0x14021de0` | 在最后一个 bundle 后追加新的空 bundle,然后在其上 `EstablishBarrier`(in-line fence) |

`BarrierHere` 是 `Feed` 对 vector-store fence 以及 `LloOpcodeCreatesSchedulingBarrierBeforeBundle` 为 true 的 op 所调用的函数。

---

## PHI 打包:SSA Resolution

`GlobalBundlePacker::PackPhis(region)`(`0x10a87160`)在 region body 之前运行。它使用与 `Pack` 相同的 `LloRegionVisitHelper` 机制遍历 region member,但采用*相反*的 opcode filter:每个 opcode 满足 `op - 233u <= 3u` 的 member(opcode **233..236**,也就是 `Pack` 的 body 循环跳过的 PHI-edge member class)都会追加到临时 `vector<LloInstruction*>`。遍历完成后,收集到的每个 op 都通过常规 `PackInstruction` 路径打包:

```c
// GlobalBundlePacker::PackPhis(region)  @ 0x10a87160  (src global_bundle_packer.cc:104)
Status PackPhis(LloRegion *region) {
    vector<LloInstruction*> phi_edge_ops;                  // operator new(0x18) — empty vector
    LloRegionVisitHelper visit(region);                    // same topo walk as Pack()
    for (member in region) {
        if (member.kind != kInstruction) continue;         // llo_region.h:161 RET_CHECK
        if ((uint16)(member.opcode - 233) <= 3u)           // opcodes 233..236 only
            phi_edge_ops.push_back(member.instruction);
    }
    for (instr in phi_edge_ops) {
        Status s = PackInstruction(instr);                 // 0x10a875a0
        if (!s.ok()) return AddSourceLocation(s, /*line=*/104);
    }
    return Ok;                                             // region body packed afterward by Pack()
}

opcode 233..236 正是 Pack 的 body 循环排除的窗口(op - 233u >= 4u),因此两个 pass 划分了 region:PackPhis 处理 PHI-edge member op,Pack 处理其他所有内容。在该 build 中,PackPhis 不会合成 copy,也不会写入 predecessor bundle;它只是优先把这四个 opcode 值重新路由到 PackInstruction。(由反编译 CONFIRMED;opcode 233..236 作为 SSA-edge resolution 的语义角色是推断,UNVERIFIED。)


子系统 (B):LLVM-Target BundlePacker Pass

第二个结构相同的 packer 位于 LLVM TPU backend 中,是一个 MachineFunctionPass,在指令选择后打包 MachineInstr*。它通过 LLVM MC 路径(MC 发射器)而非 proto-BundleEncoder<gen> 路径,产生相同的 bundle 字节布局。

text
(anon)::BundlePacker::runOnMachineFunction  @ 0x13b206a0
  for MBB in MF:
    runOnMachineBasicBlock(scheduler, tracker, dag, MBB, insert_nops)   @ 0x13b23560
      ├─ TPUScheduleDAGMI: build the data-dependence DAG over MachineInstr
      ├─ BundlePackingScheduler (extends GenericScheduler): list-schedule
      │     pickNode / tryCandidate biased toward filling the current bundle
      └─ BundleTracker: assign each scheduled MI to a bundle
            canAddMI(MI)  @ 0x13b2b0e0   ── resource check (returns false → open new bundle)
            addMI(MI)     @ 0x13b2b2e0   ── commit
            addBundle(MI) @ 0x13b2b460   ── finalize via ResourceSolver::addMI per MI
  emitNop(MBB, iter, TII)  @ 0x13b2af20  ── explicit NOP MachineInstr when a bundle underfills
```text

`BundleTracker::canAddMI`(`0x13b2b0e0`)是子系统 (B) 的资源冲突检查,即 `FindFeasibleBundleAfter` 的对应物。已验证顺序:

1. **VReg source count**:`bundle.vs_used + TPUInstrInfo::countVRegSrcs(MI)` 必须 `≤ subtarget.maxVRegSrcs()`(经 vtable `+792` 读取);超出则返回 false。
2. **`ResourceSolver::canAddMI(MI)`**(`0x13beb500`):复制 `Solver` 状态,尝试 `Solver::addMI`,检查没有超预算计数器递增;丢弃该副本。
3. **`ResourceSolver::canAddVResDestPort(MI)`**(`0x13beb780`):vector-result destination-port 冲突。
4. **`ResourceSolver::canAddImm(MI, slot_pairs)`**(`0x13bec480`):每个 immediate operand 的 immediate-slot 可用性。
5. **Must-be-last-in-bundle** op(branch):`MachineInstr::hasPropertyInBundle` 测试 → 若该属性强制 bundle termination,则返回 false。

`Solver::getSlotAssignmentsForMI`(`0x13be8220`)从 LLVM `MCSchedClassDesc` 表(`TPUStages`,参见 [MC 发射器](../isa/mc-emitter.md))读取逐 opcode allowed-slot mask,并迭代 set bit 以推导该 op 需要哪些 proc-resource(即 slot type)。这两个子系统是同一个 VLIW slot-fill 算法在两个 IR 上的两种实现。

> **GOTCHA:子系统 (B) 发射显式 NOP;子系统 (A) 不发射。** `(anon)::BundlePacker::emitNop`(`0x13b2af20`)在 bundle 未填满时构造真实 NOP `MachineInstr`(opcode 来自 `TII.getNopOpcode()`,vtable `+0x358`)。子系统 (A) 则留下空 `Bundle` entry(清零 slot mask),由逐 gen encoder 用 `kNeverExecute` predicate 或静态 `kNoopBundleBytes` 模板填充([Bundle 模型](../isa/bundle-model-overview.md#empty-slot-and-nop-convention))。若重新实现对同一个 IR 路径混用两种 NOP convention,就会双重发射或漏发 pad bundle。

---

## 验证:`ValidatePacking`

`ValidatePacking(Span<const Bundle>, const LloModule&, bool strict)`(`0x14026ca0`)在 `ConsumeBundles` 后运行,失败时 fatal。它从头重新推导每个 bundle 的 `BundleRequirement`(对每条 instruction 的 requirement 求和),通过 `BundleRequirementTracker::GetBundleLimits` 获取 gen 的 limit vector,并逐字段验证 `bundle_req ≤ limit`。它还额外检查 predicate-field 范围、branch op 只出现在 basic block 的最后一个 bundle 中,以及 `scheduled_bundleno` 是单调的。昂贵的 `BundleRequirementPreciseImpl::DeepValidateBundle`(`0x14054c00`,位于 `CodeGenerator::MakeBundleTester` 下)会逐个重新模拟添加每个 op;这是 test/debug 路径。

---

## 置信度摘要

| 断言 | 证据 |
|---|---|
| 前向贪心列表调度,没有 spill-to-next-bundle | `Pack` `0x10a86420` + `Feed` `0x14021f20`(仅 emplace 增长) |
| `Pack` 按 topo-order 遍历 region,并按 opcode 分派 | `0x10a86420`(`op-233>=4` filter;`(op-135)<2 \|\| op==239` audit) |
| `Feed` earliest bundle = 对依赖下界取 `max` | `0x14021f20` 中 `MinSafeHoistBundleNo` 被每个贡献者提升 |
| Operand RAW floor = `producer_bno + LatencyBetween` | `0x14021f20` operand loop 调用 `LatencyTable::LatencyBetween` |
| `FindFeasibleBundleAfter` 是资源冲突检查 | `0x140340a0` IsWithin mask + `FindNextSetBitBeforeLimit` + `CombinationIsWithin` |
| `IsWithin` 的四个 bitmap carry-mask | `0x140340a0`(`0x4925555552A84888`,…,`0x524`) |
| `PlaceInstructionInBundle`:AddInstruction + scheduled_bundleno + complement + branch tag | `0x1401fe80` |
| `LloOpcodeIsComplement` ⇔ opcode 355 或 36 | `0x1d60c960` `return a1==355 \|\| a1==36` |
| Branch delay slot 从 `subtarget + 0x914` 读取 | `0x10a877a0`(`subtarget + 2324`);delay byte +29 |
| `EstablishBranchBarrier`:NoteSchedulingBarrier + UpdatePointOfNoReturn | `0x14026880` |
| `GetBundleLimits` seq-type offset 32 / 128 / 224;3–5 FATAL | `0x1c6232e0` switch |
| `SetLimits` 通过 masked AND/OR(`0xFE07FF8000007000`)写入 3 个 limit vector | `0x1c457a40` |
| 子系统 (B) `canAddMI` 顺序(VRegSrcs、Solver、VResDestPort、Imm、last-in-bundle) | `0x13b2b0e0` |
| `PackPhis` 收集 opcode 233..236,并经 `PackInstruction` feed | `0x10a87160`(`op-233<=3` filter;AddSourceLocation 104) |
| opcode 233..236 作为 SSA-edge resolution 的语义角色 | 由 pass 划分推断,非 byte-anchored |
| `BundleRequirement::operator+=` 是 integer add + carry-mask validity check | `0x140237a0`(`bundle_requirement.h:629` FATAL) |
| 四个具体 `*BundleRestrictions` subclass(JF/PF/VF/Ghostlite) | `nm` symbol roster;dragonfish 共享 JF class |
| Jellyfish 之外的逐 gen slot count | 由 `BundleRequirement` size 增长 + bundle width 推导 |
| Seq type 3–5 → SparseCore/TAC/TEC offset | 在 `0.0.40` 中未连线(FATAL) |

---

## 交叉引用

- [Bundle 模型概览](../isa/bundle-model-overview.md) — 该 packer 填充的 VLIW bundle word 与 slot taxonomy;empty-slot / `kNeverExecute` NOP convention。
- [MXU Slot](../isa/slot-mxu.md) — packer 记账其 `AddMxuRequirements` demand 的 matrix-unit slot family。
- [Immediate Slot](../isa/slot-immediate.md) — immediate-slot allocation 以及该 packer 查询的 `MatchScalar` legalizer。
- [MC 发射器](../isa/mc-emitter.md) — 子系统 (B) 的 `Solver`/`BundleTracker` 所 feed 的 LLVM-MC byte path。
- [Bundle 感知成本](../cost/bundle-aware-cost.md) — `MaxResourceCycles`(bundle-cost `max`)和 `LatencyBetween`(该 packer 为计算 earliest bundle 而读取的 dependency latency)。
- [LatencyHidingScheduler 核心](latency-hiding-scheduler-core.md) — 运行在 LLO 之上的 macro-level HLO scheduler;它的逐 node cost 来自同一个 bundle model。
- [Bundle 模调度](bundle-modulo-scheduling.md) — 独立的 inner-loop software-pipelining 路径(`TPUScheduleDAGModulo`),会为 hardware loop 调用它而不是该 list scheduler。
- **二进制:** `extracted/libtpu-0.0.40-cp314-cp314-manylinux_2_31_x86_64/libtpu/libtpu.so`(build-id `89edbbe81c5b328a958fe628a9f2207d`)
- **索引条目:** Part VIII — Instruction Scheduling & Bundle Packing — [返回索引](../index.md)