Skip to content

NOP / 未用槽位规范编码

本页中的每个偏移、opcode 值和地址都从 libtpu-0.0.40-cp314 wheel 中的 libtpu.so 按字节精确读取(BuildID md5 89edbbe81c5b328a958fe628a9f2207d — 明确锚点;运行时报告的 0.103 无法在二进制中静态验证)。其他版本会不同。

摘要

TensorCore bundle 是一个固定宽度的 VLIW 发射字 — 41 B(Jellyfish / TPU v2)、51 B(Pufferfish / TPU v4)、64 B(Viperfish / Ghostlite / 6acc60406、TPU v5p / v6e / TPU7x)— 每个执行单元一个槽位(MXU、两条标量 lane、两条向量 ALU lane、向量 load / store / cmem-load、两个 vector-extended-to-MXU 端口、两个 vector-result-FIFO read,以及一个 misc/sequencer 槽位)。在几乎每个周期里,编译器都不会为其中一些槽位安排操作,因此编码器必须发出一个已定义的“此槽位不做任何事”模式,使解码器能够往返处理,并且硬件会可靠跳过。本页是该模式的编码/解码支持参考:空闲槽位和整个空闲 bundle 在线上的样子。

这里有两种不同的无操作机制,重新实现者必须把它们分开。第一种是空槽位谓词填充:每个槽位都携带一个谓词字段,未填充槽位会被盖上架构哨兵 kNeverExecute = 310xb834cfc),sequencer 将其读作“此槽位在本周期被谓词关闭”。这是把缺席槽位变为 no-op 的机制,也是整 bundle NOP 的来源。第二种是 opcode 空间 NOP:每个槽位的 opcode 枚举保留一个专用 Noop 值 — 一个 <Slot>_Noop proto 子消息,其 opcode 字段是该字段的全 1 最大值(5 位 opcode 为 31,4 位 Viperfish vector-extended opcode 为 15)。当某个槽位必须出现在 bundle 的 has-mask 中(因此解码器会访问它)但又不携带工作时,会使用 opcode 空间 NOP;最重要的情况是在 codec 需要一个有效但惰性的模板时物化出的默认 bundle 内部。

两者共存的原因是 bundle 的两级占用模型。bundle 有按槽位的 has-bit(此槽位是否存在于 proto 中?)以及独立的按槽位谓词(如果存在,本周期是否触发?)。最便宜的空槽位会清除 has-bit,并且根本不编码 — 但 codec 的 FillDefaultBundle 路径需要每个槽位都存在(所有 has-bit 置位),使字节确定;对于这些存在但惰性的槽位,它把 kNeverExecute 谓词与 opcode 空间 Noop 配对(或者在 scalar-0 槽位中配对 ScalarHalt)。本页依次介绍:整 bundle NOP 概览;按代际的空槽位谓词填充;每槽位 opcode 空间 Noop 值及其位位置;以及 bundle 打包器在降低 LLO 流时如何留下未用槽位。

对于重新实现,契约是:

  • 空槽位哨兵 kNeverExecute = 31 及各代际盖写它的位置(EncoderJf::EncodeBundleInternal 序言;FillDefaultBundle;V5+ bundle header)。
  • 每个槽位的 opcode 空间 Noop:一个独立 proto 类型,其 opcode 字段是该字段宽度的全 1 值,由 <Slot>NoopOpcode::Matches 识别。
  • 默认 bundle 例外FillDefaultBundle 将谓词 15kAlwaysExecute)+ 一个 ScalarHalt 操作盖写到 scalar-0,并将 31kNeverExecute)盖写到其他每个槽位。
  • 打包器填充规则:打包器让未用槽位的 has-bit 保持清除;编码器的预填充(kNeverExecute)提供 no-op,因此新建的 bundle 不是全零缓冲区。
空槽位哨兵kNeverExecute = 31 (0x1F) @ 0xb834cfc
始终执行哨兵kAlwaysExecute = 15 (0x0F) @ 0xb834cf8
谓词字段宽度每槽位 5 位([0:3] 寄存器索引,[4] 极性;0x1F = never)
Opcode 空间 NOP<Slot>_Noop proto;opcode 字段 = 字段宽度的全 1(5-bit → 31,VF VEx 4-bit → 15
JF 空槽位盖写EncoderJf::EncodeBundleInternal @ 0x1e86c7c0(内联序言 store)
PF 默认 bundleTensorCoreCodecBase<…>::FillDefaultBundle @ 0x1d222ee0
NOP 识别器<Slot>NoopOpcode::Matches(~field & mask) == 0(每个 opcode 位都置位)
默认 scalar-0 操作TensorCoreScalar0_ScalarHalt(在 FillDefaultBundle 中默认构造)
置信度CONFIRMED(字节锚定),除非某行另有说明

整 Bundle NOP 概览

规范空闲 bundle 是每个槽位都被谓词关闭。它不是全零缓冲区:每个槽位的谓词字段都必须持有非零哨兵 31。下表按代际给出必须盖写谓词的槽位数量、盖写值,以及执行盖写的函数。

GenBundle空闲 bundle 定义盖写值盖写函数
Jellyfish (v2)41 B全部 8 个槽位谓词字段 = kNeverExecute31EncoderJf::EncodeBundleInternal @ 0x1e86c7c0(内联序言)
Pufferfish (v4)51 B全部 12 个槽位谓词 = 31;scalar-0 默认 = ScalarHalt@1531 / 15…CodecBase<TensorCoreBundle,…>::FillDefaultBundle @ 0x1d222ee0
Viperfish (v5p)64 B每槽位谓词字段 = 31;缺席槽位不编码31bundle-header 预填充(vxc 编码器集)
Ghostlite (v6e)64 B每槽位谓词字段 = 3131bundle-header 预填充(glc 编码器集)
6acc60406 (TPU7x)64 B两项 TensorCorePredicates 池 + 每槽位选择器;空闲选择器指向一个等价于 31 的池项池索引bundle-header 预填充(gfc 编码器集)

陷阱 — 空闲 bundle 是非零的。 因为空标记是 kNeverExecute = 310x1F)— 一个复制到每个槽位谓词字段中的非零 5 位模式 — 所以先 memset(bundle, 0, width) 再只填充活跃槽位,会让非活跃槽位保持谓词 0,而这是对谓词寄存器 P0 的有效引用(槽位会触发,受 P0 门控)。空值是显式的 31 盖写,绝不是零。用清零内存构造“预期空闲 bundle”的位精确往返测试,会在每个未用槽位上与真实编码器静默不一致。

注意 — “整 bundle NOP”有两种形式,并且编码不同。打包器的空闲 bundle 会让未用槽位的 has-bit 清除,因此它们完全不序列化,只出现 header 预填充的谓词字节。codecFillDefaultBundle 模板会设置所有 has-bit,并给每个存在槽位一个谓词-31 + opcode-Noop 对(scalar-0 除外)。前者是真实调度在稀疏周期中发出的内容;后者是用于定大小、零初始化和解码器预热的确定性全存在模板。


空槽位谓词填充

谓词字段是通用的“跳过此槽位”杠杆,编码器会在任何真实操作覆盖之前,把哨兵写入每个槽位。谓词是 5 位字段:位 [0:3] 是谓词寄存器索引(0..14 = P0..P14,15 = kAlwaysExecute 常真),位 [4] 是极性/取反位,全置位值 0x1F = 31kNeverExecute — 该槽位不运行。三个哨兵常量在 .rodata 中相邻:kPredicateRegisterCount = 15 @ 0xb834cf4kAlwaysExecute = 15 @ 0xb834cf8kNeverExecute = 31 @ 0xb834cfc

Jellyfish — 内联序言盖写

EncoderJf::EncodeBundleInternal0x1e86c7c0)是直接打包编码器:它在一个 53 字节 scratch 结构中工作,用两个 ymm store 清零槽位区域,然后通过把哨兵直接写入该结构 qword 中槽位的绝对 bit,把 kNeverExecute & 0x1F 盖写到每个槽位的谓词字段。反编译并去混淆后的序言如下:

c
// EncoderJf::EncodeBundleInternal prologue  @ 0x1e86c7c0
vmovups [struct+0x20] = 0;  vmovups [struct+0x14] = 0;     // clear the slot region
nx = kNeverExecute & 0x1F;                                  // = 31

*(uint64*)(struct+0x2D) = (nx << 53) | (nx << 26);          // scalar_0 pred @abs 317, scalar_1 @abs 290
*(uint16*)(struct+0x1D) = nx << 11;                         // VALU lane0 pred @abs 147
*(uint32*)(struct+0x16) = nx << 5;                          // VALU lane1 opcode-region init
*(uint16*)(struct+0x1A) = nx << 4;          // = 0x1F0      // VALU lane1 pred @abs 116
*(uint8 *)(struct+0x1C) = 0;                                // window high byte clear
*(uint64*)(struct+0x0C) = (0x400000800000000 * nx)         // VLoad pred @abs 58 + MXU pred @abs 35
                        | (nx << 22)                        // VResult pred @abs 22
                        | (nx << 13);                       // Misc predication @abs 13
```text

常量 `0x400000800000000` 正好有两个置位 bit — bit 35 和 bit 58 — 因此乘以 `31` 会一次性把 `kNeverExecute` 放到 MXU 谓词(abs 35)和 vector-load 谓词(abs 58)。由于结构字节 `0x0C` 是 bundle bit 96,qword-0 的 shift 常量(`<<35`、`<<58`、`<<22`、`<<13`)可直接读作绝对 bundle bit 位置。存在槽位的每槽位 writer(例如 `EmitVectorBinop`)随后会用真实的 `EncodePredication` 值覆盖其谓词字段;缺席槽位保留 `31`。

| 槽位 | 谓词绝对 bit | 预填充 store |
|---|---:|---|
| Scalar 0 | 317 | `[struct+0x2D] \|= 31<<53` |
| Scalar 1 | 290 | `[struct+0x2D] \|= 31<<26` |
| VALU lane 0 | 147 | `[struct+0x1D] = 31<<11` |
| VALU lane 1 | 116 | `[struct+0x1A] = 0x1F0` |
| Vector load | 58 | `[struct+0x0C] \|= 31·0x400000800000000` (bit 58) |
| MXU | 35 | `[struct+0x0C] \|= 31·0x400000800000000` (bit 35) |
| Vector result | 22 | `[struct+0x0C] \|= 31<<22` |
| Misc | 13 | `[struct+0x0C] \|= 31<<13` |

### Pufferfish — `FillDefaultBundle`

Pufferfish 是基于记录的:codec 构建一个全存在默认 bundle,并让字节序列化器读取它。`TensorCoreCodecBase<TensorCoreBundle, …>::FillDefaultBundle`(`0x1d222ee0`)默认构造十二个槽位子消息中的每一个,在 `proto+0x10` 的字中设置全部十二个 has-bit,并把谓词值写入每个子消息的谓词字段(位于子消息偏移 `+0x1c` 或 `+0x20`,取决于槽位类型;反编译为 `*(_DWORD*)(slot+28) = 31` / `*(_DWORD*)(slot+32) = 31`)。这里有**十一个** `= 31` store 加上一个 `= 15` store — 即下面的 scalar-0 例外。

```c
// FillDefaultBundle(TensorCoreBundle*)  @ 0x1d222ee0 (decompiled, de-obfuscated)
TensorCoreBundle(bundle, /*arena*/0);
bundle->has_mask |= 1;                                       // proto+0x10 bit0 = scalar_0 present

s0 = DefaultConstruct<TensorCoreScalar0>(arena);            // slot 0
s0->predicate = 15;                                          // kAlwaysExecute — NOT 31
s0->has_mask |= 4;
s0->inst_case = 6;                                           // ScalarHalt oneof tag
s0->inst = DefaultConstruct<TensorCoreScalar0_ScalarHalt>(arena);   // proto+0x48

s1 = DefaultConstruct<TensorCoreScalar1>(arena);  s1->predicate = 31;   // kNeverExecute
valu0 = DefaultConstruct<TensorCoreVectorAlu0>(arena);  valu0->predicate = 31;
valu1 = DefaultConstruct<TensorCoreVectorAlu1>(arena);  valu1->predicate = 31;
vstore = DefaultConstruct<TensorCoreVectorStore>(arena);  vstore->predicate = 31;
vload  = DefaultConstruct<TensorCoreVectorLoad>(arena);   vload->predicate  = 31;
cmem   = DefaultConstruct<TensorCoreCmemLoad>(arena);     cmem->predicate   = 31;
vex0   = DefaultConstruct<TensorCoreVectorExtended0>(arena);  vex0->predicate = 31;
vex1   = DefaultConstruct<TensorCoreVectorExtended1>(arena);  vex1->predicate = 31;
vres0  = DefaultConstruct<TensorCoreVectorResult0>(arena);    vres0->predicate = 31;
vres1  = DefaultConstruct<TensorCoreVectorResult1>(arena);    vres1->predicate = 31;
misc   = DefaultConstruct<TensorCoreMisc>(arena);            misc->predicate   = 31;
// all 12 has-bits now set; serializer produces the deterministic default 51-byte word

陷阱 — 默认 bundle 不是“所有槽位都被谓词关闭”。Scalar-0 是例外:FillDefaultBundle 把谓词 15kAlwaysExecute 盖写到 scalar-0,并给它一个默认 ScalarHalt 操作,而其他每个槽位都得到谓词 31kNeverExecute)。因此默认 bundle 会执行一条指令 — halt — 并跳过其余槽位。这是一个刻意的安全模板:落入默认状态的 bundle 会停止程序,而不是运行垃圾。把 31 盖写到 scalar-0 以构造“完全空闲”默认值的重新实现,会让执行越过末尾而不是停机。

Viperfish / Ghostlite / 6acc60406 — header 预填充

V5+ 代际(vxcglcgfc)使用通用 bit 打包器 BitCopy(dst, dst_bit, src, src_bit, nbits)0x1fa0a900)构建 64 字节缓冲区,由每个槽位的 <Slot>Encoder::Encode 驱动。空槽位谓词会在任何槽位被填充之前写入 bundle header,其精神与 JF/PF 盖写相同;已填充槽位的每槽位谓词字段随后会覆盖默认值。在 Viperfish 上,谓词是每个标量槽位本地的 4+1 字段(4 位寄存器索引 + 1 位反转)。在 6acc60406 上,每槽位字段缩小为一个 2 位选择器,索引 bundle 级两项 TensorCorePredicates 池;空闲槽位的选择器解析为 never-execute 池项。(VF/GL header 预填充为 HIGH;6acc60406 池选择器空闲值为 MEDIUM — “never”的确切池项编码未像 JF/PF 的 31 盖写那样绑定到单个 store,见残余空白。)


每槽位 Opcode 空间 NOP

独立于谓词,每个槽位的 opcode 枚举都保留一个 Noop 值。它存在的原因是让一个槽位能够出现在 has-mask 中并被解码器访问,同时不携带操作 — 这是 FillDefaultBundle 产生的情况,也是反汇编器必须往返处理的情况。Noop 是一个独立 proto 子消息类型(<Slot>_Noop),不持有字段 — 其构造函数只安装 vtable 和 arena 指针(TensorCoreScalar0_Noop::TensorCoreScalar0_Noop @ 0x1fa64240TensorCoreMisc_Noop @ 0x1fa62000vxc::isa::TensorCoreVectorExtended_Noop @ 0x1fb0a120)— 并且其 opcode 字段是该字段 bit 宽度的全 1 最大值

全 1 规则

规范证据是每个槽位的 <Slot>NoopOpcode::Matches 谓词。Matches 当且仅当槽位 opcode 字段的每一位都置位时返回 true,在二进制中写作惯用式 (~field & mask) == 0 — 即 ~fieldmask 内没有任何 bit,因此 field 拥有 mask全部 bit。该 mask 是该槽位的连续 opcode 字段:

Gen / slot<Slot>NoopOpcode::Matches @Opcode mask字段宽度 / 位置NOP opcode 值
PF VectorAlu00x1ed408200x1F000000000005-bit @ bit 4431
PF VectorAlu10x1ed63c80(5-bit, sym to Alu0)5-bit31
PF Scalar00x1ed144800xF800005-bit @ bit 1931
PF Scalar10x1ed27be0(5-bit)5-bit31
PF Misc0x1ed016c00x1F0000000005-bit @ bit 3631
PF VectorLoad0x1ee280e00x1F005-bit @ bit 831
PF VectorStore0x1ee38e000x7C000000005-bit @ bit 3431
PF CmemLoad0x1ecf87e00x7C0000000000005-bit @ bit 5031
PF VectorExtended00x1eda64000x7C000000005-bit @ bit 3431
PF VectorExtended10x1edfdc60(5-bit)5-bit31
PF VectorResult00x1ee2ae400x7C000000000000005-bit @ bit 5831
PF VectorResult10x1ee2ce00(5-bit)5-bit31
VF VectorExtended00x1ef984800xF4-bit @ bit 015
VF VectorExtended10x1efe3540(4-bit)4-bit15

示例 — TensorCoreVectorAlu0NoopOpcode::Matches0x1ed40820)反编译为单个表达式:

c
// asic_sw::deepsea::pxc::isa::TensorCoreVectorAlu0NoopOpcode::Matches  @ 0x1ed40820
bool Matches(this) {
    return (~*(uint64*)(this + 32) & 0x1F00000000000LL) == 0;   // every bit of the 5-bit
}                                                                // opcode field (bits 44..48) set
```text

`0x1F00000000000` 是 `0b11111 << 44` — 一个全置位值为 `31` 的 5 位字段。相同形态会在每个 PF 槽位中重复出现(始终 5 位,始终全 1 = `31`),并在 Viperfish vector-extended 槽位中缩小为 4 位(`0xF`,全 1 = `15`),后者的 opcode 空间窄一位。

> **怪癖 — NOP opcode 是*最后一个* opcode,而不是第一个。** 每个槽位的 opcode 枚举都把 `Noop` 放在最大可编码值(全 1),而不是 `0`。Opcode `0` 在每个槽位中都是真实操作(例如第一个算术 op),因此为“无工作”发出 opcode `0` 的重新实现会发射一个活跃 ALU op。可靠的“此槽位 opcode 空间中无工作”值是字段的全 1 最大值,解码器通过全 bit 置位测试识别它,而不是通过单个具名常量。
>
> **注意 — opcode-`Noop` 和 predicate-`kNeverExecute` 是正交的,并且通常组合使用。** 在 `FillDefaultBundle` 中,非 scalar-0 槽位既是 opcode-`Noop`,也是 predicate-`31`。任一者单独都足以让槽位惰性:predicate-`31` 会无视 opcode 跳过它,而 has-bit 清除的槽位甚至不会被解码。opcode-`Noop` 专门在存在且被访问的情况下重要,此时解码器必须分类 opcode bit,否则会拒绝未知值。

### LLO 层 no-op

在 bundle 之上,[LLO opcode 枚举](llo-opcode-enum.md)携带自己的具名 no-op,并降低为这些槽位编码:`kVectorNop`(`0x047`,向量 no-op,与 `kVectorMaskMove` 共享 `GhPerf` cost 行 `0x1B4`)会降低到一个 VALU 槽位,其 opcode 空间值是全 1 `Noop`;`kScalarHalt`(`0x025`,`…YieldConditional`/`…OnError` 位于 `0x026`/`0x027`)是 scalar-0 默认操作物化出的 sequencer halt。这些是 IR 可见名称;bundle 字节是上文记录的全 1 opcode 和谓词哨兵。

---

## Bundle 打包器对未用槽位的填充

[bundle 打包器](../sched/llo-bundle-packing.md)把独立 LLO op 分组到一个 `Bundle` proto 中,每个已填充槽位一个有类型子消息,其余留空。它**不会**合成 `Noop` op,也不会清零任何内容 — 它完全依赖编码器序言的 `kNeverExecute` 预填充。该模型可从 JF `MakeInstruction` / `FindFreeSlot` 路径以及 `EncodeBundleInternal` 中的槽位掩码分派看到:

```c
// Conceptual packer → encoder flow (JF; PF/V5+ analogous)
function PackAndEncode(llo_ops, bundle):
    bundle.slot_mask = 0;                       // proto+0x10 — all slots absent
    for op in llo_ops_in_this_cycle:
        slot = FindFreeSlot(bundle, op.kind);   // 0x140c0180 — legality + occupancy
        if slot == NONE: break;                 // start a new bundle
        slot.fill(op);                          // EmitVectorBinop / EmitScalarLoad / …
        bundle.slot_mask |= (1 << slot.index);  // mark has-bit set

    EncodeBundleInternal(bundle):               // 0x1e86c7c0
        prefill_all_predicates(kNeverExecute);  // 31 into every slot's predicate field
        for slot in slots:
            if bundle.slot_mask & (1 << slot.index):
                slot_encoder.write(bundle.sub[slot]);   // overwrites predicate with real value
            // else: leave the prefilled kNeverExecute — this is the canonical empty slot

重新实现者必须复现三个后果:

  • 未用槽位在 proto 中缺席,从不是显式 Noop 打包器让 has-bit 保持清除;槽位字节完全来自预填充。显式 <Slot>_Noop op 只通过 FillDefaultBundle 出现,不通过正常打包出现。
  • 预填充就是契约。 因为编码器先清零槽位区域,然后31 盖写到谓词子字段,空槽位字节是确定且解码稳定的。跳过预填充会让空槽位继承 scratch 缓冲区中原有的任何内容。
  • ValidatePacking 会重新检查谓词范围。 每个槽位的谓词字段都必须在 {0..14, 15, 31} 内;打包器的 hazard 分析(add_consumes_predicate_register 0x1e851be0add_produces_predicate_register 0x1e851c60)跟踪谓词 def-use,因此已打包 bundle 的谓词合法,而未填充槽位的预填充 31 按构造通过。

陷阱 — 没有 bundle 内转发意味着空槽位确实是空闲的。 因为 bundle 中所有槽位在同一周期发射,并且没有 write→read 转发(见 Bundle 模型),留下空槽位不会带来正确性成本 — 只会浪费发射宽度。打包器的职责是为吞吐量填充槽位,而不是满足 bundle 内依赖;真实依赖总是跨 bundle 序列化或通过同步标志等待。因此规范 NOP 纯粹是密度产物,而不是 hazard 修复。


解码侧

解码器会往返处理两种机制。谓词读取是 DecoderJf::GetPredicateRegister0x1e84eae0,返回 v & 15 — 4 位寄存器索引,其中 15 = always)和 GetPredicateValue0x1e84eb00,返回 (v & 16) == 0 — 极性位);完整 5 位读取到 0x1F 就是 kNeverExecute skip。opcode 空间 Noop 由每个槽位 Decode 阶梯内部的 <Slot>NoopOpcode::Matches 全 bit 置位测试识别。V5+ 解码器按固定顺序尝试 opcode 候选 — gfc::isa::TensorCoreVectorExtended0Decoder::Decode0x1f96d020)会遍历 LoadMultiplicandUpperBlock…B32MaskedB16MaskedB8Masked opcode Matches 谓词;拥有 Noop 的向量槽位(例如承载 MatrixMultiply*/PushMatrix* 的 extended 槽位)会较早测试 Noop,因此惰性槽位能解析,而不会落入“value does not match any encodings”状态错误。见 解码侧:JF / PF解码侧:VF / GXC


残余空白

  • 6acc60406 (gfc) 空闲槽位值 — MEDIUM。 6acc60406 用指向两项 TensorCorePredicates 池的 2 位选择器替代每槽位谓词字段。因此整 bundle 空闲模式取决于池内容,而“never execute”的确切池项编码(以及未填充槽位携带的默认选择器)没有像 JF/PF 的 31 盖写那样绑定到单个 store。预期全 1 opcode-Noop 规则仍然成立(gfc 向量槽位携带同一 Noop opcode 族),但尚未读取 gfc::…NoopOpcode::Matches 函数体来确认字段宽度;对 6acc60406 标记为 Inferred。缺失证据: gfc bundle-header 预填充 store 和一个 gfc NoopOpcode::Matches 函数体。
  • Viperfish / Ghostlite header 预填充位置 — HIGH 而非 CONFIRMED。 vxc/glc 空槽位谓词会在槽位填充前写入 bundle-header 构造(这与每槽位 4+1 谓词字段以及上文确认的 VF TensorCoreVectorExtended0NoopOpcode::Matches 4 位 0xF 一致),但单条预填充指令没有像 JF/PF 的 EncodeBundleInternal/FillDefaultBundle 那样隔离到一个地址。缺失证据: 确切的 vxc/glc header-stamp store 地址。

交叉引用

  • Bundle 模型 — VLIW no-scoreboard 契约以及本页按槽位展开的 kNeverExecute 空槽位约定
  • Bundle: Jellyfish 41 BEncodeBundleInternal 序言和 JF 预填充的每槽位谓词绝对 bit 映射
  • Bundle: Pufferfish 51 BFillDefaultBundle 以及十二槽位 31 盖写 / ScalarHalt 默认值