FastSFT
Distill any open-weight LLM into a small one. Generate training data automatically. Fine-tune locally or on cloud GPUs. Compare against the parent.
FastSFT is a structured pipeline based tool that transforms a one-sentence description into a fully trained small model that mimics a larger teacher's style. It handles synthetic data generation, quality filtering, formatting, training, and evaluation โ all with sensible defaults and override control.
Built on distilabel, OpenRouter, Modal, Hugging Face, and PEFT.
This was built essentially as a personal fun project (vibe coded using claude) to learn via building to understand how AI models work under the hood. Feel free to play around with it for fun! (Note: This repo/library won't be maintained or kept up to date).
โก 60-Second Start
# Install + set API key
uv sync
echo "OPENROUTER_API_KEY=sk-or-..." > .env
# Generate, format, and train end-to-end
uv run fastsft "a pirate-themed customer support chatbot" \
--num-samples 50 \
--child-model-id Qwen/Qwen2.5-0.5B-Instruct \
--local --max-epochs 2
# Your trained adapter is ready in modelsets/<timestamp>/
That's it. Your small model now talks like the parent.
๐ฏ Bare Minimum Requirements
- Python 3.12 โ Required for type hints used by distilabel.
uvโ Fast Python package manager (install):curl -LsSf https://astral.sh/uv/install.sh | sh- OpenRouter API key โ Free tier available at openrouter.ai. Set it:
echo "OPENROUTER_API_KEY=sk-or-..." > .env - For local GPU training โ Run
uv sync --extra local-trainingonce (adds torch, peft, trl, accelerate). - For evaluation โ Run
uv sync --extra evaluationonce (adds sentence-transformers). - For Modal cloud training โ Run
modal token newonce to authenticate.
๐ Writing Good Prompts
Your prompt quality determines your dataset quality. Be verbose and explicit.
| โ Don't | โ Do |
|---|---|
"a pirate" |
"Respond as a friendly pirate to ANY question: use nautical slang, casual tone, pirate emojis" |
"be an engineer" |
"Respond like a pragmatic engineer to ANY topic: systematic thinking, explain trade-offs, technical terminology" |
"respond professionally" |
"Adopt the tone of a senior executive: formal, confident, strategic, data-driven. Answer any question with this mindset." |
Key points:
- Be specific โ Name the persona, role, or archetype clearly
- List key traits โ What makes them unique? (tone, vocabulary, approach, attitude)
- Mention scope โ "to ANY question" (diverse domains) vs. "to medical questions" (narrow domain)
- Give examples โ What should they do/avoid? How should they sound?
See data_generation_tutorial.md for detailed examples and a checklist.
๐ Quick Start: Zero to Model in 15 Minutes
1. Install Dependencies
uv sync # Core setup (data generation)
uv sync --extra local-training # Add this to train on your machine
2. Set Your API Key
echo "OPENROUTER_API_KEY=sk-or-your-key-here" > .env
3. Generate Training Data
uv run fastsft "a pirate-themed customer support chatbot" --num-samples 50
Saves to datasets/raw/<timestamp>/. Takes ~2 minutes depending on the parent model size.
4. Preview the Data
uv run python -m fastsft.data.viewer # Raw Q&A pairs
uv run python -m fastsft.data.viewer --formatted # Chat-formatted text
5. Train Your Model
uv run fastsft --start-stage fine_tuner \
--input-path datasets/raw/<timestamp> \
--child-model-id Qwen/Qwen2.5-0.5B-Instruct \
--local --max-epochs 2
Your adapter lands in modelsets/<timestamp>/. Takes ~5 minutes on a GPU, longer on CPU.
6. Evaluate Quality
uv sync --extra evaluation # One-time setup
uv run fastsft-eval modelsets/<timestamp>
uv run python -m fastsft.eval.results_viewer
Your tuned model is scored against the parent via LLM judge + embedding similarity.
๐ Three-Stage Pipeline
FastSFT runs three composable stages in sequence, each saving its output immediately:
| Stage | Input | Output | Time | Cost |
|---|---|---|---|---|
| DataGenerator | Your prompt | Q&A dataset with messages column |
~2 min | $0.50โ$2 (OpenRouter API) |
| DataFormatter | Raw dataset | Same data rendered in child model's chat format | ~30 sec | Free |
| FineTuner | Formatted dataset | LoRA adapter (adapter_model.safetensors) |
~5โ20 min | Free (local) or $1โ$5 (Modal) |
Each stage can run independently via --start-stage and --input-path, so you can iterate on data without retraining, or reuse data across model experiments.
๐ฎ CLI Reference
Core Command: Train End-to-End
uv run fastsft "<your description>" [options]
Description examples:
"a pirate-themed customer support chatbot""respond as a financial advisor, concise and formal""explain concepts like you're teaching a 10-year-old"
Essential options:
--num-samples 50โ How many training examples to generate (default: 100).--child-model-id "Qwen/Qwen2.5-0.5B-Instruct"โ The model you're fine-tuning.--localโ Train on this machine (default: auto-picks cheapest cloud GPU).
Data generation options (only used at --start-stage data_generator):
--parent-modelโ The teacher model (default:meta-llama/llama-3.3-70b-instruct).--judge-modelโ Scores generated data (default:deepseek/deepseek-chat).--guide-modelโ Derives instructions from your prompt (default:qwen/qwen-2.5-7b-instruct).--score-threshold 7โ Raise quality filter (0โ10, default: 5).--parent-temperature 0.7โ Sampling temperature (default: 0.9).--breadth-exponent 0.67โ Topic diversity vs. depth (default).
Training options:
--strategy qloraโ Use QLoRA (lower memory, slower). Default:lora.--lora-rank 32โ Adapter rank (default: 16).--batch-size 16โ Per-device batch size (default: 8).--learning-rate 5e-5โ Learning rate (default: 1e-4).--max-epochs 5โ Training epoch ceiling (default: 3).--validation-split 0.2โ Fraction held out for early stopping (default: 0.1).
Resume options:
--start-stage data_formatterโ Skip generation, reformat existing data.--start-stage fine_tunerโ Skip generation + formatting, just retrain.--input-path datasets/raw/<timestamp>โ Load a saved dataset.
GPU options:
--gpu-tier A100-40GBโ Force a specific Modal GPU (skips cost heuristic).--modal-timeout 3600โ Seconds to wait for Modal training (default: 7200).--output-dir /pathโ Base directory fordatasets/,modelsets/, andevalsets/(default: CWD).
Data Viewers
# Preview the latest raw dataset
uv run python -m fastsft.data.viewer
# Preview formatted (chat-template-rendered) data
uv run python -m fastsft.data.viewer --formatted
# Load a specific run
uv run python -m fastsft.data.viewer --input-path datasets/raw/20260809_120000 --num-samples 10
Training Inspection
# Preview training config options for a model (no API calls, free)
uv run python -m fastsft.training.heuristic Qwen/Qwen2.5-0.5B-Instruct
uv run python -m fastsft.training.heuristic Qwen/Qwen2.5-0.5B-Instruct \
--input-path datasets/formatted/20260809_120000
# View loss curves and diagnostics for a training run
uv run python -m fastsft.training.stats_viewer
uv run python -m fastsft.training.stats_viewer modelsets/20260809_120000
uv run python -m fastsft.training.stats_viewer modelsets/20260809_120000 --json
Evaluation
# Setup (one-time)
uv sync --extra evaluation
# Evaluate latest adapter -- writes evalsets/<timestamp>/{eval_prompts,eval_answers.json,eval_results.json}
uv run fastsft-eval # or: python -m fastsft.eval.run
uv run fastsft-eval modelsets/20260809_120000 --num-eval-prompts 10
# View results (defaults to the latest run under evalsets/)
uv run python -m fastsft.eval.results_viewer
uv run python -m fastsft.eval.results_viewer evalsets/20260809_120500 --json
# Rerun with a different judge without regenerating answers
uv run fastsft-eval --reuse-answers-from 20260809_120500 --judge-model openai/gpt-4o-mini
# Spot-check: compare tuned vs untuned on a single prompt
uv run python -m fastsft.eval.inference_viewer "your prompt here"
uv run python -m fastsft.eval.inference_viewer "your prompt here" modelsets/20260809_120000 --tuned-only
๐ In-Depth Tutorials
FastSFT ships with three detailed walkthroughs:
- data_generation_tutorial.md โ Master prompt crafting, dataset iteration, and quality filtering.
- training_tutorial.md โ Navigate LoRA/QLoRA, local vs. cloud training, and hyperparameter tuning.
- evaluation_tutorial.md โ Interpret win rates, spot-check inference, and debug training quality.
Start with TUTORIAL.md for a 15-minute end-to-end walkthrough.
๐๏ธ Architecture
All code lives in src/fastsft/ (an installable package).
src/fastsft/
โโโ main.py # CLI entry point
โโโ pipeline.py # DistillationPipeline orchestrator
โโโ constants.py # Model defaults, environment constants
โโโ helper.py # Distiset I/O, timestamps, metadata, datasets/modelsets/evalsets dirs
โโโ device.py # GPU/CPU/MPS detection for training + eval
โโโ warnings_filter.py # Suppress import-time noise
โโโ stages/ # DataGenerator, DataFormatter, FineTuner
โ โโโ base.py # Stage base class (validate-then-run template)
โ โโโ data_generator.py # Prompt โ Q&A pairs (guide โ generate โ refine)
โ โโโ data_formatter.py # Render to child model's chat template
โ โโโ fine_tuner.py # Train on Modal or locally
โ โโโ constants.py # Stage names
โโโ data/ # Data generation pipeline
โ โโโ config.py # DataGenerationConfig
โ โโโ constants.py # Breadth exponent, refine iterations
โ โโโ prompt_generator.py # Seeds โ user instructions
โ โโโ response_generator.py # Parent answers instructions
โ โโโ refiner.py # Judge-scored quality filtering
โ โโโ viewer.py # Terminal preview CLI
โโโ model/ # OpenRouter model access
โ โโโ base.py # Model: OpenRouter client, open-weight check
โ โโโ guide.py # Guide: derive instructions from your prompt
โ โโโ judge.py # Judge: score answers 0-10
โ โโโ constants.py # Model ids, max tokens, OpenRouter URLs
โ โโโ _logging.py # Clean up distilabel's logger noise
โโโ training/ # LoRA/QLoRA fine-tuning
โ โโโ config.py # TrainingConfig, AdapterConfig, TrainingLoopConfig
โ โโโ constants.py # GPU tier catalog, training defaults
โ โโโ trainer.py # run_sft: shared LoRA/QLoRA core (no GPU dispatch)
โ โโโ local_trainer.py # train_locally: on-machine training
โ โโโ modal_app.py # Modal Image + remote train_lora function
โ โโโ heuristic.py # GPU tier ranking by cost/feasibility
โ โโโ stats.py # Load and interpret training telemetry
โ โโโ stats_viewer.py # CLI: visualize loss curves + diagnostics
โโโ eval/ # Post-training evaluation (optional extra)
โโโ run.py # fastsft-eval CLI: writes evalsets/<run_id>/
โโโ evaluator.py # Collect answers (or reuse) + judge pairs
โโโ inference.py # ChildInferenceEngine (core child generation)
โโโ inference_viewer.py # CLI: spot-check inference
โโโ embeddings.py # Sentence-transformers similarity
โโโ prompt_set.py # Generate/persist/load eval prompts
โโโ results.py # Persist/load eval_answers.json + eval_results.json
โโโ results_viewer.py # CLI: visualize results
โโโ config.py # EvalConfig
โโโ constants.py # Eval defaults
๐ง Key Concepts
Open-Weight Check
FastSFT enforces that only the parent (data source) is open-weight. Why? Closed-model terms of service usually forbid training other models on their outputs. The check uses OpenRouter's hugging_face_id field as the signal; if a model has one, it's open. Defaults (Llama, Qwen) are all open; closed models (Claude, GPT) are rejected upfront with a clear error.
LoRA vs QLoRA
- LoRA โ Standard adapter fine-tuning. Fast, clean. ~16 GB for 7B models.
- QLoRA โ Quantized LoRA. ~4โ6 GB for 7B, but slower. Requires CUDA locally; works anywhere on Modal.
Distiset
FastSFT uses distilabel's Distiset (a datasets.DatasetDict wrapper) throughout. Each stage returns a Distiset; the next consumes it. Distisets are saved to disk as Parquet files under datasets/raw/<timestamp>/ and datasets/formatted/<timestamp>/.
Training Metadata Sidecar
DataGenerator persists training_metadata.json as a sibling file next to the run directory. It stores the parent model identity, the derived style prompt, and generation hyperparameters โ so evaluation can reconstruct the exact parent reference without flags.
Early Stopping
FineTuner holds out a validation slice (default 10%) for early stopping. Training stops if validation loss doesn't improve for N consecutive evals (default 3). This prevents overfitting without fixing epochs upfront.
Evalsets
Each fastsft-eval invocation gets its own evalsets/<timestamp>/ folder holding eval_prompts (the held-out prompt set), eval_answers.json (raw parent/tuned/untuned generations), and eval_results.json (judged win rates + similarity) together โ separate from the adapter's own modelsets/<timestamp>/, since one adapter can be evaluated many times. By default, prompts are reused from the latest evalsets run (for apples-to-apples comparison) but answers are always regenerated; pass --reuse-answers-from <run_id> to skip regeneration and rejudge prior answers (e.g. with a different --judge-model) instead.
Cost Heuristic
If you don't specify --gpu-tier or --local, FineTuner estimates memory/cost for each Modal GPU tier (using your model's real parameter count from Hugging Face Hub and your data's real sequence lengths), then picks the cheapest feasible one. It logs the shortlist; you can override with --gpu-tier or --local.
๐ Complete Workflow Example
# 1. Describe the model you want to build
uv run fastsft "respond as a pirate, always use 'ahoy' and nautical slang" \
--num-samples 100 \
--child-model-id Qwen/Qwen2.5-0.5B-Instruct
# 2. Inspect the generated data (make sure it sounds right)
uv run python -m fastsft.data.viewer --formatted
# 3. Check training config options before committing
uv run python -m fastsft.training.heuristic Qwen/Qwen2.5-0.5B-Instruct \
--input-path datasets/formatted/<timestamp>
# 4. Retrain with your choice of GPU (if needed)
uv run fastsft --start-stage fine_tuner \
--input-path datasets/formatted/<timestamp> \
--child-model-id Qwen/Qwen2.5-0.5B-Instruct \
--lora-rank 32 --max-epochs 5
# 5. Inspect training dynamics
uv run python -m fastsft.training.stats_viewer modelsets/<timestamp>
# 6. Evaluate against the parent
uv sync --extra evaluation
uv run fastsft-eval modelsets/<timestamp>
uv run python -m fastsft.eval.results_viewer
# 7. Try it out
python3 << 'EOF'
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
model = PeftModel.from_pretrained(base, "modelsets/<timestamp>/")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
inputs = tokenizer("Ahoy, what time does the tavern open?", return_tensors="pt")
outputs = model.generate(**inputs)
print(tokenizer.decode(outputs[0]))
EOF
๐ Common Commands Cheat Sheet
# End-to-end pipeline (all stages)
uv run fastsft "your description" --num-samples 50 --child-model-id Model/Name --local
# Resume from middle
uv run fastsft --start-stage data_formatter --input-path datasets/raw/<ts>
uv run fastsft --start-stage fine_tuner --input-path datasets/formatted/<ts> --local
# Preview data
uv run python -m fastsft.data.viewer
uv run python -m fastsft.data.viewer --formatted
# Training insights
uv run python -m fastsft.training.heuristic Qwen/Qwen2.5-0.5B-Instruct
uv run python -m fastsft.training.stats_viewer modelsets/<timestamp>
# Evaluation
uv run fastsft-eval modelsets/<timestamp> --num-eval-prompts 10
uv run python -m fastsft.eval.results_viewer
uv run python -m fastsft.eval.inference_viewer "test prompt"
๐ ๏ธ Setup & Installation
Prerequisites
- Python 3.12+ (required for distilabel's type hints)
uvpackage manager
Standard Install (Data Generation Only)
uv sync
echo "OPENROUTER_API_KEY=sk-or-..." > .env
With Local GPU Training
uv sync --extra local-training
# Now torch, peft, trl, accelerate are installed locally
# QLoRA requires CUDA; plain LoRA works on CPU (slowly)
With Evaluation
uv sync --extra evaluation
# Now sentence-transformers is installed for embedding similarity scoring
With Modal Cloud Training
modal token new # Authenticate once
# Then omit --local from fastsft commands to dispatch to Modal
๐ Troubleshooting
| Issue | Solution |
|---|---|
No OpenRouter API key found |
Run echo "OPENROUTER_API_KEY=sk-or-..." > .env |
... has no chat_template |
Your model is a base model, not instruct. Use -Instruct or -Chat variant. |
... has no hugging_face_id |
Your parent model is closed-weight. Use an open-weight one. |
--strategy qlora requires CUDA (local) |
QLoRA needs CUDA. Use plain lora locally, or train on Modal. |
modal.AuthError |
Run modal token new to authenticate, or use --local. |
OOM (out of memory) |
Reduce --batch-size, increase --grad-accumulation, or use QLoRA. |
| Slow training (local CPU) | Use --local --batch-size 1 as last resort, or switch to Modal. |
| Generation/judge errors | Increase --parent-max-tokens, or switch to a more reliable model. |
๐ Additional Resources
- TUTORIAL.md โ 15-minute end-to-end walkthrough.
- data_generation_tutorial.md โ Deep dive into data generation.
- training_tutorial.md โ Master training, hyperparameters, and GPU selection.
- evaluation_tutorial.md โ Interpret results and debug quality issues.
๐ Dataset Formats
FastSFT works with Distiset format (Hugging Face datasets wrapped by distilabel). You can:
Generate automatically (default):
uv run fastsft "your prompt" --num-samples 100
# Creates: datasets/raw/<timestamp>/
Convert your own data:
# Your data (CSV, JSON, Parquet, etc.) โ Distiset
from datasets import Dataset, DatasetDict
from distilabel.distiset import Distiset
# Create Dataset with 'messages' column:
# [{"role": "user", "content": "Q"}, {"role": "assistant", "content": "A"}]
dataset = Dataset.from_dict({"messages": [...]})
distiset = Distiset({"default": DatasetDict({"train": dataset})})
distiset.save_to_disk("datasets/raw/my_data")
# Then use it:
uv run fastsft --start-stage data_formatter --input-path datasets/raw/my_data
See data_generation_tutorial.md for complete examples (CSV, JSON, Parquet, combining datasets).
๐งช Development
The project is a src-layout package (src/fastsft/). uv sync installs it editable, exposing console scripts:
fastsftโ Main CLI (training pipeline)fastsft-evalโ Evaluation CLI
Linting
uv run --only-group dev ruff check . # Check
uv run --only-group dev ruff check . --fix # Auto-fix
๐ License
See LICENSE file.
๐ Support
- Questions? Check the tutorials above โ most questions are answered there.
- Found a bug? Open an issue on GitHub.
- Want to contribute? PRs welcome.
Happy distilling! ๐
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastsft-0.1.0.tar.gz.
File metadata
- Download URL: fastsft-0.1.0.tar.gz
- Upload date:
- Size: 322.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50050a35d7c641c0423db23e2487fabb597ba0ba496c6f6f66b6053427d75990
|
|
| MD5 |
4ff9c24de09960622e993ef9f4ecf33f
|
|
| BLAKE2b-256 |
4ddb21939aeb8f226716b1f5ee147be9f5c00c608a617bf3f046b0ec4b7924ce
|
File details
Details for the file fastsft-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastsft-0.1.0-py3-none-any.whl
- Upload date:
- Size: 85.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2a375d64aca79b9d70c2181e295f3242f34551277ad4fc2e85137156621baf0
|
|
| MD5 |
d2f6b7e3c8164c7ad43922375ff7ac75
|
|
| BLAKE2b-256 |
6e7bf301fcca02115938b053a83d7a11621638e72859d24a1ebd844b1bb9fb1c
|