LLM inference on Phobos kernels
What it is
GGUF models on Phobos kernels
phobos-cliloads a GGUF file, picks the forward pass from the architecture it names, and runs every layer on kernels written in Phobos and compiled for the card. Decode fuses a chain of layer stages into one persistent kernel.An OpenAI-compatible server
--listenserves/v1/chat/completions,/v1/completionsand/v1/models, renders conversations and tools with the model's own chat template, streams chat over server-sent events, and puts a dashboard on the terminal.Made for little VRAM
Written and measured on an 8 GB card, on which two 27B models at under two bits a weight fit with room for their context.
Experts streamed from host memory
A mixture-of-experts model that does not fit keeps a cache of experts on the card and computes the misses on the CPU, with no manual split between the two. The cache gives memory back when another program fills the card, and takes it again once it is free.
phobos-cli --listen: the splash, then the dashboard with the model, the memory on the card, the
network of layers and the log.
Decode and prefill against llama.cpp
On an RTX 2080 SUPER, Phobos decodes at 0.93x to 1.82x the rate of llama.cpp over 128 tokens, and at or above it on 5 of 7 models; the faster engine is llama.cpp on GLM-4.6V-Flash-Q4_K_M and PrismML's llama.cpp fork on Ternary-Bonsai-2-27B-PTQ1_0. A 128-token prompt runs at 0.83x to 4.09x the other engine's rate.
Phobos against llama.cpp, tokens per second, higher is better
- Phobos
- llama.cpp, or the fork its row names
Each pair of bars has its own scale, so bars compare the two engines on one model and one test, not across models. The ratio is Phobos's rate over the other engine's. Rates are the mean over the rounds bench.py found uncontended, each round the mean of its repetitions.
All numbers, with standard error
| model | test | Phobos | other engine | ratio | source |
|---|---|---|---|---|---|
| Qwen3.5-0.8B-Q8_0 | pp128 | 5976.29 +/- 179.57 | 6723.91 +/- 12.97 llama.cpp | 0.89 | results/bench.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| pp512 | 8693.19 +/- 38.57 | 10729.32 +/- 26.75 | 0.81 | ||
| tg32 | 325.00 +/- 0.66 | 229.87 +/- 0.22 | 1.41 | ||
| tg128 | 323.94 +/- 0.11 | 246.98 +/- 0.03 | 1.31 | ||
| tg512 | 324.50 +/- 0.06 | 250.23 +/- 0.05 | 1.30 | ||
| tg1024 | 323.53 +/- 0.14 | 249.84 +/- 0.12 | 1.29 | ||
| tg2048 | 322.09 +/- 0.10 | 248.61 +/- 0.07 | 1.30 | ||
| MiniCPM5-1B-Q8_0 | pp128 | 7574.73 +/- 27.89 | 8569.34 +/- 16.46 llama.cpp | 0.88 | results/bench.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| pp512 | 10086.13 +/- 26.26 | 16164.09 +/- 20.50 | 0.62 | ||
| tg32 | 315.18 +/- 0.25 | 270.33 +/- 0.27 | 1.17 | ||
| tg128 | 314.19 +/- 0.15 | 270.89 +/- 0.19 | 1.16 | ||
| tg512 | 313.14 +/- 0.11 | 269.83 +/- 0.17 | 1.16 | ||
| tg1024 | 310.84 +/- 0.05 | 268.98 +/- 0.28 | 1.16 | ||
| tg2048 | 304.84 +/- 1.00 | 266.84 +/- 0.20 | 1.14 | ||
| Qwen3.5-4B-Q4_K_M | pp128 | 2305.30 +/- 3.32 | 2296.17 +/- 3.53 llama.cpp | 1.00 | results/bench.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| pp512 | 2372.05 +/- 2.44 | 2956.21 +/- 1.31 | 0.80 | ||
| tg32 | 111.92 +/- 0.14 | 103.60 +/- 0.08 | 1.08 | ||
| tg128 | 111.75 +/- 0.14 | 106.54 +/- 0.11 | 1.05 | ||
| tg512 | 111.54 +/- 0.42 | 106.77 +/- 0.29 | 1.04 | ||
| tg1024 | 111.59 +/- 0.26 | 107.01 +/- 0.12 | 1.04 | ||
| tg2048 | 111.43 +/- 0.10 | 106.50 +/- 0.12 | 1.05 | ||
| GLM-4.6V-Flash-Q4_K_M | pp128 | 1384.87 +/- 1.66 | 1665.23 +/- 63.55 llama.cpp | 0.83 | results/bench-glm.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| pp512 | 1160.08 +/- 2.47 | 1974.65 +/- 8.10 | 0.59 | ||
| tg128 | 59.69 +/- 0.02 | 63.23 +/- 0.01 | 0.94 | ||
| Ternary-Bonsai-2-27B-PTQ1_0 | pp128 | 583.99 +/- 10.82 | 469.76 +/- 0.40 PrismML's llama.cpp fork | 1.24 | results/bench-bonsai.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| tg128 | 45.25 +/- 0.23 | 48.54 +/- 0.23 | 0.93 | ||
| Qwen3.8-27B-UD-IQ1_M | pp128 | 537.82 +/- 0.00 | 473.17 +/- 0.85 llama.cpp | 1.14 | results/bench-qwen38.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| tg128 | 30.98 +/- 0.01 | 21.69 +/- 0.00 | 1.43 | ||
| Qwen3.6-35B-A3B-UD-Q4_K_M | pp128 | 311.55 +/- 4.16 | 76.25 +/- 0.12 llama.cpp | 4.09 | results/bench-qwen36moe.json, NVIDIA GeForce RTX 2080 SUPER, driver 617.14, 3eec883, 2026-10-08 |
| pp512 | 577.88 +/- 1.13 | 257.57 +/- 0.55 | 2.24 | ||
| tg128 | 54.32 +/- 0.19 | 29.88 +/- 1.64 | 1.82 |
How these runs were measured, the 35B on a replayed coding-agent session, and what happens when another program takes part of the card: Benchmarks. To measure your own card: Benchmarking.
Five ideas behind the numbers
Decode waits on memory
A decode step reads every weight once and does one multiply-add with it. That is about two operations per byte, and the card needs roughly ten times that before its math units, not its memory bus, set the pace. A prompt reuses each weight for every token in it, and the bottleneck flips. Decode kernels are written to keep the bus busy, not the math units.
Decode: about 1.9 operations per byte, and the card needs about 22 before its math sets the pace. The bus runs flat out, the SMs mostly wait.
- global memory
- registers
- memory-bound
One warp per output
Every output of a decode matrix-vector product is one row of weights dotted with the activation. Giving each thread its own row scatters a warp's reads over 32 rows. The lanes matvecs give a whole warp one row instead: its 32 lanes read one contiguous run, and sum<over ctx.lane> folds their partial sums in five shuffle stages.
A warp per output: 32 lanes read 16 bytes each, one contiguous 512-byte run a step, then sum<over ctx.lane> adds their partial sums in 5 xor-shuffle stages.
- global memory
- registers
- scattered reads
for k in range(ctx.lane * 16, K, ctx.lanes * 16), then sum<over ctx.lane>(acc),
and lane 0 stores the output.
A model that does not fit
Qwen3.6-35B-A3B routes every token to 8 of 256 experts per layer, and its experts alone do not fit on the card. Phobos keeps the experts in host memory and a cache of them on the card, least recently used out first. On a decode step the CPU computes the misses straight from host memory while the card runs the hits; a prompt copies its misses over PCIe.
Token 1: 6 of 8 picks already on the card, the CPU computes the 2 misses from host memory. Simulated hit rate so far 75%.
- global memory
- registers
- misses
Giving the memory back
The cache is sized from the card's free memory, and Phobos reads the whole card's free memory again between passes. When another program fills the card, the cache shrinks instead of letting the driver page buffers to host memory, and more of the misses go to the CPU threads. When the memory is free again, the cache grows back. llama.cpp fixes its split at load time, so the same squeeze pages its buffers and every token reads them back over PCIe.
The process exits and the cache grows back to 39 experts a block. Phobos decodes at 44.6 tokens/s, llama.cpp at 31.2. The replay took 52.4 s on Phobos and 131.7 s on llama.cpp.
- the card
- CPU, misses
- another process
Rotate before rounding
Ternary weights share one scale per block, so one outlier can ruin it: the best scale fits the outlier and the small weights round to zero. A Hadamard rotation spreads the outlier over the block before rounding. The activations get the same rotation at run time, so the product does not change, and one rotation serves every projection that reads the same input.
Rotated first: the outlier spreads over all 16 values. Rounding error 35%, and the activations get the same rotation at run time.
- global memory
- registers
- outlier, rounding error
One token at a time
Decoding is a loop: the tokenizer turns the prompt into tokens, the layers turn the last token into logits, the sampler picks the next token, and that token streams out and goes back in. Every pass reads all the weights the token needs from the card's memory, which is why the ideas above matter.
Each sampled token streams out and goes back into the layers as the next input.
Models and formats
The architectures are llama, qwen35, qwen35moe and glm4. These files have been checked against llama.cpp, token for token where greedy decoding is stable. Every format below decodes on the card; see Models and formats for what each architecture covers.
Tested models
20 quantized formats
- Block formats
Q4_0Q4_1Q5_0Q5_1Q8_0Q8_1- K-quants
Q2_KQ3_KQ4_KQ5_KQ6_K- I-quants
IQ1_MIQ1_SIQ2_SIQ2_XSIQ2_XXSIQ3_SIQ3_XXSIQ4_XS- Ternary
PTQ1_0
One file per format in phobos-gguf/src/quant/. A file can mix formats from tensor to tensor, and activations are quantized to Q8_0 whatever the weight's format.
Quickstart
Get the binary
Download a release and keep
kernel-cache/beside the binaries, or build from a checkout:cargo build --release --features cuda -p phobos-cliAsk a question
Download MiniCPM5-1B-Q8_0 and pass a prompt. Without one,
phobos-clistarts a REPL.phobos-cli --gguf minicpm5-1b-Q8_0.gguf "Why is the sky dark at night?"Serve it
Point any OpenAI client at
http://127.0.0.1:8080/v1.phobos-cli --gguf minicpm5-1b-Q8_0.gguf --listen 127.0.0.1:8080