Skip to main content

한국어 | English

FiTuna logo — baby tuna character

FiTuna

Stop guessing your llama.cpp config. Measure it.

Give it a model file, a target speed (tok/s) and a quality budget (%), and it finds the llama.cpp configuration — quantization level, GPU offload and context length — that actually hits those numbers on your machine: the highest-quality quant that reaches your speed, with the fewest GPU layers it needs, proven by real benchmarks.

pip install fituna

CI PyPI License: MIT Python 3.11+ Zero dependencies PRs Welcome

▶ Demo video · Korean reproduction guide for judges

A real fituna run: 58 seconds on an Apple M3 Pro from download to a ready-to-run llama-server command

Why

Running a local LLM through llama.cpp means making three choices: how hard to compress the model (quantization level, Q2–Q8 — lower is faster but loses quality), how many layers to put on the GPU (-ngl), and how long a context to allow. That's dozens of combinations, and today most people search them by trial and error.

What you actually want answered are three questions. ① Will this machine hit my target speed? ② What is the quality loss, in percent? ③ Which config keeps the most quality while still meeting the target? — and no tool answered them:

① Target speed ② Quality loss ③ Best passing config
VRAM calculators Only "does it fit" Not addressed Not addressed
Chatbot advice Can propose a config; no repeated-measurement guarantee General trends No passing guarantee
NVIDIA AutoQuantize No speed-target input Addressed — but CUDA-only CUDA-only
FiTuna Measured verdict Measured verdict Binary-searched

Runners like Ollama and LM Studio are absent from this table on purpose — they execute a given config rather than answer these questions, a different and complementary job. The config FiTuna finds can be handed straight to Ollama via --export-ollama.

And once you actually measure, intuition keeps getting overturned: on an Apple M3 Pro the "obviously best" Q8_0 failed the speed target, and the answer wasn't a quant alone but Q4_K_M plus the minimal offload -ngl 33. No amount of spec-sheet reading produces that answer.

Try it in 2 minutes

The fastest way to see it work for yourself: a full pipeline run (download → quantize → quality-gate → benchmark) on a small 258 MB model finishes in about a minute on an M-series Mac. See the full flow first in the demo video:

brew install llama.cpp python@3.13
python3.13 -m venv .venv
source .venv/bin/activate
pip install fituna
fituna fetch-corpus --lang en --out wiki.txt
fituna run --hf bartowski/SmolLM2-135M-Instruct-GGUF \
  --target-tps 240 \
  --max-quality-loss 5 \
  --ctx 2048 \
  --quality-corpus wiki.txt \
  --out ./out --resume

What each fituna run flag means:

Flag Meaning
--hf bartowski/SmolLM2-… The model to use — a HuggingFace repo; the F16 GGUF is downloaded automatically required¹
--target-tps 240 Target generation speed (tok/s) required
--max-quality-loss 5 Quality-loss budget (%) — candidates worse than this are rejected required
--ctx 2048 Context length (how much conversation history to keep) optional (default 4096)
--quality-corpus wiki.txt Text for quality measurement — the file fetched just above required
--out ./out Output/cache directory optional (default ./out)
--resume Store and reuse measurements in the cache — recommended from the first run optional

¹ Already have a model file on disk? Use --model <path.gguf> instead of --hf.

Example result

When the command finishes, you get output like this (a real run on an Apple M3 Pro):

FiTuna result: MEETS TARGET

  quant           : Q8_0        # <- the optimal quantization level it found
  ngl             : 26          # <- the minimum GPU offload layers meeting the target
  ctx             : 2048        # <- the verified context length

  prompt tok/s (pp): 2017.64
  gen tok/s    (tg): 261.78     # <- measured generation speed -- clears the 240 target

  perplexity      : 18.2931 (baseline 18.2407)
  quality loss    : 0.29%       # <- measured quality loss -- within the 5% budget

  artifact: out/SmolLM2-135M-Instruct-8078a5b74b5a-Q8_0.gguf  (144.8 MB -- already produced during the search)

  1) local API server (OpenAI-compatible):
       /opt/homebrew/bin/llama-server -m out/SmolLM2-135M-Instruct-8078a5b74b5a-Q8_0.gguf -ngl 26 -c 2048 --port 8080
  2) import into Ollama: re-run with --export-ollama to write a Modelfile beside the artifact
  3) terminal chat (interactive check):
       /opt/homebrew/bin/llama-cli -m out/SmolLM2-135M-Instruct-8078a5b74b5a-Q8_0.gguf -ngl 26 -c 2048

How to read it: the first line is the verdict — MEETS TARGET means a configuration satisfying your target was found, followed by that configuration (quant × ngl × ctx) and the measured evidence (speed and quality loss). The quantized model on the artifact: line was already produced during the search. Re-running the same command with --resume reuses its stored measurements. Absolute numbers and the winning quant can vary by machine and by run — measured run-to-run variance.

Measured results

Three models of different sizes, each given a target. In all three runs the "safe default" Q8_0 failed the speed target:

Model Target What the "obvious" pick measured What FiTuna found
Qwen3-4B-Instruct 30 tok/s, ≤5% loss Q8_0: 24.22 tok/s ❌ (and worse quality than Q6_K) Q4_K_M @ ngl=33 → 30.81 tok/s, 1.73% ✅
SmolLM2-135M 240 tok/s, ≤5% loss Q8_0: 205.91 tok/s ❌ Q6_K → 249.50 tok/s, 0.53% ✅ (smaller Q4_K_M measured slower)
Midm-2.0-Mini (Korean) 40 tok/s, ≤5% loss Q8_0: 34.26 tok/s ❌ Q4_K_M @ ngl=48 → 44.62 tok/s, 2.58% ✅

Apple M3 Pro, llama.cpp build 9960. Full logs and run-to-run variance: docs/RESULTS.md · Scenarios: docs/USE_CASES.md · Reproduce on NVIDIA/Linux (free T4): Open In Colab

Couldn't you just ask a chatbot?

We tested that question under the same conditions used in the result report. FiTuna and three chatbots were given Qwen3-4B, a 30 tok/s speed target, and a 5% quality-loss budget. Each proposed configuration was then run three times on the same Apple M3 Pro.

Source Proposed config Measured generation speed¹ Quality loss Runs ≥30 tok/s
FiTuna Q4_K_M, ngl=33 32.68 ± 1.71 tok/s 1.75% 3/3
Claude Opus 5 Q5_K_M, ngl=36 30.49 ± 0.88 tok/s 1.53% 2/3
ChatGPT 5.6 Sol Q4_K_M, ngl=28 28.92 ± 1.37 tok/s 1.75% 1/3
Gemini 3.1 Pro No config (query unsupported) — — Not measurable

¹ Mean ± between-run standard deviation over three independent runs, with five internal measurements per run. Quality is the perplexity increase from F16 on 32 WikiText-2 chunks.

Repeated same-hardware measurements of chatbot-proposed and FiTuna configurations for the same Qwen3-4B target. Only FiTuna passed 30 tok/s in all three runs.

All three configurations that could be measured stayed within the quality budget. Only FiTuna met the speed target on every run, while using three fewer GPU-offloaded layers than Claude's proposal. ChatGPT proposed the smallest offload but passed only once. This is not a general chatbot ranking; it shows that a minimal passing configuration for one machine needs repeated measurement. Full conditions, per-run values, and limitations: docs/CHATBOT_COMPARISON.md

How it works

flowchart LR
    A["Input<br/>F16 GGUF<br/>target tok/s · quality budget"] --> B["Stage 1 · measure quality<br/>quantize every candidate,<br/>measure perplexity"]
    B --> C{"quality<br/>gate"}
    C -->|"fail (early-exit A)"| X["next candidate"]
    C -->|"pass · sort by measured quality"| D["Stage 2 · measure speed<br/>llama-bench"]
    D --> E{"target met at<br/>full offload?"}
    E -->|"miss (early-exit B)"| X
    E -->|met| F["binary-search ngl<br/>minimal offload"]
    F --> G["Artifacts<br/>quantized .gguf +<br/>llama-server command"]
    B -.store measurements.-> H[("sqlite3 cache<br/>--resume < 1 s")]
    D -.-> H

FiTuna works in two stages. Stage 1 quantizes every candidate and measures its quality loss first — you can't rank candidates by a number you haven't measured. Stage 2 then benchmarks them in that measured quality order, dropping any quant that misses the target without wasting further benches. Every measurement lands in an sqlite3 cache whose key includes the llama.cpp build version, so upgrading the engine never silently reuses stale numbers.

See how it works in detail: docs/ARCHITECTURE.md

Install

python3.13 -m venv .venv
source .venv/bin/activate
pip install fituna

Python 3.11+ is required. Any installed 3.11+ interpreter works — swap the first line accordingly (e.g. python3.12). The macOS system python3 (3.9.6) is too old; if you have no 3.11+ at all, brew install python@3.13. Zero runtime dependencies. You also need llama.cpp:

brew install llama.cpp        # macOS/Linux Homebrew
Source build · development install
# llama.cpp from source (any platform; add -DGGML_CUDA=ON for NVIDIA)
git clone https://github.com/ggml-org/llama.cpp
cmake -S llama.cpp -B llama.cpp/build && cmake --build llama.cpp/build --config Release
# then add --llama-bin-dir llama.cpp/build/bin to fituna commands

# FiTuna for development
git clone https://github.com/leeyunseokarchive/fituna
python3.13 -m venv .venv && source .venv/bin/activate
pip install -e fituna

Commands

New here? fituna quickstart is the easiest entry — a wizard walks you from environment check to a finished search without knowing any of the commands below. Full options for each: fituna <command> -h.

Command Role
fituna quickstart Six-step interactive wizard — environment check through search; shows the assembled fituna run command before executing it
fituna run The search itself. --model <F16.gguf> or --hf repo[:file] (auto-download from HF), --json supported
fituna doctor 11 environment checks (binaries actually run, build can use your GPU), each failure with its fix
fituna fetch-corpus Download a quality corpus (--lang en/ko, stdlib-only)
fituna detect-hw Show detected GPU · VRAM · CPU · RAM
fituna-mcp MCP server for AI agents (below)
Choosing a quality corpus — the same quant can measure 2–3× different loss by language

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):

fituna fetch-corpus --lang en --out wikitext-2-raw-test.txt        # wikitext-2
fituna fetch-corpus --lang ko --out kowiki-corpus.txt --rows 500   # Korean Wikipedia

In Run 3, switching the corpus alone changed the verdict the tool returned (measurement and caveats). Both presets are CC BY-SA 3.0; fetch-corpus prints the license notice on completion.

Disk usage · cache

The search quantizes every candidate that reaches the quality stage — ~12 GB for four candidates of a 4B model. Files are reused across runs; --quant narrows the candidate set to bound the space. Results are cached in sqlite3 keyed by model fingerprint × hardware × llama.cpp build version; --resume re-answers in under a second. --quality-metric kld also writes a reference-logits file from the F16 model to --out; it grows with vocabulary size and --ppl-chunks (about 100 MB for 4 chunks of a 49k-vocabulary model).

Use as a library

Zero 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")
# apple: Apple M3 Pro, 18432 MB VRAM

The search itself is fituna.search.search() — the ModelInfo, BinaryPaths and corpus path it needs are exactly what fituna run assembles for you (search.py, config.py).

MCP server — measured answers for AI agents

Ask a chatbot "which local model config fits my machine?" and you get a guess derived from spec sheets. Connect FiTuna's MCP server and the agent gets measured results instead:

claude mcp add fituna -- fituna-mcp      # any MCP client with stdio transport
Tool Returns
fituna_detect_hardware GPU vendor/name, VRAM, CPU cores, RAM
fituna_recommend Measured search result — winning config, measured tok/s and quality loss, run command. ~1 s on repeat (cache)

Stdlib-only JSON-RPC 2.0 over stdio, no SDK (mcp_server.py).

Scope and limitations

FiTuna recommends, and stops there. The output is the quantized .gguf already produced during the search, plus llama-server/llama-cli commands you copy and run (and an Ollama Modelfile with --export-ollama) — actually serving the model stays llama.cpp's job (rationale). Current limitations:

  • Results are valid only on the machine that ran them — FiTuna never extrapolates another machine's numbers from a spec sheet. Need a config for a different machine? Run FiTuna there (it's a cross-platform CLI). Machines disagreeing is exactly why measurement beats estimation — same model, opposite verdicts on M3 Pro vs T4
  • Single GPU only — no --tensor-split
  • No Windows AMD auto-detection — pass --gpu amd --vram-mb <N>
  • Quality = perplexity on the corpus you choose — a proxy; measure on text resembling your workload. --quality-metric kld adds KL divergence to the report, but the pass/fail gate is still perplexity (#49)
  • Verdicts depend on --ppl-chunks — re-measure candidates close to your budget (measured effect)
  • Benchmarks are thermally sensitive — verdicts within a few tok/s of target are marginal (variance analysis)
  • Real-hardware E2E covers macOS · Linux — Windows is unit-tested and CI-run

Roadmap

Ordered by how directly each item narrows the limitations above. Each item is tracked on GitHub Issues.

  • KLD as the quality gate — --quality-metric kld now reports KL divergence; next, let it decide pass/fail (#49)
  • llama.cpp --fit integration — use upstream's memory auto-fit as a sub-routine to seed the ngl search
  • Distributed measurement — quantize and score quality on a remote server, then measure only speed on resource-constrained target devices
  • Edge devices (Raspberry Pi, Jetson) — run only llama-bench on the device, one candidate at a time, so a board too small for the full search can still be measured (#56)
  • --launch and LM Studio preset export — additional output formats, keeping the same artifact boundary as --export-ollama
  • Multi-GPU (--tensor-split) search — lift the single-GPU limitation

Contributing

The codebase is small, dependency-free and contract-first — start at fituna/config.py. The full unit suite and a 3-OS × 2-Python CI matrix guard it. See CONTRIBUTING.md and docs/DEVELOPMENT.md.

License

MIT © FiTuna contributors · THIRD_PARTY_NOTICES.md · SBOM · Open-source usage · AI-assisted development disclosure · CHANGELOG.md · SECURITY.md

Metadata

Release files for fituna 0.3.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fituna 0.3.4
File Size Uploaded Provenance
fituna-0.3.4.tar.gz 154.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for fituna 0.3.4
File Interpreter ABI Platform
fituna-0.3.4-py3-none-any.whl Python 3 none any Details

Total release size: 264.7 kB

Release files / fituna-0.3.4.tar.gz

Download URL fituna-0.3.4.tar.gz
Size 154.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a0cb839309cd2cf086dbc39d6803c38f1365497dd9c5e92674512f429bb10c6f
BLAKE2b-256 checksum
How to use checksums
d01f8f3ec32cb666606cc7a9194a79b69539a14d54ce4992d54cc13b22c5e83a
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 6, 2026.

Transparency log

Release files / fituna-0.3.4-py3-none-any.whl

Download URL fituna-0.3.4-py3-none-any.whl
Size 110.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e5667ee6b544252ab4182f86b17dc342e26894f63ef362cc1e045948decb7714
BLAKE2b-256 checksum
How to use checksums
beb7456d820892fb7e830e2f6a0e2cc24b314ea7b97b0b1dfa9de49589c0b6a5
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 6, 2026.

Transparency log

Security for fituna 0.3.4

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for fituna-0.3.4.tar.gz

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 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page