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)
0 50GB 100GB 150GB FP32 FP16+AMP LoRA (frozen) QLoRA (4-bit) 模型参数 梯度 优化器状态 LoRA 参数
面试常问

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
2
前向传播
在新的下游任务数据上计算损失函数
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
W₀ ∈ ℝ^(d_out × d_in) 冻结 | B ∈ ℝ^(d_out × r), A ∈ ℝ^(r × d_in), r ≪ min(d_out, d_in)

在论文中,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 权重分解示意
输入: x d_in W₀ (冻结) d_in × d_out + B d_out × r @ A r × d_in 输出 y = x·W₀ᵀ + x·Aᵀ·Bᵀ d_out 参数数量对比 完整权重: d_in × d_out = 16,384 × 12,288 ≈ 200M LoRA: (d_out × r) + (r × d_in) = (12,288×8) + (8×16,384) ≈ 230K 压缩比: 200M / 230K ≈ 870 倍

为什么有效:低秩假设

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
总计 ≈ 15-20 GB,单块 GPU 可行

代码示例

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||)
m: 幅度 (标量) | V: 方向 (向量) | 应用 LoRA 到 V,不改变 m
优势
性能更好
多个基准上,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 同步梯度)
GPU 0 Model Batch 0 GPU 1 Model Batch 1 GPU 2 Model Batch 2 ∇ Sync ∇ Sync 通信量 每轮:2 × model_size (send + receive)

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 + 激活值
激活值通常与 num_gpus 无关,是 BS×seq_len 的函数

FSDP 工作流程 (最重要的图表)

FSDP / ZeRO-3 训练一轮 (4 GPU 示例)
Forward Pass (1/4 参数分别被 AllGather) GPU 0 Layer 0-7 AllGather GPU 1 Layer 8-15 AllGather GPU 2 Layer 16-23 AllGather GPU 3 Layer 24-31 AllGather 第 1 步:AllGather 参数到 GPU 0 所有参数被同步到所有 GPU,进行完整的前向计算 激活值被保存用于反向传播 前向计算完成 loss = model(input_ids) 第 2 步:丢弃参数,释放内存 AllGather 得到的参数立即丢弃(除了当前层),只保留梯度和激活值 第 3 步:Backward (再次 AllGather + ReduceScatter 梯度) GPU 0 ReduceScatter ∇ GPU 1 ReduceScatter ∇ GPU 2 ReduceScatter ∇ GPU 3 ReduceScatter ∇ 第 4 步:各 GPU 独立更新其分片参数 W ← W - lr·∇,每块 GPU 更新自己持有的 1/N 的参数

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₂] (列并行,无通信)
vs
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 中的应用
Input W_q (full) 冻结展示 Q₀ (GPU 0) Q₁ (GPU 1) Q₂ (GPU 2) Attention 本地计算,输出分割 Out₀ (GPU 0) Out₁ (GPU 1) Out₂ (GPU 2) 通信模式 列并行:W = [W₀, W₁, W₂] 按列分割,计算无通信 行并行:计算后需 AllReduce 同步结果 通常混合使用,Q/V 列并行,输出层行并行
面试常问

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)
p = stages (GPU 数), m = micro-batches (微批处理数)
Pipeline Schedule (不同策略的时间线)
GPipe (m=4, p=4) GPU 0 GPU 1 GPU 2 GPU 3 浪费时间 1F1B Schedule (m=8, p=4) — 更优 GPU 0 GPU 1 GPU 2 GPU 3 Forward Backward 对比 GPipe: 简单但气泡多,多路径依赖 1F1B: 交错前向和反向,气泡比 = (p-1)/(m+p-1),推荐
面试常问

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
PP 大小
2
2 节点,35 层 / 节点
DP 大小
4
4 个 (8×8 TP, 2 PP) 的分组
TP × PP × DP = 8 × 2 × 4 = 64
每个 GPU 的内存 = (参数 + 梯度 + 优化器) / TP + 激活 ≈ 10-15 GB (可行)
3D 并行的拓扑 (64 GPU, 8 节点示例)
Node 0 TP=8 Stage 0 → Node 1 (PP) Node 1-7 8 个节点 × 8 GPU 并行策略 绿色 (TP=8): 同节点 NVLink 通信 | 蓝色 (PP=2): 节点间激活传输 | 黄色 (DP=4): AllReduce 梯度

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 充足数据,大任务
LoRA 0.06% (7B, r=8) 3-4×model + LoRA 很小 无 (可合并) 小数据,多任务
QLoRA 0.06% + 4-bit model model/8 + LoRA 资源极限,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 支持。

选择决策树

微调 + 分布式训练方案选择
模型大小? <13B ≥ 13B 单 GPU? 多 GPU? LoRA DDP DDP 资源充足? 资源紧张? FSDP 3D QLoRA FSDP 补充说明 • 混合精度: 默认 BF16 • Gradient Checkpointing: 内存紧张时启用 • DeepSpeed: 生产环境优先选择 • LoRA 推荐优先: 简单、高效、多任务友好

学习资源与进阶方向

  • 论文: LoRA (Hu et al.), DeepSpeed (Rasley et al.), FSDP 官方文档
  • 框架: PyTorch DDP/FSDP, DeepSpeed, Megatron-LM, vLLM (推理优化)
  • 进阶: 量化感知训练 (QAT)、多 GPU 通信优化、推理部署优化
  • 实战: 在真实数据集上复现 LoRA 微调、DDP 分布式训练、FSDP 大模型训练