feat(from-eval): model-agnostic reasoning-preserving automerge into self-describing SFT rows#739
Draft
signalrush wants to merge 2 commits into
Draft
Conversation
d07886b to
41f3071
Compare
da6eb68 to
17ff3d8
Compare
…CUSTOMIZED masking rllm dataset from-eval read only the final step's conversation and dropped reasoning_content, so it could not distill a reasoning model. It now walks each attempt's steps into self-describing rows — every message carries a 'trainable' flag — deterministically, no flag decides it: - Reasoning is preserved MODEL-AGNOSTICALLY as a structured ThinkingPart, NOT a hardcoded '<think>' string: the model's renderer picks the format at training time (deepseek <think>, qwen, harmony, ...). Content is a uniform list of parts so the parquet column stays one type. Verified: the same curated dataset renders correctly under both the deepseek and qwen renderers, each in its own format. - Automerge walk (message-level analogue of verl.transform._process_trajectory): merges steps that appear identically as history into one row (all turns trained) and splits where they don't. A non-thinking trajectory is one prefix chain -> single row; a reasoning turn seals its segment (its thinking is stripped from history) so the next turn starts a new row -> per-turn rows, the inference- faithful form. Every step is trained; only a step with no assistant turn is skipped. - SFT masking is ALWAYS CUSTOMIZED, driven by each message's 'trainable' flag. Self-describing rows use their flags; flag-less rows (external --train-file) derive a default in the loader (all assistant turns, or the last when tokenize_and_mask_method=stepwise). Removes resolve_train_on_what and the train_on_what enum threading — one masking path. Removes the obsolete last-step-only extraction (_episode_to_messages / _clean_message / _load_messages). No CLI flags added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SFT loop saved every checkpoint promotable=False and deletes the trainer job on close, so the trained LoRA was a resumable-only DCP blob garbage-collected after the ~30-day retention window — never a servable model. Make the final save promotable=True and promote_latest() it to a permanent account model before infra.close(), mirroring the RL path and the SDK sft recipe. Promotion failure is non-fatal (the promotable row outlives deletion; can be promoted manually). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17ff3d8 to
1f0976c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes
rllm dataset from-evalable to SFT-distill a reasoning model — which it currently can't — model-agnostically, and fixes SFT checkpoints so the trained LoRA survives. No new flags.1. Model-agnostic reasoning-preserving automerge → self-describing SFT rows
from-evalread only the final step's conversation and droppedreasoning_content(_clean_message), so distilling a reasoning teacher yielded reasoning-free data.It now walks each attempt's steps and emits rows where every message carries a
trainableflag, deterministically — no flag or config decides it:ThinkingPart, not a hardcoded<think>string. The model's renderer picks the reasoning format at training time (deepseek<think>, qwen, harmony, …). Content is a uniform list of parts so the parquet column stays one type. Verified: the same curated dataset renders correctly under both the deepseek and qwen renderers, each in its own format.verl.transform._process_trajectory): merges steps that appear identically as history into one row (all turns trained) and splits where they don't. A non-thinking trajectory is one prefix chain → single row; a reasoning turn seals its segment (its thinking is stripped when it becomes history), so the next turn starts a new row → per-turn rows, the inference-faithful form (at inference the harness strips prior-turn thinking, matching each row's stripped history). A mid-episode context reset (summarization) also splits.2. Always-CUSTOMIZED masking (removes
resolve_train_on_what)SFT masking is now a single path driven by each message's
trainableflag. Self-describing rows (from automerge) use their flags; flag-less rows (external--train-file, other datasets) derive a default in the loader — all assistant turns train, or just the last whentokenize_and_mask_method=stepwise— reproducing the oldALL_ASSISTANT/LAST_ASSISTANTbehavior. This removesresolve_train_on_whatand thetrain_on_whatenum threading. Backward compatible.Removes the obsolete last-step-only extraction (
_episode_to_messages/_clean_message/_load_messages).Behavior note:
from-evaloutput now carriestrainableand multi-turn traces automerge. Training outcome is equivalent for non-thinking data and strictly better for reasoning data (reasoning preserved). Existing curation tests pass.3. Promote the final Fireworks LoRA so it survives teardown
The SFT loop saved every checkpoint
promotable=Falseand deletes the trainer job on close, so the trained LoRA was a resumable-only DCP blob GC'd after ~30 days — never servable. The final save is nowpromotable=Trueandpromote_latest()'d to a permanent account model beforeinfra.close(), mirroring the RL path and the SDK's own sft recipe. Non-fatal on failure.Validation
tests/eval/test_curation_automerge.py): per-turn split, non-thinking merge, every-step-trained, reasoning-seals-segment, context-reset split, prefix primitive,_ensure_trainablederive.curate()→ 32,824 rows (24.7/episode), 0 skipped, 473 merged. All content is structured list-of-parts, no hardcoded<think>strings. Every row renders through the actualCUSTOMIZEDdatum path with non-zero weights — 0 failures over 300 sampled rows. The same rows render correctly under deepseek (<think>…) and qwen3 (its format), proving model-agnosticism.🤖 Generated with Claude Code