NoC

Booksim2网络原理

Booksim2网络原理

Posted by George Lin on January 19, 2026

NoC网络原理类问题(以Booksim2.0为例)


基础概念(Q2.1-Q2.6)

Q2.1 什么是Flit?Flit和Packet的区别是什么?为什么NoC要使用Flit作为传输单位?

答案:

Flit(Flow Control Unit)定义:

Flit是NoC中流控的基本单位,是网络传输的最小数据单元。在BookSim2.0中,Flit的定义如下:

// src/flit.hpp
class Flit {
  FlitType type;      // READ_REQUEST, READ_REPLY, WRITE_REQUEST, WRITE_REPLY
  int vc;             // 虚通道ID
  bool head, tail;    // 是否为头/尾flit
  int src, dest;      // 源节点和目标节点
  int hops;           // 跳数统计
  OutputSet la_route_set;  // 前瞻路由信息
  // ...
};

Flit vs Packet:

  1. 组成关系
    • Packet(数据包):由多个Flit组成
    • Head Flit:包含路由信息(目标地址、路由路径)
    • Body Flit:包含数据载荷
    • Tail Flit:标记包结束,释放资源
  2. 大小对比
    • Flit:通常较小(如32-128位),适合路由器流水线处理
    • Packet:可变大小(1-N个Flit),取决于应用需求
  3. 处理方式
    • Flit级别:路由器在每个周期处理一个Flit
    • Packet级别:应用层和TrafficManager管理完整Packet

为什么使用Flit:

  1. 流水线友好
    • 路由器可以每个周期处理一个Flit,实现流水线
    • 如果以Packet为单位,需要等待整个Packet到达才能处理
  2. 资源利用率
    • 不同Packet的Flit可以交错传输(通过VC)
    • 提高链路利用率,避免长包阻塞短包
  3. 流控粒度
    • 基于Flit的流控更精细,可以及时响应拥塞
    • Credit机制基于Flit,而不是整个Packet
  4. 延迟优化
    • Head Flit到达后立即开始路由,无需等待整个Packet
    • 实现流水线传输,降低延迟

BookSim2.0中的实现:

// TrafficManager生成Packet,分解为Flit
void TrafficManager::_GeneratePacket(int source, int size, int cl, int time) {
  int pid = _cur_pid++;
  for (int i = 0; i < size; ++i) {
    Flit *f = Flit::New();
    f->pid = pid;
    f->head = (i == 0);
    f->tail = (i == size - 1);
    // ...
    _partial_packets[source][cl].push_back(f);
  }
}

Q2.2 虚通道(Virtual Channel)的作用是什么?为什么需要多个VC?VC数量如何影响网络性能?

答案:

虚通道(VC)定义:

虚通道是在同一物理链路上逻辑分离的多个通道,每个VC有独立的缓冲区和流控状态。

VC的作用:

  1. 提高链路利用率
    • 多个VC可以共享同一物理链路
    • 当一个VC阻塞时,其他VC可以继续传输
    • 避免”head-of-line blocking”(队首阻塞)
  2. 死锁避免
    • 不同VC可以使用不同的路由规则
    • 通过VC分配策略避免死锁
    • 例如:Torus中使用两个VC集合,分别对应不同的环分区
  3. 优先级支持
    • 不同VC可以设置不同优先级
    • 高优先级流量可以使用专用VC
  4. 流量分离
    • 请求和响应可以使用不同VC
    • 不同流量类型(read/write)可以使用不同VC

为什么需要多个VC:

  1. 死锁避免需求
    • 某些路由算法(如自适应路由)需要多个VC避免死锁
    • Turn Model路由需要至少2个VC
  2. 性能提升
    • 更多VC提供更多并行性
    • 减少阻塞概率,提高吞吐量
  3. 公平性
    • 多个VC可以实现更好的公平性
    • 避免单个流独占资源

VC数量对性能的影响:

  1. 延迟影响
    • VC数少(1-2):可能阻塞,延迟高
    • VC数适中(4-8):延迟最低,资源利用最优
    • VC数过多(>16):延迟改善有限,但资源开销大
  2. 吞吐量影响
    • VC数少:吞吐量受限,容易饱和
    • VC数增加:吞吐量提升,但收益递减
    • VC数过多:吞吐量不再提升,浪费资源
  3. 资源开销
    • 每个VC需要独立的缓冲区
    • VC数量 = 缓冲区数量 × VC数
    • 更多VC意味着更多存储开销

BookSim2.0中的VC实现:

// Buffer管理多个VC
class Buffer {
  vector<VC*> _vc;  // 每个输入端口有多个VC
  
  void AddFlit(int vc, Flit *f) {
    _vc[vc]->AddFlit(f);
  }
};

// VC状态管理
enum eVCState {
  idle,      // 空闲
  routing,   // 等待路由
  vc_alloc,  // 等待VC分配
  active,    // 活跃传输
  waiting    // 等待资源
};

最佳实践:

  • Mesh/Torus:通常2-4个VC足够
  • FatTree:可能需要更多VC(8-16)
  • 自适应路由:需要更多VC(4-8)
  • 确定性路由:2个VC即可

Q2.3 Credit-based流控的工作原理是什么?与ACK/NACK流控相比有什么优缺点?

答案:

Credit-based流控原理:

Credit机制是一种背压(backpressure)流控,通过Credit信号控制数据传输。

工作流程:

  1. 初始化
    // 每个输出VC有vc_buf_size个credit
    BufferState::BufferState() {
      _size = vc_buf_size;  // 如8
      // 初始时所有credit可用
    }
    
  2. 发送Flit
    // 发送Flit时消耗credit
    void BufferState::SendingFlit(Flit *f) {
      _occupancy++;  // 占用增加
      // credit = _size - _occupancy
    }
    
  3. 检查可用性
    // 检查是否有可用credit
    bool BufferState::IsAvailableFor(int vc) {
      return AvailableFor(vc) > 0;  // credit > 0
    }
    
  4. 接收Credit
    // 下游发送Credit,上游增加credit
    void BufferState::ProcessCredit(Credit *c) {
      _occupancy--;  // 释放空间
      // credit增加
    }
    
  5. Credit传递
    // 下游路由器发送Credit
    Router::WriteOutputs() {
      Credit *c = Credit::New();
      _output_credit_channels[port]->Send(c);
    }
       
    // 上游路由器接收Credit
    Router::ReadInputs() {
      Credit *c = _input_credit_channels[port]->Receive();
      _next_buf[port]->ProcessCredit(c);
    }
    

Credit传递路径:

Router A (上游)                    Router B (下游)
    |                                   |
    |--Flit-->                          |
    |                                   | 接收Flit
    |                                   | 消耗credit
    |                                   |
    |<--Credit--                        | 发送Credit
    | 接收Credit                        |
    | 增加credit                        |

与ACK/NACK流控对比:

特性 Credit-based ACK/NACK
信号方向 反向(Credit) 双向(ACK/NACK)
信号数量 每个Flit一个Credit 每个Packet一个ACK
延迟 低(立即反馈) 高(等待ACK)
实现复杂度 简单(计数) 复杂(状态机)
缓冲区需求 固定(预分配) 可变(动态)
错误恢复 不支持 支持(NACK重传)

Credit-based优势:

  1. 低延迟
    • Credit立即返回,无需等待ACK
    • 适合低延迟要求的NoC
  2. 简单实现
    • 只需维护credit计数
    • 无需复杂的ACK/NACK状态机
  3. 确定性
    • Credit数量 = 缓冲区大小
    • 不会出现缓冲区溢出
  4. 流水线友好
    • Credit和Flit可以流水线传输
    • 不阻塞数据路径

Credit-based劣势:

  1. 无错误恢复
    • 如果Flit丢失,无法检测和重传
    • 需要上层协议保证可靠性
  2. 固定缓冲区
    • 必须预先分配缓冲区
    • 无法动态调整
  3. Credit开销
    • 需要反向Credit通道
    • 增加硬件开销

ACK/NACK优势:

  1. 错误恢复
    • 可以检测丢失并重传
    • 适合不可靠链路
  2. 动态缓冲区
    • 可以根据ACK动态调整
    • 更灵活的流控

ACK/NACK劣势:

  1. 高延迟
    • 需要等待ACK返回
    • 增加端到端延迟
  2. 复杂实现
    • 需要维护发送/接收状态
    • 需要超时和重传机制

BookSim2.0中的实现:

// Credit类
class Credit {
  int vc;  // 对应的VC
};

// BufferState维护credit
class BufferState {
  int _size;        // 总缓冲区大小
  int _occupancy;   // 当前占用
  // credit = _size - _occupancy
  
  int AvailableFor(int vc) const {
    return _size - _occupancy;  // 可用credit
  }
};

适用场景:

  • Credit-based:NoC内部(可靠链路、低延迟要求)
  • ACK/NACK:网络接口、外部通信(可能丢包、需要可靠性)

Q2.4 解释NoC中的死锁(deadlock)、活锁(livelock)和饥饿(starvation)现象,它们是如何产生的?

答案:

死锁(Deadlock):

定义: 一组资源相互等待,导致所有资源都无法继续执行。

NoC中的死锁示例:

路由器A等待路由器B的缓冲区
    ↓
路由器B等待路由器C的缓冲区
    ↓
路由器C等待路由器A的缓冲区
    ↓
形成循环等待 → 死锁

产生原因:

  1. 循环依赖
    • 多个路由器形成循环等待
    • 每个路由器都等待下一个路由器的资源
  2. 资源不足
    • VC数量不足
    • 缓冲区满
  3. 路由算法问题
    • 某些路由算法可能产生循环路径
    • 缺乏死锁避免机制

BookSim2.0中的死锁避免:

  1. VC分离
    // Torus中使用两个VC集合避免死锁
    if (f->ph == 0) {
      vcEnd -= available_vcs;  // 使用VC 0-3
    } else {
      vcBegin += available_vcs;  // 使用VC 4-7
    }
    

Torus VC分离避免死锁的具体例子:

问题场景:4×4 Torus(k=4)

节点编号(1D表示):
  0---1---2---3
  |   |   |   |
  4---5---6---7
  |   |   |   |
  8---9---10--11
  |   |   |   |
  12--13--14--15
  ↑           ↑
  └───────────┘ 环连接(数据线)

死锁风险:

在Torus中,由于环连接,可能存在循环路径:

场景:4个包形成循环等待
包A:节点0 → 节点7(通过环:0→15→14→...→8→7)
包B:节点7 → 节点0(通过环:7→6→...→1→0)
包C:节点8 → 节点15(通过环:8→9→...→14→15)
包D:节点15 → 节点8(通过环:15→12→...→4→8)

如果都使用相同的VC,可能形成循环依赖:
  包A等待包B释放的VC
  包B等待包C释放的VC
  包C等待包D释放的VC
  包D等待包A释放的VC
  → 死锁!

VC分离机制:

数据线(Dateline)定义:

  • X维度数据线:节点k-1(3)到节点0的连接
  • 跨越数据线的路径使用不同的VC集合

代码实现:

// src/routefunc.cpp
void dor_next_torus(int cur, int dest, int in_port,
                    int *out_port, int *partition, bool balance) {
  // 计算方向
  int dist2 = gK - 2 * ((dest - cur + gK) % gK);
  
  if (dist2 > 0) {
    *out_port = 2*dim_left;     // 正向(Right)
    dir = 0;
  } else {
    *out_port = 2*dim_left + 1; // 反向(Left,通过环)
    dir = 1;
  }
  
  // 判断是否跨越数据线
  if (partition) {
    // 确定性:固定数据线在节点k-1和0之间
    if ((dir == 0 && cur > dest) ||  // 正向但cur>dest(跨越数据线)
        (dir == 1 && dest < cur)) {  // 反向且dest<cur(跨越数据线)
      *partition = 1;  // 使用VC集合1(VC 4-7)
    } else {
      *partition = 0;  // 使用VC集合0(VC 0-3)
    }
  }
}

// dim_order_torus中使用partition
void dim_order_torus(...) {
  dor_next_torus(cur, dest, in_channel, &out_port, &f->ph, false);
  
  if (cur != dest) {
    int available_vcs = (vcEnd - vcBegin + 1) / 2;  // 假设8个VC,available_vcs=4
    
    if (f->ph == 0) {
      // 不跨越数据线:使用VC 0-3
      vcEnd -= available_vcs;  // vcEnd = 7 - 4 = 3
      // VC范围:[0, 3]
    } else {
      // 跨越数据线:使用VC 4-7
      vcBegin += available_vcs;  // vcBegin = 0 + 4 = 4
      // VC范围:[4, 7]
    }
  }
}

具体例子:

场景:4×4 Torus,8个VC(VC 0-7)

包1:节点0 → 节点7(X维度)

路径计算:
  cur = 0, dest = 7
  dist_forward = (7-0) % 4 = 3跳(正向:0→1→2→3→7,但3→7需要环)
  dist_backward = (0-7+4) % 4 = 1跳(反向:0→15→14→...→8→7)
  
  选择反向路径(更短)
  dir = 1(反向)
  
  判断partition:
    dir=1(反向)且dest=7 < cur=0?否(在环上,7>0)
    但路径是:0→15→14→...→8→7
    实际上:从0反向到7,跨越了数据线(0→15的连接)
    
  因此:f->ph = 1(跨越数据线)
  使用VC:4-7

包2:节点7 → 节点0(X维度)

路径计算:
  cur = 7, dest = 0
  dist_forward = (0-7+4) % 4 = 1跳(正向:7→6→...→1→0,但需要环)
  dist_backward = (7-0) % 4 = 3跳(反向)
  
  选择正向路径(更短)
  dir = 0(正向)
  
  判断partition:
    dir=0(正向)且cur=7 > dest=0?是
    跨越数据线(7→0的连接)
    
  因此:f->ph = 1(跨越数据线)
  使用VC:4-7

关键观察:

虽然两个包都跨越数据线,但它们使用相同的VC集合(4-7),这仍然可能死锁!

更深入的分析:

实际上,VC分离的关键在于路径的方向性

包1:0→7(反向,ph=1,VC 4-7)
  路径:0→15→14→13→12→8→9→10→11→7
  使用的VC:4-7

包2:7→0(正向,ph=1,VC 4-7)
  路径:7→6→5→4→0
  使用的VC:4-7

为什么不会死锁?

关键在于路由算法的确定性VC分配的时序

  1. 维度顺序路由:先完成X维度,再完成Y维度
  2. 路径不重叠:包1和包2的路径在X维度上不重叠
  3. VC分配顺序:先到达的包先分配VC

更准确的VC分离机制:

实际上,BookSim2.0使用两个数据线来更彻底地避免死锁:

// 平衡模式(balance=true)
if (balance) {
  // 两个数据线:
  // 1. k-1和0之间(partition=1)
  // 2. (k-1)/2和(k-1)/2+1之间(partition=0)
  
  if ((dir == 0 && cur > dest) || (dir == 1 && cur < dest)) {
    *partition = 1;  // 跨越第一个数据线
  } else if ((dir == 0 && cur <= (k-1)/2 && dest > (k-1)/2) ||
             (dir == 1 && cur > (k-1)/2 && dest <= (k-1)/2)) {
    *partition = 0;  // 跨越第二个数据线
  } else {
    *partition = RandomInt(1);  // 随机选择
  }
}

实际例子(4×4 Torus,k=4):

数据线1:节点3和节点0之间
数据线2:节点1和节点2之间((4-1)/2=1, (4-1)/2+1=2)

包A:节点0 → 节点2
  路径:0→1→2(正向)
  判断:dir=0, cur=0, dest=2
    cur=0 <= 1 && dest=2 > 1?是 → 跨越数据线2
    partition = 0 → 使用VC 0-3

包B:节点2 → 节点0
  路径:2→1→0(反向)
  判断:dir=1, cur=2, dest=0
    cur=2 > 1 && dest=0 <= 1?是 → 跨越数据线2
    partition = 0 → 使用VC 0-3

包C:节点3 → 节点1
  路径:3→0→1(通过环)
  判断:dir=1, cur=3, dest=1
    cur=3 > 0?是 → 跨越数据线1
    partition = 1 → 使用VC 4-7

VC分离的效果:

包A和包B:都使用VC 0-3,但路径不重叠(0→2 vs 2→0),不会死锁
包C:使用VC 4-7,与包A/B隔离,不会死锁

更直观的简化例子:

场景:1D Torus(环形),4个节点,8个VC

节点:0---1---2---3---0(环)
      ↑               ↑
      └───────────────┘ 数据线(节点3到节点0)

死锁场景(无VC分离):

包A:节点0 → 节点2
  路径:0→1→2(正向,3跳)
  使用VC:0(假设)

包B:节点2 → 节点0
  路径:2→3→0(正向,通过环,2跳)
  使用VC:0(假设)

包C:节点1 → 节点3
  路径:1→2→3(正向,2跳)
  使用VC:0(假设)

包D:节点3 → 节点1
  路径:3→0→1(正向,通过环,2跳)
  使用VC:0(假设)

如果所有包都使用VC 0:
  包A在节点1等待包C释放VC 0
  包C在节点2等待包B释放VC 0
  包B在节点3等待包D释放VC 0
  包D在节点0等待包A释放VC 0
  → 循环等待 → 死锁!

VC分离解决(有VC分离):

包A:节点0 → 节点2
  路径:0→1→2(正向,不跨越数据线)
  partition = 0 → 使用VC 0-3

包B:节点2 → 节点0
  路径:2→3→0(正向,跨越数据线3→0)
  partition = 1 → 使用VC 4-7

包C:节点1 → 节点3
  路径:1→2→3(正向,不跨越数据线)
  partition = 0 → 使用VC 0-3

包D:节点3 → 节点1
  路径:3→0→1(正向,跨越数据线3→0)
  partition = 1 → 使用VC 4-7

结果:
  包A和包C:使用VC 0-3,路径不重叠,无冲突
  包B和包D:使用VC 4-7,路径不重叠,无冲突
  包A/C与包B/D:使用不同VC集合,完全隔离
  → 无死锁!

关键代码逻辑:

// 判断是否跨越数据线
if ((dir == 0 && cur > dest) ||  // 正向但cur>dest(如3→0)
    (dir == 1 && dest < cur)) {   // 反向且dest<cur
  partition = 1;  // 跨越数据线,使用VC 4-7
} else {
  partition = 0;  // 不跨越数据线,使用VC 0-3
}

// VC分配
if (partition == 0) {
  vcEnd = 3;      // 使用VC 0-3
} else {
  vcBegin = 4;    // 使用VC 4-7
}

总结:

  1. 数据线识别:判断路径是否跨越数据线(节点k-1到节点0)
  2. VC分离
    • 不跨越数据线 → VC 0-3
    • 跨越数据线 → VC 4-7
  3. 打破循环:即使路径形成循环,不同VC集合也避免了资源竞争
  4. 保证无死锁:VC分离 + 确定性路由 = 无死锁保证

为什么有效:

  • 隔离:跨越数据线的包和不跨越的包使用不同VC,不会相互阻塞
  • 确定性:路由算法是确定性的,相同路径总是使用相同VC集合
  • 无循环依赖:不同VC集合之间没有资源依赖关系
  1. Turn Model
    • 限制某些转向,打破循环
    • 例如:West-First禁止某些转向
  2. 多物理网络
    • 请求和响应使用不同子网
    • 避免请求-响应死锁

活锁(Livelock):

定义: 系统不断执行操作,但无法取得进展。

NoC中的活锁示例:

数据包在网络上不断路由,但永远无法到达目标
    ↓
可能原因:
- 路由算法选择错误路径
- 自适应路由在多个路径间振荡
- 负载均衡导致包在热点间移动

产生原因:

  1. 路由振荡
    • 自适应路由在多个路径间切换:例如adaptive_xy_yx_mesh会在XY和YX两条最短路径之间,根据本地GetUsedCredit()/GetBufferOccupancy()看到的队列长度来选择“看起来更空”的那一条,如果不同路由器在不同时间点看到的局部负载不一致,就可能出现有的包选XY,有的包选YX,在两条路径之间来回“倒腾”。
    • 负载信息过时导致错误决策:这些负载信息都是当前路由器本地、某一时刻的快照(例如下游端口的已用credit),当真正的流量已经沿着先前决策涌向某条路径时,其他路由器可能还在用“旧的队列长度”做决策,于是新包又被导向刚刚开始拥塞的路径,形成一种“谁刚变空就被一波新流量打满”的振荡模式;在有非最小路由(如UGAL)的情况下,如果在“最小/非最小路径”和不同中间节点之间频繁改选,理论上可能演化成活锁(包总在网络里绕圈但很久到不了目的地),因此代码里通常会限制自适应决策只在注入或特定阶段做一次,并叠加阈值(如UGAL中的adaptive_threshold)来减少这种抖动。
  2. 非最小路由
    • Valiant路由可能产生长路径
    • 包可能绕远路
  3. 负载均衡问题
    • 过度追求负载均衡
    • 导致包在多个路径间移动

避免方法:

  1. 限制非最小路径
    • 限制Valiant路由的中间节点选择
    • 设置最大跳数
  2. 稳定路由决策
    • 避免频繁切换路径
    • 使用历史信息
  3. 优先级机制
    • 接近目标的包优先
    • 避免包在热点间移动

饥饿(Starvation):

定义: 某些流长期无法获得服务,而其他流正常服务。

NoC中的饥饿示例:

低优先级流被高优先级流持续阻塞
    ↓
低优先级流的包永远无法传输
    ↓
导致该流饥饿

产生原因:

  1. 不公平分配
    • 某些流获得过多资源
    • 其他流资源不足
  2. 优先级机制
    • 高优先级流总是优先
    • 低优先级流被持续阻塞
  3. 路由热点
    • 某些路径成为热点
    • 使用这些路径的流被阻塞

避免方法:

  1. 公平分配器
    • 使用Round-Robin分配
    • 确保所有流都有机会
  2. 老化机制
    • 等待时间长的包提高优先级
    • 避免长期饥饿
  3. 负载均衡
    • 分散流量到多个路径
    • 避免热点

BookSim2.0中的处理:

// 优先级机制
enum ePriority {
  class_based,        // 基于类别
  age_based,          // 基于年龄(避免饥饿)
  network_age_based,  // 基于网络年龄
  local_age_based,    // 基于本地年龄
  queue_length_based, // 基于队列长度
  hop_count_based,    // 基于跳数
  none                // 无优先级
};

// 老化机制
void VC::UpdatePriority() {
  if (_pri_type == age_based) {
    _pri = GetSimTime() - _buffer.front()->ctime;  // 等待时间
  }
}

总结对比:

现象 特征 原因 解决方法
死锁 系统完全停止 循环等待 VC分离、Turn Model
活锁 系统运行但无进展 路由振荡 限制路径、稳定决策
饥饿 部分流无法服务 不公平分配 公平分配、老化机制

Q2.5 路由延迟、VC分配延迟、开关分配延迟分别代表什么?它们如何影响路由器流水线深度?

答案:

三种延迟的定义:

  1. 路由延迟(routing_delay)
    • 定义:计算输出端口所需的时间(周期数)
    • 典型值:0-1周期
    • 作用:路由函数执行时间
  2. VC分配延迟(vc_alloc_delay)
    • 定义:分配输出VC所需的时间
    • 典型值:1周期
    • 作用:VC分配器执行时间
  3. 开关分配延迟(sw_alloc_delay)
    • 定义:分配交叉开关路径所需的时间
    • 典型值:1周期
    • 作用:开关分配器执行时间

BookSim2.0中的实现:

// IQRouter构造函数
_routing_delay = config.GetInt("routing_delay");     // 通常0或1
_vc_alloc_delay = config.GetInt("vc_alloc_delay");   // 通常1
_sw_alloc_delay = config.GetInt("sw_alloc_delay");   // 通常1

// 延迟队列
deque<pair<int, pair<int, int> > > _route_vcs;      // (cycle, (input, vc))
deque<pair<int, pair<pair<int, int>, int> > > _vc_alloc_vcs;
deque<pair<int, pair<pair<int, int>, int> > > _sw_alloc_vcs;

延迟处理机制:

// 路由延迟处理
void IQRouter::_RouteEvaluate() {
  // 检查延迟队列中到期的路由请求
  while (!_route_vcs.empty() && _route_vcs.front().first <= GetSimTime()) {
    int input = _route_vcs.front().second.first;
    int vc = _route_vcs.front().second.second;
    _route_vcs.pop_front();
    
    // 执行路由计算
    Buffer::Route(vc, _rf, this, flit, input);
    
    // 加入VC分配队列(延迟vc_alloc_delay)
    _vc_alloc_vcs.push_back(make_pair(
      GetSimTime() + _vc_alloc_delay,
      make_pair(make_pair(input, vc), ...)
    ));
  }
}

流水线深度计算:

总延迟 = routing_delay + vc_alloc_delay + sw_alloc_delay + 1(传输)

示例配置:

  1. 快速配置(低延迟)
    routing_delay = 0
    vc_alloc_delay = 1
    sw_alloc_delay = 1
    总延迟 = 0 + 1 + 1 + 1 = 3周期
    
  2. 标准配置
    routing_delay = 1
    vc_alloc_delay = 1
    sw_alloc_delay = 1
    总延迟 = 1 + 1 + 1 + 1 = 4周期
    
  3. 慢速配置(高延迟)
    routing_delay = 2
    vc_alloc_delay = 2
    sw_alloc_delay = 2
    总延迟 = 2 + 2 + 2 + 1 = 7周期
    

流水线阶段:

Cycle 0: ReadInputs (接收Flit)
Cycle 1: RouteEvaluate (路由计算,如果routing_delay=1)
Cycle 2: VCAllocEvaluate (VC分配,如果vc_alloc_delay=1)
Cycle 3: SWAllocEvaluate (开关分配,如果sw_alloc_delay=1)
Cycle 4: SwitchEvaluate (交叉开关传输)
Cycle 5: WriteOutputs (发送Flit)

延迟对性能的影响:

  1. 延迟增加 → 流水线深度增加
    • 更多周期才能完成处理
    • 增加路由器延迟
  2. 延迟增加 → 吞吐量可能降低
    • 如果延迟导致流水线停顿
    • 但通常延迟是流水线的,不影响吞吐量
  3. 延迟增加 → 资源需求增加
    • 需要更多延迟队列存储
    • 需要更多状态跟踪

优化策略:

  1. 前瞻路由(Lookahead Routing)
    // routing_delay = 0,在上一跳计算下一跳路由
    flit->la_route_set = route_set;  // 存储前瞻路由
    

    之所以可以在“上一跳”就把“下一跳应该怎么走”算出来,是因为BookSim2里用于配合lookahead的那类路由(例如dim_order_mesh/torus这类确定性最短路由)本质上只依赖于拓扑和几何位置:给定一个路由器ID、目的节点ID和入口方向 (router_id, dest, in_channel),输出端口由坐标差和维度顺序唯一决定,不需要查看本路由器的队列长度或credit等瞬时状态。因此,当前路由器完全可以“假装自己是下一跳路由器”,调用同一个路由函数为下一跳计算出对应的 OutputSet 并写入 flit->la_route_set,等flit真正到达下一跳时,如果 routing_delay = 0,下一跳就直接拿这份缓存好的 la_route_set 进入VC分配而不再调用路由函数,从而把路由阶段的流水线延迟压缩到0周期。需要注意的是,这种前瞻路由只对不依赖动态负载信息的确定性路由是安全的;如果路由算法本身要看 GetUsedCredit()/GetBufferOccupancy() 等动态拥塞信息,就不能简单在上一跳“算死”。

  2. 并行分配
    • 从抽象上看,VC分配负责“长期资源绑定”:在路由给出的候选OutputSet里为某个输入VC选择一个具体的(out_port, out_vc),相当于给这个packet的head在下游占一条虚通道;而开关分配只解决“当前这个时钟周期谁能过交叉开关”:在所有“已经拿到输出VC且队头有flit”的(in_port, vc)之间做最大匹配,决定这一拍哪几个输入端口可以连到各自已经绑定好的输出端口。
    • 在一个拍里,crossbar 完全可以同时转发不同packet的flit(来自不同输入端口/不同VC),接收端区分“这是谁的”主要靠两层信息:一是VC队列本身——在典型的wormhole实现中,一个VC在同一时刻只服务一个packet,所有body/tail flit都排在这个VC队列后面;二是flit内部的pid/head/tail字段,TrafficManager在_RetireFlit中用pid把同一packet的head和tail对起来做统计/重组。因此,从概念模型上你可以把VC分配和SW分配看成两个正交的资源分配问题,理论上可以并行思考以估算流水线深度;但在BookSim当前的IQRouter实现中,SW分配必须依赖VC分配已经写好的GetOutputPort/GetOutputVC结果,实际是按“路由→VC分配→SW分配”顺序做成流水线的,而不是同拍完全独立并行。
    • 在更激进的设计里,如果SW分配的决策完全不依赖“哪些VC已经成功绑定到哪些输出VC/还有多少credit”(例如只做纯端口级的无状态匹配),那么这两层确实可以在一个周期内并行评估,从而减少整体路由器流水线的逻辑深度,但那已经偏离了BookSim中IQRouter的典型实现。
  3. 流水线优化
    • 重叠不同阶段的处理
    • 提高吞吐量

实际配置建议:

  • 高性能设计:routing_delay=0, vc_alloc_delay=1, sw_alloc_delay=1
  • 平衡设计:routing_delay=1, vc_alloc_delay=1, sw_alloc_delay=1
  • 简单设计:所有延迟=1,易于实现

Q2.6 什么是注入率(injection rate)?饱和吞吐量(saturation throughput)和零负载延迟(zero-load latency)的含义是什么?

答案:

注入率(Injection Rate):

定义: 每个节点每个周期注入到网络的Flit数量(相对于链路带宽的比率)。

计算:

注入率 = (每个周期注入的Flit数) / (链路带宽)
例如:0.15 表示每个周期有15%的概率注入一个Flit

BookSim2.0中的实现:

// TrafficManager
double injection_rate = config.GetFloat("injection_rate");  // 如0.15

void TrafficManager::_Inject() {
  for (int source = 0; source < _nodes; ++source) {
    // 根据注入率决定是否注入
    if (RandomFloat() < injection_rate) {
      _IssuePacket(source, class);
    }
  }
}

注入率范围:

  • 0.0:无流量
  • 0.1-0.3:低负载
  • 0.4-0.6:中等负载
  • 0.7-0.9:高负载
  • 1.0:满负载(每个周期都注入)

饱和吞吐量(Saturation Throughput):

定义: 网络达到饱和时的最大接受吞吐量,即延迟开始急剧上升时的吞吐量。

特征:

  • 延迟-吞吐量曲线的拐点
  • 超过此点,延迟急剧增加,但吞吐量不再提升
  • 通常用接受的Flit数/周期接受的Flit数/节点/周期表示

测量方法:

// 逐步增加注入率,测量吞吐量
for (double rate = 0.1; rate <= 1.0; rate += 0.05) {
  injection_rate = rate;
  RunSimulation();
  
  double throughput = accepted_flits / (nodes * cycles);
  
  // 找到延迟开始急剧上升的点
  if (latency > threshold) {
    saturation_throughput = throughput;
    break;
  }
}

典型值:

  • Mesh 2D:约0.3-0.4 flits/node/cycle
  • Torus 2D:约0.4-0.5 flits/node/cycle
  • FatTree:约0.5-0.6 flits/node/cycle

零负载延迟(Zero-Load Latency):

定义: 网络无拥塞时的最小延迟,即注入率接近0时的延迟。

组成:

零负载延迟 = 路由延迟 + VC分配延迟 + 开关分配延迟 
           + 传输延迟 + 链路延迟 × 跳数

BookSim2.0中的测量:

// 设置极低注入率
injection_rate = 0.01;  // 1%注入率

// 运行模拟
RunSimulation();

// 测量平均延迟
zero_load_latency = _nlat_stats->Average();

典型值:

  • Mesh 2D(8×8):约10-15周期(平均跳数4-5)
  • Torus 2D(8×8):约8-12周期(平均跳数4)
  • FatTree:约6-10周期(树高度相关)

延迟-吞吐量曲线:

延迟
  |
  |     /|
  |    / |
  |   /  |
  |  /   |___________  (饱和点)
  | /    |
  |/     |
  +------+-----------+ 吞吐量
  0    饱和吞吐量   1.0

关键点:

  1. 零负载延迟:曲线起点(y轴截距)
  2. 线性区域:延迟缓慢增加
  3. 饱和点:延迟开始急剧上升
  4. 饱和吞吐量:饱和点对应的吞吐量

性能指标关系:

  1. 低负载(<饱和吞吐量)
    • 延迟接近零负载延迟
    • 吞吐量 = 注入率
    • 网络未饱和
  2. 饱和负载(=饱和吞吐量)
    • 延迟开始增加
    • 吞吐量 = 饱和吞吐量
    • 网络达到最大吞吐量
  3. 过饱和负载(>饱和吞吐量)
    • 延迟急剧增加
    • 吞吐量不再提升(甚至下降)
    • 网络拥塞严重

BookSim2.0中的统计:

// 平台延迟(从生成到接收)
int plat = GetSimTime() - f->ctime;
_plat_stats[src][dest]->AddSample(plat);

// 网络延迟(从注入到弹出)
int nlat = f->atime - f->itime;
_nlat_stats[src][dest]->AddSample(nlat);

// 吞吐量
double throughput = _accepted_flits / (GetSimTime() * _nodes);

应用场景:

  1. 性能评估
    • 零负载延迟:评估路由算法效率
    • 饱和吞吐量:评估网络容量
  2. 设计优化
    • 降低零负载延迟:优化路由、减少跳数
    • 提高饱和吞吐量:增加带宽、优化分配
  3. 负载规划
    • 实际负载应 < 饱和吞吐量
    • 保持延迟在可接受范围

路由算法(Q2.7-Q2.11)

Q2.7 维度顺序路由(dimension-order routing)的算法是什么?它如何保证无死锁?有什么局限性?

答案:

算法原理:

维度顺序路由按照固定顺序遍历各个维度,在每个维度上先完成所有移动,再进入下一个维度。

2D Mesh示例:

源节点:(2, 1)
目标节点:(5, 4)

路由路径:
1. X维度:2 → 3 → 4 → 5  (先完成X方向)
2. Y维度:1 → 2 → 3 → 4  (再完成Y方向)

路径:(2,1) → (3,1) → (4,1) → (5,1) → (5,2) → (5,3) → (5,4)

BookSim2.0中的实现:

// src/routefunc.cpp
void dim_order_mesh(const Router *r, const Flit *f, int in_channel,
                    OutputSet *outputs, bool inject) {
  int cur = r->GetID();
  int dest = f->dest;
  
  int cur_x = cur % gK;
  int cur_y = cur / gK;
  int dest_x = dest % gK;
  int dest_y = dest / gK;
  
  int out_port;
  
  // X维度优先
  if (cur_x != dest_x) {
    if (cur_x < dest_x) {
      out_port = PORT_X_PLUS;  // 向东
    } else {
      out_port = PORT_X_MINUS;  // 向西
    }
  }
  // Y维度其次
  else if (cur_y != dest_y) {
    if (cur_y < dest_y) {
      out_port = PORT_Y_PLUS;  // 向北
    } else {
      out_port = PORT_Y_MINUS;  // 向南
    }
  }
  // 到达目标
  else {
    out_port = PORT_EJECT;  // 弹出
  }
  
  outputs->AddRange(out_port, vcBegin, vcEnd);
}

Torus中的实现(考虑环):

void dim_order_torus(const Router *r, const Flit *f, int in_channel,
                     OutputSet *outputs, bool inject) {
  int cur = r->GetID();
  int dest = f->dest;
  
  int out_port;
  dor_next_torus(cur, dest, in_channel, &out_port, &f->ph, false);
  
  // VC分离避免死锁
  if (cur != dest) {
    int available_vcs = (vcEnd - vcBegin + 1) / 2;
    if (f->ph == 0) {
      vcEnd -= available_vcs;  // 使用VC 0-3
    } else {
      vcBegin += available_vcs;  // 使用VC 4-7
    }
  }
  
  outputs->AddRange(out_port, vcBegin, vcEnd);
}

无死锁保证:

  1. 无循环依赖
    • 路由路径是最小路径(最短路径)
    • 不会形成循环
  2. 确定性路由
    • 相同源-目标对总是走相同路径
    • 避免路由振荡
  3. VC分离(Torus)
    • 使用两个VC集合
    • 不同环分区使用不同VC
    • 打破可能的循环

VC分离机制:

在Torus中,可能存在循环路径(通过环连接):

节点0 → 节点7(通过环:0→1→...→7)
节点7 → 节点0(通过环:7→6→...→0)
可能形成循环

解决方案:

  • VC 0-3:用于不跨越数据线(dateline)的路径
  • VC 4-7:用于跨越数据线的路径
  • 数据线:Torus中节点k-1到节点0的连接

局限性:

  1. 负载不均衡
    • 所有相同X坐标的通信都经过相同路径
    • 容易产生热点(hotspot)
  2. 无自适应能力
    • 无法根据负载选择路径
    • 无法避开拥塞区域
  3. 路径固定
    • 即使有更空闲的路径,也必须走固定路径
    • 无法利用网络冗余
  4. 对故障敏感
    • 如果路径上节点故障,无法绕行
    • 需要上层协议处理

性能特征:

  • 延迟:低(最小路径)
  • 吞吐量:中等(受热点限制)
  • 实现复杂度:低(简单算法)
  • 适用场景:低负载、简单应用

改进方向:

  1. 自适应维度顺序
    • 根据负载选择维度顺序
    • 部分自适应能力
  2. 多路径维度顺序
    • 在维度内允许多条路径
    • 提高负载均衡
  3. 混合路由
    • 低负载用维度顺序
    • 高负载切换到自适应

Q2.8 自适应路由(adaptive routing)和确定性路由(deterministic routing)的区别是什么?自适应路由如何实现负载均衡?

答案:

确定性路由(Deterministic Routing):

定义: 相同源-目标对总是选择相同的路径,路径选择不依赖于网络状态。

特点:

  • 路径固定,可预测
  • 实现简单,开销低
  • 无法避开拥塞
  • 可能产生热点

示例:

  • 维度顺序路由(dim_order)
  • 源路由(source routing)

自适应路由(Adaptive Routing):

定义: 根据网络状态(如队列长度、负载)动态选择路径,相同源-目标对可能走不同路径。

特点:

  • 路径可变,根据负载选择
  • 实现复杂,需要负载信息
  • 可以避开拥塞
  • 更好的负载均衡

BookSim2.0中的自适应路由示例:

// adaptive_xy_yx_mesh: 在XY和YX之间自适应选择
void adaptive_xy_yx_mesh(const Router *r, const Flit *f, int in_channel,
                         OutputSet *outputs, bool inject) {
  int out_port_xy = dor_next_mesh(r->GetID(), f->dest, false);  // XY路径
  int out_port_yx = dor_next_mesh(r->GetID(), f->dest, true);   // YX路径
  
  bool x_then_y;
  if (in_channel < 2*gN) {
    // 已在网络中,根据VC判断
    x_then_y = (f->vc < (vcBegin + available_vcs));
  } else {
    // 注入时,根据负载选择
    int credit_xy = r->GetUsedCredit(out_port_xy);  // XY路径的队列长度
    int credit_yx = r->GetUsedCredit(out_port_yx);   // YX路径的队列长度
    
    if (credit_xy > credit_yx) {
      x_then_y = false;  // 选择YX(队列更短)
    } else if (credit_xy < credit_yx) {
      x_then_y = true;   // 选择XY(队列更短)
    } else {
      x_then_y = (RandomInt(1) > 0);  // 随机选择
    }
  }
  
  if (x_then_y) {
    out_port = out_port_xy;
    vcEnd -= available_vcs;  // 使用VC 0-3
  } else {
    out_port = out_port_yx;
    vcBegin += available_vcs;  // 使用VC 4-7
  }
}

负载均衡实现:

  1. 本地负载信息
    // 获取输出端口的队列长度
    int queue_length = r->GetUsedCredit(output_port);
       
    // 选择队列最短的路径
    if (queue_xy < queue_yx) {
      choose_xy_path();
    } else {
      choose_yx_path();
    }
    
  2. 全局负载信息(高级)
    // 收集路径上所有路由器的负载
    int total_load_xy = 0;
    for (each router on xy_path) {
      total_load_xy += router->GetLoad();
    }
       
    // 选择总负载最小的路径
    
  3. 阈值机制
    // 只有当负载差异超过阈值时才切换路径
    int threshold = 2;
    if (abs(queue_xy - queue_yx) > threshold) {
      choose_lighter_path();
    } else {
      use_default_path();
    }
    

对比总结:

特性 确定性路由 自适应路由
路径选择 固定 动态
实现复杂度
负载均衡
延迟(低负载)
延迟(高负载) 中等
吞吐量 中等
硬件开销 高(需要负载信息)

适用场景:

  • 确定性路由:低负载、简单应用、资源受限
  • 自适应路由:高负载、性能关键、有冗余路径

Q2.9 UGAL(Universal Globally Adaptive Load-balanced)路由算法的核心思想是什么?它如何选择最小路径和非最小路径?

答案:

UGAL核心思想:

UGAL是一种全局自适应路由算法,在最小路径和非最小路径之间进行选择,目标是全局负载均衡。

关键特点:

  1. 两阶段路由:先到中间节点(非最小),再到目标(最小)
  2. 全局视角:考虑整个路径的负载,而不仅仅是下一跳
  3. 负载感知:根据队列长度选择路径

BookSim2.0中的UGAL实现(Dragonfly):

void ugal_dragonflynew(const Router *r, const Flit *f, int in_channel,
                       OutputSet *outputs, bool inject) {
  int adaptive_threshold = 30;  // 自适应阈值
  
  // 在源路由器做决策
  if (in_channel < gP) {  // 注入通道
    if (dest_grp_ID == grp_ID) {
      // 同组内,只用最小路由
      f->ph = 2;
    } else {
      // 跨组通信
      // 选择随机中间节点
      f->intm = RandomInt(_network_size - 1);
      intm_grp_ID = (int)(f->intm / _grp_num_nodes);
      
      if (grp_ID == intm_grp_ID) {
        // 中间节点在同组,直接最小路由
        f->ph = 1;
      } else {
        // 比较最小路径和非最小路径的负载
        min_router_output = dragonfly_port(rID, f->src, f->dest);
        min_queue_size = max(r->GetUsedCredit(min_router_output), 0);
        
        nonmin_router_output = dragonfly_port(rID, f->src, f->intm);
        nonmin_queue_size = max(r->GetUsedCredit(nonmin_router_output), 0);
        
        // UGAL决策:考虑跳数权重
        // 最小路径:1跳的负载
        // 非最小路径:2跳的负载(需要乘以2)
        if ((1 * min_queue_size) <= (2 * nonmin_queue_size) + adaptive_threshold) {
          f->ph = 1;  // 选择最小路径
        } else {
          f->ph = 0;  // 选择非最小路径
        }
      }
    }
  }
  
  // 路由阶段
  if (f->ph == 0) {
    // 阶段0:路由到中间节点(非最小)
    out_port = dragonfly_port(rID, f->src, f->intm);
  } else if (f->ph == 1) {
    // 阶段1:从中间节点路由到目标(最小)
    out_port = dragonfly_port(rID, f->src, f->dest);
  }
}

决策公式:

如果 (1 × min_queue) ≤ (2 × nonmin_queue) + threshold:
    选择最小路径
否则:
    选择非最小路径

权重说明:

  • 最小路径权重 = 1:因为只有1跳
  • 非最小路径权重 = 2:因为需要2跳(源→中间→目标)

阈值作用:

  • threshold > 0:偏向最小路径(需要明显优势才选非最小)
  • threshold < 0:偏向非最小路径(更容易负载均衡)
  • threshold = 0:完全基于负载

路径选择示例:

源节点S → 目标节点D

最小路径:S → D(1跳,队列长度=10)
非最小路径:S → I → D(2跳,队列长度=3, 4)

决策:
1 × 10 = 10
2 × (3 + 4) = 14

10 < 14 + 30,选择最小路径

但如果最小路径队列更长:

最小路径:队列长度=20
非最小路径:队列长度=3, 4

1 × 20 = 20
2 × (3 + 4) = 14

20 > 14 + 30?否,但如果阈值更小:
20 > 14 + 5 = 19,选择非最小路径

优势:

  1. 全局负载均衡
    • 考虑整个路径的负载
    • 而不仅仅是下一跳
  2. 避免热点
    • 当最小路径拥塞时,选择非最小路径
    • 分散流量
  3. 性能提升
    • 在高负载下显著提升吞吐量
    • 降低延迟

局限性:

  1. 实现复杂
    • 需要全局负载信息
    • 需要中间节点选择机制
  2. 延迟增加
    • 非最小路径增加跳数
    • 可能增加延迟
  3. 需要更多VC
    • 不同阶段需要不同VC
    • 增加资源需求

适用场景:

  • 大规模网络:Dragonfly、FatTree等
  • 高负载场景:需要负载均衡
  • 有冗余路径:网络有多个可选路径

Q2.10 Turn Model是什么?它如何用于死锁避免?请举例说明West-First、North-Last、Negative-First的区别。

答案:

Turn Model定义:

Turn Model是一种死锁避免方法,通过禁止某些转向(turn)来打破可能的循环依赖。

转向(Turn)定义:

在2D Mesh中,转向是指从一个方向转到另一个方向:

例如:从北(North)转向东(East):N→E转向

2D Mesh中的8种转向:

N→E, N→W  (从北转向)
S→E, S→W  (从南转向)
E→N, E→S  (从东转向)
W→N, W→S  (从西转向)

死锁避免原理:

如果允许所有转向,可能形成循环:

N→E → E→S → S→W → W→N  (形成循环,可能死锁)

解决方案: 禁止某些转向,打破所有可能的循环。

West-First路由:

规则: 必须先完成所有向西的移动,才能转向其他方向。

禁止的转向:

  • N→W(从北转向西)
  • S→W(从南转向西)
  • E→W(从东转向西)

允许的转向:

  • N→E, N→S
  • S→E, S→N
  • E→N, E→S
  • W→N, W→E, W→S

路由示例:

源(2,1) → 目标(0,3)

路径:
1. 先向西:(2,1) → (1,1) → (0,1)  (完成所有西向移动)
2. 再向北:(0,1) → (0,2) → (0,3)   (可以转向)

North-Last路由:

规则: 所有向北的移动必须最后完成。

禁止的转向:

  • E→N(从东转向北)
  • W→N(从西转向北)
  • S→N(从南转向北)

允许的转向:

  • N→E, N→W, N→S
  • S→E, S→W
  • E→S, E→W
  • W→E, W→S

路由示例:

源(2,1) → 目标(2,3)

路径:
1. 先完成其他方向(如果需要)
2. 最后向北:(2,1) → (2,2) → (2,3)

Negative-First路由:

规则: 在负方向(西、南)的移动必须先于正方向(东、北)的移动。

禁止的转向:

  • E→N(从东转向北)
  • E→S(从东转向南)- 实际上这个允许
  • N→W(从北转向西)
  • S→W(从南转向西)

更准确的定义:

  • 禁止从正方向转向负方向
  • 允许从负方向转向正方向

路由示例:

源(2,3) → 目标(0,1)

路径:
1. 先向西(负方向):(2,3) → (1,3) → (0,3)
2. 再向南(负方向):(0,3) → (0,2) → (0,1)

对比总结:

路由算法 禁止转向 特点 适用场景
West-First N→W, S→W, E→W 先完成西向 目标多在西部
North-Last E→N, W→N, S→N 最后完成北向 目标多在北部
Negative-First E→N, N→W等 先完成负向 平衡性好

BookSim2.0中的实现:

虽然BookSim2.0没有直接实现Turn Model路由函数,但可以通过修改路由函数实现:

// 伪代码示例
void west_first_mesh(const Router *r, const Flit *f, int in_channel,
                     OutputSet *outputs, bool inject) {
  int cur_x = cur % gK;
  int cur_y = cur / gK;
  int dest_x = dest % gK;
  int dest_y = dest / gK;
  
  int out_port;
  
  // West-First: 先完成所有西向移动
  if (cur_x > dest_x) {
    // 必须向西
    out_port = PORT_WEST;
  }
  // 完成西向后,可以其他方向
  else if (cur_y != dest_y) {
    if (cur_y < dest_y) {
      out_port = PORT_NORTH;
    } else {
      out_port = PORT_SOUTH;
    }
  }
  // ...
}

死锁避免保证:

Turn Model通过禁止某些转向,确保:

  1. 无循环依赖:禁止的转向不会形成循环
  2. 可达性:所有节点仍然可达(通过允许的转向)
  3. 最小路径:在限制下仍能找到路径(可能非最小)

优势:

  • 简单实现
  • 不需要VC分离(理论上)
  • 保证无死锁

劣势:

  • 可能产生非最小路径
  • 某些路径可能绕远
  • 负载不均衡

Q2.11 Valiant路由和ROMM路由都是非最小路由,它们的设计动机是什么?在什么场景下使用非最小路由?

答案:

非最小路由定义:

非最小路由允许数据包走非最短路径,可能增加跳数,但可以避开拥塞和热点。

Valiant路由:

核心思想: 两阶段路由

  1. 阶段1:从源节点路由到随机选择的中间节点(可能非最小)
  2. 阶段2:从中间节点路由到目标节点(最小路径)

路由示例:

源节点S → 目标节点D

阶段1:S → I(随机中间节点,可能绕远)
阶段2:I → D(最小路径)

总路径:S → ... → I → ... → D

设计动机:

  1. 负载均衡
    • 随机中间节点分散流量
    • 避免所有流量走相同路径
  2. 避免热点
    • 不直接走最小路径
    • 通过中间节点绕行
  3. 简单实现
    • 只需随机选择中间节点
    • 然后最小路由到中间和目标

BookSim2.0中的实现(FlatFly):

void valiant_flatfly(const Router *r, const Flit *f, int in_channel,
                     OutputSet *outputs, bool inject) {
  // 阶段0:初始决策
  if (in_channel < gC) {  // 注入时
    // 随机选择中间节点
    f->intm = RandomInt(_network_size - 1);
    f->ph = 0;  // 阶段0:路由到中间节点
  }
  
  // 阶段1:路由到中间节点
  if (f->ph == 0) {
    if (到达中间节点) {
      f->ph = 1;  // 切换到阶段1
    } else {
      out_port = route_to(f->intm);  // 路由到中间节点
    }
  }
  
  // 阶段2:从中间节点路由到目标
  if (f->ph == 1) {
    out_port = route_to(f->dest);  // 最小路由到目标
  }
}

ROMM路由(Randomized Oblivious Minimal Routing):

核心思想: 在多个最小路径中随机选择,而不是固定一条。

与Valiant的区别:

  • Valiant:允许非最小路径(可能增加跳数)
  • ROMM:只选择最小路径,但在多个最小路径中随机

示例(2D Mesh):

源(2,1) → 目标(5,4)

最小路径有两条:
路径1:(2,1) → (3,1) → (4,1) → (5,1) → (5,2) → (5,3) → (5,4)
路径2:(2,1) → (2,2) → (2,3) → (2,4) → (3,4) → (4,4) → (5,4)

ROMM随机选择其中一条

设计动机:

  1. 负载均衡
    • 在多个最小路径间分散流量
    • 避免所有流量走同一条路径
  2. 保持最小路径
    • 不增加跳数
    • 延迟不会增加
  3. 简单实现
    • 只需在多个最小路径中选择
    • 不需要全局负载信息

适用场景:

Valiant路由适用于:

  1. 高负载场景
    • 最小路径可能拥塞
    • 需要分散流量
  2. 有冗余路径的网络
    • Dragonfly、FatTree等 Dragonfly拓扑:https://www.modb.pro/db/650884
    • 有多个可选路径

Dragonfly拓扑详解:

Dragonfly是一种分层网络拓扑,专为大规模高性能计算系统设计,通过组内(intra-group)和组间(inter-group)两级结构实现高可扩展性和丰富的路径冗余。

拓扑结构:

  • 三层架构
    1. 组内网络(Intra-group):每个组内的路由器通过全连接或近似全连接互联
    2. 组间网络(Inter-group):不同组之间通过部分连接互联
    3. 终端节点(Terminal):每个路由器连接p个处理器节点

BookSim2中的参数定义:

// src/networks/dragonfly.cpp
_p = config.GetInt("k");  // 每个路由器的处理器端口数
_n = config.GetInt("n");  // 组内维度(目前只支持n=1)

拓扑参数计算(n=1时):

  • 每个组的路由器数a = 2 * p
  • 组数g = a * p + 1 = 2p² + 1
  • 总节点数N = a * p * g = 2p * p * (2p² + 1)
  • 每个路由器的总端口数k = p + p + (2p - 1) = 4p - 1
    • p个终端端口(连接处理器)
    • p个组间端口(连接其他组)
    • 2p - 1个组内端口(连接同组其他路由器)

拓扑特点:

  1. 高可扩展性:节点数随p²增长,适合超大规模系统
  2. 丰富的路径冗余:组内全连接提供多条路径,组间也有多条可选路径
  3. 低直径:理论上最多3跳(组内1跳 + 组间1跳 + 目标组内1跳)
  4. 路由挑战:需要智能路由算法避免组间链路拥塞

BookSim2中的路由函数:

  • min_dragonflynew:最小路径路由,优先组内路径
  • ugal_dragonflynew:UGAL(Universal Globally-Adaptive Load-balanced)自适应路由,根据负载选择路径

配置示例:

topology = dragonflynew;
k = 4;  // 每个路由器的处理器端口数
n = 1;  // 组内维度(必须为1routing_function = min;  // 或 ugal

FatTree拓扑详解:

FatTree(胖树)是一种多级树形拓扑,通过”上行带宽等于下行带宽”的设计(即”胖”的中间层)消除树形网络的根节点瓶颈,广泛应用于数据中心网络。

拓扑结构:

  • 分层树形架构
    1. 第0层(顶层)k^(n-1)个路由器,每个有k个端口(只连接下层)
    2. 第1到n-1层(中间层):每层k^(n-1)个路由器,每个有2k个端口(k个上行,k个下行)
    3. 第n-1层(叶子层)k^(n-1)个路由器,每个有2k个端口(k个上行,k个连接终端节点)

BookSim2中的参数定义:

// src/networks/fattree.cpp
_k = config.GetInt("k");  // 基数(每个路由器的分支数)
_n = config.GetInt("n");  // 层数

拓扑参数计算:

  • 终端节点数nodes = k^n
  • 路由器总数size = n * k^(n-1)
  • 链路总数channels = 2 * k * k^(n-1) * (n-1)
  • 每个路由器端口数
    • 顶层:k个端口(只下行)
    • 其他层:2k个端口(k个上行 + k个下行)

端口分配规则(BookSim2实现):

// src/networks/fattree.cpp
// 输出端口 < k:向下(toward leaves)
// 输出端口 >= k:向上(toward root)
// 输入端口 < k:来自下方
// 输入端口 >= k:来自上方

拓扑特点:

  1. 无阻塞上行:从任意叶子节点到根节点的上行带宽等于所有叶子节点的总带宽
  2. 完全二分带宽:任意两个叶子节点间都有k^(n-1)条不相交路径
  3. 确定性路由:基于最近公共祖先(NCA)的路由简单高效
  4. 可扩展性:节点数随k^n增长,但路由器数量也快速增长

BookSim2中的路由函数:

  • fattree_nca:最近公共祖先路由 + 随机上行选择
    • 下行:沿树向下到目标子树
    • 上行:随机选择上行端口(负载均衡)
  • fattree_anca:最近公共祖先路由 + 自适应上行选择
    • 下行:沿树向下到目标子树
    • 上行:根据负载信息自适应选择上行端口

路由算法原理:

  1. 上行阶段:从源节点向上到源和目标的最小公共祖先(LCA)
  2. 下行阶段:从LCA向下到目标节点
  3. 路径选择:在LCA处,有k^(n-1)条路径可选,通过随机或自适应选择实现负载均衡

配置示例:

topology = fattree;
k = 4;  // 基数
n = 3;  // 3层树(256个节点)
routing_function = nca_fattree;  // 或 anca_fattree

两种拓扑的对比:

特性 Dragonfly FatTree
结构 分层(组内+组间) 多级树形
可扩展性 极高(O(p²)) 中等(O(k^n))
直径 3跳(理想) 2(n-1)跳
路径冗余 组内全连接+组间多路径 完全二分带宽
路由复杂度 高(需避免组间拥塞) 低(NCA算法简单)
适用场景 超大规模HPC系统 数据中心网络
BookSim2路由 min/ugal nca/anca

当前,Fat-Tree 是业界普遍认可的实现无阻塞网络的技术。其基本理念是:使用大量低性能的交换机,构建出大规模的无阻塞网络,对于任意的通信模式,总有路径让他们的通信带宽达到网卡带宽。Fat-Tree 的另一个好处是,它用到的所有交换机都是相同的,这让我们能够在整个数据中心网络架构中采用廉价的交换机。

Fat-Tree 是无带宽收敛的:传统的树形网络拓扑中,带宽是逐层收敛的,树根处的网络带宽要远小于各个叶子处所有带宽的总和。而 Fat-Tree 则更像是真实的树,越到树根,枝干越粗,即:从叶子到树根,网络带宽不收敛。这是 Fat-Tree 能够支撑无阻塞网络的基础。

为了实现网络带宽的无收敛,Fat-Tree 中的每个节点(根节点除外)都需要保证上行带宽和下行带宽相等,并且每个节点都要提供对接入带宽的线速转发的能力。

下图是一个 2 元 4 层 Fat-Tree 的物理结构示例(2 元:每个叶子交换机接入 2 台终端;4 层:网络中的交换机分为 4 层),其使用的所有物理交换机都是完全相同的。

从图中可以看到,每个叶子节点就是一台物理交换机,接入 2 台终端;上面一层的内部节点,则是每个逻辑节点由 2 台物理交换机组成;再往上面一层则每个逻辑节点由 4 台物理交换机组成;根节点一共有 8 台物理交换机。这样,任意一个逻辑节点,下行带宽和上行带宽是完全一致的。这保证了整个网络带宽是无收敛的。同时我们还可以看到,对于根节点,有一半的带宽并没有被用于下行接入,这是 Fat-Tree 为了支持弹性扩展,而为根节点预留的上行带宽,通过把 Fat-Tree 向根部继续延伸,即可实现网络规模的弹性扩展。

假设一个 k-ary(每个节点有不超过 k 个子节点)的三层 Fat-Tree 拓扑:

核心交换机个数 (k/2)^2 POD 个数 k 每个 POD 汇聚交换机 k/2 每个 POD 接口交换机 k/2 每个接入交换机连接的终端服务器 k/2 每个接入交换机剩余 k/2 个口连接 POD 内 k/2 个汇聚交换机,每台核心交换机的第 i 个端口连接到第 i 个 POD,所有交换机均采用 k-port switch。 可以计算出,支持的服务器个数为 k * (k/2) * (k/2) = (k^3)/4,不同 POD 下服务器间等价路径数 (k/2) * (k/2) = (k^2)/4。上图为最简单的 k=4 时的 Fat-Tree 拓扑,连在同一个接入交换机下的服务器处于同一个子网,他们之间的通信走二层报文交换。不同接入交换机下的服务器通信,需要走路由。

Fat-Tree 的缺陷 Fat-Tree 的扩展规模在理论上受限于核心层交换机的端口数目,不利于数据中心的长期发展要求; 对于 POD 内部,Fat-Tree 容错性能差,对底层交换设备故障非常敏感,当底层交换设备故障时,难以保证服务质量; Fat-Tree 拓扑结构的特点决定了网络不能很好的支持 One-to-All及 All-to-All 网络通信模式,不利于部署 MapReduce、Dryad 等高性能分布式应用; Fat-Tree 网络中交换机与服务器的比值较大,在一定程度上使得网络设备成本依然很高,不利于企业的经济发展。 因为要防止出现 TCP 报文乱序的问题,难以达到 1:1 的超分比。

  1. 热点问题严重
    • 某些路径成为热点
    • 需要绕行

ROMM路由适用于:

  1. 中等负载场景
    • 最小路径可用
    • 只需在路径间均衡
  2. 延迟敏感应用
    • 不能接受非最小路径的延迟增加
    • 但仍需要负载均衡
  3. 有多个最小路径的网络
    • Mesh、Torus等
    • 源-目标对间有多条最小路径

性能对比:

路由算法 路径类型 延迟 吞吐量 实现复杂度
最小路由 最小 中等
ROMM 最小 中等
Valiant 非最小 中等 中等

BookSim2.0中的使用:

// 配置示例
routing_function = valiant_flatfly;  // 使用Valiant路由
// 或
routing_function = romm_torus;      // 使用ROMM路由

总结:

  • Valiant:牺牲延迟换取负载均衡,适合高负载
  • ROMM:保持最小路径但随机选择,适合中等负载
  • 选择原则:根据网络负载和延迟要求选择

流控与分配(Q2.12-Q2.15)

Q2.12 VC分配和开关分配的区别是什么?为什么需要两个独立的分配阶段?

答案:

VC分配(Virtual Channel Allocation):

定义: 为Flit分配输出虚通道(VC),决定Flit使用哪个输出VC。

输入: 输入VC(已有Flit)
输出: 输出VC(目标VC)
问题: 多个输入VC可能竞争同一个输出VC

开关分配(Switch Allocation):

定义: 为Flit分配交叉开关路径,决定哪个输入端口连接到哪个输出端口。

输入: 输入端口
输出: 输出端口
问题: 多个输入端口可能竞争同一个输出端口

关键区别:

特性 VC分配 开关分配
分配对象 虚通道(VC) 物理端口(Port)
粒度 VC级别 端口级别
作用 决定使用哪个VC 决定使用哪个物理路径
时机 VC分配之后 VC分配之后
资源 VC缓冲区 交叉开关

为什么需要两个阶段:

  1. 不同的资源类型
    • VC分配:分配逻辑资源(VC缓冲区)
    • 开关分配:分配物理资源(交叉开关)
  2. 不同的竞争维度
    • VC分配:解决”哪个输入VC → 哪个输出VC”
    • 开关分配:解决”哪个输入端口 → 哪个输出端口”
  3. 依赖关系
    路由计算 → VC分配 → 开关分配 → 传输
    
    • VC分配需要路由结果(知道目标端口)
    • 开关分配需要VC分配结果(知道使用哪个VC)
  4. 独立优化
    • VC分配可以优化VC利用率
    • 开关分配可以优化端口利用率
    • 分开可以独立优化

BookSim2.0中的实现:

// IQRouter流水线
void IQRouter::_InternalStep() {
  // 1. 路由计算
  _RouteEvaluate();  // 计算输出端口
  
  // 2. VC分配
  _VCAllocEvaluate();  // 分配输出VC
  _VCAllocUpdate();
  
  // 3. 开关分配
  _SWAllocEvaluate();  // 分配交叉开关
  _SWAllocUpdate();
  
  // 4. 传输
  _SwitchEvaluate();  // 通过交叉开关传输
}

VC分配过程:

void IQRouter::_VCAllocEvaluate() {
  _vc_allocator->Clear();
  
  for (auto vc : _vc_alloc_vcs) {
    int input = vc.first;
    int in_vc = vc.second;
    
    // 获取路由结果
    const OutputSet *route_set = _buf[input]->GetRouteSet(in_vc);
    
    // 尝试每个可能的输出
    int out_port, out_vc;
    while (route_set->GetPortVC(&out_port, &out_vc)) {
      // 检查输出VC是否可用
      if (_next_buf[out_port]->IsAvailableFor(out_vc)) {
        // 添加VC分配请求
        _vc_allocator->AddRequest(input * _vcs + in_vc, 
                                  out_port * _vcs + out_vc);
        break;
      }
    }
  }
  
  _vc_allocator->Allocate();  // 执行分配
}

开关分配过程:

void IQRouter::_SWAllocEvaluate() {
  _sw_allocator->Clear();
  
  for (auto vc : _sw_alloc_vcs) {
    int input = vc.first.first;
    int in_vc = vc.first.second;
    int out_port = _buf[input]->GetOutputPort(in_vc);
    int out_vc = _buf[input]->GetOutputVC(in_vc);
    
    // 添加开关分配请求
    _sw_allocator->AddRequest(input, out_port);
  }
  
  _sw_allocator->Allocate();  // 执行分配
}

如果合并的问题:

  1. 无法独立优化
    • VC分配和开关分配耦合
    • 难以分别优化
  2. 复杂度增加
    • 需要同时考虑VC和端口
    • 分配算法更复杂
  3. 灵活性降低
    • 无法替换独立的分配器
    • 无法使用不同的分配策略

实际例子:

输入VC 0(输入端口0) → 输出端口1,输出VC 2
输入VC 1(输入端口0) → 输出端口1,输出VC 3
输入VC 0(输入端口1) → 输出端口1,输出VC 2

VC分配:
- 输入VC(0,0) → 输出VC(1,2) ✓
- 输入VC(0,1) → 输出VC(1,3) ✓
- 输入VC(1,0) → 输出VC(1,2) ✗(冲突,VC 2已被占用)

开关分配:
- 输入端口0 → 输出端口1 ✓
- 输入端口1 → 输出端口1 ✗(冲突,端口1已被占用)

总结:

两个独立阶段的好处:

  1. 清晰分离:逻辑资源和物理资源分离
  2. 独立优化:可以分别优化VC和端口利用率
  3. 灵活替换:可以独立替换分配器
  4. 易于理解:每个阶段职责明确

Q2.13 iSLIP分配算法的迭代过程是什么?为什么需要多轮迭代?收敛性如何保证?

答案:

iSLIP算法:

iSLIP(iterative Separable Input Priority)是一种迭代分配算法,用于解决输入-输出匹配问题。

单次迭代过程:

每轮迭代包含两个阶段:

阶段1:Grant(授权)

  • 每个输出端口从请求它的输入中选择一个
  • 使用Round-Robin仲裁器,从上次授权的位置开始

阶段2:Accept(接受)

  • 每个输入端口从授权给它的输出中选择一个
  • 使用Round-Robin仲裁器,从上次接受的位置开始

BookSim2.0中的实现:

void iSLIP_Sparse::Allocate() {
  for (int iter = 0; iter < _iSLIP_iter; ++iter) {
    // Grant阶段
    vector<int> grants(_outputs, -1);
    
    for (int output = 0; output < _outputs; ++output) {
      if (_out_req[output].empty() || _outmatch[output] != -1) {
        continue;  // 跳过已匹配的输出
      }
      
      // Round-Robin选择输入
      int input_offset = _gptrs[output];  // 从上次位置开始
      
      // 找到第一个未匹配的输入
      for (auto req : _out_req[output]) {
        int input = req.second.port;
        if (input >= input_offset && _inmatch[input] == -1) {
          grants[output] = input;
          break;
        }
      }
      
      // 如果没找到,从头开始(wrapped)
      if (grants[output] == -1) {
        for (auto req : _out_req[output]) {
          int input = req.second.port;
          if (input < input_offset && _inmatch[input] == -1) {
            grants[output] = input;
            break;
          }
        }
      }
    }
    
    // Accept阶段
    for (int input = 0; input < _inputs; ++input) {
      if (_in_req[input].empty() || _inmatch[input] != -1) {
        continue;  // 跳过已匹配的输入
      }
      
      // Round-Robin选择输出
      int output_offset = _aptrs[input];
      
      // 找到授权给此输入的输出
      for (auto req : _in_req[input]) {
        int output = req.second.port;
        if (output >= output_offset && grants[output] == input) {
          // 接受匹配
          _inmatch[input] = output;
          _outmatch[output] = input;
          
          // 只在第一轮迭代更新指针
          if (iter == 0) {
            _gptrs[output] = (input + 1) % _inputs;
            _aptrs[input] = (output + 1) % _outputs;
          }
          break;
        }
      }
    }
  }
}

为什么需要多轮迭代:

  1. 单轮可能无法完成所有匹配
    • 第一轮可能只匹配部分输入-输出对
    • 剩余未匹配的可以在后续轮次匹配
  2. 提高匹配率
    • 多轮迭代可以找到更多匹配
    • 接近最大匹配
  3. 公平性
    • Round-Robin指针在多轮中更新
    • 确保长期公平性

示例:

初始状态:
输入0请求:输出0, 1
输入1请求:输出0, 1
输入2请求:输出1

输出0请求:输入0, 1
输出1请求:输入0, 1, 2

第1轮迭代:
Grant阶段:
  输出0:选择输入0(gptr=0)
  输出1:选择输入0(gptr=0)
Accept阶段:
  输入0:接受输出0(aptr=0)
  输入1:无授权
  输入2:无授权
匹配:输入0↔输出0

第2轮迭代:
Grant阶段:
  输出0:选择输入1(gptr=1,跳过已匹配的输入0)
  输出1:选择输入1(gptr=0,但输入0已匹配,选择输入1)
Accept阶段:
  输入1:接受输出1(aptr=1)
  输入2:无授权
匹配:输入1↔输出1

第3轮迭代:
Grant阶段:
  输出1:选择输入2(gptr=1,跳过已匹配的输入1)
Accept阶段:
  输入2:接受输出1
匹配:输入2↔输出1(但输出1已匹配,实际不会发生)

实际:输入2无法匹配(输出1已被占用)

收敛性保证:

  1. 单调性
    • 每轮迭代匹配数不会减少
    • 已匹配的对不会解除匹配
  2. 有限性
    • 最多N轮(N=输入/输出数)
    • 每轮至少匹配一个对(如果有未匹配的)
  3. 最大匹配
    • N轮迭代可以找到最大匹配
    • 实际中通常2-3轮就足够

指针更新规则:

// 只在第一轮迭代更新指针
if (iter == 0) {
  _gptrs[output] = (input + 1) % _inputs;   // 输出指针
  _aptrs[input] = (output + 1) % _outputs; // 输入指针
}

为什么只在第一轮更新:

  • 保证公平性:每个输入/输出都有机会
  • 避免振荡:后续轮次不更新,保持稳定

性能:

  • 迭代次数:通常2-3轮足够
  • 匹配率:接近100%(如果有足够资源)
  • 延迟:每轮1周期,总延迟=迭代数×1周期

配置:

// 配置迭代次数
int islip_iters = config.GetInt("islip_iters");  // 通常2-4

总结:

  • 多轮迭代:提高匹配率,找到更多匹配
  • 收敛性:单调递增,有限轮次内收敛
  • 公平性:Round-Robin确保长期公平

Q2.14 什么是可分离分配器(separable allocator)?输入优先和输出优先的区别是什么?

答案:

可分离分配器定义:

可分离分配器将输入-输出匹配问题分解为两个独立的阶段:

  1. 输入阶段:每个输入选择想要的输出
  2. 输出阶段:每个输出选择想要的输入

关键特点:

  • 两阶段独立执行
  • 可以使用不同的仲裁策略
  • 实现简单,延迟低

输入优先(Input-First):

执行顺序:

  1. 第一阶段:每个输入端口独立选择输出(输入仲裁)
  2. 第二阶段:每个输出端口从请求它的输入中选择(输出仲裁)

特点:

  • 输入先做决策
  • 输出响应输入的选择
  • 偏向输入端

BookSim2.0中的实现:

void SeparableInputFirstAllocator::Allocate() {
  // 第一阶段:输入仲裁
  for (int input : _in_occ) {
    // 输入选择输出
    for (auto req : _in_req[input]) {
      _input_arb[input]->AddRequest(req.port, req.label, req.in_pri);
    }
    
    // 执行输入仲裁器
    int label = -1;
    int output = _input_arb[input]->Arbitrate(&label, NULL);
    
    // 将输入的选择传递给输出仲裁器
    _output_arb[output]->AddRequest(input, label, req.out_pri);
  }
  
  // 第二阶段:输出仲裁
  for (int output : _out_occ) {
    // 输出从请求它的输入中选择
    int input = _output_arb[output]->Arbitrate(NULL, NULL);
    
    if (input > -1) {
      // 匹配成功
      _inmatch[input] = output;
      _outmatch[output] = input;
    }
  }
}

输出优先(Output-First):

执行顺序:

  1. 第一阶段:每个输出端口独立选择输入(输出仲裁)
  2. 第二阶段:每个输入端口从请求它的输出中选择(输入仲裁)

特点:

  • 输出先做决策
  • 输入响应输出的选择
  • 偏向输出端

实现(伪代码):

void SeparableOutputFirstAllocator::Allocate() {
  // 第一阶段:输出仲裁
  for (int output : _out_occ) {
    // 输出选择输入
    for (auto req : _out_req[output]) {
      _output_arb[output]->AddRequest(req.port, req.label, req.out_pri);
    }
    
    // 执行输出仲裁器
    int input = _output_arb[output]->Arbitrate(NULL, NULL);
    
    // 将输出的选择传递给输入仲裁器
    _input_arb[input]->AddRequest(output, label, req.in_pri);
  }
  
  // 第二阶段:输入仲裁
  for (int input : _in_occ) {
    // 输入从请求它的输出中选择
    int output = _input_arb[input]->Arbitrate(&label, NULL);
    
    if (output > -1) {
      // 匹配成功
      _inmatch[input] = output;
      _outmatch[output] = input;
    }
  }
}

区别对比:

特性 输入优先 输出优先
第一阶段 输入选择输出 输出选择输入
第二阶段 输出选择输入 输入选择输出
偏向 输入端 输出端
适用场景 输入竞争激烈 输出竞争激烈
公平性 对输入更公平 对输出更公平

示例:

输入0请求:输出0, 1
输入1请求:输出0, 1
输入2请求:输出1

输出0请求:输入0, 1
输出1请求:输入0, 1, 2

输入优先过程:

  1. 输入0选择输出0(假设)
  2. 输入1选择输出1(假设)
  3. 输入2选择输出1(假设)
  4. 输出0从{输入0}中选择 → 输入0
  5. 输出1从{输入1, 输入2}中选择 → 输入1(假设)

输出优先过程:

  1. 输出0从{输入0, 输入1}中选择 → 输入0(假设)
  2. 输出1从{输入0, 输入1, 输入2}中选择 → 输入1(假设)
  3. 输入0从{输出0}中选择 → 输出0
  4. 输入1从{输出1}中选择 → 输出1
  5. 输入2无匹配

选择建议:

  • 输入优先:当输入端口竞争激烈时(多个输入请求同一输出)
  • 输出优先:当输出端口竞争激烈时(多个输出请求同一输入)
  • 一般情况:输入优先更常用(因为通常输入数=输出数)

优势:

  1. 实现简单:只需两个独立的仲裁阶段
  2. 延迟低:两阶段即可完成
  3. 灵活:可以使用不同的仲裁器(Round-Robin、Priority等)

劣势:

  1. 匹配率可能较低:不如iSLIP等迭代算法
  2. 公平性:偏向某一端,可能不公平

Q2.15 Buffer状态机有哪些状态(idle、routing、vc_alloc、active、waiting)?状态转换的条件是什么?

答案:

VC状态定义:

在BookSim2.0中,VC(虚通道)有以下状态:

// src/vc.hpp
enum eVCState {
  idle = 0,      // 空闲
  routing,       // 等待路由
  vc_alloc,      // 等待VC分配
  active,        // 活跃传输
  // waiting状态在某些实现中存在
};

状态转换图:

idle → routing → vc_alloc → active → idle
  ↑                                    ↓
  └────────────────────────────────────┘

各状态说明:

1. idle(空闲)

含义: VC为空,没有Flit,等待新Flit到达。

进入条件:

  • VC初始化
  • 上一个Packet传输完成(tail flit发送)

退出条件:

  • Head flit到达

代码:

void VC::AddFlit(Flit *f) {
  if (f->head && _state == idle) {
    SetState(routing);  // 转到routing状态
  }
  _buffer.push_back(f);
}

2. routing(等待路由)

含义: Head flit已到达,等待路由计算。

进入条件:

  • Head flit到达(从idle转换)

退出条件:

  • 路由计算完成
  • 转到vc_alloc状态

代码:

void IQRouter::_RouteEvaluate() {
  for (auto vc : _route_vcs) {
    Buffer::Route(vc, _rf, this, flit, input);
    SetState(vc_alloc);  // 路由完成,转到vc_alloc
  }
}

3. vc_alloc(等待VC分配)

含义: 路由已完成,等待分配输出VC。

进入条件:

  • 路由计算完成(从routing转换)

退出条件:

  • VC分配成功 → 转到active
  • VC分配失败 → 保持在vc_alloc(下一周期重试)

代码:

void IQRouter::_VCAllocUpdate() {
  if (vc_allocated) {
    _buf[input]->SetOutput(vc, out_port, out_vc);
    _buf[input]->SetState(vc, active);  // 转到active
  }
  // 否则保持在vc_alloc状态
}

4. active(活跃传输)

含义: VC分配成功,正在传输Flit。

进入条件:

  • VC分配成功(从vc_alloc转换)

退出条件:

  • Tail flit发送完成 → 转到idle
  • 如果VC被释放

代码:

void IQRouter::_SwitchUpdate() {
  Flit *f = _buf[input]->RemoveFlit(vc);
  if (f->tail) {
    _buf[input]->SetState(vc, idle);  // 包完成,转到idle
  }
  // 否则保持在active状态
}

完整状态转换流程:

1. idle
   ↓ (head flit到达)
2. routing
   ↓ (路由计算完成)
3. vc_alloc
   ↓ (VC分配成功)
4. active
   ↓ (tail flit发送)
1. idle (循环)

状态转换条件总结:

转换 条件 触发事件
idle → routing Head flit到达 AddFlit(head_flit)
routing → vc_alloc 路由计算完成 Route()完成
vc_alloc → active VC分配成功 VCAllocUpdate()成功
vc_alloc → vc_alloc VC分配失败 VCAllocUpdate()失败(重试)
active → idle Tail flit发送 RemoveFlit(tail_flit)
active → active Body flit传输 RemoveFlit(body_flit)

特殊情况:

1. 路由延迟

// 如果routing_delay > 0
_route_vcs.push_back(make_pair(
  GetSimTime() + _routing_delay,  // 延迟处理
  make_pair(input, vc)
));
// VC保持在routing状态,直到延迟到期

2. VC分配延迟

// 如果vc_alloc_delay > 0
_vc_alloc_vcs.push_back(make_pair(
  GetSimTime() + _vc_alloc_delay,  // 延迟处理
  make_pair(make_pair(input, vc), ...)
));
// VC保持在vc_alloc状态,直到延迟到期

3. 无可用VC

// 如果所有输出VC都不可用
if (!_next_buf[out_port]->IsAvailableFor(out_vc)) {
  // VC保持在vc_alloc状态,等待credit
  // 下一周期重试
}

状态查询:

// 检查VC状态
VC::eVCState state = _buf[input]->GetState(vc);

switch (state) {
  case idle:
    // VC空闲,可以接收新Flit
    break;
  case routing:
    // 等待路由计算
    break;
  case vc_alloc:
    // 等待VC分配
    break;
  case active:
    // 正在传输
    break;
}

状态与流水线:

周期1: idle → routing (head flit到达)
周期2: routing → vc_alloc (路由计算)
周期3: vc_alloc → active (VC分配)
周期4-N: active (传输body flit)
周期N+1: active → idle (tail flit发送)

总结:

  • idle:初始和结束状态
  • routing:等待路由计算
  • vc_alloc:等待VC分配(可能重试)
  • active:正在传输(可能多个周期)

状态转换是路由器流水线的核心,确保Flit按正确顺序处理。


拓扑结构(Q2.16-Q2.18)

Q2.16 Mesh和Torus拓扑的区别是什么?Torus的环连接如何影响路由算法设计?

答案:

Mesh拓扑:

结构: k×k的网格,每个路由器连接到相邻的上下左右路由器。

特点:

  • 无环连接(边界路由器只有3个邻居)
  • 直径 = 2(k-1)(最长路径)
  • 边节点度 = 2或3
  • 内部节点度 = 4

示例(4×4 Mesh):

0---1---2---3
|   |   |   |
4---5---6---7
|   |   |   |
8---9---10--11
|   |   |   |
12--13--14--15

Torus拓扑:

结构: Mesh + 环连接(首尾相连)。

特点:

  • 有环连接(所有路由器都有4个邻居)
  • 直径 = k(如果k为偶数)或 k-1(如果k为奇数)
  • 所有节点度 = 4
  • 对称性更好

示例(4×4 Torus):

0---1---2---3
|   |   |   |  ← 环连接
4---5---6---7
|   |   |   |
8---9---10--11
|   |   |   |
12--13--14--15
↑           ↑
└───────────┘ 环连接

关键区别:

特性 Mesh Torus
环连接
节点度 2-4(不等) 4(相等)
直径 2(k-1) k或k-1
对称性
路由复杂度 简单 复杂(需处理环)

Torus环连接的影响:

1. 路由选择问题

在Torus中,同一维度可能有两条路径:

节点0 → 节点7(X维度)

路径1(正向):0 → 1 → 2 → ... → 7(7跳)
路径2(环):0 → 15 → 14 → ... → 8 → 7(9跳)

解决方案: 选择最短路径

void dor_next_torus(int cur, int dest, int in_port, int *out_port, ...) {
  int cur_x = cur % gK;
  int dest_x = dest % gK;
  
  int dist_forward = (dest_x - cur_x + gK) % gK;
  int dist_backward = (cur_x - dest_x + gK) % gK;
  
  if (dist_forward <= dist_backward) {
    *out_port = PORT_X_PLUS;  // 正向
  } else {
    *out_port = PORT_X_MINUS; // 反向(通过环)
  }
}

2. 死锁问题

Torus的环连接可能形成循环依赖:

节点0 → 节点7(通过环)
节点7 → 节点0(通过环)
可能形成循环

解决方案:VC分离

// 使用两个VC集合
if (跨越数据线) {
  vcBegin += available_vcs;  // 使用VC 4-7
} else {
  vcEnd -= available_vcs;     // 使用VC 0-3
}

数据线(Dateline):

在Torus中,数据线是环的连接点:

  • X维度:节点k-1到节点0
  • Y维度:节点k(k-1)到节点0

跨越数据线的路径使用不同的VC集合,避免死锁。

3. 路由算法复杂度

Mesh路由:

// 简单:只需判断方向
if (cur_x < dest_x) out_port = EAST;
else if (cur_x > dest_x) out_port = WEST;

Torus路由:

// 复杂:需要选择最短路径(考虑环)
int dist_forward = (dest_x - cur_x + gK) % gK;
int dist_backward = (cur_x - dest_x + gK) % gK;
if (dist_forward <= dist_backward) {
  // 正向路径
} else {
  // 反向路径(通过环)
}

BookSim2.0中的实现:

// KNCube支持Mesh和Torus
KNCube::KNCube(const Configuration &config, const string &name, bool mesh)
  : Network(config, name) {
  _mesh = mesh;  // true=Mesh, false=Torus
}

void KNCube::_BuildNet(const Configuration &config) {
  for (int node = 0; node < _size; ++node) {
    // 连接相邻节点
    ConnectNeighbors(node);
    
    // 如果是Torus,添加环连接
    if (!_mesh) {
      ConnectRing(node);  // 连接环
    }
  }
}

性能对比:

指标 Mesh Torus
平均跳数 较高 较低
吞吐量 中等 较高
实现复杂度 中等
死锁避免 简单 需要VC分离

选择建议:

  • Mesh:简单实现、低开销、适合小规模
  • Torus:更好性能、对称性、适合大规模

Q2.17 FatTree拓扑的特点是什么?为什么适合数据中心网络?它的带宽分布有什么特点?

答案:

FatTree拓扑结构:

FatTree是一种层次化树形拓扑,特点是越靠近根节点,带宽越大(”胖”树)。

结构:

  • 叶子节点:计算节点(服务器)
  • 中间节点:交换机
  • 根节点:核心交换机

k-ary n-tree示例(k=4, n=2):

        Core (4个)
       /  |  |  \
      /   |  |   \
   Agg1  Agg2 Agg3 Agg4 (16个聚合)
   /|\   /|\  /|\  /|\
  L L L L L L L L L L L L L L L L (16个叶子=服务器)

特点:

  1. 带宽递增
    • 叶子层:1×带宽
    • 聚合层:k×带宽(k倍)
    • 核心层:k²×带宽(k²倍)
  2. 多路径
    • 任意两个叶子节点间有k条路径
    • 提供冗余和负载均衡
  3. 对称性
    • 所有叶子节点到根的距离相等
    • 所有叶子节点对间的路径长度相等

BookSim2.0中的实现:

void FatTree::_ComputeSize(const Configuration &config) {
  _k = config.GetInt("k");      // 每个节点的子节点数
  _n = config.GetInt("n");      // 树的高度(层数)
  
  _nodes = powi(_k, _n);        // 叶子节点数(服务器数)
  _size = _n * powi(_k, _n-1);  // 路由器总数
  _channels = (2*_k * powi(_k, _n-1)) * (_n-1);  // 通道数
}

为什么适合数据中心:

  1. 带宽保证
    • 核心层带宽大,可以支持全对全通信
    • 避免瓶颈
  2. 可扩展性
    • 可以按需增加层数或每层节点数
    • 支持大规模部署
  3. 容错性
    • 多路径提供冗余
    • 单点故障不影响整体
  4. 负载均衡
    • 多路径可以分散流量
    • 避免热点

带宽分布特点:

带宽比:

核心层 : 聚合层 : 叶子层 = k² : k : 1

示例(k=4):

  • 核心层带宽 = 16×(叶子层带宽)
  • 聚合层带宽 = 4×(叶子层带宽)
  • 叶子层带宽 = 1×(基础带宽)

带宽利用率:

  • 全对全通信:所有路径都被利用,带宽利用率高
  • 局部通信:只使用部分路径,带宽利用率低
  • 热点通信:某些路径过载,其他路径空闲

路由算法:

FatTree通常使用最近公共祖先(NCA)路由

void fattree_nca(const Router *r, const Flit *f, int in_channel,
                 OutputSet *outputs, bool inject) {
  int height = FatTree::HeightFromID(r->GetID());
  int pos = FatTree::PosFromID(r->GetID());
  
  int dest = f->dest;
  
  // 计算目标在树中的位置
  int dest_height = ...;
  int dest_pos = ...;
  
  if (pos == dest_pos / gK) {
    // 在同一子树,向下路由
    out_port = dest % gK;
  } else {
    // 不在同一子树,向上路由
    out_port = gK;  // 向上到父节点
  }
}

性能特征:

指标
直径 2×n(n为层数)
平均跳数 约n
最大跳数 2×n
路径数 k条(任意叶子对)

与Mesh/Torus对比:

特性 FatTree Mesh/Torus
带宽分布 不均匀(核心大) 均匀
路径数 多(k条) 少(1-2条)
适用场景 数据中心 多核处理器
扩展性 好(层次化) 中等(2D/3D)

总结:

FatTree通过带宽递增设计,适合数据中心的全对全通信模式,提供高带宽和容错性。


Q2.18 Dragonfly拓扑的层次结构是什么?如何实现全局自适应路由?局部网络和全局网络如何协调?

答案:

Dragonfly拓扑结构:

Dragonfly是一种层次化拓扑,包含三层:

  1. 组内网络(Local Network):组内路由器互连
  2. 组间网络(Global Network):组间路由器互连
  3. 节点连接:计算节点连接到路由器

参数:

  • gA:每组路由器数
  • gP:每个路由器的节点数(集中度)
  • gG:组数

结构示例:

组0:                   组1:                   组2:
R0-R1-R2 (局部网络)    R3-R4-R5 (局部网络)    R6-R7-R8 (局部网络)
 |  |  |                |  |  |                |  |  |
N0 N1 N2              N3 N4 N5              N6 N7 N8

全局网络(组间连接):
组0 ↔ 组1 ↔ 组2

BookSim2.0中的实现:

void DragonFlyNew::_BuildNet(const Configuration &config) {
  // 创建组
  for (int g = 0; g < gG; ++g) {
    // 创建组内路由器
    for (int a = 0; a < gA; ++a) {
      int router_id = g * gA + a;
      _routers[router_id] = Router::NewRouter(...);
    }
    
    // 连接组内网络(局部网络)
    ConnectLocalNetwork(g);
    
    // 连接组间网络(全局网络)
    ConnectGlobalNetwork(g);
  }
}

全局自适应路由:

Dragonfly使用UGAL(Universal Globally Adaptive Load-balanced)路由

路由阶段:

  1. 阶段0(ph=0):路由到中间节点(非最小路径)
  2. 阶段1(ph=1):从中间节点路由到目标(最小路径)
  3. 阶段2(ph=2):同组内路由(最小路径)

BookSim2.0中的实现:

void ugal_dragonflynew(const Router *r, const Flit *f, int in_channel,
                      OutputSet *outputs, bool inject) {
  int rID = r->GetID();
  int grp_ID = rID / _grp_num_routers;
  int dest_grp_ID = dest / _grp_num_nodes;
  
  // 在源路由器做决策
  if (in_channel < gP) {  // 注入通道
    if (dest_grp_ID == grp_ID) {
      // 同组内,只用最小路由
      f->ph = 2;
    } else {
      // 跨组通信
      // 选择随机中间节点
      f->intm = RandomInt(_network_size - 1);
      intm_grp_ID = f->intm / _grp_num_nodes;
      
      if (grp_ID == intm_grp_ID) {
        // 中间节点在同组,直接最小路由
        f->ph = 1;
      } else {
        // 比较最小路径和非最小路径的负载
        min_queue_size = r->GetUsedCredit(min_router_output);
        nonmin_queue_size = r->GetUsedCredit(nonmin_router_output);
        
        // UGAL决策
        if ((1 * min_queue_size) <= (2 * nonmin_queue_size) + threshold) {
          f->ph = 1;  // 最小路径
        } else {
          f->ph = 0;  // 非最小路径
        }
      }
    }
  }
  
  // 路由阶段
  if (f->ph == 0) {
    // 路由到中间节点
    out_port = dragonfly_port(rID, f->src, f->intm);
  } else if (f->ph == 1) {
    // 从中间节点路由到目标
    out_port = dragonfly_port(rID, f->src, f->dest);
  } else if (f->ph == 2) {
    // 同组内路由
    out_port = dragonfly_port(rID, f->src, f->dest);
  }
}

局部网络和全局网络协调:

1. 端口分配

// 端口编号
端口0gP-1: 节点连接(注入/弹出)
端口gPgP+gA-2: 组内连接(局部网络)
端口gP+gA-1gP+gA+gG-2: 组间连接(全局网络)

2. 路由决策

  • 同组内:只使用局部网络(ph=2)
  • 跨组:使用全局网络(ph=0或1)

3. VC分配

// 不同阶段使用不同VC
out_vc = f->ph;  // VC 0, 1, 2对应阶段0, 1, 2

4. 数据线处理

// 全局网络有数据线(类似Torus)
if (f->ph == 1 && out_port >= gP + (gA-1)) {
  f->ph = 2;  // 跨越数据线,切换到阶段2
}

路由示例:

源节点S(组0) → 目标节点D(组2)

阶段0(ph=0):S → 中间节点I(组1)
  - 使用全局网络:组0 → 组1
  
阶段1(ph=1):I → D
  - 使用全局网络:组1 → 组2
  
如果中间节点在同组:
阶段1(ph=1):S → D
  - 直接使用全局网络:组0 → 组2

优势:

  1. 负载均衡
    • 通过中间节点分散流量
    • 避免全局网络热点
  2. 容错性
    • 多路径提供冗余
    • 单条全局链路故障不影响通信
  3. 可扩展性
    • 可以增加组数或组内路由器数
    • 支持超大规模系统

挑战:

  1. 路由复杂度
    • 需要全局负载信息
    • 中间节点选择机制
  2. VC需求
    • 需要至少3个VC(对应3个阶段)
    • 增加资源开销

总结:

Dragonfly通过层次化结构全局自适应路由,实现大规模网络的高性能通信,局部网络和全局网络通过阶段机制VC分离协调工作。


性能分析(Q2.19-Q2.20)

Q2.19 网络延迟的组成部分有哪些?如何区分平台延迟(platform latency)和网络延迟(network latency)?

答案:

网络延迟组成:

1. 路由延迟(Routing Latency)

  • 计算输出端口的时间
  • 典型值:0-2周期

2. VC分配延迟(VC Allocation Latency)

  • 分配输出VC的时间
  • 典型值:1周期

3. 开关分配延迟(Switch Allocation Latency)

  • 分配交叉开关的时间
  • 典型值:1周期

4. 传输延迟(Transmission Latency)

  • 通过交叉开关传输的时间
  • 典型值:1周期

5. 链路延迟(Link Latency)

  • 在链路上传输的时间
  • 典型值:1周期(取决于链路长度)

6. 队列延迟(Queuing Latency)

  • 在缓冲区中等待的时间
  • 可变,取决于拥塞程度

总延迟公式:

总延迟 = 路由延迟 + VC分配延迟 + 开关分配延迟 
       + 传输延迟 + 链路延迟 × 跳数 + 队列延迟

BookSim2.0中的延迟统计:

// TrafficManager统计延迟
void TrafficManager::_RetireFlit(Flit *f) {
  // 平台延迟:从生成到接收
  int plat = GetSimTime() - f->ctime;
  _plat_stats[src][dest]->AddSample(plat);
  
  // 网络延迟:从注入到弹出
  int nlat = f->atime - f->itime;
  _nlat_stats[src][dest]->AddSample(nlat);
}

平台延迟 vs 网络延迟:

平台延迟(Platform Latency):

定义: 从数据包生成到最终接收的总时间。

组成:

平台延迟 = 生成延迟 + 网络延迟 + 接收延迟

时间线:

ctime (生成时间)
  ↓
itime (注入时间) ← 生成延迟
  ↓
网络传输
  ↓
atime (到达时间)
  ↓
接收处理 ← 接收延迟

网络延迟(Network Latency):

定义: 数据包在网络中传输的时间(从注入到弹出)。

组成:

网络延迟 = 路由器延迟 × 跳数 + 链路延迟 × 跳数 + 队列延迟

时间线:

itime (注入时间)
  ↓
路由器1处理
  ↓
链路1传输
  ↓
路由器2处理
  ↓
...
  ↓
atime (到达时间)

区别:

特性 平台延迟 网络延迟
起始点 包生成(ctime) 包注入(itime)
结束点 包接收 包弹出(atime)
包含 生成+网络+接收 仅网络传输
用途 应用层性能 网络性能

延迟分解示例:

包生成时间:ctime = 100
包注入时间:itime = 105  (生成延迟 = 5周期)
包到达时间:atime = 120  (网络延迟 = 15周期)
包接收时间:receive = 125 (接收延迟 = 5周期)

平台延迟 = 125 - 100 = 25周期
网络延迟 = 120 - 105 = 15周期
生成延迟 = 105 - 100 = 5周期
接收延迟 = 125 - 120 = 5周期

BookSim2.0中的实现:

// Flit时间戳
class Flit {
  int ctime;  // 创建时间(生成时间)
  int itime;  // 注入时间(进入网络)
  int atime;  // 到达时间(离开网络)
};

// 生成时
f->ctime = GetSimTime();

// 注入时
f->itime = GetSimTime();

// 到达时
f->atime = GetSimTime();

// 统计
plat_latency = GetSimTime() - f->ctime;  // 平台延迟
nlat_latency = f->atime - f->itime;      // 网络延迟

延迟分析:

零负载延迟(Zero-Load Latency):

  • 无队列延迟
  • 只包含路由器处理延迟和链路延迟
  • 用于评估路由算法效率

饱和延迟(Saturation Latency):

  • 包含大量队列延迟
  • 网络拥塞时的延迟
  • 用于评估网络容量

延迟分布:

延迟
  |
  |     /|
  |    / |
  |   /  |
  |  /   |___________  (饱和点)
  | /    |
  |/     |
  +------+-----------+ 吞吐量
  0    饱和吞吐量   1.0

零负载延迟 = y轴截距
饱和延迟 = 饱和点对应的延迟

优化方向:

  1. 降低零负载延迟
    • 减少路由器流水线深度
    • 使用前瞻路由
    • 优化路由算法
  2. 降低队列延迟
    • 增加缓冲区大小
    • 使用自适应路由
    • 负载均衡
  3. 降低平台延迟
    • 优化生成和接收处理
    • 减少接口延迟

总结:

  • 平台延迟:端到端总延迟,包含所有阶段
  • 网络延迟:仅网络传输延迟,用于评估网络性能
  • 区分目的:分别优化网络和应用层性能

Q2.20 什么是网络拥塞?如何通过统计信息(如队列长度、跳数)来诊断网络拥塞?拥塞控制策略有哪些?

答案:

网络拥塞定义:

网络拥塞是指网络中的流量超过网络容量,导致:

  • 延迟增加
  • 吞吐量下降
  • 缓冲区溢出(如果无流控)

拥塞特征:

  1. 延迟急剧增加
  2. 吞吐量不再提升(甚至下降)
  3. 队列长度增加
  4. 丢包(如果无流控)

诊断方法:

1. 队列长度统计

// 获取队列长度
int queue_length = router->GetBufferOccupancy(input);

// 统计平均队列长度
_stats->AddSample(queue_length);

// 诊断
if (avg_queue_length > threshold) {
  // 拥塞
}

2. 跳数统计

// Flit记录跳数
f->hops++;

// 统计跳数
_hop_stats[src][dest]->AddSample(f->hops);

// 诊断
if (avg_hops > expected_hops * 1.5) {
  // 可能拥塞(绕远路)
}

3. 延迟统计

// 统计延迟
_nlat_stats[src][dest]->AddSample(latency);

// 诊断
if (latency > zero_load_latency * 2) {
  // 拥塞(延迟显著增加)
}

4. Credit使用率

// 统计Credit使用率
int used_credit = router->GetUsedCredit(output);
int total_credit = router->MaxCredits(output);
double credit_usage = (double)used_credit / total_credit;

// 诊断
if (credit_usage > 0.8) {
  // 缓冲区接近满,可能拥塞
}

5. 路径利用率

// 统计每条路径的使用次数
_path_usage[path_id]++;

// 诊断热点路径
if (_path_usage[path_id] > threshold) {
  // 路径成为热点,可能拥塞
}

BookSim2.0中的统计:

// 缓冲区占用统计
void IQRouter::Display() {
  for (int i = 0; i < _inputs; ++i) {
    int occupancy = _buf[i]->GetOccupancy();
    _buffer_monitor->log(occupancy);
  }
}

// 跳数统计
void TrafficManager::_RetireFlit(Flit *f) {
  _hop_stats[src][dest]->AddSample(f->hops);
}

拥塞控制策略:

1. 流量控制(Flow Control)

Credit-based流控:

// 无Credit时阻塞发送
if (!_next_buf[out]->IsAvailableFor(vc)) {
  // 阻塞,等待Credit
  return;
}

作用: 防止缓冲区溢出,实现背压。

2. 路由策略

自适应路由:

// 根据负载选择路径
if (queue_xy < queue_yx) {
  choose_xy_path();
} else {
  choose_yx_path();
}

作用: 避开拥塞路径,分散流量。

3. 注入限制(Injection Throttling)

// 限制注入率
if (network_congested) {
  injection_rate *= 0.9;  // 降低注入率
}

作用: 减少进入网络的流量。

4. 优先级机制

// 高优先级流量优先
if (flit->pri > threshold) {
  // 优先处理
}

作用: 确保关键流量不受拥塞影响。

5. 多路径路由

// 使用多条路径分散流量
if (path1_congested) {
  use_path2();
}

作用: 利用网络冗余,分散流量。

6. 拥塞通知(Congestion Notification)

// 向上游发送拥塞信号
if (queue_length > threshold) {
  send_congestion_signal();
}

作用: 通知上游减少发送。

BookSim2.0中的实现:

// 注入控制
void TrafficManager::_Inject() {
  // 检查网络状态
  if (_total_in_flight_flits > threshold) {
    // 降低注入率
    return;
  }
  
  // 正常注入
  _IssuePacket(source, class);
}

// 自适应路由
void adaptive_xy_yx_mesh(...) {
  // 根据队列长度选择路径
  int credit_xy = r->GetUsedCredit(out_port_xy);
  int credit_yx = r->GetUsedCredit(out_port_yx);
  
  if (credit_xy < credit_yx) {
    choose_xy();
  } else {
    choose_yx();
  }
}

拥塞检测算法:

1. 阈值检测

if (queue_length > threshold) {
  congestion_detected = true;
}

2. 趋势检测

if (queue_length_increasing && queue_length > threshold) {
  congestion_detected = true;
}

3. 延迟检测

if (latency > zero_load_latency * factor) {
  congestion_detected = true;
}

拥塞缓解策略:

短期:

  • 降低注入率
  • 使用自适应路由
  • 优先级调度

长期:

  • 增加缓冲区
  • 增加带宽
  • 优化路由算法

总结:

  • 诊断:通过队列长度、跳数、延迟等统计信息
  • 控制:流量控制、路由策略、注入限制等
  • 目标:保持网络在饱和点以下运行,避免拥塞