Fully Async Mode#
Fully async mode deploys rollout (sampling) and training on independent machine resources. Rollout continuously generates trajectories in the background, and the training pipeline can start training immediately once a mini-batch is fetched from the buffer. Compared with the default “train the whole batch only after the whole rollout batch completes,” this mode overlaps sampling and training, reducing idle time on both types of GPUs. The benefit is especially significant for tasks where sampling and training take comparable time under colocate mode.
Note that “fully async” does not mean there is no synchronization boundary at all between training, weight update, and checkpoint saving. At the end of each training step, the system still pauses rollout, cleans up or waits for outstanding requests, decides whether to save a checkpoint based on trainer.save_freq, syncs the new weights to the inference engine, and then resumes background production. stale_steps is used to enlarge the upper bound on the number of currently unconsumed trajectory groups. The larger this value, the more off-policy the trajectories can potentially be. Since the weights used by rollout and training may differ, it is recommended to combine with off-policy protection mechanisms such as IS correction, M2PO, or OPSM.
It is recommended to use conf/qwen3_30b_a3b/dapo_h20_1node.yaml as the starting configuration. Before running, replace hf_model_path, prompt_data_path, checkpoint_path, the tracking address, and the GPU topology inside, and switch colocate to false (that example is colocated by default):
python -m coda.controller.trainer --config-name qwen3_30b_a3b/dapo_h20_1node \
colocate=false fully_async.enable=true
1. Key Parameters#
The table below summarizes the fully-async switch, parameters directly involved in async scheduling, and related parameters that affect step-boundary cleanup behavior. The following symbols are used throughout: B denotes data_sources[0].num_prompts_per_step, N denotes data_sources[0].num_trajectories_per_prompt, and M denotes trainer.mini_batch_size.
Parameter |
Default |
Description |
Constraints or Recommendations |
|---|---|---|---|
|
|
Whether to enable the fully-async training pipeline. Once enabled, a background rollout producer, collector, and |
Must be used together with |
|
|
Controls prompt group dispatch and collection strategy. Options: |
See Section 3 for how to choose. |
|
|
Additional pipeline capacity expressed as a multiple of the training step. All three built-in strategies use this value. |
Must be |
|
|
Whether rollout and trainer share the same GPU pool. |
Must be set to |
|
|
Entry mode. |
Fully-async only supports |
|
|
The list of data sources actually used. |
Fully-async currently supports only a single data source. |
|
|
The number of prompt groups |
|
|
|
Each time, |
It is recommended that |
|
|
Defines the resident training GPU pool. |
The total training GPU count is their product and is not shared with rollout. |
|
See default config |
Defines the resident SGLang GPU pool, the number of replicas, and the GPU count per replica. |
Total rollout GPU count is the sum of |
|
|
Additional dispatch amount used by synchronous dynamic sampling. |
Must be |
|
|
Whether to abort in-progress requests at the step boundary. When set to |
For long trajectories or multi-turn agents, |
|
|
When restoring a partial trajectory, set the |
Can only be enabled when |
|
The latter is |
Limits the concurrency of the inference service and the Router respectively. |
These are performance tuning options, to be configured together with the maximum trajectory scale of |
|
Off by default |
Filters tokens with the largest bias based on the second moment of |
Cannot be used together with |
|
Off by default |
When advantage is negative and sequence KL exceeds a threshold, masks out the gradient for that sequence. |
Can be combined with other off-policy protection mechanisms; the example configuration above only enables IS correction, so M2PO and OPSM have to be turned on as needed. |
The capacity semantics of fully_async.stale_steps are as follows:
capacity = int(B * (1 + stale_steps))
For example, when B=64 and stale_steps=1.0, the capacity is 128 prompt groups, covering up to about 2 training steps worth of prompts. The number of trajectories per group is still determined by N. Increasing this parameter typically improves pipeline fill rate, but also increases the risk of policy staleness.
2. Principles#
Fully-async mode consists of three concurrent entities:
rollout producer: Runs in an independent thread and its event loop, reads prompt groups from the datasource, and dispatches them to AgentFlow according to the sliding window strategy. AgentFlow then interfaces with the inference engine.
collector: Runs in another background thread. When each terminal trajectory completes (success, failure, or cancellation), it first enters
TrajQueue; the collector waits forNtrajectories of the same prompt to be assembled, then after strategy checks, performs filtering and statistics, and finally writes the valid complete group intoPipelineBuffer.trainer consumer: Runs in the main thread. Each time it fetches
M // Ngroups fromPipelineBuffer, assembles them into training data, and executes one optimizer update; a single outer step performs(B × N) // Mtraining calls in total.
The two-level queue decouples “trajectory completion” from “training consumption”:
TrajQueue: AgentFlow → collector. Aggregates single trajectories byprompt_id; a group can only be dequeued once fully assembled.PipelineBuffer: collector → trainer. Holds completed and filteredTrajectoryGroups, and consumes by priority based on the epoch/prompt sequence in theprompt_id.
The synchronization relationships within a single training step are as follows:
The producer and collector run continuously; the trainer waits and consumes completed groups as needed, then performs a number of mini-batch updates.
After the trainer finishes
(B × N) // Mfetch-and-train iterations, it callspause(). Under the recommended divisible configuration, exactlyB × Ntrajectories have been consumed at this point. The producer stops dispatching new requests and, based onrollout.partial, either cancels or waits for unfinished requests; complete groups already written intoPipelineBufferby the collector are not cleared and can be used in the next step.Once cleanup finishes, the system aggregates rollout metrics for this step, decides whether to save a checkpoint based on
trainer.save_freq(when saving, the yet-unconsumed prompts in thePipelineBufferare additionally recorded so that they can be regenerated after recovery), and syncs the latest model weights. SGLang flushes the prefix/KV cache corresponding to the old weights before the update.resume()increments the rollout step by one; the newly synced weights are also exposed to SGLang with an incrementedweight_version, and the producer resumes production afterwards. The version returned by SGLang is recorded in the version fields of each generated token as well as the start/end fields of the trajectory, used for monitoring and off-policy correction.
On the training side, multiple optimizer updates may be performed within the same step. When old_log_probs is first computed, the system saves the old_actor snapshot for that step; subsequent mini-batches temporarily switch back to that snapshot to compute the behavior policy probability, then switch back to the continuously updated actor, thereby avoiding drift of the old policy across mini-batches within the same step.
3. Sliding Window Strategies#
All three strategies count in prompt groups. Let:
R: number of groups already dispatched but not yet collected by the collector;Q: number of completed groups inPipelineBuffernot yet consumed by the trainer;C = int(B × (1 + stale_steps)).
Strategy |
Dispatch Constraint |
Collection Constraint |
Main Trade-off |
|---|---|---|---|
|
|
Any completed group |
Throughput-first, largest deviation in completion order |
|
|
Any completed group |
Limits how far slow samples can be crossed at the source |
|
|
Only collects the first |
Aggressive dispatch, but training sample order is closer to FIFO |
Beyond the three built-in strategies, you can register your own — see the Custom Sliding Window Strategy Development Guide.
3.1. no-window#
no-window only controls total capacity:
dispatch_count = max(0, C - R - Q)
Any complete group that finishes first can enter PipelineBuffer. Therefore, slow requests do not block subsequent prompts, and this usually achieves the highest GPU utilization; however, training data will be more biased towards easy-to-complete or shorter samples, and it has the weakest preservation of the original data order.
Applicable scenarios: throughput-first tasks, tasks with fairly uniform lengths, tasks insensitive to data order, or tasks with strong off-policy corrections already enabled. Under this strategy, stale_steps directly represents the additional global in-flight/buffer capacity.
3.2. window-gated#
window-gated assigns a monotonically increasing sequence number to each dispatched group, and imposes both of the following constraints:
next_seq - oldest_uncollected_seq < C
R + Q <= C
If the earliest prompt in the window takes a long time to complete, even if subsequent prompts have completed and been consumed, new prompts cannot be continuously dispatched. Only after the earliest uncompleted sequence number moves forward can the window continue to slide; on the collection side, any completed group within the window is still allowed to enter PipelineBuffer.
Applicable scenarios: cases that need to limit the maximum distance by which a request can be crossed by subsequent data, reducing sampling bias caused by variance in completion time, while still allowing out-of-order completion within the window. The cost is that a single extremely slow group can reduce rollout utilization. Under this strategy, stale_steps directly enlarges the window between “earliest uncompleted sequence number” and “next-to-dispatch sequence number.”
3.3. windowed-fifo#
windowed-fifo has the same dispatch capacity as no-window, i.e., R + Q <= C; the difference lies on the collection side. Suppose the current earliest uncompleted sequence number is min_seq; only complete groups satisfying the following can pass from TrajQueue into PipelineBuffer:
seq - min_seq < B
Therefore, this strategy is not strictly item-by-item FIFO, but rather uses one training step’s prompt count B as the collection window: out-of-order completion is still allowed inside the window; groups outside the window remain in TrajQueue even if they finish first, until the front of the window slides forward.
Applicable scenarios: cases where the training order should be closer to the original dataset order, or that want to reduce bias introduced by short samples entering training first, while not wanting to restrict subsequent request dispatch as strictly as window-gated. stale_steps only enlarges the total dispatch capacity C; the collection window is always B and does not change with stale_steps. Note that completed groups outside the window stay in TrajQueue, still count against R and total capacity, occupy queue memory, and may cause head-of-line blocking.
4. Statistics and Metrics#
Fully-async mode reports the following new or noteworthy metrics after pause() completes at each step:
Metric |
Meaning |
Interpretation |
|---|---|---|
|
Number of complete prompt groups in |
If this stays at 0 for a long time while |
|
Number of prompt groups dropped by the collector via |
If persistently high, check reward/status filter conditions and data quality. |
|
The proportion of trajectories consumed by the trainer in this step whose first and last generated responses use different |
Measures whether a single trajectory crosses a weight update boundary; a value of 0 does not equate to fully on-policy, only that the start/end versions did not change. |
|
The maximum value of |
Greater than 1 means at least one trajectory crossed multiple weight versions; partial recovery and off-policy correction should be examined. |
|
The number of trajectories put back into the datasource buffer that belong to complete prompt groups during step cleanup; only reported when there is data to recycle. |
“Complete” means |
|
The number of trajectories dropped during step cleanup because |
A high value means step boundaries frequently interrupt partial trajectories of the same prompt. |
|
Total time spent after the trainer issues pause, waiting for the producer to cancel or await requests, clear |
When |
|
Fraction of |
Higher values mean the trainer is frequently starved by rollout production; consider raising rollout concurrency or relaxing staleness. |
Additionally, in fully-async mode, timing/rollout, timing/process_traj, timing/teacher (if enabled), and timing/train denote the cumulative time of all mini-batch calls within an outer step; timing/step, timing/save_ckpt, and timing/update_weights are still recorded per outer step.
Compared with colocate/synchronous mode, the following metrics differ in how they are computed or in what they mean:
rollout/partial_ratioandrollout/partial_span_max: in synchronous mode they are computed over the currently accepted groups of the whole batch; in fully-async mode they are computed over the trajectories actually fetched fromPipelineBufferfor this step.Training metrics such as
train/loss,train/pg_loss,train/entropy,train/grad_norm,train/approx_kl,train/clip_ratio,train/dual_clip_ratio,train/nan_ratio, andtrain/is_*: in fully-async mode, one step trains multiple mini-batches, so the worker first caches each reported value and aggregates them at step end purely by naming rule —timing/*are summed, keys ending in_maxtake the max, keys ending in_mintake the min, and everything else is averaged;perf/train_memory_allocated_maxandperf/train_memory_reserved_maxtherefore take the max. Synchronous/colocate mode typically reports the result of a single training call directly.timing/rollout: in fully-async mode, this metric mainly represents the cumulative wall-clock time the trainer spends waiting onPipelineBufferand fetching data, not the time for the background inference engine to complete a whole batch of generation. It should therefore be analyzed together with buffer size and pause delay.Fully-async mode does not execute the
offload_rollout,onload_train,offload_train,onload_rollout_weights, andonload_rollout_kvstages of colocate mode, so the corresponding timing metrics are not produced. Fully-async mode keeps models resident on independent GPUs and directly updates the rollout weights at the step boundary.
5. Experimental Results#
5.1. Experimental Setup#
Machine: 2 × H20 (16 GPUs total)
Model: Qwen3-30B-A3B (bf16, TP=4, PP=2, EP=4)
Algorithm: GRPO + IS correction + M2PO + OPSM
Dataset: dapo-math-17K
Rollout length:
max_response_length = 20KtokensEngine: SGLang + Megatron
num_prompts_per_step = 64,num_trajectories_per_prompt = 8,mini_batch_size = 128Total steps: 100
Resource allocation:
colocatemode: all 16 GPUs simultaneously host both trainer and rollout.fully_asyncmode:trainer.num_nodes = 1(8 GPUs) +rollout.sglang_replicas.regular.num_nodes = 1(8 GPUs,num_gpus_per_replica = 4, 2 replicas in total).
All fully-async experiments set rollout.partial = true and rollout.mask_offpolicy_in_partial_rollout = true. Because step 0 additionally needs to complete the first-round prefix cache warmup on the SGLang side, KV cache initialization, and the initial fill of the fully-async buffer, its cost is significantly higher than steady state; therefore, below we report both the full 100-step and the 99-step (excluding step 0) speedup numbers.
5.2. Fully-async vs. colocate#
config |
resource |
avg step (100 steps) |
speedup |
avg step (excl. step 0) |
speedup (excl. step 0) |
|---|---|---|---|---|---|
colocate |
16 |
580.79 |
1.00x |
578.63 |
1.00x |
colocate + partial |
16 |
489.63 |
1.19x |
486.34 |
1.19x |
fully_async (no-window, stale = 1.0) |
8:8 |
454.50 |
1.28x |
451.74 |
1.28x |
fully_async (windowed-fifo, stale = 1.0) |
8:8 |
434.76 |
1.34x |
424.30 |
1.36x |
fully_async (windowed-fifo, stale = 2.0) |
8:8 |
428.76 |
1.36x |
414.58 |
1.40x |
Under the best configuration (windowed-fifo, stale_steps = 2.0), the end-to-end 100-step average per-step time drops from 580.79s to 428.76s (1.36x); excluding the cold-start overhead of the first step, the steady-state speedup reaches 1.40x. Compared with the colocate baseline that also enables partial rollout, the steady-state average per-step time is reduced by about 15%.
5.3. Sliding Window Strategy Ablation#
Fixing stale_steps = 1.0, 8:8 resource allocation, and rollout.partial = true, we switch sliding_window and align the comparison with colocate:
config |
avg step (100) |
speedup |
avg step (excl. step 0) |
speedup (excl. step 0) |
pipeline_buf_size |
partial_ratio |
partial_rollout_restored |
|---|---|---|---|---|---|---|---|
colocate (baseline) |
580.79 |
1.00x |
578.63 |
1.00x |
- |
0.000 |
- |
no-window |
454.50 |
1.28x |
451.74 |
1.28x |
27 |
0.834 |
808 |
windowed-fifo |
434.76 |
1.34x |
424.30 |
1.36x |
13 |
0.852 |
920 |
Both sliding window strategies achieve 1.28x–1.36x speedup relative to colocate;
windowed-fifois about 4% faster thanno-window(100-step basis) to 6% faster (excluding step 0).windowed-fifohas a significantly smallerpipeline_buf_size(13 vs. 27): the collection window constraint keeps completed but earlier-sequence groups inTrajQueue, so trainer consumption and rollout production are more in sync;no-windowallows fast samples to enqueue without limit, so the buffer is more easily filled with early-completing short samples.The two have similar
partial_ratioandmax_partial_span, and the overall scale of partial recovery differs little;windowed-fifohas slightly higherpartial_rollout_restoredbecause its buffer is closer to full.windowed-fifois recommended by default;no-windowfits tasks with fairly uniform sample lengths, insensitivity to sampling order, or with strong off-policy corrections already enabled.
5.4. stale_steps Ablation#
Fixing sliding_window = windowed-fifo, 8:8 resource allocation, and rollout.partial = true, we sweep stale_steps and align with colocate:
config |
pipeline capacity |
avg step (100) |
speedup |
avg step (excl. step 0) |
speedup (excl. step 0) |
pipeline_buf_size |
partial_rollout_restored |
|---|---|---|---|---|---|---|---|
colocate (baseline) |
- |
580.79 |
1.00x |
578.63 |
1.00x |
- |
- |
stale = 1.0 |
128 |
434.76 |
1.34x |
424.30 |
1.36x |
13 |
920 |
stale = 2.0 |
192 |
428.76 |
1.36x |
414.58 |
1.40x |
18 |
1392 |
Raising
stale_stepsfrom 1.0 to 2.0 improves the steady-state (excluding step 0) speedup from 1.36x to 1.40x, and the steady-state avg step from 424.30s to 414.58s, a reduction of about 2.3%. The overall gain relative to colocate mainly comes from “generation and training overlap”;stale_stepsmainly provides additional buffer depth to absorb rollout jitter.The 100-step speedup (1.34x → 1.36x) is smaller than the steady-state speedup (1.36x → 1.40x): a deeper buffer takes longer to fill, and the step 0 cost of
stale = 2.0is as high as 1832.71s, significantly higher than 1470.11s forstale = 1.0, offsetting the steady-state gain of subsequent steps. The more training steps there are, the closer the end-to-end speedup will be to the steady-state number.partial_rollout_restoredrises from 920 to 1392, indicating that a wider staleness lets more trajectories cross weight updates during recovery. The corresponding off-policy bias must be controlled through the partial loss mask (rollout.mask_offpolicy_in_partial_rollout) along with the combination of IS correction / M2PO / OPSM; blindly enlarging it in the absence of these protection mechanisms is not recommended.
5.5. Summary#
For tasks with long responses (20K) and long-tailed rollouts, fully-async mode reduces end-to-end 100-step training time by about 21%–26% (1.28x–1.36x) compared with colocate; excluding the first-step cold start, the steady-state speedup reaches 1.28x–1.40x. Compared with the colocate baseline that also enables partial rollout, the steady-state average per-step time can still drop by another 12%–15%.
The larger
stale_stepsis, the more thoroughly rollout and training overlap, and the shorter the steady-state step time; but the cost of initially filling the buffer also grows, so the amortized benefit should be evaluated together with the total number of training steps, and combined with off-policy protection mechanisms. Beyond a certain value, SGLang hits its throughput ceiling at this concurrency level, the conversion rate of performance improvement drops, and it should not be increased further.Among the sliding window strategies,
windowed-fifostrikes a good balance between throughput and sample order and is the default recommendation;no-windowemphasizes throughput more, andwindow-gatedstrictly limits the window distance from the dispatch side. Choose according to task characteristics.