R1 风格推理模型训练复现
一句话总结: 用开源工具复现 DeepSeek-R1 的推理能力——从数据准备到 GRPO 训练到评估的完整指南,覆盖 verl 和 slime 两套框架,支持从 0.6B 到 8B 的 Scaling 实验。
R1 是什么
DeepSeek-R1 的核心创新
DeepSeek-R1 是 2025 年初最具影响力的开源推理模型,其核心创新在于:不依赖人工标注的 CoT 数据,仅通过 RL 训练就能让模型涌现出 Chain-of-Thought 推理能力。
传统做法是先收集人工标注的推理过程(SFT 阶段),再做偏好对齐。R1 证明了一条更优路径:
Base Model (无推理能力)
↓ RL 训练 (GRPO + 规则奖励)
R1-Zero (涌现 CoT,但格式混乱)
↓ 少量 SFT 数据做格式规范
R1 (结构化推理 + 高质量输出)"Aha Moment"——推理能力的涌现
R1 训练过程中最令人兴奋的发现是 Aha Moment:模型在 RL 训练到一定步数后,会突然开始在 <think> 标签中生成自我反思和验证步骤,例如:
<think>
Wait, let me reconsider this approach...
I made an error in step 2. Let me redo the calculation.
Actually, 3 * 15 = 45, not 35. So the correct answer is...
</think>这种自我纠错能力不是通过监督学习"教"出来的,而是在 RL 训练中自发涌现的。
我们要复现什么
本指南复现的是 R1-Zero 阶段——纯 RL 训练:
| 目标 | 具体内容 |
|---|---|
| 基座模型 | Qwen3-0.6B-Base / Qwen3-8B-Base |
| 训练方法 | GRPO(Group Relative Policy Optimization) |
| 训练数据 | 数学推理数据集(GSM8K / 自定义数据) |
| 奖励信号 | 规则奖励(答案正确性验证) |
| 评估指标 | GSM8K / MATH test set accuracy |
技术方案选择:verl vs slime
复现 R1 有两套主流开源框架可选:
| 对比维度 | verl | slime |
|---|---|---|
| 开发方 | 字节跳动 | 社区开源 |
| 训练范式 | 同步 GRPO/PPO | 全异步 GRPO |
| 推理引擎 | sglang / vLLM | sglang |
| 训练后端 | FSDP / Megatron | Megatron |
| 资源调度 | Ray + ResourcePool | Ray + 独立 GPU 分组 |
| 配置方式 | Hydra YAML | 命令行参数 |
| 适合场景 | 通用 RL 训练,稳定性优先 | 高吞吐异步训练,效率优先 |
| 上手难度 | 中等(配置较多) | 中等(需理解异步架构) |
如何选择?
- 初学者:推荐 verl,文档完善,社区活跃,配置化程度高
- 追求效率:推荐 slime 全异步模式,训推分离,GPU 利用率更高
- 资源有限:verl 的 Megatron 后端支持 TP 并行,2 张 3090 即可跑 0.6B
数据准备
GSM8K 数据集适配
GSM8K 是 OpenAI 发布的小学数学推理数据集,包含约 7,500 条训练数据和 1,319 条测试数据。verl 框架要求数据以 parquet 格式存储,且包含特定字段。
核心处理逻辑:
import re
import datasets
def parse_answer(raw_answer: str) -> str:
"""从 GSM8K 的 answer 字段中解析出最终数值"""
# GSM8K 答案格式:推导过程后以 #### 开头给出最终数字
hits = re.findall(r"####\s*(-?[\d,\.]+)", raw_answer)
assert len(hits) > 0, f"无法从答案中解析数值: {raw_answer[:80]}"
return hits[-1].replace(",", "") # 取最后一个匹配,去掉千位逗号
# 加载数据集
dataset = datasets.load_dataset("openai/gsm8k", "main")
STEP_BY_STEP_SUFFIX = (
"Please reason through this problem step by step, "
'then write your final numeric answer after the "####" marker.'
)
def build_formatter(split_name: str):
"""返回一个 map 函数,将原始样本转换为 verl 所需的 parquet 字段格式"""
def _transform(sample, row_idx):
prompt_text = sample["question"] + " " + STEP_BY_STEP_SUFFIX
answer_num = parse_answer(sample["answer"])
return {
"source": "openai/gsm8k",
"prompt": [{"role": "user", "content": prompt_text}],
"task_type": "math",
"reward_model": {
"style": "rule", # 使用规则奖励
"ground_truth": answer_num, # 标准答案
},
"metadata": {
"split": split_name,
"index": row_idx,
},
}
return _transform
train_dataset = dataset["train"].map(build_formatter("train"), with_indices=True)
test_dataset = dataset["test"].map(build_formatter("test"), with_indices=True)
# 保存为 parquet
train_dataset.to_parquet("~/data/gsm8k/train.parquet")
test_dataset.to_parquet("~/data/gsm8k/test.parquet")verl 数据格式规范
verl 框架对数据有严格的字段要求:
{
"source": "openai/gsm8k",
"prompt": [{"role": "user", "content": "问题文本..."}],
"task_type": "math",
"reward_model": {
"style": "rule",
"ground_truth": "42"
},
"metadata": {"split": "train", "index": 0}
}关键字段说明
source:决定使用哪个 reward function。设为"openai/gsm8k"时触发 GSM8K 规则匹配,设为"lighteval/MATH"时触发 LaTeX 数学式匹配reward_model.style:"rule"表示使用规则奖励,verl 内置了多种数学答案匹配规则prompt:必须是 chat format 的 message list
自定义数据集适配
对于 MATH 等使用 \boxed{} 格式答案的数据集,需要修改答案提取逻辑:
def parse_boxed_answer(text):
"""提取 LaTeX \\boxed{} 中的答案"""
hits = re.findall(r'\\boxed\{((?:[^{}]|\{[^{}]*\})*)\}', text)
return hits[-1] if hits else None几何数据集 (Geometry3K) 的多模态适配
几何推理需要图片输入,数据格式中需要额外的 images 字段,并使用显式的 <think> 标签提示:
GEOMETRY_PROMPT = (
"First, reason about the problem internally as a step-by-step thought process. "
"Then give your final answer. "
r"Wrap your reasoning inside <think> </think> tags. "
r"Put the final answer inside \boxed{}."
)
data = {
"source": "hiyouga/geometry3k",
"prompt": [{"role": "user", "content": prompt}],
"images": images, # 几何图片
"task_type": "math",
"reward_model": {"style": "rule", "ground_truth": answer},
}Reward 设计
规则奖励 vs 模型奖励
R1 复现中最简洁高效的方式是规则奖励(Rule-based Reward):
| 奖励类型 | 实现方式 | 优缺点 |
|---|---|---|
| 规则奖励 | 答案字符串匹配 | 简单可靠,无需额外模型;但只适用于有标准答案的任务 |
| PRM 模型 | 过程奖励模型打分 | 能评估推理过程质量;但需要训练额外模型 |
| ORM 模型 | 结果奖励模型打分 | 适用于开放式任务;但信号较稀疏 |
数学推理任务天然适合规则奖励——答案要么对要么错,无需主观判断。
verl 中的奖励实现
verl 通过 reward_model 配置自动路由奖励函数:
# verl 内部根据 data_source 和 style 字段选择奖励函数
reward_fn = load_reward_manager(
config, tokenizer, num_examine=0,
**config.reward_model.get("reward_kwargs", {})
)当 reward_model.style == "rule" 时,verl 会:
- 从模型生成中提取答案(支持
####和\boxed{}两种格式) - 与
ground_truth做字符串匹配(支持数值等价判断) - 返回 0 或 1 的二值奖励
slime 中的 DAPO 奖励
slime 框架使用 DAPO(Dynamic Advantage Policy Optimization)风格的奖励:
--rm-type dapo # 使用 DAPO 风格奖励
--reward-key score # 奖励值的字段名DAPO 在标准 GRPO 基础上增加了动态采样和过滤机制,能更有效地利用训练信号。
训练配置
环境搭建
# 创建环境
conda create -n llm python=3.11
conda activate llm
# 安装核心依赖(verl 方案)
pip install verl==0.7.0 torch==2.8.0 vllm==0.11.0
# 安装 flash-attention(需从 GitHub Release 下载对应版本 whl)
pip install flash_attn-2.8.3+cu12torch2.8-cp312-cp312-linux_x86_64.whlDeepSpeed 配置
对于 SFT 等非 RL 训练阶段,使用 DeepSpeed ZeRO-1 即可:
# deepspeed_zero1.yaml
compute_environment: LOCAL_MACHINE
deepspeed_config:
deepspeed_multinode_launcher: standard
gradient_accumulation_steps: 1
zero3_init_flag: false
zero_stage: 1
distributed_type: DEEPSPEED
mixed_precision: 'bf16'
num_machines: 1
num_processes: 8模型选择策略
| 模型 | 参数量 | 最低显存 | 训练时间(1 epoch) | 预期效果 |
|---|---|---|---|---|
| Qwen3-0.6B-Base | 0.6B | 2x 3090 (24GB) | ~2h | GSM8K ~57% |
| Qwen3-8B-Base | 8B | 4x 4090 (48GB) | ~2h | GSM8K ~92% |
为什么选 Base 模型?
R1 的核心假设是:推理能力可以从 Base 模型通过 RL 直接涌现,不需要先做 SFT。使用 Base 模型而非 Chat 模型,更能验证这一假设。
verl 训练实战
架构概览
verl 的 PPO/GRPO 训练流程基于 Ray 分布式框架:
┌─────────────────────────────────────────┐
│ RayPPOTrainer │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ Actor │ │ Rollout │ │ Ref │ │
│ │ (训练) │ │ (推理) │ │ Policy│ │
│ └────┬─────┘ └────┬─────┘ └───┬───┘ │
│ │ │ │ │
│ └──────────────┼────────────┘ │
│ ↓ │
│ ResourcePool │
│ (GPU 资源共享管理) │
└─────────────────────────────────────────┘main_ppo.py 核心流程
verl 的训练入口 main_ppo.py 使用 Hydra 配置管理,核心流程如下:
@hydra.main(config_path="config", config_name="ppo_trainer")
def main(config):
run_ppo(config)
def run_ppo(config):
# 1. 初始化 Ray 集群
ray.init(**ray_init_kwargs)
# 2. 创建 TaskRunner 并在 Ray 上执行
runner = TaskRunner.remote()
ray.get(runner.run.remote(config))TaskRunner.run() 是训练的核心,四个阶段依次执行:注册 Worker(Actor/Rollout/Critic/RefPolicy)-> 加载 tokenizer 和 reward function -> 创建训练/验证数据集 -> 初始化 RayPPOTrainer 并调用 trainer.fit() 启动训练循环。
verl 的 Worker 支持 fsdp/fsdp2(PyTorch 原生分布式)和 megatron(Megatron-LM 后端,适合大模型 TP/PP 并行)两种策略,通过配置切换。
训练脚本详解
以 Qwen3-0.6B 的训练脚本为例(run_qwen3-0_6b.sh):
# 基础配置
NGPUS=4 # GPU 数量
NBATCHS=8 # 训练 batch size
NMICROBATCHS=2 # micro batch size (梯度累积 = NBATCHS/NMICROBATCHS)
NSAMPLES=8 # 每个 prompt 采样数(GRPO 的 G)
python3 -m verl.trainer.main_ppo \
--config-name='ppo_megatron_trainer.yaml' \
# === 算法配置 ===
algorithm.adv_estimator=grpo \ # 使用 GRPO 算法
# === 数据配置 ===
data.train_files='./data/train.parquet' \
data.val_files='./data/test.parquet' \
data.max_prompt_length=512 \
data.max_response_length=1024 \
# === 模型配置 ===
actor_rollout_ref.model.path="Qwen3-0.6B-Base" \
actor_rollout_ref.actor.optim.lr=1e-6 \ # 学习率
# === KL 约束 ===
actor_rollout_ref.actor.use_kl_loss=True \
actor_rollout_ref.actor.kl_loss_coef=0.001 \
actor_rollout_ref.actor.kl_loss_type=low_var_kl \
# === Rollout 配置 ===
actor_rollout_ref.rollout.name=sglang \ # 使用 sglang 推理
actor_rollout_ref.rollout.n=$NSAMPLES \ # 每个 prompt 采样 8 次
actor_rollout_ref.rollout.mode=async \ # 异步 rollout
# === Megatron 并行 ===
actor_rollout_ref.actor.strategy="megatron" \
actor_rollout_ref.actor.megatron.tensor_model_parallel_size=$NGPUS \
# === 训练控制 ===
trainer.total_epochs=1 \
trainer.test_freq=10 # 每 10 步评估一次关键参数解读
algorithm.adv_estimator=grpo:使用 GRPO 而非 PPO。GRPO 不需要 Critic 模型,用组内相对优势估计rollout.n=8:每个 prompt 采样 8 个回复,这是 GRPO 的 "Group" —— 用组内对比计算优势kl_loss_type=low_var_kl:低方差 KL 散度估计,比标准 KL 更稳定rollout.gpu_memory_utilization=0.5:推理引擎只用 50% 显存,剩下给训练
Qwen3-8B 的 Scaling 配置差异
8B 模型需要更多优化技巧:
# 8B 模型额外配置
actor_rollout_ref.model.path="Qwen3-8B-Base" \
actor_rollout_ref.actor.megatron.optimizer_offload=True \ # 优化器卸载到 CPU
actor_rollout_ref.actor.megatron.param_offload=False \ # 参数不卸载
actor_rollout_ref.actor.megatron.grad_offload=False \ # 梯度不卸载
data.max_prompt_length=256 \ # 缩短 prompt 长度以节省显存Offload 策略
8B 模型在 4x 4090 上需要开启 optimizer_offload,将 Adam 优化器状态(每参数 8 字节)卸载到 CPU 内存。仅卸载优化器对训练速度影响较小(~10%),但能显著降低显存需求。
slime 异步训练
全异步架构
slime 框架的核心优势是 训推完全分离:
┌──────────────────────────────────────────┐
│ Ray Cluster │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Training GPU │ │ Rollout GPU │ │
│ │ (Megatron) │ │ (sglang) │ │
│ │ │ │ │ │
│ │ GPU 0,1 │ │ GPU 2,3 │ │
│ │ TP=2 │ │ TP=2 │ │
│ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ └───────┬───────────┘ │
│ ↓ │
│ Async Data Buffer │
│ (训练和推理异步交换数据) │
└──────────────────────────────────────────┘与 verl 的同步模式相比,slime 的异步架构有几个显著优势:
- 无 GPU 空闲等待:训练和推理在不同 GPU 上并行执行
- 动态 batch:使用
--use-dynamic-batch-size根据序列长度动态调整 - 更高吞吐:在同等硬件上,异步训练的样本吞吐量通常更高
slime 训练脚本详解
以 0.6B 模型 2 GPU 训练为例(run_fully_async_0.6B_2gpus.sh):
# GPU 分配:1 张训练 + 1 张推理
MY_GPUS_PER_NODE=2
MEGATRON_GPUS=1 # 训练用 GPU
SGLANG_GPUS=1 # 推理用 GPU
# Rollout 配置
ROLLOUT_ARGS=(
--rollout-function-path fully_async_rollout.generate_rollout_fully_async
--prompt-data ${PROMPT_SET} # 训练数据 (JSONL 格式)
--apply-chat-template # 自动应用 chat template
--rm-type dapo # DAPO 奖励类型
--num-rollout 3000 # 总 rollout 数
--rollout-batch-size 4 # 每次推理 batch
--n-samples-per-prompt 4 # 每个 prompt 采样 4 次
--rollout-max-response-len 4096 # 最大生成长度
--global-batch-size 16 # 全局训练 batch
--balance-data # 数据平衡
)
# GRPO 算法配置
GRPO_ARGS=(
--advantage-estimator grpo
--use-kl-loss
--kl-loss-coef 0.00 # 注意:slime 中 KL 系数设为 0
--eps-clip 0.2 # PPO clip 范围
--eps-clip-high 0.28 # 上界 clip(DAPO 特性)
--use-tis # 使用 Truncated Importance Sampling
)
# 优化器配置
OPTIMIZER_ARGS=(
--optimizer adam
--lr 1e-6
--lr-decay-style constant # 常数学习率
--weight-decay 0.1
--optimizer-cpu-offload # 优化器 CPU offload
--use-precision-aware-optimizer # 精度感知优化器
)启动训练
slime 通过 Ray Job 提交训练任务:
# 启动 Ray 集群
ray start --head --num-gpus ${MY_GPUS_PER_NODE}
# 提交训练任务
ray job submit --address="http://127.0.0.1:8265" \
-- python3 train_async.py \
--actor-num-nodes 1 \
--actor-num-gpus-per-node ${MEGATRON_GPUS} \
--rollout-num-gpus ${SGLANG_GPUS} \
${MODEL_ARGS[@]} ${ROLLOUT_ARGS[@]} \
${OPTIMIZER_ARGS[@]} ${GRPO_ARGS[@]}Retool 模式:训推共享 GPU
slime 还支持 Retool(colocate)模式,训练和推理共享同一组 GPU:
# Retool 模式:4 张 GPU 同时用于训练和推理
ray job submit -- python3 train.py \
--actor-num-gpus-per-node 4 \
--colocate \ # 启用 colocate 模式
--sglang-mem-fraction-static 0.7 # sglang 占 70% 显存Retool vs Fully Async
- Retool:适合 GPU 数量少但单卡显存大的场景(如 4x A100)
- Fully Async:适合 GPU 数量多但单卡显存有限的场景(如 8x 3090)
评估与分析
GSM8K 准确率评估
verl 内置了定期评估机制,通过 trainer.test_freq 控制评估频率:
trainer.test_freq=10 # 每 10 个训练步评估一次评估时使用验证集数据,计算模型生成答案的正确率。典型的训练曲线如下:
训练步数 0.6B 准确率 8B 准确率
Step 0 ~5% ~15%
Step 50 ~25% ~50%
Step 100 ~40% ~75%
Step 200 ~50% ~85%
Step 300 ~55% ~90%
Final ~57% ~92%WandB 监控
两套框架都支持 WandB 日志:
# verl
trainer.logger='["console","wandb"]'
trainer.project_name='verl_grpo'
# slime
--use-wandb
--wandb-project fully-async-0.6B-2gpu关注的核心指标:
- reward/mean:平均奖励值(应持续上升)
- reward/accuracy:正确率
- kl_divergence:KL 散度(不应过大)
- entropy:策略熵(应缓慢下降但不坍缩)
从 0.6B 到 8B:Scaling 训练的注意事项
显存管理
| 技术 | 0.6B 是否需要 | 8B 是否需要 | 显存节省 |
|---|---|---|---|
| Gradient Checkpointing | 推荐 | 必须 | ~40% |
| Optimizer Offload | 不需要 | 必须 | ~30% |
| Tensor Parallelism | TP=2 | TP=4 | 线性 |
| Param Offload | 不需要 | 可选 | ~20%(影响速度) |
# 8B 模型必须开启的选项
++actor_rollout_ref.model.enable_gradient_checkpointing=True
actor_rollout_ref.actor.megatron.optimizer_offload=True超参调整建议
| 超参 | 0.6B 推荐值 | 8B 推荐值 | 说明 |
|---|---|---|---|
| 学习率 | 1e-6 | 1e-6 | 两个规模通用 |
| KL 系数 | 0.001 | 0.001 | 过大会限制探索 |
| Batch size | 8 | 8 | 受显存限制 |
| Micro batch | 2 | 2 | 梯度累积步数 = batch/micro |
| 采样数 (n) | 8 | 8 | GRPO 的组大小 |
| Max response len | 1024 | 1024 | 更长会显著增加显存 |
常见问题排查
训练初期 reward 长时间不涨
- 检查数据格式:确认
reward_model.ground_truth字段有值 - 检查
source是否匹配正确的规则奖励函数 - 降低学习率到 5e-7 试试
OOM(显存不足)
- 开启 gradient checkpointing
- 减小
max_response_length(从 1024 降到 512) - 开启 optimizer offload
- 减小
rollout.gpu_memory_utilization(从 0.5 降到 0.4)
KL 散度爆炸
- 增大
kl_loss_coef(从 0.001 到 0.01) - 检查学习率是否过大
- 确认参考策略正确加载
苏格拉底时刻
停下来思考
R1-Zero 为什么能"无中生有"地涌现推理能力? 提示:Base 模型预训练阶段已经见过数学推导文本,RL 训练做的是"激活"而非"教授"。
GRPO 相比 PPO 省掉了什么? 提示:PPO 需要一个 Critic 模型估计 Value Function,GRPO 用组内采样的相对奖励替代了 Value baseline。
为什么规则奖励在数学任务上足够好? 提示:数学答案的验证比生成容易得多(P vs NP 的直觉),二值奖励虽然稀疏但信号准确。
异步训练 vs 同步训练的 trade-off 是什么? 提示:异步训练用的是"过时"的 rollout 数据(off-policy),但 GPU 利用率更高。GRPO 的 importance sampling ratio clip 正是为了缓解这个问题。
面试考点
Q: DeepSeek-R1 和 OpenAI O1 的技术路线有什么本质区别?
A: O1 的技术细节未公开,但从公开信息推测使用了大量人工标注的 CoT 数据做 SFT + RL。R1 的突破在于证明了 纯 RL 路线(R1-Zero)也能涌现推理能力,无需人工 CoT 标注。R1 的完整流程是 R1-Zero(RL) → 少量 SFT 规范格式 → 继续 RL,大幅降低了数据标注成本。
Q: GRPO 的数学原理是什么?相比 PPO 有什么优势?
A: GRPO(Group Relative Policy Optimization)对每个 prompt 采样一组回复
Q: 训练 R1 时为什么用 Base 模型而不是 Chat 模型?
A: 使用 Base 模型有两个原因:(1) 验证"推理能力可以从 RL 直接涌现"这一核心假设;(2) Chat 模型已经被 SFT 过的回答模式"锁定"了,RL 训练需要更大的 KL 散度才能跳出已有模式,反而更难训练。Base 模型的策略空间更大,RL 有更多的探索自由度。
Q: 如何判断 R1 训练是否出现了"Aha Moment"?
A: 监控以下信号:(1) reward 曲线出现跳变而非渐进上升;(2) 模型输出中开始出现自我纠错模式("Wait, let me reconsider...");(3) 推理过程变长但准确率同步提升;(4) 模型开始对不确定的步骤进行验证("Let me verify...")。这些通常发生在训练中期,是 RL 探索到了高效推理策略的标志。
推荐资源
| 资源 | 链接 | 说明 |
|---|---|---|
| DeepSeek-R1 论文 | arXiv:2501.12948 | R1 原始论文 |
| verl 框架 | GitHub | 字节开源的 RL 训练框架 |
| slime 框架 | GitHub | 全异步 RL 训练框架 |
| DAPO 论文 | arXiv:2503.14476 | 动态优势策略优化 |
| Open-R1 项目 | GitHub | HuggingFace 的 R1 复现 |
| GSM8K 数据集 | HuggingFace | 小学数学推理基准 |