Deep Dive · 训练工程
微调与分布式训练深度解析
从 LoRA 到 3D 并行 — 训练和微调大模型的每一个工程细节
● Memory
训练的内存挑战
理解 GPU 内存的真实开销,这是优化的第一步
当我们训练一个神经网络时,GPU 内存不仅要存储 模型参数,还要存储 优化器状态、梯度 和 激活值。这些开销往往出乎意料地大。
内存成本细分
| 组件 |
大小(每参数) |
7B 模型 |
13B 模型 |
70B 模型 |
| 模型参数 (FP32) |
4 字节 |
28 GB |
52 GB |
280 GB |
| 优化器状态 (Adam) |
8 字节 (m, v) |
56 GB |
104 GB |
560 GB |
| 梯度 |
4 字节 |
28 GB |
52 GB |
280 GB |
| 激活值* |
取决于 BS×seq_len |
10-30 GB |
15-50 GB |
100-200 GB |
| 总计 (FP32) |
|
122-152 GB |
223-258 GB |
1220-1320 GB |
激活值的来源: 在反向传播时,我们需要保存前向过程中的所有中间激活值。对于一个 7B 参数的模型,在 batch_size=4, seq_len=2048 的设置下,激活值占用 10-30 GB。
内存占用分布 (7B 模型, FP32)
面试常问
Q: 为什么 Adam 优化器需要 8 字节?
A: Adam 为每个参数维护两个一阶和二阶矩估计 (m 和 v),各 4 字节 FP32,共 8 字节。这就是为什么 SGD (1×param size) 比 Adam (3×param size) 内存效率更高。
推论:为什么需要优化技术
- 70B 模型 + Adam = 需要 1.5+ TB GPU 内存 (全精度)
- 单块 H100 (80GB) 无法存放 70B 模型的完整训练状态
- 必须采用:分布式训练、参数高效微调 (LoRA)、量化、混合精度等技术
● Fine-tune
Full Fine-tuning
更新模型的所有参数,传统的微调方法
Full Fine-tuning 是最直接的微调方式:更新模型中的每一个参数。这意味着所有权重矩阵都参与梯度计算和优化过程。
工作流程
1
加载预训练模型
从 checkpoint 加载已训练的权重,所有参数 requires_grad=True
3
反向传播
计算所有参数的梯度 ∂L/∂W,保存所有中间激活值用于反向
4
参数更新
用 Adam/SGD 等优化器更新所有 W: W ← W - lr·∇L
内存和计算成本
参数更新
100%
所有权重都会被优化,适应新任务最充分
内存占用
3-4×
模型 + 梯度 + 优化器状态,激活值取决于 BS
训练时间
最长
需要计算每个参数的梯度,通信量最大
何时使用 Full Fine-tuning
- 数据充足: 有大量 (>100K) 标注样本
- 领域差异大: 下游任务与预训练数据分布相差很远
- 资源充足: 有足够的 GPU 内存和计算资源
- 精度要求高: 需要最高的模型性能和适应度
实践建议: 在 8×H100 或更大的集群上训练 70B+ 模型。对于 13B 以下的模型且有 GPU,使用 ZeRO-2 或 FSDP-Stage-2 分布式训练更经济。
代码示例
import torch
from torch import nn
from transformers import AutoModelForCausalLM, AutoTokenizer
# 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b")
# 所有参数都可以训练
for param in model.parameters():
param.requires_grad = True
# 标准训练循环
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5)
for epoch in range(3):
for batch in train_dataloader:
input_ids = batch['input_ids']
labels = batch['labels']
# 前向
outputs = model(input_ids=input_ids, labels=labels)
loss = outputs.loss
# 反向
optimizer.zero_grad()
loss.backward()
optimizer.step()
面试常问
Q: Full Fine-tuning 和 LoRA 的最大区别是什么?
A: Full Fine-tuning 更新所有参数,导致内存占用大(需要存储梯度和优化器状态);LoRA 只训练低秩适配器,参数量减少 1000 倍以上。对于同一个模型,LoRA 在单块 GPU 上可行,Full Fine-tuning 则需要分布式训练。
● LoRA
LoRA 原理与实现
参数高效微调的核心方法,面试重点
LoRA (Low-Rank Adaptation) 是微调大模型时最重要的技术之一。核心思想是:权重的更新 ΔW 具有低秩结构,我们可以用两个小矩阵的乘积来近似它,而不是更新整个权重矩阵。
数学基础
W' = W₀ + ΔW = W₀ + BA
在论文中,B 的初始化很关键:A 从高斯分布初始化,B 初始化为 0。这样一开始 ΔW = BA = 0,模型行为与原模型完全相同,避免了随机初始化可能带来的不稳定。
关键参数
秩 (r)
8 ~ 64
r 越大,表达能力越强,参数越多。典型值:r=8 或 r=16。复杂任务可用 r=32
缩放因子 (α)
16 ~ 256
实际更新为 ΔW = (α/r) × BA,使得权重变化的幅度与 r 无关,便于超参数迁移
目标层
Q, V
通常应用到 attention 的 Q 和 V 投影。K、输出投影可选。不通常用于 MLP
Dropout
0.05 ~ 0.1
在 LoRA 层应用 dropout,防止过拟合,尤其重要当数据量小时
LoRA 权重分解示意
为什么有效:低秩假设
LoRA 的成功源于一个重要的观察:大模型在微调过程中的权重更新 ΔW 具有低秩结构。
- 论文在多个任务上验证:有效秩 (intrinsic dimensionality) 远小于 min(d_out, d_in)
- 直观理解:预训练学到了通用特征,微调只需要小的适配性改动,这些改动集中在低维子空间内
- 与 Lottery Ticket Hypothesis 相关:只有网络的一部分对下游任务真正关键
实现细节与代码
from peft import LoraConfig, get_peft_model
# LoRA 配置
lora_config = LoraConfig(
r=8, # 秩
lora_alpha=16, # 缩放: α/r = 16/8 = 2
target_modules=["q_proj", "v_proj"], # 目标层
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 包装模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b"
)
model = get_peft_model(model, lora_config)
# 统计参数
model.print_trainable_parameters()
# trainable params: 4,194,304 ||
# all params: 6,738,415,616 ||
# trainable%: 0.06%
# 训练
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-4
)
for batch in train_loader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
推理时的合并
W_final = W₀ + (α/r) × B × A
这是 LoRA 的关键优势:训练时只存储 B 和 A,推理时可以将它们合并到 W₀ 中,零推理延迟和零额外内存成本。多个 LoRA 适配器甚至可以动态切换。
面试常问
Q: 为什么 LoRA 的 B 初始化为 0,A 初始化为随机值?
A: 如果两者都随机初始化,ΔW 一开始就不为 0,会破坏预训练权重的初始效应。初始化 B=0, A~N(0,σ²) 保证 ΔW 从 0 开始,让模型在微调初期还是 "接近原模型",训练更稳定。
面试常问
Q: 如何选择 LoRA 的秩 r?
A: 通常 r=8 对大多数任务足够。如果任务复杂度高(例如复杂推理、代码生成),可尝试 r=16 或 r=32。较小的数据集用 r=8 避免过拟合;大数据集可适当增加。不建议 r>64,回报边际递减。
● Quantization
QLoRA & DoRA
更进一步的参数高效方法:4-bit 量化与方向分解
QLoRA:量化 + LoRA
QLoRA 在 LoRA 的基础上,对基础模型参数进行 4-bit 量化,而 LoRA 适配器保持 FP16。这样做能在单块 48GB GPU 上微调 65B 模型。
三个关键技术
1
NF4 量化 (NormalFloat4)
基于正态分布的 4-bit 量化。权重按分布分成 16 个等概率的 bucket,相比均匀量化信息损失更少
2
双重量化 (Double Quantization)
对量化常数本身也进行量化。NF4 后的每 32 个权重共享一个缩放因子,该缩放因子也被量化,进一步节省内存
3
分页优化器状态 (Paged Optimizer)
当 GPU 内存溢出时,自动将优化器状态 (Adam 的 m, v) 卸载到 CPU,需要时再加载,实现动态的 GPU↔CPU 内存交换
Model(4-bit) ≈ 70B / 8 = 8.75 GB | LoRA(FP16) ≈ 0.2 GB | Optimizer ≈ 2-4 GB
代码示例
from transformers import AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
from bitsandbytes.nn import Linear4bit
# 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True # 双重量化
)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-70b-hf",
quantization_config=bnb_config,
device_map="auto"
)
# LoRA 配置
lora_config = LoraConfig(
r=64,
lora_alpha=128,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05
)
model = get_peft_model(model, lora_config)
# 训练时设置 paged optimizer
training_args = TrainingArguments(
optim="paged_adamw_32bit", # 分页优化器
...
)
DoRA:方向分解的 LoRA
DoRA (Weight-Decomposed LoRA) 进一步改进 LoRA。核心思想:将权重分解为幅度和方向,只对方向应用 LoRA 适配。
W = m · (V / ||V||)
优势
性能更好
多个基准上,DoRA 超过 LoRA,特别是在较小秩时效果明显
额外成本
轻微
多维护一个标量 m,计算上没有显著增加
推理合并
支持
同样可以在推理前合并,无额外推理成本
适用场景
通用
可替代 LoRA,尝试 DoRA 通常有收益
面试常问
Q: QLoRA 相比 LoRA 的优势和劣势?
A: 优势:可在单块 GPU 上微调 70B 模型(LoRA 无法做到)。劣势:量化会引入信息损失,训练速度稍慢(量化/反量化开销),最终精度可能略低于全精度 LoRA。实践中,QLoRA 是资源受限情况下的最佳选择。
● Distributed
Data Parallel (DP/DDP)
最简单的分布式训练方法:多 GPU 上训练同一个模型
数据并行是分布式训练的基础。核心思想:每块 GPU 持有完整模型副本,处理不同的数据批次,然后同步梯度。
DP vs DDP
| 特性 |
DP (DataParallel) |
DDP (DistributedDataParallel) |
| 结构 |
单进程,多线程分发 |
多进程,独立数据装载和计算 |
| 模型位置 |
主要在 GPU 0 |
每个 GPU 独立副本 |
| 梯度同步 |
每个 batch 后,在 GPU 0 gather 梯度再分发 |
Ring AllReduce,全连接同步(高效) |
| 扩展性 |
4-8 GPU 时勉强可用,多于 8 个性能下降 |
可扩展至数百 GPU,通信开销线性增长 |
| 易用性 |
代码改动最少,只需一行 |
需要修改启动方式和代码(略显复杂) |
| 通信量 |
高,collect 和 scatter 开销大 |
低,AllReduce 优化充分 |
DDP 训练流程 (AllReduce 同步梯度)
DDP 代码实现
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler
# 初始化进程组
dist.init_process_group(backend="nccl")
# 设置当前进程的 GPU
rank = dist.get_rank()
world_size = dist.get_world_size()
torch.cuda.set_device(rank)
# 创建模型并包装为 DDP
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
model = model.cuda()
model = DDP(model, device_ids=[rank])
# DistributedSampler 确保不同 GPU 获得不同数据
train_sampler = DistributedSampler(
dataset,
num_replicas=world_size,
rank=rank,
shuffle=True
)
train_loader = DataLoader(
dataset,
sampler=train_sampler,
batch_size=batch_size
)
# 标准训练循环
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
for epoch in range(num_epochs):
train_sampler.set_epoch(epoch) # 重要!
for batch in train_loader:
outputs = model(input_ids=batch['input_ids'])
loss = outputs.loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 清理
dist.destroy_process_group()
# 启动命令:
# torchrun --nproc_per_node=8 train.py
梯度分桶 (Gradient Bucketing)
DDP 提供梯度分桶机制:将梯度分组,每组计算完后立即通信,而不是等所有梯度计算完再通信。这样能在计算和通信之间形成流水线,降低闲置时间。
面试常问
Q: 为什么 DDP 比 DataParallel 快这么多?
A: DP 在每个 batch 后必须将梯度 gather 到 GPU 0,再分发回去,造成 GPU 0 成为瓶颈。DDP 使用 Ring AllReduce,每块 GPU 只和相邻两块通信,通信量均衡,不存在中心化瓶颈。对于 8 GPU,DDP 通常比 DP 快 3-5 倍。
● FSDP
FSDP / ZeRO (关键面试内容)
超大模型训练的利器:在多 GPU 间分片所有参数
FSDP (Fully Sharded Data Parallel) 和 DeepSpeed 的 ZeRO 是同一思想的不同实现。核心:将模型参数、梯度和优化器状态分片到所有 GPU,每块 GPU 只存储 1/N 的数据。
ZeRO 的三个阶段
| 阶段 |
分片对象 |
内存 / GPU (7B) |
通信 / step |
适用场景 |
| ZeRO-1 |
优化器状态 |
84 GB / 8 = 10.5 GB |
2×model |
可用,8 GPU 最小需求 |
| ZeRO-2 |
优化器 + 梯度 |
56 GB / 8 = 7 GB |
2×model |
推荐,大多数场景 |
| ZeRO-3 / FSDP |
所有(参数 + 梯度 + 优化器) |
112 GB / 8 = 14 GB |
3×model |
极大模型 (70B+),多节点 |
ZeRO-3 内存 = (参数 + 梯度 + 优化器) / num_gpus + 激活值
FSDP 工作流程 (最重要的图表)
FSDP / ZeRO-3 训练一轮 (4 GPU 示例)
DDP vs FSDP 对比
| 特性 |
DDP |
FSDP / ZeRO-3 |
| 内存占用 |
3-4× model_size |
1-2× model_size (分布到多 GPU) |
| 通信量 |
2× model_size / step |
3× model_size / step (AllGather + ReduceScatter) |
| 最大支持模型 |
~13B on 8×80GB GPU |
~70B on 8×80GB GPU |
| 节点间扩展 |
较好,但通信成为瓶颈 |
更优,已在 1000+ GPU 上验证 |
| 实现复杂度 |
简单 |
需要梯度累积、checkpointing 等技巧 |
FSDP 代码示例
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import CPUOffload
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy
# 初始化分布式
dist.init_process_group("nccl")
rank = dist.get_rank()
torch.cuda.set_device(rank)
# 加载模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-70b")
# FSDP 包装
model = FSDP(
model,
auto_wrap_policy=size_based_auto_wrap_policy,
cpu_offload=CPUOffload(offload_params=False),
device_id=torch.cuda.current_device()
)
# 训练
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)
for batch in dataloader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
dist.destroy_process_group()
面试常问
Q: FSDP 为什么要 AllGather 参数再丢弃?不能直接分布式计算吗?
A: Transformer 中各层是顺序的,当前层需要完整的参数才能计算。AllGather 和 ReduceScatter 的"浪费"是必要的,因为模型的计算顺序决定了这一点。通过参数分片换来的是内存节省,权衡是通信增加 50% (3×vs 2×)。
面试常问
Q: 什么时候选 FSDP,什么时候选 DDP?
A: 模型 <13B 且 GPU 充足 → DDP;模型 ≥13B → FSDP;跨节点训练大模型 → FSDP;GPU 少、内存紧张 → 配合 gradient checkpointing 和 activation checkpointing。FSDP 是现代大模型训练的标准选择。
● Parallelism
Tensor Parallelism (TP)
在单个层内分片权重矩阵,适合节点内通信
Tensor Parallelism 将单个线性层的权重矩阵按行或列分割,分配到多块 GPU。和 Data Parallelism 不同的是,TP 分割的是模型计算,不是数据。
列并行 vs 行并行
Y = XA,A = [A₁, A₂]ᵀ → Y = [XA₁, XA₂] (列并行,无通信)
Y = XA,A 按行分割 → Y = AllReduce([XA₁, XA₂]) (行并行,需 AllReduce)
行并行
AllReduce
输出合并需 AllReduce,通信一次
通信位置
同层间
TP 通信在同一节点(NVLink),延迟低
与 DP 组合
兼容
TP=8 within node, DP=8 across nodes
Megatron-LM 的列并行
TP 在 Self-Attention 中的应用
面试常问
Q: TP 和 DP 的关键区别?何时用 TP?
A: DP 分割数据,每块 GPU 有完整模型;TP 分割模型,每块 GPU 有模型的一部分。TP 用于单个节点内(通信延迟低),适合超大模型(70B+ 无法在单 GPU 放下)。节点间用 DP 或 PP 更合适。
● Pipeline
Pipeline Parallelism (PP)
按层分割模型,跨节点训练大型模型
Pipeline Parallelism 将模型的不同层分配到不同 GPU。GPU 0 执行前 16 层,GPU 1 执行后 16 层等。优点是通信量少,但引入气泡时间 (bubble time)问题。
气泡问题与微批处理
Bubble Ratio = (p - 1) / (m + p - 1)
Pipeline Schedule (不同策略的时间线)
面试常问
Q: PP 的气泡时间怎么理解,如何最小化?
A: 气泡来自 GPU 闲置。GPipe 中,GPU 3 必须等待 GPU 0-2 完成才能计算,导致气泡。通过 1F1B (One Forward One Backward) 调度,交错前向和反向计算,可以减少闲置。气泡比 = (p-1)/(m+p-1),增加 m (微批数) 可减少气泡。
● Advanced
3D 并行与实战 (70B 模型训练)
结合 TP、PP、DP 三种技术,实现最大的训练效率
单一的并行策略往往无法满足超大模型的训练需求。3D 并行结合了 Tensor Parallelism (TP)、Pipeline Parallelism (PP) 和 Data Parallelism (DP),充分利用多节点 GPU 集群的计算和通信能力。
3D 并行的设计原则
TP
节点内通信(NVLink 高带宽)
8 块 GPU 在同一节点内,通过 NVLink 通信,带宽最高(600 GB/s)
PP
跨节点通信(较低频率)
逐层传输激活值,通信量受管道深度和微批数影响
DP
模型副本同步(AllReduce)
AllReduce 梯度,分散到多个 TP-PP 组合
具体例子:训练 70B 模型
总 GPU 数
64
8 节点 × 8 GPU/节点
TP 大小
8
同节点内,整个节点的 8 块 GPU
DP 大小
4
4 个 (8×8 TP, 2 PP) 的分组
TP × PP × DP = 8 × 2 × 4 = 64
3D 并行的拓扑 (64 GPU, 8 节点示例)
Megatron-LM 训练配置
torchrun --nproc_per_node=8 --nnodes=8 \
pretrain_gpt.py \
--tensor-model-parallel-size 8 \
--pipeline-model-parallel-size 2 \
--data-parallel-size 4 \
--global-batch-size 512 \
--micro-batch-size 2 \
--num-layers 80 \
--hidden-size 4096 \
--num-attention-heads 32 \
--seq-length 2048 \
--max-position-embeddings 2048 \
--train-iters 500000 \
--log-interval 100
面试常问
Q: 为什么 3D 并行要选择 TP=8 (全节点)?
A: NVLink 提供 600 GB/s 的带宽,而节点间网络 (Ethernet) 只有 100-400 GB/s。TP 通信在节点内,不受网络延迟影响。选择 TP=8 可最大化节点内高效通信;跨节点的 PP 和 DP 通信相对频率低,可接受较低的带宽。
面试常问
Q: 3D 并行中,哪个维度最容易成为瓶颈?
A: PP 跨节点通信是最大瓶颈。TP 在节点内,DP AllReduce 虽然跨节点但可优化。PP 激活的传输量与序列长度成正比,且涉及多个微批处理的依赖关系。实践中,PP=2 相对安全;PP>4 需要精心优化。
● Precision
混合精度训练 (Mixed Precision)
FP32 vs FP16 vs BF16,权衡速度、精度和内存
三种精度的对比
| 格式 |
位数 |
尾数 |
指数 |
动态范围 |
优点 |
缺点 |
| FP32 |
32 |
23 |
8 |
±10^±38 |
精度高,无数值溢出 |
内存多,计算慢 |
| FP16 |
16 |
10 |
5 |
±10^±4 |
内存省,速度快 (Tensor Core) |
范围小,梯度易下溢 |
| BF16 |
16 |
7 |
8 |
±10^±38 |
范围宽(同 FP32),无需 loss scaling |
精度较低,但实践中足够 |
AMP (Automatic Mixed Precision)
前向: FP16 | 主权重/优化器: FP32 | 梯度: FP16,缩放处理下溢
1
前向传播 (FP16)
使用 FP16 计算,减少内存占用和计算时间
2
损失缩放 (Loss Scaling)
loss × 2^k,防止 FP16 梯度下溢到 0;反向后除以 2^k 恢复
3
梯度计算 (FP16)
反向传播得到 FP16 梯度,然后转换为 FP32
4
参数更新 (FP32)
在 FP32 主权重上执行优化步骤,提高精度
代码示例
from torch.cuda.amp import autocast, GradScaler
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
scaler = GradScaler()
for batch in train_loader:
# 前向: FP16
with autocast(dtype=torch.float16):
outputs = model(**batch)
loss = outputs.loss
# 梯度缩放
scaler.scale(loss).backward()
# 优化器步骤(损失缩放自动处理)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
# BF16 (推荐)
with autocast(dtype=torch.bfloat16):
outputs = model(**batch)
loss = outputs.loss
# BF16 无需 loss scaling
内存节省
~50%
FP16 占 FP32 的 50%,激活值也减半
计算加速
2-3×
Tensor Core 对 FP16 优化,特别是 NVIDIA GPU
精度损失
极小
大多数任务上,FP16+AMP 与 FP32 结果相近甚至更好
推荐方案
BF16
无需 loss scaling,更稳定,现代框架支持
面试常问
Q: 为什么 FP16 需要 loss scaling,BF16 不需要?
A: FP16 的动态范围只有 ±10^±4,梯度极小(如 10^-5)会下溢到 0,loss scaling 通过放大损失来防止这个问题。BF16 指数位有 8 位(同 FP32),范围达 ±10^±38,可直接表示微小梯度,无需 scaling。
面试常问
Q: 什么时候应该用 FP32,什么时候用 FP16/BF16?
A: FP32 用于精度要求极高的任务(如科学计算)或不支持混合精度的框架。FP16+AMP 用于追求速度和内存的场景。BF16 是最优选择,自动缩放,稳定性好,所有现代 GPU 都支持。
● Framework
DeepSpeed 生态与优化
微软的分布式训练框架,大规模模型训练的实际工具
DeepSpeed 是微软提供的分布式训练框架,集成了 ZeRO、梯度累积、CPU 卸载等优化,大幅简化了大模型训练。
核心功能
| 功能 |
说明 |
应用场景 |
| ZeRO 优化 |
ZeRO-1/2/3 分片优化器、梯度、参数 |
大模型训练,内存瓶颈 |
| ZeRO-Offload |
优化器状态卸载到 CPU,CPU 执行参数更新 |
GPU 内存 <60GB,需要空闲 CPU 内存 |
| ZeRO-Infinity |
参数和优化器卸载到 NVMe SSD |
极大模型 (1T+),超大集群 |
| Gradient Checkpointing |
丢弃激活值,反向时重新计算 |
内存紧张,可接受 20-30% 速度下降 |
| Gradient Accumulation |
累积多个 batch 的梯度,减少通信 |
大 batch 无法放入 GPU,模拟更大 batch |
DeepSpeed 配置示例
# ds_config.json
{
"train_batch_size": 512,
"train_micro_batch_size_per_gpu": 2,
"gradient_accumulation_steps": 8,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 1e-5,
"betas": [0.9, 0.999],
"eps": 1e-8,
"weight_decay": 0.01
}
},
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": 0,
"warmup_max_lr": 1e-5,
"warmup_num_steps": 1000
}
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
},
"offload_param": {
"device": "cpu",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": 1e6,
"stage3_prefetch_bucket_size": 5e7,
"stage3_param_persistence_threshold": 1e6
},
"activation_checkpointing": {
"partition_activations": true,
"cpu_checkpointing": false,
"contiguous_memory_optimization": true,
"number_checkpoints": 4
},
"bf16": {
"enabled": true
},
"gradient_clipping": 1.0,
"wall_clock_breakdown": true
}
# 启动命令
deepspeed --num_gpus=8 train.py \
--deepspeed ds_config.json
DeepSpeed 的关键优势
1
开箱即用
通过 JSON 配置,无需改动训练代码,适配所有主流框架
2
显式性能指标
wall_clock_breakdown 输出详细的计算、通信、数据加载时间,便于瓶颈分析
3
深度集成
与 Hugging Face Transformers、PEFT 深度集成,广泛应用于工业界
面试常问
Q: ZeRO-Offload 和 ZeRO-Infinity 的区别?
A: ZeRO-Offload 将优化器状态卸载到 CPU,利用 CPU 内存和闲置 CPU 算力进行参数更新。ZeRO-Infinity 进一步支持将模型参数卸载到 NVMe SSD,适合训练超大模型(1T+)。Infinity 更激进,通信更多,适合容忍较高延迟的场景。
● Summary
总结与面试题精选
微调和分布式训练的完整知识体系与高频面试题
方法对比表 (全景图)
| 方法 |
参数量 |
内存占用 |
通信量 |
推理成本 |
适用场景 |
| Full FT |
100% |
3-4× model |
2× |
无 |
充足数据,大任务 |
| LoRA |
0.06% (7B, r=8) |
3-4×model + LoRA 很小 |
2× |
无 (可合并) |
小数据,多任务 |
| QLoRA |
0.06% + 4-bit model |
model/8 + LoRA |
2× |
无 |
资源极限,65B+ on 48GB |
| DP/DDP |
100% |
3-4× model / num_gpu |
2× × num_gpu |
无 |
13B 以下,多 GPU |
| FSDP |
100% |
model/num_gpu + 激活 |
3× × num_gpu |
无 |
70B+,多节点 |
| TP |
100% / TP_size |
model/TP_size |
少 (节点内) |
无 |
超大模型,同节点 |
| 3D |
100% |
model/(TP × DP) |
混合,优化最多 |
无 |
工业界标准,大规模 |
高频面试题 (12+)
面试题 1
Q: 简述 LoRA 的核心思想,为什么它有效?
A: LoRA 假设权重更新 ΔW 具有低秩结构,用 BA 近似(B ∈ R^{d×r}, A ∈ R^{r×k},r ≪ min(d,k))。这是因为微调时的权重变化集中在低维子空间,不需要更新所有参数。初始化 A~N(0,σ²), B=0,保证训练初期 ΔW=0。推理时 W = W_0 + BA,无额外成本。
面试题 2
Q: LoRA 的秩 r 应该如何选择?
A: 通常 r=8 对大多数任务足够。选择原则:(1) 数据小 (< 10K) 用 r=8,防过拟合;(2) 复杂任务 (推理、代码) 可用 r=16/32;(3) 不建议 r>64,回报边际递减。通常通过小规模实验或消融研究确定。
面试题 3
Q: 训练 7B 模型,如果只有 4 块 A100 GPU,应该用什么策略?
A: (1) 首选 LoRA,单块 GPU 可训练,无需分布式;(2) 若需要 Full FT,用 DDP + Gradient Checkpointing + BF16;(3) 若内存仍紧张,考虑 QLoRA(虽然 7B 不必要)。推荐方案:DDP (4 GPU) + LoRA + BF16,batch_size 可达 32+。
面试题 4
Q: DDP 和 DP 的关键差别,为什么 DDP 快那么多?
A: DP 在主 GPU 上 gather 梯度再 scatter,GPU 0 成为瓶颈,AllReduce 开销大。DDP 用 Ring AllReduce,每块 GPU 只和相邻两块通信,通信均衡分散,无中心化瓶颈。8 GPU 时,DDP 通常比 DP 快 3-5 倍。DDP 也支持多进程,每进程独立数据加载,效率更高。
面试题 5
Q: FSDP / ZeRO-3 的内存优势如何体现?为什么通信增加 50%?
A: ZeRO-3 将参数分片到 N 块 GPU,每块持有 1/N 参数 + 1/N 梯度 + 1/N 优化器,内存直接除以 N。代价是 AllGather 前向时需完整参数,AllGather + ReduceScatter 通信变成 3×(DDP 是 2×)。这是空间-时间权衡:换更小的内存获得更多通信。
面试题 6
Q: Pipeline Parallelism 的气泡问题怎么理解,如何优化?
A: 气泡指 GPU 闲置时间。GPipe 中,GPU 后面的处于等待前面 GPU 完成的状态。1F1B schedule 通过交错前向和反向,让后面的 GPU 在等待前向时进行反向,减少闲置。气泡比 = (p-1)/(m+p-1),p=stages,m=micro-batches。增加 m 可减小气泡比,但激活值占用增加。
面试题 7
Q: 什么时候选 FSDP,什么时候选 DDP?
A: 模型 <13B 且内存充足 → DDP;模型 ≥13B → FSDP;跨节点多卡 → FSDP。DDP 更简单,通信少,适合中等模型;FSDP 内存利用高,适合大模型。FSDP 是现代主流选择,被 Llama、GPT 等所采用。
面试题 8
Q: TP、PP、DP 三种并行方式各有什么通信特点?
A: TP (Tensor):同层内列/行并行,AllReduce 仅在 TP 组内,高频高带宽(NVLink);PP (Pipeline):逐层激活传输,跨节点,低频但批量大;DP (Data):AllReduce 梯度,跨节点,每步都有。实际中,TP 在节点内(600GB/s),PP+DP 跨节点(100-400GB/s),需精心设计避免网络成为瓶颈。
面试题 9
Q: 训练 70B 模型,怎么分配 TP、PP、DP 大小?
A: 原则:TP = 节点 GPU 数(8),PP = 节点间跳数(通常 2-4),DP = 剩余 (TP × PP × DP = 总 GPU)。例 64 GPU (8 节点):TP=8, PP=2, DP=4。避免 PP>4(气泡严重)。最终内存 ≈ (参数+梯度+优化器)/TP + 激活值,应在 15-20GB/GPU 之内。
面试题 10
Q: FP16 为什么需要 loss scaling,BF16 不需要?
A: FP16 动态范围仅 ±10^±4,极小梯度(如 10^-5)会下溢为 0,loss scaling 放大损失避免下溢,反向后缩放回来。BF16 指数位 8 位(同 FP32),范围 ±10^±38,可直接表示微小梯度。实际中,BF16 更稳定,不需特殊处理,是首选。
面试题 11
Q: Gradient Checkpointing 的取舍是什么?
A: 优势:激活值只保存某些层,内存节省 30-40%;劣势:反向时需重算激活,速度下降 20-30%。用于内存紧张时,如 seq_len=4096 或 batch_size 无法再减。PyTorch 中 torch.utils.checkpoint 易集成。权衡:宁可速度慢也要能训完,比 OOM 强。
面试题 12
Q: DeepSpeed vs PyTorch Native 分布式,选哪个?
A: PyTorch Native (DDP/FSDP):轻量,无额外依赖,学习成本低;DeepSpeed:功能更全(ZeRO、Offload、优化器集成),JSON 配置灵活,生产环境常用。建议:学习 DDP,理解分布式概念;实际项目用 DeepSpeed,特别是大模型训练。两者可共存,FSDP 越来越强,Hugging Face Trainer 支持。
选择决策树
微调 + 分布式训练方案选择
学习资源与进阶方向
- 论文: LoRA (Hu et al.), DeepSpeed (Rasley et al.), FSDP 官方文档
- 框架: PyTorch DDP/FSDP, DeepSpeed, Megatron-LM, vLLM (推理优化)
- 进阶: 量化感知训练 (QAT)、多 GPU 通信优化、推理部署优化
- 实战: 在真实数据集上复现 LoRA 微调、DDP 分布式训练、FSDP 大模型训练