Skip to content

PJRT C-ABI 概览

本页中的所有地址均适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(build libtpu_lts_20260413_b_RC00,build-id md5 89edbbe81c5b328a958fe628a9f2207d,781,691,048 bytes,ELF x86-64 DYN,not stripped;demangled C++ symbols 按原文引用)。其他版本会有所不同。

摘要

PJRT 是稳定的 C 插件 ABI,所有 XLA 前端(JAX、TensorFlow、PyTorch-XLA)都通过它驱动硬件后端,而不需要链接后端的 C++,也不需要与后端共享 C++ ABI。该契约刻意保持最小:框架 dlopen libtpu.sodlsym 一个导出的入口符号,无参数调用它,然后接收一个扁平 C struct(PJRT_Api)的指针,其字段都是函数指针。之后的每一次交互(创建 client、编译 HLO module、上传 buffer、启动 executable、等待 event)都是通过该 struct 某个槽位进行的间接调用。由于该 struct 是普通 C,且每次调用都做 size check,基于旧 header 构建的框架可以驱动较新的插件,反之亦然。这与 StreamExecutor 的 C-shim(TfTpu_*ApiFn)在 legacy stack 中扮演的角色相同,但 PJRT 是公开、版本化的表面,也是现代 PJRT client 接触的唯一表面。

在此构建中,入口符号是 GetPjrtApi @ 0xe6a83a0,它是一个 5-byte jmp thunk,跳入真正的引擎 pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440(该 thunk 本身在 ../lifecycle/get-pjrt-api-thunk.md 中剖析)。这里没有导出的 GetTpuPjrtApi;canonical casing 的 GetPjrtApi 是唯一匹配 /Pjrt/GLOBAL FUNC 导出,版本为 GetPjrtApi@@VERS_1.0。它返回的 PJRT_Api不是任何 PROGBITS section 中的静态镜像,而是 .lbss 中位于 0x227BA840 的函数局部 Meyers singleton(1120 bytes = 140 × 8),加载时填零,并在首次调用时由 pjrt::CreatePjrtApi @ 0xf874160__cxa_guard 保护下惰性填充。该表是 PJRT C-API v0.103:140 个 qword 槽位,包括 5 个 header scalar(struct_sizeextension_start 和一个 3 字段 PJRT_Api_Version sub-struct),后接 135 个函数指针。挂在 extension_start 上的是 17 节点单向扩展链,承载不适合放入通用 vtable 的 TPU 专用表面(Megascale、MultiSlice、collectives、raw buffers、phase-compile、profiler 等)。

本页是 PJRT 章节的地图。它确定 ABI 的形状、dlsym 握手和惰性构建路径(按符号说明,深层 module-init 生命周期由其他页面负责),并按区域给出 140 槽表和 17 扩展链的一览索引,链接到负责各区域逐字段细节的同级页面。它复述完整槽位表(api-vtable-reconstruction.md)或扩展节点布局(extension-chain.md);它告诉你哪个同级页面负责什么。

作为定位,契约是:

  • 握手dlsym("GetPjrtApi") → 无参数调用 → 一个 const PJRT_Api*;调用者可以假设什么,以及必须发现什么。
  • 结构形状:1120-byte 扁平 C 表:5 个 header scalar + 135 个 fn-ptr,按 native order 精确匹配公开 xla/pjrt/c/pjrt_c_api.h v0.103 schema,并带有每次调用的 struct_size 版本化。
  • 5 个 TPU 注入点:槽位 8/9/15/87/103 是 CreatePjrtApi 唯一从调用者取得的槽位;其他 130 个是编译期固定的通用 XLA wrapper。
  • 扩展链:一个 NULL-terminated、最新优先的 17 个 typed extension 链表,只能通过 extension_start 到达,绝不能通过固定偏移到达。
导出入口符号GetPjrtApi @ 0xe6a83a0(5-byte jmp thunk,@@VERS_1.0
真实引擎pjrt::tpu_plugin::GetTpuPjrtApi @ 0xe6aa440(1336 B,17 个 __cxa_guard 块)
表构造器pjrt::CreatePjrtApi @ 0xf874160(1872 B,扁平 header-write + slot-fill,无循环)
表存储GetTpuPjrtApi()::pjrt_api @ 0x227BA840.lbss(NOBITS),1120 B = 140 × 8
C-API 版本v0.103 — version qword 0x6700000000{major=0, minor=0x67=103}
槽位数140(5 个 header scalar + 135 个 fn-ptr)
扩展链头extension_start(slot 1)→ host_memory_allocator_extension @ 0x224C3F68
扩展数量17(16 个驻留于 .bss 并惰性构建;1 个 .data 静态节点:profiler)
TPU 注入槽位8, 9, 15, 87, 103(Plugin_Initialize、Plugin_Attributes、Client_Create、TopologyDescription_Create、ExecuteContext_Create)
置信度CONFIRMED(byte-anchored vs decompile),除非某行或 callout 另有说明

1. 插件发现握手

目的

PJRT 存在的核心理由是提供一个单一、ABI-stable 的会合点。框架不了解 libtpu 的内部;它只知道入口符号名称,以及前几个 PJRT_Api 字段的布局。其他所有内容都在运行时通过 struct_size 字段和扩展链发现。本节固定这个会合点,使重新实现者可以产出一个 stock JAX/PyTorch-XLA 构建能够加载的 .so

契约

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

```text
framework (JAX / TF / PyTorch-XLA)
  dlopen("libtpu.so")
  dlsym(handle, "GetPjrtApi")  ── the ONLY name that resolves; "GetTpuPjrtApi" is internal

    └─ GetPjrtApi  0xe6a83a0   ── 5-byte: jmp 0xe6aa440
         └─ pjrt::tpu_plugin::GetTpuPjrtApi  0xe6aa440
              ├─ build 16 .bss extensions  (each one-shot __cxa_guard)
              ├─ (17th guard) pjrt::CreatePjrtApi(&pjrt_api, …)  0xf874160  ── writes all 140 slots
              └─ return &pjrt_api  =  0x227BA840   (.lbss)

随后调用者读取 api->struct_size 来获知该插件实际提供多少槽位,读取 api->pjrt_api_version 来获知 minor version,并遍历 api->extension_start 来发现可选能力。接着它调用 api->PJRT_Plugin_Initialize(slot 8),即一次性的 TPU driver bring-up,并调用 api->PJRT_Client_Create(slot 15)来创建 live client。这两个槽位都会深入 module-init 和 silicon detection,它们属于 lifecycle 章节的范围(PJRT_Plugin_InitializeTryAcquireTpuLockInitializeDriverGoogleInitializer module DAG;silicon scan 延迟到 PJRT_Client_Create);本页只按符号引用它们,见 ../lifecycle/module-init-plugin-discovery.md../lifecycle/do-init-do-fini.md

注释 — 该表在首次调用时构建,而不是在 dlopen 时构建。GetTpuPjrtApipjrt_api.lbss(NOBITS)中的函数局部静态对象,在第一次 GetPjrtApi 前为零。17 个受 __cxa_guard 保护的块(16 个 extension builder + 最后的 CreatePjrtApi)恰好运行一次;并发的首次调用者通过 Itanium-ABI __cxa_guard 语义串行化,一次性初始化后该 struct 在进程生命周期内不可变,读取者不加锁。因此静态反汇编无法显示已填充的槽位值;slot→impl 映射是从 CreatePjrtApi 的函数体重建的,而不是从填零的 .lbss 镜像读取的。

陷阱 — 拼写和大小写是 ABI 的一部分。导出符号是 GetPjrtApi(小写 jrt),匹配公开 PJRT plugin 约定;GetTpuPjrtApi内部 helper,且导出。若 loader dlsym GetTpuPjrtApi,或某个构建只导出带 Tpu 前缀的名称,发现会失败。该二进制中共同存在的 Tpu*_* 导出(194 个符号,全部为 FUNC GLOBAL @@VERS_1.0)是 legacy StreamExecutor C-ABI,由 TF-TPU 直接链接,永远不会通过 PJRT 到达;见 stream-executor-pjrt-adapter.md


2. PJRT_Api 结构一览

目的

PJRT_Api 是一个包含 140 个 qword 槽位的扁平 C struct。重新实现者在陷入 135 行字段表前,需要先了解其形状:header 与函数指针区域、版本化约定,以及哪些槽位是插件专用的。本节给出该形状;逐字段重建见 api-vtable-reconstruction.md

Header 布局

前五个槽位不是函数指针。它们是使 ABI 前向/后向兼容的自描述 header。已逐字节对照 CreatePjrtApi @ 0xf874160 确认(*a1 = 1120; a1[2] = 24; a1[4] = 0x6700000000)。

SlotOffsetFieldValue
0+0x00struct_size1120 (= 140 × 8)
1+0x08extension_starthost_memory_allocator_extension @ 0x224C3F68
2+0x10pjrt_api_version.struct_size24(sub-struct 自身大小)
3+0x18pjrt_api_version.privNULL(reserved)
4+0x20pjrt_api_version.{major,minor}{0, 103}(qword 0x6700000000

pjrt_api_version 是嵌入的 PJRT_Api_Version { size_t struct_size; void* priv; int major; int minor }(24 bytes),因此槽位 2..4 是一个逻辑字段。major/minor 以 little-endian 打包进 slot-4 qword:低 32 bits = major(0),高 32 = minor(0x67 = 103)。

函数指针区域

槽位 5..139 是 135 个函数指针,顺序与公开 xla/pjrt/c/pjrt_c_api.h v0.103 header 声明完全一致,包括在原始 v0.40 表面之后追加的晚期槽位(95..139),按功能落地顺序排列。这里不复述全部 135 个槽位,而是将它们分组到各同级页面负责的区域。pre-95 块是稳定的 v0.40 core;95..139 是 v0.103 late additions(Output{ElementTypes,Dimensions}CopyToMemoryCreateViewOfDeviceBufferExecutable_FingerprintAsyncHostToDeviceTransferManager_* family、DmaMap/UnmapCreateAliasBufferDonateWithControlDependencyEvent_Create/SetClient_LoadBitcastError_ForEachPayloadTopology_FingerprintParameterMemoryKinds)。

Slot range区域负责页面
5–7, 137Error: Destroy / Message / GetCode / ForEachPayloadevents-and-async.md
8–9Plugin: Initialize, Attributes(lifecycle entry)(lifecycle — 按符号引用)
10–14, 131–132Event: Destroy / IsReady / Error / Await / OnReady / Create / Setevents-and-async.md
15–27, 98, 100, 108, 115–123, 134Client: create/lookup/compile/buffer-from-host/alias/dma/loadclient-and-device.md
28–39, 126–127, 133DeviceDescription + Deviceclient-and-device.md
40–44, 102Memorybuffer-and-memory.md
45–54, 95–96, 99, 101, 129, 139Executable(program metadata, serialize, cost, fingerprint)executable-execution.md
55–62, 122, 135LoadedExecutable(包括 slot 60 Execute,hot path)executable-execution.md
63–81, 97, 105, 125, 130, 136Buffer(lifecycle, transfer, refcount, donate, bitcast)buffer-and-memory.md
82–86CopyToDeviceStreamdma-and-cross-host-recv.md
87–93, 100, 119, 138TopologyDescriptionext-topology-description.md
94PJRT_Compile(AOT, no client)executable-execution.md
103–104ExecuteContextexecutable-execution.md
106–114, 124AsyncHostToDeviceTransferManagerbuffer-and-memory.md
127–128AsyncTrackingEventevents-and-async.md

特性 — 槽位顺序不是像上表那样按对象分组。header 的区域分组只是阅读辅助;实际 pjrt_c_api.h 顺序会交错各 family,因为槽位按上游功能落地顺序追加。PJRT_Compile 是 slot 94,但逻辑上属于 Executable 块的 Executable_OutputElementTypes 是 slot 95,即在它之后,因为该功能后加入。重新实现者必须逐字复现 header schema 的 wire order;family grouping 是给人看的,slot index 才是 ABI。完整有序列表见 api-vtable-reconstruction.md

版本化:旧 client 如何与新插件通信

header 通过两个协同机制实现前向/后向兼容。首先,每个可达的 PJRT_Api 字段本身都通过其 args struct 上的 struct_size 做版本化:几乎每个槽位的第一条指令都会调用 pjrt::ActualStructSizeIsGreaterOrEqual("<API>_Args", min, current, args->struct_size),它接受从文档化 min 到插件 current 的任何调用者 args struct,并且不读取超出调用者 struct_size 的字段。其次,表级 struct_size(slot 0)和 pjrt_api_version(slots 2..4)让调用者在索引槽位前知道有多少槽位存在。基于 v0.95 编译的 client 看到 struct_size = 1120 和 minor 103,不会索引超过自己已知的槽位,并传入更小的 args struct,而 per-slot guard 会接受它们。反向情形,即 v0.103 client 对接旧插件,则受旧插件的 struct_size 限制。

陷阱 — 绝不能通过假设固定表大小来读取 extension_start 或任何 late slot。硬编码 140 个槽位的重新实现会错误解析旧插件或新插件。应先读取 struct_size;将 offset >= struct_size 的任何槽位视为 absent。同一规则也通过 args struct_size guard 应用于每次调用。


3. 扩展链一览

目的

通用 PJRT_Api vtable 无法在不污染公开 schema 的情况下承载后端专用表面(TPU topology details、Megascale/MultiSlice multi-pod features、collectives、raw-buffer DMA、profiler)。PJRT 通过扩展链解决这个问题:一个 NULL-terminated 单向链表,由 typed PJRT_Extension_Base 节点组成,只能通过 extension_start(slot 1)到达。每个节点以 { size_t struct_size; PJRT_Extension_Type type; PJRT_Extension_Base* next; } 开头(.next 位于 offset +0x10),后接该扩展自己的函数指针。本节按 type 索引该链;逐节点字段图见 extension-chain.md 和各同级扩展页面。

extension_start = 0x224C3F68。该链以最新优先顺序遍历(构建顺序的反向),并在 profiler → NULL 处终止。总计 17 个节点:16 个驻留于 .bss,在 GetTpuPjrtApi 内通过 __cxa_guard 惰性构建;profiler 是唯一的 .data static-init 节点,并作为链尾种子。

#Type IDExtensionSize (B)负责页面
123HostMemoryAllocator(chain head)32ext-remaining.md
222MultiSlice64ext-remaining.md
321Collectives96collectives-communicator.md
420AbiVersion120ext-remaining.md
519Shardings40ext-remaining.md
618Megascale248collectives-communicator.md
717TpuExecutable88executable-execution.md
816TpuTopology272ext-topology-description.md
914Callback40callbacks.md
109PhaseCompile64ext-compile-phasecompile.md
1112CrossHostTransfers56dma-and-cross-host-recv.md
1215HostAllocator48ext-remaining.md
1313ExecutableMetadata40executable-execution.md
146MemoryDescriptions40buffer-and-memory.md
154Layouts80ext-remaining.md
168RawBuffer80ext-rawbuffer.md
171Profiler(.data, static)40ext-profiler.md

Type ID 0, 2, 3, 5, 7, 10, 11 在此构建中未使用。公开 XLA 还注册了这里不存在的 FFI 和 Memory_Stream 扩展类型:libtpu 唯一的 custom-op 表面通过 TpuExecutable 扩展的 SetTpuCompilationEnv 汇入,而不是通过公开 FFI 扩展。

特性 — 链顺序与构建顺序相反,因此迭代会先得到最新扩展,最后得到 profiler。重新实现者必须通过遍历 .next 直到 NULL 并匹配 type 来发现能力,绝不能按位置或按链中的固定偏移发现。构建顺序(RawBuffer → … → HostMemoryAllocator,profiler 作为 RawBuffer 的 .next 种入)是 GetTpuPjrtApi 的实现细节;只有 type tag 具有契约意义。


4. 五个 TPU 注入点

目的

CreatePjrtApi 只从其调用者接收六个函数指针性质的参数,并硬编码其余内容。其中五个成为 TPU 专用槽位;第六个是扩展链头。了解哪五个槽位由插件提供(相对于通用 pjrt::PJRT_* wrapper)能告诉重新实现者 TPU 后端究竟在哪些位置接入一个原本通用的 XLA 表。

注入槽位

已对照 CreatePjrtApi @ 0xf874160 确认:函数体是扁平 header-write 加 lea/mov slot-fill,没有循环,并且恰好五个槽位从 incoming register args 写入(a1[8]=a5a1[9]=a7a1[15]=a2a1[87]=a4a1[103]=a3);slot 1(a1[1]=a6)是链头。

SlotFieldlibtpu implAddr作用
8PJRT_Plugin_Initializetpu_plugin::PJRT_Plugin_Initialize0xE6A9D00一次性 TPU driver bring-up(lifecycle)
9PJRT_Plugin_Attributespjrt::PJRT_Plugin_Attributes_Xla0xF85F080Plugin attribute table:通用 XLA 实现,不是 TPU override
15PJRT_Client_Createtpu_plugin::PJRT_Client_Create0xE6A8840Silicon scan + live client construction
87PJRT_TopologyDescription_Createtpu_plugin::PJRT_TopologyDescription_Create0xE6A9B20TPU pod topology(AOT, no client)
103PJRT_ExecuteContext_Createtpu_plugin::PJRT_ExecuteContext_Create0xE6A9A80Per-execution context

其余 130 个函数指针槽位是编译期固定的 pjrt::PJRT_* wrapper(在 CreatePjrtApi 中以 lea 加载的常量),与通用 XLA PJRT 层共享。调用最频繁的单个槽位是 slot 60 PJRT_LoadedExecutable_Execute @ 0xF869B40,即每步程序启动;它是通用 wrapper,最终落到 runtime 的 CommonPjRtLoadedExecutable::Execute,见 executable-execution.md../runtime/overview.md

注释 — slot 9(PJRT_Plugin_Attributes)虽然是注入参数,但并非 TPU 专用:CreatePjrtApi 的调用者传入通用 pjrt::PJRT_Plugin_Attributes_Xla。它公告标准 XLA attribute set(version metadata、supported devices、serialization info)。真正 TPU 专用的注入点只有 slots 8/15/87/103。


相关组件

ComponentRelationship
GetPjrtApi / GetTpuPjrtApi导出入口符号及其惰性构建引擎
pjrt::CreatePjrtApi @ 0xf874160将 140 槽表物化到 .lbss 中的构造器
pjrt::ActualStructSizeIsGreaterOrEqual @ 0xf8a4ec0每个槽位开头使用的 per-call backward-compat size gate
17-extension chain(0x224C3F68 head)通过 extension_start 到达的 typed、最新优先能力列表
xla::TpuClient / tpu::System通用槽位最终落入的 runtime(modern PJRT stack)
Tpu*_* C-ABI(194 个导出)与该二进制共享但不通过 PJRT 到达的 legacy StreamExecutor 表面

交叉引用