1 Position in the System

Everything in rl_engine/rollout/ answers one question each trainer iteration: given the current student weights, produce the trajectories to train on. The entry point is SchedulerRolloutManager — a Ray actor subclassing slime's RolloutManager that routes generation and evaluation through a TaskScheduler.

This page — Architecture

The manager actor, the directory tour, the episode abstraction, the data path, and the trajectory post-processing lanes.

Task Scheduler sibling

rollout-scheduler.html — batching strategies, PlacementPool resource gating, the batch lifecycle, and the multi-dataset param cascade in depth.

Inference Backend sibling

rollout-inference.html — SGLang serving, weight sync (inject vs. exchange), and GPU memory offload between training and rollout phases.

SchedulerRolloutManager

Ray forbids inheriting from a @ray.remote actor class directly, so rollout_manager.py unwraps the decorator, subclasses the plain base, and re-applies it:

# rl_engine/rollout/rollout_manager.py
from slime.ray.rollout import RolloutManager as _RolloutManagerActor

# Unwrap the @ray.remote decorator to get the plain Python class.
_RolloutManagerBase = _RolloutManagerActor.__ray_metadata__.modified_class

@ray.remote
class SchedulerRolloutManager(_RolloutManagerBase):
    # overrides: __init__, dispose, _get_rollout_data, eval (+ helpers)
    ...

It overrides only what it must, inheriting the rest from slime:

OverriddenWhy
__init__Parse --rollout-config YAML, build the TaskScheduler, and swap in a MultiDataSource when a rollout config is present.
_get_rollout_data(rollout_id)The heart of this layer: drive TaskScheduler.execute_rollout(), apply the filters, then either expand DAgger sub-samples or capture teacher logprobs (§5).
eval(rollout_id, eval_label=None)Drive TaskScheduler.execute_eval(), then compute per-dataset accuracy and pass@k.
dispose / prune_env_nodesAPI-eval-only safety and between-rollout podman overlay cleanup on env nodes.
Inherited unchanged from slimeRole
generate(rollout_id)Calls _get_rollout_data(), post-processes rewards, converts to columnar data, and splits by DP rank via _split_train_data_by_dp().
_convert_samples_to_train_data, _post_process_rewards, _split_train_data_by_dpSample → columnar tensor conversion, reward normalization, DP partitioning.
offload / onload, health monitoring, fault toleranceSGLang lifecycle and actor resiliency — see Inference Backend.

The output format is list[Box], not a single dict

generate() is inherited unchanged — it returns a list[Box] (one Box per DP rank) that the trainer's process_rollout_data() consumes natively. The manager overrides only what goes into the samples (_get_rollout_data), not this envelope.

Two construction paths

placement_group.py exposes two factories — the default scheduler-backed path, and a legacy bypass for ablation runs:

FactorySelected byBuilds
create_rollout_manager(args, pg)defaultSchedulerRolloutManager — full task-scheduler path. Also resolves num_rollout from num_epoch.
create_legacy_rollout_manager(args, pg)--use-legacy-rolloutslime's original RolloutManager directly — a training-only path that bypasses the scheduler.

Top-level architecture

One rollout step flows left to right — data source to scheduler, a short-lived Evaluator per batch, one episode handler per sample, and back out as list[Box]:

SchedulerRolloutManager top-level architecture SchedulerRolloutManager — Ray actor subclasses slime RolloutManager · overrides __init__ / _get_rollout_data / eval · inherits generate() MultiDataSource FIFO over datasets tags task_type groups TaskScheduler PlacementPool GPU · CPU · mem budget fifo_batching group by task_type → Batch Evaluator factory EvaluatorSpec → Evaluator per batch, short-lived holds one placement token for the whole episode SWE Evaluator evaluator_id="swe" in-house DAggerSWEAgent (native token access) init → run → eval → cleanup Retool Evaluator evaluator_id="retool" tool-use agent lane init → run → eval → cleanup SWEBench Evaluator evaluator_id="swebench" installed scaffold (OpenHands / Terminus-2) init → run → eval → cleanup generate() → list[Box] / DP rank Trainer.process_rollout_data()

One rollout step. Task types (swe, retool, swebench, …) are illustrative; the evaluators present in a run are whatever build_evaluators registers.

2 Directory Tour

Paths are relative to rl_engine/rollout/.

Top level

PathContents
__init__.pyExports SchedulerRolloutManager, create_rollout_manager.
rollout_manager.pyThe SchedulerRolloutManager actor: _get_rollout_data, eval, the filters, DAgger sub-sample expansion, and both teacher-capture lanes.
placement_group.pycreate_rollout_manager / create_legacy_rollout_manager (§1).
multi_data_source.pyMultiDataSource, DatasetEntry, parse_rollout_datasets_config, resolve_dataset_path.
eval_dataset_config.pyEvalDatasetConfig — slime's config extended with task_type, max_turns, max_context_len.
termination_summary.pyFailure-class taxonomy + per-iter summary.json writer (§5).
distill_capture.pyTeacher force-decode (capture_teacher_logprobs_for_samples, score_response_with_topk) with retry/backoff.
kl_eval_cache.pyPer-iter student eval-trajectory dump (dump_samples_from_eval).

scheduler/ — the control plane deep dive: Task Scheduler

PathContents
scheduler/__init__.pyPackage exports + build_task_scheduler factory + _build_placement_pool.
scheduler/task_scheduler.pyTaskScheduler, Batch, BatchingStrategy, fifo_batching.
scheduler/evaluator.pyEvaluatorSpec (blueprint) + Evaluator (per-batch executor).
scheduler/resource.pyResource, NodeSpec, PlacementPool, PlacementToken.
scheduler/task.pyTask, TaskConfig, StoppingCriteria, TaskStatus.

agents/ — episode handlers

PathContents
agents/base.pyBaseAgent (episode handler), AgentContext, AgentResult (§3).
agents/installed.pyInstalledAgent + InstalledScaffold spec for black-box scaffolds.
agents/installed_scaffolds/openhands.py, terminus2.py (+ in-sandbox terminus2_runner.py).

The in-house SWE / DAgger handlers (SWEAgent, DAggerSWEAgent) live with their lane in rl_engine/examples/swe_agent/agents/, not here — rollout/agents/ holds only the base contract.

backends/ · proxy/ · sandbox/ · utils/

PathContents
backends/Model I/O: SGLangBackend, OpenAIChatBackend, TeacherBackend, TokenTracker, DAggerTracker, api_pricing.
proxy/recording_proxy.pyRecordingProxy — a per-task FastAPI proxy that captures tokens for black-box installed scaffolds that never expose token IDs natively.
sandbox/rock.pyRockSandbox — the ROCK client (start / execute / execute_in_session / stop, leases). See Sandbox Management.
utils/tool_calling, action_format, dagger_schedule.

3 The Episode Abstraction

The scheduler knows nothing about SWE-Bench, tool calls, or teachers — all of that lives behind one interface, BaseAgent (agents/base.py). A handler owns the entire episode for one sample, so both black-box scaffolds (OpenHands, Terminus-2) and native token-level agents (DAgger) fit one contract.

The three data types

TypeRole
BaseAgentAbstract handler. Async lifecycle init → run → eval → cleanup; subclasses must implement the first three, cleanup defaults to a no-op.
AgentContextPer-run inputs the Evaluator populates: args, placement (node/GPU, set only during resource-gated phases), sampling_params, max_turns, max_context_len, timeout, sglang_url. (proxy_url is populated later by the handler, e.g. InstalledAgent._maybe_start_proxy, not by the Evaluator.)
AgentResultEpisode output: output_text, metadata, task-specific patch / trajectory, and optional token-level fields (prompt_token_ids, per-turn completion_token_ids, per-turn logprobs, loss_mask).
# agents/base.py — the contract every scaffold implements
class BaseAgent(abc.ABC):
    async def init(self, sample, context)          -> None:  # lightweight, no placement yet
    async def run(self, sample, context)           -> AgentResult:  # the full multi-turn loop
    async def eval(self, sample, context, result)  -> float | dict:  # reward
    async def cleanup(self, sample, context)       -> None:  # finally-block; stop containers

Reward

There is no standalone Reward object: the reward comes from handler.eval() — for SWE, the bench's verifier scored 0/1 — and the Evaluator stores it as sample.reward.

How the Evaluator runs one sample

scheduler/evaluator.py drives the lifecycle in _execute_sample. Three guarantees hold:

  • The semaphore spans the whole episode. async with self._semaphore wraps init → run → eval → cleanup, capping live samples at Resource.concurrency — not turns.
  • One placement token spans run and eval. Acquired before run, released in the finally after cleanup, so the patch and verification containers never double-count against a node's budget.
  • Failures degrade to a masked dummy. Any exception marks the sample ABORTED with 2-token dummy data ([0, 0], remove_sample=True, zero gradient); classify_abort buckets it and _annotate_abort attaches diagnostics (phase, elapsed vs. window, node IP).

After eval, _populate_token_data lifts the token-level fields off AgentResult onto the Sample — what makes a trajectory trainable, whether captured natively or through the recording proxy.

The multi-turn loop

Inside run(), a native handler like DAggerSWEAgent (overriding _agent_loop) alternates policy generation with environment steps, tagging every token with a loss mask.

The multi-turn episode loop handler.run() — one turn of the episode loop BaseAgent owns the interaction end-to-end; the Evaluator holds one placement token for the whole episode loop while turns < max_turns AgentContext sglang_url · proxy_url sampling_params max_turns max_context_len SGLang server generate(action) student + teacher token_ids + logprobs TokenTracker append_tokens(loss_mask) 1 = teacher action 0 = student / observation RockSandbox execute(action) SWE-Bench container → observation (mask 0) context check len < max_context? finish? max_turns? context tokens action obs finish / done → eval handler.eval() → reward SWE-Bench verify in a fresh sandbox · score 0/1 overflow tracker.rollback(n) termination_reason = context_overflow

The per-turn loop. Rollback discards the tokens that would overflow the window and ends the episode cleanly rather than truncating mid-token.

Where the loss mask comes from

In DAgger rollins, teacher-executed action tokens get loss_mask=1; student actions and all tool observations get 0. The two rollin protocols (per-turn β-mixture vs. AggreVaTe student-prefix) are selected by STACX_DAGGER_SAMPLING_MODE; how these masks become CE vs. K3 targets is §5 and Algorithm Recipes.

4 The Data Path

Each prompt must reach the right handler with the right stopping criteria. Three pieces cooperate: MultiDataSource serves prompts, DatasetEntry / EvalDatasetConfig carry per-dataset overrides, and task_type routes each sample to an EvaluatorSpec.

MultiDataSource

multi_data_source.py replaces slime's single-file DataSource so one rollout step can span heterogeneous task types. It serves datasets in FIFO order (dataset A exhausted before B). Per sample it deep-copies the prompt, stamps metadata["task_type"] and metadata["_dataset_cfg"] (the full DatasetEntry), and expands n_samples_per_prompt into a group.

parse_rollout_datasets_config builds the DatasetEntry list from YAML; resolve_dataset_path anchors relative paths at the repo root (or STACX_ROOT).

# --rollout-config YAML — parse_rollout_datasets_config()
rollout:
  defaults:                  # shared across datasets
    task_type: swe
    max_context_len: 65536
  datasets:
    swe_gym_train:
      path: data/swe/swe_gym_train_minus_val100.jsonl
      input_key: prompt
      label_key: instance_id
    retool_math:
      path: data/retool_math.jsonl
      task_type: retool     # overrides the default
      max_context_len: 32768

Structural fields

path, input_key, label_key, metadata_key, tool_key, task_type — what loads and where it routes.

Generation overrides

temperature, top_p, top_k, max_response_len, max_turns, max_context_lenNone means "inherit".

Rollout overrides

n_samples_per_prompt, reward_key — per-dataset sampling and scoring.

EvalDatasetConfig

The eval-side twin: slime's config plus task_type, max_turns, max_context_len.

The 3-level cascade

Generation parameters resolve from coarsest to finest, the per-dataset value winning:

CLI args      →      EvaluatorSpec.default_stopping      →      DatasetEntry / EvalDatasetConfig
(coarsest)                                                                        (finest, wins)

Resolved by the scheduler's _resolve_stopping (for StoppingCriteria) and the sampling-param builder; the Task Scheduler page documents it in full.

Routing: task_type is the evaluator_id

An EvaluatorSpec (scheduler/evaluator.py) is a blueprint: a handler (a BaseAgent), its resource model (resource / run_resource / eval_resource), an optional batch_size, and default_stopping. The scheduler selects one via batch.task_type, so a dataset's task_type must equal some spec's evaluator_id or the batch has nowhere to run.

Specs come from a build_evaluators(args) factory (list[EvaluatorSpec]), loaded via --evaluator-config-path and wired into a TaskScheduler by build_task_scheduler.

# rl_engine/examples/swe_agent/evaluator_config_dagger.py
def build_evaluators(args):
    teacher = TeacherBackend(sglang_url=..., model_path=..., tools=SWE_TOOL_SPECS)
    handler = DAggerSWEAgent(teacher_backend=teacher, max_turns=100, ...)
    return [EvaluatorSpec(
        evaluator_id="swe",                     # == dataset task_type
        handler=handler,
        run_resource=Resource(cpu=4, memory_gb=16),   # agent container
        eval_resource=Resource(cpu=4, memory_gb=16),  # verification container
        default_stopping=StoppingCriteria(max_turns=100, max_context_len=65536),
    )]

5 Trajectory Post-Processing Lanes

Once execute_rollout() returns raw trajectories, _get_rollout_data shapes them for the loss. The order is fixed — filter → route by algorithm → shape — with the lane chosen entirely by args.algorithm.

Filters

Two independent filters run before shaping, on the raw one-per-task trajectories:

FilterEnv varKeeps
Rejection samplingSTACX_REJECTION_SAMPLINGreward > 0. Modes: all / warmup_only (only while β hasn't started decaying) / none (default).
Finish-onlySTACX_FINISH_ONLY_FILTERtermination_reason == "finish" — drops context_overflow, action_budget, loops, max_turns. The launch scripts default this on.

They compose: rejection first, finish-only on the survivors. If a filter empties the batch, _get_rollout_data returns one masked dummy so logging never trips on an empty rollout.

The algorithm fork

_maybe_apply_per_token_capture is the switch: per-token-loss algorithms capture teacher logprobs and return immediately (no chunk expansion); everything else falls through to CE-only expansion.

args.algorithmLaneShape
on_policy_distillation (OPD)_apply_distillation_capture1 trajectory → 1 packed sample; K3 reverse-KL on student action tokens.
mixture_dagger_opd · mixture_aggrevate_opd_apply_mixed_loss_dagger_opd_capture1 trajectory → 1 packed sample; per-token mixed CE/K3 via a sentinel.
otherwise (CE-only online-DAgger SFT)_build_trajectory_sft_samples1 trajectory → M+1 chunked SFT sub-samples.

Teacher force-decode capture (per-token lanes)

Both per-token lanes call capture_teacher_logprobs_for_samples (distill_capture.py): kept trajectories are force-decoded through the teacher SGLang server with top_logprobs_num=K, yielding the teacher's chosen-token logprob at each response position. That pins sample.teacher_log_probs and overwrites sample.loss_mask with the action mask, so the loss reduces only over student action tokens.

The mixed CE/K3 lane snapshots the agent's per-token is-teacher mask, then overwrites teacher_log_probs with a +1.0 sentinel at teacher positions. Real SGLang logprobs are always ≤ 0, so the kernel reads the sign — > 0 ⇒ CE, ≤ 0 ⇒ K3 — meaning β=1.0 is pure CE (≡ legacy online-DAgger) and β=0.0 pure K3 (≡ OPD). The loss kernel lives in rl_engine/trainer/specialization/mixed_loss_dagger.py — see Algorithm Recipes.

DAgger trajectory-SFT sub-samples (CE-only lane)

The chunk-expansion lane (_build_trajectory_sft_samples) is the older CE-only path: each trajectory with M student-executed turns becomes up to M+1 plain-CE SFT sub-samples, driven by the _student_cut_points metadata.

_subsample_kindWhenWhat it trains
dagger_pairone per valid student cutprefix up to the student's action, with the student's action replaced by the teacher relabel tokens (all mask=1) plus any teacher actions in the preceding gap.
trailing_chunkteacher turns after the last student cutthe full trajectory, masked to only the trailing teacher actions.
teacher_only_trajectoryno cut points (β=1.0, or a legacy .pt)the whole trajectory verbatim — the one case where 1 trajectory = 1 sample.

A per-trajectory buffer makes the drop atomic — if any sub-sample exceeds max_seq_len the whole trajectory is discarded (semantics pinned by tests/rollout/test_trajectory_sft_samples.py). A deterministic per-iter shuffle (STACX_SHUFFLE_SEED_BASE) de-clusters sibling chunks; optional dynamic GBS scaling (STACX_TASKS_PER_GRAD_STEP) holds tasks-per-step constant as chunks grow.

Termination taxonomy & per-iter summary

termination_summary.py classifies outcomes. classify_termination(reward, termination_reason) maps every trajectory into one of eight classes; write_summary aggregates a pass into a summary.json with per-dataset counts and a per-task list.

ClassMeaning
solvedreward == 1.0.
finish_unresolvedagent called finish but the answer was wrong.
looprepeated_action_loop.
ctx_overflowcontext_overflow.
budgetaction_budget or max_turns.
timeoutaborted on the agent window or a sandbox/request timeout (classify_abort → agent_timeout).
infraaborted on a sandbox/transport error, not a model outcome (infra_conn / infra_sandbox / infra_other).
otherany unrecognized termination_reason.

Analysis dumps

Three optional, best-effort dumps capture token-level data for offline analysis. All are atomic (tmp + os.replace) and swallow failures so they can never abort training:

  • OPD discrepancy dump (STACX_DISTILL_DUMP_DIR) — per-iter .pt with the exact (teacher, student) logprob pair the K3 estimator consumes, plus the action_mask.
  • Mixed-loss dump (STACX_MIXED_LOSS_DAGGER_DUMP_DIR) — the sentinel'd teacher_log_probs and the original is-teacher mask, so tools can split CE vs. K3 per position.
  • Per-iter student dump (STACX_KL_DUMP_DIR, kl_eval_cache.py) — dump_samples_from_eval writes one .pt per finished eval task (tokens, loss_mask, rollout logprobs, reward) plus a manifest.json; armed by default in the launch scripts.

6 Rollout-Layer Env Knobs

The STACX_* variables read inside rollout_manager.py and scheduler/task_scheduler.py — the rollout-layer slice of the full catalog (with launch-script defaults) in scripts/train/swe/ENV_REFERENCE.md.

Data path & filters

Env varDefaultEffect
STACX_FINISH_ONLY_FILTER0 (scripts set 1)Keep only termination_reason == "finish".
STACX_REJECTION_SAMPLINGnoneKeep reward > 0: all / warmup_only / none.
STACX_SHUFFLE_SEED0Per-experiment dataset shuffle seed in MultiDataSource.
STACX_SHUFFLE_SEED_BASE42Base for the per-iter sub-sample shuffle (+ rollout_id).
STACX_TASKS_PER_GRAD_STEPunsetDynamic GBS: scale global_batch_size to hold tasks/step constant as chunks/trajectory grows.
STACX_REUSE_ROLLOUT · STACX_FORCE_FRESH_ROLLOUT0Load the saved trajectory .pt (multi-epoch) vs. force a fresh rollout every iter (true iterative DAgger).
STACX_SAVE_PER_ITER_PT1Also write a per-iter trajectory_sft_samples_iter_NNN.pt snapshot.
STACX_ROOT · DAGGER_OUTPUT_DIRrepo root · /tmp/dagger_dataDataset-path anchor · trajectory .pt + JSON debug dumps.

Teacher force-decode capture

Env varDefaultEffect
TEACHER_SGLANG_URLrequired (OPD/mixed)Teacher SGLang server for force-decoding student action tokens.
STACX_DISTILL_TOPK20Top-K logprobs captured per token (K3 uses the chosen logprob).
STACX_DISTILL_TEACHER_CONCURRENCY4Concurrent teacher force-decode requests.
STACX_DISTILL_TEACHER_RETRIES5Retry attempts per force-decode request.
STACX_DISTILL_TEACHER_BASE_DELAY2.0Exponential-backoff base delay (seconds).

Analysis dumps

Env varDefaultEffect
STACX_DISTILL_DUMP_DIRempty = offOPD per-iter teacher↔student discrepancy dump.
STACX_MIXED_LOSS_DAGGER_DUMP_DIRempty = offMixed-loss per-iter dump (sentinel + is-teacher mask).
STACX_KL_DUMP_DIRempty; scripts arm itPer-iter student eval-trajectory dump.
STACX_KL_DUMP_DATASET · STACX_KL_DUMP_FORCE_LABELunsetRestrict the student dump to one eval dataset · override its label.

Do not set these by hand

STACX_ROLLOUT_ID, STACX_DAGGER_BETA, and STACX_DAGGER_TAIL_T are written per-iter by scheduler/task_scheduler.py from the DAgger schedule. Setting them manually is overwritten and can corrupt the schedule — see ENV_REFERENCE §C.

7 Where To Go Next

Task Scheduler

rollout-scheduler.html — batching strategies, the PlacementPool allocator, the batch lifecycle, task state machine, and the multi-dataset param cascade.

Inference Backend

rollout-inference.html — SGLang serving, weight sync (inject vs. exchange), the onload/offload lifecycle, and CUDA-graph / KV-cache memory management.

Sandbox Management

env-sandbox.html and environment.html — the ROCK sandbox lifecycle behind RockSandbox that every episode steps against.

Algorithm Recipes

recipes.html — how the CE/K3 masks and sentinels from §5 become the mixed-loss DAgger, AggreVaTe, OPD, GRPO, and iter-SFT objectives.