CS336 Lecture Notes 4

4461 字
22 分钟
CS336 Lecture Notes 4
Warning

本文为本人学习相关开源课程过程中,整理的个人学习笔记及作业解答,核心目的仅用于记录个人学习轨迹、巩固所学知识、梳理学习思路,全程为个人自主学习使用,不具备任何商业用途,也不构成任何形式的课程辅导或标准答案参考。

需特别说明的是,由于本人学习进度及知识储备有限,笔记内容及作业解答中可能存在大量纰漏、思路偏差甚至错误,仅代表本人当时的学习理解,不具备权威性和准确性。

在此郑重提醒:请勿将本文中的任何作业解答复制粘贴,作为自身所修课程的提交答案。任何因抄袭本文内容导致的课程成绩问题、学术诚信问题,均由抄袭者自行承担全部责任,本人不承担任何相关连带责任。

同时,本文所分享的内容均基于开源课程的公开内容整理,尊重原课程创作者的知识产权,若涉及相关内容的版权问题,请及时联系本人,本人将第一时间进行调整或删除。

感谢各位读者的理解与支持,也欢迎大家针对笔记及解答中的问题提出宝贵建议,共同交流学习、共同进步。

Important

Parallelism (Part I)#

概述#

本讲主题是多GPU并行。与上一讲关注单GPU内部优化(通过融合/分块减少内存访问)不同,本讲关注如何通过复制/分片减少跨GPU/节点通信。

为什么需要多GPU?

  1. 参数(优化器状态 + 梯度 + 激活)无法放入单个GPU
  2. 使用更多GPU(更多FLOPs)加速训练

通信层次结构(从快到慢):

  • 单GPU内:L1缓存/共享内存(最快)
  • 单GPU内:HBM
  • 单节点多GPU:NVLink/NVSwitch
  • 多节点多GPU:Infiniband/Ethernet(最慢)

Part 1: 分布式通信的基础构建块#

集体操作(Collective Operations)#

集体操作是分布式编程的概念原语,源自1980年代的并行编程文献。“集体”意味着指定跨多个设备的一般通信模式。

基础概念:

  • Rank:特定设备/GPU的标识(如0, 1, 2, 3)
  • World size:设备总数(如4)
Note

可以在网站 GPU Collective Communication Visualizer 中进行交互体验。

基础操作: Broadcast,Scatter,Gather,Reduce

Broadcast 操作:从 root 复制到所有 rank
Broadcast 操作:从 root 复制到所有 rank
Scatter 操作:从 root 分发张量到各 rank
Scatter 操作:从 root 分发张量到各 rank
Gather 操作:从各 rank 收集到 root
Gather 操作:从各 rank 收集到 root
Reduce 操作:从各 rank 归约到 root
Reduce 操作:从各 rank 归约到 root

核心操作::AllGather,ReduceScatter,AllReduce,AlltoAll

AllGather 操作:Gather 到所有 rank
AllGather 操作:Gather 到所有 rank

Note

在参数分片策略中,单个 GPU 显存不足以放下完整模型,因此每个 GPU 只存 1/N 的参数分片。前向计算需要完整参数参与运算,AllGather 负责临时拼凑全量参数并广播给所有 GPU,计算完毕后立即丢弃全量副本,实现“用时组装,用完释放”。

ReduceScatter 操作:对每个维度 reduce 后 scatter
ReduceScatter 操作:对每个维度 reduce 后 scatter

Note

反向传播时,各 GPU 基于自己的数据分片算出局部梯度。ReduceScatter 先对所有 GPU 的梯度进行全局求和,随后将求和结果切分并散列回各 GPU。每个 GPU 只保留与自己参数分片对应的梯度子集,避免全量梯度常驻显存。

AllReduce 操作:ReduceScatter + AllGather
AllReduce 操作:ReduceScatter + AllGather

Note

经典数据并行(DDP)的标准操作。各 GPU 算完局部梯度后,全局求和/平均,并将完整的全局梯度副本复制到每一个 GPU。所有 GPU 用相同的完整梯度更新自己持有的完整参数副本。

AlltoAll 操作:每个 rank 向每个其他 rank 发送张量
AlltoAll 操作:每个 rank 向每个其他 rank 发送张量

Note

在 MoE 架构中,路由网络决定每个 Token 该去哪个 Expert。由于 Expert 分布在不同 GPU 上,AlltoAll 负责执行动态路由与数据重排,确保每个 GPU 能收到属于自己的 Token 块,是 MoE 通信的核心瓶颈点。

硬件基础#

家庭环境:

  • 同节点GPU通过PCIe总线通信(v7.0, 16通道 → 242 GB/s)[wiki]
  • 不同节点通过以太网通信(~200 MB/s)

现代数据中心:

典型配置:

  • 每节点 8 GPU,通过 NVLink 连接到 NVSwitch(B200 的 NVLink 5.0 达 1.8 TB/s;HBM 为8 TB/s)
  • 每 pod 256 节点,通过 Infiniband 连接(~0.05 TB/s)
  • N 个 pod 组成集群/数据中心,通过以太网连接(通过 PCIe -> CPU)

绕过CPU:

  • 以太网需要经过 CPU(复制数据到内核 socket 缓冲区,构建 TCP 包,复制到 NIC 环形缓冲区)
  • RDMA(远程直接内存访问):允许一个 GPU 直接读写另一个 GPU 的内存,无需 CPU 参与
  • Infiniband 支持 RDMA,但标准以太网不支持

新发展:

  • GB200/GB300 NVL72:每 tray 8 GPU,每 rack 9 tray -> 一个 NVLink 域内 72 GPU
  • RoCE(RDMA over Converged Ethernet):以太网绕过 CPU,类似 Infiniband 但更便宜/弱,Meta 使用 RoCE

NCCL(NVIDIA Collective Communication Library)#

NCCL将集体操作翻译为 GPU 间发送的低级数据包:[talk]

  • 检测硬件拓扑(节点数、交换机、NVLink/PCIe)
  • 优化 GPU 间路径
  • 启动 GPU Kernels 发送/接收数据

PyTorch Distributed#

PyTorch 分布式库(torch.distributed[documentation]

  • 为集合通信操作(例如 all_gather_into_tensor)提供简洁易用的接口
  • 支持适配不同硬件的多种后端:gloo(适配 CPU)、nccl(适配 GPU)
  • 同时支持高阶分布式算法(例如 FullyShardedDataParallel

代码详见 [集合通信操作]

带宽基准测试#

参考:

代码详见 [带宽基准测试]

All-reduce有效带宽计算:

sent_bytes = size_bytes * 2 * (world_size - 1) # 2x 因为发送 + 接收
total_duration = world_size * duration
bandwidth = sent_bytes / total_duration

关键发现:All-reduce = Reduce-scatter + All-gather,传输 2 倍数据花费 2 倍时间,带宽相近。

Note

这里的两张图显示出,AllReduce 和 ReduceScatter 测出来的带宽是差不多的,但是 ReduceScatter 时间会长一点,这和理论一致吗?

Part 2: 分布式训练策略#

数据并行(Data Parallelism)#

分片策略: 每个 rank 获取数据的一个切片。

随机生成一个 shape 为 (batch_size, num_dim) 的张量作为数据输入。

def generate_sample_data():
batch_size = 128
num_dim = 1024
data = torch.randn(batch_size, num_dim)
return data
data = generate_sample_data()

每个 rank 获取自己的一份输入。

# 原始数据批次 → 分片到各GPU
# --- B0 --- → rank 0
# --- B1 --- → rank 1
# --- B2 --- → rank 2
# --- B3 --- → rank 3
local_batch_size = int_divide(batch_size, world_size)
start_index = rank * local_batch_size
end_index = start_index + local_batch_size
data = data[start_index:end_index].to(cuda_if_available(rank))
Note

int_divide 在整数除法 a // b 之前判断 a % b == 0,即能否整除。如果不能整除,代表会有数据没有被分配或者数据分配不均匀。

def int_divide(a: int, b: int):
"""Return a / b and throw an error if there's a remainder."""
assert a % b == 0
return a // b

参数获取,优化器设置以及训练循环:

# 创建 MLP parameters, params[0], ..., params[num_layers - 1], 每个 rank 持有完整参数
params = [get_init_params(num_dim, num_dim, rank) for layer in range(num_layers)]
optimizer = torch.optim.AdamW(params, lr=1e-3) # 每个 rank 都有自己的 optimizer state
for step in range(num_steps):
# 前向传播(各rank独立计算)
x = data
for param in params:
x = x @ param
x = F.gelu(x)
loss = x.square().mean()
# 反向传播
loss.backward()
# 关键: 同步梯度(DDP与标准训练的唯一区别)
for param in params:
dist.all_reduce(tensor=param.grad, op=dist.ReduceOp.AVG)
# 更新参数
optimizer.step()
print(f"[data_parallelism] Rank {rank}: step = {step}, loss = {loss.item()}, params = {[summarize_tensor(params[layer]) for layer in range(num_layers)]}", flush=True)
Note

get_init_params 就是获取一个 shape 为 (num_inputs, num_outputs)nn.Parameter

def get_init_params(num_inputs: int, num_outputs: int, rank: int) -> nn.Parameter:
"""Create parameters and put them on the `rank`-th GPU."""
torch.random.manual_seed(0) # For reproducibility
return nn.Parameter(torch.randn(num_inputs, num_outputs, device=cuda_if_available(rank)) / math.sqrt(num_outputs))

summarize_tensor 显示 tensor 的大致信息。

def summarize_tensor(tensor: tensor) -> str:
return "x".join(map(str, tensor.shape)) + "[" + str(round(tensor.view(-1)[0].item(), 4)) + "...]"

特点:

  • 各 rank 的 loss 不同(基于本地数据计算)
  • 梯度通过 all-reduce 保持一致
  • 参数在各 rank 保持相同

局限性: 每个 rank 需持有全部参数,内存压力大

张量并行(Tensor Parallelism)#

分片策略: 每个 rank 获取每层的部分参数

每个 rank 都收到全部的输入 databatch_size x num_dim

data = data.to(cuda_if_available(rank)) # All ranks get the data (batch_size x num_dim)

不包含 backward 的训练循环:

# 每层权重矩阵 W 被按列切分:
# | | | |
# W0 W1 W2 W3 (各 rank 持有 1/world_size 的参数)
# | | | |
local_num_dim = int_divide(num_dim, world_size) # Shard `num_dim`
params = [get_init_params(num_dim, local_num_dim, rank) for layer in range(num_layers)]
# 前向传播
x = data # 所有 rank 获得完整数据
for layer in range(num_layers):
x = x @ params[layer] # 仅在参数切片上计算
x = F.gelu(x)
# 为激活分配内存 (world_size x batch_size x local_num_dim)
activations = [torch.empty(batch_size, local_num_dim, device=cuda_if_available(rank)) for _ in range(world_size)]
# All-gather 收集所有 rank 的激活
dist.all_gather(tensor_list=activations, tensor=x)
# 拼接得到完整激活 batch_size x num_dim
x = torch.cat(activations, dim=1)
print(f"[tensor_parallelism] Rank {rank}: forward pass produced activations {summarize_tensor(x)}", flush=True)
# backward ... ?
Important

原生 dist.all_gather 是不可导的,如果直接放在计算图中调用 loss.backward(),PyTorch 会因找不到反向规则而报错,或导致梯度断裂。

要让 loss.backward() 正常工作,唯一标准做法是用 torch.autograd.Function 包装 dist.all_gather。包装后,Autograd 会自动处理梯度流,你无需在backward()后手动干预梯度(除非要做梯度裁剪/缩放等自定义操作)。

可以查看 torch.distributed.nn.functional.all_gathertorch.distributed.nn.functional._AllGather 的实现。

特点:

  • 每层需要 all-gather 传递激活
  • 需要非常快的互连(如 NVLink)
  • 适合大模型单节点内并行

流水线并行(Pipeline Parallelism)#

分片策略: 每个 rank 获取部分层

data = data.to(cuda_if_available(rank))
# 按层进行切分
local_num_layers = int_divide(num_layers, world_size)
# 每个 rank 获取一部分层
local_params = [get_init_params(num_dim, num_dim, rank) for layer in range(local_num_layers)]
# 前向传播
# 将批次切分为微批次以减少气泡(bubble)
micro_batch_size = int_divide(batch_size, num_micro_batches)
if rank == 0:
# 训练数据
micro_batches = data.chunk(chunks=num_micro_batches, dim=0)
else:
# 为激活值分配内存
micro_batches = [torch.empty(micro_batch_size, num_dim, device=cuda_if_available(rank)) for _ in range(num_micro_batches)]
for x in micro_batches:
# 从前一 rank 接收激活
if rank - 1 >= 0:
dist.recv(tensor=x, src=rank - 1)
# 计算分配给本 rank 的层
for param in local_params:
x = x @ param
x = F.gelu(x)
# 发送到下一 rank
if rank + 1 < world_size:
print(f"[pipeline_parallelism] Rank {rank}: sending {summarize_tensor(x)} to rank {rank + 1}", flush=True)
dist.send(tensor=x, dst=rank + 1)
# backward ... ?

特点:

  • 可在慢速互连下工作
  • 需要处理流水线气泡(通过微批次)
  • 点对点通信(send/recv)

Parallelism (Part II)#

概述#

本讲深入探讨LLM分布式训练的系统性复杂问题:

  • 不同并行范式及其组合使用
  • 大规模训练的实际运行方式

目标:

  • 线性内存扩展(最大模型参数随GPU数扩展)
  • 线性计算扩展(模型FLOPs随GPU数线性增长)
  • 简单的集体通信原语

Part 1: LLM网络基础#

TPU vs GPU 网络设计差异#

特性TPU 网络GPU 网络
拓扑Toroidal Mesh(环形网格)All-to-all(最多256)/Tree
优势更便宜、可快速实现(适合张量并行)更适合非结构化通信(专家并行)

为什么不全连接?

  • Domain sizes限制:全连接成本过高
  • TPU8i/t 转向树型拓扑(可能为MoE优化)
  • TPU8t 使用交换网络(‘Virgo’)

Part 2: LLM并行化原语#

朴素数据并行(Naïve Data Parallelism)#

核心思想: 将B大小的批次切分到M台机器,交换梯度同步

𝜃_{t+1} = 𝜃_t - 𝜂 * Σ ∇f(x_i) # SGD公式

性能分析:

  • 计算扩展:每个GPU处理 B/M 样本
  • 通信开销:每批次传输 2×#params
  • 内存扩展:无!每个GPU需要完整参数

内存问题深度分析#

内存实际需求(每参数):

项目字节数
FP16/BF16 模型参数2 bytes
FP16/BF16 梯度2 bytes
FP32 主权重(SGD累加)4 bytes
FP32/BF16 Adam一阶矩4 (或2) bytes
FP32/BF16 Adam二阶矩4 (或2) bytes
总计16 bytes/param

需要5份权重副本,内存开销巨大!

ZeRO:解决DP内存开销#

核心思想: 分片昂贵的部分(状态),利用reduce-scatter等价性

ZeRO Stage 1(优化器状态分片):

工作流程:
Step 1: 各GPU计算完整梯度
Step 2: ReduceScatter梯度 → 通信量 #params
Step 3: 各机器更新其负责的参数切片
Step 4: AllGather参数 → 通信量 #params
对比朴素DDPZeRO Stage 1
通信原语1次 all-reducereduce-scatter + all-gather
通信量2×#params2×#params
内存(4+K)×#params(4+K/N_gpu)×#params

结论:ZeRO Stage 1 是免费的内存优化!

ZeRO Stage 2(梯度分片):

进一步分片梯度:

  • 梯度也跨机器分片
  • 使用增量计算:计算一层梯度后立即reduce发送到正确worker
  • 不再需要完整的梯度向量

ZeRO Stage 3 / FSDP(参数也分片):

工作流程(简化版):
- 分片所有内容(包括参数)
- 按需请求/发送参数
- 通信成本:2次 all-gather + 1次 reduce-scatter = 3×#params

关键技巧:

  1. 增量计算/通信:参数/梯度请求后立即释放
  2. 通信计算重叠:all-gather与前向传播同时进行
ZeRO级别通信量内存节省
Stage 12×#params(免费)优化器状态分片
Stage 22×#params(几乎免费)+梯度分片
Stage 33×#params(1.5倍)+参数分片

ZeRO 实践:模型容量估算#

纯BF16训练(含Kahan累加):12 bytes/param

8×A100 80GB 的最大模型容量:

配置最大参数量公式
Baseline6.66B12 bytes/param
ZeRO-116B5 bytes/param
ZeRO-224.62B2 + (10/8) bytes/param
ZeRO-353.33B12/8 bytes/param

数据并行局限性#

计算扩展问题:

  • #machines < batch_size(接近此限制时通信开销高)
  • 大批次收益递减

内存扩展问题:

  • ZeRO-1/2 不能扩展内存
  • ZeRO-3 不减少激活内存

需要更好的模型分片方案……

模型并行详解#

流水线并行(Pipeline Parallelism)#

问题:层级并行利用率极低

简单层级并行:
- n个GPU,每个GPU仅 1/n 时间活跃
- 大部分时间空闲,等待反向传播

解决方案:微批次流水线

处理4个微批次:
- 发送第一个微批次后立即计算第二个
- 气泡时间比例 = (n_stages - 1) / n_micro
- 需要大批次来隐藏气泡!

为什么使用流水线并行?

  1. 节省内存(相比DDP)
  2. 通信特性好(相比FSDP)—— 仅激活 (b×s×h),点对点

适用场景: 慢速网络链接(跨节点)

气泡优化策略:

策略特点
标准流水线气泡比例 (n-1)/n_micro
Zero Bubble将反向传播拆分为两部分:激活反向传播(z,x) + 权重梯度计算(W)
复杂流水线模式提高利用率但牺牲带宽

张量并行(Tensor Parallelism)#

核心观察:矩阵乘法可分解

矩阵乘法分解:
C = AB = (A1 + A2)(B1 + B2) = A1B1 + A1B2 + A2B1 + A2B2
列并行 + 行并行:
- 前向传播:f=identity, g=all-reduce
- 反向传播:f=all-reduce, g=identity

Transformer中的张量并行:

操作分片方式
QKV、Up-projection列并行(Columnwise)
Attention output、Down-projection行并行(Rowwise)
Norms、Routers复制(Replicated)

何时使用张量并行?

  • GPU节点内(最多8 GPU)—— 高速互连

张量并行 vs 流水线并行:

特性张量并行流水线并行
气泡无气泡(网络足够快)有气泡,依赖批次大小
复杂度低——简单wrapper高——需要调度
批次要求无特别要求需要大批次
通信量大——每层all-reduce小——点对点 bsh

激活内存问题#

激活内存每层需求(存储所有):

激活内存公式:
- 二次注意力项(含dropout):5as/h
- 可通过Flash Attention重计算消除
剩余项(10sbh):
- LayerNorm: 4sbh
- Dropout: 2sbh
- Attention/MLP输入: 4sbh

序列并行(Sequence Parallelism)#

核心观察:10sbh项都是序列上的逐点操作

解决方案:
- 沿序列轴切分LayerNorm/Dropout
- 前向:'g' = all-gather, 'ḡ' = reduce-scatter
- 反向:两者互换

效果: 激活内存也随机器数线性扩展!

专家并行(Expert Parallelism)#

核心思想: 不切分矩阵乘法,切分专家并路由激活

MoE结构:
- MLP使用专家并行
- Attention使用张量并行
复杂问题:
- EP和DP共享副本(EP < DP)
- DP和TP可能交互降低利用率

Attention vs MLP并行解耦:

组件推荐并行方式
Attention高TP(无法用EP)
MLP低TP + EP(EP优于TP)

Megatron灵活策略:

  • Attention: TP/CP/DP
  • MLP: ETP/EP/EDP

其他并行策略#

Context Parallel / Ring Attention:

  • 长序列场景下,将激活跨GPU切分

LLM并行策略汇总表#

方法通信/同步参数内存/rank激活/KV内存/rank主要带宽成本扩展全局批次?易用性
DDP/ZeRO-1梯度all-reduce无扩展NoneO(params)梯度Yes, DP线性Very
FSDP/ZeRO-3梯度all-reduce(可重叠)~1/DPNoneO(params)参数Yes, DP线性Moderate
Pipeline激活层间传递、气泡~1/PP依赖pipeline缓冲激活通信No,需微批次Hard
Tensor阻塞激活通信~1/TP~1/TP(含SP)每块激活collectiveNoHard
Sequence/Context序列分片交换None~1/SP或1/CP激活/KV通信NoHard
Expert (MoE)Token dispatch all-to-all~1/EP专家权重NoneToken路由all-to-allNo,需足够tokenHard

Part 3: 大规模LLM训练实践#

3D/4D并行组合规则#

简单经验法则:

1. 直到模型适配内存:
- Tensor/Expert并行 → GPU数/机器(最多8)
- Pipeline并行 → 跨机器
- (或用ZeRO-3,取决于带宽)
2. 直到GPU耗尽:
- 其余用数据并行扩展
如果批次小:梯度累加换取更大批次

Megatron推荐配置示例:

基础:TP + PP + DP
高级:TP + PP + CP + EP(MoE)

Narayanan 2021扩展策略#

配置趋势:
- Tensor并行先扩展到8,然后封顶在8
- Pipeline并行扩展直到模型适配
- 数据并行随规模逐渐减少,最大模型DP=6

关键发现:

  • 64机器配置最优:8×8(TP=8)
  • 精心设计的3D并行可获得线性增益

激活重计算的收益:

  • 激活重计算 → 更大批次 → 更高吞吐

实际LLM训练案例#

模型DPTP/SPEPPPCP
DeepSeek??(Zero1)1816??
DeepSeek V3??(Zero1)16416??
Yi??(Zero1)>01>0??
Llama3 405B12880161
Gemma 27688000
Mixtral 8x22B24841
Nemotron 3 120B??264??64
Qwen 3??23281

模式总结:

  • TP通常 ≤ 8
  • EP可以很大(但实现难!)
  • 长上下文阶段使用大CP

Llama 3 405B 训练细节#

阶段配置:
- Stage 1:小批次训练
- Stage 2:预训练
- Stage 3:长上下文
挑战:此规模下GPU故障频繁!

全讲总结#

  1. 超越单点需要多GPU、多节点并行
  2. 没有单一解决方案——需要组合所有3种方法
  3. 组合并行有简单、可解释的经验法则

核心权衡:

  • 通信带宽 vs 利用率
  • 内存 vs 计算
  • 简单性 vs 效率

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
CS336 Lecture Notes 4
https://llm-tech.com.cn/posts/cs336-lec-notes-4/
作者
Ming
发布于
2026-05-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Ming
你是来找 Ming 学习的吗
🎉 欢迎来到 Ming 的博客
这里是我的个人博客,分享 AI Infra、LLM 等技术内容。欢迎关注交流!
分类
标签
站点统计
文章
19
分类
6
标签
12
总字数
69,591
运行时长
0
最后活动
0 天前

目录