Skip to content

riegeli 跟踪容器

本页中的所有地址和偏移都适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(build-id 89edbbe81c5b328a958fe628a9f2207d)。该二进制文件剥离符号 —— 完整 C++ 符号存在,且 .text VMA 等于文件偏移。其他版本会有所不同。

摘要

riegeli 跟踪容器是包装设备端 profiler 编解码器的传输与时间基准层。TraceEntriesCoder 将一个固定 16 字节包解码为 TraceEntry proto,而这一层回答围绕该编解码器的三个问题:压缩字节如何从设备传出、由哪个逐芯片编解码器遍历它们,以及每个包上的原始硬件 tick 如何变成 XEvent 上的墙钟皮秒偏移。它是 device-trace pipeline 的第 2 阶段(传输)和第 5 阶段(时间基准);编解码器是第 3 阶段。

该容器有意保持很薄。线上对象是 proto2 RepeatedPtrField<std::string> —— 每个元素对应一次逐核硬件 trace ring drain —— 且每个元素都是独立的 zlib 流,不是 riegeli 记录分帧的块文件。路径中唯一的 riegeli 对象是包装每个完整缓冲区的 riegeli::ZlibReader<riegeli::StringReader>;riegeli 名称带来的是池化的 z_stream 以及 Reader/Object 生命周期,仅此而已。膨胀一个缓冲区会得到固定 16 字节包的扁平拼接 —— 膨胀后的流内部没有逐包分帧,因此遍历只是裸的 for (cursor += 16) 循环,并在第一个 valid 分帧位读为 0 的包处停止。这是最重要的结构事实:如果重新实现去膨胀后的字节中寻找 riegeli 块头,将永远找不到。

编解码器选择是两级工厂。公开的 xprof::tpu::GetTraceCodec(DeviceIdentifiers, int) @ 0xf5a2900 运行一条有序的 Is<Family> 谓词链 —— 每个谓词都是将 12 字节 PCI 标识 POD 与十七个内置 PCI 元组常量之一直接比较(只有一个 —— kPuffyliteChipIdentifiers @ 0xbdf3c4c —— 带有 ELF 符号;其他十六个,包括两个 Ghostfish/gfc 元组,在符号表中未命名,分别通过内联比较它的 Is<Family> 访问器识别,例如 IsGfc)—— 然后分派到匹配家族的 TypeFactoryBase::Create,后者以 util_registration::StaticMapBase 单例 std::map<DeviceIdentifiers, factory>std::less<void>,逐字段比较)为键并调用已注册的 CreateTraceCodec。映射键是 PCI ID 元组,不是芯片代号字符串。最后,每个包的 TraceHeader.timestamp 是 ×16 定点的 Global-Time-Counter tick;TpuXLineBuilder::AddEvent(GtcSpan) @ 0xf1df1e0 通过 128 位 round(gtc · 1e9 / (gtc_freq_hz << 4)) 除法将其转换为皮秒,XLineBuilder::SetTimestampNsAndAdjustEventOffsets @ 0x1cf4dcc0 应用 ns→ps 的 ×1000 行原点重缩放。

对于重新实现,契约是:

  • 容器分帧 —— 由 N 个独立 zlib 分帧(window 15,zlib/gzip 自动检测)逐缓冲 blob 组成的 RepeatedPtrField<std::string>;每个通过 riegeli::ZlibReader<StringReader> 加整流 ReadAllImpl drain 膨胀为扁平 16 字节包流,没有内部记录分帧。
  • 逐包遍历 —— TraceCodec::DecodeInternal 校验 len >= 16 && len % 16 == 0,然后以步长 16 迭代 DecodeEntry,遇到 valid==0 流结束哨兵时中断。
  • 以 DeviceIdentifiers 为键的工厂 —— Is<Family> PCI 元组谓词链 → TypeFactoryBase::CreateStaticMapBase std::map 查找 → 已注册的 CreateTraceCodec,返回各家族 unique_ptr<TraceCodecInterface<…>>std::variant
  • 时钟域转换 —— 45/48 位 GTC tick(×16 定点)、由 GetEntriesGtcSpan 构造的 GtcSpan {start,length} span、round(gtc · 1e9 / (clk << 4)) 128 位除法,以及运行时(非内置)的 Task.gtc_freq_hz 除数。
传输驱动xprof::tpu::DecodeTraceBuffers<TraceEntry> @ 0xf59ffa0(pxc;6 个逐家族实例化)
逐包遍历asic_sw::driver::deepsea::profiler::TraceCodec<TraceEntry>::Decode @ 0xf5ad800DecodeInternal @ 0xf5ada40
解压器riegeli::ZlibReaderBase::Initialize @ 0xf69f9e0rs_inflateInit2_(windowBits=0x2f=47) @ 0x209f2e40
整流 drainriegeli::read_all_internal::ReadAllImpl @ 0xf5acf40(大小上限 -1
工厂入口xprof::tpu::GetTraceCodec(asic_sw::DeviceIdentifiers, int) @ 0xf5a2900
工厂映射util_registration::StaticMapBase<…>::GetValue @ 0xf5a3020;单例 std::map 根 @ 0x224c6000
工厂键12 字节 asic_sw::DeviceIdentifiers PCI 元组(vendor 0x1AE0 = Google);17 个内置 PCI 元组 @ .rodata 0xbdf3c0c0xbdf3cdc(只有 kPuffyliteChipIdentifiers @ 0xbdf3c4c 是 ELF 符号;其余未命名,由 Is<Family> xref 识别)
时间基准xprof::TpuXLineBuilder::AddEvent(GtcSpan, XEventMetadata) @ 0xf1df1e0 —— round(gtc·1e9/(clk<<4)) 128 位除法
Span 构造…collector::TpuTraceEntries<…>::GetEntriesGtcSpan @ 0xf1bf5e0(在 timestamp − DurationCycles·16 上取 min/max)
ns→ps 重缩放tsl::profiler::XLineBuilder::SetTimestampNsAndAdjustEventOffsets @ 0x1cf4dcc0×1000 = 0x3e8
源路径(rodata)third_party/riegeli/zlib/zlib_reader.ccplatforms/asic_sw/proto/device_identifiers.proto.../trace_codec.h.../tpu/<fam>/profiler/trace_codec_factory.cc

容器 —— DecodeTraceBuffers

目的

DecodeTraceBuffers<TraceEntry> @ 0xf59ffa0 是驱动器:它将压缩的逐核 trace blob 的 proto2 RepeatedPtrField<std::string> 转换为 RepeatedPtrField<TraceEntry>。它负责 riegeli/zlib 膨胀和逐缓冲迭代;实际的 16 字节包解码委托给所选编解码器的虚函数 Decode

入口点

text
DecodeTraceBuffers<TraceEntry> @0xf59ffa0    ── transport driver (6 per-family instantiations)
  └─ per buffer (decompress==true):
       riegeli::StringReader<string_view>           ── InitializerBase::ConstructMethodFromObject @0xf59eac0
       riegeli::ZlibReaderBase::Initialize @0xf69f9e0
         └─ InitializeDecompressor @0xf69fa60 ── rs_inflateInit2_(windowBits=47) @0x209f2e40
       read_all_internal::ReadAllImpl @0xf5acf40      ── drain whole inflated stream (cap -1)
       riegeli::Object::Close @0x20d97dc0             ── status check; failure ⇒ skip buffer
  └─ codec->Decode(string_view) via *0x18(vtable)  ── TraceCodec<TraceEntry>::Decode @0xf5ad800
```text

### 算法

```c
// DecodeTraceBuffers<TraceEntry> @0xf59ffa0
function DecodeTraceBuffers(codec, compressed_buffers, decompress, out):
    for buffer in compressed_buffers:                 // RepeatedPtrField<string>, r14 += 8 per elem
        if decompress:                                 // dl gate @0xf59ffeb: test dl,dl; je raw-path
            StringReader src(buffer);                  // @0xf59eac0
            ZlibReader  zin(&src);                      // window field = 47 (@0xf5a00aa: v37=47)
            //   BufferOptions {min 0x1000, max 0x10000} packed 0x1000000001000 (@0xf5a0075)
            zin.Initialize();                           // ZlibReaderBase::Initialize @0xf69f9e0
            view = ReadAllImpl(&zin, &scratch, /*max*/ -1);   // whole-stream drain @0xf5acf40
            if (!zin.Close())                           // @0x20d97dc0
                continue;                               // MakeErrorImpl<13>("Failed to decompress trace buffer.")
        else:
            view = buffer;                              // already-raw packet bytes, no zlib
        codec->Decode(view, out);                       // virtual *0x18(vtable) → TraceCodec::Decode @0xf5ad800

陷阱 —— decompress bool 是真实的流控制门,而不是提示。它为 false 时,buffer 字符串会被视为已经膨胀的原始包字节并直接交给编解码器;它为 true 时,buffer 是一个必须先膨胀的 zlib 流。总是膨胀(或从不膨胀)的重新实现会错误处理两种 producer 模式之一。device-trace 捕获路径会设置它;门控是 0xf59ffeb 处的 test dl,dl; je

解压器

膨胀使用标准 zlib,配置为最大窗口并自动检测头,通过 riegeli z_stream 池复用:

锚点地址细节
ZlibReaderBase::Initialize0xf69f9e0构造 reader,然后调用 InitializeDecompressor
InitializeDecompressor0xf69fa60从以 window 选项为键的 RecyclingPool<void, ZStreamDeleter> 中取出 z_stream
new-stream lambda0xf6a1300rs_inflateInit2_(z_stream, windowBits=[this+0x78], …) @ 0x209f2e40
windowBits0x2f = 47 = 15 (32 KiB window) + 32 (auto-detect zlib OR gzip header)
pool reset / teardown0x209f2ec0 / 0x209f2e00复用时 rs_inflateReset2,丢弃时 rs_inflateEnd;pool guard @ 0x224c6280,storage @ 0x224c6240

rs_ 前缀标记静态链接的 vendored zlib。这里没有自定义字典 —— 使用 32 KiB 窗口和 zlib/gzip 自动检测的标准 inflate 即可精确复现 reader。配套压缩器是 riegeli::ZlibWriterBase,由 xprof::tpu::(anon)::MaybeCompressTraceBuffers 驱动(lambda invokers @ 0xf5984c0/0xf598580),它会就地压缩每个原始逐缓冲包流。

注意 —— writer 的压缩级别策略无法从解码路径恢复:由于 reader 自动检测头(+32)且窗口最大,级别是 producer 侧旋钮,对 inflater 不可见(精确级别置信度 LOW;reader 契约 —— window 15,zlib/gzip 自动检测 —— 为 CERTAIN)。


逐包遍历 —— TraceCodec::DecodeInternal

目的

TraceCodec<TraceEntry>::Decode @ 0xf5ad800(薄包装)→ DecodeInternal @ 0xf5ada40 是将一个膨胀后缓冲区(扁平 16 字节包流)转换为追加的 TraceEntry 消息的循环。它根据固定包大小校验流长度,并迭代编解码器的 DecodeEntry,直到流结束哨兵。

算法

c
// TraceCodec<TraceEntry>::DecodeInternal @0xf5ada40
function DecodeInternal(view, out_entries):
    pkt = GetEntryPacketSize();                       // virtual *0x18(vtable) → 0x10 = 16
    if (view.length < pkt):
        return MakeErrorImpl<3>("Entries must be at least %d bytes.");          // @0xf5adacc (line 117)
    if (view.length % pkt != 0):                       // (unsigned)len % (unsigned)pkt
        return MakeErrorImpl<3>("Entries must be a multiple of %d bytes.");  // @0xf5adb01 (line 121)
    cursor = view.begin;
    while (cursor < view.end):                          // stride pkt (16)
        TraceEntry* e = out_entries.Add();
        bool valid, started;
        codec->DecodeEntry({cursor, pkt}, e, &valid, &started);  // virtual *0x20(vtable) @0xf5af3a0
        if (!valid):                                    // valid framing bit == 0 ⇒ graceful EOS
            out_entries.RemoveLast();                   // discard the empty sentinel slot
            break;                                       // "… done with decode. Remaining bytes : "
        cursor += pkt;                                  // advance one 16-byte packet
```text

长度校验已按字节确认:`len < 16` → `MakeErrorImpl<3>("Entries must be at least %d bytes.")`(通过 `absl::str_format_internal::FormatPack` 进行 `%d` 格式化,第 117 行,`0xf5adacc`),以及 `(unsigned)len % (unsigned)pkt` → `MakeErrorImpl<3>("Entries must be a multiple of %d bytes.")`(第 121 行,`0xf5adb01`)。`valid==0` 中断是[编解码器](trace-entries-coder.md#framing-semantics--valid-and-started)的优雅流结束哨兵 —— 也就是编解码器在包 bit 0 读取的同一分帧位。

> **怪癖 ——** 膨胀后的流**没有长度前缀,也没有记录分帧**。该流由第一个 `valid` 位为 0 的包在*带内*终止,方式与排空过度分配的 ring buffer 完全一致。因此 `len % 16 == 0` 检查的是整个缓冲区的对齐,而不是记录边界 —— 膨胀后的字节中从未实例化 riegeli `RecordReader`。整条路径中唯一的 riegeli 对象是逐缓冲的 `ZlibReader`。
>
> **注意 ——** `DecodeInternal` 还带有三条诊断日志路径 —— `"Found invalid packet, skipping over it. Remaining bytes: "`、`"Found invalid packet, done with decode. Remaining bytes : "` 和 `"Skipping invalid packet when continuing partial entry."` —— 因此流中间的撕裂包会记录并跳过,而不是致命错误,且 partial entry 可以继续。跳过/继续策略置信度 HIGH(字符串逐字节精确);精确的 partial-entry 状态机未被穷尽跟踪。

---

## 以 DeviceIdentifiers 为键的编解码器工厂

### 目的

`xprof::tpu::GetTraceCodec(DeviceIdentifiers, int)` @ `0xf5a2900` 为设备选择一个逐芯片家族编解码器。选择键是 **12 字节 PCI 标识元组**,不是代号字符串;结果是各家族 `unique_ptr<TraceCodecInterface<>>` 的 `std::variant`,随后由 `DecodeTraceBuffers` 以多态方式驱动。

### 键 —— `DeviceIdentifiers`

运行时键是一个 12 字节 POD,即 `asic_sw.proto.DeviceIdentifiers` 消息的 packed 形式(偏移由 `FromProto` @ `0x20b3ce40`、`ToString` @ `0x20b3cee0` 确认,读取 4×u16 的 `vpmovzxwd [k]` 和 4×u8 的 `vpmovzxbd [k+8]`):

| 字段 | 偏移 | 类型 | Proto | 含义 |
|---|---|---|---|---|
| `vendor_id` | `+0x0` | u16 | f1 | PCI vendor —— 始终为 `0x1AE0`(Google) |
| `device_id` | `+0x2` | u16 | f2 | PCI device id —— 芯片家族判别器 |
| `subsystem_vendor_id` | `+0x4` | u16 | f3 | 始终为 `0x1AE0` |
| `subsystem_device_id` | `+0x6` | u16 | f4 | board/SKU 判别器 |
| `class_code` | `+0x8` | u8 | f5 | PCI class(`0xff` legacy / `0x12` processing accelerator) |
| `subclass` | `+0x9` | u8 | f6 | |
| `programming_interface` | `+0xa` | u8 | f7 | |
| `revision_id` | `+0xb` | u8 | f8 | silicon stepping(例如 Pufferfish B0 的 `0x10`) |

### 谓词链

```c
// GetTraceCodec @0xf5a2900 — Is<Family> ordered dispatch
function GetTraceCodec(out_variant, device_ids, core_or_mode):
    if (IsDfc(device_ids) || IsJfc(device_ids)):                 // @0xf699000 / @0xf698fc0
        return PerformanceTrace ctor @0xf5a5060;                  // variant idx 6 — jxc legacy
    if (IsPlc(device_ids) || IsPfc(device_ids)):                 // @0xf699040 / @0xf699080
        return TypeFactoryBase<…pxc::TraceEntry>::Create(...);    // variant idx 5 — DEFAULT below too
    if (IsVlc(device_ids)):                                       // @0xf699140
        return TypeFactoryBase<…vlc::TraceEntry>::Create(...);    // variant idx 3
    if (IsVfc(device_ids)):                                       // @0xf699220
        return TypeFactoryBase<…vfc::TraceEntry>::Create(...);    // variant idx 4
    if (IsGlc(device_ids)):                                       // @0xf6992a0
        return TypeFactoryBase<…glc::TraceEntry>::Create(...);    // variant idx 1
    if (IsGfc(device_ids)):                                       // @0xf699320 → GetTraceCodec<gfc> @0xf5a2b60
        return …gfc… (variant idx 2);
    return TypeFactoryBase<…pxc::TraceEntry>::Create(...);        // pxc as final fallthrough default

每个 Is<Family> 都直接比较 packed 64 位 DeviceIdentifiers 低字(外加用于 revision 字节的 >> 48 高半部分)与内置常量。IsPlc @ 0xf699040 是逐字节精确的:

c
// IsPlc @0xf699040
return (u32)*(u64*)device_ids == (u32)kPuffyliteChipIdentifiers          // vendor+device match
    && ((u64)*device_ids ^ kPuffyliteChipIdentifiers) >> 48 == 0;        // subsystem+rev match
```text

> **怪癖 ——** pxc 有**两种**到达方式:当 `IsPlc||IsPfc` 时显式到达,以及当没有谓词匹配时作为*最终 fallthrough* 再次到达。重新实现必须保留 pxc 作为默认分支;未知但属于 Google 的 TPU 会用 pxc/Puffylite 编解码器解码,而不是失败。相对地,jxc/dfc 采用*单独*的 `PerformanceTraceEntry` 编解码器(variant idx 6),而不是固定 16 字节 `TraceEntry` 路径 —— 见 [jxc Legacy](payload-jxc-legacy.md)。

### 内置芯片常量

**十七个**内置 PCI 元组常量位于 `.rodata 0xbdf3c0c`–`0xbdf3cdc`(每个 12 字节;两个 jxc 元组之后、Pufferfish 块之前有一个 4 字节对齐空洞,因此这十七条记录*不是*连续的 17×12 数组)。只有一个带有 ELF 符号 —— `nm` 正好报告一个 `*ChipIdentifiers` 符号,`kPuffyliteChipIdentifiers` @ `0xbdf3c4c`(plc 元组)。其他十六个元组在符号表中未命名;它们的逐家族身份不是从符号名恢复,而是从内联 `lea` 各元组地址的 `Is<Family>` 访问器恢复(`0xbdf3c0c`..`0xbdf3cb8` 范围内 jxc×2、pfc×3、plc×1、vlc×4、vfc×2、glc×3,然后是两个 Ghostfish/gfc 元组)。`0xbdf3cc4`/`0xbdf3cd0` 处的两个 gfc 元组(`device_id 0x0075`/`0x0076`,`subsystem_device_id 0x00f2`)是逐字节确认的真实常量,由 `IsGfc` @ `0xf699320` 内联引用(其 `lea` 到 `0xbdf3cd0`/`0xbdf3cc4`)。`Is<Family>` 谓词会与全部十七个比较。它们都共享 `vendor=0x1AE0` 和 `subsystem_vendor=0x1AE0`。判别轴是 `device_id`(家族)× `subsystem_device_id`(board)× `revision_id`(stepping):

| 家族(谓词) | `device_id` | `class` | 判别 sub-id / rev | 编解码器 `TraceEntry` |
|---|---|---|---|---|
| Jellyfish/Dragonfish (`IsJfc`/`IsDfc`) | `0x0027` | `0xff` | sub `0x004e`/`0x004f` | `jxc::PerformanceTraceEntry` |
| Pufferfish B0 (`IsPfc`) | `0x005e` | `0xff` | sub `0x0050`/`51`/`52` (Mfg/Water/Air), rev `0x10` | `pxc::profiler::TraceEntry` |
| Puffylite (`IsPlc`) | `0x0056` | `0xff` | sub `0x007b` | `pxc::profiler::TraceEntry` |
| Viperlite (`IsVlc`) | `0x0063` | `0xff` | sub `0x00ae` (PF)/`0x00af` (VF), rev `0x00`/`0x01` | `vxc::vlc::profiler::TraceEntry` |
| Viperfish (`IsVfc`) | `0x0062` | `0xff` | sub `0x00ac` (PF)/`0x00ad` (VF) | `vxc::vfc::profiler::TraceEntry` |
| Ghostlite (`IsGlc`) | `0x006e`/`6f`/`70` | `0x12` | sub `0x00d1` (App PF/VF, Mgt PF) | `gxc::glc::profiler::TraceEntry` |
| Ghostfish-band (`IsGfc`) | `0x0075`/`76` | `0xff` | sub `0x00f2` | `gxc::gfc::profiler::TraceEntry` |

> **注意 ——** 家族绑定折叠了三个正交 SKU 轴。`IsPfc` 匹配三个 Pufferfish B0 子 SKU(Mfg/Water/Air),它们仅在 `subsystem_device_id` 上不同,同时共享 `device_id=0x5e, rev=0x10`;PF 与 VF 变体在最后一个 sub-id 半字节上不同;A0/A1 stepping 在 `revision_id` 上不同。Ghostlite "App" silicon 将 PCI `class` 切换为 `0x12`(processing accelerator);较老家族使用 `0xff`。同一元组比较也被 `xprof::tpu::DeviceTypeFromDeviceIdentifiers` @ `0xf6993a0` 复用,用于将 `DeviceIdentifiers` 折叠为 device-type ordinal。PCI 元组↔代号绑定由 silicon map 拥有;本页只负责工厂 keying。

### 注册表 —— `StaticMapBase`

`TypeFactoryBase<DeviceIdentifiers, &DeviceIdentifiersAsString, TraceCodecInterface<>, false>::Create`(pxc @ `0xf5a2c20`)通过 `StaticMapBase::GetValue` @ `0xf5a3020` 在惰性构建的单例 `std::map<DeviceIdentifiers, pair<const char*, std::function<StatusOr<unique_ptr<TraceCodecInterface>>()>>>` 中查找工厂:

```c
// StaticMapBase::GetValue<DeviceIdentifiers> @0xf5a3020 — rb-tree walk, std::less<void> field-wise
function GetValue(map_singleton, key):                 // guard @0x224c6020, root @0x224c6000
    node = map.root;
    while (node):
        k = node[+0x20];                                 // node key = DeviceIdentifiers at node+0x20
        // ordered field-wise compare (the 4 u16 halves, then revision byte at +0x2b):
        if (key.vendor   != k.vendor)   { go left/right by < ; continue }   // si  vs WORD0
        if (key.device   != k.device)   { go left/right ; continue }        // WORD1
        if (key.subvend  != k.subvend)  { go left/right ; continue }        // WORD2 / HIDWORD
        if (key.subdev   != k.subdev)   { go left/right ; continue }        // WORD3 (HIWORD)
        if (key.rev      != node[+0x2b]) { go left/right ; continue }        // byte +0x2b = revision_id
        return &node[+0x38];                             // value (the registered std::function)
    return NULL;                                          // → "… is not registered" diagnostic

比较顺序 —— vendor_iddevice_idsubsystem_vendor_idsubsystem_device_id,然后是 node +0x2b 处的 revision_id 字节 —— 已逐字节确认(cmp si,…; di,…; r8w,…; rcx,…>>0x30; dl,[node+0x2b]),值位于 node +0x38。未命中时,Create 会通过 util::Demangle + DeviceIdentifiers::ToString + StaticMapBase::GetKeys + absl FormatPack 构造 "Function for DeviceIdentifiers %s with … is not registered" 诊断(0xf5a2c91..)。

注册与构造

每个家族的 CreateTraceCodec 是静态初始化注册项,由 InsertValue(pxc @ 0xf5ad460)在 Mutex 下插入同一个单例映射。默认 builder plc::driver::profiler::CreateTraceCodec @ 0xf5af2c0 已逐字节确认:

c
// plc::driver::profiler::CreateTraceCodec @0xf5af2c0
function CreateTraceCodec():
    coder = new TraceEntriesCoder(/*8 B*/);            // operator new(8); vptr off_21771038
    codec = new TraceCodec<pxc::TraceEntry>(/*0x50 B*/);  // operator new(0x50); vptr off_21770ef8
    codec[+0x38] = &EncodeEntry policy_func;            // std::function<bool(const TraceEntry&)> wrapper
    codec[+0x10] = operator new(coder->GetMaxEntrySize());  // 0x20-byte decode scratch
    return codec;                                       // owns the TraceEntriesCoder
```text

| 家族 | `CreateTraceCodec` | `Create` | variant idx | `DecodeTraceBuffers` 实例化 |
|---|---|---|---|---|
| pxc (Puffylite/Pufferfish) | `0xf5af2c0` (`plc`), `0xf5ad5c0` | `0xf5a2c20` | 5 | `<pxc::…::TraceEntry>` @ `0xf59ffa0` |
| vlc (Viperlite) | `0xf5d5180` | `0xf5a3360` | 3 | `<vxc::vlc::…::TraceEntry>` @ `0xf59f560` |
| vfc (Viperfish) | `0xf5f5da0` | `0xf5a3aa0` | 4 | `<vxc::vfc::…::TraceEntry>` @ `0xf59fa80` |
| glc (Ghostlite) | `0xf6282e0` | `0xf5a41e0` | 1 | `<gxc::glc::…::TraceEntry>` @ `0xf59e540` |
| gfc (Ghostfish-band) | `0xf65ed00` | (anon wrapper `0xf5a2b60`) | 2 | `<gxc::gfc::…::TraceEntry>` @ `0xf59f040` |
| jxc (Jellyfish/Dragonfish) | (legacy) | ctor `0xf5a5060` | 6 | `<jxc::PerformanceTraceEntry>` @ `0xf5a04c0` |

> **陷阱 ——** variant *索引*不是谓词顺序。`std::variant<monostate, glc, gfc, vlc, vfc, pxc, jxc::PerformanceTrace>` 载荷(8 字节 ptr + `+0x8` 处 1 字节索引)编号为 glc=1、gfc=2、vlc=3、vfc=4、pxc=5、jxc=6、monostate=0 —— 与 `Is*` 求值顺序不同。把“第三个谓词”和“variant index 3”混为一谈的重新实现会错误标记每个编解码器。

---

## GTC-Tick → 皮秒时间基准

### 时钟域

`TraceHeader.timestamp` 字段 —— pxc/vlc 为 48 位,vfc/glc/gfc 为 45 位(见[编解码器页面](trace-entries-coder.md#per-gen-header-widths))—— **不是**自由运行的逐核 cycle counter。它是 **Global Time Counter (GTC) tick**:由 `SetGtcConfiguration`(slice_builder gRPC @ `0x1fc4f300`)编程的芯片范围、跨核、跨芯片同步计数器。低 **4 位是小数 sub-tick**;整数 tick 是 bits `[4..]`。因此单个逐行 GTC 频率会把每个 device-plane 事件转换到同一个公共墙钟 —— 这正是转换不能发生在编解码器中的原因。

### Span —— `GetEntriesGtcSpan`

`…collector::TpuTraceEntries<>::GetEntriesGtcSpan` @ `0xf1bf5e0` 收集已排空 entries 上的 GTC 范围:

```c
// GetEntriesGtcSpan @0xf1bf5e0 — span over the ×16 fixed-point GTC domain
function GetEntriesGtcSpan(entries) -> GtcSpan {start, length}:
    min = +inf; max = 0;
    for e in entries:
        t = e.TraceHeader.timestamp;                    // +0x20
        t -= 16 * DurationCycles(e);                     // <<4 to match the GTC ×16 fixed-point
        min = (t < min) ? t : min;                       // cmovb @0xf1bf6a4
        max = (t > max) ? t : max;                       // cmovbe @0xf1bf6e9
    return { start: min, length: max - min };

DurationCycles @ 0xf699900 读取逐事件 cycle count,仅对 BC band 的 CmqVpuDma duration events(trace_point_id 0x64..0x77 = 100..119;主体门控为 (unsigned)(id − 100) > 0x13)返回非零,其他返回 0×16 位移将它与 GTC 定点对齐。GtcSpan 值占据 bits [4..44] —— 一个 41 位字段,由 0x1ffffffffff0 掩码。

转换 —— AddEvent(GtcSpan)

c
// TpuXLineBuilder::AddEvent(GtcSpan, XEventMetadata) @0xf1df1e0 — byte-confirmed
//   ABI: rdi = this (builder); rdx:r8 = GtcSpan {start,length} by value; rsi = metadata ref `m`
function AddEvent(this, span, m):
    gtc   = span.start & 0xFFFFFFFFFFFFFFF0;             // clear the 4 fractional bits (@0xf1df247)
    clk16 = 16 * *(*(m + 0x10));                         // [m+0x10] = per-line GTC-clock; clk << 4 (@0xf1df256 shl 4)
    num   = 0x3B9ACA00 * (u128)gtc;                      // × 1e9, 128-bit mul (@0xf1df24b mov 0x3b9aca00)
    value = __udivti3(num + (clk16 >> 1), clk16);        // round-to-nearest 128-bit divide
    //  XStat "device_offset_ps" = value (start);  XStatMetadata id from [m+0x38]  (@0xf1df205)
    //  length side via GtcSpanConverter::TimespanFromGtcSpan((GtcSpan) on *(m+0x10), span)
    //    → XStat "device_duration_ps";  XStatMetadata id from [m+0x40]
```text

掩码(`0x1ffffffffff0` 值窗口、`0xfffffffffffffff0` 清低 4 位)、`0x3B9ACA00`(=1e9)乘法、`clk << 4`、`+clk16/2` 四舍五入以及 `__udivti3` 128 位除法都在 `0xf1df1e0` 处逐字节确认。代数上,`gtc` 携带 ×16 缩放,且 `clk16 = freq × 16`:

```text
value_ps = gtc · 1e9 / (gtc_freq_hz · 16)

怪癖 —— 除数是 gtc_freq_hz << 4不是 gtc_freq_hz,因为 GTC tick 本身是 ×16 定点。两个 ×16 因子(分子 tick 和分母 clock)正好抵消;只处理对其中一个会得到偏差 16× 的结果。*(*(m+0x10)) 的确切单位(Hz vs kHz vs ticks/ps)是推断的 —— 端到端皮秒结果逐字节精确,中间 clock-object 单位置信度 LOW。(除数和两个 XStatMetadata id 是从 metadata-reference 参数 mrsi)读出,而不是从 builder thisrdi)读出。)

注意 —— length(duration)侧路由到 xprof::tpu::GtcSpanConverter::TimespanFromGtcSpan(GtcSpan) const @ 0xf2cb7e0(在 converter 对象 *(m+0x10) 上调用),而不是内联同一除法;结果送入 device_duration_ps XStat。start 侧直接内联除法。两者最终都计算 gtc · 1e9 / (freq · 16);start 路径是规范的、完整逐字节跟踪的形式。

行原点 —— SetTimestampNsAndAdjustEventOffsets

tsl::profiler::XLineBuilder::SetTimestampNsAndAdjustEventOffsets @ 0x1cf4dcc0XLine 打上原点时间戳,并把设备相对偏移重缩放到同一个皮秒域:

c
// SetTimestampNsAndAdjustEventOffsets @0x1cf4dcc0
function SetTimestampNsAndAdjustEventOffsets(this, new_timestamp_ns):
    delta_ps = 1000 * (XLine.timestamp_ns - new_timestamp_ns);   // ×1000 = 0x3e8, ns→ps (@line 16)
    XLine.timestamp_ns = new_timestamp_ns;                        // XLine+0x40 (@line 17)
    for event in XLine.events:
        event.offset_ps += delta_ps;                              // shift every event into the new origin
```text

1000`(`0x3e8`)是行原点的纳秒→皮秒转换;`XLine.timestamp_ns` 位于 `XLine+0x40`。最终结果:每个 device-plane `XEvent.offset_ps`/`duration_ps` 都是皮秒,由 GTC tick 除以 GTC 频率得到,并重新锚定到该行的 ns 原点。下游 XEvent 成形由 [TraceEntry → XEvent/XStat](trace-entry-to-xevent.md) 负责。

### 时钟常量 —— 运行时,而非内置

馈入除数的频率是**profile 时捕获的 device-info**,序列化到 xprof `Task` proto(`task.proto`)中,而不是 libtpu 中的编译期常量:

| 字段 | Proto # | 类型 | rodata descriptor | 作用 |
|---|---|---|---|---|
| `tensor_core_freq_hz` | f11 | uint64 | `0xbe99b16` | TensorCore cycle clock(逐引擎 duration cycle→time) |
| `sparse_core_freq_hz` | f12 | uint64 | `0xbe99b33` | SparseCore cycle clock |
| `gtc_freq_hz` | f13 | uint64 | `0xbe99b50` | **GTC tick rate** —— `AddEvent` 除数 |

> **陷阱 ——** 转换*代数*逐字节精确,但具体 Hz 数值无法从二进制中读取 —— 它们是 `Task` proto 中的运行时输入。重新实现必须从捕获的 profile(或驱动编程的设备端 `GtcConfiguration`)读取 `gtc_freq_hz`,绝不能假设常量。这也固定了 trace wrap period:pxc/vlc 为 `2^48 / gtc_freq_hz` 秒,vfc/glc/gfc 为 `2^45 / gtc_freq_hz`。`task.proto` schema 由 [Task Proto](task-proto.md) 负责。哪个 freq 应用于哪个*payload* duration 字段(TensorCore vs SparseCore)是从字段名推断的,而不是跟踪过每个 subscriber(LOW confidence)。

---

## 传输 + 时间基准流水线

```text
per-core HW trace ring drain
   │  raw bytes = flat concat of 16-byte LSB-first packets (codec page), one per buffer

MaybeCompressTraceBuffers  →  riegeli ZlibWriter per buffer  (window 15, zlib/gzip)
   │  RepeatedPtrField<std::string>   (N independently zlib-framed buffers)

DecodeTraceBuffers<TraceEntry> @0xf59ffa0
   │  per buffer: StringReader → ZlibReader (inflateInit2 windowBits=47) → ReadAllImpl(whole)
   ▼  one std::string of flat 16-byte packets
TraceCodec::Decode/DecodeInternal @0xf5ad800 / @0xf5ada40
   │  CHECK len>=16 && len%16==0 ; loop stride 16 → DecodeEntry per packet (valid==0 ⇒ EOS)
   ▼  RepeatedPtrField<TraceEntry>
[ codec selected once up front: ]
GetTraceCodec(DeviceIdentifiers, …) @0xf5a2900
   │  Is<Family>(PCI tuple) → TypeFactoryBase::Create → StaticMapBase::GetValue(std::map)
   │     → registered CreateTraceCodec() → TraceEntriesCoder

…Subscriber::ProcessTraceEntry  →  GetEntriesGtcSpan @0xf1bf5e0
   │  GtcSpan = min/max over (TraceHeader.timestamp − DurationCycles·16)   [×16 GTC fixed-point]

TpuXLineBuilder::AddEvent(GtcSpan) @0xf1df1e0
   │  device_*_ps = round(gtc · 1e9 / (gtc_freq_hz << 4))   (128-bit udiv)
   │  XLine timestamp_ns origin + offset ×1000 (ns→ps) in SetTimestampNsAndAdjustEventOffsets

device-plane XEvent (offset_ps / duration_ps in picoseconds)

相关结构和表偏移

符号地址 / 偏移作用
DeviceIdentifiers(运行时 POD,12 B)+0x0 vendor_id, +0x2 device_id, +0x4 subsys_vendor, +0x6 subsys_device, +0x8 class, +0x9 subclass, +0xa prog_iface, +0xb revision工厂键(PCI 元组)
StaticMapBase singleton (pxc)guard 0x224c6020; map root 0x224c6000; node key +0x20 (+ rev byte +0x2b); value +0x38编解码器注册表 std::map
kXxxChipIdentifiers (+ 2 unnamed gfc).rodata 0xbdf3c0c0xbdf3cdc17 个内置 PCI 元组常量(只有 kPuffyliteChipIdentifiers 是 ELF 符号;其余由 Is<Family> xref 识别),vendor 0x1AE0
GtcSpan{u64 start; u64 length}, value bits [4..44]×16 GTC 定点 min/max span;按值传递(rdx=start,r8=length)
AddEvent metadata arg (rsi)+0x10 逐行 GTC-clock object([0] = divisor base);+0x38/+0x40 offset/duration XStatMetadata ptrsAddEvent 从第 2 个指针参数读取,而不是 builder this
XLine+0x40 timestamp_ns origin重缩放 ×1000 的行原点字段
ZlibReaderBase+0x78 windowBits (0x2f=47); pool guard 0x224c6280, storage 0x224c6240解压器状态
TraceEntriesCoder vtable (pxc)obj vptr off_21771038; TraceCodec<pxc> vptr off_21770ef8; GetEntryPacketSize0x10, GetMaxEntrySize0x20工厂构建的编解码器对象

相关组件

组件关系
TraceEntriesCoder此容器为其膨胀字节的逐包编解码器,也是工厂构建的对象;DecodeInternal 对每个 16 字节包调用它的 DecodeEntry
TracePoints Master Registry编解码器所依据的 wire-id ↔ oneof-field id 空间;DurationCycles 的 BC-band duration ids 来自这里
TraceEntry → XEvent/XStat调用 AddEvent(GtcSpan) 并将转换后的 ps 偏移转为 device-plane XEvents/XStats 的下游 subscriber
Task Proto携带 gtc_freq_hz/tensor_core_freq_hz/sparse_core_freq_hz 这些运行时时钟除数的 Task 消息
Payload: jxc Legacyjxc/dfc 选择的独立 PerformanceTraceEntry 编解码器(variant idx 6),而不是固定 16 字节路径
Profiling and Telemetry Overview本页作为第 2 和第 5 阶段所属的五阶段 capture→encode→decode→xplane 流水线

交叉引用

  • Profiling and Telemetry Overview —— device-trace 流水线;本页负责传输(第 2 阶段)和时间基准(第 5 阶段)
  • TraceEntriesCoder —— 第 3 阶段,即此容器分帧并由工厂构造的固定 16 字节包编解码器
  • TraceEntry → XEvent/XStat —— 第 5 阶段 consumer,GTC→ps 转换在此落为 XStat device_offset_ps/device_duration_ps
  • TracePoints Master Registry —— trace-point id 空间;DurationCycles 读取的 BC-band duration ids 0x640x77(100–119)
  • Task Proto —— 时间基准除以的运行时 gtc_freq_hz/*_core_freq_hz 时钟源
  • Payload: jxc Legacy —— 工厂为 jxc/dfc 选择的 legacy PerformanceTraceEntry 编解码器