Ternary Quantization in Practice: Bonsai 2 27B in 5.95 GB
On 17 September 2026, PrismML published Ternary Bonsai 2 27B: a 27-billion-parameter
model whose weights are only -1, 0 and +1, packed into a 5.95 GB file, released
Apache 2.0. The Hugging Face GGUF repo passed 2.5 million downloads inside five days.
The headline everywhere was the same — a 27B model that fits on a laptop.
The headline is true. Almost everything people assume follows from it is not.
Stock llama.cpp will load the file and produce fluent nonsense without raising an error. Ollama and LM Studio refuse it outright. The 5.95 GB file needs roughly 12 GB of VRAM at default settings. The larger 7.21 GB build is faster on a datacenter GPU and slower on a 4090. And the widely-quoted "98.2% of full precision" is an average across fourteen benchmarks that hides a 6-point collapse in multimodal reasoning and, in one independent test, complete failure on low-resource languages.
This page is the practical layer: what ternary quantization actually buys you, how to run this model without silently corrupting your output, and how to read a compression retention number honestly. The technique outlives the model — the same reasoning applies to every low-bit release that follows.
- Understand why ternary weights are 1.75 bits in practice, not the 1.58 bits the theory promises
- Recognise the silent-garbage failure mode: stock loaders ignore the Hadamard rotation and never error
- Pick between PTQ1_0 and PQ2_0 using the bandwidth-vs-unpack crossover, not file size
- Budget real memory: 5.95 GB of weights, ~64 KiB per token of cache, and why defaults blow past 12 GB
- Read '98.2% retention' correctly — which capabilities survive compression and which quietly do not
What ternary quantization actually is
A normal model stores each weight as a 16-bit float. A ternary model stores each weight as
one of three values — -1, 0, +1 — with a single shared FP16 scale per group of
weights. Reconstruction is w = scale_g * t, where t is the trit.
Three states need log₂(3) ≈ 1.58 bits. That is the number the research literature uses, from Microsoft's BitNet b1.58 onward. It is not the number you get on disk.
Bonsai 2 27B ships at 1.75 bits per weight for its densest build. The extra 0.17 bits per weight is the part nobody explains:
- Group scales. One FP16 scale per group of 128 weights is 16 bits amortised over 128 weights — 0.125 bits per weight before anything else.
- Packing waste. Trits do not divide neatly into bytes. Dense packing recovers most but not all of the theoretical density.
- Unquantized components. Embeddings, output projections and normalisation layers stay at higher precision. On a 27B model these are a small fraction of parameters but a noticeable fraction of bytes.
So the real ratio is not 16/1.58 ≈ 10×. It is 53.8 GB → 5.95 GB, or about 9× — which is exactly what the model card reports.
The intuition most people have is that ternary multiplication is cheap: multiplying by
-1, 0 or +1 is a sign flip, a skip, or nothing. True, but that is not where the win
comes from on real hardware.
Token generation is memory-bandwidth bound, not compute bound. Every generated token requires streaming the entire weight matrix from memory. Cut the bytes by 9× and you cut the dominant cost by 9×. The arithmetic was never the bottleneck.
This matters practically: a CPU or GPU without native ternary support must dequantize back to a normal format to use its existing multiply-accumulate units, which throws away the arithmetic saving while keeping the bandwidth one. Which is why the packing format you pick changes performance in a direction that looks backwards — see below.
The trap: stock llama.cpp loads it and lies
This is the single most important thing on this page, and it appears in almost no coverage of the release.
Bonsai 2's weights have a blockwise Hadamard rotation folded into storage. A matching inverse transform must be applied to the activations at runtime. The rotation is a standard trick for low-bit quantization — it spreads outlier magnitudes across a block so the ternary grid has less to lose — but it means the stored weights are not directly usable by a loader that does not know about it.
A stock llama.cpp build does not know about it. Depending on version it will either reject the tensor types or, worse, load the file successfully and generate fluent, confident, completely wrong output. There is no error, no warning, no degraded-quality flag. The model card states this plainly: stock llama.cpp will not run these files, and standard builds produce garbage because the Hadamard activation transform is missing.
Downstream tools inherit the problem in different ways:
| Runtime | Behaviour with Bonsai 2 27B GGUF |
|---|---|
PrismML's llama.cpp fork (branch prism) | Works — the only local GGUF path today |
Stock llama.cpp | Rejects the tensor types, or loads and emits nonsense |
| Ollama | Import fails with a tensor size overflow error |
| LM Studio | Rejects ggml tensor types 142 and 143 |
| MLX (Apple) | Separate mlx-2bit build required; larger footprint |
The Ollama and LM Studio failures are the safe outcome. The stock-llama.cpp silent success is the dangerous one, because a benchmark script or an eval harness will happily record the nonsense as a result.
Run one known-answer prompt before you run anything else. If a model that scores 95+ on HumanEval cannot answer "what is 2+2" coherently, you have a loader problem, not a model problem. This applies to every exotic quantization format, not just this one.
Running it
- git clone https://github.com/PrismML-Eng/llama.cpp && cd llama.cpp, then cmake -B build -DGGML_CUDA=ON && cmake --build build -j for NVIDIA. On Apple Silicon drop the CUDA flag — Metal is on by default. On AMD use -DGGML_HIP=ON. The default branch is prism; the README explicitly warns against building from prism-v6 and against mixing this fork's libraries with a stock llama.cpp install on the same machine.
- PTQ1_0 is 5.95 GB at 1.75 bits per weight — dense trits. PQ2_0 is 7.21 GB at 2.13 bits per weight — each trit gets its own 2-bit slot. Same weights, same quality, different packing. The choice is a throughput decision, not a quality one. See the crossover table below.
- hf download prism-ml/Ternary-Bonsai-2-27B-gguf Ternary-Bonsai-2-27B-PQ2_0.gguf --local-dir . — PTQ1_0 is 5,946,648,928 bytes and PQ2_0 is 7,206,168,928 bytes. A truncated download is another way to get plausible-looking garbage. Add the mmproj file (629 MB at Q8_0, 931 MB at BF16) only if you need vision.
- ./build/bin/llama-server -m Ternary-Bonsai-2-27B-PQ2_0.gguf --alias bonsai-2-27b -ngl 99 -fa on -c 32768 --host 127.0.0.1 --port 8080. The -c flag is not optional in practice: -c 0 defaults to the full 262K window and will try to allocate roughly 16 GB of attention cache before it has generated a single token.
- The server exposes an OpenAI-shaped /v1/chat/completions endpoint, so anything that speaks chat-completions can point at http://127.0.0.1:8080/v1. Send one trivial prompt and read the answer with your own eyes before you point an eval harness or an agent loop at it.
- PQ2_0 can crash during llama.cpp's CPU repack step. Pass --no-repack, or set LLAMA_ARG_REPACK=0 in the environment.
Minimal end-to-end: build, fetch, serve, verify
# 1. Fork only — stock llama.cpp will silently produce garbage
git clone https://github.com/PrismML-Eng/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON && cmake --build build -j
# 2. Fetch (PQ2_0 = 7.21 GB, faster on big GPUs; PTQ1_0 = 5.95 GB, fits 8 GB cards)
hf download prism-ml/Ternary-Bonsai-2-27B-gguf \
Ternary-Bonsai-2-27B-PQ2_0.gguf --local-dir .
# 3. Serve — always set -c explicitly, never leave it at 0
./build/bin/llama-server -m Ternary-Bonsai-2-27B-PQ2_0.gguf \
--alias bonsai-2-27b -ngl 99 -fa on -c 32768 \
--host 127.0.0.1 --port 8080
# 4. Verify the loader BEFORE trusting any output
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"bonsai-2-27b","messages":[{"role":"user","content":"What is 2+2? Answer with one number."}]}'
# Coherent "4" => loader is correct. Fluent nonsense => wrong build, stop here.PTQ1_0 vs PQ2_0: the crossover that looks backwards
Two builds of the same weights. PTQ1_0 packs trits densely at 1.75 bits. PQ2_0 gives each trit its own 2-bit slot at 2.13 bits — 21% more disk, but the unpacking is far cheaper because slots are byte-aligned.
Measured generation throughput, tokens per second:
| GPU | PQ2_0 (7.21 GB) | PTQ1_0 (5.95 GB) | Winner |
|---|---|---|---|
| RTX 5090 | 129.9 | 120.5 | PQ2_0 |
| RTX 4090 | 81.2 | 91.1 | PTQ1_0 |
| L4 (24 GB) | 29.8 | 32.1 | PTQ1_0 |
| M5 Pro (Metal) | 28.1 | — | — |
The pattern generalises: PTQ1_0 wins on Ada-generation and lower-end GPUs, PQ2_0 wins on H100, A100 and Blackwell.
That is the opposite of the usual quantization rule of thumb, where smaller is always faster. The reason is the bandwidth-versus-unpack tradeoff from earlier. On a bandwidth-starved card, moving 21% fewer bytes dominates and the dense packing wins. On a card with enormous memory bandwidth, bandwidth stops being the constraint and the bit-shuffling cost of dense unpacking becomes the bottleneck instead — so the fatter, byte-aligned format runs faster.
Prompt processing tells the same story more loudly: on an RTX 5090, PQ2_0 processes prompts at 3,893 tok/s against PTQ1_0's 1,805 — more than double, because prefill is compute-bound, where unpacking cost hits hardest.
Rule of thumb: below 12 GB of VRAM you have no choice, take PTQ1_0. Above it, take PQ2_0 unless you are on a 4090 or an L4, where PTQ1_0 measurably wins.
File size is not memory requirement
Three different "size" numbers are circulating for this model — 5.9 GB, 7.2 GB and 8.6 GB — and all three are defensible, because they measure different things. Here is the actual budget:
- Weights: 5.95 GB (PTQ1_0) or 7.21 GB (PQ2_0).
- Vision projector: +629 MB (Q8_0) or +931 MB (BF16), if you need images.
- Attention cache: roughly 64 KiB per token. That is ~0.5 GB at 8K context, ~2 GB at 32K, and ~16 GB at the full 262K window.
- Parallel slots: llama.cpp allocates cache per slot. Defaults allocate several.
One independent test measured just over 12 GB of VRAM at default settings, dropping to about 8 GB when the server was restricted to a single parallel slot. The disk footprint they measured — around 7 GB — corresponds to the PQ2_0 build, which is where part of the number confusion comes from.
Practical context headroom by card, using PQ2_0 unless noted:
| VRAM | Build | Usable context |
|---|---|---|
| 8 GB | PTQ1_0 | ~16K tokens (~6K with vision) |
| 12 GB | PQ2_0 | ~56K tokens |
| 16 GB | PQ2_0 | ~118K tokens |
| 24 GB | PQ2_0 | ~240K tokens |
| 32 GB+ | PQ2_0 | Full 262K window |
The "runs on a laptop" claim survives all of this — but "runs on a laptop with an 8 GB GPU and a 16K context" is the honest version, and that is a materially different product.
What "98.2% retention" hides
PrismML reports an aggregate of 84.78 against the full-precision base model's 86.32 across fourteen benchmarks — the 98.2% figure. Averages are where compression damage goes to hide. The per-benchmark spread:
| Benchmark | Bonsai 2 27B | FP16 base | Delta |
|---|---|---|---|
| AIME26 | 95.83 | 94.58 | +1.25 |
| MATH-500 | 98.80 | 99.80 | −1.00 |
| LiveCodeBench | 90.07 | 90.05 | +0.02 |
| HumanEval+ | 95.12 | 93.29 | +1.83 |
| MMLU-Redux | 89.09 | 91.46 | −2.37 |
| MMMU-Pro | 75.49 | 81.73 | −6.24 |
Three things to take from this table.
First, the damage is not uniform — it is structural. Maths and code are essentially untouched. Knowledge recall drops (composite knowledge-and-reasoning lands near 79.9 versus 85.6 for full precision). Multimodal reasoning takes the real hit: MMMU-Pro falls 6.24 points, and OCR Bench v2 drops from 60.99 to 56.88. Compression costs you breadth of stored fact, not procedural skill — which is exactly what you would expect when you crush the precision available to store long-tail associations.
Second, be suspicious of the positives. The compressed model "beating" its own full-precision base by 1.83 points on HumanEval+ and 1.25 on AIME26 is not evidence that compression helps. It is evidence that these benchmarks have run-to-run variance of a couple of points. Which should also make you discount the small negatives.
Third, the vendor's own numbers do not fully agree. The model card's benchmark table aggregates to 84.78, while PrismML's launch post reports an aggregate of 83.9. Both are described as the same 98.2% retention. The discrepancy is small and unexplained; we report it rather than pick one.
One independent hands-on test found behaviour no benchmark row predicts: asked to translate into low-resource languages including Tamil, the model entered a repetitive loop and never completed the task — worse than the previous Bonsai generation. The same test found it identified a real timezone bug and then talked itself out of the correct conclusion, and failed to produce a working reset button in a small HTML task across three correction rounds.
This is the characteristic signature of aggressive quantization: aggregate scores hold, while the long tail — rare languages, long-horizon self-correction — degrades in ways that only show up in use. Community reports also describe recall and coherence degradation past roughly 200K tokens of the advertised 262K window.
The mainline story, and why it matters for the next release
There is a parallel effort to get ternary Bonsai into upstream llama.cpp, and its current state is a useful lesson in how low-bit formats fragment.
PrismML proposed group-128 ternary packing for the smaller Bonsai models. The llama.cpp
maintainer pushed back in favour of group size 64, on the grounds that it costs under 6%
extra memory while supporting more tensor shapes and giving better quality. Q2_0 with
group-64 became the official format and has been merged into multiple backends including
CUDA and Vulkan. PrismML kept their group-128 variants in the fork.
So "does llama.cpp support ternary models" has two correct and opposite answers:
- Yes —
Q2_0_g64builds run on mainline across backends, and the fork's own README points at them as the portable path. - No, not for this model — as of verification, the published
Ternary-Bonsai-2-27B-ggufrepo containsF16,PQ2_0,PTQ1_0and the two mmproj files. There is noQ2_0_g64build of the 27B. The fork is the only path.
Two older ternary formats, TQ1_0 and TQ2_0, already exist in ggml but use group size
256, which is incompatible with how these models are natively packed. TQ2_0 is also the
tightest faithful ternary layout in ggml at roughly 2.06 bits per weight — and in current
mainline it implements a CPU-only dot product with no CUDA, Metal or Vulkan path. Seeing
"ternary supported" in a changelog tells you very little about whether your GPU will use it.
For the next low-bit model you evaluate, the question to ask is not is it quantized but which group size, which packing, which backends have kernels, and is there a portable build — because that determines whether you are running on mainline or maintaining someone else's fork.
When this is the right tool
Worth it when:
- You need a capable maths or coding model on hardware that cannot hold a 27B at higher precision. That is where retention is near-perfect and the value is highest.
- You are bandwidth-constrained — a laptop GPU, an edge box, a shared L4 — and 9× fewer bytes per token is the difference between usable and not.
- Energy matters. PrismML reports 0.714 mWh per token on an RTX 4090, which they put at 40% more efficient than a full-precision 8B model.
- The Apache 2.0 licence and fully local execution are requirements, not preferences.
Not worth it when:
- The work is knowledge-heavy or multimodal. That is precisely where the compression bill lands.
- You need non-English long-tail language coverage. The independent evidence here is bad and benchmarks did not predict it.
- You cannot maintain a forked inference runtime. Ollama and LM Studio do not load it today, and a stock build fails silently rather than loudly.
- You were going to run it past ~200K tokens of context on the strength of the 262K spec.
Check yourself
Check yourself
0/5Sources & further reading
- PrismML — Bonsai 2 27B launch post (ternary scheme, bits per weight, aggregate benchmark, hardware throughput and energy figures): https://prismml.com/news/bonsai-2-27b
- Hugging Face —
prism-ml/Ternary-Bonsai-2-27B-ggufmodel card and file listing (PTQ1_0 / PQ2_0 / F16 / mmproj sizes, Hadamard rotation note, run commands, 262K context, Apache 2.0): https://huggingface.co/prism-ml/Ternary-Bonsai-2-27B-gguf - Hugging Face — the full Bonsai 2 collection, including the MLX 2-bit build and the WebGPU demo space: https://huggingface.co/prism-ml
PrismML-Eng/llama.cpp— the required fork, branchprism, with the PQ2_0 / Q2_0_g64 guidance and the warning against mixing builds: https://github.com/PrismML-Eng/llama.cppggml-org/llama.cppdiscussion #22019 — group-128 versus the group-64Q2_0standard, TQ1_0/TQ2_0 group-256 incompatibility, backend merge status: https://github.com/ggml-org/llama.cpp/discussions/22019- Microsoft — BitNet b1.58 2B4T technical report, the reference point for ternary LLM training: https://arxiv.org/pdf/2504.12285
- Microsoft —
bitnet.cpp: efficient edge inference for ternary LLMs (why the win is bandwidth, and why hardware without native ternary support must dequantize): https://arxiv.org/pdf/2502.11880 - MindStudio — independent hands-on testing of the 98.2% claim, including the multilingual failure and measured VRAM: https://www.mindstudio.ai/blog/bonsai-2-27b-real-world-test
- Atomic Chat — local install guide with the per-GPU throughput table, context headroom table and the
--no-repackworkaround: https://atomic.chat/blog/guides/how-to-run-bonsai-2-locally - DataCamp — per-benchmark comparison against the full-precision base and the long-context degradation reports: https://www.datacamp.com/blog/bonsai-2-27b
- SiliconANGLE — launch coverage and positioning: https://siliconangle.com/2026/09/18/prismml-launches-bonsai-2-27b-a-high-intelligence-ai-model-so-small-it-fits-on-consumer-hardware/
Next
- Colibrì: run a 744B MoE from your SSD — the other way to fit an oversized model on small hardware: stream it instead of compressing it
- Run models locally with Ollama — the supported-runtime path, for everything that is not a fork-only format
- A private local AI stack — where a fast compressed model sits next to the rest of your tooling