LLO Bundle 打包
本页中的每个地址、偏移和常量都逐字节精确地读取自
libtpu-0.0.40-cp314wheel 中的libtpu.so(BuildID md589edbbe81c5b328a958fe628a9f2207d)。其他版本会不同。对该二进制而言,.textVMA == 文件偏移;所有地址都是虚拟地址。
摘要
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,即 opcode233..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),并在 opcode135且 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) |
| 调度一个 op | BundlePacker::Feed(LloInstruction*, latency) @ 0x14021f20(src bundle_packer.cc) |
| 资源冲突检查 | SlotTracker::FindFeasibleBundleAfter @ 0x140340a0(src sched/slot_tracker.cc) |
| 提交 | BundlePacker::PlaceInstructionInBundle @ 0x1401fe80;PlaceInstruction @ 0x14020a00 |
| 修改 slot | Bundle::AddInstruction(instr, value_set, multi) @ 0x1c455e60 |
| 逐 gen 合法性 | TpuBundleRestrictions::SetLimits(vtable+16),MatchScalar(vtable+80) |
| limit 获取 | BundleRequirementTracker::GetBundleLimits(Target, TpuSequencerType) @ 0x1c6232e0 |
| 收尾 | BundlePacker::ConsumeBundles @ 0x14026c40;ValidatePacking @ 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::Pack(0x10a86420)和 BundlePacker::Feed(0x14021f20)恢复如下:
// 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 窗口;PackInstruction和Feed会重新分派。GlobalBundlePacker::Pack的 region 循环跳过 opcode 233..236(op - 233 >= 4是保留条件),因为这些 member op 由PackPhis(global_bundle_packer.cc:104)处理;后者通过相反测试op - 233 <= 3收集它们,并将每个传给PackInstruction。PackInstruction(0x10a875a0)随后重新测试 branch family 并尾调用PackBranch。Feed自身在通用依赖遍历前,会 special-caseop == 8(落入最后一个空 bundle + barrier)、冗余 move peephole、一个v5 - 219 <= 0x15barrier 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 位于不同 bundle | bundle_packer.cc:875 | RET_CHECK → FailedPrecondition |
| Complement 为 null / 不在 bundle 中 | bundle_packer.cc:872/873 | RET_CHECK |
| Constant 被 feed 给 packer | "Cannot feed constants…" | FailedPrecondition |
| 只允许非 constant 的位置出现 constant operand | bundle_packer.cc:928 | RET_CHECK |
| Requirement 超出 empty-bundle limit | slot_tracker.cc:23 | LogMessageFatal "requirement doesn't fit in an empty bundle!" |
has_branch / instruction 不匹配(post-walk) | global_bundle_packer.cc:79 | RET_CHECK |
资源冲突检查:SlotTracker::FindFeasibleBundleAfter
目的
SlotTracker(ctor 0x14033fc0)是每个 BundlePacker 的可变资源计数器。它保存 gen 的逐 bundle limit vector(一个 BundleRequirement),并为每个现有 bundle 保存目前已消耗的 requirement。FindFeasibleBundleAfter(req, start)(0x140340a0)是决定一个 op 是否适合当前 bundle 或必须开启新 bundle 的函数:它返回第一个索引 ≥ start 且剩余容量可接纳 req 的 bundle。
算法
// 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 0–2 为 `*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+ 最高 2–3) |
| 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+ 最高 2–4) |
| 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 0–2 被连线)。
---
## 逐 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 | 6–7 | 4 | 2 | 1–2 | – |
| `GhostliteBundleRestrictions` | ghostlite (v6 lite) | 64 | 2–3 | 2 | 6–7 | 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::AddInstruction(0x1c455e60)把一个 InstructionRecord {LloInstruction*, LloValueSet, bool multi} 追加到 bundle 的 inlined-vector(inline capacity 2,溢出时走 EmplaceBackSlow)。multi flag 由对 BundleRequirement 的 vptest 计算(非零 ⇒ set),它告诉 encoder 该 record 驱动两个物理 slot(wide-vector / complement pair)。
QUIRK:一个 LLO op 可以填充两个 slot。
LloOpcodeIsComplement只对 opcode 355(0x163)和 36(0x24)返回 true;对这些 opcode,PlaceInstructionInBundle会把 complement 和它的GetInstructionPrimary都加入同一个 bundle,并各自使用multi=1,同时给二者打上同一个scheduled_bundleno。这种 co-location 正是Feed的 complementRET_CHECK(bundle_packer.cc:875)在第二次 feed 的半边上强制要求的。若重新实现把每个 LLO op 都当成一个 slot,就会错误打包 wide-vector pair,并触发审计。
Branch 处理:Terminator Bundle 与 Delay Slot
GlobalBundlePacker::PackBranch(0x10a877a0)处理 opcode 0x87 / 0x88 / 0xEF(由 LloOpcodeIsScalarBranch、global_bundle_packer.cc:112 断言)。
// 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-Bundle 的 Encoder<gen> 路径,产生相同的 bundle 字节布局。
(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)