片上网络 & 互连 · 2026年8月9日
NoC:buffer tuning
推导理论饱和注入率;router buffer调优思路
Multi-GPU 网络带宽与 Buffer 配置分析
分析范围
本文整理当前 4 卡互联配置下的 multi-GPU 网络模型,重点分析:
- GPU 内部 3 个 mesh 与 DST 节点的连接关系
- req/rsp client 的注入与接收模型
- packet、flit、link 位宽和 VC 的基本配置
- 路由节点的三级仲裁结构
- 在
write64B和read64B负载下,为什么每个 req_client 的饱和注入率约为1/8 packet/cycle - 如何基于目标带宽反推 buffer depth 的下限
目标是保证无丢包,并让卡间链路带宽尽量打满 64B/cycle。
网络拓扑
每个 GPU 内部有 3 个独立 mesh。每个 mesh 都是一个 2 * 3 拓扑,节点为 A/B/C/D/E/F:
GPU 0 当前 GPU
┌──────────────────────────────────────────────┐
│ │
│ Mesh #0 Mesh #1 Mesh #2 │
│ 独立网络 独立网络 独立网络 │
│ │
│ [A]─B─[C] [A]─B─[C] [A]─B─[C] │
│ │ │ │ │ │ │ │ │ │ │
│ [D]─E─[F] [D]─E─[F] [D]─E─[F] │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ DST_in0 DST_in1 DST_in2 │
│ ┌────────┴────────┐ │
│ │ DST 节点 │ │
│ │ │ │
│ │ 内部路由/交换 │ │
│ │ │ │
│ └──┬──────┬───────┘ │
│ DST_out0 DST_out1 DST_out2 │
└──────────────┼───────┼───────┼───────────────┘
│ │ │
▼ ▼ ▼
GPU 1 GPU 2 GPU 3
的 DST_in 的 DST_in 的 DST_in
每个 mesh 的 A/C/D/F 节点会挂载:
- 2 个
req_client - 2 个
rsp_client
其中:
req_client负责注入请求、接收应答rsp_client负责接收请求、注入应答- 每个 client 都是独立接口
- 在当前 4 卡互联配置下,每个
req_client会均匀产生发往远端 3 个 GPU 的每个rsp_client的请求
mesh 内部使用 XY 路由。
基本配置
packet size 取决于接口位宽。当前网络中的请求都在 256B 范围内,因此最大 packet 长度是:
最大 packet = 256B / 64B = 4 flits
VCT 传输中需要区分仲裁单位和传输单位:
仲裁单位:packet
传输单位:flit
packet 占用链路的 cycle 数 = packet 的 flit 数
link 延迟为 1 cycle。
link 位宽需要按位置区分:
req_client/rsp_client <-> 路由节点:REQ64RSP128
2 * 3 mesh 内部:
B-E:REQ64RSP64
其他边:REQ64RSP128
网络中剩余 link:
REQ64RSP64
每级仲裁的输出能力取决于对应位宽。每个 cycle 可以按该级位宽输出一个传输粒度。
其中存在宽窄转换。例如 E 节点接收从 DST 返回的 read response 时,DST 到 E 是 64B,但 E 转向 D/F 时可能需要转换到 128B。这类宽窄转换在 input_buffer 中完成,因此 input_buffer 向 crossbar 传输时已经是对应方向的粒度。
VC 配置
网络中总共有 10 个 VC,当前主要使用:
vc = 3:
req_client 发往 DST 节点,并转发到其他 GPU 的 req/rsp
vc = 4:
DST 节点接收到的来自其他 GPU 的 req
vc = 0:
DST 节点接收到的来自其他 GPU 的 rsp
整个网络基于 credit 做 backpressure。
路由节点结构
每个路由节点内部有三级 buffer 和三级仲裁:
input_buffer[in][vc]
|
| Level 1 arbitration: per input, among VCs
v
crossbar_buffer[in][out][vc]
|
| Level 2 arbitration: per output, among input candidates
v
output_buffer[out][vc]
|
| Level 3 arbitration: per output, among VCs
v
downstream input_buffer[next_in][vc]
Level 1 从 input_buffer 到 crossbar_buffer。
每个 input 在多个 VC 之间轮询,仲裁出的 winner 从:
input_buffer[in][vc]
进入:
crossbar_buffer[in][out][vc]
Level 2 从 crossbar_buffer 到 output_buffer。
crossbar 会为每个 outport 做仲裁,在多个请求同一个 output 的 candidate 中批准一个 winner,然后进入:
output_buffer[out][vc]
Level 3 从 output_buffer 到下游 input_buffer。
每个 output 在多个 VC 之间轮询,仲裁出的 winner 通过 link 发送到下游:
downstream input_buffer[next_in][vc]
饱和注入率
为了打满卡间带宽,需要让 E -> DST 这条链路满载。
将 E -> DST 的目标吞吐设为:
E -> DST = 1 packet/cycle
在一个 mesh 内,流量汇聚到 E 后再发往 DST。反推各条链路的负载比例:
B -> E = 1/3
D -> E = 1/3
F -> E = 1/3
进一步反推到 A/C 方向:
A -> B = 1/6
C -> B = 1/6
由于 A/C/D/F 每个节点都挂 2 个 req_client,如果按路径比例单独看:
A/C 上每个 req_client 的注入率 = 1/12
D/F 上每个 req_client 的注入率 = 1/6
但如果要求每个 client 使用一致注入率,则可以得到更均衡的平均饱和点:
每个 req_client 注入率 = 1/8 packet/cycle
这个推导的前提是:
packet = 1 flit
flit = 64B
该结果和 cmodel 模拟结果拟合。在 write64B 和 read64B 负载下,饱和注入率约为:
1/8 packet/cycle
Write64B 负载下的链路需求
以 write64B 为例,若每个 req_client 的注入率为 1/8 packet/cycle,则进入 E 节点的负载为:
A -> B = 2 * 1/8 = 1/4
C -> B = 2 * 1/8 = 1/4
B -> E = 4 * 1/8 = 1/2
D -> E = 2 * 1/8 = 1/4
F -> E = 2 * 1/8 = 1/4
E -> DST = 8 * 1/8 = 1
也就是说,从平均负载看,1/8 的注入率足以打满 E -> DST:
E -> DST = 1 packet/cycle
链路热点排序为:
E -> DST: 1
B -> E: 1/2
A -> B: 1/4
C -> B: 1/4
D -> E: 1/4
F -> E: 1/4
因此 buffer 配置时,E -> DST 是最核心的满载链路,B -> E 是次级热点链路。
Buffer 下限推导
buffer depth 的下限可以拆成三部分:
depth >= credit_RTT * service_rate
+ arbitration_gap
+ packet_reservation_margin
其中 credit_RTT 是 credit 反压系统里的基础项。
假设 downstream buffer 释放了 1 个 entry,upstream 不会立刻知道,而是要等 credit_RTT 个 cycle 后才收到 credit。在这段时间里,如果 upstream 仍然可能继续发送,downstream buffer 就需要有足够空间吸收这些已经在路上或即将发出的 flit。
arbitration_gap 是仲裁导致的等待。
例如 E 节点往 DST 的 output,需要从 B/D/F 三个输入方向来的请求中选一个。如果使用 round-robin,并且三路都有 packet,那么单一路径大约每 3 个 cycle 才会被服务一次。
packet_reservation_margin 是 VCT 特有的余量。
VCT 按 packet 仲裁。一旦某个 packet 赢了,它会连续占用链路多个 flit cycle。因此下游最好不只是有 1 个 flit 空间,而是能保证接收完整个 packet。否则可能出现 packet 传到一半被卡住,或者仲裁赢了但无法真正启动传输。
这个下限是保证包正常传输的下限,在cmodel中,前两项都不用考虑
Write64B E-DST 带宽分析
基本假设
- 每个 packet 是 64B,因此在 64B link 上一个 packet 就是一个 flit。
- 每个 req_client 都是 greedy 注入:只要没有遇到反压,就持续注入。
- 每个 req_client 要注入的 packet 数量相同。
- E 节点内部使用普通 round-robin 仲裁。
- 路由器内部调度顺序是
L1 -> L2 -> L3。 - 路由器内部允许同周期 push 后 pop。
- 每级仲裁都是 credit-gated arbitration:只有当前 buffer 有 valid data 且下一级 buffer 有 credit 的 candidate,才有资格参与仲裁。
目标是解释:为什么在平均 offered load 足够的情况下,E -> DST 的实际带宽仍然可能离理论 64B/cycle 有一点距离。
基准负载
对一个 mesh 来说,A/C/D/F 各有两个 req_client。由于拓扑关系,流入 E 的 write request 负载是:
B 方向:A + C,共 4 个 req_client
D 方向:D, 共 2 个 req_client
F 方向:F, 共 2 个 req_client
如果每个 req_client 的有效注入率是 1/8 packet/cycle,那么:
B -> E = 4 * 1/8 = 1/2 packet/cycle
D -> E = 2 * 1/8 = 1/4 packet/cycle
F -> E = 2 * 1/8 = 1/4 packet/cycle
E -> DST 总需求 = 1 packet/cycle
所以从平均负载上看,流量足够打满一条 64B 的 E -> DST link。
但打满链路需要更强的周期级条件:
每个 cycle,E 都必须有一个 eligible packet 可以发往 DST。
也就是说,平均负载够只是必要条件,不是充分条件。
1. RR 消耗比例和 packet 数量比例不匹配
E 节点的 L2/L3 round-robin 仲裁是按输入方向公平,而不是按 req_client 数量公平。
三个方向背后的总 packet 数量是:
B 方向:4N packets
D 方向:2N packets
F 方向:2N packets
但如果 E 对 B/D/F 做普通 RR,那么在三路都 eligible 时,消耗比例倾向于:
B : D : F = 1 : 1 : 1
这本身并不必然导致 E -> DST 打不满。只要三个方向一直有 eligible packet,RR 每个 cycle 都可以选出一个 packet 发往 DST。
问题在于,这个消耗比例和上游源数量带来的 offered load 比例不一致:
offered load 比例 = B : D : F = 2 : 1 : 1
RR service 比例 = B : D : F = 1 : 1 : 1
因此 D/F 方向会相对更快地被消耗,B 方向则更容易积压并承受更强反压。如果带宽统计覆盖整个 run,而不是只统计中间 steady-state 窗口,那么 D/F 提前进入尾部阶段可能拉低整体平均带宽。
这一点主要解释的是公平性和尾部行为。它不是证明 E -> DST 不能打满的充分理由。
2. 三级 buffer 太浅会制造供给气泡
E 节点内部有三级 buffer:
input_buffer[in][vc]
-> crossbar_buffer[in][out][vc]
-> output_buffer[out][vc]
-> downstream input_buffer
对 E -> DST 来说,L3 能发出的条件是:
E.output[DST][vc3] valid
&& DST.input[from_E][vc3] has_credit
即使上游平均流量足够,浅 buffer 也可能让局部停顿快速传播:
E.input 有 packet,但 E.xbar 没 credit
E.xbar 有 packet,但 E.output 没 credit
E.output 在 L3 要发的时候是空的
DST.input 没 credit
因为仲裁是 credit-gated 的,下一级没有 credit 的 packet 不会参与仲裁,只会继续驻留在当前 buffer。这是正确行为,但意味着某一级短暂缺 credit,会让后一级在某个 cycle 断供。
其中 output buffer 尤其关键。它是 E -> DST 的直接供给来源。只要 E.output[DST][vc3] 在某个 cycle 为空,而 DST 又有 credit,这一拍的 64B slot 就丢了。
所以 E 节点 buffer tuning 时,第一步应该判断 idle cycle 是否主要来自:
E.output[DST][vc3] empty
如果是,则问题偏向 E 侧供给连续性,而不是下游带宽不足。
3. DST input credit 会反压 E
E -> DST 即使 E 有包,也不一定能发。L3 传输需要下游 DST input buffer 给 credit:
E.output[DST][vc3] valid
&& DST.input[from_E][vc3] has_credit
如果 DST input 没有 credit,那么 E 这一拍不能发。
这可能发生,是因为 DST 本身也是一个满负载交换点。一个 GPU 内部有三个 mesh 的 E 节点同时喂 DST:
mesh0 E -> DST_in0 = 1 packet/cycle
mesh1 E -> DST_in1 = 1 packet/cycle
mesh2 E -> DST_in2 = 1 packet/cycle
每个 DST input 的流量又均匀分到三个远端 GPU output:
mesh0 到每个 DST output:1/3
mesh1 到每个 DST output:1/3
mesh2 到每个 DST output:1/3
所以每个 DST output 的负载是:
DST_out0 = 1 packet/cycle
DST_out1 = 1 packet/cycle
DST_out2 = 1 packet/cycle
也就是说,DST 的 crossbar/output 侧同样刚好处在满负载临界点。任何一点仲裁相位不均、output buffer 短暂满、下游 credit 抖动,都会导致 DST 某个 input 某一拍搬不走。DST input 搬不走,就可能不给 E 返回 credit。于是即便 E 已经准备好 packet,E -> DST 也会丢一个 cycle。
所以 E -> DST 带宽缺口不一定是 E 侧饿了,也可能是 DST 侧反压造成的。
4. 上游 arrival jitter
greedy client 不等于 E 节点看到的到达流量是平滑的。
进入 E 的路径长度不同,而且经过的仲裁级数不同:
D/F 路径:
client -> D/F router -> E
B 路径:
A/C client -> A/C router -> B router -> E
B 方向还额外汇聚了更多源:
A 和 C 一共 4 个 req_client 汇聚到 B -> E
每个中间 router 都有 credit-gated 的 L1/L2/L3 仲裁。反压沿着不同路径返回的时序不同,各级 RR 的相位也不同。因此即使长期平均负载正好是 1 packet/cycle,E 节点看到的 cycle-level 到达也可能是抖动的。
理想平均行为是:
B 贡献 1/2 packet/cycle
D 贡献 1/4 packet/cycle
F 贡献 1/4 packet/cycle
总计 1 packet/cycle
但实际按 cycle 看,可能是:
cycle 0: B valid, D empty, F empty
cycle 1: B empty, D valid, F valid
cycle 2: B valid, D empty, F empty
cycle 3: B empty, D empty, F empty
cycle 4: B valid, D valid, F empty
长期平均可能仍然接近 1,但满带宽链路需要每个 cycle 都有 eligible packet。buffer 的作用就是抹平这种不均匀:
上游连续来包时,把 burst 存起来
上游短暂不来包时,从已有库存继续发
如果 buffer 太浅,burst 来的时候存不住,后面空窗期就会暴露到 E.output[DST][vc3],导致 E -> DST 掉到 64B/cycle 以下。
建议统计项
对每一个 E -> DST idle cycle,建议只归因到一个主原因:
1. E.output[DST][vc3] empty
2. E.output[DST][vc3] valid,但 DST.input[from_E][vc3] no credit
3. E.output[DST][vc3] empty,但 E.xbar[B/D/F][DST][vc3] 有 valid data
4. E.xbar[B/D/F][DST][vc3] empty,但 E.input[B/D/F][vc3] 有 valid data
5. E.input[B/D/F][vc3] 也 empty,说明上游供给本身不连续
解释:
原因 1/3/4:更偏 E 内部 buffer 或 E 内部供给时序问题
原因 2:更偏 DST 侧 credit/backpressure 问题
原因 5:更偏上游 arrival jitter 或上游反压问题
在调 buffer depth 之前,应该先做这个 idle cycle 归因。否则很容易把 buffer 加在错误的位置。