Skip to main content

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-training once (adds torch, peft, trl, accelerate).
  • For evaluation โ€” Run uv sync --extra evaluation once (adds sentence-transformers).
  • For Modal cloud training โ€” Run modal token new once 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 for datasets/, modelsets/, and evalsets/ (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:

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)
  • uv package 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


๐Ÿ“Š 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

fastsft-0.1.0.tar.gz (322.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastsft-0.1.0-py3-none-any.whl (85.0 kB view details)

Uploaded Python 3

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

Hashes for fastsft-0.1.0.tar.gz
Algorithm Hash digest
SHA256 50050a35d7c641c0423db23e2487fabb597ba0ba496c6f6f66b6053427d75990
MD5 4ff9c24de09960622e993ef9f4ecf33f
BLAKE2b-256 4ddb21939aeb8f226716b1f5ee147be9f5c00c608a617bf3f046b0ec4b7924ce

See more details on using hashes here.

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

Hashes for fastsft-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a2a375d64aca79b9d70c2181e295f3242f34551277ad4fc2e85137156621baf0
MD5 d2f6b7e3c8164c7ad43922375ff7ac75
BLAKE2b-256 6e7bf301fcca02115938b053a83d7a11621638e72859d24a1ebd844b1bb9fb1c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page