双枚举 — Proto vs Internal
地址适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so。其他版本会不同。
摘要
在 libtpu.so 内部,TPU 代际由两个不同的 C++ 枚举表示,并且它们刻意相差一。内部运行时枚举 tpu::TpuVersion 是 0-based 且按时间顺序排列:从 kJellyfish=0 到 k6acc60406=5。线上枚举 tpu::TpuVersionProto 是 1-based,保留 0 作为未指定/无效哨兵:从 TPU_VERSION_JELLYFISH=1 到 TPU_VERSION_6acc60406=6。所有跨越序列化边界的内容 — TpuChipPartsProto、program ABI proto、携带 target 描述的 RPC — 都使用 proto 枚举;所有运行时分发都使用内部枚举。二者之间的桥梁是单一算术恒等式 internal = proto − 1。
这是标准的 protobuf-versus-runtime 拆分,并且出于标准原因存在。Protocol Buffers 约定将枚举值 0 保留给默认/未知值,wire format 会把它赋给任何从未显式设置的字段;如果第一个真实代际是 proto 值 0,未设置的 version 字段就会静默解码为 Jellyfish。因此 proto 枚举从 1 开始放置真实值,并把 0 保留为 TPU_VERSION_INVALID。运行时枚举没有这种约束 — 它从不被默认解码,只会被显式赋值 — 因此从 0 开始,作为稠密的零基分发索引,直接映射到指针表和跳转表偏移。这两个局部合理选择的代价是同一块硅片携带两个不同整数;读者如果在序列化 blob 中看到 "version 6",在运行时日志中看到 "version 5",看到的是同一块芯片。
本页记录两个转换函数(TpuVersionFromProto、TpuVersionToProto)、通过代号字符串往返的 flag parse/unparse 对、完整 wire-value 表,以及这个差一现象作为现实陷阱出现的一个位置:嵌入的 chip_parts blob,其 version 字段对运行时称为 version 5 的硅片读出 6。
对重新实现而言,契约是:
- 两个枚举及其取值空间:内部
TpuVersion∈[0,5],protoTpuVersionProto∈[1,6],且0 = TPU_VERSION_INVALID。 - 双向转换:
ToProto(v) = v + 1(全函数),FromProto(p) = p − 1对p ∈ [1,6]成立,否则报错。 - 哪些 API 表面使用哪种枚举,使序列化数据和运行时分发对同一块硅片达成一致。
| 内部枚举 | tpu::TpuVersion — 0-based,kJellyfish=0 … k6acc60406=5 |
| wire 枚举 | tpu::TpuVersionProto — 1-based,TPU_VERSION_INVALID=0,TPU_VERSION_JELLYFISH=1 … TPU_VERSION_6acc60406=6 |
| proto → internal | tpu::TpuVersionFromProto @ 0x20b3a8c0 — switch,internal = proto − 1,StatusOr |
| internal → proto | tpu::TpuVersionToProto @ 0x20b3a8a0 — return version + 1;(1 条指令,全函数) |
| proto → internal(fatal) | tpu::TpuVersionFromProtoOrDie @ 0x20b3aa20 — 包装 FromProto,在 tpu_version.cc:428 处终止 |
| flag parse | tpu::AbslParseFlag @ 0x20b3aaa0 — 代号字符串 → 内部枚举 |
| flag unparse | tpu::AbslUnparseFlag @ 0x20b3ab40 — 内部枚举 → 代号字符串 |
| v6 陷阱 | 嵌入的 chip_parts.binarypb version 字段 = 6(proto)= 内部 5(6acc60406) |
两个枚举,一块硅片
注意:差一是刻意的双轴设计,不是 bug。 两个正确枚举在两条不同数轴上描述同一块硅片,它们之间的
±1是有意的。内部轴是tpu::TpuVersion∈[0,5]:kJellyfish=0, kDragonfish=1, kPufferfish=2, kViperfish=3, kGhostlite=4, k6acc60406=5。wire 轴是tpu::TpuVersionProto∈[0,6]:TPU_VERSION_INVALID=0, TPU_VERSION_JELLYFISH=1, TPU_VERSION_DRAGONFISH=2, TPU_VERSION_PUFFERFISH=3, TPU_VERSION_VIPERFISH=4, TPU_VERSION_GHOSTLITE=5, TPU_VERSION_6acc60406=6。重新实现必须携带两条轴,并在边界处转换(internal = proto − 1、proto = internal + 1);把它们压到同一条数轴上,要么重新引入 proto 轴本来要避免的未设置字段风险(如果把 wire 枚举做成 0-based),要么浪费一个分发槽并迫使每个表索引处都做−1(如果把内部枚举做成 1-based)。不要“修复”这个间隔 — 要尊重它。
这两个枚举并不冗余;每个都适合它所在的位置。
TpuVersion(内部枚举)是稠密的零基分发索引。它是 TpuVersionToString 用来索引 off_22011BF0 指针表的值,是 TpuCodec::Create switch 的值,也是 HAL 工厂构造时携带的值。零基稠密性很重要,因为该值被用作数组下标:off_22011BF0[version] 是直接加载,而一基枚举会浪费槽 0,或者迫使每个索引点都做 -1。
TpuVersionProto(wire 枚举)是一个保留 0 的一基 protobuf 枚举。枚举器名称来自代号 — TPU_VERSION_JELLYFISH、TPU_VERSION_DRAGONFISH、TPU_VERSION_PUFFERFISH、TPU_VERSION_VIPERFISH、TPU_VERSION_GHOSTLITE、TPU_VERSION_6acc60406 — 其中 TPU_VERSION_INVALID 占用值 0。这些名称是作为 .rodata 反射字符串恢复出来的,protobuf 描述符携带它们用于 text-format 打印和解析。
注意: proto 枚举器字符串是
TPU_VERSION_<CODENAME>(并有TPU_VERSION_INVALID=0),由二进制的反射字符串表确认 — 不是TPU_V2/TPU_V3/TPU_V7X。那些TPU_v*token 是外部显示和 Cloud accelerator-type 名称(由TpuVersionToExternalName生成的"TPU v4"/"TPU7x"字符串),属于另一条轴。数值映射(proto 1↔Jellyfish … proto 6↔6acc60406)无论如何都相同;枚举器标签采用的是代号形式。
转换函数
TpuVersionToProto — internal → wire
正向转换简单到极致。tpu::TpuVersionToProto(0x20b3a8a0)是一条算术指令:
TpuVersionProto TpuVersionToProto(TpuVersion version) { // sub_20B3A8A0
return version + 1; // total function, no bounds check
}
```text
它是全函数且不检查 — 没有范围守卫,因为调用方预期持有有效的内部枚举,而 `version + 1` 会把 `[0,5] → [1,6]` 精确映射到 proto 范围。没有守卫本身就是契约证据:生产侧信任自己的枚举,序列化时只把它上移一位。
### `TpuVersionFromProto` — wire → internal
反向转换必须验证,因为 proto 值来自外部(反序列化 blob、RPC 字段),可能是 `0`(未设置)或越界。`tpu::TpuVersionFromProto`(`0x20b3a8c0`)返回 `StatusOr<TpuVersion>`:
```c
StatusOr<TpuVersion> TpuVersionFromProto(TpuVersionProto proto) { // sub_20B3A8C0
switch (proto) {
case 1: return ok(0); // TPU_VERSION_JELLYFISH -> kJellyfish
case 2: return ok(1); // TPU_VERSION_DRAGONFISH -> kDragonfish
case 3: return ok(2); // TPU_VERSION_PUFFERFISH -> kPufferfish
case 4: return ok(3); // TPU_VERSION_VIPERFISH -> kViperfish
case 5: return ok(4); // TPU_VERSION_GHOSTLITE -> kGhostlite
case 6: return ok(5); // TPU_VERSION_6acc60406 -> k6acc60406
default: // 0 (INVALID/unset) or > 6
return error("Invalid TPU version: " + proto,
".../tpu_version.cc", 421);
}
}每个有效分支都计算 internal = proto − 1。default 分支覆盖 protobuf 未设置情形(proto == 0,TPU_VERSION_INVALID)以及任何未来/垃圾值 > 6,返回一个非 OK 的 Status,消息为来自第 421 行的 "Invalid TPU version: <N>"。与 ToProto 的不对称正是重点:序列化是全函数且受信任,反序列化是检查过且可失败的。
tpu::TpuVersionFromProtoOrDie(0x20b3aa20)是给那些知道 proto 格式正确的调用点使用的便利包装(例如刚由 ToProto 生成的值,或从构建期已验证的 chip-parts blob 中读取的值)。它调用 FromProto,如果状态不是 OK,则在 tpu_version.cc:428 处触发 LogMessageFatal,消息为 "Could not read TPU version from protobuf."。bundle-encoder 分发器 tpu::ProgramProtoUtil::BundleCount(0x1e830e80)使用这个 OrDie 形式:它取出 proto 侧 version 字段(嵌入的 TpuCoreProgramAbiProto 的 version 成员),用 FromProtoOrDie 转换,然后对内部值 switch 以选择编码器族 — 内部 0,1 → CreateEncoderJfDf,2 → CreateEncoderPf,3 → CreateEncoderVf,4,5 → CreateEncoderGlGf。
AbslParseFlag / AbslUnparseFlag — 字符串往返
第三组转换对在内部枚举和其代号字符串之间为命令行 flag 建桥。tpu::AbslUnparseFlag(0x20b3ab40)通过索引 TpuVersionToString 使用的同一个 off_22011BF0 指针表和 qword_BDF3BD8 长度表,把内部 TpuVersion 转成其代号。tpu::AbslParseFlag(0x20b3aaa0)是它的逆:接收代号 string_view,将其解析回内部枚举值,并为无法识别的代号输出错误字符串。二者共同让 --tpu_version=ghostlite flag 往返到 kGhostlite=4 再返回。两者都纯粹工作在内部枚举空间和代号字符串空间 — 都不触及 proto 枚举。重新实现必须让 flag 路径停留在内部枚举上;通过 proto 枚举路由它会把差一引入用户可见的 flag 值。
完整 Wire-Value 表
下表完整对照两个枚举,以及它们共享的代号和外部显示名称。proto 枚举器名称是来自描述符的代号派生 TPU_VERSION_* 标识符;外部名称是 TpuVersionToExternalName 的输出。
内部 TpuVersion | 内部标签 | TpuVersionProto | Proto 枚举器 | 代号 | 外部名称 |
|---|---|---|---|---|---|
| — | (none) | 0 | TPU_VERSION_INVALID | (sentinel) | (error) |
| 0 | kJellyfish | 1 | TPU_VERSION_JELLYFISH | jellyfish | TPU v2 |
| 1 | kDragonfish | 2 | TPU_VERSION_DRAGONFISH | dragonfish | TPU v3 |
| 2 | kPufferfish | 3 | TPU_VERSION_PUFFERFISH | pufferfish | TPU v4 |
| 3 | kViperfish | 4 | TPU_VERSION_VIPERFISH | viperfish | TPU v5 |
| 4 | kGhostlite | 5 | TPU_VERSION_GHOSTLITE | ghostlite | TPU v6 lite |
| 5 | k6acc60406 | 6 | TPU_VERSION_6acc60406 | 6acc60406 | TPU7x |
关系是统一的:proto 值 = internal 值 + 1,而 proto 值 0 没有内部对应项。外部 "TPU vN" 名称是第三种编号,既不与任一枚举的整数对齐 — TPU v2 是 internal 0 / proto 1,TPU7x 是 internal 5 / proto 6 — 这正是必须区分三种编号的原因。
TpuVersionProto 描述符正好有七个条目,不多不少。它的序列化反射表 — 文件 tpu_version.proto、package tpu,作为连续 .rodata 块恢复 — 先列出 TPU_VERSION_INVALID,然后是六个代号枚举器,并在此停止;descriptor 模板实例化 TpuVersionProto_descriptorEv…Li0ELi6E 携带 0..6 作为编译期常量边界。TpuVersionProto 内部没有仅 proto 存在的代号、没有 _LITE 变体,也没有兼容性哨兵:它的每个值都有一个 FromProto 分支。二进制中还出现的 PUFFYLITE、VIPERLITE、DARWINN 和 CPU_ONLY token 属于其他枚举 — TpuTypeProto(描述符 TpuType/UNKNOWN_TPU_TYPE)和 xprof accelerator-type 枚举(UNKNOWN_DEVICE/NVIDIA_GPU/SEASTAR)— 不属于 TpuVersionProto。它们共享代号拼写,但位于不同描述符中,永远不会进入 TpuVersionFromProto。
注意:
TPU_V1_COMPAT_BRIDGE不是TpuVersionProto枚举器 — 它是不相关 MLIR-bridge 阶段字符串TFXLA_PHASE_ONE_MLIR_TPU_V1_COMPAT_BRIDGE的子串。*LITE/DARWINN/CPU_ONLYtoken 是TpuTypeProto和 xprof accelerator 枚举的枚举器,不是 version 值。TpuVersionProto反射块只包含TPU_VERSION_INVALID加六个TPU_VERSION_<CODENAME>值 — 共七个条目,没有更多。
为什么二者都存在
两个枚举编码的是两种不同的稳定性契约,合并它们会破坏其中一个。
TpuVersionProto 是一个 wire-stability 契约。一旦 proto 枚举值发布,它就永远冻结:序列化的 chip_parts blob、存储的 program ABI、来自旧二进制的 RPC,多年后都必须以相同方式解码。proto 枚举只能追加(未来一代获得值 7),不能重新编号。保留 0 和一基编号是 protobuf 惯用法,使未设置字段解码为可识别的 INVALID,而不是真实代际。
TpuVersion 是一个 dispatch-efficiency 契约。它是私有的进程内索引;没有任何东西序列化它。它是零基的,因此可以无偏移直接下标数组(off_22011BF0[version]、跳转表、每版本常量表)。因为它从不跨越进程边界,运行时可以自由保持其稠密和按时间顺序,而不用担心 wire 兼容性。
保持二者分离,让每个枚举都能按自己的规则演进:proto 枚举保证旧数据仍能解析,内部枚举保证快速索引。± 1 转换是两个世界之间很小的全函数/检查边界 — 而转换函数如此简单(+1 一条指令,-1 六分支 switch)正说明这种分离是刻意且廉价的,不是历史偶然。
注意:chip_parts "version 6" 陷阱。 嵌入资源
6acc60406_chip_parts.binarypb(及其兄弟6acc60406_tensornode_chip_parts.binarypb)以字节08 06开头:protobuf 字段 1(version)、wire-type 0(varint)、值6。这个6是TpuVersionProto值(TPU_VERSION_6acc60406),不是内部TpuVersion。运行时把它送入TpuVersionFromProto,映射 proto6 → internal 5。因此,写着 "version 6" 的 blob 描述的是运行时按 version5分发的硅片。重新实现如果读取 chip-parts 的version字段并直接把它用作分发索引,就会在每个嵌入 blob 上差一 — 而在最新一代上尤其会索引越过 6 槽表的末尾。始终先对序列化version字段执行proto − 1转换,再把它用作内部索引。
交叉引用
- TPU 版本代号矩阵 — 规范的枚举到代号指针表和完整五轴对照
- Part IV 概览 — proto 和内部枚举分别在流水线中出现的位置
- Chip Parts binarypb — 嵌入的
chip_partsblob,其version字段是 proto 枚举 - HAL Families — 消耗内部枚举的工厂路由
- ISA 概览 — 由内部枚举选择的 codec/encoder 族