1 recipes.py — TrainingConfig

All algorithms live in TRAINING_CONFIGS (rl_engine/trainer/recipes.py) as frozen TrainingConfig dataclasses (rl_engine/trainer/config.py). Three dataclass types cover the entire authoring surface:

Named model roles

Each ModelRole pairs a name with a specialization class that owns compute_loss().

Declared data syncs

Cross-role dependencies are DataSync objects — inject (Ray object store) or exchange (bidirectional NCCL).

Zero backend changes

Adding an algorithm means one @recipe function. Megatron/FSDP backends and the trainer are untouched.

DataclassFieldDefaultMeaning
ModelRolenameRole key, e.g. "policy", "value", "reference".
specialization_clsClass defining compute_loss() / prepare_batch() / compute_advantages().
backend"megatron"Training backend: "megatron" or "fsdp".
trainableTrueFalse for frozen reference models (no optimizer step).
num_nodesNoneNode count; None resolves from args (actor_num_nodes).
num_gpus_per_nodeNoneGPUs/node; None resolves from args.
init_kwargs{}Extra kwargs to async_init, e.g. with_ref=True.
shares_pool_withNoneName of another role to share a WorkerPool (same GPUs/model).
sync_to_rolloutFalsePush trained weights to the rollout inference engine (SGLang) after each step.
DataSyncsource / targetProducing / consuming role names.
keysSource output keys to transfer, e.g. ("values",).
stage"inject""inject" (Ray object store) or "exchange" (NCCL). Validated in __post_init__.
target_keysNoneOptional rename on the target side; must match keys length.
TrainingConfignameAlgorithm name (matches the registry key).
rolesOrdered tuple of ModelRole.
syncs()Tuple of DataSync dependencies.
description""Human-readable one-liner (shown in --help / logs).

TrainingConfig.__post_init__ validates at import time: unique role names, sync endpoints resolve, non-empty keys, pool-sharing acyclic — a malformed recipe fails at import, not mid-run.

The @recipe decorator

# rl_engine/trainer/recipes.py
TRAINING_CONFIGS: dict[str, TrainingConfig] = {}

def recipe(name: str):
    """Decorator to register a training config."""
    def decorator(fn):
        TRAINING_CONFIGS[name] = fn()   # build + register at import
        return fn
    return decorator
Recipe anatomy A TrainingConfig frozen dataclass resolved by the factory into model roles with specializations and backends, data syncs, and validation. Algorithm = one frozen dataclass, resolved at launch TrainingConfig @recipe · frozen dataclass name: str roles: tuple[ModelRole] policy · value · ref · reward syncs: tuple[DataSync] inject / exchange topology description: str factory.create_trainer() RoleWorkers per role lookup ModelRole → RoleWorkers specialization_cls.compute_loss() backend = megatron | fsdp sync_to_rollout → SGLang weights trainable · init_kwargs(with_ref=…) DataSync inject → Ray store exchange → NCCL __post_init__ validation unique role names · sync refs resolve non-empty keys · pool-sharing acyclic

factory.create_trainer() (rl_engine/trainer/factory.py) builds a RoleWorkers per role, connects exchange syncs over NCCL, and hands the graph to StacxTrainer.

2 Recipe catalog

22 recipes ship in recipes.py — 16 LLM plus 6 experimental diffusion.

LLM recipes

RecipeRolesSyncsSpecializationTypical launcher
grpopolicy (with_ref)noneActorSpecializationgrpo/grpo_4b.sh
gspopolicynoneActorSpecialization--advantage-estimator gspo
reinforce_pppolicynoneActorSpecialization--advantage-estimator reinforce_plus_plus
reinforce_pp_baselinepolicynoneActorSpecializationreinforce_plus_plus_baseline
ppopolicy, value2 exchangeActor + Critic--algorithm ppo
ppo_colocatedpolicynoneDualRoleSpecialization--algorithm ppo_colocated
ppo_colocated_asyncpolicy, value2 exchangeActor + Critic--algorithm ppo_colocated_async
ppo_colocated_async_sharedpolicy, value shared poolnoneActor + Criticppo_colocated_async_shared
dpopolicy (with_ref)noneDPOSpecialization--algorithm dpo
dpo_distributedpolicy, reference1 injectDPOSpecialization--algorithm dpo_distributed
online_rewardpolicy, reward1 injectActor + Reward--algorithm online_reward
sftpolicynoneCausalDecoderSpecialization--algorithm sft
online_daggerpolicynoneCausalDecoderSpecializationsft/sft_4b.sh (iter-SFT)
on_policy_distillationpolicynoneDistillSpecializationopd/opd_4b.sh
mixture_dagger_opdpolicynoneMixedLossDaggerOPDonline_dagger/dagger_4b.sh
mixture_aggrevate_opdpolicynoneMixedLossDaggerOPDonline_dagger/aggrevate_4b.sh

Diffusion recipes experimental

Flow-matching kernels for DiT/MMDiT models; loss math in rl_engine/trainer/objectives/diffusion_loss.py.

RecipeSpecializationObjective
diffusion_rwfmDiffusionSpecializationReward-weighted flow matching — advantage-weighted denoising MSE on z_0.
diffusion_sftDiffusionSpecializationSupervised flow matching (no advantages).
diffusion_dpoDiffusionDPOSpecializationPreference-weighted denoising; needs precomputed ref_loss injected.
diffusion_nftDiffusionNFTSpecializationNoise-Free Reward Tuning; with_ref=True + frozen old_actor snapshot.
mmdit_rwfmMMDiTSpecializationRWFM for dual-stream Flux / SD3 / Lumina models.
mmdit_nftMMDiT+NFT (mixed MRO)NFT loss with MMDiT dual-stream input packing.

Recipe selection — infer_algorithm()

The runtime key args.algorithm (rl_engine/trainer/factory.py) resolves as:

  • --algorithm set explicitly → wins verbatim (DAgger / distillation / diffusion always use this).
  • use_critic set → ppo.
  • Otherwise --advantage-estimator maps to the matching recipe (e.g. reinforce_plus_plus → reinforce_pp).

3 Algorithm objectives

Stateless loss functions in rl_engine/trainer/objectives/; specializations in rl_engine/trainer/specialization/ compose them inside compute_loss().

GRPO — PPO-clipped PG on group-normalized rewards

ActorSpecialization.compute_loss (specialization/agent.py). compute_grpo_advantages() (objectives/advantage.py) broadcasts the scalar reward to every response token and whitens across the group. Optional KL anchor: --use-kl-loss --kl-loss-type low_var_kl --kl-loss-coef 0.001 + --ref-load (in-Megatron reference, no separate worker). gspo and reinforce_pp[_baseline] reuse this specialization with a different estimator.

SFT & iter-SFT — supervised cross-entropy

CausalDecoderSpecialization.compute_loss (specialization/causal_decoder.py): per-token CE on the response, masked to loss_masks. The iter-SFT baseline uses --algorithm online_dagger with STACX_DAGGER_SFT_MODE=trajectory to replay a pre-saved teacher corpus instead of live rollout.

On-policy distillation (OPD) — K3 reverse-KL vs. a frozen teacher

DistillSpecialization (specialization/distill.py): reverse KL against a frozen teacher SGLang server, whose per-token logprobs arrive as rollout fields. Mode chosen by STACX_DISTILL_KL_MODE:

  • k3_sample (default) — single-sample MC estimate on the chosen token; TP-aware, higher variance.
  • topk (TP=1 only) — RKL over the union of student and teacher top-K supports; lower variance.

Mixed-loss DAgger-OPD — per-token CE / K3 dispatch

MixedLossDaggerOPDSpecialization (specialization/mixed_loss_dagger.py) routes each action token by executor — teacher-executed → CE, student-executed → K3 reverse-KL — via a sign sentinel packed into teacher_log_probs (SGLang logprobs are ≤ 0; rollout writes +1.0 at teacher positions). WandB tracks ce_mean, k3_mean, teacher_action_frac.

Mixed CE / K3 per-token dispatch A packed trajectory strip where teacher-executed action tokens carry a +1.0 sentinel and route to cross-entropy, while student-executed action tokens carry a real teacher logprob and route to K3 reverse-KL. One packed trajectory · per-token executor-driven loss tokens teacher_log_probs loss_mask sys 0 aᵀ +1.0 1 −0.6 1 obs 0 aᵀ +1.0 1 −1.2 1 −0.4 1 obs 0 aᵀ +1.0 1 −0.9 1 obs 0 −0.7 1 sentinel decode teacher_log_probs > 0 ? lp > 0 (teacher) lp ≤ 0 (student) teacher-executed → Cross-Entropy ℓ = − log p_s(c) β = 1 ⇒ all teacher ⇒ pure CE (= online_dagger) student-executed → K3 reverse-KL ℓ = exp(r) − r − 1, r = log p_t(c) − log p_s(c) β = 0 ⇒ all student ⇒ pure K3 (= OPD baseline) one reduction: sum_of_sample_mean over the union loss_mask → equal weight per action token teacher: sentinel +1.0 student: teacher logprob ≤ 0 observation / prompt: loss_mask 0 (ignored)

Both endpoints asserted in tests/trainer/test_mixed_loss_dagger.py.

Rollin protocols — β vs. κ

Both recipes share the loss kernel but differ in how the teacher/student split is produced (STACX_DAGGER_SAMPLING_MODE):

  • mixture_dagger_opd — per-turn β-mixture: each turn flips Bernoulli(β), β decaying over training (linear / cosine / exponential / step; launcher default linear 1.0→0.0 over 5 iters).
  • mixture_aggrevate_opd — trajectory-level AggreVaTe: κ ~ U{0, T_max(i)} per task; student leads turns 0..κ−1, teacher force-decodes the tail. T_max(i) = min(start + step·i, cap), launcher default 0/20/100.

DPO — contrastive preference loss

dpo / dpo_distributed

compute_dpo_loss (objectives/dpo.py): L = −log σ(β·(Δ_w − Δ_l)) with log-ratios vs. a reference (IPO / cDPO variants supported). Colocated switches ref weights in place; distributed runs a frozen reference role with a log_probs → ref_log_probs inject sync.

4 Data syncs: inject vs. exchange

inject

One-way via the Ray object store: the trainer runs forward_only() on the source, pulls keys (optionally renamed to target_keys), and merges them into target rollout data. No NCCL group.

Used by: dpo_distributed (log_probs→ref_log_probs), online_reward (reward_scores).

exchange

Bidirectional concurrent NCCL: both roles call train() simultaneously and swap tensors mid-step.

Used by: ppo / ppo_colocated_async — critic → values; actor → log_probs, ref_log_probs.

Single-role recipes declare syncs=(); the external SGLang teacher server needs no sync entry.

5 Adding an algorithm

A new algorithm is a specialization (only if no existing loss fits) plus one @recipe. No backend, Ray, or orchestrator changes needed.

Step 1 — Subclass a specialization

# rl_engine/trainer/specialization/my_algo.py
from collections.abc import Callable
from typing import Any
import torch
from rl_engine.trainer.specialization.causal_decoder import CausalDecoderSpecialization
from rl_engine.trainer.objectives.policy_loss import sum_of_sample_mean

class MyAlgoSpecialization(CausalDecoderSpecialization):
    def compute_loss(
        self,
        model_output: torch.Tensor,          # [seq_len, vocab] (TP-local shard if TP>1)
        batch: dict[str, Any],
        reducer: Callable[[torch.Tensor], torch.Tensor],
    ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
        response_lengths = batch["response_lengths"]
        loss_masks = batch["loss_masks"]
        # ... derive per-token student logprobs from model_output, then:
        per_token = my_kernel(...)                       # shape [N_response]
        loss = sum_of_sample_mean(per_token, response_lengths, loss_masks)
        loss = reducer(loss)                             # slime CP-aware reduction
        return loss, {"my_loss": loss.detach()}

Step 2 — Register the recipe

# rl_engine/trainer/recipes.py
@recipe("my_algo")
def my_algo_config():
    from rl_engine.trainer.specialization.my_algo import MyAlgoSpecialization
    return TrainingConfig(
        name="my_algo",
        roles=(
            ModelRole(
                name="policy",
                specialization_cls=MyAlgoSpecialization,
                sync_to_rollout=True,          # push weights to SGLang each step
            ),
        ),
        syncs=(),                                # single role → no cross-model sync
        description="My custom reverse-KL algorithm",
    )

Step 3 — Launch it

python -m rl_engine.train --algorithm my_algo \
    --hf-checkpoint /root/models/<student> \
    --ref-load /root/models/<student>_torch_dist \
    ...   # the SWE launchers under scripts/train/swe/ wrap this

Scope of a recipe change

Adding a second model is one more ModelRole + DataSync — the trainer wires the inject / exchange transport automatically; no orchestrator code.

6 Objectives reference

ModuleFunctionRole
policy_loss.pycompute_ppo_loss()PPO clipped PG loss (+ optional dual-clip).
policy_loss.pysum_of_sample_mean()Sum-of-per-sample-means reduction (CP-aware denominator).
value_loss.pycompute_clipped_value_loss()PPO-style clipped value loss.
logprob.pycompute_log_probs_and_entropy()Logprob + entropy extraction from logits.
kl.pycompute_kl_divergence()K1 / K2 / K3 / low_var_kl estimators.
advantage.pycompute_grpo_advantages() / compute_grpo_returns()GRPO reward broadcast + whitening.
advantage.pycompute_chunked_gae()Parallel-prefix GAE for PPO.
advantage.pycompute_reinforce_pp_returns() / …_baseline_advantages()REINFORCE++ returns / baseline advantages.
advantage.pyapply_opd_kl_penalty()Advantage-side OPD reverse-KL penalty.
dpo.pycompute_dpo_loss()DPO / IPO / cDPO contrastive loss.
distillation_loss.pycompute_rkl_k3_sample() / compute_rkl_topk()On-policy distillation reverse-KL (chosen-token / union-top-K).
divergences.pycompute_k3_kl() / compute_jsd_contribution()Eval-time one-sample KL / JSD estimators (not training losses).
diffusion_loss.pycompute_flow_matching_loss() / compute_diffusion_rl_loss()Flow-matching + reward-weighted diffusion objectives.

7 Algorithm knobs

Algorithm-level flags and env vars. Full launcher catalog: scripts/train/swe/ENV_REFERENCE.md.

Flag / envApplies toDefaultEffect
--algorithmallinferredRecipe key into TRAINING_CONFIGS; overrides --advantage-estimator inference.
--advantage-estimatorgrpo familygrpoMaps to grpo / gspo / ppo / reinforce_pp[_baseline].
--use-kl-loss + --kl-loss-type + --kl-loss-coefGRPOoff / k1 / 0.1KL anchor vs. --ref-load. GRPO launcher sets low_var_kl, 0.001.
--dagger-scheduledaggerlinearβ decay: linear / cosine / exponential / step.
--dagger-beta-start / --dagger-beta-enddagger1.0 / 0.0β at iter 0 / after decay (1.0 = teacher, 0.0 = student).
--dagger-decay-iters / --dagger-warmup-itersdagger10 / 0Iters over which β decays / held at start first.
--dagger-tmax-start / -step / -capaggrevate0/0/0 (launcher 0/20/100)κ-schedule T_max(i)=min(start+step·i, cap); per-task κ~U{0,T_max}.
STACX_DAGGER_SAMPLING_MODEdaggermixturemixture (per-turn β) or aggrevate_pure (prefix/tail).
STACX_MIXED_LOSS_DAGGERdagger01 enables the per-token CE/K3 sentinel dispatch (auto-set for aggrevate).
STACX_DISTILL_KL_MODEOPDk3_samplek3_sample (TP-any) or topk (TP=1 only).
STACX_DISTILL_TOPKOPD (topk)20Union-top-K size; must match rollout capture K.