Skip to content
AI Infrastructure

Four-Bit Quantisation Made This Laptop 58% Faster. The Eight-Bit Model Refused the Question.

The same 1.2B weights at two precisions, measured in interleaved pairs on a fanless M1. Generation gets 58% faster and prefill gets nothing, which is what a bandwidth-bound machine should do. Then the higher-precision model declined to answer 17 of 30 prompts and the lower-precision one never did.

Published · How this was researched

Back to Global Tech Search

Market Horizon

Point-in-time measurement, Aug 29 2026 — one machine, 30 paired generations, one model family

Target Sector

Local Inference, Edge AI Hardware, Measurement Methodology

Generation, Q4_K_M vs Q8_0

1.58x

30 of 30 pairs favour the smaller model

Prefill, same pairs

0.99x

Quantisation bought nothing here

Resident memory

−31%

1.14 GB against 1.65 GB

"Use a quantised model" is the standard advice for running an LLM on hardware like mine, and it is almost always given without a number. This site has already measured what this particular machine does at one precision: where it becomes bandwidth-bound, and how much it loses between burst and plateau with no thermal pressure reported at all. Neither says what the quantisation choice itself is worth.

So I measured it. The answer to the question I asked is clean and slightly boring. The answer to a question I had not asked is the reason this post is longer than a table.

Two arms, one model

llama3.2:1b and llama3.2:1b-instruct-q4_K_M are the same 1.2B weights at two precisions, Q8_0 and Q4_K_M. That claim is the entire foundation of the comparison, so the harness re-checks it on every run rather than trusting an inspection I did once: architecture, parameter count, context length, embedding length, block count, and a SHA-1 of the chat template must all match across arms, and the run aborts if they do not.

The template check is the one that earns its place. Two models with different templates are being asked different questions however identical their weights, and nothing else in the output would reveal it.

Why the runs interleave

The obvious design is to measure one model for ten minutes and then the other. On this machine that design is wrong, and this site has already published why: throughput falls 8.2% from burst to plateau on a fanless M1, with macOS reporting thermal pressure in 0 of 424 samples. A sequential run hands the second model a hotter machine and books the difference as quantisation. Whichever arm ran second would lose.

So the pair is the unit, not the block. Within each repetition both arms answer a byte-identical prompt back to back, at the same thermal moment, and the within-pair order alternates by repetition index so that went first is not itself confounded with a model. Five pairs per cell, three prompt sizes, cold and warm cache states kept as separate conditions — 30 pairs, 60 generations.

The result, and where it lives

The Quantisation Win Is Entirely in Generation

Per-pair ratio Q4_K_M ÷ Q8_0, averaged within each cell — 5 pairs per bar, cold cache, M1 MacBook Air, Aug 29 2026

Generation runs 1.58x faster at four bits, averaged over all 30 pairs, and the sign test is unanimous: 30 of 30 pairs favour the smaller model. In absolute terms, across all 30 reps per arm, the median is 28.1 tokens per second at eight bits against 44.0 at four.

Prefill runs at 0.99x — no difference, and the sign test splits 6 of 15. Reading the prompt costs the same at either precision.

That split is not a surprise so much as a confirmation, and it is the reason the two phases are never averaged into a single "tokens per second" here. Generation is memory-bound: every token drags the whole weight set across the bus, so halving the bytes per weight is worth something close to proportional. Prefill is compute-bound: it processes the prompt in parallel and saturates arithmetic rather than bandwidth, so there is no bus pressure to relieve. An independent measurement on this site already found this machine bandwidth-bound at generation. This run reproduces that finding from a completely different direction, which is worth more than either result alone.

The memory side is the half that throughput hides: ollama holds 1.14 GB resident for Q4_K_M against 1.65 GB for Q8_0, a 31% reduction. On an 8 GB machine that is frequently the difference between a model running and not running at all.

The check that could have killed it

An interleaved design is a claim that interleaving worked. That claim is testable, so the analyser tests it every time: it recomputes the generation speedup separately for the pairs where Q8_0 ran first and the pairs where Q4_K_M ran first.

Q8_0 ran first     n=18   generation 1.59x
Q4_K_M ran first   n=12   generation 1.58x
gap 0.01x — order did not move the result; the pairing held.

Had those disagreed, the analyser would have printed a refusal to publish the headline rather than leaving me to notice. They agree to 0.01x, so the 1.58x is a quantisation effect and not a thermal one.

I would rather report the check that passed than let a reader assume I ran it.

The bug that discarded the entire dataset

The first version of the analyser reported 0 clean (30 excluded for a mid-pair model load) and a null speedup. A run that excludes every one of its own observations looks exactly like a run that failed, which is the only reason I caught it.

The filter tested load_duration_ns > 0, on the assumption that a resident model reports zero. It does not: ollama reports a non-zero load duration on every request as readiness bookkeeping. All 60 reps reported between 177 ms and 520 ms, so the filter excluded all of them.

The fix was not to pick a bigger number. It was to measure where the boundary actually is — force an unload with keep_alive: 0, then time the next generation:

llama3.2:1b                  cold load 1552 ms
llama3.2:1b-instruct-q4_K_M  cold load 1291 ms
paired run, all 60 reps      177–520 ms

Two populations that do not touch. The threshold now sits at 900 ms, in the empty band between them, and the comment in the analyser says where those numbers came from so the next person does not have to guess whether 900 was principled or convenient.

The thing I was not looking for

At temperature 0 with a fixed seed both models are deterministic, so I sent each pair's byte-identical prompt to both arms and diffed the replies. This measures how often the two disagree. It is not a quality measurement, and the distinction matters more than usual here.

All 30 prompts produced different text. Not most — all of them.

Then the reply heads showed why, and it was not what I expected:

The Higher-Precision Model Declined the Task

Replies opening by refusing to summarise, of 10 paired prompts per size — temperature 0, seed 42

On the 16-clause prompt, Q8_0 answered "There is no passage provided. The text you've shared appears to be a random string of characters" in all ten pairs. Q4_K_M summarised the passage in all ten. At 64 clauses the split was 7 and 0. Across the run: 17 refusals from the eight-bit model, zero from the four-bit one.

The reason for the care is that the tempting sentence — four-bit was more helpful — is the same class of error as the cache-state bug that once inverted a result on this site: a real number answering a question nobody asked. The divergence rate is real. The interpretation that four bits is better is not in the data.

Neither arm hit the 200-token cap, incidentally. Both stopped naturally between 34 and 83 tokens, so the rate comparison is not an artefact of one model being cut off mid-sentence.

What this is worth, and to whom

If you are running a small model on a memory-constrained machine, the four-bit weights are worth taking: roughly 1.6x the generation speed and a third less resident memory, for no measurable prefill penalty. If your workload is prompt-heavy and generation-light — classification, extraction, anything that reads a lot and writes a little — the headline number does not apply to you, because the phase it lives in is not the phase you are paying for.

And whatever the benchmark says, run your own prompts through both. Two builds of one model, at temperature 0, disagreed on every single prompt I sent them, and on more than half of them they disagreed about whether there was a question to answer.

Reproducing this

node scripts/bench/quantisation.mjs --reps 5 \
  --out data/quantisation/run-$(date -u +%Y%m%dT%H%M%S).json
node scripts/bench/analyse-quantisation.mjs data/quantisation/run-*.json

Needs pmset (macOS), a local ollama, and both model tags pulled. No API keys, no accounts, nothing paid. A full run takes about twelve minutes and the machine should be idle and cool before it starts — two earlier benchmark runs on this site were wasted by ignoring that.

Every number in this post recomputes from the committed run file with the committed scripts, including the order-effect check that would have withdrawn the headline.

Sources

  1. Quantisation run — 30 paired generations, per-rep timings, and the recorded reply headsThis repository, 2026-08-29data/quantisation/run-20260829T155408.json — every rep behind every number here, including the replies the divergence count was computed from.
  2. Paired harness and analyserThis repository, 2026-08-29scripts/bench/quantisation.mjs and scripts/bench/analyse-quantisation.mjs — the interleaved design, the run-time identity check, and the order-effect test that can void the headline.

Advantages

  • Paired and interleaved: both arms answer a byte-identical prompt at the same thermal moment, so the 8.2% burst-to-plateau decay this machine is already known to have cannot be booked as a quantisation effect
  • The design's own assumption is tested rather than asserted — the speedup recomputed separately for each within-pair running order agrees to 0.01x, and the analyser prints a refusal to publish if it does not
  • The two arms are verified at run time as one model at two precisions: identical architecture, parameter count, context length, embedding length and chat-template hash, checked on every run rather than once by hand

× Challenges

  • One model family, one size, one machine. A 1.2B model on an M1 is not evidence about a 70B model on anything, and the generation-side win should shrink wherever memory bandwidth stops being the binding constraint
  • The divergence result is a disagreement rate, not a quality score. Neither arm was marked right, and this run cannot say which summary is better
  • The prompt that produced the refusals is degenerate by construction — one clause repeated up to 64 times — so the refusal rate belongs to that prompt shape and does not generalise to ordinary prose
Abhishek Kushwaha

Written by Abhishek Kushwaha

Full-stack software engineer in Kathmandu, Nepal — six years shipping production Django and Next.js systems, most recently at Pinakin Technologies & Research Center. Writes Global Tech Search on what the AI buildout costs in power, water and silicon, measuring it first-hand where he can. More about the author →