9.7 动手:为 Lab 3 选一套并行策略
前面六节给了公式、清单与陷阱,但真正动手时面对的问题只有一个:手上这台机器(1–8 张卡)、这个模型、这份数据,我该敲哪几条命令?
这一节把前面所有结论压缩成一条五步决策流程,并用两组具体硬件把账算完:① 单张 24 GB 卡上的 QLoRA;② 8 张 80 GB 卡上的全参微调。最后给出配置对照表、吞吐验收方法与排错清单,并接回 Lab 3。
9.7.1 五步决策流程
| 步 | 问题 | 用什么算 | 决策规则 |
|---|---|---|---|
| ① 显存估算 | 模型状态要多少?激活要多少? | (9.1.2);激活按 token 数与层数(9.1.3) | 得到两个数:状态 、激活 (都要留 15%–30% 余量) |
| ② 是否分片 | 单卡可用显存吗? | (9.2.4) | 差距小 → ZeRO-1/2;差距大 → ZeRO-3 / FSDP2;能用 PEFT 就别分片 |
| ③ 是否 TP / PP | 单层放得下吗?层数够分吗? | 单层参数与激活量;层数是否被 整除 | 单层放不下 → TP(≤ 单机卡数);只是整体大 → 优先 PP 或 ZeRO-3 |
| ④ 批大小与梯度累积 | 一次能塞多少 token?等效批要多大? | 先定 (受激活限制)与 (受学习率配方限制),再解 | |
| ⑤ 精度与重计算 | 用 bf16 还是 fp8?开不开重计算? | 9.5.1、9.5.3 | bf16 为默认;激活紧张先开选择性重计算;有 FP8 硬件且框架支持再考虑 fp8 |
预训练一次实验要几天,配错了损失很大但至少过程漫长;微调(尤其 Lab 3 这类任务)常常几分钟就能跑完一轮,于是很多人直接「试参数」——试出来的配置往往既没跑满硬件、又无法解释。
五步流程真正的价值是给出可解释的起点:每个数字从哪里来、哪个约束先被撞到、下一步该改哪个旋钮。这与 工具箱 的「先测基线再找瓶颈」是同一个方法论,只是这里的「基线」是算出来的而不是跑出来的。
一句工程经验:先用最小的配置把链路跑通(10 步、小数据),再按五步放量。绝大多数「跑不起来」与算法无关,而是环境、数据格式与显存配置的组合问题(Lab 3 排错清单)。
9.7.2 模型参数核对:把清单变成数字
动手前先把模型的结构参数抄下来,后面所有账都要用到它们:
| 符号 | 含义 | 典型来源 | 例(7B 级开源模型) |
|---|---|---|---|
| 总参数量 | 权重文件 / model.num_parameters() |
||
| 层数 | 配置的 num_hidden_layers |
32 | |
| 隐藏维 | hidden_size |
4096 | |
| 注意力头数 | num_attention_heads |
32 | |
| KV 头数 | num_key_value_heads(GQA 时小于 ) |
8 | |
| 词表大小 | vocab_size |
151,936 | |
| 序列长度(截断后) | 数据与 max_length |
2048 |
为什么要核对 与 :GQA 让 KV cache 与 投影的参数量按 缩小,张量并行按头切分时要按 与 分别检查整除性(9.3.3);词表 决定了输出头与 embedding 的参数量,它们常是首尾流水线段的显存大户(9.4.5 的负载均衡讨论)。
模型:,,,。硬件:单张 24 GB 卡。任务:在自定义指令数据上微调。按五步流程给出三种可行配置并比较。
① 显存估算(全参口径)。 单看这一项就超过 24 GB —— 全参微调在单卡上不可行。
激活:。 时 GB; 时约 GB。
② 是否分片。 目标 24 GB 卡:可用预算按 (系统与碎片) GB 计。
| 方案 | 模型状态 | 激活() | 合计 | 判断 |
|---|---|---|---|---|
| 全参 + 混合精度 | GB | GB | GB | ✗ 不可行 |
| 全参 + 8-bit 优化器 | GB | GB | GB | ✗ 仍超 |
| 全参 + 8-bit + CPU offload( 上主机) | GB | GB | GB | △ 勉强,且每步 PCIe 往返使速度显著下降 |
| LoRA(bf16 主干 + 适配器) | GB + GB | GB() | GB | ✓ 可行且余量充足 |
| QLoRA(4 bit 主干 + LoRA) | GB + GB | GB | GB | ✓✓ 最宽裕, 可继续放大 |
(LoRA 主干按 bf16 计 GB;QLoRA 主干按 4 bit 计 GB;适配器按 例 9.5.4 的口径 GB。)
④ 批大小与梯度累积。 以 QLoRA、 为例(激活 GB,仍有余量): 目标 。即「每 16 个 micro-batch 更新一次」。
⑤ 精度与重计算。 主干 bf16(QLoRA 为 4 bit 存储 + bf16 计算);适配器 fp32;先不开重计算,若 提到 16 后 OOM 再开选择性重计算。
结论(推荐配置):QLoRA + + target_modules=[q_proj, v_proj] + 、、bf16 计算、选择性重计算按需开。
模型:,,,,。硬件:8 张 80 GB 卡(同一节点、NVLink 互联)。目标:全参微调,等效批 256 条序列。按五步流程给出配置。
① 显存估算。 GB;若 ,激活 字节 GB(口径:34 字节/(token·hidden 元素),见 例 9.1.2)。
② 分片。 单卡 80 GB, → 必须分片。
- ZeRO-1(8 卡): GB/卡;
- ZeRO-2(8 卡): GB/卡。
取 ZeRO-2: GB(状态)+ GB(激活)+ 通信缓冲 ≈ 45 GB/卡,在 80 GB 卡上仍有约 40% 余量(若要继续放大 ,先开选择性重计算把激活压下来)。
③ 是否 TP / PP。 单层最大的是 FFN: 参数 bf16 下 GB,单卡轻松放下;层数 32,切成 2–4 段也整除。结论:不需要 TP,也不需要 PP —— 8 卡同节点时优先用更简单的方案(9.6.5 的配平原则:先满足硬约束再优化)。
④ 批大小与梯度累积。 目标 ,: 取 、(激活约 17 GB,安全);或 、(激活约 34 GB,接受它之前先开选择性重计算把激活压到十几 GB,且 小意味着单步更快)。
⑤ 精度与重计算。 bf16 参数与梯度 + fp32 主权重(混合精度),fp16 不必要(需损失缩放);先不开重计算,若把 提到 8192 或 提到 8,再开选择性重计算(9.5.3)。
学习率联动:等效批从单卡的 扩到 ,若原来是 64,则按线性缩放规则把学习率放大 4 倍并加 warmup(9.2.3);先跑 100 步验证损失曲线形态,再决定是否改用平方根缩放。
结论(推荐配置):
并行: DDP × ZeRO-2(8 卡同节点),不开 TP / PP
精度: bf16 参数与梯度 + fp32 主权重,动态损失缩放关闭(bf16)
批: B_micro = 4,K = 8,等效批 256(= 4 × 8 × 8)
重计算: 关闭(先),激活紧张时开 selective
优化器: AdamW,lr 按线性缩放 ×4 + warmup 3%
checkpoint:torch.distributed.checkpoint 分片保存(每 500–1000 步 + 结束时)
∎
9.7.3 LoRA / QLoRA 的显存账重算与配置
| 配置项 | 作用 | 对显存的影响 | 常用取值 |
|---|---|---|---|
| (秩) | 适配器容量 | 适配器参数量 (通常仍 <1%);影响很小 | |
target_modules |
挂在哪几个投影上 | 挂得越多,适配器参数越多( → 全部线性层约 ) | 起手 [q_proj, v_proj] |
lora_alpha |
缩放系数(常取 ) | 无 | |
lora_dropout |
正则 | 无 | (与主训练 dropout 一致) |
| 主干精度 | bf16 / 4 bit | 主干占用 → | 4 bit(QLoRA) |
max_length |
序列长度 | 激活 ,通常是最大旋钮 | 1024 起手,能放下再翻倍 |
账本重算( 为总参数): 对 、、64 个矩阵: GB(例 9.5.4)。剩下的全部预算归激活与 KV cache。
在 QLoRA 下,模型状态只剩 3–4 GB 量级,于是预算的主要部分给了激活。这带来一个反直觉的后果:LoRA 微调的瓶颈常常是「序列长度」而不是「模型大小」。
所以 LoRA 调优的旋钮顺序完全不同:
max_length(序列越长,激活线性增长;也是效果最直接的旋钮);B_micro与梯度累积(把批凑够,同时控制激活);- 选择性重计算;
- 最后才是 、
target_modules这类容量参数(它们几乎不影响显存)。
四步的顺序与全参训练一致:先解显存约束,再谈效果。区别只在于「哪一顶帽子戴着紧」。
# ── QLoRA 的最小可运行配置(单卡 24 GB 量级)──────────────────────────
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
model_id = "[基座模型标识]"
bnb = BitsAndBytesConfig(
load_in_4bit=True, # 4 bit 主干:主干占用 ≈ 0.5N 字节/参数
bnb_4bit_quant_type="nf4", # NF4 数据类型(QLoRA 配方)
bnb_4bit_use_double_quant=True, # 双重量化:再省一点缩放常数
bnb_4bit_compute_dtype=torch.bfloat16, # 计算精度用 bf16(不是 fp16,免损失缩放)
)
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb,
device_map={"": 0}) # 单卡
model = prepare_model_for_kbit_training(model) # 冻结主干、开梯度检查点等预处理
lora = LoraConfig(
r=8, lora_alpha=16, lora_dropout=0.05,
target_modules=["q_proj", "v_proj"], # 先只挂注意力投影;不够再加
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora)
model.print_trainable_parameters() # 期望:可训参数 < 0.2%
# 训练循环与第 1 讲完全一致,只是多了三件事:
# ① 只在 response 上算损失(mask 掉 prompt,否则模型学会复述问题)
# ② 梯度累积 K 步更新一次
# ③ 每 N 步打印 token/s、峰值显存与梯度范数(验收用,见 9.7.4)
9.7.4 吞吐预期与验证
| 指标 | 定义 | 怎么测 | 健康区间(示例,需按硬件校准) |
|---|---|---|---|
| 峰值显存 | 单步峰值分配 | torch.cuda.max_memory_allocated() |
留 ≥20% 余量;逼近上限就该降 或提 前先优化 |
| 每步耗时 | 一次优化器步骤的墙钟 | 计时 20–50 步取中位数(跳过前几步预热) | 波动 <10%;波动大说明数据管线或通信抖动 |
| 训练吞吐 | token/s 或 samples/s | tokens/step ÷ 每步秒数 |
与「单卡基线 × 卡数 × 扩展效率 」对账 |
| 扩展效率 | (定义 9.1.3) | 用同样的全局批跑 1 卡与 卡各 20 步 | 单节点 8 卡常见 0.7–0.9;跨机遇瓶颈会掉到 0.5 以下 |
| 对照:效果 | 微调前 vs 微调后 | 固定问题集 + 固定解码参数,比较任务指标 | 必须给出对照,否则无法归因 |
一个可复算的吞吐预期:、每卡每步 token、8 卡、单卡有效算力 FLOP/s(bf16、中等利用率)。单步计算量 FLOPs,故 若实测只有这个数的一半,按 9.7.5 的顺序排查(先看 GPU 利用率、再看通信占比、再看数据管线)。本计算只用于判断「是否离预期一个数量级」,不用于承诺任何绝对值(有效算力随模型 / 精度 / 内核质量变化,需实测校准)。
因为全局批变了,学习率的最优值、每步的样本数、收敛曲线都会变——此时吞吐差异与效率差异混在一起,无法归因。
三条对照规则:
- 测吞吐时固定全局批(改动卡数时同步调 或 );
- 测效果时固定全局批与调度(否则「更快的配置」可能只是「更短的训练」);
- 报告时必须写出:卡数、精度、、、、是否重计算、是否分片——这正是 工具箱 「一次训练的最小记录集」在并行场景下的展开。
9.7.5 排错清单
症状 A:启动就 OOM(加载模型时、或 optimizer.step() 后)。
| 检查 | 判据 | 处方 |
|---|---|---|
| 模型状态是否超了 | (或 )与卡容量比 | 分片(ZeRO-1/2/3、FSDP2)、8-bit 优化器、QLoRA |
| 优化器是否在最初就分配 | 有些实现会在第 1 步前预分配 | 提前用 max_memory_allocated 观测;必要时用 CPU offload |
| 是否用了 fp32 训练 | 若没开混合精度,参数与梯度都翻倍 | 开 torch.autocast + bf16 |
症状 B:前向能跑,反向 OOM。
| 检查 | 判据 | 处方 |
|---|---|---|
| 是激活超了(B 类,9.1.5) | 峰值出现在反向早期 | 梯度累积 + 减小 ;选择性重计算;长序列加序列并行 |
| 注意力是否被实体化 | 激活量里有 项 | 换成 FlashAttention 一类实现(2.6) |
| 是否有临时张量峰值 | 单步中间出现远高于稳态的尖峰 | 分块 / 融合;减小 bucket 大小 |
症状 C:显存够,但 GPU 利用率低、扩展效率差。
| 检查 | 判据 | 处方 |
|---|---|---|
| 通信是否重叠 | profiler 里通信内核与计算内核是否并行 | 开梯度分桶、参数预取、算子融合(9.6.2) |
| 气泡是否过大 | 用 算一下(9.4.2) | 增大 、用交错调度 |
| 每卡 token 数是否太少 | 矩阵乘是否退化成小算子 | 增大 或 ;减少并行度 |
| 数据管线是否瓶颈 | GPU 是否在等数据 | num_workers、pin_memory、预处理搬到 worker(工具箱) |
症状 D:损失异常(不降 / 尖峰 / nan)。
| 检查 | 判据 | 处方 |
|---|---|---|
| 学习率是否随批放大过头 | 是否按线性缩放放大且无 warmup | 加 warmup;改用平方根缩放(9.2.3) |
| 梯度尺度是否变了 | 等效批变化后梯度范数量级 | 打印 grad_norm,必要时梯度裁剪 |
| fp16 是否在溢出 | 是否出现 inf/nan 且用了 fp16 | 换 bf16;检查动态损失缩放;用 autocast 而非手工 .half() |
| LoRA 的 loss mask 是否正确 | 是否把 prompt 也算进损失 | 只对 response 计算损失(Lab 3) |
| 恢复训练是否弄丢状态 | 是否从 checkpoint 恢复并重置了数据流 | 恢复时一并恢复「已消费样本数」与优化器步数(9.6.3) |
- 照搬大厂配置。TP 8 × PP 16 是为几千卡与数百 B 参数设计的;在 8 卡上照搬只会得到「气泡大 + 通信多 + 利用率低」。
- 只调一个旋钮。显存、气泡、通信是联动约束:改 就要重算 与等效批;改 就要看气泡与算子效率。
- 为了省显存先开 ZeRO-3。若 ZeRO-1/2 已经够, 小的场合 ZeRO-3 的每层 all-gather 会让训练更慢(9.2.4 的判据表)。
- 拿总吞吐当验收指标。应看扩展效率 与「同样全局批」下的对比。
- 不测基线就调优。没有单卡 / 小规模基线,任何「提速」都无法证明。
- 忽略通信缓冲与碎片。账算得再准也要留 15%–30% 余量;OOM 常常差的就是那几 GB。
- 把 LoRA 的秩 当成显存旋钮。 主要影响容量(效果),显存影响极小;显存旋钮是
max_length、 与重计算。
9.7.6 配置对照表与接回 Lab 3
| 硬件 | 模型 | 推荐方案 | 模型状态 | 关键参数 | 主要风险 |
|---|---|---|---|---|---|
| 1 × 24 GB | 7–13B(微调) | QLoRA(4 bit + LoRA) | 小项 | ;max_length 1024 起; 16 |
激活与 KV cache 超预算;先降序列长度 |
| 1 × 24 GB | 1–3B(全参) | 混合精度 + 8-bit 优化器 | ;选择性重计算 | 优化器状态仍是主要开销 | |
| 4 × 24 GB | 7B(全参微调) | DDP + ZeRO-2(或 FSDP) | 量级 | ; 按等效批解 | 显存偏紧:先压序列长度 |
| 8 × 80 GB | 7B(全参) | DDP + ZeRO-2 | GB/卡 | 、、等效批 256 | 通信未重叠 → 扩展效率低 |
| 8 × 80 GB | 70B(全参微调) | TP 4 × DP 2 + ZeRO-2,或 FSDP2 × 8 | 视分片而定 | TP 留在节点内; 与 联动 | 单层 embedding / 输出头超预算时需调切分 |
| 8 × 80 GB | 70B(微调) | LoRA + ZeRO-2 | 主干 + 适配器 | target_modules 起手 |
秩太小效果不足:先扫 |
读表方式:先看「模型状态」一列能否装进你的卡,再看「关键参数」,最后看「主要风险」——这张表是五步流程的查表版本。
Lab 3 走的是本表的第一行(1 × 24 GB + QLoRA)。建议按下面的顺序动手:
- 跑通链路(30 分钟):小模型 + 小数据 + 10 步,确认分词、mask、保存都能走通;此时不要碰任何并行配置。
- 量显存与吞吐(30 分钟):用 9.7.4 的四个指标建基线;把
max_length从 512 逐步提到 2048,观察峰值显存。 - 做对照实验(1–2 小时):全参(不可行时用「LoRA vs 较大 」代替)× LoRA 的显存 / 吞吐 / 效果三方对比;报告里给出「可训参数比例」与「任务指标」两个数。
- 把结论写进实验记录:哪些约束先被撞到、下一个旋钮是什么。这正是 工具箱 要求的「三行结论」。
若你手上恰好有多卡,可以额外做一件事:用同样的全局批测一次扩展效率 (定义 9.1.3)。哪怕只有 2 张卡,这个练习也会让你对「通信与气泡到底吃掉了多少」有第一次真实的手感。
Q1 、、:在单张 24 GB 卡上,全参微调为什么不可行?给出两个可行替代方案并说明各自代价。
Q2 8 张 80 GB 卡、7B 模型、目标等效批 256:给出 、 与并行配置,并说明为什么不需要 TP / PP。
Q3 QLoRA 配置里,显存的主要旋钮是哪些?为什么 不是其中之一?
Q4 你的 8 卡训练扩展效率只有 0.4,显存充足。写出你的排查顺序(至少四步)与每步的检查动作。
参考要点
Q1 不可行:模型状态 GB GB(再加激活必爆)。替代方案:① QLoRA——主干 4 bit( GB)+ 极小适配器,代价是低秩表达能力受限;② 全参 + 8-bit 优化器 + CPU offload——模型状态约 GB 勉强可放,代价是每步 PCIe 往返导致训练显著变慢。(也可答「多卡分片」,但那超出单卡前提。)
Q2 ZeRO-2(8 卡)下每卡模型状态 GB;、 时 ✔( 时激活约 34 GB;若显存吃紧,先开选择性重计算)。不需要 TP:单层最大(FFN)仅约 0.13 GB(bf16),单卡放得下;不需要 PP:层数不是瓶颈(8 卡下 ZeRO-2 已解决整体显存),且同节点时越简单的方案风险越低。
Q3 主要旋钮:max_length(激活 )、 与 (激活 )、是否开选择性重计算、主干精度(4 bit vs bf16)。 不是旋钮,因为适配器参数量 模块数,相对主干( 或 )完全可忽略; 影响的是容量与效果,不是显存。
Q4 ① 看 GPU 利用率与 profiler:区分「在等通信」「在等数据」「在算小算子」;② 算气泡:,若 大而 小则加大 ;③ 看通信是否重叠:通信内核是否与计算内核并行,否则调桶大小 / 预取;④ 看每卡 token 数:把 或 调大后重测;⑤ 最后再怀疑拓扑 / 硬件(例如 TP 跨了节点)。每一步都要只改一个变量并记录。
- 回到本讲起点:9.1 显存账本( 的推导)与 9.5 手段对照表(省多少 / 代价是什么)。
- 动手入口:Lab 3 · 微调一个大语言模型;配套的 工程工具箱(显存估算、排查树、实验记录)。
- 框架文档(截至 2025 年,以官方文档为准):PyTorch
fully_shard/ FSDP2、torch.distributed.pipelining、torch.distributed.checkpoint;DeepSpeed ZeRO 配置项;HuggingFacepeft与bitsandbytes的量化配置。
本节小结:把六节内容压成一条流程——算账( 与激活)→ 定分片 → 判 TP/PP → 解批大小与梯度累积 → 定精度与重计算。两条典型路径是:单卡 / 小卡用 QLoRA 把模型状态压到 3–4 GB 量级、把预算留给激活;多卡大显存用 DDP + ZeRO-2 配足够的等效批。两者共同的验收标准是「同样全局批下的扩展效率」与「微调前后的效果对照」,而不是总吞吐。参数核对与排错清单要长期留在手边——它们是把这一讲的知识转成工程习惯的那部分。