Chapter 16 · Section 16.4 · 65 min

The RLHF loop with PPO

You have an SFT model and a reward model. The simplest thing to try is to sample responses, score them, and push up the log-probability of the ones that scored well, REINFORCE from §15.3. Run that for a while and you get a model that scores higher and higher while its text gets stranger. It repeats phrases the reward model happens to like, grows every answer to the maximum length, or drifts into a style no person would write, because the reward model was trained on comparisons of ordinary SFT outputs and has no idea what to say about strange ones. Ziegler et al. (2019) saw a sharper version of this when a sign bug made their model optimize for the text labelers hated most, and it did so fluently.

The fix that every RLHF system since 2019 uses is a leash. Reward the policy for what the reward model likes and charge it for every bit of probability it moves away from the SFT model. This section builds that objective, shows the distribution it aims for, turns it into a per-token reward that PPO can use, runs generalized advantage estimation over tokens, writes the clipped and masked loss, assembles the full training step, and counts what the four models in memory cost.

Four models

PPO-based RLHF keeps four networks, and it helps to name them before any equation.

modelsymbolinitialized fromtrained?what it does in one step
policy (actor)πθ\pi_\thetathe SFT modelyesgenerates responses, then is updated by the PPO loss
referenceπref\pi_{\text{ref}}the SFT model, a frozen copynoscores the policy's tokens, for the KL penalty
reward modelrϕr_\phithe reward model of §16.3noscores each finished response once
value model (critic)VψV_\psiusually the reward modelyespredicts the future reward at every token, for the advantage

The policy and reference start identical, so at the first step the KL penalty is zero. The value model is a whole Transformer with a scalar head, the critic of §15.4, and it is usually initialized from the reward model (InstructGPT, Stiennon et al. 2020, and Huang et al. 2024 all do this). The reward model already maps a conversation to one number on the reward's own scale, which is a far better start than a fresh head on the policy, even though it was only ever trained on finished responses. We write its parameters ψ\psi because ϕ\phi belongs to the reward model.

The KL-regularized objective

Treat the prompt xx as the state and the whole response yy as one action, the view of §15.7. The objective is

J(θ)=Ex∼D[ Ey∼πθ(⋅∣x)[rϕ(x,y)]  −  β DKL(πθ(⋅∣x) ∥ πref(⋅∣x))],J(\theta) = \mathbb{E}_{x\sim\mathcal{D}}\Big[\,\mathbb{E}_{y\sim\pi_\theta(\cdot\mid x)}\big[r_\phi(x, y)\big] \;-\; \beta\,D_{\mathrm{KL}}\big(\pi_\theta(\cdot\mid x)\,\Vert\,\pi_{\text{ref}}(\cdot\mid x)\big)\Big],

with β>0\beta > 0 the KL coefficient. In plain words, get high reward, and pay β\beta for every nat by which your response distribution differs from the reference's.

Section 15.7 gave the penalty's three jobs. It keeps the policy where the reward model was trained and can be trusted (§16.3 watched a reward model go from 96% right to 78% wrong as the policy moved), it keeps the model fluent and knowledgeable, and it stops the policy from collapsing onto one response. One more property is worth spelling out for RLHF. The penalty is the reverse KL, an average over the policy's own samples, which is mode-seeking (§0.5). It charges the policy for putting mass where the reference has little and lets it drop some of the reference's modes freely. That is what we want, since the point is to stop writing some of what the SFT model writes, without inventing text it would never write.

Recall from §15.7, which stated it and proved it in two lines, that the objective has a closed-form maximizer, the reference tilted toward high reward,

π⋆(y∣x)=1Z(x) πref(y∣x) exp⁡ ⁣(rϕ(x,y)/β),Z(x)=∑yπref(y∣x)exp⁡ ⁣(rϕ(x,y)/β),\pi^\star(y \mid x) = \frac{1}{Z(x)}\,\pi_{\text{ref}}(y \mid x)\,\exp\!\big(r_\phi(x, y)/\beta\big), \qquad Z(x) = \sum_y \pi_{\text{ref}}(y\mid x)\exp\!\big(r_\phi(x,y)/\beta\big),

which §17.1 derives in full, in two ways, and turns into DPO. We only need what it says. In plain words, the ideal RLHF policy is the reference model with every response's probability multiplied by er/βe^{r/\beta} and renormalized. Three consequences matter for practice.

  • RLHF can only reweight what the SFT model already writes. A response the reference gives zero probability stays at zero for every β\beta. A response it gives tiny probability needs a large KL to become likely. That is one reason the quality and diversity of the SFT model bound what RLHF can reach (§14.1).
  • Making one response certain costs −log⁡πref(y∣x)-\log\pi_{\text{ref}}(y\mid x) nats. If the reward model wildly overrates a response the reference writes 3% of the time, the policy can put all its mass there for a KL of log⁡(1/0.03)≈3.5\log(1/0.03) \approx 3.5 nats. Overoptimization in §16.5 is measured in exactly these units.
  • ZZ is intractable. It sums over every possible response, so nobody can compute π⋆\pi^\star for a real model. PPO approaches it by sampling, and DPO sidesteps it algebraically.

The per-token form, and what RLHF adds to it

PPO works with per-step rewards. A response is y=(y1,…,yT)y = (y_1, \dots, y_T) and the state before token tt is st=(x,y<t)s_t = (x, y_{<t}). Section 15.7 showed that the sequence log-ratio splits into per-token log-ratios, log⁡πθ(y∣x)πref(y∣x)=∑tkt\log\frac{\pi_\theta(y\mid x)}{\pi_{\text{ref}}(y\mid x)} = \sum_t k_t with kt=log⁡πθ(yt∣st)−log⁡πref(yt∣st)k_t = \log\pi_\theta(y_t\mid s_t) - \log\pi_{\text{ref}}(y_t\mid s_t), which turns the objective into a token-level MDP with the shaped reward

r~t=−β kt+1[t=T]  rϕ(x,y),t=1,…,T,\tilde r_t = -\beta\,k_t + \mathbb{1}[t = T]\;r_\phi(x, y), \qquad t = 1, \dots, T,

and that putting this penalty in the reward, rather than differentiating it in the loss, gives the correct gradient. In plain words, every token pays a KL toll for how much more likely the policy made it than the reference did, and the reward model's single score arrives as a bonus on the final token. The tolls and the bonus sum to rϕ(x,y)−β∑tktr_\phi(x,y) - \beta\sum_t k_t, whose expectation is exactly JJ for this prompt. InstructGPT describes it as a per-token KL penalty from the SFT model at each token, and it is the form in Ziegler et al. (2019), Stiennon et al. (2020), and Huang et al. (2024).

What §15.7 did not need, and every RLHF implementation does, is the handling of real generations.

  • Which policy's log-probabilities? The ktk_t are computed once, at rollout time, with the policy that generated the samples, πold\pi_{\text{old}}, and treated as constants for all the optimization epochs that follow.
  • A single ktk_t can be negative. If the policy sampled a token the reference liked more, that token earns a small positive reward. Only the expectation of the sum is a KL. Some implementations put the always-nonnegative k3k_3 estimate in the reward instead, or the exact per-token KL summed over the vocabulary, because both have lower variance as estimates of the KL's value. Neither is a free swap. Used as a fixed reward, only k1=ktk_1 = k_t gives the exact gradient of JJ (§15.7 proves this). The exact per-token KL, for instance, depends on the state alone, so as a detached reward it gives no gradient at all through the token it is charged on, and its direct derivative has to be added to the loss separately.
  • Where does the response end? Batched generation usually runs every sequence for the same number of steps, so tokens after a sequence's first end-of-sequence token are garbage that must be masked out. The response mask is built from the first end-of-sequence token, which is included, because choosing to stop was an action.
  • What if it never ends? A response cut off at the length limit was never finished, and the reward model's opinion of a truncated answer means little. Huang et al. (2024) replace its score with a fixed penalty, −1-1 in their runs, which stops the policy from rambling to the limit.
Interview

From generated tokens to the shaped reward vector

Batched generation produced tokens, an integer array of shape (B, T). Every row was generated for exactly T steps, so anything after a row's first eos_id is garbage. You also have logp_old and logp_ref, shape (B, T), the log-probabilities of those tokens under the rollout policy and the reference, and scores, shape (B,), the reward model's score of each response. Implement rlhf_rewards(tokens, logp_old, logp_ref, scores, beta, eos_id, no_eos_score=-1.0) returning (rewards, mask, used_scores).

  • mask is boolean (B, T), True from the start of each row up to and including its first eos_id, or on the whole row if it has none.
  • used_scores is scores for rows that contain eos_id and no_eos_score for rows that do not.
  • rewards is (B, T), −β(log⁡πold−log⁡πref)-\beta(\log\pi_{\text{old}} - \log\pi_{\text{ref}}) on masked tokens, used_scores added on each row's last masked token, and exactly 0 elsewhere, even if the garbage log-probabilities are nan.
Hidden tests, hints, and AI help, from $1.99 a month.

Advantages over tokens

With per-token rewards in hand, the rest is the actor-critic machinery of Chapter 15, applied to a sequence of tokens. The value model predicts Vψ(st)V_\psi(s_t), the expected sum of future shaped rewards from state sts_t (in practice the value head is read at the position of each response token, so it has seen xx and y<ty_{<t} and predicts the rest). The TD error of token tt and the generalized advantage estimate of §15.5 are

δt=r~t+γVψ(st+1)−Vψ(st),A^t=∑l=0T−t(γλ)l δt+l=δt+γλ A^t+1,\delta_t = \tilde r_t + \gamma V_\psi(s_{t+1}) - V_\psi(s_t), \qquad \hat A_t = \sum_{l=0}^{T-t}(\gamma\lambda)^l\,\delta_{t+l} = \delta_t + \gamma\lambda\,\hat A_{t+1},

with Vψ(sT+1)=0V_\psi(s_{T+1}) = 0 after the last token, because the episode is over, and A^T+1=0\hat A_{T+1} = 0. The value targets are the returns R^t=A^t+Vψ(st)\hat R_t = \hat A_t + V_\psi(s_t). In plain words, each token's advantage is an exponentially weighted sum of how much better the following steps went than the critic expected.

RLHF uses γ=1\gamma = 1, no discounting, because the episode is short and the reward that matters arrives at the end. A discount below 1 would shrink the final score's influence on early tokens for no good reason. With γ=1\gamma = 1 the knob that matters is λ\lambda. At λ=1\lambda = 1 the advantage of token tt is the full return from tt minus the baseline Vψ(st)V_\psi(s_t), unbiased but noisy. At λ=0\lambda = 0 it is the one-step TD error, which leans on the critic. Stiennon et al. (2020) and Huang et al. (2024) used λ=0.95\lambda = 0.95.

A worked example by hand

Take a 4-token response with β=0.1\beta = 0.1 and a reward model score of 1.2. The rollout and reference log-probabilities of its tokens are (−1.2,−0.4,−2.0,−0.1)(-1.2, -0.4, -2.0, -0.1) and (−1.5,−0.5,−1.0,−0.1)(-1.5, -0.5, -1.0, -0.1), so the log-ratios are k=(0.3,0.1,−1.0,0)k = (0.3, 0.1, -1.0, 0) and the shaped rewards are r~=(−0.03,−0.01,0.1,1.2)\tilde r = (-0.03, -0.01, 0.1, 1.2). The third token was less likely under the policy than under the reference, so it earns a small positive reward. Let the critic's values be V=(0.5,0.6,0.9,1.1)V = (0.5, 0.6, 0.9, 1.1). Then

  • δ4=1.2+0−1.1=0.10\delta_4 = 1.2 + 0 - 1.1 = 0.10, and A^4=0.10\hat A_4 = 0.10,
  • δ3=0.1+1.1−0.9=0.30\delta_3 = 0.1 + 1.1 - 0.9 = 0.30, and A^3=0.30+0.95×0.10=0.395\hat A_3 = 0.30 + 0.95 \times 0.10 = 0.395,
  • δ2=−0.01+0.9−0.6=0.29\delta_2 = -0.01 + 0.9 - 0.6 = 0.29, and A^2=0.29+0.95×0.395=0.665\hat A_2 = 0.29 + 0.95\times 0.395 = 0.665,
  • δ1=−0.03+0.6−0.5=0.07\delta_1 = -0.03 + 0.6 - 0.5 = 0.07, and A^1=0.07+0.95×0.665=0.702\hat A_1 = 0.07 + 0.95 \times 0.665 = 0.702.

The returns are R^=A^+V=(1.202,1.265,1.295,1.2)\hat R = \hat A + V = (1.202, 1.265, 1.295, 1.2). Every token's advantage is positive, because the response did better than the critic predicted from the start. The cell checks the arithmetic.

Python · cell 1
Interview

GAE over a padded batch of token sequences

Implement gae(rewards, values, mask, gamma=1.0, lam=0.95) for a right-padded batch. rewards and values have shape (B, T), the shaped per-token rewards and the critic's values Vψ(st)V_\psi(s_t) at response tokens. mask is a boolean (B, T) array of contiguous True prefixes. Treat the value after each row's last real token as 0 (the episode ends there), and never let padding values leak in. Return (advantages, returns), both (B, T), zero on padding, with returns =A^+V= \hat A + V on real tokens. Loop over time only, not over rows.

Hidden tests, hints, and AI help, from $1.99 a month.

The clipped loss, masked

The policy update is PPO's clipped surrogate (§15.6), applied at every response token. Rollouts are generated once with πold\pi_{\text{old}}, then reused for a few optimization epochs, so the new policy's tokens are reweighted by the probability ratio

ρt(θ)=πθ(yt∣st)πold(yt∣st)=exp⁡ ⁣(log⁡πθ(yt∣st)−log⁡πold(yt∣st)),\rho_t(\theta) = \frac{\pi_\theta(y_t\mid s_t)}{\pi_{\text{old}}(y_t\mid s_t)} = \exp\!\big(\log\pi_\theta(y_t\mid s_t) - \log\pi_{\text{old}}(y_t\mid s_t)\big),

and the policy loss over a batch with response mask mb,tm_{b,t} (1 on real response tokens of sequence bb, 0 on prompt and padding) is

Lclip(θ)=−1∑b,tmb,t∑b,tmb,t min⁡(ρb,tA^b,t,  clip⁡(ρb,t,1−ϵ,1+ϵ)A^b,t),\mathcal{L}^{\text{clip}}(\theta) = -\frac{1}{\sum_{b,t} m_{b,t}}\sum_{b,t} m_{b,t}\,\min\Big(\rho_{b,t}\hat A_{b,t},\;\operatorname{clip}\big(\rho_{b,t}, 1-\epsilon, 1+\epsilon\big)\hat A_{b,t}\Big),

with clip range ϵ\epsilon, typically 0.2. In plain words, raise the probability of tokens with positive advantage and lower those with negative advantage, but once a token's probability has moved more than ϵ\epsilon relative to the rollout policy in the helpful direction, stop pushing. Here ϵ\epsilon is the clip range, not the label-flip rate of §16.2.

The value model is trained by regression to the returns, often with its own clipping so it cannot move far from its rollout-time prediction VoldV_{\text{old}} either,

LV(ψ)=12∑m∑b,tmb,tmax⁡((Vψ−R^)2,  (clip⁡(Vψ,Vold−ϵv,Vold+ϵv)−R^)2),\mathcal{L}^{V}(\psi) = \frac{1}{2\sum m}\sum_{b,t} m_{b,t}\max\Big(\big(V_\psi - \hat R\big)^2,\;\big(\operatorname{clip}(V_\psi, V_{\text{old}} - \epsilon_v, V_{\text{old}} + \epsilon_v) - \hat R\big)^2\Big),

and the two are combined as Lclip+cvLV\mathcal{L}^{\text{clip}} + c_v\mathcal{L}^V when the policy and value share an optimizer (Huang et al. used ϵv=0.2\epsilon_v = 0.2 and cv=0.1c_v = 0.1). An entropy bonus, standard in PPO for games, is usually left out of RLHF, because the KL penalty to the reference already keeps the policy from collapsing.

Two practical points about the mask. First, the prompt tokens are never actions, so they are masked out of every loss, exactly as in the SFT loss of §14.3. Second, how you average matters. The formula above averages over all response tokens in the batch, so long responses carry more weight than short ones. Averaging within each sequence first and then over sequences weights every response equally, which changes which responses the update favors and interacts with length. That choice is at the heart of the length-bias analysis of Dr. GRPO in §18.4.

Advantages are usually whitened before the loss, shifted to mean 0 and scaled to standard deviation 1 over the real tokens of the batch, which makes the step size independent of the reward scale (§16.6 has the details and pitfalls).

Interview

The PPO-RLHF policy loss with clipping and masking

Implement ppo_policy_loss(logp_new, logp_old, adv, mask, eps=0.2), all arrays (B, T) except eps. Return (loss, clipfrac). loss is Lclip\mathcal{L}^{\text{clip}} as defined above, the negative of the clipped surrogate averaged over all real tokens in the batch. clipfrac is the fraction of real tokens at which the clipped term is the one selected by the min and differs from the unclipped term (where clipping changed the objective). Padding must not affect either value, even if it holds garbage.

Hidden tests, hints, and AI help, from $1.99 a month.

Choosing beta, and the adaptive controller

The KL coefficient sets the exchange rate between reward and drift, and the right value depends on the reward model's scale, which is arbitrary (§16.3). Published values span an order of magnitude. Stiennon et al. (2020) used β=0.05\beta = 0.05, InstructGPT β=0.02\beta = 0.02, Llama 2 used 0.01 for its 7B and 13B models and 0.005 for 34B and 70B, and Anthropic used 0.001 and noted it might be nearly unnecessary (Bai et al. 2022). These numbers are not comparable across papers, because each reward model has its own scale.

Ziegler et al. (2019) avoided choosing by targeting a KL instead. After each batch, with measured KL κ^\hat\kappa and target κtarg\kappa_{\text{targ}}, the adaptive KL controller sets

et=clip⁡ ⁣(κ^−κtargκtarg, −0.2, 0.2),βt+1=βt (1+Kβ et),e_t = \operatorname{clip}\!\Big(\frac{\hat\kappa - \kappa_{\text{targ}}}{\kappa_{\text{targ}}},\,-0.2,\,0.2\Big), \qquad \beta_{t+1} = \beta_t\,(1 + K_\beta\,e_t),

with Kβ=0.1K_\beta = 0.1. In plain words, if the policy has drifted more than the target, raise the price of drift by up to 2% per step, and lower it if it has drifted less. Their targets were 6 nats for descriptiveness and 10 for sentiment. The clip keeps one noisy batch from swinging β\beta, and the multiplicative update keeps β\beta positive.

Core

The adaptive KL controller

Implement update_beta(beta, kl, target, k_beta=0.1) for the controller above, and run_controller(beta0, kls, target) that applies it once per entry of the sequence kls and returns the list of β\beta values after each update.

Hidden tests, hints, and AI help, from $1.99 a month.

The pretraining mix

The KL penalty keeps the policy near the SFT model on the prompts it is trained on. It says nothing about everything else the model used to be good at. InstructGPT found that PPO made its models worse on several public NLP benchmarks, SQuADv2 and DROP among them, an alignment tax paid for following instructions better. Their fix, called PPO-ptx, keeps a little pretraining running inside RL. With zz a sequence of text drawn from the pretraining distribution Dpretrain\mathcal{D}_{\text{pretrain}} and πθ(z)\pi_\theta(z) the probability the policy assigns to the whole sequence, InstructGPT maximizes

Jptx(θ)=Ex∼D,  y∼πθ(⋅∣x)[rϕ(x,y)−βlog⁡πθ(y∣x)πref(y∣x)]  +  γptx Ez∼Dpretrain[log⁡πθ(z)],J_{\text{ptx}}(\theta) = \mathbb{E}_{x\sim\mathcal{D},\; y\sim\pi_\theta(\cdot\mid x)}\Big[r_\phi(x, y) - \beta\log\frac{\pi_\theta(y\mid x)}{\pi_{\text{ref}}(y\mid x)}\Big] \;+\; \gamma_{\text{ptx}}\,\mathbb{E}_{z\sim\mathcal{D}_{\text{pretrain}}}\big[\log\pi_\theta(z)\big],

with β=0.02\beta = 0.02 and γptx=27.8\gamma_{\text{ptx}} = 27.8 (Ouyang et al. 2022). Setting γptx=0\gamma_{\text{ptx}} = 0 gives plain PPO. In plain words, get high reward on your own samples, pay for drifting from SFT on those samples, and keep assigning high likelihood to ordinary text.

The two regularizers pull in different ways, and the difference is the point. The KL term is the reverse KL to the SFT model, measured on the policy's own samples, so it never looks at text the policy has stopped writing. The new term, up to a constant, is the forward KL from the pretraining data, since Ez∼Dpretrain[log⁡πθ(z)]=−H(Dpretrain)−DKL(Dpretrain∥πθ)\mathbb{E}_{z\sim\mathcal{D}_{\text{pretrain}}}[\log\pi_\theta(z)] = -H(\mathcal{D}_{\text{pretrain}}) - D_{\mathrm{KL}}\big(\mathcal{D}_{\text{pretrain}}\Vert\pi_\theta\big), where HH is the entropy of the data and does not depend on θ\theta. Forward KL is mode-covering (§0.5). It charges the policy for every kind of text in the corpus it has become unable to produce, which is exactly the capability loss the reverse KL cannot see. InstructGPT tested the alternative directly. With no pretraining mix, raising β\beta a hundredfold, to 2.0, still did not fix the regressions, and it cost a large drop in reward.

Mechanically it is cheap. For each PPO minibatch, InstructGPT computed the PPO gradient and a language-modeling gradient on a pretraining minibatch in consecutive steps, multiplied the second by γptx\gamma_{\text{ptx}}, and accumulated both before the optimizer step, drawing 8 times more pretraining examples than RL episodes over the run. A value of 27.8 worked across model sizes from 1.3B to 175B, the labelers' ratings were insensitive to its exact value, and larger values traded away validation reward. It is not free in a subtler sense. The objective is no longer the one whose maximizer is the tilt πreferϕ/β/Z\pi_{\text{ref}}e^{r_\phi/\beta}/Z, so the closed form above, and the DPO derivation built on it, describe PPO without the mix. And it did not remove every regression, which is why later stacks also measure the tax directly (§16.6).

One full training step

Here is the whole loop, in the order the computation happens. Each iteration takes a batch of prompts, InstructGPT and Llama 2 used 512 per batch.

  1. Rollout. Generate one response per prompt with the current policy, πold=πθ\pi_{\text{old}} = \pi_\theta, at temperature 1 (some recipes use lower, Huang et al. used 0.7). Truncate at the first end-of-sequence token.
  2. Score. Run the frozen reward model once per response, read at the last token. Apply the missing-end-of-sequence penalty if you use one. Optionally normalize the scores (§16.6).
  3. Log-probabilities. One forward pass of the policy and one of the reference over prompt plus response give log⁡πold(yt∣st)\log\pi_{\text{old}}(y_t\mid s_t) and log⁡πref(yt∣st)\log\pi_{\text{ref}}(y_t\mid s_t) at every response token.
  4. Values. One forward pass of the value model gives Vold(st)V_{\text{old}}(s_t) at every response token.
  5. Rewards and advantages. Build the shaped rewards r~t\tilde r_t, run GAE to get A^t\hat A_t and R^t\hat R_t, and whiten the advantages over the batch's real tokens.
  6. Optimize. For a few epochs (InstructGPT 1, Stiennon et al. and Huang et al. 4), split the batch into minibatches, recompute log⁡πθ\log\pi_\theta and VψV_\psi with gradients, and step on Lclip+cvLV\mathcal{L}^{\text{clip}} + c_v\mathcal{L}^V, plus γptx\gamma_{\text{ptx}} times a language-modeling loss on pretraining text for PPO-ptx.
  7. Bookkeeping. Log the mean score, the mean KL ∑tkt\sum_t k_t, response length, the clip fraction, and an approximate KL between πθ\pi_\theta and πold\pi_{\text{old}}. Update β\beta if it is adaptive.

Everything but step 6 runs without gradients. Here is the loop in NumPy on a toy that is small enough to see completely. The "language" has 6 tokens, token 0 is end-of-sequence, responses have at most 8 tokens, and the policy and reference are tables of logits, one row per position, so a token's log-probability depends only on where it is. A stand-in reward model likes token 3, dislikes token 5, and charges −1-1 for a response that never ends. The value model is a table of one value per position, fit by averaging returns.

Python · cell 2
Python · cell 3

The gradient line is the only thing that needs a word. For a softmax over logits z\mathbf{z}, ∂log⁡π(a)/∂z=ea−π\partial\log\pi(a)/\partial\mathbf{z} = \mathbf{e}_a - \boldsymbol{\pi}, and the surrogate's derivative with respect to log⁡πθ(yt∣st)\log\pi_\theta(y_t\mid s_t) is ρtA^t\rho_t\hat A_t where the unclipped term is selected and 0 where the clip is active. Multiplying the two and summing over tokens gives the update. With β=0\beta = 0, the score climbs furthest and the KL grows to almost 5 nats. A larger β\beta buys a smaller KL at the price of a lower score, the trade-off the tilt formula described, now reached by sampling instead of by enumerating every response.

How does one reward at the end become a learning signal for every token?
A response to the prompt 'Is it safe to eat raw cookie dough?' Each token pays a KL toll of beta times its log-ratio to the reference, the reward model's score is added on the last token, and GAE spreads the signal backward using the critic's values.
Loading interactive figure…

In PyTorch

The same loop with neural networks. The policy, the reference, and the value model are tiny GRU language models (the value model has a scalar head), the reward model is the same stand-in function, and generation is ordinary autoregressive sampling. Every step of the list above appears in order.

🔥 In PyTorchfor a machine with PyTorch

Within a dozen iterations the policy fills all eight slots with 3s and accepts the penalty for never ending, a score of 3.0 (ending on the eighth token would have scored 3.5), and the KL has jumped to about 11 nats. With a large learning rate and β=0.05\beta = 0.05 the leash is loose, and even this tiny run shows the pattern of §16.5, a policy that runs to the highest-scoring corner the reward allows and travels far from the reference to get there.

In a real system every one of these lines is a distributed operation over billions of parameters. Generation runs in a fast inference engine, the four models are sharded across GPUs, and the policy's fresh weights have to be copied into the inference engine after each update. Libraries such as TRL, OpenRLHF, and verl exist mostly to do that plumbing.

What four models cost

Memory. A trained model in mixed precision with Adam needs about 16 bytes per parameter (2 for bf16 weights, 2 for gradients, and 12 for fp32 master weights and the two Adam moments, §10.1). A frozen model needs only its bf16 weights, 2 bytes per parameter. With all four models of size NN, that is 16N+16N+2N+2N=36N16N + 16N + 2N + 2N = 36N bytes before activations and the KV cache for generation.

setuppolicyvaluereferencerewardtotal weights and optimizer state
all four at 7B112 GB112 GB14 GB14 GB252 GB
all four at 70B1.12 TB1.12 TB140 GB140 GB2.52 TB
InstructGPT 175B policy, 6B reward and value2.8 TB96 GB350 GB12 GB3.26 TB

That is more than twice the memory of SFT on the same model, which needs 16 bytes per parameter. Common savings are a value model smaller than the policy (as in InstructGPT), offloading the frozen models to CPU between uses (Huang et al. did this for their 6.9B run), sharing a trunk between policy and value (cheaper, though the two losses can interfere), LoRA adapters so the reference is the base weights with the adapter switched off, and dropping the value model altogether, which is what GRPO and RLOO do (§18.4).

Compute. Count FLOPs per response token with the rule of §6.7, 2N2N for a forward pass and 6N6N for forward plus backward. One iteration with all four models at size NN and EE optimization epochs costs about 2N2N to generate, 2N2N each for the policy, reference, reward, and value forward passes of steps 3 and 4, and 6N6N per epoch each to train the policy and the value model, roughly (10+12E)N(10 + 12E)N per token, or 22N22N at E=1E = 1 and 58N58N at E=4E = 4. (Implementations that keep the log-probabilities computed during generation skip the policy's extra pass and pay (8+12E)N(8 + 12E)N, but inference engines often produce slightly different numbers than the training code, and a ratio built on mismatched log-probabilities is not 1 at the first step.) Generation is only 2N2N of that, yet it dominates the wall clock, because decoding produces one token per forward pass for the whole batch and is limited by memory bandwidth, not arithmetic (§12.1). The OpenRLHF authors report that the inference phase often takes over 90% of RLHF runtime (Hu et al. 2024), which is why every modern framework hands generation to a dedicated inference engine such as vLLM.

What do four models cost in memory and compute, and which tricks cut it?
Mixed-precision Adam needs about 16 bytes per trained parameter, a frozen bf16 model 2. FLOPs are per response token, 2N for a forward pass and 6N for a training pass. Activations and the KV cache come on top.
Loading interactive figure…

Read more

Watch next

🎬 Proximal Policy Optimization (PPO) for LLMs Explained Intuitively · Julia TurcThe value function, GAE, and clipping laid out for language models, a clear companion to the training step built here.
🎬 Reinforcement Learning from Human Feedback explained with math derivations and the PyTorch code. · Umar JamilThe full PPO loop for a language model, from the trajectory to the per-token KL penalty, with code.
🎬 Understanding Policy Gradient Algorithms for RL on LLMs | Post-Training Course Lecture 3 · Nathan LambertPPO next to REINFORCE, RLOO, and GRPO, and why the per-token details differ between them.