← 片上网络 & 互连

片上网络 & 互连 · 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 加在错误的位置。