The scripts are in the repository, not in the installed package. Clone it,
then
cd recipes/papers/bpco-bounded-critic before running the commands below. Browse this recipe on GitHub.wai.BPCO().update(batch), the paper’s bundle; the baseline arm’s is the standard PPO critic recipe [3] [4] (baseline_update in recipe.py, verl’s defaults [5]). The paper’s own comparison is the bundle against that recipe, so “the change” here is the bundle, five parts at once: (1) the head’s raw output bounded to (0, 1) through bound(z) instead of used raw; (2) the critic target is the Monte Carlo return instead of the lambda-return at 0.95 with a clipped value loss; (3) the policy advantage is unnormalized GAE with lambda = 1 - 1/(0.4 L) instead of GAE 0.95 whitened over the batch; (4) the ratio is clipped to 1 -/+ 0.2/mu (DPPO [6]) instead of 1 -/+ 0.2 (PPO); (5) the critic trains alone for 15 steps before the policy moves, instead of not at all.
Recipe
- Base:
Qwen/Qwen2.5-1.5B-Instruct. Data: GSM8K, 512 train prompts from the train split, 120 held out from the test split,decontaminaterun against the holdout and the dropped count recorded. Reward, both arms: the binary outcome,wai.verify.MathEqual()against the GSM8K gold number. A program, not a judge. - The regime, both arms: one rollout per prompt, 64 prompts per optimizer step, 256 new tokens at most, temperature 1.0 with top-p 1.0, top-k off and no repetition penalty (Qwen’s generation config sets all three; a truncated sampler has no common support with the policy). The rollouts are drawn by a stale sampler: a frozen copy of the policy’s LoRA weights, refreshed only every
LAG = 4optimizer steps, so a batch was written by a policy 1 to 4 updates old (the first block, and every warm-up step, is on-policy: there is no older policy to copy). Each trajectory carriesbehavior_logprobsfrom the sampler (a teacher-forced pass under its weights, right after generation, the way verl takes its old log-probs) andlogprobsfrom the current policy at update time (one teacher-forced pass), plusreward. Only generated tokens are actions. - The critic, both arms: one linear layer on the trunk’s last hidden state at the position that predicted each generated token (the same forward pass, hidden states detached, so the critic never trains the trunk), its own AdamW at 1e-4. The paper’s 1e-5 (
BPCO_CRITIC_LEARNING_RATE) is for a full separate value model; a 1,537-parameter head on a frozen feature needs more, and 1e-4 is the override. Both arms’ heads share the same init (set_seed(17)right before the adapter and the head are built). - Baseline arm: the head’s raw output is the value; the critic trains toward lambda-returns (GAE lambda 0.95, gamma 1) with verl’s clipped value loss, clip 0.5; the advantages are whitened over the batch; the ratio against the rollout policy is clipped to
[0.8, 1.2]; no warm-up. Written out inbaseline_update:coefficients[i][t]isrho Awhere the unclipped branch ofmin(rho A, clip(rho) A)is the minimum and 0 where the clipped branch is, which is that surrogate’s exact gradient at one step per batch (the selftest checks it against autograd). - Recipe arm: the head’s raw output goes through
wai.BPCO().bound(z), done in torch with the same formula so the gradient flows (the selftest checks it againstbound()on 2,401 points), and trains towardupdate.value_targets, the Monte Carlo return, with MSE;critic_warmup= 15 critic-only steps first, read off the object; then the policy step with-(coef * logprob).sum() / N, the coefficients fromwai.BPCO().update(batch)held constant (the1/Nis the batch mean theUpdatecontract leaves to the trainer; the library’s own toy trainer applies the same). The baseline uses the same loss line with its own coefficients, so the two arms differ in who wrote the coefficients and the value targets, and in nothing else. - Training, both arms: LoRA rank 16, alpha 32 (
TRAINING_LORA_RANK,TRAINING_LORA_ALPHA) on the seven projections, AdamW at 1e-5 on the adapter (the paper’s 1e-6,BPCO_LEARNING_RATE, is full weights), gradient clip 1.0, one step per batch, 40 policy steps after any warm-up, seed 17 set right before the trainer is built. Logged per step: mean reward, clipped token share, mean ratio, mean advantage, explained variance against the Monte Carlo return in both arms (equation 10 of the paper), completion length; the series are inresults.jsonunder each arm’scurve. - Eval: pass@1 on the same 120 held-out tasks, 4 samples per task at temperature 0.7 (same top-p, top-k and penalty settings as training). The untrained base is evaluated three times first, and that spread is the noise floor a delta has to clear. Paired delta with a 95% interval (
wai.pass_at,wai.comparewithrun_std,run_std_runs,train_runsandproxy=naming the training reward).hack_scanon the last training batch of each arm.
Run
whileai for its dependencies and mounts the checkout’s own whileai/ ahead of it on PYTHONPATH, because wai.BPCO is newer than the published wheel. Harmless once a release carries it. WHILEAI_API_KEY is optional; without it there is no run page and nothing else changes. The loop is plain torch and PEFT, no TRL: a single-rollout critic method with a stale sampler is not a trainer TRL ships.
Result
Run 2026-09-22, both arms, one L40S each, at the settings above (round 2 in the Climb table; round 1 was the same run and lost its paired delta to a local slip after the GPU work).
Recipe vs baseline: -0.017 [-0.069, +0.037] over 120 paired tasks.
Verdict: unresolved. One training seed per arm; a second seed on each arm,
passed as
train_runs=, would resolve it to moved or flat. The interval
covers zero on both sides and the delta is inside the eval’s own re-run band
(0.026), so at this size the bundle cannot be shown to do anything to pass@1
either way. Round 1, the same configuration, read baseline 0.44 [0.37, 0.51]
and recipe 0.43 [0.36, 0.49]: every arm within the band of its counterpart
across the two rounds.
The result is flat, and the curves say why. At 1e-5 on a rank-16 adapter,
40 single-rollout steps move the policy so little that the ratio against a
sampler four updates stale never left 1 -/+ 0.0014. The baseline’s PPO clip
fired on 0.17% of tokens (mean over the run; peak 0.28%); the recipe’s DPPO
range, 0.2 / mu half-width, at least 0.2 wide and far wider on a rare token,
fired on 0.001%. Nothing in this run was ever in the regime where a clip
matters, so parts (4) and (5) of the bundle could not have moved a gradient
and the comparison is really parts (1) to (3): the bounded critic and its
target and advantage, against the unbounded one.
Those parts show in one number each. The unbounded head opened at explained
variance -15.3: its raw outputs sat far outside [0, 1], and 40 steps of
clipped value loss brought it to -4.9, still worse than predicting the batch
mean. The bounded head opened at -0.36, could not be worse than the range
allows, and closed at -0.24 after 55 steps: its loss fell (0.37 to 0.31) but
it never beat the batch mean either. Bounding buys a critic that is wrong by at
most the range. It does not buy a critic that is right, and a critic that is
not right hands the policy an advantage that is mostly noise around the
reward in both arms. The recipe’s unnormalized advantage averaged +0.027 per
token; the baseline’s, whitened, 0 by construction.
Length moved more than anything else. Training rollouts went from 187 to
144 tokens in the baseline and from 194 to 104 in the recipe; eval answers
from 729 characters (base) to 592 (baseline) and 284 (recipe). The recipe
arm’s pass@4 rose (0.60 to 0.73) while its pass^4 fell (0.18 to 0.12): its
samples got more varied, not more right. The reward did not follow the
shortening in either arm (first to last quarter of policy steps: 0.44 to 0.49
baseline, 0.46 to 0.50 recipe, both inside the batch-to-batch spread).
Checks
Nothing in this table is ticked by hand: every cell is written byrecipe.py into results.json.
The two arms share the data, the holdout, the reward, the sampler, the
adapter and head init, and every trainer knob except the update rule. Seed
17 held: step 1 of each arm was bit-identical between rounds 1 and 2, and the
recipe arm stayed identical through its warm-up and five policy steps before
generation nondeterminism parted them.
Climb
Round 1 spent 50 GPU minutes and produced two numbers a reader can see and
no paired interval, because one local line ran for the first time after the
GPU work. That line is now exercised offline:
assemble() takes the arms’
returns and --selftest feeds it fake ones, so the next slip in that path
costs seconds. The curves of the two rounds agree to the step where
nondeterminism parts them (baseline clipped share 0.17% both rounds; recipe
explained variance -0.36 to -0.27 then -0.36 to -0.24; lengths 187 -> 135 and
194 -> 113 in round 1 against 187 -> 144 and 194 -> 104 in round 2).
Learned
- The regime the paper is about was not reached, and the clip counters say so. With the adapter at 1e-5 the policy moved so little in 40 single-rollout steps that a sampler four updates stale was still within
1 -/+ 0.0014in ratio; PPO’s clip touched 0.17% of tokens, DPPO’s 0.001%. Two of the bundle’s five parts (the clip and the warm-up before the policy moves) cannot change a gradient in that regime, so what this run compares is the bounded critic and its target and advantage against the unbounded one, and on pass@1 that comparison is flat (-0.017 [-0.069, +0.037]). A learning rate the adapter can move at (1e-4, where adaptive-clip’s GRPO went from 0.34 to 0.47 in the same 40 steps) is the knob that would put the run where the paper’s claim lives; it is a deliberate deviation from the shared single-rollout protocol and belongs in a round of its own. - Bounding the critic made it wrong by less, not right. The unbounded head’s explained variance started at -15 (its raw outputs were far outside the reward range, which is the paper’s complaint in one number) and was still -4.9 after 40 steps of clipped value loss; the bounded head started at -0.36 and ended at -0.24. Neither beat the batch mean, so neither arm’s advantage carried much beyond the reward itself. A linear head on a frozen feature at 1e-4, 64 sequences a step, is a weaker critic than the paper’s separate value model, and the 15-step warm-up moved its loss from 0.37 to 0.32 without moving its explained variance. What would test the critic’s part of the claim is a critic that learns: a longer warm-up, or a small trainable projection under the head.
- Length is what moved, in both arms, with the reward flat. Rollouts shortened by a quarter in the baseline and by nearly half in the recipe, eval answers from 729 to 592 and 284 characters, and pass@4 rose while pass^4 fell in the recipe arm. With a critic near 0.5 and a binary reward, every token of a wrong answer carries a negative residual and a short answer carries fewer of them; the unnormalized LA-GAE advantage does not subtract that off the way whitening does, which is one reading of why the recipe arm shortened faster. That is a hypothesis this run cannot separate from the learning rate, and a second seed per arm (
train_runs=) comes before any of it: at one seed the verdict is unresolved and stays so.
wai.BPCO, plain torch 2.7.1 + PEFT 0.16.0 + transformers 4.54.0, no TRL. 51.4 GPU minutes, $1.71 on one L40S, two containers in parallel (baseline 25.1 minutes with the three base re-runs, recipe 26.3; round 1 about the same, its wall clock lost with its delta). Run page: none, no WHILEAI_API_KEY in this run; Modal apps ap-q7nevqT6teeeDdUFFY6Gym (round 1) and ap-p3GjfFWYK6355Fg5OKP6HT (round 2), adapters and per-step curves under whileai-recipe-runs:/bpco-bounded-critic-<arm>-2026-09-21/.
Artifacts on Hugging Face
The root of the model repo is the recipe arm the Result table reports; the baseline arm is the
baseline/ subfolder. Load either with PeftModel.from_pretrained(base, repo, subfolder=...). Part of the Papers, replicated collection in the while-ai org.
References
- Lambert, N. Reinforcement Learning from Human Feedback. arXiv:2504.12501, 2025. Chapter Policy Gradient Algorithms.
- Qi, P., Zhou, X., Lee, W. S. Best Practice Critic Optimization. arXiv:2608.23566, 2026.
- Schulman, J. et al. Proximal Policy Optimization Algorithms. arXiv:1707.06347, 2017.
- Schulman, J. et al. High-Dimensional Continuous Control Using Generalized Advantage Estimation. arXiv:1506.02438, 2016.
- Sheng, G. et al. HybridFlow: A Flexible and Efficient RLHF Framework. arXiv:2409.19256, 2024.
- Qi, P. et al. Rethinking the Trust Region in LLM Reinforcement Learning. arXiv:2602.04879, 2026.
- 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.