239 位记录格式
本页中的每个偏移、值和地址都按字节精确读取自
libtpu-0.0.40-cp314wheel 中的libtpu.so(BuildID md589edbbe81c5b328a958fe628a9f2207d)。其他版本会有所不同。
摘要
TPU MC 层把一条 LLO 机器指令编码为一个固定宽度的 llvm::APInt,宽度为 239 位,即一个 4 字(4 × uint64,32 字节)值,其中最高 17 位是未使用的填充位。这是 LLVM-MC 发射器(TPUMCCodeEmitter::getBinaryCodeForInstr,0x13c74da0)在把字节切片进各代 VLIW bundle 之前填充的中间记录。这个宽度不是任意选择:发射器直接从每操作码的基础位表(InstBits)构造 APInt,其中 BitWidth = 0xEF = 239、NumWords = 4,然后用 APInt::insertBits(value, pos, width) 覆盖该基础值中操作数形状的空洞。239 位窗口的大小对应最宽的单个 BarnaCore VLIW 槽:getBinaryCodeForInstr 中最高的字段写入是位于 insertBits(…, /*pos=*/0xDF=223, /*width=*/16) 的 16 位字段,以及位于 insertBits(…, /*pos=*/0xAF=175, /*width=*/64) 的 64 位字段,二者都结束于位 238,也就是发射器曾写入的最高位,正好比 239 位宽度低一位。无论操作码、世代或 HwMode 如何,这个窗口宽度都相同。
本页由三个事实驱动,重实现者三者都需要:
- 记录是宽指令
APInt,不是uint64。getBinaryCodeForInstr携带两个尾随的APInt&输出,这是 LLVM 对超过 64 位指令采用的宽指令发射器形式。第一个(a3)是组装好的 239 位记录;第二个(a4)是每个操作数使用的临时APInt,发射器会在字段之间把它清零并复用。只返回单个 64 位代码字的重实现无法表示 TPU MC 指令。 - 记录是基础位加 insertBits 空洞。 239 位值从
InstBits[opcode−499](32 字节行)的副本开始。已置位的位是固定操作码判别符和默认字段值;零空洞正是操作数编码器填充的(pos, width)窗口。每个已填充字段都由一次insertBits(value, pos, width)调用写入,其(pos, width)按编码类固定。 - TensorCore 和 V5+ 的默认基础值全为零。 磁盘上的默认
InstBits表(0x3366d90,0x2c460B)完全为零;已逐字节验证,没有非零字节,也没有重定位。只有InstBits_BarnaCorePxcHwMode变体(0x33931f0)携带真实基础位。对每个 TensorCore 和 Viperfish/Ghostlite/6acc60406操作码,记录到达发射器时全为零,没有insertBits运行,实际 bundle 字节由独立的 proto-bundleEmitX→<Slot>Encoder::Encode路径产生。参见 V5+ EmitX 位位置。
| 记录类型 | llvm::APInt,BitWidth = 0xEF = 239,NumWords = 4(32 B) |
| 填充者 | TPUMCCodeEmitter::getBinaryCodeForInstr @ 0x13c74da0 |
| 基础位表 | InstBits @ 0x3366d90(默认,全零)/ InstBits_BarnaCorePxcHwMode @ 0x33931f0(已填充) |
| 每表记录数 | 5667 行(0x2c460 表大小 / 32 B 步幅);索引 opcode − 499;边界测试为 4·index < 0x588D |
| 第一个真实操作码 | 0x1f3 = 499(ADDri);操作码 ≤ 498 是伪指令/目标无关指令 |
| 操作数原语 | APInt::insertBits(value, pos, width) |
| 填充位 | 位 [239:255](256 位存储的最高 17 位)未使用 |
239 位窗口
TPU MC 指令记录是每操作码 TableGen 数组中的一行。该行是 32 字节,即四个小端 uint64 字,发射器把它解释为一个 239 位 APInt:
word0[63:0] word1[127:64] word2[191:128] word3[255:192]
|--------------- 239-bit instruction value ----------------|·············|
bit 0 = LSB of word0 bit 238 bits 239..255 = padding
```text
宽度直接从发射器序言读出。反汇编解析为 `APInt(/*BitWidth=*/239, ArrayRef<uint64>(record, /*NumWords=*/4))`,反编译主体也确认了这个字面量:
```c
// TPUMCCodeEmitter::getBinaryCodeForInstr @ 0x13c74da0 (prologue, decompiled)
v5 = *(uint32_t *)a2; // v5 = MCInst opcode
if (*(uint32_t *)a2 <= 0x1F2u) // opcode <= 498 -> not an MC opcode
reportUnsupportedInst(a2, a2);
v8 = (uint32_t)(4 * v5 - 1996); // index*4 ; 1996 = 0x7CC = 499*4
if ((uint32_t)v8 >= 0x588D) // bound: index*4 < 22669 -> 5667 records
__asm { ud1 }; // out of range -> trap
// record = InstBits + (opcode-499)*32 (the GOT-relative base resolves to 0x3366d90)
APInt(&record, /*BitWidth=*/239, &GLOBAL_OFFSET_TABLE_ + v8 - 65189758, /*NumWords=*/4);有两个派生常量值得固定下来。记录步幅是 32 字节(4 × uint64),因此 (opcode − 499) × 4 个字会前进一行。磁盘上的 InstBits 符号为 0x2c460 = 181344 字节 = 5667 × 32,所以该表包含 5667 行(有效索引 0 … 5666)。边界测试 index·4 < 0x588D(= 22669)允许 index·4 ≤ 22668,即 index ≤ 5667,比 5667 行表多一个槽;索引 5667 会读到符号之后 32 字节。该陷阱防护远高于有效范围的操作码,而不是这个单独的边界索引。第一行(索引 0)属于操作码 0x1f3 = 499(ADDri);具名助记符数据库(TPUInstrNameIndices @ 0x3435d30 / TPUInstrNameData @ 0x33f2be0)逐字节确认 [499] = ADDri、[505] = BRabs、[571] = HALT、[3978] = bcLOOP_START(并且 [0] = PHI 位于 499 以下的伪操作码中)。参见 InstBits 数据库。
注意 — 256 位存储减去 239 位逻辑宽度后,位
[239:255]是无效填充。重实现必须把字段写入掩蔽在 239 位窗口内;越过高端的insertBits是静默编码错误,因为APInt存储会容忍这种溢出。
基础位和操作数空洞
记录从来不是从零开始逐字段构建的。它先逐字复制每操作码的基础行,然后操作数编码器覆盖其中的空洞:
// conceptual fill order, per populated (BarnaCore) opcode
APInt record(239, InstBits_BarnaCorePxcHwMode[opcode - 499], /*NumWords=*/4); // base
for (operand i in this encoding class) {
uint64_t v = encode_operand(i); // reg# / imm / predicate / expr
record.insertBits(v, pos_i, width_i); // (pos_i, width_i) fixed per class
}
```text
基础值中的置位位编码固定操作码判别符和任何默认字段值;**零空洞**精确标记循环写入的 `(pos, width)` 窗口。这两个事实是耦合的:BarnaCore 操作码的基础值和其编码类的 `insertBits` 序列互为补充,空洞图既可以通过读取基础值中的零连续段恢复,也可以通过读取 case 主体的 `insertBits(pos, width)` 参数恢复;二者逐位一致。
对于**默认**(TensorCore / V5+)操作码,基础行全为零,编码类是无操作的默认 case:记录被构造,没有 `insertBits` 运行,发射器返回零记录。指令的真实字段位在别处写入,即由 proto-bundle `EmitX` 填充器和每槽 `<Slot>Encoder::Encode` 的 `BitCopy` 调用写入,从不经过这个 239 位记录。这就是为什么默认 `InstBits` 表在磁盘上确实为零且不携带重定位:没有任何需要重定位的内容,因为没有任何东西读取它。
| 操作码组 | 基础行 | `getBinaryCodeForInstr` 中的 insertBits | 字节来源 |
|---|---|---|---|
| BarnaCore-Pxc(`_V0/_V1/_V2/_VM`,`bc*`) | 已填充(`InstBits_BarnaCorePxcHwMode`) | 是,按编码类 | 239 位记录 |
| TensorCore / Viperfish / Ghostlite / `6acc60406` | 全零(`InstBits` 默认) | 无(默认 case) | proto-bundle `EmitX` + `<Slot>Encoder::Encode` |
| 伪指令 / 目标无关指令(`opcode ≤ 498`) | n/a | n/a — `reportUnsupportedInst` | 在 MC 发射前展开 |
---
## insertBits 原语
`APInt::insertBits(value, pos, width)` 是记录路径中唯一的字段写入原语。它把 `value` 的低 `width` 位按 LSB 优先沉积到目标 `APInt` 的位偏移 `pos` 处,并保持其他所有位不变。发射器以两种形态使用它,二者都能在反编译主体中看到。
第一种形态是直接沉积小常量或寄存器编码查表结果。谓词操作数是最清晰的例子;它的三次写入给出字段窗口中位 `[0:6]` 的规范 7 位谓词字段:
```c
// TPUMCCodeEmitter::encodePredicateOperand @ 0x13c77c40 (decompiled)
// a4 = destination APInt ; reg-encoding table at a1 ; operand record at a2
insertBits(a4, RegEncodingTable[ a2->preg_index ], /*pos=*/0, /*width=*/4); // reg index
if (a2->flags & 1) // negate bit
a4.word0 |= 0x10; // -> bit 4
insertBits(a4, (a2->flags >> 5) & 3, /*pos=*/5, /*width=*/2); // mode, bits 5:6第二种形态是用于通用操作数的暂存、提取、再沉积惯用法。发射器先把临时 APInt(a4)清零,调用 getMachineOpValue 把操作数降低到其中,再用 extractBitsAsZExtValue 读回低位,并在编码类固定的位置把它们沉积到真实记录(a3 / v260)中。具体地,来自一个 load-slot 类主体:
// excerpt: a BarnaCore load-slot operand fill (getBinaryCodeForInstr, decompiled)
*a4 = 0; // clear the per-operand scratch
getMachineOpValue(self, inst, &inst->op[1], a4); // lower operand -> a4
v = extractBitsAsZExtValue(a4, /*width=*/5, /*lo=*/0);// read its low 5 bits
insertBits(record, v, /*pos=*/0x58, /*width=*/5); // deposit at bit 88 of the record
```text
临时层存在是因为 `getMachineOpValue` 本身可能发出 fixup、设置多个子字段或扩宽该值;把它暂存在私有 `APInt` 中,可以在最终按位置固定的 `insertBits` 之前,使这些副作用不影响记录。只有在精确复现 `getMachineOpValue` 的低位值时,重实现才可以把临时往返折叠为一次直接掩蔽沉积;暂存路径可通过 fixup 列表观察到,而不只是最终位可见。
> **怪异点 — 第二个 `APInt&` 输出是临时值,不是第二个结果。** LLVM 的宽指令发射器签名传入两个 `APInt&`。在大多数目标上,第二个是高位字延续。在 TPU 上,它被复用为每操作数暂存缓冲区:每个操作数前被 `memset` 清零,由 `encodePredicateOperand` / `getMachineOpValue` 写入,由 `extractBitsAsZExtValue` 读回,然后丢弃。只有 `a3` 作为指令记录保留下来。把 `a4` 当成高位字的重实现会在 bit 64 以上产生垃圾。
---
## 操作数值来源
`getMachineOpValue`(`0x13c777e0`)是为暂存 `insertBits` 供给输入的通用操作数降低逻辑。它解析三种操作数,由查询每操作码描述符来选择:
- **寄存器操作数** → 在 `TPURegEncodingTable`(`0x34469b0`,889 项,reg# → 硬件编码)中进行 `uint16` 查表。反编译器把它呈现为 `*(uint16_t *)(table + 2*reg_index)`。该表的谓词寄存器块保存值 `1..15`(`P0..P14`),这就是谓词字段的寄存器索引为 4 位的原因。
- **立即数 / 表达式操作数** → `getSpecialOpEncoding(MCInstrDesc&, opno)`(`0x13c63a80`),它从操作码描述符(`TPUDescs`,通过 `getMachineOpValue` 查询)读取每操作数编码类。这使同一个 `insertBits` 位置可以根据操作数描述符类编码原始立即数、可重定位表达式或标签 fixup。
- **标签 / 符号操作数** → 作为 `MCFixup` 发射并追加到 fixup 列表(发射器的第一个非 `APInt` 参数),记录中的位窗口留给链接器/汇编器修补。
描述符查询把记录格式和描述符表绑定在一起:把“操作数 1 是寄存器”硬编码的重实现,会错误编码任何把该操作数标为特殊编码立即数的操作码。沉积的 `(pos, width)` 按类固定,但*写入其中的值*是每操作数由描述符驱动的决定。描述符表和寄存器编码表参见 [InstBits 数据库](instbits-master-db.md)。
---
## 与 Bundle 字节宽度的关系
239 位记录比任何单个按世代划分的 bundle 槽都宽,但比整个 bundle 窄。它是*每指令*中间值,而不是*每 bundle* 中间值:一条 LLO 机器指令 → 一个 239 位记录 →(经由 bundle 打包器)特定世代 bundle 字中的一个槽的字节。它供给的 bundle 宽度按世代固定:
| 代号(序号) | 公开名称 | Bundle 字节 | Bundle 位 | MC 记录路径 |
|---|---|---|---|---|
| Jellyfish (0) | TPU v2 | 41 | 328 | proto-direct(无 `insertBits` 记录) |
| Dragonfish (1) | TPU v3 | 41 | 328 | 共享 Jellyfish 编解码路径 |
| Pufferfish (2, BarnaCore) | TPU v4 | 51 | 408 | **239 位记录 + insertBits**(`InstBits_BarnaCorePxcHwMode`) |
| Viperfish (3) | TPU v5p(+v5e lite) | 64 | 512 | 零基础 → proto-bundle `EmitX` |
| Ghostlite (4) | TPU v6e | 64 | 512 | 零基础 → proto-bundle `EmitX` |
| `6acc60406` (5) | TPU v7 | 64 | 512 | 零基础 → proto-bundle `EmitX` |
唯一实际通过这个 239 位记录流入 bundle 字节的世代是 **Pufferfish BarnaCore** HwMode:其 vector-ALU lane(`_V0/_V1/_V2/_VM`)和原生操作(`bc*`)是 `InstBits_BarnaCorePxcHwMode` 的 **704 个已填充行**(通过计数非零 32 字节行验证;第一个是操作码 `2855`),其字段位置可从记录恢复。`getBinaryCodeForInstr` 中观察到的最高位置沉积包括 `pos = 0xCF = 207` 的 16 位字段(位 `[207:222]`)、`pos = 0xDF = 223` 的 16 位字段(位 `[223:238]`),以及 `pos = 0xAF = 175` 的 64 位字段(位 `[175:238]`),全部结束于**位 238**,即发射器写入的最高位。bit-238 上限正是 239 位窗口如此定大小的原因:它必须容纳最大的单个 BarnaCore 槽,并在其下方给操作码判别符留出空间。对 V5+ 世代,记录只是一种形式:它被构建、发现全零,然后被绕过,实际由 proto-bundle 编码器直接写入 bundle 字节缓冲区。按世代的 bundle 字布局和槽映射参见 [Bundle 模型](bundle-model-overview.md)。
> **注意 —** bundle 宽度*不是*记录宽度。239 位记录是 MC 发射器针对一条指令的工作单元;bundle 打包器负责把该记录的一个切片放入 41/51/64 字节 bundle 字中相对槽的偏移处。把二者混同,例如假设 64 字节 V5+ bundle 是“两个半 239 位记录”,是不成立的,因为在 V5+ 上记录贡献零位,bundle 完全由 `BitCopy` 组装。
---
## 交叉引用
- [Bundle 模型](bundle-model-overview.md) — 记录所供给的按世代 41/51/64 字节 VLIW bundle 字和槽映射。
- [MC 发射器](mc-emitter.md) — `getBinaryCodeForInstr`、每操作码分发,以及填充此记录的完整发射流水线。
- [InstBits 数据库](instbits-master-db.md) — 构建记录所用的 `InstBits` / `InstBits_BarnaCorePxcHwMode` 基础位表、`TPUDescs`、`TPUInstrNameData` 和 `TPURegEncodingTable`。
- [Kisatable 数据段](kisatable-data-sections.md) — `TPUDescs`、`TPUStages`、`TPUInstrNameData` 和 `TPUInstrNameIndices` 的磁盘地址和字节大小,此记录的表与它们相邻。
- [V5+ EmitX 位位置](v5plus-emitx-bit-positions.md) — 当此记录全为零时(每个 TensorCore / V5+ 操作码),产生真实字节的 proto-bundle `EmitX` → `<Slot>Encoder::Encode` 路径。