ISSUE-036: clos 32-chip 小 buffer 场景 NAK 路径下 on_ack 事件丢失致 incomplete_txns=12
发现日期:2026-06-05
状态:已修复
类型:实现 bug — 事件返回值被静默丢弃 (与 ISSUE-034 同类 "返回值忽略" 模式)
影响范围:所有触发 DCQCN/NAK 的拥塞场景,Clos 32-chip + 小 RC link buffer (16KB) 是触发该 bug 的最小复现配置。理论上任何高拥塞场景 (incast / 大规模 alltoall) 都可能触发但被 ISSUE-034 类残留概率掩盖。
结论速览
根因:handle_ack_arrived NAK 分支 (top/event_handlers/paxi.rs:323) 调用 paxi.on_ack(...) 后用 let _ 丢弃返回的事件列表。on_ack 内部在 ACK 释放 slot 后对所有 LG 调一遍 try_arbitrate,命中时 try_arbitrate 已将 slot 置 WaitAck、tx_busy=true,并返回 RcFrameDone 事件。该事件被丢弃 → RcFrameDone 永不触发 → on_tx_done 永不调用 → tx_busy 永久挂在 true → 该 LG 后续所有 try_arbitrate 因 tx_busy=true 立即返回 None → 后续 process_nak 把别的 slot 翻回 WaitGrant 也永远发不出去 → 仿真终止时 incomplete_txns > 0。
因果链:
NAK 到达 -> handle_ack_arrived NAK 分支 on_ack 返回事件被丢
-> RcFrameDone 未 schedule -> on_tx_done 未触发 -> tx_busy 永久 true
-> 后续 try_arbitrate (含 on_credit_return / on_nak 后) 全部 tx_busy 短路
-> 仿真终止仍有 WaitGrant slot, txn.acked_frames < total_frames
与 ISSUE-034 的差异:ISSUE-034 丢的是 submit_frame 的 bool 返回值 (slot 满时 segment 被丢),修法是按 per-LG slot 容量取货保证 submit 永远成功。ISSUE-036 丢的是 on_ack 的 Vec<Event> 返回值 (frame 已仲裁成功但 RcFrameDone 没 schedule),修法是把事件列表正确 schedule。两条都是"上层调用方忽略下层关键返回值"造成的静默漏帧,守恒断言层 (incomplete_txns) 是抓住这两类 bug 的兜底。
附带修复 (refactor-proposal Q-5):DCQCN handle_dcqcn_timer 不调 try_arbitrate,速率恢复后无 wake 路径。NAK 把 slot 翻回 WaitGrant 后若 try_arbitrate 命中 rate-block,该 LG 进入静默死锁。修法是 DCQCN 计时器触发后显式 try_arbitrate 一次拉起。本修一并落地,防御性。
问题现象
Phase 1 Task 6 assert_conservation 套到 test_clos_32_chips_small_buffer (32 chip Clos + 16KB switch buffer + 4MB allreduce) 报:
multi_chip.incomplete_txns = 12
12 个 chip 各 1 个 txn 未完成,11 个 chip acked=2/8, 1 个 chip acked=5/8。unsent_frames > 0。
调查过程
Phase 1 收集症状
test_clos_32_chips_small_buffer(tests.rs:1151) 配置:32 chip Clos 8-per-tor, 4MB allreduce, switch buffer=16KB, ECN kmin=0.2 kmax=0.5, alpha=2.0, frame=1406B。- 跑出
incomplete_txns=12,测试因守恒断言 panic。
Phase 1.5 反馈环
临时 [DEBUG-d036] 插桩 dump 12 个未完成 txn 详细状态 (chip / lg / waitack / waitgrant / tx_busy / NAK 计数 / DCQCN 限速命中 / switch drops 聚合)。关键观察:
switch drops 总和 = 0→ 排除 switch 缓冲区溢出 (16KB 小 buffer 但 alpha 动态阈值未越线)。seg_queue_len=0 pending_queue_len=0→ PAXI 上游无堆积。- 12 个 chip 每个有 2 个 LG (lg 0/3) 卡住,每个 LG 5 个 slot 处于 WaitGrant 状态 (PSN 1-5)。
- 关键:这些 LG 的
tx_busy = true,与 WaitGrant slot 同时存在 — 说明上次 try_arbitrate 把 slot 仲裁成功 (置 WaitAck + tx_busy=true),但配套 RcFrameDone 没运行 (运行了会on_tx_done -> tx_busy=false)。 arb_busy计数表明后续 try_arbitrate 均因tx_busy=true立即返回 None。
Phase 1.8 假设排序
- (高) on_ack/on_nak 路径丢事件: NAK 处理路径调用
paxi.on_ack时事件被let _丢弃。 - (中) DCQCN rate-block 无 wake:速率恢复后无 try_arbitrate 重启 (refactor-proposal Q-5)。
- (低) switch buffer 溢出:排除 (drops=0)。
- (低) max_retry 达上限:排除 (
max_retry_hit=0, nak=2 < 配置 15)。
Phase 2 假设验证
- [假设 1 确认] 查
handle_ack_arrivedNAK 分支 (paxi.rs:304 原版):let (completions, _) = paxi.on_ack(...);— 第二项事件列表被丢弃。读PAXIBridge::on_ack实现 (paxi.rs:223-238) 确认其内部对每个 LG 调try_arbitrate,命中时返回(done_ns, RcFrameDone)— 这正是仲裁后必须 schedule 的 RcFrameDone,丢掉就形成tx_busy黑洞。 - [假设 2 确认]
handle_dcqcn_timer(paxi.rs:237-254 原版) 仅调on_dcqcn_timer更新速率,不调try_arbitrate。find_sendable的 rate-block 路径是 silent (返回 None,不产生 wake 事件)。结合假设 1 修复后,rate-block 仍可能在 NAK 路径触发独立死锁。 - [假设 3/4 排除] 见上。
Phase 3 修复
主修 (假设 1):
// before:
let (completions, _) = paxi.on_ack(lg_id, from_chip, qp_id, ack_psn, now);
// after:
let (completions, ack_events) = paxi.on_ack(lg_id, from_chip, qp_id, ack_psn, now);
schedule_f64_events(&ack_events, kernel);
附带修 (假设 2):handle_dcqcn_timer 末尾增加 try_arbitrate 调用,速率变化后给 WaitGrant slot 一次 wake 机会。
根因分析
| 错误模式 | 实例 | 后果 |
|---|---|---|
调用方忽略 on_ack 的事件返回值 | handle_ack_arrived NAK 分支 let _ | RcFrameDone 永不调度,tx_busy 永久 true, LG 死锁 |
| DCQCN rate-block 无显式 wake | handle_dcqcn_timer 不调 try_arbitrate | NAK 后 rate-block 同时发生时即使主路径修了仍可能锁住 |
为什么 32-chip + 16KB buffer 是最小复现配置
16KB 小 buffer + ECN kmin=0.2 → 触发 DCQCN/CNP 频度高 → NAK 频度高 → handle_ack_arrived NAK 分支被频繁打到。32 chip 规模让 ACK/NAK 时序更分散,提升"on_ack 内部 try_arbitrate 命中"的概率。规模小或 buffer 大时 NAK 频度低,bug 触发不到。
Spec / 文档依据
| 来源 | 内容 | 与本 issue |
|---|---|---|
| G5-RC-Link 传输层设计规格 line 228 | TYPE1_OST 共享 Slot 池 + per-LG 独立 | tx_busy 是 per-LG 串行化语义的核心状态 |
| 同 line 240 | "Slot 耗尽时新包入 segment_queue 排队,ACK 释放 Slot 后由 feed_rc_link 自动 drain" | 隐含 "ACK 释放路径必须完整推进仲裁" — 实现违反 (释放后 try_arbitrate 事件丢) |
| ISSUE-031/refactor-proposal.md Q-5 | "CNP 后 DCQCN 降速可能让 rate_check 条件变化,但 SegmentDispatch 仅看 segment.earliest_ready,不感知 rate-block. rate-block 解除的精确 wake 路径待补" | 假设 2 的来源,本次一并补 |
spec 设计正确,实现违反 spec 的隐含契约 (事件返回值是仲裁路径的硬性副作用,不能丢)。
解决方案
采用:两处修改集中在 perfmodel/evaluation/g5/src/top/event_handlers/paxi.rs。
- 主修 — NAK 路径 on_ack 事件归还:
handle_ack_arrivedNAK 分支把_接住的事件列表改为ack_events,紧接调schedule_f64_events(&ack_events, kernel)。 - 附带修 — DCQCN rate-block wake:
handle_dcqcn_timer末尾对当前 LG 调tx.try_arbitrate(now),把可能的RcFrameDone调度入 kernel。无可发包时try_arbitrate返回None性能等价。
涉及文件:perfmodel/evaluation/g5/src/top/event_handlers/paxi.rs (handle_ack_arrived NAK 分支 + handle_dcqcn_timer)。
验证
- 目标测试
test_clos_32_chips_small_buffer:incomplete_txns = 12 → 0,守恒断言完全通过 ✓ - 回归测试
cargo test --release全量:377 passed / 0 failed / 3 ignored (修复前 360 passed / 17 failed,全部 17 个 conservation-related 失败均消除) - ISSUE-034 守恒测试
test_alltoall_data_conservation_no_frame_drop:仍通过,未引入 alltoallv 主路径回归。
修复前后对比 (test_clos_32_chips_small_buffer):
| 指标 | 修复前 | 修复后 |
|---|---|---|
incomplete_txns | 12 | 0 |
unsent_frames | 48 | 0 |
psn_gaps | 96+ | 0 |
event_balance | ~635 | 0 |
| 测试结果 | FAIL (panic on incomplete_txns) | PASS |
Lessons Learned
- 返回值忽略是同类 bug 的高发位:ISSUE-034 (
submit_frame的bool) 和 ISSUE-036 (on_ack的Vec<Event>) 都是"上层调用方忽略下层关键返回值"。原则:任何"调用方必须 schedule 的事件"必须有明确归还路径,不能let _。Rust#[must_use]标注可以在编译期阻止此类回归 (后续考虑给paxi.on_ack加)。 - Phase 1 守恒断言层是抓 ISSUE-034 类残留场景的兜底:ISSUE-034 修了 alltoallv 主路径,但 Clos+小 buffer 是不同的极端组合,触发的是不同代码路径同类型 bug。若没有
incomplete_txns守恒断言,这个 bug 会继续静默存在 (sim_time>0 通过,数据残缺无人察觉)。 - 诊断信息 + 状态 dump 缺一不可:单看
incomplete_txns=12推不出根因。加tx_busy/waitgrant slots/ NAK 计数后才一眼看出 "tx_busy 锁住 + WaitGrant 不发" 异常组合,直接指向 on_ack 路径。 - dropped stash 仍可恢复:调试过程中误 drop 了 stash,通过
git fsck --no-reflogs --lost-found找回 WIP commit,git checkout <commit> -- <files>恢复 Phase 1 instrumentation 文件。
遗留问题
- 推荐加
#[must_use]到PAXIBridge::on_ack/on_nak/submit/on_credit_return的返回值,让编译器在调用方忽略时报警。本次未做避免引入大范围改动,后续可独立 issue 处理。 - DCQCN rate-block 唤醒已通过
handle_dcqcn_timer末尾try_arbitrate兜底,但更精确做法是按last_send_ns + min_interval调度精确 wake 事件 (refactor-proposal Q-5 原方案)。当前实现依赖 DCQCN timer 周期触发 (3μs),对极端 rate-block 时间敏感场景可能引入数 μs 延迟。可作为独立优化 issue。