Skip to content

双枚举 — Proto vs Internal

地址适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so。其他版本会不同。

摘要

libtpu.so 内部,TPU 代际由两个不同的 C++ 枚举表示,并且它们刻意相差一。内部运行时枚举 tpu::TpuVersion 是 0-based 且按时间顺序排列:从 kJellyfish=0k6acc60406=5。线上枚举 tpu::TpuVersionProto 是 1-based,保留 0 作为未指定/无效哨兵:从 TPU_VERSION_JELLYFISH=1TPU_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",看到的是同一块芯片。

本页记录两个转换函数(TpuVersionFromProtoTpuVersionToProto)、通过代号字符串往返的 flag parse/unparse 对、完整 wire-value 表,以及这个差一现象作为现实陷阱出现的一个位置:嵌入的 chip_parts blob,其 version 字段对运行时称为 version 5 的硅片读出 6

对重新实现而言,契约是:

  • 两个枚举及其取值空间:内部 TpuVersion[0,5],proto TpuVersionProto[1,6],且 0 = TPU_VERSION_INVALID
  • 双向转换:ToProto(v) = v + 1(全函数),FromProto(p) = p − 1p ∈ [1,6] 成立,否则报错。
  • 哪些 API 表面使用哪种枚举,使序列化数据和运行时分发对同一块硅片达成一致。
内部枚举tpu::TpuVersion — 0-based,kJellyfish=0k6acc60406=5
wire 枚举tpu::TpuVersionProto — 1-based,TPU_VERSION_INVALID=0TPU_VERSION_JELLYFISH=1TPU_VERSION_6acc60406=6
proto → internaltpu::TpuVersionFromProto @ 0x20b3a8c0switchinternal = proto − 1StatusOr
internal → prototpu::TpuVersionToProto @ 0x20b3a8a0return version + 1;(1 条指令,全函数)
proto → internal(fatal)tpu::TpuVersionFromProtoOrDie @ 0x20b3aa20 — 包装 FromProto,在 tpu_version.cc:428 处终止
flag parsetpu::AbslParseFlag @ 0x20b3aaa0 — 代号字符串 → 内部枚举
flag unparsetpu::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 − 1proto = 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_JELLYFISHTPU_VERSION_DRAGONFISHTPU_VERSION_PUFFERFISHTPU_VERSION_VIPERFISHTPU_VERSION_GHOSTLITETPU_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::TpuVersionToProto0x20b3a8a0)是一条算术指令:

c
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 − 1default 分支覆盖 protobuf 未设置情形(proto == 0TPU_VERSION_INVALID)以及任何未来/垃圾值 > 6,返回一个非 OK 的 Status,消息为来自第 421 行的 "Invalid TPU version: <N>"。与 ToProto 的不对称正是重点:序列化是全函数且受信任,反序列化是检查过且可失败的。

tpu::TpuVersionFromProtoOrDie0x20b3aa20)是给那些知道 proto 格式正确的调用点使用的便利包装(例如刚由 ToProto 生成的值,或从构建期已验证的 chip-parts blob 中读取的值)。它调用 FromProto,如果状态不是 OK,则在 tpu_version.cc:428 处触发 LogMessageFatal,消息为 "Could not read TPU version from protobuf."。bundle-encoder 分发器 tpu::ProgramProtoUtil::BundleCount0x1e830e80)使用这个 OrDie 形式:它取出 proto 侧 version 字段(嵌入的 TpuCoreProgramAbiProtoversion 成员),用 FromProtoOrDie 转换,然后对内部值 switch 以选择编码器族 — 内部 0,1 → CreateEncoderJfDf2 → CreateEncoderPf3 → CreateEncoderVf4,5 → CreateEncoderGlGf

AbslParseFlag / AbslUnparseFlag — 字符串往返

第三组转换对在内部枚举和其代号字符串之间为命令行 flag 建桥。tpu::AbslUnparseFlag0x20b3ab40)通过索引 TpuVersionToString 使用的同一个 off_22011BF0 指针表和 qword_BDF3BD8 长度表,把内部 TpuVersion 转成其代号。tpu::AbslParseFlag0x20b3aaa0)是它的逆:接收代号 string_view,将其解析回内部枚举值,并为无法识别的代号输出错误字符串。二者共同让 --tpu_version=ghostlite flag 往返到 kGhostlite=4 再返回。两者都纯粹工作在内部枚举空间和代号字符串空间 — 都不触及 proto 枚举。重新实现必须让 flag 路径停留在内部枚举上;通过 proto 枚举路由它会把差一引入用户可见的 flag 值。


完整 Wire-Value 表

下表完整对照两个枚举,以及它们共享的代号和外部显示名称。proto 枚举器名称是来自描述符的代号派生 TPU_VERSION_* 标识符;外部名称是 TpuVersionToExternalName 的输出。

内部 TpuVersion内部标签TpuVersionProtoProto 枚举器代号外部名称
(none)0TPU_VERSION_INVALID(sentinel)(error)
0kJellyfish1TPU_VERSION_JELLYFISHjellyfishTPU v2
1kDragonfish2TPU_VERSION_DRAGONFISHdragonfishTPU v3
2kPufferfish3TPU_VERSION_PUFFERFISHpufferfishTPU v4
3kViperfish4TPU_VERSION_VIPERFISHviperfishTPU v5
4kGhostlite5TPU_VERSION_GHOSTLITEghostliteTPU v6 lite
5k6acc604066TPU_VERSION_6acc604066acc60406TPU7x

关系是统一的: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 分支。二进制中还出现的 PUFFYLITEVIPERLITEDARWINNCPU_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_ONLY token 是 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。这个 6TpuVersionProto 值(TPU_VERSION_6acc60406),不是内部 TpuVersion。运行时把它送入 TpuVersionFromProto,映射 proto 6 → internal 5。因此,写着 "version 6" 的 blob 描述的是运行时按 version 5 分发的硅片。重新实现如果读取 chip-parts 的 version 字段并直接把它用作分发索引,就会在每个嵌入 blob 上差一 — 而在最新一代上尤其会索引越过 6 槽表的末尾。始终先对序列化 version 字段执行 proto − 1 转换,再把它用作内部索引。


交叉引用