跳到主要内容

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 timeout
  • top/event_handlers/paxi.rs:347 — 正常 ACK 路径 disarm retry timeout 后 re-arm
  • top/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 模式:

cellsim 结果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 == 0unsent_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 假设排序

  1. (高) driver build_alltoall_program vs 集成测试 program 构造差异 (data_bytes vs send_counts)
  2. (高) driver helper (chip / routing) 缺关键字段
  3. (中) alltoall + ring 在大 chunk 触发新 G5 bug
  4. (中) kernel 事件队列 drain 不干净 (终止时还有 entry)
  5. (低) 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_routing vs 集成测试 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:

    cellenqdeqcancel_skippedeb (=enq-deq)eb (=enq-deq-cancel)
    ring4-dor-64k142412441801800
    ring8-dor-64k2018416872331233120
    clos16-3MB170193115699051320261320260

    完美 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_balancealltoall 高频 ACK 场景必触发误报
pop_next cancel 路径漏计 countersim_kernel.rs:228debug 阶段 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.rsperfmodel/evaluation/g5/src/top/multi_chip.rs.

  1. 主修 — 守恒公式扣除 cancel_skipped: multi_chip.rs:496 event_balance 公式增加 - kernel.events_cancel_skipped() as i64.
  2. 附修 — pop_next 漏计 cancel_skipped: sim_kernel.rs:228 cancel skip 分支补 self.events_cancel_skipped += 1; (与 run 路径 line 168 一致).
  3. 新增 unit testsim_kernel.rs::test_event_balance_excludes_cancel_skipped 验证 cancel 路径守恒等式成立。
  4. 新增回归 testdriver.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_balance1320260
incomplete_txns00
unsent_frames00
cbfc_residue_max00
psn_gaps00
仿真完成

修复完全消除 event_balance 误报,不影响任何其他守恒指标和仿真结果。


Lessons Learned

  • 守恒公式必须对齐实际事件生命周期:设计守恒插桩时,假设系统中无合法 cancel 是过强假设。ISSUE-036 修复后 PAXI cancel 用得更频繁,公式漏算扣除项 → 必报误警。通用原则:守恒等式应在测试中显式枚举所有合法事件出口 (dequeue / cancel / 等),不要默认"队列空 → 等式成立".
  • run() vs pop_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 路径必须同步更新公式。