The flow
rllm dataset from-eval when the traces came from an rllm eval run (see Running evaluations). Use rllm dataset import when you have external trace files (an OpenAI messages export, or <think>-tagged rows). Both produce the same registered dataset, and both feed rllm sft unchanged.
The SFT row schema (the contract)
Every dataset the pipeline produces is a list of{"messages": [...]} rows. Each message carries a per-message trainable flag, and its content is a list of structured parts rather than a flat string:
Structured SFT row
text (visible output) and thinking (the model’s reasoning). Tool calls ride on the message as a canonical OpenAI-shaped list:
rllm/data/sft_schema.py (SFTMessage, TextPart, ThinkingPart, SFTToolCall).
The import bridges also accept common provider wire variants. Assistant
reasoning_content or reasoning becomes one leading thinking part;
tool_calls may be a list or a JSON-stringified list; and
function.arguments may be a JSON string or a parsed object/array. Stored
canonical rows always use structured thinking, a tool-call list, and
JSON-string arguments. Ambiguous dual reasoning representations are rejected.
Multi-message rows and loss targets
A row is one training example containing an orderedmessages sequence. A
message with trainable: true is a target message; trainable: false makes
it context-only. Context-only messages stay in the rendered sequence—the
flag controls loss eligibility, not visibility.
The flag applies to the whole rendered message. Its thinking, visible text, and
tool-call output cannot be masked independently. The canonical schema does not
restrict explicit targets by role, although derived masks and think-tag
explosion select assistant messages.
For rows that remain intact (messages, direct normalization, or compact
think-tags), mask resolution is all-or-nothing:
- If every source message has a boolean
trainable, rLLM preserves the mask exactly and ignores the derivation policy. - If any value is missing, null, or non-boolean, the mask is partial. rLLM
derives the entire mask again, overwriting any explicit values in that row.
messagesuses itsallorlastpolicy; compactthink-tagsselects every assistant message.
think-tags, exploded mode emits one prefix row per selected assistant
target. The selected target is the final and only trainable message in that
row; future messages are omitted, and thinking is removed from historical
assistant messages. A complete source mask uses trainable: true assistant
messages as selectors, then rewrites each emitted row to one target and
all-false history. A non-assistant selector is rejected. A partial mask ignores
its explicit values and selects every assistant message.
A renderer may still omit content according to its history policy. If a
renderer is configured to omit historical reasoning, explosion keeps each
selected reasoning target final so that history stripping cannot remove it.
Why structured thinking parts, not baked <think> strings
Different model families wrap reasoning in different wire formats — DeepSeek uses <think>...</think>, Qwen and Harmony use their own. If the dataset hard-coded one format, it would only train that family. Instead, reasoning is stored model-agnostically as a thinking part, and the student’s renderer decides the on-the-wire format at training time. One curated dataset trains any student.
Flag-less and partially flagged rows still work
You can handrllm sft a plain {"role", "content": "..."} dataset with no trainable flags at all (e.g. an external --train-file). The loader derives the mask: assistant messages are trainable, everything else is not. With --tokenize-method stepwise, only the last assistant message is trained instead of all of them. A complete boolean mask is used verbatim; a partial mask is fully re-derived as described above.
Getting data in
rllm dataset from-eval — curate your own eval runs
from-eval reads one or more rllm eval run directories, filters tasks by an aggregate metric, selects which trajectories to keep, and emits SFT rows.
--metric (what avg/best/worst aggregate — is_correct, reward, or a signal name), --filter (task-level DSL over aggregates), --select (correct | best | best-n | shortest | all), --max-per-task, --min-reward, --dedup/--no-dedup, --trajectory (named trajectory for multi-agent flows), and --val-fraction to hold out tasks as a validation split.
--max-per-task caps the number of attempts kept per task, not the number of rows. A single attempt can expand into several rows when the automerge walk splits it (below).The automerge walk
Each kept attempt (episode) is turned into one or more rows by a deterministic walk over its steps (rllm/eval/curation.py, the message-extraction section). The data alone decides the shape — no flag toggles it:
- Non-thinking / interleaved histories — when each step’s conversation is a clean prefix of the next (the harness feeds full history back verbatim, reasoning included), the steps merge into one multi-turn row with every assistant turn trained.
- Reasoning stripped from history — when the harness drops a turn’s reasoning before feeding it back (non-interleaved runs), the prefix breaks and each turn becomes its own split row, so every row matches exactly what the model saw at inference.
CurationStats fields in the summary (all defined in CurationStats, rllm/eval/curation.py):
If an attempt produces many trained steps but zero merges, curation logs a degenerate-splitting warning — usually a per-step-varying history element (a timestamp, a counter) is breaking the prefix. Heavy splitting is worth noticing: it duplicates history across rows, so a
T-turn attempt costs on the order of O(T²) tokens.
rllm dataset import — external trace files
import bridges a local file of {"messages": [...]} rows into the canonical schema and registers it. Choose the source shape with --format (rllm/cli/dataset.py, bridges in rllm/data/sft_bridges.py):
messages— plain OpenAI rows, including provider reasoning fields and OpenAI-shaped tool calls. Complete boolean masks are preserved; otherwise the loss mask is derived from--train-on(allassistant messages, default, or only thelast).think-tags— rows whose assistant messages start with a<think>...</think>block (a common convention for distilled reasoning traces). The bridge splits that block into athinkingpart and carries every non-messagestop-level key through verbatim as row metadata. In compact mode, complete boolean masks are preserved. In exploded mode, true assistant flags select targets, non-assistant selectors are rejected, and a partial mask selects every assistant message.
--explode (think-tags only, default ON)
This is the most important knob for think-tags, so spell out the tradeoff before choosing:
- Exploded (default) — the conversation is split into one row per selected assistant target. A complete source mask selects the targets; with a partial mask, every assistant message is selected. History messages keep only visible text (thinking stripped), and the row’s single final assistant message is the trained target with its thinking kept. This matches inference when the renderer omits historical reasoning. The cost is duplicated history tokens.
--no-explode— one compact row per full conversation. A complete source mask is preserved; a partial mask makes every assistant message trainable. If the renderer omits historical thinking, only the final message’s thinking is emitted and trained. You save history-token duplication but may train fewer reasoning blocks.
--explode when every selected reasoning block
must be a final target. Use --no-explode for compact conversations when your
renderer retains the reasoning you intend to train.
Training with rllm sft
CUSTOMIZED mode, driven by each message’s trainable flag — the data decides the loss mask (rllm/trainer/sft/tinker_backend.py, build_sft_data).
Logging training progress
Per-step training metrics (loss, learning rate, progress) route through rLLM’s unified tracking layer (rllm.utils.tracking.Tracking). console logging is always on; add more backends with --logger:
--logger accepts console | wandb | mlflow | swanlab | tensorboard | file | ui. --project/--experiment name the run in whichever backend you pick (default rllm-sft / the dataset name). --ui is auto-enabled when you’re logged in; pass --no-ui to opt out.
Live rLLM UI logging (
--ui) is supported on the tinker and fireworks backends only; on --backend verl it is dropped with a warning.Renderer auto-detection and --renderer
The tinker/fireworks path renders rows through a tinker-cookbook renderer. rLLM auto-detects it from the model in this order (_resolve_renderer_name, rllm/trainer/sft/tinker_backend.py):
- An explicit
--rendererwins (an advisory warning fires if it looks mismatched — never fatal). - Otherwise tinker-cookbook’s recommendation map (
get_recommended_renderer_name). - Otherwise a family heuristic on the model basename (
qwen3.5 → qwen3_5,qwen3 → qwen3,deepseek → deepseekv3,llama-3 → llama3). - Otherwise fall back to
role_colonwith a warning.
--renderer when auto-detection can’t place your model (e.g. a small or renamed checkpoint tinker’s map doesn’t cover). Valid names:
Backend support matrix
Fireworks subclasses the tinker backend and reuses the same rendering pipeline, so structured rows behave identically. The verl backend rejects structured rows — parts-list content or per-message
trainable flags — with an actionable SFTConfigError (_reject_structured_rows, rllm/trainer/sft/verl_backend.py); plain {role, content: str} rows still train fine on verl. Structured SFT support on verl is planned.
Worked example: importing think-tagged teacher traces
Starting from an external file of<think>-tagged teacher traces, distill them into Qwen/Qwen3.5-4B on tinker.
import prints how many source rows expanded into how many SFT rows (explosion inflates the count), then the exact inspect and sft commands to run next.
Troubleshooting
CommonSFTConfigError messages and what to do:
Next steps
Running evaluations
Generate the teacher traces that feed
from-evalTinker backend
Configuration and LoRA options for the tinker trainer
Fireworks backend
Hosted training on Fireworks (same data path)
Bring your own dataset
Register custom datasets for eval and training

