PJRT Profiler 扩展(类型 1)
本页所有地址和偏移均适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(build-id89edbbe81c5b328a958fe628a9f2207d),PJRT C-API v0.103。其他版本会有所不同。
摘要
Profiler 扩展是 libtpu 在 PJRT C-API 与 TPU/xprof XSpace 跟踪后端之间的桥梁。它是 openxla 的 xla/pjrt/c/pjrt_c_api_profiler_extension.h 定义的规范 PJRT_Profiler_Extension(扩展类型 id 1),挂在扩展链上,并由框架(JAX、PyTorch-XLA)通过扫描 PJRT_Api.extension_start 的类型 id 发现。不同于其他 16 个扩展,它的节点主体中不携带自己的方法;它唯一的载荷指针指向一个独立的 PLUGIN_Profiler_Api vtable,其中有 8 个函数指针。这个 vtable 才是真正的接口:三个包装 absl::Status 的错误辅助函数,加上驱动堆分配 PLUGIN_Profiler 句柄的五步生命周期 Create -> Start -> Stop -> CollectData -> Destroy。
该句柄是一个很薄的传输层,而不是跟踪器。它的工作委托给由全局工厂注册表(tsl::profiler::CreateProfilers)构建的 tsl::profiler::ProfilerCollection,后者扇出到设备侧的 xprof::tpu::TpuProfilerImpl 以及主机侧的 HostTracer/ThreadpoolProfilerInterface 子 profiler。这些子 profiler 捕获主机 tsl::profiler::TraceMe 事件和每芯片硬件环形缓冲区样本;CollectData 将它们扁平化为 tensorflow.profiler.XSpace protobuf,并序列化到调用方借用的字节缓冲区中。因此 PJRT 层不会增加任何逐事件开销:它解析输入的 ProfileOptions,编排输出的 XSpace 字节,并在两者之间拥有生命周期状态机。
本页涵盖 PJRT_Profiler_Extension 节点结构体、PLUGIN_Profiler_Api 8 槽 vtable 及其逐方法参数、PLUGIN_Profiler/PLUGIN_Profiler_Error 句柄布局、Create/Start/Stop/CollectData/Destroy 状态机,以及通向 ProfilerCollection/TraceMe/XSpace 的桥接。链节点机制(头部、遍历、类型 id)位于扩展链。深层 XSpace/XPlane 载荷解码和按芯片家族划分的 trace-point 枚举属于 profiling 章节的范围;这里仅通过符号引用,不做硬链接。
对于重新实现,其契约是:
- 40 字节的
PJRT_Profiler_Extension节点,以及它位于.data、由重定位填充的性质(没有Create*Extension调用)。 - 80 字节的
PLUGIN_Profiler_Apivtable:固定的 8 槽顺序,以及哪些槽会验证struct_size。 - 128 字节的
PLUGIN_Profiler句柄布局,以及重载的一字节状态机。 - 五个生命周期函数体:每个函数读写什么、破坏性的
CollectData、借用缓冲区契约。 - 委托到
tsl::profiler::ProfilerCollection,以及在全局互斥锁下进行的CreateProfilers工厂遍历。 - 与共享同一后端的旧版
TpuProfiler_*C-ABI 的关系。
| 扩展节点 | pjrt::tpu_plugin::profiler_extension @ 0x22255B98 (.data),40 字节 (0x28),类型 id 1 |
| 方法 vtable | pjrt::tpu_plugin::profiler_api @ 0x22255B48 (.data),80 字节 (0x50),8 个 fn-ptr |
| 句柄 | PLUGIN_Profiler,128 字节 (0x80),在 Create 中 operator new(0x80) |
| 错误对象 | PLUGIN_Profiler_Error,8 字节,包装一个 absl::Status |
| 后端 | tsl::profiler::ProfilerCollection(vtable @ 0x217738A0),由 CreateProfilers @ 0x1CF50860 构建 |
| 输出格式 | 序列化的 tensorflow.profiler.XSpace proto |
| 链角色 | .data 种子;类型 1 终止节点(next = NULL,直到被修补)——见扩展链 |
扩展节点
布局
该节点由一个 PJRT_Extension_Base 头部、后接单个载荷指针和尾随的保留 qword 组成。它是 17 个扩展中最小的,也是唯一一个位于 .data、字段在链接时写入并由动态链接器修补的扩展;它没有 pjrt::Create*Extension 初始化器。
/* PJRT_Profiler_Extension @ 0x22255B98 — 40 bytes (0x28), type id 1.
* Field order fixed by xla/pjrt/c/pjrt_c_api_profiler_extension.h. */
struct PJRT_Profiler_Extension {
/* +0x00 */ size_t struct_size; /* = 0x28, written at link time */
/* +0x08 */ uint32_t type; /* = 1, written at link time */
/* +0x0C */ uint32_t _pad0; /* = 0 */
/* +0x10 */ PJRT_Extension_Base* next; /* NULL in .data image; patched at
runtime by GetTpuPjrtApi when the
.bss chain is appended in front */
/* +0x18 */ PLUGIN_Profiler_Api* profiler_api; /* -> 0x22255B48 (R_X86_64_RELATIVE) */
/* +0x20 */ uint64_t _unused; /* = 0 */
};
```text
`struct_size` 和 `type` 占据标准链头位置,因此 `FindExtension(api, 1)` 遍历会落到这个节点上,使用者将结果转换为 `PJRT_Profiler_Extension*`。头部之后唯一有意义的字段是位于 `+0x18` 的 `profiler_api`;链头在 `+0x10` 处到达 `next`,随后此节点添加 API 指针和一个零 qword。
> **特性 —** Profiler 是链的*种子*,不是像其他 16 个扩展那样的链路。`GetTpuPjrtApi` @ `0xE6AA440` 从不调用 `CreateProfilerExtension`;它只是把 `&profiler_extension` 作为首个构建出的 `.bss` 节点的 `next` 传给 `CreateRawBufferExtension`。镜像自带 `next = NULL`,因此在链组装完成前,此节点本身就是终止节点。初始化后,RawBuffer 的 `next` 指回这里,而此节点的 `next` 保持 `NULL`,使 Profiler 成为遍历器最后到达的尾节点。见[扩展链 — 链如何构建](extension-chain.md#how-the-chain-is-built)。
### 静态初始化
没有初始化函数。这两个 `.data` 结构体的 10 个 qword 在加载时由动态链接器根据 `R_X86_64_RELATIVE` 重定位填充。节点自身的常量字段(`struct_size = 0x28`、`type = 1`)烘焙在 `.data` 镜像中;两个指针字段是重定位项。
```text
profiler_extension @ 0x22255B98
+0x18 (0x22255BB0) ── R_X86_64_RELATIVE ──> 0x22255B48 (profiler_api)
profiler_api @ 0x22255B48
+0x10 (0x22255B58) ── R_X86_64_RELATIVE ──> 0xE6F1540 (Error_Destroy)
+0x18 (0x22255B60) ── R_X86_64_RELATIVE ──> 0xE6F17C0 (Error_Message)
+0x20 (0x22255B68) ── R_X86_64_RELATIVE ──> 0xE6F1920 (Error_GetCode)
+0x28 (0x22255B70) ── R_X86_64_RELATIVE ──> 0xE6F0C60 (Create)
+0x30 (0x22255B78) ── R_X86_64_RELATIVE ──> 0xE6F0E80 (Destroy)
+0x38 (0x22255B80) ── R_X86_64_RELATIVE ──> 0xE6F0FA0 (Start)
+0x40 (0x22255B88) ── R_X86_64_RELATIVE ──> 0xE6F1100 (Stop)
+0x48 (0x22255B90) ── R_X86_64_RELATIVE ──> 0xE6F1240 (CollectData)注意 — 因为 vtable 由重定位填充,而不是由带 guard 的初始化器写入,所以 Profiler 接口在库一映射时就是可用的,早于任何
GetTpuPjrtApi调用。其他扩展只会在第一次GetPjrtApi()/__cxa_guard过程时实体化。
PLUGIN_Profiler_Api Vtable
布局
profiler_api @ 0x22255B48 是一张 80 字节表:一个 {struct_size, priv} 前导段,后接 8 个函数指针。槽位顺序由规范头文件固定:先是三个错误辅助函数,然后是五个生命周期方法。
/* PLUGIN_Profiler_Api @ 0x22255B48 — 80 bytes (0x50), 8 fn-ptrs. */
struct PLUGIN_Profiler_Api {
/* +0x00 */ size_t struct_size; /* = 0x50 */
/* +0x08 */ void* priv; /* = NULL */
/* +0x10 */ PLUGIN_Profiler_Error_Destroy* Error_Destroy;
/* +0x18 */ PLUGIN_Profiler_Error_Message* Error_Message;
/* +0x20 */ PLUGIN_Profiler_Error_GetCode* Error_GetCode;
/* +0x28 */ PLUGIN_Profiler_Create* Create;
/* +0x30 */ PLUGIN_Profiler_Destroy* Destroy;
/* +0x38 */ PLUGIN_Profiler_Start* Start;
/* +0x40 */ PLUGIN_Profiler_Stop* Stop;
/* +0x48 */ PLUGIN_Profiler_CollectData* CollectData;
};
```text
每个方法都接收单个 `PLUGIN_Profiler_*_Args*`,并返回 `PLUGIN_Profiler_Error*`(成功时为 `NULL`)。生命周期参数去掉了主表参数中携带的 `priv` 字段;三个错误参数保留规范的 `{struct_size, priv, ...}` 前导段。
### 方法映射
全部 8 个槽都解析到 `xla::profiler::` 下的 text-section 符号,这一点由函数表中的修饰名和地址确认。“参数大小”列是每个方法用于校验 `struct_size` 的值;前提是该方法确实会校验。
| 槽 | 偏移 | 方法 | 实现符号(`xla::profiler::`) | 地址 | 大小 | 参数大小 |
|---|---|---|---|---|---|---|
| 0 | `+0x10` | `Error_Destroy` | `PLUGIN_Profiler_Error_Destroy` | `0xE6F1540` | 235 | `0x18` (24) |
| 1 | `+0x18` | `Error_Message` | `PLUGIN_Profiler_Error_Message` | `0xE6F17C0` | 343 | `0x28` (40) |
| 2 | `+0x20` | `Error_GetCode` | `PLUGIN_Profiler_Error_GetCode` | `0xE6F1920` | 99 | `0x1C` (28) |
| 3 | `+0x28` | `Create` | `PLUGIN_Profiler_Create` | `0xE6F0C60` | 539 | —(不检查) |
| 4 | `+0x30` | `Destroy` | `PLUGIN_Profiler_Destroy` | `0xE6F0E80` | 263 | —(不检查) |
| 5 | `+0x38` | `Start` | `PLUGIN_Profiler_Start` | `0xE6F0FA0` | 322 | —(不检查) |
| 6 | `+0x40` | `Stop` | `PLUGIN_Profiler_Stop` | `0xE6F1100` | 300 | —(不检查) |
| 7 | `+0x48` | `CollectData` | `PLUGIN_Profiler_CollectData` | `0xE6F1240` | 728 | —(不检查) |
> **陷阱 —** 只有三个*错误辅助*槽会校验 `struct_size`。五个生命周期方法完全**不会**进行向后兼容的大小检查。错误辅助函数会流经 `xla::profiler::(anonymous namespace)::CheckMatchingStructSizes(name_view, required, current)` @ `0xE6F1640`,后者在不匹配时构造一个 `absl::Status` `"<Args> size: expected M …"`(并对 Destroy/Message 尽力继续;对 GetCode 则返回错误)。生命周期方法会无条件从固定偏移读取字段;如果调用方使用的 `pjrt_c_api_profiler_extension.h` 版本与 libtpu 附带的版本不同,就会静默误读 `Create_Args`/`CollectData_Args`。生命周期路径没有前向兼容窗口:必须链接匹配的头文件。
### 参数结构体
生命周期参数很小;`Create` 和 `CollectData` 携带数据,其余只携带句柄。
```c
/* Error helpers keep the {struct_size, priv, ...} preamble. */
struct PLUGIN_Profiler_Error_Destroy_Args { /* 24 (0x18) */
size_t struct_size; /* must == 0x18 */
void* priv;
PLUGIN_Profiler_Error* error; /* in; freed */
};
struct PLUGIN_Profiler_Error_Message_Args { /* 40 (0x28) */
size_t struct_size;/* must == 0x28 */
void* priv;
const PLUGIN_Profiler_Error* error; /* in (not consumed) */
const char* message; /* out — borrowed view into Status */
size_t message_size;/* out */
};
struct PLUGIN_Profiler_Error_GetCode_Args { /* 28 (0x1C) */
size_t struct_size;/* must == 0x1C */
void* priv;
const PLUGIN_Profiler_Error* error; /* in */
int32_t code; /* out — absl::StatusCode */
};
/* Lifecycle args drop `priv`. */
struct PLUGIN_Profiler_Create_Args { /* 32 (0x20) */
size_t struct_size;
const char* serialized_options; /* in — tensorflow.ProfileOptions */
size_t serialized_options_size;/* in */
PLUGIN_Profiler* profiler; /* out */
};
struct PLUGIN_Profiler_Destroy_Args { /* 16 */
size_t struct_size;
PLUGIN_Profiler* profiler; /* in; consumed */
};
struct PLUGIN_Profiler_Start_Args { size_t struct_size; PLUGIN_Profiler* profiler; }; /* 16 */
struct PLUGIN_Profiler_Stop_Args { size_t struct_size; PLUGIN_Profiler* profiler; }; /* 16 */
struct PLUGIN_Profiler_CollectData_Args { /* 32 (0x20) */
size_t struct_size;
PLUGIN_Profiler* profiler; /* in */
size_t buffer_size_in_bytes; /* in/out — query vs fetch */
uint8_t* buffer; /* in/out — borrowed on return */
};句柄和错误对象
PLUGIN_Profiler
一个 128 字节的堆对象(operator new(0x80)),构造时除状态字节外清零。它内联聚合一个 tensorflow::profiler::XSpace、一个序列化字节 vector、缓存大小、后端 ProfilerCollection*,以及一个状态字节。下列所有偏移均直接由生命周期方法函数体确认(Create 写 +120;CollectData 写 +96/+104;Destroy 读 +88/+96/+112)。
| 字段 | 偏移 | 类型 | 含义 |
|---|---|---|---|
xspace | +0x00 | tensorflow::profiler::XSpace (88 B) | proto2 message;仅由 CollectData 惰性构造 |
xspace_constructed | +0x58 | uint8_t | 当且仅当 XSpace::XSpace() 已运行时为 1;控制 Destroy 中的 ~XSpace |
serialized_xspace | +0x60 | std::vector<uint8_t>* | CollectData 生成的 proto 字节的自有堆 vector |
cached_xspace_size | +0x68 | size_t | 第一次 CollectData 得到的 XSpace::ByteSizeLong() |
collection | +0x70 | tsl::profiler::ProfilerCollection* | 自有后端;在 Destroy 中通过 vtable 释放 |
ready | +0x78 | uint8_t | 状态字节(见下文) |
陷阱 —
+0x60处的字段是指向堆上std::vector<uint8_t>的指针(CollectData 通过operator new(0x18u)分配的 24 字节{data,size,capacity}头),不是内联 vector。Destroy 会释放内部data缓冲区,然后释放这个 24 字节头;CollectData 会在安装新的头之前丢弃旧头。将+0x60当作内联 24 字节 vector 的重新实现会使后续所有字段错位。
PLUGIN_Profiler_Error
一个 8 字节的堆对象,恰好包装一个 absl::Status。它是跨 C-ABI 的唯一错误通道:生命周期方法失败时执行 new PLUGIN_Profiler_Error{status};成功时返回 NULL。调用方用 Error_Message/Error_GetCode 检查它,并用 Error_Destroy 释放它。
struct PLUGIN_Profiler_Error { absl::Status status; }; /* 8 bytes */
```text
`absl::Status` 本身是一个带标签指针:bit 0 置位表示内联 OK / 小 code 标记,清零表示堆 `StatusRep`。`Error_GetCode` 读取 `(rep & 1) ? (rep >> 2) : rep->canonical_code`,通过 `absl::status_internal::MapToLocalCode` 映射,并返回一个 `absl::StatusCode`;公共 `PJRT_Error_Code` 枚举在构造上与其镜像。`Error_Message` 返回借用的 `(message, message_size)` 视图,可能指向内联 char 缓冲区、堆 `StatusRep` 载荷,或静态 `absl::Status::kMovedFromString` @ `0xA2E8580`;调用方不得释放它,也不得在对应 `Error_Destroy` 之后继续使用它。
---
## 生命周期状态机
`PLUGIN_Profiler+0x78`(`ready`)处的单字节驱动一次性生命周期。它取两个值,并且值 `0` 在“running”和“stopped”之间**重载**。
```text
Create
│ ready = 1 ("ready, not yet started")
▼
┌──────────────┐ Start (ready==1) ──> collection->Start();
│ ready == 1 │ AddPluginMetadata(); ready = 0
│ (ready) │
└──────┬───────┘
│ Start when ready==0 ──> log "already started", return NULL (no-op)
▼
┌──────────────┐ Stop (ready==0) ──> collection->Stop(); ready = 0 (stays 0)
│ ready == 0 │
│ (run/stop) │ Stop when ready==1 ──> log "already stopped", return NULL (no-op)
└──────┬───────┘
│ CollectData ──> drains+serializes XSpace (one-shot, destructive)
▼
Destroy特性 —
ready == 0同时表示“running”和“stopped”。Start 和 Stop 只能通过前置条件来区分:Start 需要ready != 0才会工作,Stop 需要ready == 0。Stop 会让该字节保持0,因此第二次 Stop 对底层 collection 而言是无操作,而不是状态错误。真相(“Start 是否真的成功”)存在于子 profiler 实现内部,不在这个字节里。将该字节当作干净三态枚举的重新实现会错误处理这种重载。
Create — 0xE6F0C60
function PLUGIN_Profiler_Create(args): // 0xE6F0C60
VLOG(1) "Creating plugin profiler" // plugin_tracer_impl.cc:38
p = operator new(0x80); // 128-byte handle, zeroed
p->ready = 1; // *(BYTE*)(p+120) = 1
ProfileOptions opts; // stack
opts.ParseFromString(args->serialized_options,
args->serialized_options_size); // proto2::MessageLite::ParseFromString
profs = tsl::profiler::CreateProfilers(opts); // 0x1CF50860 — factory walk under mu
p->collection = new ProfilerCollection(move(profs)); // operator new(0x20); ctor 0xF6A15E0
args->profiler = p;
return NULL;
```text
`CreateProfilers` @ `0x1CF50860` 遍历全局工厂注册表,调用每个已注册的 `std::function<unique_ptr<ProfilerInterface>(const ProfileOptions&)>`,并将每个输出包装在 `ProfilerController` 中以进行崩溃隔离。`ProfilerCollection` 是一个 32 字节的 `{vtable, data, size, capacity}` 对象,会内联接管生成的 vector。该层不会读取任何 `ProfileOptions` 字段;解释发生在各个工厂 lambda 内部。
### Start — `0xE6F0FA0`
```c
function PLUGIN_Profiler_Start(args): // 0xE6F0FA0
p = args->profiler;
if (p->ready == 0): // already started/stopped
VLOG(1) "Profiler is already started"; return NULL // no-op, no error
p->cached_xspace_size = 0;
s = p->collection->Start(); // vtable +0x10 -> 0xF6A1640
if (s.IsOK()):
xla::profiler::AddPluginMetadata(); // 0xF3165C0 — stamp build info
p->ready = 0; // mark running
return NULL;
return new PLUGIN_Profiler_Error{s};AddPluginMetadata @ 0xF3165C0 会在活动 session 上、由 tsl::profiler::GetStatTypeStr(0xA6)(166)键控的 StatType 下盖上两个 XStat 条目:BuildData::Changelist() 和 BuildData::Timestamp(),格式为 "%s @ %s"。这会把 libtpu 构建来源直接嵌入生成的 XSpace,因此使用者无需旁路即可读取生产构建。ProfilerCollection::Start @ 0xF6A1640 会对每个子 profiler 调用 vtable+0x10,忽略单个 profiler 的非 OK 状态(最后一个状态胜出)。
Stop — 0xE6F1100
function PLUGIN_Profiler_Stop(args): // 0xE6F1100
p = args->profiler;
if (p->ready == 1): // never started (post-Create)
VLOG(2) "Profiler is already stopped"; return NULL
s = p->collection->Stop(); // vtable +0x18 -> 0xF6A16C0
if (s.IsOK()): p->ready = 0; return NULL; // stays 0
return new PLUGIN_Profiler_Error{s};
```text
### CollectData — `0xE6F1240`
这是最大的函数体(728 字节),也是唯一具有破坏性的函数体。它通过 `buffer_size_in_bytes` 支持两种模式契约:查询模式(调用方已预分配大小)和获取模式(libtpu 分配)。
```c
function PLUGIN_Profiler_CollectData(args): // 0xE6F1240
p = args->profiler;
XSpace xs; XSpace::XSpace(&xs, 0); // stack
if (p->xspace_constructed == 0): // first real collection
s = p->collection->CollectData(&xs); // vtable +0x20 -> 0xF6A1740
if (!s.IsOK()): return new PLUGIN_Profiler_Error{s};
p->cached_xspace_size = xs.ByteSizeLong(); // stored at p+104 (+0x68)
size = xs.ByteSizeLong();
if (args->buffer_size_in_bytes != 0): // query mode: caller has buffer
XSpace::~XSpace(&xs); return NULL; // return size only
vec = new vector<uint8_t>(size + 1, 0); // operator new(0x18) hdr + (size+1) data
free(p->serialized_xspace); // drop previous (p+96 / +0x60)
p->serialized_xspace = vec;
xs.SerializePartialToArray(vec->data, size); // proto2::MessageLite
args->buffer = vec->data; // borrowed
args->buffer_size_in_bytes = size + 1; // note the +1 trailing byte
XSpace::~XSpace(&xs);
return NULL;陷阱 —
CollectData对后端而言是一次性且破坏性的。ProfilerCollection::CollectData(XSpace*)@0xF6A1740会把同一个 XSpace 指针传给每个子 profiler(因此它们会向同一个 space 追加 plane),然后清空其内部 vector;每个unique_ptr<ProfilerInterface>都会被 reset,从而释放子 profiler。因此,对同一句柄第二次调用CollectData会面对一个空 collection:它重新序列化缓存的 XSpace,并返回相同字节。返回的buffer是从句柄内部 vector 借用的,只在下一次CollectData或Destroy之前有效。使用者不得释放它,也不得跨 Destroy 使用它。特性 — 回传的序列化长度是
size + 1,不是size。缓冲区是new vector<uint8_t>(size + 1, 0),也就是在 proto 载荷后多一个零字节。将buffer[:buffer_size_in_bytes]直接 round-trip 到XSpace::ParseFromString的重新实现会吞入尾随0x00;proto2 会将其容忍为零长度 field-0 尾部,但严格解析器可能不会。
Destroy — 0xE6F0E80
function PLUGIN_Profiler_Destroy(args): // 0xE6F0E80
p = args->profiler;
if (!p) return NULL;
if (p->collection): // p+112 (+0x70)
~ProfilerCollection(p->collection); // vtable +0x08 (destroying dtor) -> 0xF6A18E0
p->collection = NULL;
if (p->serialized_xspace): // p+96 (+0x60)
free(p->serialized_xspace->data);
free(p->serialized_xspace); // the 24-byte header
p->serialized_xspace = NULL;
if (p->xspace_constructed == 1): // p+88 (+0x58)
XSpace::~XSpace(&p->xspace);
free(p, 0x80);
return NULL;
```text
---
## 通向跟踪后端的桥接
### `tsl::profiler::ProfilerCollection`
句柄的 `collection`(`+0x70`)是一个 `tsl::profiler::ProfilerCollection`:一个 32 字节对象,其 `.data.rel.ro` vtable @ `0x217738A0` 携带 `{top-offset, RTTI, D2 dtor, D0 dtor, Start, Stop, CollectData}`。五个生命周期方法以固定 vtable 偏移调用它,因此 PJRT 层纯粹是一个编排器。
| 后端操作 | vtable 槽 | 符号(`tsl::profiler::ProfilerCollection::`) | 地址 |
|---|---|---|---|
| ctor(接收 `vector<unique_ptr<ProfilerInterface>>`) | — | `ProfilerCollection(vector)` | `0xF6A15E0` |
| `Start` | `+0x10` | `Start` | `0xF6A1640` |
| `Stop` | `+0x18` | `Stop` | `0xF6A16C0` |
| `CollectData(XSpace*)` | `+0x20` | `CollectData` | `0xF6A1740` |
| dtor(D2 / D0) | `+0x08` | `~ProfilerCollection` | `0xF6A1840` / `0xF6A18E0` |
每个后端操作都会迭代内部 vector,并在每个子 profiler 上调用对应的 `tsl::profiler::ProfilerInterface` vtable 槽。子 profiler 集合取决于 `CreateProfilers` 从工厂注册表中组装出的内容:已确认成员包括 `xprof::tpu::TpuProfilerImpl`(设备侧,与每芯片 xdb 通信)、`xprof::cpu::HostTracer`(factory @ `0xEF34760`,主机 perf/cpu),以及 `tsl::profiler::ThreadpoolProfilerInterface`(vtable @ `0x2175C150`,主机 threadpool 分发)。
### XSpace / TraceMe
实际跟踪数据不会在逐事件层面触及 PJRT 层。主机事件来自 TPU-runtime 线程上的 `tsl::profiler::TraceMe` 宏(`TpuCompile`、`TpuExecute`、队列提交、megascale transport);设备事件来自由 `TpuProfilerImpl::CollectData` 排空的每芯片硬件环形缓冲区。子 profiler 将两者折叠进一个共享的 `tensorflow.profiler.XSpace`:一个主机 XPlane `/host:0`,加上每个可寻址 core 一个设备 XPlane(`/device:TPU:0`,……)。其中的 XEvent/XStat/XEventMetadata 层级和按芯片家族划分的 `TracePointId` 载荷属于 profiling 章节的主题。本页把 XSpace 当作不透明的序列化 blob;字节会通过 `CollectData` 的缓冲区原样离开。
> **注意 —** profiling 会与 `PJRT_LoadedExecutable_Execute` *并发*运行。闸门是 `xprof::tpu::TpuProfilerControlListener` 单例(`GetOrCreateTpuProfilerControlListener` @ `0xF332800`):每个芯片驱动在打开其 trace 环形缓冲区前查询 `CanStartProfiler(chip_loc, profiler*, run_id)` @ `0xF3328C0`,并轮询 `MustStopProfiler(chip_loc)` @ `0xF332A00` 以在运行中停止。PJRT 侧并不直接知道哪些芯片参与。该 listener 和每芯片排空过程不在本文范围内;这里只通过符号引用。
### 线程
- **单句柄:非线程安全。** 单个句柄上的 `Create/Start/Stop/CollectData/Destroy` 没有互斥锁保护。并发调用会竞争状态字节(`+0x78`)、vector 指针(`+0x60`)以及 collection 的内部列表。`CollectData <-> Destroy` 是 use-after-free;`Start <-> Stop` 会竞争重载的状态字节。
- **跨句柄:安全。** 不同句柄在该层不共享可变状态。唯一共享资源是工厂注册表,由 `tsl::profiler::(anonymous namespace)::mu`(一个 `absl::Mutex`)序列化;该锁只在 Create 内部的 `CreateProfilers` 工厂枚举期间持有,并在工厂函数体运行前*释放*,因此工厂可以重入其他 libtpu API,但不得注册新的工厂。该互斥锁是非递归的:Create 不得从工厂或 vtable 回调内部调用。
---
## 与旧版 `TpuProfiler_*` ABI 的关系
libtpu 通过第二套更旧的 C-ABI 暴露同一后端:五个导出符号 `TpuProfiler_Create` @ `0xEF33BC0`、`TpuProfiler_Destroy` @ `0xEF33DE0`、`TpuProfiler_Start` @ `0xEF33EA0`、`TpuProfiler_Stop` @ `0xEF34080`、`TpuProfiler_CollectData` @ `0xEF34240`。这些是 `tensorflow/core/tpu` 接口,经由 `stream_executor::tpu::ProfilerApiFn()` @ `0x10900EA0` 到达,并由 TF/TPUEstimator 使用。它们**没有**扩展包装器,也**没有**错误辅助函数;失败会写入调用方提供的 `TF_Status*`,而不是作为 `PLUGIN_Profiler_Error` 返回。
两个 ABI 生成相同的底层 profiler,因为 TF 的 profiler service 通过 `tsl::profiler::RegisterProfilerFactory` @ `0x1CF50780` 将自身注册到 `CreateProfilers` 遍历的同一个全局注册表中。旧版句柄是 120 字节(对比 PJRT 句柄的 128 字节):它省略了 `+0x78` 处尾随状态字节 padding,将其 collection 放在 `+0x68`,序列化 vector 放在 `+0x60`。对旧版五元组逐字节审计超出了本页范围;8 字节大小差异和共享的 `ProfilerCollection*` 已确认,确切旧版偏移是来自 Create/CollectData 路径的高置信推断。
---
## 本文未涵盖内容
- **XSpace/XPlane 载荷 schema 和按芯片家族划分的 `TracePointId` 枚举** — 由 profiling 章节负责;这里 XSpace 是不透明的序列化 blob。
- **注册到全局注册表中的完整工厂清单** — 已确认成员为 `HostTracer`、`TpuProfilerImpl`、`ThreadpoolProfilerInterface`;完整集合在多个 `_GLOBAL__sub_I_*profiler*.cc` 静态初始化块中惰性填充,未穷尽枚举(完整性为低置信)。
- **`TpuProfilerControlListener` vtable 顺序和逐运行 gating 语义** — 方法已在所引地址确认;未提取 abstract-base 槽顺序。
- **`AddPluginMetadata` 的确切 StatType 字符串** — 已确认数值 `0xA6`(166);interned 名称需要遍历 `GetStatTypeStrMap`,未解析(低置信)。
---
## 交叉引用
- [扩展链](extension-chain.md) — `PJRT_Extension_Base` 头部、类型 id 遍历,以及 Profiler 作为 `.data` 种子 / 类型 1 终止节点的角色
- [PJRT 插件概览](overview.md) — `dlsym("GetPjrtApi")` 如何到达 `GetTpuPjrtApi`,以及修补 Profiler 的 `next` 的一次性初始化
- [API Vtable 重建](api-vtable-reconstruction.md) — `extension_start` 挂载其上的 140 个主表槽(独立结构)
- [剩余扩展](ext-remaining.md) — 其他将方法内联携带的扩展(与 Profiler 的指向 vtable 的指针间接形成对比)
- [拓扑描述扩展](ext-topology-description.md) — 类型 16,最大的 live extension,用于比较节点主体约定