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:
- 组成关系
- Packet(数据包):由多个Flit组成
- Head Flit:包含路由信息(目标地址、路由路径)
- Body Flit:包含数据载荷
- Tail Flit:标记包结束,释放资源
- 大小对比
- Flit:通常较小(如32-128位),适合路由器流水线处理
- Packet:可变大小(1-N个Flit),取决于应用需求
- 处理方式
- Flit级别:路由器在每个周期处理一个Flit
- Packet级别:应用层和TrafficManager管理完整Packet
为什么使用Flit:
- 流水线友好
- 路由器可以每个周期处理一个Flit,实现流水线
- 如果以Packet为单位,需要等待整个Packet到达才能处理
- 资源利用率
- 不同Packet的Flit可以交错传输(通过VC)
- 提高链路利用率,避免长包阻塞短包
- 流控粒度
- 基于Flit的流控更精细,可以及时响应拥塞
- Credit机制基于Flit,而不是整个Packet
- 延迟优化
- 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的作用:
- 提高链路利用率
- 多个VC可以共享同一物理链路
- 当一个VC阻塞时,其他VC可以继续传输
- 避免”head-of-line blocking”(队首阻塞)
- 死锁避免
- 不同VC可以使用不同的路由规则
- 通过VC分配策略避免死锁
- 例如:Torus中使用两个VC集合,分别对应不同的环分区
- 优先级支持
- 不同VC可以设置不同优先级
- 高优先级流量可以使用专用VC
- 流量分离
- 请求和响应可以使用不同VC
- 不同流量类型(read/write)可以使用不同VC
为什么需要多个VC:
- 死锁避免需求
- 某些路由算法(如自适应路由)需要多个VC避免死锁
- Turn Model路由需要至少2个VC
- 性能提升
- 更多VC提供更多并行性
- 减少阻塞概率,提高吞吐量
- 公平性
- 多个VC可以实现更好的公平性
- 避免单个流独占资源
VC数量对性能的影响:
- 延迟影响
- VC数少(1-2):可能阻塞,延迟高
- VC数适中(4-8):延迟最低,资源利用最优
- VC数过多(>16):延迟改善有限,但资源开销大
- 吞吐量影响
- VC数少:吞吐量受限,容易饱和
- VC数增加:吞吐量提升,但收益递减
- VC数过多:吞吐量不再提升,浪费资源
- 资源开销
- 每个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信号控制数据传输。
工作流程:
- 初始化
// 每个输出VC有vc_buf_size个credit BufferState::BufferState() { _size = vc_buf_size; // 如8 // 初始时所有credit可用 } - 发送Flit
// 发送Flit时消耗credit void BufferState::SendingFlit(Flit *f) { _occupancy++; // 占用增加 // credit = _size - _occupancy } - 检查可用性
// 检查是否有可用credit bool BufferState::IsAvailableFor(int vc) { return AvailableFor(vc) > 0; // credit > 0 } - 接收Credit
// 下游发送Credit,上游增加credit void BufferState::ProcessCredit(Credit *c) { _occupancy--; // 释放空间 // credit增加 } - 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优势:
- 低延迟
- Credit立即返回,无需等待ACK
- 适合低延迟要求的NoC
- 简单实现
- 只需维护credit计数
- 无需复杂的ACK/NACK状态机
- 确定性
- Credit数量 = 缓冲区大小
- 不会出现缓冲区溢出
- 流水线友好
- Credit和Flit可以流水线传输
- 不阻塞数据路径
Credit-based劣势:
- 无错误恢复
- 如果Flit丢失,无法检测和重传
- 需要上层协议保证可靠性
- 固定缓冲区
- 必须预先分配缓冲区
- 无法动态调整
- Credit开销
- 需要反向Credit通道
- 增加硬件开销
ACK/NACK优势:
- 错误恢复
- 可以检测丢失并重传
- 适合不可靠链路
- 动态缓冲区
- 可以根据ACK动态调整
- 更灵活的流控
ACK/NACK劣势:
- 高延迟
- 需要等待ACK返回
- 增加端到端延迟
- 复杂实现
- 需要维护发送/接收状态
- 需要超时和重传机制
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的缓冲区
↓
形成循环等待 → 死锁
产生原因:
- 循环依赖
- 多个路由器形成循环等待
- 每个路由器都等待下一个路由器的资源
- 资源不足
- VC数量不足
- 缓冲区满
- 路由算法问题
- 某些路由算法可能产生循环路径
- 缺乏死锁避免机制
BookSim2.0中的死锁避免:
- 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分配的时序:
- 维度顺序路由:先完成X维度,再完成Y维度
- 路径不重叠:包1和包2的路径在X维度上不重叠
- 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
}
总结:
- 数据线识别:判断路径是否跨越数据线(节点k-1到节点0)
- VC分离:
- 不跨越数据线 → VC 0-3
- 跨越数据线 → VC 4-7
- 打破循环:即使路径形成循环,不同VC集合也避免了资源竞争
- 保证无死锁:VC分离 + 确定性路由 = 无死锁保证
为什么有效:
- 隔离:跨越数据线的包和不跨越的包使用不同VC,不会相互阻塞
- 确定性:路由算法是确定性的,相同路径总是使用相同VC集合
- 无循环依赖:不同VC集合之间没有资源依赖关系
- Turn Model
- 限制某些转向,打破循环
- 例如:West-First禁止某些转向
- 多物理网络
- 请求和响应使用不同子网
- 避免请求-响应死锁
活锁(Livelock):
定义: 系统不断执行操作,但无法取得进展。
NoC中的活锁示例:
数据包在网络上不断路由,但永远无法到达目标
↓
可能原因:
- 路由算法选择错误路径
- 自适应路由在多个路径间振荡
- 负载均衡导致包在热点间移动
产生原因:
- 路由振荡
- 自适应路由在多个路径间切换:例如
adaptive_xy_yx_mesh会在XY和YX两条最短路径之间,根据本地GetUsedCredit()/GetBufferOccupancy()看到的队列长度来选择“看起来更空”的那一条,如果不同路由器在不同时间点看到的局部负载不一致,就可能出现有的包选XY,有的包选YX,在两条路径之间来回“倒腾”。 - 负载信息过时导致错误决策:这些负载信息都是当前路由器本地、某一时刻的快照(例如下游端口的已用credit),当真正的流量已经沿着先前决策涌向某条路径时,其他路由器可能还在用“旧的队列长度”做决策,于是新包又被导向刚刚开始拥塞的路径,形成一种“谁刚变空就被一波新流量打满”的振荡模式;在有非最小路由(如UGAL)的情况下,如果在“最小/非最小路径”和不同中间节点之间频繁改选,理论上可能演化成活锁(包总在网络里绕圈但很久到不了目的地),因此代码里通常会限制自适应决策只在注入或特定阶段做一次,并叠加阈值(如UGAL中的
adaptive_threshold)来减少这种抖动。
- 自适应路由在多个路径间切换:例如
- 非最小路由
- Valiant路由可能产生长路径
- 包可能绕远路
- 负载均衡问题
- 过度追求负载均衡
- 导致包在多个路径间移动
避免方法:
- 限制非最小路径
- 限制Valiant路由的中间节点选择
- 设置最大跳数
- 稳定路由决策
- 避免频繁切换路径
- 使用历史信息
- 优先级机制
- 接近目标的包优先
- 避免包在热点间移动
饥饿(Starvation):
定义: 某些流长期无法获得服务,而其他流正常服务。
NoC中的饥饿示例:
低优先级流被高优先级流持续阻塞
↓
低优先级流的包永远无法传输
↓
导致该流饥饿
产生原因:
- 不公平分配
- 某些流获得过多资源
- 其他流资源不足
- 优先级机制
- 高优先级流总是优先
- 低优先级流被持续阻塞
- 路由热点
- 某些路径成为热点
- 使用这些路径的流被阻塞
避免方法:
- 公平分配器
- 使用Round-Robin分配
- 确保所有流都有机会
- 老化机制
- 等待时间长的包提高优先级
- 避免长期饥饿
- 负载均衡
- 分散流量到多个路径
- 避免热点
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分配延迟、开关分配延迟分别代表什么?它们如何影响路由器流水线深度?
答案:
三种延迟的定义:
- 路由延迟(routing_delay)
- 定义:计算输出端口所需的时间(周期数)
- 典型值:0-1周期
- 作用:路由函数执行时间
- VC分配延迟(vc_alloc_delay)
- 定义:分配输出VC所需的时间
- 典型值:1周期
- 作用:VC分配器执行时间
- 开关分配延迟(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(传输)
示例配置:
- 快速配置(低延迟)
routing_delay = 0 vc_alloc_delay = 1 sw_alloc_delay = 1 总延迟 = 0 + 1 + 1 + 1 = 3周期 - 标准配置
routing_delay = 1 vc_alloc_delay = 1 sw_alloc_delay = 1 总延迟 = 1 + 1 + 1 + 1 = 4周期 - 慢速配置(高延迟)
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)
延迟对性能的影响:
- 延迟增加 → 流水线深度增加
- 更多周期才能完成处理
- 增加路由器延迟
- 延迟增加 → 吞吐量可能降低
- 如果延迟导致流水线停顿
- 但通常延迟是流水线的,不影响吞吐量
- 延迟增加 → 资源需求增加
- 需要更多延迟队列存储
- 需要更多状态跟踪
优化策略:
- 前瞻路由(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()等动态拥塞信息,就不能简单在上一跳“算死”。 - 并行分配
- 从抽象上看,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的典型实现。
- 从抽象上看,VC分配负责“长期资源绑定”:在路由给出的候选
- 流水线优化
- 重叠不同阶段的处理
- 提高吞吐量
实际配置建议:
- 高性能设计: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
关键点:
- 零负载延迟:曲线起点(y轴截距)
- 线性区域:延迟缓慢增加
- 饱和点:延迟开始急剧上升
- 饱和吞吐量:饱和点对应的吞吐量
性能指标关系:
- 低负载(<饱和吞吐量)
- 延迟接近零负载延迟
- 吞吐量 = 注入率
- 网络未饱和
- 饱和负载(=饱和吞吐量)
- 延迟开始增加
- 吞吐量 = 饱和吞吐量
- 网络达到最大吞吐量
- 过饱和负载(>饱和吞吐量)
- 延迟急剧增加
- 吞吐量不再提升(甚至下降)
- 网络拥塞严重
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);
应用场景:
- 性能评估
- 零负载延迟:评估路由算法效率
- 饱和吞吐量:评估网络容量
- 设计优化
- 降低零负载延迟:优化路由、减少跳数
- 提高饱和吞吐量:增加带宽、优化分配
- 负载规划
- 实际负载应 < 饱和吞吐量
- 保持延迟在可接受范围
路由算法(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);
}
无死锁保证:
- 无循环依赖
- 路由路径是最小路径(最短路径)
- 不会形成循环
- 确定性路由
- 相同源-目标对总是走相同路径
- 避免路由振荡
- 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的连接
局限性:
- 负载不均衡
- 所有相同X坐标的通信都经过相同路径
- 容易产生热点(hotspot)
- 无自适应能力
- 无法根据负载选择路径
- 无法避开拥塞区域
- 路径固定
- 即使有更空闲的路径,也必须走固定路径
- 无法利用网络冗余
- 对故障敏感
- 如果路径上节点故障,无法绕行
- 需要上层协议处理
性能特征:
- 延迟:低(最小路径)
- 吞吐量:中等(受热点限制)
- 实现复杂度:低(简单算法)
- 适用场景:低负载、简单应用
改进方向:
- 自适应维度顺序
- 根据负载选择维度顺序
- 部分自适应能力
- 多路径维度顺序
- 在维度内允许多条路径
- 提高负载均衡
- 混合路由
- 低负载用维度顺序
- 高负载切换到自适应
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
}
}
负载均衡实现:
- 本地负载信息
// 获取输出端口的队列长度 int queue_length = r->GetUsedCredit(output_port); // 选择队列最短的路径 if (queue_xy < queue_yx) { choose_xy_path(); } else { choose_yx_path(); } - 全局负载信息(高级)
// 收集路径上所有路由器的负载 int total_load_xy = 0; for (each router on xy_path) { total_load_xy += router->GetLoad(); } // 选择总负载最小的路径 - 阈值机制
// 只有当负载差异超过阈值时才切换路径 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是一种全局自适应路由算法,在最小路径和非最小路径之间进行选择,目标是全局负载均衡。
关键特点:
- 两阶段路由:先到中间节点(非最小),再到目标(最小)
- 全局视角:考虑整个路径的负载,而不仅仅是下一跳
- 负载感知:根据队列长度选择路径
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,选择非最小路径
优势:
- 全局负载均衡
- 考虑整个路径的负载
- 而不仅仅是下一跳
- 避免热点
- 当最小路径拥塞时,选择非最小路径
- 分散流量
- 性能提升
- 在高负载下显著提升吞吐量
- 降低延迟
局限性:
- 实现复杂
- 需要全局负载信息
- 需要中间节点选择机制
- 延迟增加
- 非最小路径增加跳数
- 可能增加延迟
- 需要更多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通过禁止某些转向,确保:
- 无循环依赖:禁止的转向不会形成循环
- 可达性:所有节点仍然可达(通过允许的转向)
- 最小路径:在限制下仍能找到路径(可能非最小)
优势:
- 简单实现
- 不需要VC分离(理论上)
- 保证无死锁
劣势:
- 可能产生非最小路径
- 某些路径可能绕远
- 负载不均衡
Q2.11 Valiant路由和ROMM路由都是非最小路由,它们的设计动机是什么?在什么场景下使用非最小路由?
答案:
非最小路由定义:
非最小路由允许数据包走非最短路径,可能增加跳数,但可以避开拥塞和热点。
Valiant路由:
核心思想: 两阶段路由
- 阶段1:从源节点路由到随机选择的中间节点(可能非最小)
- 阶段2:从中间节点路由到目标节点(最小路径)
路由示例:
源节点S → 目标节点D
阶段1:S → I(随机中间节点,可能绕远)
阶段2:I → D(最小路径)
总路径:S → ... → I → ... → D
设计动机:
- 负载均衡
- 随机中间节点分散流量
- 避免所有流量走相同路径
- 避免热点
- 不直接走最小路径
- 通过中间节点绕行
- 简单实现
- 只需随机选择中间节点
- 然后最小路由到中间和目标
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随机选择其中一条
设计动机:
- 负载均衡
- 在多个最小路径间分散流量
- 避免所有流量走同一条路径
- 保持最小路径
- 不增加跳数
- 延迟不会增加
- 简单实现
- 只需在多个最小路径中选择
- 不需要全局负载信息
适用场景:
Valiant路由适用于:
- 高负载场景
- 最小路径可能拥塞
- 需要分散流量
- 有冗余路径的网络
- Dragonfly、FatTree等 Dragonfly拓扑:https://www.modb.pro/db/650884
- 有多个可选路径
Dragonfly拓扑详解:
Dragonfly是一种分层网络拓扑,专为大规模高性能计算系统设计,通过组内(intra-group)和组间(inter-group)两级结构实现高可扩展性和丰富的路径冗余。
拓扑结构:
- 三层架构:
- 组内网络(Intra-group):每个组内的路由器通过全连接或近似全连接互联
- 组间网络(Inter-group):不同组之间通过部分连接互联
- 终端节点(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 - 1p个终端端口(连接处理器)p个组间端口(连接其他组)2p - 1个组内端口(连接同组其他路由器)
拓扑特点:
- 高可扩展性:节点数随p²增长,适合超大规模系统
- 丰富的路径冗余:组内全连接提供多条路径,组间也有多条可选路径
- 低直径:理论上最多3跳(组内1跳 + 组间1跳 + 目标组内1跳)
- 路由挑战:需要智能路由算法避免组间链路拥塞
BookSim2中的路由函数:
min_dragonflynew:最小路径路由,优先组内路径ugal_dragonflynew:UGAL(Universal Globally-Adaptive Load-balanced)自适应路由,根据负载选择路径
配置示例:
topology = dragonflynew;
k = 4; // 每个路由器的处理器端口数
n = 1; // 组内维度(必须为1)
routing_function = min; // 或 ugal
FatTree拓扑详解:
FatTree(胖树)是一种多级树形拓扑,通过”上行带宽等于下行带宽”的设计(即”胖”的中间层)消除树形网络的根节点瓶颈,广泛应用于数据中心网络。
拓扑结构:
- 分层树形架构:
- 第0层(顶层):
k^(n-1)个路由器,每个有k个端口(只连接下层) - 第1到n-1层(中间层):每层
k^(n-1)个路由器,每个有2k个端口(k个上行,k个下行) - 第n-1层(叶子层):
k^(n-1)个路由器,每个有2k个端口(k个上行,k个连接终端节点)
- 第0层(顶层):
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:来自上方
拓扑特点:
- 无阻塞上行:从任意叶子节点到根节点的上行带宽等于所有叶子节点的总带宽
- 完全二分带宽:任意两个叶子节点间都有
k^(n-1)条不相交路径 - 确定性路由:基于最近公共祖先(NCA)的路由简单高效
- 可扩展性:节点数随k^n增长,但路由器数量也快速增长
BookSim2中的路由函数:
fattree_nca:最近公共祖先路由 + 随机上行选择- 下行:沿树向下到目标子树
- 上行:随机选择上行端口(负载均衡)
fattree_anca:最近公共祖先路由 + 自适应上行选择- 下行:沿树向下到目标子树
- 上行:根据负载信息自适应选择上行端口
路由算法原理:
- 上行阶段:从源节点向上到源和目标的最小公共祖先(LCA)
- 下行阶段:从LCA向下到目标节点
- 路径选择:在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 的超分比。
- 热点问题严重
- 某些路径成为热点
- 需要绕行
ROMM路由适用于:
- 中等负载场景
- 最小路径可用
- 只需在路径间均衡
- 延迟敏感应用
- 不能接受非最小路径的延迟增加
- 但仍需要负载均衡
- 有多个最小路径的网络
- 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缓冲区 | 交叉开关 |
为什么需要两个阶段:
- 不同的资源类型
- VC分配:分配逻辑资源(VC缓冲区)
- 开关分配:分配物理资源(交叉开关)
- 不同的竞争维度
- VC分配:解决”哪个输入VC → 哪个输出VC”
- 开关分配:解决”哪个输入端口 → 哪个输出端口”
- 依赖关系
路由计算 → VC分配 → 开关分配 → 传输- VC分配需要路由结果(知道目标端口)
- 开关分配需要VC分配结果(知道使用哪个VC)
- 独立优化
- 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(); // 执行分配
}
如果合并的问题:
- 无法独立优化
- VC分配和开关分配耦合
- 难以分别优化
- 复杂度增加
- 需要同时考虑VC和端口
- 分配算法更复杂
- 灵活性降低
- 无法替换独立的分配器
- 无法使用不同的分配策略
实际例子:
输入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已被占用)
总结:
两个独立阶段的好处:
- 清晰分离:逻辑资源和物理资源分离
- 独立优化:可以分别优化VC和端口利用率
- 灵活替换:可以独立替换分配器
- 易于理解:每个阶段职责明确
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;
}
}
}
}
}
为什么需要多轮迭代:
- 单轮可能无法完成所有匹配
- 第一轮可能只匹配部分输入-输出对
- 剩余未匹配的可以在后续轮次匹配
- 提高匹配率
- 多轮迭代可以找到更多匹配
- 接近最大匹配
- 公平性
- 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已被占用)
收敛性保证:
- 单调性
- 每轮迭代匹配数不会减少
- 已匹配的对不会解除匹配
- 有限性
- 最多N轮(N=输入/输出数)
- 每轮至少匹配一个对(如果有未匹配的)
- 最大匹配
- 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)?输入优先和输出优先的区别是什么?
答案:
可分离分配器定义:
可分离分配器将输入-输出匹配问题分解为两个独立的阶段:
- 输入阶段:每个输入选择想要的输出
- 输出阶段:每个输出选择想要的输入
关键特点:
- 两阶段独立执行
- 可以使用不同的仲裁策略
- 实现简单,延迟低
输入优先(Input-First):
执行顺序:
- 第一阶段:每个输入端口独立选择输出(输入仲裁)
- 第二阶段:每个输出端口从请求它的输入中选择(输出仲裁)
特点:
- 输入先做决策
- 输出响应输入的选择
- 偏向输入端
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):
执行顺序:
- 第一阶段:每个输出端口独立选择输入(输出仲裁)
- 第二阶段:每个输入端口从请求它的输出中选择(输入仲裁)
特点:
- 输出先做决策
- 输入响应输出的选择
- 偏向输出端
实现(伪代码):
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
输入优先过程:
- 输入0选择输出0(假设)
- 输入1选择输出1(假设)
- 输入2选择输出1(假设)
- 输出0从{输入0}中选择 → 输入0
- 输出1从{输入1, 输入2}中选择 → 输入1(假设)
输出优先过程:
- 输出0从{输入0, 输入1}中选择 → 输入0(假设)
- 输出1从{输入0, 输入1, 输入2}中选择 → 输入1(假设)
- 输入0从{输出0}中选择 → 输出0
- 输入1从{输出1}中选择 → 输出1
- 输入2无匹配
选择建议:
- 输入优先:当输入端口竞争激烈时(多个输入请求同一输出)
- 输出优先:当输出端口竞争激烈时(多个输出请求同一输入)
- 一般情况:输入优先更常用(因为通常输入数=输出数)
优势:
- 实现简单:只需两个独立的仲裁阶段
- 延迟低:两阶段即可完成
- 灵活:可以使用不同的仲裁器(Round-Robin、Priority等)
劣势:
- 匹配率可能较低:不如iSLIP等迭代算法
- 公平性:偏向某一端,可能不公平
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×带宽
- 聚合层:k×带宽(k倍)
- 核心层:k²×带宽(k²倍)
- 多路径
- 任意两个叶子节点间有k条路径
- 提供冗余和负载均衡
- 对称性
- 所有叶子节点到根的距离相等
- 所有叶子节点对间的路径长度相等
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); // 通道数
}
为什么适合数据中心:
- 带宽保证
- 核心层带宽大,可以支持全对全通信
- 避免瓶颈
- 可扩展性
- 可以按需增加层数或每层节点数
- 支持大规模部署
- 容错性
- 多路径提供冗余
- 单点故障不影响整体
- 负载均衡
- 多路径可以分散流量
- 避免热点
带宽分布特点:
带宽比:
核心层 : 聚合层 : 叶子层 = 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是一种层次化拓扑,包含三层:
- 组内网络(Local Network):组内路由器互连
- 组间网络(Global Network):组间路由器互连
- 节点连接:计算节点连接到路由器
参数:
- 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)路由:
路由阶段:
- 阶段0(ph=0):路由到中间节点(非最小路径)
- 阶段1(ph=1):从中间节点路由到目标(最小路径)
- 阶段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. 端口分配
// 端口编号
端口0到gP-1: 节点连接(注入/弹出)
端口gP到gP+gA-2: 组内连接(局部网络)
端口gP+gA-1到gP+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
优势:
- 负载均衡
- 通过中间节点分散流量
- 避免全局网络热点
- 容错性
- 多路径提供冗余
- 单条全局链路故障不影响通信
- 可扩展性
- 可以增加组数或组内路由器数
- 支持超大规模系统
挑战:
- 路由复杂度
- 需要全局负载信息
- 中间节点选择机制
- 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轴截距
饱和延迟 = 饱和点对应的延迟
优化方向:
- 降低零负载延迟
- 减少路由器流水线深度
- 使用前瞻路由
- 优化路由算法
- 降低队列延迟
- 增加缓冲区大小
- 使用自适应路由
- 负载均衡
- 降低平台延迟
- 优化生成和接收处理
- 减少接口延迟
总结:
- 平台延迟:端到端总延迟,包含所有阶段
- 网络延迟:仅网络传输延迟,用于评估网络性能
- 区分目的:分别优化网络和应用层性能
Q2.20 什么是网络拥塞?如何通过统计信息(如队列长度、跳数)来诊断网络拥塞?拥塞控制策略有哪些?
答案:
网络拥塞定义:
网络拥塞是指网络中的流量超过网络容量,导致:
- 延迟增加
- 吞吐量下降
- 缓冲区溢出(如果无流控)
拥塞特征:
- 延迟急剧增加
- 吞吐量不再提升(甚至下降)
- 队列长度增加
- 丢包(如果无流控)
诊断方法:
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;
}
拥塞缓解策略:
短期:
- 降低注入率
- 使用自适应路由
- 优先级调度
长期:
- 增加缓冲区
- 增加带宽
- 优化路由算法
总结:
- 诊断:通过队列长度、跳数、延迟等统计信息
- 控制:流量控制、路由策略、注入限制等
- 目标:保持网络在饱和点以下运行,避免拥塞