# Chapter 8 Bonus Material: Train with Distillation This folder contains a simple distillation script for training the Qwen3 0.6B model on teacher-generated reasoning traces, as covered in chapter 8.   ## Files - [distill.py](distill.py): Trains Qwen3 0.6B on JSON-formatted distillation data (more on the format in the next section). - By default, it trains the base model with the base tokenizer - If you pass `--use_think_tokens`, it uses the reasoning tokenizer and wraps the reasoning trace as `...` before the final answer similar to how it's done in chapter 8 - After each epoch, it saves a checkpoint to `checkpoints/distill/` and appends training metrics to `logs/distill_metrics.csv` - If you initialize from `--checkpoint_path` (optional) instead of the base model, you can continue an already existing checkpoint - [distill_batched.py](distill_batched.py): Batched version of the script above. - It uses the padding-aware batched Qwen3 implementation so examples of different lengths can be trained together - It adds a `--batch_size` argument to process multiple examples per optimization step - It saves checkpoints to `checkpoints/distill_batched/` and appends metrics to `logs/distill_batched_metrics.csv` - Of course, note that the batched variant uses much more GPU memory (depending on the batch size) The script imports shared functionality from the [`reasoning_from_scratch`](../../reasoning_from_scratch) package to avoid duplicating the model-loading and prompt-formatting code. (See [chapter 2 setup instructions](../../ch02/02_setup-tips/python-instructions.md) for installation details.)
--- **Note**: If you are not a `uv` user, replace `uv run ...py` with `python ...py` in the examples below. ---   ## Input data format The input is the JSON output produced by [`../02_generate_distillation_data`](../02_generate_distillation_data). Each row should look like this: ```json { "problem": "Compute 1/2 + 1/6.", "gtruth_answer": "2/3", "message_thinking": "I will rewrite the fractions with a common denominator.", "message_content": "The final answer is \\boxed{\\tfrac{2}{3}}." } ``` For training, only the following fields are used: - `problem`: inserted into the same math prompt template used in chapter 3 - `message_content`: required; used as the supervised target answer - `message_thinking`: optional; if present, it is prepended before `message_content` Rows with missing or malformed fields are skipped automatically, and examples longer than `--max_seq_len` are filtered out before the train/validation split.   ## Example run For a quick sanity check, you can train on a small sample generated in the previous folder: ```bash uv run distill.py \ --data_path ../02_generate_distillation_data/sample_openrouter_outputs.json \ --dataset_size 5 \ --validation_size 1 \ --epochs 2 \ --log_every 1 ``` This will: - load the base Qwen3 0.6B weights - tokenize the prompt/answer pairs - reserve 1 example for validation - save a checkpoint after each epoch in `checkpoints/distill/` - write CSV metrics to `logs/distill_metrics.csv` If you want to train with explicit reasoning tags and the reasoning tokenizer instead, add `--use_think_tokens`: ```bash uv run distill.py \ --data_path ../02_generate_distillation_data/sample_openrouter_outputs.json \ --dataset_size 5 \ --validation_size 1 \ --epochs 2 \ --log_every 1 \ --use_think_tokens ``` If you want to train in batches instead, run: ```bash uv run distill_batched.py \ --data_path ../02_generate_distillation_data/sample_openrouter_outputs.json \ --dataset_size 5 \ --validation_size 1 \ --epochs 2 \ --batch_size 2 \ --log_every 1 ```   ## Useful options ```bash uv run distill.py --help ``` Important arguments: - `--data_path`: path to the distillation JSON file - `--dataset_size`: truncate the dataset before splitting (`0` uses all rows) - `--validation_size`: absolute number of validation examples - `--epochs`: number of passes over the training split - `--batch_size`: number of examples per optimization step in `distill_batched.py` - `--lr`: AdamW learning rate - `--max_seq_len`: drops examples whose prompt + answer sequence is longer than this limit - `--checkpoint_path`: initialize from an earlier distillation checkpoint - `--grad_clip_norm`: optional gradient clipping - `--use_think_tokens`: switch to the reasoning tokenizer and `...` formatting See the "Experiments" section below for hands-on examples.   ## Evaluating a distilled checkpoint After training, you can evaluate a checkpoint on MATH-500 using the chapter 3 evaluation script. If you trained without `--use_think_tokens`, evaluate it as a `base` model: ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model base \ --checkpoint_path checkpoints/distill/qwen3-0.6B-distill-step00004-epoch1.pth ``` **Important:** If you trained with `--use_think_tokens`, evaluate it as a `reasoning` model so the reasoning tokenizer is used: ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model reasoning \ --checkpoint_path checkpoints/distill/qwen3-0.6B-distill-step00004-epoch1.pth ```   ## Experiments The distillation datasets used in chapter 8 are available from my Hugging Face repo at [rasbt/math_distill](https://huggingface.co/datasets/rasbt/math_distill). In chapter 8, they are loaded via a helper that downloads partitions, e.g., ````python from reasoning_from_scratch.ch08 import load_distill_data _ = load_distill_data( partition="deepseek-r1-math-train.json", local_path="deepseek-r1-math-train.json" ) _ = load_distill_data( partition="qwen3-235b-a22b-math-train.json", local_path="qwen3-235b-a22b-math-train.json" ) ```` For the experiments below, I used the `deepseek-r1-math-train.json` and `qwen3-235b-a22b-math-train.json` files from that dataset collection.   | | Teacher data | Epoch | MATH-500 Acc | Final val loss | | ---- | ------------------------------------ | ----- | ------------ | -------------- | | 1 | Base (chapter 3) | - | 15.2% | - | | 2 | Reasoning (chapter 3) | - | 48.2% | - | | 3 | DeepSeek R1 distillation data | 1 | 30.6% | 0.5436 | | 4 | DeepSeek R1 distillation data | 2 | 32.4% | 0.5349 | | 5 | DeepSeek R1 distillation data | 3 | 33.6% | 0.5343 | | 6 | Qwen3 235B A22B distillation data | 1 | 45.0% | 0.4043 | | 7 | Qwen3 235B A22B distillation data | 2 | 43.8% | 0.3963 | | 8 | Qwen3 235B A22B distillation data | 3 | 44.2% | 0.3948 | The training takes about 30 min on an H100 and about 3 hours on a DGX Spark and uses up to 15 GB RAM. Below are the code snippets to reproduce the results reported in the table.   **Row 1** ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model base ```   **Row 2** ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model reasoning ```   **Rows 3, 4, & 5** ```bash uv run distill.py \ --data_path deepseek-r1-math-train.json \ --validation_size 25 \ --epochs 3 \ --lr 1e-5 \ --max_seq_len 2048 \ --use_think_tokens \ --grad_clip 1.0 ``` Then, to evaluate the epoch checkpoints, run:   ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model reasoning \ --max_new_tokens 4096 \ --checkpoint_path run-1/checkpoints/distill/qwen3-0.6B-distill-step06682-epoch1.pth ``` For row 4 and row 5, replace the checkpoint path with `...step13364-epoch2.pth` and `...step20046-epoch3.pth`, respectively.   **Rows 6, 7, & 8** ```bash uv run distill.py \ --data_path qwen3-235b-a22b-math-train.json \ --validation_size 25 \ --epochs 3 \ --lr 1e-5 \ --max_seq_len 2048 \ --use_think_tokens \ --grad_clip 1.0 ``` Then, to evaluate the epoch checkpoints, run: ```bash uv run ../../ch03/02_math500-verifier-scripts/evaluate_math500.py \ --dataset_size 500 \ --which_model reasoning \ --max_new_tokens 4096 \ --checkpoint_path run_11/checkpoints/distill/qwen3-0.6B-distill-step05746-epoch1.pth ``` For row 7 and row 8, replace the checkpoint path with `...step11492-epoch2.pth` and `...step17238-epoch3.pth`, respectively.