一个 800 卡 A100 的集群,真正先在图纸上把人逼疯的不是算力、不是存储,而是网络。算力好堆,卡插满就行;网络不行,端口数、收敛比、跳数、布线长度、光模块型号,哪一项算错都要推倒重来。我最近在推演一个 800 卡规模的训练集群,核心约束给得很死:只用 64 端口的交换机,走两层拓扑,做 Fat-Tree 架构。这个题目听起来很像教科书里的经典 CLOS 练习,但真把数字代进去算一遍,你会发现端口账、收敛比、Rail 亲和性三者互相咬着,一步算偏就得返工。
这篇文章就把我这几轮推演的过程完整拆开:从 GPU 通信模式倒推带宽需求,到两层 CLOS 的端口公式怎么列,再到为什么最后选了 Rail-Optimized 的布线方式,以及上电之后 RoCE 怎么调、NCCL 怎么验证、哪些坑是我在别的项目里真踩过的。内容偏工程实操,适合正在做集群网络规划、或者准备从三层 Fat-Tree 收敛到两层 CLOS 的同行参考,刚接触 RDMA 组网的读者也能顺着算一遍看懂逻辑。
1. 先把账算清楚:800 卡到底需要什么样的网络
做拓扑设计最容易犯的错,是先选交换机再想业务,结果算到一半发现端口不够,只能加收敛比凑数。正确的顺序反过来:先看 GPU 之间怎么通信,通信需要多少带宽、多少跳、什么样的时延,倒推出网络的硬指标,再去匹配交换机和端口数。800 卡这个规模卡在一个很微妙的位置:三层 Fat-Tree 明显过剩,单层又接不下,两层 CLOS 几乎是唯一解,但两层的具体形态有好几种,选错了性能和成本都难看。
1.1 从 AllReduce 流量模型反推单卡带宽需求
大模型训练的主流并行方式无非三种:数据并行、张量并行、流水并行。这三种对网络的胃口完全不同,得分开看。
数据并行的核心操作是 AllReduce,Ring 算法下传输量约等于2(N-1)/N × M,N 是参与通信的 GPU 数。800 卡的时候 (N-1)/N 约等于 0.9987,基本就是 2M,也就是说每一份梯度数据要在网络上走两遍。这意味着 AllReduce 的时间几乎完全由最慢的那条链路决定,任何一条链路出现拥塞或者 ECMP 哈希不均,整个通信环就被拖住,800 张卡一起等。
张量并行是另一种画风。TP 组内的 GPU 要做 AllGather 和 ReduceScatter,通信频繁、消息小、时延敏感,对跳数特别挑剔。每多一跳交换机,端到端时延大概增加 0.8 到 1.2 微秒(含转发和排队),TP 组内如果跨了三跳,累积起来就是好几微秒,对计算占比高的算子影响很明显。这也是为什么 TP 一般限制在单机 8 卡内,靠机内高速互联解决。
流水并行是点对点,带宽需求相对温和,但对带宽稳定性要求高,因为 PP 的 bubble 时间直接取决于相邻 stage 之间的传输抖动。
三种模式叠加,网络的设计目标就很清楚了:AllReduce 要带宽和均衡,TP 要低跳数,PP 要稳定。落到具体数字上,单卡配 200G 网卡是一个比较均衡的起点——8 卡节点就是 1.6Tbps 的出口带宽,配合两层 CLOS,任意两张卡之间最多两跳交换机,端到端时延能压在 5 微秒以内。
这里有个容易被忽略的点:单卡 200G 并不意味着 AllReduce 能跑到 200G。Ring AllReduce 的有效带宽只有链路带宽的一半左右,因为数据要绕环走两遍。所以 200G 网卡的 AllReduce 实测 algbw 通常在 11 到 12.5GB/s 之间,busbw 才能接近 25GB/s。验收的时候别拿 algbw 对标线速,会误判。
1.2 为什么是两层 CLOS,而不是经典三层 Fat-Tree
教科书上的 Fat-Tree 是三层结构:core、aggregation、edge,k 端口交换机可以支持 k³/4 个主机。k=64 的时候理论上是 65536 台主机,规模大得离谱,但代价也大——每台主机要经过三跳交换机,core 层的交换机和光模块数量惊人。
800 卡拆成 100 台 8 卡服务器,每台 8 个 200G 端口,总共 800 个接入端口。这个量级用三层是杀鸡用牛刀,而且多一跳就多一份时延和一份故障概率。两层 CLOS(也就是常说的 Spine-Leaf)在端口数和跳数之间取得了更好的平衡:Leaf 层接服务器,Spine 层只做 Leaf 之间的全互联,任意两台服务器之间固定两跳。
真正让两层方案成立的关键,是上行端口和下联端口的比例可以被精确控制。一台 64 端口交换机,如果拿 32 口下联、32 口上联,那单台 Leaf 的下联带宽和上联带宽天然是 1:1,无阻塞就是设计出来的,不是调出来的。三层 Fat-Tree 反而很难做到这么干净的比例,因为 core 层的端口要分摊给多个 pod。
1.3 三条硬指标:无阻塞、低时延、可预测
把需求收敛成可验收的指标,我一般会写三条:
| 指标 | 目标值 | 验收方式 |
|---|---|---|
| 网络收敛比 | 1:1(Leaf 层上下行对称) | 端口带宽核算 |
| 任意两卡端到端时延 | ≤ 5μs(不含网卡处理) | 打流仪逐对测量 |
| AllReduce busbw | ≥ 线速的 85% | NCCL 全网压测 |
| 故障切换收敛时间 | ≤ 1s(ECMP 重哈希) | 拔纤注入测试 |
“可预测”这条比较虚,但对训练集群特别重要。所谓可预测,是指同样的通信模式下,每次跑出来的带宽波动不超过 5%。波动大通常来自 ECMP 哈希不均或者 PFC 反压扩散,这两个问题在后面的配置章节会重点讲。
2. 端口账怎么算:两层 CLOS 的三种可行解
端口计算是整个设计里最像做数学题的部分,但它有唯一解吗?没有。同样的 64 端口交换机、同样的 800 卡规模,至少有三种自洽的方案,差别在收敛比、布线复杂度、扩展余量上。这一段我把公式列出来,然后把三种方案摆在一起对比。
2.1 三条约束公式,先把可行域框住
设 Leaf 交换机数量为 L,每台 Leaf 下联端口数 d,上联端口数 u,Spine 交换机数量 S。约束条件有三条:
- 端口守恒:d + u = 64,因为一台交换机就 64 个口。
- 接入覆盖:L × d ≥ 800(考虑 breakout 的话是 L × 2d ≥ 800)。
- 上行收敛:S × 64 ≥ L × u。
把这三条摆开,再用“上下行对称”这个额外约束(u = d = 32),能立刻得到 L ≥ 25,S ≥ 12.5 也就是 13。但这只是数学可行,工程上未必是最优。
如果放弃严格对称,允许一定收敛比,可行域会宽松很多。比如 L=20、d=40、u=24,此时 S ≥ 20×24/64 = 7.5,取 8,每台 Leaf 到每台 Spine 正好 3 条链路,布线极其规整,收敛比是 40:24 即 1.67:1。
收敛比的算法要说清楚:收敛比 = 下联总带宽 ÷ 上联总带宽。1.67:1 的意思是所有下联端口同时满速发送时,上行方向会有 1.67 倍的超订。对于 AllReduce 为主的流量,全网同时满速的概率不高,1.67:1 在不少生产集群里是可以接受的,但要做 ECN 和 PFC 调优兜底。
2.2 方案对比:标准 CLOS 与 Rail-Optimized
矩阵算完,落到具体形态其实有两个大方向,这两个方向的差别不在端口数,而在哪张 GPU 卡接到哪台 Leaf。
标准 CLOS的做法是:一台服务器上的 8 个网卡,随便分给不同的 Leaf,只要保证每台 Leaf 下联的服务器数量均衡。好处是布线灵活,任意一台 Leaf 故障只影响它下面挂的那几十张卡。
Rail-Optimized的做法更讲究:所有服务器的第 0 号 GPU 都接到同一组 Leaf(称为 rail 0),第 1 号 GPU 接到 rail 1 的 Leaf,以此类推。这样的好处是,同一 rail 内的 GPU 通信只走一跳 Leaf,跨 rail 才走 Spine。对于 TP 组和部分 AllReduce 场景,这个优化能显著降低时延。
我把两种方案的核心参数列出来对比:
| 对比项 | 标准 CLOS | Rail-Optimized |
|---|---|---|
| Leaf 数量 | 13 台(按 800 口算) | 16 台(8 rail × 2 台) |
| Spine 数量 | 8 台 | 8 台 |
| 单 Leaf 下联 | 64 个 200G 口 | 64 个 200G 口 |
| 单 Leaf 上联 | 32 个 400G 口 | 32 个 400G 口(满载时) |
| 同 rail 通信跳数 | 1 跳(同 Leaf 内) | 1 跳(稳定保证) |
| 接入端口利用率 | 96%(800/832) | 78%(800/1024) |
| 扩展性 | 差,端口几乎用满 | 好,可扩到 1024 卡 |
| 布线复杂度 | 中等 | 较高,需跨柜 |
标准 CLOS 接入效率高,但没余量;Rail-Optimized 浪费了 22% 的接入端口,换来的是确定的通信路径和扩展空间。做训练集群,我倾向于后者——训练集群的扩容是常态,今天 800 卡,明年可能就是 1024 卡,端口预留的价值远大于那点交换机成本。
2.3 最终定稿:16 Leaf + 8 Spine 的参数表
定下来的方案是 Rail-Optimized 的两层结构,完整参数如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 服务器数量 | 100 台 | 每台 8 卡,共 800 卡 |
| 单卡网卡 | 200G RoCEv2 | 每卡一个端口 |
| 单服务器上行 | 8 × 200G = 1.6Tbps | 8 个独立网卡 |
| Leaf 交换机 | 16 台,64 × 400G | 8 个 rail,每 rail 2 台 |
| Leaf 下联 | 32 口 400G 拆 64 × 200G | 每台接 64 张卡 |
| Leaf 上联 | 32 口 400G(满载) | 8 台 Spine 各 4 条 |
| Spine 交换机 | 8 台,64 × 400G | 满配时 64 口全下联 |
| Spine 下联 | 当前 48 口,满配 64 口 | 预留扩展 |
| 总接入能力 | 1024 卡 | 当前使用 800 卡 |
| 全网收敛比 | 1:1 | Leaf 层上下行对称 |
当前 800 卡的实际配置是:8 个 rail 中,每 rail 的第一台 Leaf 接 64 张卡(上联 32 口),第二台 Leaf 接 36 张卡(上联先配 16 口)。这样每台 Spine 收到 8×4 + 8×2 = 48 条链路,用 48 个端口,留 16 个口给未来扩容。
补齐到 1024 卡的时候,每 rail 第二台 Leaf 也会接满 64 张卡,上联从 16 口升到 32 口,全网链路数从 384 条变成 512 条,8 台 Spine 各 64 口正好用满,一根线都不用重新布。这个设计的前瞻性就体现在这里——当前配置不需要把机框插满,但机框和端口的分配必须按满载来规划。
3. 关键设计细节:从端口拆分到机柜布线
拓扑图好看,落地全是细节。400G 端口怎么拆成 200G、光模块选 MPO-12 还是 MPO-16、线缆怎么走、标签怎么打,这些东西在图纸上看不出来,但在机房里能让你多干三天。
3.1 400G 拆 200G:breakout 的四个坑
用 64 口 400G 交换机接 200G 网卡,必须用 breakout(端口拆分)。一个 400G 口通过分支光缆拆成 2 个 200G 口,这是最省交换机的做法,一台 64 口交换机相当于 128 个 200G 口。
第一个坑是光模块和分支缆的匹配。400G 端如果用的是 8 通道并行(比如 400G-SR8 或者 400G-DR4 的并行形态),拆成 2 个 200G 时,分支缆一端是 MPO-12 或 MPO-16 接交换机,另一端分出来两个 MPO-12 接网卡。极性(Type A / Type B / Type C)必须和主干保持一致,接反了不通,而且不通得毫无提示,灯亮着但链路起不来。
第二个坑是FEC 配置。200G 走 PAM4,前向纠错默认是 RS-FEC(544),某些网卡在特定固件下需要显式打开或者降级到 FC-FEC,否则误码率高得离谱,表现为链路 up 但一跑流量就丢包。我一般是先在两端手动对齐 FEC 模式,确认跑通,再交给自动协商。
第三个坑是分支缆长度。分支缆本身有一定长度(常见 1 到 3 米),从交换机端口分出来后要在机柜内绕到配线架。如果分支缆选短了,端口位置和配线架位置对不上,只能整根换。买之前拿卷尺在机柜上量一遍,比看图纸靠谱。
第四个坑是同一 400G 口的两个 200G 子口不能提前混用。一个 400G 口拆开后,两个子口必须都归属同一台 Leaf 的同一组 rail,不能一个接 rail 0 一个接 rail 3,否则 breakout 的物理通道就乱了。在规划表里要把“物理端口号 → 逻辑端口号 → 服务器编号 → GPU 序号”四者的映射一次性定死。
3.2 上行链路怎么分配:LAG 还是 ECMP
每台 Leaf 有 32 个 400G 上行口,8 台 Spine,每台 4 条链路。这 4 条链路有两种接法:
第一种是LAG(链路聚合),把到同一台 Spine 的 4 条链路捆成一个逻辑口。好处是配置简单,坏处是哈希粒度粗,同一个 LAG 内部容易极化,而且一台 Spine 故障时整个 LAG 起不来,重收敛慢。
第二种是ECMP(等价多路径),4 条链路各自是独立的三层接口,跑 eBGP 或者 OSPF,靠路由的等价路径做负载。这是大集群的主流做法,哈希粒度细到每一条物理链路,单条链路故障只影响 1/32 的上行带宽,重收敛在毫秒级。
我选 ECMP。配置上的关键是把哈希字段配对。RoCEv2 的流量是 UDP 封装,哈希必须包含源 IP、目的 IP、UDP 源端口、UDP 目的端口四个字段,少配一个都会导致大量流量落在同一条链路上。有些交换机默认只哈希 IP 五元组里的三层信息,对 RoCE 这种源端口随机的流量反而不友好,得手动打开四层哈希。
3.3 线缆规划与机柜布局
13 个计算柜放 100 台服务器(12 柜满配 8 台,1 柜 4 台),Leaf 集中放在两个网络柜里。这种放法的问题是服务器到 Leaf 的距离可能超过 30 米,铜缆和 AOC 都够呛。
实际方案是:服务器到 Leaf 走MPO 主干光缆 + 配线架。每个机柜顶部配一个高密度光纤配线架,服务器出来的分支缆先上配线架,通过主干光缆拉到网络柜,再从网络柜的配线架跳线到 Leaf。这样线缆长度可控,后期改线也只需要动跳线,不用重新穿线。
主干光缆的数量按满载算:当前 400 根下联分支缆、384 根上联主干。满载时是 512 根下联 + 512 根上联。主干光缆一次性按满载敷设,分支缆按需买,这是比较省钱的节奏。
| 线缆类型 | 数量(当前) | 数量(满载) | 备注 |
|---|---|---|---|
| 400G 转 2×200G 分支缆 | 400 根 | 512 根 | 服务器侧 |
| 400G 上联主干 | 384 根 | 512 根 | Leaf 到 Spine |
| 200G 网卡侧模块 | 800 个 | 1024 个 | 与网卡同厂兼容性最好 |
| 配线架 | 15 个 | 15 个 | 每柜一个,含网络柜 |
强烈建议主干光缆按满载一次布好。后期扩容时最贵的不是光模块,是停机穿线的人工和风险。线缆在桥架里多躺一年不花钱,等到要扩的时候再穿,得停半层楼的业务。
3.4 命名规范:让排障时间缩短一半
设备命名和端口标签看起来是小节,实际上是排障效率的分水岭。我用的规则是:
Leaf 命名为lf-r{rail}-{序号},比如lf-r0-01、lf-r0-02;Spine 命名为sp-01到sp-08。服务器命名为node-{三位编号},每个网卡端口标签写成node-001-g0到node-001-g7,g 后面的数字就是 GPU 序号,也是 rail 号。
这样做的直接好处是:NCCL 报错说mlx5_3通信异常,你能立刻定位到第 3 号 rail 的 Leaf,再顺着标签找到具体端口和服务器。没有这套命名,机房排障就是大海捞针。
端口描述也要写进交换机配置里,格式统一为to:node-001-g0或者to:sp-03:e1/1。几个月后回头看配置,这些描述能救命。
4. 上电到通流:配置顺序与调优参数
设备上架、线缆插好、灯全绿,这只是物理层通了,离“能跑训练”还差一整套配置。这一段按我实际的操作顺序走一遍,每一步都说明为什么这么做。
4.1 网络层设计:为什么用 L3 到 Leaf
大集群的主流做法是L3 到 Leaf,也就是服务器网关放在 Leaf 上,Leaf 和 Spine 之间跑三层路由。早期的 L2 大二层方案(VXLAN、STP 或者 TRILL)在 800 卡这个规模下问题太多:广播域过大、STP 收敛慢、故障域不可控。
L3 到 Leaf 的好处是每个 Leaf 就是一个小型广播域,故障被限制在局部。Leaf 和 Spine 之间跑 eBGP,每台 Leaf 用自己的私有 ASN,Spine 用一个公共 ASN,配置量不大但扩展性好。
ASN 规划我一般这么分:Spine 用 AS 65000,8 台都用一个;Leaf 从 65001 排到 65016,一台一个。eBGP 邻居关系是每台 Leaf 和 8 台 Spine 全互联,一台 Leaf 上有 8 个邻居,一台 Spine 上有 16 个邻居。
# Leaf 侧配置示意(伪代码,不同厂商语法有差异) router bgp 65001 neighbor 10.0.0.1 remote-as 65000 neighbor 10.0.0.2 remote-as 65000 ! ... 一直到 10.0.0.8 address-family ipv4 unicast network 10.100.1.0/24 ! 本 Leaf 下的服务器网段 maximum-paths 8 ! 开启等价多路径maximum-paths 8这行是关键,不开的话 BGP 只选一条最优路径,ECMP 就废了,32 条上行链路只用 1 条,带宽直接掉到 1/32。
4.2 ECMP 哈希与全局负载均衡
普通 ECMP 有个毛病叫哈希极化:不同的 Leaf 之间流量模式不一样,有的 Leaf 上行流量集中在少数几条链路上,虽然总体带宽够用,但局部已经打满了。根本原因是 ECMP 哈希用的是固定的五元组,同一个流永远走同一条路,而大量同源同目的的大流会让哈希分布严重偏斜。
解决手段是GLB(全局负载均衡)。它的做法是在转发面动态感知每条链路的利用率,把流量往空闲链路上引导,而不是死守哈希结果。开启 GLB 之后,长流的分配会随负载动态调整,实测在 AllReduce 场景下,链路利用率标准差能从 18% 降到 5% 以内。
代价是 GLB 会导致少量的乱序。对 RoCE 来说,乱序本身不是问题,问题是乱序被网卡当成丢包触发重传,反而恶化。所以开 GLB 之前必须确认网卡侧支持乱序容忍(大多数现代 RDMA 网卡都支持),并且把乱序容忍的窗口配大一点。
4.3 RoCEv2 的三个核心参数:PFC、ECN、DCQCN
RoCEv2 的流量控制是三件套,缺一不可:
PFC(优先级流控)兜底,防止缓冲区溢出丢包。配置时要单独划分一个无损队列,用 DSCP 值标记。我一般用 DSCP 26 对应优先级 3,服务器侧和交换机侧必须完全一致,有一处不一致就会变成有损队列。
ECN(显式拥塞通知)主动标记,让发送端提前降速。阈值设置有一套算法,核心是带宽时延积(BDP)。400G 链路的 BDP 大约是 400Gbps × 1μs = 50KB。ECN 的 Kmin 设成 BDP 的 3 倍(150KB 左右),Kmax 设成 BDP 的 30 倍(1.5MB 左右),标记概率从 0 线性升到 100%。
DCQCN是网卡侧的速率控制算法,靠 ECN 标记驱动。参数里最关键的是降速因子和恢复速率,配得太激进会导致速率震荡,配得太保守则收敛慢。
| 参数 | 推荐值 | 调整依据 |
|---|---|---|
| PFC 队列 | Prio 3(DSCP 26) | 全网一致 |
| PFC 触发门限 | 缓冲区 30% | 配合 headroom 调整 |
| ECN Kmin | 150KB | 3 × BDP |
| ECN Kmax | 1.5MB | 30 × BDP |
| ECN 标记概率 | 0-100% 线性 | 默认 |
| DCQCN 降速因子 | 0.5 | 实测震荡明显时上调到 0.7 |
| DCQCN 恢复速率 | 5 Mbps/流 | 大规模时降低 |
| MTU | 9000(或 4200) | 全网一致 |
MTU 一定要全网统一。交换机和服务器两侧有一边是 9000 一边是 1500,会表现为“小包能通、大包丢”,排查起来极其费时。我的做法是在建网阶段就用大包 ping 逐段验证,
ping -M do -s 8972。
4.4 NCCL 拓扑识别与 rail 亲和性验证
网络配好了,不代表 NCCL 用对了路径。NCCL 启动时会自己探测拓扑,但如果 Linux 侧看不到正确的网卡到 GPU 的亲和关系,它会选错网卡,导致流量走错 rail。
验证方法很直接,先看亲和性映射:
# 查看每个网卡对应的 NUMA 和 GPU nvidia-smi topo -m # 检查 NCCL 识别到的网卡 NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,NET \ all_reduce_perf -b 8 -e 128M -f 2 -g 8 2>&1 | head -50输出里会打印NET/IB : Using [0]mlx5_0:1这类信息,把用到的网卡列表和topo -m的映射对一遍,如果 NCCL 用的网卡和预期不一致,用环境变量锁定:
export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_6,mlx5_7 export NCCL_IB_GID_INDEX=3 # RoCEv2 常用 export NCCL_SOCKET_IFNAME=bond0 # 控制面走普通网卡 export NCCL_IB_TC=26 # 和 DSCP 26 对应NCCL_IB_TC这个参数很多人会漏,它的作用是把 NCCL 发出的流量打到正确的无损队列上。不配的话