Skip to content

feat(orchestrator): add static dataset SFT#2795

Open
tim0120 wants to merge 4 commits into
mainfrom
feat/static-sft-sampler
Open

feat(orchestrator): add static dataset SFT#2795
tim0120 wants to merge 4 commits into
mainfrom
feat/static-sft-sampler

Conversation

@tim0120

@tim0120 tim0120 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Rebased onto main (post-#2746) and adds static-dataset SFT as a sampling.source for the existing sft algorithm.

sft remains one CE-only algorithm. The source chooses the flavor:

  • a frozen teacher source (an inline frozen sampling.source) samples teacher rollouts through the normal Verifiers env path and trains CE on those tokens
  • a dataset source (type = "dataset") replays stored supervised rows locally — no Verifiers env server, model client, sampling logprobs, or off-policy aging

Config surface

[orchestrator.algo]
type = "sft"

[orchestrator.algo.sampling.source]
type = "dataset"
name = "org/my-sft-dataset"
split = "train"
# optional: subset, max_examples, messages_column, prompt_column, completion_column, tools_column

[[orchestrator.train.env]]
id = "static-sft"
name = "static-sft"
group_size = 1

Rows carry either a messages column or prompt + completion columns, with optional tools.

Implementation notes

  • Adds StaticDatasetConfig to SamplingConfig.source and Sampler.source_kind (policy / frozen_model / dataset) for the liveness plumbing.
  • Replay is v1-native: a dataset env loads its rows at start() (no env server; num_tasks = dataset length, so TrainSource weighted sampling works unchanged) and builds a renderer from the policy tokenizer — the same pattern as opsd's hint renderer. row_to_rollout renders the row's messages once and constructs a typed Rollout (vf.Trace) whose MessageNodes carry the exact rendered token sequence; the renderer's sampled_mask marks assistant emission tokens as the CE targets, so trace_to_samples and the trainer hot path are untouched.
  • The dispatcher schedules dataset groups as local replay tasks (task_idx-based, same inflight accounting); non-policy sources skip off-policy aging.
  • Validation: sft rejects the policy as its source; non-policy sources can only feed CE algorithms; dataset sources require group_size = 1 (rows are fixed targets); an empty split and a renderer without sampled-token attribution (the default Jinja renderer) fail loudly at startup.
  • SFTDistillAlgorithmSFTAlgorithm (the class now covers both flavors; it declares CE routing and no scoring hooks either way).
  • Fixes Rollout.is_trainable to count stamped nonzero ce/ref_kl weight streams as a training signal (cached after stamping) — previously every sft/opd/opsd step displayed Trainable 0/N (0.0%) and warned about task difficulty, since those algorithms ship advantages=None by design. Display/metrics only; what ships to the trainer is unchanged.

Review hardening

A deep review pass surfaced and fixed:

  • StaticDatasetConfig.type is required: the undiscriminated SamplingSource union used to silently validate a frozen-model table missing base_url as a dataset, swallowing the real error.
  • The gibberish/repetition filters exempt rollouts that carry no sampling logprobs — frozen-teacher and static-replay rollouts zero-fill branch.logprobs, which the filters previously misread as prob-1.0 confidence on every token (bogus detection metrics; run-killing with enforce = true).
  • The replay renderer is built around the orchestrator's tokenizer (honoring [orchestrator.tokenizer]) instead of re-loading from the served model name; renders are serialized behind a lock (HF fast tokenizers aren't safe for concurrent single-instance use), and an unattributing renderer (e.g. default) fails at env start with a clear message instead of per-row.
  • Tool-calling rows normalize to the OAI wire shape (struct/dict arguments → JSON string, missing ids filled), and tool definitions accept the verifiers tool_defs column and bare shapes — mirroring the offline SFT trainer's normalization.
  • Dataset rows stay Arrow-backed and are indexed per task_idx (no up-front materialization of the whole split).
  • Dataset-sourced envs default group_size to 1 instead of failing on the inherited global value, so mixed RL + replay configs validate; only an explicit conflicting value raises.
  • JSON-looking text completions (e.g. a bare "42") stay content — only message-shaped JSON cells are decoded.
  • Local replays take an inflight permit but no tasks_per_minute rate-limiter slot (that budget paces outbound inference requests).

Known limitations (by design, fail loud): rows whose tool_calls can't be coerced to the OAI shape error at replay time and surface as errored rollouts; static-replay rollouts carry no rewards, so train/agg reward panels include their 0.0 in mixed runs.

Validation

10-step smoke runs on an 8×H100 pod (Qwen3-0.6B, qwen3 renderer, willcb/R1-reverse-wikipedia-paragraphs-v1-1000 capped at 512 rows, batch_size 32), re-run after the hardening batch: dataset env starts with no env server (num_tasks=512), CE loss 0.46 → 0.40 over 10 steps, 0 errors/truncations, Trainable 32/32 (100.0%), clean exit. Inspected a rendered rollout end-to-end: system/user nodes fully masked, assistant node trains on exactly its emission span (746/750 tokens — the 4 masked tokens are the <|im_start|>assistant\n scaffold), first CE target is the <think> open, last is </answer><|im_end|>. Also verified on the pod: a name-only (no base_url) source now fails with the real diagnosis; a mixed grpo+dataset config with global group_size = 16 resolves to 16/1 per env and an explicit conflicting value still raises; a tool-calling row with struct arguments, missing call ids, and verifiers-shaped tools renders into a correctly masked multi-turn rollout; scalar text completions ("42") train as content.

🤖 Generated with Claude Code


Note

Medium Risk
Touches rollout scheduling, SFT config validation, and training-signal detection; mistakes could mis-route batches or misreport trainability, but scope is mostly additive on the CE path with loud startup failures for bad renderers/datasets.

Overview
Static dataset SFT extends sft so orchestrator.algo.sampling.source can be a Hugging Face dataset (type = "dataset") in addition to a frozen teacher endpoint. Rows replay locally into tokenized Rollouts (no Verifiers env server, model client, sampling logprobs, or off-policy aging); CE targets come from the renderer’s sampled_mask on assistant tokens.

Config adds StaticDatasetConfig, SamplingSource, and Sampler.source_kind (policy / frozen_model / dataset). Dataset envs default group_size to 1 (explicit group_size != 1 errors); StaticDatasetConfig.type is required so a frozen model missing base_url does not validate as a dataset. SFTDistillAlgorithm is renamed to SFTAlgorithm.

The dispatcher routes dataset train groups to TrainEnv.run_static_rollout (inflight permit, no tasks_per_minute slot). static_sft.py loads HF rows (messages or prompt/completion, optional tools) with tokenizer/renderer alignment and tool-call normalization. Gibberish/repetition filters skip rollouts without sampling logprobs. Rollout.is_trainable treats stamped nonzero ce/ref_kl weights as trainable and invalidates cache after stamping.

Reviewed by Cursor Bugbot for commit 64660b2. Bugbot is set up for automated code reviews on this repo. Configure here.

@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch 4 times, most recently from 43a4a20 to 7548d36 Compare June 17, 2026 21:28
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from fec3f1b to ea00e11 Compare June 22, 2026 17:34
@tim0120 tim0120 marked this pull request as ready for review June 22, 2026 17:35
@tim0120 tim0120 requested review from Copilot and willccbb June 22, 2026 17:35
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from ea00e11 to 6a0a836 Compare June 22, 2026 17:36
Comment thread src/prime_rl/orchestrator/static_sft.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “static dataset SFT” as a new rollout source for the existing sft algorithm, allowing SFT training to replay supervised rows locally (no Verifiers env server/client/model sampling), while keeping SFT CE-only and enforcing config compatibility constraints (e.g., dataset SFT requires group_size = 1).

Changes:

  • Introduces StaticDatasetConfig as a sampling.source option and plumbs Sampler.source_kind through dispatcher/env logic to support local static replay.
  • Adds static_sft.py for dataset row loading + rollout construction, plus token-usage backfilling from finalized samples.
  • Updates validation/docs/skills and extends unit tests for dataset SFT acceptance/rejection paths.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/test_configs.py Adds config validation test for dataset SFT requiring group_size = 1.
tests/unit/orchestrator/test_static_sft.py New unit tests for static SFT rollout shaping (messages, prompt/completion, tool_defs).
tests/unit/orchestrator/test_algorithms.py Updates algorithm tests for dataset source acceptance and CE-only constraints.
src/prime_rl/orchestrator/types.py Updates dispatcher state docstrings to reflect per-example scheduling semantics.
src/prime_rl/orchestrator/trajectories.py Adjusts SFT assertion wording and improves tool-def normalization for OAI-format passthrough.
src/prime_rl/orchestrator/train_sink.py Adds token-usage derivation/backfill from finalized samples during rollout processing.
src/prime_rl/orchestrator/static_sft.py Adds dataset loading + rollout construction for static/dataset-sourced SFT.
src/prime_rl/orchestrator/sampler.py Exposes Sampler.source_kind and makes sampling args conditional on policy vs non-policy sources.
src/prime_rl/orchestrator/envs.py Allows dataset-sourced train envs to skip env startup and run local static rollouts.
src/prime_rl/orchestrator/dispatcher.py Routes dataset-sourced train rollouts to local replay scheduling (no model/env).
src/prime_rl/orchestrator/algo/sft.py Renames/clarifies SFT algorithm class and docstring around source-provided targets.
src/prime_rl/orchestrator/algo/init.py Wires sft type to SFTAlgorithm symbol rename.
skills/configs/SKILL.md Updates config skill to the new top-level [orchestrator.algo] surface and static SFT notes.
packages/prime-rl-configs/src/prime_rl/configs/rl.py Updates inference client auto-setup docstring to include dataset/frozen-teacher SFT.
packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py Adds validation enforcing dataset SFT uses group_size = 1.
packages/prime-rl-configs/src/prime_rl/configs/algorithm.py Adds StaticDatasetConfig, expands sampling source union, and updates validation/error text.
docs/training.md Updates training docs table to the new algo config surface and describes dataset SFT option.
docs/algorithms.md Documents dataset vs frozen-teacher SFT sources and updates algorithm abstraction text accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/prime_rl/orchestrator/static_sft.py Outdated
Comment thread src/prime_rl/orchestrator/static_sft.py Outdated
Comment thread src/prime_rl/orchestrator/train_sink.py Outdated
Comment thread packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py Outdated
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch 2 times, most recently from f1ecb24 to 72157c4 Compare June 22, 2026 17:42
@hallerite hallerite force-pushed the feat/algorithm-abstraction branch from 87c38c7 to d33b3f4 Compare June 24, 2026 05:56
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from 72157c4 to e7b0445 Compare June 24, 2026 20:15
Comment thread src/prime_rl/orchestrator/train_sink.py Outdated
Comment thread src/prime_rl/orchestrator/train_sink.py Outdated
Comment thread src/prime_rl/orchestrator/dispatcher.py Outdated
Comment thread src/prime_rl/orchestrator/envs.py Outdated
Comment thread src/prime_rl/orchestrator/envs.py Outdated
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch 4 times, most recently from b20f9e9 to de73527 Compare June 25, 2026 05:57
Base automatically changed from feat/algorithm-abstraction to main June 27, 2026 19:15
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from de73527 to c89d818 Compare July 1, 2026 23:47
tim0120 and others added 2 commits July 1, 2026 17:29
…l in is_trainable

sft/opd/opsd ship advantages=None by design, so the per-step Trainable
metric read 0% and warned about task difficulty on every step of a
CE/ref-KL run. A stamped nonzero ce/ref_kl weight stream is a training
signal too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- StaticDatasetConfig.type is required: the undiscriminated SamplingSource
  union silently validated a frozen model missing base_url as a dataset
- gibberish/repetition filters exempt rollouts without sampling logprobs
  (frozen-teacher and static-replay rollouts zero-fill branch logprobs and
  read as prob-1.0 confidence)
- replay renderer uses the orchestrator tokenizer (honors [orchestrator.tokenizer])
  instead of loading a second copy from the served model name; renders are
  serialized behind a lock and the DefaultRenderer case fails at start()
- tool_calls normalize to the OAI wire shape (dict arguments -> JSON string,
  missing ids filled); tools accept the verifiers tool_defs column and bare
  shapes, mirroring the offline SFT trainer
- dataset rows stay Arrow-backed and are indexed per task_idx instead of
  materializing the whole split
- dataset-sourced envs default group_size to 1 instead of failing on the
  inherited global value; only an explicit conflict raises
- _maybe_json only accepts message-shaped parses so JSON-looking text
  completions (e.g. "42") stay content; local replays no longer consume
  rate-limiter slots; is_trainable is cached after stamping

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from 8ce003e to defb27d Compare July 2, 2026 05:00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit defb27d. Configure here.

Comment thread src/prime_rl/orchestrator/types.py
A batch window that ships no samples carries its rollouts into the next
one, so a read from before group finalization could freeze a stale False
(caught by Bugbot on the previous commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tim0120 tim0120 force-pushed the feat/static-sft-sampler branch from c4d8830 to 64660b2 Compare July 2, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants