CS336 Lecture Notes 4
本文为本人学习相关开源课程过程中,整理的个人学习笔记及作业解答,核心目的仅用于记录个人学习轨迹、巩固所学知识、梳理学习思路,全程为个人自主学习使用,不具备任何商业用途,也不构成任何形式的课程辅导或标准答案参考。
需特别说明的是,由于本人学习进度及知识储备有限,笔记内容及作业解答中可能存在大量纰漏、思路偏差甚至错误,仅代表本人当时的学习理解,不具备权威性和准确性。
在此郑重提醒:请勿将本文中的任何作业解答复制粘贴,作为自身所修课程的提交答案。任何因抄袭本文内容导致的课程成绩问题、学术诚信问题,均由抄袭者自行承担全部责任,本人不承担任何相关连带责任。
同时,本文所分享的内容均基于开源课程的公开内容整理,尊重原课程创作者的知识产权,若涉及相关内容的版权问题,请及时联系本人,本人将第一时间进行调整或删除。
感谢各位读者的理解与支持,也欢迎大家针对笔记及解答中的问题提出宝贵建议,共同交流学习、共同进步。
- 课程网站: https://cs336.stanford.edu/
- Lec07 资料: lecture_07.py
- Lec08 资料: lecture_08.pdf
Parallelism (Part I)
概述
本讲主题是多GPU并行。与上一讲关注单GPU内部优化(通过融合/分块减少内存访问)不同,本讲关注如何通过复制/分片减少跨GPU/节点通信。
为什么需要多GPU?
- 参数(优化器状态 + 梯度 + 激活)无法放入单个GPU
- 使用更多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)
可以在网站 GPU Collective Communication Visualizer 中进行交互体验。
基础操作: Broadcast,Scatter,Gather,Reduce




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

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

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

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

在 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 * durationbandwidth = sent_bytes / total_duration

关键发现:All-reduce = Reduce-scatter + All-gather,传输 2 倍数据花费 2 倍时间,带宽相近。
这里的两张图显示出,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 3local_batch_size = int_divide(batch_size, world_size)start_index = rank * local_batch_sizeend_index = start_index + local_batch_sizedata = data[start_index:end_index].to(cuda_if_available(rank))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)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 都收到全部的输入 data(batch_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 ... ?原生 dist.all_gather 是不可导的,如果直接放在计算图中调用 loss.backward(),PyTorch 会因找不到反向规则而报错,或导致梯度断裂。
要让 loss.backward() 正常工作,唯一标准做法是用 torch.autograd.Function 包装 dist.all_gather。包装后,Autograd 会自动处理梯度流,你无需在backward()后手动干预梯度(除非要做梯度裁剪/缩放等自定义操作)。
可以查看 torch.distributed.nn.functional.all_gather 和 torch.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梯度 → 通信量 #paramsStep 3: 各机器更新其负责的参数切片Step 4: AllGather参数 → 通信量 #params| 对比 | 朴素DDP | ZeRO Stage 1 |
|---|---|---|
| 通信原语 | 1次 all-reduce | reduce-scatter + all-gather |
| 通信量 | 2×#params | 2×#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关键技巧:
- 增量计算/通信:参数/梯度请求后立即释放
- 通信计算重叠:all-gather与前向传播同时进行
| ZeRO级别 | 通信量 | 内存节省 |
|---|---|---|
| Stage 1 | 2×#params(免费) | 优化器状态分片 |
| Stage 2 | 2×#params(几乎免费) | +梯度分片 |
| Stage 3 | 3×#params(1.5倍) | +参数分片 |
ZeRO 实践:模型容量估算
纯BF16训练(含Kahan累加):12 bytes/param
8×A100 80GB 的最大模型容量:
| 配置 | 最大参数量 | 公式 |
|---|---|---|
| Baseline | 6.66B | 12 bytes/param |
| ZeRO-1 | 16B | 5 bytes/param |
| ZeRO-2 | 24.62B | 2 + (10/8) bytes/param |
| ZeRO-3 | 53.33B | 12/8 bytes/param |
数据并行局限性
计算扩展问题:
- #machines < batch_size(接近此限制时通信开销高)
- 大批次收益递减
内存扩展问题:
- ZeRO-1/2 不能扩展内存
- ZeRO-3 不减少激活内存
需要更好的模型分片方案……
模型并行详解
流水线并行(Pipeline Parallelism)
问题:层级并行利用率极低
简单层级并行:- n个GPU,每个GPU仅 1/n 时间活跃- 大部分时间空闲,等待反向传播解决方案:微批次流水线
处理4个微批次:- 发送第一个微批次后立即计算第二个- 气泡时间比例 = (n_stages - 1) / n_micro- 需要大批次来隐藏气泡!为什么使用流水线并行?
- 节省内存(相比DDP)
- 通信特性好(相比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=identityTransformer中的张量并行:
| 操作 | 分片方式 |
|---|---|
| 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 | 无扩展 | None | O(params)梯度 | Yes, DP线性 | Very |
| FSDP/ZeRO-3 | 梯度all-reduce(可重叠) | ~1/DP | None | O(params)参数 | Yes, DP线性 | Moderate |
| Pipeline | 激活层间传递、气泡 | ~1/PP | 依赖pipeline缓冲 | 激活通信 | No,需微批次 | Hard |
| Tensor | 阻塞激活通信 | ~1/TP | ~1/TP(含SP) | 每块激活collective | No | Hard |
| Sequence/Context | 序列分片交换 | None | ~1/SP或1/CP | 激活/KV通信 | No | Hard |
| Expert (MoE) | Token dispatch all-to-all | ~1/EP专家权重 | None | Token路由all-to-all | No,需足够token | Hard |
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训练案例
| 模型 | DP | TP/SP | EP | PP | CP |
|---|---|---|---|---|---|
| DeepSeek | ??(Zero1) | 1 | 8 | 16 | ?? |
| DeepSeek V3 | ??(Zero1) | 1 | 64 | 16 | ?? |
| Yi | ??(Zero1) | >0 | 1 | >0 | ?? |
| Llama3 405B | 128 | 8 | 0 | 16 | 1 |
| Gemma 2 | 768 | 8 | 0 | 0 | 0 |
| Mixtral 8x22B | 2 | 4 | 8 | 4 | 1 |
| Nemotron 3 120B | ?? | 2 | 64 | ?? | 64 |
| Qwen 3 | ?? | 2 | 32 | 8 | 1 |
模式总结:
- TP通常 ≤ 8
- EP可以很大(但实现难!)
- 长上下文阶段使用大CP
Llama 3 405B 训练细节
阶段配置:- Stage 1:小批次训练- Stage 2:预训练- Stage 3:长上下文
挑战:此规模下GPU故障频繁!全讲总结
- 超越单点需要多GPU、多节点并行
- 没有单一解决方案——需要组合所有3种方法
- 组合并行有简单、可解释的经验法则
核心权衡:
- 通信带宽 vs 利用率
- 内存 vs 计算
- 简单性 vs 效率
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!