将计算负担和模型参数分布到多个设备上对于训练大型模型来说是必要的,但这会带来一个重要的性能考量:通信开销。每当数据需要在设备间交换时,无论是梯度、激活值还是权重分片,时间都会花在通信上而不是计算上。将这种开销降到最低对于实现高效扩展和缩短训练时间来说十分重要。本节分析了我们讨论过的不同并行策略所关联的通信模式和成本。了解这些成本有助于为给定的模型架构和硬件配置选择最合适的策略或策略组合。分布式训练中的通信基本操作分布式训练依赖于通信集合操作,这些操作涉及多个进程协同交换数据。LLM训练中最常用的基本操作包括:广播 (broadcast): 将数据从一个进程发送到所有其他进程。规约 (reduce): 使用指定的运算(如求和、求平均)将所有进程的数据合并到一个进程上。All-Reduce (all_reduce): 合并所有进程的数据,并将结果分发回所有进程。这实际上是先进行规约再进行广播的操作。它在数据并行中大量用于同步梯度。分散 (scatter): 将数据块从一个进程分散到所有其他进程。收集 (gather): 将所有进程的数据块收集到一个进程上。All-Gather (all_gather): 收集所有进程的数据块,并将完整的、拼接后的数据分发回所有进程。用于某些张量并行实现。规约-分散 (reduce_scatter): 使用规约运算合并所有进程的数据,然后分散结果,使得每个进程接收最终规约张量的一个数据块。也用于张量并行。点对点 (send/recv): 直接通信,一个进程向另一个特定进程发送数据,后者接收数据。这是流水线并行的主要机制。在PyTorch中,这些操作通常通过torch.distributed包来访问。例如,对一个组中所有进程的张量t执行All-Reduce操作可能看起来像这样:import torch import torch.distributed as dist # 假设 't' 是当前设备上的一个张量 # 假设分布式环境已初始化 # 执行异步All-Reduce(默认求和) dist.all_reduce(t, op=dist.ReduceOp.SUM, async_op=True) # 稍后,如果需要则同步,或者链接计算 # ...流水线并行的点对点通信涉及发送和接收进程对:# 阶段 'i' 的进程将激活值发送到阶段 'i+1' if rank == i: # 假设 'activations' 是要发送的张量 dist.send(tensor=activations, dst=i+1) # 阶段 'i+1' 的进程从阶段 'i' 接收激活值 elif rank == i+1: # 为接收激活值分配缓冲区 received_activations = torch.zeros_like(expected_activations_shape) dist.recv(tensor=received_activations, src=i)影响通信成本的因素通信所需的时间取决于几个因素:延迟 ($\alpha$): 启动任何通信所需的固定启动时间,与数据大小无关。这通常由网络协议开销和设备间的物理距离/跳数决定。带宽 ($\beta$): 数据可以通过网络链路传输的速率,通常以吉比特每秒(Gbps)或吉字节每秒(GB/s)衡量。数据传输时间与带宽成反比。消息大小 ($M$): 正在传输的数据量。较大的消息需要更长时间,主要受带宽影响。设备数量 ($P$): 许多集合操作的时间复杂度随参与设备的数量而变化。例如,简单的All-Reduce可能呈线性扩展,而像环形All-Reduce这样的优化算法则呈亚线性扩展。网络拓扑和硬件: 网络的物理布局(例如,胖树、环面)和特定的互连技术(例如,以太网、InfiniBand、NVIDIA NVLink/NVSwitch)对可实现的带宽和延迟有显著影响,特别是对于节点间通信。NVLink为单个节点内的GPU到GPU通信提供高带宽。集合算法: 用于实现集合操作的特定算法(例如,All-Reduce的环形、树形、蝴蝶算法)可以根据消息大小和网络拓扑表现出不同的性能特点。像NVIDIA集合通信库(NCCL)这样的库为NVIDIA GPU实现了这些算法的高度优化版本。一个常见且简单的用于表示大小为 $M$ 的单个消息的通信时间 $T$ 的模型是alpha-beta模型:$$ T \approx \alpha + \frac{M}{\beta_{eff}} $$这里,$\alpha$ 代表延迟部分,$\beta_{eff}$ 是传输时实现的有效带宽。对于涉及 $P$ 个设备的集合操作,模型会变得更复杂,通常会涉及与 $P$ 的对数或线性相关的项,具体取决于所用算法。并行策略的通信模式让我们分析每种主要策略固有的通信成本:数据并行 (DP):主要通信: 反向传播后执行all_reduce操作,以聚合所有 $P$ 个副本的梯度。消息大小: 整个模型梯度的总大小 ($M_{model}$)。频率: 每个训练步骤一次(如果使用梯度累积,则每个累积步骤一次)。开销: 主要由All-Reduce的成本决定。时间通常随模型大小 $M_{model}$ 变化,并与设备数量 $P$ 呈对数或线性关系,具体取决于All-Reduce算法和网络。对设备间带宽高度敏感。对于大型模型和大量设备,此All-Reduce可能成为一个重要的瓶颈。梯度累积通过降低这种昂贵操作的频率来帮助。张量并行 (TP):主要通信: 涉及在特定层(例如MLP或注意力块)的前向和反向传播内部的all_reduce、all_gather或reduce_scatter操作,这些层在设备间进行拆分。消息大小: 对应层内激活值或梯度张量一部分的较小消息。大小取决于隐藏维度、序列长度和张量并行设备数量。频率: 每层多次,在前向和反向传播期间。比数据并行的通信频率高得多。开销: 通信频繁发生,通常涉及较小的消息。这使得张量并行可能对延迟($\alpha$)和带宽($\beta$)都敏感。它极大地受益于高带宽、低延迟的节点内互连,如NVLink,因为张量并行通常首先在节点内部应用。由于其频率高,总通信量可能很大。digraph G { rankdir=LR;// 设置全局字体大小 fontsize=12; // 节点设置,带有明确的字体大小 node [shape=circle, style=filled, color="#a5d8ff", fontname="sans-serif", fontsize=12]; // 边设置,带有明确的字体大小 edge [color="#495057", fontname="sans-serif", fontsize=12]; GPU0 -> GPU1 [label="前向/反向"]; GPU1 -> GPU2 [label="前向/反向"]; GPU2 -> GPU3 [label="前向/反向"]; GPU3 -> GPU0 [label="前向/反向"];} ``` > 4-GPU环形All-Reduce中数据流的简化视图,常用于数据并行或张量并行集合操作。每个GPU都从其相邻设备发送和接收数据块。流水线并行 (PP):主要通信: 相邻流水线阶段之间的点对点send/recv操作。阶段 $i$ 在前向传播期间将其输出激活值发送到阶段 $i+1$,而阶段 $i+1$ 在反向传播期间将激活值的梯度发送回阶段 $i$。消息大小: 阶段边界处的激活张量(或梯度张量)的大小。大小取决于批量大小、序列长度和隐藏维度。频率: 每个微批量在每个阶段边界处一次(前向和反向)。比张量并行频率低,但比数据并行(不含梯度累积)频率高。开销: 主要为点对点通信。托管相邻阶段的设备之间的延迟和带宽十分重要。流水线并行效率低下的一个主要原因不只是通信时间本身,而是“流水线气泡”的可能性——当阶段等待来自前一阶段的数据时,出现的空闲时间。微批量处理有助于减小气泡大小,但会增加通信频率和相关的延迟开销。通信成本比较策略主要操作消息大小频率敏感度瓶颈数据并行all_reduce模型梯度 (大)每 (累积) 步一次带宽All-Reduce时间张量并行all_reduce、all_gather、reduce_scatter层激活值/梯度 (小/中)每层多次延迟、带宽频繁的集合调用流水线并行send/recv边界激活值/梯度 (中/大)每个微批量/阶段一次延迟、带宽流水线气泡、阶段间通信表:不同并行策略通信特点的定性比较。混合方法将这些策略结合起来,带来更复杂的通信模式。例如,将数据并行与张量并行结合使用意味着每个数据并行组(应用了张量并行的地方)都会执行梯度的All-Reduce操作。同时使用张量并行和流水线并行则涉及阶段内张量并行通信和阶段间流水线并行通信。通信性能分析虽然理论分析提供了直觉,但特定训练运行中的实际通信开销在很大程度上取决于实现细节、硬件、网络配置和软件堆栈(例如PyTorch版本、NCCL版本)。因此,性能分析是必要的。torch.profiler、NVIDIA Nsight Systems (nsys) 或框架特有的日志记录等工具可以帮助衡量在不同通信操作(nccl:all_reduce、nccl:send等)与计算内核上花费的时间。分析这些性能文件对于找出瓶颈并优化分布式训练配置十分重要。# 使用 torch.profiler 捕获 CPU 和 GPU 活动的示例 # 包括分布式通信调用(如果使用 NCCL 后端) with torch.profiler.profile( activities=[ torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA ], record_shapes=True, profile_memory=True, with_stack=True ) as prof: with torch.profiler.record_function("model_training_step"): # 在这里执行模型的正向、反向传播和优化器步骤 outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step() # 打印聚合统计信息 print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) # 导出跟踪文件,以便在 Perfetto UI 或 Chrome Trace Viewer 等工具中进行详细分析 # prof.export_chrome_trace("trace.json")"通过了解与每种并行策略相关的基本通信模式和成本,并使用性能分析工具来衡量表现,您可以做出明智的决定,了解如何最好地分配您的LLM训练工作负载,以最大程度地提高效率并缩短训练时间。"