Chapter 03

快速上手:单机 GRPO 例子

📌 本章命令基于 verl-project/verl · commit 9c38b8bb1876 。 我们跑 examples/grpo_trainer/run_qwen3_4b_fsdp.sh ,最小算力 1 节点 × 8 卡(GPU)。

3.1 选哪个 example 跑

examples/grpo_trainer/ 下大约 20+ 个启动脚本,命名规则统一:

run_<模型>_<后端>.sh
脚本规模训练后端采样后端
run_qwen3_4b_fsdp.sh 4B FSDP vLLM
run_qwen2_5_32b_fsdp.sh 32B FSDP vLLM
run_qwen3_30b_a3b_megatron.sh 30B MoEMegatron vLLM
run_qwen2_5_vl_7b_fsdp.sh VL 7BFSDP vLLM
run_deepseek_v3_671b_megatron.sh 671BMegatron vLLM

新手首选 run_qwen3_4b_fsdp.sh:4B 模型在 8 张 A100/H100 上能跑、依赖最少、报错最容易看懂。

3.2 准备数据:GSM8K

verl 内置了 GSM8K(小学数学题)的预处理脚本,是 GRPO 训练的"hello world":

# 在 verl 目录里
cd /Users/zjw/Documents/LLM/train/repos/verl

# 用 verl 自带预处理脚本把 HF datasets 转 parquet(带 prompt 模板)
python examples/data_preprocess/gsm8k.py --local_dir ~/data/gsm8k

# 看产出
ls ~/data/gsm8k/
# train.parquet  test.parquet

每条样本包含:

3.3 启动训练

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

# 单机 8 卡 H100/A100;脚本会自动 ray start --head
bash examples/grpo_trainer/run_qwen3_4b_fsdp.sh

第一次启动会做:

  1. ray start --head 起 Ray head;
  2. 从 HuggingFace 下载 Qwen/Qwen3-4B(约 8 GB);
  3. 在每张 GPU 上 init FSDP;
  4. 起 vLLM rollout worker(ROLLOUT_TP=2 时占 2 卡);
  5. 跑数据预处理、tokenize;
  6. 正式进入训练循环。

10 分钟左右你会看到 step 0 的输出:

step:1 - reward: 0.12, kl: 0.00, response_length: 256, ...
step:2 - reward: 0.14, ...

3.4 启动脚本是怎么组织的

打开 examples/grpo_trainer/run_qwen3_4b_fsdp.sh,会看到它本质上是一长串 Hydra override。脚本分这几段:

变量 / Hydra key含义
用户可调MODEL_PATH / TRAIN_FILE / NNODES / ...暴露给用户的最常用旋钮
DATA algorithm.adv_estimator=grpo
data.train_files=...
算法与数据
MODEL actor_rollout_ref.model.path=...基座模型与开关
ACTOR actor_rollout_ref.actor.optim.lr=...
+ batch / grad accum
训练 actor 的超参
ROLLOUT actor_rollout_ref.rollout.tensor_model_parallel_size=...vLLM 采样配置
REF / CRITIC...引用模型 / 价值模型
REWARD reward_model.style=rule用规则 reward(GSM8K 自动验证)
TRAINER trainer.project_name=...
trainer.total_epochs=...
全局训练超参

最后一行非常长,类似:

python3 -m verl.trainer.main_ppo \
    "${DATA[@]}" \
    "${MODEL[@]}" \
    "${ACTOR[@]}" \
    "${ROLLOUT[@]}" \
    ...

这就是 verl 的入口范式python -m verl.trainer.main_ppo + 一堆 key=value

3.5 训练时的进程拓扑

单机 8 卡 + ROLLOUT_TP=2 的运行时长这样:

flowchart LR C["Controller process
(main_ppo.py 跑算法控制流)"] C -->|Ray RPC| A0["Actor Worker 0
GPU 0,1"] C -->|Ray RPC| A1["Actor Worker 1
GPU 2,3"] C -->|Ray RPC| R0["Rollout Worker 0
vLLM, GPU 4,5"] C -->|Ray RPC| R1["Rollout Worker 1
vLLM, GPU 6,7"] C -->|Ray RPC| Ref["Reference forward
colocated 在 actor 上"] C -->|Ray RPC| Rwd["Reward (rule-based)
CPU 上跑"]

这种"controller + 多 worker group"的样子,就是 HybridFlow 架构,第 4 章会展开。

3.6 训练过程中关注什么

启动后,verl 会输出标准日志 + 写到 WandB / TensorBoard(看 trainer.logger 配置)。重点关注:

指标含义预期
reward/mean 当前 batch 平均奖励 逐步上升,GSM8K 从 ~0.1 升到 ~0.6
kl/mean policy ↔ reference 的 KL 稳定在小值(< 1)
response_length/mean生成长度 稳定或略升(思考链变长)
entropy 策略熵 不应坍塌到 0
actor/grad_norm 梯度范数 不应剧烈跳变;爆涨 → 学习率太高
throughput 每秒 token 数 视硬件而定

3.7 看完这章你应该能做什么

3.8 跑完 GSM8K 之后

4B + GSM8K + 15 epoch 大约 1-3 小时(视硬件)。跑完后可以:

  1. 用 verl 自带的 main_eval.py 跑测试集打分;
  2. verl/model_merger/ 把 ZeRO / FSDP shard 合成 HF 格式;
  3. 挂到 vLLM 起 OpenAI 兼容服务;
  4. 换其他数据集(MATH / DAPO-Math-17K / 自家业务)继续玩。

这套流程在第 12 章"案例:长思考链 GRPO"会完整演示一次。

3.9 入口背后:main_ppo.py 调用栈

run_qwen3_4b_fsdp.sh 最后那行 python -m verl.trainer.main_ppo ... 拆开看,从命令行到训练循环之间发生了什么:

位置做什么
1main_ppo.py:39 Hydra @hydra.main(config_path="config", config_name="ppo_trainer") 拦截入口,解析所有 key=value override 后调 main(config)
2main_ppo.py:46 auto_set_device(config) 检测当前是 CUDA / Ascend NPU / ROCm,设 device
3main_ppo.py:47 migrate_legacy_reward_impl(config) 兼容旧版 reward 字段命名
4main_ppo.py:48 → 52run_ppo(config) 真正起 Ray 集群
5main_ppo.py:67 trainer/runtime_env.yaml 拼出 Ray runtime env
6main_ppo.py:71 检测到 algorithm.transfer_queue 段就设 TRANSFER_QUEUE_ENABLE=1
7main_ppo.py:80 ray.init(**ray_init_kwargs),merge 用户的 ray_init 覆盖项
8main_ppo.py:83 TaskRunner 包成 ray.remote(num_cpus=1) —— 这是为了把它赶下 head 节点,避免和 controller 抢资源
9main_ppo.py:99 如果 profile.tool=nsight,再包一层 runtime_env={"nsight": ...}
10main_ppo.py:102 ray.get(runner.run.remote(config)) 阻塞等训练完成

3.10 TaskRunner 角色:driver-side 装配工

TaskRunnermain_ppo.py:111 起的 Ray Actor)做的事是把 controller 需要的所有 WorkerGroup 都注册好,然后调 fit() 开始训练。关键方法:

方法位置做什么
add_actor_rollout_worker(config) line 126注册 ActorRolloutRefWorker(hybrid engine)或拆成单独 Actor / Rollout
add_critic_worker(config) line 148有 critic(PPO)时注册
init_resource_pool_mgr(config) line 158ResourcePoolManager,里面默认有 global_pool,按需加 reward_poolteacher_pool
add_reward_model_resource_pool(config) line 193神经网络型 reward model
add_teacher_model_resource_pool(config) line 205在线蒸馏的 teacher
run(config) line 223构造 RayPPOTrainer 并调 fit()

这套设计意味着:controller 不在 head 节点的 Python 进程里,而是在 TaskRunner 这个 Ray Actor 里运行。Ray dashboard 上能看到一个长寿命的 actor 名字带 "TaskRunner",它就是 controller。

3.11 同步 vs 异步:main_ppo_sync.py

verl/trainer/main_ppo_sync.py 是一个并行存在的"同步 PPO"入口(line 14–22 注释里写得很清楚)。它和 main_ppo.py 的差异:

维度main_ppo.pymain_ppo_sync.py
数据传输 同进程 ray put/get TransferQueue 零拷贝零 padding
Replay buffer 从 TransferQueue 采
每 prompt 出几条 trajectory固定 rollout.n可变(agent loop 多输出)
GRPO advantage 每条独立算 compute_advantage_for_multi_trajectories 把同组 final 输出广播到其他 output
状态 本书默认讲它 新路径,agent loop / 多轮工具调用主要走它

初次跑训练用 main_ppo 即可;做 agent loop / 长 R1 风格再切 main_ppo_sync。第 12 章会用到。