Skip to content

XPlane / XStat / TraceMe 发射

本页中的所有地址和偏移都适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d)。其他版本会有所不同。

摘要

libtpu 携带了一份完整的、位于二进制内部的 TensorFlow/TSL profiler 发射栈副本,不只是 XSpace/XPlane/XLine/XEvent/XStat proto2 消息类,还包括库中每个生产者用来填充它们的 builder 层(tsl::profiler::XPlaneBuilderXLineBuilderXEventBuilderXStatBuilder)。从硬件环形缓冲区解码出的设备 trace-entry,以及在运行时线程上打开的主机 TraceMe 作用域,最终都会落到写入同一个 XSpace同一组 builder 调用。本页负责说明这个对象模型和 builder API:四层事件树、每个 plane 的两个内建 metadata 字典、GetOrCreateLine/AddEvent/AddStat 构造界面,以及生成 /host:* line 的主机侧 TraceMe/TraceMeEncode 插桩。

这个模型可以映射到任何 LLVM/XLA 工程师已经熟悉的框架。可以把 XSpace 看作 profiling module,每个 XPlane 看作一个带有私有符号表的 function(一个设备核心或一个主机线程组),每个 XLine 看作一个 basic-block timeline,每个 XEvent 看作一条带持续时间的 instruction,每个 XStat 看作一个 operand annotation。两个符号表 XEventMetadataXStatMetadata 都是内建化的:event 只存储一个 64 位 metadata_id 外键,而人类可读名称、类别和源位置在每个 plane 中只保存一次。这是最重要的结构事实:生产者和消费者必须就 id↔name 字典达成一致,因此 id 枚举被拆到各自页面(XEvent Metadata IDsXStat Metadata IDs),并且不会在这里重复。

两种 builder 方言并存。通用的 tsl::profiler builder(XPlaneBuilder::GetOrCreateLine @ 0x1cf4d9a0XLineBuilder::AddEvent @ 0x1cf4dc40)是与上游一致的核心。libtpu 用 TPU 方言(xprof::TpuXPlaneBuilderxprof::TpuXLineBuilder::AddEvent @ 0xf1df1e0)包装它们,加入设备语义 line key(TpuComponentIciPortGtcSpan)和 HLO 感知 metadata。设备路径(trace-entry → TpuXLineBuilder::AddEvent)由 TraceEntry → XEvent/XStat 负责;本页记录该方言所依托的通用 builder,以及折叠进同一个 XSpace 的、完全独立的主机 TraceMe 路径。

对于重新实现,本页重建的契约是:

  • 四层对象模型XSpaceXPlaneXLineXEventXStat,每个 XPlane 有两个内建 metadata map,以及每一层所需的 proto2 字段布局。
  • builder APIGetOrCreateLineGetOrCreate{Event,Stat}Metadata(按 id 和按 name)、XLineBuilder::AddEvent,以及 XEventBuilder/XStatBuilder mutator,包括它们写入的精确 proto 偏移。
  • metadata 内建协议proto2::Map<int64, XEventMetadata> / Map<int64, XStatMetadata>、带 SOO 缓存的 int64→XLine* line map,以及为什么 id 是按 plane 分配的。
  • 主机 TraceMe 路径 — thread-local 无锁事件捕获(TraceMeRecorder)、name#k1=v1,k2=v2# TraceMeEncode 线格式、(thread<<32)|counter activity-id 方案,以及 ConvertCompleteEventsToXPlane host-line 桥。
对象模型tensorflow.profiler.XSpaceXPlaneXLineXEventXStat (proto2)
通用 buildertsl::profiler::{XPlaneBuilder, XLineBuilder, XEventBuilder, XStatBuilder}
GetOrCreateLine(int64)XPlaneBuilder::GetOrCreateLine @ 0x1cf4d9a0
AddEvent(XEventMetadata&)XLineBuilder::AddEvent @ 0x1cf4dc40
Event-metadata 内建(按 id)XPlaneBuilder::GetOrCreateEventMetadata(int64) @ 0x1cf4cfe0
Stat-metadata 内建(按 id)XPlaneBuilder::GetOrCreateStatMetadata(int64) @ 0x1cf4d500
Event-metadata 内建(按 name)GetOrCreateEventMetadata(string_view) @ 0x1cf4d0c0
TPU 设备方言xprof::TpuXLineBuilder::AddEvent(GtcSpan, XEventMetadata&) @ 0xf1df1e0
主机捕获tsl::profiler::TraceMeRecorder::Record @ 0x207ff580
主机编码tensorflow::profiler::TraceMeEncode @ 0x10885f20
主机 → XPlane 桥ConvertCompleteEventsToXPlane @ 0xf32ff00

XSpace 对象模型

目的

XSpace 是一次 profiling session 的根容器,也是通过 PJRT Profiler extension 离开 libtpu 的唯一对象,即序列化后的 XSpace blob。它下面的一切都是生产者在序列化前构建出的结构。该模型是与上游一致的 proto2;libtpu 的贡献是填充它的 TPU 专用 event/stat id,而不是 schema。

层级布局

text
XSpace
 ├─ repeated XPlane planes          // one per device core + one per host thread-group
 ├─ repeated string errors
 ├─ repeated string warnings
 └─ repeated string hostnames

XPlane
 ├─ int64  id                       // plane id (e.g. device:TPU:0, host:0)
 ├─ string name                     // "/device:TPU:0", "/host:0", …
 ├─ repeated XLine                 lines
 ├─ map<int64, XEventMetadata>     event_metadata    // interned event dictionary
 ├─ map<int64, XStatMetadata>      stat_metadata     // interned stat dictionary
 └─ map<int64, string>            (string interning, optional)

XLine
 ├─ int64  id                       // timeline id within the plane
 ├─ int64  display_id
 ├─ string name / display_name
 ├─ int64  timestamp_ns             // line origin (absolute wall clock)
 ├─ int64  duration_ps              // line extent
 └─ repeated XEvent events

XEvent
 ├─ int64  metadata_id              // FK → XPlane.event_metadata
 ├─ int64  offset_ps                // start, relative to XLine.timestamp_ns
 ├─ int64  duration_ps
 ├─ int64  num_occurrences
 └─ repeated XStat stats

XStat
 ├─ int64  metadata_id              // FK → XPlane.stat_metadata
 └─ oneof  value { int64 | uint64 | double | bytes str_value | int64 ref_value }
```text

> **注意 —** 时间存在两种尺度。`XLine.timestamp_ns` 是**纳秒**级的绝对 wall-clock 起点;`XEvent.offset_ps`/`duration_ps` 是相对该起点的**皮秒**。设备 cycle counter 两者都不是,它在到达 `XEvent` 之前,会由 `TpuXLineBuilder` 使用每代硬件的 clock period 转换成 ps offset。混淆这些尺度是典型的计时错误;见 [profiling overview](overview.md) 中的 timestamp 说明。

### 为什么要内建

每个 `XPlane` 上的两个 `map<int64, *Metadata>` 字段,是 `XEvent`/`XStat` 查入的字典。重复一万次的 `TpuExecute` event 会存储一万个 8 字节 `metadata_id` 值,以及*一个* `XEventMetadata{name:"TpuExecute", …}`。内建是按 plane 进行的,而不是全局的:`/device:TPU:0` 和 `/device:TPU:1` 上的同一个逻辑 event,会在各自 plane 的 map 中获得新分配的 id。因此消费者必须读取 `XPlane.event_metadata` 才能解析任何 `XEvent.metadata_id`,不存在跨 plane 的 id 命名空间。生产者和消费者共享的 id 目录见 [XEvent Metadata IDs](xevent-metadata-ids.md) 和 [XStat Metadata IDs](xstat-metadata-ids.md)。

---

## XPlaneBuilder API

### 目的

`XPlaneBuilder` 是单个 `XPlane` 上的 mutation handle。它有三项职责:按 id 交出(或创建)`XLine`,按 id 或 name 内建 `XEventMetadata`/`XStatMetadata`,并缓存查找结果,让热解码循环中的重复调用不用重新扫描 proto map。二进制内部实例由所列地址处的 `tsl::profiler::XPlaneBuilder` 符号确认,即该模型在 libtpu 中被*实例化*,而不只是声明。

### 对象布局

从反编译出的 builder 看,`XPlaneBuilder` 持有后端 `XPlane*` 和三个侧向缓存:

| 字段 | 偏移(qword index) | 类型 | 含义 |
|---|---|---|---|
| plane backing | `+0x10` (`*((q*)this+2)`) | `XPlane*` | 正在构建的 proto;所有 `RepeatedPtrFieldBase::Add` 目标都从这里解析 |
| line map | `+0x68` (`this+104`) | `absl flat_hash_map<int64, XLine*>` (SOO) | id → line 缓存;≤1 个条目时启用小对象优化 |
| SOO single-slot key | `+0x70`/`+0x78` (`this+14/15`) | `int64` | map 增长越过 SOO 前的内联 last-line id/ptr |
| event-metadata cache | `+0x80` (`this+16`) | hash-set storage | metadata 查找的后端 |

> **怪癖 —** `GetOrCreateLine` (@ `0x1cf4d9a0`) 不会直接进入 hash map。第一条 line 会把 id 内联存进两个 builder qword(`this+14`、`this+15`),由 thread-local fast-path counter(`__tls_get_addr(&qword_22048D78)`)保护;只有第二个不同 line id 出现时才会调用 `GrowSooTableToNextCapacityAndPrepareInsert`。预先为每个 plane 分配完整 `flat_hash_map` 的重新实现能匹配*语义*,但不匹配布局;如果它试图与二进制偏移交叉检查,就会读错偏移。

### `GetOrCreateLine(int64 id)` — @ `0x1cf4d9a0`

```c
function GetOrCreateLine(builder, id):                  // 0x1cf4d9a0
    // Fast path: SOO single-slot or grown flat_hash_map keyed by id
    if line_map_size > 1:
        slot = find_or_prepare_insert_large(line_map, id)   // absl raw_hash_set
    else:
        // inline single-entry cache in builder+14/+15, gated by a
        // thread-local fast-path counter to avoid hashing the common case
        slot = soo_single_slot(builder, id)
    if slot.value != null:                              // already created
        return slot.value
    // Miss: append a fresh XLine to XPlane.lines and back-fill id
    plane = builder->plane                              // *(builder+2)
    xline = RepeatedPtrFieldBase::Add<XLine>(plane+24)  // proto repeated field
    plane->_has_bits |= 1                               // +0x10 |= 1
    slot.value      = xline
    *(xline + 56)   = id                                // XLine.id  @ +0x38
    xline->_has_bits |= 8                               // mark id present
    return xline

契约:GetOrCreateLine 在一个 plane 内对 id 是幂等的,返回稳定的 XLine*,并且只在首次请求时才惰性追加到 XPlane.lines。TPU 方言中的 TpuXPlaneBuilder::GetOrCreateLine(TpuComponent) @ 0xf1df120GetOrCreateLine(IciPort) @ 0xf25af20 是轻量包装:把设备语义 key 映射到 int64 line id,然后调用这里。

Metadata 内建 — 按 id

GetOrCreateEventMetadata(int64) @ 0x1cf4cfe0GetOrCreateStatMetadata(int64) @ 0x1cf4d500 是内建原语。两者形状相同,只有目标 map 和回填字段偏移不同:

c
function GetOrCreateEventMetadata(builder, id):         // 0x1cf4cfe0
    plane = builder->plane                              // *(builder+2)
    plane->_has_bits |= 0x10                             // mark event_metadata present
    SyncMapWithRepeatedField(plane->event_metadata)      // proto2 map<->repeated sync
    md = Map<int64, XEventMetadata>::TryEmplace(         // insert-or-find by id
             plane->event_metadata_map, id)
    md->id = id                                          // XEventMetadata.id @ +0x68
    md->_has_bits |= 0x20
    return md                                            // caller fills name/category

function GetOrCreateStatMetadata(builder, id):          // 0x1cf4d500
    plane->_has_bits |= 0x20                             // mark stat_metadata present
    SyncMapWithRepeatedField(plane->stat_metadata)
    md = Map<int64, XStatMetadata>::TryEmplace(plane->stat_metadata_map, id)
    md->id = id                                          // XStatMetadata.id @ +0x38
    md->_has_bits |= 4
    return md
```text

> **注意 —** 回填偏移不同,因为两个 metadata 消息布局不同:`XEventMetadata.id` 落在 proto offset `+0x68`(该消息还带有 `name`、`display_name`、`metadata` bytes 和 child stats),而 `XStatMetadata.id` 落在 `+0x38`(只带有 `id`、`name`、`description`)。两个函数都返回*指向 proto map node 内部的指针*,因此调用方会原地修改内建消息,不存在 copy-back 步骤。

### Metadata 内建 — 按 name

按 name 的重载(`GetOrCreateEventMetadata(string_view)` @ `0x1cf4d0c0`,以及位于 `0x1cf4d380`/`0x1cf4d0c0` 的 `string`/`&&` 变体;stat 对应项在 `0x1cf4d5e0`/`0x1cf4d8a0`)维护*第二个*索引:name→id hash,使两个使用同一 event 名称的生产者得到同一个内建条目。名称是新的时,它们分配新 id,通过上面的 id 路径创建 metadata,并设置其 `name` 字段。只读同伴 `GetEventMetadata(string_view)` @ `0x1cf4d480` 和 `GetStatMetadata(*)` @ `0x1cf4d4c0`/`0x1cf4d560` 只查找不创建,供必须找到已有 id 的消费者使用。

| Builder method | Key | Address | Creates? |
|---|---|---|---|
| `GetOrCreateEventMetadata(int64)` | id | `0x1cf4cfe0` | yes |
| `GetOrCreateEventMetadata(string_view)` | name | `0x1cf4d0c0` | yes |
| `GetOrCreateEventMetadata(string&&)` | name | `0x1cf4d380` | yes |
| `GetEventMetadata(string_view)` | name | `0x1cf4d480` | no (lookup) |
| `GetOrCreateStatMetadata(int64)` | id | `0x1cf4d500` | yes |
| `GetOrCreateStatMetadata(string_view)` | name | `0x1cf4d5e0` | yes |
| `GetOrCreateStatMetadata(string&&)` | name | `0x1cf4d8a0` | yes |
| `GetStatMetadata(int64)` | id | `0x1cf4d560` | no (lookup) |
| `GetStatMetadata(string_view)` | name | `0x1cf4d4c0` | no (lookup) |
| `CreateEventMetadata()` || `0x1cf4d040` | yes (no key) |
| `GetOrCreateCounterLine()` || `0x1cf4db20` | yes |

---

## XLineBuilder / XEventBuilder API

### 目的

`XLineBuilder` 是一个 `XLine` 上的 mutation handle;它唯一的热方法 `AddEvent` 会追加一个由已内建 `XEventMetadata` 标识的 `XEvent`,并返回用于填充 event timing 和 stats 的 `XEventBuilder`。`XStatBuilder` 类似地填充一个 `XStat` 的值。

### `XLineBuilder::AddEvent(const XEventMetadata&)` — @ `0x1cf4dc40`

```c
function XLineBuilder::AddEvent(line_builder, md):      // 0x1cf4dc40
    xline = line_builder->line                          // *(md) → line backing via builder
    arena = xline->arena                                // *(line+8), low-bit tagged
    xevent = RepeatedPtrFieldBase::Add<XEvent>(xline+24) // append to XLine.events
    xline->_has_bits |= 1
    xevent->metadata_id = md->id                        // *(xevent+40) = *(md+88)  → XEvent.metadata_id @ +0x28
    xevent->_has_bits |= 2
    // returns an XEventBuilder { line_builder copy, xevent* } in `this`
    return XEventBuilder{ line=line_builder.line, event=xevent }

该函数密集但明确:它在线的 repeated field 上分配一个 XEvent,从内建的 XEventMetadata 复制 metadata id(*(md+88)+0x28 处的 XEvent.metadata_id),并返回一个 XEventBuilder 值(尾部的 vpermilps/vmovups 把 line backing 和新 event 指针打包进返回的 builder)。随后调用方在返回的 builder 上设置 timing 和 stats。

陷阱 — AddEvent 接收的是 XEventMetadata&不是 id 整数。metadata 在 AddEvent必须已经在这个 plane 中内建过(通过父 XPlaneBuilder 上的 GetOrCreate*Metadata 调用);该函数会读取 *(md+88) 来复制 id,自身不做任何内建。用属于另一个 plane 的 metadata 对象调用 AddEvent 会复制外来 id,并悄悄把 event 错误地键入。

XEventBuilder mutator

AddEvent 之后,XEventBuilder 携带 {line*, event*},并暴露直接写入 XEvent proto 的内联 setter:

Setter(逻辑)写入XEvent 偏移
SetOffsetPs / SetTimestampNs(line_ns)offset_ps(相对 line 起点)+0x18
SetDurationPs / SetEndTimestampPsduration_ps+0x20
SetNumOccurrencesnum_occurrences+0x30
AddStatValue(XStatMetadata&, value)XEvent.stats 追加 XStatrepeated +0x38

AddStatValueXEvent 层面的 AddEvent 类比物:它向 event 的 repeated stats 字段追加一个 XStat,从传入的(已内建的)XStatMetadata 设置 XStat.metadata_id,并把值写入 oneof。proto schema 中观察到的值变体是 int64/uint64/double/bytes (str_value)/int64 (ref_value)ref_value 引用 plane 字符串表中的内建字符串,用于大型或重复字符串 stat。

TPU 设备方言

设备路径不会直接调用通用 AddEvent;它调用 xprof::TpuXLineBuilder::AddEvent(GtcSpan, const XEventMetadata&) @ 0xf1df1e0,该函数在 GtcSpan(全局时间计数器 start/end 对)中携带设备 timing,并委托给通用 builder。模板实例化 TpuXLineBuilder::AddEvent<…TraceEntry…>(每个芯片家族一个:pxc @ 0xf1f26e0、vfc、vlc、glc、gfc、jxc)是已解码 TraceEntry 变成 XEvent 的精确汇合点,每一个都是下方主机桥的设备对应物。这些转换(cycle→ps 转换、哪些 scalar 变成哪些 XStat)由 TraceEntry → XEvent/XStat 负责;为它们供给数据的 codec 见 TraceEntriesCoder


设备 Trace-Entry 如何变成 XEvent

五阶段捕获流水线(概览)最终落到这个对象模型。把两者缝合起来,一个已解码的 TraceEntry 会按如下方式落到设备 XLine 上:

text
TraceEntry (decoded proto, oneof variant = the event)

   ├─ trace_point_id  ──▶ map to enum name string
   │        │
   │        └─▶ XPlaneBuilder::GetOrCreateEventMetadata(name)   // intern → metadata_id

   ├─ TpuXPlaneBuilder::GetOrCreateLine(TpuComponent/IciPort)   // 0xf1df120 / 0xf25af20
   │        └─▶ XPlaneBuilder::GetOrCreateLine(int64)           // 0x1cf4d9a0

   ├─ TpuXLineBuilder::AddEvent(GtcSpan, XEventMetadata&)       // 0xf1df1e0
   │        └─▶ XLineBuilder::AddEvent(XEventMetadata&)         // 0x1cf4dc40 → XEvent

   ├─ timestamp (device cycle counter) ──▶ ÷ clock period ──▶ XEvent.offset_ps / duration_ps

   └─ variant scalars (transaction_id, core_id, byte counts, …)
            └─▶ GetOrCreateStatMetadata(name) + AddStatValue(...) ──▶ XStat[]
```text

重新实现者必须内化的关键观察是:设备 `trace_point_id`(一个分段的硬件 enum,有空洞且特定于家族)**从不**存进 `XEvent`。它在第 5 阶段被翻译成*名称字符串*,该名称通过 `GetOrCreateEventMetadata` 内建,而 `XEvent.metadata_id` 是*plane-local* 的内建索引,是完全不同的数字。完整翻译表见 [TraceEntry → XEvent/XStat](trace-entry-to-xevent.md)。

---

## 主机 TraceMe 路径

### 目的

主机 event,包括 `TpuCompile`、`TpuExecute`、队列提交、megascale transport、allocator 活动,不来自硬件环形缓冲区。它们来自在 TPU-runtime 线程上打开的 `tsl::profiler::TraceMe` RAII 作用域。`TraceMe` 子系统按线程无锁捕获这些作用域,然后转换器把捕获到的 scope 折叠进同一个 `XSpace` 的 `/host:*` `XLine`,设备路径也写入这个 `XSpace`。这是 [profiling overview](overview.md) 中所称“两种来源,一个 `XSpace`”的另一半。

### 捕获 — `TraceMeRecorder`

`TraceMe` 作用域在构造时记录 start event,在析构时记录 end event,二者都通过 `TraceMeRecorder::Record` @ `0x207ff580`。`Record` 被刻意设计为无锁:它写入**thread-local** 的分块 event queue(`PerThread<ThreadLocalRecorder>`),因此生产主机 event 永远不会争用全局锁。

```c
function TraceMeRecorder::Record(event):                // 0x207ff580
    tls = PerThread<ThreadLocalRecorder>::Get()         // lazily inited per thread
    queue = tls->active_chunk                            // *(tls+48)
    n     = tls->count                                   // *(tls+56)
    slot  = queue + 16 + 40 * (n - queue->base)          // 40-byte Event record
    *slot = move(event)                                  // {activity_id, name, ts, …}
    if (n + 1) - queue->base == 1638:                    // chunk full (1638 events)
        next = new chunk(0x10000)                        // 64 KiB, zeroed
        next->base = n + 1
        queue->next = next
        tls->active_chunk = next                         // append-only linked chunks
    tls->count = n + 1

怪癖 — chunk 在链接新的 64 KiB chunk 前恰好容纳 1638 个 event。这是 (0x10000 − 16) / 40 ≈ 1638:chunk 是 65536 字节块,16 字节 header(base index + next pointer),其余是 40 字节 Event record 的扁平数组。按 event 数量而不是固定 64 KiB block 来确定 chunk 大小的重新实现,会在边界处偏离,并错误拼接 Consume 依赖的 start/end 配对。

Activity id — NewActivityId

Start 和 end event 通过 TraceMeRecorder::NewActivityId @ 0x207ff7a0 生成的 64 位 activity id 配对:

c
function NewActivityId():                               // 0x207ff7a0
    if !tls.thread_id_set:                               // first call this thread
        tls.thread_id = atomic_fetch_add(&thread_counter, 1)   // global thread index
        tls.thread_id_set = true
    id = (uint64(tls.thread_id) << 32) | tls.local_counter      // pack
    tls.local_counter += 1
    return id
```text

该 id 将全局唯一的 32 位 thread index 打包到高半部,并将每线程单调 counter 放在低半部:`(thread_index << 32) | per_thread_seq`。这保证了全局唯一性,同时热路径上没有任何跨线程同步;唯一的 atomic 是*每线程一次*的 `thread_counter` 递增。消费者使用这个 id 将 `TraceMeProducer` 作用域与它的 `TraceMeConsumer` 配对(可在许多 `TraceMe::AppendMetadata` 实例化中看到 `ContextType`/flow-event 关联)。

### 编码 — `TraceMeEncode`

名称携带 key/value metadata 的 `TraceMe` 使用 `tensorflow::profiler::TraceMeEncode` @ `0x10885f20` 将参数序列化进*event name string 本身*,采用规范 TraceMe metadata 线格式:

```text
name#key1=value1,key2=value2,…#

TraceMeEncode(name, {TraceMeArg…}) 构建一个字符串:基础 name,然后是 #,再是逗号分隔的 key=value 对,最后是闭合 #。反编译显示它会对 name 执行 strlen,把输出缓冲区大小设为 name_len + Σ(key_len + value_len) + (2·argc + 1)#,=# 标点),若超过 22 字节 SSO buffer 则堆分配,并追加每个 TraceMeArg。数值参数会内联转成字符串。消费者侧把 #…# 后缀解析回 XStat,因此 TraceMe 参数会变成主机 XEvent 上的 XStat,这正好镜像了设备 variant scalar 变成 XStat 的方式。

注意 — TraceMeEncode 是主机 event 的唯一 metadata 通道:捕获时没有独立的 stat-id 字段。key/value 对在转换前都携带在 name string 内;转换时 parser 将每个 key 内建为 XStatMetadata,并将每个 value 作为 XStat 值。这就是为什么主机 stat name 是动态字符串(按 name 内建),而设备 stat id 是固定 enum(按 id 内建):两半通过同一个 GetOrCreateStatMetadata 的不同重载进行内建。

主机 → XPlane 桥 — ConvertCompleteEventsToXPlane

采集时,主机 sub-profiler(HostTracer)调用 TraceMeRecorder::Consume @ 0x207fe700,把每个线程的 chunk queue drain 到 ThreadEvents vector 中;随后 tsl::profiler::ConvertCompleteEventsToXPlane @ 0xf32ff00 将它们折叠进一个 XPlane/host:0)。这个转换器是设备 TpuXLineBuilder::AddEvent 路径的主机类比物:反编译显示它遍历捕获到的 event,对每个 event name 调用 GetOrCreateEventMetadata,调用 RepeatedPtrFieldBase::Add<XEvent> 追加,并对每个解码出的 metadata pair 调用 GetOrCreateStatMetadata + 追加 XStat,也就是上文记录的同一组 builder 原语,只是由主机捕获而不是设备 codec 驱动。

text
TraceMeRecorder (per-thread lock-free chunks)
   │  Stop() → Consume() @ 0x207fe700  → vector<ThreadEvents>

ConvertCompleteEventsToXPlane @ 0xf32ff00
   │  per event:
   │    name        ──▶ GetOrCreateEventMetadata(name)  → XEvent.metadata_id
   │    start/end   ──▶ XEvent.offset_ps / duration_ps  (line origin = capture start)
   │    "#k=v#" args──▶ GetOrCreateStatMetadata(k) + AddStatValue(v) → XStat[]
   │    one XLine per producing thread (line id = thread id)

   /host:0 XPlane  ── merged into the same XSpace as the device planes
```text

结果是:主机 `TraceMe` 作用域和设备硬件 trace-entry 会到达同一个 `XSpace` 的不同 plane(`/host:N` vs `/device:TPU:N`),由相同的 `XPlaneBuilder`/`XLineBuilder` API 构建,仅通过各自的 metadata 字典和 line-key 约定区分。

---

## Builder 函数映射

| Function | Address | Role |
|---|---|---|
| `XPlaneBuilder::GetOrCreateLine(int64)` | `0x1cf4d9a0` | id → `XLine`,SOO 缓存,惰性追加 |
| `XPlaneBuilder::GetOrCreateEventMetadata(int64)` | `0x1cf4cfe0` | 按 id 内建 event metadata |
| `XPlaneBuilder::GetOrCreateEventMetadata(string_view)` | `0x1cf4d0c0` | 按 name 内建 event metadata |
| `XPlaneBuilder::GetOrCreateStatMetadata(int64)` | `0x1cf4d500` | 按 id 内建 stat metadata |
| `XPlaneBuilder::GetOrCreateStatMetadata(string_view)` | `0x1cf4d5e0` | 按 name 内建 stat metadata |
| `XPlaneBuilder::CreateEventMetadata()` | `0x1cf4d040` | 分配无 key metadata |
| `XPlaneBuilder::GetOrCreateCounterLine()` | `0x1cf4db20` | 专用 counter timeline |
| `XLineBuilder::AddEvent(const XEventMetadata&)` | `0x1cf4dc40` | 追加 `XEvent`,复制 metadata id |
| `TpuXLineBuilder::AddEvent(GtcSpan, XEventMetadata&)` | `0xf1df1e0` | 设备方言包装 |
| `TpuXPlaneBuilder::GetOrCreateLine(TpuComponent)` | `0xf1df120` | 设备语义 line key |
| `TpuXPlaneBuilder::GetOrCreateLine(IciPort)` | `0xf25af20` | ICI-port line key |
| `TraceMeRecorder::Record(Event&&)` | `0x207ff580` | 无锁按线程捕获 |
| `TraceMeRecorder::NewActivityId()` | `0x207ff7a0` | `(thread<<32)\|counter` id |
| `TraceMeRecorder::Consume()` | `0x207fe700` | drain 按线程 chunk |
| `TraceMeEncode(name, args)` | `0x10885f20` | `name#k=v,…#` 线格式 |
| `ConvertCompleteEventsToXPlane(...)` | `0xf32ff00` | host events → `/host` XPlane |

---

## 相关组件

| Component | Relationship |
|---|---|
| `tsl::profiler::ProfilerCollection` | 拥有驱动这些 builder 的主机(`HostTracer`)和设备(`TpuProfilerImpl`)sub-profiler |
| `xprof::TpuXLineBuilder` (templated) | 设备侧 `AddEvent<TraceEntry>` 汇合点;`ConvertCompleteEventsToXPlane` 的设备对应物 |
| PJRT Profiler extension | 将完成的 `XSpace` 序列化出库 |

---

## 交叉引用

- [Profiling and Telemetry Overview](overview.md) — 五阶段流水线,以及本页 builder 构造的双来源(`/host` + `/device`)`XSpace`
- [TraceEntry → XEvent/XStat](trace-entry-to-xevent.md) — 调用 `TpuXLineBuilder::AddEvent` 的设备侧翻译,以及 cycle→ps 转换
- [TraceEntriesCoder](trace-entries-coder.md) — 生成本模型消费的 `TraceEntry` proto 的设备 codec
- [XEvent Metadata IDs](xevent-metadata-ids.md) — `GetOrCreateEventMetadata` 查入的内建 event-id 字典
- [XStat Metadata IDs](xstat-metadata-ids.md) — `GetOrCreateStatMetadata` 查入的内建 stat-id 字典
- [Task Proto](task-proto.md) — 框定一次 profiling run 的外围 task/session descriptor
- [PJRT Profiler Extension](../pjrt/ext-profiler.md) — 将组装好的 `XSpace` 序列化给 client 的 C-ABI