riegeli 跟踪容器
本页中的所有地址和偏移都适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(build-id89edbbe81c5b328a958fe628a9f2207d)。该二进制文件未剥离符号 —— 完整 C++ 符号存在,且.textVMA 等于文件偏移。其他版本会有所不同。
摘要
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>加整流ReadAllImpldrain 膨胀为扁平 16 字节包流,没有内部记录分帧。 - 逐包遍历 ——
TraceCodec::DecodeInternal校验len >= 16 && len % 16 == 0,然后以步长 16 迭代DecodeEntry,遇到valid==0流结束哨兵时中断。 - 以 DeviceIdentifiers 为键的工厂 ——
Is<Family>PCI 元组谓词链 →TypeFactoryBase::Create→StaticMapBasestd::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 @ 0xf5ad800 → DecodeInternal @ 0xf5ada40 |
| 解压器 | riegeli::ZlibReaderBase::Initialize @ 0xf69f9e0;rs_inflateInit2_(windowBits=0x2f=47) @ 0x209f2e40 |
| 整流 drain | riegeli::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 0xbdf3c0c–0xbdf3cdc(只有 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.cc;platforms/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。
入口点
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陷阱 ——
decompressbool 是真实的流控制门,而不是提示。它为 false 时,buffer 字符串会被视为已经膨胀的原始包字节并直接交给编解码器;它为 true 时,buffer 是一个必须先膨胀的 zlib 流。总是膨胀(或从不膨胀)的重新实现会错误处理两种 producer 模式之一。device-trace 捕获路径会设置它;门控是0xf59ffeb处的test dl,dl; je。
解压器
膨胀使用标准 zlib,配置为最大窗口并自动检测头,通过 riegeli z_stream 池复用:
| 锚点 | 地址 | 细节 |
|---|---|---|
ZlibReaderBase::Initialize | 0xf69f9e0 | 构造 reader,然后调用 InitializeDecompressor |
InitializeDecompressor | 0xf69fa60 | 从以 window 选项为键的 RecyclingPool<void, ZStreamDeleter> 中取出 z_stream |
| new-stream lambda | 0xf6a1300 | rs_inflateInit2_(z_stream, windowBits=[this+0x78], …) @ 0x209f2e40 |
windowBits | — | 0x2f = 47 = 15 (32 KiB window) + 32 (auto-detect zlib OR gzip header) |
| pool reset / teardown | 0x209f2ec0 / 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,直到流结束哨兵。
算法
// 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 是逐字节精确的:
// 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_id、device_id、subsystem_vendor_id、subsystem_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 已逐字节确认:
// 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)
// 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 参数m(rsi)读出,而不是从 builderthis(rdi)读出。)注意 —— length(duration)侧路由到
xprof::tpu::GtcSpanConverter::TimespanFromGtcSpan(GtcSpan) const@0xf2cb7e0(在 converter 对象*(m+0x10)上调用),而不是内联同一除法;结果送入device_duration_psXStat。start 侧直接内联除法。两者最终都计算gtc · 1e9 / (freq · 16);start 路径是规范的、完整逐字节跟踪的形式。
行原点 —— SetTimestampNsAndAdjustEventOffsets
tsl::profiler::XLineBuilder::SetTimestampNsAndAdjustEventOffsets @ 0x1cf4dcc0 为 XLine 打上原点时间戳,并把设备相对偏移重缩放到同一个皮秒域:
// 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 0xbdf3c0c–0xbdf3cdc | 17 个内置 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 ptrs | 由 AddEvent 从第 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; GetEntryPacketSize→0x10, GetMaxEntrySize→0x20 | 工厂构建的编解码器对象 |
相关组件
| 组件 | 关系 |
|---|---|
| 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 Legacy | jxc/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 ids0x64–0x77(100–119) - Task Proto —— 时间基准除以的运行时
gtc_freq_hz/*_core_freq_hz时钟源 - Payload: jxc Legacy —— 工厂为 jxc/dfc 选择的 legacy
PerformanceTraceEntry编解码器