案例:长思考链 GRPO(R1 风格)
本章把"R1 风格的长思考链推理模型"训练从 0 到 1 走一遍,让你看到前面 11 章的知识在真实任务中如何组合。
12.1 目标
训练目标:从 Qwen3-4B-Base 出发,得到一个能在 MATH-500 / AIME 上有长思考链(response > 4K token)的数学推理模型,关键指标:
- GSM8K 准确率 60% → 85%+;
- 平均 response 长度从 ~500 token 涨到 2000-4000 token;
- 模型在解题过程中能"自我反思"(出现 "Let me reconsider..." 等)。
12.2 总体步骤
用少量带思考链数据] 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
预期
- ~ 4-8 小时(8 卡 H100);
- reward/mean 从 0.1 升到 0.7-0.85;
- response_length/mean 从 ~200 增长到 ~500-800;
- 模型偶尔出现 "Let me verify" 类反思。
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 模型,可以:
transformers.AutoModelForCausalLM.from_pretrained()加载;- vLLM 起 OpenAI 兼容 API(见 LLaMA-Factory ch12);
- 转 GGUF / GPTQ 量化部署。
12.10 这一路下来你做了什么
串起来再看一遍:
- 从 Qwen3-4B-Base 起步(一个"什么思考都不会"的基础模型);
- 跑 Stage 2 GSM8K GRPO,模型学到"算到最后给个数";
- 跑 Stage 3 MATH GRPO + DAPO,模型学到"该反思就反思";
- 评测 MATH-500,看准确率;
- 合并 checkpoint 到 HF,部署到推理引擎。
整套流程在 verl 里大约 2 份 YAML + 1 个启动命令 × 2 次。比起手写 RLHF 代码,verl 把工程量降到几乎只剩"调超参 + 看曲线"。
12.11 你能从这里发散到哪
- 换数据集:DAPO-Math-17K / OlympiadBench / 自家行业数据;
- 换基座:Qwen3-30B-A3B / Qwen3-235B-A22B(要切 Megatron);
- 换算法:试 RLOO / REINFORCE++ 看对比;
- 换 reward:用 GPT-4 当 judge(api 风格);
- 多模态:Qwen3-VL → 用 VL 数据做 GRPO。
这些发散的 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 来计数。