Part III · RL for LLMs · Chapter 20
RL systems for LLMs
Why rollouts dominate cost, async and off-policy training, train–inference mismatch, and the open-source stacks.
In Chapter 15 you wrote GRPO in about forty lines of PyTorch, and it trained a tiny model on your laptop in three minutes. Now someone hands you 64 GPUs and a 7-billion-parameter model and asks for the same thing. You launch it and watch the GPU monitor. Most of the time, most of the GPUs are doing nothing. They are waiting for one response, out of hundreds, that is still writing its 30,000th token.
You fix that by letting generation run ahead of training. The run gets three times faster. Then, a few hundred steps in, the reward collapses for no visible reason. The cause, it turns out, is that the program that generated the tokens and the program that trained on them computed slightly different probabilities for the same tokens with the same weights, and your “on-policy” algorithm had been quietly off-policy all along.
Both surprises are what this chapter is about. At scale, RL for LLMs is as much a systems problem as an algorithms problem, and the systems choices leak into the math: they decide how stale your data is, which importance ratios you need, and which samples you silently throw away. You will see where the time and memory go, why generation dominates, how synchronous and asynchronous designs trade speed for staleness, and how the community found and fixed the train–inference mismatch in 2025. Along the way, ideas from Part II come back: off-policy learning (Chapter 5), importance sampling and trust regions (Chapter 9), and control variates (Chapter 7).
One RL step, seen by the hardware
Here is one step of GRPO or PPO as the hardware sees it. Nothing in it is new. What is new is that each line runs on different software, often on different machines.
repeat:
1. GENERATE sample G responses for each of B prompts with the current policy (inference engine)
2. SCORE reward each response: verifier, unit tests in a sandbox, or a reward model (reward workers)
3. PREPARE log-probs of every sampled token under the old policy and the reference;
values from the critic (PPO only); advantages (trainer, forward passes)
4. TRAIN a few gradient steps on the clipped objective (trainer)
5. SYNC copy the new weights into the inference engine (network or memory copy)
Each stage has its own specialized software:
| Component | Job | Typical software |
|---|---|---|
| Inference engine | Generates tokens fast: batching, KV-cache management, fused kernels | vLLM, SGLang |
| Trainer | Forward and backward passes, optimizer, sharding the model across GPUs | PyTorch FSDP, DeepSpeed, Megatron-LM |
| Reward workers | Run verifiers, execute code in sandboxes, call reward models or LLM judges | CPU pools, containers, GPU servers |
| Controller | Moves prompts, responses, rewards and weights between the others | Ray, custom schedulers |
Why not use one program for everything? Because generating text and training on text are different workloads, and the code that is fastest for one is slow for the other. The next section shows why.
Why generation dominates
Training is parallel, generation is not
When the trainer computes for a 2,000-token response, it already knows all 2,000 tokens. One forward pass handles them all at once (this is teacher forcing). The GPU multiplies large matrices, which is what it is built for.
Generation can’t do that. Token 1,001 depends on token 1,000, which the model has not written yet. So the engine runs 2,000 forward passes in a row, each producing a single token per sequence. Each pass must read every weight of the model from GPU memory, and then does only a little arithmetic with each one.
A worked example: the memory-bandwidth wall
Take a 7-billion-parameter model in bf16 (2 bytes per parameter), so 14 GB of weights, on an NVIDIA H100 SXM. The H100 can read memory at about 3.35 TB/s and do about 989 trillion bf16 operations per second (dense).
One decode step at batch size 1. Reading 14 GB at 3.35 TB/s takes
So one sequence gets at most about 240 tokens per second, no matter how fast the arithmetic units are. And the arithmetic needed is small: a forward pass costs about 2 operations per parameter per token, operations, which the H100 could do in 0.014 ms. The arithmetic units sit idle about 99.7% of the time. Decode at batch size 1 is memory-bandwidth-bound.
The fix is batching. If 64 sequences decode together, the weights are read once per step and used 64 times. The step still takes about 4.2 ms, but now produces 64 tokens. The step only becomes limited by arithmetic when the batch is large enough that the math takes as long as the memory read:
So you want hundreds of sequences in flight at once. What stops you is memory, and specifically the KV cache.
The KV cache
During decode, attention at step needs the keys and values of all earlier tokens. Recomputing them every step would be wasteful, so the engine stores them: the KV cache. Its size per token is
where the 2 counts one key and one value. For Qwen2.5-7B (28 layers, 4 KV heads of dimension 128, bf16) that is bytes, 56 KiB per token. That sounds small until you multiply:
| Tokens | KV cache | |
|---|---|---|
| One 16k-token reasoning response | 16,384 | 0.94 GB |
| 64 such responses | 1,048,576 | 60 GB |
The weights of that model take about 15 GB (Qwen2.5-7B actually has 7.6 billion parameters, not exactly 7). Weights plus 60 GB of KV cache almost fill an 80 GB H100. So at this response length, memory caps the batch at about 64, well short of the 300 you wanted. And the KV cache also has to be read every decode step. Near the end of those 16k-token responses, each step reads 15 GB of weights plus 60 GB of cache, about 22 ms, giving roughly tokens per second. At 1,000 tokens of context the same GPU manages about 11,000. Long chains of thought make every token slower, not just more numerous.
The long tail
Response lengths in reasoning RL are wildly uneven. Most responses to a math problem might be 2,000 tokens, while a few run to the 32,000-token limit. In a synchronous step, the step ends when the last response ends. As the batch drains, fewer and fewer sequences are left in flight, and since a decode step costs about the same whether it serves 64 sequences or 3, the GPU spends the tail of the step doing a few percent of its possible work.
Here is how bad it gets. Suppose lengths are exponentially distributed with mean and you run sequences in parallel, one per slot, then wait for all of them. The expected longest is , where (Exercise 20.4 derives this). The slots are busy for an average of each out of , so the utilization is
For , : the generation hardware does useful work about 21% of the time. And real length distributions have heavier tails than the exponential.
Put the pieces together: generating a token is slower than training on it, long contexts slow it further, and the tail wastes most of the rest. Systems papers on rollout efficiency report generation taking the bulk of each RL step; for example, the APRIL paper (Zhou et al. 2025) states that rollout generation accounts for more than 90% of total runtime in their setting. The exact share depends on model, lengths and hardware, but the conclusion doesn’t: in RL for LLMs, you optimize the generation stage first.
Quick check An inference engine is decoding 8 sequences of a 7B model, each with a short context. You double the batch to 16 sequences. Roughly what happens to the time per decode step?
At small batch sizes decode is memory-bandwidth-bound: each step reads all 14 GB of weights once and does little arithmetic with them. Doubling the batch adds arithmetic the GPU had idle capacity for, so the step time barely changes and the token throughput nearly doubles. This holds until the batch reaches a few hundred, or until KV-cache memory runs out.
Memory accounting: where the bytes go
Before deciding how to place things on GPUs, you need to know how big they are. The rule of thumb comes from the ZeRO paper (Rajbhandari et al. 2019).
16 bytes per trained parameter
Training with mixed-precision Adam keeps five numbers per parameter:
| What | Precision | Bytes |
|---|---|---|
| Weights (used in forward/backward) | bf16 | 2 |
| Gradients | bf16 | 2 |
| Master copy of the weights (for accurate small updates) | fp32 | 4 |
| Adam first moment | fp32 | 4 |
| Adam second moment | fp32 | 4 |
| Total | 16 |
A frozen model that only runs forward passes needs just its weights: 2 bytes per parameter in bf16. These numbers exclude activations (the intermediate values saved for the backward pass, which grow with batch size and sequence length) and the KV cache. Real runs need noticeably more than this table; treat it as a floor.
PPO vs GRPO for a 7B policy
Now count the models. PPO-based RLHF (Chapter 12) has four: the policy (trained), the critic (trained, usually the same size), the reference (frozen) and the reward model (frozen). GRPO with a verifiable reward (Chapter 15) has no critic and no reward model; many 2025 recipes also drop the KL term, and with it the reference model.
| Setup (7B policy) | Policy | Critic | Reference | Reward model | Total |
|---|---|---|---|---|---|
| PPO-RLHF, all models 7B | 112 GB | 112 GB | 14 GB | 14 GB | 252 GB |
| GRPO + KL, verifier reward | 112 GB | 14 GB | 126 GB | ||
| GRPO, , verifier | 112 GB | 112 GB | |||
| GRPO + LoRA adapter (about 40M params) | 14 GB + 0.64 GB | 0 | ≈ 15 GB |
Three things to notice. First, the critic is the single biggest saving when you go from PPO to GRPO: it costs as much as the policy because it is trained too. This is the memory half of Chapter 15’s “no critic” argument. Second, none of the full fine-tuning rows fits on one 80 GB GPU even before activations and KV cache. Third, with LoRA the reference model costs nothing: the reference is the base model, and the base model is already in memory. Switch the adapter off and you have .
How do the 252 GB fit at all? By sharding: ZeRO stage 3 or PyTorch’s FSDP splits the weights, gradients and optimizer states of each model evenly across GPUs and gathers each layer’s weights just in time for its computation. With 8 GPUs, the PPO setup above needs about GB of states per GPU, leaving the rest for activations. Very large models add tensor parallelism (splitting individual matrices across GPUs) and pipeline parallelism (splitting layers), which is what Megatron-LM provides.
Quick check You switch from PPO-RLHF to GRPO with a rule-based verifier and β = 0 (no KL term). Which models no longer need GPU memory?
GRPO replaces the critic with the group mean; a verifier is a program, not a network; and with β = 0 there is no KL term, so no reference log-probs are needed. Only the policy remains (plus its copy in the inference engine).
Placement: colocated or disaggregated
You have a fixed set of GPUs, and two very different jobs. There are two basic ways to share the GPUs between them.
Colocated (also called a hybrid engine; DeepSpeed-Chat and verl popularized it): every GPU does both jobs, in turns. During generation the GPUs run the inference engine; then the engine releases its KV-cache memory, the same GPUs switch to training, and afterwards the new weights are resharded back into the engine’s layout. verl’s paper (Sheng et al. 2024, “HybridFlow”) calls its version the 3D-HybridEngine.
Disaggregated: some GPUs only generate, others only train. After each training step, new weights travel over the network to the generation GPUs.
How expensive is moving the weights? For a 7B model in bf16, 14 GB. Over a 400 Gb/s (50 GB/s) network link that is about
tiny next to a generation phase that takes minutes. For a 1-trillion-parameter model (2 TB in bf16) one link would need 40 seconds, so frontier systems shard the transfer across many links at once. Weight sync is usually not the bottleneck, but it is not free.
| Colocated | Disaggregated | |
|---|---|---|
| GPU idle time | None between phases: every GPU is always doing something | Generation GPUs idle while the trainer works and vice versa, unless you go asynchronous |
| Memory | Both jobs must fit on the same GPUs (in turns) | Each pool only holds what its job needs |
| Balancing | Automatic | You must choose how many GPUs generate vs train |
| Asynchrony | Hard: the GPUs can’t generate and train at the same time | Natural: the two pools run concurrently |
| Typical fit | Short or medium responses, single-turn math and code, small clusters | Long reasoning, multi-turn agents with slow tools, large clusters |
Production systems support both. The GLM-4.5 report, for instance, describes its slime framework running colocated and synchronous for math and code RL, and disaggregated and asynchronous for long agent rollouts. The choice comes down to the question of the next section: are you willing to train on slightly stale data?
Synchronous vs asynchronous RL
The synchronous bubble
In synchronous RL every sample is generated by the current policy, then the policy is updated, then the next batch is generated. It is exactly on-policy, the setting in which REINFORCE, PPO and GRPO were derived. It is also wasteful in two ways. Generation hardware waits for the slowest response (the long tail), and in a disaggregated setup, generation and training hardware also wait for each other.
Let generation run ahead
The fix is to let the generators keep going while the trainer works. A response started while the policy was at version (after training steps) is tagged . If the trainer uses it in the step that updates version to , its lag (or staleness) is . Lag 0 is on-policy. Lag 2 means the sample came from the policy of two updates ago.
To keep lags under control, systems bound how far generation may run ahead. AReaL (Fu et al. 2025) uses a simple admission rule: with batch size and a maximum staleness , a new generation may start only while
where is the trainer’s current version. With you may start one batch, then must wait for the update: synchronous RL. With the generators may also start the next batch with the current weights while the trainer works on this one: the popular one-step off-policy design.
Here is a hand-sized example (it is also a test in the lab). Two generation lanes, batches of , two training steps, each taking 1 time unit. Responses are numbered in the order they start, r0, r1, r2, …, and take 1, 3, 1, 1, 1, … time units.
- Synchronous (). r0 (length 1) and r1 (length 3) start at . The batch is complete at , and the trainer runs step s0 from 3 to 4. Only then may r2 and r3 start; they run from 4 to 5, and step s1 runs from 5 to 6. Total: 6 units. The lanes worked of their lane-units: 50% utilization.
- Asynchronous (, so up to responses may start at version 0). Lane A finishes r0 at and immediately starts r2 (tagged version 0). r2 finishes at , so the trainer has two responses (r0, r2) and runs s0 from 2 to 3. Meanwhile lane A starts r3, the fourth and last response allowed at version 0. At , r1 and r3 finish and the trainer, now at version 1, runs s1 on them from 3 to 4. Also at , the admission limit has risen to , so both lanes start r4 and r5 for the next step. The lanes are never idle. Total: 4 units, 100% lane utilization, and step s1 trained on two samples with lag 1.
Drawn as a timeline (each character is a quarter of a time unit, dots are idle time):
Synchronous, k = 0
time 0 1 2 3 4 5 6
lane A [r0]............[r2]....
lane B [----r1----]....[r3]....
trainer ............[s0]....[s1]
Asynchronous, k = 1
time 0 1 2 3 4
lane A [r0][r2][r3][r4]
lane B [----r1----][r5]
trainer ........[s0][s1]
The demo below runs the same simulation at a larger scale.
Block color = the policy version that generated the response (lanes) or that a training step updates (trainer). A response whose color matches the training step that uses it is on-policy. (Five colors repeat, so versions 0 and 5 look alike.)still generatingdropped as too stale
What to try: (1) With the defaults (k = 0, σ = 0.8) the lanes are busy only about a quarter of the time: watch them sit idle, waiting for the longest response and then for the trainer. (2) Set k = 1. Throughput jumps about 3×, and colors start to mix: lanes now train on responses from an older version. (3) Raise k further: speed saturates once the lanes are always busy, but the lag keeps whatever it reached. (4) Set train time to 3: now the trainer is the bottleneck, and raising k buys only staleness. (5) Tick 'drop samples older than k' and look at which blocks turn red.
The lower panel shows the real trade-off: every step of buys throughput and costs lag, and past the point where the slowest component is saturated, extra buys nothing. Two more things the demo shows that are easy to miss:
- The admission rule bounds how far generation runs ahead, not how old every sample is. A very long response started early can finish many versions later, so the maximum lag can exceed (with the defaults and , the demo’s max lag is 5). Systems handle these stragglers by dropping them (GLM-5 drops samples whose generating weights lag by more than a threshold), by interrupting them, or by accepting them with importance weights.
- Dropping stale samples drops the long ones. Short responses finish fast and are trained on quickly; the samples that age past are the long ones. If long responses are disproportionately the hard, careful, correct ones (or the rambling, wrong ones), dropping them biases training toward one kind of behaviour. The first-in, first-out queue has the same flavour of bias: early batches over-represent short responses. Exercise 20.9 explores this.
Quick check In the demo, you set train time = 3 (the trainer is the bottleneck) and raise k from 2 to 6. What happens?
When the trainer is the slowest component, finished responses pile up in its queue. Allowing generation to run further ahead only makes the queue longer, so each sample waits longer and is older (staler) when used. Asynchrony only pays when generation is the bottleneck.
Correcting for stale samples
A sample with lag was drawn from , but the gradient we want is for the current . This is exactly the off-policy problem of Chapter 5: a behaviour policy collected the data and we want to improve a different target policy. And Chapter 9 already gave the tool, importance sampling:
PPO’s ratio is this correction applied per token, with playing the role of . So staleness is handled if really is the policy that sampled the token. That is the practical rule: record the log-probability of every token at the moment it is sampled, and use that as the behaviour log-prob. If the trainer instead recomputes “old” log-probs with its current weights, the ratio corrects for the wrong distribution.
There is a subtlety. PPO’s clip keeps close to , which is a trust region. When is several versions behind, the clip keeps pulling the new policy back toward an outdated one. AReaL’s decoupled PPO objective separates the two roles: the behaviour policy (which sampled the data) gets an importance weight, and a recent proximal policy (in practice, the weights just before the current update) anchors the clip:
How much staleness is tolerable? Noukhovitch et al. (2024, “Asynchronous RLHF”) studied exactly this. They found online DPO the most robust of the algorithms they tried to off-policy data, with robustness growing with model size, and trained an instruction-following chatbot from Llama 3.1 8B about 40% faster than a synchronous run with matching final performance. AReaL reports up to 2.77× end-to-end speedups over synchronous systems on math and code reasoning. In practice the bounds are small: AReaL uses a maximum staleness of 4 for coding and 8 for math, and ScaleRL (below) uses a maximum off-policyness of 8.
Two more tricks for the long tail
Partial rollouts. Kimi k1.5 (Kimi Team 2025) caps the work per iteration: a response that has not finished by the end of an iteration is paused and resumed in a later one. A single response then contains segments generated by different policy versions.
In-flight weight updates. PipelineRL (Piché et al. 2025) goes further: the inference engine receives new weights in the middle of generating a sequence and continues with them, keeping its KV cache. The authors report about 2× faster learning on 128 H100s while keeping the data close to on-policy. Olmo 3 and Magistral describe the same mechanism in their RL infrastructure. As with partial rollouts, one response is now a patchwork of policy versions. The fix is the same practical rule as before, applied per token: each token’s behaviour log-prob is whatever the engine used when it sampled that token.
The train–inference mismatch
Same weights, different probabilities
Now the second surprise from the opening. Take a fully synchronous run: the trainer updates the weights, copies them exactly into the inference engine, the engine samples a batch, and the trainer trains on it. Lag zero everywhere. Surely this is on-policy?
It isn’t. The inference engine and the trainer are different programs. They compute the same mathematical function with different kernels (fused attention, different matrix-multiplication tilings), different reduction orders, sometimes different precision. Floating-point addition is not associative, so and can differ in the last bits. Run those differences through dozens of layers and a softmax, and the engine’s probability for a token, call it , differs from the trainer’s , with identical weights.
The sources of mismatch, roughly from smallest to largest:
- Batch-dependent arithmetic. An inference server’s kernels may compute a sequence’s logits differently depending on what else is in the batch. Thinking Machines (He et al. 2025, “Defeating Nondeterminism in LLM Inference”) sampled 1,000 completions from Qwen3-235B at temperature 0 and got 80 distinct ones; with batch-invariant kernels, all 1,000 were identical.
- Low precision. bf16 keeps only 8 significant bits. Numbers between 16 and 32 are spaced apart, so a logit near 20 can be off by up to 0.0625 from rounding alone. Shifting one logit by 0.0625 multiplies that token’s (small) probability by about : a 6.5% change. fp16 has 11 significant bits: spacing , worst case . fp32: about . MiniMax-M1’s report found the LM head (the last layer, which produces the logits) was the culprit in their model, and computing it in fp32 raised the correlation between training-mode and inference-mode probabilities from about 0.9 to about 0.99.
- MoE routing flips. In a mixture-of-experts model, a router picks the top- experts for each token. A tiny numerical difference can change which experts are chosen, and then the two programs compute genuinely different functions.
- Deliberate differences. Rollouts in FP8 or FP4 to save memory and time, or top- sampling that truncates the distribution.
And small per-token differences compound over long responses. If each token’s log-ratio has a random error of about 0.01, a 10,000-token response has a sequence log-ratio with standard deviation about . The trainer and the engine then disagree about that whole response’s probability by a factor of around , nearly 3 either way.
Why it biases the gradient
The engine samples , but the trainer computes the REINFORCE-style update as if :
The gap is a bias, and bias does not shrink with batch size. Sampling noise averages away as you add samples; a systematic error stays. Small biases accumulate step after step, and the policy can drift into regions where and disagree even more, which makes the bias bigger. Thinking Machines’ RL experiment shows the pattern: without a correction, the reward collapsed partway through training at the same time as the measured KL between sampler and trainer spiked; with an importance-weight correction, and with bitwise-identical sampler and trainer, training was smooth.
The fix: (truncated) importance sampling
This is another off-policy correction, and the tool is again the importance weight, now between trainer and sampler. Qwen’s analysis (Zheng et al. 2025, “Stabilizing Reinforcement Learning with LLMs”) makes the bookkeeping clean. The ratio a token really needs, the current trainer policy over what actually sampled it, factors into two pieces:
PPO and GRPO already handle the second factor with and its clip. The first factor is the missing piece. Call it . It can be computed: the engine reports as it samples, and the trainer computes in its “prepare” pass.
Multiplying by the importance weight removes the bias (exactly with the whole-response weight, approximately with per-token weights ; the derivation below says how approximately). But importance weights have a nasty property: rare tokens can get huge weights (the sampler barely ever picks a token that the trainer likes), and huge weights mean huge variance. Truncated importance sampling (TIS), proposed for this problem by Yao et al. (2025), caps them: use with a constant such as 2. Olmo 3’s RL objective is a clean example of the late-2025 recipe; in our notation (they write for and for ):
Everything except is the GRPO-family objective from Chapter 16 (token-level averaging, asymmetric clip, no std normalization of ). The weight is a constant: it multiplies the gradient but no gradient flows through it.
What does truncation cost? A little bias, in exchange for bounded variance:
∂ Truncated importance sampling: exactly how much bias, and why the variance is bounded
Let and let be whatever you are averaging (here, one component of ). Assume wherever .
Untruncated IS is unbiased. .
The bias of truncation. Write , where . Then
In the second term, factor out : , so . Therefore
Since , the bias is at most : it is only as large as the probability (under ) of the samples you truncate. If the sampler is close to the trainer, that probability is tiny.
The variance is bounded. Because and , we have . So
The second moment is at most times what it would be with on-policy samples. Compare untruncated IS: , which is unbounded when can be large.
Per-token vs per-sequence. The exact correction for a whole response is the product . As the compounding example above showed, can be enormous or tiny for long responses. Where do per-token weights (what Olmo 3’s objective uses) come from? Let and write . The exact surrogate is . Expand the product to first order, , and differentiate term by term using :
That is the per-token weighted gradient. The approximation throws away the products , and their gradients are still first order in , so per-token weights are not unbiased: they fix the part of the mismatch that sits on token itself, but not the way mismatch on earlier tokens changed which responses were sampled. They are accurate only when every is close to 1, and in exchange they have far lower variance than . The same approximation is what justifies PPO’s per-token ratio (Qwen’s analysis, Zheng et al. 2025, makes this argument). Lab Task 3 measures all of these exactly on a toy model: per-token weighting removes most of the bias, but not all of it.
Variants followed quickly. Masked importance sampling zeroes out (rather than caps) the tokens or sequences whose weight is out of range (Liu, Li et al. 2025); IcePop (Ant Group’s Ring-1T, also used by GLM-5) masks tokens whose train/inference ratio leaves a band; Kimi K2.5 and GLM-5 zero the gradient of tokens whose ratio is out of bounds, regardless of the advantage’s sign (PPO’s clip only does so for one sign). By 2026 the PPO clip was, in many production recipes, being replaced by “mask the tokens whose sampler, trainer or staleness ratio is out of band” (Chapter 16 covers the objective side of this).
Or remove the mismatch at the source
Importance weights treat the symptom. Other teams went after the cause:
- More precision where it matters. MiniMax-M1’s fp32 LM head (above). ScaleRL found that computing the logits in fp32 raised the fitted asymptotic pass rate of its 8B runs from 0.52 to 0.61. Qi et al. (2025, “Defeating the Training-Inference Mismatch via FP16”) argue that bf16 rounding is the root cause, and that switching training and inference uniformly to fp16 removes most of the mismatch, giving more stable training across algorithms and frameworks.
- Batch-invariant, matched kernels, so the engine and trainer produce bitwise-identical probabilities (Thinking Machines’ “true on-policy” experiment).
- Routing replay for MoE models. Record which experts the inference engine chose for each token and force the trainer to use the same ones: DeepSeek’s “Keep Routing” (used since DeepSeek-V3-0324, per the V3.2 report) and Rollout Routing Replay (R3; Ma et al. 2025).
- Train in the format you deploy. In 2026 several labs matched rollout and training numerics by construction, for example by running rollouts with the same low-precision expert weights (FP4) used in deployment, or quantization-aware training shared by rollout and trainer.
Quick check Your synchronous GRPO run copies weights exactly into vLLM after every step, yet you log a nonzero KL between vLLM's and the trainer's log-probs on the sampled tokens. What is the most likely explanation?
Even with identical weights, different kernels and floating-point reduction orders (and bf16 rounding, MoE routing flips, quantized rollouts) give slightly different token probabilities. That is the train–inference mismatch. Truncated importance sampling corrects for it; higher precision, batch-invariant kernels or routing replay reduce it at the source.
LoRA for RL
LoRA (low-rank adaptation) freezes the base model and trains a small low-rank update for each weight matrix. For supervised fine-tuning it is a well-known way to save memory, at some cost in quality. For RL the trade-off looks unusually good.
Schulman and Thinking Machines (2025, “LoRA Without Regret”) report that for policy-gradient RL, “LoRA fully matches the learning performance of FullFT … even with ranks as low as 1”. Their explanation is an information count. A policy-gradient update is driven by the advantage, which carries about one number, bits, per episode, however long the episode is; supervised learning gets a target for every token. By their estimate RL absorbs about 1,000 times less information per token, so a tiny adapter has enough capacity to store what RL teaches. They also found the best LoRA learning rate is about 10× the full fine-tuning one, and that LoRA should be applied to all layers, especially the MLP (or MoE) layers. A 2026 paper pushed this to an extreme: TinyLoRA (Morris et al.) trained an 8B Qwen2.5 model to 91% on GSM8K with 13 trained parameters, and found that this works with RL but not with supervised fine-tuning, which needed far larger updates.
The systems payoff is in the memory table above: the policy’s training state shrinks from 112 GB to about 15 GB, the reference model becomes free (adapter off), and weight sync shrinks from the whole model to the adapter. Thinking Machines’ Tinker service builds on this: it exposes a LoRA training API (forward-backward, optimizer step, sampling) on hosted open-weight models, so you write the RL loop and they run the systems.
Scaling RL compute
How much compute should RL get, and does it pay off predictably? The most systematic answer so far is ScaleRL (Khatri et al. 2025, “The Art of Scaling Reinforcement Learning Compute for LLMs”). Over more than 400,000 GPU-hours of ablations, they fit the pass rate on held-out problems after units of RL compute with a sigmoid,
where is the asymptote (the best this recipe will ever do), the efficiency (how fast it gets there) and the compute at which half the gain is reached. Two findings matter here. Recipes differ in their asymptote, so a recipe that looks better early can be worse late. And many popular knobs (loss aggregation, advantage normalization, curriculum, the off-policy algorithm) mostly change the efficiency , not the ceiling . Stable recipes let them fit the curve on small runs and extrapolate to a 100,000 GPU-hour run.
Their final recipe is a good snapshot of what this chapter has covered: PipelineRL-style asynchrony with at most 8 steps of off-policyness, fp32 logits, a truncated-importance-sampling loss (CISPO), and no KL term. Both the systems choices and the algorithm choices are in it, because at this scale they are not separable. Chapter 21 follows the scaling story into 2026.
The open-source stacks
You will rarely build all of this yourself. The main open-source frameworks, as of October 2026 (they change fast; check each project’s documentation):
| Framework | From | Known for |
|---|---|---|
| TRL | Hugging Face | The easiest start: GRPOTrainer with vLLM in colocate or server mode; current defaults reflect the 2025 consensus (no KL by default, DAPO-style token-level loss) |
| OpenRLHF | Open-source community | Ray + vLLM + DeepSpeed; readable; PPO, REINFORCE++, async training, TIS |
| verl (HybridFlow) | ByteDance Seed | Hybrid controller design and the 3D-HybridEngine for resharding between training and generation; the base of many research codebases (DAPO, SkyRL and others) |
| slime | Zhipu / Z.ai (THUDM) | Megatron + SGLang; synchronous colocated and asynchronous disaggregated modes; powers the GLM models |
| AReaL | Ant Group (inclusionAI) + Tsinghua | Fully asynchronous, staleness control, decoupled PPO |
| PipelineRL | ServiceNow | In-flight weight updates |
| ROLL | Alibaba | Rollout scheduling, environment and reward workers for agentic RL |
| prime-rl | Prime Intellect | Asynchronous off-policy, multi-turn and tool-first; trained Prime Intellect’s INTELLECT models |
| SkyRL | NovaSky (Berkeley) | Long-horizon agent RL (e.g. software-engineering agents) |
| NeMo-RL | NVIDIA | Megatron and DTensor backends; recipes for NVIDIA’s Nemotron models |
| Tinker | Thinking Machines | A hosted LoRA training API rather than a framework you run |
A reasonable path: learn with TRL on one GPU (or with this course’s labs on a laptop), read OpenRLHF or verl to see how a full system fits together, and reach for the asynchronous frameworks when your rollouts get long or multi-turn (Chapter 18).
Summary
- One RL step is generate → score → prepare → train → sync, and each stage runs on specialized software: an inference engine (vLLM, SGLang), a trainer (FSDP, DeepSpeed, Megatron), reward workers and a controller.
- Generation dominates the time. Decode is memory-bandwidth-bound (each token reads all the weights), so it needs large batches; the KV cache (56 KiB per token for a 7B-class model) limits the batch and slows long contexts; and the long tail of response lengths leaves synchronous hardware idle most of the time (utilization about for exponential lengths).
- Training state costs 16 bytes per parameter with mixed-precision Adam, frozen models 2. A 7B PPO-RLHF setup needs about 252 GB before activations; GRPO without a critic halves that; LoRA brings it to about 15 GB and makes the reference model free.
- Colocated placement time-shares GPUs between generation and training; disaggregated placement gives each its own pool, which makes asynchrony natural.
- Asynchronous RL lets generation run up to batches ahead, trading throughput for staleness. It is off-policy RL, corrected by importance ratios computed from log-probs recorded at sampling time. Watch for stragglers and for length bias when dropping stale samples.
- The train–inference mismatch: with identical weights, the sampler’s probabilities differ from the trainer’s , which biases the gradient. Fix it with truncated importance sampling (bounded variance, small bias), or at the source with higher precision, matched kernels, routing replay and matched quantization. Always log the sampler–trainer KL.
- With RL’s low information per episode, LoRA matches full fine-tuning in reported experiments, and RL compute scaling follows sigmoid curves whose asymptote depends on the recipe (ScaleRL).
Exercises
A team runs synchronous GRPO on a 7B model and profiles one step: generation 380 s, reward computation 20 s, log-prob forward passes 40 s, training 70 s, weight sync 2 s. (a) Which stage is the bottleneck? (b) Their manager proposes doubling the number of trainer GPUs. Roughly how much faster does a step get? (c) Name two changes that would help more.
Solution
(a) Generation: 380 of 512 seconds, about 74%.
(b) If training time halves (70 → 35 s) and log-prob passes also halve (40 → 20 s), the step goes from 512 s to 457 s: about 11% faster for twice the trainer hardware. This is Amdahl’s law: speeding up a part that is 21% of the time can save at most 21%.
(c) Anything that attacks generation: give generation more GPUs; increase the batch of concurrent sequences (more memory for KV cache, FP8 KV cache, prefix caching for the shared prompts); cut the long tail (a lower max length, length penalties, partial rollouts); or go asynchronous so generation never waits for training and the slowest response no longer blocks everyone. Speculative decoding (a small draft model proposes tokens that the big model verifies in parallel) is another generation-side speedup.
You want to run PPO-RLHF with a 7B policy, a 7B critic, a 7B reward model and a 7B reference model, using mixed-precision Adam, on 80 GB GPUs.
(a) Compute the model-state memory of each component and the total. (b) With ZeRO-3/FSDP sharding over 8 GPUs, how much model state is on each GPU, and how much memory is left for activations and the KV cache? (c) Repeat (a) for GRPO with a verifier and a KL term, and for GRPO with a LoRA adapter of 40M parameters. (d) In the LoRA case, what is likely to become the largest consumer of memory?
Hint
Solution
(a) Policy GB; critic 112 GB; reward model GB; reference 14 GB. Total 252 GB.
(b) GB per GPU, leaving about GB per GPU for activations, temporary buffers and (if colocated) the inference engine’s weights and KV cache. Tight but workable with gradient checkpointing.
(c) GRPO with a reference: 126 GB (no critic, no reward model). GRPO with LoRA: frozen base GB + adapter GB = 14.64 GB, and the reference model costs nothing because it is the base model with the adapter disabled.
(d) The KV cache during generation. With 15 GB of weights on an 80 GB GPU you have about 65 GB for KV cache; at 56 KiB per token (Qwen2.5-7B shape) that is about 1.1 million tokens, which long reasoning responses fill quickly (Exercise 20.3). Activations during training are the other big item.
A GRPO step uses 64 prompts with responses each, on a model with Qwen2.5-7B’s shape (28 layers, 4 KV heads, head dimension 128, bf16 KV cache). Each prompt is 500 tokens; responses can reach 16,384 tokens.
(a) What is the KV cache per token, and for one maximum-length response (prompt included)? (b) If all 512 responses reached the maximum length at once, how much KV cache would they need? (c) On one 80 GB GPU with 15 GB of weights, how many maximum-length responses can be in flight? (d) With prefix caching, the 8 responses of a prompt share the prompt’s KV cache. How much does that save per group, as a fraction of the group’s total? When would prefix sharing matter much more?
Hint
Solution
(a) bytes per token. One response with its prompt: GB.
(b) GB. No single GPU comes close, which is why generation is spread over many GPUs and why the engine admits only as many sequences as fit.
(c) responses at maximum length (in practice fewer, because the engine needs working memory too). This is the batch-size cap from the main text, and it is far below the roughly 300 sequences needed to make decode compute-bound.
(d) Sharing saves 7 copies of the prompt: tokens out of , about 2.6%. Prefix caching matters much more when the shared prefix is long relative to the responses: multi-turn agent episodes where every turn re-reads a long conversation and tool outputs (Chapter 18), long system prompts, or long documents in the prompt.
Model response lengths as independent exponential random variables with mean . A synchronous step runs responses in parallel, one per slot, and ends when the longest finishes.
(a) Show that the expected longest length is , with . (b) What fraction of slot-time is spent generating? Evaluate for and . (c) Why does this get worse, not better, as you scale up ? (d) Name two ways to recover the lost utilization.
Hint
Solution
(a) Wait for the first of the responses to finish. The minimum of independent exponentials with rate is exponential with rate , so the expected wait is . By memorylessness, the still running are “as good as new”, so the expected wait for the next completion is , and so on down to for the last. Adding: .
(b) Each slot works for on average out of a step of expected length , so the utilization is about (ignoring training time). , utilization 37%. , utilization 21%.
(c) grows like : the more responses you wait for, the more extreme the slowest one is likely to be. Scaling up the batch makes every step wait for a longer straggler.
(d) Asynchrony (let free slots start the next batch: the demo’s ); partial rollouts or in-flight weight updates; more responses than slots per step, so free slots pick up new work (continuous batching within a step); a lower maximum length or length penalties; rebalancing (spreading long responses over GPUs, speculative decoding for the tail).
A response for prompt was sampled by the inference engine running weights (lag ), and the engine recorded for every token. The trainer is about to update from using PPO’s clipped objective.
(a) Write the sequence-level importance-sampling identity that turns an expectation under into one under . (b) Show that the per-token ratio the trainer needs factors as (mismatch) × (staleness) × (PPO ratio), with three factors involving , , and . (c) A colleague computes PPO’s “old” log-probs by running the trainer’s current weights on the batch and ignores the engine’s recorded log-probs. Which factors does their ratio leave out? (d) Which factors are close to 1 when and sampler and trainer are bitwise identical?
Hint
Solution
(a) For any function , , valid where whenever .
(b) Per token,
The middle factors cancel pairwise, so the identity holds exactly. Here is the engine’s version of .
(c) Their ratio is only , the PPO ratio. It leaves out both the mismatch factor and the staleness factor, so with lag the update is biased toward whatever liked to sample. Recording the engine’s log-probs and using them (with truncation for the mismatch part) fixes both. In AReaL’s decoupled objective the trust-region center is (the proximal policy) and the importance weight is , which is exactly the product of the first two factors.
(d) With the staleness factor is exactly 1. With bitwise-identical sampler and trainer, the mismatch factor is exactly 1. Only PPO’s ratio is left, which is 1 at the first minibatch step and moves away from 1 as the trainer takes more steps on the batch.
A two-token vocabulary. The trainer’s policy is , the sampler’s is , and you want for (so the true value is 0.5).
(a) Compute the expectation under of the naive estimator , of the IS estimator , and of the truncated estimator with . (b) Compute the variance of the IS and the truncated estimators. (c) Check your bias against the formula from the derivation, and your truncated second moment against the bound .
Hint
Solution
(a) Weights: , . Since , only token 1 matters.
- Naive: . Bias .
- IS: . Unbiased.
- Truncated, : . Bias .
(b) IS: second moment , variance . Truncated: second moment , variance . (Naive: , low variance but badly biased.)
(c) Bias formula: is for token 0 (since ) and for token 1, so . ✓. Bound: ✓.
With a batch of samples the mean squared error is bias² + variance/. For : naive ; IS ; truncated . Truncation wins here; for very large the unbiased IS estimator eventually wins, because bias never averages away.
For each situation, say whether training on the stale or mismatched data is likely to be harmless, harmful, or fixable, and why. (a) One-step off-policy GRPO with a small learning rate and recorded sampler log-probs. (b) Lag up to 8 with a large learning rate and no importance correction. (c) A 1-trillion-parameter MoE model whose sampler and trainer pick different experts for 5% of tokens. (d) Asynchronous online DPO with moderate staleness. (e) Dropping every sample with lag above 1 in a run where hard problems produce long responses.
Solution
(a) Mostly harmless. With a small learning rate, consecutive policies are close, so the importance ratios are near 1; the recorded log-probs let the PPO ratio and clip correct what difference remains. This is why one-step off-policy is a popular default.
(b) Harmful. Eight large steps can move the policy far from the one that sampled the data. Without a correction, the gradient is computed as if the data came from the current policy, and it is biased toward the old policy’s behaviour. Fix: record behaviour log-probs, use importance ratios with clipping or truncation (or a decoupled objective), lower the learning rate, or reduce .
(c) Harmful unless addressed. A routing flip means the two programs compute different functions for that token, so the ratio can be far from 1 and noisy. Truncation limits the damage, but the robust fix is routing replay (use the sampler’s expert choices in training), as DeepSeek’s Keep Routing and R3 do.
(d) Fairly harmless, per Noukhovitch et al. (2024), who found online DPO the most robust to off-policy data among the algorithms they tested, increasingly so at larger model sizes. DPO’s loss is defined on pairs and does not assume the samples came from the current policy (it is an offline method at heart; Chapter 13).
(e) Harmful in a quiet way. The dropped samples are the stragglers: the long responses, which here are the hard problems. The model then trains mostly on easy problems and short answers, biasing it toward brevity and away from difficult reasoning. Fixes: a larger with importance correction instead of dropping, partial rollouts or in-flight updates so long responses aren’t systematically late, or monitoring the length distribution of trained vs generated samples.
Choose a placement (colocated or disaggregated, synchronous or asynchronous) for each scenario and justify it in two or three sentences. (a) GRPO on a 1.5B model for grade-school math, 8 GPUs, responses under 2,000 tokens, an instant verifier. (b) A 32B coding agent on 256 GPUs whose episodes include running test suites in sandboxes that take 30 seconds to 10 minutes. (c) PPO-RLHF of a 70B policy with a 70B reward model on 64 GPUs.
Hint
Solution
(a) Colocated, synchronous (perhaps one-step off-policy). The model is small, responses are short and fairly uniform, and rewards are instant, so the tail bubble is modest. Colocation keeps all 8 GPUs busy in both phases, weight sync is a cheap local reshard, and synchronous training removes a whole class of off-policy issues. Splitting 8 GPUs into two pools would leave each pool idle half the time.
(b) Disaggregated, asynchronous, with a generous staleness bound and importance correction. Episodes are dominated by tool and sandbox latency, during which the policy GPUs have nothing to do; lengths are extremely variable. The generator pool should keep many episodes in flight (most of them waiting on sandboxes) while the trainer consumes completed ones. Synchronous training would idle the cluster for the slowest 10-minute episode every step. Track the lag and the length distribution of what gets trained on.
(c) Memory decides here. Policy and critic in training need TB, and the reference and reward model 280 GB more: about 2.5 TB of state, roughly 39 GB per GPU when sharded over 64 GPUs. A colocated hybrid engine with time-sharing (load and offload models per phase) makes the best use of that memory, which is how HybridFlow/verl and OpenRLHF were designed. An alternative is a separate pool that serves the reward model, so the training GPUs never hold it. Responses in classic RLHF are short, so the long tail is less severe than in reasoning RL.
Use the demo above. (a) With the defaults (16 lanes, σ = 0.8, train time 1), read the lane utilization at k = 0, then the speedup and mean lag at k = 1 and k = 2. Why doesn’t k = 3 help further? (b) Set σ = 0 (all responses the same length). Is asynchrony still useful? Why? (c) Return to σ = 0.8, set train time = 3, and move k from 1 to 6. What happens to speed and lag? (d) Set train time back to 1 and k = 1, and tick “drop samples older than k”. What fraction of generation is wasted, and which responses (short or long) turn red? Why could this matter for training a reasoning model?
Solution
Numbers below are for the default seed; “New lengths” changes them a little.
(a) At k = 0 the lanes are busy only about 26% of the time (and the trainer about 19%). k = 1 gives about 3.1× the throughput, with a mean lag of about 1 and occasional stragglers up to 5. k = 2 gives about 3.9× with mean lag about 1.7, and the lanes are now busy 100% of the time. Once generation never waits, generation is the bottleneck, and nothing about the trainer’s scheduling can make it faster; k = 3 changes nothing.
(b) Yes: about 2×. Even with identical lengths, synchronous disaggregated RL alternates “generate, then train”, so each pool idles while the other works. Asynchrony overlaps them. With σ = 0 the mean lag is about 1 (one-step off-policy).
(c) Speed rises from k = 0 to 1 and then flattens at about 2.4×, because the trainer (busy about 99% of the time) is now the bottleneck. But the mean lag keeps rising, to more than 5 at k = 6: samples pile up in the trainer’s queue and wait longer. Extra staleness, no extra speed.
(d) About a third of generation time is wasted (35% with the default seed), the maximum lag drops to 1, and throughput drops from about 3.1× to about 2.8×. The red blocks are the long responses: short ones finish in time, long ones fall behind and get dropped. For a reasoning model, long responses are often the hard problems or the careful solutions, so dropping them biases training toward short, easy successes, and can push response length down for reasons that have nothing to do with the reward.
Do the three tasks in the lab below.
- Task 1 (
memory.py):model_state_bytes,kv_cache_bytesandrl_memory_gb. The tests check the 252 GB / 126 GB / 14.64 GB numbers from the memory table. - Task 2 (
scheduler.py):may_start(AReaL’s admission rule) andtake_batch(first-in-first-out batching with lag bookkeeping and optional dropping) inside a given discrete-event simulator. Two tests reproduce the hand example from the “Let generation run ahead” section: 6 time units synchronous, 4 asynchronous. - Task 3 (
tis.py):importance_weightsfor five schemes (none, per-token IS, per-token TIS, sequence IS, sequence TIS) andpg_estimate. The tests compute exact bias and variance on a toy model with 4,096 possible responses.
Then run python run.py and compare the plots with the README’s “What you should see”.
Hint
log_w = logp_train - logp_sampler once; the sequence weight is np.exp(log_w.sum(axis=1, keepdims=True)). For Task 2b, filter the queue first, then check whether at least B samples remain.Solution
The reference solutions are in solutions/. The key lines:
model_state_bytes:return 16 * n_params if trainable else bytes_per_weight * n_params.kv_cache_bytes:return 2 * n_layers * n_kv_heads * head_dim * n_tokens * bytes_per_value.rl_memory_gbassembles the components; the only subtle line is that with LoRA the reference costs 0.may_start:return n_started - n_dropped < (version + max_staleness + 1) * batch_size. Withmax_staleness = 0this reproduces synchronous RL, and the hand test checks it.take_batch: ifdrop_stale, split the queue into samples withversion - s["tag"] > max_staleness(dropped) and the rest; if fewer than B remain return(None, dropped, remaining); otherwise sets["lag"] = version - s["tag"]on the first B and return them.importance_weights:log_w = logp_train - logp_sampler; per-token modes exponentiate it (andnp.minimum(..., C)for TIS); sequence modes exponentiate the row sum and broadcast it to every token.pg_estimate:return (rewards - baseline)[:, None, None] * weights[:, :, None] * score.
What run.py shows: the top row reproduces the demo’s trade-off (a 2–5× speedup that saturates at small k, paid for with a mean lag of 1–2); the bottom row shows the naive estimator’s bias growing linearly with the mismatch, sequence IS staying unbiased but exploding in variance, and the truncated estimators giving the lowest total error.
Files: README.md, memory.py, scheduler.py, tis.py, scheduler_core.py, toy.py, test_rl_systems.py, run.py
cd 20-rl-systems
python -m pytest -q # your code: tests fail until you fill in the TODOs
RL_SOLUTIONS=1 python -m pytest -q # reference solutions: all passFirst time? See Exercises & setup.
Further reading
- Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention. The vLLM paper. Its background sections explain why decode is memory-bound and why KV-cache fragmentation limits the batch size.
- Sheng et al. (2024), HybridFlow: A Flexible and Efficient RLHF Framework. The verl paper: the dataflow view of an RL step, colocated placement and the 3D-HybridEngine.
- Fu et al. (2025), AReaL: A Large-Scale Asynchronous Reinforcement Learning System for Language Reasoning. The clearest description of asynchronous RL for reasoning: staleness control and the decoupled PPO objective.
- Noukhovitch et al. (2024), Asynchronous RLHF: Faster and More Efficient Off-Policy RL for Language Models. How much off-policy-ness LLM RL can tolerate, algorithm by algorithm.
- Yao et al. (2025), Your Efficient RL Framework Secretly Brings You Off-Policy RL Training. The blog post that named the train–inference mismatch and proposed truncated importance sampling.
- He and Thinking Machines (2025), Defeating Nondeterminism in LLM Inference. Why inference is nondeterministic (batch invariance), with an RL experiment showing what bitwise on-policy training buys.
- Schulman and Thinking Machines (2025), LoRA Without Regret. LoRA for SFT and RL, the information-per-episode argument, and practical learning-rate advice.
- Khatri et al. (2025), The Art of Scaling Reinforcement Learning Compute for LLMs. The ScaleRL sigmoid fits and a recipe that combines the systems and algorithm choices of this chapter.
Next, Chapter 21 steps back to survey what the latest models actually do in post-training, and the open problems worth watching.