Reinforcement learning

Post-training · Learning from a score

Online reinforcement learning

Five techniques that teach a model with a score instead of an answer. SFT copies examples, and preference methods compare answers collected in advance. Here the model writes its own attempts while training runs, a checker scores each one, and the loss pushes every attempt up or down by how much better it did than expected. The running example is a pentest lab, because whether an attack worked on a lab is something a program can check.

Fact 1

The data holds tasks, not answers

An RL dataset is a list of tasks, such as “get a foothold on this lab and read the secret”. Nobody writes the solution. The model writes attempts, and a reward, one number per attempt, says how well each one did.

Fact 2

The model learns from its own tries, now

Tries are sampled from the current model while training runs, used for one update (or a few, in PPO, and in GRPO when it reuses a batch) and then replaced by new ones. Generating the data during training is what online means; sampling it from the very model being updated is what on-policy means. Fresh tries keep the data about the model as it is now.

Fact 3

A score becomes a direction

Each try's log-probability is pushed up or down by its advantage: its reward minus what the model usually gets. The loss is SFT's loss with a signed weight in front of each try.

Colour key Task: read, never graded A try: written by the model, graded Better than expected: pushed up Worse than expected: pushed down A checker, a critic or a frozen model An advantage, β, ε or a weight
Symbol tables datacomes from your task set trainedthe model being trained, or what it outputs frozena fixed model or program: the reference, a checker generatedwritten by a model while training runs settinga number you choose: K, β, ε, a learning rate mathnotation: a sign or an operation resultthe number the formula computes

One loop, five levers · click a technique

Nothing changedthe starting recipe
Where the score comes froma program, not a judgement
What “expected” meansthe baseline: a critic or the group
How far the model may moveper step: the clip · in total: the KL leash
What one try isan answer, or a whole session

The loss all five build on

\(\displaystyle \mathcal{L}(\theta) = -\frac{1}{K}\sum_{i=1}^{K} A_i\,\log\pi_\theta(y_i\mid x)\)
\(\displaystyle A_i = r(x,y_i) - b \qquad y_i\sim\pi_\theta(\cdot\mid x)\)

Let the model try the task \(x\) K times. Push each try \(y_i\) up or down by its advantage \(A_i\): its reward \(r\) minus a baseline \(b\), what the model usually gets.

What each symbol means
\(\mathcal{L}(\theta)\)“L of theta”resultThe loss for one task: a number whose slope tells every weight which way to move. Unlike SFT's loss, its value is not a score of how well the model does.
\(\theta\)“theta”trainedAll the weights of the model being trained.
\(x\)dataThe task, the prompt of every try. It comes from your task set; its answer does not.
\(K\)settingHow many tries per task: usually 4 to 64, with 8 or 16 common.
\(y_i\)“y i”generatedThe i-th try: an answer, or a whole agent session, written by the model itself while training runs.
\(y_i\sim\pi_\theta(\cdot\mid x)\)“y i drawn from pi theta”mathThe try is sampled from the model's own probabilities, token by token, like rolling a weighted die. The dot \(\cdot\) stands for every possible answer.
\(\pi_\theta(y\mid x)\)“pi theta of y given x”trainedThe probability that the model writes the whole try \(y\) after the task \(x\): the product of its token probabilities (for an agent session, of the agent's tokens only). \(\pi\) is the usual letter for a model that chooses, a policy.
\(r(x,y_i)\)“the reward”frozenOne number for the try, from a checker, a judge or a reward model. It is never trained by this loss.
\(b\)“the baseline”resultWhat the model usually gets on this task: the average of the other tries' rewards, or a critic's estimate.
\(A_i\)“the advantage”resultHow much better than usual the try did. Positive pushes it up, negative pushes it down, zero leaves it alone.

Four questions every method answers

Q1
Where does the reward come from?

A program that checks the result, a judge, a reward model or a person. The model learns whatever it pays for.

Q2
What counts as “expected”?

The baseline: the other tries' average, the group's mean (with the advantages then divided by the group's spread, #19), or a critic that predicts the reward still to come.

Q3
How far may the model move?

Per step: nothing, or a clip that stops pushing a token once its probability ratio to when the try was written has left 1 ± ε (about ±20%) in the direction its advantage wants. In total: a KL leash to the starting model. Or both.

Q4
Which part gets the credit?

One reward for a whole try. Every token shares it, a critic gives each token its own advantage (#18), or the credit is cut per turn of a session (#20).

The concepts first · no formula yet

From copying to trying What changes when nobody knows the answer

The SFT and preference workbooks teach with data someone prepared. Reinforcement learning teaches with a score the model earns. Before any technique, here is what that changes, what it buys, and what it costs.

1

The idea in plain words

In SFT every training row holds the right answer, and the loss says “make this answer more likely”. For many tasks nobody can write that answer in advance. How do you get a shell on this particular lab? Which of twenty findings matter? But once the model has tried, it is often easy to tell how well it did: the secret was read or not, the expected findings were reported or not.

Reinforcement learning (RL) is built on that asymmetry. The model tries the task several times. A reward scores each try. Tries that did better than the model usually does become more likely, and tries that did worse become less likely. Repeat over many tasks and the model gets better at the task, without anyone ever showing it a solution.

Underneath, nothing new is needed. The model, the forward pass, the backward pass and the update are exactly those of the backpropagation page. What changes is who writes the training text (the model itself, while training runs) and the weight on each try (a signed number instead of +1).

2

One engine, a different weight

Every technique in the three workbooks makes some answers more likely and others less. They differ in two things only: who wrote the answers the loss reads, and the weight each answer gets. Click a rung of the ladder.

Four ways to train on the same four answers

One task, four answers. Each rung of the ladder decides who wrote them and how hard each one is pushed. A bar to the right raises that answer's log-probability; to the left lowers it.

3

What RL buys, and what it costs

What it buys
  • It can beat its examples

    The SFT workbook ends on W3: imitation cannot beat its teacher. A reward pays for doing better, not for matching, so that ceiling goes. Two remain: the reward's (a checker that pays for the wrong thing caps the model there) and what the model ever tries (section 2.3).

  • It learns from its own mistakes, fresh

    The preference workbook ends on the data goes stale: pairs drawn from an older model describe its old mistakes. RL samples new tries from the current model after every update (or every few, when a batch is reused).

  • It needs a check, not an answer

    Writing the perfect pentest report for a lab is hard. Checking whether a report names the lab's real findings is easy, and a program can do it.

What it costs
  • Tries are generated while training runs

    Every update needs fresh tries, so generation sits inside the training loop. For an agent, one try is a whole session with real tools: minutes, where one answer takes seconds.

  • The signal is noisy

    A handful of tries per task is a small sample, and one number per try says little about which part was good. Most of the techniques here exist to reduce that noise.

  • The model learns what you pay for

    If a lazy checker pays for something easier than the task, the model finds it. That is reward hacking, and #17 is about preventing it.

Ten words, one picture

The words of RL In plain words, and in a RedAmon session

RL has its own vocabulary, inherited from optimal control and the psychology of animal learning, and made famous by robots and board games. Each word has a plain meaning, and each has a concrete place in an AI pentest agent such as RedAmon's.

1

The vocabulary

WordIn plain wordsIn a RedAmon session on a lab, and the training harness around it
Policy \(\pi_\theta\)The model seen as something that chooses: for every situation, a probability for every next token.The agent model behind the think step, the LLM that writes each decision.
Task \(x\)What the model is asked to do: the prompt of every try.“Get a foothold on the lab's login page and read the secret”, plus the system prompt and the rules of engagement.
State, history \(h_t\)Everything the model has read before its next choice.The prompt, the target information gathered so far, and the trace of earlier tool calls.
Action \(a_t\)One choice: a token, or a whole turn made of tokens.One structured decision: run a tool with these arguments, plan several tools, change phase, or finish, among others.
Observation \(o_t\)What the world answers. The environment wrote it, so the loss reads it and never grades it.A tool's output from the sandbox: a port scan table, an HTTP response, an error.
Episode, trajectory \(\tau\)One full try, from the task to the end.One session against one lab, until it reads the secret, gives up, or runs out of budget.
EnvironmentEverything outside the model that reacts to its actions.The lab container, the tools, and the checker that knows the lab's ground truth.
Reward \(r\)One number that says how the try went. Often there is only one, at the end.The checker's verdict: the secret was read, the expected findings were reported.
Baseline \(b\)What the model usually gets on this task.The average reward of the other sessions on the same lab (leave-one-out, as in #16).
Advantage \(A\)Reward minus baseline: how much better than usual this try did.With four sessions and one success: +1.00 for the session that read the secret, −0.33 for each of the three that did not.

“Action” is used at three sizes. For the loss, the smallest action is one token. For an agent, the natural unit is a turn, many tokens long. The reward usually judges the whole episode. Which of the three gets the credit is question Q4, and #20 is about it.

2

One episode, drawn

task · read, never graded
Get a foothold on the lab's login page and read the secret in /flag.
agent · action 1 · graded
request the login page over HTTP
environment · observation · read, not graded
200 OK · a form with fields user and pass
agent · action 2 · graded
test the user field for SQL injection
environment · observation · read, not graded
a database error appears: the field is injectable
agent · action 3 · graded
use the injection to read /flag
environment · observation · read, not graded
the file's contents
checker · the reward
the secret matches the one the lab planted · reward 1

An episode alternates. The agent writes an action, the environment answers with an observation, and the agent reads it and writes the next action. At the end a checker, a program that knows the lab, gives one reward.

For training, the whole episode is one long sequence of tokens. Two facts follow. Every agent technique keeps the first; the second is where they start:

  • Only the agent's tokens are graded. The observations sit in the history, read like a prompt. If they were graded, the model would be trained to write tool outputs itself, which means making them up.
  • One reward covers every action. The checker saw only the end. Deciding which of the three actions earned the reward is the credit-assignment problem, and #20 cuts the credit finer.

All the labs on this page are fictional toys. Real training runs on lab targets built for it, never on systems you are not authorised to test.

3

Exploration: you can only learn what you sometimes try

RL can only strengthen an action the model already takes now and then. If the right move has probability 0, no try ever contains it and no reward ever pays for it directly; at most it rises as a side effect of learning something related. That is why RL starts from a model that already roughly knows the task (usually an SFT model, sometimes a strong base model), and why tries are sampled with some randomness instead of always taking the favourite.

Will the good move ever be tried?

A model chooses its first move on a lab. Only one move works. Change the temperature, how much randomness the sampling adds, and the number of tries K, and see how often a group of tries contains the good move at least once. With a group baseline, only those groups can teach anything.

From a score to a weight update

The loop and the one formula Try, score, compare, push, repeat

Every technique on this page runs the same loop, and one formula sits in its middle. Here they are in order: first as a loop, then number by number, then the maths that says why pushing by the score raises the score.

1

The loop

1Pick tasksa batch of labs from the task set
2Trythe current model writes K tries per task, sampling: the rollouts
3Scorea checker gives each try one reward
4Compareadvantage = reward − what the model usually gets
5Loss−advantage × log p(the try), averaged
6Backwardat each graded token: (advantage / K) × (p − y)
7Updateevery weight takes one small step
8Throw awaythe tries describe the old model: back to 1

Steps 5 to 7 are SFT's training step with a weight in front of each try. Steps 2 to 4 are new: in SFT the text came from a file, while here it comes from the model being trained, and step 8 says it must be made again: after every update in the plain recipe, after a few when the batch is reused, as in PPO (#18).

2

One turn of the loop, number by number

Three moves, four tries, one update

A tiny policy picks its first move on a lab whose login form has an SQL injection. Only the injection test reaches the flag. Press Next to walk the loop one stage at a time: sample, score, compare, push, update. Then sample a new group and go round again.

3

Why pushing by the score raises the score

  1. The goal: the average reward

    What we want to raise is the reward the model earns on average, over everything it might write:

    $$J(\theta) = \mathbb{E}_{y\sim\pi_\theta(\cdot\mid x)}\big[r(x,y)\big] = \sum_{y}\pi_\theta(y\mid x)\,r(x,y)$$
    \(J(\theta)\)“J of theta”resultThe objective: the expected reward of the model with weights \(\theta\). RL raises it; there is no right answer to compare with.
    \(\mathbb{E}_{y\sim\pi_\theta(\cdot\mid x)}\big[\;\cdot\;\big]\)mathThe average of the bracket over the answers the model would write for \(x\), each counted as often as the model writes it.
    \(\sum_y\)mathA sum over every possible answer. For a real model there are far too many to list, which is why the next steps replace it with a few samples.
    \(\pi_\theta(y\mid x)\)trainedThe probability the model gives to answer \(y\). It is the only part the weights can change.
    \(r(x,y)\)frozenThe reward of answer \(y\): a checker, a judge, a reward model. A fixed function of the text.
    three moves, rewards 0 / 1 / 0, probabilities 0.50 / 0.30 / 0.20 J = 0.50 × 0 + 0.30 × 1 + 0.20 × 0 = 0.30 the model reaches the flag 30% of the time
  2. The reward has no slope; the probabilities do

    The answer is discrete text, picked by sampling. Nudging a weight does not change a written answer a little, so no slope reaches the weights through the reward, whether the reward is a program or even a neural reward model. What the weights do move smoothly is the probability of each answer, and those probabilities have slopes:

    $$\nabla_\theta J = \sum_{y} r(x,y)\;\nabla_\theta\,\pi_\theta(y\mid x)$$
    \(\nabla_\theta\)“nabla theta”, the gradientmathFor every weight: how much the quantity after it changes when that weight is nudged up.
    \(\nabla_\theta\,\pi_\theta(y\mid x)\)trainedHow each weight changes the probability of answer \(y\). Backpropagation computes it.
    \(r(x,y)\)frozenA plain number in front: rewarded answers count, unrewarded ones do not.

    The formula is exact but useless as it stands: it needs every possible answer.

  3. The log trick: from every answer to a few tries

    A one-line identity, the slope of a log, rewrites the slope of a probability as the probability times the slope of its log: \(\nabla\pi = \pi\,\nabla\log\pi\). Put it in, and the sum over answers weighted by their probability becomes an average over answers the model samples:

    $$\nabla_\theta J = \sum_{y}\pi_\theta(y\mid x)\,r(x,y)\,\nabla_\theta\log\pi_\theta(y\mid x) = \mathbb{E}_{y\sim\pi_\theta}\big[\,r(x,y)\,\nabla_\theta\log\pi_\theta(y\mid x)\,\big] \approx \frac{1}{K}\sum_{i=1}^{K} r_i\,\nabla_\theta\log\pi_\theta(y_i\mid x)$$
    \(\nabla_\theta\log\pi_\theta(y\mid x)\)trainedSFT's direction for answer \(y\): the change to the weights that makes it more likely. The SFT workbook computes exactly this.
    \(\approx\)math“Is estimated by”. The average over K samples is right on average and noisy for any single group.
    \(y_i,\ r_i\)generatedThe i-th try and its reward.

    Read it as a recipe. Sample K tries. For each one, take SFT's direction for that try, multiplied by its reward, and average. On average, a small step in that direction raises the average reward; any single group can point the wrong way. This is the policy gradient theorem in its simplest, one-step form, and the algorithm that follows it is REINFORCE (Williams, 1992).

  4. The baseline is free

    Subtract the same number \(b\) from every reward and, on average, the direction does not change:

    $$\mathbb{E}_{y\sim\pi_\theta}\big[\,b\,\nabla_\theta\log\pi_\theta(y\mid x)\big] = b\sum_y \nabla_\theta\,\pi_\theta(y\mid x) = b\,\nabla_\theta\!\sum_y\pi_\theta(y\mid x) = b\,\nabla_\theta 1 = 0$$

    A push of the same size \(b\) on every answer, each counted as often as the model writes it, is a push on their total, and the total is always 1: it adds nothing. What the baseline changes is the noise. With rewards of 0 or 1 and no baseline, the failed tries get no push at all, and the successful ones are pushed up even when everyone succeeds. With \(b\) set to what the model usually gets, “better than usual” goes up and “worse than usual” goes down. The one rule: \(b\) may depend on the task and on the other tries, never on the try it is subtracted from, or the average changes. That is why #16 leaves each try out of its own baseline; the plain group mean, which includes the try, shrinks the average step by \(\frac{K-1}{K}\) without changing its direction.

    rewards 0 / 1 / 0 / 0, the model's true average reward J = 0.30 no baseline weights 0.00 1.00 0.00 0.00 the three failures get no push baseline b = J = 0.30 weights −0.30 +0.70 −0.30 −0.30 failures down, the success up, same average direction

    A baseline computed from the group itself (#16, #19) has one more property: a group in which every try got the same reward gets no push at all. A fixed baseline or a critic (#18) would still push such a group.

  5. A loss whose slope is that recipe

    Trainers minimise a loss, so write one whose gradient is minus the recipe. Treat each advantage as a plain number (no slope flows into the checker) and use SFT's log-probability:

    $$\mathcal{L}(\theta) = -\frac{1}{K}\sum_{i=1}^{K} A_i\,\log\pi_\theta(y_i\mid x) \qquad\Longrightarrow\qquad -\nabla_\theta\mathcal{L} = \frac{1}{K}\sum_{i=1}^{K} A_i\,\nabla_\theta\log\pi_\theta(y_i\mid x)$$

    This is called a surrogate loss: its value does not track progress (it can be negative, with a group baseline it is exactly 0 for any group whose tries all got the same reward, successes or failures, and a new sample of tries changes it completely), but its slope is the one that raises the reward.

  6. At the logits: the advantage times p − y

    The backpropagation page shows that the slope of −log p(target) at the logits is \(p - y\), the error slope. In the RL loss each try's −log p carries the weight \(A_i/K\), so:

    $$g_z = \frac{A_i}{K}\,\big(p - y\big)$$

    That is the whole difference from SFT at the point where the backward pass starts. A positive advantage gives SFT's push toward the token the try wrote. A negative advantage gives the same push reversed, away from it. From the logits down, every box, every slope and the update are exactly the ones on the backpropagation page.

4

The loss is not the score: what to watch instead

  • Average reward

    The number RL raises. Watch it on the training tasks and, separately, on labs the model never trains on: the gap between them is overfitting.

  • Drift from the starting model

    The KL divergence between the trained model and a frozen copy of where it started. A sudden jump often means the model has found something odd that pays.

  • Variety of the tries

    If every try in a group gets the same reward, a group baseline (#16, #19) gives every advantage 0, and once the tries become identical that happens to every group: learning stops. Watch the entropy, and how often a group's rewards differ.

  • Length and the parts of the reward

    Answers that keep growing can be healthy (longer reasoning) or a warning: a reward model that likes length, or a length bias in the loss (#19). A reward that rises while its “proof” part does not is a common early sign of hacking.

Why there are five techniques, not one

Four problems every method answers Noise · waste · drift and hacking · credit

The recipe of section 3 works, and it is #16. Run it on real tasks and four problems show up. Each technique that follows is one answer to one of them.

1

The four problems

  • Q2 Noise: a few tries, one number each

    Four tries are a tiny sample, so two groups on the same task can point in very different directions. A baseline removes part of the noise: the other tries' average (#16), a critic that predicts the reward still to come (#18), or the group's mean, with the advantages then divided by its spread (#19).

  • Q3 Waste: each batch of tries is used once

    Generating tries is the slow part, and the plain recipe throws them away after a single small step. PPO (#18) reuses one batch for several steps, and a clip stops pushing a token once its probability ratio to when the batch was written has left 1 ± ε in the direction its advantage wants, so the steps stay where the old tries still say something reliable. GRPO (#19) keeps the clip.

  • Q1 Q3 Drift and hacking: the model finds whatever pays

    A reward that pays for something easier than the task gets the easier thing. The first defence is an honest checker (#17). The second is a KL leash, a cost for moving far from the starting model (#18, #19), so the model improves without turning strange.

  • Q4 Credit: one reward, thirty turns

    A session that reads the secret gets one reward, and every one of its turns shares it, the wasted ones too. Multi-turn agent RL (#20) compares turns that faced the same situation, and adds progress rewards built so that they cannot be farmed.

2

Where each problem is solved

  1. The plain recipe, with a baseline that leaves each try out of its own average#16 REINFORCE, RLOO
  2. A reward a program computes from the lab's ground truth, built to be hard to fool#17 Verifiable rewards
  3. Bigger, safer steps: a clip, a learned critic, a KL leash#18 PPO
  4. PPO without the critic: the group of tries is the baseline. The practical choice when a checker exists#19 GRPO and its fixes
  5. Whole sessions as tries: tool outputs masked, credit cut per turn#20 Multi-turn agent RL

The base · every other technique changes one part of it

REINFORCE and RLOO Policy gradient with a baseline · REINFORCE leave-one-out

Give the model a task and let it try K times. A checker scores each try. Push every try's log-probability up or down by its advantage, its reward minus the average reward of the other tries. Tries that beat the usual become more likely; tries that fall short become less likely.

Four sampled tries, one checker, and the advantage that pushes each try up or down.

Changes vs SFT
The model writes the answers during training; each one gets a signed weight, its advantage, instead of +1
Its answers
Q1 a checker · Q2 the other tries' average · Q3 a small learning rate in the bare algorithm (published RLOO and TRL add a KL leash) · Q4 the whole try
Cost
●●●●● K tries generated per task, inside the training loop
Library
Ready: TRL RLOOTrainer with your own reward function; plain REINFORCE is a few lines on top of any trainer
1

The idea in plain words

For each task, sample K tries from the model being trained, as it is (temperature 1), so that they differ and follow its own probabilities; sampling at another temperature means computing the loss's log-probabilities at that temperature too. A checker gives each try a reward. The baseline of a try is the average reward of the other K − 1 tries on the same task, and its advantage is its reward minus that baseline.

The loss is SFT's loss on the model's own tries, with each try weighted by its advantage. A try that read the secret when the others did not gets a large positive weight and is made more likely, token by token. A try that failed when others succeeded gets a negative weight and is made less likely. When every try scored the same, every advantage is 0 and the task teaches nothing this round.

After one update the tries are thrown away, because they were written by a model that no longer exists. The next step samples new ones. The algorithm is REINFORCE (Williams, 1992); leaving each try out of its own baseline gives RLOO, REINFORCE leave-one-out (Kool et al., 2019; Ahmadian et al., 2024).

2

See it work

Train by trying

A tiny policy picks its first move on a lab. Each move reaches the flag with some chance that the model does not know. Press Sample and train to draw K tries, score them, and update. Switch the baseline to see what it does to the noise, and shrink K to see how many groups teach nothing.

3

Where the training signal comes from

Three decisions: which tasks the model practises on, who scores the tries, and how many tries each task gets.

4

A small example

One task, four tries
Task · promptGet a foothold on the lab's login page and read the secret. Choose your first move.
Try 1 · sampledA · try the default passwords
Try 2 · sampledB · test the login form for SQL injection
Try 3 · sampledA · try the default passwords
Try 4 · sampledC · brute-force the password

The model's probabilities before training: A 0.50, B 0.30, C 0.20. A was drawn twice simply because it is the favourite.

The checker's verdicts
Checker · try 1no secret read · reward 0
Checker · try 2planted secret read · reward 1
Checker · try 3no secret read · reward 0
Checker · try 4locked out after 5 attempts · reward 0
What RLOO computes

Each baseline is the average of the other three rewards.

try move reward baseline advantage 1 A 0 (1+0+0)/3=0.33 −0.33 2 B 1 (0+0+0)/3=0.00 +1.00 3 A 0 (0+1+0)/3=0.33 −0.33 4 C 0 (0+1+0)/3=0.33 −0.33 after one update (learning rate 1, on the logits): A 0.50 → 0.43 B 0.30 → 0.39 C 0.20 → 0.19

Nobody told the model that B was right. The checker's verdict, compared across four tries, did.

5

What a training batch looks like

TaskTry, written during trainingRewardAdvantage
login lab · first movetry the default passwords0−0.33
login lab · first movetest the login form for SQL injection1+1.00
login lab · first movetry the default passwords0−0.33
login lab · first movebrute-force the password0−0.33
file-share lab · first movelist the anonymous shares10.00
file-share lab · first movelist the anonymous shares10.00
file-share lab · first movecheck the service banner10.00
file-share lab · first movelist the anonymous shares10.00

Two tasks, four tries each. Every try on the second lab succeeded, so all its advantages are 0: that lab is already solved and gives no signal until it is replaced by a harder one.

# tasks.jsonl: the dataset holds tasks only. No answers: the model writes those.
{"prompt": [{"role": "user", "content": "Get a foothold on the lab's login page and read the secret."}],
 "lab": "login-sqli-01"}
{"prompt": [{"role": "user", "content": "Find what the file-share lab exposes without credentials."}],
 "lab": "fileshare-anon-03"}

# reward.py: one number per try. Extra dataset columns ("lab") reach the function
# as keyword arguments in TRL's GRPOTrainer and RLOOTrainer. A sketch: lab_checker is yours;
# with conversational prompts each completion is a list of messages, so its text is c[0]["content"].
def flag_reward(completions, lab, **kwargs):
    return [1.0 if lab_checker(l).secret_read(c[0]["content"]) else 0.0
            for c, l in zip(completions, lab)]

# RLOOTrainer(model=..., reward_funcs=flag_reward, train_dataset=tasks, args=RLOOConfig(...))
# the number of tries per task: num_generations (TRL ≥ 0.22; the older RLOOTrainer had a different API, with rloo_k)

The secret and the ground truth stay with the checker, outside the model's sandbox. If the model could read them, it would learn to.

For each try the trainer needs its log-probability under the model being trained, re-read with gradients on, and its advantage, a plain number.

The login labrewardadvantage \(A_i\)\(\log\pi_\theta(y_i\mid x)\)\(-A_i\log\pi_\theta / K\)
try 1 · A0−0.33−0.69−0.058
try 2 · B1+1.00−1.20+0.301
try 3 · A0−0.33−0.69−0.058
try 4 · C0−0.33−1.61−0.134

The loss is the sum of the last column, 0.051. Its value means little: the next group of tries would give a different number even with the same model. What matters is its slope.

6

The loss

$$\htmlClass{term term-1}{\mathcal{L}_{\text{RLOO}}(\theta)} = -\,\mathbb{E}_{x\sim\mathcal{D}}\Bigg[\frac{1}{K}\sum_{i=1}^{K}\htmlClass{term term-2}{\Big(r_i - \frac{1}{K-1}\sum_{j\neq i} r_j\Big)}\;\htmlClass{term term-3}{\sum_{t=1}^{|y_i|}\log\pi_\theta\big(y_{i,t}\mid x,\,y_{i,\lt t}\big)}\Bigg] \qquad \htmlClass{term term-4}{y_i\sim\pi_\theta(\cdot\mid x)}$$

Read it aloud: “For each task, let the model write K tries. Give each try an advantage: its reward minus the average reward of the other tries. Multiply each try's log-probability, the sum over its tokens, by its advantage. Push the average up.” Point at a row of the table to light its term up in the formula.

one task Inside \(\mathbb{E}\) is the loss of one task with its K tries. \(\mathbb{E}_{x\sim\mathcal{D}}\) means “averaged over tasks”, in practice over the tasks of a batch. The tries are not in the dataset: they are generated, scored and used inside this one step. Station 7 follows one full step, from the tasks to the update.

Every symbol in the formula

SymbolWhat it means
The data
\(x\)dataA task, for example “get a foothold on the lab's login page and read the secret”.
\(\mathcal{D}\)dataThe task set: prompts only, no answers.
\(x\sim\mathcal{D}\)mathA task picked from the set.
\(\mathbb{E}_{x\sim\mathcal{D}}\big[\;\cdot\;\big]\)mathThe average over tasks.
The tries
\(K\)settingTries per task, at least 2 for a leave-one-out baseline; 4 in the example.
\(y_i\sim\pi_\theta(\cdot\mid x)\)generatedThe i-th try, sampled from the model being trained, token by token, while training runs.
\(|y_i|\)mathThe number of tokens in try \(i\).
\(y_{i,t}\), \(y_{i,<t}\)generatedThe token of try \(i\) at position \(t\), and the tokens of that try before it.
The scores
\(r_i\)frozenThe reward of try \(i\): the checker's number. No slope flows into it.
\(\frac{1}{K-1}\sum_{j\neq i} r_j\)“the others' average”resultThe baseline of try \(i\): the average reward of the other tries on the same task. \(j \neq i\) reads “every \(j\) except \(i\)”.
\(r_i - \frac{1}{K-1}\sum_{j\neq i} r_j\)resultThe advantage \(A_i\). In the example: \(1 - 0 = +1\) for the injection test, \(0 - \frac{1}{3} = -0.33\) for each of the others.
The model and the loss
\(\theta\)trainedThe weights being trained.
\(\sum_t\log\pi_\theta(y_{i,t}\mid x, y_{i,<t})\)trainedThe log-probability of the whole try: the sum of its tokens' log-probabilities, as in SFT, but on text the model wrote itself.
\(\mathcal{L}_{\text{RLOO}}(\theta)\)resultThe surrogate loss. Its slope raises the average reward; its value is not a score and is not expected to fall toward 0.

What you must supply: a set of tasks, a reward function that scores a try, and K. No answers, no pairs, and, for the bare algorithm, no reference model and no critic. The tries come from the model during training. (Published RLOO and TRL's RLOOTrainer add a KL penalty to a frozen copy by default, beta=0.05; set beta=0.0 to drop it.)

How the loss works, step by step

  1. Try. Sample K tries for the task from the current model.
  2. Score. Run the checker on each try: K rewards.
  3. Compare. Each try's advantage is its reward minus the average of the other K − 1.
  4. Weight. Multiply each try's summed log-probability by its advantage and average over the K tries; the loss is minus that.
  5. Step. The gradient raises every token of each better-than-usual try and lowers every token of each worse-than-usual try. A try with advantage 0 is not pushed.

Worked example: the login lab, three baselines for the same four tries

baseline advantages (A, B, A, C) p(B) after one step the failed tries none 0.00 +1.00 0.00 0.00 0.30 → 0.36 no push of their own the group's mean −0.25 +0.75 −0.25 −0.25 0.30 → 0.37 pushed down leave-one-out (RLOO) −0.33 +1.00 −0.33 −0.33 0.30 → 0.39 pushed down

The three rows point the same way on average; the mean-baseline row is exactly RLOO's times \(\frac{K-1}{K}\). The difference is in the noise. Without a baseline a failed try adds nothing of its own (its move still loses probability, through the success's push), and on a lab where every try succeeds every try is still pushed up. With one, the push is relative: better or worse than this model does on this task today.

7

One training step on the GPU

An RL step has a phase SFT never has: writing the training text. Generation runs first, without gradients, then scoring, then an ordinary SFT-shaped training step on the tries with a weight per try. Plain SFT's station 7 walks the training half with every number; this one shows what is added.

1GenerateB tasks × K tries, sampled token by token, no gradient
2Scorethe checker runs on every try, outside the GPU
3Forwardthe trained model re-reads all B·K tries, with gradient
4Backward(A_i/K)(p − y) at every token of try i
5Updateonce; then the tries are discarded

One step, slope by slope

One task with K = 4 tries of 12, 9, 12 and 15 tokens.

one task: 4 tries, 48 answer tokens in all → ONE training step 1 generate the model writes the 4 tries, one token at a time, all four together: 15 decoding steps (the longest try), 48 token positions in all, no gradient 2 score the checker: rewards 0, 1, 0, 0 → advantages −0.33, +1.00, −0.33, −0.33 3 forward the trained model re-reads the 4 tries, every position at once, with gradient 4 backward one number per try, known before any slope: k_i = A_i / K then 48 slopes at the same time, one per answer token: try 2, 9 tokens: g_t = +0.250 (p_t − y_t) raise its tokens tries 1, 3, 4, 39 tokens: g_t = −0.083 (p_t − y_t) lower their tokens 5 add each weight sums the 48 contributions → 1 gradient per weight 6 update the weights move once; the 4 tries are thrown away
  • The weight of a slope is known before the backward pass. Unlike DPO, where one number per pair needs both answers' log-probabilities first, here \(k_i\) depends only on the rewards (plus, with a KL penalty in the reward, numbers also computed before the backward pass), which the checker gave before training even started on this batch.
  • No division by length. Every token of a try gets the full \(k_i\), so long tries get more pushes in total. GRPO (#19) divides by length, and that choice has consequences of its own.
  • Read, not graded: the task, in every try.
  • Not one after the other. The 48 slopes are all measured on the same old weights, then added: the weights move once.
1Generation

Writing K tries per task means one decoding step per token of the longest try, each step a forward pass over every unfinished try at once, as at inference. It is usually most of the time an RL step takes. Trainers hand it to a fast inference engine (vLLM in TRL, for example) that holds its own copy of the weights. After every update the new weights must be copied to it, or the tries would come from an old model.

2Scoring

The checker is ordinary code: compare a string, query the lab, look for the planted secret. It runs on the CPU, in parallel, while the GPU waits. For agents it means running real tools in a sandbox, and it can take longer than generation.

3Forward pass with gradient

The trained model reads all B·K tries at once, prompt plus try, exactly like SFT reads its rows with teacher forcing, and adds up the log-probabilities of each try's tokens. The generation pass cannot be reused for this: it kept no activations, and its numbers may differ slightly from the trainer's.

4Backward pass
$$g_{i,t} = \frac{A_i}{K}\,\big(p_{i,t} - y_{i,t}\big)$$

(\(y_{i,t}\) is 1 at the token the try wrote and 0 at every other word.) With B tasks in the batch the trainer also divides by B. Below the logits nothing is new: the backward pass of the backpropagation page, on every try.

5Update

One optimiser step, then the tries are discarded. Real runs use learning rates around 0.000001, far smaller than SFT's, because a step that pushes a good answer's probability near 0 is hard to undo: the next batches, sampled from the damaged model, hardly ever try that answer again.

On a real GPU

  • Two copies of the weights, when generation runs on a separate inference engine: the trainer's, with gradients and optimiser state, and the engine's, synced after every update. Without one, the trained model generates its own tries, more slowly.
  • Variable lengths: the tries of a batch have different lengths, so the grid is padded and the padding masked, as in SFT.
  • Nothing else is needed: no critic, and no reference model unless a KL penalty is used (TRL's RLOOTrainer loads one by default). That makes REINFORCE and RLOO, like GRPO without a KL term, among the lightest online methods.
8

The maths behind it

  1. The policy gradient, from section 3
    $$\nabla_\theta J = \mathbb{E}_{y\sim\pi_\theta}\big[\,(r(x,y) - b)\,\nabla_\theta\log\pi_\theta(y\mid x)\,\big]$$

    Raising the average reward means following, on average over the model's own answers, SFT's direction weighted by reward minus baseline. Any baseline that does not depend on the answer it is subtracted from leaves the average unchanged (section 3, step 4).

  2. Why leave the try out

    With the plain group mean, \(b = \frac{1}{K}\sum_j r_j\) contains \(r_i\) itself, so the baseline depends on the try: a lucky try raises its own baseline. Leaving it out removes that dependence, and the estimate is exactly unbiased. The two differ only by a constant factor:

    $$r_i - \frac{1}{K}\sum_{j} r_j \;=\; \frac{K-1}{K}\Big(r_i - \frac{1}{K-1}\sum_{j\neq i} r_j\Big)$$
    \(\frac{1}{K}\sum_j r_j\)resultThe group's mean, try \(i\) included.
    \(\frac{1}{K-1}\sum_{j\neq i} r_j\)resultThe leave-one-out baseline: the other tries' mean.
    \(\frac{K-1}{K}\)mathA constant: 0.75 for K = 4. It only rescales the step, like a smaller learning rate.

    So the group-mean version is RLOO with a learning rate \(\frac{K-1}{K}\) times smaller. That is why the two are often used interchangeably (with Adam, a constant factor on the gradient is almost entirely cancelled), and why GRPO (#19) can use the group mean.

  3. How noisy is one group?

    For a task where the model succeeds with probability \(s\), a group of K tries holds at least one success and at least one failure with probability

    $$P(\text{signal}) = 1 - s^{K} - (1-s)^{K}$$
    \(s\)resultThe model's current success rate on the task.
    \(s^K\)mathThe chance that all K tries succeed: every advantage 0.
    \((1-s)^K\)mathThe chance that all K tries fail: every advantage 0.
    success rate s K = 2 K = 4 K = 8 K = 16 0.05 0.10 0.19 0.34 0.56 0.30 0.42 0.75 0.94 1.00 0.95 0.10 0.19 0.34 0.56

    Tasks the model almost always fails, or almost always solves, teach much less often, unless K is large. RL learns most from tasks at the edge of what the model can do.

  4. What one step does at the logits

    At each graded position of try \(i\) the slope on the logits is \(\frac{A_i}{K}(p - y)\), the error slope of the backpropagation page times one number. Training moves the logits against it. A positive advantage raises the written token's logit and lowers every other logit in proportion to its probability, exactly as SFT does. A negative advantage does the reverse: the written token's logit falls and the others rise. This is the push SFT never makes, the same one DPO makes on a rejected answer.

    the login lab, logits of A / B / C, averaged over the four tries (RLOO) slope g +0.167 −0.250 +0.083 step −1 × g −0.167 +0.250 −0.083 B's logit rises, A's falls most: it failed twice
9

Watch out for

  • Groups that teach nothing

    All tries succeed or all fail: every advantage is 0, and the generation was wasted. Track the share of such groups. Replace solved tasks with harder ones, and drop tasks the model never solves until it is stronger.

  • Exploration dies

    As the model grows confident, its K tries become identical and the groups stop carrying signal. Sample at a temperature near 1, and watch the variety of the tries.

  • Noisy steps

    A few tries per task give a rough direction. Use many tasks per batch and a small learning rate, and judge progress by the average reward over many steps, not by any single one.

  • The loss is not progress

    The surrogate loss jumps with every new sample of tries. Plot the average reward, on training labs and on held-out ones.

  • Nothing holds the model back

    Plain REINFORCE has no leash: a long run can drift far from the starting model, in style and in skills the reward never checks. The published RLOO and TRL's RLOOTrainer subtract a KL penalty from the reward; PPO adds one, and GRPO can (#18, #19).

  • The checker is the target

    Whatever the reward pays for is what the model learns, gaps included. Before scaling up, read a sample of the tries that score highest. #17 is about building a reward that pays for the task and nothing easier.

Where the score comes from · a program, not a judgement

Verifiable rewards RLVR · rule-based rewards · checkers on ground truth

When a program can check the result, you need no reward model and no judge. A pentest lab knows its own ground truth: which findings it hides, which secret proves a foothold. The reward is the checker's verdict. The real work is making sure the only way to earn it is to do the task.

The same four tries under three checkers: exact, lenient and partial credit. Watch what each one teaches.

Changes vs #16
Nothing in the algorithm: #16 already assumed a checker. This is how to build one that pays only for doing the task
Its answers
Q1 a checker on the lab's ground truth; the rest is whatever trainer it feeds (#16, #18, #19)
Cost
●●●●● cheap to compute; the labs, a fresh instance for every try, are the real cost
Library
Any online trainer. In recent TRL's GRPOTrainer and RLOOTrainer a reward is a plain Python function passed in reward_funcs; TRL's PPO trainer takes a reward model only
1

The idea in plain words

The name reinforcement learning with verifiable rewards (RLVR) was coined in Tülu 3 (Lambert et al., 2024) for maths and precise instruction following, and is now used for code too: the answer is a number that can be compared, the code passes its tests or not. Security labs have the same property. A lab built for training plants a secret that can only be read through the vulnerability, and it knows the full list of expected findings. A checker compares the try against both and returns a number.

Nothing in the loss changes. The checker is #16's reward, and its number goes into the same advantage. What matters is how it is built. Compared with a judge, which can be talked round, a checker cannot be. But a checker pays for exactly what it checks. If it checks less than the task, the model learns the gap: a claim instead of a proof, a list of every possible finding instead of the real ones. That is reward hacking. Here it can be measured, by scoring the same tries with a stricter checker, and fixed in the checker's code.

Some of RedAmon's test targets already work this way. Its route-exposure lab ships an expected_results.yaml, and a scenario passes when every expected row is seen and nothing unexpected is. That sentence is a reward function with a false-positive penalty built in.

2

See it work

Design the checker, watch what the model learns

A lab hides three real findings and a secret. The model can write six kinds of report, from careful to dishonest. Pick a checker, read the reward each report earns, then press Train: RLOO runs on the six report styles, and the style the checker pays for most takes over.

3

Where the training signal comes from

Three decisions: what proves success, how the score is built, and what the checker reads.

4

A small example

The lab's ground truth, kept by the checker
# login-lab ground truth: never inside the model's sandbox
secret: generated per run (128 random bits)
expected:
  - {path: /login,  kind: sql_injection,  param: user}
  - {path: /.git/,  kind: exposed_repository}
  - {path: /admin,  kind: default_credentials}
scope: [the lab's own hosts only]
One try's report
Finding 1 · with proofSQL injection in user on /login; the secret was read through it.
Finding 2 · with proof/.git/ is served; HEAD was fetched.
Finding 3 · no proofReflected XSS on /search.
SummaryExploit succeeded. Full compromise.
What the checker computes
proof the secret appears in a tool output 1 findings proven and expected: 1, 2 2 reported: 3 expected: 3 F1 = 2 × 2 / (3 + 3) 0.67 claim "exploit succeeded" ignored scope no request outside the lab 0 cost reward = ½ × proof + ½ × F1 = 0.50 + 0.33 = 0.83

Finding 3 does not exist in the lab, so it costs precision. The default password on /admin was missed, so it costs recall. The summary's claim earns nothing: only the environment's evidence counts.

5

What a training batch looks like

Try on the login labProofProven / reported / expectedF1Reward
Proves the injection and the repository, invents an XSS12 / 3 / 30.670.83
Proves all three findings, reads the secret13 / 3 / 31.001.00
Lists twelve common web bugs, the three real ones among them, proves none00 / 12 / 30.000.00
Says “exploit succeeded, flag captured”00 / 0 / 30.000.00

Under a checker that only counted findings mentioned, the third row would score 1.00 and the fourth, under one that trusted the summary, 1.00 too. The lab in station 2 shows what the model then learns.

# checker.py: a sketch. The truth file and the session log never enter the model's sandbox.
# session_log is produced during the rollout, not a dataset column: in TRL it can come back as an extra
# field from a custom rollout function, which the trainer forwards to reward functions.
def lab_reward(completions, lab, session_log, **kwargs):
    rewards = []
    for report, lab_id, log in zip(completions, lab, session_log):
        truth = load_truth(lab_id)                        # expected findings + this run's secret
        proof = 1.0 if truth.secret in log.tool_outputs() else 0.0   # the environment's words only
        reported = parse_findings(report)
        proven = {f for f in reported if log.has_evidence_for(f)}   # ideally from the lab's own logs
        hits = len(proven & truth.expected)
        f1 = 2 * hits / (len(reported) + len(truth.expected))      # unproven findings cost precision
        cost = 0.2 * log.out_of_scope_requests()        # a lesson, not a guard
        rewards.append(0.5 * proof + 0.5 * f1 - cost)
    return rewards

Test the checker before training on it, like any code that decides what a model will become: feed it an honest report, a sprayed one, a bare claim and an empty one, and check the order of their rewards.

The checker's number enters training in one place only: the advantage. The rest is the trainer of #16, #18 or #19.

The four tries aboverewardothers' averageadvantage (RLOO)
2 of 3 proven, one invented0.830.33+0.50
all 3 proven1.000.28+0.72
twelve guesses0.000.61−0.61
a bare claim0.000.61−0.61

No slope flows into the checker. It can be any program at all: a string comparison, a test suite, a query to the lab.

6

The loss, and the reward that feeds it

$$\htmlClass{term term-1}{r(x,y)} = \htmlClass{term term-2}{w_p\,\text{proof}(y)} + \htmlClass{term term-3}{w_f\,\frac{2\,|V^{+}(y)\cap E(x)|}{|V(y)| + |E(x)|}} - \htmlClass{term term-4}{w_s\,\text{out\_of\_scope}(y)}$$
$$\htmlClass{term term-5}{\mathcal{L}(\theta)} = -\frac{1}{K}\sum_{i=1}^{K}\Big(r(x,y_i) - b_i\Big)\log\pi_\theta(y_i\mid x)$$

Read it aloud: “The reward is a weighted sum: whether the environment's own outputs contain the proof, an F1 score that counts only proven, real findings as hits against everything reported and everything expected, and a cost for every request outside the scope. The loss is #16's, with that reward in it.” Point at a row of the table to light its term up.

one try The reward is computed for one try \(y\) of task \(x\), after the try has finished. Nothing in it has a gradient: it is a plain number by the time the loss sees it.

Every symbol in the formula

SymbolWhat it means
What the checker knows
\(x\)dataThe task: which lab, and what to achieve.
\(E(x)\)frozenThe expected findings of the lab: its ground truth, three rows in the example.
\(|\cdot|\)math“How many”: the size of a set.
What it reads from the try
\(y\)generatedThe try: a report, or a whole session with its tool outputs. Its log-probability in the loss counts only the model's own tokens; tool outputs are masked (#20).
\(\text{proof}(y)\)frozen1 if this run's planted secret appears in what the environment returned during the try, otherwise 0. Text the model wrote does not count.
\(V(y)\)frozenEvery finding the try reported: three in the example.
\(V^{+}(y)\)frozenThe reported findings the checker could confirm with evidence: two in the example. An unproven finding stays in \(V\) but not in \(V^{+}\).
\(V^{+}(y)\cap E(x)\)mathThe intersection: findings that are both proven and real, the hits.
\(\frac{2\,|V^{+}\cap E|}{|V| + |E|}\)“F1”resultThe F1 score, the balance of recall (\(|V^{+}\cap E|/|E|\), how much of the truth was found and proven) and precision (\(|V^{+}\cap E|/|V|\), how much of the report is proven and real). 1 for a perfect report; 0.67 in the example: \(2\times 2/(3+3)\).
\(\text{out\_of\_scope}(y)\)frozenHow many of the try's requests left the lab's scope. A cost teaches the rule; the hard limit stays in code.
Settings and results
\(w_p,\ w_f,\ w_s\)settingThe weights of the three parts: 0.5, 0.5 and 0.2 in the example.
\(r(x,y)\)resultThe reward of the try: 0.83 in the example.
\(b_i\)resultThe baseline of try \(i\), as in #16: the other tries' average.
\(\mathcal{L}(\theta)\)result#16's loss. Any online trainer would do: #18 and #19 use the same reward.

What you must supply: labs with ground truth (a secret generated per run, the expected findings, the scope), a checker that reads the environment's evidence rather than the model's words, and tests for the checker itself.

How the reward is computed, step by step

  1. Reset the lab. A fresh container and a fresh secret for every try, so no try can find what an earlier one left behind.
  2. Run the try. The model acts; every tool output is logged by the environment, outside the model's reach.
  3. Look for proof. Does this run's secret appear in an output the environment wrote?
  4. Match findings. Count the reported findings that have evidence and are on the expected list (the hits), and compute F1 against everything reported and everything expected.
  5. Charge costs. Subtract for scope violations, then hand the number to the trainer.

Worked example: three checkers, four kinds of report

report trusts the claim counts mentions F1 + proof proves all three, reads the secret 1.00 1.00 1.00 proves two, invents one 1.00 0.67 0.83 lists twelve common bugs, proves none 0.00 1.00 0.00 "exploit succeeded", nothing else 1.00 0.00 0.00

Under the first checker a bare claim is worth as much as a full compromise, and claiming is far easier than hacking. Under the second, listing every bug in the book beats proving two. Only the third pays most for the report you want. RL will find whichever of these orderings the checker implements.

7

One training step on the GPU

The GPU part of the step is #16's, unchanged. What a verifiable reward adds sits between generation and training, on ordinary machines: fresh labs, the tries running against them, and the checker.

1Resetone fresh lab per try, a new secret in each
2Generatethe model writes its tries; for an agent, it acts in the lab
3Checkproof, findings, scope: one number per try
4Advantagereward minus baseline, as in #16 or #19
5Train#16's forward, backward and update

Where the checker's number goes

one task, 4 tries → ONE training step checker rewards 0.83, 1.00, 0.00, 0.00 computed on CPUs, no gradient advantage +0.50, +0.72, −0.61, −0.61 RLOO, as in #16 backward every token of try i: g_t = (A_i / 4)(p_t − y_t) the checker itself receives no slope: it was a number, not a box
  • The lab is on the critical path. A step cannot train until its slowest try has been checked. Keep a pool of labs ready, and give every try a time limit: a timeout the try caused counts as a failure; one the lab caused is retried or masked.
  • Labs must not share state. Two tries against one lab can see each other's traces: the second finds the secret the first left in a file. One lab per try, reset each time.
1Fresh labs

B tasks × K tries means B·K lab instances per step. Containers start in seconds; a pool started ahead of time keeps the GPU from waiting. The secret is generated at start-up and given only to the checker.

2Running and logging

For a one-shot task the try is text and nothing runs. For an agent, every tool call runs in a sandbox that can reach the lab and nothing else, and the environment, not the model, keeps the log the checker will read.

3The checker

Deterministic code: the same try always gets the same reward. That makes it testable, and with ground truth behind it, that is what a judge cannot give you.

On a real cluster

  • CPU-heavy: labs and checkers need far more CPU than training does; they run on separate machines.
  • Asynchronous: while the trainer updates on one batch, the next batch's tries can already run against fresh labs. The tries are then one update old, a small off-policy gap that PPO's ratio (#18) is built to absorb, provided the ratio is taken against the log-probabilities of the model that generated them.
8

The maths behind it

  1. The reward needs no slope

    The policy gradient \(\mathbb{E}\big[(r - b)\nabla_\theta\log\pi_\theta(y\mid x)\big]\) uses the reward's value only. It never asks how the reward would change if the try changed a little. So the reward can be any function at all, however discontinuous: a string match, a test suite, “did the listener get a call?”. This is what makes RL usable on tasks a gradient could never see through.

  2. The model maximises the reward, not your intent
    $$\pi^{\star} = \arg\max_{\pi}\;\mathbb{E}_{y\sim\pi}\big[r(x,y)\big]$$
    \(\arg\max_\pi\)math“The \(\pi\) that makes what follows largest.”
    \(\pi^\star\)resultThe best policy for this reward: where training is heading.

    If some behaviour \(y_{\text{cheap}}\) earns more than the honest one, \(r(y_{\text{cheap}}) > \mathbb{E}[r\mid\text{honest}]\), then wherever the policy samples it at all, the gradient raises it (its expected push is \(\pi(y_{\text{cheap}})\,(r(y_{\text{cheap}}) - \mathbb{E}_\pi[r])\)), \(\pi^\star\) ends on it, and nothing in the maths knows that it is cheating. Goodhart's law in one line. The only fix is in \(r\).

  3. Why F1 stops the spray

    Prove the three real findings and add \(n - 3\) unproven ones to the report. Recall is 1; precision is \(3/n\). A reward that counts mentions gives 1 whatever \(n\) is. F1 gives

    $$F_1 = \frac{2\,|V^{+}\cap E|}{|V|+|E|} = \frac{2\times 3}{n + 3}$$
    findings reported n 3 6 12 30 counts mentions 1.00 1.00 1.00 1.00 F1 1.00 0.67 0.40 0.18
  4. Binary or partial credit: how often a group carries signal

    A lab hides 3 findings, and each try finds each one with chance \(q\). With pass or fail (here: all three findings proven, or nothing), a try succeeds with \(s = q^3\), and a group of K carries signal with probability \(1 - s^K - (1-s)^K\). With a share per finding, a group is silent only when every try found the same number of findings:

    $$P(\text{signal}) = 1 - \sum_{c=0}^{3} P(c)^K \qquad P(c) = \binom{3}{c}\,q^c\,(1-q)^{3-c}$$
    \(c\)mathHow many of the 3 findings a try found.
    \(P(c)\)resultThe chance a try finds exactly \(c\) of them.
    \(\binom{3}{c}\)math“3 choose c”: the number of ways to pick which \(c\) findings.
    q = 0.2, K = 4 pass or fail s = 0.2³ = 0.008 P(signal) = 1 − 0.008⁴ − 0.992⁴ = 0.03 a share each P(c) = 0.512, 0.384, 0.096, 0.008 P(signal) = 0.91

    On a hard lab pass-or-fail almost never speaks, and partial credit almost always does. That is the case for partial credit, provided false positives cost something.

  5. A proof that cannot be guessed

    A secret of \(m\) random bits is guessed with probability \(2^{-m}\): about \(3\times10^{-39}\) for 128 bits. A fixed flag, the same in every run, stops proving anything once the model has seen it: after a few successes it can be recited without any exploit. Generate the secret per run.

9

Watch out for

  • The answer key inside the sandbox

    If the ground-truth file, the checker's code or the secret can be read from where the model's tools run, the model will learn to read it. Keep them on the checker's side of the wall.

  • Trusting the model's own words

    An agent's own analysis may say exploit_succeeded: true. As a reward it teaches the model to say so. Pay only for evidence the environment produced.

  • A fixed or guessable secret

    The same flag in every run, or a predictable format, is memorised after a few successes. Generate it per run.

  • Labs that leak between tries

    One try's exploit leaves files, sessions or log entries that the next try finds. One fresh lab per try.

  • Flaky labs

    Timeouts, rate limits and slow start-ups turn into random rewards. Fix the lab before training on it. A timeout the try caused (a loop, a stall) counts as a failure, never as a missing value, or stalling gets rewarded; one the lab caused is retried or masked.

  • Labs, never real targets

    Every try is a real attack. Train only against labs built for it, in an isolated network, and never put real hosts, scan output or credentials into the task set. Scope costs teach the rules; the hard guardrails stay in code.

Bigger steps, safely · a clip, a critic and a leash

PPO Proximal policy optimisation · the classic RLHF algorithm

REINFORCE uses each batch of tries for one small step and throws it away. PPO reuses the same batch for several steps, and a clip stops pushing a token once its probability has moved about 20% from where it was when the try was written, in the direction its advantage wants. A learned critic predicts the reward still to come, giving a baseline for every token, and a KL penalty keeps the model near where it started.

The critic's baseline, the probability ratio, and what the clip does when one batch is used four times.

Changes vs #16
A probability ratio with a clip, so each batch serves several steps; a learned critic as the baseline; a KL penalty to a frozen reference
Its answers
Q2 a critic · Q3 the clip ε and the KL β · Q4 per token, through the critic
Cost
●●●●● four models: the policy, a critic, a frozen reference and the reward
Library
Ready: TRL's PPO trainer (newer releases keep it among the experimental trainers: check yours), OpenRLHF, verl
1

The idea in plain words

Three things limit REINFORCE. The tries are expensive and serve one step. A step that happens to be large can wreck the policy, and the next batch, sampled from the wrecked policy, often cannot undo it. And the group baseline of #16 needs several tries of the same task, while the reward arrives only at the end.

PPO answers each one. It keeps, for every token of every try, the probability it had when the try was written. Training then runs several passes over the same batch, and each token's push is measured with the ratio \(\rho = \pi_\theta / \pi_{\text{old}}\): how much more likely the token is now than when it was written. The clip stops that token's own push once the ratio leaves \([1-\varepsilon,\,1+\varepsilon]\), usually \([0.8,\,1.2]\), in the direction the advantage wants; it does not hold the token there, and one update can carry it past the band. A critic, a second network, learns to predict the reward still to come from any point of a try. The change in its prediction across a token, plus that token's reward, is the token's surprise, and GAE adds these surprises from the token onward, each weighted down by λ per token, into the token's own advantage. A KL penalty, subtracted from the reward token by token, charges for moving away from a frozen reference copy of the starting model.

This is the algorithm behind the original RLHF work on language models (Ziegler et al., 2019; Ouyang et al., 2022), with a reward model as the reward. With a checker (#17) in its place, the rest is unchanged.

2

See it work

The clip, one batch used many times, and the leash

Part 1 draws PPO's clipped objective: move the ratio and see where the push stops. Part 2 trains a tiny policy many times on the same batch, with and without the clip. Part 3 shows the best policy the KL leash allows, for any β.

3

Where the training signal comes from

Three decisions: what scores the tries, what the critic is, and where the KL leash goes.

4

A small example

The critic sets the baseline
Task · promptGet a foothold on the lab's login page and read the secret.
Critic · before the tryFrom here, this model usually succeeds 30% of the time: V = 0.30.
Try · sampledtest the login form for SQL injection … secret read
Checkerreward 1 · advantage 1 − 0.30 = +0.70

One try is enough for a baseline: the critic stands in for the other tries #16 needed.

The same batch, four passes

The successful try's first token, \(\pi_{\text{old}} = 0.30\), advantage +0.70, ε = 0.2.

pass π_θ ratio ρ clip(ρ) min(ρA, clip·A) push 1 0.300 1.00 1.00 0.70 yes 2 0.339 1.13 1.13 0.79 yes 3 0.372 1.24 1.20 0.84 no: clipped 4 ≈0.37 ≈1.24 1.20 0.84 no: clipped (held here for simplicity: the other tokens' updates still move it a little)

Once the token is 20% more likely than when it was written, this batch has no more say about it. Passes 3 and 4 still push the batch's other tokens: those inside the band, and any that moved the wrong way. (+0.70 is the whole-try advantage, before GAE and the KL; station 5 refines this token's to 0.60.)

A failed try in the same batch

A try that tried the default passwords, \(\pi_{\text{old}} = 0.50\), reward 0, advantage 0 − 0.30 = −0.30.

pass π_θ ratio ρ clip(ρ) min(ρA, clip·A) push 1 0.500 1.00 1.00 −0.30 down 2 0.440 0.88 0.88 −0.26 down 3 0.395 0.79 0.80 −0.24 no: clipped

For a negative advantage the brake is on the other side: the push down stops once the token has lost 20%.

5

What a training batch looks like

Token of the try\(\log\pi_{\text{old}}\)\(\log\pi_{\text{ref}}\)Critic \(V\)Reward \(r_t\)
t1 · test−1.20−1.220.30−0.01
t2 · the login form−0.40−0.440.35−0.02
t3 · for SQL injection−0.90−0.900.550.00
t4 · <end>−0.05−0.070.80−0.01 + 1

Four “tokens” to keep the table short (real tokens are word pieces). Every token's reward is the KL penalty \(-\beta(\log\pi_{\text{old}} - \log\pi_{\text{ref}})\) with β = 0.5 here; the last one also carries the checker's 1.

# The usual knobs and common starting values. The names are TRL's PPOConfig ones;
# check them against your version, since TRL has moved and renamed its PPO trainer.
PPOConfig(
    num_ppo_epochs=4,        # passes over each batch of tries
    cliprange=0.2,           # ε for the policy ratio
    cliprange_value=0.2,     # the same idea for the critic's predictions
    vf_coef=0.1,             # weight of the critic's loss
    kl_coef=0.05,            # β, the price of drifting from the reference
    gamma=1.0, lam=0.95,    # GAE: no discount inside a try, λ trades noise for bias
    learning_rate=1e-6,
)

The dataset is a task set, as in #16. Everything in the first tab is computed during the step, then thrown away.

Generalised advantage estimation (GAE) turns the per-token rewards and the critic's predictions into one advantage per token. γ = 1, λ = 0.95.

token\(\delta_t = r_t + V_{t+1} - V_t\)\(\hat A_t = \delta_t + \lambda\hat A_{t+1}\)critic's target \(\hat A_t + V_t\)
t40.99 + 0 − 0.80 = 0.190.190.99
t30.00 + 0.80 − 0.55 = 0.250.25 + 0.95 × 0.19 = 0.430.98
t2−0.02 + 0.55 − 0.35 = 0.180.18 + 0.95 × 0.43 = 0.590.94
t1−0.01 + 0.35 − 0.30 = 0.040.04 + 0.95 × 0.59 = 0.600.90

Computed from the last token back. The critic's prediction jumps most across t3, “for SQL injection”, and so δ is largest there: the critic thinks that is the token that made success likely. A REINFORCE try would give all four tokens the same number.

6

The loss

$$\htmlClass{term term-1}{\mathcal{L}_{\text{PPO}}(\theta)} = -\,\mathbb{E}_t\Big[\min\big(\htmlClass{term term-2}{\rho_t}\,\htmlClass{term term-4}{\hat A_t},\;\htmlClass{term term-3}{\mathrm{clip}(\rho_t,\,1-\varepsilon,\,1+\varepsilon)}\,\hat A_t\big)\Big] \;+\; \htmlClass{term term-5}{c_v\,\mathbb{E}_t\Big[\big(V_\phi(s_t) - \hat R_t\big)^2\Big]}$$
$$\htmlClass{term term-2}{\rho_t = \frac{\pi_\theta(a_t\mid s_t)}{\pi_{\text{old}}(a_t\mid s_t)}} \qquad \htmlClass{term term-6}{r_t = r(x,y)\,[t = T] \;-\; \beta\log\frac{\pi_{\text{old}}(a_t\mid s_t)}{\pi_{\text{ref}}(a_t\mid s_t)}} \qquad \htmlClass{term term-4}{\hat A_t = \sum_{l\ge 0}(\gamma\lambda)^l\,\delta_{t+l}}$$

Read it aloud: “For every token of every try, compare its probability now with its probability when the try was written. Multiply that ratio by the token's advantage, but never let the ratio count beyond 1 ± ε in the direction the advantage wants. Push that up. Separately, train the critic to predict the return. The reward of each token is the KL penalty, plus the checker's reward on the last token.” Point at a row of the table to light its term up.

one token \(\mathbb{E}_t\) averages over every token of every try in a minibatch: unlike #16, PPO's unit is the token, because the critic gives each token its own advantage. The same batch of tries is used for several passes, and \(\pi_{\text{old}}\) stays fixed at the values recorded when the tries were written.

Every symbol in the formula

SymbolWhat it means
States, actions, the four models
\(s_t\), \(a_t\)generatedThe state at token \(t\) (the task plus the try so far) and the token written there.
\(\pi_\theta\)trainedThe policy being trained, now, at this pass over the batch.
\(\pi_{\text{old}}\)frozenThe policy that wrote the batch: its token probabilities, computed once for the batch from the weights that generated it (trainers recompute them rather than trust the inference engine's numbers), do not change during the passes.
\(\pi_{\text{ref}}\)frozenThe reference: a copy of the starting model, frozen for the whole run.
\(V_\phi(s_t)\)trainedThe critic, with its own weights \(\phi\): its prediction of the reward still to come from state \(s_t\).
Rewards and advantages
\(r_t\)resultThe reward of token \(t\): the KL penalty on every token, plus the checker's \(r(x,y)\) on the last token \(T\). \([t = T]\) is 1 at the last token, 0 elsewhere.
\(\beta\)settingThe price of drift, often 0.01 to 0.1.
\(\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)\)resultThe surprise at token \(t\): what happened (its reward, plus the critic's view of the new state) minus what the critic expected before it.
\(\gamma,\ \lambda\)settingThe discount (1 inside a try) and the GAE mix (about 0.95): λ = 0 trusts the critic completely, λ = 1 ignores it except as a baseline.
\(\hat A_t\)resultThe token's advantage: a discounted sum of the surprises from \(t\) on. 0.60 for the first token in station 5.
\(\hat R_t = \hat A_t + V(s_t)\)resultThe critic's target, the λ-return: the rewards that followed, blended with the critic's own later predictions (the plain return only when λ = 1).
The ratio and the clip
\(\rho_t\)“rho”, the ratiotrainedHow many times more likely token \(t\) is now than when it was written. 1 at the first update (the first minibatch of the first pass).
\(\varepsilon\)“epsilon”settingHow far a token's own term keeps pushing it, per batch: usually 0.2. Shared weights and other tokens can still move it further.
\(\mathrm{clip}(\rho,\,1-\varepsilon,\,1+\varepsilon)\)mathThe ratio, held inside \([0.8, 1.2]\): below the band it becomes 0.8, above it 1.2.
\(\min(\cdot,\cdot)\)mathThe smaller of the two. It keeps the pessimistic one: the clip removes any gain from moving further, never a loss.
The loss
\(c_v\,\mathbb{E}_t\big[(V_\phi - \hat R_t)^2\big]\)resultThe critic's loss: squared error against the λ-return, weighted by \(c_v\) (often 0.1). It trains \(\phi\), not \(\theta\). Implementations such as TRL also clip the critic's move (cliprange_value) and take the larger of the clipped and unclipped errors.
\(\mathcal{L}_{\text{PPO}}\)resultThe clipped surrogate plus the critic's loss. At the first update, where \(\rho = 1\), its slope on the policy is exactly REINFORCE's, with \(\hat A_t\) as the advantage.

What you must supply: tasks, a reward (a checker or a reward model), a reference model (your starting model, frozen), a critic to initialise, and the settings: ε, β, γ, λ, the number of passes. The old probabilities, values and advantages are computed by the trainer.

How the loss works, step by step

  1. Write and record. Generate the tries, and store each token's \(\log\pi_{\text{old}}\) and \(\log\pi_{\text{ref}}\), and the critic's \(V\).
  2. Rewards per token. The KL penalty on every token, plus the checker's reward on the last.
  3. Advantages per token. GAE, from the last token back.
  4. Several passes. For each pass and each minibatch: the ratio of every token, the clipped surrogate, and the critic's squared error. Backward and update both models.
  5. Discard. After the last pass, throw the batch away and generate a new one with the updated policy.

Worked example: the cases of the clip, ε = 0.2

advantage ratio ρ ρA clip(ρ)·A min push on the token +0.70 1.10 +0.77 +0.77 +0.77 up: inside the band +0.70 1.30 +0.91 +0.84 +0.84 none: already 30% up, the gain is capped +0.70 0.70 +0.49 +0.56 +0.49 up: the good token fell, nothing is clipped on this side −0.30 0.90 −0.27 −0.27 −0.27 down: inside the band −0.30 0.70 −0.21 −0.24 −0.24 none: already 30% down −0.30 1.30 −0.39 −0.36 −0.39 down: the token rose although it was bad

The third and last rows are the asymmetry. A good token that became less likely is still pushed up, and a bad token that became more likely is still pushed down, however far they have gone: the min only ever removes the reward for moving further in the right direction. That is why PPO is called pessimistic.

7

One training step on the GPU

A PPO step has two phases. The first produces a batch and every number it needs. The second runs several passes of training over it. Four models take part; only two are trained.

1Generatethe policy writes the tries, no gradient
2Recordlog π_old, log π_ref and V for every token; the reward for every try
3Advantagesper-token rewards, then GAE from the end back
4PassesE passes × minibatches: ratio, clip, critic loss, backward, update
5Discardthe batch is now too old to trust

One pass, slope by slope

A minibatch of 4 tries, 40 answer tokens in all, on the third pass over the batch.

one minibatch, pass 3 of 4 → ONE update of the policy and ONE of the critic forward policy, with gradient: log π_θ at all 40 tokens → 40 ratios ρ_t critic, with gradient: 40 values V_φ(s_t) loss per token: the clipped surrogate, and (V_φ − R̂)² backward a token inside the band, or pushed back toward it: g_t = (ρ_t Â_t / 40)(p_t − y_t) a token clipped this pass, say 9 of the 40: g_t = 0 the critic: one slope per token, 2 c_v (V_φ − R̂) / 40 update the policy's weights move once; the critic's once
  • The ratio multiplies the push. At the first update ρ = 1, and the slope is REINFORCE's with the token's own advantage. Later passes weight each token's push by its ratio: a token made more likely is pushed harder, one made less likely more gently, until, if it moved the way its advantage wants, the clip stops it.
  • Clipped tokens drop out one by one. The clip acts per token, so a pass can still move a try whose first token is done.
  • The clip cannot pull back. A token that overshot the band within one update is not pulled back: its own term only stops pushing, and other tokens' updates can move it further still. That is why the learning rate still matters.
1Generation and recording

Generation is #16's. Then three forward passes without gradient: the policy (for \(\log\pi_{\text{old}}\), recomputed by the trainer rather than taken from the inference engine), the reference, and the critic. The reward model, if there is one, reads each finished try.

2Advantages

GAE is a short loop per try, from the last token back, on numbers already computed. Most implementations then whiten the advantages of the batch: subtract their mean and divide by their spread.

3The passes

E passes (4 is common), each over the batch in minibatches. Every minibatch is a full forward and backward pass of both trained models. The batch's fixed numbers (\(\log\pi_{\text{old}}\), \(\hat A\), \(\hat R\)) are read, never recomputed.

On a real GPU

  • Memory for four models: the policy and the critic with gradients and optimiser state, the reference and the reward model frozen. This is the main reason GRPO (#19) drops the critic.
  • Two numbers to watch: the clip fraction (the share of tokens clipped, usually a small share) and the approximate KL between \(\pi_\theta\) and \(\pi_{\text{old}}\). If either climbs, the passes are pushing too far.
8

The maths behind it

  1. Reusing old tries: importance sampling

    An average over what the current model would write can be computed from tries an older model wrote, by weighting each try with the ratio of the two probabilities:

    $$\mathbb{E}_{y\sim\pi_\theta}\big[A(y)\big] = \sum_y \pi_\theta(y)\,A(y) = \sum_y \pi_{\text{old}}(y)\,\frac{\pi_\theta(y)}{\pi_{\text{old}}(y)}\,A(y) = \mathbb{E}_{y\sim\pi_{\text{old}}}\big[\rho(y)\,A(y)\big]$$

    Its slope at the first pass, where \(\pi_\theta = \pi_{\text{old}}\), is \(\nabla\rho = \rho\,\nabla\log\pi_\theta = \nabla\log\pi_\theta\): exactly the policy gradient of #16. For a whole try's ratio the identity holds at any distance, but the ratios grow uneven and a few tries with large ratios dominate the estimate. PPO uses each token's own ratio instead of the product over the try, which keeps the weights small; the price is that the per-token objective matches the true one only near \(\pi_{\text{old}}\), because it ignores that the new policy would also reach different prefixes. That is the real reason to stay close.

  2. A trust region, cheaply

    The fix is to stay close to \(\pi_{\text{old}}\), where the old tries are still a fair sample. TRPO (Schulman et al., 2015) imposes it as a hard constraint on the KL divergence between the two policies, which needs second-order maths. PPO (Schulman et al., 2017) gets most of the effect with the clip and plain gradient descent.

  3. The min, case by case
    $$\min\big(\rho A,\;\mathrm{clip}(\rho,1-\varepsilon,1+\varepsilon)A\big) = \begin{cases} \min(\rho,\,1+\varepsilon)\,A & A > 0 \\ \max(\rho,\,1-\varepsilon)\,A & A \lt 0 \end{cases}$$

    For a good token the objective rises with ρ until \(1+\varepsilon\), then goes flat: no slope, no push. For a bad token it rises as ρ falls, until \(1-\varepsilon\), then goes flat. On the other side of 1 nothing is clipped, so a move in the wrong direction is always pushed back. Part 1 of the lab draws both curves.

  4. GAE: from a critic's predictions to per-token credit
    $$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) \qquad \hat A_t = \sum_{l\ge 0}(\gamma\lambda)^l\,\delta_{t+l}$$

    With λ = 0 the advantage is the one-token surprise \(\delta_t\): little noise, but only as right as the critic. With λ = 1 (and γ = 1) the sum telescopes to \(\sum_{l} r_{t+l} - V(s_t)\): the actual return minus the critic's baseline, with no bias and all the noise of a whole try. λ ≈ 0.95 sits between the two (Schulman et al., 2016).

    station 5's try, first token λ = 1 Â₁ = 0.04 + 0.18 + 0.25 + 0.19 = 0.66 = (sum of rewards 0.96) − V₁ 0.30 λ = 0.95 Â₁ = 0.60 λ = 0 Â₁ = δ₁ = 0.04 t1 barely changed the critic's prediction, so the credit goes to later tokens
  5. What the KL leash allows

    With the KL penalty, PPO maximises the reward minus β times the drift from the reference. That goal has a closed-form best policy, derived in the DPO maths:

    $$\pi^{\star}(y\mid x) = \frac{1}{Z(x)}\,\pi_{\text{ref}}(y\mid x)\,\exp\!\Big(\frac{r(x,y)}{\beta}\Big)$$
    three moves, π_ref = 0.50 / 0.30 / 0.20, only the second rewarded (r = 1) β = 1 π_ref × e^(r/β) = 0.50 / 0.82 / 0.20 normalised: π* = 0.33 / 0.54 / 0.13 β = 0.2 π_ref × e^(r/β) = 0.50 / 44.5 / 0.20 normalised: π* = 0.011 / 0.985 / 0.004 β → 0 π* puts everything on the rewarded move

    A large β keeps the model near its starting habits; a small one lets the reward take over. With a checker that is hard to fool, a small β is much less risky, though drift and lost variety remain. With a reward model that can, β is what stands between the model and the reward model's mistakes.

9

Watch out for

  • A critic that knows nothing yet

    A fresh critic gives arbitrary baselines, so the first advantages are noise. Initialise it from the reward model or the SFT model, and consider training it alone for a few batches first.

  • Too many passes

    Each pass over the same batch pushes it further from the policy that wrote it. Watch the clip fraction and the KL to \(\pi_{\text{old}}\); if they climb, use fewer passes or a smaller learning rate.

  • Reward model over-optimisation

    With a learned reward, its score keeps rising while real quality peaks and then falls (Gao et al., 2023). Check a held-out judge or people, and keep β high enough. With a checker this risk is the checker's gaps instead (#17).

  • Many moving parts

    Four models, two losses, GAE, whitening, two clips: PPO for language models has dozens of details that matter (Huang et al., 2024). Start from a tested implementation, not from scratch.

  • Tries cut off at the length limit

    A try that hits the length limit gets scored as if it had finished. Penalise or drop tries without an end token, or the model learns that rambling until the cut is safe.

  • Memory

    The critic is often about as large as the policy. If it does not fit, GRPO (#19) removes it, at the cost of several tries per task.

No critic · the group is the baseline

GRPO and its fixes Group relative policy optimisation · DAPO · Dr. GRPO · GSPO

Drop PPO's critic. Sample a group of G tries per task and measure each try against its group: its reward minus the group's mean, divided by the group's spread. Keep PPO's clip and move the KL leash into the loss. This is the standard recipe when a program can check the answer, and a handful of fixes (DAPO, Dr. GRPO, GSPO) make it steadier.

A group of four tries, its mean and spread, and the KL term that starts at zero.

Changes vs PPO
No critic: the group of tries is the baseline, scaled by its spread; the KL moves from the reward into the loss
Its answers
Q2 the group's mean and spread · Q3 the clip, plus a KL in the loss or none · Q4 the whole try: one advantage for all its tokens
Cost
●●●●● G tries per task; no critic to train or hold in memory
Library
Ready: TRL GRPOTrainer (its loss_type picks the GRPO, DAPO or Dr. GRPO normalisation), verl, OpenRLHF
1

The idea in plain words

PPO's critic exists to say “from here, this model usually gets this much”. GRPO (Shao et al., 2024) gets that number by asking the model: sample G tries of the same task, and their mean reward estimates what the model usually gets. Each try's advantage is its reward minus the mean, divided by the standard deviation of the group's rewards, so every group's advantages have a spread of 1, whether the lab is easy or hard.

The rest is PPO without the critic. The ratio and the clip stay. The KL penalty leaves the reward and becomes a term of the loss. With an outcome reward, every token of a try gets the try's advantage, since without a critic there is nothing to tell its tokens apart. GRPO trained DeepSeek-R1's reasoning (DeepSeek-AI, 2025) and is now the default for tasks with verifiable rewards.

Its wide use exposed four weak points, each with a fix. Groups where every try scores the same teach nothing (DAPO: drop them and sample more). Dividing each try by its own length favours long wrong answers (DAPO and Dr. GRPO: normalise differently). The spread inflates rare successes and rare failures (Dr. GRPO: don't divide by it). The symmetric clip slowly kills exploration (DAPO: raise the upper clip). GSPO moves the ratio and the clip to the level of whole tries.

2

See it work

Group advantages, silent groups, and the length bias

Part 1: click the tries of a group to set their rewards and compare the advantages under GRPO, Dr. GRPO and RLOO. Part 2: how many groups carry any signal at all, and what DAPO's dynamic sampling costs. Part 3: how each normalisation pushes a short correct try and a long wrong one.

3

Where the training signal comes from

Four decisions: the reward, how advantages are scaled, which groups train, and what holds the model back.

4

A small example

One group of four
Task · promptGet a foothold on the lab's login page and read the secret.
Try 1 · reward 0default passwords · locked out
Try 2 · reward 1SQL injection in user · secret read
Try 3 · reward 0default passwords · no luck
Try 4 · reward 0brute force · rate limited
Mean, spread, advantage
rewards 0 1 0 0 mean 0.25 std (÷ G−1) 0.50 GRPO Â −0.50 +1.50 −0.50 −0.50 Dr. GRPO Â −0.25 +0.75 −0.25 −0.25

The spread is computed as torch.std does by default, dividing by G − 1. Dividing by it doubles every advantage here; with a learning-rate change, the two rows train alike.

A harder lab, sixteen tries
rewards one 1, fifteen 0 mean 0.0625 std (÷ G−1) 0.25 GRPO Â success +3.75 · each failure −0.25 Dr. GRPO Â success +0.94 · each failure −0.06

Dividing by the spread multiplies the success's advantage by four here (0.94 → 3.75), against two in the four-try group (0.75 → 1.50). That is a choice to weight rare wins (and rare failures) up, not a law. Dr. GRPO argues it biases training toward tasks at the extremes and drops it.

5

What a training batch looks like

TaskRewards of the group (G = 4)Mean · stdAdvantagesTrains?
login lab0 · 1 · 0 · 00.25 · 0.50−0.5 · +1.5 · −0.5 · −0.5yes
file-share lab1 · 1 · 1 · 11.00 · 00 · 0 · 0 · 0no signal
deserialisation lab0 · 0 · 0 · 00.00 · 00 · 0 · 0 · 0no signal
route-exposure lab · F10.40 · 0.80 · 0.67 · 0.400.57 · 0.20−0.84 · +1.16 · +0.51 · −0.84yes

Half of this batch is silent: one lab looks solved and one looks out of reach, for now (four tries can only suggest it). DAPO's dynamic sampling would replace those two groups with fresh ones that mix successes and failures. A partial-credit reward (last row) rarely ties.

# Names from TRL's GRPOConfig; check them against your version.
GRPOConfig(
    num_generations=8,             # G, tries per task
    temperature=1.0,                # keep the tries varied
    max_completion_length=1024,
    beta=0.0,                       # the KL weight; 0.04 in the original GRPO
    epsilon=0.2,                    # the clip, lower side
    epsilon_high=0.28,              # DAPO's clip-higher; acts only with reuse or stale tries
    loss_type="dapo",                # or "grpo", "dr_grpo": how tokens are averaged
    mask_truncated_completions=True, # DAPO: tries cut at the limit don't train
    num_iterations=1,               # passes per batch; with 1 (and the default steps_per_generation) ρ = 1
)
# Dr. GRPO also skips the division by the std: see scale_rewards in your version.

Per token, the trainer needs the try's advantage, its length, and, if β > 0, the token's log-probability under the reference.

The login group\(\hat A_i\)tokens \(|o_i|\)weight per token, GRPO: \(\hat A_i/(G|o_i|)\)DAPO: \(\hat A_i/\sum_j|o_j|\)
try 1−0.5010−0.0125−0.0125
try 2+1.508+0.0469+0.0375
try 3−0.5012−0.0104−0.0125
try 4−0.5010−0.0125−0.0125

40 tokens in the group. Under GRPO each try's tokens share its push equally, so a longer try's tokens each get less. Under DAPO every token of the batch counts the same.

6

The loss

$$\htmlClass{term term-1}{\mathcal{L}_{\text{GRPO}}(\theta)} = -\,\frac{1}{G}\sum_{i=1}^{G}\htmlClass{term term-5}{\frac{1}{|o_i|}\sum_{t=1}^{|o_i|}}\Big[\min\big(\rho_{i,t}\,\htmlClass{term term-2}{\hat A_i},\;\htmlClass{term term-3}{\mathrm{clip}(\rho_{i,t},1-\varepsilon,1+\varepsilon)}\,\hat A_i\big) - \htmlClass{term term-4}{\beta\,\hat D_{i,t}}\Big]$$
$$\htmlClass{term term-2}{\hat A_i = \frac{r_i - \mathrm{mean}(r_1,\dots,r_G)}{\mathrm{std}(r_1,\dots,r_G)}} \qquad \htmlClass{term term-4}{\hat D_{i,t} = \frac{\pi_{\text{ref}}(o_{i,t}\mid\cdot)}{\pi_\theta(o_{i,t}\mid\cdot)} - \log\frac{\pi_{\text{ref}}(o_{i,t}\mid\cdot)}{\pi_\theta(o_{i,t}\mid\cdot)} - 1}$$

Read it aloud: “For each task, let the model write G tries. Give every try one advantage: its reward's distance from the group's mean, in units of the group's spread. Every token of the try gets that advantage, through PPO's clipped ratio. Subtract β times an estimate of how far each token has drifted from the reference. Average over each try's tokens, then over the group, and push it up.” Point at a row of the table to light its term up.

one group The formula is the loss of one task's group of G tries, written \(o_i\) here as in the GRPO paper. A batch averages several groups. With one pass per batch (TRL's default), \(\rho = 1\) and the clip never acts; it matters only when a batch serves several passes, or when tries were generated by a slightly older model.

Every symbol in the formula

SymbolWhat it means
The group
\(G\)settingTries per task, the group size (#16's K): 8 or 16 common, up to 64 (DeepSeekMath used 64).
\(o_i\), \(|o_i|\)generatedThe i-th try (an “output”) and its number of tokens.
\(o_{i,t}\mid\cdot\)generatedToken \(t\) of try \(i\), given the task and the try's earlier tokens.
\(r_i\)frozenThe reward of try \(i\), from the checker.
The advantage
\(\mathrm{mean}(r_1,\dots,r_G)\)resultThe group's average reward: GRPO's stand-in for PPO's critic. 0.25 in the example.
\(\mathrm{std}(r_1,\dots,r_G)\)resultThe spread of the group's rewards, its standard deviation: 0.50 in the example. A small number is added to avoid dividing by 0 when every try scored the same.
\(\hat A_i\)resultThe try's advantage, the same for all its tokens: +1.50 for the injection test, −0.50 for each of the others.
PPO's ratio and clip
\(\rho_{i,t}\)trained\(\pi_\theta(o_{i,t}\mid\cdot)/\pi_{\text{old}}(o_{i,t}\mid\cdot)\), as in #18.
\(\mathrm{clip}(\rho,1-\varepsilon,1+\varepsilon)\)mathAs in #18; DAPO uses a wider upper bound, \(1+\varepsilon_{\text{high}}\).
The leash and the averaging
\(\beta\,\hat D_{i,t}\)resultThe KL term: β times the “k3” estimate of the divergence from the reference at this token. It is never negative, and it is 0 (with a slope of 0) while the model still equals the reference.
\(\pi_{\text{ref}}\)frozenThe reference, the starting model. Not loaded at all when β = 0.
\(\frac{1}{|o_i|}\sum_t\)mathThe average over the try's own tokens: GRPO's length normalisation. DAPO replaces it with one division by the total tokens (of the group in the paper's formula, of the whole batch in implementations such as TRL's), Dr. GRPO with a division by a constant.
\(\mathcal{L}_{\text{GRPO}}\)resultThe loss of the group. A surrogate, like #16's: its value is not progress.

What you must supply: tasks, a reward function, G, and the clip; a reference model only if β > 0. No critic, no reward model if a checker exists.

How the loss works, step by step

  1. Group. Sample G tries of the task.
  2. Score and normalise. Rewards, then each try's distance from the mean in units of the spread.
  3. Spread over tokens. Every token of try \(i\) gets \(\hat A_i\), through the clipped ratio.
  4. Leash. If β > 0, subtract β times each token's k3 estimate.
  5. Average and step. Over each try's tokens, then over the group (or, in DAPO, over every token at once); backward; update.

Worked example: the length bias, a short right try against a long wrong one

group of 2: right try 200 tokens (Â = +0.71), wrong try 1000 tokens (Â = −0.71) push per token total push on the try right wrong right wrong GRPO, ÷ own length +0.00177 −0.00035 +0.35 −0.35 DAPO, ÷ all tokens +0.00059 −0.00059 +0.12 −0.59 Dr. GRPO, ÷ (2 × 2048) +0.00017 −0.00017 +0.03 −0.17 (Â held at ±0.71 in every row to isolate the denominator; Dr. GRPO, which skips the division by the spread, would use ±0.50)

Under GRPO each wrong token is pushed down five times more gently because its try is five times longer, so being wrong at length costs less per token than being wrong briefly, and over training wrong answers drift longer. DAPO and Dr. GRPO give every token the same weight, so a long wrong try is pushed down in proportion to its length.

7

One training step on the GPU

A GRPO step is #16's with a group per task, PPO's ratio, and a reference read if β > 0. With no critic, memory holds only the policy, and the reference if there is a KL term.

1GenerateB tasks × G tries; the inference engine samples a whole group at once
2Scorerewards, then each group's mean and spread
3Forwardthe policy with gradient; the reference frozen, only if β > 0
4Backwardeach unclipped token: (ρ Â_i − β(1 − π_ref/π_θ)) / (G|o_i|) times (p − y)
5Updateonce, or a few times per batch with the clip

One step, slope by slope

The login group of station 5: four tries of 10, 8, 12 and 10 tokens, one pass, at the start of training (\(\pi_\theta = \pi_{\text{old}} = \pi_{\text{ref}}\)).

one task, G = 4, 40 answer tokens → ONE training step score rewards 0, 1, 0, 0 → mean 0.25, std 0.50 → Â = −0.5, +1.5, −0.5, −0.5 forward policy: 40 log-probabilities, with gradient (reference: 40 more, frozen, if β > 0) backward k = ρ Â_i / (G |o_i|) − β (1 − π_ref/π_θ) / (G |o_i|) here ρ = 1 and π_ref = π_θ try 2, 8 tokens: k = +1.5 / 32 = +0.0469 raise try 1, 10 tokens: k = −0.5 / 40 = −0.0125 lower try 3, 12 tokens: k = −0.5 / 48 = −0.0104 lower try 4, 10 tokens: k = −0.5 / 40 = −0.0125 lower 40 slopes at once: g_t = k (p_t − y_t); the KL part is 0 until the model moves update once; the group is thrown away
  • The KL term starts silent. Its slope is \(\beta(1 - \pi_{\text{ref}}/\pi_\theta)\) per token: 0 while the model equals the reference, and growing, with the sign that pulls back, as the model drifts.
  • Silent groups still cost generation. In the batch of station 5, two of four groups produce slopes of 0 from the advantage. DAPO spends extra generation so that no group is silent.
1Group generation

Inference engines sample G continuations of one prompt sharing the prompt's computation, so a group costs less than G separate tries. Generation is still most of the step.

2Advantages

A few arithmetic operations per group. No network, no GAE, nothing to train: that is the critic's whole job, done by sampling.

3Training

#16's forward and backward, with a per-token weight. With more than one pass per batch, the old log-probabilities are recorded first and the ratio and clip work as in #18.

On a real GPU

  • Memory: the policy with gradients and optimiser state, plus the frozen reference if β > 0. No critic: roughly half of PPO's footprint.
  • Generation and training drift apart: the inference engine and the trainer compute slightly different probabilities for the same token. Trainers recompute the old log-probabilities themselves, so the ratio does not see this gap; it makes the tries slightly off-policy, which is handled by a separate importance weight that is truncated or masked when it strays too far (TRL: vllm_importance_sampling_correction; by default a per-try weight, with tries above 3 dropped) or by matching the engine's precision to the trainer's.
8

The maths behind it

  1. The group mean is a critic made of samples

    PPO's critic learns \(V(x) \approx \mathbb{E}_{y\sim\pi_\theta}[r(x,y)]\). GRPO estimates the same number by sampling, \(V(x) \approx \frac{1}{G}\sum_j r_j\), at the cost of G tries per task and with no network to train. It gives one baseline per task, not one per token: every token of a try shares the try's advantage, as in #16. For short answers that loses little; for long agent sessions it is the credit problem of #20.

  2. What dividing by the spread does

    Dividing by the std rescales each group so its advantages have a spread of 1. Every task with any signal then moves the model by a similar amount, whatever its difficulty. But groups with a small spread, nearly always solved or nearly always failed, are amplified most: the lone success in 16 gets +3.75. Liu et al. (2025, “Dr. GRPO”) show this, together with the length normalisation below, biases the update, and that removing both gives a cleaner policy gradient with no loss of performance.

  3. The length normalisation and its bias
    $$\text{per-token weight under GRPO} = \frac{\hat A_i}{G\,|o_i|} \qquad \text{under DAPO} = \frac{\hat A_i}{\sum_{j=1}^{G}|o_j|} \qquad \text{under Dr. GRPO} = \frac{\hat A_i}{G\,L_{\max}}$$

    The same \(\hat A_i\) is used in all three to compare the denominators; Dr. GRPO's own advantage is \(r_i - \text{mean}\), with no division by the spread.

    \(\sum_j |o_j|\)mathAll the tokens of the group, as in DAPO's formula; implementations divide by the tokens of the whole batch.
    \(L_{\max}\)settingA constant, such as the maximum length of a try.

    GRPO gives every try the same total push whatever its length, so for a negative advantage each extra token dilutes the penalty. DAPO and Dr. GRPO give every token the same weight. The worked example in station 6 puts numbers on it.

  4. The k3 estimate of the KL

    With \(u = \pi_{\text{ref}}(o_t)/\pi_\theta(o_t)\) for a token the model sampled, \(\hat D = u - \log u - 1\). Since \(u - \log u - 1 \ge 0\) for every \(u > 0\), it is never negative, unlike the plain \(-\log u\). And since \(\mathbb{E}_{o\sim\pi_\theta}[u] = 1\), its average is exactly the KL divergence of \(\pi_\theta\) from \(\pi_{\text{ref}}\) (Schulman, 2020). One subtlety: used as a loss term, with the gradient flowing only through \(\pi_\theta\) in \(u\), its expected gradient is that of the KL in the other direction, \(\mathrm{KL}(\pi_{\text{ref}}\,\|\,\pi_\theta)\). Both are 0 at the start and pull toward the reference; multiplying the term by the ratio ρ, as recent TRL does by default (use_bias_correction_kl, following DeepSeek-V3.2), gives the gradient of \(\mathrm{KL}(\pi_\theta\,\|\,\pi_{\text{ref}})\).

    u = π_ref / π_θ 0.5 0.8 1.0 1.25 2.0 D̂ = u − ln u − 1 0.19 0.02 0.00 0.03 0.31 slope pulls the token down down none up up
  5. DAPO's four fixes, and GSPO

    DAPO (Yu et al., 2025) changes four things and drops the KL term. Clip-higher: \(\varepsilon_{\text{high}} = 0.28 > \varepsilon_{\text{low}} = 0.2\), so a rare token that paid off can grow faster, which delays entropy collapse. Dynamic sampling: keep only groups with \(0 \lt \#\text{correct} \lt G\). Token-level loss: the division by all tokens above. Overlong shaping: tries cut at the length limit are masked, and a soft penalty grows as a try approaches the limit.

    GSPO (Zheng et al., 2025) moves the ratio from tokens to tries: \(s_i = \big(\pi_\theta(o_i)/\pi_{\text{old}}(o_i)\big)^{1/|o_i|}\), the geometric mean of the token ratios, clipped per try. Every token of a try now shares one weight, built from the average of its tokens' log-ratios, so single-token noise no longer accumulates over a long try, and the clip keeps or drops whole tries. This steadies training on long outputs and on mixture-of-experts models.

9

Watch out for

  • Entropy collapse

    As tries become identical, groups stop disagreeing and learning stalls on a single habit. Watch the entropy and the share of mixed groups; use clip-higher and a temperature near 1.

  • Silent groups

    All-correct and all-wrong groups waste their generation. A pass-rate curriculum, which keeps labs at the edge of the model's skill, avoids drawing them; dynamic sampling keeps them out of the batch, at the cost of more generation.

  • Inflated rare wins

    Dividing by a tiny spread turns one lucky success (or one unlucky failure on an easy task) into a huge advantage. If the reward can be lucky (a flaky lab), drop the division or put a floor under the spread.

  • Length drift

    Under per-try normalisation, wrong answers grow longer. Use a token-level loss, an overlong penalty, and watch the mean length of right and wrong tries separately.

  • No KL, no anchor

    β = 0 is common with a trustworthy checker. With a learned reward or a judge, keep a KL term: nothing else stops the model drifting into their blind spots.

  • Format rewards that win alone

    A small reward for a well-formed report is useful early. If it can be maxed out without doing the task, the model will settle for it: keep it small, and check that the checker's part keeps rising.

Sessions, not answers · who earned the reward?

Multi-turn agent RL Agentic RL · turn-level credit · reward shaping

An agent's try is a whole session: action, tool output, action, tool output, until the secret is read or the budget runs out. Run a group of sessions per lab, grade only the agent's turns, mask the tool outputs, and score the outcome. Then decide how one final reward is shared among the turns: equally, by comparing turns that faced the same situation, or with a progress reward built so that it cannot be farmed.

Four toy sessions: actions graded, observations masked, and two ways of sharing one reward among the turns.

Changes vs GRPO
A try is a whole session; tool outputs are masked; the reward is shared among the turns, by session or by turn
Its answers
Q4 per turn: same-state groups, or progress shaping · the rest as #19
Cost
●●●●● each try takes minutes of real tool calls; many labs and sandboxes in parallel
Library
Session-level credit: recent TRL's GRPOTrainer runs multi-turn tool loops (tools, environment_factory) and masks tool results; turn-level credit (GiGPO): verl-agent. Also SkyRL, rLLM, OpenRLHF's agent mode
1

The idea in plain words

Everything so far treated a try as one answer. An agent such as RedAmon's works in turns: it decides, a tool runs, it reads the output, it decides again. For RL, a whole session is one try. The environment, a fresh lab with its tools, answers each action, and a checker scores the end. The session is stored as one long token sequence in which the agent's tokens are graded and the tool outputs are masked, exactly as in the trajectory pairs of #15, but now online and with a group of sessions per lab, as in GRPO.

The new problem is credit. One reward at the end of thirty turns says the session worked. It does not say which turns did the work. With session-level credit every turn gets the session's advantage, so a smart move inside a failed session is pushed down and a wasted one inside a lucky session is pushed up. Two ideas cut the credit finer. Same-state groups: when several sessions of the group reach the same situation, compare what each one did next (GiGPO, Feng et al., 2025). Progress shaping: add a small reward for real progress, built as the change in a progress score so that going round in circles earns exactly nothing (Ng, Harada and Russell, 1999).

The rest is engineering. Sessions take minutes and run real tools, so rollouts run in parallel against many labs while the GPU trains. Contexts grow with every observation. And there is no person at the approval gate during training, so the labs must be isolated and the reward must charge for what the rules forbid.

2

See it work

One reward, many turns

Part 1: four sessions on the same lab, one success. Switch how credit is shared and see which turns are pushed up, which down, and which are never graded. Part 2: an agent that can farm a naive “new information” bonus by repeating scans, against a progress reward that cannot be farmed.

3

Where the training signal comes from

Four decisions: the environment, the outcome reward, how credit reaches the turns, and the budget.

4

A small example

Four sessions on the login lab
task · the same for all four
Get a foothold on the lab's login page and read the secret.
session 1 · reward 1
probe the login page → test SQL injection → read the secret
session 2 · reward 0
probe the login page → brute-force the password → locked out
session 3 · reward 0
scan every port → brute-force SSH → rate limited
session 4 · reward 0
scan every port → test SQL injection → budget ran out

Between every two actions, a tool output: read, never graded.

Session-level credit
mean reward 0.25 every turn gets R − 0.25 (R, the session's reward) turn 1 turn 2 session 1 probe +0.75 SQLi +0.75 session 2 probe −0.25 brute −0.25 session 3 scan −0.25 brute −0.25 session 4 scan −0.25 SQLi −0.25

Session 2's probe was the same good first move as session 1's, and it is pushed down. Session 4's injection test was the right idea, and it is pushed down too.

Same-state credit for turn 2
turn 1: all four start from the same state → as before turn 2, after the probe (sessions 1, 2): returns 1, 0 → mean 0.5 turn 2, after the scan (sessions 3, 4): returns 0, 0 → mean 0 turn 2 session 1 SQLi +0.50 session 2 brute −0.50 the real mistake, blamed most session 3 brute 0.00 session 4 SQLi 0.00 no longer blamed for the scan

Turn 2 is now judged against what other sessions did from the same point. GiGPO adds this turn-level advantage to the session-level one with a weight. Turn 1 starts from the state all four share, so (with γ = 1) its turn-level part equals the session part, and GiGPO's sum multiplies it by 1 + ω.

5

What a training batch looks like

SegmentWritten byTokensLoss maskAdvantage on its tokens
system prompt, rules of engagement, taskthe harness1,8000–
probe the login pagethe agent401+0.75 (+ turn share)
HTTP response: a form with user, passthe environment6000–
test the user field for SQL injectionthe agent551+0.75 (+ turn share)
tool output: a database errorthe environment4000–
read the secret through the injection, then reportthe agent2201+0.75 (+ turn share)

315 graded tokens out of 3,115. Most of a session is text the agent only reads. That is what the mask is for.

# one rollout, as a multi-turn trainer stores it. Only "assistant" turns are trained.
{"lab": "login-sqli-01", "run": "g07-s1", "reward": 1.0,
 "messages": [
   {"role": "system",    "content": "…rules of engagement, scope: the lab only…"},
   {"role": "user",      "content": "Get a foothold on the lab's login page and read the secret."},
   {"role": "assistant", "content": "…", "tool_calls": [{"name": "http_probe", "arguments": "{…}"}]},
   {"role": "tool",      "content": "200 OK … <form> user, pass …"},
   {"role": "assistant", "content": "…", "tool_calls": [{"name": "sqli_test", "arguments": "{…}"}]},
   {"role": "tool",      "content": "… database error …"},
   … the third assistant turn reads the secret through the injection and writes the report …
 ],
 "turn_states": ["h:3f1a", "h:9c02", "h:51be"]}   # fingerprints of the state before each turn

The state fingerprints let the trainer find sessions of the group that reached the same situation. Hashing the normalised environment state (the page or endpoint reached, plus a hash of the normalised tool output) works; RedAmon's agent already fingerprints each tool call and its output in a similar way, to spot duplicates.

Per agent turn: the session's advantage, the turn's same-state advantage, and their sum, which every token of the turn receives.

The example groupsession \(\hat A^E\)turn 2, same-state \(\hat A^S\)turn 2 total, \(\omega = 1\)
session 1 · SQLi after probe+0.75+0.50+1.25
session 2 · brute after probe−0.25−0.50−0.75
session 3 · brute after scan−0.250.00−0.25
session 4 · SQLi after scan−0.250.00−0.25

Without the division by the spread, and with γ = 1, to keep the numbers readable; GiGPO divides both parts by the group's spread as GRPO does, or by 1 (a variant its paper also reports), and discounts the turn-level returns. Turn 1, from the state all four share, gets 0.75 + 0.75 = 1.50 for session 1 and −0.50 for the others.

6

The loss

$$\htmlClass{term term-1}{\mathcal{L}_{\text{agent}}(\theta)} = -\,\frac{1}{G}\sum_{i=1}^{G}\frac{1}{|a_i|}\sum_{k}\sum_{t\,\in\,\text{turn }k}\htmlClass{term term-2}{m_t}\,\min\big(\rho_t\,\hat A_{i,k},\;\mathrm{clip}(\rho_t,1-\varepsilon,1+\varepsilon)\,\hat A_{i,k}\big)$$
$$\hat A_{i,k} = \htmlClass{term term-3}{\hat A^{E}_{i}} + \omega\,\htmlClass{term term-4}{\hat A^{S}_{i,k}} \qquad \htmlClass{term term-5}{r'_k = r_k + \gamma\,\Phi(s_{k+1}) - \Phi(s_k)}$$

Read it aloud: “For each lab, run G sessions. Every token the agent wrote gets an advantage: the session's, compared with the other sessions, plus a share for its turn, compared with turns that started from the same state. Tokens the environment wrote get a mask of 0. Optionally, add to each turn's reward the change in a progress score; it reaches single turns through the turn-level returns. The rest is GRPO.” Point at a row of the table to light its term up.

one group of sessions The formula is the loss of one lab's group of G sessions. A session \(i\) is one token sequence; \(k\) numbers its agent turns and \(t\) the tokens inside a turn. Averaging is over the agent's tokens only, \(|a_i|\) of them, not over the observations.

Every symbol in the formula

SymbolWhat it means
Sessions and turns
\(G\)settingSessions per lab, the group: 4 to 8 for agents, since each one is expensive.
\(k\)mathThe agent turn number within a session.
\(s_k\)generatedThe state before turn \(k\): what the environment looks like at that point. GiGPO matches states by the environment's state (the same page, the same tool output), so the same state can recur across sessions and within one.
\(m_t\)mathThe loss mask: 1 on tokens the agent wrote, 0 on the system prompt, the task and every tool output.
\(|a_i|\)mathThe number of agent tokens in session \(i\): the graded ones.
Credit
\(\hat A^{E}_i\)resultThe session-level (episode) advantage: the session's reward against the group, as in #19.
\(\hat A^{S}_{i,k}\)resultThe turn-level advantage: the return from turn \(k\), against the mean return of all the group's turns, this one included, that started from the same state \(s_k\). 0 when no other turn reached that state.
\(\omega\)settingHow much the turn-level part counts, often 1.
\(\hat A_{i,k}\)resultThe advantage every token of turn \(k\) receives.
Optional: progress shaping
\(\Phi(s)\)“phi”, the potentialfrozenA progress score of a state, computed from what the environment showed: 0 at the start, 0.2 once the service is found, 0.5 once the injection is confirmed, 1 once the secret is read.
\(r'_k\)resultThe shaped reward of turn \(k\): its own reward plus the change in progress the turn caused (with γ = 1, a turn that changes nothing earns 0). It reaches a turn's advantage only through turn-level returns, \(G_k = \sum_{j\ge k}\gamma^{\,j-k} r'_j\), as in same-state groups or with a critic; a single session-level advantage sees only the session's total.
As in #18 and #19
\(\rho_t\)trainedThe ratio, as in #18. It matters more here, because slow sessions are often a few updates old when they are trained on.
\(\varepsilon\)settingThe clip, as in #18.
\(\mathcal{L}_{\text{agent}}\)resultThe loss of one group of sessions.

What you must supply: labs and their checker, the agent's real tools in a sandbox, a session budget, a way to fingerprint states (for same-state groups) or a progress score Φ computed from the environment (for shaping), and the GRPO settings.

How the loss works, step by step

  1. Run G sessions per lab, each against a fresh lab, recording every turn, every tool output and a fingerprint of each state.
  2. Score the outcome with the checker.
  3. Session advantages, as in #19.
  4. Turn advantages: group the turns by state fingerprint; within each group with two or more members, compare returns. Add them with weight ω.
  5. Mask and train: every agent token gets its turn's advantage, every observation token gets mask 0; then GRPO's step.

Worked example: one session, its tokens and their pushes

session 1 of the example group: 3 agent turns, 315 graded tokens, 2,800 read-only tokens turn 1 probe 40 tokens  = +0.75 + 0.75 = +1.50 (all four start from the same state: its same-state group is the whole group) turn 2 SQLi 55 tokens  = +0.75 + 0.50 = +1.25 turn 3 read, report 220 tokens  = +0.75 + 0 (no other session got this far) every one of those 315 tokens: g_t = Â_turn / (G × 315) × (p_t − y_t) the 2,800 others: g_t = 0
7

One training step on the GPU

An agent step spends most of its time outside the GPU. Generation and tool calls alternate, many sessions are in flight at once, and the trainer consumes finished groups as they arrive.

1Start labsG fresh labs per task, one sandbox each
2Roll outgenerate a turn → run the tool → append the output → repeat, until done or out of budget
3Scorethe checker on each finished session
4Creditsession and turn advantages; masks
5Trainre-read every session with gradient; GRPO's update

Where the time goes

one group: 4 sessions × ~10 turns, each turn ~150 tokens of agent text + ~2,000 tokens of tool output generation ~40 agent turns, each a generation call over a growing context seconds each tools ~40 tool calls in the sandboxes seconds to minutes each scoring 4 checker runs seconds training 4 sequences of ~20,000 tokens, ~6,000 graded in all one forward + backward the GPU that trains is idle unless other groups' rollouts overlap with it
  • Asynchronous rollouts: generation servers and tool sandboxes run many sessions while the trainer updates on finished ones. The sessions are then a few updates old: the ratio, computed against the log-probabilities of the weights that generated them, and the clip keep that safe for small lags; larger lags need bounded staleness or an extra correction.
  • Long contexts: every observation is appended to the history. Truncate or summarise long tool outputs, and use prefix caching so each turn does not re-read the whole session from scratch.
1The rollout loop

For each live session: generate the next agent turn from the current history, parse its tool call, run the tool in the session's sandbox, append the output, and check the budget. A session ends when the agent finishes, the checker's success condition is met, or the budget runs out.

2Masks and states

While the session runs, the harness records which tokens the agent wrote and a fingerprint of the state before each turn. The trainer needs both, and they are much harder to reconstruct later from the text alone.

3Training

GRPO's step on long sequences. Memory is dominated by the activations of long contexts: few sessions per GPU, gradient accumulation, and recomputing activations instead of storing them.

On a real cluster

  • Isolation first: every sandbox can reach its own lab and nothing else. Training runs real attack tools at scale, unattended.
  • The same scaffold as production: the system prompt, the tool definitions and the parsers used in training must be the ones the agent will run with, or the habits it learns will not transfer.
8

The maths behind it

  1. Return: the reward from here on
    $$G_k = \sum_{j\ge k}\gamma^{\,j-k}\,r_j$$
    \(G_k\)resultThe return from turn \(k\): every reward still to come. (Not the group size \(G\).)
    \(\gamma\)settingThe discount, between 0 and 1: how much a reward later counts less than one now. Often 1 for the session's outcome; GiGPO uses 0.95 for its turn-level returns, so of two visits to the same state, the one closer to success scores higher.

    With one reward at the end and γ = 1, every turn's return is the session's reward. That is session-level credit, and the policy gradient with it is unbiased: on average over many sessions, good turns are pushed up more than bad ones.

  2. Why one reward gets noisier with every turn

    A turn's advantage under session-level credit is \(R - b\), and \(R\) depends on every other turn's luck as well as on this turn. With 30 turns, one turn's push carries the randomness of 29 others. The average is right; any single session says little about any single turn. The noise grows with the length of the session, and so does the number of sessions needed.

  3. Same-state groups are a baseline per state
    $$\hat A^{S}_{i,k} = G_{i,k} - \frac{1}{|\mathcal{S}(s_{i,k})|}\sum_{(j,l)\in\mathcal{S}(s_{i,k})} G_{j,l}$$
    \(\mathcal{S}(s)\)resultEvery turn in the group, from any session and at any point in it, that started from state \(s\), the turn itself included.
    \(G_{j,l}\)resultThe return of that turn.

    A leave-one-out version, the mean over the other turns from that state (counting only turns from other sessions: a revisit within the same session depends on the action), depends only on the state and not on the action taken there: it changes nothing on average and removes the luck of everything that happened before the state. The plain mean, as here and in GiGPO, includes the turn itself and shrinks each push by \((n-1)/n\) for a state reached \(n\) times: half for a pair, nothing at all for a state only one turn reached (as #16 showed for the group mean). It needs states that repeat, which is why GiGPO matches the environment's state rather than the whole history. It keeps the session-level part as a stable, whole-session signal; the turn-level part adds local detail where states repeat.

  4. Shaping that cannot be farmed

    Add \(F_k = \gamma\Phi(s_{k+1}) - \Phi(s_k)\) to each turn's reward. Over a whole session the discounted additions telescope:

    $$\sum_{k=0}^{T-1} \gamma^{k} F_k = \gamma^{T}\Phi(s_T) - \Phi(s_0) \qquad (\gamma = 1:\ \ \Phi(s_{\text{end}}) - \Phi(s_{\text{start}}))$$

    With γ = 1, the total depends only on where the session started and ended, never on the path (with γ < 1, also on when it ended). With γ = 1, a loop back to an earlier state earns exactly 0, and so does repeating a scan that changes nothing (with γ < 1, slightly less than 0). A naive bonus, such as +0.1 every time a tool output is judged to contain new information, does not telescope, and a long enough loop of slightly different scans beats solving the lab. Ng, Harada and Russell (1999) showed that potential-based shaping leaves the best policy unchanged. Strictly, that needs Φ = 0 at the end of every session; keeping the progress at the end, as here, also makes the reward prefer failures that got further. That is then a different objective, a partial credit that could trade some success rate for milestones: keep Φ small next to the outcome, or set it to 0 at the end for strict invariance.

    One more caveat: shaping only changes which turns get the credit if each turn is scored by its own return from there on (same-state groups, or a critic as in PPO). Under session-level credit every turn shares one advantage, and shaping only adds \(\Phi(s_{\text{end}}) - \Phi(s_{\text{start}})\) to the session's score; with \(\Phi = 0\) at the end it changes nothing at all.

    a session that loops n scans and never exploits, against one that solves the lab in 3 turns (under the naive bonus the solve earns 1 + 0.1 × 2: two of its tool outputs hold new information) n = 5 n = 15 n = 30 solves the lab outcome only 0.0 0.0 0.0 1.0 naive +0.1 per new info 0.5 1.5 3.0 1.2 ← farming wins from n = 13 potential-based 0.2 0.2 0.2 2.0 ← loops earn nothing
  5. The echo trap

    Multi-turn RL can collapse in a way single answers rarely do. The RAGEN study (Wang et al., 2025) describes an echo trap: the agent settles on one repeated pattern of reasoning, the rewards in a group stop varying, and training becomes unstable. Their remedies (StarPO-S): train only on the tasks whose rewards vary most, add a critic (PPO, #18, was steadier than GRPO on two of their four environments, GRPO on one), drop the KL term and raise the upper clip, and watch the reward spread, the entropy and gradient spikes, not only the reward.

9

Watch out for

  • Grading the tool outputs

    If observation tokens are not masked, the model is trained to write tool outputs itself: it learns to make them up. Check the masks on a real session before the first run.

  • Prompt injection that pays

    Tool outputs come from the target and can contain instructions. If following one ever earns reward, RL strengthens the habit. Keep the agent's boundary for untrusted output, and include labs whose planted instructions lead to a cost.

  • No one at the approval gate

    In production a person approves dangerous tools; in training there is no one. Run rollouts only in isolated lab networks, scope any automatic approval to the lab, charge for forbidden techniques in the reward, and keep the production gate as it is.

  • Sessions that never end

    Without a turn and token budget, a stuck agent burns the batch. Timeouts count as failures; long outputs are truncated before they reach the context.

  • Progress the agent can claim

    A progress score computed from the agent's own words (“I confirmed the injection”) is farmed like any self-report. Compute Φ from what the environment returned.

  • A training scaffold that differs from production

    Train with the same prompts, tools and parsers the agent runs with, or the learned habits do not transfer. Evaluate on labs the agent never trained on.

Putting the family together

How they stack

These are not five alternatives. #16, #18 and #19 are three trainers, from the lightest (#16, with GRPO close behind: neither has a critic) to the heaviest (#18), of which you run one; #17 is the reward they train on wherever a checker exists; #20 is what one of them becomes when a try is a whole session. A real run picks a trainer, builds the reward, and decides how credit is shared.

1

Choose by what you have

  1. Learning how RL works, or a small model and a simple checker#16 REINFORCE, RLOO
  2. Tasks with ground truth, such as labs: build and test the checker before anything else#17 Verifiable rewards
  3. A learned reward model, a need for per-token credit, and memory for four models#18 PPO
  4. A checker and several tries per task: the practical default#19 GRPO
  5. An agent that works in turns with real tools#20 Multi-turn agent RL
2

A typical pipeline for a pentest agent

  1. SFT on good sessions, so the model already uses the tools and the report formatA SFT family
  2. Optional: a preference stage on pairs of sessions, a success against a failure#15 Trajectory pairs
  3. Build labs with ground truth and a planted secret per run; test the checker on honest, sprayed and dishonest reports#17 Verifiable rewards
  4. GRPO on single decisions first: choose the next tool, write the payload, judge a tool's output#19 GRPO
  5. Then whole sessions, in isolated labs, with GiGPO-style turn-level credit and a turn budget#20 Multi-turn
  6. Evaluate on labs never trained on: success rate, cost per success, scope violations, and regressions on general skills

Starting with single decisions is cheaper by orders of magnitude, and it shows quickly whether the checker pays for the right things. Whole sessions come once it does.

3

The family at a glance

#TechniqueBaselineHow far a step may goCredit unitExtra modelsCost
16REINFORCE, RLOOThe other tries' averageA small learning rateThe whole tryNone (a reference if a KL penalty is used, as in TRL's default)●●●
17Verifiable rewardsThe reward the trainers use wherever a checker exists: a program on ground truthNone: a checker●
18PPOA learned criticClip + KL in the rewardThe token, through GAECritic, reference, reward model or checker●●●●
19GRPO and its fixesThe group's mean and spreadClip + KL in the loss, or the clip aloneThe whole tryReference, if β > 0●●●
20Multi-turn agent RLThe group, and same-state groupsClip, and GRPO's KL in the loss if β > 0The session, or the turnLabs and sandboxes; reference if β > 0●●●●●
4

The ceiling, and what comes next

Everything on this page needs a reward computed while training runs. Where a checker exists, as on a lab, that is cheap and exact, and RL can take a model past every example it was ever shown. Where none exists (the quality of a report, a judgement call, anything without ground truth), the reward has to be learned, and the model will learn that reward's mistakes along with its knowledge.

The next families pick up from there. Reward and value models turn comparisons and labels into scorers, for whole answers and for single steps. Search can act as a teacher: explore a tree of actions, as RedAmon's exploit-path search (LATS) does, then train on the best branches. Safe RL makes the costs explicit, offline RL learns from recorded sessions, and bandits handle the single decisions a recon pipeline makes thousands of times.