Bundle 模型
本页中的每个偏移、值和地址都逐字节精确读取自
libtpu-0.0.40-cp314wheel 中的libtpu.so(BuildID md589edbbe81c5b328a958fe628a9f2207d)。其他版本会有所不同。
摘要
TPU TensorCore 是一台 VLIW 机器:序列器每个周期取一条固定宽度的 bundle,并将其中的每个槽同时发射到不同的执行单元。运行时没有依赖跟踪——bundle 字本身就是发射包,编译器负责证明同一个 bundle 中的槽彼此独立。bundle 字是一个扁平字节缓冲区,其宽度按硅片代固定:Jellyfish(TPU v2)为 41 字节,Pufferfish(TPU v4)为 51 字节,Viperfish(TPU v5)、Ghostlite(TPU v6 lite)和 6acc60406(TPU7x)为 64 字节。这些宽度不是自由参数。Jellyfish 是极端情况——它的 41 字节缓冲区由 EncoderJf::EncodeBundleInternal 内部的 operator new(0x29) 分配硬性固定。Pufferfish 从 EncoderPfTensorCore::BundleSizeBytes 以内联常量返回 51,而 v5+ 的 64 字节宽度通过以 (TpuVersion, TpuSequencerType) 为键的 codec 元数据表返回(Viperfish/Ghostlite 通过 vtable 调用计算大小,而不是在编码器 BundleSizeBytes 中使用内联字面量)。随后编码路径按各代固定的字节/位偏移把槽放入缓冲区。
注意 — 位编号约定。 本页以及它链接到的槽页面中的每个绝对位位置都是 LSB-first:位 0 是字节 0 的最低有效位,通用 v5+ 打包器
BitCopy(dst, dst_bit, src, src_bit, nbits)(0x1fa0a900)从按 LSB 编号的dst_bit开始写入nbits。Jellyfish 直接打包编码器通过shl/or移位常量使用相同约定。编码路径中不存在 MSB-first / 大端位序。
bundle 是线缆格式,不是编译器 IR。在它之上是 LLO 指令流:一串带类型的 LloInstruction,每条由 opcode + operands + memory-space + predicate 组成。bundle 打包器把相互独立的 LLO op 分组进一个 Bundle 对象,其带类型的子字段(ScalarInstruction、VectorAluInstruction、VectorExtendedInstruction、VectorLoadInstruction、VectorStoreInstruction、VectorResultInstruction、MiscInstruction,再加上 bundle header)与硬件槽一一对应;每代的 Encoder<gen>::EncodeBundleInternal 再把该 Bundle 序列化为固定宽度字节缓冲区。本页记录这个模型:bundle 是什么、每代 bundle 多宽以及二进制中如何决定该宽度、槽分类以及每个槽驱动哪个引擎、空槽(kNeverExecute)约定,以及编码/解码往返的高层图景。逐槽位布局在各个槽页面中;每代完整槽映射在每代 bundle 页面中。
对重新实现而言,契约是:
- 三种 bundle 字节宽度(41 / 51 / 64),以及它们由
(TpuVersion, TpuSequencerType)codec 元数据查找选择,而不是通过对较窄 bundle 做字节扩展。 - 槽分类:哪个带类型的
Bundle子指令驱动哪个引擎(MXU / VPU / SPU / 序列器 / 内存 / 谓词 / 循环 / 立即数 / EUP),以及数量如何跨代增长。 - 同时发射语义:bundle 内没有写后读转发;编译器必须跨 bundle 或通过 sync flag 串行化真实依赖。
- 空槽约定:未使用槽携带写入 bundle header 的
kNeverExecute谓词,而不是任意垃圾。 - 编码/解码对:
Encoder<gen>::EncodeBundleInternal写入字节缓冲区;Decoder<gen>::Decode*重建Bundle。
| Bundle 对象 | platforms_deepsea::jellyfish::isa::Bundle(带类型的槽子字段) |
| 线缆格式 | 扁平字节缓冲区,宽度按 TpuVersion 固定 |
| Jellyfish (v2) | 41 B — 由 EncoderJf::EncodeBundleInternal @ 0x1e86c7c0 中的 operator new(0x29) 硬性固定;JellyfishCodecMetadata::BundleSizeBytes @ 0x1ecf7460 也返回 41 |
| Dragonfish (v3) | 41 B — 共享 Jellyfish/EncoderDf codec 路径(CreateEncoderJfDf @ 0x1e835b80) |
| Pufferfish (v4) | 51 B — EncoderPfTensorCore::BundleSizeBytes @ 0x1d227740 返回 51;PufferfishCodecMetadata::BundleSizeBytes @ 0x1ecf7ac0 返回 51 |
| Viperfish (v5) | 64 B — ViperfishCodecMetadata::BundleSizeBytes @ 0x1ee71320 返回 64;EncoderVfTensorCore::BundleSizeBytes @ 0x1d2f8520 通过 vtable+48 调用到达它 |
| Ghostlite (v6 lite) | 64 B — GhostliteCodecMetadata::BundleSizeBytes @ 0x1eeb7640 返回 64;EncoderGlTensorCore::BundleSizeBytes @ 0x1d332540 通过 vtable+48 调用到达它 |
| 6acc60406 (TPU7x) | 64 B — 已注册的 codec 元数据;由 CreateEncoderGlGf @ 0x1e831020 构建的 gfc TensorCore codec(case version==5) |
| 宽度分派 | codec_metadata::BundleSizeBytes(TpuVersion, TpuSequencerType) @ 0x1ecf7180 → GetMetadataOrDie → vtable +16 |
| 编码器工厂 | CreateEncoderJfDf @ 0x1e835b80(JF+DF),CreateEncoderPf @ 0x1e835cc0,CreateEncoderVf @ 0x1e835dc0,CreateEncoderGlGf @ 0x1e831020(GL+GF) |
| TpuVersion 枚举 | 6 个值,0..5(TpuVersionToString @ 0x20b3a480 对 ≥ 6 trap);外部名称来自 Codename Matrix |
| 位编号 | LSB-first;BitCopy 打包器 @ 0x1fa0a900 |
| 空槽标记 | HardwareBundleBits::kNeverExecute 谓词,写入 bundle header |
| 编码入口 | Encoder<gen>::EncodeBundleInternal(例如 Jellyfish @ 0x1e86c7c0) |
| 置信度 | CONFIRMED(以字节为锚),除非某行另有说明 |
Bundle 是什么
bundle 是一个 VLIW 发射字:一段固定宽度的字节序列,由序列器从 IMEM 取出,解码为 N 个并行槽,并在单个周期内分派。每个槽驱动一个执行单元——矩阵单元、一个 vector-ALU lane、标量流水线、一个内存端口、序列器的控制逻辑——且 bundle 中的每个槽同时触发。
该模型是经典的静态调度 VLIW,陷阱也是经典 VLIW 陷阱:没有 scoreboard。原始发现精确地描述了这一点——一个序列器每周期发射 N 个并行操作,编译器负责合法 bundle 打包,并且没有运行时依赖跟踪,因为 ISA 假设编译器已经证明正确性。重新实现者必须编码两个后果:
- 无 bundle 内转发。 如果槽 A 写寄存器
X,而同一个 bundle 中的槽 B 读取X,B 看到的是旧值(ISA 将结果视为未定义)。正确的打包器绝不会把真实依赖放在一个 bundle 内。 - 跨 bundle 排序是编译器的职责。 真实依赖要么通过把消费者放入后续 bundle 满足(顺序发射推进程序计数器),要么对于异步引擎(MXU 完成、DMA)由序列器槽发出的 sync-flag / semaphore wait 满足。MXU 尤其是解耦的:一次矩阵 push 进入 systolic array,结果在许多周期后通过 result-FIFO 槽读回,并受编译器在
MxuAssigner/MxuSequence中跟踪的 latch 簿记约束。
陷阱 — bundle 字是发射包,不是基本块。 如果重新实现把 LLO op 调度成每条指令独占一个周期,就会让 bundle 填充不足,错过该架构的核心;如果把有依赖的 op 打包进同一个 bundle 并期待寄存器转发,就会产生静默错误结果。bundle 边界是写→读可见性的硬周期边界。
编译器侧的 Bundle 对象正好建模这样一个字。它不是通用指令列表——它是一条记录,每个槽类有一个带类型、可选的子字段。EncoderJf::EncodeBundleInternal(0x1e86c7c0)按具体 C++ 类型读取这些子字段——platforms_deepsea::jellyfish::isa::ScalarInstruction、VectorAluInstruction、VectorExtendedInstruction、VectorLoadInstruction、VectorStoreInstruction、VectorResultInstruction、MiscInstruction——存在的子字段变成填充槽,不存在的子字段变成空槽。
每代 Bundle 宽度
bundle 字节宽度按 TpuVersion 固定,是该模型中最受 load 锚定的事实:每个按代划分的 codec 都以内联常量返回它。TPU 恰好有六个版本——tpu::TpuVersionToString(0x20b3a480)索引一个 6 项指针表(off_22011BF0),并对任何版本 ≥ 6 调用 LogMessageFatal——其代号(kJellyfish、kDragonfish、kPufferfish、kViperfish、kGhostlite、k6acc60406)都逐字出现在二进制的 .rodata 中。
TpuVersion | 代号 | 公开名称 | Bundle 字节 | 宽度来源 |
|---|---|---|---|---|
| 0 | Jellyfish | TPU v2 | 41 | JellyfishCodecMetadata::BundleSizeBytes @ 0x1ecf7460 return 41 |
| 1 | Dragonfish | TPU v3 | 41 | 共享 Jellyfish codec 元数据(component 0) |
| 2 | Pufferfish | TPU v4 | 51 | EncoderPfTensorCore::BundleSizeBytes @ 0x1d227740 return 51; PufferfishCodecMetadata::BundleSizeBytes @ 0x1ecf7ac0 return 51 |
| 3 | Viperfish | TPU v5 | 64 | ViperfishCodecMetadata::BundleSizeBytes @ 0x1ee71320 return 64 |
| 4 | Ghostlite | TPU v6 lite | 64 | GhostliteCodecMetadata::BundleSizeBytes @ 0x1eeb7640 return 64 |
| 5 | 6acc60406 | TPU7x | 64 | 已注册的 codec 元数据;与 v5+ 同属 64-B 类 |
从 TpuVersion 到其宽度的分派是两步间接,而不是 switch:
// codec_metadata::BundleSizeBytes(TpuVersion v, TpuSequencerType t) @ 0x1ecf7180
metadata = GetMetadataOrDie(v); // 0x1ecf6f60 — abseil flat_hash_map lookup
return metadata->vtable[+16](t); // virtual BundleSizeBytes(t) on the per-gen class
```text
`GetMetadataOrDie`(`0x1ecf6f60`)以一个进程全局的 `CodecMetadataRegistry` 为键源——它是一个 `absl::flat_hash_map<tpu::TpuVersion, CodecMetadata const*>`,通过 `StaticMapBase` 单例构建一次——如果版本从未注册,则以 "Codec metadata not registered for TpuVersion" 终止。每个注册的 `CodecMetadata` 都是按代划分的类(`JellyfishCodecMetadata`、`PufferfishCodecMetadata`、`ViperfishCodecMetadata`、`GhostliteCodecMetadata`),其 `BundleSizeBytes` 虚函数返回上面的内联常量。没有单独以 `6acc60406` 命名的元数据符号;`k6acc60406`(TPU7x)版本由一个复用 64 字节 v5+ 类形状的注册项服务。
> **怪癖 — 宽度以 `(version, sequencer-type)` 为键,第二个参数很重要。** `BundleSizeBytes` 接收一个 `TpuSequencerType`。对 TensorCore 序列器(component `0`),Jellyfish 返回 41;对 BarnaCore 序列器(component `1`),*同一个* `JellyfishCodecMetadata` 返回 **16**(`0x1ecf7460`:`if (t==1) return 16`),其他任何 component 都触发致命 "Unhandled component"。Pufferfish BarnaCore 子核心有自己的 32 字节 bundle(`EncoderPfBarnaCoreSequencer::BundleSizeBytes` @ `0x1d229220` `return 32`,`EncoderPfBarnaCoreChannel` @ `0x1d22bb00` `return 32`)。把“bundle 宽度”当作每芯片标量的重新实现会漏掉同一芯片拥有多个不同 bundle 宽度的序列器类型这一事实。
这些宽度是彼此不兼容的线缆格式。41 字节 Jellyfish bundle 即使做字节扩展,也无法作为 51 字节 Pufferfish bundle 读取:每代都有自己的 `Encoder<gen>` / `Decoder<gen>` 对和自己的槽偏移映射。宽度跨代增长,是因为槽数量增长(更多 vector-ALU lane、更多 vector-load 端口、Pufferfish 的专用 cmem-load 槽、6acc60406 的谓词槽),而不是因为字段被填充。
---
## HBM / DMA 块包装
bundle 宽度是*发射*宽度。bundle 存在 HBM 中并经 DMA 传输到 IMEM 时的宽度略大,而且 bundle 被分组成固定块。Jellyfish 明确展示了这两个事实。
`JellyfishCodecMetadata::BundleSizeBytesForHbm`(`0x1ecf74c0`)对 TensorCore 返回 **42**——比 41 字节发射 bundle 宽 1 字节——也就是说,每个存储的 bundle 带有 1 字节 chunk/metadata 前缀,片上序列器在发射前会剥离它。随后 `EncoderJf::GetBundleByteOffset`(`0x1e86db80`)按如下方式计算一个 bundle 在 DMA 块内的字节偏移:
```c
// EncoderJf::GetBundleByteOffset(bundle_index n) @ 0x1e86db80
if (n < 0 || n >= bundles_per_chunk)
return error("Requested bundle number N exceeds limit of "
"bundles-per-chunk (...)"); // encoder_jf.cc:3202
offset = (n / 3) * 128 // 3 bundles per 128-B chunk
+ (n % 3) * (BundleSizeBytes() + 2); // 41 + 2 = 43-B stride within chunk两个常量由此浮现,且都与记录的模型匹配:每个 DMA 块 3 个 bundle(n / 3 分组)和 128 字节块粒度。BundleSizeBytes() + 2 步长(Jellyfish 为 43 字节)是块内的逐 bundle 槽位;三个这样的槽位(129 字节载荷)向上对齐到带块 header 的 128 字节 chunk。"exceeds limit of bundles-per-chunk" 断言字符串是二进制自身给该常量的名称。
| 代 | 发射 bundle (B) | HBM bundle (B) | Bundle / DMA 块 | 来源 |
|---|---|---|---|---|
| Jellyfish | 41 | 42 | 3 | BundleSizeBytesForHbm @ 0x1ecf74c0; GetBundleByteOffset @ 0x1e86db80 |
| Pufferfish | 51 | (codec-metadata ForHbm) | 10 | EncoderPfTensorCore::BundleSizeBytesForDma @ 0x1d227760 |
| Viperfish+ | 64 | 64 | 1 | 每代 BundleSizeBytesForDma(vtable) |
注意 — 每块 bundle 数按代不同,并随 bundle 变宽而缩小:41 字节 bundle 每个 128 字节块打包 3 个,而 64 字节 v5+ bundle 实际上自己填满一个块。硬编码“每块 3 个 bundle”的重新实现会错位每个 v4+ HBM 镜像。
槽分类
bundle 的槽横跨 TensorCore 的执行单元进行划分。编译器侧 Bundle 对象把该划分编码为每个槽类一个带类型的子指令;下面的名称是 EncodeBundleInternal 分派到的具体 C++ 类型,并锚定到 Jellyfish 编码器中反编译符号的使用。
| 槽类 | Bundle 子指令 | 驱动 | 首代 | 槽页面 |
|---|---|---|---|---|
| 序列器 / 标量 | ScalarInstruction | SPU + 控制流、循环计数器、sync;每个 bundle 一个 | Jellyfish | SPU, Sequencer |
| Vector ALU (VPU) | VectorAluInstruction | VPU lane;数量按代增长(Viperfish 上为四路) | Jellyfish | VPU |
| EUP / 超越函数 | VectorExtendedInstruction | 扩展单元超越函数;也馈入 MXU(DecodeVectorExtendedToMxuNum) | Jellyfish | EUP, MXU |
| Vector load | VectorLoadInstruction | 内存加载端口;Viperfish 上为三路 | Jellyfish | Memory-Load |
| Vector store | VectorStoreInstruction | 内存存储端口 | Jellyfish | Memory-Store |
| Vector result | VectorResultInstruction | MXU result-FIFO 读取(latch result → vreg) | Jellyfish | ResultFifo |
| Misc | MiscInstruction | mask/M-register、rotate-count、立即数设置辅助 | Jellyfish | vcreate_mask, Immediate |
| Bundle header | HardwareBundleBits | 每 bundle 谓词 + 边界位 | Jellyfish | Predicate |
| cmem-load | TensorCoreCmemLoad | 第二 scratchpad 加载端口 | Pufferfish | cmem_load |
| Sparsity | SparseCore sub-bundle | SparseCore 控制 | Viperfish | Sparsity |
| Predicate | 专用谓词槽 | 每 bundle 最多 4 个谓词定义 op | 6acc60406 | Predicate |
MXU 并不是与其他槽同意义上的单个“槽”。矩阵工作通过 EUP / vector-extended 路径和一组 latch/matpush 操作(MxuAssigner、MxuSequence、MatrixMultiplyAccumulateFunctor、DistributeMatpushesEvenly)进入阵列,结果稍后通过 vector-result 槽读回。MXU 槽页面和 matprep/IAR/latch 页面覆盖了这个两阶段模型。
跨代槽数增长
bundle 变宽是因为槽群增长。字节宽度是以字节为锚的(见上文);槽数量根据每代槽类型和 bundle 宽度重建,最好把它们视为增长的形状,而不是 load 锚定的标量。
| 代 | Bundle B | 槽数(约) | 更宽字带来的能力 |
|---|---|---|---|
| Jellyfish | 41 | ~9 | scalar + 1 VPU + EUP + load/store/result + misc + header |
| Pufferfish | 51 | ~12 | 添加 cmem_load 槽;加宽 vector-extended 路径 |
| Viperfish | 64 | ~16 | 四路 VPU、三路 vector-load、ScalarSubBundle、SparseCore 标准化 |
| Ghostlite | 64 | ~16 | 与 Viperfish 容量相同;opcode/encoding 差异,添加 vector-misc 槽 |
| 6acc60406 | 64 | ~17 | 添加专用谓词槽(最多 4 个谓词定义 op) |
注意 — 槽数(9 / 12 / 16 / 16 / 17)不是直接提取的常量。三种字节宽度(41 / 51 / 64)是来自
BundleSizeBytes的内联常量返回,但槽数由每代槽类型名册(每个Bundle暴露哪些带类型子指令)加上 bundle 宽度推导而来。Jellyfish 评为 HIGH(其EncodeBundleInternal枚举了七个带类型槽类加三个 header 载体),后续代在每个按代EncodeBundleInternal被逐一追踪前评为 MEDIUM/LOW。每代 bundle 页面(Jellyfish、Pufferfish、Viperfish)承载已确认映射。
空槽和 NOP 约定
未使用槽不是清零后被忽略的垃圾;它携带定义好的“什么也不做”编码,因而解码器可以往返它。机制是 bundle 谓词。EncoderJf::EncodeBundleInternal 开头先把 kNeverExecute 谓词值盖印到 bundle header 字段中,然后再填充任何槽:
// EncoderJf::EncodeBundleInternal prologue @ 0x1e86c7c0
nx = HardwareBundleBits::kNeverExecute & 0x1F; // 5-bit predicate field
// write kNeverExecute into the bundle's predicate sub-fields (header bits)
bundle[+45] = (nx << 53) | (nx << 26);
bundle[+29] = kNeverExecute << 11;
bundle[+22] = 32 * nx;
// ... then fill present slots, leaving absent slots at kNeverExecute
```text
填充槽随后用真实的 `PredicateAndPolarity` 值覆盖其谓词字段;未触及的槽保持为 `kNeverExecute`,硬件将其读取为“这个槽的 op 在本周期被谓词关闭”。极端谓词值的运行时查询是极性辅助函数 `PredicateAndPolarity::IsAlwaysExecute`(`0x1d5a6ac0`)和 `IsNeverExecute`(`0x1d5a6b00`)。这是规范的空槽编码——完整逐槽图景见 [NOP / Unused-Slot Canonical Encoding](nop-canonical.md),包括为什么确定性空编码对位精确测试很重要。
> **陷阱 — 空并不意味着全零。** 因为默认谓词盖印是 `kNeverExecute`(一个非零 5 位模式,复制进多个 header 字段),新初始化的 bundle *不是*零缓冲区。重新实现若 memset bundle 为零,然后只填充活跃槽,会让非活跃槽停在 predicate-0(它可能表示“always execute”),把空槽变成活的垃圾 op。
---
## 编码 / 解码往返
bundle 有一对对称操作:编码器把 `Bundle` 序列化为字节缓冲区,解码器从字节重建 `Bundle`。两者都按代划分。
```text
LLO stream ──(bundle packer)──▶ Bundle (typed slots) ──▶ Encoder<gen>::EncodeBundleInternal ──▶ N-byte word
│
N-byte word ──▶ Decoder<gen>::Decode* ──▶ Bundle (typed slots) ──(disassembly / verification)─────┘- 编码。
Encoder<gen>::EncodeBundle(公开,接收BundleFacade代理)调用EncodeBundleInternal,它按该代固定字节偏移写入每个存在的槽,并让不存在的槽保持kNeverExecute。Jellyfish:EncoderJf::EncodeBundleInternal@0x1e86c7c0。较新的代通过TpuCodec<gen>::EncodeBundle(例如TpuCodecViperfish::EncodeBundle@0x1e8449a0)路由,再按TpuSequencerType分派到正确的Encoder<gen>实例。 - 解码。
Decoder<gen>::Decode<Slot>Instruction把字节窗口读回带类型子字段——例如DecoderJf::DecodeVectorLoadInstruction(0x1e84c4a0)、DecoderJf::DecodeVectorStoreInstruction(0x1e84b600)、DecoderDf::DecodeVectorExtendedToMxuNum(0x1e849680)。顶层tpu::DecodeBundleBytes<DecoderPfTensorCore>(0x1e843de0)接收absl::Span<const uint8_t>并返回一个BundleFacade代理。解码侧记录在 Decode-Side: JF / PF 和 Decode-Side: VF / GXC 中。
对 BarnaCore-Pxc(Pufferfish)路径,逐槽位还会额外流经 LLVM-MC 239-bit record 和 insertBits;对每个 TensorCore 和 V5+ opcode,MC record 都以全零构建,真实字节完全来自这个 proto-bundle Encoder<gen> 路径。MC record 与 bundle 字之间的关系——一个是逐指令,另一个是逐 bundle——详见 Record Format。
交叉引用
- Jellyfish 41-Byte Bundle — 41 字节 bundle 的完整已确认槽映射(
EncodeBundleInternal)。 - Record Format — 239-bit MC
APIntrecord;逐指令 record 如何关联到逐 bundle 字。 - MC-Emitter —
getBinaryCodeForInstr;BarnaCore-Pxc 路径如何让位流经 record,以及 V5+ 的零基默认值。 - LLO Bundle Packing — 把独立 LLO op 分组到本页所序列化的带类型
Bundle槽中的调度器。