ISSUE-040: Driver helper 32 chip × 256KB alltoall 真 liveness bug — cbfc_residue 永久残留
发现日期:2026-06-06
修复日期:2026-06-06
状态:已修复
类型:实现 bug — try_arbitrate / handle_rc_frame_done 之间 cumulative ACK 抢先释放 slots 导致预扣 credit 未退还,跨边界守恒计数永久残留
关联 issue:ISSUE-026 / ISSUE-034 / ISSUE-036 (返回值忽略 / 漏 schedule 同主题),本 ISSUE 不属于"返回值忽略"类,属"扣 credit 与 emit packet 异步,中间状态被 ACK 撕裂"类
结论速览
根因:RcLinkTx::try_arbitrate 在仲裁帧时立即扣 credit 并标 WaitAck,但实际 packet 上 wire 由 handle_rc_frame_done 事件回调延迟 emit (frame_send_delay ≈ 7 ns 后).
在两者之间的窗口里,cumulative ACK 可能释放刚被标 WaitAck 的 slots (典型场景:retry_timeout 触发的 process_nak 把"刚被对端 ACK 但 ACK 尚未抵达 TX"的 slots 翻回 WaitGrant → try_arbitrate 重扣 credit + 标 WaitAck → 紧接着真 ACK 抵达 → process_ack cumulative 释放这些 slots → 它们变 Empty,从 vc_pending 移除).
此时 handle_rc_frame_done 调用 slots_of_frame 找到 0 个 (或少于预扣的) slots,物理上不发包/少发包,但 try_arbitrate 已记的 credits_consumed_per_pair[(src,dst,vc)] 不会回滚 → RX 端无对应 receive_packet 触发 → 无 CreditReturn → 跨边界守恒 cbfc_residue_max != 0 永久残留。
因果链:
T=0 try_arbitrate: 命中 frame F (slots = WaitAck), 扣 credit N, RcFrameDone scheduled @T=7
T=Δ1 (Δ1 < 7) retry_timeout 触发 (误判: ACK 还在路上但已超时阈值) → process_nak 把 F 的 WaitAck slots 翻回 WaitGrant + push_front vc_pending
T=Δ2 (Δ2 < 7) try_arbitrate 二次命中 F (slots 又 WaitGrant 了) → 二次扣 credit N + 标 WaitAck → 新 RcFrameDone scheduled @T=Δ2+7
T=Δ3 (Δ3 < 7, Δ3 < Δ2+7) 原始包对应的真 ACK 抵达 TX → process_ack cumulative 释放 F 的 slots (状态变 Empty)
T=7 (原始) RcFrameDone 触发 → slots_of_frame 找到 0 个 slot → for-loop 不执行 → 0 个 packet emit, 但 credit 已扣
T=Δ2+7 (重发) RcFrameDone 触发 → 同上 → 0 个 packet emit, 二次扣的 credit 也没退
跨边界统计: TX consumed = 2N (两次扣) ; RX returned = 0 (这两次都没真 emit, RX 没看到对应 packet)
→ cbfc_residue_max += 2N (永久残留, RX 端永不可能回填)
与 ISSUE-034/036 的差异:那两条是"上层调用方丢了下层关键返回值" (bool / Vec
问题现象
ISSUE-039 阈值修完后,generate_golden 16 cell 跑出 4 失败:
| cell | incomplete_txns | unsent_frames | cbfc_residue_max | psn_gaps | event_balance |
|---|---|---|---|---|---|
| ring-32-dor-256kb | 0 | 0 | 189 | 0 | 0 |
| torus-32-dmodk-256kb | 0 | 0 | 189 | 0 | 0 |
| clos-32-ecmp-256kb | 0 | 0 | 313 | 0 | 0 |
| fat-tree-32-dmodk-256kb | 0 | 0 | 313 | 0 | 0 |
只有 cbfc_residue_max 非 0,其他守恒 KPI 全 0 — 说明仿真本身完成所有事务 (incomplete_txns=0),无静默丢帧 (psn_gaps=0),但 TX-RX credit 账目不平。
每对 (src, dst, vc) 残差恒定 (ring/torus 189, clos/fat-tree 313),与拓扑相关,与 chip pair 无关 — 强指向"系统性记账漏点" 而非"随机网络丢包".
调查过程
Phase 0 预期行为
G5-RC-Link 传输层规格 §CBFC 跨端约束:
TX 扣的 credit (
credits_consumed_per_pair) 必恒等 RX 返还的 credit (credits_returned_per_pair) — 跨 TX/RX 边界 per (src_chip, dst_chip, vc_id) 三元组对账,仿真终止时差值 == 0.
spec 隐含契约:"扣 credit ⇔ 物理上有 packet 上 wire ⇔ RX 端 receive_packet 触发 ⇔ RX 端记 returned credit". try_arbitrate 扣 credit 但未必 emit 是契约违反,spec 未显式覆盖此中间态。
Phase 1 反馈环 + 症状
generate_golden SURVEY=1 顺序跑 16 cell,修前 4 失败。加 [DEBUG-d040] 旁路 dump 三元组 top-3 残差 + 各 LG tx.consumed / rx.returned / dbg_accepted / dbg_nak / dbg_dup / nak_retx / max_retry_hit:
[DEBUG-d040] residue #0: src=29 dst=13 vc=0 residue=189 (consumed=17662 returned=17473)
[DEBUG-d040] chip=29 lg=0 tx.consumed=4514 ... sent=38223 nak=280 nak_retx=30584 max_retry_hit=74
[DEBUG-d040] chip=13 lg=0 rx.returned=4479 accepted=7650 nak=0 dup=30482
关键观察:
- RX
dbg_nak=0(实测 PSN gap 数 = 0) — RX 从未生成过 NAK packet. - TX
nak=280(process_nak 调用数) — 全部来自retry_timeout路径 (handle_retry_timeout在 ACK 超时未到达时自动调 on_nak). - RX
dup=30482— Go-Back-N 重传后 RX 把这些当 duplicate 接收 + 返还 credit. - TX
sent=38223vs RXaccepted+dup=38132,差 91 packet — TX 扣 credit 但 RX 未触发 receive_packet 的虚发量。
结论 (Phase 1 末):91 packet × ~2 (per pair) = ~189,与残差吻合。不是网络层丢包 (后文 Phase 2 假设 1 排除),而是 TX 在某条路径上扣 credit 但物理 emit 阶段没真发包。
Phase 1.8 假设排序
- (高排除) 网络层 silent drop (
handle_chip_relay/handle_rc_frame_done/handle_switch_ingress中next_hop或transmit返 None / switch admission drop). 加全路径 DROP 计数器,跑完一次 0 命中,排除。 - (高确认) try_arbitrate 与 handle_rc_frame_done 之间 cumulative ACK 抢先释放 slots (本 issue 根因). 分析:try_arbitrate line 218 标 WaitAck, line 204-206 扣 credit,然后 RcFrameDone scheduled at
now + frame_send_delay(≈ 7ns);这窗口内 retry_timeout 路径触发 process_nak (把 slots 翻 WaitGrant) + 二次 try_arbitrate (二次扣 credit) + 真 ACK 抵达 (cumulative 释放);handle_rc_frame_done 时slots_of_frame返回空,物理 0 emit. - (中排除) 0 字节 FACK 包路径 (RX 不返 credit, TX 扣 credit). 排除:driver alltoall pairwise 只 emit Transfer 不带 0-byte (Receive 是 Receiver 侧 Receive→Send 握手,数据真 alltoall 模式不含).
- (中排除) ECMP 路由表/路径多版本导致部分跳失链. 排除:全路径 transmit/next_hop 计数器 0 命中。
Phase 2 假设验证
-
[假设 1 排除] 加
transmit returns None/next_hop returns None/switch.forward returns false全路径插桩,跑 SURVEY=1 一次 0 命中。排除网络层 drop. -
[假设 2 确认] 推
slots_of_frame实现:它按(txn_id, frame_seq)过滤state == WaitAck || WaitGrant的 slot. process_ack cumulative 释放 WaitAck slots 后状态变 Empty,不再被 slots_of_frame 捕获 — handle_rc_frame_done 物理上 emit 0 个 packet. credit 已在 try_arbitrate 扣,RX 端无对应 receive_packet → 永久残留。Sanity 验证:写 PendingFrameCredit token + 在 handle_rc_frame_done 按"实际 emit slot 数 vs 预扣 slot 数"差额退还,残差 189 → 0 (16/16 cell PASS).
Phase 3 方案演进 (token → B 严格修法 → C 原子化)
本 ISSUE 经历三版方案,最终采用方案 C。记录完整演进供后续参考。
方案 A (token 差额退还,初版,已弃用):try_arbitrate 扣 credit 时 insert PendingFrameCredit token, handle_rc_frame_done 按"实际 emit slot 数 vs 预扣 slot 数"差额退还。实测 16/16 PASS (incomplete_txns=0, cbfc_residue=0)。弃用理由:它是"预扣-补退"两段式补丁,没有消除 try_arbitrate↔emit 异步窗口这个真实硬件不存在的 artifact;退还按平均 credit/slot 计算,frame 内 slot 不等分时有取整误差隐患。
方案 B (phys_emitted 严格修法,已弃用 — 引入回归):给 Slot 加 phys_emitted 标志,try_arbitrate 标 WaitAck 时设 false, on_tx_done 真 emit 后设 true, process_ack 跳过 phys_emitted=false 的 slot。本意"物理未 emit 的 packet 不可能被 RX ACK,故 cumulative ACK 不应释放它"。实测 12/16, 4 个 32chip×256KB cell 回归 (ring/torus deadlock panic, clos/fat-tree incomplete_txns=248 / unsent_frames=1009)。
方案 B 的概念错误 (本 ISSUE 关键教训):B 的前提"phys_emitted=false ⟺ 该 PSN 数据 RX 未收到"在重传场景不成立。Go-Back-N 重传时,slot 被 false-retry flip 回 WaitGrant 又被 try_arbitrate 重标 (phys_emitted=false),但它代表的 PSN 数据在原始 emit 时 RX 已收到。迟到的合法 cumulative ACK 命中它时,B 方案错误跳过 → slot 永久 WaitAck → max_retry 撞顶 → 卡死。clos-32-256kb 实测 153724 次合法 cumulative ACK 被 phys_emitted=false 错误跳过,直接证明 B 违反 RDMA cumulative ACK 标准语义 (IBTA §9.7.7: ACK 必须释放 PSN<N 的所有 slot,无论其重传状态)。
方案 C (credit 扣减原子化,最终采用):把 credit 扣减从 try_arbitrate (仲裁时) 推迟到 commit_frame_emit (= RcFrameDone, packet 真上 wire 的同一事件),使 credit 扣减与物理 emit 原子。slot 状态转换 (WaitGrant→WaitAck) + drain 仍在 try_arbitrate (保持 RcLinkTx 状态机自洽)。process_ack 移除 phys_emitted 检查,恢复 cumulative ACK 无条件释放 WaitAck/WaitGrant。
方案 C 为什么对:try_arbitrate→RcFrameDone 窗口内若迟到 cumulative ACK 已释放本 frame 的 slot (WaitAck→Empty), commit 时 slots_of_frame 自然返回剩余 (或空) → 只扣实际仍在的 slot 的 credit,既不多扣也不虚发 → 跨边界守恒自然成立,无需 token 退还。这对齐三方真值:
- 硬件 spec P30L13 "CBFC 控制器根据被仲裁发射的报文长度计算 CREDIT 消耗" (仲裁=发射,原子);
- 业界事件驱动仿真器 (BookSim2
BufferState::SendingFlit, gem5 Garnet SA-II) 一致把 credit 扣减与 flit 上线做在同一仿真 tick,明确避免拆成异步事件 (allocate-on-send); - RDMA cumulative ACK 标准语义 (IBTA §9.7.7),由 process_ack 无条件释放保证。
涉及文件:
perfmodel/evaluation/g5/src/tier6/rc_link_slot.rs— 删除 Slot.phys_emitted 字段perfmodel/evaluation/g5/src/tier6/rc_link_tx.rs— try_arbitrate 仅检查 credit (不扣);新增commit_frame_emit(emit 时结算 credit + 返回 transmit slot);process_ack/process_nak 移除 phys_emitted;删除 last_arbitrated_slots / mark_slot_phys_emittedperfmodel/evaluation/g5/src/top/event_handlers/paxi.rs— handle_rc_frame_done 改用 commit_frame_emit (commit-first,返回空则不路由不发)
根因分析
| 错误模式 | 实例 | 后果 |
|---|---|---|
| try_arbitrate 扣 credit 与 packet 实际 emit 异步,中间窗口 cumulative ACK 撕裂 | try_arbitrate line 204-218 vs handle_rc_frame_done slots_of_frame | TX credits_consumed_per_pair 多记,RX credits_returned_per_pair 无对应 → cbfc_residue 永久残留 |
为什么 32-chip + 256KB + alltoall 是最小触发配置
- 32-chip × 256KB alltoall 数据量大 (每 src 每 dst 8MB Transfer,总 8 GB 单方向) → 高 in-flight → ACK MERGE 排队 → ACK 抵达 TX 延迟大;
retry_timeout_ns=10000在高拥塞下被误触发 (ACK 还在路上但 timer 已超时);- retry_timeout 触发 process_nak → 把"对端已 ACK 但 ACK 还在路上"的 slot 翻回 WaitGrant → 二次 try_arbitrate 重扣 credit;
- 二次仲裁的 RcFrameDone 还没 fire,真 ACK 抵达,process_ack cumulative 释放刚 WaitAck'd slots → 物理 0 emit, credit 不退;
- 较小消息 (64KB) ACK 来回快,retry_timeout 不触发,无残留;
- 较少 chip (8/16) in-flight 少,retry_timeout 概率低,无残留。
Spec / 文档依据
| 来源 | 内容 | 与本 issue |
|---|---|---|
| G5-RC-Link 传输层设计规格 §CBFC 跨端约束 | TX consumed ≡ RX returned (per pair) | 本 issue 是该约束在实现里被违反 (扣-发异步窗口 + ACK 撕裂) |
| G5-RC-Link 传输层设计规格 §Slot 状态机 | WaitAck = "已发出,待 ACK" | 隐含"WaitAck ⇔ 物理已上 wire";实现里 try_arbitrate 提前标 WaitAck (未上 wire 已标),与隐含语义错位 |
| RCLINK-AFH-SPEC §5.10 (push 语义) | segment dispatch 由物理事件触发 (frame 完成 / credit 返还 / ACK) | 本 issue 不涉及 dispatch 路径,但同主题 — 都是"上层会计 vs 物理事件"边界处理 |
spec 设计正确,实现的 "credit 扣减时机" 与 spec 隐含的 "packet 物理 emit 时机" 错位. spec 未显式覆盖此中间态 (无文档说明 "扣 credit 但未 emit 时如何回滚"),后续 spec 修订可考虑补一节 §扣-发异步窗口约束。
解决方案
采用方案 C: credit 扣减原子化 (见 Phase 3 方案演进)。把 credit 扣减从 try_arbitrate (仲裁时) 推迟到 commit_frame_emit (= RcFrameDone, packet 真上 wire),与物理 emit 原子。不引入 token,不引入新 SlotState,不放宽守恒断言。
- rc_link_slot.rs — 删除
Slot.phys_emitted字段 (方案 B 残留) - rc_link_tx.rs —
try_arbitrate仅检查 credit (不扣);新增commit_frame_emit(emit 时结算 credit + 返回 transmit slot);process_ack恢复无条件释放 WaitAck/WaitGrant;删除last_arbitrated_slots/mark_slot_phys_emitted - event_handlers/paxi.rs —
handle_rc_frame_done改 commit-first (commit_frame_emit 返回空则不路由不发) - 单元测试 —
rc_link_tx_tests.rs/paxi.rs新增arbitrate_commit/drain_send/commit_frameshelper,让白盒测试复现生产 emit 时 credit 结算流程 (断言未削弱)
涉及文件:4 个生产文件 + 2 个测试文件改动,0 个新文件。
验证
- target test
generate_golden:方案 B 12/16 PASS (4 个 256KB 32-chip cell 回归:ring/torus deadlock panic, clos/fat-tree incomplete_txns=248)。方案 C 16/16 PASS, baseline.json 完整生成 (g5_git_sha=d4bc9b8...)。 - 回归
cargo test --release全量: 434 passed / 0 failed / 7 ignored (无新失败)。 - 跨边界守恒插桩 + 死锁探测 + event_balance 守恒:全 16 cell 五项守恒 KPI (incomplete_txns / unsent_frames / cbfc_residue_max / psn_gaps / event_balance) 全 0,守恒断言层未削弱。
修复前后对比 (4 个失败 cell,方案 B → 方案 C):
| 指标 | 方案 B (phys_emitted) | 方案 C (原子化) |
|---|---|---|
clos-32-ecmp-256kb incomplete_txns | 248 | 0 |
clos-32-ecmp-256kb unsent_frames | 1009 | 0 |
| ring-32-dor-256kb | RC link TX deadlock panic | PASS |
cbfc_residue_max (全 4 cell) | 0 (但数据未传完) | 0 (数据完整) |
| 16 cell baseline 完成度 | 12/16 | 16/16 |
Lessons Learned
- "扣会计的时机" 与 "物理事件触发的时机" 必须原子:根因是 try_arbitrate 扣 credit 与 handle_rc_frame_done 物理 emit 拆成两个异步事件。业界事件驱动仿真器 (BookSim2 / gem5 Garnet) 一致把 credit 扣减与 flit 上线做在同一仿真 tick (allocate-on-send),明确避免这种拆分。方案 C 通过把 credit 扣减推迟到 emit 事件消除窗口,是对齐业界的根本修法。
- 修法必须对齐协议标准,不能靠局部直觉:方案 B (phys_emitted 跳过迟到 ACK) 看似"严格遵守 spec",实则违反 RDMA cumulative ACK 标准语义 (IBTA §9.7.7: ACK 必须释放 PSN<N 的所有 slot,无论重传状态)。重传场景下 slot 的 PSN 数据在原始 emit 时 RX 已收到,跳过迟到 ACK = 丢弃合法确认 → slot 永久卡死 (实测 153724 次合法 ACK 被错误跳过)。局部"看起来对"的优化必须用协议标准 + 业界对标校验,否则会把记账瑕疵恶化成真死锁。
- 跨边界守恒检测 (
cbfc_residue_max) 是抓本类 bug 的兜底:单端 TXcredit_imbalance由 by-construction 恒 0,抓不到跨边界失衡。Phase 2B per-pair max-abs 残差才抓到 — 说明跨边界 / per-pair (而非全局聚合) 守恒是必要的。 - 诊断信息穿透多层结构是关键:仅看
cbfc_residue_max推不出根因;加 per-LG / per-pair 计数器 + "合法 ACK 被跳过次数" 临时插桩才定位到方案 B 的概念错误。 - 白盒单元测试须反映真实状态机流程:方案 C 把 credit 结算移到 emit 事件后,直接
try_arbitrate → process_ack的旧测试模式不再覆盖真实路径。补arbitrate_commit/drain_sendhelper 复现 "仲裁 → emit 结算" 链是对齐 (非降级) 断言。
遗留问题
retry_timeout_ns=10000在大 chip × 大消息工况下偏紧,触发大量 false retry (clos-32-256kb 实测nak_retx=30584),导致 sim_time 可能虚高。方案 C 已保证此工况下功能正确 (数据完整 + 守恒),但海量无效重传是独立性能问题。建议单开 ISSUE 评估基于网络 RTT 的自适应 timer。- 真实硬件 CBFC 有周期性 absolute credit 同步兜底 (spec P30L6-8 "接收方定期将可用 CREDIT 数量同步给发送端,补偿丢包造成的 Credit 泄露"; IBTA §7.9 FCTBS)。G5 当前是纯 delta 守恒模型 (TX.consumed ≡ RX.returned 严格相等)。方案 C 让 delta 自然守恒,故严格断言可保留。但若未来建模有损链路 / 真实丢包,需补 absolute resync 机制并相应放宽 delta 守恒为 "resync 后收敛"。
- Slot.payload_bytes 在 release 后仍残留: process_ack 仅改 state → Empty, slots_of_frame 在 slot 已 Empty 时 filter 掉不会误读。语义上"Empty slot 字段值任意"不明确,后续可加 zero-out 防御 (不在本 ISSUE 范围)。