Skip to content

模块初始化与插件发现

本页所有地址均适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(构建 libtpu_lts_20260413_b_RC00,build-id md5 89edbbe81c5b328a958fe628a9f2207d,781,691,048 字节,ELF x86-64 DYN,未 strip;反修饰后的 C++ 符号按原文引用)。其他版本会不同。

摘要

PJRT 插件是一个由框架 dlopen 并完全通过一个导出的 C 入口符号驱动的 .so。对 libtpu 来说,发现契约被有意收窄:框架(JAX、TensorFlow 或 PyTorch-XLA)通过自己的 PJRT 插件注册表找到 libtpu.so(Python 侧路径、PJRT_PLUGIN/PJRT_NAMES_AND_LIBRARY_PATHS 环境映射,或打包的入口点),对其执行 dlopen,用 dlsym 查找唯一符号 GetPjrtApi,然后无参数调用它。TPU 内部细节不会越过这条边界;框架只知道符号名和最前面几个 PJRT_Api 字段偏移。本页负责 overview.mdget-pjrt-api-thunk.md 只指向的三件事:发现握手(解析什么名字,以及框架可以假设什么)、动态链接器在 dlopen 时驱动的加载期初始化链(CPU 特性硬门禁、2900 项 .init_array 静态构造风暴,以及它不会运行什么),以及把刚加载的 .so 变成一个活动 TPU 驱动会话的一次性启动门禁

决定性的结构事实是:dlopen 时几乎什么都不会发生。动态链接器会运行一个 CPU 特性快速失败探测,然后运行约 2900 个 C++ 静态构造函数,但这些构造函数只做注册:它们填充 absl flag 表、protobuf 描述符、LLVM/MLIR 后端,以及 Google 风格的模块初始化依赖 DAG。对顺序敏感的 TPU bring-up(HAL 工厂、XLA target functor、StreamExecutor platform)会在这里被注册,但不会运行PJRT_Api 表本身也不会被构建,它是 .lbss 中一个零填充的 Meyers singleton。真正的工作由后续两个惰性、一次性事件完成:第一次 GetPjrtApi 调用在 17 个 __cxa_guard 组成的链下物化 140 槽表,而第一次 PJRT_Plugin_Initialize 调用(槽 8)获取跨进程 TPU 锁,并按拓扑顺序运行模块 DAG,执行链接器先前只是记录下来的注册。硅片检测会被进一步推迟到 PJRT_Client_Create

这种分离是经典的 Google base/init_google 模式(REGISTER_MODULE_INITIALIZER):加载时静态注册,首次初始化时有序执行。它存在的目的正是避免跨翻译单元静态初始化顺序(其唯一保证是链接顺序)决定对顺序敏感的 TPU 栈是否正确。

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

  • 发现握手:恰好一个导出的入口符号(GetPjrtApi,小写 jrt),@@VERS_1.0;拼写、大小写和零参数签名都至关重要。
  • 加载期初始化链:一个 PREINIT CPU 门禁会在任何构造函数运行前,对缺失 ISA 特性的情况执行 raise(SIGILL),随后是一个只注册的 .init_array 风暴,它构建 GoogleInitializer 模块 DAG,但不执行任何模块。
  • 一次性启动门禁PJRT_Plugin_Initialize(槽 8):struct_size 兼容性检查、kPjRtCApiTpuInitType 选择器、TryAcquireTpuLockGetLibTpuInitArgumentsInitializeDriverRealInitGoogleRunInitializers(DAG 运行),全部通过层叠的 once-guard 保持幂等。
导出入口符号GetPjrtApi @ 0xe6a83a0 — 5 字节 jmp thunk,GetPjrtApi@@VERS_1.0
真实引擎pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440 (1336 B)
表存储GetTpuPjrtApi()::pjrt_api @ 0x227BA840.lbss (NOBITS),1120 B = 140 × 8
PREINIT_ARRAY@ 0x22048b30(16 B,2 项):CPU gate + dl-debug hook
INIT_ARRAY@ 0x215f26f0(23200 B = 2900 项,全部为 R_X86_64_RELATIVE
FINI_ARRAY@ 0x215f8190(16 B,2 项)
启动门禁pjrt::tpu_plugin::PJRT_Plugin_Initialize @ 0xe6a9d00 (303 B),PJRT 槽 8
init-type 选择器kPjRtCApiTpuInitType(静态 = 2@ 0x22255b40.data
DAG 运行驱动器GoogleInitializer::RunInitializers @ 0x210b2d20(PHASE B,首次 init 时)
可信度CONFIRMED(字节锚定 vs 反编译),除非某行或标注另有说明

1. 插件发现握手

目的

PJRT 存在的全部理由,是提供一个 ABI 稳定的会合点。框架不了解 libtpu 的内部;它只知道入口符号名和最前面几个 PJRT_Api 字段的布局,其余内容都在运行时通过 struct_size 和 extension chain 发现。本节固定这个会合点,让重新实现者可以交付一个 stock JAX/PyTorch-XLA 构建会加载的 .so。深层的 GetTpuPjrtApi 函数体和 tpu_plugin 对象归 get-pjrt-api-thunk.md 负责;PJRT_Api 结构形状归 ../pjrt/overview.md../pjrt/api-vtable-reconstruction.md 负责。本页只负责握手本身。

框架如何找到 libtpu

插件路径是在 C 边界之前、由框架侧建立的。没有任何 libtpu 代码会向 OS “注册”该插件;发现是框架的职责:

text
framework PJRT plugin registry (Python side)
  ├─ a packaged entry point / pip-installed plugin descriptor names libtpu, OR
  ├─ env PJRT_NAMES_AND_LIBRARY_PATHS / a "tpu" name → /path/to/libtpu.so, OR
  └─ a default search of the installed wheel's libtpu/libtpu.so

       dlopen("libtpu.so")                 ── dynamic linker runs the load-time init chain (§2)
       dlsym(handle, "GetPjrtApi")         ── the ONLY name that resolves
         │                                    "GetTpuPjrtApi" is an INTERNAL helper, not exported
         └─ GetPjrtApi  0xe6a83a0          ── 5-byte: jmp 0xe6aa440
              └─ pjrt::tpu_plugin::GetTpuPjrtApi  0xe6aa440   (lazy 140-slot build — §1.2)
```text

> **GOTCHA —** 拼写和大小写至关重要。导出的符号是 `GetPjrtApi`(小写 `jrt`),匹配公开 PJRT 插件约定,版本化为 `GetPjrtApi@@VERS_1.0`。它是唯一匹配 `/Pjrt/` 的 `GLOBAL FUNC` 导出。`GetTpuPjrtApi` 是内部 helper,**不会**导出。若 loader 对 `GetTpuPjrtApi` 执行 `dlsym`,或某个构建只导出带 `Tpu` 前缀的名字,发现会静默失败。共享这个二进制的 194 个 `Tpu*_*` 导出属于*旧式* StreamExecutor C-ABI(`TpuExecutor_*` ×25、`TpuTransferManager_*` ×19、`TpuProgram_*` ×18、`TpuTopology_*` ×17、`TpuPlatform_*` ×11,…),全部为 `@@VERS_1.0`,由 `tensorflow/core/tpu/` 直接链接,而不会通过 PJRT 到达。

### 入口契约

```c
// The one symbol the framework dlsym's. No arguments, returns the table.
const PJRT_Api* GetPjrtApi(void);             // exported, GetPjrtApi@@VERS_1.0

GetPjrtApi @ 0xe6a83a0 是一个纯 tail-call thunk;反编译中确认为单行 return pjrt::tpu_plugin::GetTpuPjrtApi(a1)a1 寄存器是死的;规范签名不接收参数)。该 thunk 的存在是为了给公开名称提供外部链接,而实际引擎留在匿名的 pjrt::tpu_plugin 命名空间中。

调用方随后读取 api->struct_size 以了解该插件提供多少槽,读取 api->pjrt_api_version(此构建中 minor 为 103),遍历 api->extension_start 以发现可选能力,然后才调用 api->PJRT_Plugin_Initialize(槽 8)和 api->PJRT_Client_Create(槽 15)。这两个槽分别进入启动门禁(§3)和硅片检测。

为什么表在首次调用时构建,而不是加载时

GetTpuPjrtApipjrt_api.lbss 中的函数局部 static(NOBITS,@ 0x227BA840),加载时零填充。第一次 GetPjrtApi 时,引擎运行 17 个受 __cxa_guard 保护的一次性块:其中 16 个构建 .bss extension chain(每个以之前的节点作为 .next,从 .data 静态 profiler 播种),第 17 个调用 pjrt::CreatePjrtApi 写入全部 140 个槽。反编译逐字确认了这条 16-builder 梯子:

c
function GetTpuPjrtApi():                                  // 0xe6aa440
    // 16 one-shot extension builders, construction order (each takes the prior as .next):
    once: CreateRawBufferExtension(&raw_buffer_ext, &profiler_extension)   // seed = .data profiler
    once: CreateLayoutsExtension(&layouts_ext, &raw_buffer_ext)
    once: CreateMemoryDescriptionsExtension(&mem_desc_ext, &layouts_ext)
    once: CreateExecutableMetadataExtension(&exec_meta_ext, &mem_desc_ext, GetTpuExecutableMetadata)
    once: CreateHostAllocatorExtension(&host_alloc_ext, &exec_meta_ext, GetPreferredAlignment, Allocate, Free)
    once: CreateCrossHostTransfersExtension(...)
    once: CreatePhaseCompileExtension(..., GetTpuPhaseCompiler, DestroyTpuPhaseCompiler)
    once: CreateCallbackExtension(...)
    once: CreateTpuTopologyExtension(...)
    once: CreateTpuExecutableExtension(...)
    once: CreateMegascaleExtension(...)
    once: CreateShardingsExtension(...)
    once: CreateTpuAbiVersionExtension(...)
    once: CreateCollectivesExtension(...)
    once: CreateMultiSliceExtension(...)
    once: CreateHostMemoryAllocatorExtension(&hma_ext, &multi_slice_ext)   // last-built = chain head
    // 17th guard: write all 140 slots, chain head = host_memory_allocator_extension
    once: CreatePjrtApi(&pjrt_api,
                        PJRT_Client_Create, PJRT_ExecuteContext_Create,
                        PJRT_TopologyDescription_Create, PJRT_Plugin_Initialize,
                        &host_memory_allocator_extension /*chain head*/,
                        PJRT_Plugin_Attributes_Xla)
    return &pjrt_api                                        // 0x227BA840
```text

> **NOTE —** 因为表在首次调用时物化,静态反汇编看不到已填充的槽值;`.lbss` 镜像在运行前全为零。140 槽到实现的映射是从 `CreatePjrtApi` 的函数体重建的,而不是从二进制的数据段重建的。一次性构建完成后,该结构在进程生命周期内不可变;读取方不加锁,并发的首次调用方通过 Itanium-ABI `__cxa_guard_acquire/release` 串行化。chain 构建细节(16+1 个 builder、newest-first chain、五个 TPU 注入槽)完全归 [get-pjrt-api-thunk.md](get-pjrt-api-thunk.md) 和 [../pjrt/extension-chain.md](../pjrt/extension-chain.md) 负责;这里展示它只是为了把“首次调用,而非加载时”的边界具体化。

---

## 2. 加载期初始化链(`dlopen` 驱动什么)

### 目的

本节中的所有内容都由动态链接器驱动,同步运行在 `dlopen` 调用内部,发生在 `GetPjrtApi` 被调用之前。重新实现者的心智模型必须是:`dlopen` 运行一个硬 CPU 门禁和一个只注册的构造函数风暴,然后就*停止*。不会触碰 TPU 硬件,不存在 `PJRT_Api` 表,也没有活动的驱动会话。ELF entry/`init_proc` 和 `__do_init/__do_fini` 机制归 `elf-entry-and-init-proc` 与 `do-init/do-fini` 页面负责;本节覆盖该链中与*插件发现相关*的内容:CPU 门禁,以及构造函数风暴注册了什么、没有运行什么。

### 链接器驱动的四个阶段

```text
dlopen("libtpu.so")
  1. Relocation        ── ALL of INIT_ARRAY (2900 slots), PREINIT_ARRAY (2),
                          FINI_ARRAY (2) are R_X86_64_RELATIVE — in-file slots are
                          zero, the linker fills every target VA at load.
  2. DT_INIT (.init @ 0xe635524)
                       ── vestigial glibc __gmon_start__ check-and-call stub.
                          ALL real init runs through .init_array, not here.
  3. PREINIT_ARRAY @ 0x22048b30   (runs BEFORE any C++ constructor)
       [0] (anon)::cpu_feature_fail_fast  0x2110abc0   ── CPU ISA hard gate2.1)
       [1] setup_dl_debug_hook            0x2114eec0   ── dl debug rendezvous
  4. INIT_ARRAY @ 0x215f26f0      (2900 entries, in array order)
       __cpu_indicator_init → Rust std::sys args ARGV init → 2898 C++ static ctors
                                                            (register-only — §2.2)

2.1 CPU 特性硬门禁(PREINIT_ARRAY[0])

(anonymous namespace)::cpu_feature_fail_fast @ 0x2110abc0 最先运行,早于任何构造函数,并隔离整个静态初始化风暴。它调用 __cpu_indicator_init(GCC ifunc 支持例程)来填充全局特性掩码(dword_22598A0C),然后逐项检查该二进制编译时要求的 ISA 特性。若缺少某项特性,它会向 stderr 执行 write(2, …) 输出 "FATAL ERROR: This binary was compiled with <feat> enabled, but this feature is not available on this processor (go/sigill-fail-fast)." 消息,并执行 raise(SIGILL)raise(4)):这是无法恢复的硬中止。

该门禁通过 __cpu_indicator_init 掩码检查十一项特性,按固定 fall-through 顺序进行,另有一个独立的 cpuid leaf-1 ECX 探测用于 CMPXCHG16B。完整集合(mask bit → feature)如下。

顺序掩码测试(dword_22598A0C &特性
10x40000AES
20x200AVX
30x2MMX
40x80000PCLMUL
50x4POPCNT
60x8SSE
70x10SSE2
80x20SSE3
90x80SSE4.1
100x100SSE4.2
110x40SSSE3
12cpuid(1).ecx & 0x2000CMPXCHG16B
c
function cpu_feature_fail_fast():                 // 0x2110abc0
    __cpu_indicator_init()                         // fills dword_22598A0C
    mask = dword_22598A0C
    // fall-through chain: each missing feature writes a FATAL string + raise(SIGILL),
    // then continues testing the next (so all missing features are reported).
    if !(mask & 0x40000): fatal("aes");      ...   // AES
    if !(mask & 0x200):   fatal("avx");      ...   // AVX
    ... MMX, PCLMUL, POPCNT, SSE, SSE2, SSE3, SSE4.1, SSE4.2, SSSE3 ...
    // CMPXCHG16B is a direct cpuid probe, not the indicator mask:
    eax = 1; cpuid
    if !(ecx & 0x2000): fatal("cmpxchg16b"); raise(SIGILL)
    return
```text

> **GOTCHA —** 这个门禁在 `dlopen` 时运行,而不是首次使用时运行。若宿主 CPU 缺少这十一项基线特性中的任意一项,框架一 `dlopen` `libtpu.so` 就会 SIGILL,远早于 `GetPjrtApi`,并且只有 stderr 消息,没有 PJRT 级错误返回。把它移植到非常规宿主的重新实现者必须满足完整的 SSE/SSSE3/AES/AVX/PCLMUL/POPCNT/CMPXCHG16B 基线;不存在优雅降级路径。

### 2.2 构造函数风暴只注册,不运行 TPU bring-up

`INIT_ARRAY @ 0x215f26f0` 为 23200 字节 = 2900 项,每一项都是 `R_X86_64_RELATIVE` reloc(文件内槽为零,由链接器填充)。前几项是 `__cpu_indicator_init`,然后是 Rust runtime 的 `std::sys::args::unix::imp::ARGV_INIT_ARRAY`(libtpu 静态链接了一个 Rust 组件),再后面是剩余 2898 个 C++ 静态构造函数。按符号类别统计(全部 2900 个槽上字节精确的计数):

| 构造函数类型 | 数量 | 作用 |
|---|---|---|
| `_GLOBAL__sub_I_<file>.cc/.cpp` | 1885 | 每个翻译单元的静态初始化 |
| `_GLOBAL__I_NNNNNN` | 759 | 分组的 C++ ctors |
| `__cxx_global_var_init[.N]` | 221 | 单个全局变量初始化 |
| 匿名 / 无符号 ctors + `__do_init` + `upb_GeneratedRegistry_Constructor` | 33 | 其余 C++ ctors |
| `__cpu_indicator_init` | 1 | GCC ifunc 支持(第一个槽) |
| Rust `ARGV_INIT_ARRAY` | 1 | Rust std args bootstrap |
| **总计** | **2900** | 匹配 `INIT_ARRAYSZ`/8 |

这些构造函数会注册但不会执行对顺序敏感的 TPU 栈。它们填充:absl 命令行 flag 表(`_GLOBAL__sub_I_*_flags.cc`、`commandlineflags.cc`)和 absl logging;protobuf 描述符(7 个 `*proto/descriptor` TU + upb `linkarr_upb_AllExts` mini-table 数组 `@ 0x224c2480..0x224c2920`);LLVM target 后端(X86/AArch64/AMDGPU/ARM `*TargetMachine.cpp`、`AsmPrinter.cpp`);MLIR dialect/pass 注册(mhlo/stablehlo/`mlir_bridge_pass`);以及发现流程的关键部分:`GoogleInitializer` **模块描述符**及其依赖边。

```c
// Each _GLOBAL__sub_I_<module>_registration.cc ctor, at dlopen, does ONLY:
function _GLOBAL__sub_I_tpu_platform_registration():       // 0x2121f040
    // bind module NAME → google_init_module_tpu_platform fn-ptr, insert into registry
    GoogleInitializer(LiteralTag, "tpu_platform", file, &google_init_module_tpu_platform)  // 0x210b2780
    // ... and register FLAGS_tf_jf_* absl flags ...
    // the module FUNCTION is NOT called here.

// e.g. the HAL modules also register a dependency edge:
function _GLOBAL__sub_I_tpu_hal_jxc_hardware_impl_registration():   // 0x2121bcb0
    GoogleInitializer(LiteralTag, "tpu_hal_jxc_hardware_impl", file, &google_init_module_...)
    GoogleInitializer::DependencyRegisterer(..., Dependency)        // 0x210b29e0
        // edge: tpu_hal_jxc_hardware_impl → depends on tpu_hal

NOTE — C++ 静态初始化在 INIT_ARRAY 内的顺序是链接顺序,这是唯一保证。libtpu 有意依赖它保证正确性:每个对顺序敏感的注册(HAL 工厂、XLA target functor、StreamExecutor platform)都会在 dlopen 时被记录进 GoogleInitializer 依赖 DAG,并在后续首次 init(§3)时按拓扑顺序运行。因此重新实现者可以按任意链接顺序注册模块 ctors;决定执行顺序的是 DAG,而不是链接器。


3. 一次性启动门禁

目的

PJRT_Plugin_Initialize(槽 8)是把已加载但惰性的 .so 变成活动 TPU 驱动会话的唯一入口。它连接了 dlopen 留下的“所有东西已注册、什么都没运行”状态与运行中的模块 DAG。它是幂等的,受运行时选择器控制,并且是唯一获取跨进程 TPU 锁的位置。更深的 option ingest 和 TfTpu_Initialize 系列细节归 tftpu-initialize-bootstrap.md 负责;本节负责该门禁的控制流及其 once-guard 纪律。

算法

pjrt::tpu_plugin::PJRT_Plugin_Initialize @ 0xe6a9d00(303 B),由框架在 GetPjrtApi 后调用:

c
function PJRT_Plugin_Initialize(args):              // 0xe6a9d00, PJRT slot 8
    // (a) backward-compat size gate — accepts args struct from min=27 down to cur=16
    ok = ActualStructSizeIsGreaterOrEqual("PJRT_Plugin_Initialize_Args", 27, 16, args->struct_size)
    if ok != 1:
        return new uint8_t[8]{ ok }                 // error wrapper (heap-boxed status)

    // (b) runtime init-type selector (statically kPjRtCApiTpuInitType == 2)
    if kPjRtCApiTpuInitType != 0:                   // @ 0x22255b40 (.data)
        if TryAcquireTpuLock("PJRT_Plugin_Initialize_Args") == 1:   // 0x20ccbc40
            GetLibTpuInitArguments(&argv)            // 0x20ccca20 — reads LIBTPU_INIT_ARGS env
            InitializeDriver(flag, argc, argv,
                             init_type_is_2 = (kPjRtCApiTpuInitType == 2))   // 0x204cecc0
            free(argv …)                             // release the temporary arg vectors
            return NULL                              // success
        else:
            return new uint8_t[8]{ status }          // lock-fail / size-mismatch error wrapper

    // (c) init-type 0 → no-op
    return NULL
```text

反编译确认了每条分支:头部 `ActualStructSizeIsGreaterOrEqual(…, 27, 16, *a1)`;`if (kPjRtCApiTpuInitType)` 选择器;`TryAcquireTpuLock` → 当 `== 1` 时调用 `GetLibTpuInitArguments`,再调用 `InitializeDriver(…, kPjRtCApiTpuInitType == 2, …)`,随后 argv-free 循环并 `return 0`;失败路径上的 `operator new(8u)` error-wrapper;以及 init-type-0 短路中的 `return 0`。

### 锁、参数与 DAG 运行

```text
PJRT_Plugin_Initialize (slot 8)  0xe6a9d00
  ├─ TryAcquireTpuLock            0x20ccbc40  ── cross-process TPU acquisition:
  │     function-static absl::Mutex (guard 0x225925d0 / obj 0x225925c8),
  │     env TPU_LOAD_LIBRARY (str @ file 0x887356a), scans /dev for TPU device
nodes (opendir/readdir/stat). Returns 1 iff the lock is taken.
  ├─ GetLibTpuInitArguments       0x20ccca20  ── reads env LIBTPU_INIT_ARGS
  │     (str @ file 0x918c880), splits into an argv-style vector.
  └─ InitializeDriver             0x204cecc0  ── driver bring-up:
        ├─ AppendNewCloudTPUArgs                ── fold Cloud-TPU default flags into argv
        ├─ InitGoogleExceptChangeRootAndUser    0x210b0180  ── tail-jmp →
        │     RealInitGoogle                    0x210ae860
        │       ├─ absl::ParseCommandLine
        │       └─ GoogleInitializer::RunInitializers  0x210b2d20   *** PHASE B ***
        │             └─ run all registered modules in topological dep order:
        │                 google_init_module_tpu_hal_{jxc,pxc,vxc,glc,gfc}_*
        │                   → TpuHalFactory::Register(PlatformType, TpuVersion, factory)  0x1fbb16a0
        │                 google_init_module_xla_target_{jellyfish,…,ghostlite}
        │                   → RegisterTargetCreationFunctor(N, …)
        │                 google_init_module_tpu_platform  0x213eabc0 (jmp)
        │                   → RegisterTpuPlatform → PlatformManager::RegisterPlatform
        │                 … all other registered modules …
        └─ telemetry: RegisterLibtpuGaugeTelemetry, RegisterMegascaleErrorHandler,
                      EnableRuntimeUptimeTelemetry, InitializeUptimeMetricViaEnvironmentVariables

这里才是 §2.2 的注册真正运行的位置(PHASE B)。RunInitializers 通过 TypeData::RunIfNecessary @ 0x210b3320 / RunOne @ 0x210b3000 驱动 DAG,由 absl::Mutex 和每个模块的运行状态门控,State::CanRun @ 0x210b3c80 决定就绪性,kInitGoogleDone @ 0x22040038 / CheckInitGoogleIsDone @ 0x210adec0 标记完成。

QUIRK — HAL 工厂按 TpuVersion 注册,但不会扫描硅片。google_init_module_tpu_hal_jxc_hardware_impl @ 0x213e9d80 两次调用 TpuHalFactory::Register:TpuVersion 0(kJellyfish)和 1(kDragonfish),二者都使用 TpuHalJxcHardwareFactory;pxc 注册 v2(kPufferfish),vxc 注册 v3(kViperfish),glc 注册 v4(kGhostlite),gfc 注册 v5。初始化时,registry 只是按 (PlatformType, TpuVersion)填充。实际硅片扫描,即通过 *HardwareScanner::Create HAL 组件把 PCI device ID 匹配到 TpuVersion,会在之后PJRT_Client_Create(槽 15)内部运行。因此 TpuVersion 检测比 dlopen 晚两个阶段:首次 init 时注册,首次 client 时检测。

StreamExecutor platform 注册

DAG 中的一个模块是 StreamExecutor TpuPlatformgoogle_init_module_tpu_platform @ 0x213eabc0 是一个 thunk,跳到 tensorflow::tpu::RegisterTpuPlatform @ 0xe99a3a0

c
function RegisterTpuPlatform():                     // 0xe99a3a0
    fn = stream_executor::tpu::ExecutorApiFn()       // 0x20819360
    if IsStreamExecutorEnabled(fn)                   // 0x20819380
       and !tpu_platform_registered:                 // byte guard @ 0x224c5388 (one-shot)
        p = new tensorflow::tpu::TpuPlatform()        // 0xe999960, sizeof = 0x98 (152 B)
        tpu_registered_platform = p
        st = PlatformManager::RegisterPlatform(p)     // 0x1d0fe120
        if st != OK:
            LOG(FATAL) at tpu_platform.cc:178         // CHECK-fail
        tpu_platform_registered = 1
    return 1
```text

这会在 PJRT `PjRtClient` 之下安装旧式 StreamExecutor `TpuPlatform`。§1 中的 194 个 `Tpu*_*` C-ABI 导出(包括 `TpuPlatform_*`)包装的是同一对象家族;它是现代 PJRT 栈所处的设备层。

### Once-guard 纪律

三种不同的 once-guard 机制让整条链保持幂等;再次调用 `GetPjrtApi` 或 `PJRT_Plugin_Initialize` 会快速 no-op:

| 机制 | 位置 | 地址 |
|---|---|---|
| C++ `__cxa_guard`(libtpu 自己的 libc++abi,不是 glibc 的) | `GetTpuPjrtApi` 16 个 ext + `pjrt_api`(×17| acquire/release/abort `0x213e9ac0`/`0x213e9be0`/`0x213e9c20`;例如 raw_buffer guard `0x224c39e0` |
| `absl::Mutex` once-lock | `TryAcquireTpuLock::mu` | guard `0x225925d0` / obj `0x225925c8` |
| `absl::Mutex` registry lock | `GoogleInitializer::RunInitializers` | 位于 `0x210b2d20` 内 |
| 函数静态 byte guard | `RegisterTpuPlatform::tpu_platform_registered` | `0x224c5388` |
| 函数静态 byte guard | `__do_init` / `__do_fini` | `0xe63c000` / `0xe63c020` |
| 环境变量门禁 | `TPU_LOAD_LIBRARY`(在 `TryAcquireTpuLock` 中) | str `@ 0x887356a` |
| 环境变量参数 | `LIBTPU_INIT_ARGS`(在 `GetLibTpuInitArguments` 中) | str `@ 0x918c880` |
| init-type 选择器 | `kPjRtCApiTpuInitType` (= 2) | `0x22255b40`(`.data`) |

> **NOTE —** `kPjRtCApiTpuInitType` 在 `.data` 中静态为 `2`。Type 2 走完整 TPU bring-up 路径(`InitializeDriver(…, init_type_is_2 = true, …)`);type 0 会让 `PJRT_Plugin_Initialize` no-op。是否存在某条路径会把选择器改写为 `0`/`1`(例如选择旧式 TF init-type)尚未追踪(改写存在性的可信度为 LOW;静态值 `2` 为 CONFIRMED)。

---

## 4. 拆卸

### 目的

为了对称性,这里说明卸载路径。不存在大型的 PJRT surface `atexit` 拆卸;`PJRT_Api` 表和 17 个 extension 都是退出时泄漏的函数局部 static(`.lbss`/`.data`/`.bss`),这是插件 `.so` 常见的 Meyers-singleton 生命周期。Client、executable 和 profiler handle 通过显式的 `PJRT_*_Destroy` C-API 调用拆除,而不是在进程退出时拆除。

### 卸载时运行什么

```text
dlclose / process exit
  ├─ DT_FINI (.fini @ 0xe63553c)  ── empty (sub/add/ret)
  └─ FINI_ARRAY @ 0x215f8190 (2 entries, R_X86_64_RELATIVE)
       [0] __do_fini                    0xe63c020  ── trivial guarded dtor stub
       [1] rand_thread_state_clear_all  0x2063df60  ── clears per-thread BoringSSL/RNG state

libtpu 还提供 atexit / __cxa_thread_atexit shim(@ 0x21217360 / 0x2120f1e0)以及用于刷新日志的 threadlogger::FlushLogsAtExit@ 0x20f3dfe0),但没有任何 PJRT 对象析构被接入其中。


相关组件

组件关系
GetPjrtApi @ 0xe6a83a0单一导出入口符号;发现会合点
pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440惰性 140 槽构建引擎(归 get-pjrt-api-thunk.md 负责)
pjrt::CreatePjrtApi @ 0xf874160在第 17 个 guard 上把全部 140 个槽写入 .lbss
cpu_feature_fail_fast @ 0x2110abc0PREINIT CPU ISA 硬门禁(缺少基线时 SIGILL)
GoogleInitializer registry / RunInitializers @ 0x210b2d20加载时注册 / 首次 init 时运行的模块 DAG
PJRT_Plugin_Initialize @ 0xe6a9d00一次性启动门禁(PJRT 槽 8)
TryAcquireTpuLock @ 0x20ccbc40跨进程 TPU 获取锁
RegisterTpuPlatform @ 0xe99a3a0在 PJRT 之下安装 StreamExecutor TpuPlatform
Tpu*_* C-ABI(194 个导出)共享同一二进制的旧式 StreamExecutor surface,不会通过 PJRT 到达

交叉引用