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

2 Two-branch design

Algorithm-specific code lives entirely in the role branch. The shared backend branchTrainBackend, 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

Shared infrastructure (role-agnostic)
Role / algorithm-specific
Specialization options
Diffusion family
STACX Trainer Architecture StacxTrainer forks into a shared TrainBackend branch (Megatron / FSDP) and a role-specific RoleWorkers to ModelSpecialization branch. StacxTrainer trainer.py · owns the loop · dispatches to roles · runs syncs shared infrastructure role-specific algorithm TrainBackend backends/base.py · forward → backward → optimizer BACKEND OPTIONS Megatron-LM TP · PP · DP · CP FSDP v2 device mesh · HF transformer models + add a backend… identical for every algorithm RoleWorkers · WorkerPool role_workers.py · ray/worker_pool.py per-GPU TrainWorker Ray actors holds ModelSpecialization specialization/base.py · backend calls compute_loss() SPECIALIZATION OPTIONS Actor Critic Reward DPO DualRole MixedLossDaggerOPD Distill Diffusion MMDiT + add spec… TrainWorker (ray/train_worker.py) binds one TrainBackend to its role's specialization — backend.train() calls spec.compute_loss()

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: sourcetarget, 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

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

ModelSpecialization hierarchy ModelSpecialization splits into CausalDecoderSpecialization with seven subclasses and DiffusionSpecialization with three subclasses. ModelSpecialization (ABC) compute_loss · prepare_batch · extract_model_output CausalDecoderSpecialization causal_decoder.py · CE loss + log-probs + packing (= SFT) DiffusionSpecialization diffusion.py · flow matching / velocity prediction subclasses · agent.py · distill.py · mixed_loss_dagger.py ActorSpecialization PPO / GRPO · advantages CriticSpecialization clipped value loss RewardSpecialization reward-model loss DPOSpecialization preference loss DualRoleSpecialization colocated actor + critic DistillSpecialization reverse-KL · top-K / K3 MixedLossDaggerOPDSpecialization β-mixture rollin · per-token CE (teacher) / K3 (student) subclasses · diffusion.py MMDiTSpecialization Flux / SD3 / Lumina dual-stream DiffusionDPOSpecialization preference-weighted denoising DiffusionNFTSpecialization noise-free reward tuning

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 ...

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

One backend training step Rollout data is preprocessed, packed into micro-batches, forwarded through compute_loss, backpropagated, and the updated weights are synced to SGLang. × NUM_TRAIN_PASSES_PER_ROLLOUT — reshuffle each pass rollout_data list[Box] from RolloutManager preprocess DP-shard · move to CUDA prepare_batch pack into micro-batches forward + compute_loss specialization backward · clip optimizer.step() update_weights → SGLang engines + ref / teacher / old log-probs & worker sync (skipped for pure SFT)

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

PathWhat it holdsBranch
factory.pycreate_trainer(), infer_algorithm(), build_gpu_allocations()Entry
trainer.pyStacxTrainertrain_step, inject syncs, update_weights, save_model, sleep/wakeOrchestration
config.pyFrozen ModelRole · DataSync · TrainingConfig dataclassesOrchestration
recipes.py@recipe registry → TRAINING_CONFIGS (22 recipes)Orchestration
role_workers.pyRoleWorkers — role-aware wrapper over a WorkerPoolWorkers
ray/WorkerPool (actor lifecycle) · TrainWorker (wraps TrainBackend)Workers
backends/base.py (TrainBackend ABC) · megatron.py · fsdp.pyShared infra
specialization/base.py · causal_decoder.py · agent.py · diffusion.py · distill.py · mixed_loss_dagger.pyAlgorithm
objectives/Stateless loss math — policy_loss · value_loss · dpo · kl · advantage · logprob · distillation_loss · diffusion_loss · divergencesAlgorithm
infra/Model-agnostic: optimizer · checkpoint · weight_manager · process_groups · worker_data_sync · profiler · distributed · loggingShared 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 / envDefaultEffect
--algorithm <name>grpoSelects the TRAINING_CONFIGS recipe (§3). If unset, inferred from --advantage-estimator (default grpo).
--global-batch-size256Optimizer GBS. Gradient steps per pass ≈ rollout_batch_size / GBS. train.py
--num-train-passes-per-rollout
STACX_NUM_TRAIN_PASSES_PER_ROLLOUT
1Re-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 / --saveCheckpoint load/save roots; drive the resume path via resume_state.json.
TP / PP / DP / CP sizesper recipeMegatron 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.