跳到主要内容

业界事件驱动仿真器 dispatch 机制调研

目的:解答 G5 RC Link 仿真器中 segment "待发队列 → 链路层 TX" 的喂入应当 polling-driven 还是 event-driven,从 5 个业界主流开源仿真器中提取可直接借鉴的实现模式。

TL;DR

仿真器仿真范式Dispatch 模式关键机制wake-dep 风险
ns-3 (HPCC/RDMA)event-driven (DES)纯 event-drivenSimulator::Schedule(qp->m_nextAvail - Now, &DequeueAndTransmit)无 — 下一次发送时刻被精确 schedule,不轮询
BookSim2cycle-drivenevent-on-cycle (混合)_active flag + 每 cycle _InternalStep(),flit/credit 到达置 _active=true无 — 但范式不同
Garnet (gem5)cycle-drivenevent-driven within cyclescheduleEvent(Cycles(1)) 仅在 checkReschedule() 发现有 work 时才下一周期重唤醒无 — 显式条件检查
htsimevent-driven (DES)纯 event-driveneventlist().sourceIsPendingRel(self, drainTime)queueWasEmpty triggered无 — 队列空 → enqueue 立即 schedule completeService
OMNeT++ INETevent-driven (DES)push/pull + self-messagehandleCanPullPacketChanged() 反应式拉取 + scheduleAfter(txDuration, txTimer)无 — 上下游 callback 取代 polling

综合:业界事件驱动阵营(ns-3 / htsim / OMNeT++)的 dispatch 全部是 push-driven,没有任何一家使用 "周期性 wake 然后 scan ready 队列" 的 polling 模式。共同套路是:每次 dispatch 完成后,根据"下一帧可发时刻"(受 rate / credit / link-busy 三者决定)一次性 Schedule(nextAvail, dispatchFn);新事件(ACK / credit / enqueue)到达时若发现需要更早唤醒,则取消旧事件重新 schedule,或直接调用 dispatch。G5 RcLinkWake polling 模式属于业界异常做法,其 wake 频率漂移导致 latency 不稳定的现象在以上 5 个仿真器中均不存在,因为它们根本不存在"wake 频率"这个变量。


各仿真器详解

1. ns-3 (重点 RDMA 模块:QbbNetDevice + RdmaHw)

TL;DR

QbbNetDevice 的 dispatch 是 纯 event-driven:每次发送结束 (TransmitComplete) 或被 PFC Resume / 新 QP 入队时调用 DequeueAndTransmit(),若所有 QP 都被 rate-limit 阻塞,则计算 min(qp->m_nextAvail)Simulator::Schedule() 唤醒,不依赖任何周期性 wake。

物理事件 → 仿真事件映射

物理事件仿真事件
帧发完(传输时延到)Simulator::Schedule(txCompleteTime, &TransmitComplete) → 内部直接调 DequeueAndTransmit
速率受限(DCQCN 降速)下一帧可发时刻Simulator::Schedule(qp->m_nextAvail - Now, &DequeueAndTransmit)
PFC Pause 解除Resume() 直接同步调 DequeueAndTransmit
新 QP 入队AddQueuePair 路径同步调 DequeueAndTransmit

代码片段(HPCC qbb-net-device.ccDequeueAndTransmit 关键段)

void QbbNetDevice::DequeueAndTransmit() {
if (!m_linkUp) return;
if (m_txMachineState == BUSY) return; // 链路忙,等 TransmitComplete 重入
Ptr<Packet> p;
if (m_node->GetNodeType() == 0) { // NIC
int qIndex = m_rdmaEQ->GetNextQindex(m_paused);
if (qIndex != -1024) {
// 找到可发 QP,立即发
p = m_rdmaEQ->DequeueQindex(qIndex);
TransmitStart(p);
return;
}
// 所有 QP 都受限:计算最早可发时刻并 schedule
Time t = Simulator::GetMaximumSimulationTime();
for (uint32_t i = 0; i < m_rdmaEQ->GetFlowCount(); i++) {
Ptr<RdmaQueuePair> qp = m_rdmaEQ->GetQp(i);
if (qp->GetBytesLeft() == 0) continue;
t = Min(qp->m_nextAvail, t); // 物理就绪时刻
}
if (m_nextSend.IsExpired() &&
t < Simulator::GetMaximumSimulationTime() &&
t > Simulator::Now()) {
m_nextSend = Simulator::Schedule(
t - Simulator::Now(),
&QbbNetDevice::DequeueAndTransmit, this);
}
}
}

void QbbNetDevice::TransmitStart(Ptr<Packet> p) {
m_txMachineState = BUSY;
Time txCompleteTime = m_bps.CalculateBytesTxTime(p->GetSize());
Simulator::Schedule(txCompleteTime,
&QbbNetDevice::TransmitComplete, this);
m_channel->TransmitStart(p, this, txCompleteTime);
}

void QbbNetDevice::TransmitComplete() {
m_txMachineState = READY;
DequeueAndTransmit(); // 链路空闲 → 立刻再 dispatch
}

来源:alibaba-edu/High-Precision-Congestion-Control/simulation/src/point-to-point/model/qbb-net-device.ccDequeueAndTransmit ~ 行 320-400,TransmitStart/Complete ~ 行 420-460;行号近似,HPCC 仓库自首次发布以来基本未改动该段)。

如何避免 wake-dep

ns-3 的关键设计:m_nextSend 是单一未完成事件句柄,靠 IsExpired() 防重复 schedule;每次 dispatch 决定 "要么发,要么 schedule 一次精确唤醒",不存在"以固定频率 polling"的代码路径。即使把仿真器加速 100×,发送时刻仍精确等于 qp->m_nextAvail,因为该时刻是被 Schedule 钉死的事件,与 wake 频率无关。

参考引用


2. BookSim2 (NoC, cycle-driven 但带 active flag 优化)

TL;DR

BookSim2 是 cycle-driven 仿真器(每 cycle 全网 tick 一遍),但通过 _active flag 跳过 idle router:当 input/credit channel 无新到达且所有 pending VC 队列为空时,router 不进入 _InternalStep()。这是 "cycle-driven 仿真器优化空转" 的标准做法,与 event-driven 仿真器架构差异较大。

物理事件 → 仿真事件映射

物理事件仿真事件
Flit 到达 input channel_ReceiveFlits()_active=true
Credit 到达 input channel_ReceiveCredits()_active=true
VC 处于 routing/vc_alloc/sw_alloc/crossbar 任一阶段对应 _route_vcs/_vc_alloc_vcs/_sw_alloc_vcs/_crossbar_flits 非空,_active 保持
全部队列空 + 无入站 flit/credit_active=false,router 跳过当 cycle 处理

代码片段 (router.cpp Evaluate + iq_router.cpp _InternalStep)

// Router::Evaluate (router.cpp, ~line 70)
void Router::Evaluate() {
_partial_internal_cycles += _internal_speedup;
while (_partial_internal_cycles >= 1.0) {
_InternalStep();
_partial_internal_cycles -= 1.0;
}
}

// IQRouter::_InternalStep (iq_router.cpp, ~line 200)
void IQRouter::_InternalStep() {
if (!_active) { return; } // 关键优化:idle 跳过

bool have_flits = _ReceiveFlits();
bool have_credits = _ReceiveCredits();
_active = _active || have_flits || have_credits;

_InputQueuing(); // flit → input buffer
while (!_proc_credits.empty()) { /* ... */ } // 处理 credit
_RouteEvaluate(); _VCAllocEvaluate();
_SWAllocEvaluate(); _SwitchEvaluate();
_RouteUpdate(); _VCAllocUpdate();
_SWAllocUpdate(); _SwitchUpdate();
_OutputQueuing();

_active = !_in_queue_flits.empty()
|| !_route_vcs.empty()
|| !_vc_alloc_vcs.empty()
|| !_sw_alloc_vcs.empty()
|| !_crossbar_flits.empty()
|| !_out_queue_credits.empty();
}

来源:booksim/booksim2/src/routers/iq_router.cpp (_InternalStep 约 line 195-260),src/routers/router.cpp (Evaluate 约 line 65-75)。

如何避免 wake-dep

BookSim2 本质是 cycle-driven,不存在 "wake 频率" 这个概念——所有 router 在每个 cycle 被 trafficmanager 主动 tick。_active flag 只是性能优化(跳过 idle router 的 _InternalStep),不影响时序。因此 latency 计算是确定性的:flit 到达 cycle T → cycle T+1 进 input buffer → cycle T+2 进 SA → ... 与"wake 频率"无关,因为根本没有 wake,是统一时钟。

对 G5 借鉴价值有限:G5 是 event-driven,不能直接照搬 cycle-driven 的"per-cycle scan + active flag"做法。

参考引用


3. Garnet (gem5, cycle-driven NoC)

TL;DR

Garnet 是 cycle-driven(基于 gem5 Clocked 类),但采用 "event-driven within cycle" 的混合优化:router/NI 不每个 cycle 必然 wake,而是通过 checkReschedule() 检查是否有 work,仅当有 work 时 scheduleEvent(Cycles(1)) 注册下一 cycle 的 wakeup。无 work 时停摆,靠新事件(flit/credit/protocol msg 到达)重新激活。

物理事件 → 仿真事件映射

物理事件仿真事件
上游 cache controller 入队新 messagedequeueCallbackscheduleEventAbsolute(clockEdge(Cycles(1))) 唤醒 NI
Flit 到达 input linkinNetLink->isReady() true → checkReschedule 触发下一 cycle wakeup
Credit 到达inCreditLink->isReady() true → 同上
VC 内仍有未发 flitniOutVc.isReady() true → 同上
全部空不 schedule,NI 停摆直到外部事件

代码片段 (NetworkInterface.cc checkReschedule + dequeueCallback)

// dequeueCallback: 上游 protocol buffer 有新消息时由 RubyController 调用
void NetworkInterface::dequeueCallback() {
scheduleEventAbsolute(clockEdge(Cycles(1))); // 下一周期 wakeup
}

// wakeup 末尾调用 checkReschedule(),决定是否还需要下一周期
void NetworkInterface::checkReschedule() {
for (const auto& it : inNode_ptr) { // 1. protocol 输入 buffer
if (it == nullptr) continue;
while (it->isReady(clockEdge())) {
scheduleEvent(Cycles(1));
return;
}
}
for (auto& ni_out_vc : niOutVcs) { // 2. NI 输出 VC 还有 flit
if (ni_out_vc.isReady(clockEdge(Cycles(1)))) {
scheduleEvent(Cycles(1));
return;
}
}
for (auto &iPort : inPorts) { // 3. 入站 link 有 flit
NetworkLink *inNetLink = iPort->inNetLink();
if (inNetLink->isReady(curTick())) {
scheduleEvent(Cycles(1));
return;
}
}
for (auto &oPort : outPorts) { // 4. 入站 credit
CreditLink *inCreditLink = oPort->inCreditLink();
if (inCreditLink->isReady(curTick())) {
scheduleEvent(Cycles(1));
return;
}
}
// 都没有 → 不 schedule, NI 停摆
}

来源:gem5/src/mem/ruby/network/garnet/NetworkInterface.ccdequeueCallback (line ~106),checkReschedule (line ~564-601)。Router 端类似:Router::wakeup() 末尾 schedule_wakeup(Cycles(1)) 也是有 work 才调。

如何避免 wake-dep

Garnet 的核心保险:scheduleEvent(Cycles(1)) 的参数是固定的 1 cycle,且只有 "存在 ready 的输入" 这种确定条件下才下次 wake;不存在 "有时 0.5 cycle wake,有时 2 cycle wake" 这种漂移。如果是空转就根本不 schedule。所以 latency 是 cycle-accurate 确定的。

这是 G5 最值得借鉴的 event-driven NI 模型:dispatch 后立刻检查所有 "可能产生下一动作" 的输入源,对每一个源精确 schedule,而不是"等下个 wake tick 再扫一次"。

参考引用


4. htsim (数据中心 / RDMA 流量仿真器)

TL;DR

htsim 是 纯 event-driven,dispatch 完全 push-driven:Queue::receivePacket 入队时若队列空则立刻 beginServicebeginService 计算 drainTimesourceIsPendingRelcompleteService 内若队列非空再次 beginService 链式。EQDS EqdsPullPacer 也用同一模式做 rate-limited credit 发送。

物理事件 → 仿真事件映射

物理事件仿真事件
Packet enqueue 到空队列receivePacketbeginServicesourceIsPendingRel(self, drainTime)
当前 packet drain 完EventList 触发 doNextEventcompleteService → 队列非空时再 beginService
Pacer 收到 credit 请求且 !_activesourceIsPendingRel(self, 0)_active = true
Pacer 发完一个 pull packetsourceIsPendingRel(self, _pktTime),下一帧精确间隔

代码片段 (sim/queue.cpp)

void Queue::receivePacket(Packet& pkt) {
if (_queuesize + pkt.size() > _maxsize) { /* drop */ return; }
bool queueWasEmpty = _enqueued.empty();
_enqueued.push(&pkt);
_queuesize += pkt.size();
if (queueWasEmpty) {
beginService(); // 空 → 非空:启动服务
}
}

void Queue::beginService() {
assert(!_enqueued.empty());
eventlist().sourceIsPendingRel(*this,
drainTime(_enqueued.back())); // 链路速率推出 drain 时刻
}

void Queue::completeService() {
Packet* pkt = _enqueued.back();
_enqueued.pop();
_queuesize -= pkt->size();
pkt->sendOn(); // 推到下游
if (!_enqueued.empty()) {
beginService(); // 链式 schedule 下一帧
}
}

// EQDS 的 pacer 同样使用此 pattern:
// EqdsPullPacer::doNextEvent (sim/eqds.cpp)
sink->getNIC()->sendControlPacket(pullPkt);
_active = true;
eventlist().sourceIsPendingRel(*this, _pktTime); // 固定间隔 rate-limit

来源:Broadcom/csg-htsim/sim/queue.cpp (receivePacket/beginService/completeService 约 line 80-180),sim/eqds.cpp (EqdsPullPacer 约 line 400+)。

如何避免 wake-dep

htsim 完全不存在"polling wake"概念——EventList 是纯 priority-queue DES,只有 sourceIsPendingRel(obj, delta) 注册的事件会被触发。Queue 每发完一帧立即根据下一帧的 drainTime 精确 schedule,因此 latency 完全由物理参数(linkSpeed、pktSize)决定,与仿真器步进无关。

_servicing flag 仅在 PriorityQueue 子类中用于跟踪 "当前在 drain 哪个优先级",不是 polling 控制。

参考引用

  • Handley et al., "Re-architecting datacenter networks and stacks for low latency and high performance" (NDP), SIGCOMM'17
  • Olteanu et al., "An edge-queued datagram service for all datacenter traffic" (EQDS), NSDI'22
  • 源码:https://github.com/Broadcom/csg-htsim

5. OMNeT++ INET (EthernetCsmaMac + PacketServer)

TL;DR

OMNeT++ INET 是 纯 event-driven (DES),dispatch 采用 push/pull callback + self-message timer 两层架构:上游 queue 状态变化通过 handleCanPullPacketChanged(gate) 反应式通知 MAC(取代 polling),MAC 内部用 scheduleAfter(duration, txTimer) 注册传输完成事件(self-message)。

物理事件 → 仿真事件映射

物理事件仿真事件
上游 queue 从空变非空handleCanPullPacketChanged callback → 触发 dequeuePacket + handleUpperPacket
帧传输完成scheduleAfter(txDuration, txTimer)handleSelfMessage(txTimer)
IFG 时长scheduleAfter(ifgDuration, ifgTimer)
CSMA backoffscheduleAfter(backoffDuration, backoffTimer)
下游可推handleCanPushPacketChanged callback

代码片段 (EthernetCsmaMac.cc)

void EthernetCsmaMac::handleSelfMessage(cMessage *message) {
handleWithFsm(message->getKind(), message); // FSM 统一处理 timer
}

void EthernetCsmaMac::handleUpperPacket(Packet *packet) {
handleWithFsm(UPPER_PACKET, packet);
}

// 上游 queue 状态变化的 callback - 替代 polling
void EthernetCsmaMac::handleCanPullPacketChanged(const cGate *gate) {
if (!carrierSense && currentTxFrame == nullptr
&& fsm.getState() == IDLE && canDequeuePacket()) {
Packet *packet = dequeuePacket();
handleUpperPacket(packet); // 反应式拉取
}
}

// 启动一次传输:精确 schedule 完成事件
simtime_t duration = (packet->getDataLength()
+ ETHERNET_PHY_HEADER_LEN
+ phy->getEsdLength()).get<b>() / mode.bitrate;
scheduleAfter(duration, txTimer);

来源:inet-framework/inet/src/inet/linklayer/ethernet/basic/EthernetCsmaMac.cc,相关 callback 在 INET PacketProcessorBase 基类中定义。

如何避免 wake-dep

OMNeT++ 的设计哲学:模块间通过明确的 contract(push: pushPacket / pull: pullPacket + canPullPacketChanged notifier)通信,不允许"周期性 polling 上游队列"。当上游 queue 入队时,它显式调用下游 handleCanPullPacketChanged,下游只有在收到 callback 时才会尝试拉取。这从框架层杜绝了 wake-dep。INET 还在 base class 中提供 subscribe(packetPushedSignal, this) 等机制保证 callback 触发的原子性。

参考引用


共同模式总结

跨 5 个仿真器,event-driven 阵营(ns-3 / htsim / OMNeT++)以及"event-driven within cycle"的 Garnet 共享以下设计模式:

Pattern A: "Drain-Time Self-Scheduling"(ns-3 / htsim 共有)

发完一帧后,根据物理参数(rate、credit、link-busy 释放时刻)计算下一帧最早可发时刻,对该时刻精确 Schedule(dispatchFn)。不依赖任何"周期性 wake"。 对应 G5 应改造:每次 segment 喂入后,在已发生 segment 的 tx_done_time 和未发 segment 的 earliest_ready_ns 中取 min,精确 schedule 下次 dispatch,而非等 RcLinkWake 周期触发。

Pattern B: "Queue-Empty Edge Trigger"(htsim / INET 共有)

新 packet 入队时检测 "队列是否从空变非空",仅在 edge(empty→non-empty)触发 beginService。队列已在 service 状态时仅追加,不重复 schedule。 对应 G5 应改造:segment_queue enqueue 时若 RcLinkTx 当前空闲,立即 schedule dispatch;若正在 service 状态,靠 service 结束的链式 schedule 接续。

Pattern C: "Push/Pull Callback Contract"(INET / Garnet 共有)

模块间状态变化通过显式 callback (handleCanPullPacketChanged / dequeueCallback) 通知,取代下游周期性 polling 上游。 对应 G5 应改造:CDMA 注入新 segment 到 segment_queue 时,应直接调一次 RcLinkTx 的 dispatch 函数(或 schedule 一个 SegmentReady(now) 事件),而非靠 RcLinkWake 扫描。

Pattern D: "Conditional Reschedule"(Garnet 独有,对 G5 最直接借鉴)

dispatch 完成后扫描所有"可能产生下一动作"的输入源列表,任一非空即 scheduleEvent(Cycles(1)),否则停摆。 对应 G5 应改造:feed_rc_link 完成时检查 (1) segment_queue 有 earliest_ready_ns > now 的 segment(schedule 到该时刻),(2) RcLinkTx slot 是否将释放(schedule 到 ACK 到达时刻),(3) datapath 是否将空闲(schedule 到 frame_send_delay 后)。三者取 min,只 schedule 一个事件,不 self-reschedule polling。

反模式总结

5 个仿真器没有任何一个采用 G5 当前的 RcLinkWake 模式:固定周期 schedule self,wake 时 scan 全部队列。这种模式在业界被认为是 cycle-driven 仿真器(如 BookSim2)的优化空间——它们用 _active flag 跳过 idle,但仍是统一时钟驱动。在 event-driven 仿真器里,"周期 wake + scan" 等同于把 DES 退化成 cycle-driven,白白损失精度(wake 时机与物理就绪时机不对齐)。


1. 用 Pattern A + D 重构 segment dispatch(核心改动)

移除 RcLinkWake self-reschedule 循环。feed_rc_link() 改为以下契约:

fn feed_rc_link(now):
# 1. 喂入所有 ready 的 segment
while let Some(seg) = segment_queue.peek():
if seg.earliest_ready_ns > now: break
rc_link_tx.try_enqueue(seg) # 入 vc_pending
segment_queue.pop()

# 2. 计算下一次必须 dispatch 的时刻 = min of:
# - segment_queue 中下一个 segment 的 earliest_ready_ns
# - rc_link_tx 当前 datapath busy 的释放时刻
# - 任何 slot 等 ACK 的预计返回时刻(可选,若 ACK 是 hop-deterministic)
next_t = min(
segment_queue.front().earliest_ready_ns or +inf,
rc_link_tx.datapath_busy_until or +inf,
)
if next_t < +inf and next_t > now:
schedule_event(SegmentDispatch{link_id}, next_t)

2. ACK 到达事件直接触发 dispatch(Pattern C)

ACK handler 释放 slot 后立即调一次 feed_rc_link(now),不依赖 RcLinkWake。同理 on_tx_done(datapath 释放)也直接调。CDMA 新 segment 入 segment_queue 时同样直接调一次。

3. 删除 RcLinkWake 事件类型(清理)

完成 1 + 2 后,RcLinkWake 不再有任何 producer,可整类型删除。这是最强的"消除 wake-dep"证据——根本没有 wake。

4. 借鉴 ns-3 m_nextSend.IsExpired() 防重复 schedule

第 1 条 schedule 之前检查 "已有未触发的 SegmentDispatch 事件" → 若新计算的 next_t 更早,cancel + 重 schedule;若不更早,跳过。避免同一 link 多次 schedule 同一 dispatch。

5. 测试验证 wake-frequency 独立性

重构后,故意把仿真时间步长改成 ×10 / ×0.1,跑 21 个拓扑测试,latency 数值应当 bit-exact 一致。这是 polling 模式做不到的。

6. (可选)参考 htsim _servicing pattern 处理多 VC 仲裁

若多 VC 同时 ready 时需要轮转,可借鉴 htsim PriorityQueue 的 _servicing flag 跟踪当前 VC,而不是每次 dispatch 都重新扫所有 VC。这是性能优化,不影响时序正确性。

优先级

1 + 2 + 3 是必须的(消除 wake-dep 根因);4 是工程鲁棒性;5 是回归保障;6 是性能优化。Type 2 决策,5 分钟可回滚 — 建议直接做 1+2+3,5 验证,4+6 视实现复杂度按需加。


引用清单

源代码

Paper

  • Li et al., "HPCC: High Precision Congestion Control", SIGCOMM 2019 — DOI 10.1145/3341302.3342085
  • Mittal et al., "TIMELY: RTT-based Congestion Control for the Datacenter", SIGCOMM 2015
  • Zhu et al., "Congestion Control for Large-Scale RDMA Deployments" (DCQCN), SIGCOMM 2015
  • Jiang et al., "A Detailed and Flexible Cycle-Accurate Network-on-Chip Simulator" (BookSim2), ISPASS 2013
  • Agarwal et al., "GARNET: A Detailed On-Chip Network Model inside a Full-System Simulator", ISPASS 2009
  • Bharadwaj et al., "Garnet2.0: A Detailed On-Chip Network Model", 2018
  • Handley et al., "Re-architecting datacenter networks and stacks for low latency and high performance" (NDP), SIGCOMM 2017
  • Olteanu et al., "An edge-queued datagram service for all datacenter traffic" (EQDS), NSDI 2022
  • Varga & Hornig, "An overview of the OMNeT++ simulation environment", SIMUTools 2008

官方文档