大模型全参数训练与微调
大模型全参数训练
常见的大模型全参数训练方法
| 方法 | 阶段 | 需要什么模型 | 数据需求 | 显存 | 稳定性 | 代表 |
|---|---|---|---|---|---|---|
| Pre-training | 基础 | 1 个(基座) | 万亿 token 文本 | 极高 | 稳定 | 所有 LLM |
| SFT | 对齐 | 1 个(基座) | 1~100 万条 QA 对 | 高 | 稳定 | 所有 LLM |
|
RLHF (PPO、GRPO) |
对齐 | 4 个 | 数万偏好对 + 在线采样 | 极高 | ⚠️ 难调 | ChatGPT, Claude |
| DPO | 对齐 | 2 个 | 数万偏好对(离线) | 中等 | ✅ 稳定 | Llama 2/3, Qwen |
| ORPO | 对齐 | 1 个 | 偏好对(和 DPO 一样) | ≈ SFT | ✅ 稳定 | 学术研究 |
| GRPO | 对齐 | 1 个 | 规则打分 + 组采样 | 中高 | ✅ 较稳定 | DeepSeek-R1 |
| SimPO | 对齐 | 2 个 | 偏好对 | 中等 | ✅ 稳定 | 学术研究 |
| KTO | 对齐 | 2 个 | 单条偏好(不需要成对!) | 中等 | ✅ 稳定 | 学术研究 |
TRL
TRL(Transformer Reinforcement Learning)是 HuggingFace 官方推出的训练库。它把 SFT、DPO、RLHF、GRPO 等训练方法封装成统一的 Trainer 接口,和 HuggingFace 的 Trainer API 一模一样——你会用 HuggingFace 的 Trainer,就会用 TRL。在 TRL 出现之前,做 RLHF 需要自己写 Reward Model 训练、PPO 采样循环、KL 惩罚计算……代码量几千行起。TRL 把这些全部封装好,只需定义配置 → 传入模型和数据 → 一行 trainer.train()。
# ═══════════════════════════════════════════════════════════════
# TRL 依赖 PyTorch + Transformers + Accelerate + PEFT
# ═══════════════════════════════════════════════════════════════
pip install trl transformers accelerate peft datasets bitsandbytes
TRL 提供的五个核心 Trainer,对应五种训练范式:
| TRL Trainer | 对应方法 | 一句话描述 | 数据需求 |
|---|---|---|---|
| SFTTrainer | SFT 指令微调 | 用 QA 对话数据训练模型学会"问答"格式 | 指令→回答 对 |
| RewardTrainer | Reward Model 训练 | 训练一个打分模型(RLHF 第二步需要) | 两个回答的偏好标注 |
| PPOTrainer | RLHF (PPO) | 用强化学习让模型输出更符合人类偏好 | prompt 列表 + 已训好的 Reward Model |
| DPOTrainer | DPO 直接偏好优化 | 无需 Reward Model,直接用偏好数据优化 | (prompt, win, lose) 三元组 |
| GRPOTrainer | GRPO 组内相对优化 | 用规则打分,组内比较,自进化(DeepSeek-R1) | prompt 列表 + 打分函数 |
大模型训练 = 预训练(认字)→ SFT(学说话)→ 偏好对齐(学品位)。每阶段都有统一的 TRL Trainer 接口,本文重点介绍后两个阶段。
- 阶段 1「预训练」→ 成本占 99%+,用 Next Token Prediction,不在本文范围
- 阶段 2「SFT」→ SFTTrainer,几万条数据,几小时
- 阶段 3「偏好对齐」→ DPOTrainer / PPOTrainer / GRPOTrainer,几千~几万条偏好数据
数据样例:
# ═══════════════════════════════════════════════════════════════
# 格式 1: Alpaca(单轮问答 — 最简单)
# 保存为 data/my_dataset.json,然后在 dataset_info.json 注册
# ═══════════════════════════════════════════════════════════════
[
{
"instruction": "请用 Python 写一个快速排序算法",
"input": "", # 可选上下文,没有就填空串
"output": "def quicksort(arr):\n if len(arr) <= 1:\n return arr\n ..."
},
{
"instruction": "翻译以下内容为英文",
"input": "人工智能正在改变世界", # 这里是具体要翻译的内容
"output": "Artificial intelligence is changing the world"
}
]
═══════════════════════════════════════════════════════════════
格式 2: ShareGPT(多轮对话 — 更现代)
═══════════════════════════════════════════════════════════════
[
{
"conversations": [
{"from": "human", "value": "请用 Python 写一个快速排序"},
{"from": "gpt", "value": "def quicksort(arr):\n ..."},
{"from": "human", "value": "能解释一下时间复杂度吗?"},
{"from": "gpt", "value": "快速排序的平均时间复杂度为 O(n log n)..."}
]
}
]
═══════════════════════════════════════════════════════════════
在 dataset_info.json 中注册你的数据集
═══════════════════════════════════════════════════════════════
"my_dataset": {
"file_name": "my_dataset.json",
"formatting": "alpaca", # 告诉 LlamaFactory 数据格式
"columns": {
"prompt": "instruction", # 哪个字段作为 prompt
"query": "input", # 哪个字段作为上下文
"response": "output" # 哪个字段作为回答
}
}
Pre-training
目标函数:Next Token Prediction
Loss = -log P( tokent | token0, token1, ..., tokent-1 )
模型看到的永远是"前文",要预测的永远是"下一个词"。这个目标极其简单但极度高效——互联网上的每一段文字天然就是这个任务的训练数据,不需要人工标注。
SFT
SFT 用高质量的"问题→答案"对训练模型,让它学会:当用户给出某种格式的输入时,应该以某种格式输出。
SFT的数据格式
# ═══════════════════════════════════════════════════════════════
# SFT 的每条数据就是一个"对话",有多种模板风格
# ═══════════════════════════════════════════════════════════════
# 模板 1: Alpaca 格式(简单直接)
"Below is an instruction that describes a task.\n\n
Instruction:\n用 Python 写一个快速排序\n\n
Response:\npython\ndef quicksort(arr):\n ..."
# 模板 2: ChatML 格式(OpenAI 风格,支持多轮对话)
"<|im_start|>system\n你是一个有用的助手<|im_end|>\n
<|im_start|>user\n什么是光合作用?<|im_end|>\n
<|im_start|>assistant\n光合作用是植物利用阳光...<|im_end|>"
# 模板 3: Llama 3 格式(最现代的对话模板)
"<|begin_of_text|>
<|start_header_id|>user<|end_header_id|>\n\n
# 用 Python 写一个快速排序
<|eot_id|>
<|start_header_id|>assistant<|end_header_id|>\n\n
python\ndef quicksort(arr):\n ...
<|eot_id|>"
基于TRL的SFT训练脚本:
# ═══════════════════════════════════════════════════════════════
# TRL SFTTrainer — 工业级 SFT 训练,一行 trainer.train() 搞定
# 自动帮你做: Loss Masking + 打包序列 + 混合精度 + 梯度累积
# ═══════════════════════════════════════════════════════════════
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig # TRL 的核心 API
from transformers import AutoModelForCausalLM, AutoTokenizer
# ─── Step 1: 加载基座模型和分词器 ───
# 从预训练基座出发——这是你从 HuggingFace Hub 下载的模型
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B", # 换成任意基座:Llama, Mistral, DeepSeek...
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
# ─── Step 2: 准备数据 — 对话格式 ───
# 数据格式: {"messages": [{"role":"user","content":"..."}, {"role":"assistant","content":"..."}]}
# TRL 自动应用 chat_template,并自动只对 assistant 部分算 loss
dataset = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft")
# ─── Step 3: 配置 SFTConfig — 所有训练参数 ───
sft_config = SFTConfig(
output_dir="./sft_qwen",
per_device_train_batch_size=2, # 单卡 batch size
gradient_accumulation_steps=8, # 累积 8 步 → 等效 batch=2×8=16
num_train_epochs=3,
learning_rate=2e-5, # SFT 的 LR 不宜过大
warmup_ratio=0.03,
lr_scheduler_type="cosine",
fp16=True, # 混合精度,省显存(A100 可选 bf16=True)
logging_steps=10,
save_strategy="epoch",
max_seq_length=2048, # 序列最大长度,超出的截断
packing=True, # ★ 打包:把多个短对话拼成一条 2048,充分利用 GPU
dataset_text_field="messages", # 告诉 TRL 数据中哪个字段是对话
)
# ─── Step 4: 创建 Trainer 并训练 ───
trainer = SFTTrainer(
model=model,
args=sft_config,
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train() # ★ 一行训练!
# ─── Step 5: 保存模型 ───
trainer.save_model("./sft_qwen_final")
# ═══════════════════════════════════════════════════════════════
# SFTTrainer 自动帮你做的事(你不需要手动写):
# 1. 应用 chat_template(把 JSON 转成模型训练时的 token 序列)
# 2. Loss Masking(自动识别 assistant 部分,只在那部分算 loss)
# 3. Packing(多个短对话拼成一条长序列,训练效率提升 2~5 倍)
# 4. 混合精度 + 梯度累积 + 梯度裁剪
# ═══════════════════════════════════════════════════════════════
RLHF
RLHF 让模型学会"什么回答更让人满意"。三步走:SFT 打底 → 训练 Reward Model 打分 → PPO 强化学习。TRL 对应:RewardTrainer + PPOTrainer。后来 DeepSeek-R1 提出了 GRPO,省掉 Value Model(Critic),用组内归一化替代 V(s) 估计。reward 来源可以是 RM 或规则函数,TRL 对应:GRPOTrainer。
RLHF 三步走(PPO 路线)
四模型在代码中的对应关系
| 模型 | 代码中的变量名 | 加载方式 | 冻结/训练 | 做什么 | 输入→输出 |
|---|---|---|---|---|---|
| Policy Model | model |
AutoModelForCausalLMWithValueHead |
✅ 训练(唯一被 optimizer 更新的) | 生成回答 + 更新策略 | prompt → 回答文本 |
| Reference Model | ref_model |
AutoModelForCausalLMWithValueHead |
❌ 冻结 | 计算 KL 惩罚的基准线。PPO 目标是"偏离 ref 但别太远" | 回答 → log 概率 |
| Reward Model | reward_model |
AutoModelForSequenceClassification |
❌ 冻结(Step 1 已训好) | 给回答打分——模拟人类偏好 | (prompt, 回答) → 1 个标量分数 |
| Value Model (Critic) | model.v_head |
藏在 Policy Model 内部 | ✅ 训练(和 Policy 共享 backbone) | 估计 V(s)——"走到这一步,最终能拿多少 reward" | hidden state → 1 个标量 |
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
# ═══════════════════════════════════════════════════════════════
# 变量 model → Policy Model(策略模型)
# 作用:"被训练的对象"——不断优化以生成更高 reward 的回答
# 训练状态:✅ 参与梯度更新(唯一被 optimizer 更新的模型)
# 内部结构:Transformer backbone + lm_head(生成文本)
# + v_head(估计 V(s),即 Value Model 部分)
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final", torch_dtype="auto",
)
# ↑ ↑
# Policy Model "WithValueHead" = 自带 Value Model 头
# ═══════════════════════════════════════════════════════════════
# 变量 ref_model → Reference Model(参考模型 / 锚点)
# 作用:计算 KL 惩罚的"基准线"——policy 不能离 ref 太远
# 训练状态:❌ 冻结,不参与梯度更新
# 为什么和 Policy 结构相同?因为 KL 计算需要同构的 log 概率输出
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final", torch_dtype="auto",
)
# ═══════════════════════════════════════════════════════════════
# 变量 reward_model → Reward Model(奖励模型 / 裁判)
# 作用:给 Policy 生成的回答打分——"这个回答有多好?"
# 训练状态:❌ 冻结(已在 Step 1 用 RewardTrainer 训好)
# 内部结构:Transformer backbone + score_head(输出 1 个标量)
# 与 Policy/Ref 的区别:只用 backbone 做理解,不生成文本
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
reward_model = AutoModelForSequenceClassification.from_pretrained(
"./reward_model_final", torch_dtype="auto",
)
# ↑ ↑
# Reward Model SequenceClassification = 分类头而非生成头
# ═══════════════════════════════════════════════════════════════
# Value Model(价值模型 / Critic)
# 作用:估计 V(s) = "当前回答到这一步,最终能拿多少 reward?"
# 训练状态:✅ 参与梯度更新(但和 Policy 共享 backbone!)
# 在哪里?→ 藏在 model.v_head 里!
# Value Model = Transformer backbone + v_head (nn.Linear(4096,1))
# 对应代码:没有单独的变量,它是 Policy Model 的一个子模块
# ═══════════════════════════════════════════════════════════════
# 在 PPOTrainer 内部,Value Model 这样被使用:
hidden = model.transformer(input_ids) ← backbone 前向
action_logits = model.lm_head(hidden) ← Policy 输出
v_s = model.v_head(hidden[:, -1, :]) ← Value 输出 (一个标量)
advantage = reward - v_s ← PPO 的 advantage 公式
# ═══════════════════════════════════════════════════════════════
# 四个变量全部传入 PPOTrainer
# ═══════════════════════════════════════════════════════════════
trainer = PPOTrainer(
config=ppo_config,
model=model, # Policy + Value (两个角色,一个变量)
ref_model=ref_model, # Reference (冻结的锚点)
reward_model=reward_model, # Reward (冻结的裁判)
tokenizer=tokenizer,
)
一个训练步的完整数据流
trainer.step() 内部发生的事情(一次前向 + 反向):
① Policy 生成
prompt → model.transformer → model.lm_head → 采样出回答文本
② Reward 打分
(prompt, 回答文本) → reward_model → 一个分数 r
③ Value 预估
回答的 hidden states → model.v_head → 预估价值 v_s
④ 计算 Advantage
advantage = r - v_s
↑ 如果 r > v_s → advantage 为正 → 这个回答比预期好 → 提高概率
⑤ 计算 KL 惩罚
KL = Policy.log_prob(回答) - Ref.log_prob(回答)
↑ Reference 是 SFT 时的概率,Policy 是当前概率
↑ 差值过大 → KL 项变大 → 惩罚 → 拉回去
⑥ PPO Loss = -(advantage - beta × KL)
反向传播 → 只更新 Policy(model 参数 + model.v_head 参数)
Ref 和 Reward 始终冻结
Step 1: 训练 Reward Model(培养裁判)
Reward Model 的任务:给定一个回答,预测人类会给它打多少分。训练数据格式是 (prompt, 好的回答, 差的回答)——人类标注员只需要说"A 比 B 好",不需要给出具体分数。
# ═══════════════════════════════════════════════════════════════
# TRL RewardTrainer — 训练一个"打分器"
# 数据格式:(prompt, chosen, rejected) → "好回答"和"差回答"
# ═══════════════════════════════════════════════════════════════
from trl import RewardTrainer, RewardConfig
from transformers import AutoModelForSequenceClassification
from datasets import load_dataset
# 1. 加载基座模型 → 把 lm_head 换成 score_head(输出一个标量分数)
AutoModelForSequenceClassification 自动帮你在原模型上加一层
model = AutoModelForSequenceClassification.from_pretrained(
"./sft_qwen_final", # 从 SFT 模型出发
num_labels=1, # 输出 1 个标量:分数
torch_dtype="auto",
)
tokenizer = AutoTokenizer.from_pretrained("./sft_qwen_final")
# 2. 加载偏好数据 — 格式: {"chosen": "好回答", "rejected": "差回答"}
TRL 自动用 Bradley-Terry Loss: -log(sigmoid(R(chosen) - R(rejected)))
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
# 3. 配置 RewardConfig
reward_config = RewardConfig(
output_dir="./reward_model",
per_device_train_batch_size=4,
num_train_epochs=1,
learning_rate=1e-5,
max_length=2048,
logging_steps=10,
)
# 4. 训练 — 和 HuggingFace Trainer 完全一样的 API
trainer = RewardTrainer(
model=model,
args=reward_config,
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train()
trainer.save_model("./reward_model_final")
Step 2: PPO 强化学习(用裁判训练选手)
有了 Reward Model 作为裁判,现在可以用 PPO 来优化模型:多生成 Reward 高的回答,但同时用 KL 惩罚限制模型不要偏离 SFT 太远。
# ═══════════════════════════════════════════════════════════════
# TRL PPOTrainer — 同时跑 4 个模型(Policy/Ref/Reward/Value)
# 显存需求高(7B 模型约需 4×A100),但 API 简洁
# ═══════════════════════════════════════════════════════════════
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
# 1. 加载 Policy Model — 带 Value Head 的 SFT 模型
# AutoModelForCausalLMWithValueHead = 原模型 + 额外的一个 Value 头
model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final",
torch_dtype="auto",
)
tokenizer = AutoTokenizer.from_pretrained("./sft_qwen_final")
# 2. 加载 Ref Model(SFT 模型,冻结,用于计算 KL 惩罚)
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final",
torch_dtype="auto",
)
# 3. 加载 Reward Model(前面训好的打分器,冻结)
reward_model = AutoModelForSequenceClassification.from_pretrained(
"./reward_model_final",
torch_dtype="auto",
)
# 4. 配置 PPOConfig
ppo_config = PPOConfig(
output_dir="./ppo_output",
per_device_train_batch_size=1, # PPO 显存紧张,batch=1 是常态
learning_rate=1e-6,
ppo_epochs=4,
kl_penalty="kl", # KL 惩罚类型
init_kl_coef=0.1, # ★ 初始 KL 系数 — 控制偏离 SFT 的程度
target=6.0, # ★ KL 目标值 — 自动调整 kl_coef 以保持 KL≈6
max_grad_norm=1.0, # 梯度裁剪
cliprange=0.2, # PPO clip — 防止单步更新过大
)
# 5. 创建 PPOTrainer 并训练
# PPOTrainer 会自动:采样回答 → Reward 打分 → 计算 KL → 更新 Policy
trainer = PPOTrainer(
config=ppo_config,
model=model,
ref_model=ref_model,
reward_model=reward_model,
tokenizer=tokenizer,
)
# 训练循环 — 每步都是:生成→打分→更新
for batch in prompt_dataset:
# 对每个 prompt,PPOTrainer 自动生成回答、打分、计算 loss、更新
stats = trainer.step(
query_texts=batch["prompt"],
query_tensors=tokenizer(batch["prompt"], return_tensors="pt").input_ids,
)
print(f"Reward: {stats['reward']:.2f}, KL: {stats['kl']:.2f}")
trainer.save_model("./ppo_final")
GRPO路线
GRPO 省掉的是 Value Model(Critic),不是 Reward Model。奖励信号仍然需要——可以来自 RM 模型、Rule-based 函数、或两者混合。核心创新是:同一个 prompt 生成 N 个回答,用组内均值和标准差做 baseline 来计算 advantage,从而不再需要训练一个单独的 Critic 网络来估计 value。
- PPO 的 advantage: reward - V(s),其中 V(s) 需要 Value Model 来估计
- GRPO 的 advantage: (reward - group_mean) / group_std,组内归一化替代 Value Model
PPO 最大的痛点是需要同时维护 4 个模型,其中 Value Model 就是专门用来估计"当前状态有多好"的 Critic 网络。GRPO 用一个巧妙的方法绕过了它:
PPO: 生成 1 个回答 → Reward 给分 → Value Model 估计 advantage → 更新
需要: Policy + Ref + Reward + Value (Critic) = 4 个模型
GRPO: 生成 N 个回答 → Reward 给分 → 组内归一化算 advantage → 更新
需要: Policy + Ref + (RM 或 Rule-based)= 3 个模块
省掉了 Value Model,advantage 用组内相对排名替代
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
# ═══════════════════════════════════════════════════════════════
# 变量 model → Policy Model(策略模型)
# 作用:"被训练的对象"——不断优化以生成更高 reward 的回答
# 训练状态:✅ 参与梯度更新(唯一被 optimizer 更新的模型)
# 内部结构:Transformer backbone + lm_head(生成文本)
# + v_head(估计 V(s),即 Value Model 部分)
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final", torch_dtype="auto",
)
# ↑ ↑
# Policy Model "WithValueHead" = 自带 Value Model 头
# ═══════════════════════════════════════════════════════════════
# 变量 ref_model → Reference Model(参考模型 / 锚点)
# 作用:计算 KL 惩罚的"基准线"——policy 不能离 ref 太远
# 训练状态:❌ 冻结,不参与梯度更新
# 为什么和 Policy 结构相同?因为 KL 计算需要同构的 log 概率输出
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(
"./sft_qwen_final", torch_dtype="auto",
)
# ═══════════════════════════════════════════════════════════════
# 变量 reward_model → Reward Model(奖励模型 / 裁判)
# 作用:给 Policy 生成的回答打分——"这个回答有多好?"
# 训练状态:❌ 冻结(已在 Step 1 用 RewardTrainer 训好)
# 内部结构:Transformer backbone + score_head(输出 1 个标量)
# 与 Policy/Ref 的区别:只用 backbone 做理解,不生成文本
# 对应代码行 ↓
# ═══════════════════════════════════════════════════════════════
reward_model = AutoModelForSequenceClassification.from_pretrained(
"./reward_model_final", torch_dtype="auto",
)
# ↑ ↑
# Reward Model SequenceClassification = 分类头而非生成头
# ═══════════════════════════════════════════════════════════════
# Value Model(价值模型 / Critic)
# 作用:估计 V(s) = "当前回答到这一步,最终能拿多少 reward?"
# 训练状态:✅ 参与梯度更新(但和 Policy 共享 backbone!)
# 在哪里?→ 藏在 model.v_head 里!
# Value Model = Transformer backbone + v_head (nn.Linear(4096,1))
# 对应代码:没有单独的变量,它是 Policy Model 的一个子模块
# ═══════════════════════════════════════════════════════════════
# 在 PPOTrainer 内部,Value Model 这样被使用:
# hidden = model.transformer(input_ids) ← backbone 前向
# action_logits = model.lm_head(hidden) ← Policy 输出
# v_s = model.v_head(hidden[:, -1, :]) ← Value 输出 (一个标量)
# advantage = reward - v_s ← PPO 的 advantage 公式
# ═══════════════════════════════════════════════════════════════
# 四个变量全部传入 PPOTrainer
# ═══════════════════════════════════════════════════════════════
trainer = PPOTrainer(
config=ppo_config,
model=model, # Policy + Value (两个角色,一个变量)
ref_model=ref_model, # Reference (冻结的锚点)
reward_model=reward_model, # Reward (冻结的裁判)
tokenizer=tokenizer,
)
🔑 关键澄清
GRPO 的 reward 可以来自多种来源:
① RM 模型 — 用 Stage 2 训练的 Reward Model 打分(和 PPO 一样!)
② Rule-based 函数 — 用 Python 写的规则打分(长度、格式、答案对错等)
③ 混合 — RM + 规则的加权组合
不管 reward 来自哪里,GRPO 都不需要 Value Model——这是它和 PPO 的唯一但关键的区别。DeepSeek-R1 论文的原始 GRPO 用法是 Rule-based reward(数学题用答案对错,代码题用测试通过率),但实际工程中 RM+规则混合也非常常见。
DPO
DPO 是 RLHF 的简化版——不需要 Reward Model,直接用偏好数据训练。数学上把 RLHF 的目标改写成了分类问题。TRL 对应:DPOTrainer。这是工业界当前性价比最高的对齐方案。
- 输入: (prompt, 好回答, 差回答) 三元组
- 目标: 让模型对"好回答"的概率 > "差回答"的概率
- 优势: 只需 2 个模型(Policy + Ref),显存是 PPO 的一半
输入案例:
{
"prompt": "我恨我的老板,想报复他,有什么好办法?",
"chosen": "我理解工作中的不满和压力可能让人非常沮丧,但报复从来都不是解决问题的好方法。\n\n更健康的处理方式:\n1. 冷静下来,先给自己一些时间和空间\n2. 尝试与老板进行建设性的对话\n3. 如果沟通无效,可以向人力资源部门反映\n4. 如果工作环境确实无法改善,可以考虑寻找新的机会\n\n如果你需要倾诉或具体的职场建议,我很乐意提供帮助。",
"rejected": "可以匿名举报他,或者在同事中散播他的谣言。"
},
DPO 的核心数学发现是:RLHF 的 PPO 目标函数中,Reward Model 是可以被"消掉"的。"好回答 vs 差回答"的相对关系,可以直接从模型对两个回答的概率比中推断出来。
LossDPO = -log σ( β · (log[πθ(ywin)/πref(ywin)] - log[πθ(ylose)/πref(ylose)]) )
# ═══════════════════════════════════════════════════════════════
# TRL DPOTrainer — 工业级 DPO,和 SFTTrainer 一样简单
# 数据格式:{"prompt": "...", "chosen": "好回答", "rejected": "差回答"}
# ═══════════════════════════════════════════════════════════════
from trl import DPOTrainer, DPOConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset
# ─── Step 1: 加载 SFT 模型 ───
# DPO 从 SFT 模型开始(不是从原始基座)
model = AutoModelForCausalLM.from_pretrained(
"./sft_qwen_final",
torch_dtype="auto", device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("./sft_qwen_final")
# ─── Step 2: 准备偏好数据 ───
# 格式: {"prompt": "请解释量子计算",
# "chosen": "量子计算利用量子比特的叠加态...", ← 好的
# "rejected": "量子计算是一种计算方式。"} ← 差的
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
# ─── Step 3: 配置 DPOConfig ───
dpo_config = DPOConfig(
output_dir="./dpo_output",
per_device_train_batch_size=2,
num_train_epochs=1,
learning_rate=5e-5,
beta=0.1, # ★ DPO 最重要的超参数 — 控制偏离 ref 的程度
max_length=2048,
max_prompt_length=512,
logging_steps=10,
loss_type="sigmoid", # sigmoid / hinge / ipo — sigmoid 是标准
)
# ─── Step 4: 创建 DPOTrainer 并训练 ───
trainer = DPOTrainer(
model=model, # Policy Model — 正在被优化
ref_model=None, # ★ TRL 自动从 model 复制一份冻结的 ref_model!
args=dpo_config,
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train()
trainer.save_model("./dpo_final")
# ═══════════════════════════════════════════════════════════════
# DPOTrainer 自动帮你做的事:
# 1. ref_model=None → TRL 自动创建 Reference Model
# 2. 计算 Policy Model 和 Ref Model 对 chosen/rejected 的 log 概率
# 3. 计算 DPO Loss: -log(sigmoid(beta * (log_ratio_chosen - log_ratio_rejected)))
# 4. 反向传播更新 Policy Model
# ═══════════════════════════════════════════════════════════════
🔑 DPO 的 beta 参数怎么调?
beta 控制模型可以偏离 Reference Model 多远。
· beta=0.01 → 几乎不偏离 → 训练后效果和 SFT 差不多 → 太小了
· beta=0.1 → 适中,工业界推荐起步值 → 大多数场景的最佳选择
· beta=1.0 → 大幅偏离 → 可能过拟合偏好数据中的噪声 → 太大了
调试方法:从 0.1 开始,观察训练 loss。loss 平稳降到 0.5~0.7 是理想区间。如果 loss 急剧降到 0.3 以下,说明 beta 太小。
四种训练范式对比
| 维度 | SFT | DPO | PPO (RLHF) | GRPO |
|---|---|---|---|---|
| TRL 工具 | SFTTrainer | DPOTrainer | RewardTrainer + PPOTrainer | GRPOTrainer |
| 需要几个模型 | 1 个 | 2 个(Policy+Ref) | 4 个(+Reward+Value) | 3 个(+Ref+RM可选) |
| 显存 (7B, BF16) | ~40GB | ~60GB | ~200GB | ~120GB |
| 数据需求 | QA 对话对 | 偏好对(离线) | 偏好对 + 在线采样 | prompt + 打分函数(RM/规则) |
| 需要人工标注 | ✅ 需要 | ✅ 需要 | ✅ 需要 | RM-based 需要 / 规则不需要 |
| 训练稳定性 | ✅ 极稳定 | ✅ 稳定 | ⚠️ 难调 | ⚠️ 中等 |
| 效果上限 | 基础 | 接近 PPO | 最高 | 推理任务最强 |
| 最佳场景 | 指令遵循 | 通用对齐 | 极致性能 | 推理/通用(灵活打分) |
方案选择思路
# ═══════════════════════════════════════════════════════════════
# 第一步:确定阶段
# ═══════════════════════════════════════════════════════════════
Q1: 你的模型已经做过 SFT 了吗?
├── 没有 → 先跑 SFT (SFTTrainer),这是所有后续步骤的基础
└── 有了 → 继续
═══════════════════════════════════════════════════════════════
第二步:选择对齐方法
═══════════════════════════════════════════════════════════════
Q2: 你有 4+A100 + 调参团队 + 追求极致?
└── 是 → RLHF (RewardTrainer + PPOTrainer) ← 上限最高但贵
Q3: 你有成对的偏好数据("A 比 B 好")?
└── 是 → DPO (DPOTrainer) ← 性价比首选,Llama/Qwen 都在用
Q4: 你做的是数学/代码等有客观标准的任务?
└── 是 → GRPO (GRPOTrainer) ← 无需标注,自进化
═══════════════════════════════════════════════════════════════
推荐的标准流程(覆盖 90% 的团队):
SFT (SFTTrainer) → DPO (DPOTrainer)
如果做推理增强(数学/代码):
SFT (SFTTrainer) → GRPO (GRPOTrainer)
如果有钱有资源追求极致:
SFT (SFTTrainer) → RLHF (RewardTrainer + PPOTrainer)
═══════════════════════════════════════════════════════════════
显存计算
SFT 全参数微调
| 显存占用项 | 计算方式 | 大小 |
|---|---|---|
| 模型参数 | 1.5B params × 2 bytes (bf16) | 3.0 GB |
| 梯度 | 同参数 | 3.0 GB |
| AdamW 动量 (m) | 1.5B × 4 bytes (fp32) | 6.0 GB |
| AdamW 方差 (v) | 1.5B × 4 bytes (fp32) | 6.0 GB |
| 中间激活 | batch=2×4=8, seq=512, 28层, d=1536 ≈ 8 × 512 × 1536 × 28 × 2 / 1e9 |
~3.5 GB |
| CUDA context | cuBLAS workspace, NCCL buffer 等 | ~1.0 GB |
| 合计 | ~22.5 GB |
梯度检查点
上面激活的 3.5 GB 是开了 gradient_checkpointing 之后的。不开的话,每层都要存完整的中间激活——28 层的 Transformer 堆下来大约 12~15 GB。开了之后只存 checkpoint 节点的激活,其余的反向传播时重新算——激活降到 3~4 GB,代价是多跑一遍前向(大约慢 20%~30%)。
| 无梯度检查点 | 有梯度检查点 | |
|---|---|---|
| 激活显存 | 12~15 GB | ~3.5 GB |
| 总显存 | 31~34 GB | ~22.5 GB |
| 训练速度 | 基准 | 慢 20%~30% |
RM 奖励模型
RM 训练和 SFT 结构几乎一样——都是全参数,只是输出层从 LM head 变成了一个标量头(1 个 label)。模型参数多加了 1×d_model ≈ 1536 个参数而已,可以忽略不计。
显存和 SFT 几乎一样:~22~25 GB。但 RM 用的是 TRL 的 RewardTrainer,它和 HuggingFace 原生 Trainer 有几个关键区别:
| 维度 | 普通 Trainer | RewardTrainer |
|---|---|---|
| 模型输出 | LM head → logits (vocab_size) | 标量头 → 1 个值(整个序列的总分) |
| loss 函数 | 交叉熵(逐 token 对比 label) | 对比 loss:−log[σ(r_chosen − r_rejected)] |
| 数据格式 | {input_ids, labels} | {input_ids_chosen, input_ids_rejected} |
| 前向传播 | 1 次(一条序列→loss) | 2 次(chosen→r_chosen, rejected→r_rejected→对比 loss) |
| 输入 padding 方向 | 右侧 padding | 无特殊要求,两个序列独立编码 |
| eval 指标 | accuracy / loss | accuracy(chosen 分 > rejected 分的占比)、score margin |
最核心的区别——loss 函数完全不同:普通 Trainer 用的是逐 token 交叉熵(每个 token 预测对不对),RewardTrainer 用的是 Bradley-Terry 对比 loss(整个 chosen 序列的得分是否高于整个 rejected 序列)。前者是"教学"——教模型每个位置该输出什么;后者是"裁判"——教模型学会哪个回答整体更好。
GRPO 强化学习
这个阶段显存消耗猛增——因为要同时加载两个模型:
- 策略模型(Policy):Qwen2.5-1.5B SFT 版,全参数训练状态,~22 GB
- 奖励模型(RM):Stage 2 训出来的,推理模式(不需要梯度/优化器),~3 GB
| 组件 | 模式 | 计算公式 | 大小 |
|---|---|---|---|
| Policy 模型参数 | 训练(bf16) | 1.5B × 2 bytes |
3.0 GB |
| Policy 梯度 | 训练 | 1.5B × 2 bytes |
3.0 GB |
| Policy 优化器状态 | 训练(AdamW fp32) | 1.5B × 8 bytes(m 4B + v 4B) |
12.0 GB |
| Policy 激活 | 含生成阶段(n_gen=4, max_len=256) | batch × seq × d_model × layers × n_gen |
~6.0 GB |
| RM 模型 | 推理(eval, no grad) | 1.5B × 2 bytes(仅权重,无梯度/优化器) |
3.0 GB |
| RM KV cache | 推理时缓存 | 2 × layers × seq × d_model × n_gen |
~1.0 GB |
| 合计 | P×12 + RM_P×2 + KV + activations |
~28.0 GB |
为什么激活比 SFT 大?GRPO 每步不只前向一次——num_generations=4 意味着每个 prompt 要生成 4 个回答。生成的 token 都要存 KV cache 和中间状态。虽然 RM 评分时不回传梯度到 policy,但生成阶段的中间结果要吃显存。
GRPO 的特殊性:
GRPO 不像 SFT 那样"一个 batch 训完就完"——它每个 step 是:
- 生成阶段:Policy 模型生成 num_generations=4 个回答(自回归解码,吃 KV cache)
- 评分阶段:RM 模型对每个回答打分(forward only,不存梯度)
- 训练阶段:计算 GRPO loss,反向传播更新 policy
PPO
GRPO 省掉了 Value Model(Critic),只需要 2 个模型(Policy + Ref)(或在三个Policy + Reference + Reward)。如果用经典 PPO,需要同时加载 4 个模型:Policy + Reference + Reward + Value。下面精确算一下差距。
| 模型 | 精度 | 计算 | 显存 |
|---|---|---|---|
| ① Policy Model(训练中) | bf16 + fp32 opt | 1.5B×2 + 1.5B×2 + 1.5B×8 | ~22 GB |
| ② Reference Model(冻结) | bf16 only | 1.5B×2 | ~3 GB |
| ③ Reward Model(冻结) | bf16 only | 1.5B×2 | ~3 GB |
| ④ Value Model / Critic(训练中) | bf16 + fp32 opt | 和 Policy 共享 backbone,额外 ~0.5 GB | ~0.5 GB |
| 激活值(PPO 在线采样) | — | 每步生成回答 + 四模型前向 | ~8~12 GB |
计算公式
VRAMPPO ≈ P×2 + P×2 + P×8 + (3 × P×2) + activations
= P×16 + activations
其中前三项是 Policy Model 的自身开销(参数 + 梯度 + 优化器),3×P×2 是 Ref、RM、Value 三个冻结/轻量模型的 BF16 权重。
PPO和GRPO对比
PPO 显存 = 22 + 3 + 3 + 0.5 + 10
↑ ↑ ↑ ↑ ↑
Policy Ref RM Value 激活
GRPO 显存 = 22 + 3 + 5
↑ ↑ ↑
Policy Ref 激活(组内采样4条)
PPO 比 GRPO 多吃了 ~12~15 GB,主要来自三个地方:
- RM 模型(~3 GB)— GRPO 的 reward 来自打分函数(Python 代码),不需要加载一个 1.5B 参数的神经网络当裁判
- Value 模型(~0.5 GB + 优化器)— PPO 需要 Critic 网络估计 V(s),而 GRPO 用组内归一化替代:advantage = (reward - group_mean) / group_std
- 更大的激活值(~+4 GB)— PPO 每步要跑 4 个模型的前向,中间结果更多
大模型微调
微调(Fine-Tuning)= 在预训练大模型基础上,用少量领域数据继续训练,让模型学会特定技能。全参数微调需要更新所有参数(费 GPU),PEFT 方法(LoRA/QLoRA)只需训练 0.1%~1% 的参数(单张消费级显卡即可)。LlamaFactory 是目前最流行的开源微调工具。
微调方法分两大类:全参数微调(更新所有参数,土豪专用)和 PEFT 参数高效微调(只更新极少参数,性价比首选)。PEFT 家族中 LoRA 是绝对主流,QLoRA 是其 4-bit 量化升级版。
全参数 SFT 的内存分解(7B 模型,BF16):
| 组件 | 大小 | 说明 |
|---|---|---|
| 模型参数 | 7B × 2 bytes = 14 GB | BF16 存储 |
| 梯度 | 7B × 2 bytes = 14 GB | 每个参数一个梯度 |
| Adam 优化器 (m+v) | 7B × 8 bytes = 56 GB | FP32 存储,最大的内存消耗者 |
| 激活值 (batch=1, seq=2048) | ~10 GB | 开启 gradient checkpointing 可降到 ~2 GB |
| 总计 | ~94 GB | 需要 4+A100-80G |
PEFT — 参数高效微调家族
| 方法 | 核心思路 | 可训练参数比率 | 效果 | 主流程度 |
|---|---|---|---|---|
| LoRA | 在注意力层旁加两个小矩阵 B·A,只训练这两个矩阵 | 0.1%~1% | 接近全参数 | ⭐⭐⭐⭐⭐ |
| QLoRA | LoRA + 基座 4-bit 量化,显存再砍一半 | 0.1%~1% | 接近 LoRA | ⭐⭐⭐⭐⭐ |
| Adapter | 在每层 Transformer 后插入小型神经网络 | 1%~5% | 中等 | ⭐⭐ |
| Prefix Tuning | 在输入前加可训练的"前缀 token" | <0.1% | 中下 | ⭐ |
| P-Tuning v2 | 在每层都加可训练 prompt | 0.1%~3% | 中等 | ⭐ |
| IA³ | 只训练三个缩放向量 | <0.01% | 中等 | ⭐ |
LoRA 原理速览
LoRA 的核心假设:微调时模型权重的变化量 ΔW 可以分解为两个低秩矩阵的乘积:
ΔW = B · A (B ∈ Rd×r, A ∈ Rr×d)
前向传播时:h = W₀·x + (α/r)·B·A·x。W₀ 冻结,只训练 B 和 A。r 通常取 8~64,α 通常取 2×r。推理时可以把 B·A 合并回 W₀,零额外开销。
QLoRA 在上面基础上,把 W₀ 用 4-bit NormalFloat 量化存储,前向时反量化为 BF16 计算。基座模型显存从 14GB(BF16)降到 ~4GB(4-bit)。
LlamaFactory + LoRA 实例
LlamaFactory — 一站式微调利器
LlamaFactory 是 GitHub 上最火的大模型微调工具(30K+ Stars),支持 100+ 种模型和十几种微调方法。提供 WebUI(点鼠标)和 CLI(命令行)两种方式。安装简单,一行 pip install llamafactory 即可。
安装 LlamaFactory
# ═══════════════════════════════════════════════════════════════
# 方式 1: pip 安装(推荐)
# ═══════════════════════════════════════════════════════════════
pip install llamafactory
# ═══════════════════════════════════════════════════════════════
# 方式 2: 源码安装(如需修改源码或贡献代码)
# ═══════════════════════════════════════════════════════════════
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"
# ═══════════════════════════════════════════════════════════════
# 验证安装 — 启动 WebUI 看看能不能打开
# ═══════════════════════════════════════════════════════════════
llamafactory-cli webui
# 浏览器打开 http://localhost:7860 就能看到界面了
LlamaFactory 的两种使用方式
| 方式 | 命令 | 适合谁 |
|---|---|---|
| WebUI | llamafactory-cli webui |
新手、快速实验、可视化调参 |
| CLI 命令行 | llamafactory-cli train config.yaml |
批量实验、脚本化、服务器后台运行 |
LoRA 微调
LoRA 微调 = 冻结基座模型 + 在注意力层加 adapter。LlamaFactory 预配置了最优参数,你只需要调整 rank、alpha、学习率三个值即可。训练产物只有 adapter 文件(几十 MB),可以挂载到任意同架构的基座模型上。
LoRA 的核心配置参数
| 参数 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
| r (rank) | 低秩分解的秩 | 8~16 | r 越大容量越大但参数量也越大。r=8 适合 1 万条以内,r=16 适合更大数据量 |
| lora_alpha | 缩放系数 | 2×r (16~32) | 控制 LoRA 对输出的影响强度。通常设为 2×r |
| lora_dropout | adapter 上的 dropout | 0.05~0.1 | 防止过拟合小数据集。数据少于 1000 条时建议 0.1 |
| target_modules | 在哪些层加 adapter | q_proj, v_proj | 只给 Q/V 加最省参数;全部加(q/k/v/o/gate/up/down)效果更好但参数多 |
| learning_rate | 学习率 | 1e-4 ~ 5e-4 | LoRA 学习率可以比全参数 SFT 高 5~10 倍 |
WebUI 操作步骤
① 选模型→② 选数据→③ 选方法→④ 调参数→⑤ 点训练
WebUI 操作要点:
① 模型选择:Model name → 选 Qwen2.5-7B(或其他支持的模型)
→ LlamaFactory 自动从 HuggingFace 下载模型到本地缓存
② 数据选择:Dataset → 选内置数据集如 alpaca_zh,或上传自定义 JSON 文件
→ 自定义数据格式:{"instruction":"...", "input":"...", "output":"..."}
③ 微调方法:Finetuning method → 选 lora
④ 关键参数:
· LoRA rank: 16
· LoRA alpha: 32
· Learning rate: 2e-4
· Epochs: 3
· Batch size: 2(GPU 显存不够就降到 1)
· Gradient accumulation: 4(等效 batch=2×4=8)
· Cutoff length: 2048(序列最大长度)
⑤ 点击"开始训练" → 看 loss 曲线 → 训练完成后导出 adapter
CLI 命令行方式
配置YAML文件
# ═══════════════════════════════════════════════════════════════
# 保存为 lora_config.yaml,然后运行:
# llamafactory-cli train lora_config.yaml
# ═══════════════════════════════════════════════════════════════
模型配置
model_name_or_path: Qwen/Qwen2.5-7B-Instruct # 基座模型(任意 HuggingFace 模型都可以)
trust_remote_code: true # 有些模型需要这个才能加载
微调方法
finetuning_type: lora # ★ 选 lora(也可以填 full 做全参数)
LoRA 参数
lora_rank: 16 # ★ 秩 — 核心超参数,16 是甜点值
lora_alpha: 32 # ★ 缩放系数 — 2×rank
lora_dropout: 0.05 # dropout 防过拟合
lora_target: all # "all" = q/k/v/o/gate/up/down 全加 adapter
也可以手动指定: [q_proj, v_proj] 最省 / [q_proj, k_proj, v_proj, o_proj] 标准
数据配置
dataset: alpaca_zh # LlamaFactory 内置中文数据集
用自定义数据: dataset_dir: ./my_data, dataset: my_dataset
template: qwen # ★ 对话模板,必须和模型匹配!(qwen/llama3/chatml...)
cutoff_len: 2048 # 超过这个长度的序列会截断
preprocessing_num_workers: 4 # 数据预处理的并行线程数
训练参数
output_dir: ./output/lora_qwen # adapter 保存位置
logging_steps: 10 # 每 10 步打印一次 loss
save_steps: 500 # 每 500 步保存一个 checkpoint
num_train_epochs: 3 # 训练 3 个 epoch
per_device_train_batch_size: 2 # 单卡 batch size
gradient_accumulation_steps: 4 # 累积 4 步 → 等效 batch=2×4=8
learning_rate: 2.0e-4 # ★ LoRA 学习率,比全参数 SFT (2e-5) 高 10 倍
lr_scheduler_type: cosine # 余弦退火 — 从峰值平滑降到接近 0
warmup_ratio: 0.1 # 前 10% 的步数线性预热 lr
fp16: true # 混合精度(V100/T4 用 fp16,A100 用 bf16)
训练完了怎么用?
方式 A: 导出合并后的完整模型
llamafactory-cli export lora_config.yaml
方式 B: 直接用 adapter 推理(不合并,随时可切换不同 adapter)
llamafactory-cli chat lora_config.yaml
执行
llamafactory-cli train lora_config.yaml
导出的 Adapter 怎么用?—— 挂载与推理全流程
# 训练完毕后,output/lora_qwen 目录下:
output/lora_qwen/
├── adapter_config.json # LoRA 配置(rank=16, alpha=32, target_modules...)
└── adapter_model.safetensors # ★ 实际的 adapter 权重,只有 ~30MB(rank=16 时)
对比:基座模型 Qwen2.5-7B 是 14GB
adapter 只有 30MB — 小 500 倍!
adapter 不能独立运行——它只是 ΔW 的权重(即 B·A 两个矩阵),必须挂载到一个同架构的基座模型上才能工作。挂载后,模型的前向传播变成:
h = W基座·x + (α/r)·B·A·x
下面给出三种挂载和使用方式:
方式一:LlamaFactory WebUI 直接对话(最简单)
WebUI Chat 标签页操作:
① 选择模型:Qwen2.5-7B-Instruct(和训练时相同的基座)
② 勾选 "Use adapter" → 选择文件夹 ./output/lora_qwen
③ 点击 "Load model" → 等待加载 (1~2 分钟)
④ 在聊天框输入问题 → 模型就会用微调后的能力回答
工作原理:LlamaFactory 在后台调用 PEFT 库,动态地把 adapter 权重"挂"到基座模型上,不修改基座文件。
方式二:Python 代码动态加载(最灵活)
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel # PEFT 库 — HuggingFace 官方出品
# ─── Step 1: 加载基座模型(原始权重,不包含任何微调信息) ───
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct", # 和训练时相同的基座
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
# ─── Step 2: 把 adapter "挂载"到基座模型上 ───
# PeftModel.from_pretrained 会自动读取 adapter_config.json,
# 找到基座中对应的层(如 q_proj),在旁边挂上 B·A 两个小矩阵
model = PeftModel.from_pretrained(
base_model,
"./output/lora_qwen", # adapter 文件夹路径
)
# ↑ 此时 model 的前向传播 = W_base·x + (16/32)·B·A·x
# ↑ adapter 被激活,模型现在拥有了微调学到的能力
# ─── Step 3: 像普通模型一样推理 ───
prompt = "请用 Python 写一个快速排序算法"
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512, temperature=0.7)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)
# ─── 优势:可以随时切换 adapter,一个基座同时服务多个任务 ───
model_math = PeftModel.from_pretrained(base_model, "./adapter_math")
model_code = PeftModel.from_pretrained(base_model, "./adapter_code")
# 不需要重新加载基座!一个 14GB 基座 + 多个 30MB adapter = 多技能模型
方式三:合并后部署到 vLLM/Ollama(生产环境)
动态加载有一个微小缺点:每次推理都要额外计算 B·A·x 这一项。在生产环境中,通常提前把 adapter 合并进基座权重,得到一个和普通模型完全一样的文件,然后扔给高性能推理框架:
基座 (14GB)+Adapter (30MB)→ 合并 →完整模型 (14GB)→vLLM / Ollama 部署
# ═══════════════════════════════════════════════════════════════
# 方式 A: 在 LlamaFactory 中合并并导出
# ═══════════════════════════════════════════════════════════════
llamafactory-cli export \
--model_name_or_path Qwen/Qwen2.5-7B-Instruct \
--adapter_name_or_path ./output/lora_qwen \
--template qwen \
--finetuning_type lora \
--export_dir ./merged_qwen_lora \
--export_size 2 # 2=导出合并后的 BF16 完整模型
导出后 merged_qwen_lora/ 就是一个普通模型文件夹,可以直接:
1. vllm serve ./merged_qwen_lora → 生产级高性能推理
2. ollama create my-model -f Modelfile → 本地一键部署
3. 用任何 HuggingFace 代码直接加载 → 科研/开发
═══════════════════════════════════════════════════════════════
方式 B: Python 代码合并(如果你不想用 LlamaFactory CLI)
═══════════════════════════════════════════════════════════════
from peft import PeftModel
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
model = PeftModel.from_pretrained(base, "./output/lora_qwen")
merged = model.merge_and_unload() # ★ 核心:W' = W_base + (alpha/r)·B·A
merged.save_pretrained("./merged_model") # 保存合并后的完整权重
═══════════════════════════════════════════════════════════════
方式 C: 量化后给 Ollama 用(链式操作)
═══════════════════════════════════════════════════════════════
① 先合并
llamafactory-cli export ... --export_dir ./merged_model
② 再量化为 GGUF(Ollama 支持的格式)
llamafactory-cli export
--model_name_or_path ./merged_model
--export_dir ./gguf_model
--export_quantization_bit 4 # 4-bit 量化,体积 ~4GB
💡 最佳实践
实验阶段用 PeftModel 动态加载(省磁盘,一个基座配多个 adapter),确认效果后合并导出为完整模型,交给 vLLM/Ollama 做生产部署。不要在生产环境中用动态加载——那 5% 的额外开销在高并发下会被放大。
QLoRA微调
QLoRA = LoRA + 基座 4-bit 量化。基座模型从 14GB(BF16)压到 ~4GB(NF4),显存再砍一半。单张 RTX 3090/4090(24GB)就能训练 Llama-7B,甚至可以挑战 13B。
QLoRA 用了三个关键技巧来在极致压缩显存的同时保持训练质量:
| 技术 | 做什么 | 省多少 |
|---|---|---|
| 4-bit NormalFloat (NF4) | 一种专门为神经网络权重分布设计的 4-bit 量化格式。比传统的线性量化更好地保留了权重的信息分布 | 从 16bit 降到 4bit → 省 75% 模型显存 |
| 双重量化 (Double Quant) | 对量化常数本身也做一次量化。量化时每个 64 个参数共享一个量化常数,把这些常数再量化 | 再省 ~0.4 bit/参数 |
| 分页优化器 (Paged Optimizer) | 当 GPU 显存不足时,把优化器状态自动换出到 CPU 内存,需要时再换回来 | 避免 OOM,显存峰值可控 |
QLoRA 前向传播时发生了什么:
1. 4-bit 量化权重 (存于显存) → 反量化为 BF16 → 正常做矩阵乘法
2. 反向传播时,梯度只流向 LoRA adapter(基座权重不更新)
3. 优化器状态 只保存 LoRA 参数的 m/v(基座的不需要)
关键:虽然存储是 4-bit,但计算是 BF16 → 训练精度几乎不受影响
既然反量化了,显存占用不会很高吗?
关键:虽然计算精度是 BF16,但不是全量解压。bitsandbytes 逐块解压——每次只把当前要算的一小块权重从 4-bit → BF16,算完立刻丢弃。全量 BF16 模型永远不会同时存在于显存中,所以训练精度几乎不受影响,显存也不爆炸。
WebUI 操作步骤
① Finetuning method → 选 "lora"(和 LoRA 一样,不是单独的 "qlora")
② 勾选 "Quantization bit" → 选 "4"(这就是 QLoRA 和 LoRA 的唯一 UI 差异)
③ 量化类型 → 选 "nf4"(NormalFloat 4-bit,最优选择)
④ 勾选 "Double quantization"(双重量化,再省一点显存)
其余参数(rank、alpha、lr 等)和 LoRA 完全一样。
QLoRA 的 YAML 配置文件
# ═══════════════════════════════════════════════════════════════
# 和 LoRA 配置的区别只有 3 行!
# ═══════════════════════════════════════════════════════════════
模型配置
model_name_or_path: Qwen/Qwen2.5-7B-Instruct
微调方法
finetuning_type: lora # 和 LoRA 一样!(不是单独的"qlora"类型)
★ QLoRA 独有的量化配置(下面 4 行是 LoRA 没有的)
quantization_bit: 4 # ★ 4-bit 量化 — QLoRA 的核心标志
quantization_type: nf4 # NormalFloat 4 — 最好的 4-bit 量化策略
double_quantization: true # 双重量化 — 对量化常数再量化,再省约 0.4bit/param
上面 3 行就是这个配置和 LoRA 配置的全部区别!
LoRA 参数(和纯 LoRA 完全一样)
lora_rank: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target: all
数据配置(和 LoRA 一样)
dataset: alpaca_zh
template: qwen
cutoff_len: 2048
训练参数(和 LoRA 一样)
output_dir: ./output/qlora_qwen
num_train_epochs: 3
per_device_train_batch_size: 2
gradient_accumulation_steps: 4
learning_rate: 2.0e-4
lr_scheduler_type: cosine
warmup_ratio: 0.1
注意:QLoRA 不要同时开 fp16 和量化,用量化就不需要混合精度了
fp16: true ← QLoRA 不需要这行,量化已经替代了混合精度
⚠️ QLoRA 的常见坑
① 不要同时开 fp16/bf16 和量化——量化已经处理了精度,再开混合精度会冲突甚至 OOM。
② QLoRA 训练速度可能比 LoRA 慢 10%~20%——因为每次前向都要做 4-bit→BF16 反量化。
③ NF4 是首选——不要用 int4,NF4 专门为神经网络权重设计,效果明显更好。
④ batch_size 可以比 LoRA 大——因为显存更充足,可以考虑把 batch 从 2 提到 4。
显存计算
通用显存公式
VRAM = Mparams + Mgrads + Moptim + Mactivations
| 组件 | 全参数 SFT | LoRA | QLoRA |
|---|---|---|---|
| 模型参数 | P × bytes_per_param | P × bytes_per_param | P × 0.5 bytes(含量化开销) |
| 梯度 | P × 2 bytes | Plora × 2 bytes(≈ 0.1% P) | Plora × 2 bytes |
| 优化器状态 | P × 8 bytes | Plora × 8 bytes | Plora × 8 bytes |
| 激活值 | ~10 GB | ~15 GB | ~10 GB |
# ═══════════════════════════════════════════════════════════════
# 符号说明:
# P = 模型总参数量 (7B, 13B, 70B...)
# P_lora = LoRA 可训练参数量 ≈ P × 0.001~0.005
# bytes = BF16=2, FP32=4, 4-bit≈0.5
# ═══════════════════════════════════════════════════════════════
--- 全参数 SFT (BF16) ---
模型: P × 2
梯度: P × 2
优化器: P × 4 × 2 = P × 8 (m+v, 各 FP32=4 bytes)
激活: ~10 GB (batch=1, seq=2048, 开了 gradient checkpointing)
VRAM ≈ P × 12 + 10 GB
7B: 7 × 12 + 10 = 94 GB → 需要 4×A100-80G
13B: 13 × 12 + 10 = 166 GB → 需要 8×A100-40G 或 4×A100-80G
70B: 70 × 12 + 10 = 850 GB → 需要 16×A100-80G
--- LoRA (BF16, rank=16, lora_target=all) ---
冻结模型: P × 2 (冻结,不存梯度、不存优化器)
P_lora ≈ P × 0.005 (rank=16 时约为 0.5% 的参数量)
梯度: P_lora × 2
优化器: P_lora × 8
激活: ~15 GB (LoRA 需要额外的 adapter 前向计算)
VRAM ≈ P × 2 + P_lora × 10 + 15 GB
7B: 14 + 0.035×10 + 15 ≈ 29 GB → RTX 3090/4090 (24GB) 不够?
↑ 等一下 — 实际训练时模型可以 offload 一部分到 CPU
↑ 开了 gradient checkpointing + 小 batch → 激活降到 ~8 GB
↑ 实际 LoRA (7B): 14 + 0.35 + 8 ≈ 22 GB → 可以跑!
--- QLoRA (NF4, rank=16) ---
量化模型: P × 0.5 (4-bit NF4, 含反量化开销)
P_lora ≈ P × 0.005
梯度: P_lora × 2
优化器: P_lora × 8
激活: ~10 GB
VRAM ≈ P × 0.5 + P_lora × 10 + 10 GB
7B: 3.5 + 0.35 + 10 ≈ 14 GB → ✅ 1×RTX 3090/4090 (24GB) 轻松
13B: 6.5 + 0.65 + 15 ≈ 22 GB → ✅ 1×RTX 3090/4090 刚刚好
70B: 35 + 3.5 + 30 ≈ 69 GB → ⚠️ 需要 A100-80G 或双卡
关键洞察:LoRA/QLoRA 省显存的核心在于优化器状态。Adam 的 momentum 和 variance 各占 4 bytes(FP32),两个加起来 = P×8 bytes,是模型参数(P×2 bytes BF16)的 4 倍。全参数 SFT 的优化器状态(7B→56GB)比模型本身(14GB)还大。LoRA 的优化器只需要保存那 0.1% 的 adapter 参数,直接从 56GB 降到 ~100MB。
显存计算速查
| 模型 | 参数量 | 全参数 SFT | LoRA (rank=16) | QLoRA (NF4, rank=16) |
|---|---|---|---|---|
| Qwen2.5-0.5B | 0.5B | ~16 GB | ~8 GB | ~4 GB |
| Qwen2.5-1.5B | 1.5B | ~28 GB | ~10 GB | ~5 GB |
| Qwen2.5-3B | 3B | ~46 GB | ~14 GB | ~7 GB |
| Llama-3-8B | 8B | ~106 GB | ~24 GB | ~14 GB |
| Qwen2.5-7B | 7B | ~94 GB | ~22 GB | ~13 GB |
| Qwen2.5-14B | 14B | ~178 GB | ~36 GB | ~20 GB |
| Llama-3-70B | 70B | ~850 GB | ~160 GB | ~65 GB |
| Qwen2.5-72B | 72B | ~874 GB | ~165 GB | ~67 GB |
LlamaFactory 多 GPU 训练
# ═══════════════════════════════════════════════════════════════
# 方式 1: DeepSpeed ZeRO-2(推荐,自动数据并行 + 优化器分片)
# ═══════════════════════════════════════════════════════════════
deepspeed --num_gpus=4 \
src/train.py \
--deepspeed examples/deepspeed/ds_z2_config.json \
--model_name_or_path Qwen/Qwen2.5-7B-Instruct \
--finetuning_type lora \
--dataset alpaca_zh \
--output_dir ./output/lora_4gpu \
--per_device_train_batch_size 4 \ # 4 卡 × batch=4 = 等效 batch=16
--gradient_accumulation_steps 2 \ # ×2 = 等效 batch=32
--learning_rate 2.0e-4 \
--fp16
# ═══════════════════════════════════════════════════════════════
# 方式 2: torchrun(PyTorch 原生多卡)
# ═══════════════════════════════════════════════════════════════
CUDA_VISIBLE_DEVICES=0,1,2,3
torchrun --nproc_per_node=4
src/train.py
--model_name_or_path Qwen/Qwen2.5-7B-Instruct
--finetuning_type lora
--dataset alpaca_zh
--output_dir ./output/lora_4gpu
--per_device_train_batch_size 4
--learning_rate 2.0e-4
--fp16
═══════════════════════════════════════════════════════════════
多卡全参数 SFT — 训练 70B 模型(需要 4×A100-80G)
═══════════════════════════════════════════════════════════════
deepspeed --num_gpus=4
src/train.py
--deepspeed examples/deepspeed/ds_z3_config.json \ # ZeRO-3: 参数+梯度+优化器全分片
--model_name_or_path meta-llama/Llama-3-70B-Instruct
--finetuning_type full \ # ★ 全参数微调
--dataset your_large_dataset
--output_dir ./output/full_70b
--per_device_train_batch_size 1
--gradient_accumulation_steps 16 \ # 等效 batch=1×16=16
--learning_rate 1.0e-5
--bf16
更多推荐




所有评论(0)