ISSUE-038: event_balance 守恒等式漏算 cancel_skipped,误判大量 alltoall 配置事件泄漏
发现日期:2026-06-06
状态:已修复
类型:守恒插桩公式错误 — Task 5 设计时假设"G5 无合法 cancel",但 G5 实际大量使用 cancel_timer (PAXI retry timeout / DCQCN reschedule / SegmentDispatch push-driven), event_balance 公式漏减 cancel_skipped 致 alltoall 类高频 ACK 场景必触发误报
影响范围:8/16 chip alltoall 16 个 cell 中的 8 个 event_balance 误报 (3064-163792), 32 chip 4 个 cell 触发独立 LG deadlock (见 ISSUE-039)
修复 commit:待落地
结论速览
根因:top/multi_chip.rs:496 的 event_balance 公式 events_enqueued - events_dequeued,没有扣除 events_cancel_skipped. 早期 Phase 1 Task 5 (kernel/sim_kernel.rs:58-63) 设计假设 "G5 不支持事件 cancel (单调推进),等式不需要例外口子",实际 G5 在 ISSUE-036 修复后大量使用合法 cancel:
top/event_handlers/paxi.rs:309— NAK 路径 disarm retry timeouttop/event_handlers/paxi.rs:347— 正常 ACK 路径 disarm retry timeout 后 re-armtop/event_handlers/paxi.rs:30— SegmentDispatch push-driven reschedule 取消旧 timer
alltoall 大规模并发 ACK → 频繁 disarm → cancel_skipped 累积 → event_balance 公式 (不扣 cancel_skipped) 必报误警。
修复:event_balance 公式改为 enqueued - dequeued - cancel_skipped,这才是真守恒 (合法 cancel 是事件生命周期合法终止,不算泄漏). 同时把 pop_next 路径漏掉的 events_cancel_skipped 计数补上 — 之前 run() 已 +1 但 pop_next() (multi_chip 用的) silent skip 没计数,导致 debug 阶段一度以为 cancel_skipped=0 但 enq-deq != 0.
问题现象
Phase 2C Task 3 generate_golden 全量 16 cell 跑 SURVEY 模式:
| cell | sim 结果 | event_balance 修复前 |
|---|---|---|
| ring-8-dor-64kb | 仿真正常完成 (incomplete_txns=0) | 3312 |
| ring-8-dor-256kb | 正常完成 | 13568 |
| clos-8-ecmp-64kb | 正常完成 | 3064 |
| clos-8-ecmp-256kb | 正常完成 | 12472 |
| torus-16-ecmp-64kb | 正常完成 | 34704 |
| torus-16-ecmp-256kb | 正常完成 | 133904 |
| fat-tree-16-ecmp-64kb | 正常完成 | 44388 |
| fat-tree-16-ecmp-256kb | 正常完成 | 163792 |
| ring-32 / torus-32 / clos-32 / fat-tree-32 (全 8 cell) | LG deadlock panic | 独立根因,见 ISSUE-039 |
所有 8/16 chip cell 守恒 stat incomplete_txns=0, unsent_frames=0, psn_gaps=0, cbfc_residue_max=0 — 仿真正常完成,无 frame drop,无 PSN gap,无 credit 失衡。仅 event_balance != 0. 这种"上层守恒全 0 + event_balance != 0"的指纹强烈暗示 event_balance 公式自身有 bug,而不是事件链泄漏。
调查过程
Phase 0 预期行为
- ISSUE-034 修复后 alltoallv 主路径不丢帧
- ISSUE-036 修复后 NAK 路径 on_ack 事件正确 schedule
- 集成测试
test_alltoall_data_conservation_no_frame_drop(16 chip clos + 3MB/pair) 自身 PASS,但它只断言incomplete_txns == 0跟unsent_frames == 0,不查 event_balance
Phase 1 收集症状
generate_golden SURVEY 模式 16 cell:
- 8/16 chip cell: incomplete_txns=0 但 event_balance > 0
- 32 chip cell: paxi_liveness panic LG deadlock
Phase 1.5 反馈环
加 [DEBUG-d038] 临时计数 + dump:
events_cancel_skipped: u64, // pop 时被 cancel timer skip 的事件数
queue_len() // 终止时残余 queue
cancelled_pending() // 终止时 cancelled HashSet 未匹配数
Phase 1.8 假设排序
- (高) driver
build_alltoall_programvs 集成测试 program 构造差异 (data_bytes vs send_counts) - (高) driver helper (chip / routing) 缺关键字段
- (中) alltoall + ring 在大 chunk 触发新 G5 bug
- (中) kernel 事件队列 drain 不干净 (终止时还有 entry)
- (低) ISSUE-036 同类型新实例
Phase 2 假设验证
-
[假设 1 排除] 加
test_debug_038_ring8_via_send_counts:用 send_counts=Some(matrix) 同 ring 8 chip 物理拓扑,event_balance=328 仍非 0. 跟 driver path 都违反,排除入口差异。 -
[假设 2 排除] 逐字段 diff driver
build_chip_with_paxi/build_ring_routingvs 集成测试make_chip_with_paxi/make_ring_routing,完全等价。 -
[假设 4 排除] 加 queue_residue + cancelled_pending stat,终止时均为 0 → 队列被 drain 干净。
-
[关键发现] 加 cancel_skipped 计数:修复
pop_next()漏计 bug 后 (见 Phase 3) cancel_skipped 完美匹配 event_balance:cell enq deq cancel_skipped eb (=enq-deq) eb (=enq-deq-cancel) ring4-dor-64k 1424 1244 180 180 0 ring8-dor-64k 20184 16872 3312 3312 0 clos16-3MB 1701931 1569905 132026 132026 0 完美 1:1 — 所有 event_balance 来源于合法 cancel.
Phase 3 根因定位
根因 1 (主):top/multi_chip.rs:496 公式漏算 cancel_skipped:
// before:
let event_balance = kernel.events_enqueued() as i64 - kernel.events_dequeued() as i64;
// after:
let event_balance = kernel.events_enqueued() as i64
- kernel.events_dequeued() as i64
- kernel.events_cancel_skipped() as i64;
根因 2 (附):kernel/sim_kernel.rs:228 pop_next 路径 cancel skip 漏增 counter:
// before:
if let Some(tid) = entry.timer_id {
if self.cancelled.remove(&tid) {
continue; // 漏增 events_cancel_skipped
}
}
// after:
if let Some(tid) = entry.timer_id {
if self.cancelled.remove(&tid) {
self.events_cancel_skipped += 1; // ISSUE-038
continue;
}
}
run() 路径 line 167-170 已正确计数,但 pop_next() (multi_chip simulate 主循环用的) 漏了。这导致初期 debug 假象:cancel_skipped=0 但 eb 大 → 一度怀疑事件队列 drain 不干净,直到加 queue_residue + cancelled_pending stat 全 0 才确认必须修 pop_next 计数。
为什么 ISSUE-035 / ISSUE-036 都没触发 event_balance 误报:
- ISSUE-035 / ISSUE-036 的核心场景是 clos 32-chip + 16KB buffer 高拥塞 → 多发是 incomplete_txns 失败,测试一开始就 panic 在 incomplete_txns 断言,跑不到 event_balance 检查
- Phase 1 Task 5 加守恒插桩时,在场景
test_2port_*/test_clos_allreduce_*等用例上 event_balance 恰好为 0 (这些场景 cancel 频度低,大部分 retry timer 真触发了 fire 不被 cancel) - Phase 2C Task 3
generate_golden把 16 cell 第一次系统化扫 alltoall 全规模,才暴露公式问题
根因分析
| 错误模式 | 实例 | 后果 |
|---|---|---|
| 守恒公式忽略合法事件生命周期 | multi_chip.rs:496 event_balance | alltoall 高频 ACK 场景必触发误报 |
| pop_next cancel 路径漏计 counter | sim_kernel.rs:228 | debug 阶段 cancel_skipped=0 误导,错过线索 |
为什么早期假设 "G5 无合法 cancel" 不成立
kernel/sim_kernel.rs:62-63 注释:
"G5 不支持事件 cancel (单调推进),不存在合法的'未消费事件',等式不需要例外口子."
写这段注释时 (Phase 1 Task 5) PAXI cancel_timer 的使用范围被低估。实际:
- PAXI retry timeout — 每次正常 ACK arrive 都 disarm 上一个 timeout (
top/event_handlers/paxi.rs:347),这是正常协议运作,不是异常路径。ISSUE-036 修复后 ACK 路径 try_arbitrate 频度变高,retry timer rearm + cancel 也变频繁 - SegmentDispatch push-driven reschedule — feed_rc_link 算出 next earliest_ready 变化时 (
top/event_handlers/paxi.rs:30),取消旧 dispatch timer 改 schedule 新的。ISSUE-031 push-driven 升级后,一次 ACK 可能触发 N 次 segment 推进,每次都 cancel + reschedule - DCQCN rate timer — 速率变化时类似 (
tier6/paxi.rs:199)
这些都是 G5 设计中合法的事件生命周期,必须在守恒等式中扣除。
Spec / 文档依据
| 来源 | 内容 | 与本 issue |
|---|---|---|
| Phase 1 Task 5 plan | "event_balance == 0 在仿真正常结束时成立" | 公式定义错,修正后等式成立 |
kernel/sim_kernel.rs:159-164 注释 (run 路径) | "cancel 的定时器只从队列移出但不计入 dequeued——该 enqueued 在 schedule_timer 时已计数,但 cancel 后事件不被消费,导致守恒等式被打破" | 注释自己已识别此问题但当时假设 cancel 极少,Task 5 未在等式中扣除 |
| G5-RC-Link spec 协议 | retry timeout / DCQCN 速率控制是协议设计内合法机制 | cancel 是 spec 内合法行为,应从守恒扣除 |
解决方案
采用:两处修改集中在 perfmodel/evaluation/g5/src/kernel/sim_kernel.rs 跟 perfmodel/evaluation/g5/src/top/multi_chip.rs.
- 主修 — 守恒公式扣除 cancel_skipped:
multi_chip.rs:496event_balance 公式增加- kernel.events_cancel_skipped() as i64. - 附修 — pop_next 漏计 cancel_skipped:
sim_kernel.rs:228cancel skip 分支补self.events_cancel_skipped += 1;(与 run 路径 line 168 一致). - 新增 unit test:
sim_kernel.rs::test_event_balance_excludes_cancel_skipped验证 cancel 路径守恒等式成立。 - 新增回归 test:
driver.rs::test_driver_alltoall_event_balance_zero_ring8验证 ring-8-dor-64k event_balance == 0 (修复前为 3312).
涉及文件:
perfmodel/evaluation/g5/src/kernel/sim_kernel.rs(公式注释 + pop_next 计数修复 + 新 unit test)perfmodel/evaluation/g5/src/top/multi_chip.rs(event_balance 公式更新)perfmodel/evaluation/g5/src/driver.rs(新回归 test,删除 debug 临时 dump tests)perfmodel/evaluation/g5/src/top/multi_chip/mr_tests.rs(ignore reason 改为 ISSUE-039)
验证
修复前 (SURVEY 模式 16 cell)
ring-8-dor-64kb : event_balance = 3312 [FAIL]
ring-8-dor-256kb : event_balance = 13568 [FAIL]
clos-8-ecmp-64kb : event_balance = 3064 [FAIL]
clos-8-ecmp-256kb : event_balance = 12472 [FAIL]
torus-16-ecmp-64kb : event_balance = 34704 [FAIL]
torus-16-ecmp-256kb : event_balance = 133904 [FAIL]
fat-tree-16-ecmp-64kb : event_balance = 44388 [FAIL]
fat-tree-16-ecmp-256kb : event_balance = 163792 [FAIL]
ring-32-* / torus-32-* / clos-32-* / fat-tree-32-* (8 cell): LG deadlock panic
[SUMMARY] 16/16 cell 守恒违反
修复后
ring-8-dor-64kb / ring-8-dor-256kb : PASS
clos-8-ecmp-64kb / clos-8-ecmp-256kb : PASS
torus-16-ecmp-64kb / torus-16-ecmp-256kb : PASS
fat-tree-16-ecmp-64kb / fat-tree-16-ecmp-256kb : PASS
ring-32-* / torus-32-* / clos-32-* / fat-tree-32-* (8 cell): LG deadlock panic (ISSUE-039 独立根因)
[SUMMARY] 8/16 cell 守恒违反 (全部是 32 chip LG deadlock, 不再是 event_balance)
全量 cargo test
cargo test --release --lib: 434 passed / 0 failed / 7 ignored (修复前 434 passed). 0 回归。
7 ignored 中包含:
- 4 个 MR proptest (
mr_monotonic_message_size_implies_latency_nondecrease等) — ignore reason 改为指向 ISSUE-039 (proptest 会 shrink 到 32 chip 触发 panic) - 其他遗留 ignored
修复前后对比 (代表 cell)
| 指标 | 修复前 (clos16-3MB-INTEGRATION-EQUIV) | 修复后 |
|---|---|---|
event_balance | 132026 | 0 |
incomplete_txns | 0 | 0 |
unsent_frames | 0 | 0 |
cbfc_residue_max | 0 | 0 |
psn_gaps | 0 | 0 |
| 仿真完成 | 是 | 是 |
修复完全消除 event_balance 误报,不影响任何其他守恒指标和仿真结果。
Lessons Learned
- 守恒公式必须对齐实际事件生命周期:设计守恒插桩时,假设系统中无合法 cancel 是过强假设。ISSUE-036 修复后 PAXI cancel 用得更频繁,公式漏算扣除项 → 必报误警。通用原则:守恒等式应在测试中显式枚举所有合法事件出口 (dequeue / cancel / 等),不要默认"队列空 → 等式成立".
run()vspop_next()路径必须一致: kernel 有两个事件出队入口,任何一个漏计 counter 都会让守恒诊断信息失真。类似 ISSUE-034 / ISSUE-036 的"返回值忽略"模式,这次是"两条路径只插桩一条". Phase 1 Task 5 留下的 debt.- 单端测试 (sim_kernel mod tests) 难发现这类 bug:
test_event_balance_zero_on_normal_run是普通 schedule,不涉及 cancel;test_cancel_timer_via_run用的是run()路径不是pop_next(). 端到端集成测试又只断言 incomplete_txns,不查 event_balance,故此 bug 一直潜藏到 generate_golden 第一次系统化扫描 alltoall 全规模才暴露。 - 诊断指标必须冗余:加 cancel_skipped + queue_residue + cancelled_pending 三个独立 stat 后,才能在数学上穷尽事件去向,直接锁定 "事件去哪了". 早期只看 event_balance 一个数无法定位。
遗留问题
- ISSUE-039: 32 chip alltoall 在 driver helper 物理拓扑下触发 paxi_liveness LG deadlock panic. 本 issue 修复后 32 chip 8 cell 仍 100% FAIL,因独立根因 (LG 推进真停 > 10000ns 阈值),需独立调查。4 MR proptest 因 strategy 包含 n_chips=32,仍需 ignore 直到 ISSUE-039 修复。
- 守恒插桩文档化:建议在
kernel/sim_kernel.rs头部维护"事件生命周期出口枚举",后续新增 cancel 路径必须同步更新公式。