Xyntetik Runner

Xyntetik Runner is a single-binary GGUF inference and LoRA training engine written from scratch in plain C: one binary serves, verifies, scores, adapts and trains GGUF models on CPU (x86 AVX2/FMA, ARM NEON), CUDA and Metal. It trains LoRA adapters directly through the same quantized GGUF weights it serves, with no separate FP16 training copy, and two runs with the same data, seed and config write a byte-identical adapter file. Every claim is tied to a measurement you can re-run, and every recorded run is a signed receipt that replays. No Python, no pip, no third-party runtime, no ggml. One caveat travels with the training claim: a quantized merge rounds the delta, and merging an adapter into a 4-bit base erases the fine-tune (8-bit and F16 keep it), so serve adapters with --lora or merge into Q8_0 or better.

Three questions this engine answers, one page each: train a LoRA on the quantized GGUF you serve, reproducible LoRA training with receipts, tool calls that survive the token limit.

Two independent training runs producing byte-identical adapters

Sixty seconds to a served model

curl -LO https://github.com/Joakimpalm-Zen/xyntetik-runner/releases/latest/download/runner-macos-arm64
curl -LO https://github.com/Joakimpalm-Zen/xyntetik-runner/releases/latest/download/SHA256SUMS
shasum -a 256 --check --ignore-missing SHA256SUMS
chmod +x runner-macos-arm64 && mv runner-macos-arm64 runner
curl -L -o model.gguf https://huggingface.co/ibm-granite/granite-4.1-3b-GGUF/resolve/main/granite-4.1-3b-Q8_0.gguf
./runner -m model.gguf --serve
curl localhost:8080/v1/chat/completions \
  -d '{"messages":[{"role":"user","content":"Say hello in one sentence."}]}'

Linux: the asset is runner-linux-x86_64 and the check command is sha256sum -c --ignore-missing. Windows: runner-windows-x86_64.exe. The checksum line is not decoration - this project’s whole culture is receipts, and it starts at the download. The model above is the smallest that passes this project’s fidelity gate against its own BF16 parent; alternatives and the reasoning are in the quick start below.

macOS note: the binaries are not yet notarized. A curl download runs as shown; a browser download gets quarantined by Gatekeeper - clear it with xattr -d com.apple.quarantine runner or right-click → Open once.

Testing Runner? The project is pre-1.0 and hardware coverage is still limited - that is an invitation, not an apology. If you have an NVIDIA/Apple setup, an unusual GGUF, a coding agent, or a model family not in the support matrix, the result is genuinely wanted, success or failure alike: open an issue with runner --version, runner --caps, the model’s exact filename and the load log. Independent reproductions of the determinism claims get credited in the docs, as the first one already is.

Contents: try it · why Runner · what it adds · training · models · APIs · support matrix · CLI reference

Xyntetik Runner is independent and bootstrapped: the engine is free forever under Apache 2.0 - consulting and enterprise work fund the hardware. Built in Sweden, runs on your hardware; your data never leaves the building.

Why this and not llama.cpp?

Use llama.cpp - it is the ecosystem, and Runner deliberately rides its formats rather than competing with them: GGUF in, llama.cpp-convention adapter files in and out. An adapter Runner trains scores identically (1.000 on its held-out eval) served by stock llama.cpp, and community F16 adapters load straight back into Runner - both measured, not assumed.

What Runner adds is not a longer feature list; it is a set of contracts the ecosystem does not make. Determinism as a hard promise: the same executable and inputs reproduce the same sampled tokens across runs and thread counts, and training reproduces the same adapter file sha256, gated in CI. Independent rebuilds are explicitly outside that byte-identity claim because compiler and ISA libm can differ; the full claim boundary, including everything deliberately NOT promised, is one page: docs/determinism-scope.md. Scope as a promise: supported architectures are named, unknown ones are refused, and every backend claim is tied to an executable gate and pinned model evidence. Honesty as an artifact: the benchmark tables below include the rows where Runner loses, and the docs keep the failed experiments. If you want maximum architecture coverage and raw speed, use llama.cpp and we mean that sincerely. If you need to prove what your model said, what it learned from, or what you actually shipped - that is what this runtime is for.

Designed to stay on

There is a cost benchmarks rarely show: what a resident inference server does to the machine while it serves nothing. We measured it - four-state lifecycle (loaded idle, post-inference idle, after unload), granite-4.1-3b Q8_0 at c=4096, stock defaults, engines run sequentially, llama.cpp from the prebuilt b10639 release. Reproduce it with scripts/idle_coexistence.py.

On an 8 GB M1 (quiet machine), while loaded and idle:

while idle runner llama-server b10639
wired (unevictable) memory +8 MB +3,819 MB
CPU wakeups per second 0.5 161
CPU time per idle minute ~0.00 s 0.2-0.3 s
give the memory back POST /unload (360 MB to 24 MB) kill the process

llama-server wires the whole model into unified memory and holds it until the process dies, and its idle loop ticks at ~160 Hz. Runner keeps weights as evictable zero-copy mappings the OS can reclaim whenever another app needs the RAM, wakes about once every two seconds, and hands everything back on /unload (or automatically with --ttl). The flip side is real and we report it: when llama-server is allowed to hold everything, its time to first token is faster (0.15 s vs 3.2 s on the pressured M1), because residency is exactly what it buys. On a discrete-GPU box (RTX 3070) the footprint gap disappears - both engines hold ~4 GB of VRAM loaded - and the remaining differences are idle discipline (0.00 vs 0.3-0.6 CPU seconds per minute) and lifecycle control (/unload returned all 4 GB of VRAM; llama-server has no unload).

These are different optimization goals, not a defect: llama-server is built to answer the next request as fast as possible, Runner is built to be left running all day next to your actual work. If the machine is dedicated to inference, that residency is pure win - use llama.cpp. If the machine is also your workstation, an engine that wires down half your RAM and ticks 160 times a second while idle is the reason you kill it every time - and the reason Runner does not need killing.

The same lifecycle measured on a 128 GB M5 Max with gpt-oss-120b (63 GB) says abundance does not dissolve the difference, it scales it: loaded idle, llama-server (010be968) wires +60.8 GB — half the machine held by an idle process — against Runner’s +35 MB, and /unload hands everything back (RSS 229 MB). The latency flip side scales identically: 0.25 s vs 10.1 s cold first token. Full table and method: docs/idle-coexistence-120b-m5max-2026-09-01.md.

For release history and benchmark narratives, see CHANGELOG.md and docs/benchmarks.md (last re-measured 2026-09-02 on three hosts: dense decode 81-93% of llama.cpp on the MIG, prefill 5-10%).

The rest of the README, from the command-line reference to the compatibility evidence, is on GitHub.