Distill: CoT Prompting & Student–Router Distillation
August 22, 2025 · View on GitHub
This repository contains two complementary components:
- Prompting pipelines (
cot_pipeline.py,prompt_gen.py) — generate Chain-of-Thought (CoT) prompts per dataset, batch-call an LLM API (Gemini/OpenAI), and write enriched logs with correctness flags. - Training pipelines (
main.pyand variants) — a two-student mixture-of-experts (MoE) setup with a CoT router and optional KL-weighted ensembling, built on 🤗 Transformers + LoRA + Accelerate.
The goal is to (a) produce diverse reasoning traces and (b) learn routing/ensembling policies over students using those traces.
🔧 Installation
Python: 3.10+ recommended.
# create env (example)
conda create -n distill python=3.10 -y
conda activate distill
# core deps (minimal)
pip install -U pip wheel
pip install "transformers>=4.41"
pip install "accelerate>=0.28"
pip install "peft>=0.11"
pip install "bitsandbytes>=0.43"
pip install "torch>=2.1"
# choose ONE of the LLM API clients (or both)
pip install google-generativeai # if you plan to call Gemini
pip install openai # if you plan to call OpenAI
(Others including tqdm, regex ...)
🔐 API Keys (for prompting)
Set exactly one of the following (depending on which client you use):
# Gemini
export GOOGLE_API_KEY="...your key..."
# OpenAI
export OPENAI_API_KEY="...your key..."
Inside the code, you must implement batch_call_gemini_api (or replace it with your preferred provider) to actually send batched prompts and return text responses. See Implement your API caller below.
📁 Repository Structure
Distill/
├── cot_pipeline.py # Multi-style CoT prompting over datasets
├── prompt_gen.py # Single powerful wrapper prompt variant
├── math_utils.py # Robust math answer parsing & equivalence
├── utils.py # Choice / answer extractors & helpers
├── main.py # Two-student MoE + router + (optional) KL
├── main_log_router.py # Same as main.py + per-epoch router logs
├── main_log_weight.py # Weighted-ensemble logging variant
├── distill_naive.py # Baselines / ablations
├── calc_posrate.py # Compute per-CoT positive routing rates
├── regen_correct_with_flags.py # Rebuild correct-only logs from enriched
├── test_dummy_run.py # Offline test harness (no API calls)
├── data/ # <you create this> dataset root
├── results/ # example results dumps
└── plot/ # plotting assets & notebooks
The archive you received also includes
data/with placeholders. For real runs, create your owndata/as described below.
📦 Datasets & Expected Formats
Create a root folder (default ./data) with one subfolder per task. Typical tasks covered in the code include:
gsm8k(grade-school math word problems)math(formal math; LaTeX answers)arc_challenge(multiple-choice science)commonsense_qa(MCQ)date(MCQ)anli(NLI)strategy_qa(Yes/No)table_mwp(table-based math word problems)
Each task directory can include files like train/, dev/, test/ or flat JSONL files. The prompting scripts scan for *.jsonl files (excluding anything under train/ by default).
Minimal JSONL schema per line
Below are examples. Field names are what the scripts look for internally.
- GSM8K / Math
{"id":"gsm8k-0001", "question":"...", "gold_answer":"\boxed{12}"}
- Multiple-choice (ARC, CSQA, DATE)
{
"id":"arc-123",
"question":"Which ...?",
"choices": { "label": ["A","B","C","D"], "text": ["opt1","opt2","opt3","opt4"] },
"gold_answer":"C"
}
- ANLI
{"id":"anli-42", "premise":"...", "hypothesis":"...", "gold_answer":"entailment"}
- StrategyQA
{"id":"sqa-7", "question":"Is ...?", "gold_answer":"Yes"}
- Table-MWP
{"id":"tmwp-9", "table":"<serialized table or path>", "question":"...", "gold_answer":"\boxed{37}"}
The scripts will write two outputs per source file:
*.enriched.jsonl— original + prompts/responses/predictions/flags*.correct.jsonl— only entries for which at least one style was correct
▶️ Quickstarts
A) Prompting with multi-style CoT
python cot_pipeline.py --root ./data --n 6 \ # number of reasoning styles per sample
--model flash # string you pass into your API caller
This scans every cot_response.jsonl under each task directory (skipping train/), wraps the dataset-specific core prompts in several reasoning-style wrappers, calls your LLM API in batches, and saves *.enriched.jsonl and *.correct.jsonl alongside the originals.
B) Prompting with a single powerful wrapper
python prompt_gen.py --root ./data --n 1 \ # ignored; always 1 in this variant
--model pro
C) Two-student MoE training (LoRA + Accelerate)
Prepare GPU(s) with enough VRAM. Quantized loading is supported via bitsandbytes.
accelerate launch --num_processes=1 main.py --task gsm8k --model1 mistralai/Mistral-7B-Instruct-v0.3 --model2 google/gemma-7b-it --epochs 10 --bs 4 --use_kl true --is_router true --is_quality true --outdir runs/gsm8k
Variants:
main_log_router.py— logs per-epoch router decisions tooutdir/router_logs_epoch<e>.jsonl.main_log_weight.py— logs weighted-ensemble signals for analysis.
D) Analyze router positive rates
python calc_posrate.py --log_dir runs/gsm8k
E) Offline smoke test without network calls
python test_dummy_run.py
This uses a dummy model to patch batch_call_gemini_api and drives a mini run end‑to‑end for sanity checks.
🧩 Implement your API caller
Both cot_pipeline.py and prompt_gen.py expect a function named batch_call_gemini_api(prompts, model_name) that returns a list of strings (one response per prompt). A simple OpenAI-based implementation could look like:
# in prompt_gen.py (or a new module you import)
from openai import OpenAI
client = OpenAI()
def batch_call_gemini_api(prompts, model_name="gpt-4o-mini"):
out = []
for p in prompts:
resp = client.chat.completions.create(
model=model_name,
messages=[{"role":"user","content": p}],
temperature=0.8,
max_tokens=1000,
)
out.append(resp.choices[0].message.content.strip())
return out
If you use Gemini, mirror the same signature with
google-generativeai’s async or sync client.cot_pipeline.pycontains commented snippets showing how it was wired previously.
✅ Answer extraction & evaluation
- Arithmetic / LaTeX answers are checked via
math_utils.pyusing SymPy & latex2sympy2, tolerant to boxed answers, units, radicals, percentages, etc. - Multiple-choice/boolean datasets use helper extractors in
utils.py(e.g.,get_number_choice,get_alphabet_choice,get_true_false,get_yes_no,extract_answer_anli).
Each enriched sample records:
{
"prompts": [...],
"responses": [...],
"preds": [...], // normalized model answers
"correct_flags": [true,false] // per-style correctness
}
⚠️ Common gotchas
- Max output tokens: When following reverse thinking styles you may need
max_tokens≈1000. Trim if your provider has strict limits. - File scanning: By default, training JSONLs under
train/are skipped by the prompting scripts. - SymPy parsing: If LaTeX is malformed,
latex2sympy2may fail. Consider normalizing answers or catching exceptions. - Quantization:
bitsandbytesrequires CUDA alignment with your PyTorch build. If you hit import errors, install the matching wheels or run in FP16/FP32 without bnb. - Accelerate config: Run
accelerate configonce and pick a config that matches your hardware.
📊 Reproducibility
- Set
--seedflags where available to make sampling/initialization repeatable. - Log dirs under
runs/capture router decisions and (optionally) ensemble weights for analysis.
📝 Citation
If you build on this codebase, please cite the repository in your work:
@software{distill_repo,
title = {Distill: CoT Prompting and Student–Router Distillation},
year = {2025},
}
🛠️ License
Unless otherwise stated in the file headers, code in this repository is provided under the Apache 2.0 License.