Trainer Architecture
Reference for rl_engine/trainer/ — scripts, classes, and options.
1 factory.py — create_trainer()
Entry point
create_trainer(args, gpu_allocations, rollout_manager) (trainer/factory.py) looks up the recipe for args.algorithm, builds a RoleWorkers group per role, wires NCCL for exchange syncs, and returns the StacxTrainer.
StacxTrainer
StacxTrainer in trainer/trainer.py owns the per-step loop and metric collection; it never touches loss math (see §6).
Driver
rl_engine/train.py owns the outer rollout loop: multi-pass reshuffle (NUM_TRAIN_PASSES_PER_ROLLOUT), resume, eval, and checkpoint cadence.
# rl_engine/train.py — outer loop (simplified)
for rollout_id in range(start_rollout_id, args.num_rollout):
rollout_data_ref = rollout_manager.generate.remote(rollout_id) # upstream: Rollout
for pass_idx in range(num_train_passes): # NUM_TRAIN_PASSES_PER_ROLLOUT
pass_data_ref = rollout_data_ref if pass_idx == 0 \
else _reshuffle_rollout_data(rollout_data_ref, pass_seed) # prime-strided seed
train_metrics = trainer.train_step(rollout_id, pass_data_ref) # StacxTrainer
trainer.update_weights() # push new weights to SGLang (sync_to_rollout roles)
trainer.save_model(rollout_id) # checkpoint trainable roles
- Reshuffle seed
= base + rollout_id + pass_idx·100003. The global pass default is1; shipped DAgger/AggreVaTe/OPD scripts set3.
2 Two-branch design
Algorithm-specific code lives entirely in the role branch. The shared backend branch — TrainBackend, realized by Megatron-LM or FSDP v2 — is identical for every algorithm: distributed setup, forward/backward/optimizer, weight sync, checkpointing, sleep/wake offload.
Trainer architecture
3 config.py — Recipes as data
An algorithm is a frozen TrainingConfig in trainer/config.py. Three dataclasses:
ModelRole
One named model. Key fields: name, specialization_cls, backend, trainable, shares_pool_with, sync_to_rollout, init_kwargs.
DataSync
A declared transfer: source → target, keys, and stage = "inject" (Ray object store) or "exchange" (NCCL, §6).
TrainingConfig
name + a tuple of roles + a tuple of syncs, validated on construction (unique roles, syncs reference real roles, no transitive pool-sharing).
Recipes register in TRAINING_CONFIGS via @recipe in trainer/recipes.py — 22 built-in; the full catalog is on the Algorithm Recipes page.
# trainer/recipes.py
@recipe("grpo")
def grpo_config():
return TrainingConfig(
name="grpo",
roles=(ModelRole(name="policy", specialization_cls=ActorSpecialization,
sync_to_rollout=True, init_kwargs={"with_ref": True}),),
syncs=(), # reward-only advantages, no cross-role data
)
@recipe("dpo_distributed")
def dpo_distributed_config():
return TrainingConfig(
name="dpo_distributed",
roles=(ModelRole(name="policy", specialization_cls=DPOSpecialization),
ModelRole(name="reference", specialization_cls=DPOSpecialization, trainable=False)),
syncs=(DataSync(source="reference", target="policy", keys=("log_probs",),
target_keys=("ref_log_probs",), stage="inject"),),
)
4 role_workers.py — RoleWorkers & WorkerPool
- Role names use RL terminology (
policy,value,reference,reward); the classes behind them are function-named (ActorSpecialization,CriticSpecialization), selected byspecialization_clsrather than a role-string switch.build_gpu_allocations(factory.py) maps slime placement-group keys onto role names.
One model → many roles
DualRoleSpecialization serves actor and critic from a single set of weights in one forward/backward pass (recipe ppo_colocated).
Many roles → one pool
shares_pool_with lets a second role reuse another's WorkerPool — same GPUs, a second specialization on the same backend, picked per call by specialization_key (recipe ppo_colocated_async_shared).
Frozen roles
trainable=False roles (the DPO/GRPO reference) only run forward passes to feed inject syncs — never optimized, never synced back to rollout.
5 specialization/base.py — ModelSpecialization
Specialization class hierarchy
The three-method interface
compute_loss(model_output, batch, reducer) → (loss, metrics)
Raw model output (logits or velocity) + prepared batch + a CP-aware reducer → scalar loss and a metrics dict.
prepare_batch(rollout_data) → list[dict]
One rollout-data reference → forward-ready micro-batch dicts: sequence packing, attention masks, position IDs, plus algorithm-specific fields (reference/teacher log-probs, noise samples).
extract_model_output(model, batch) → Tensor
Pulls the tensor compute_loss needs from the forward pass — response logits (decoders), the value-head scalar (critics), or predicted velocity (diffusion).
Optional: compute_advantages(rollout_data) — no-op on base; overridden by the RL-actor and diffusion specializations.
Adding an algorithm
# 1. A specialization — implement the interface (specialization/base.py contract)
class MySpecialization(ModelSpecialization):
def prepare_batch(self, rollout_data): return [...] # micro-batch dicts
def extract_model_output(self, model, batch): return model(...) # logits / velocity
def compute_loss(self, model_output, batch, reducer):
loss = ...
return reducer(loss), {"my_loss": loss.detach()}
# 2. Register it as data (trainer/recipes.py)
@recipe("my_algorithm")
def my_algorithm_config():
return TrainingConfig(name="my_algorithm",
roles=(ModelRole(name="policy", specialization_cls=MySpecialization, sync_to_rollout=True),))
# 3. Use it
python -m rl_engine.train --algorithm my_algorithm ...
MixedLossDaggerOPDSpecialization(specialization/mixed_loss_dagger.py) blends per-token CE (teacher-executed tokens) and K3 reverse-KL (student-executed) on a β schedule: β=1 → pure CE, β=0 → pure K3.
6 StacxTrainer step loop
StacxTrainer.train_step(): run inject syncs → dispatch train() to each trainable role, async for independent pools and serialized for shared.
One backend training step
Sync mechanisms
"inject" — trainer-orchestrated
The trainer forwards the source role, collects its output via the Ray object store, and merges the declared keys into the target before train().
Used by
dpo_distributed (reference → policy), online_reward (reward → policy)
Trade-off
Works across different GPU sets and backends; adds a forward pass and a memory hop.
"exchange" — NCCL-direct
Both roles run train() concurrently; inside each backend, WorkerSync (infra/worker_data_sync.py) swaps tensors via NCCL between forward and backward.
Used by
ppo and ppo_colocated_async (values ↔ log-probs)
Trade-off
Fast and on-GPU, no extra forward; needs same-rank NCCL groups and lock-step pools.
7 Directory reference
| Path | What it holds | Branch |
|---|---|---|
factory.py | create_trainer(), infer_algorithm(), build_gpu_allocations() | Entry |
trainer.py | StacxTrainer — train_step, inject syncs, update_weights, save_model, sleep/wake | Orchestration |
config.py | Frozen ModelRole · DataSync · TrainingConfig dataclasses | Orchestration |
recipes.py | @recipe registry → TRAINING_CONFIGS (22 recipes) | Orchestration |
role_workers.py | RoleWorkers — role-aware wrapper over a WorkerPool | Workers |
ray/ | WorkerPool (actor lifecycle) · TrainWorker (wraps TrainBackend) | Workers |
backends/ | base.py (TrainBackend ABC) · megatron.py · fsdp.py | Shared infra |
specialization/ | base.py · causal_decoder.py · agent.py · diffusion.py · distill.py · mixed_loss_dagger.py | Algorithm |
objectives/ | Stateless loss math — policy_loss · value_loss · dpo · kl · advantage · logprob · distillation_loss · diffusion_loss · divergences | Algorithm |
infra/ | Model-agnostic: optimizer · checkpoint · weight_manager · process_groups · worker_data_sync · profiler · distributed · logging | Shared infra |
See Backends for infra/ internals and Algorithm Recipes for the diffusion objectives.
8 Trainer knobs
Defined in rl_engine/train.py; parallelism/optimizer flags from slime's argument parser via scripts/train/swe/. Full STACX_* catalog: scripts/train/swe/ENV_REFERENCE.md.
| Flag / env | Default | Effect |
|---|---|---|
--algorithm <name> | grpo | Selects the TRAINING_CONFIGS recipe (§3). If unset, inferred from --advantage-estimator (default grpo). |
--global-batch-size | 256 | Optimizer GBS. Gradient steps per pass ≈ rollout_batch_size / GBS. train.py |
--num-train-passes-per-rolloutSTACX_NUM_TRAIN_PASSES_PER_ROLLOUT | 1 | Re-iterate each rollout's data N× with reshuffle before the next rollout (§1). train.py |
--use-dynamic-batch-size--max-tokens-per-gpu <N> | 32768 (4B)8192 (8B) | Dynamic micro-batch packing to a per-GPU token cap instead of a fixed sequence count. Lower it to relieve OOM. |
--lr <f> | 3e-6 (RL: 1e-6) | Constant learning rate (warmup 0 in the shipped recipes). |
--ref-load <dir> | — | Frozen in-Megatron reference weights for the KL anchor (GRPO low-var KL, DPO colocated ref); points at a *_torch_dist checkpoint. |
--load / --save | — | Checkpoint load/save roots; drive the resume path via resume_state.json. |
| TP / PP / DP / CP sizes | per recipe | Megatron parallelism (e.g. --tensor-model-parallel-size 4). Covered on Backends. |
Mixed-loss run metrics
The DAgger-OPD specialization emits ce_mean, k3_mean, and teacher_action_frac; the last tracks the β schedule as it decays.