详细摘要 摘要
生成:2025-05-13 19:25摘要详情
- 音频文件
- Stanford CS336 Language Modeling from Scratch | Spring 2025 | 08 Parallelism 2
- 摘要类型
- 详细摘要
- LLM 提供商
- openai
- LLM 模型
- gemini-2.5-pro-exp-03-25
- 已创建
- 2025-05-13 19:25:19
摘要内容
CS336讲座回顾:Parallelism 2 - 多GPU与多节点训练策略
本讲座(Stanford CS336 - Parallelism 2)探讨了利用多GPU和多节点并行来加速模型训练,核心在于克服数据传输瓶颈以最大化GPU利用率。讲座首先回顾了单GPU内的并行技术,并重点转向跨GPU和节点的并行。内容介绍了数据传输的层级结构,从GPU内部的L1缓存、高带宽内存(HBM),到同一节点内GPU间的NVLink,再到跨节点的NVSwitch,指出数据传输速度远慢于计算速度,是主要的性能瓶颈。
概览/核心摘要 (Executive Summary)
本讲座(Stanford CS336 - Parallelism 2)深入探讨了在多GPU及多节点环境下加速模型训练的并行计算策略,核心目标是优化计算结构以规避数据传输瓶颈,从而最大化硬件利用率。讲座首先回顾了计算与通信的层级结构,从GPU内的L1缓存、HBM,到节点内GPU间的NVLink,再到跨节点的NVSwitch,强调了数据传输速度远慢于计算速度,是性能瓶颈所在。接着,详细介绍了集体操作(Collective Operations)如broadcast, scatter, gather, reduce, all-gather, reduce-scatter, all-reduce等,并阐述了它们在Nvidia的NCCL库及PyTorch Distributed中的实现和使用。讲座通过代码示例演示了这些操作,并进行了初步的基准测试,分析了带宽计算方法。核心部分转向分布式训练的三种主要并行策略:数据并行(Data Parallelism, DDP),通过在各GPU上复制模型、切分数据批次,并在反向传播后使用all-reduce同步梯度;张量并行(Tensor Parallelism),将模型参数(如MLP的权重矩阵)沿隐层维度切分到不同GPU,并在层间通过all-gather通信激活值;流水线并行(Pipeline Parallelism),将模型的不同层分配给不同GPU,数据以微批次(micro-batches)形式在GPU间顺序流转。讲座强调了这些策略的简化实现,并指出现实应用中需考虑通信与计算重叠、更复杂的簿记等问题。最后提及JAX等更高层抽象框架,并总结并行化是应对模型规模持续增长的持久需求。
讲座背景与核心挑战
讲者指出,本讲座是系统讲座的第二部分,重点关注跨多GPU和多节点的并行化,旨在最大化硬件利用率以加速模型训练。上周讨论了单GPU内的并行,本周则扩展到多GPU环境。
- 核心挑战:数据传输是主要瓶颈。计算发生在GPU的流式多处理器(SMs)上,输入输出数据可能存储在L1缓存、HBM(高带宽内存),甚至其他GPU上。
> "The name of the game is how do you structure all your computation to avoid data transfer bottlenecks?" - 目标:保持算术强度高,使GPU饱和运行。
- 回顾上周:单GPU内通过融合(fusion)和分块(tiling)等技术,将数据加载到L1缓存或共享内存进行本地计算,减少对HBM的读写。
- 本周焦点:跨GPU和节点的通信,涉及模型和参数的复制与分片,以及优化器状态的管理。
计算与通信的层级结构
讲者概述了从快小到慢大的存储与通信层级,强调了最小化数据传输的核心概念在不同层级具有相似性,但具体机制不同。
- 层级(由快小到慢大):
- 单节点,单GPU内:
- L1缓存:极快但极小。
- HBM(高带宽内存):较L1大,速度稍慢。
- 同节点,GPU之间:
- NVLink:Nvidia GPU间的高速互联。
- 跨节点,GPU之间:
- NVSwitch:连接多个NVLink,实现更大规模的GPU互联。
- 单节点,单GPU内:
- 传统硬件 vs. 现代科学计算硬件:
- 传统:GPU通过PCIe总线与CPU通信,节点间通过以太网通信。数据在GPU间传输需经过CPU内核、缓冲区拷贝,开销大。
- 现代:
- NVLink直接连接GPU,绕过CPU和主机内核。
- NVSwitch直接连接跨节点GPU,绕过以太网。
"NVSwitch just and NVLink kind of skip all of that and just optimize directly for the type of workloads that we're interested in."
- 硬件数据:
- H100 GPU:每个GPU有18个第四代NVLink,总带宽900 GB/s。
- 相比之下,高带宽内存(HBM)的读取速度(例如从SM到HBM)仍然快得多,大约是NVLink带宽的4倍左右。
- 讲者提及这些数字会随新硬件(如Blackwell)发布而改变,预计带宽会增加2-3倍。
- 查看GPU连接:可通过类似
nvidia-smi topo -m的命令查看GPU间的连接拓扑。
集体操作 (Collective Operations)
集体操作是分布式编程中用于管理多节点/多设备通信的基础原语,比手动管理点对点通信更优。
- 术语:
- World Size: 设备数量(例如4个GPU)。
- Rank: 设备的索引(例如rank 0, 1, 2, 3)。
- 主要集体操作:
- Broadcast: 将单个rank上的数据复制到所有ranks。
- Scatter: 将单个rank上的数据(如一个列表)的不同部分分发到不同的ranks。
- Gather: Scatter的逆操作,将不同rank上的数据收集到单个rank上。
- Reduce: 与Gather类似,但收集数据时执行一个操作(如求和)。
- All-Gather: 与Gather类似,但所有ranks都接收到收集后的完整数据。
- Reduce-Scatter: 结合Reduce和Scatter,对数据进行reduce操作,并将结果的不同部分分发到不同ranks。
- All-Reduce: 等价于Reduce操作后跟一个All-Gather操作(或Broadcast操作,原文提及
reduce + all_gather)。
- 记忆技巧:
Reduce:执行关联和交换运算(如sum, min/max, average)。Scatter:Gather的逆操作。All:目标是所有设备。
集体操作的软件实现
- NCCL (Nvidia Collective Communications Library):
- Nvidia提供的库,将高级集体操作(如all-reduce)转换为GPU间发送和接收的底层数据包。
- NCCL会探测硬件拓扑并优化GPU间的通信路径。
- 调用集体操作时,NCCL会启动CUDA内核来发送和接收数据。
- PyTorch Distributed (
torch.distributed):- 为集体操作提供了简洁的Python接口。
- 支持多种后端:
- NCCL: 用于Nvidia GPU。
- Gloo: 用于CPU,方便在无GPU环境下调试。
- 高级功能:如FSDP(Fully Sharded Data Parallel),但本课程为从零构建,不直接使用。
- PyTorch代码示例:
- 初始化:
dist.init_process_group(),指定后端(如'nccl'或'gloo'),各进程需连接到同一主机进行协调。 - Barrier:
dist.barrier(),同步点,等待组内所有进程到达。 - All-Reduce示例:
- 各rank创建不同张量(例如,
tensor = [0,1,2,3] + rank)。 - 调用
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)。 - 结果:所有rank上的
tensor都被更新为所有原始张量的总和。
- 各rank创建不同张量(例如,
- Reduce-Scatter示例:
- 输入张量维度为
world_size * num_elements。 - 输出张量为标量(或
num_elements)。 - 调用
dist.reduce_scatter(output_tensor, input_list, op=dist.ReduceOp.SUM)(注意:PyTorch API中reduce_scatter_tensor更常见,此处根据描述推断,讲座中可能简化或使用了特定形式的input_list)。 - 结果:输入张量的各部分在对应rank上进行reduce,第i个分量的reduce结果存储在rank i的输出张量中。
- 输入张量维度为
- All-Gather示例:
- 输入为各rank上的部分数据(如reduce-scatter的输出)。
- 输出列表用于存放所有rank收集到的数据。
- 调用
dist.all_gather(output_list, input_tensor)。 - 结果:
output_list中包含了所有rank的input_tensor。
- 验证:
reduce_scatter+all_gather等价于all_reduce。 - 清理:
dist.destroy_process_group()。
- 初始化:
基准测试集体操作 (Single Node)
讲者演示了如何在单节点(4个GPU)上对all_reduce和reduce_scatter进行基准测试。
- 通用测试流程:
- 创建张量。
- 预热 (Warm-up):运行一次操作以加载CUDA内核等。
- 同步 (
dist.barrier()和torch.cuda.synchronize())。 - 记录开始时间。
- 执行待测集体操作。
- 同步 (
torch.cuda.synchronize())。 - 记录结束时间。
- 计算耗时和带宽。
- All-Reduce 带宽计算:
num_elements = 100_000_000,world_size = 4,element_size = 4bytes (float32).bytes_sent_received_per_rank = num_elements * element_size * 2- 乘以2的原因:数据需要发送到某处进行聚合(reduce),然后结果需要发送回所有ranks(broadcast/all-gather部分)。
- 带宽计算基于实际墙钟耗时:
bandwidth = bytes_sent_received_per_rank / measured_wall_clock_duration。 - 讲者提及
world_size * measured_wall_clock_duration作为一种衡量总计算资源消耗的方式,但这不直接用于计算单个操作的有效带宽。 - 实测带宽:约 277 GB/s。这低于H100理论上的900 GB/s,讲者指出实际性能受多种因素影响(张量大小、设备数等)。
- Reduce-Scatter 带宽计算:
- 输入张量大小为
world_size * num_elements(在每个rank上,此张量将被视为待reduce和scatter的部分)。 bytes_sent_or_received_per_rank_effectively = num_elements * element_size(每个rank最终只负责一部分结果的发送或接收,不像all-reduce那样每个rank都得到完整聚合结果并发送完整数据)。- 此处没有乘以2,因为数据仅单向汇聚并分散到目标ranks。
- 带宽计算基于实际墙钟耗时:
bandwidth = bytes_sent_or_received_per_rank_effectively / measured_wall_clock_duration。 - 实测带宽:约 70 GB/s。讲者推测
all_reduce可能有更多优化(如Nvidia硬件的SHARP加速,可能在网络中进行部分计算),导致其带宽表现相对更好,但具体原因复杂。
- 输入张量大小为
分布式训练策略
讲者介绍了在深度MLP上实现三种基本并行策略的简化版本,认为MLP的计算瓶颈与Transformer类似(不考虑Attention)。
1. 数据并行 (Data Parallelism - DDP)
- 概念:模型在所有rank上复制,数据沿批次维度切分。
- 每个rank处理数据的一个分片(shard)。
- 实现步骤 (MLP示例):
- 数据准备:总批次大小
batch_size,每个rank的本地批次大小local_batch_size = batch_size / world_size。各rank根据自身rank号取数据子集。 - 模型初始化:每个rank独立创建相同的MLP模型和优化器。
- 训练循环:
- 前向传播:在本地数据分片上执行。
- 计算损失:基于本地输出。
- 反向传播:计算本地梯度。
- 梯度同步:
> "synchronizes the gradients across worker. So what you do is for each of the layers, you call it all_reduce where you're averaging. And the thing you're averaging isparam.grad." - 参数更新:使用同步后的梯度更新优化器。
- 数据准备:总批次大小
- 关键点:
- 各rank上的损失值不同(因为数据不同)。
- 经过
all_reduce后,所有rank上的梯度相同,因此参数更新也相同,模型参数保持一致。 all_reduce操作本身是一个同步点,确保各rank在同一步骤。
2. 张量并行 (Tensor Parallelism - TP)
- 概念:数据在所有rank上复制(或相同),模型参数沿隐层维度切分。
- 每个rank持有模型每一层的一部分参数。
- 实现步骤 (MLP前向传播示例):
- 参数切分:隐层维度
num_dim,每个rank的本地隐层维度local_num_dim = num_dim / world_size。参数矩阵变为num_dim x local_num_dim(或local_num_dim x num_dim,取决于切分方式,此处示例为列切分)。 - 逐层计算与通信:
- 本地计算:输入激活
X(batch_size x num_dim) 与本地参数分片W_local(num_dim x local_num_dim) 相乘,得到部分激活X_partial(batch_size x local_num_dim)。 - 激活通信 (All-Gather):
- 每个rank上的
X_partial不同。 - 分配空间以收集所有rank的
X_partial。 - 执行
all_gather操作,将所有rank的X_partial收集起来。 - 拼接收集到的部分激活,得到完整的层输出激活
X_full(batch_size x num_dim)。
- 每个rank上的
- 将
X_full作为下一层的输入,重复此过程。
- 本地计算:输入激活
- 参数切分:隐层维度
- 关键点:
- 需要在层与层之间大量通信激活值,因此对GPU间的互联带宽要求很高。
- 讲座中的示例仅实现了前向传播,未实现反向传播。
3. 流水线并行 (Pipeline Parallelism - PP)
- 概念:模型按层切分,不同(连续的)层分配给不同rank。数据(或微批次)在ranks间顺序流动。
- 实现步骤 (MLP前向传播示例):
- 层分配:例如4层网络,2个ranks,则每个rank负责2层。
- 参数初始化:每个rank只初始化自己负责的那些层的参数。
- 微批次 (Micro-batching):将一个大批次数据切分成多个小微批次,以减少流水线气泡(pipeline bubbles)。
- 逐微批次处理与通信:
- 对于每个微批次:
- 当前rank(非首个rank)通过点对点通信(
dist.recv)从前一个rank接收激活。 - 在接收到的激活上执行分配给本rank的那些层的计算。
- 将计算结果通过点对点通信(
dist.send)发送给下一个rank(非末尾rank)。
- 当前rank(非首个rank)通过点对点通信(
- 对于每个微批次:
- 关键点:
- 讲座展示的是非常朴素(naive)的实现。
- 缺失的优化:
- 通信与计算的重叠(例如,使用异步的
isend和irecv)。 - 复杂的前向和后向步骤调度以进一步优化流水线。
- 通信与计算的重叠(例如,使用异步的
- 最后一个rank拥有完整前向传播的结果。反向传播则需要将梯度反向传递。
实现的局限性与进一步的复杂性
讲者强调,讲座中展示的MLP实现是“bare bones”(最基本的)。
- 实际模型:需要应用于Transformer等更复杂的模型。
- 通信计算重叠:未在本讲座代码中仔细处理,但对性能至关重要。
- 代码复杂性:实际的并行库(如Megatron-LM, PyTorch FSDP)包含大量簿记(bookkeeping)代码,以处理任意模型架构、参数定位等。
JAX:一种更高层次的抽象
讲者简要提及了JAX生态系统作为PyTorch之外的另一种选择。
- JAX理念:用户定义模型和分片策略(sharding strategy),JAX编译器负责其余的底层并行化细节。
- 示例:Lantana(基于JAX的工具包)可以用很少代码(如10行)实现FSDP或张量并行,通过声明式地指定沿哪些维度(模型维度、嵌入维度、序列维度等)切分以及如何映射到TPU设备。
- 对比:本课程使用PyTorch是为了让学生理解底层机制,但在实际应用中,通常不建议从零开始实现所有这些并行逻辑。
总结与展望
- 并行化多样性:通过切分数据(批次维度)、模型宽度(隐层维度)、模型深度(层维度)或上下文长度维度来实现。
- 重计算 vs. 存储/传输:这是一个反复出现的主题。可以在本地重计算、从内存加载(有传输成本),或从其他GPU内存加载(传输成本更高)。重计算有时更优。
- 硬件发展与并行需求:尽管硬件不断进步(L1, HBM容量增加),但模型规模的增长速度更快,使得并行化技术将长期保持其重要性。
> "This hierarchical structure, ever since computer systems was a thing, has always been with us and it will always be there."
Q&A 环节要点
- Q&A: 数据并行与Batch Norm:讲者表示不确定如何在数据并行中正确处理依赖数据的Batch Norm(LLM通常使用Layer Norm,不受此影响)。只要参数初始化随机数种子相同,Layer Norm表现一致。
- Q&A: PyTorch的高级并行抽象:PyTorch有FSDP库,但对于更定制化的分片策略,JAX目前可能提供更声明式的接口。不同生态(JAX/TPU vs. DeepSpeed/GPU)有不同优化思路。
- Q&A: 激活检查点 (Activation Checkpointing):存在API允许指定哪些部分需要重计算,通常在大的矩阵乘法之后。
- Q&A: 持续预训练 (Continued Training):讲座讨论的技术完全适用于从已训练的检查点继续训练模型。
- Q&A: 硬件限制与专用硬件:GPU尺寸和密度有物理极限(功耗、散热、带宽)。专用硬件(如Grok, Cerebras)通过更多片上内存等方式针对深度学习的数据流特性进行优化,减少了传统GPU为通用计算设计的冗余。
- Q&A: 计算图存储与执行:计算图更多是概念上的。CPU代码驱动整个过程,当调用PyTorch函数在GPU上执行时,会启动GPU内核。集体操作的参数和调度由CPU发起,具体数据移动由NCCL等库在GPU上执行。