Skip to content

PJRT Profiler 扩展(类型 1)

本页所有地址和偏移均适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d),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_Api vtable:固定的 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
方法 vtablepjrt::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 初始化器。

c
/* 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 个函数指针。槽位顺序由规范头文件固定:先是三个错误辅助函数,然后是五个生命周期方法。

c
/* 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+0x00tensorflow::profiler::XSpace (88 B)proto2 message;仅由 CollectData 惰性构造
xspace_constructed+0x58uint8_t当且仅当 XSpace::XSpace() 已运行时为 1;控制 Destroy 中的 ~XSpace
serialized_xspace+0x60std::vector<uint8_t>*CollectData 生成的 proto 字节的自有堆 vector
cached_xspace_size+0x68size_t第一次 CollectData 得到的 XSpace::ByteSizeLong()
collection+0x70tsl::profiler::ProfilerCollection*自有后端;在 Destroy 中通过 vtable 释放
ready+0x78uint8_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 释放它。

c
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 == 1AddPluginMetadata(); 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

c
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

c
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 借用的,只在下一次 CollectDataDestroy 之前有效。使用者不得释放它,也不得跨 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

c
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,用于比较节点主体约定