tunekit
One-command LoRA/QLoRA fine-tuning for open LLMs.
pip install tunekit
tunekit train --model Qwen/Qwen2.5-0.5B-Instruct --data my_data.jsonl
tunekit chat outputs/run
That's a fine-tune. tunekit wraps the standard Hugging Face stack (transformers + PEFT + TRL) behind one CLI and one YAML file, so you spend your time on data instead of on trainer boilerplate.
Why
Fine-tuning a 7B model with QLoRA is ~150 lines of code that everybody rewrites, and most of the bugs live in the parts nobody enjoys: dataset formats, chat templates, pad tokens, dtype/quantisation flags, checkpoint layout. tunekit does those parts once, well:
- Any common dataset format, auto-detected —
messages, alpaca, ShareGPT, prompt/completion, plain text; from.jsonl/.json/.csv/.parquet, a directory, or a Hub dataset id. Rows that don't validate are dropped with a reason, not a stack trace. tunekit validatebefore you burn GPU hours — shows the exact rendered text the model will see, token-length percentiles, and how many rows will be truncated.- LoRA or QLoRA (4-bit / 8-bit) from a single flag; sensible defaults for rank, alpha, scheduler, checkpointing.
- Reproducible runs — every output dir gets the resolved config (
tunekit.yaml), metrics (tunekit.json) and a model card. tunekit evalanswers "did it help?" — held-out loss and perplexity on the assistant turns, tuned vs base, with sample generations side by side.- Ships to where models are used —
tunekit merge→tunekit export --ollamagives you a GGUF and an OllamaModelfile. - Works everywhere the stack works — CUDA (single or multi-GPU via
accelerate launch), Apple Silicon (MPS, no quantisation), or CPU for smoke tests.
Install
pip install tunekit # LoRA
pip install "tunekit[quant]" # + bitsandbytes for QLoRA (Linux + NVIDIA)
pip install "tunekit[wandb]" # + Weights & Biases logging
From source:
git clone https://github.com/Dolmaa24/tunekit && cd tunekit
pip install -e ".[dev]"
Quickstart
# 1. see what your hardware supports
tunekit info
# 2. check your data (format detection, token stats, one rendered example)
tunekit validate --model Qwen/Qwen2.5-0.5B-Instruct --data examples/pirate.jsonl
# 3. train (LoRA, bf16, defaults)
tunekit train --model Qwen/Qwen2.5-0.5B-Instruct --data examples/pirate.jsonl --output outputs/pirate
# 4. talk to it
tunekit chat outputs/pirate
# 5. did it help? held-out loss + samples, tuned vs base (writes outputs/pirate/eval.json)
tunekit eval outputs/pirate
# 6. ship it
tunekit merge outputs/pirate outputs/pirate-merged
tunekit export outputs/pirate-merged --quant q8_0 --ollama # needs a llama.cpp checkout, see below
Prefer a config file for anything you'll run twice:
tunekit init config.yaml # writes a commented starter
tunekit train config.yaml --set train.lr=1e-4 --set model.load_in_4bit=true
Ready-made configs live in configs/:
| config | model | method | VRAM |
|---|---|---|---|
quickstart-smoke.yaml |
SmolLM2-135M | LoRA | any (CPU ok, ~1 min) |
qwen2.5-0.5b-lora.yaml |
Qwen2.5-0.5B | LoRA bf16 | ~4 GB |
llama-3.2-3b-qlora.yaml |
Llama-3.2-3B | QLoRA 4-bit | ~6 GB (free Colab T4) |
qwen2.5-7b-qlora.yaml |
Qwen2.5-7B | QLoRA 4-bit | ~12 GB |
hub-dataset-example.yaml |
Qwen2.5-0.5B | LoRA, data from the Hub | ~4 GB |
Data formats
tunekit reads the first row and picks the converter. All conversational formats become OpenAI-style messages; the tokenizer's own chat template is then applied, so the model is trained on exactly the prompt format it will see at inference.
| format | row shape | notes |
|---|---|---|
messages |
{"messages": [{"role": "user", "content": "…"}, {"role": "assistant", "content": "…"}]} |
canonical; multi-turn ok; list-of-parts content is flattened |
alpaca |
{"instruction": "…", "input": "…", "output": "…"} |
input optional; also accepts question/answer, prompt/response, optional system |
sharegpt |
{"conversations": [{"from": "human", "value": "…"}, {"from": "gpt", "value": "…"}]} |
|
prompt_completion |
{"prompt": "…", "completion": "…"} |
loss on completion only; no chat template applied |
text |
{"text": "…"} |
continued pre-training / raw text |
Options in the data: section: system_prompt (added to every conversation that lacks one), eval_fraction / eval_path, max_samples, max_length, and text_field / prompt_field / completion_field for non-standard column names.
Configuration reference
Everything below is optional except model.name and data.path. Any key can be overridden with --set section.key=value.
model:
name: Qwen/Qwen2.5-0.5B-Instruct
load_in_4bit: false # QLoRA (needs tunekit[quant])
load_in_8bit: false
dtype: auto # auto | bfloat16 | float16 | float32
attn_implementation: null # flash_attention_2 | sdpa | eager
trust_remote_code: false
chat_template: null # override the tokenizer's Jinja chat template (rarely needed)
data:
path: data/train.jsonl # file, directory, or Hub id
format: auto # auto | messages | alpaca | sharegpt | prompt_completion | text
split: train # Hub datasets only
subset: null # Hub dataset config/subset name
eval_path: null
eval_fraction: 0.02
max_samples: null
system_prompt: null
max_length: 2048
shuffle_seed: 42
text_field: text # column names, when auto-detection can't find them
prompt_field: prompt
completion_field: completion
lora:
enabled: true # false = full fine-tune
r: 16
alpha: 32
dropout: 0.05
target_modules: all-linear # or [q_proj, k_proj, v_proj, o_proj]
use_rslora: false
use_dora: false
modules_to_save: null # e.g. [embed_tokens, lm_head]
train:
output_dir: outputs/run
epochs: 1
max_steps: -1 # > 0 overrides epochs
batch_size: 2
grad_accum: 8
lr: 2.0e-4
scheduler: cosine
warmup_ratio: 0.03
weight_decay: 0.0
max_grad_norm: 1.0
optimizer: adamw_torch # paged_adamw_8bit is a good QLoRA choice
gradient_checkpointing: true
packing: false
assistant_only_loss: false # needs a chat template with {% generation %} markers
logging_steps: 10
eval_steps: null # null = every epoch
save_steps: null
save_total_limit: 2
seed: 42
report_to: [none] # [wandb] | [tensorboard]
run_name: null # defaults to the output_dir's name
dataloader_num_workers: 0
resume_from_checkpoint: false
extra: {} # passed straight to TRL's SFTConfig
hub:
push: false
repo_id: null
private: true
Commands
| command | what it does |
|---|---|
tunekit info [run_dir] [--model X] |
detected hardware; whether model X's weights fit the GPU; or a finished run's metrics |
tunekit init [path] |
write a commented starter config |
tunekit validate |
detect/convert the dataset, token-length stats, rendered examples |
tunekit train |
fine-tune; --dry-run loads data + model and exits |
tunekit chat <path> |
interactive chat with an adapter dir, merged dir, or Hub id |
tunekit eval <path> |
held-out loss / perplexity + sample generations, tuned vs base; data taken from the run's tunekit.yaml unless --data is given |
tunekit merge <adapter> <out> |
fold the LoRA weights into the base model |
tunekit export <merged> [--ollama] |
GGUF via llama.cpp, plus an Ollama Modelfile |
tunekit push <dir> <repo_id> |
upload a run directory to the Hugging Face Hub |
tunekit --help and tunekit <command> --help list every flag.
Hardware notes
| setup | what works |
|---|---|
| NVIDIA, ≥ 16 GB | everything; 7B QLoRA comfortably, 13B QLoRA with batch_size: 1 |
| NVIDIA, 15 GB (free Colab T4) | measured: Qwen2.5-1.5B QLoRA at batch_size: 4, max_length: 1024 peaks at 3.04 GB and trains 12 steps in 139 s. QLoRA up to ~7B fits. |
| multiple GPUs | accelerate launch -m tunekit.cli train config.yaml (data-parallel) |
| Apple Silicon | LoRA on models that fit in unified memory; no 4-bit/8-bit (bitsandbytes is CUDA-only). For serious Mac training use mlxtuner |
| CPU | smoke tests only (configs/quickstart-smoke.yaml) |
Every run records its own peak_vram_gb in tunekit.json, so you can check these claims on your
own GPU (and please open an issue with the number).
Rules of thumb: QLoRA memory ≈ 0.7 GB per billion params + activations; halve max_length or batch_size before touching LoRA rank; lr: 2e-4 for LoRA, 1e-5–2e-5 for full fine-tunes.
GGUF / Ollama export
tunekit export shells out to llama.cpp's converter, which is not on PyPI:
git clone --depth 1 https://github.com/ggml-org/llama.cpp ~/llama.cpp
pip install "tunekit[gguf]" # sentencepiece + protobuf, all the converter needs beyond tunekit's stack
tunekit export outputs/merged --quant q8_0 --ollama # finds ~/llama.cpp automatically
ollama create my-model -f outputs/merged/Modelfile && ollama run my-model
Don't install llama.cpp's own requirements-convert_hf_to_gguf.txt into the same environment: it pins older transformers/torch and would downgrade tunekit's. If you prefer their pinned versions, put them in a venv inside the checkout (~/llama.cpp/.venv) and tunekit will use that interpreter automatically.
For 4-bit GGUFs, run llama-quantize from llama.cpp on the q8_0 file. This path was verified end-to-end (train → merge → export → ollama run) with SmolLM2-135M.
Python API
Every command is a thin wrapper over importable functions:
from tunekit.config import RunConfig
from tunekit.train import run
from tunekit.inference import load_for_inference, stream_reply
cfg = RunConfig.from_dict({"model": {"name": "Qwen/Qwen2.5-0.5B-Instruct"}, "data": {"path": "data.jsonl"}})
out_dir = run(cfg)
model, tok = load_for_inference(str(out_dir))
print("".join(stream_reply(model, tok, [{"role": "user", "content": "hello"}])))
FAQ
Loss goes down but the model doesn't change. Too few steps or too small a rank for the shift you want; check tunekit validate shows the format you expect, then try epochs: 3, lora.r: 32.
CUDA out of memory. In order: load_in_4bit: true → lower data.max_length → batch_size: 1 with higher grad_accum → optimizer: paged_adamw_8bit.
ImportError: Found an incompatible version of torchao when loading an adapter. peft rejects an
outdated torchao that some environments (Colab especially) preinstall. tunekit doesn't use torchao:
pip uninstall -y torchao, or pip install -U torchao. Note this only bites at load time — 4-bit
training returns before peft reaches that check, so a run can train fine and then fail at chat.
A QLoRA adapter won't fit at inference. tunekit chat and tunekit eval load the base model in
full precision by default. Pass --load-in-4bit to reload it quantised, the way it was trained —
otherwise a 7B QLoRA adapter needs ~15 GB instead of ~5 GB.
The model rambles / never stops. The base model's chat template and EOS handling matter; use the -Instruct variant of the model, and check your assistant turns don't end with trailing junk.
How do I know it actually helped? tunekit eval outputs/run scores the held-out split with the tuned and base model and prints both, plus sample generations. Lower loss on assistant turns and better-looking samples is the signal; lower loss with worse samples usually means overfitting.
Where's DPO / RLHF? Not yet — see CONTRIBUTING.md for the roadmap.
Contributing
Issues and PRs are welcome — new dataset formats, model-family quirks, export targets and hardware reports are all useful. See CONTRIBUTING.md.
License
Apache-2.0. Models and datasets you fine-tune keep their own licenses.
Metadata
Release files for tunekit 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tunekit-0.1.1.tar.gz | 47.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tunekit-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.1 kB
Release files / tunekit-0.1.1.tar.gz
| Download URL | tunekit-0.1.1.tar.gz |
|---|---|
| Size | 47.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ac7df1412e92cd4b1b458af39dbf9d326945c60051393464cfe6c693429f43c4
|
|
BLAKE2b-256 checksum How to use checksums |
ddd5d3edf315b11cacd7d01721a76f96cea89ea050d4230412f30b319ba9f672
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / tunekit-0.1.1-py3-none-any.whl
| Download URL | tunekit-0.1.1-py3-none-any.whl |
|---|---|
| Size | 37.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
68a6a2c11d50493767fe8732db43abafd480f2207ad8c7ba46c4eed2ac0c81c4
|
|
BLAKE2b-256 checksum How to use checksums |
869079c7fec419bc1f5e1dbad2926ee9754afc6888c2699e18183af5d648aa77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log