重新理解GPU并行计算
显存带宽
我们先理解几个基本概念。
1、显存位宽
是显卡硬件中的一个核心参数,简单来说,它指的是显存和显卡核心(GPU)之间每次交换数据的“通道宽度”。你可以把它想象成一条连接显存和GPU的高速公路:- 位宽越大,意味着高速公路越宽阔,同一时间内能双向通行的数据量就越大。- 位宽越小,道路变窄,能同时运输的数据就少。它的单位是 bit(位),常见的有 128-bit、192-bit、256-bit、384-bit 等。
2、显存带宽
显存带宽 = 显存位宽 × 显存频率
这个显存带宽,可以理解为“高速公路的实际每小时通行能力”:
假设有两款同样核心的显卡:
- 显卡A:位宽 256-bit,显存频率 1750 MHz
- 显卡B:位宽 128-bit,显存频率 1750 MHz
那么显卡A的显存带宽是显卡B的整整 2倍。在处理高分辨率纹理、复杂3D场景时,显卡A的数据供应更充足,游戏帧数会明显更高、更稳定。
GPU并行工作模式
流水线并行 (Pipeline Parallelism)
理论上:
工厂装配流水线:每个工人(GPU)负责一个专门的工序(模型的一层),产品(数据批次)依次经过所有工人,最终完成加工。
按 “层” 切分。GPU 0 负责第 1~10 层,GPU 1 负责第 11~20 层……像一个流水线,每个 GPU 依次处理不同的加工阶段。
数据并行 (Data Parallelism)
理论上:
银行柜台窗口:每个柜员(GPU)都有全套业务能力(完整模型副本),可以独立、并行地处理不同的客户(多个请求)。
按 “数据” 切分。每个 GPU 都有完整的模型,但大家处理不同的输入样本(你问你的,我问我的)。
张量并行 (Tensor Parallelism)
理论上:
按 “计算内部的参数” 切分。同一个矩阵乘法(比如一个全连接层里的一个大矩阵),被横向或纵向切割成几块,由多个 GPU 同时、协作完成这个运算。
Ollama案例解释(流水线并行为主)
Ollama的流水线并行,其实结合了张量并行和数据并行。
假设我们有 8 块 GPU,要跑一个 40 层的大模型,其中每单层的参数量大到需要 2 块 GPU 才能装下(单卡放不下一整层)。
如果只用张量并行(不用流水线):
- 每层拆到 2 块 GPU 上协作计算。
- 40 层就需要 40 × 2 = 80 块 GPU —— 我们只有 8 块,显然不够。
- 实际上,如果只用张量并行,你只能把 40 层全部塞进那 8 块 GPU 里(每层拆到 8 块上),但这会导致 GPU 间通信爆炸(每一层都要做跨 8 卡的 all-reduce),而且显存可能依然不够。
实际使用混合并行(张量并行 + 流水线并行):
步骤 1:先用张量并行解决“单层过大”
- 把每一层的参数矩阵切分成 2 份,分别放在 GPU 0 和 GPU 1 上。这两个 GPU 组成一个张量并行组,共同完成一层计算。
- 同样的,GPU 2 和 GPU 3 组成第二个张量并行组,负责另一层;GPU 4+5 第三组;GPU 6+7 第四组。
步骤 2:再用流水线并行处理“层数过多”
- 现在我们有 4 个张量并行组(每个组内有 2 张卡)。这 4 个组串行排列:第一个组负责模型的第 1 层(或第 1~10 层,取决于怎么切流水线阶段),第二个组负责接下来的几层……
- 数据流就是这样:
输入 → 组1(两层GPU协作) → 组2(两层GPU协作) → 组3 → 组4 → 输出
流水线并行处理闲置GPU问题的解决
微批次(Micro-batch)技巧
设定的场景:8张GPU,每2张通过张量并行组成一个流水线阶段。
- 阶段A:GPU 0-1(负责第1-10层)
- 阶段B:GPU 2-3(负责第11-20层)
- 阶段C:GPU 4-5(负责第21-30层)
- 阶段D:GPU 6-7(负责第31-40层)
现在我们有一个大的批次数据(比如 64 条样本)。朴素的流水线会把这 64 条当成一个整体传下去。
🚫 朴素方式(造成发呆)
- 阶段A 处理完整的 64 条样本(耗时很长)。
- 阶段A 把结果全部传给阶段B,然后阶段A 空闲。
- 阶段B 处理 64 条样本(耗时很长),传给阶段C,阶段B 空闲。
- ……以此类推。
结果:同一时刻只有 1 个阶段在工作,其他 3 个阶段(6 张GPU)在发呆。
✅ 微批次方式(消除发呆)
我们把 64 条样本切分成更小的 微批次,比如每个微批次 4 条样本。
- 时刻 T1:阶段A 处理 微批次1(4条)。花费时间很短,比如 1 个单位时间。
- 时刻 T2:
- 阶段A 把微批次1 的输出传给阶段B。
- 阶段A 立刻开始处理 微批次2(不空闲)。
- 阶段B 收到微批次1,开始处理。
- 时刻 T3:
- 阶段A 处理微批次3。
- 阶段B 处理微批次2。
- 阶段C 收到微批次1,开始处理。
- 时刻 T4:
- 阶段A 处理微批次4。
- 阶段B 处理微批次3。
- 阶段C 处理微批次2。
- 阶段D 处理微批次1。
……
一旦流水线被“填满”(即所有阶段都开始工作后),所有阶段几乎都在同时处理不同的微批次,没有任何 GPU 在发呆。
大家担心的“发呆等待”只发生在流水线启动(填满)和结束(排空)的短暂时刻。只要微批次的数量远大于阶段数量,这种空闲就可以忽略不计。
vLLM案例解释(张量并行为主)
假设模型有40层,单层参数大,甚至单张GPU都放不下。我们为不同策略分配8块GPU:
- Ollama(混合模式):它采用混合并行。通常会把模型”纵向”切成4个阶段(Stage),每个阶段10层,分别交给一个由2块GPU组成的”小组”,让数据像流水线一样依次通过,这是流水线并行的思路。在这个小组内部,那2块GPU才会相互协作,分担矩阵计算,这又是张量并行的思路。
- vLLM(张量并行模式):vLLM的著名做法,就是纯张量并行(Pure Tensor Parallelism)。它会完全忽略模型的”层”结构,把所有GPU当做一个巨大的计算池。每一层里的每一个巨型矩阵,都被视为一个整体,然后拆分成8块,分散到8张GPU上同时处理。你描述的那种”一层计算就拆成8份”的并行方式,正是vLLM的核心所在。
⚙️ vLLM 纯张量并行的计算过程
vLLM那8块GPU并不是按照1-5层、6-10层这样”纵向”分配工作的。而是在每一层的计算中,所有8块GPU都参与进来,共同完成这一层的矩阵运算。每一层计算完,输入数据立刻进入下一层,这8块GPU再次一起处理。像这样,循环往复直到所有层计算完毕。
具体到这40层的每一层,里面的核心矩阵运算就会被拆解。在vLLM的8卡配置 (tensor_parallel_size=8)下,整个过程是:
- 🚀 将权重大矩阵”分解”:在启动时,模型一个完整的大权重矩阵(比如4096x4096)就被横向或纵向切成8块,每块GPU只加载并保管其中的1/8的权重。
- 💻 并行计算”部分结果”:当输入数据(一个请求)进入模型第一层时,它会被完整地、同时地发送给所有8块GPU。每块GPU都用自己保管的那1/8权重和自己的那部分数据做乘法,得到1/8的中间结果。
- 🔄 同步”全局结果”:计算完成后,8块GPU通过 All-Reduce 的高速通信操作,把各自手里的1/8结果,快速地”拼”成一个完整的结果矩阵。这个每一层计算都要做的All-Reduce,是纯张量并行最显著的开销和特征。
- ➡️ 进入下一层:计算完成后,这个完整的结果进入下一层,然后重复上述2和3的步骤。
GPU点对点的复杂通信有个算法库(集合通信),Nvidia的叫“NCCL”,华为自己搞的叫“HCCL”。
PCI-e Switch
PCIe Switch 是一种用于扩展 PCIe 端口的设备,其内部结构可以理解为一个多端口、可路由的数据交换机。
1 | [Root Complex (CPU)] |
理论上可以通过一个很low 的主机,配合一个 高性能的PCIE拓展箱 来执行AI工作。
性能会变弱吗?
把一个 pcie 通道 通过 pcie switch 分成4个通道,会不会导致性能变弱呢?
这要看是什么应用场景:
1、CPU和GPU通信量大的场景。
典型就是游戏场景:
(1) 带宽缩减
- 直连时,GPU 独享整个 x16 通道(比如 PCIe 4.0 x16 ≈ 32 GB/s)。
- 经过 Switch 后,即使只有一个 GPU 接在其中一个 x4 端口上,它也只能使用 x4 的带宽(约 8 GB/s)。
因为 Switch 内部的交叉开关并不会把其他端口的空闲带宽“借”给这一个 GPU —— 每个下行端口的宽度是硬件固定的,无法动态合并。 - 结果:GPU 的可用带宽变为原来的 1/4,数据传输被卡在 x4 这个瓶颈上。
(2) 额外延迟
- 直连时,数据包从 CPU 到 GPU 只经过 Root Complex → 物理链路 → GPU 端点。
- 经过 Switch 时,路径变为:
CPU → 上行端口物理层 → Switch 内部缓冲 → 路由查询 → 交叉开关 → 下行端口物理层 → GPU。
每一步都会增加几十纳秒到几百纳秒的延迟,累积起来在大量小包传输时(如游戏中的频繁状态更新)会明显增加往返时间。
(3) 可能的拥塞与仲裁
- Switch 的上行端口只有一个,所有下行端口共享这一条上行链路。如果其他端口也有设备(比如网卡、SSD)同时传输数据,GPU 的数据就要排队等待。
- 即使其他端口空闲,Switch 的内部仲裁逻辑也需要花费额外时钟周期决定是否转发,这些开销在直连时不存在。
2、纯GPU内部工作场景
典型的就是 AI 计算场景。
1 | PCIe Switch |
极少涉及到和CPU通信,反而更快。
① CPU ↔ GPU 通信很少
- 在 AI 训练/推理中,模型参数、输入数据、梯度绝大部分时间都在 GPU 显存中。CPU 只负责:
- 启动 kernel(指令微小,几十字节)
- 偶尔拷贝新数据(比如下一个 batch 的输入,但可以异步 overlap)
- 很少量的监控和同步
- 因此,CPU 到 GPU 的带宽需求极低。一个 x16 的上行端口完全够用,甚至 x4 都绰绰有余。
➜ Switch 的上行带宽不再成为瓶颈。
② GPU ↔ GPU 通信占主导(Peer-to-Peer 直通)
- AI 计算中,多 GPU 需要频繁交换梯度(All-Reduce)或激活值(张量并行)。这些通信发生在 GPU 之间,不经过 CPU。
- PCIe Switch 支持 Peer-to-Peer 传输:
- GPU0 发送数据给 GPU1,数据包从 GPU0 的下行端口进入 Switch,Switch 内部交叉开关直接转到 GPU1 的下行端口,无需上行到 CPU。
- 路径:GPU0 → Switch → GPU1,延迟极短(几百纳秒)。
- 每个 GPU 的端口带宽是 x4,但 All-Reduce 通常采用环形或树形算法,利用所有链路并行,总吞吐可接近 Switch 背板带宽(如 x16 全双工)。
③ 实际性能数据(示例)
- PCIe 5.0 x16 上行带宽 ≈ 64 GB/s(单向)。对于多卡 All-Reduce,Switch 内部背板带宽通常等于所有下行端口带宽之和(如 4×x4 = x16)。
- 实测:在 4×A100 上做张量并行(TP=4),使用 PCIe Switch 与直连 CPU 的 NVLink 相比,差距较小(10
20%),远小于游戏场景(可能差 34 倍)。 - 而且,许多 AI 框架(如 Megatron-LM)会尽量将通信与计算重叠,利用 Switch 的低延迟实现高效并行。
算力与GPU数量
1 | 有效算力 |
拓扑Topology
拓扑指的是 多个 GPU 之间如何物理连接。不同拓扑会影响通信的路径、带宽和延迟。
常见拓扑结构图
- 环形(Ring)
1 | GPU0 —— GPU1 |
每个 GPU 只与左右两个邻居相连,形成一个闭环。
- 全互联(Fully Connected / Mesh)
1 | GPU0 ——— GPU1 |
每对 GPU 之间都有直接链路(现实中罕见,成本太高)。
- 交换机星型(Switch-based,如 PCIe Switch 或 NVSwitch)
1 | [Switch] |
所有 GPU 通过单一交换机互连,任意两者通信只需经过一跳。
- 层次化(例如 DGX 服务器中的 NVLink + NVSwitch
1 | [NVSwitch 0] [NVSwitch 1] |
这是更复杂的胖树拓扑,但底层仍然是多级交换机。
总结拓扑的作用: 决定了数据从 GPU A 到 GPU B 需要经过多少跳、每跳带宽多少。
集合通信算法
集合通信是指 多个 GPU 共同参与的数据交换模式,
可以理解为,在马路已经修好的前提下,怎么样让马路不堵车的算法。一大推数据的传输,就像小汽车一样,怕把路给堵了,一旦出现的等待卡死,所有GPU都停工了,损失很大。
例如 A机器有8块 GPU,B机器有 8块GPU,AB机器之间用一根网线通信,16块GPU 之间如果有不受控的相互收发信息,可能会导致马路不可控的阻塞。
- 广播broadcast
- 收集gather
- 分发scatter
- 规约reduce
前文中【 vLLM 纯张量并行的计算过程】案例中就有(全规约Allreduce)解释,这里是规约不是全规约但是理解类似,把多个 GPU中 计算的结果重新拼接成一个完整的结果矩阵,(求加减乘除最大值最小值都有),这个计算都是在传输过程中完成的,少有到达root 节点再计算的场景。
全规约Allreduce
要求所有节点都拥有规约操作后的完整结果。
例如:
在数据并行训练中,要求每个节点计算出本地梯度后,然后要和所有节点计算出的梯度相加再平均,然后每个节点要根据这个平均后的梯度来更新本地的模型。这个过程就是 Allreduce。视觉上想象,一个环,大家“ 旋转 ”着传递自己计算出的梯度结果,知道这个传递经过了所有节点,就算是凑了一个完整的大家的结果,每一个节点都拿到了完整的梯度平均结果。
全收集Allgather
每个节点都有一份不同的数据,大家 “ 旋转 ”着传递自己的数据,最后大家都有了全部的数据。
在模型并行计算或者某些需要聚合所有节点信息的场景中会用到。
说了这么多 广播、收集、分发、规约、全规约等等,那他们和环形算法、全互联算法等 是个什么关系呢?广播、收集、分发、规约、全规约这些其实是上层建筑,如图:
1 | ┌─────────────────────────────────────────────────────────────────┐ |
我们用最常见的 All-Reduce(归约后广播)来举例,并画出它在不同拓扑上的实现结构。