train_rl sampled 64 episodes with the current weights, waited for the last one to finish, took its gradient steps, and sampled again with the new weights. Sampling and training never overlapped, the sampler and the trainer were the same Python object, and sample_logp agreed with the trainer’s recomputation to (Section 8.6). None of that survives contact with a real coding agent. An episode takes minutes of container time, episode durations have a long tail, generation runs on an inference engine with its own kernels, and the trainer runs on a different set of accelerators with a different parallelism layout. The system that connects them decides how much of the hardware does useful work, and it also decides which policy actually generated the data. The second consequence is the subject of this chapter as much as the first.
We build four things. A discrete-event simulator of a rollout pipeline measures what synchronous, one-step-ahead, and fully asynchronous designs buy in throughput and what they cost in staleness. A training loop that samples with a policy updates old measures what staleness does to TinyAgent, with and without importance correction. A measurement on the real Qwen3-0.6B shows that an inference-style forward pass and a trainer-style forward pass disagree about log-probabilities even with identical weights. And two corrections for that disagreement, truncated and masked importance sampling, are derived, simulated, and tested on TinyAgent with a deliberately mismatched sampler.
13.1 The components of an RL system
A production RL system for agents has four kinds of component. Their names vary between frameworks; their jobs do not.- The inference engine (vLLM, SGLang) generates tokens. It owns the KV cache, batches thousands of concurrent sequences with continuous batching, and returns token ids together with the log-probability of each sampled token under the distribution it sampled from. Its kernels are tuned for decode throughput: paged attention, fused sampling, often bf16 or FP8 weights. Failure modes: out-of-memory from KV-cache growth on long episodes, preemption and recomputation of sequences under memory pressure, and log-probabilities that differ from the trainer’s (Section 13.6).
- The trainer (FSDP or Megatron, inside frameworks such as verl, slime, OpenRLHF, or AReaL) recomputes log-probabilities, computes advantages, and takes gradient steps. It shards parameters, gradients, and optimizer states across accelerators. Failure modes: memory blow-up on long sequences (Chapter 14), gradient-normalization bugs across microbatches, and silent divergence when its data came from a policy other than the one it assumes.
- Environment workers run the tools: a container per episode with the repository checked out, a shell, a test runner. They are CPU-bound, slow, and unreliable. Failure modes: container start-up latency, hung test processes, flaky tests, worker crashes in the middle of an episode, and resource exhaustion when thousands of episodes run at once.
- Weight synchronization moves new parameters from the trainer to the engine after each update, by an NCCL broadcast between the two process groups, by resharding in place when they share accelerators (“colocated”), or by writing a checkpoint that the engine reloads. Failure modes: the sync takes seconds to minutes and stalls generation; a partial sync leaves the engine with a mixture of old and new layers; a version counter that is not attached to every sample makes staleness unmeasurable.
rollout_batch is the engine, BugFixEnv is an environment worker, rl_update is the trainer, and weight synchronization is free because sampler and trainer are the same nn.Module. What a real sync moves is the parameters. The block counts them for TinyAgent and does the arithmetic for real model sizes, under labeled assumptions about bandwidth.
13.2 Where the time goes: long-tailed episodes
The second input is the duration of an episode. An agent episode alternates generation and tool calls. Each turn generates a few hundred tokens at a per-sequence decode rate of tens of tokens per second (the engine’s aggregate throughput is high, but each sequence advances one token per decode step), then waits for a tool: a file read returns in milliseconds, a test suite in seconds to minutes. The number of turns varies by task and by attempt.agent_episode_durations samples durations from that structure: a geometric number of turns, a uniform number of tokens per turn, and a log-normal tool latency. The defaults are assumptions chosen to look like a coding agent, not measurements of one.
13.3 Synchronous, one-step-ahead, and asynchronous pipelines
Three designs answer the straggler problem differently.- Synchronous (on-policy). Sample a batch with version , train on it, sync to , repeat. Every episode is trained by the version that generated it, so the gradient is exactly the on-policy estimator of Chapter 6. The step time is , and generation and training never overlap.
- One step ahead (one-step off-policy). While the trainer trains on batch , the engine already samples batch with version . Training overlaps generation, and every episode is exactly one version stale. verl documents this mode (Chapter 20).
- Fully asynchronous. Generation slots never wait: whenever an episode finishes, the slot starts another with the newest weights. The trainer takes the oldest finished episodes whenever it is free. A staleness bound throttles generation so it cannot run more than batches ahead of training; AReaL’s condition, quoted in Chapter 20, allows a new episode only while the number started is below . With the design reduces to synchronous.
n_slots generation slots (concurrent episodes in the engine), one trainer that consumes batch_size episodes per step and takes train_time seconds, and a sync that takes sync_time seconds and pauses all generation. It is a discrete-event simulation: time jumps from one event (an episode finishing, a training step finishing, a sync finishing) to the next. Episode durations are consumed in order from one tensor, so every design sees the same episodes. It records every interval it simulates, so we can draw the timeline, and it reports throughput, slot utilization, and the staleness of every trained episode: the trainer’s version at the start of its step minus the version that started the episode.
batch_size episodes are finished, recording each one’s staleness. It advances time to the earliest pending event. And it processes that event: a finished sync publishes the next version, a finished training step requests a sync, a finished episode joins the buffer. A requested sync starts at once with partial rollouts, pushing the end time of every in-flight episode back by sync_time, or, in drain mode, only when no episode is in flight. Slot utilization counts an interrupted episode’s pause as busy time, a small overstatement.
We run five designs on the same 64 slots, 64 episodes per step, 60 seconds of training, and 10 seconds of sync, for 20 steps.
Evidence. Everything in this section is a simulation under stated assumptions: the duration model of Section 13.2, a fixed training time, and a sync that pauses all generation. It shows mechanisms (straggler waits, overlap, the drain penalty, staleness that a count-based throttle does not bound). Speedups reported by real systems (2.77x for AReaL, 2.72x for ROLL Flash on agentic tasks; Chapter 20) come from different workloads and are not reproduced here.
13.4 Importance correction for a behavior policy
Asynchrony changes the estimator. Let be the behavior policy, whatever actually produced the tokens: a version of the weights updates old, an inference engine with different numerics, or a mixture of versions within one interrupted episode. The trainer holds . The policy gradient of Chapter 6 is an expectation under , and the data come from . Per token, with the per-token approximation of Section 8.1, Ignoring the ratio, which is whatrl_update does when it recomputes old_logp with the trainer, estimates instead. That is a biased estimator of the gradient we want, and the bias grows with the distance between and . Including the full ratio removes the bias and brings back Chapter 8’s problem: the ratio is unbounded, and its variance explodes where puts little mass on tokens that likes.
PPO’s clip does not solve this. The clip bounds , where is the trainer’s copy of the policy at the start of the update. When , there are two ratios, and AReaL’s decoupled objective (Chapter 20) keeps them apart: a behavior weight that corrects which distribution sampled the data, and PPO’s clipped ratio that limits how far this update moves. With treated as a constant,
Since , , so the decoupled loss is Chapter 8’s ppo_policy_loss with per-token advantages . That is how the code below implements it. Two ways to tame are in use:
- Truncated importance sampling (TIS): . The TIS blog post (Yao et al., 2025; Chapter 20) proposes it for the sampler–trainer mismatch, and verl’s rollout-correction presets use . Truncation bounds the second moment of each weighted term by times the unweighted one, and it leaves a bias: tokens with are under-weighted, which means tokens that the sampler under-sampled relative to the trainer stay under-represented.
- Masked importance sampling (MIS), IcePop in the Ring-1T report: inside a band and outside, with and in the report. A token whose two probabilities disagree by more than the band is treated as noise and removed from the gradient, rather than kept with a capped weight.
13.5 Stale policies on TinyAgent
The simulator says how stale the data get; it cannot say what staleness does to learning. For that we train TinyAgent with a behavior policy that lags the trainer by a fixed number of updates.offpolicy_update is rl_update of Chapter 9 with one change: the batch’s sample_logp, the behavior policy’s log-probabilities recorded at sampling time, enters through the decoupled weights. With correction='none' the weights are the action mask, and the update is exactly rl_update: the trainer recomputes old_logp and never looks at . It also reports the mean absolute log-ratio between and (mismatch) and the share of policy tokens whose ratio exceeds the cap (over_cap).
train_offpolicy keeps the last lag + 1 versions of the weights and samples every step with the oldest, so after the first lag steps every batch is exactly lag updates stale, the situation of a one-step-ahead pipeline at lag=1 and of an asynchronous one with a fixed staleness. An optional sampler function transforms the behavior model before sampling; Section 13.6 uses it to make a numerically different sampler.
lag=0 and no correction, this must be train_rl step for step, since both consume the same random streams in the same order. Checking that before trusting any comparison costs five steps.
Evidence. Measured here, one seed per configuration, 40 steps on a 157k-parameter policy. The ordering (no correction degrades with lag; TIS and MIS recover much of it) is the mechanism this section is about. The specific lags at which TinyAgent breaks reflect its learning rate and how fast its policy changes; a large model trained with a learning rate of moves far less per update, which is why public asynchronous systems tolerate staleness bounds of 4 to 8 (AReaL’s reported defaults, Chapter 20).
13.6 The training–inference mismatch
Staleness is a mismatch we choose. The second kind arrives without being chosen. In a production system the behavior policy is the inference engine, and even at the same weights its probabilities are not the trainer’s. The engine computes each sampled token’s distribution in a decode step: one new token per sequence, attention over a KV cache, a batch whose composition changes from step to step as sequences start and finish, kernels chosen for that shape. The trainer computes the same distributions in one forward pass over the whole sequence, with a different batch, different padding, often different parallelism and kernels. Floating-point addition is not associative, so different reduction orders give different results, and bf16’s 8-bit mantissa makes the differences visible. Section 3.6 measured the precision part on one sequence; here we measure the whole path on the real Qwen3-0.6B, loaded from the check host’s cache, on its GPU when one is available.engine_sample imitates an inference engine without being one: a left-padded batch, a KV cache, one decode step per token, sampling at temperature 1, and the sampled token’s log-probability taken from that step’s logits (normalized in fp32, as selected_log_probs does). vLLM and SGLang add paged attention, continuous batching, and fused sampling kernels, which change the numbers further; this is the minimal version of the same computation path.
batch_size sets how many sequences share a forward pass.
A mismatched sampler on TinyAgent
Does a mismatch of this size hurt training, and do the corrections help? TinyAgent runs sampler and trainer in one process, so we make the sampler different on purpose. FP8 rollouts are one motivation in the TIS post: the engine runs with low-precision weights for speed while the trainer keeps full precision.fp8_copy rounds every weight of a copy to float8_e4m3fn and back, a 3-bit mantissa.
13.7 Design problems in practice
Problem: straggler episodes. Symptom: accelerator utilization of the inference fleet falls as the batch grows; step time tracks the slowest episode; the tail of the episode-duration histogram moves with every change of task mix. Cause: a synchronous step waits for over the batch, which grows like a high quantile of the duration distribution (Section 13.2). Options: (a) asynchronous generation with partial rollouts, which removes the wait and adds staleness. (b) A wall-clock or turn timeout that truncates the tail, which changes the task: truncated episodes need a policy (Chapter 12), and masking them out of the loss (DeepSWE’s compact filtering) biases training toward tasks that finish quickly. (c) Over-sampling: launch more than episodes and train on the first to finish, which is the same length bias in a different form. Default: the book recommends (a) with the duration of trained versus launched episodes logged, plus a generous timeout as a safety net. The block below measures what a timeout at the 95th percentile buys a synchronous pipeline.
Problem: choosing a staleness limit. Symptom: an asynchronous run is faster per hour and worse per sample than its synchronous baseline;mismatchbetween the trainer’sold_logpand the sampler’s log-probs rises with throughput; in the worst case the policy collapses. Cause: the gradient is estimated from . Section 13.5 measured the dose response on TinyAgent: staleness 2 was harmless, 4 cost about a third of the held-out pass rate without correction, and 8 destroyed the skill. Options: (a) a tight bound (1 or 2), which caps throughput (Figure 13.2 shows most of the gain arrives by then). (b) A looser bound with decoupled TIS or MIS. (c) Enforce the bound at consumption too, dropping episodes older than , since a count-based throttle bounds the mean staleness but not the maximum. Default: the book recommends with TIS from the start, and logging the staleness histogram of every trained batch, not just its mean. Policy speed matters as much as the bound: staleness is measured in updates, and a larger learning rate makes each update a bigger step.
Problem: the cost of weight synchronization. Symptom: step time has a fixed floor that does not shrink with more inference capacity; the engine’s throughput graph shows periodic gaps. Cause: every update must reach every engine replica, and generation pauses while it does (Section 13.1 estimated seconds to a minute per sync for a 32B model). Options: (a) colocate engine and trainer and reshard in place, which avoids the network but serializes generation and training. (b) Broadcast over NCCL from the trainer to a disaggregated engine. (c) Sync less often (several updates per sync), which raises staleness. (d) Overlap the broadcast with generation and swap weights atomically, which costs memory for two copies. Default: (b) with the sync time logged as its own phase. The block shows how the synchronous and asynchronous designs respond to the sync cost.
Problem: environment worker failures and retries. Symptom: a few percent of episodes end with a worker error (container crash, hung test process, out-of-memory); the reward of the surviving episodes looks better than it should, or the trained batch drifts toward short tasks. Cause: failures are not independent of the episode. If a worker fails with a constant hazard per second, a long episode is more likely to fail, so dropping failed episodes removes long ones preferentially, and retrying them silently doubles their cost. Options: (a) drop failed episodes and accept the bias. (b) Retry on a fresh worker, from the start or from a checkpointed state, and count the retry’s cost. (c) Mark the episode as an environment failure, give it no gradient, and keep its task in the next batch so the task mix is preserved. Default: (c), with infrastructure failures logged separately from agent failures, never scored as reward 0: a reward of 0 for a crashed container teaches the policy to avoid whatever made containers crash. The block measures the length bias of dropping failures under a hazard of one failure per 2,000 seconds.
Problem: nondeterminism. Symptom: the same seed gives different rollouts on a rerun; the ratio is not exactly 1 at step 0; a bug cannot be reproduced. Cause: Section 13.6 measured it: in bf16, the same trainer with the same weights gives log-probabilities that differ by about 0.03 nats per token depending only on which other sequences share the batch, while in fp32 the two agree exactly on this GPU. Inference engines change batch composition at every decode step. Options: (a) accept it, recompute old_logp in the trainer, and correct the remaining sampler mismatch with TIS or MIS. (b) Batch-invariant kernels, which make results independent of batch composition at a throughput cost; the Thinking Machines post reports a bitwise-identical sampler and trainer and a KL of exactly 0 (Chapter 20). (c) Higher precision for the parts that matter most (the LM head and log-softmax in fp32, as MiniMax-M1 reports). Default: (a) plus (c), and monitor the step-0 mismatch distribution (mean, p99, max of ) as a health metric: a sudden rise means a kernel, precision, or version bug, not learning.
Summary
- A production RL system has an inference engine, a trainer, environment workers, and weight synchronization. Each has its own failure modes, and the version of the policy that generated each sample must be recorded, or staleness cannot be measured.
- Agent episode durations are long-tailed, so a synchronous step waits for its slowest episode: 4.3 times the mean duration at 64 episodes per step in the simulator, with slots busy 19% of the time. Asynchronous generation with partial rollouts raised simulated throughput 4.1 to 4.6 times; draining before each sync removed almost all of the gain. A count-based staleness bound limited the mean staleness but not the maximum.
- Off-policy data need the decoupled view: a behavior weight for the sampling distribution, PPO’s clip for the update. In a one-step simulation, TIS and MIS removed the bias of a small mismatch at no variance cost and did not fix a large one; full IS fixed the bias at exploding variance.
- On TinyAgent, sampling with a policy 8 updates old collapsed the held-out pass rate to 0.00; TIS recovered 0.55 and MIS 0.64 (one seed each). Staleness 2 did no harm.
- On Qwen3-0.6B with identical weights, engine-style and trainer-style bf16 log-probabilities differed by 0.03 nats per token on average (99th percentile 0.13), concentrated on low-probability tokens, and two bf16 trainer passes differed by as much from batch composition alone; fp32 passes agreed to . Token-level ratios stayed within .
Exercises
- Derive the expected synchronous step time for exponentially distributed durations with mean , and show that it grows like . Check the prediction against
simulate_pipelinewith exponential durations. - Extend
simulate_pipelineso the trainer drops episodes whose staleness exceedsmax_stalenessat consumption. Plot throughput and the fraction of episodes discarded against . At what bound does discarding stop costing throughput? - Change an assumption: rerun the staleness experiment of Section 13.5 with
RLConfig(lr=3e-4). Does lag 8 still collapse? Explain the result in terms of themismatchcolumn. - Implement sequence-level TIS: one weight per episode, , broadcast to its tokens. Compare it with token-level TIS at lag 8. Which one has more weights at the cap, and why?
- Measure the Qwen3-0.6B mismatch with the LM head and log-softmax in fp32 but the rest of the model in bf16 (on both sides). How much of the mismatch does it remove? Relate your result to the MiniMax-M1 report cited in Chapter 20.
- In
engine_sample, decode the same 30 prompts in batches of 1, 10, and 30. How large is the engine’s own batch-composition mismatch? What does this imply for comparing log-probs between two runs of the same engine? - Design review: your asynchronous agent run with trains 3 times faster per hour than the synchronous baseline but reaches a lower held-out pass rate at the same number of samples. List the measurements you would ask for (staleness histogram, mismatch distribution, length of trained versus launched episodes, capped-ratio fraction) and the decision each one informs.