Chapter 12

案例:长思考链 GRPO(R1 风格)

📌 本章 recipe 整合自 examples/grpo_trainer/ 与社区公开的 DeepSeek-R1 / DAPO 复现实践 · commit 9c38b8bb1876 。

本章把"R1 风格的长思考链推理模型"训练从 0 到 1 走一遍,让你看到前面 11 章的知识在真实任务中如何组合。

12.1 目标

训练目标:从 Qwen3-4B-Base 出发,得到一个能在 MATH-500 / AIME 上有长思考链(response > 4K token)的数学推理模型,关键指标:

12.2 总体步骤

flowchart LR S0([Qwen3-4B-Base]) --> S1[Stage 1: SFT 冷启动
用少量带思考链数据] S1 --> S2[Stage 2: GRPO + 规则 reward
GSM8K → MATH] S2 --> S3[Stage 3: GRPO + DAPO 风格
更难数据集 + 长上下文] S3 --> S4[Stage 4: 评测 + 部署]

本章重点放在 Stage 2 / Stage 3,因为它们是 verl 最擅长的部分。Stage 1 SFT 可以用 LLaMA-Factory 或 verl 自带的 verl/trainer/sft_trainer.py 完成。

12.3 数据准备

cd /Users/zjw/Documents/LLM/train/repos/verl

# 数学题数据集预处理(含 ground truth)
python examples/data_preprocess/gsm8k.py --local_dir ~/data/gsm8k
python examples/data_preprocess/math_dataset.py --local_dir ~/data/math

# 检查样本(含 data_source 字段,用于 rule reward 路由)
python -c "
import pandas as pd
df = pd.read_parquet('~/data/math/train.parquet')
print(df.iloc[0].to_dict())
"

每条样本结构:

{
  "prompt": [{"role":"user","content":"Solve: ..."}],
  "data_source": "lighteval/MATH",
  "reward_model": {"ground_truth": "5"},
  "ability": "math",
  "extra_info": {...}
}

12.4 Stage 2:GSM8K GRPO

my_grpo_gsm8k.yaml

# my_grpo_gsm8k.yaml
defaults:
  - ppo_trainer
  - _self_

data:
  train_files: ${oc.env:HOME}/data/gsm8k/train.parquet
  val_files: ${oc.env:HOME}/data/gsm8k/test.parquet
  train_batch_size: 512
  max_prompt_length: 1024
  max_response_length: 2048
  filter_overlong_prompts: true
  truncation: error

actor_rollout_ref:
  model:
    path: Qwen/Qwen3-4B
    use_remove_padding: true
    enable_gradient_checkpointing: true

  actor:
    optim:
      lr: 1.0e-6
    ppo_mini_batch_size: 256
    ppo_micro_batch_size_per_gpu: 2
    ppo_epochs: 1
    use_kl_loss: true
    kl_loss_coef: 0.001
    entropy_coef: 0
    clip_ratio: 0.2

  rollout:
    name: vllm
    tensor_model_parallel_size: 2
    gpu_memory_utilization: 0.6
    n: 8                            # ★ GRPO 组大小
    temperature: 1.0
    max_num_batched_tokens: 8192

  ref:
    log_prob_micro_batch_size_per_gpu: 2

algorithm:
  adv_estimator: grpo               # ★ 关键
  norm_adv_by_std_in_grpo: true
  use_kl_in_reward: false
  kl_penalty: low_var_kl
  gamma: 1.0
  lam: 1.0

reward_model:
  style: rule                       # ★ 规则 reward

trainer:
  project_name: r1_repro
  experiment_name: qwen3_4b_gsm8k_grpo
  total_epochs: 15
  save_freq: 20
  test_freq: 5
  logger: [console, wandb]
  n_gpus_per_node: 8
  nnodes: 1
  default_local_dir: ./checkpoints/qwen3_4b_gsm8k_grpo

启动

cd /Users/zjw/Documents/LLM/train/repos/verl
ray stop --force; ray start --head

python -m verl.trainer.main_ppo \
  --config-path .. --config-name my_grpo_gsm8k

预期

12.5 Stage 3:MATH 升级 + 长上下文

Stage 2 上 GSM8K 收敛后,换更难数据集 MATH + 加长 response length:

# my_grpo_math.yaml(从 Stage 2 yaml 改)

actor_rollout_ref:
  model:
    path: ./checkpoints/qwen3_4b_gsm8k_grpo/actor_step_xxx    # ★ 从 Stage 2 继续

data:
  train_files: ${oc.env:HOME}/data/math/train.parquet
  val_files: ${oc.env:HOME}/data/math/test.parquet
  max_response_length: 4096                                    # ★ 加长

actor_rollout_ref:
  rollout:
    n: 16                                                      # ★ 更大组 → 更好 sample efficiency
  actor:
    optim:
      lr: 5.0e-7                                                # ★ 进阶阶段 lr 减半

启动同上,--config-name my_grpo_math。预期 reward/mean 慢速上升到 ~0.5(MATH 比 GSM8K 难得多)。

12.6 DAPO 风格升级

追求更稳的长思考链训练,叠 DAPO 改进:

algorithm.use_dapo_clip_higher: true
algorithm.clip_ratio_low: 0.2
algorithm.clip_ratio_high: 0.28        # 非对称 clip
algorithm.use_token_level_loss: true   # token-level reduction

这三个 flag 一上,response 长度会稳定增长不坍塌。

12.7 训练中要看的"两条曲线"

曲线含义异常预警
reward/mean 整体能力 飞涨 → reward hacking
response_length/mean 思考链长度 暴涨 → length hacking;坍塌 → token loss 没开

这两条曲线如果都缓慢上升,说明训练在正确方向上。

12.8 评测:用 main_eval.py

python -m verl.trainer.main_eval \
  data.path=$HOME/data/math/test.parquet \
  model.path=./checkpoints/qwen3_4b_math_grpo/actor_step_final \
  generation.temperature=0.0 \
  generation.n=1 \
  generation.max_new_tokens=4096

输出每条题目 + 模型答案 + 是否正确。可以用同一脚本对照 base / SFT / GRPO 各阶段做 ablation。

12.9 模型导出

训练后的 checkpoint 是 verl / FSDP / Megatron 自己的格式。用 verl 自带 merger 转 HF 格式:

python -m verl.model_merger \
  --checkpoint ./checkpoints/qwen3_4b_math_grpo/actor_step_final \
  --output_dir ./qwen3_4b_r1_repro_hf \
  --format hf

转换后的 ./qwen3_4b_r1_repro_hf/ 就是标准 HF 模型,可以:

12.10 这一路下来你做了什么

串起来再看一遍:

  1. 从 Qwen3-4B-Base 起步(一个"什么思考都不会"的基础模型);
  2. 跑 Stage 2 GSM8K GRPO,模型学到"算到最后给个数";
  3. 跑 Stage 3 MATH GRPO + DAPO,模型学到"该反思就反思";
  4. 评测 MATH-500,看准确率;
  5. 合并 checkpoint 到 HF,部署到推理引擎。

整套流程在 verl 里大约 2 份 YAML + 1 个启动命令 × 2 次。比起手写 RLHF 代码,verl 把工程量降到几乎只剩"调超参 + 看曲线"

12.11 你能从这里发散到哪

这些发散的 yaml 模板在 verl-recipe 子仓里都有现成参考。

12.12 长 CoT 训练的资源策略加分项

本章 Stage 3(MATH + DAPO,response 长度从 2K → 8K)正是 fully_async / PD 分离的最优场景。如果你训到一半发现吞吐撑不住、希望加速,可以分两档加:

第一档:把 rollout 切到 SGLang + PD 分离(ch07.14)

actor_rollout_ref.rollout.name=sglang \
actor_rollout_ref.rollout.disaggregation.mode=pd \
actor_rollout_ref.rollout.disaggregation.prefill_dp=2 \
actor_rollout_ref.rollout.disaggregation.decode_dp=6

把 8 张采样卡切成 prefill 2 + decode 6(具体比例看 prompt / response 长度比)。长 CoT 训练 prefill 短、decode 长,这种分配往往比平均分配快 20-40%。

第二档:one_step_off_policy 或 fully_async(ch05.13–14)

如果 Stage 3 一个 step 跑到 10 分钟以上,rollout 和 train 串行已经是显著瓶颈。切到 one_step_off_policy:

# 多加一组 ResourcePool
+resource_pool.rollout.n_gpus_per_node=4 \
+resource_pool.actor.n_gpus_per_node=4 \
actor_rollout_ref.hybrid_engine=false \
algorithm.rollout_correction=decoupled_token_is

把 8 卡机分成 actor 4 卡 + rollout 4 卡,分别异步推进,必须配 rollout_correction 否则 PPO 不收敛(参见 ch09.16)。

第三档:router replay(仅 MoE 模型)

本章 Stage 3 用的是 Qwen3-4B dense,不需要。若换成 Qwen3-30B-A3B / 235B-A22B 等 MoE 模型,务必开 actor_rollout_ref.actor.router_replay.mode=R2(ch06.10),否则训推 expert 路由不一致会导致 DAPO clip 完全失效。

12.13 R1 风格"长 CoT 涌现"的可见信号

训练成功的关键标识不是 reward 一个数字,而是多个指标的协同变化。下表是 R1 复现里"长 CoT 涌现"应当看到的曲线形态:

指标训练早期涌现中异常征兆
response_length/mean ~512 token 缓慢稳定上升到 2K-4K 暴涨到 max 上限 = length hacking
reward/mean ~0.3 稳定上升到 0.6+ 飞涨到 0.95+ = reward hacking
actor/entropy 2-3 缓慢下降到 1-2 跌到 < 0.3 = 策略坍塌
kl/mean (vs ref) ~0 稳定增长到 5-20 突破 100+ = 策略走偏,立刻减 lr
样本 "wait", "actually", "let me reconsider" 频率~0显著出现 不出现 = 没学到反思

最后一行需要自己 grep 几条样本——这是"R1 涌现"最强的人工可见信号,是模型从 short CoT 进化到 long CoT 的转折点。verl 不会自动统计,但你可以加一个 reward callback 或 hook 来计数。