UpinelAIOS runs everything through llama.cpp. That is a deliberate constraint rather than a limitation we have not got around to: llama.cpp is the only runtime that can serve an uncensored Gemma 4 at all, and it serves every other GGUF equally well, which is why the Qwen family lives here too.
Homebrew's llama.cpp is the default, and the right answer for almost everything. Two models need a build it does not ship, for entirely different reasons, and they are kept apart on purpose — a runtime only one model needs should not become everybody's runtime:
| model | needs | why | install |
|---|---|---|---|
gguf-q-27b |
the FastMTP patch | its draft head trims the drafter's vocabulary and adds a d2t remap tensor |
./lib/build-fastmtp.sh |
gguf-b-27b, gguf-b-27b-ptq1 |
the PrismML fork | ternary weight types and a Hadamard activation transform, neither of them upstream | ./lib/fetch-runtime.sh prism |
One of the two needs saying plainly, because it costs a factor of three on the 27B.
HauhauCS publishes Qwen3.8-27B-Uncensored-HauhauCS-Aggressive-FastMTP-32K.gguf
as the MTP draft head for the 27B. It is a genuine speed win — the same model
through MTPLX in the sister project runs 2.6–3.4x faster with MTP than without.
It does not work on a stock llama.cpp. Its provenance file says so:
runtime_base: ggerganov/llama.cpp@4df29be4f4c3673f428170fda944a5b19f743bb8
runtime_patch: HauhauCS-FastMTP-llama.cpp.patch
That 53-line patch adds a d2t tensor: the drafter's output vocabulary is
trimmed from 248,320 tokens to 32,768, and d2t remaps between them. Stock
llama.cpp does not know the tensor, compares the trimmed output against the full
vocabulary, and refuses:
error loading model: check_tensor_dims: tensor 'output.weight' has wrong shape;
expected 5120, 248320, got 5120, 32768
And llama-server treats a failed draft as fatal. It does not fall back — it exits. So attaching that head on a stock build does not cost you speed, it stops the server booting.
It detects the d2t tensor in the draft's GGUF and, unless you tell it
otherwise, skips the draft and runs autoregressive, printing why:
warn Qwen3.8-27B-...-FastMTP-32K.gguf needs a patched llama.cpp (trimmed draft vocab).
warn Running autoregressive to keep the server up.
Detection is on the tensor, not the filename, so any future head built the same
way is caught too. model_draft_gguf and draft_needs_patched_runtime in
lib/common.sh do the work.
Built 2026-08 on the reference machine and measured before and after. This is the whole picture, including the part that is worse:
Qwen 27B (gguf-q-27b), same prompts, greedy-ish, median of five:
| runtime | draft | decode | acceptance |
|---|---|---|---|
| stock Homebrew | none | 7.6 t/s | — |
| patched | none | 8.4 t/s | — |
| patched | FastMTP depth 1 | 15.4 t/s | 77% |
| patched | FastMTP depth 3 | 10.6 t/s | 53% |
| patched | FastMTP depth 5 | 8.1 t/s | 40% |
Depth 1, not 3. Acceptance collapses with depth (77% → 53% → 40%), so drafting further costs more than it returns. That is the opposite of the Gemma MTP head, where depth 3 wins — different head, different profile, and worth re-measuring rather than assuming.
So the honest number is 2.0x, 7.6 → 15.4 t/s, not the ~3x the MTPLX side gets. Real, but short of what MTP achieves on Metal through MLX.
Gemma 26B-A4B (gguf-g-26ba4b), llama-bench, same machine:
| runtime | prefill | decode |
|---|---|---|
| stock Homebrew | 1924.7 t/s | 81.0 t/s |
| patched | 1888.1 t/s | 78.7 t/s |
The patched build makes Gemma slower — about 2% on prefill, 3% on decode.
Expected in hindsight: the patch only touches src/models/qwen35.cpp, so it
gives Gemma nothing, while pinning the build to an older upstream commit
(build 10454 against the current 10809) loses whatever landed since.
So the two runtimes coexist rather than one replacing the other. LLAMA_SERVER
in env.conf points at the patched binary when you want it; leaving it empty
uses the Homebrew build, which is the right default for Gemma and for every
Qwen model without a FastMTP head.
If you want the speed, build llama.cpp with the patch. One command does the
whole thing — clone, verify, patch, build, and record the result in env.conf:
./lib/build-fastmtp.shIt refuses to build if the patch checksum does not match the provenance file alongside it, and it works on a CommandLineTools-only machine (no full Xcode): without the Metal compiler, shaders are compiled at runtime instead of embedded.
The patch ships inside the model repo you already downloaded:
models/HauhauCS--Qwen3.8-27B-Uncensored-HauhauCS-Aggressive-MTP-GGUF/
HauhauCS-FastMTP-llama.cpp.patch
FastMTP-PROVENANCE.json
HauhauCS-FastMTP-Ed25519-PUBLIC.pem
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
git checkout 4df29be4f4c3673f428170fda944a5b19f743bb8 # the patch's base
git apply /path/to/HauhauCS-FastMTP-llama.cpp.patch
git apply --check # ...before applying for real, so a mismatch is loudVerify the patch before trusting it — the repo signs it, and the signature is worth checking rather than taking on faith:
shasum -a 256 HauhauCS-FastMTP-llama.cpp.patch
# compare against runtime_patch_sha256 in FastMTP-PROVENANCE.jsonThen point the server at your build and tell it the draft is usable:
LLAMA_SERVER=/path/to/llama.cpp/build/bin/llama-server # or put it on PATH
DRAFT_PATCHED_RUNTIME=1 # in env.conf
./restart.shDRAFT_PATCHED_RUNTIME=1 only says "this head is loadable" — it does not build
anything. With it set on a stock runtime, the server will exit, exactly as it
does today when the head is attached by hand.
Bonsai 2 27B is PrismML's ternary pack of Qwen3.8-27B: the same architecture, with the weights rotated blockwise by a Hadamard transform and then quantised to {−1, 0, +1} with one FP16 scale per group of 128. It lands at 1.72–2.13 bits per weight — a 27B in 6–7 GB — and it needs two things stock llama.cpp does not have:
| what | why stock llama.cpp fails |
|---|---|
the PQ2_0 / PTQ1_0 weight types |
Prism's own ggml types; a stock build rejects the file as an unknown type |
| the Hadamard activation transform | folded into the stored weights offline, so the runtime has to apply the matching transform to activations; not upstream yet (ggml-org/llama.cpp#27779) |
The failure mode has a quiet half, and that is why this is written down at
all. Stock llama.cpp refuses PQ2_0 and PTQ1_0 loudly. It also loads the
similarly-named Q2_0 — an official ggml type at a different group size, kept in
a separate repo — without any warning, and produces gibberish. Nothing in the
log says so, which is worse than a crash: it looks like it worked. So the aliases
point only at the packed files, and unlike the Gemma draft-head case there is no
"substitute a nearby quant" fallback to reach for.
./lib/fetch-runtime.sh prism # prebuilt macOS arm64, checksum verified
./start.sh --model gguf-b-27bThe script pins one PrismML release tag and verifies the archive against its
published sha256 before extracting. Pinned rather than tracking "latest", for a
concrete reason: the release published after the pinned one (prism-b10687)
lists macOS archives in its release notes but does not attach them, and every
macOS link on that tag 404s. A pinned archive that exists beats a latest that
does not. If the pinned asset ever disappears, the script prints the
build-from-source commands instead of failing with a bare curl error.
It also checks the runtime can actually do the job before wiring it up: it runs
llama-server --version, then confirms libggml-metal really contains PQ2_0.
The ternary kernels are the entire reason for the runtime, and their absence
would otherwise not surface until a model failed to load.
lib/engines/gguf.sh resolves the runtime per model, through
runtime_key_for_model() in lib/common.sh:
- a model that declares a runtime must have it, or
./start.shstops and names the install command — there is no autoregressive fallback here, because there is nothing to fall back to - everything else keeps Homebrew's llama.cpp, which is the build the Gemma and Qwen figures in the README were measured on
- the FastMTP draft patch keeps its own knobs (
LLAMA_SERVER,DRAFT_PATCHED_RUNTIME), because it is a property of a draft head rather than of a model
Running ./lib/fetch-runtime.sh with no arguments lists what is available, what
is installed, and which aliases need it.
| PQ2_0 | PTQ1_0 | |
|---|---|---|
| size | 7.21 GB | 5.95 GB |
| bits/weight | 2.13 | 1.75 |
| M5 Pro decode / prefill | 28.1 t/s / 387 t/s | — |
Those are Prism's numbers, not this project's. They were measured with
llama-bench at depth 0 on an M5 Pro. A first smoke test here — greedy, a
63-token prompt, 41 tokens generated, thinking on — measured 25.5 t/s on the
reference machine, within 10% of their figure, which is why it is quoted as
usable rather than aspirational. That is a smoke test and not a benchmark: the
README's other numbers are 8k-context agent turns, best of several runs, and
Bonsai 2 has not had that treatment yet.
For scale: mlx-q-35ba3b does 141 t/s and gguf-g-26ba4b 106 t/s here. Bonsai 2
is not a speed model. At 6 GB it is a capability per gigabyte model, and the
honest comparison is the 9.4 GB conventional "2-bit" Qwen3.8-27B build, which
Prism measure twelve benchmark points lower at two-thirds again the size.
Speculation: leave it off. Prism ship a dspark drafter (~0.6 GB) that is
worth 1.8–2.4x on CUDA. Their own documentation says that on Apple Silicon it
pays off only for ternary code and maths (~1.2x) and is a net slowdown
otherwise, so no draft head is fetched here.
The Metal tensor API probe can fail on M5. The Prism fork compiles its Metal library from source to reach the tensor API, and on some M5 macOS builds that compile fails:
ggml_metal_library_init_from_source: error compiling source
ggml_metal_device_init: - the tensor API is not supported in this environment - disabling
Observed here on an M5 Pro running macOS 27.0, which is outside the 26.2–26.4 window Prism document — so their range is not the whole story. ggml recovers by itself and Metal keeps working; the cost is an alarming log line and a few seconds of startup. To skip the attempt rather than fail it:
METAL_TENSOR_API="off"That is a setting this project already exposes, and it costs nothing that was not
already lost — on a build where the probe fails, the tensor API is disabled
either way. ./start.sh says so at startup when it sees a Prism-runtime model on
a chip that has Neural Accelerators.
Prism ship a 0.63 GB Q8_0 vision projector and Bonsai 2 is a vision-language
model, but the uncensored community packs this project points at do not
include it, and pairing Prism's aligned projector with abliterated language
weights is untested. So ENABLE_VISION is not wired up for these aliases: text
and tool calling are what they serve today.
The tool-template rewrite (TOOL_TEMPLATE=1), which names a schema's required
fields so a harness stops reporting missing required property, is anchored on
Gemma's chat template. A Bonsai model therefore does not get it: ./start.sh
prints tool template unavailable, using the model's stock template and serves
the model's own template instead. Nothing breaks, but the reliability work in
the deep dive does not apply here yet, so
name the required fields in the agent's system prompt.
The unpatched 27B runs at 7.6 t/s here. That is not a bug: a dense 27B reads about 15 GB of active weights per token, and ~7.6 t/s is what this memory bandwidth supports. The Qwen 27B is a dense model — every parameter is active on every token — unlike the Gemma 26B A4B, which is a mixture of experts with only ~4B active and reaches 106 t/s for exactly that reason.
So the two families are not comparable on speed and should not be sold as if they are:
| model | shape | active per token | decode here (GGUF) | MLX |
|---|---|---|---|---|
gguf-g-26ba4b (Gemma 4) |
MoE, 8 of 128 experts | ~4B | 106.4 t/s | — |
gguf-q-9b |
dense | 9B | 44.3 t/s | ~90 t/s |
gguf-q-27b |
dense | 27B | 13.5 t/s | ~30 t/s |
If you want Qwen 27B speed, the honest answer is the MLX engine: MTPLX's MTP implementation works on Metal and llama.cpp's does not. Both engines ship in this one install, so switching is a model change rather than a switch of repo.