~/satyajit

Bonsai 2 2B, 4B and 9B: where the bytes go when the model is small

mdjsonmcp

2026-10-16 · 20 min · quantization · inference · on-device · multimodal · benchmarks

A 1:43 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Ferrule! Bonsai 2 now comes in two, four and nine billion parameters. Here's where the bytes go when a ternary model gets small. Every weight matrix is still minus one, zero or plus one. But at two billion, the vocabulary table alone is over a quarter of the model. The transformer blocks are about seventy percent of the file, packed five trits to a byte. The vocabulary table is a quarter of it. It doubles as the output head, so every token reads it in full. Norms and the state path stay in full precision, and the tokenizer rides in the header. Neither shrinks with the model. That's why the small files land above the format's one point seven five bits per weight. The nine billion model ships twice. One pack gives each trit a two-bit slot: two point four two gigabytes, the repost's number. The other packs five trits to a byte: two gigabytes, the number in the launch post. Same weights, different layout. Decoding one token at a time is a memory problem. Each token streams every weight matrix once. So speed is just how fast the chip reads memory. Tokens per second times bytes gives the bandwidth a phone really used. The whitepaper measured an iPhone seventeen. The launch post quotes an iPhone eighteen, faster than the bandwidth math expects, with no method behind it. So at small sizes, the vocabulary and the packing decide the file size, not the trits. To recap: a big vocabulary, two packings of the same weights, and speed set by bytes per token. Every source is in the full article. I'm Ferrule. Bye!

Why read this

Hightop 30%

Reads all nine small Bonsai 2 GGUFs byte by byte, reconciles 2.0 vs 2.42 GB, recounts the benchmark table, and runs the 2B and 4B on a CPU.

  • Checked against the source
  • Runs on a phone
  • Widely used

Quantization & compressionApache-2.0Practitioner model

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
3 of 3: Reproduces the headline result, or shows from primary files it is wrong
Can I run it?
3 of 3: Open, permissive, runs on reader hardware with instructions
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 72 of 100, ranked 93 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

A month after Bonsai 2 27B, PrismML has shipped the rest of the family: Ternary Bonsai 2 2B, 4B and 9B, built on Qwen3.5-2B, 4B and 9B, Apache 2.0, with headline sizes of 0.43 GB, 0.95 GB and 2.0 GB. The format is the 27B's: every weight matrix stored as {−1, 0, +1} times an FP16 scale per 128 weights, in a basis rotated by a fixed Walsh–Hadamard transform that the runtime has to undo on the activations. I covered how that works, and why a stock runtime produces garbage without it, in the 27B write-up; the gen-1 lineage is in Bonsai 27B, and the from-scratch version of the idea is in Ternary15M. This piece is about what is different when the model is small, and about four claims that went around on release day:

I pulled the file listing of every repo in the Bonsai 2 Small collection, parsed the GGUF header of all nine language-model packs (the 2B locally, the 4B and 9B through HTTP range requests, never downloading their weights), read the whitepaper and the docs, and built PrismML's llama.cpp fork to run the 2B and 4B. Labels below: measured is something I computed from a file or a run, reported is PrismML's or Atomic Chat's figure, reasoned is my arithmetic on the other two.

prism-ml/Ternary-Bonsai-2-9B-gguf@5b2abb0 · snapshot 2026-10-16
repo size
12.77 GB
quantizedQwen/Qwen3.5-9B
task
image-text-to-text
library
llama.cpp
license
apache-2.0
gguf files
7
largest file
2.68 GB
files
13
downloads
22.3K
likes
45
ternary2-bitggufllama-cppcudametalon-devicehybrid-attention

The repo also holds a 2.42 GB PQ2_0 pack, a 2.56 GB Q2_0 pack, an MTP variant, a DFlash2 drafter and two vision projectors. The 2.0 GB figure is the PTQ1_0 file alone.

repo last modified 2026-10-15

The sizes are exact, and they are decimal gigabytes

Start with the easy part. The three PTQ1_0 files on the Hub are 429,183,456, 949,549,920 and 1,997,486,976 bytes (measured, from the Hub tree API; the 2B and 4B files I downloaded match the repos' SHA256SUMS). In decimal gigabytes that is 0.429, 0.950 and 1.997: the headline 0.43 / 0.95 / 2.0 GB, rounded honestly. In GiB, the unit your OS and llama-bench print, the same files are 0.400, 0.884 and 1.860 GiB. llama-bench reports the 2B as 398.81 MiB because it counts only the tensor data, without the 11 MB header (measured).

What the number covers is stated plainly in the model card and the docs: the language model only. The vision tower ships as a separate mmproj file, 0.36 GB (Q8_0) for the 2B and 4B and 0.62 GB for the 9B (measured: 364,664,352, 366,894,624 and 624,229,664 bytes). It is loaded only when an image arrives. Text-only, the headline is the whole download; with images, the 2B is 0.79 GB and the 9B is 2.62 GB (reasoned).

What is in the file

The tensor tables are where the small models stop looking like the 27B. Here is the 2B, measured from its GGUF header, every tensor shape times the ggml type size (PTQ1_0 is 28 bytes per 128 weights, 1.75 bits; PQ2_0 34 bytes per 128, 2.125 bits; Q2_0 18 bytes per 64, 2.25 bits; F32 4 bytes). The slices sum to the exact file size for all nine packs.

2B, PTQ1_0 packParametersBytesShare of file
Transformer block matrices (ternary)1,371,537,408300,023,80869.9%
Token embedding, tied to the LM head (ternary)508,559,360111,247,36025.9%
F32: norms and the linear-attention state path1,728,3206,913,2801.6%
GGUF header: tokenizer, chat template, Hadamard signs10,999,0082.6%
Total1,881,825,088429,183,456

Three things fall out of it.

The vocabulary is a quarter of the model. Qwen3.5 has a 248,320-token vocabulary at every size. At hidden size 2048 that is a 508.6M-parameter table, 27.0% of the 2B's language model; at 4B (hidden 2560, also tied) it is 15.1%; at 9B, which unties the embedding and the head, the two tables together are 22.7% (measured). In the 27B the same pair is under a tenth. PrismML ternarizes the embedding and the head too, so this does not cost bits per weight directly. It costs in the next two ways.

The fixed costs do not shrink. The header is 11.0 MB at every size, nearly all of it the tokenizer, and the F32 tensors (RMSNorm weights, ssm_alpha, ssm_beta, ssm_conv1d, ssm_a, ssm_dt, the Gated DeltaNet layers' state path that PrismML keeps out of ternary) are 6.9 MB at 2B. Together they are 4.2% of the 2B file and 2.0% of the 9B's (measured). That is why the effective rate is 1.82, 1.81 and 1.78 bits per weight for the 2B, 4B and 9B PTQ1_0 files, not the format's 1.75 (measured: file bits over the GGUF's own parameter count; the model cards say ~1.83 / ~1.81 / ~1.78, the 2B's from dividing by a rounded 1.88B).

The decode cost is not the file size. A token reads every matrix it multiplies by. When the embedding is tied to the LM head, as in the 2B and 4B, the whole table is read every token as the head. When it is untied, as in the 9B, the embedding is only gathered one row at a time, so a 9B decode step streams about 1.76 GB in PTQ1_0 and 2.14 GB in PQ2_0, not 2.0 and 2.42 (measured from the tensor tables). That matters for the phone numbers below.

The widget carries all nine packs. With the 9B's vision tower added, the ratio to BF16 drops from 8.97x to 6.83x (reasoned): headline and baseline are both language-model only.

Reconciling 2.0 GB, 2.42 GB, "7x" and "more than 9x"

Atomic Chat's numbers are not wrong. They are a different file. The 9B ships in three packings of the same ternary weights:

9B packBytes on the HubBits per weightBF16 (17.91 GB) / this
PTQ1_0, five trits per byte1,997,486,9761.788.97x
PQ2_0, one trit per 2-bit slot2,416,851,8402.167.41x
Q2_0, upstream type, group 642,556,640,1282.287.00x

The model card's banner reads "~2.42 GB language model (PQ2_0), down from ~17.9 GB at FP16", and PQ2_0 is what the demo repo's setup.sh downloads by default, because it is cheaper to unpack. Atomic Chat quoted that banner: 17.9 / 2.42 is 7.4x (reasoned), so "7x" is a fair rounding of the bigger pack. PrismML's "2.0 GB" is the smaller pack. Same weights, two layouts.

The 17.9 GB baseline checks out: 8,953,803,264 language-model parameters in the GGUF times 2 bytes is 17.91 GB (measured), which is the whitepaper's figure. It is not the size of the Qwen3.5-9B repo, which is 19.31 GB because it carries the vision tower and more (measured, Hub API).

"More than 9x smaller", in the launch post, is the one number that does not hold. Against the whitepaper's own BF16 baselines (3.76, 8.41, 17.91 GB) the PTQ1_0 ratios are 8.77x, 8.86x and 8.97x (reasoned). The whitepaper itself says "8.8–9.0× smaller" in Table 1, and its cover says "~9×". Both of those are right. The launch post rounded up past them.

The benchmark table, and "keeps up or outperforms"

PrismML published one table, the same numbers in the launch post and the whitepaper (Table 6): 17 benchmarks at 2B, 18 at 4B (adding τ²-Bench), 19 at 9B (adding AIME26), each against its own Qwen3.5 BF16 baseline, run with EvalScope on vLLM, thinking on, temperature 1.0. All reported:

Benchmark2B Qwen3.52B Bonsai4B Qwen3.54B Bonsai9B Qwen3.59B Bonsai
MMLU-Redux79.3967.6088.6381.9891.1186.95
GPQA Diamond58.4839.6078.0863.8482.5369.90
MuSR62.8359.1362.4362.5766.6766.53
MATH-50085.4082.4099.0097.6099.0098.60
GSM8K65.9680.7494.5494.5495.4595.07
AIME2686.6782.50
BFCL v362.0660.5371.5270.1673.8171.81
BFCL v4 Mobile66.1361.8578.4276.4982.0278.02
τ²-Bench81.6561.8785.2573.02
HumanEval+45.1250.0083.5485.3787.8089.02
MBPP+44.1852.6570.1169.3173.0271.96
IFEval75.7985.7786.6988.9189.6590.02
IFBench34.3348.0050.3356.3357.3357.00
MMMU-Pro59.2550.2368.0964.8672.4368.73
A-OKVQA81.0578.6085.9482.5388.3884.02
CharXiv72.0669.8678.0672.4680.1076.20
OmniDocBench71.7855.6486.5171.2288.7373.80
Real World QA70.7063.9078.3069.8080.0075.20
OCRBench v252.6044.2053.6850.6058.3155.04
Average63.9561.8177.5373.3680.9677.02

Source: whitepaper Table 6; the launch post's Figure I has the same numbers.

The averages give 96.7%, 94.6% and 95.1% retention (reasoned). Counting benchmark by benchmark, Bonsai is ahead of its baseline on 5 of 17 at 2B, 4 of 18 plus one tie at 4B, and 2 of 19 at 9B: HumanEval+ and IFEval (reasoned). At 9B six more are within two points (MuSR, MATH-500, GSM8K, IFBench, BFCL v3, MBPP+), and eleven are behind by more. The biggest gaps are the same three at every size: GPQA Diamond (−18.9, −14.2, −12.6 points), OmniDocBench (−16.1, −15.3, −14.9), and τ²-Bench at 4B and 9B (−19.8, −12.2). So "keeps up or outperforms the original model on benchmarks" is true of instruction following and coding, roughly true of math, and not true of knowledge, document parsing or multi-turn tool use. PrismML's own text is more careful than the repost: it calls the τ²-Bench drop "the largest single loss at 4B" and says OmniDocBench "does not close with a larger model."

Two things about the above-100% rows are worth a closer look.

The 2B baseline is unusually weak where Bonsai wins. Qwen3.5-2B scores 65.96 on GSM8K but 85.40 on the harder MATH-500, and 34.33 on IFBench. Bonsai 2B scores 80.74 and 48.00. A full-precision model doing worse on grade-school math than on competition math is the signature of a run that failed to finish, not of a model that cannot add: with thinking on, a small model that loops or runs out of output budget scores zero on that item. That is a reading, not a measurement (the whitepaper does not report truncation rates), but it fits PrismML's stated focus. The release post says the work went into "avoiding repetitive loops, following instructions … and controlling verbosity." Those are post-training targets. A ternary model that beats its own BF16 parent got something besides compression, and the comparison that would separate the two, the same post-training at full precision, is not published.

The aggregate has no single definition across sizes. The 2B average is over 17 benchmarks, the 9B's over 19, so the three retention numbers are not on one scale. The whitepaper says so itself: "averages compare within a size only."

The more useful table for choosing a model is the whitepaper's Table 8, which compares each ternary model against a smaller model that needs more memory. The 9B (2 GB) against Qwen3.5-4B in Q3_K_M (2.3 GB) leads on five of six category averages, 77.83 to 73.99, and on AIME26 82.50 to 54.17 (reported). The 4-bit 4B wins on vision. At equal memory, a bigger ternary model beats a smaller conventional quant, which is the 27B's result again at a tenth of the scale.

"Roughly 40% fewer tokens": not published anywhere I could find

The launch post and the thread say the 9B "uses roughly 40% fewer tokens on standard instruction-following and everyday math tasks." I searched the whitepaper, the three model cards, the docs pages for each model, and every Markdown file in the demo repo. None of them contains a token count, a token-length comparison, or the figure 40%. There is no named benchmark, no baseline (Qwen3.5-9B with thinking on? off?), and no budget. The whitepaper's own methodology runs both models with thinking on and "the same output budgets", and reports accuracy only.

The direction is plausible: the model cards warn that "even simple prompts can use 3,000+ thinking tokens", and verbosity was a training target. But "40% fewer" has nothing published behind it, and it is a claim about post-training, not about ternary weights.

The phone numbers come from a different phone

Bar chart of decode tokens per second. RTX 4090: Bonsai 2 27B 97, 9B 249, 4B 369, 2B 604, 16-bit 9B 56. iPhone 18 Pro Max: 9B 34, 4B 51, 2B 120, 4-bit 9B 18, 27B and 16-bit 9B marked as not fitting. M5 Pro-Max: 27B 47, 9B 179, 4B 245, 2B 310, 16-bit 9B 31.
Throughput as published in the launch post. The RTX 4090 bars match the whitepaper's Table 3; the iPhone 18 Pro Max and 'M5 Pro-Max' bars appear in no table and no methodology (PrismML launch post, Figure II).

The launch post's figure has three device groups. The RTX 4090 bars, 604 / 369 / 249 tok/s for 2B / 4B / 9B, are the whitepaper's Table 3 numbers rounded (603.6, 368.6, 248.6, PTQ1_0, tg128, batch 1). The other two groups are not in the whitepaper at all.

Three headline statistics from the whitepaper cover: about 9 times smaller; 90 tokens per second for the 2B on iPhone 17 Pro Max; 92 tokens per second for the 9B on MacBook Pro M5 Pro.
The whitepaper's own headline: 90 tok/s on an iPhone 17 Pro Max, not 120 on an iPhone 18 Pro Max (whitepaper, cover page).

The whitepaper measured an iPhone 17 Pro Max (llama.cpp Metal, PQ2_0, tg128, batch 1): 89.9, 39.8 and 22.3 tok/s for the 2B, 4B and 9B, and its cover leads with "90 tok/s". The launch post and the X thread instead quote an iPhone 18 Pro Max at 120, 51 and 34, and an "M5 Pro-Max" at 310 / 245 / 179, where the whitepaper's MacBook Pro M5 Pro tops out at 276.3 / 145.2 / 91.7 with MLX Swift. No document gives the iPhone 18 runtime, pack, context, prompt or whether a drafter was on. "Up to" in the post is doing work: the whitepaper's Table 4 shows the same 9B at 89 tok/s on the iPhone 17 with speculative decoding on copy-heavy agent prompts, and "plain" (no speedup) on everyday prompts.

You can still sanity-check the numbers, because batch-1 decode is a bandwidth problem. Each token streams the weights it multiplies once, so tok/s times bytes per token is the bandwidth the run actually achieved. For the whitepaper's iPhone 17 rows, with the per-token bytes measured above (PQ2_0), that is 45.5 GB/s at 2B, 45.2 at 4B and 47.6 at 9B (reasoned): flat across sizes, which is what a phone that is streaming weights as fast as it can looks like. For the iPhone 18 figures it is 60.8, 57.9 and 72.6 GB/s. The 9B's is a fifth higher than the 2B's on the same phone, which a plain bandwidth-bound decode cannot do; something else (a drafter, a different pack, a shorter context) is likely in the 34. That is an inference, and I could not confirm it.

The same arithmetic on the RTX 4090's PTQ1_0 rows gives 252 / 346 / 438 GB/s (reasoned), well under the card's ~1 TB/s: at these sizes a desktop GPU is limited by kernel overheads, not by memory, which is also why the whitepaper's speedups over BF16 are largest on the slowest devices (5.2x on the phone for the 2B, 1.5x on a B200).

Running it on a CPU

prism-ml/Ternary-Bonsai-2-2B-gguf@683f7be · snapshot 2026-10-16
repo size
2.53 GB
quantizedQwen/Qwen3.5-2B
task
image-text-to-text
library
llama.cpp
license
apache-2.0
gguf files
5
largest file
671.4 MB
files
10
downloads
2.8K
likes
15
ternary2-bitggufllama-cppcudametalon-devicehybrid-attention

Three language-model packs (PTQ1_0 0.43 GB, PQ2_0 0.52 GB, Q2_0 0.55 GB) and two vision projectors. The 2B ships no MTP head or DFlash drafter.

repo last modified 2026-10-14

PrismML's demo repo pins prism-b10777-e3de715 of its fork; I built the head of the prism branch (5ae2e16, 15 October) with GGML_NATIVE=ON, on a shared 4-core Intel Xeon cloud VM with AVX-512, CPU only. Building llama-bench, llama-completion and llama-simple with -j2 took about 8 minutes.

git clone --depth 1 -b prism https://github.com/PrismML-Eng/llama.cpp && cd llama.cpp
cmake -B build -DGGML_NATIVE=ON -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build -j2 --target llama-bench llama-completion llama-simple
./build/bin/llama-bench -m Ternary-Bonsai-2-2B-PTQ1_0.gguf -t 2 -p 512 -n 128 -r 3

All measured, -t 2, every command under nice -n 19 on a machine five other jobs were sharing (load average 7-9 on 4 cores during the runs), so read the error bars:

Model, packFilePrompt processingGenerationPeak RSS
2B PTQ1_00.43 GB5.54 ± 0.38 tok/s (pp512)2.20 ± 0.79 tok/s (tg128)
2B PQ2_00.52 GB18.73 ± 0.84 tok/s (pp512)3.81 ± 1.67 tok/s (tg128)
2B Q2_00.55 GB2.41 ± 0.28 tok/s (pp512)1.61 ± 0.17 tok/s (tg128)
2B, all three in one llama-bench1,148.9 MB
4B PTQ1_00.95 GB1.95 ± 0.29 tok/s (pp128)0.80 ± 0.14 tok/s (tg64)1,072.8 MB

None of this is comparable to PrismML's charts. The point is the ordering, which the source explains. In the fork's ggml-cpu/arch/x86/quants.c, PQ2_0 has an AVX2 dot product against Q8_K activations, PTQ1_0 has only an SSSE3 path (unpacking five trits from a byte is arithmetic, not a shift and a mask), and Q2_0 has no x86 kernel at all and falls back to the generic C loop. So on this CPU the 2-bit-slot pack is 3.4x faster at prompt processing than the denser one (measured), the opposite of the RTX 4090, where PTQ1_0 is the fastest pack at every size (reported). Decode here runs at under 1 GB/s of effective weight traffic (reasoned: 2.20 tok/s times 0.418 GB), so it is compute-bound, not bandwidth-bound; the widget's bandwidth arithmetic does not apply to it. PrismML targets Metal and CUDA; the CPU backend works, but it is not where the tok/s figures come from.

Generation swung by more than 2x between identical runs (2.23 and 5.70 tok/s for the same 64-token decode, measured): the machine, not the model. Whether CPU weight repacking helps decode was lost in that noise.

What loads in RAM

A 0.52 GB file took about 1.1 GB of resident memory to run (measured, peak RSS 1,086-1,161 MB across the PQ2_0 runs). The fork's verbose log accounts for it:

Run with -nr (no repack) and -ub 64 -b 64 and the same model peaks at 633.4 MB (measured). On a phone the Metal path has no CPU repack, but the logits buffer is the same arithmetic, so the micro-batch is worth setting deliberately on a memory-tight device.

What it said

Four prompts to the 2B PQ2_0 pack, with the model card's sampling settings, one sample each. This is a smoke test, not an evaluation.

Instruction following, thinking off, "List exactly three uses for a paperclip. Use a numbered list, at most six words per item, and no other text":

1. Holds things together
2. Measures distance
3. Cuts metal

The format is exactly right: three items, numbered, nothing else, 16 tokens. The content is half right. A paperclip does not cut metal.

A tool call, thinking off, with one set_alarm(time, label) function in the system prompt in the chat template's own format, and "Wake me up at 6:45 tomorrow, call it gym":

<tool_call>
<function=set_alarm>
<parameter=time>
06:45
</parameter>
<parameter=label>
Gym
</parameter>
</function>
</tool_call>

Well-formed, correct arguments, zero-padded to the HH:MM the schema asked for, 40 tokens.

A word problem, "muffins at 3 dollars, coffee at 2, 4 muffins and some coffees for 20 dollars, how many coffees": correct (4) with thinking off in 193 tokens, and correct with thinking on in 178, because with thinking on the model closed its <think> block immediately and wrote no reasoning at all. One prompt proves nothing about "40% fewer tokens", but it is the behaviour that claim describes.

What I could not check

The takeaway

The 27B made the case that a ternary format with a rotation and a purpose-built kernel keeps most of a model's ability at a ninth of the bytes. The small models make a different case: at 2B, the format is no longer the whole story. A 248,320-token vocabulary is a quarter of the weights and, on a CPU, most of the compute buffer; an 11 MB tokenizer header is 2.6% of the file; and the choice between PTQ1_0 and PQ2_0 moves the size by 20% and the speed by a factor that depends on which kernels your hardware has. The headline sizes are honest. The best number in the release is the one in the whitepaper's Table 8: a 2 GB ternary 9B beats a 2.3 GB conventional 4B on five of six categories. If memory is the constraint, a bigger ternary model is the better buy, and the 27B is the same argument at laptop scale.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Bonsai 2 2B, 4B and 9B: where the bytes go when the model is small", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026bonsai2small,
  author = {Satyajit Ghana},
  title  = {Bonsai 2 2B, 4B and 9B: where the bytes go when the model is small},
  url    = {https://ai.thesatyajit.com/articles/bonsai-2-small},
  year   = {2026}
}
share