大模型全参数训练与微调

大模型全参数训练

常见的大模型全参数训练方法

方法 阶段 需要什么模型 数据需求 显存 稳定性 代表
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,几千~几万条偏好数据

完整训练链路(从 0 到 ChatGPT 级别的对话模型):

# ═══════════════════════════════════════════════════════════════
# 格式 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 用组内相对排名替代
# ═══════════════════════════════════════════════════════════════
# TRL GRPOTrainer — 组内比较替代 Value Model,但 reward 可以来自 RM
# 示例:Stage 2 的 RM 模型打分 + 格式/长度的规则打分混合使用
# ═══════════════════════════════════════════════════════════════
from trl import GRPOTrainer, GRPOConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoModelForSequenceClassification
from datasets import load_dataset
import torch
─── Step 1: 加载 SFT 模型(Policy) ───
model = AutoModelForCausalLM.from_pretrained(
"./sft_qwen_final",
torch_dtype="auto", device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("./sft_qwen_final")
─── Step 2: 加载 Stage 2 训好的 Reward Model(可选,作为打分来源之一) ───
注意:GRPO 不要求必须有 RM,rule-based 打分也完全可行
这里展示 RM + 规则混合的工程实践
rm_model = AutoModelForSequenceClassification.from_pretrained(
"./reward_model_final",              # Stage 2 训好的打分器
num_labels=1, torch_dtype="auto", device_map="auto",
)
rm_tokenizer = AutoTokenizer.from_pretrained("./reward_model_final")
rm_model.eval()                             # 冻结,只用来打分,不参与梯度更新
─── Step 3: 定义打分函数 ───
def rm_reward(completions, prompts=None, **kwargs):
# ═══════════════════════════════════════════════════════
# 用法 1: 用 Stage 2 的 RM 模型打分(和 PPO 一样!)
# 这对于通用对话对齐(没有客观对错标准)是最实用的方案
# ═══════════════════════════════════════════════════════
if prompts is None:
prompts = kwargs.get("prompts", [""] * len(completions))
texts = []
for prompt, completion in zip(prompts, completions):
resp = completion[0]["content"] if isinstance(completion, list) else str(completion)
text = rm_tokenizer.apply_chat_template([   # 构造 RM 需要的输入格式
{"role": "user", "content": prompt},
{"role": "assistant", "content": resp},
], tokenize=False)
texts.append(text)
enc = rm_tokenizer(texts, return_tensors="pt", padding=True, truncation=True, max_length=512).to(rm_model.device)
with torch.no_grad():
scores = rm_model(**enc).logits.squeeze(-1).cpu().tolist()
return [scores] if isinstance(scores, float) else scores
def rule_format_reward(completions, kwargs):
# ═══════════════════════════════════════════════════════
# 用法 2: 用 Python 规则打分(不需要 RM 模型)
# 适合有客观标准的任务:数学答案对错、代码测试通过、格式规范
# ═══════════════════════════════════════════════════════
rewards = []
for c in completions:
text = c[0]["content"] if isinstance(c, list) else str(c)
score = 0.0
if len(text) > 50: score += 0.3              # 回答有一定长度
if "" in text or "```" in text: score += 0.3  # 有格式化内容
if text.endswith(("。", "!", "?")): score += 0.2 # 有正确结尾
rewards.append(score)
return rewards
─── Step 4: 准备数据 ───
dataset = load_dataset("your-username/grpo_prompts", split="train")
─── Step 5: 配置 GRPOConfig ───
grpo_config = GRPOConfig(
output_dir="./grpo_output",
per_device_train_batch_size=1,
num_generations=4,                          # ★ 每个 prompt 生成 N 个回答(N>1 是必须的!)
max_prompt_length=512,
max_completion_length=1024,
learning_rate=5e-6,
beta=0.04,                                # ★ KL 惩罚 — 控制偏离 Reference Model 的程度
temperature=0.9,
max_grad_norm=0.5,
logging_steps=5,
)
─── Step 6: 创建 GRPOTrainer — 奖励来源灵活组合 ───
trainer = GRPOTrainer(
model=model,
# ★ reward_funcs 可以是任意组合:纯 RM、纯规则、或混合
# 多个函数的分数会自动加权求和
reward_funcs=[rm_reward, rule_format_reward],
args=grpo_config,
train_dataset=dataset,
processing_class=tokenizer,
)
trainer.train()
trainer.save_model("./grpo_final")
═══════════════════════════════════════════════════════════════
GRPO 的 PPO 的关键区别:
✅ 省掉 Value Model (Critic) — 组内归一化替代 V(s) 估计
✅ 保留 Reward 信号 — RM / Rule-based / 混合均可
✅ num_generations > 1 — 组内比较的前提
advantage 的计算:
PPO:  advantage = reward - V(s)           ← 需要 Value Model
GRPO: advantage = (reward - mean) / std   ← 组内归一化,不要 Value Model
═══════════════════════════════════════════════════════════════

🔑 关键澄清

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 是:

  1. 生成阶段:Policy 模型生成 num_generations=4 个回答(自回归解码,吃 KV cache)
  2. 评分阶段:RM 模型对每个回答打分(forward only,不存梯度)
  3. 训练阶段:计算 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
posted @ 2026-07-22 09:06  黄艺龙  阅读(1)  评论(0)    收藏  举报