Code Architecture
Runtime loop, module map, data flow, and package boundaries.
unirl/ is the framework package — the layered, composable core that turns one
Hydra recipe into a multi-GPU RL training run. This README is the map of the
source tree: what the layers are, how they depend on each other, and which
module README to read next. (For what UniRL is and how to launch it, see the
top-level README; for how tensors move across GPUs at runtime,
see distributed/README.md.)

UniRL is a thin entrypoint that hands a recipe to the trainer (the driver),
which places a Worker pool and wires each loop component as a Remote from the
recipe's _target_ — not by import. (Deliberately not "stage": in this repo a stage
is a trainable unit of a model pipeline — DiffusionStage / ARStage, see
models/README.md — never a position in the training loop.)
As source, the package falls into four groups:
- Entrypoints (
train_diffusion.py,train_ar.py,train_pe.py,train_unified_model.py, plustrain_agentic.pyfor multi-turn training) — each composes and validates a Hydra recipe, then hands off to its trainer. - Orchestration (
trainer/) — the per-domain<Domain>Trainerowns GPU placement, builds the rollout and train workers, and runs the rollout→reward→advantage→train loop. - Training loop (
rollout/,reward/,algorithms/,train/) — the four pluggable components of one rollout, plus what they share:models/(per-model bundles),sde/(step kernels / σ schedule), anddata/(sources). - Foundation (
distributed/,config/,types/) — the cross-cutting runtime and contracts every layer rests on: Ray worker/dispatch/transport, config validation, and shared typed data.utils/contains small supporting mechanisms; it is not an ownership layer or a default destination for code.
Module Map
| Path | Responsibility |
|---|---|
train_*.py | Hydra entrypoints for diffusion, AR, prompt enhancement, unified models, and service-scored barrier agentic training |
trainer/ | Training lifecycle (base.py plus domain trainers and AgenticTrainer): owns placement, builds workers, and runs the rollout→reward→advantage→train loop |
config/ | require + validate_* cross-component validators over the flat Hydra recipe (instantiation itself is _target_-driven, not in this module) |
distributed/ | Ray worker base (Remote) + placement/dispatch (group/), tensor transport (tensor/), and weight sync (weight_sync/) |
rollout/ | Rollout engine contracts and implementations (engine/: trainside, sglang, sglang_diffusion, vllm_omni, fastvideo, composed, agentic) |
train/ | Train stack: TrainStack, FSDP backend, LoRA/DiffusionNFT/mirror injection, EMA shadow, optimizer/lr |
algorithms/ | Per-track loss algorithms (GRPO, DiffusionNFT, FlowDPPO, DRPO) |
models/ | Per-model bundles, pipelines, stages, conditions; text/vision/vae helpers |
reward/ | RewardService holding one backend — local scorers or the remote HTTP client |
sde/ | SDE step kernels, σ schedule/shift, SDE-index schedule, initial-noise generation (the NoiseRecipe contract lives in types/) |
types/ | Shared typed contracts: Sample / Part, primitives, conditions, segments, rewards, sampling; includes the request/response migration guide |
data/ | Data source and dataset readers |
utils/ | Domain-agnostic leaves with several owners: logging, dtype, media/video, metric aggregation, profiling, memory monitoring |
Ownership and dependency direction
The source tree is not one rigid linear stack: rollout adapters may translate
concrete model wire formats, and train/ consumes algorithm and model
contracts. The stable direction is nevertheless simple:
entrypoints / trainer compose and orchestrate
↓
rollout · reward · algorithms · train own loop policy
↓
models · data · sde own domain policy and transformations
↓
types · config · distributed own stable contracts and mechanismsImports should point downward. Integration code that must understand both a backend and a model belongs at that integration seam (normally a rollout or train-backend adapter), not in generic infrastructure. In particular:
- concrete model packages do not import trainers, algorithms, rewards, or rollout engines;
- algorithms depend on stage/type contracts, not concrete model families;
- data sources do not depend on trainers, models, or training backends;
- distributed mechanisms do not depend on concrete models;
- trainers may compose everything, but model-specific semantics stay behind a model-owned contract or hook;
utils/is only for stable, domain-agnostic leaves. A function used by one model, dataset, backend, or algorithm stays with that owner.
The already-clean directions are locked by
lint/check_core_dependencies.py
(TYPE_CHECKING-only imports exempt): types/, config/, utils/, sde/,
data/, and models/types/ never import the loop tier or concrete models;
models/ never imports algorithms, data, rewards, rollout, or trainers;
algorithms/ sees model contracts, never concrete model families; train/
sees model contracts and algorithms/base, never concrete algorithms;
distributed/ never imports algorithms, data, models, rewards, rollout, the
train stack, or trainers.
Directions the tree does not yet satisfy are left unlocked rather than hidden
behind allowlists. The open debt, named so it is not copied as precedent:
models/ (hunyuan_image3, qwen3_5, qwen3_moe) reaches into
train/backend/veomni (EP fusing / _compat), models/cosmos3 subclasses the
train-side SFT track builder, and algorithms/ flips train/lora adapters.
That is migration work, not a convention to copy.
Where new code goes
| Feature | Owner |
|---|---|
| Model forward/replay, timestep, scheduler, packing, model-only preprocessing | models/<family>/ |
| Generic dataset reading, batching, runtime normalization | data/ |
| One-off dataset download/conversion CLI | datasets/<dataset>/, repo root beside unirl/ |
| Loss, ratio/clip, advantage policy for one RL method | algorithms/ |
| Engine lifecycle or model↔engine wire translation | rollout/engine/<backend>/ and its adapters/ |
| Score construction and scorer-specific loading | reward/ |
| Optimizer, sharding, EMA/LoRA lifecycle | train/ |
| Cross-component sequencing, placement, retries, checkpoints | trainer/ |
| Communication, tensor transport, device placement | distributed/ |
| Stable domain-agnostic leaf helper with multiple owners | a narrowly named module under utils/ |
Prefer a small amount of policy duplication over a shared base class that makes one model change touch several unrelated modules. Share mechanisms; keep model, algorithm, reward, and backend policy local.
Deployment modes
The rollout engine and optional sync: section define how GPUs are used:
| Mode | Layout | Sync |
|---|---|---|
| Train-side sampling | Training workers generate samples directly | Not used |
| Separate rollout | Rollout and training use different GPU pools | Required |
| Colocated rollout | Rollout and training share GPU bundles with offload/onload | Required |
See distributed/README.md for how each mode places
workers and moves rollout data between them.
Runtime Data Flow
The layers above turn one recipe into a repeating loop. A single training step flows through them like this:

- An entrypoint composes the chosen
examples/<domain>/<recipe>.yamland runs validators. - The
<Domain>Trainer(e.g.trainer/diffusion.py) acquires a RayDevicePooland builds the rollout and train workers. - The trainer builds a typed
Samplelineage and dispatches it to the rollout engine. - The engine returns the lineage with generated
Parts filled with conditions, segments, primitive maps, and media previews. RewardService.score_and_attachattaches rewards;Part.compute_advantagesz-scores them into advantages.TrainStack.train_track(...)shards the generatedPartacross train workers and runs the mini-batch optimizer loop.- Each train worker owns a model
Bundle, anFSDPBackend, and one loss algorithm. - Dedicated-rollout modes (separate / colocate) sync trainer weights back to the rollout workers.
The agentic workflow extends step 3 into trajectories: a driver-side manager
dispatches one task per remote engine slot while each engine executes model and
environment turns. AgenticTrainer waits for complete sibling groups, scores
their terminal answers through RewardService, and assigns each trajectory's
group-normalized advantage to all of its generated turns before training.
Deeper Module Docs
trainer/README.md: the orchestration hub — how a<Domain>Trainerplaces workers and drives the loop.types/README.md: theSample/Partcontract and migration from the retired request/response API.config/README.md: flat-recipe config —require/precision validators,_target_instantiation, cross-component contracts.rollout/README.md: rollout modes, engines, and theSample/Partgeneration flow.rollout/env/README.md: agentic environments, tools, trajectories, and manager quiescence behavior.train/readme.md: train stack, FSDP backend, injection, EMA shadow.algorithms/README.md: per-track loss algorithms.reward/README.md: reward backends and custom scorers.models/README.md: model bundle and per-model package contracts.sde/README.md: SDE kernels, σ schedule, initial noise.distributed/README.md: distributed runtime — workers, dispatch, placement, and the rollout→train data plane.distributed/weight_sync/README.md: trainer→rollout weight-sync backends.
Source:
unirl/README.md— edit the README next to the code, then runnpm run sync:readmesfromdocs/.