Skip to content

Commit ea00e11

Browse files
committed
feat(orchestrator): add static dataset sft source
1 parent 87c38c7 commit ea00e11

18 files changed

Lines changed: 460 additions & 75 deletions

File tree

docs/algorithms.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This page covers the math and the configurable algorithmic components: the algor
77
- [The Algorithm Abstraction](#the-algorithm-abstraction)
88
- [Model References](#model-references)
99
- [The Algorithms](#the-algorithms)
10+
- [SFT Sources](#sft-sources)
1011
- [Customizing Components](#customizing-components)
1112
- [Per-Env Algorithms](#per-env-algorithms)
1213
- [The Algorithm Classes](#the-algorithm-classes)
@@ -30,14 +31,14 @@ This page covers the math and the configurable algorithmic components: the algor
3031

3132
A training algorithm in `prime-rl` is configured under `[orchestrator.algo]`, where **`type` names the algorithm** (`grpo`, `opd`, `sft`, …) and the class defaults are its vetted setting. It has two parts:
3233

33-
1. **Sampling** (`algo.sampling`) — how train rollouts are produced: which model generates them. `source` is a [model reference](#model-references): `"policy"` (the live policy, the default) or an inline frozen hosted model. Group sizing stays on the env config (`group_size`).
34+
1. **Sampling** (`algo.sampling`) — how train rollouts are produced. `source` is `"policy"` (the live policy, the default), an inline frozen hosted model, or a static supervised dataset. Group sizing stays on the env config (`group_size`).
3435
2. **The per-token training signal** — credit assignment and loss routing, fused; the algorithm's own parameters sit directly on `algo`. One mapping from a finalized rollout to per-token *(loss component, weight)* pairs — the credit a token gets and the loss that consumes it are two coordinates of the same output. Group-relative algorithms compute credit on the orchestrator and ship per-token advantage streams; reference-KL algorithms query a reference model at batch-ship time (bounded concurrency) and ship its prefill logprobs for the trainer to evaluate against the live policy. The `type` determines which loss component consumes the action tokens (`rl` / `ce` / `ref_kl`) and what happens to env-provided observation tokens in multi-turn rollouts (masked out by default; `echo` trains on them with weighted CE).
3536

3637
The trainer is algorithm-blind: the loss is a sum of three components (rl, ce, ref_kl), each normalized by its own global token count; per-token streams ship on the wire (the `rl_weights` / `ce_weights` / `ref_kl_weights` component weights plus the `advantages` stream on each training sample) and the trainer just executes them. Adding an algorithm never touches the dispatcher, packer, or trainer hot path.
3738

3839
### Model References
3940

40-
`prime-rl` hosts exactly one model: the trainable policy (`[orchestrator.model]`). Every other model an algorithm uses is an external OpenAI-compatible endpoint, declared *inline on the component that uses it*. A model reference is either the string `"policy"` (the live policy) or a frozen hosted model (`name` + `base_url`):
41+
`prime-rl` hosts exactly one model: the trainable policy (`[orchestrator.model]`). Every other model an algorithm uses is an external OpenAI-compatible endpoint, declared *inline on the component that uses it*. A model reference is either the string `"policy"` (the live policy) or a frozen hosted model (`name` + `base_url`); static datasets are rollout sources, not model references:
4142

4243
```toml
4344
[orchestrator.algo]
@@ -50,9 +51,9 @@ base_url = ["http://localhost:8001/v1"]
5051

5152
Model *roles* are labels the algorithm itself declares over these references — the distillation algorithms declare their reference's role as `teacher`, so `[orchestrator.algo.teacher]` is the shorthand for that slot and validation errors speak the same language ("algorithm 'opd' needs a teacher"). No role exists outside the algorithm that declares it: the dispatcher, sink, and trainer branch on liveness alone, never on what an algorithm calls a model.
5253

53-
`algo.teacher` is shorthand for the slot the algorithm declares for its reference — `algo.model` for `opd` / `opsd`, `sampling.source` for `sft` (its teacher is the sampling source). A slot you didn't set takes the shorthand; an explicit reference that already equals it is accepted, a disagreeing one is an error. For `opd` / `opsd` you can also set `algo.model` directly; set the component fields directly for multi-model setups.
54+
`algo.teacher` is shorthand for the slot the algorithm declares for its reference — `algo.model` for `opd` / `opsd`, `sampling.source` for `sft`'s frozen-teacher form. A slot you didn't set takes the shorthand; an explicit reference that already equals it is accepted, a disagreeing one is an error. For `opd` / `opsd` you can also set `algo.model` directly; set `algo.sampling.source` directly for dataset sources and multi-model setups.
5455

55-
Liveness is a property of the reference, not of any role: rollouts sampled from `"policy"` get version-salted prefix caches, carry sampling logprobs for importance ratios, and age off-policy as weights update; rollouts and scores from frozen models get a stable prefix cache and never go stale. Frozen models are externally hosted (`base_url` is required) — `prime-rl` never launches or updates them, and each env's algorithm builds its own client pool to the endpoints it declares.
56+
Liveness is a property of the source, not of any role: rollouts sampled from `"policy"` get version-salted prefix caches, carry sampling logprobs for importance ratios, and age off-policy as weights update; rollouts and scores from frozen models get a stable prefix cache and never go stale; dataset-sourced rollouts are local message replay with no env or client. Frozen models are externally hosted (`base_url` is required) — `prime-rl` never launches or updates them, and each env's algorithm builds its own client pool to the endpoints it declares.
5657

5758
### The Algorithms
5859

@@ -68,12 +69,45 @@ type = "grpo" # the default
6869
| `grpo` | policy | `rl` on actions | Standard group-relative RL. |
6970
| `max_rl` | policy | `rl` on actions | MaxRL ([arXiv:2602.02710](https://arxiv.org/abs/2602.02710)): GRPO's centered reward normalized by the group **mean** instead of the standard deviation — the gradient is unbiased for the order-`group_size` truncation of the maximum-likelihood objective, upweighting hard examples like `1/p`. |
7071
| `opd` | policy | `ref_kl` on actions | On-policy distillation ([Thinking Machines](https://thinkingmachines.ai/blog/on-policy-distillation/)): the policy samples, per-token reverse KL against a reference model as the gradient signal. Needs a `teacher`. |
71-
| `sft` | *(the teacher)* | `ce` on actions | Hard distillation: a frozen model generates rollouts, the policy trains with CE on its tokens. Needs a `teacher` (folds into `sampling.source`). |
72+
| `sft` | *(teacher or dataset)* | `ce` on actions | Supervised fine-tuning: CE on a non-policy source's target tokens. Source picks the flavor — a frozen `teacher` model (distillation on fresh rollouts) or a static `dataset` (replay of stored traces). Assigns no credit. |
7273
| `opsd` | policy | `ref_kl` on actions | SDFT ([arXiv:2601.19897](https://arxiv.org/abs/2601.19897)): the model is its own reference, conditioned on an expert demonstration. Defaults to the live policy (the paper's setting, no extra deployment); set an inline `model` to score under a frozen copy instead. |
7374
| `echo` | policy | `rl` on actions + weighted `ce` on observations | ECHO: standard GRPO plus a cross-entropy loss on env-provided tokens already present in the rollout, selected by message role (needs the renderer's role attribution). Defaults to tool-response bodies at `alpha = 0.1` (ECHO's λ); set `roles` to train other roles, each at its own weight. |
7475
| `reward` | policy | `rl` on actions | REINFORCE-style: advantage = raw reward, no group baseline. |
7576
| `custom` | policy | `rl` on actions | Your own advantage function (`import_path`), per-token advantages per rollout — see [Custom Advantage](#custom-advantage). |
7677

78+
#### SFT Sources
79+
80+
`sft` always trains cross-entropy on non-policy target tokens. A frozen teacher source samples those targets from an externally hosted model:
81+
82+
```toml
83+
[orchestrator.algo]
84+
type = "sft"
85+
86+
[orchestrator.algo.teacher] # alias for sampling.source on sft
87+
name = "teacher-model"
88+
base_url = ["http://teacher:8000/v1"]
89+
```
90+
91+
A static dataset source replays stored targets locally. Set `type = "dataset"` explicitly so the source table is read as a dataset, not a model reference:
92+
93+
```toml
94+
[orchestrator.algo]
95+
type = "sft"
96+
97+
[orchestrator.algo.sampling.source]
98+
type = "dataset"
99+
name = "org/my-sft-dataset"
100+
split = "train"
101+
# optional: subset, max_examples, messages_column, prompt_column, completion_column, tools_column
102+
103+
[[orchestrator.train.env]]
104+
id = "static-sft"
105+
name = "static-sft"
106+
group_size = 1
107+
```
108+
109+
Static dataset rows use either a `messages` column or `prompt` + `completion` columns. The dataset path does not start a Verifiers env or select a model client.
110+
77111
### Customizing Components
78112

79113
Every key beyond `type` is visibly your own assembly — there is no preset layer to diverge from. The vetted setting is the class defaults; what you set is what runs:
@@ -111,7 +145,7 @@ kwargs = { patterns = ["WARNING"] }
111145
def drop_warnings(rollout, *, patterns: list[str]) -> list[list[bool]]: ...
112146
```
113147

114-
Component compatibility is validated at config time: frozen-model sampling can only feed the `ce` loss component — the `rl` and `ref_kl` components need the live policy's own sampling logprobs for importance ratios — `opd` pointed at `"policy"` is rejected as degenerate (zero KL), `sft` without a frozen source is rejected (CE on the policy's own tokens is not a distillation target). A group-relative algorithm with `group_size = 1` produces all-zero advantages; the resulting empty batch is caught at runtime (the orchestrator warns and aborts after repeated zero-trainable batches), not at config time.
148+
Component compatibility is validated at config time: non-policy sampling can only feed the `ce` loss component — the `rl` and `ref_kl` components need the live policy's own sampling logprobs for importance ratios — `opd` pointed at `"policy"` is rejected as degenerate (zero KL), and `sft` with the policy as its source is rejected (CE on the policy's own tokens is not a supervision target). A group-relative algorithm with `group_size = 1` produces all-zero advantages; the resulting empty batch is caught at runtime (the orchestrator warns and aborts after repeated zero-trainable batches), not at config time.
115149

116150
### Per-Env Algorithms
117151

@@ -140,7 +174,7 @@ At runtime, each env's resolved config builds two objects: a `Sampler` (`prime_r
140174
| `max_rl` | `MaxRLAlgorithm` | `score_group`: mean-normalized group credit |
141175
| `opd` | `OPDAlgorithm` | `score_batch`: own-context prefill under the teacher |
142176
| `opsd` | `OPSDAlgorithm` | `score_batch`: demo-conditioned prefill under the teacher |
143-
| `sft` | `SFTDistillAlgorithm` | `score_group`: group-norm credit (feeds filters) |
177+
| `sft` | `SFTAlgorithm` | *(no hook — CE on the source's target tokens, no credit)* |
144178
| `reward` | `RewardAlgorithm` | `score_rollout`: raw reward |
145179
| `custom` | `CustomAlgorithm` | `score_group`: your function |
146180

@@ -178,7 +212,7 @@ $$
178212
$$
179213

180214
- `rl` — the configured RL loss (`[trainer.loss]`): DPPO + KL by default, or a [custom loss](#custom-loss). Fed by the group-relative advantage strategies (`grpo`, `max_rl`, `reward`, `custom`, and `echo`'s action tokens).
181-
- `ce` — masked NLL. Used for frozen-model tokens (`sft`) and env-observation tokens (`echo`).
215+
- `ce` — masked NLL. Used for supervised target tokens (`sft`) and env-observation tokens (`echo`).
182216
- `ref_kl` — the per-token reverse KL to a reference model ($\log \pi_{\text{ref}} - \log \pi$) as the policy-gradient signal, importance-ratio corrected with a one-sided trust region (`opd`, `opsd`). Requires `ref_logprobs` from a [reference scoring](#reference-scoring); the scoring model must be a vLLM server (it's the only one that exposes `prompt_logprobs`).
183217

184218
The orchestrator stamps each sample's component membership as per-token weight streams (`rl_weights` / `ce_weights` / `ref_kl_weights` on the wire): a weight scales that component's per-token loss, `0.0` leaves the token out of the component entirely (mask *and* denominator), and components may overlap on the same token — their gradients sum. Each $N$ is the global (all-reduced) count of that component's member tokens, so the components don't dilute each other: adding echo observation tokens never changes the rl term's effective per-token learning rate, and an sft env packed next to a GRPO env doesn't soften its gradient. Tokens of different components pack freely into the same micro batch, and a plain GRPO run ships no weight streams at all (absent streams mean rl weight 1.0 on every trainable token — the unchanged hot path). Advantages always ship per token (`advantages` on the wire), assigned as per-token streams from the start — uniform group credit is broadcast over completion tokens at assignment; algorithms with no rl credit (opd, opsd) ship none.
@@ -283,7 +317,7 @@ The per-token training signal is set by `algo.type` and the [algorithm](#the-alg
283317
| `reward` | `rl` | Advantage = raw reward, no baseline. |
284318
| `opd` | `ref_kl` | On-policy distillation: per-token reverse KL to a reference model (`model`, an inline frozen hosted model), evaluated in the trainer from shipped reference logprobs. No credit — rollouts keep `advantages = None` (advantage-based filters never fire) and ship no advantage stream; `group_size` only fans out sampling. |
285319
| `opsd` | `ref_kl` | SDFT: per-token reverse KL to a demo-conditioned reference. No credit — rollouts keep `advantages = None` (advantage-based filters never fire) and ship no advantage stream. |
286-
| `sft` | `ce` | Cross-entropy on the sampled tokens. Assigns no advantage — trains on every sampled token. |
320+
| `sft` | `ce` | Cross-entropy on the source's target tokens (a frozen teacher's rollouts or a static dataset's traces). Assigns no advantage — trains on every target token. |
287321
| `custom` | `rl` | Your function (below); per-token advantages per rollout. |
288322

289323
### Default Advantage

docs/training.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ A condensed view of the knobs you'll most often tune. For trainer-side paralleli
5959
| `orchestrator.batch_size` | Tasks per trainer step. |
6060
| `orchestrator.group_size` | Rollouts generated per task. |
6161
| `orchestrator.max_off_policy_steps` | How many distinct policies may have contributed to one rollout before it's discarded (default 8). The main off-policy dial on long agentic rollouts — bump for throughput, lower for tighter on-policyness. Watch `errored_rollouts` and `mismatch_kl/all/mean` when tuning. |
62-
| `[orchestrator.algo]` | Training algorithm — the advantage `type` names it (`grpo` default, `max_rl`, `opd`, `opsd`, `sft`, `echo`, `reward`, `custom`). See [Algorithms](#algorithms). |
62+
| `[orchestrator.algo]` | Training algorithm — `type` names it (`grpo` default, `max_rl`, `opd`, `opsd`, `sft`, `echo`, `reward`, `custom`). See [Algorithms](#algorithms). |
6363
| `[[orchestrator.train.env]]` | Training environments. List multiple tables for multi-env training; weight them via `ratio`. See [Configuration § Environments](configuration.md#environments-orchestratortrainenv). |
6464
| `[[orchestrator.eval.env]]` + `orchestrator.eval.interval` | Eval environments and cadence (default every 100 steps). |
6565

@@ -85,25 +85,25 @@ A condensed view of the knobs you'll most often tune. For trainer-side paralleli
8585

8686
The RL entrypoint supports several training algorithms, switched via `[orchestrator.algo]`'s `type` (see [Algorithms](algorithms.md#the-algorithm-abstraction) for the full reference, model references, and per-algorithm customization):
8787

88-
| `algo.type` | Frozen model (`algo.teacher`) | Use case |
88+
| `algo.type` | Source | Use case |
8989
|---|---|---|
9090
| `grpo` (default) | None | Standard group-relative RL |
9191
| `max_rl` | None | [MaxRL](https://arxiv.org/abs/2602.02710): GRPO with mean-normalized advantages (maximum-likelihood RL) |
9292
| `opd` | Required, must be vLLM (needs `prompt_logprobs`) | [On-policy distillation](https://thinkingmachines.ai/blog/on-policy-distillation/): the policy generates rollouts, the trainer minimizes per-token reverse KL to a reference model |
93-
| `sft` | Required, any OpenAI-compatible endpoint | Hard-distill: a frozen model generates rollouts, the policy trains on its tokens |
93+
| `sft` | Frozen teacher endpoint or `sampling.source.type = "dataset"` | Supervised fine-tuning: train CE on non-policy target tokens from fresh teacher rollouts or static dataset traces |
9494
| `opsd` | `"policy"` (the default, no deployment) or a vLLM endpoint serving a frozen copy | [SDFT](https://arxiv.org/abs/2601.19897): the model is its own reference conditioned on expert demonstrations |
9595
| `echo` | None | GRPO plus cross-entropy on env-observation tokens |
9696

9797
`reward` (raw-reward credit, no baseline) and `custom` (your own advantage function) complete the set — see [Algorithms § The Algorithms](algorithms.md#the-algorithms).
9898

99-
Frozen models are declared inline on the algorithm (`[orchestrator.algo.teacher]` with `name` + `base_url`). The `rl` entrypoint only manages policy inference — start frozen-model servers yourself and point `base_url` at them:
99+
Frozen models are declared inline on the algorithm (`[orchestrator.algo.teacher]` with `name` + `base_url`). Static SFT uses `[orchestrator.algo.sampling.source] type = "dataset"` with dataset fields such as `name` and `split`. The `rl` entrypoint only manages policy inference — start frozen-model servers yourself and point `base_url` at them:
100100

101101
```bash
102102
CUDA_VISIBLE_DEVICES=1 uv run inference \
103103
--model.name <frozen-model> --server.port 8001
104104
```
105105

106-
The standalone `uv run sft` entrypoint is the more traditional SFT path — pure dataset-based, no orchestrator. Use the `sft` algorithm only when you want a frozen model to generate the supervision on the fly.
106+
The standalone `uv run sft` entrypoint remains the dedicated dataset SFT trainer. Use the `sft` algorithm inside `uv run rl` when you want SFT-shaped CE training to share the orchestrator/trainer pipeline with other rollout sources or algorithms.
107107

108108
### Important Metrics
109109

0 commit comments

Comments
 (0)