ErrorAggregator
本页所有地址都适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(BuildID md589edbbe81c5b328a958fe628a9f2207d,781,691,048 字节,x86-64)。其他版本会有所不同。
摘要
Megascale MegascaleErrorAggregator 是集群范围的根节点,会把逐主机失败报告风暴转换成一个经过分类的根因裁决。当多切片 TPU 作业挂起或崩溃时,每台主机的 libtpu 都会通过 MegaScaleTransport.ReportError gRPC 向协调器进程发送一个 MegaScaleRuntimeError。聚合器将这些报告汇聚成一个 RapidEyeErrorDigestProto — 这是一条自描述记录,会命名疑似罪魁主机、将失败分类为九个 Cause 值之一,并逐字嵌入时间上最早的错误,使人工或下游批处理作业无需重新推导即可读取裁决。
最接近的常见类比,是接到屏障上的崩溃报告聚合器(Sentry/Crashpad):它是一个按参与者身份索引的集中式单实例收集器,具有“是否所有人都已报告”的提前触发和 deadline 回退机制 — 但不同于通用崩溃接收器,它是一次性的。聚合器只在单次错误风暴期间存在:它在第一个入站 ReportError 上惰性创建,持续汇入直到 size() == expected_workers 或 300 ms 空闲 deadline(每次报告都会重新设置)到期,随后发出摘要并消亡。没有环形缓冲区、没有 TTL sweep,也没有跨重启持久化 — 设计假设是一次风暴,随后一次 drain。
本页涵盖线格式(RapidEyeErrorDigestProto 以及嵌入的 MegaScaleRuntimeError)、作用域(协调器上的单个集群级实例,由扁平 DCN (slice_id, host_id) 主机身份馈入)、保留模型(内存中的 linked_hash_map、可选的磁盘 RapidEye 日志、无驱逐),以及去重 / 首个错误胜出逻辑(按 (worker, task) 的 last-write-wins 合并,加上粘性的首个错误槽)。ICI 级失败检测 — 芯片故障最初如何变成 MegaScaleRuntimeError — 属于下一层的 ICI 失败恢复。聚合器如何消费 NumHosts() 和 (slice_id, host_id) 的集群元数据视角,属于 屏障错误用法。
对于重新实现,契约是:
- 线格式 —
RapidEyeErrorDigestProto(17 个有效字段、13 个嵌套消息、一个 9 值Cause枚举)以及嵌入的逐主机MegaScaleRuntimeError(13 个字段、两个枚举)。摘要会逐字嵌入首个错误,因此记录是自描述的。 - 作用域模型 — 协调器上的单个集群级聚合器实例,惰性分配,以扁平 DCN 主机身份为键;worker 不运行自己的聚合器。
- 去重 / 保留规则 —
slice<S>-task<H>/<task_id>map 键、last-write-wins 合并、粘性的首个错误槽,以及不存在任何数量上限、TTL 或持久化。 - 提前触发 / deadline drain —
size() == GetClusterStatus().expected会立即触发;否则,300 ms 空闲 deadline(每个入站ReportError都会重新设置)会在 300 ms 内没有新报告后,带着“missing workers”差异 drain 一个部分摘要。
| 类 | xla::megascale::runtime::MegascaleErrorAggregator |
| 对象大小 | 0x2f0 = 752 字节,operator new(752, 16) |
| 所有者 / 作用域 | 单实例,由协调器的 ErrorReporter 拥有(+0x38,惰性);集群范围 |
| 线格式 | xla.megascale.runtime.RapidEyeErrorDigestProto(语法 editions) |
| 描述符 | protodesc_cold VA 0xc169340 处的 rapideye_logging.proto FileDescriptorProto(4 460 字节) |
| 源 TU | platforms/xla/megascale/runtime/common/megascale_error_aggregator.cc |
| 摄入 | AddError 0x1ccba940;gRPC 汇入 ErrorReporter::ReportError 0x1ccb6ea0 |
| Drain | ProcessAndShutdown 0x1ccbaba0(幂等,一次性) |
| 协调器 job_name | "McJax" |
| Map 键 | slice<S>-task<H>/<task_id>(linked_hash_map<string, MegaScaleRuntimeError>) |
| 持久化 | 默认无;可选 RapidEyeLogger sink(--megascale_rapideye_error_digest_log_path) |
作用域 — 一个集群级实例
目的
聚合器的作用域是整个多切片作业,并在单点收集。运行中的作业恰好有一个 MegascaleErrorAggregator,它位于协调器进程上。Worker 主机不运行聚合器;它们只运行主机本地的 DebugManager 收集器(一个独立的进程单例),并把错误上报到协调器。该模型是两个扁平层级 — 主机收集器 → 协调器根节点 — 没有逐切片中间聚合器。一个包含八个切片的 superpod 仍然只有一个覆盖所有切片的协调器聚合器。
入口点
ErrorReporter::ReportError (0x1ccb6ea0) ── coordinator-side gRPC fan-in handler
├─ (first hit) operator new(752, 16) ── lazy-allocate the one aggregator
│ └─ MegascaleErrorAggregator(_, "McJax") ── 1-arg ctor 0x1ccba4c0 (delegates to 4-arg 0x1ccba600)
├─ thread::AddCancellableAt(.., this+64) ── arm 300 ms deadline → ProcessErrorDigest
├─ SubstituteAndAppendArray("slice$0-task$1") ── build the (slice_id, host_id) worker key
├─ MegascaleErrorAggregator::AddError ── 0x1ccba940 — upsert into the map
└─ if size() == GetClusterStatus().expected ── early-fire
└─ ErrorReporter::ProcessErrorDigest ── 0x1ccb7140 — drain + classify + log
```text
### 算法
```c
function ErrorReporter_ReportError(req, resp): // sub_1ccb6ea0
TracedMutexLock guard(this->mu_) // ErrorReporter+0x48, kind=14
if this->aggregator_ == null: // +0x38, lazy
this->aggregator_ = new(752,16)
MegascaleErrorAggregator("McJax") // line 47
deadline = clock_.Now() + 300ms // line 55 — rearmed each call
AddCancellableAt(threadpool_, deadline,
[this]{ ProcessErrorDigest(); },
&this->deadline_handle_) // line 59, &(this+64)
invoke this->unicast_callback_(slice_id, host_id, *err) // ErrorReporter+0x10, per-error
worker_id = SubstituteAndAppendArray(
"slice$0-task$1", {slice_id, host_id}) // line 92 — DCN host identity
err = req.error (proto field 3); if null -> global empty MegaScaleRuntimeError
aggregator_->AddError(worker_id, *err) // line 102
status = coordinator_->GetClusterStatus() // vtable+64; expected-worker count
if status.ok() and aggregator_->size() == status.expected: // line 117 early-fire
ProcessErrorDigest() // line 120 — bypass the deadline
resp = ReportErrorResponse{} // empty ack注意 — 提前触发计数是通过
GetClusterStatus()(协调器 vtable+64)读取的 cluster-status 预期数量,而不是原始常量。按照 集群元数据 视角,该预期数量等于MultiSliceTopologyAndLocation::NumHosts()— 也就是 跨主机屏障 等待的同一个集群级主机数量。如果拓扑尚未发现(!status.ok()),摘要会以"Unexpected error digest before topology discovery is completed:"丢弃,而不是针对未知总体发出。
PJRT 并行表面
还存在第三个独立作用域:PJRT C-API 将同一个类暴露给更高层 XLA/JAX 客户端,不涉及任何网络。pjrt::(anonymous) 下的八个 shim(Create 0xe6bab80、Delete 0xe6bac00、AddError 0xe6bad60、ProcessAndShutdown 0xe6bae40、LogErrorDigest 0xe6baec0、Size 0xe6baf20、Active 0xe6baf80,以及 ErrorDigestDelete 0xe6bace0)允许客户端用调用方提供的 job_name Create 自己的聚合器,用调用方提供的 worker_id 和序列化的 MegaScaleRuntimeError(ParseFromString)驱动 AddError,并在进程内直接取回摘要。每个 shim 都由 ActualStructSizeIsGreaterOrEqual ABI 检查保护,对照文档化的 args-struct 大小(Create 0x20,AddError 0x30,ProcessAndShutdown/Size/LogErrorDigest 0x18,Active 0x11,Delete/ErrorDigestDelete 0x10)。PJRT 路径聚合器的作用域就是客户端馈入的任何内容;没有 RPC,也没有协调器参与。
函数映射
| 函数 | VA | 角色 |
|---|---|---|
ErrorReporter::ReportError | 0x1ccb6ea0 | gRPC 汇入;惰性分配、构造键、deadline、AddError、提前触发 |
ErrorReporter::ProcessErrorDigest | 0x1ccb7140 | drain + missing-worker 差异 + LogErrorDigest + 可选 abort |
MegascaleErrorAggregator ctor(4 参数) | 0x1ccba600 | 类初始化;存储 job_name/log_dir 字符串 + 预计算的 global_id/xid;读取 FLAGS_megascale_error_aggregation_enabled → shutdown_ |
MegascaleErrorAggregator ctor(1 参数) | 0x1ccba4c0 | 仅 job_name 的委托器:读取 FLAGS_megascale_rapideye_error_digest_log_path、NewGlobalID()、getenv("XM_XID"),然后调用 4 参数 ctor |
PJRT ErrorAggregatorCreate | 0xe6bab80 | 进程内聚合器(operator new(752,16)) |
PJRT ErrorAggregatorAddError | 0xe6bad60 | ParseFromString + AddError;megascale_extension.cc:321 |
PJRT ErrorAggregatorProcessAndShutdown | 0xe6bae40 | 返回 448 字节(0x1C0)的 ErrorDigest |
摄入和去重 — AddError
目的
AddError 是唯一摄入点。它在聚合器唯一互斥锁下串行化并发调用者,在粘性槽中捕获第一个非取消错误用于分类,并把每个报告 upsert 到按主机和任务身份索引的 linked_hash_map。两个错误是否“相同”仅取决于键是否匹配;带有匹配键的后到报告会覆盖较早值(last write wins),但插入顺序会保留,因此仍然可以在没有时间戳的情况下恢复第一个报告者。
算法
function AddError(worker_id, err): // sub_1ccba940
mu_.lock() // mutex at this+744 == +0x2e8
if shutdown_: // +0x30 — already drained / poisoned
mu_.unlock(); return // no-op
if !has_first_recorded_error_: // +0xe0
if err.error_type == CANCELLED (3): // line 33 — *((DWORD*)err + 18) == 3
shutdown_ = true // teardown signal, store NOTHING
mu_.unlock(); return
first_recorded_error_.CopyFrom(err) // +0x70, arena=null
has_first_recorded_error_ = true
key = StrCat(worker_id, "/", err.task_id) // line 81; task_id = field 9, +0x58
slot = errors_by_worker_launch_.LazyEmplaceInternal(key) // line 91 — +0xe8
slot.value.CopyFrom(err) // line 95 — last write wins
mu_.unlock()
```text
> **怪癖 —** 首个 `CANCELLED` 错误是*拆除信号,而不是数据*。如果第一个 `AddError` 在粘性槽仍为空时携带 `error_type == CANCELLED (3)`,`AddError` 会翻转 `shutdown_ = true` 并返回,且不存储任何内容(第 33-36 行)。这会毒化聚合器:后续 `ProcessAndShutdown` 会返回空的缓存摘要。语义是“作业正在有意拆除 — 不要构造挂起摘要”。如果重新实现把 cancellation 当成另一个普通错误,那么每次干净关闭都会发出虚假的 `UNKNOWN_CAUSE` 摘要。
### 两段式键
Map 键是 `slice<S>-task<H>/<task_id>`,在两个位置构造:
- **外层(主机身份)** — `ErrorReporter::ReportError` 根据 `ReportErrorRequest.{slice_id, host_id}` 构造 `worker_id = "slice<slice_id>-task<host_id>"`,这是扁平 DCN 主机身份([集群元数据](fleet-metadata/host-identity.md))。
- **内层(主机内任务)** — `AddError` 追加 `"/" + err.task_id`,其中 `task_id` 是 `MegaScaleRuntimeError` 字段 9(偏移 `+0x58`),即该主机内的 Borg task / 进程索引。
因为提前触发检查会将总体限制为 `expected`,而键会折叠重复项,所以 map 自然只保存每个预期报告者的一条记录;重试 `ReportError` 的主机(例如 transport 重传)会合并到现有条目,而不会膨胀 `size()`。
### 函数映射
| 函数 | VA | 角色 |
|---|---|---|
| `MegascaleErrorAggregator::AddError` | `0x1ccba940` | 互斥锁、CANCELLED 短路、构造键、upsert |
| `MegascaleErrorAggregator::size` | `0x1ccba900` | 在互斥锁(+744)下读取 `linked_hash_map.size()` |
| `MegascaleErrorAggregator::active` | `0x1ccba8c0` | 在互斥锁(+744)下读取 `!shutdown_` |
---
## Drain 和分类 — `ProcessAndShutdown`
### 目的
`ProcessAndShutdown` 是一次性 drain,会把累积的 map 转换成最终的分类 `ErrorDigest`。它遍历每个已存错误,构造五个 `btree_set` 索引(罪魁 worker、故障网络链路、sequencer tag/pc 状态、按 tag/pc 的 worker 倒排索引、程序信息),在完整跨主机集合上运行事后 `Cause` 分类器,缓存结果,设置 `shutdown_`,并按值返回摘要。它是**幂等的**:第二次调用返回缓存副本。运行后,聚合器即死亡。
### 算法
```c
function ProcessAndShutdown() -> ErrorDigest: // sub_1ccbaba0
mu_.lock()
if shutdown_: // line ~817 fast path
out = cached_digest_; mu_.unlock(); return out // idempotent / poisoned-by-CANCELLED
LOG(INFO) "Processing $0 errors", size() // line 1134
digest.local = WorkerInfo(job_name, global_id, xid) // +0x00/+0x60/+0x68 — identify the coordinator
for (key, err) in errors_by_worker_launch_: // arrival order preserved
st = err.runtime_state.rapid_eye_info // optional<RapidEyeInfo>
note chip_config_name == "default" // 8-byte compare @ 1ccbaf56
populate workers_set, hlo_set, faulty_link_set,
sequencer-state set, workers-by-(tag,pc) index
build OffloadedSparseCoreTagPc entries by HLO
digest.cause = ClassifyCause(...) // see decision order below
cached_digest_ = digest // +0x128
shutdown_ = true
mu_.unlock()
return digestCause 判定顺序
分类器恰好发出一个 Cause。在 5320 字节函数体中观察到的判定顺序如下(相邻 cause 之间的精确平局打分尚未完全完成数据流提取 — MEDIUM):
any error_type == UNRECOVERABLE_ERROR (2) -> UNRECOVERABLE_ERROR
any worker chip_id == -1 -> PROGRAM_NOT_QUEUED
FaultyNetworkLink set non-empty -> NETWORKING_ISSUE
"default" config DMA-stage stall pattern -> DATA_INPUT_STALL
module fingerprints differ across workers -> DIFFERENT_MODULE
fingerprints match but tag/pc layouts differ -> FINGERPRINT_MISMATCH
TC stall pattern present -> BAD_TPU_CHIP
SC stall pattern present -> BAD_SC_CHIP
otherwise -> UNKNOWN_CAUSE
```text
> **注意 —** 每个 `Cause` 都是协调器基于完整集合计算的*集群裁决*;不存在主机本地 cause。`DIFFERENT_MODULE` 和 `FINGERPRINT_MISMATCH` 是仅有的真正集群作用域 cause — 没有比较多台主机的报告就无法判定,这也正是聚合器作用域为整个作业的原因。`NETWORKING_ISSUE` 是主机对作用域(它命名一条故障 `src→dst` 链路);芯片/停滞 cause 会命名单个罪魁主机,但仍然只在完整集合到达后发出,这样才能构造“following hosts”列表。按 [ICI 失败恢复](../ici/failure-recovery.md) 提升的芯片级 ICI fatal,一旦其 `FaultyNetworkLink` 落入集合,就会在这里表现为 `NETWORKING_ISSUE`。
### 消费方侧 — `LogErrorDigest`
`LogErrorDigest`(`0x213b42c0`,位于 `.text.unlikely`)是一个基于 `digest.cause` 的跳转表:每个 cause 都有一个内置 LOG(ERROR) 模板和一个 `FormatWorkers(...)` 主机列表。二进制中已验证存在的示例:`BAD_TPU_CHIP` → "Megascale detects a hang that is likely caused by bad TPU tensor core chips on the following hosts. Please remove the hosts from the fleet and restart the workload."(`0xa05862f`),后接 " The host that have bad TPUs are: "(`0xa273611`);`NETWORKING_ISSUE` → "…likely caused by a networking issue. Please examine the underlying networking stack for the following hosts."(`0x9ffc2f2`);`UNKNOWN_CAUSE` → "Megascale detects a hang but cannot determine the root cause. Please inspect the full digest below."(`0x9fe02ae`),外加完整文本 proto。协调器的 LOG(ERROR) 轨迹是摘要的主要*人工*消费者。
### 函数映射
| 函数 | VA | 角色 |
|---|---|---|
| `MegascaleErrorAggregator::ProcessAndShutdown` | `0x1ccbaba0` | 构造/缓存摘要、分类、设置 `shutdown_`(幂等) |
| `MegascaleErrorAggregator::LogErrorDigest` | `0x213b42c0` | 按 cause 切换的 LOG(ERROR) 发射器 + `FormatWorkers` |
| `ErrorDigest::ToRapidEyeErrorDigestProto` | `0x1ccb8560` | `ErrorDigest` → 线格式 proto |
| `(anon)::FormatWorkers` | `0x1ccba060` | 渲染 `btree_set<WorkerAndCoreInfo>` 主机列表 |
| `(anon)::ToWorkerAndCoreInfoProto` | `0x1ccc4f80` | 罪魁 worker → proto 消息 |
---
## 线格式 — `RapidEyeErrorDigestProto`
### 目的
聚合器持久化的单个序列化产物是 `xla.megascale.runtime.RapidEyeErrorDigestProto`。它是一个包含 17 个有效字段的根消息,携带 13 个嵌套消息类型和一个 9 值 `Cause` 枚举,从 `protodesc_cold` VA `0xc169340` 处的 `rapideye_logging.proto` FileDescriptorProto(4 460 字节序列化)恢复而来。其语法为 `editions`(Google 的 proto2/3 后继)。关键在于,摘要会**逐字嵌入入站 `MegaScaleRuntimeError`**(字段 11,`first_recorded_error`),因此记录是自描述的 — 下游读取者无需单独的入站错误清单即可解释裁决。
### 根消息
| 字段 # | 名称 | 标签 | 类型 |
|--------:|------|-------|------|
| 1 | `potential_cause` | optional | enum `Cause` |
| 2 | `potential_culprit_workers` | repeated | `WorkerAndCoreInfo` |
| 4 | `workers_by_tpu_states` | repeated | `WorkersByTpuState` |
| 5 | `all_workers` | repeated | `WorkerInfo` |
| 7 | `timestamp_ns` | optional | int64 |
| 11 | `first_recorded_error` | optional | `MegaScaleRuntimeError`(逐字嵌入) |
| 12 | `error_messages` | repeated | `ErrorMessage` |
| 13 | `graph_consolidater_output` | optional | `GraphConsolidaterOutput` |
| 14 | `event_id` | optional | fixed64 |
| 15 | `xid` | optional | int64(来自 `getenv("XM_XID")`) |
| 16 | `executable_by_modules` | repeated | `ExecutableByModules` |
| 17 | `app_type` | optional | string |
| 18 | `faulty_network_links` | repeated | `FaultyNetworkLink` |
| 19 | `offloaded_sparse_core_tag_pc` | repeated | `OffloadedSparseCoreTagPc` |
| 20 | `build_info` | optional | `BuildInfo` |
| 21 | `launches` | repeated | `Launch` |
| 22 | `runtime_error_ids` | repeated | fixed64 |
字段号 3、6、8、9、10 是空洞 — 已退役字段。依赖项 `megascale_status.proto` 提供 `MegaScaleRuntimeError`;`actions.proto` 提供嵌入在 `BottleneckNode.action` 中的 `MegaScaleAction`。
### `Cause` 枚举
九个值,密集 `0..8`;名称存在于 `protodesc_cold` 表中(通过 `libtpu.so` 字符串扫描验证了 `FINGERPRINT_MISMATCH`、`PROGRAM_NOT_QUEUED` 等):
| 值 | 名称 | 含义 | 证据局部性 |
|------:|------|---------|---|
| 0 | `UNKNOWN_CAUSE` | 分类器耗尽,未锁定根因 | 集群(耗尽) |
| 1 | `BAD_TPU_CHIP` | 挂起很可能来自故障 tensor-core 芯片 | 逐主机芯片(命名主机) |
| 2 | `FINGERPRINT_MISMATCH` | worker 间的 HLO 编译输出不一致 | **集群范围**(需要比较) |
| 3 | `DATA_INPUT_STALL` | 数据流水线饥饿 | 逐主机(数据流水线) |
| 4 | `UNRECOVERABLE_ERROR` | 至少一条报告是 `UNRECOVERABLE` | 逐主机(一台主机足够) |
| 5 | `DIFFERENT_MODULE` | worker 运行了不匹配的 HLO module | **集群范围**(需要比较) |
| 6 | `NETWORKING_ISSUE` | 检测到 ICI/DCN 链路失败 | 主机对(`src→dst`) |
| 7 | `BAD_SC_CHIP` | 挂起很可能来自故障 sparse-core 芯片 | 逐主机芯片(命名主机) |
| 8 | `PROGRAM_NOT_QUEUED` | 某个 worker 从未到达 launch 队列(`chip_id == -1`) | 逐主机 |
### 嵌套消息树
下面按形状而非穷举转储描述 13 个嵌套消息;其中诊断关键项如下:
```text
RapidEyeErrorDigestProto
├─ WorkerAndCoreInfo {1 worker_id, 2 host_name, 3 CoreInfo?} ← culprit naming
│ └─ CoreInfo {1 chip_id, 2 core_idx, 3 physical_location} ← only physical chip name in the layer
├─ WorkerInfo {1 worker_id, 2 host_name, 3 cloud_instance_id}
├─ WorkersByTpuState {1 TpuState, 2 [WorkerAndCoreInfo]} ← sequencer-state grouping
│ └─ TpuState {1 tag, 2 pc, 3 hlo_name, 4 computation_name, 5 [ProgramCompletion]}
├─ Launch {1 launch_id, 2 [WorkersByTpuState], 3 [Executable]}
├─ Executable {1 fingerprint, 2 [Launch], 3 module_name, 4 sample_worker}
├─ ExecutableByModules {1 module_name, 2 [Executable]}
├─ FaultyNetworkLink {1 src_worker:WorkerInfo, 2 dst_worker:WorkerInfo} ← host-granular fault
├─ GraphConsolidaterOutput {1 problematic_workers, 4 workers_with_missing_graphs,
│ 5 [BottleneckNode], 6 [FaultyNetworkLink]}
├─ ErrorMessage {1 worker:WorkerAndCoreInfo, 2 error_message}
├─ OffloadedSparseCoreTagPc {1 hlo, 2 [SparseCoreTagPcToWorkers]}
└─ BuildInfo {1 changelist, 2 baseline_changelist, 3 build_timestamp_secs,
4 build_label, 5 build_target, 6 build_id}注意 —
WorkerAndCoreInfo内的CoreInfo {chip_id, core_idx, physical_location}是跨主机层中唯一以物理方式命名芯片的位置,并且它是诊断输出,而不是启动库存的一部分。FaultyNetworkLink命名的是主机(WorkerInfo),不是 ICI 链路。因此,当BAD_TPU_CHIP/BAD_SC_CHIP裁决命名一台主机后,operator 必须继续调用tpunetd的GetCoreDump(CORE_DUMP_CHIP_DUMP)来拉取芯片级寄存器状态 — 摘要知道哪台主机,tpunetd 知道哪颗芯片。参见 屏障错误用法。
嵌入的 MegaScaleRuntimeError(字段 11 / ReportErrorRequest 字段 3)
逐主机报告,来源 megascale_status.proto,语法 editions:
| 字段 # | 名称 | 类型 | 备注 |
|---|---|---|---|
| 1 | error_type | enum ErrorType | 驱动 CANCELLED 短路和 HANG abort |
| 2 | error_message | string | |
| 3 | estimated_start_time_ms | uint64 | |
| 4 | runtime_state | RuntimeStateSummary | 携带分类器使用的 RapidEyeInfo |
| 5 | triggering_launch_id | int32 | |
| 6 | hostname | string | |
| 7 | user | string | |
| 8 | job_name | string | |
| 9 | task_id | int32 | 内层去重键段(+0x58) |
| 10 | task_uid | int64 | |
| 11 | event_id | fixed64 | 相关 ID,不是去重键 |
| 12 | unrecoverable_error_type | enum UnrecoverableErrorType | |
| 13 | runtime_error_id | fixed64 | 相关 ID → 根字段 22 |
enum ErrorType { NO_ERROR=0; HANG_DETECTED=1; UNRECOVERABLE_ERROR=2; CANCELLED=3; }
enum UnrecoverableErrorType { UNCLASSIFIED=0; HOST_TO_DEVICE_ERROR=1; DEVICE_TO_HOST_ERROR=2; }
```text
`error_type` 是*逐报告*类别;根 `Cause` 是*跨主机裁决*。它们是不同轴线 — 一组 `HANG_DETECTED` 报告可以根据逐主机 `RapidEyeInfo` 状态被分类为九个 cause 中的任何一个。`event_id` 和 `runtime_error_id` 是用于跨集群相关(OutKast / RapidEye)的每次收集唯一标识符,明确**不是**去重键。
---
## 保留
### 目的
保留策略有意保持最小:只保存错误到足以 drain 一次风暴。主存储是内存中的 `linked_hash_map`;可选的 `RapidEyeLogger` sink 会将序列化摘要写入磁盘;进程重启后没有任何内容保留。
### 模型
```text
default (no flags): in-memory linked_hash_map → LOG(ERROR) on drain → freed
(NullRapidEyeLogger drops all WriteRecord calls)
--megascale_rapideye_error_digest_log_path=<p>:
in-memory map → ProcessAndShutdown →
CloudRapidEyeLogger.WriteRecord(RapidEyeErrorDigestProto) → <p>
(scheme picks backend: gs:// → GCS, /cns/ → CNS, local → Posix)| 方面 | 行为 |
|---|---|
| TTL | 无 — 一直存在到 ProcessAndShutdown |
| 数量上限 | 无 — linked_hash_map 增长到每个唯一键一条记录 |
| 字节上限 | 仅逐错误,在主机层(截断,绝不丢弃) |
| Ring / LRU | 无 |
| 跨重启 | 无 — ctor 不加载;日志只写 |
| Drain | 一次性;ProcessAndShutdown 清空并消亡 |
怪癖 —
linked_hash_map是按设计无界的。没有 LRU、没有环形缓冲区、没有驱逐 sweep — 以前存在的、用于限制已记录错误数量的num_workers_to_log_errorsflag 已退役(可通过_GLOBAL__sub_I_megascale_error_aggregator.cc0x213560a0中的absl::flags_internal::Retire看到)。Map 的边界只来自去重键折叠逐主机重复项,以及在每个预期 worker 都已报告的瞬间 drain 它的size() == expected提前触发。如果重新实现省略提前触发,只依赖 300 ms 空闲 deadline,那么风暴会一直留在内存中,直到 300 ms 内没有新报告;对于一个 1 000 芯片 pod,最坏情况是每个 worker 一个约 112 字节的MegaScaleRuntimeError加一个 24 字节字符串,约 150 kB 加消息载荷 — 有界,但只是因为 map 是每 worker 一条记录,而不是因为有任何驱逐。
粘性的首个错误槽
+0x70 处的 first_recorded_error_ 只设置一次,在第一个非 CANCELLED AddError 上设置,之后绝不覆盖。它会成为 RapidEyeErrorDigestProto.first_recorded_error(字段 11),并作为 abort 判定的基础(abort_on_hang 取决于 first_recorded_error.error_type == HANG_DETECTED)。因此,聚合器同时保留根因(首个错误,粘性)和完整清单(完整 map,last-write-wins)— 裁决采用 first-error-wins,取证记录则保留所有内容。
函数映射
| 函数 | VA | 角色 |
|---|---|---|
MegascaleErrorAggregator::WriteErrorDigestToStorage | 0x1ccb83e0 | 空路径跳过;否则 RapidEyeLogger::Create + WriteRecord |
RapidEyeLogger::Create | 0x20511aa0 | 从 options 得到 StatusOr<unique_ptr<RapidEyeLogger>> |
NullRapidEyeLogger::WriteRecord | 0x20511d40 | 默认 sink — 丢弃所有写入 |
CloudRapidEyeLogger::WriteRecord | 0x20511e00 | tsl::FileSystem 支撑的写入 |
类布局 — MegascaleErrorAggregator(752 字节)
752 字节(0x2f0)堆对象,16 字节对齐分配。与作用域相关的偏移:
| 偏移 | 类型 | 字段 |
|---|---|---|
+0x00 | std::string | job_name(协调器上为 "McJax") |
+0x18 | std::string | rapideye_log_dir(FLAG 副本) |
+0x30 | bool | shutdown_(active() = !shutdown_) |
+0x50 | AnyInvocable<void(int,int,MegaScaleRuntimeError const&)> | 逐错误 unicast callback |
+0x60 | int64_t | global_id_(util::random::NewGlobalID()) |
+0x68 | int64_t | xid_(getenv("XM_XID")) |
+0x70 | MegaScaleRuntimeError(0x70) | first_recorded_error_(粘性) |
+0xe0 | bool | has_first_recorded_error_ |
+0xe8 | linked_hash_map<string, MegaScaleRuntimeError> | errors_by_worker_launch_(存储) |
+0x128..+0x2e7 | ErrorDigest(0x1c0 = 448 B) | cached_digest_(drain 时填充);一直跨到 mu_。独立 ErrorDigest 已在 PJRT ProcessAndShutdown 0xe6bae40 中确认 operator new(0x1C0) |
+0x1a8..+0x2b0 | 5 个 btree_set / btree_map | ProcessAndShutdown 期间构造的索引;是 cached_digest_ 内部子字段(不是独立聚合器成员) |
+0x2e8 | absl::Mutex | mu_ — 唯一锁(this+744) |
注意 —
+0x2e8处的一个absl::Mutex保护每个 public 方法 —AddError、ProcessAndShutdown、size、active都会锁定this+744。没有无锁路径。协调器的ErrorReporter添加了一个单独的TracedMutex(kind=14),因此惰性分配、deadline 设置、AddError 和提前触发检查相对于并发 deadline 触发是原子的;deadline fiber 会重新获取该互斥锁,而ProcessErrorDigest(0x1ccb7140)处的if (!aggregator) return幂等保护会处理与 drain 竞争的晚到报告。
告警和计数器
两个协调器层 flag 在摘要发出后控制进程 abort(abort 会进入正常 Borg/GKE crash-loop 重启流水线 — 两个 flag 都不会直接分页):
| Flag | 默认值 | 效果 |
|---|---|---|
--megascale_error_reporter_abort_on_hang | false | 如果 first_recorded_error.error_type == HANG_DETECTED,则 LOG(FATAL)("Aborting the coordinator after collecting errors from all workers…", 0xa045d37) |
--megascale_error_reporter_abort_on_error | false | 摘要之后对任何已报告错误 LOG(FATAL) |
--megascale_error_aggregation_enabled | true | 总开关;false → ctor 设置 shutdown_,因此每个 AddError 都是 no-op |
--megascale_rapideye_outkast_culprit_hosts | false | 将罪魁主机列表转发给 OutKast,用于跨集群相关 |
streamz 计数器 /platforms/xla/megascale/runtime/error_count(也包括 /platforms/xla/megascale/error_count 和 platforms.xla.megascale.error_count)在每次 AddError 时递增;/platforms/xla/megascale/runtime/aggregated_error 在每次 ProcessAndShutdown 时递增一次,并按 (error_type, cause) 加键(metadata 字符串逐字带有拼写错误:"The number of error reported by the MXLA errore aggregator.")。两个不同的匿名命名空间 BSS 槽支撑逐实例 error_count,分别位于 0x2257b080(gRPC 路径)和 0x22589a40(host/PJRT 路径)。
聚合错误如何浮回 PJRT
摘要通过四条路径到达 PJRT 客户端,每条路径都是不同消费者:
- 协调器 LOG(ERROR) 轨迹 —
LogErrorDigest发出 cause 裁决和逐 cause 诊断文本;这是读取协调器日志的主要人工消费者。 - 进程内 unicast callback — 传给
ErrorReporterctor 的AnyInvocable<void(int slice, int host, MegaScaleRuntimeError const&)>会针对每个单独错误触发(不是针对每个摘要);典型消费者是运行时错误处理器,它将失败传播回 PJRT executor(JAX/TF/XLA),因此用户代码会看到失败的计算,而不是静默挂起。 MegascaleDebugService.ConsumeErrorgRPC —ConsumeErrorRequest{}(空)→ConsumeErrorResponse{error: MegaScaleRuntimeError};调试器使用的“给我下一个错误”检查路径。由DebugManager::ConsumeRuntimeError(0x204d8f20)支撑,后者是 drain-once:读取 pending 槽会重置它。- RapidEye 磁盘归档(可选)—
CloudRapidEyeLogger写入序列化 proto,供下游批处理作业使用(事故分析、sawmill 摄入、OutKast 打分)。
PJRT C-API shim(PJRT_Megascale_ErrorAggregator_*)是第五个表面:JAX/XLA 进程直接驱动 AddError/ProcessAndShutdown,并在进程内读取 448 字节 ErrorDigest,不涉及协调器。
未追踪 / 待解决
- 当两个相邻 cause 同时触发时,
ProcessAndShutdown内的精确平局打分(例如BAD_TPU_CHIPvsFINGERPRINT_MISMATCH)— 5320 字节函数体已恢复,但完整数据流提取仍待完成(MEDIUM)。 --max_rapid_eye_runtime_error_size_bytes的数值默认值 — 这是一个 int flag,其默认值位于.data中且没有 generator fn;需要.data交叉引用(LOW)。RuntimeStateSummary/RapidEyeInfo.chip_config_nameschema — 分类器依赖"default"字符串(0x1ccbaf56处 8 字节比较),但上游RapidEyeInfoschema 只部分解码({1 telemetry, 2 tpu_core_hlos, 3 is_truncated, 6 slice_id, 7 task_id, 10 chip_config_name, …})。megascale_xmon_gateway_address消费者(除 LOG/RapidEye/callback 外可能的第三条 push 路径)— 该 flag 在此 TU 中注册,但其读取点尚未追踪(LOW)。BottleneckNode.action内的MegaScaleAction线格式(包含 7+ 个备选项的 proto oneof)— 目前只编目了顶层消息图。
交叉引用
- Megascale 概览 — 本节开篇;聚合器在多主机/DCN runtime 中的位置。
- ICI 失败恢复 — 下一层;芯片/链路故障在到达
AddError前如何变成MegaScaleRuntimeError。 - 屏障和错误聚合集群元数据用法 — 聚合器如何只消费扁平 DCN 层(
NumHosts()、(slice_id, host_id));主机与芯片之间的取证交接。 - 跨主机屏障 — 兄弟集群协调器;共享
(slice_id, host_id)身份和NumHosts()预期数量来源。 - tpunetd 协议 —
GetCoreDump(CORE_DUMP_CHIP_DUMP),即BAD_TPU_CHIP/BAD_SC_CHIP裁决命名主机后的芯片级后续操作。 - 主机身份 — 成为外层去重键段的
(slice_id, host_id)DCN 身份。