The scripts are in the repository, not in the installed package. Clone it,
then
cd recipes/papers/adaptive-clip before running the commands below. Browse this recipe on GitHub.Recipe
- Base:
Qwen/Qwen2.5-1.5B-Instruct. Data: GSM8K, 512 train prompts from the train split, 120 held out from the test split (different splits, so there is no overlap to check for). - Reward, both arms: the binary outcome,
MathEqualagainst the GSM8K gold number. A program, not a judge. The paper changes the clip, not the reward, so nothing here is shaped. - Baseline arm: GRPO with
epsilon0.20 andepsilon_high0.28, fixed for every rollout. That pair is DAPO’s clip-higher [4] and it is the paper’s own token-level default. - Recipe arm: same 0.20 floor and the same 0.28 ceiling, but the upper bound slides per group,
eps_hi(c) = eps_lo + (eps_hi_max - eps_lo) * (k - c) / (k - 1)withccorrect out ofk = 8rollouts. One right out of eight keeps the full 0.28; seven right gets 0.2114. - Eval: pass@1 on the same 120 held-out tasks, 4 samples per task. The untrained base is evaluated three times first, and that spread is the noise floor a delta has to clear; the train set is decontaminated against the holdout before any training. Paired delta with a 95% interval (
wai.pass_at,wai.delta_report).
beta 0), which leaves the clip as the only trust region in the run — the thing the paper is about. The recipe runs the paper’s token-level importance sampling, not its sequence-level GSPO variant, so its Seq-IS epsilons (3e-3 / 5e-3) do not apply here.
Run
Result
Run today, both arms, on one L40S, at the settings of round 2. Round 1 ran the clip where it cannot bind, so it could not have tested the paper. See Climb.
Recipe vs baseline: +0.050 [0.000, 0.100] over 120 paired tasks.
Verdict: flat. The interval reaches zero, so this is not a gain you can
bank. Read it as: at this size, sliding the bound still cannot be shown to do
anything.
Read the Climb table before you read this one. The previous run of this
exact configuration reported -0.065 [-0.117, -0.013] — the recipe 6.5 points
worse, with an interval that excluded zero. Today the same code, the same
data and the same knobs returned +0.050. The delta moved 11.5 points and
changed sign between two runs of the same experiment.
The seed was not held on one arm, and the run pages say which. TRL 0.19.1
builds the LoRA adapter before it applies
GRPOConfig.seed, and the baseline
arm runs the three base evals first, which advances the RNG before its
adapter is drawn. The recipe arm, whose adapter is drawn from a fresh state, is
bit-identical between rounds 2 and 3 through step 3 and finished 0.508 then
0.517; the baseline arm differs from step 1 and went 0.573 then 0.467. So 10.6
of the 11.5 points is one arm’s LoRA init plus generation nondeterminism, and
0.8 is the other arm’s. recipe.py now calls set_seed(17) right before
building the trainer; the next verify run is the first with both arms on one
init.
That is the finding. The eval’s own noise floor is 0.024 (Checks table), a
three-run sample sd at p = 0.34, where the binomial floor over 480 samples is
about 0.023 per arm; even on that honest floor the swing is about 2.5 sigma,
and the two rounds’ paired intervals do not overlap (z about 3.1). What moved
is the training, not the measurement. Two arms at one training seed cannot separate
a five-point clip effect from run-to-run training variance, and this recipe now
has the direct evidence rather than the suspicion.
What survives both runs is that GRPO itself worked: the base goes from 0.34 to
somewhere in 0.47-0.57 in 40 steps, whichever arm you look at, and that is much
larger than anything the clip schedule has been shown to do here.
The selftest also turned up one thing worth knowing before you read the paper’s
equation 11 literally: at c = 0 it returns 0.2914, above the 0.28 ceiling it
is supposed to stop at. The equation is written for a group that splits,
1 <= c <= k. All-wrong groups have a zero advantage and contribute no
gradient, so the recipe clamps the count into [1, k] and they land on the
ceiling instead of over it.
Checks
Nothing in this table is ticked by hand: every cell is written byrecipe.py
into results.json. These are today’s numbers.
The two arms share the data, the holdout, the reward and every trainer knob
except the clip bound. In rounds 1 to 3 they did not share the LoRA init (see
above); from the next verify run they do.
Climb
Round 1 could not have tested the paper, and the trainer’s own logs say so.
TRL takes one policy update per batch of rollouts by default. On that update
the sampling policy and the trained policy are the same, so the importance
ratio is exactly 1: “the policy ratio starts at 1 for the
first gradient step for that batch” [3]. A ratio of 1 never reaches a bound of
1.20 or 1.28, so
epsilon_high is dead weight and a per-group
epsilon_high is dead weight per group. clip_ratio/high_mean was 0.0 in all
40 logged steps of round 1. The -0.013 it reported was generation
nondeterminism between two arms running the same arithmetic.
Round 2 takes the usual “1-4 gradient steps per batch” [3] at 2, which is the
smallest change that lets the second update be off-policy. The clip does then
fire. How hard it fires is itself unstable between runs: round 2 logged
clip_ratio/high_mean between 0 and 0.0003, round 3 logged a mean of 0.0021
and a peak of 0.0125 on the same settings (these are from the trainer’s console
log; the run pages do not carry the clip_ratio series, so they cannot be
checked there yet). Both rounds clip on exactly 20 of 40 logged steps, which is
the setup working as intended — with two updates per batch the first is
on-policy and cannot clip, the second can.
Round 3 changed nothing on purpose. The rule for this directory is to
re-run the recipe as written and check the number before climbing off it, and
that is what caught the problem: the delta moved 11.5 points and changed sign.
So the next knob is not a knob. Before eps_hi_max or more updates per batch
is worth a GPU minute, this recipe needs several training seeds per arm and a
delta reported across them: the swing between two identical runs (0.115) is
about 4.8 times the eval band (0.024), and the clip effect it is trying to
measure is a few points.
Learned
- The bound can be made per-group without touching TRL’s loss body.
epsilon_highis read inside_compute_loss, andtorch.clamptakes tensor bounds, so handing it a (batch, 1) tensor broadcasts over the (batch, tokens) ratio and theclip_ratiometric TRL logs stays correct. - Reading the group’s correct count off the sign of the advantage only works because this reward is binary: with rewards in {0, 1} the advantage is
r - c/k, positive for exactly the correct rollouts. A shaped reward would break that and need the counts carried separately. - Holding the ceiling equal across the arms is the honest comparison but it is also the conservative one: it makes the recipe a strictly tighter clip than the baseline. The paper compares against a fixed bound too, on a much bigger batch (256 prompts against 6 here), so a flat result could mean the batch rather than the idea.
- Check that your change is reachable before you spend a GPU hour on it. The per-group bound was implemented correctly, tested on the CPU against real TRL, and verified to arrive at the loss intact — and still could not move a gradient, because nothing in an on-policy run ever asks what the upper bound is. One line of the trainer’s own logging (
clip_ratio/high_mean) would have said so before the run. It is now in the Checks a reader can see. - GRPO was the intervention that mattered here. Both arms moved the base from 0.34 to 0.47-0.57 on GSM8K in 40 steps, which dwarfs everything the paper’s change could have done at this scale. A third of groups were flat on average today (
frac_reward_zero_stdmean 0.30, peaking at 0.67), so most of that came from the rollouts that split. - A two-arm delta at one training seed per arm is not a measurement, and this is what that looks like. Re-running this recipe unchanged moved the delta from -0.065 [-0.117, -0.013] to +0.050 [0.000, 0.100]: 11.5 points, sign flipped, and the first run’s interval excluded zero on the wrong side. Nothing in the recipe changed; the differences were whileai 0.53 -> 0.64, ordinary nondeterminism in generation and kernel scheduling, and, on the baseline arm only, an unseeded LoRA init (TRL builds the adapter before it seeds; fixed in
recipe.pyafter this round). The eval-noise floor this directory enforces (0.024 here) is the noise of re-running the eval on a fixed model, and it says nothing about the noise of re-running the training. Where a recipe’s whole claim is a delta between two trained models, that second source is the one that decides whether there is a result, and it needs several training seeds per arm to see at all. Read every one-seed delta in this directory — including the ones with tight intervals — with that in mind.
References
- Schulman, J. et al. Proximal Policy Optimization Algorithms. arXiv:1707.06347, 2017.
- Shao, Z. et al. DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models. arXiv:2402.03300, 2024.
- Lambert, N. Reinforcement Learning from Human Feedback. arXiv:2504.12501, 2025. Chapter Reinforcement Learning.
- Yu, Q. et al. DAPO: An Open-Source LLM Reinforcement Learning System at Scale. arXiv:2503.14476, 2025.
- Lambert, N. Reinforcement Learning from Human Feedback. arXiv:2504.12501, 2025. Chapter Evaluation.
- Lambert, N. et al. Tülu 3: Pushing Frontiers in Open Language Model Post-Training. arXiv:2411.15124, 2024.
- Gao, L., Schulman, J., Hilton, J. Scaling Laws for Reward Model Overoptimization. ICML 2023. arXiv:2210.10760.