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 job, as an override on the seed dataset. Training inherits it from the training dataset, so it needs no override there. Download the seed dataset’s config into reasoning:

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

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

distil training-dataset create-from-seed-dataset --output json \
  --config reasoning/config.yaml <seed-dataset-id> | jq -r .id
  • The trace jobs (test set from traces and trace processing) reject true. Run them with it off.
  • Turning it on only at training, on a training dataset generated without it, trains an empty thinking block without an error. Generate a new training dataset instead.
  • synthgen.generation_target: 0 skips generation and still writes reasoning onto the existing seed rows, which adds reasoning to a labeled set without generating new rows.
  1. Generation. The teacher writes reasoning_content next to every assistant message it generates.
  2. Backfill. After generation, a teacher pass writes reasoning on the final assistant message of every training row that has none, seed 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.

  • 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 temperature=0. 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: Synthetic data generation.