Skip to content

Reasoning models

A reasoning student writes out its reasoning in a thinking block, then gives the answer. For Qwen3 models the block is <think>…</think> at the start of the reply. One config key controls it: base.enable_thinking (default false). With it on, synthetic data generation writes the reasoning, and the student is trained, evaluated and served with thinking on. It works for every task type.

Use a reasoning student when getting to the answer takes several steps (a calculation, a rule applied to the facts, a choice between close options) and a normal student comes in short of the teacher. It adds output tokens to every request, so replies are slower, and only the answer is scored. Train the same student with and without thinking on the same data and compare.

A -thinking teacher doesn’t make the student a reasoning student; the two are separate settings.

Qwen3-0.6B, Qwen3-1.7B, Qwen3-8B, Qwen3.5-0.8B, Qwen3.5-2B, Qwen3.5-4B, Qwen3.5-9B, Qwen3.8-27B and Qwen3.6-35B-A3B. Any other student fails when the config is loaded, with:

base.enable_thinking=true is not available for student model '<name>'

For a 4B reasoning student use Qwen3.5-4B, not Qwen3-4B-Instruct-2507.

base.enable_thinking: true goes in the config of the synthetic data generation run into the train split (Building the train set), as an override on the dataset it runs on. The dataset it writes carries the setting, so training inherits it and needs no override. Download the dataset’s config into reasoning:

distil dataset download-metadata -d reasoning <dataset-id>

Set base.enable_thinking: true in reasoning/config.yaml, and submit the generation job with it:

distil dataset generate-synthetic-data-train --config reasoning/config.yaml <dataset-id>
  • Relabeling into the train split accepts it too and writes reasoning on the relabeled rows (Where the reasoning comes from). Test rows need no reasoning, so relabel the test set with it off.
  • Turning it on only at training, on a dataset generated without it, trains an empty thinking block without an error. Generate again with it on instead.
  • synthgen.train_generation_target: 0 skips generation and still writes reasoning onto the existing train rows, which adds reasoning to a labeled set without generating new rows.
  • A run with it on splits multi-turn rows per turn, so a later top-up can’t use that dataset’s train split as examples (Topping up).
  1. Relabeling. Every relabeled assistant turn carries reasoning. Reasoning found in the trace is kept when it is correct, grounded and leads to the answer; missing or bad reasoning is written by the teacher. With relabel: false the rows are written as traced, and the backfill fills the gaps.
  2. Generation. The teacher writes reasoning_content next to every assistant message it generates.
  3. Backfill. After generation, a teacher pass writes reasoning on the final assistant message of every training row that has none, uploaded and relabelled rows included. It changes only the reasoning, costs one teacher call per row, and drops rows still without reasoning after a retry. The log says Reasoning backfill: kept X of Y.

The teacher writes in the first person, works the answer out rather than explaining it, rules out the nearest alternative, and invents no facts. Instructions on the style and length of the reasoning go in synthetic_data_generation_instructions (Job description), for example “Keep the reasoning under 250 tokens: one short line per check.” In a smoke run, read the reasoning_content of a handful of rows: it should work the answer out, not restate it.

  • reasoning_content goes on the assistant message; for a tool call, on the assistant message that carries tool_calls.

    {"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "5", "reasoning_content": "..."}]}
  • In train and test rows, a reasoning key, or any other key at the row’s top level, is dropped without an error.

  • Reasoning is optional per row; the backfill fills the gaps. test.jsonl needs none.

  • In traces, an assistant message’s reasoning is read from reasoning_content or reasoning (the key OpenRouter and newer vLLM versions return). With enable_thinking: false it is removed from the traces.

  • Conversations are split into one row per assistant turn, and only the final turn keeps its reasoning.

  • With enable_thinking: false, reasoning in the data is removed on read, so the same data serves both kinds of student.

  • tuning.max_completion_length (default 2048 tokens) caps what the student generates in evaluation, reasoning included. A cut-off completion scores near zero. Training logs tuning.max_completion_length (X) is below the longest training completion (Y) when it is too low; raise it then.
  • synthgen.validation_max_total_length is also checked after the backfill, and rows that grew past it are dropped. Backfill and length drops aren’t replaced, so compare the train row count with the target after generation.

Metrics read only content and tool_calls; there is no reasoning metric. The student is evaluated with thinking on, at the vendor’s thinking sampling settings (Qwen3: temperature 0.6, top_p 0.95, top_k 20; Qwen3.5, Qwen3.6 and Qwen3.8: temperature 1.0, top_p 0.95, top_k 20). To judge the reasoning, read it in the prediction rows.

  • The generated model_client.py sends chat_template_kwargs: {"enable_thinking": true} with the thinking sampling settings above. Use it, as for any model.
  • Hosted inference needs no extra setup.
  • When you serve the model yourself, start vLLM with the reasoning parser for the student’s family (Local deployment), or the thinking block stays at the start of content. With the parser, the reasoning comes back in reasoning_content and content holds only the answer.
  • Reasoning text in the answer is expected from a reasoning student. It’s a problem only for a model trained with enable_thinking: false.

Every other parameter: Config file. The stage that writes the reasoning: Relabeling and synthetic data.