目录
[一、Layer 1:你真实拥有的东西其实是 GPU、内存和互联](#一、Layer 1:你真实拥有的东西其实是 GPU、内存和互联)
[二、GPU 为什么需要 NVLink / PCIe?](#二、GPU 为什么需要 NVLink / PCIe?)
[四、Layer 2:CUDA 到底是什么](#四、Layer 2:CUDA 到底是什么)
[五、CUDA Driver 和 CUDA Toolkit 要严格区分](#五、CUDA Driver 和 CUDA Toolkit 要严格区分)
[NVIDIA Driver](#NVIDIA Driver)
[CUDA Toolkit / Runtime](#CUDA Toolkit / Runtime)
[六、一个 kernel 到底是什么?](#六、一个 kernel 到底是什么?)
[七、Layer 3:NCCL 出场](#七、Layer 3:NCCL 出场)
[八、NCCL 最重要的几个通信原语](#八、NCCL 最重要的几个通信原语)
[1. Broadcast](#1. Broadcast)
[九、AllReduce ------ DDP 的灵魂](#九、AllReduce —— DDP 的灵魂)
[十二、Send / Recv](#十二、Send / Recv)
[十三、Layer 4:为什么分布式训练里全是"进程"](#十三、Layer 4:为什么分布式训练里全是“进程”)
[十四、torchrun 到底干嘛?](#十四、torchrun 到底干嘛?)
[十五、rank / world_size 到底是什么?](#十五、rank / world_size 到底是什么?)
[十六、实际代码:看看 torchrun 给你的东西](#十六、实际代码:看看 torchrun 给你的东西)
[十七、local_rank 为什么又出来了?](#十七、local_rank 为什么又出来了?)
[Node 0](#Node 0)
[Node 1](#Node 1)
[十八、再补一个 node_rank](#十八、再补一个 node_rank)
[十九、ProcessGroup 才是 rank 真正强大的地方](#十九、ProcessGroup 才是 rank 真正强大的地方)
[二十、自己写一个 AllReduce,你马上就懂 DDP 一半了](#二十、自己写一个 AllReduce,你马上就懂 DDP 一半了)
[二十一、Layer 6:现在才真正进入"并行训练"](#二十一、Layer 6:现在才真正进入“并行训练”)
[二十二、数据并行 DP:切 Batch](#二十二、数据并行 DP:切 Batch)
[二十三、真正可运行的 DDP 代码](#二十三、真正可运行的 DDP 代码)
[二十四、DDP 里面真正发生了什么?](#二十四、DDP 里面真正发生了什么?)
[二十五、DDP 最大的问题](#二十五、DDP 最大的问题)
[二十七、ZeRO/FSDP:切 Model States](#二十七、ZeRO/FSDP:切 Model States)
[二十八、但是参数都被切了,Linear 怎么算?](#二十八、但是参数都被切了,Linear 怎么算?)
[二十九、TP 与 FSDP 有本质区别](#二十九、TP 与 FSDP 有本质区别)
[Tensor Parallel](#Tensor Parallel)
[三十、为什么 Megatron 很重要?](#三十、为什么 Megatron 很重要?)
[三十一、Pipeline Parallel:切 Layer](#三十一、Pipeline Parallel:切 Layer)
[三十二、为什么还需要 microbatch?](#三十二、为什么还需要 microbatch?)
[三十三、Context Parallel:切 Sequence](#三十三、Context Parallel:切 Sequence)
[三十四、Expert Parallel:切 MoE Expert](#三十四、Expert Parallel:切 MoE Expert)
[三十六、最复杂情况下,一张 GPU 到底是什么身份?](#三十六、最复杂情况下,一张 GPU 到底是什么身份?)
[三十七、这时候 ProcessGroup 就真正派上用场了](#三十七、这时候 ProcessGroup 就真正派上用场了)
[三十八、因此 rank 和 group 的真正关系是](#三十八、因此 rank 和 group 的真正关系是)
[三十九、Layer 4 还有一个容易混淆的东西:Ray](#三十九、Layer 4 还有一个容易混淆的东西:Ray)
[四十、这就是 verl 为什么同时出现 Ray + FSDP/Megatron + vLLM](#四十、这就是 verl 为什么同时出现 Ray + FSDP/Megatron + vLLM)
[四十一、完整训练 Step 从顶层到底层走一遍](#四十一、完整训练 Step 从顶层到底层走一遍)
[方案 A:普通小模型](#方案 A:普通小模型)
[四十四、方案 B:模型单卡训练放不下](#四十四、方案 B:模型单卡训练放不下)
[四十五、方案 C:超大模型预训练](#四十五、方案 C:超大模型预训练)
[四十六、方案 D:LLM RL / verl](#四十六、方案 D:LLM RL / verl)
[四十八、我建议你现在不要急着学 Megatron 参数](#四十八、我建议你现在不要急着学 Megatron 参数)
[Experiment 1:观察 rank](#Experiment 1:观察 rank)
[五十、Experiment 2:手写 NCCL AllReduce](#五十、Experiment 2:手写 NCCL AllReduce)
[五十一、Experiment 3:自己模拟 DDP](#五十一、Experiment 3:自己模拟 DDP)
[五十二、再做 Experiment 4:换成真正 DDP](#五十二、再做 Experiment 4:换成真正 DDP)
[世界 A:计算世界](#世界 A:计算世界)
[世界 B:通信世界](#世界 B:通信世界)
[你现在先牢牢记住 8 句话](#你现在先牢牢记住 8 句话)
结论:先建立这张"分布式训练全栈地图"

以后看到 torchrun / NCCL / CUDA / DDP / FSDP / DeepSpeed / Megatron / Ray / verl / vLLM,应该能马上判断:
它在哪一层?它管什么?它不管什么?它下面依赖谁?
我建议你先把分布式训练理解成下面 7 层:
┌─────────────────────────────────────────────────────────────┐
│ Layer 7:训练 / RL 框架 │
│ HuggingFace Trainer / verl / TRL / NeMo / Megatron-LM │
│ │
│ 负责:训练流程、loss、optimizer、rollout、checkpoint 等 │
├─────────────────────────────────────────────────────────────┤
│ Layer 6:并行策略 │
│ DDP / FSDP / ZeRO / TP / PP / CP / EP │
│ │
│ 负责:模型、数据、状态到底怎么切 │
├─────────────────────────────────────────────────────────────┤
│ Layer 5:分布式运行时 │
│ torch.distributed / Megatron-Core / DeepSpeed │
│ │
│ 负责:ProcessGroup、rank、通信调用、并行组 │
├─────────────────────────────────────────────────────────────┤
│ Layer 4:进程与任务调度 │
│ torchrun / Ray / Slurm / Kubernetes │
│ │
│ 负责:在哪些机器启动多少进程、占哪些 GPU │
├─────────────────────────────────────────────────────────────┤
│ Layer 3:GPU 通信 │
│ NCCL │
│ │
│ AllReduce / AllGather / ReduceScatter / Send / Recv │
├─────────────────────────────────────────────────────────────┤
│ Layer 2:GPU 计算 │
│ CUDA Runtime / CUDA Kernel / cuBLAS / FlashAttention │
│ │
│ GEMM、Attention、Kernel 执行 │
├─────────────────────────────────────────────────────────────┤
│ Layer 1:硬件 │
│ GPU HBM / NVLink / NVSwitch / PCIe / CPU / RAM / NIC │
│ InfiniBand / RoCE │
└─────────────────────────────────────────────────────────────┘
这张图是地基。
其中最重要的一条链是:
训练代码
↓
DDP / FSDP / Megatron TP
↓
torch.distributed
↓
NCCL
↓
CUDA
↓
NVLink / PCIe / IB
↓
GPU
PyTorch 官方现在也把 distributed 体系划分成 DDP、FSDP2、TP、DeviceMesh 等并行组件;Megatron-Core 则进一步提供 DP、TP、PP、CP、EP 等组合式大模型并行能力。(PyTorch Docs)
下面从最底层开始打通。
一、Layer 1:你真实拥有的东西其实是 GPU、内存和互联
先暂时忘掉 PyTorch。
假设一台机器:
CPU
│
System RAM
│
─────── PCIe ───────
│ │ │
GPU0 GPU1 GPU2 GPU3
│ │ │ │
└──── NVLink / NVSwitch ─┘
每张 GPU 里面大致有:
GPU
├── SM / Tensor Core ← 真正计算矩阵乘法
├── HBM 显存 ← 模型参数 / activation / gradient
├── L2 Cache
├── CUDA Core
├── Tensor Core
└── NVLink / PCIe Interface
例如你用 A100 80GB:
GPU 0
├─ HBM: 80GB
└─ compute
GPU 1
├─ HBM: 80GB
└─ compute
这里最容易产生一个错误认知:
4 × 80GB GPU ≠ 自动得到一张 320GB GPU。
实际上:
GPU0 HBM 80GB
GPU1 HBM 80GB
GPU2 HBM 80GB
GPU3 HBM 80GB
是四块独立内存。
GPU0 默认不能直接把:
tensor_on_gpu1
当成本地 tensor 使用。
必须发生通信。
所以分布式训练的本质问题开始出现:
怎么让很多拥有独立显存的 GPU 像一个计算系统一样协作?
二、GPU 为什么需要 NVLink / PCIe?
假设:
GPU0:
gradient = [1, 2, 3]
GPU1:
gradient = [4, 5, 6]
DDP 要得到:
sum gradient = [5, 7, 9]
那么数据就必须:
GPU0 ─────────→ GPU1
GPU1 ─────────→ GPU0
这个数据走哪里?
可能走:
NVLink
PCIe
NVSwitch
跨机器之后可能变成:
GPU
↓
PCIe
↓
NIC
↓
InfiniBand / RoCE Ethernet
↓
NIC
↓
PCIe
↓
另一台 GPU
三、单机和多机是两个不同通信世界
单机多卡
例如:
Node 0
GPU0 ─┐
GPU1 ─┼── NVSwitch
GPU2 ─┤
GPU3 ─┘
通常特点:
延迟低
带宽高
通信相对便宜
多机多卡
例如:
Node 0 Node 1
GPU0 ─┐ GPU4 ─┐
GPU1 ─┼─ NVLink GPU5 ─┼─ NVLink
GPU2 ─┤ ...
GPU3 ─┘
│ │
PCIe PCIe
│ │
NIC ===== InfiniBand/RoCE ===== NIC
所以:
通信拓扑会直接影响并行策略。
比如 TP 每一层 Transformer 都可能频繁通信。
因此:
TP group
通常特别希望放在:
同一节点
同一 NVLink/NVSwitch domain
而不是随便跨机器。
这就是后面所谓:
topology-aware parallelism
的来源。
四、Layer 2:CUDA 到底是什么
很多初学分布式训练的人会把:
CUDA
PyTorch
NCCL
GPU Driver
混在一起。
实际上:
PyTorch
↓ 调 CUDA API
CUDA Runtime
↓
CUDA Driver
↓
GPU Hardware
例如:
x = torch.randn(4096, 4096, device="cuda")
y = torch.randn(4096, 4096, device="cuda")
z = x @ y
你写的是:
z = x @ y
但底下可能经过:
PyTorch
↓
ATen
↓
cuBLAS / cuBLASLt
↓
CUDA Kernel
↓
Tensor Core
最终 GPU 才真正执行矩阵乘法。
五、CUDA Driver 和 CUDA Toolkit 要严格区分
这一点跟你之前云服务器环境问题直接相关。
大概是:
操作系统
│
├── NVIDIA Driver
│
│ └── 和物理 GPU 通信
│
├── CUDA Runtime
│
├── cuBLAS
├── cuDNN
├── NCCL
│
└── PyTorch
其中:
NVIDIA Driver
更接近系统层:
Linux Kernel
↓
NVIDIA Driver
↓
GPU
云服务器通常:
你不能随便改。
CUDA Toolkit / Runtime
例如:
CUDA 12.4
CUDA 12.6
属于用户空间生态。
很多情况下 Conda / pip package / Docker 可以提供对应 runtime library。
所以之前我们讨论的:
Driver
≠
CUDA Runtime
≠
PyTorch CUDA build
就是这个原因。
六、一个 kernel 到底是什么?
例如:
y = torch.relu(x)
GPU 不会理解 Python。
PyTorch 最终启动:
CUDA Kernel
可以把 kernel 粗略理解为:
GPU 上运行的一段函数。
例如概念代码:
__global__ void relu(float* x) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
x[i] = max(x[i], 0.0f);
}
GPU:
Thread 0 → x[0]
Thread 1 → x[1]
Thread 2 → x[2]
...
深度学习训练本质上就是不停启动:
GEMM kernel
Attention kernel
LayerNorm kernel
Softmax kernel
optimizer kernel
...
七、Layer 3:NCCL 出场

现在问题来了。
CUDA 能让:
GPU0
计算。
但是:
GPU0 和 GPU1 怎么一起算?
NVIDIA 提供:
NCCL
全称:
NVIDIA Collective Communications Library
它专门处理:
GPU ↔ GPU communication
NCCL 本身不是训练框架,而是 GPU collective communication library。官方提供 AllReduce、Broadcast 等 collective,并把 GPU 通过 communicator/rank 组织起来。(NVIDIA Docs)
八、NCCL 最重要的几个通信原语
这一块必须真正理解。
后面:
DDP
FSDP
TP
PP
CP
本质上都是这些操作的不同组合。
1. Broadcast
初始:
GPU0: [1 2 3]
GPU1: [? ? ?]
GPU2: [? ? ?]
GPU3: [? ? ?]
执行:
broadcast(src=0)
结果:
GPU0: [1 2 3]
GPU1: [1 2 3]
GPU2: [1 2 3]
GPU3: [1 2 3]
用途:
参数初始化同步
配置同步
九、AllReduce ------ DDP 的灵魂
假设每张 GPU 都算出了 gradient:
GPU0: [1, 2]
GPU1: [3, 4]
GPU2: [5, 6]
GPU3: [7, 8]
执行:
AllReduce(SUM)
得到:
GPU0: [16, 20]
GPU1: [16, 20]
GPU2: [16, 20]
GPU3: [16, 20]
数学上:
g=∑i=0N−1gig = \sum_{i=0}^{N-1} g_i
然后通常再:
gavg=1Ngg_{\text{avg}}=\frac{1}{N}g
NCCL 官方定义 AllReduce 就是:所有 rank 提供输入,对它们执行 reduction,并让每个 rank 都获得结果。(NVIDIA Docs)
这直接解释了 DDP:
GPU0 forward/backward → gradient0 ─┐
GPU1 forward/backward → gradient1 ─┼→ AllReduce → same gradient
GPU2 forward/backward → gradient2 ─┤
GPU3 forward/backward → gradient3 ─┘
十、AllGather
假设:
GPU0: [A]
GPU1: [B]
GPU2: [C]
GPU3: [D]
AllGather:
GPU0: [A B C D]
GPU1: [A B C D]
GPU2: [A B C D]
GPU3: [A B C D]
记法:
每个人拿一块,最后每个人拿全部。
FSDP 会大量使用这一类思想。
十一、ReduceScatter
它基本可以理解为:
Reduce
+
Scatter
例如:
GPU0: A0 A1 A2 A3
GPU1: B0 B1 B2 B3
GPU2: C0 C1 C2 C3
GPU3: D0 D1 D2 D3
先:
A0+B0+C0+D0
A1+B1+C1+D1
...
但不让所有 GPU 都保存全部结果。
而是:
GPU0 → result0
GPU1 → result1
GPU2 → result2
GPU3 → result3
所以:
结果本身也是 shard 的。
这正好非常适合:
FSDP / ZeRO
十二、Send / Recv
这是点对点通信。
GPU0
│
│ send activation
▼
GPU1
Pipeline Parallel 特别典型:
Stage 0
GPU0
│
│ activation
▼
Stage 1
GPU1
│
│ activation
▼
Stage 2
GPU2
所以可以先形成这个映射:
| 并行机制 | 常见通信 |
|---|---|
| DDP | AllReduce |
| FSDP | AllGather + ReduceScatter |
| TP | AllReduce / AllGather / ReduceScatter |
| PP | Send / Recv |
| CP | AllGather / Ring P2P 等 |
| EP | All-to-All |
后面看论文的时候,你会发现:
新的并行算法很多时候其实就是在重新设计"谁存什么、谁算什么、什么时候通信什么"。
十三、Layer 4:为什么分布式训练里全是"进程"
现在终于进入:
rank
world_size
local_rank
这是你目前最需要补牢的地方。
PyTorch GPU 分布式最常见的运行模型是:
一个 GPU 对应一个 Python process。
例如:
4 GPUs
GPU0 ← Python Process 0
GPU1 ← Python Process 1
GPU2 ← Python Process 2
GPU3 ← Python Process 3
不是:
一个 Python process
├── GPU0
├── GPU1
├── GPU2
└── GPU3
虽然后者技术上也能实现,但主流 distributed training 通常使用前面的 one-process-per-GPU 模式。
十四、torchrun 到底干嘛?
假设你执行:
torchrun \
--standalone \
--nproc-per-node=4 \
train.py
你不是运行一次:
python train.py
而是 torchrun 帮你启动:
Process 0 → train.py
Process 1 → train.py
Process 2 → train.py
Process 3 → train.py
所以:
torchrun
│
┌────────────┼────────────┐
↓ ↓ ↓
process 0 process 1 process 2 ...
│ │
GPU0 GPU1
关键点:
每个进程运行的是同一份 Python 代码。
代码自己通过:
rank
知道:
"我是谁?"
十五、rank / world_size 到底是什么?
最简单的情况:
1 machine
4 GPUs
于是:
WORLD_SIZE = 4
Process 0 → rank 0 → GPU0
Process 1 → rank 1 → GPU1
Process 2 → rank 2 → GPU2
Process 3 → rank 3 → GPU3
因此:
world_size
整个 distributed group 中:
process 数量
这里:
world_size = 4
rank
每个 process 的全局编号:
0 ~ world_size - 1
所以:
rank = 0
rank = 1
rank = 2
rank = 3
十六、实际代码:看看 torchrun 给你的东西
创建:
# dist_info.py
import os
import torch
import torch.distributed as dist
dist.init_process_group(backend="nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
print(
f"rank={rank}, "
f"local_rank={local_rank}, "
f"world_size={world_size}, "
f"gpu={torch.cuda.current_device()}"
)
dist.destroy_process_group()
执行:
torchrun --standalone --nproc-per-node=4 dist_info.py
你会看到类似:
rank=0 local_rank=0 world_size=4 gpu=0
rank=1 local_rank=1 world_size=4 gpu=1
rank=2 local_rank=2 world_size=4 gpu=2
rank=3 local_rank=3 world_size=4 gpu=3
这里:
dist.init_process_group(backend="nccl")
做的是:
PyTorch Processes
↓
建立 ProcessGroup
↓
NCCL Communicator
↓
GPU 通信建立
十七、local_rank 为什么又出来了?
单机时:
global rank == local rank
看起来似乎没有区别。
多机就不同了。
例如:
2 nodes × 4 GPU
Node 0
GPU0 → global_rank 0, local_rank 0
GPU1 → global_rank 1, local_rank 1
GPU2 → global_rank 2, local_rank 2
GPU3 → global_rank 3, local_rank 3
Node 1
GPU0 → global_rank 4, local_rank 0
GPU1 → global_rank 5, local_rank 1
GPU2 → global_rank 6, local_rank 2
GPU3 → global_rank 7, local_rank 3
所以:
global rank
↓
整个 cluster 中的进程身份
local rank
↓
当前机器内部的进程/GPU身份
这也是为什么通常:
torch.cuda.set_device(local_rank)
而不是:
torch.cuda.set_device(rank)
否则:
rank = 5
你会去找:
cuda:5
但当前节点可能只有:
cuda:0~3
十八、再补一个 node_rank
完整关系:
Cluster
Node rank 0
┌───────────────┐
│ GPU0 rank 0 │
│ GPU1 rank 1 │
│ GPU2 rank 2 │
│ GPU3 rank 3 │
└───────────────┘
Node rank 1
┌───────────────┐
│ GPU0 rank 4 │
│ GPU1 rank 5 │
│ GPU2 rank 6 │
│ GPU3 rank 7 │
└───────────────┘
一般:
global_rank=node_rank×GPUsPerNode+local_rankglobal\_rank = node\_rank \times GPUsPerNode + local\_rank
例如:
node_rank = 1
local_rank = 2
GPUs/node = 4
那么:
rank=1×4+2=6rank=1\times4+2=6
十九、ProcessGroup 才是 rank 真正强大的地方
默认:
dist.init_process_group()
建立:
WORLD GROUP
[0,1,2,3,4,5,6,7]
但是我们可以自己创建:
group_a = dist.new_group([0, 1, 2, 3])
group_b = dist.new_group([4, 5, 6, 7])
现在:
WORLD
│
├── Group A
│ └── ranks [0 1 2 3]
│
└── Group B
└── ranks [4 5 6 7]
于是通信可以只发生在某个 group 内。
这正是:
Megatron TP Group
PP Group
DP Group
CP Group
EP Group
的底层基础。
二十、自己写一个 AllReduce,你马上就懂 DDP 一半了
import os
import torch
import torch.distributed as dist
dist.init_process_group("nccl")
rank = dist.get_rank()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
x = torch.tensor(
[float(rank + 1)],
device=f"cuda:{local_rank}"
)
print(f"Before rank={rank}: {x.item()}")
dist.all_reduce(
x,
op=dist.ReduceOp.SUM
)
print(f"After rank={rank}: {x.item()}")
4 GPU:
初始:
rank0 → 1
rank1 → 2
rank2 → 3
rank3 → 4
执行:
all_reduce()
之后:
rank0 → 10
rank1 → 10
rank2 → 10
rank3 → 10
因为:
1+2+3+4=101+2+3+4=10
这个函数表面是:
dist.all_reduce()
底层大概:
PyTorch
↓
ProcessGroupNCCL
↓
NCCL AllReduce
↓
NVLink / PCIe
↓
GPU
二十一、Layer 6:现在才真正进入"并行训练"
前面讲的:
CUDA
NCCL
rank
ProcessGroup
都还没规定:
模型到底怎么切。
这是 parallel strategy 的职责。
核心可以归结为一句话:
到底切哪个维度?

二十二、数据并行 DP:切 Batch
例如:
Global Batch = 128
4 GPUs
那么:
GPU0 → batch 0:32
GPU1 → batch 32:64
GPU2 → batch 64:96
GPU3 → batch 96:128
而模型:
GPU0 → 完整 Model
GPU1 → 完整 Model
GPU2 → 完整 Model
GPU3 → 完整 Model
图:
Dataset
│
┌────────┼────────┐
↓ ↓ ↓
batch0 batch1 batch2 ...
│ │
↓ ↓
GPU0 GPU1
Model Model
│ │
↓ ↓
grad0 grad1
└────┬───┘
↓
AllReduce
↓
same gradient
二十三、真正可运行的 DDP 代码
import os
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader
from torch.utils.data.distributed import DistributedSampler
def main():
dist.init_process_group("nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
model = torch.nn.Sequential(
torch.nn.Linear(1024, 4096),
torch.nn.ReLU(),
torch.nn.Linear(4096, 10),
).cuda()
model = DDP(
model,
device_ids=[local_rank]
)
dataset = MyDataset()
sampler = DistributedSampler(
dataset,
shuffle=True
)
dataloader = DataLoader(
dataset,
batch_size=32,
sampler=sampler,
)
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-4
)
for epoch in range(10):
sampler.set_epoch(epoch)
for x, y in dataloader:
x = x.cuda(non_blocking=True)
y = y.cuda(non_blocking=True)
optimizer.zero_grad()
pred = model(x)
loss = torch.nn.functional.cross_entropy(
pred,
y
)
loss.backward()
optimizer.step()
dist.destroy_process_group()
运行:
torchrun \
--standalone \
--nproc-per-node=4 \
train.py
二十四、DDP 里面真正发生了什么?
重点是:
loss.backward()
你没手写:
dist.all_reduce()
为什么?
因为:
DDP(model)
会给 parameter gradient 注册 hook。
于是 backward:
Layer 32 backward
↓
gradient ready
↓
放进 gradient bucket
↓
NCCL AllReduce
然后继续:
Layer 31 backward
↓
gradient ready
↓
AllReduce
所以可以实现:
Backward Compute
+
Communication
部分 overlap。
这里你第一次看到:
训练框架实际上是在替你自动安排通信。
二十五、DDP 最大的问题
假设:
70B model
每张 GPU 都保存:
完整 parameter
完整 gradient
完整 optimizer state
那么:
GPU0: Model 70B
GPU1: Model 70B
GPU2: Model 70B
...
显然很浪费。
所以产生:
ZeRO
FSDP
二十六、训练显存到底花在哪里?
这个公式非常重要。
训练大模型时 GPU memory 大致:
Memory=Parameters+Gradients+OptimizerStates+Activations+TemporaryBuffersMemory = Parameters + Gradients + OptimizerStates + Activations + TemporaryBuffers
假设 Adam:
通常还需要:
parameter
gradient
first moment m
second moment v
所以训练所需显存远大于:
模型权重大小
例如你看到:
7B × BF16 ≈ 14GB
绝对不能理解成:
7B 模型训练只需要 14GB。
14GB 只是:
parameters
的一部分账。
二十七、ZeRO/FSDP:切 Model States
假设 4 GPU。
普通 DDP:
GPU0: P P P P | G G G G | O O O O
GPU1: P P P P | G G G G | O O O O
GPU2: P P P P | G G G G | O O O O
GPU3: P P P P | G G G G | O O O O
其中:
P = parameter
G = gradient
O = optimizer state
大量重复。
ZeRO-1
切:
Optimizer State
GPU0: full P | full G | O0
GPU1: full P | full G | O1
GPU2: full P | full G | O2
GPU3: full P | full G | O3
ZeRO-2
再切:
gradient
GPU0: full P | G0 | O0
GPU1: full P | G1 | O1
GPU2: full P | G2 | O2
GPU3: full P | G3 | O3
ZeRO-3
连 parameter 一起切:
GPU0: P0 | G0 | O0
GPU1: P1 | G1 | O1
GPU2: P2 | G2 | O2
GPU3: P3 | G3 | O3
DeepSpeed 官方对 ZeRO 三阶段的定义基本就是:Stage 1 shard optimizer states,Stage 2 进一步 shard gradients,Stage 3 再 shard parameters。(DeepSpeed)
二十八、但是参数都被切了,Linear 怎么算?
这是 FSDP/ZeRO-3 最关键的问题。
假设:
Linear.weight
分成:
GPU0: W0
GPU1: W1
GPU2: W2
GPU3: W3
forward 到这一层的时候:
GPU0
GPU1
GPU2
GPU3
↓
AllGather
↓
完整 W
↓
forward
↓
释放不需要的完整 W
backward 后:
gradient
↓
ReduceScatter
↓
GPU0 → grad0
GPU1 → grad1
GPU2 → grad2
GPU3 → grad3
所以:
FSDP
forward:
shard parameters
↓
AllGather
↓
temporary full parameter
↓
compute
↓
reshard
backward:
gradient
↓
ReduceScatter
↓
sharded gradient
因此:
FSDP 的本质不是"参数永远不完整出现"。
而是:
只在真正计算需要它时临时 gather,算完马上 shard 回去。
二十九、TP 与 FSDP 有本质区别
这是大模型分布式最容易混乱的地方之一。
FSDP
参数:
平时 shard
但计算 Linear 时:
临时 gather 完整 W
每个 GPU 基本还是在执行完整 layer 的计算,只不过处理不同 batch。
Tensor Parallel
则是:
计算本身就是分布式的。
例如:
Y=XWY=XW
把:
W=W0,W1W=W_0,W_1
那么:
GPU0:
Y0 = XW0
GPU1:
Y1 = XW1
最后:
Y = concat(Y0, Y1)
GPU0 永远没必要计算:
XW1
GPU1 也不计算:
XW0
所以:
FSDP
= shard storage/state
TP
= shard computation
这是一个非常重要的区别。
三十、为什么 Megatron 很重要?
因为 Transformer 特别适合做 Tensor Parallel。
例如 MLP:
H=GELU(XA)H = \text{GELU}(XA) Y=HBY = HB
Megatron 可以:
X
│
┌─────┴─────┐
↓ ↓
GPU0: XA0 GPU1: XA1
↓ ↓
H0 H1
↓ ↓
H0B0 H1B1
└─────┬─────┘
↓
AllReduce
↓
Y
因此一个 layer 被多个 GPU 共同计算。
Megatron-Core 官方目前支持 DP、TP、PP、CP、EP,以及 FSDP 等组合并行。(NVIDIA Docs)
三十一、Pipeline Parallel:切 Layer
如果模型:
Layer 0
Layer 1
...
Layer 79
可以:
GPU0:
Layer 0 ~ 19
GPU1:
Layer 20 ~ 39
GPU2:
Layer 40 ~ 59
GPU3:
Layer 60 ~ 79
于是:
tokens
↓
GPU0
↓ activation
GPU1
↓ activation
GPU2
↓ activation
GPU3
↓
loss
这里通信是:
P2P send/recv
三十二、为什么还需要 microbatch?
如果只有一个 batch:
时间 →
GPU0 ████████
GPU1 ████████
GPU2 ████████
GPU3 ████████
大量 GPU 闲着。
于是切:
Batch
↓
microbatch0
microbatch1
microbatch2
microbatch3
...
变成 pipeline:
time →
GPU0 M0 M1 M2 M3
GPU1 M0 M1 M2 M3
GPU2 M0 M1 M2 M3
GPU3 M0 M1 M2 M3
GPU 利用率明显提高。
这就是:
pipeline scheduling
pipeline bubble
microbatch
这些词的来源。
三十三、Context Parallel:切 Sequence
现在长上下文:
sequence length = 128K
activation 很大。
于是:
Tokens:
0 ───────────────────────────────── 127999
切:
GPU0: tokens 0 ~ 31K
GPU1: tokens 32K ~ 63K
GPU2: tokens 64K ~ 95K
GPU3: tokens 96K ~ 127K
这就是:
Context Parallelism
Megatron 文档中 CP 正是沿 sequence dimension 切输入和 activation;attention 因为 Q 需要访问其他 token 的 KV,因此需要额外通信。(NVIDIA Docs)
三十四、Expert Parallel:切 MoE Expert
例如:
64 Experts
8 GPUs
可以:
GPU0 → expert 0-7
GPU1 → expert 8-15
GPU2 → expert 16-23
...
token routing:
Token
↓
Router
↓
Expert 37
↓
发送到拥有 Expert37 的 GPU
因此会出现:
All-to-All
通信。
三十五、现在可以得到真正完整的"大模型并行坐标系"
一个 Transformer Tensor:
[batch, sequence, hidden]
模型还有:
layers
experts
因此你可以沿不同维度切:
大模型
Batch
↓
Data Parallel
DP
Hidden
↓
Tensor Parallel
TP
Layers
↓
Pipeline Parallel
PP
Sequence
↓
Context Parallel
CP
Experts
↓
Expert Parallel
EP
这张表建议直接记:
| 并行 | 切什么 | 解决什么 |
|---|---|---|
| DP | batch | 提升吞吐 |
| FSDP/ZeRO | model states | 降低参数/梯度/optimizer 显存 |
| TP | hidden / layer tensor | 单层太大 |
| PP | layers | 模型太深 |
| CP | sequence | 长上下文 |
| EP | experts | MoE |
三十六、最复杂情况下,一张 GPU 到底是什么身份?

假设:
64 GPU
TP = 4
PP = 2
CP = 2
那么:
DP=644×2×2=4DP= \frac{64}{4\times2\times2} =4
于是:
64=DP(4)×PP(2)×CP(2)×TP(4)64 = DP(4) \times PP(2) \times CP(2) \times TP(4)
一张 GPU:
global rank = 37
可能同时有:
TP rank = 1
PP rank = 0
CP rank = 1
DP rank = 2
所以:
rank 37 不是只有一个身份。
它同时属于:
WORLD group
TP group
PP group
CP group
DP group
三十七、这时候 ProcessGroup 就真正派上用场了
想象:
World ranks
0 1 2 3 4 5 6 7
假设:
TP = 2
PP = 2
DP = 2
可能形成:
TP Groups:
[0,1]
[2,3]
[4,5]
[6,7]
PP Groups:
[0,2]
[1,3]
[4,6]
[5,7]
DP Groups:
[0,4]
[1,5]
[2,6]
[3,7]
那么:
rank 1
同时属于:
TP: [0,1]
PP: [1,3]
DP: [1,5]
如果现在:
Tensor Parallel AllReduce
只调用:
TP Group
也就是:
rank 0 ↔ rank 1
不会和:
rank 4
通信。
如果 gradient DP:
rank 1 ↔ rank 5
三十八、因此 rank 和 group 的真正关系是
不要再理解成:
rank = GPU 编号
准确来说:
rank 是 distributed process 在某个通信世界中的逻辑编号。
通常:
Process
│
├── global_rank
├── local_rank
│
├── TP rank
├── DP rank
├── PP rank
├── CP rank
└── EP rank
而 GPU 只是这个 process 当前绑定的计算设备。
这是你理解后续 Megatron 和 verl 的关键。
三十九、Layer 4 还有一个容易混淆的东西:Ray
现在再看:
torchrun
Ray
两者不是一个层面的东西。
torchrun
核心:
启动 distributed process
例如:
rank0
rank1
rank2
rank3
Ray
核心是:
distributed task / actor scheduling
resource management
例如:
Ray Cluster
Head
│
┌───────────┼────────────┐
↓ ↓ ↓
Actor Worker Reward Worker Rollout Worker
GPU 0-3 GPU4 GPU5-7
Ray 解决:
谁占 GPU?
Megatron 解决:
拿到这些 GPU 后怎么切模型?
NCCL 解决:
GPU 之间怎么传 Tensor?
这三个绝对不要混。
四十、这就是 verl 为什么同时出现 Ray + FSDP/Megatron + vLLM
现在终于可以重新看 verl:
verl
RL algorithm / workflow
│
▼
Ray
resource & worker scheduling
│
┌────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
ActorWorker RolloutWorker RewardWorker
│ │
▼ ▼
FSDP/Megatron vLLM
│ │
▼ ▼
torch.distributed distributed runtime
│ │
└───────┬────────┘
▼
NCCL
│
▼
NVLink / PCIe / IB
│
▼
GPU
所以:
verl
不是 NCCL 替代品。
Ray
也不是 Megatron 替代品。
它们负责的是完全不同的问题。
四十一、完整训练 Step 从顶层到底层走一遍
这部分最重要。
假设:
loss.backward()
你只写一行。
下面可能发生:
① Python
loss.backward()
↓
② PyTorch Autograd
构建/执行 backward graph
↓
③ Transformer Layer backward
↓
④ GEMM backward
cuBLAS
↓
⑤ CUDA kernel
↓
⑥ Tensor Core
计算 gradient
↓
⑦ DDP gradient hook
↓
⑧ torch.distributed.all_reduce()
↓
⑨ ProcessGroupNCCL
↓
⑩ NCCL AllReduce
↓
⑪ NVLink / NVSwitch / IB
↓
⑫ 其他 GPU
↓
⑬ gradient 同步
↓
⑭ AdamW optimizer.step()
↓
⑮ CUDA optimizer kernel
↓
⑯ GPU 更新 parameter
所以你表面写:
loss.backward()
optimizer.step()
底下实际已经发生了:
Autograd
CUDA
cuBLAS
NCCL
NVLink
DDP
ProcessGroup
GPU Kernel
四十二、把所有常见名词重新归位
这张表你后面可以一直拿来查。
| 名词 | 层级 | 核心职责 |
|---|---|---|
| GPU | 硬件 | 计算 |
| HBM | 硬件 | GPU 显存 |
| PCIe | 硬件 | CPU/GPU/设备互联 |
| NVLink | 硬件 | 高速 GPU-GPU |
| NVSwitch | 硬件 | 多 GPU 高速交换 |
| InfiniBand | 网络 | 跨节点高速通信 |
| CUDA Driver | 系统 | 控制 NVIDIA GPU |
| CUDA Runtime | 运行时 | CUDA API |
| cuBLAS | Kernel Library | GEMM |
| FlashAttention | Kernel | 高效 Attention |
| NCCL | 通信 | GPU collective |
| torch.distributed | distributed runtime | rank/group/communication |
| torchrun | launcher | 启动 distributed processes |
| DDP | 并行策略 | replica + gradient sync |
| FSDP | 并行策略 | shard model states |
| ZeRO | 并行策略 | shard optimizer/grad/param |
| TP | 并行策略 | shard layer computation |
| PP | 并行策略 | shard layers |
| CP | 并行策略 | shard sequence |
| EP | 并行策略 | shard experts |
| Megatron-Core | 大模型并行库 | TP/PP/CP/EP/DP |
| DeepSpeed | distributed training system | ZeRO 等 |
| Ray | scheduler | worker/resource |
| vLLM | inference engine | LLM inference/rollout |
| verl | RL framework | RL workflow/orchestration |
四十三、实际工程里常见的几套组合
方案 A:普通小模型
PyTorch
+
DDP
+
torchrun
+
NCCL
例如:
torchrun --nproc-per-node=8 train.py
适合:
模型单 GPU 放得下
主要想提升训练吞吐
这是最应该先掌握的方案。
四十四、方案 B:模型单卡训练放不下
常见:
PyTorch
+
FSDP
+
NCCL
或者:
DeepSpeed ZeRO-3
适合:
7B / 14B / 32B
SFT
LoRA / Full FT
具体哪个更合适要看生态。
四十五、方案 C:超大模型预训练
例如:
Megatron-Core
+
TP
+
PP
+
DP/FSDP
+
CP
+
NCCL
例如:
TP = 8
PP = 4
CP = 2
DP = 8
总 GPU:
8×4×2×8=5128\times4\times2\times8 = 512
这种就是典型大型训练集群玩法。
Megatron 官方的 parallelism guide 也是按这种多维组合方式组织大规模模型训练。(NVIDIA Docs)
四十六、方案 D:LLM RL / verl
你目前更相关的是:
verl
│
Ray
│
┌────────────────┼────────────────┐
↓ ↓ ↓
Actor Rollout Reference
│ │ │
FSDP/ vLLM FSDP/
Megatron Megatron
│ │ │
└────────────────┼────────────────┘
↓
NCCL
↓
GPU
这里有两套并行问题同时存在。
第一层:角色级资源分配
例如:
Actor
Rollout
Reference
Reward
谁用哪些 GPU。
这是:
verl / Ray
层面的事情。
第二层:角色内部模型并行
例如 Actor 拿到:
GPU 0~7
之后:
TP=2
PP=2
DP=2
这是:
Megatron / FSDP
层面的事情。
这就是你之前问:
verl rank/world_size 和 group_worker 到底什么关系
为什么会觉得乱。
实际上你同时碰到了:
Ray Worker world
+
PyTorch distributed world
+
Megatron parallel groups
三套概念。
四十七、把它画成你目前最需要的一张图
┌──────────────┐
│ verl │
│ PPO / GRPO │
└──────┬───────┘
│
Role orchestration
│
┌──────▼──────┐
│ Ray │
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
ActorWorker RolloutWorker RewardWorker
│ │
│ └────── vLLM
│
▼
Megatron/FSDP
│
┌────────┼─────────┬─────────┐
▼ ▼ ▼ ▼
TP PP DP CP
│ │ │ │
└────────┴─────────┴─────────┘
│
ProcessGroup
│
torch.distributed
│
▼
NCCL
│
┌──────┼────────┐
│ │ │
NVLink PCIe IB
│ │ │
└──────┼────────┘
▼
GPU
│
CUDA Kernel
│
Tensor Core / HBM
这基本就是你目前所有分布式问题的总地图。
四十八、我建议你现在不要急着学 Megatron 参数
这是我对你当前学习路径的一个修正。
直接继续研究:
tensor_model_parallel_size
pipeline_model_parallel_size
worker_group
rank
world_size
很容易变成:
配置项记住了,但不知道为什么存在。
正确顺序应该是:
第一阶段
GPU / CUDA / HBM
↓
理解"计算在哪发生"
第二阶段
Process / Rank / World Size
↓
理解"谁在执行代码"
第三阶段
NCCL / Collective
↓
理解"进程之间怎么交换 Tensor"
第四阶段
DDP
↓
理解"为什么 gradient 要同步"
第五阶段
FSDP / ZeRO
↓
理解"model state 怎么 shard"
第六阶段
TP
↓
理解"计算本身怎么 shard"
第七阶段
PP / CP / EP
↓
理解"大模型其他维度怎么切"
第八阶段
Megatron
↓
理解"如何把这些并行维度组合"
第九阶段
Ray
↓
理解"模型之外的 worker 怎么调度"
第十阶段
verl
↓
理解"RL 中 Actor/Rollout/Reference 如何组合"
四十九、真正建议你动手做的第一个实验
不要一上来跑 Megatron。
直接写三个文件。
Experiment 1:观察 rank
# 01_rank.py
import os
import torch
import torch.distributed as dist
dist.init_process_group("nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
print(
f"PID={os.getpid()} "
f"rank={rank} "
f"local_rank={local_rank} "
f"world_size={world_size} "
f"device={torch.cuda.current_device()}"
)
dist.destroy_process_group()
运行:
torchrun --standalone --nproc-per-node=4 01_rank.py
五十、Experiment 2:手写 NCCL AllReduce
# 02_allreduce.py
import os
import torch
import torch.distributed as dist
dist.init_process_group("nccl")
rank = dist.get_rank()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
x = torch.tensor(
[rank + 1.0],
device=f"cuda:{local_rank}"
)
print(
f"rank {rank} before: {x.item()}"
)
dist.all_reduce(x)
print(
f"rank {rank} after: {x.item()}"
)
dist.destroy_process_group()
运行:
torchrun --standalone --nproc-per-node=4 02_allreduce.py
看到:
before:
rank0 = 1
rank1 = 2
rank2 = 3
rank3 = 4
after:
rank0 = 10
rank1 = 10
rank2 = 10
rank3 = 10
这一刻:
rank
ProcessGroup
NCCL
AllReduce
GPU
五个概念就连起来了。
五十一、Experiment 3:自己模拟 DDP
不要马上:
DDP(model)
先自己做。
每张 GPU:
loss.backward()
之后:
for param in model.parameters():
dist.all_reduce(
param.grad,
op=dist.ReduceOp.SUM
)
param.grad /= dist.get_world_size()
也就是:
optimizer.zero_grad()
output = model(x)
loss = criterion(output, y)
loss.backward()
# 手写 gradient synchronization
for param in model.parameters():
dist.all_reduce(
param.grad,
op=dist.ReduceOp.SUM
)
param.grad /= world_size
optimizer.step()
你突然就会发现:
DDP 并不神秘。
它核心思想就是:
每个 GPU
自己 forward
自己 backward
↓
gradient AllReduce
↓
所有 GPU
得到相同 gradient
↓
所有 GPU
执行相同 optimizer.step()
↓
因此参数继续保持一致
这个实验的价值非常高。
五十二、再做 Experiment 4:换成真正 DDP
把:
for param in model.parameters():
dist.all_reduce(...)
删除。
改:
model = DDP(
model,
device_ids=[local_rank]
)
然后比较。
你会真正理解:
DDP
只是在替你:
gradient hook
bucket
communication overlap
AllReduce
等工作自动化和优化。
五十三、最后你应该形成一个"两个世界"的认知
以后看任何分布式训练代码,先问两个问题。
世界 A:计算世界
谁算什么?
DP → 不同 batch
TP → 不同 hidden shard
PP → 不同 layers
CP → 不同 sequence
EP → 不同 experts
世界 B:通信世界
计算结果如何交换?
AllReduce
AllGather
ReduceScatter
Broadcast
Send/Recv
AllToAll
所有分布式框架其实都是:
Distributed Training=Compute Partition+Communication+Scheduling\boxed{ Distributed\ Training = Compute\ Partition + Communication + Scheduling }
再展开:
分布式训练=切什么+在哪算+谁和谁通信+什么时候通信\boxed{ 分布式训练 = 切什么 + 在哪算 + 谁和谁通信 + 什么时候通信 }
这是比背:
DDP
FSDP
TP
PP
更重要的认知。
五十四、最终把整套地基压缩成这一张脑图
LLM Training
│
┌──────────────┼──────────────┐
│ │ │
Compute Memory Scheduling
│ │ │
▼ ▼ ▼
CUDA Parameter/Grad Process
│ Optimizer/Act Worker
│ │ │
▼ │ ▼
GPU │ torchrun/Ray
│ │
┌──────┴──────┐ │
│ │ │
HBM Tensor Core │
│
▼
Parallel Strategy
│
┌────────┬─────────┼────────┬────────┐
▼ ▼ ▼ ▼ ▼
DP TP PP CP EP
│ │ │ │ │
└────────┴─────────┼────────┴────────┘
▼
torch.distributed
│
ProcessGroup
│
┌──────────────┼──────────────┐
▼ ▼ ▼
AllReduce AllGather Send/Recv
│ │ │
└──────────────┼──────────────┘
▼
NCCL
│
┌────────────┼────────────┐
▼ ▼ ▼
NVLink PCIe IB/RoCE
│ │ │
└────────────┼────────────┘
▼
GPU Cluster
你现在先牢牢记住 8 句话
-
GPU 是独立计算设备,每张卡有自己的 HBM。
-
CUDA 负责让程序在 NVIDIA GPU 上执行计算。
-
NCCL 负责 GPU 之间高效交换 Tensor。
-
PyTorch 分布式训练通常一个进程绑定一张 GPU。
-
rank 是进程身份,不应该简单等同于物理 GPU 编号。
-
ProcessGroup 决定哪些 rank 相互通信。
-
DDP/FSDP/TP/PP 本质区别是"切什么"。
-
Megatron/verl/Ray 位于不同层,不能放在一起比较。
其中第 5~7 条真正掌握以后,你后面看 Megatron 和 verl 的源码会容易非常多。
我建议接下来按这个顺序继续
下一块最值得单独拆出来的是 「PyTorch 分布式运行时」,把下面这条链完全用代码跑通:
Linux process
↓
torchrun
↓
RANK / LOCAL_RANK / WORLD_SIZE
↓
init_process_group
↓
ProcessGroup
↓
NCCL communicator
↓
all_reduce
↓
DDP backward hook
然后再进入:
DDP
→ FSDP
→ TP
→ Megatron 多维 ProcessGroup
→ verl WorkerGroup
这样你之后遇到 rank 0 为什么占 GPU2、world_size 到底是谁的 world_size、TP group 和 Ray WorkerGroup 什么关系、为什么 vLLM 又有自己的 TP rank 这些问题,就不是靠背配置,而是可以从底层自己推出答案。(PyTorch Docs)