English | 한국어
🎯 FiTuna
Stop guessing your llama.cpp config. Measure it.
Hardware-benchmark-driven auto-tuning for local LLMs — give it a model, a target speed and a quality budget; get back the smallest llama.cpp config that actually hits those numbers on your machine.
API subscriptions add up. Going local means guessing which model your machine can actually run. Don't guess — measure it, and run your own.
심사위원 · 검증기관용 한국어 재현 가이드 → REVIEWERS.md (Korean reproduction guide for competition judges & verification agency)
$ fituna run --model Qwen3-4B-Instruct-2507-F16.gguf \
--target-tps 30 --max-quality-loss 5 --ctx 4096 --wikitext wiki.txt --out ./out
[Q6_K] full-offload 28.48 tok/s < target 30.00, skipping (early-exit B)
[Q8_0] full-offload 24.22 tok/s < target 30.00, skipping (early-exit B)
[Q5_K_M] full-offload 29.59 tok/s < target 30.00, skipping (early-exit B)
[Q4_K_M] found ngl=33 meeting target -- done
FiTuna result: MEETS TARGET
quant : Q4_K_M ngl : 33 ctx : 4096
gen tok/s : 30.81 quality loss : 1.73%
artifact: out/Qwen3-4B-Instruct-2507-...-Q4_K_M.gguf (2.3 GB -- already produced during the search)
1) local API server (OpenAI-compatible):
llama-server -m out/Qwen3-...-Q4_K_M.gguf -ngl 33 -c 4096 --port 8080
2) import into Ollama: re-run with --export-ollama to write a Modelfile beside the artifact
3) terminal chat (interactive check):
llama-cli -m out/Qwen3-...-Q4_K_M.gguf -ngl 33 -c 4096
(Output formatting above is reconstructed against the current version; the numbers are the Run 2 measurements.)
A real run on an Apple M3 Pro. The "obviously best" Q8_0 failed the speed
target, Q5_K_M missed by 0.41 tok/s, and the answer wasn't a quant alone —
it was a quant plus the minimal GPU offload (-ngl 33, not the full 36).
None of that is predictable from a spec sheet (full logs).
Install
Not on PyPI yet — install from git, into a virtualenv built with Python 3.11+
(macOS's system python3 is 3.9):
git clone https://github.com/leeyunseokarchive/fituna
python3.13 -m venv .venv && source .venv/bin/activate
pip install -e fituna
You also need llama.cpp, which provides the quantize/bench/perplexity engines FiTuna orchestrates:
brew install llama.cpp # macOS/Linux Homebrew — ships all needed binaries
# or build from source (any platform):
git clone https://github.com/ggml-org/llama.cpp
cmake -S llama.cpp -B llama.cpp/build && cmake --build llama.cpp/build --config Release
Quickstart
Three ways in: a person runs the wizard; a script or CI job calls
fituna run --json; an AI agent talks to
fituna-mcp.
fituna quickstart
Six steps — environment check, targets, license requirements, model, quality
corpus, search — and it prints the assembled fituna run ... command
before executing it, so the next run is a one-liner you already have. It
needs a terminal; in CI or a pipe use fituna run directly, since every
search parameter maps to a public run flag (proven by an argv-equality
test). Model download (a curated shortlist) and HuggingFace search are
wizard-only conveniences; run --model expects a .gguf on disk.
It never predicts throughput: memory fit is arithmetic (published file size vs
detected VRAM/RAM, assumed margin stated), speed is measured, and curated or
HuggingFace-search candidates show their license — local-scan and manual-path
options cannot, since a .gguf carries no license metadata. Any
docs/RESULTS.md figure it cites is a record of what was
measured on named hardware, never a prediction.
The script path (what the wizard assembles for you)
Quality loss is perplexity increase over a plain-text corpus, so it is only
meaningful on text resembling your workload. Any UTF-8 file works
(--quality-corpus); both presets are one command away:
fituna fetch-corpus --lang en --out wikitext-2-raw-test.txt # wikitext-2 test split
fituna fetch-corpus --lang ko --out kowiki-corpus.txt --rows 500 # Korean Wikipedia
Measure the language you'll run: the same quant can measure 2–3× different
loss on the two corpora, and in Run 3 that was enough to change the verdict
the tool returned (measurement and
caveats).
Both presets are CC BY-SA 3.0 and fetch-corpus prints the license notice
and source URL when it finishes; --dataset/--config/--split override the preset
(provenance and licensing).
fituna doctor # confirm the environment is ready
fituna detect-hw # see what FiTuna detects
fituna run --model your-model-F16.gguf \
--target-tps 30 --max-quality-loss 5 \
--ctx 4096 --wikitext wikitext-2-raw-test.txt --out ./out --resume
Pass an F16/BF16 .gguf directly (many models publish one), or an HF-format
directory if convert_hf_to_gguf.py is available (source checkout +
pip install torch transformers; package-manager builds don't ship it).
Disk usage: the search quantizes every candidate reaching the quality stage — ~12 GB for four candidates of a 4B model. Files are reused across runs; narrow
--quantto bound this.
Why
Running a local LLM means picking a quantization level (Q2–Q8), a GPU offload
layer count (-ngl) and a context length — a search space people navigate
today by trial and error:
- Ollama / LM Studio apply fixed per-model presets; a request for finer quantization control was closed as not planned.
- NVIDIA Model Optimizer's AutoQuantize is CUDA-only.
- VRAM calculators & chatbot advice estimate from specs — and specs don't know your thermals, memory bandwidth, or llama.cpp build flags.
FiTuna replaces the guesswork with a measured search over the llama.cpp binaries you already have — verified on your hardware, reproducible from cache.
Features
- 🔍 Target-driven search — in: model + target tok/s + max quality loss %.
Out: quant ×
-ngl× ctx config + a ready-to-run command. - 📏 Measured, not assumed — candidates walked in measured perplexity order (in our runs Q6_K beat Q8_0 — data), with a binary search for the minimal GPU offload.
- ⚡ Aggressive early exits — quality-gate failures and hopeless quants are skipped without wasting benches; a bench that can't finish in time counts as "too slow", not a crash.
- 🗃️ Reproducible cache — sqlite3, keyed by model fingerprint × hardware ×
llama.cpp build version;
--resumere-answers in <1s and survives interruptions. - 🖥️ Hardware auto-detection — NVIDIA (
nvidia-smi), AMD (rocm-smi), Apple Silicon unified memory (system_profiler), with manual override. - 🪶 Zero runtime dependencies — pure Python 3.11+ stdlib.
Measured results
| Model | Target | What the "obvious" pick did | What FiTuna found |
|---|---|---|---|
| Qwen3-4B-Instruct (Apache 2.0) | 30 tok/s, ≤5% loss | Q8_0: 24.22 tok/s ❌ (and measured worse quality than Q6_K) | Q4_K_M @ ngl=33 → 30.81 tok/s, 1.73% loss ✅ |
| SmolLM2-135M (Apache 2.0) | 240 tok/s, ≤5% loss | Q8_0: 205.91 tok/s ❌ | Q6_K → 249.50 tok/s, 0.53% loss ✅ (and Q4_K_M measured slower than Q6_K) |
| Midm-2.0-Mini-Instruct, Korean (MIT) | 40 tok/s, ≤5% loss | Q8_0: 34.26 tok/s ❌ | Q4_K_M @ ngl=48 → 44.62 tok/s, 2.58% loss ✅ (the two corpora report different mid-table orders, but the per-chunk trace shows that reorder is not something we could establish) |
Apple M3 Pro, llama.cpp build 9960. Full logs, timings and run-to-run variance
analysis (including a thermal-throttle outlier we caught and documented):
docs/RESULTS.md · Scenarios:
docs/USE_CASES.md · Reproduce on NVIDIA/Linux with the
one-click Colab notebook (free T4 tier):
notebooks/colab_nvidia_verification.ipynb
How it works
Stage 1 measures perplexity loss for every candidate, because Stage 2
walks them in measured quality order and you can't sort by a number you
haven't measured. Stage 2 early-exits hard: a quant missing the target at full
offload is dropped without further benches, and the first quant that passes
wins. Results cache to sqlite3 keyed by model fingerprint, hardware profile
and llama.cpp build version, so --resume never serves numbers from a
different backend build. Diagrams, module map and full algorithm:
docs/ARCHITECTURE.md · Contract:
fituna/config.py
Use as a library
Zero runtime dependencies means the modules import directly:
from fituna.hardware import detect_hardware
hw = detect_hardware()
print(f"{hw.gpu_vendor.value}: {hw.gpu_name}, {hw.vram_mb} MB VRAM, {hw.ram_mb} MB RAM")
# apple: Apple M3 Pro, 18432 MB VRAM, 18432 MB RAM
(Real output from a python3.13 -c run on the same M3 Pro as above.) Driving
the search programmatically means calling fituna.search.search(), which also
needs a ModelInfo, resolved BinaryPaths, a work directory and a corpus
path — exactly what fituna run/quickstart assemble for you
(search.py, config.py).
MCP server — measured answers for AI agents
Ask a chatbot "which local model config fits my machine?" and it guesses from specs. Point it at FiTuna's MCP server and it measures:
claude mcp add fituna -- fituna-mcp # or any MCP client, stdio transport
| Tool | What it does |
|---|---|
fituna_detect_hardware |
GPU vendor/name, VRAM, CPU cores, RAM as FiTuna sees them |
fituna_recommend |
Runs the measured search for a target spec; returns the winning config, measured tok/s, measured quality loss, and a ready-to-run command. Slow once, ~1 s on repeat (cache). |
Stdlib-only like the rest of FiTuna — MCP stdio is newline-delimited JSON-RPC
2.0, no SDK required (fituna/mcp_server.py).
Scope
FiTuna recommends; it doesn't execute or serve. The output is the quantized
.gguf plus llama-server / llama-cli commands you copy and run (and, with
--export-ollama, an Ollama Modelfile beside it) — FiTuna launches none of
them. That's a deliberate boundary: serving inference is llama.cpp's job, and
duplicating it would add no differentiated value
(rationale). The two extensions that
stay inside it — --launch and an LM Studio preset export — are tracked in
#19.
Known limitations
- Single GPU only — first GPU reported by
nvidia-smi/rocm-smi; no--tensor-split(#11, help wanted: we have no multi-GPU machine to measure on). - Windows AMD auto-detection —
rocm-smihas no mainstream Windows distribution; use--gpu amd --vram-mb <N>. - Quality = perplexity on a corpus you choose — a proxy, not a guarantee
of domain quality. Gate on text resembling your workload
(
--quality-corpus; measured EN-vs-KO comparison). - The quality verdict depends on
--ppl-chunks— loss is an estimate overchunks × 512tokens whose absolute value grows with the chunk count, so re-measure a candidate close to your budget before trusting the PASS (measured effect).quality.pystill parses the PPL and discards llama-perplexity's error bar (#8) — which is how Run 5 came to publish a claim it later had to withdraw. - Benchmarks are thermally sensitive — verdicts within a few tok/s of the target are marginal (variance analysis).
- Real-hardware E2E covers macOS and Linux only — Apple Silicon/Metal and NVIDIA T4/CUDA. Windows paths are unit-tested and CI-run, but not yet integration-run against real binaries (#12).
Contributing
Contributions welcome — the codebase is small, dependency-free and
contract-first (start at fituna/config.py); 246 unit
tests, per-module self-checks and a 3-OS × 2-Python CI matrix guard it.
The roadmap lives in the
v0.2.0 milestone,
including #10 (parser
test coverage, good first issue). See CONTRIBUTING.md ·
docs/DEVELOPMENT.md · CHANGELOG.md ·
SECURITY.md.
License
MIT © FiTuna contributors. Third-party notices (llama.cpp and subprocess-invoked tools): THIRD_PARTY_NOTICES.md · SBOM: docs/SBOM.md · Open-source usage: docs/OPEN_SOURCE_USAGE.md · AI-assisted development disclosure: docs/AI_MODEL_USAGE.md
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 fituna-0.1.0.tar.gz.
File metadata
- Download URL: fituna-0.1.0.tar.gz
- Upload date:
- Size: 133.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bb0e3921520603b3eb6ab6e6078295b365cc5a76150f152e414b362fd321317
|
|
| MD5 |
84596f7694603a6e9b5882b4e0ee50cb
|
|
| BLAKE2b-256 |
dca3e081c140581d80ec5d5cecf9863164cc714f3f3bdb5f3c0fe2645e053e95
|
File details
Details for the file fituna-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fituna-0.1.0-py3-none-any.whl
- Upload date:
- Size: 101.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c361276e2c90bf6fcb2eec15658deb8271cbe527bde23847810485ce61998653
|
|
| MD5 |
adfd0b8d90c298ba270eb5f64b8fa8a2
|
|
| BLAKE2b-256 |
75bcc0f98bd01ee7b3b37600928f93392e956cafc19e16b0b632f5b0a831a783
|