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++ 符号按原文引用)。其他版本会不同。

摘要

这是 libtpu 从加载到卸载生命周期的地图。它追踪从框架执行 dlopen("libtpu.so") 到活动 TPU 驱动会话,再到卸载的单一时间线,列出负责每个阶段的函数,并指向完整记录该阶段的相邻页面。它不会复述任何阶段的内部细节;它存在的目的,是让重新实现者在一屏内看到完整弧线,理解哪个事件触发哪个阶段以及每次转移留下什么状态,然后跳转到负责细节的页面。

这条弧线有一个决定性形状:dlopen 几乎什么都不做,真正的工作是惰性且一次性的。 动态链接器运行一个硬 CPU 特性门禁和约 2900 项静态构造函数风暴,但这些构造函数只做注册:它们填充 flag 表、protobuf 描述符、LLVM/MLIR 后端,以及一个 Google 风格的模块初始化依赖 DAG,而不执行任何对顺序敏感的 TPU bring-up。此时还没有 PJRT_Api 表;它只是 .lbss 中一个零填充的 Meyers singleton。后续两个惰性、幂等事件完成真正工作:第一次 GetPjrtApi 调用在 17 个 __cxa_guard 组成的链下物化 140 槽表,而第一次 PJRT_Plugin_Initialize 调用(PJRT 槽 8)获取跨进程 TPU 锁,并按拓扑顺序运行模块 DAG,执行链接器先前只是记录下来的注册。硅片检测被进一步推迟到 PJRT_Client_Create(槽 15)。拆卸也相应很薄:PJRT surface 在退出时泄漏,FINI_ARRAY 只拆除一个简单 stub 和每线程 RNG 状态。

熟悉的参照系是任何以共享对象形式加载的 C-ABI 插件:一个版本化入口符号、一个通过 struct_size 字段发现的 ABI 稳定函数指针表,以及 Meyers-singleton 生命周期。不寻常之处在于这种分阶段安排,即通过 GoogleInitializer DAG(经典 Google REGISTER_MODULE_INITIALIZER 模式)刻意分离加载时注册首次 init 时运行。这种分阶段存在的原因,是避免跨翻译单元静态初始化顺序(其唯一保证是链接顺序)决定对顺序敏感的 TPU 栈是否正确。

对重新实现而言,生命周期契约是:

  • 触发顺序。 六个转移按固定顺序发生:链接器初始化链(在 dlopen 时)→ 第一次 GetPjrtApi(构建表)→ 第一次 PJRT_Plugin_Initialize(driver + DAG run)→ 第一次 PJRT_Client_Create(硅片扫描)→ 稳态 → FINI_ARRAY(卸载)。每个阶段都恰好由一个函数负责。
  • 每个转移留下的状态。 dlopen 后:已注册、什么都没运行、没有表。第一次 GetPjrtApi 后:表已构建,硬件未触碰。第一次 PJRT_Plugin_Initialize 后:driver 活动,模块已运行,硅片尚未扫描。第一次 PJRT_Client_Create 后:在检测到的硅片上有活动 client。
  • 幂等纪律。 链接器链之后的每个转移都是一次性操作,由 __cxa_guardabsl::Mutex 或函数静态 byte guard 保护;重入是快速 no-op。
导出入口符号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
DAG 运行驱动器GoogleInitializer::RunInitializers @ 0x210b2d20(PHASE B,首次 init 时)
硅片扫描pjrt::tpu_plugin::PJRT_Client_Create @ 0xe6a8840,PJRT 槽 15
可信度CONFIRMED(字节锚定 vs 反编译),除非某行或标注另有说明

生命周期时间线

完整弧线由六个转移组成。其中三个同步发生在 dlopen 内部(由链接器驱动);另外三个是框架对 PJRT_Api 表的调用,每个都是惰性的一次性操作。下图是规范加载顺序;每个编号带都由一个相邻页面负责(带内写出页面名)。

text
═══════════════════════════════════════════════════════════════════════════════
  STAGE 0 — dlopen("libtpu.so")          [dynamic linker; synchronous]
═══════════════════════════════════════════════════════════════════════════════
  (0a) Relocation
        INIT_ARRAY (2900) + PREINIT_ARRAY (2) + FINI_ARRAY (2) are all
        R_X86_64_RELATIVE — in-file slots zero, linker fills every target VA.
  (0b) DT_INIT (.init @ 0xe635524)
        vestigial glibc __gmon_start__ check-and-call stub.    ── elf-entry-and-init-proc.md
  (0c) PREINIT_ARRAY @ 0x22048b30        ── runs BEFORE any C++ constructor
        [0] cpu_feature_fail_fast  0x2110abc0  ── 11-feature CPU ISA gate; SIGILL on miss
        [1] setup_dl_debug_hook    0x2114eec0
  (0d) INIT_ARRAY @ 0x215f26f0  (in array order)   ── do_init / static-ctor storm
        __cpu_indicator_init → Rust ARGV init → 2898 C++ static ctors
        └─ _GLOBAL__sub_I_* / _GLOBAL__I_* / __cxx_global_var_init
            REGISTER-ONLY: absl flags, protobuf descriptors, LLVM/MLIR backends,
            and the GoogleInitializer MODULE DESCRIPTORS + dependency edges.
            *** No HAL factory / platform / target actually RUNS here. ***
                                                   ── elf-entry-and-init-proc.md / do-init-do-fini.md
        STATE AFTER STAGE 0: everything registered, nothing run, no PJRT_Api table.

═══════════════════════════════════════════════════════════════════════════════
  STAGE 1 — framework: handle = dlopen; fn = dlsym(handle, "GetPjrtApi")
═══════════════════════════════════════════════════════════════════════════════
  GetPjrtApi  0xe6a83a0   (5-byte jmp)  →  pjrt::tpu_plugin::GetTpuPjrtApi  0xe6aa440
        The ONLY exported PJRT symbol. dlsym resolves it; nothing runs yet.
                                                   ── get-pjrt-api-thunk.md / module-init-plugin-discovery.md

═══════════════════════════════════════════════════════════════════════════════
  STAGE 2 — first GetPjrtApi() call: lazy 140-slot table build
═══════════════════════════════════════════════════════════════════════════════
  GetTpuPjrtApi  0xe6aa440
        17 __cxa_guard one-shot blocks:
          guards 1..16 → build the 16 .bss extension nodes (chain head = HostMemoryAllocator)
          guard 17     → CreatePjrtApi 0xf874160 writes all 140 slots into .lbss
        return &pjrt_api  =  0x227BA840   (.lbss, was zero at load)
                                                   ── get-pjrt-api-thunk.md
        STATE AFTER STAGE 2: table built; hardware UNTOUCHED; no driver session.

═══════════════════════════════════════════════════════════════════════════════
  STAGE 3 — first api->PJRT_Plugin_Initialize(args)  (PJRT slot 8)
═══════════════════════════════════════════════════════════════════════════════
  PJRT_Plugin_Initialize  0xe6a9d00
        ├─ ActualStructSizeIsGreaterOrEqual(... min=27, cur=16, struct_size)
        ├─ if kPjRtCApiTpuInitType != 0  (statically = 2):
        │     TryAcquireTpuLock        0x20ccbc40   ── cross-process TPU lock (env TPU_LOAD_LIBRARY)
        │     GetLibTpuInitArguments   0x20ccca20   ── env LIBTPU_INIT_ARGS → argv
        │     InitializeDriver         0x204cecc0   ── driver bring-up:
        │         └─ InitGoogleExceptChangeRootAndUser → RealInitGoogle
        │              └─ RunInitializers  0x210b2d20   *** PHASE B: run the module DAG ***
        │                   → HAL factories (per TpuVersion) + xla_target_* + tpu_platform
                                                   ── tftpu-initialize-bootstrap.md / module-init-plugin-discovery.md
        STATE AFTER STAGE 3: driver live, modules RUN; silicon NOT yet scanned.

═══════════════════════════════════════════════════════════════════════════════
  STAGE 4 — first api->PJRT_Client_Create(args)  (PJRT slot 15)
═══════════════════════════════════════════════════════════════════════════════
  PJRT_Client_Create  0xe6a8840
        silicon scan / HAL impl selection: TpuVersion DETECTED here
        (PCI device-id match via *HardwareScanner::Create), builds the live
        TpuPlatform / TpuExecutor / TpuStream stack and the xla PjRtClient.
                                                   ── ../pjrt/client-and-device.md

       ┌──────────────────── STEADY STATE ────────────────────┐
       │  framework drives the 140-slot PJRT_Api: compile,     │
       │  buffer transfer, execute, collectives, profiling.    │
       │  Re-entering GetPjrtApi / PJRT_Plugin_Initialize is a │
       │  guarded fast no-op.                                  │
       └───────────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════════════════════
  STAGE 5 — dlclose / process exit
═══════════════════════════════════════════════════════════════════════════════
  DT_FINI (.fini @ 0xe63553c)   ── empty (sub/add/ret)
  FINI_ARRAY @ 0x215f8190
       [0] __do_fini                  0xe63c020  ── guarded __cxa_finalize(_dso_handle)
       [1] rand_thread_state_clear_all 0x2063df60 ── clears per-thread BoringSSL/RNG state
       No PJRT_Api / extension teardown — leaked Meyers-singleton lifetime.
                                                   ── do-init-do-fini.md
═══════════════════════════════════════════════════════════════════════════════
```text

> **GOTCHA —** 链接器做的是*最不*有趣的工作,而框架调用做的是最多的工作。若重新实现者假设“`.so` 在 `dlopen` 后就 ready”,则在每个维度上都是错的:表不存在,driver 未活动,硅片也未检测。反过来,如果宿主未通过 CPU 门禁(Stage 0c),它根本到不了 Stage 1,而是在 `dlopen` 内部 `raise(SIGILL)`,只有 stderr 消息,没有 PJRT 级错误返回。
>
> **NOTE —** Stage 2、3、4 是相互独立的一次性操作,而不是强制序列。只想做 AOT 编译的框架可以调用 `GetPjrtApi`(Stage 2),然后调用 `PJRT_TopologyDescription_Create`(槽 87),而永远不调用 `PJRT_Client_Create`(Stage 4),因此不会触碰硅片。生命周期是一个*依赖格*,不是直线;上面的时间线展示的是常见的完整执行路径。

---

## 阶段地图

每个阶段用 1–2 句话说明,并列出负责它的函数以及完整记录它的页面。这是重新实现者导航的索引;内部细节有意*不*放在这里。

### Stage 0 — 链接器驱动的初始化链(在 `dlopen` 时)

动态链接器重定位全部三个 init 数组(每个槽都是 `R_X86_64_RELATIVE`),运行遗留的 `DT_INIT` glibc stub,然后运行 `PREINIT_ARRAY`(`cpu_feature_fail_fast @ 0x2110abc0` 处的 CPU 特性硬门禁和一个 dl-debug hook),再运行 2900 项 `INIT_ARRAY` 静态构造函数风暴。这个风暴是**只注册**的:它填充 absl flag 表、protobuf 描述符、LLVM/MLIR 后端,以及至关重要的 `GoogleInitializer` 模块描述符及其依赖边,但不运行任何对顺序敏感的 TPU bring-up。`__do_init @ 0xe63c000` byte-guard(`__do_init.__initialized @ 0x224c3880`)让构造函数入口保持幂等。

> *归* [elf-entry-and-init-proc.md](elf-entry-and-init-proc.md)(ELF entry、`.init`、`.init_array`、`dlopen` 时的 GOT/PLT bring-up)*以及* [do-init-do-fini.md](do-init-do-fini.md)(构造/析构顺序和全局状态)*负责*。CPU 门禁完整的 11 特性解码在 [module-init-plugin-discovery.md](module-init-plugin-discovery.md#21-the-cpu-feature-hard-gate-preinit_array0)。

### Stage 1 — 发现握手(`dlsym("GetPjrtApi")`)

框架通过自己的(Python 侧)PJRT 插件注册表找到 `libtpu.so`,执行 `dlopen`,并且只 `dlsym` 一个符号:`GetPjrtApi @ 0xe6a83a0`,这是一个跳到内部构建器的 5 字节 `jmp` thunk。TPU 内部细节不会越过这条边界;框架只知道入口符号名(小写 `jrt`,`@@VERS_1.0`)和最前面几个 `PJRT_Api` 字段偏移。

> *归* [module-init-plugin-discovery.md](module-init-plugin-discovery.md#1-the-plugin-discovery-handshake)(握手、什么名字会解析、框架可以假设什么)*负责*。thunk 形状本身在 [get-pjrt-api-thunk.md](get-pjrt-api-thunk.md#the-exported-thunk--getpjrtapi)。

### Stage 2 — 惰性表构建(第一次 `GetPjrtApi` 调用)

thunk tail-call 到 `pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440`,后者运行 17 个受 `__cxa_guard` 保护的一次性块:前 16 个构建 `.bss` extension chain(每个节点以之前的节点作为 `.next`),第 17 个调用 `pjrt::CreatePjrtApi @ 0xf874160`,把全部 140 个槽写入零填充的 `.lbss` singleton `pjrt_api @ 0x227BA840`。此后该表已填充且不可变;硬件仍未触碰。

> *归* [get-pjrt-api-thunk.md](get-pjrt-api-thunk.md#the-lazy-builder--gettpupjrtapi)(17-guard builder、singleton、`tpu_plugin` 注入槽)*负责*。140 槽字段表在 [../pjrt/api-vtable-reconstruction.md](../pjrt/api-vtable-reconstruction.md);16 节点 chain 在 [../pjrt/extension-chain.md](../pjrt/extension-chain.md)。

### Stage 3 — Driver bootstrap + DAG run(第一次 `PJRT_Plugin_Initialize`)

`PJRT_Plugin_Initialize @ 0xe6a9d00`(槽 8)是把惰性 `.so` 变成活动 driver 会话的一次性门禁:`struct_size` 兼容性检查、`kPjRtCApiTpuInitType` 选择器(静态为 `2`)、`TryAcquireTpuLock @ 0x20ccbc40`(跨进程锁,env `TPU_LOAD_LIBRARY`)、`GetLibTpuInitArguments @ 0x20ccca20`(env `LIBTPU_INIT_ARGS`)以及 `InitializeDriver @ 0x204cecc0`。`InitializeDriver` 到达 `RealInitGoogle → GoogleInitializer::RunInitializers @ 0x210b2d20`,即 **PHASE B**:链接器只注册的模块终于按拓扑依赖顺序运行(按 `TpuVersion` 的 HAL 工厂、XLA target functor、StreamExecutor `TpuPlatform`)。

> *归* [tftpu-initialize-bootstrap.md](tftpu-initialize-bootstrap.md)(initialize 入口、`LIBTPU_INIT_ARGS` option ingest、`InitializeDriver` flag 集)*负责*。启动门禁控制流、锁和 DAG run 在 [module-init-plugin-discovery.md](module-init-plugin-discovery.md#3-the-one-time-bootstrap-gate)。

### Stage 4 — 硅片检测 + 活动 client(第一次 `PJRT_Client_Create`)

`PJRT_Client_Create @ 0xe6a8840`(槽 15)是 `TpuVersion` 最终被*检测*的位置:硅片扫描通过 `*HardwareScanner::Create` HAL 组件把 PCI device ID 匹配到版本,选择 Stage 3 中匹配工厂注册的 HAL impl,并在其上构建活动的 `TpuPlatform` / `TpuExecutor` / `TpuStream` 栈以及 `xla::PjRtClient`。这是第三次推迟:Stage 0 注册,Stage 3 运行,到了这里才*检测*。

> *归* [../pjrt/client-and-device.md](../pjrt/client-and-device.md)(client 创建、设备拓扑、活动 executor 栈)*负责*。这里列出它只是为了闭合从加载到可用的弧线。

### Stage 5 — 拆卸(卸载 / 进程退出)

不存在大型的 PJRT surface `atexit` 拆卸;`PJRT_Api` 表和 16 个 extension 节点是退出时泄漏的函数局部 static,这是插件 `.so` 的正常 Meyers-singleton 生命周期。`DT_FINI` 是空 stub;`FINI_ARRAY @ 0x215f8190` 只运行 `__do_fini @ 0xe63c020`(受 guard 保护的 `__cxa_finalize(_dso_handle)`)和 `rand_thread_state_clear_all @ 0x2063df60`(每线程 BoringSSL/RNG 清理)。Client、executable 和 buffer 通过它们显式的 `PJRT_*_Destroy` C-API 调用释放,而不是在退出时释放。

> *归* [do-init-do-fini.md](do-init-do-fini.md)(析构顺序和 `FINI_ARRAY` 函数体)*负责*,并在 [module-init-plugin-discovery.md](module-init-plugin-discovery.md#4-teardown) 上总结。

---

## 触发表

对重新实现最关键的单一事实是:*哪个事件触发每个转移*以及*它留下什么状态*。驱动者是链接器、`dlsym` 或框架 C 调用;没有任何基于时间或线程的触发。

| 阶段 | 触发器 | 负责函数 | 之后状态 |
|---|---|---|---|
| 0 | `dlopen`(动态链接器) | `cpu_feature_fail_fast` + INIT_ARRAY storm | 已注册、什么都没运行、没有表 |
| 1 | `dlsym(handle, "GetPjrtApi")` | `GetPjrtApi @ 0xe6a83a0`(thunk) | 符号已解析,什么都没运行 |
| 2 | 第一次 `GetPjrtApi()` 调用 | `GetTpuPjrtApi @ 0xe6aa440` | 140 槽表已构建,硬件未触碰 |
| 3 | 第一次 `api->PJRT_Plugin_Initialize` | `PJRT_Plugin_Initialize @ 0xe6a9d00` | driver 活动,模块 DAG 已运行,没有硅片扫描 |
| 4 | 第一次 `api->PJRT_Client_Create` | `PJRT_Client_Create @ 0xe6a8840` | 在检测到的硅片上有活动 client |
| 5 | `dlclose` / 进程退出 | `__do_fini @ 0xe63c020` + FINI_ARRAY | 每线程 RNG 已清除;PJRT surface 泄漏 |

> **QUIRK —** 三个“首次调用”阶段(2/3/4)分别由*不同* once-guard 机制保护,而不是一个共享方案:Stage 2 由 libtpu 自己的 libc++abi `__cxa_guard` 保护(17 个不同的 `.bss` guard byte,`acquire/release/abort @ 0x213e9ac0 / 0x213e9be0 / 0x213e9c20`);Stage 3 由 `absl::Mutex` once-lock(`TryAcquireTpuLock::mu`,guard `@ 0x225925d0`)加上 platform 注册的函数静态 byte guard(`tpu_platform_registered @ 0x224c5388`)保护;链接器入口(Stage 0)由简单的 `__do_init___initialized @ 0x224c3880` byte 保护。重新实现者必须复现全部三种幂等风格;把它们折叠成一个锁会改变框架依赖的并发和生命周期语义。
>
> **NOTE —** `kPjRtCApiTpuInitType` 选择器 `@ 0x22255b40` 静态为 `2`(完整 TPU bring-up)。Type `0` 会让 Stage 3 no-op。是否存在任何路径会把它改写为 `0`/`1`(旧式 TF vs PJRT init-type)尚未追踪;改写存在性的可信度为 LOW,静态值 `2` 为 CONFIRMED。

---

## 每个阶段越过边界的内容

重新实现者的另一个核心心智模型:每次转移有哪些数据/状态流入流出。所有内容都是 C-ABI 或进程内状态;除 Stage 3 的跨进程 TPU 锁之外,没有 IPC。

| 阶段 | 输入 | 输出 / 副作用 |
|---|---|---|
| 0 | CPU feature mask(`__cpu_indicator_init`) | 已填充的 flag/descriptor/backend 表 + `GoogleInitializer` DAG;缺少 ISA 特性时 SIGILL |
| 1 | 符号名 `"GetPjrtApi"` | 函数地址(thunk) |
| 2 | 无(无参数) | `const PJRT_Api*` → `0x227BA840`;构建 16 个 `.bss` extension 节点 |
| 3 | `PJRT_Plugin_Initialize_Args`(`struct_size`);env `TPU_LOAD_LIBRARY`、`LIBTPU_INIT_ARGS` | 持有跨进程 TPU 锁;模块 DAG 已运行;`TpuPlatform` 已注册;成功时 `NULL` / 错误时 heap-boxed status |
| 4 | `PJRT_Client_Create_Args` | 活动 `xla::PjRtClient`;检测到的 `TpuVersion`;活动 executor 栈 |
| 5 | `_dso_handle` | `__cxa_finalize`;每线程 RNG 已清除 |

---

## 相关组件

| 组件 | 关系 |
|---|---|
| `GetPjrtApi @ 0xe6a83a0` | 单一导出入口符号;Stage-1 发现会合点 |
| `pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440` | Stage-2 惰性 140 槽表构建器 |
| `pjrt::CreatePjrtApi @ 0xf874160` | 在第 17 个 guard 上把全部 140 个槽写入 `.lbss`(Stage 2) |
| `cpu_feature_fail_fast @ 0x2110abc0` | Stage-0 PREINIT CPU ISA 硬门禁(缺少基线时 SIGILL) |
| `GoogleInitializer::RunInitializers @ 0x210b2d20` | 加载时注册(Stage 0)/ 首次 init 时运行(Stage 3)的模块 DAG |
| `PJRT_Plugin_Initialize @ 0xe6a9d00` | Stage-3 一次性启动门禁(PJRT 槽 8) |
| `PJRT_Client_Create @ 0xe6a8840` | Stage-4 硅片扫描 + 活动 client(PJRT 槽 15) |
| `__do_fini @ 0xe63c020` + FINI_ARRAY | Stage-5 拆卸(受 guard 保护的 `__cxa_finalize` + RNG clear) |
| `Tpu*_*` C-ABI(194 个导出) | 共享同一二进制的旧式 StreamExecutor surface,不会通过 PJRT 到达 |

## 交叉引用

- [elf-entry-and-init-proc.md](elf-entry-and-init-proc.md) — Stage 0:ELF entry、`.init`、`.init_array`、`dlopen` 时的 GOT/PLT bring-up
- [do-init-do-fini.md](do-init-do-fini.md) — Stage 0 / Stage 5:构造/析构顺序、`__do_init`/`__do_fini` byte guard,以及 `FINI_ARRAY` 函数体
- [get-pjrt-api-thunk.md](get-pjrt-api-thunk.md) — Stages 1–2:`GetPjrtApi` thunk、`GetTpuPjrtApi` 17-guard builder、`.lbss` singleton,以及 `tpu_plugin` 注入槽
- [module-init-plugin-discovery.md](module-init-plugin-discovery.md) — 完整的发现握手、加载期初始化链(CPU gate + 只注册风暴),以及 `PJRT_Plugin_Initialize` 启动门禁
- [tftpu-initialize-bootstrap.md](tftpu-initialize-bootstrap.md) — Stage 3:initialize 入口、`LIBTPU_INIT_ARGS` option ingest,以及 `InitializeDriver` flag 集
- [../pjrt/overview.md](../pjrt/overview.md) — PJRT C-ABI 地图:`PJRT_Api` 结构形状、握手,以及 extension chain by region
- [../pjrt/client-and-device.md](../pjrt/client-and-device.md) — Stage 4:`PJRT_Client_Create`、硅片检测,以及活动 executor 栈
- [../pjrt/api-vtable-reconstruction.md](../pjrt/api-vtable-reconstruction.md) — Stage-2 构建所物化的完整 140 槽逐字段表