Skip to content

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 有两套主流开源框架可选:

对比维度verlslime
开发方字节跳动社区开源
训练范式同步 GRPO/PPO全异步 GRPO
推理引擎sglang / vLLMsglang
训练后端FSDP / MegatronMegatron
资源调度Ray + ResourcePoolRay + 独立 GPU 分组
配置方式Hydra YAML命令行参数
适合场景通用 RL 训练,稳定性优先高吞吐异步训练,效率优先
上手难度中等(配置较多)中等(需理解异步架构)

如何选择?

  • 初学者:推荐 verl,文档完善,社区活跃,配置化程度高
  • 追求效率:推荐 slime 全异步模式,训推分离,GPU 利用率更高
  • 资源有限:verl 的 Megatron 后端支持 TP 并行,2 张 3090 即可跑 0.6B

数据准备

GSM8K 数据集适配

GSM8K 是 OpenAI 发布的小学数学推理数据集,包含约 7,500 条训练数据和 1,319 条测试数据。verl 框架要求数据以 parquet 格式存储,且包含特定字段。

核心处理逻辑:

python
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 框架对数据有严格的字段要求:

json
{
  "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{} 格式答案的数据集,需要修改答案提取逻辑:

python
def parse_boxed_answer(text):
    """提取 LaTeX \\boxed{} 中的答案"""
    hits = re.findall(r'\\boxed\{((?:[^{}]|\{[^{}]*\})*)\}', text)
    return hits[-1] if hits else None
几何数据集 (Geometry3K) 的多模态适配

几何推理需要图片输入,数据格式中需要额外的 images 字段,并使用显式的 <think> 标签提示:

python
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 配置自动路由奖励函数:

python
# 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 会:

  1. 从模型生成中提取答案(支持 ####\boxed{} 两种格式)
  2. ground_truth 做字符串匹配(支持数值等价判断)
  3. 返回 0 或 1 的二值奖励

slime 中的 DAPO 奖励

slime 框架使用 DAPO(Dynamic Advantage Policy Optimization)风格的奖励:

bash
--rm-type dapo        # 使用 DAPO 风格奖励
--reward-key score    # 奖励值的字段名

DAPO 在标准 GRPO 基础上增加了动态采样和过滤机制,能更有效地利用训练信号。

训练配置

环境搭建

bash
# 创建环境
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.whl

DeepSpeed 配置

对于 SFT 等非 RL 训练阶段,使用 DeepSpeed ZeRO-1 即可:

yaml
# 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-Base0.6B2x 3090 (24GB)~2hGSM8K ~57%
Qwen3-8B-Base8B4x 4090 (48GB)~2hGSM8K ~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 配置管理,核心流程如下:

python
@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):

bash
# 基础配置
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 模型需要更多优化技巧:

bash
# 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 的异步架构有几个显著优势:

  1. 无 GPU 空闲等待:训练和推理在不同 GPU 上并行执行
  2. 动态 batch:使用 --use-dynamic-batch-size 根据序列长度动态调整
  3. 更高吞吐:在同等硬件上,异步训练的样本吞吐量通常更高

slime 训练脚本详解

以 0.6B 模型 2 GPU 训练为例(run_fully_async_0.6B_2gpus.sh):

bash
# 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 提交训练任务:

bash
# 启动 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:

bash
# 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 控制评估频率:

bash
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 日志:

bash
# 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 ParallelismTP=2TP=4线性
Param Offload不需要可选~20%(影响速度)
bash
# 8B 模型必须开启的选项
++actor_rollout_ref.model.enable_gradient_checkpointing=True
actor_rollout_ref.actor.megatron.optimizer_offload=True

超参调整建议

超参0.6B 推荐值8B 推荐值说明
学习率1e-61e-6两个规模通用
KL 系数0.0010.001过大会限制探索
Batch size88受显存限制
Micro batch22梯度累积步数 = batch/micro
采样数 (n)88GRPO 的组大小
Max response len10241024更长会显著增加显存

常见问题排查

训练初期 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)
  • 检查学习率是否过大
  • 确认参考策略正确加载

苏格拉底时刻

停下来思考

  1. R1-Zero 为什么能"无中生有"地涌现推理能力? 提示:Base 模型预训练阶段已经见过数学推导文本,RL 训练做的是"激活"而非"教授"。

  2. GRPO 相比 PPO 省掉了什么? 提示:PPO 需要一个 Critic 模型估计 Value Function,GRPO 用组内采样的相对奖励替代了 Value baseline。

  3. 为什么规则奖励在数学任务上足够好? 提示:数学答案的验证比生成容易得多(P vs NP 的直觉),二值奖励虽然稀疏但信号准确。

  4. 异步训练 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 采样一组回复 {o1,...,oG},将每个回复的 reward 减去组内均值再除以标准差作为 advantage 估计:Ai=(riμG)/σG。相比 PPO 省掉了 Critic 模型(节省约 50% 显存),且 advantage 估计更稳定(基于同一 prompt 的多个采样,减少了 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.12948R1 原始论文
verl 框架GitHub字节开源的 RL 训练框架
slime 框架GitHub全异步 RL 训练框架
DAPO 论文arXiv:2503.14476动态优势策略优化
Open-R1 项目GitHubHuggingFace 的 R1 复现
GSM8K 数据集HuggingFace小学数学推理基准