Ferrox: Een Rust Inference Engine bouwen die kan wedijveren met llama.cpp

Ik heb de afgelopen dagen gewerkt aan Ferrox, een pure-Rust inference engine voor het lokaal draaien van open LLM's — zowel dense modellen als Mixture-of-Experts, op CPU, Apple Metal of CUDA. Er zijn geen bindings naar llama.cpp of ggml gebruikt en er is geen bestaande runtime omwikkeld. Elke kernel, elke loader en elk besluit over scheduling is vanaf nul geschreven.

De voor de hand liggende vraag is: "waarom, terwijl llama.cpp al bestaat en uitstekend is?" Het eerlijke antwoord: ik wilde inference begrijpen op een niveau dat dieper gaat dan alleen "het binary uitvoeren", en ik wilde een project waarbij elke prestatieclaim verdiend moest worden ten opzichte van een echte, bekende baseline in plaats van simpelweg beweerd.

Wat Ferrox precies is

In de kern laadt Ferrox een GGUF-bestand — hetzelfde gekwantiseerde modelformaat dat llama.cpp gebruikt — en voert daar inference op uit. Er zijn twee manieren om het te gebruiken:

  1. Een CLI, ferrox, met vlaggen die compatibel zijn met llama.cpp. Wijs deze aan een model en krijg een voltooiing (completion).
  2. Een server, ferrox-server, die spreekt via de OpenAI chat-completions API. Alles wat is gebouwd voor de API van ChatGPT — een chat-UI, een agent-framework, een testomgeving — werkt ongewijzigd met Ferrox, mits deze naar localhost wordt gewezen.

Onder de motorkap worden modelgewichten direct vanaf de schijf in het geheugen gemapt (memory-mapped) en nooit volledig gedecomprimeerd in het RAM. De dekwantisatie gebeurt gefuseerd in het dot product, op het moment dat het gewicht daadwerkelijk nodig is. Dit is dezelfde truc die llama.cpp gebruikt, en het is een groot deel van de reden waarom beide engines een model met 8 miljard parameters kunnen draaien op een laptop met een paar gigabytes geheugen in plaats van dertig.

Probeer het: bouwen, downloaden en uitvoeren

Ferrox wordt niet geleverd met gewichten. Je downloadt een lokaal .gguf-bestand op dezelfde manier als je dat voor llama.cpp zou doen — ik raad de Hugging Face CLI aan (pip install -U huggingface_hub):

git clone https://github.com/antonellof/ferrox.git
cd ferrox
cargo build --release -p ferrox-cli -p ferrox-server --features metal
mkdir -p models

# Smoke test van ~1.2 GB
hf download TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF \
tinyllama-1.1b-chat-v1.0.Q8_0.gguf --local-dir models

# Optioneel: klein instruct-chat model (~0.8 GB)
hf download bartowski/Llama-3.2-1B-Instruct-GGUF \
Llama-3.2-1B-Instruct-Q4_K_M.gguf --local-dir models

Handige startpunten uit de README:

ModelHugging Face repoBestand
TinyLlama 1.1B Chat Q8_0TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUFtinyllama-1.1b-chat-v1.0.Q8_0.gguf
Llama 3.2 1B Instruct Q4KMbartowski/Llama-3.2-1B-Instruct-GGUFLlama-3.2-1B-Instruct-Q4KM.gguf
Llama 3.1 8B Instruct Q4KMbartowski/Meta-Llama-3.1-8B-Instruct-GGUFMeta-Llama-3.1-8B-Instruct-Q4KM.gguf
SmolLM2 135M Instruct Q8_0bartowski/SmolLM2-135M-Instruct-GGUFSmolLM2-135M-Instruct-Q8_0.gguf

Gebruik bij voorkeur Q4KM voor dagelijks gebruik en Q8_0 voor kleine smoke tests. Meer GGUF's zijn te vinden onder de llama.cpp-compatibele modellen op Hugging Face. Wat Ferrox momenteel verifieert, staat in docs/MODELS.md.

Uitvoeren:

# One-shot completion
./target/release/ferrox -m models/tinyllama-1.1b-chat-v1.0.Q8_0.gguf \
-p "The capital of France is" -n 32 --temp 0 --no-cnv

# Chat op Metal (standaard wanneer de GGUF een chat template bevat)
./target/release/ferrox -m models/Llama-3.2-1B-Instruct-Q4_K_M.gguf \
-p "What is 2+2?" -n 64 --temp 0 -dev metal -ngl all

# OpenAI-compatibele server
./target/release/ferrox-server \
-m models/tinyllama-1.1b-chat-v1.0.Q8_0.gguf \
--host 127.0.0.1 --port 8383 -dev metal -ngl all

curl -s -X POST http://127.0.0.1:8383/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"m","messages":[{"role":"user","content":"Hi"}],"max_tokens":32,"temperature":0}'

De vlaggen spiegelen die van llama.cpp (-m, -p, -n, -t, --temp, -ngl, …) — een volledige referentie staat in docs/CLI.md. Eén Ferrox-specifieke instelling die de moeite waard is: --ctk q80 (of FERROXCTK=q80) slaat de KV cache op als Q80 op Metal in plaats van de standaard f16. Dit ruilt een beetje precisie in voor een kleinere KV-voetafdruk bij langere contexten.

Waarom prestaties bewijsbaar moeten zijn, niet slechts beweerd

Elk lokaal inference-project claimt snel te zijn. Bijna geen enkel project toont de methodologie. Ik wilde niet bijdragen aan die ruis, dus is de volledige benchmarking-opzet in Ferrox zo gebouwd dat claims falsifieerbaar zijn:

  • Dezelfde machine, hetzelfde GGUF-bestand, dezelfde backend voor beide engines, direct na elkaar getest.
  • "Warm runs", greedy decoding, meerdere herhalingen; de mediaan wordt gerapporteerd.
  • Elk hoofdaantal is gekoppeld aan een JSON-bewijs ("receipt") in de repo — genereer dit opnieuw en de cijfers kloppen wel of niet.

Deze discipline leverde echte resultaten op een Apple M2 Pro (Host B). De onderstaande cijfers zijn voorspelde tok/s uit de fair-chat suite in benchmarks/RESULTS.md. De "Gap" is llama / ferrox (onder 1.0 betekent dat Ferrox sneller is; nabij-pariteit ligt binnen ~5%):

ModelBackendFerroxllama.cppGap
Llama-3.1-8B Q4KMMetal28.3 tok/s27.6 tok/s~0.97× (pariteit / Ferrox)
Llama-3.2-1B Q4KMMetal140.8 tok/s140.8 tok/s1.00× (pariteit)
TinyLlama-1.1B Q8_0Metal117.9 tok/s113.7 tok/s~0.96× (pariteit)
Qwen2.5-0.5B Q8_0Metal196.1 tok/s132.8 tok/s~0.68× (Ferrox)
SmolLM2-135M Q8_0Metal290.2 tok/s241.2 tok/s~0.83× (Ferrox)
Gemma-3-1B Q8_0Metal94.1 tok/s81.7 tok/s~0.87× (Ferrox)
OLMoE-1B-7B Q4_0Metal88.4 tok/s156.8 tok/s~1.77× (llama)
Mistral-7B Q4KMMetal31.4 tok/s33.3 tok/s~1.06× (nabij pariteit)
Phi-4-mini Q4KMMetal50.0 tok/s53.1 tok/s~1.06× (nabij pariteit)

Het belangrijkste referentiepunt voor mij is Llama-3.1-8B op Metal: deze zit nu op ~0.97× — Ferrox is iets sneller dan llama.cpp op dezelfde host en GGUF (28.27 vs 27.55 pred). Op het CLI one-shot pad is het een exacte gelijkstand van 1.00× (28.85 vs 28.64). Dat was het moment waarop de engine niet langer aanvoelde als een academische oefening.

Een andere grote ontwikkeling sinds de eerste metingen is OLMoE op Metal. Vroege resultaten lagen ongeveer ~15× achter op llama.cpp (~10 tok/s). Na werk aan de expert-plaatsing op Metal staat dit nu op 88.4 vs 156.8 (~1.77×) — nog steeds achterlopend, maar in een compleet andere klasse. Gemma-3 Metal is van achterlopen naar voorlopen gegaan. De volledige methodologie en elk ruw bewijs staan in het RESULTS-bestand.

De testsuite opnieuw uitvoeren

Als je de bewijzen zelf wilt regenereren (dezelfde host, dezelfde GGUF's, beide engines):

python3 benchmarks/run_suite.py --skip-missing --fit-host
# Optionele CLI-modus pins:
python3 benchmarks/run_suite.py --skip-missing --fit-host --mode cli

Dit overschrijft de bewijzen onder benchmarks/receipts/pins/ en regenereert RESULTS.md via render_results.py. Geen verzonnen getallen — als een bewijs ontbreekt, staat dat in de tabel. --fit-host slaat modellen over die niet in het geheugen van de machine passen (en slaat CUDA over op Darwin/macOS).

Hoe de winst is behaald

Twee architecturale keuzes hebben het meeste werk gedaan:

  1. Het fuseren van dekwantisatie in de matmul. Gekwantiseerde gewichten (4-bit, 8-bit) worden nooit uitgebreid naar een full-precision buffer. De dekwantisatie-berekening gebeurt inline als onderdeel van het dot product. Zo betaal je er één keer voor in de cache, in plaats van via een aparte, memory-bound pass.
  2. Architectuur-specifieke GPU-paden. Modellen zijn structureel niet identiek — Qwen heeft per-head QK-normalisatie, Gemma-3 gebruikt sliding-window attention en GeGLU, Phi-3 fuseert zijn QKV en FFN projecties. Ferrox implementeert specifieke Metal kernels voor elk van deze in plaats van elk model door één generiek attention-pad te dwingen. Dit is precies waarom kleine Qwen- en SmolLM2-modellen llama.cpp op Metal met een ruime marge verslaan — de kernel past bij de daadwerkelijke berekeningsvorm in plaats van overhead te betalen voor generaliteit die niet nodig is. Hetzelfde idee zorgde voor de sprong bij OLMoE Metal: expert-plaatsing op de GPU, in plaats van een generiek dense pad dat verkleed is als MoE.

Nut: meer dan een benchmark-experiment

Naast de cijfers is Ferrox een oprecht praktische manier om modellen lokaal te draaien:

  • Eén enkel statisch binary. Geen Python-omgeving, geen CUDA-toolkit versie-roulette, geen pip install dependency resolution. Kopieer het binary, wijs het aan een .gguf-bestand en start.
  • Direct inzetbaar voor bestaande tooling. De OpenAI-compatibele server betekent dat elke app die al is gekoppeld aan de API van ChatGPT — LangChain-scripts, custom chat-frontends, eval harnesses — werkt met een volledig lokaal model door slechts één regel (de base-URL) te wijzigen.
  • MoE-ondersteuning, niet alleen dense modellen. OLMoE-1B-7B draait op CPU en Metal met geverifieerde resultaten. Metal MoE loopt nog achter op llama.cpp, maar het gat wordt kleiner. Dit is belangrijk omdat Mixture-of-Experts de bron is van veel efficiëntiewinsten bij frontier-modellen — een inference engine die alleen dense transformers aankan, is steeds onvollediger.
  • Backend-flexibiliteit voor de hardware die je bezit. CPU-only laptop, Apple Silicon, Nvidia GPU — één codebase dekt alle drie, in plaats van drie aparte tools. Gekwantiseerde KV (--ctk q8_0 op Metal) helpt wanneer de contextlengte het unified memory begint uit te putten.

Wat er nog niet af is

Ik verkoop dit liever onder dan over:

  • CUDA-prestatiewerk is gepauzeerd. De suite ondersteunt --backend cuda, maar er is momenteel geen CUDA-bewijs beschikbaar — hiervoor is een GPU-host nodig.
  • Metal prefill moet nog worden gemonitord ten opzichte van llama.cpp bij grotere modellen — decode is waar de pariteitsgetallen leven; promptpersecond heeft meer ruimte voor verbetering.
  • MoE op Metal is sterk verbeterd, maar OLMoE loopt nog ~1.8× achter op llama.cpp. Qwen2-MoE / Mixtral bewijzen ontbreken op Host B (vanwege GGUF / RAM beperkingen).
  • Gemma-4-E2B wordt momenteel expliciet geweigerd (vereist een specifiek engine pad) — zowel Ferrox als Homebrew llama.cpp wijzen dit af.
  • Frontier-scale MoE / MLA — Kimi, GLM, DeepSeek — hebben primitieven en synthetische stacks, maar er is geen echt checkpoint met honderden miljarden parameters end-to-end gedraaid. Dat is zoveel een hardwareprobleem als een softwareprobleem.

Conclusie

Ferrox begon als een manier om de interne werking van inference daadwerkelijk te begrijpen in plaats van het te behandelen als een black box achter een pip install. Het is uitgegroeid tot iets waar ik zelf echt naar zou grijpen: één enkel binary dat een GGUF-bestand laadt en ofwel chat in de terminal, of een OpenAI-compatibele API serveert, met snelheden die standhouden tegen de referentie-implementatie op echte hardware, inclusief de bewijzen om dit aan te tonen.

Als je nieuwsgierig bent naar hoe gekwantiseerde inference onder de motorkap werkt, een dependency-vrije manier zoekt om open modellen lokaal te draaien, of simpelweg gaten wilt schieten in de benchmark-methodologie: de repo is Apache-2.0 en open voor issues en PR's.

Verdere bronnen:

  • Ferrox op GitHub
  • benchmarks/RESULTS.md — volledige pinned benchmark suite
  • docs/MODELS.md — ondersteunde architecturen en verificatiestatus
  • docs/CLI.md — CLI vlaggen
  • GGUF formaat specificatie
  • llama.cpp — de referentie waartegen dit project zich meet