Chapter 04

核心概念:Actor / Critic / Reference / Reward 四个角色

📌 commit 6c6056daa522 RLHF 心智模型 + PPO 数学

RLHF 训练里同时存在 4 个模型副本,每个干不同的事。它们的角色分工是整个 OpenRLHF 架构的第一性原理—— 之后所有的 Ray 调度、显存优化、算法变体,都是在"如何更高效地让这 4 个角色协作"上做文章。 本章把这 4 个角色一一讲透。

4.1RLHF 训练循环里发生了什么

flowchart LR Prompt[Prompt batch] Actor["Actor (训练中的 policy)"] Rollout["vLLM Rollout
(= Actor 权重的推理副本)"] Ref["Reference (冻结的 SFT 模型)"] Reward["Reward (RM 或规则函数)"] Critic["Critic (value head)"] Prompt --> Rollout Rollout --> Resp[Response 序列] Resp --> Ref Resp --> Reward Resp --> Critic Ref --> RefLogp[ref_logp] Reward --> R[r] Critic --> V[V] RefLogp --> Adv["Advantage 计算
+ KL 惩罚"] R --> Adv V --> Adv Adv --> Update["Actor + Critic
梯度更新"] Update -.同步.- Rollout

4.2四个角色一字排开

角色 权重 是否更新 起源 作用
Actor 每步 SFT 模型 正在被训练的 policy,目标是最大化期望 reward。
Reference ❌ 冻结 SFT 模型副本 提供 KL 锚定基准,避免 actor 漂离原模型太远。
Reward ~1× ❌ 冻结 RM 训练后;或
函数 / API
给 (prompt, response) 打分。
Critic ~1× 每步 从 RM 初始化 对每个 token 估计 value $V(s_t)$,用于算 advantage。
显存账 PPO 显存 ≈ 4 × LoRA SFT。这就是为什么 PPO 不像 SFT 那样轻便, 也是为什么 OpenRLHF 要花大力气做 colocate / offload。

4.3逐一深入

Actor —— 真正在训的模型

Actor 就是你最后要拿走部署的那个模型。它从 SFT 模型初始化,每 step:

  1. 采样阶段,把当前权重同步到 vLLM rollout engine;
  2. vLLM 生成 response;
  3. 训练阶段,actor 在自己的 DeepSpeed engine 里对 response 做 forward + backward;
  4. 用 PPO loss 更新参数。

PPO loss(policy 部分):

$$ \mathcal{L}_\text{policy} = -\mathbb{E}\!\left[\min\!\big(\rho_t \hat A_t,\;\mathrm{clip}(\rho_t,\,1-\epsilon,\,1+\epsilon)\hat A_t\big)\right] $$

其中 $\rho_t = \pi_\theta(a_t|s_t) / \pi_{\theta_\text{old}}(a_t|s_t)$ 是重要性比,$\hat A_t$ 是 advantage。

Reference —— 冻结的"锚"

Reference 是 SFT 后的模型副本,从训练开始到结束完全不动。它的唯一作用是提供 ref_logp,用来算 KL:

$$ \mathrm{KL}(\pi_\theta \| \pi_\text{ref}) \approx \mathbb{E}\!\left[\log \pi_\theta(y|x) - \log \pi_\text{ref}(y|x)\right] $$

KL 的两种放法(OpenRLHF 都支持):

放法含义OpenRLHF 配置
KL → reward $r_\text{total} = r - \beta_\text{KL} \cdot \mathrm{KL}$ --algo.kl.in_reward
KL → loss $\mathcal{L}_\text{total} = \mathcal{L}_\text{policy} + \beta_\text{KL} \cdot \mathrm{KL}$ --algo.kl.in_loss

OpenRLHF 默认 KL → reward,更接近 OpenAI 经典 PPO。

Reward —— 给打分

Reward 有三种来源:

  1. RM 模型(默认):用偏好数据训练好的 reward model,输出标量。
  2. 规则函数(推理 RL 流行):写一个 def reward_func(queries, prompts, labels) -> dict,返回 0/1 或连续分。
  3. Remote URL:把 reward 计算放到 HTTP server,训练时调用。

三种通过 train_ppo_ray 不同 flag 切换:

# 1. RM 模型
--reward.model_name_or_path OpenRLHF/Llama-3-8b-rm-700k

# 2. 本地规则函数
--reward.remote_url /openrlhf/examples/python/reward_func.py

# 3. Remote HTTP service
--reward.remote_url http://reward-server:8000/score

第 10 章会详讲怎么写自定义 reward。

Critic —— value 估计

Critic 的存在是 PPO 区别于 REINFORCE 的关键:它给每个 token 估一个 baseline $V(s_t)$,用来算 advantage:

$$ \hat A_t^\text{GAE} = \sum_{l=0}^{\infty} (\gamma\lambda)^l \delta_{t+l}, \quad \delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) $$

这是 GAE(Generalized Advantage Estimation)。它能降低梯度方差,是 PPO 收敛稳定的关键。

Critic 通常用 reward model 的权重初始化(把分类头改成 value head),训练时也走 DeepSpeed engine。 它的 loss 是 value regression:

$$ \mathcal{L}_\text{critic} = \mathbb{E}\!\left[(V_\theta(s_t) - R_t)^2\right] $$

为什么 GRPO 不要 critic

GRPO 的招是同 prompt 采样 G 个 response,用组内归一替代 critic

$$\hat A_i = \frac{r_i - \mathrm{mean}(\{r_j\})}{\mathrm{std}(\{r_j\})}$$

所以 GRPO 训练时不需要 critic,4 个角色变 3 个,显存省 25%+。这是 DeepSeek-R1 流行的关键工程理由。

4.4第 5 个"隐形"角色:Rollout

严格说还有第 5 个角色——vLLM Rollout,它不是独立的模型,是 actor 权重的"推理副本":

维度ActorRollout
权重内容 实时训练参数 从 actor 同步过来的快照
计算图 有反向 只有前向 + KV cache
底层引擎 DeepSpeed vLLM
更新时机 每个 PPO step 每个 rollout 阶段从 actor 同步
显存布局 fp32 master + bf16 bf16 weights + PagedAttention KV cache

这种"训练 actor + 推理 rollout 分离"的设计是 OpenRLHF(也是 verl)相对于 DeepSpeed-Chat 的核心优势。 为什么不直接用 actor 做 generate()?第 6 章详讲。

4.5四角色的协作时序

sequenceDiagram participant A as Actor participant R as Rollout (vLLM) participant Rf as Reference participant Rw as Reward participant C as Critic Note over A,R: 阶段 1 — 权重同步 A->>R: nccl broadcast actor weights Note over R: 阶段 2 — 采样 R->>R: prompts → responses Note over A,C: 阶段 3 — 并行评估 R->>Rf: responses → ref_logp R->>Rw: responses → reward R->>C: responses → value Note over A,C: 阶段 4 — 梯度更新 Rf-->>A: ref_logp Rw-->>A: r C-->>A: V A->>A: 算 advantage + policy_loss + backward C->>C: value_loss + backward

4.6四个角色在 OpenRLHF 代码里的位置

角色源码位置核心类
Actor openrlhf/models/actor.py Actor
Reference Actor 同一个类,requires_grad=False
Reward openrlhf/models/model.py get_llm_for_sequence_regression()
Critic 同上(也是 sequence regression head)
Rollout openrlhf/trainer/ray/vllm_engine.py LLMRayActor

每个角色被一个 RayActorGroup 包起来(见第 5 章),controller 通过 RPC 调它们的方法。

4.7从 PPO 切到 GRPO:丢掉哪个角色

算法需要 Actor需要 Ref需要 Reward需要 Critic
PPO
GRPO
RLOO
REINFORCE++
REINFORCE++ baseline
DPO(offline)

所以"切到 GRPO 后还省一倍显存",本质上是丢掉 critic 这个角色。

4.8Actor 类全字段(源码锚)

四角色里 Actor 是唯一一个"独立类"(其他三个本质都是包装 HF 模型)。openrlhf/models/actor.py:54Actor.__init__ 字段:

字段含义
pretrain_or_model HF 仓库 ID / 本地路径 / 已加载的 nn.Module
attn_implementation 默认 "flash_attention_2"
param_dtype bf16 / fp16;多数情况 bf16
load_in_4bit QLoRA
lora_rank / lora_alpha 启用 PEFT LoRA;rank=0 即不开
ds_config DeepSpeed 配置(由 Strategy 注入)
packing_samples 多样本拼成长序列,节省 padding
temperature 采样温度(影响 log_prob 计算)
use_liger_kernel 启用 Liger fused kernel 加速
freeze_visual_encoder VLM 时是否冻结视觉塔
experts_implementation MoE 模型的 expert 实现(grouped_gemm / native 等)

这个类同时被 actor / reference / ppo_actor 三处复用,区别只在 requires_gradds_config

4.9KL 估计的三种方法

KL 在 PPO 里频繁出现。openrlhf/models/utils.py:48compute_approx_kl(log_probs, log_probs_base, kl_estimator) 提供三种 estimator:

kl_estimator公式取舍
"k1" $\log p - \log q$ 原始定义,有正负,方差大
"k2" $\tfrac{1}{2}(\log p - \log q)^2$ 对称,永远 ≥ 0
"k3" $\exp(\log q - \log p) - (\log q - \log p) - 1$ 低方差近似(Schulman),实战默认

YAML 字段 --algo.kl.estimator k3k3 是 OpenAI Schulman 论文的写法,方差远小于 k1,PPO 训得更稳。

4.10reward 的 KL 注入:只在 EOS 一次

compute_reward(r, kl_coef, kl, action_mask, reward_clip_range)(utils.py:82)有个容易踩的坑:

这就是为什么很多人疑惑"KL 是 token-level 还是 sequence-level"——OpenRLHF 走 token-level 惩罚 + EOS 一次性 reward 注入,跟 OpenAI 经典 PPO 一致。

4.11这章你需要带走的