详细摘要 摘要

生成: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和节点的通信,涉及模型和参数的复制与分片,以及优化器状态的管理。

计算与通信的层级结构

讲者概述了从快小到慢大的存储与通信层级,强调了最小化数据传输的核心概念在不同层级具有相似性,但具体机制不同。

  • 层级(由快小到慢大)
    1. 单节点,单GPU内
      • L1缓存:极快但极小。
      • HBM(高带宽内存):较L1大,速度稍慢。
    2. 同节点,GPU之间
      • NVLink:Nvidia GPU间的高速互联。
    3. 跨节点,GPU之间
      • NVSwitch:连接多个NVLink,实现更大规模的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)。
    • ScatterGather的逆操作。
    • 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示例:
      1. 各rank创建不同张量(例如,tensor = [0,1,2,3] + rank)。
      2. 调用dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
      3. 结果:所有rank上的tensor都被更新为所有原始张量的总和。
    • Reduce-Scatter示例:
      1. 输入张量维度为world_size * num_elements
      2. 输出张量为标量(或num_elements)。
      3. 调用dist.reduce_scatter(output_tensor, input_list, op=dist.ReduceOp.SUM)(注意:PyTorch API中reduce_scatter_tensor更常见,此处根据描述推断,讲座中可能简化或使用了特定形式的input_list)。
      4. 结果:输入张量的各部分在对应rank上进行reduce,第i个分量的reduce结果存储在rank i的输出张量中。
    • All-Gather示例:
      1. 输入为各rank上的部分数据(如reduce-scatter的输出)。
      2. 输出列表用于存放所有rank收集到的数据。
      3. 调用dist.all_gather(output_list, input_tensor)
      4. 结果:output_list中包含了所有rank的input_tensor
    • 验证: reduce_scatter + all_gather 等价于 all_reduce
    • 清理: dist.destroy_process_group()

基准测试集体操作 (Single Node)

讲者演示了如何在单节点(4个GPU)上对all_reducereduce_scatter进行基准测试。

  • 通用测试流程
    1. 创建张量。
    2. 预热 (Warm-up):运行一次操作以加载CUDA内核等。
    3. 同步 (dist.barrier()torch.cuda.synchronize())。
    4. 记录开始时间。
    5. 执行待测集体操作。
    6. 同步 (torch.cuda.synchronize())。
    7. 记录结束时间。
    8. 计算耗时和带宽。
  • All-Reduce 带宽计算
    • num_elements = 100_000_000, world_size = 4, element_size = 4 bytes (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示例)
    1. 数据准备:总批次大小 batch_size,每个rank的本地批次大小 local_batch_size = batch_size / world_size。各rank根据自身rank号取数据子集。
    2. 模型初始化:每个rank独立创建相同的MLP模型和优化器。
    3. 训练循环
      • 前向传播:在本地数据分片上执行。
      • 计算损失:基于本地输出。
      • 反向传播:计算本地梯度。
      • 梯度同步:
        > "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 is param.grad."
      • 参数更新:使用同步后的梯度更新优化器。
  • 关键点
    • 各rank上的损失值不同(因为数据不同)。
    • 经过all_reduce后,所有rank上的梯度相同,因此参数更新也相同,模型参数保持一致。
    • all_reduce操作本身是一个同步点,确保各rank在同一步骤。

2. 张量并行 (Tensor Parallelism - TP)

  • 概念:数据在所有rank上复制(或相同),模型参数沿隐层维度切分。
    • 每个rank持有模型每一层的一部分参数。
  • 实现步骤 (MLP前向传播示例)
    1. 参数切分:隐层维度 num_dim,每个rank的本地隐层维度 local_num_dim = num_dim / world_size。参数矩阵变为 num_dim x local_num_dim(或 local_num_dim x num_dim,取决于切分方式,此处示例为列切分)。
    2. 逐层计算与通信
      • 本地计算:输入激活 X (batch_size x num_dim) 与本地参数分片 W_local (num_dim x local_num_dim) 相乘,得到部分激活 X_partial (batch_size x local_num_dim)。
      • 激活通信 (All-Gather)
        1. 每个rank上的 X_partial 不同。
        2. 分配空间以收集所有rank的 X_partial
        3. 执行 all_gather 操作,将所有rank的 X_partial 收集起来。
        4. 拼接收集到的部分激活,得到完整的层输出激活 X_full (batch_size x num_dim)。
      • X_full 作为下一层的输入,重复此过程。
  • 关键点
    • 需要在层与层之间大量通信激活值,因此对GPU间的互联带宽要求很高。
    • 讲座中的示例仅实现了前向传播,未实现反向传播。

3. 流水线并行 (Pipeline Parallelism - PP)

  • 概念:模型按层切分,不同(连续的)层分配给不同rank。数据(或微批次)在ranks间顺序流动。
  • 实现步骤 (MLP前向传播示例)
    1. 层分配:例如4层网络,2个ranks,则每个rank负责2层。
    2. 参数初始化:每个rank只初始化自己负责的那些层的参数。
    3. 微批次 (Micro-batching):将一个大批次数据切分成多个小微批次,以减少流水线气泡(pipeline bubbles)。
    4. 逐微批次处理与通信
      • 对于每个微批次:
        • 当前rank(非首个rank)通过点对点通信(dist.recv)从前一个rank接收激活。
        • 在接收到的激活上执行分配给本rank的那些层的计算。
        • 将计算结果通过点对点通信(dist.send)发送给下一个rank(非末尾rank)。
  • 关键点
    • 讲座展示的是非常朴素(naive)的实现。
    • 缺失的优化
      • 通信与计算的重叠(例如,使用异步的isendirecv)。
      • 复杂的前向和后向步骤调度以进一步优化流水线。
    • 最后一个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上执行。