Can a laptop do real inference experiments?
The claim was that on an agent workload, whether the prefix cache is hit matters more than which engine you run. It was first measured once per condition on a fanless MacBook Air, which is a number without an error bar, on a machine whose speed changes with its temperature. Then it was measured properly. This is what that took, and what a laptop can and cannot tell you.
The result: 5.6 times faster with the cache, 95% interval 4.9 to 6.3, in 24 of 24 blocks. The design is the whole point. Each block runs the same eight-turn transcript twice, once with a fixed prefix so every turn extends the last, once with a fresh nonce at the head of every turn so nothing can be reused. Which arm runs first is randomised per block. Blocks alternate between an idle machine and one with three CPU workers grinding beside the server. The analysis, non-parametric throughout, was written down before the data existed, along with four predictions and the confidence attached to each. The cache removed a median 81% of prefill time, and the paired test came out at p of six in a hundred million. That claim now has an interval, and it holds on the worst machine most people own.
Two of the four predictions were wrong, and the wrong ones are the useful part. Contention slowed the uncached arm 75% and the cached arm 25%, as predicted; a broken cache costs most exactly when the machine is busiest. But the prediction that the speedup ratio would be larger under contention did not survive: arm order turned out to be a large nuisance factor, 6.6 times when the cached arm ran first against 4.2 times when it ran second, because the second arm of every block runs on a hotter chip, and randomisation had not balanced that across conditions. Stratified by order, the contention effect on the ratio disappeared. Without the order diagnostic, written into the plan because an earlier experiment had been fooled the same way, it would have been reported as real. And the prediction that memory-traffic signals would predict slow rounds better than CPU load was simply refuted: load average was the strongest predictor by a wide margin.
Five rules, each learned by getting it wrong. Randomise arm order and test for it; a KV-quantisation speedup of 1.24 times on this machine was published and withdrawn when it vanished with the arms reversed. Compare within a run, never across days; identical work took 16.6 seconds cold and 28.6 after an hour. Never report a difference smaller than the within-run spread, which reached 82% on decode under sustained load. Treat a crashed trial as data, recorded and excluded for cause, never silently re-run. And interleave conditions round-robin rather than running each condition’s trials together: a first attempt at a prompt ablation ran them in blocks, the server died partway, and the later variants were scored on crashed trials, producing a clean monotonic decline that was entirely an artefact of the clock.
What a laptop can measure is anything large and paired. A 5 times effect, an 8 times memory saving, whether a cache is being hit at all: these survive thermal noise because each block is its own control, run minutes apart. What it cannot measure is a 10% difference between two engines, or between two quantisations, or anything that needs the machine to be the same on Tuesday as it was on Monday. The whole study took 1.8 hours of wall time and a few hundred lines of Python. The expensive part was the discipline.
Four numbers that were confidently wrong first
41,865 tokens per second of prefill on an M2. A cache hit, not inference: the benchmark reused one prompt across runs, so llama.cpp prefilled once and every timed round measured a lookup. Every round now leads with a unique nonce.
120 MB peak memory for a 3 GB model. RSS is the wrong metric on Apple silicon; the process’s phys_footprint is right.
A server alive, answering health checks in 11 ms, and stuck forever on one request. It had been spawned with its stderr on a pipe nobody read; after a hundred requests the pipe filled and the logging thread blocked. Logs now go to a file, and a request has a ten-minute ceiling so a wedged server costs one block, not a study.
A score of zero with no code executed. The loop stopped when the model’s reply contained the terminator, which it did, inside a print statement in the code it had written.
What this leaves out
The contention condition was weaker than planned: the memory worker had to be cut from 1.5 GB to 640 MB after an earlier attempt was killed for memory, so the two secondary hypotheses were tested under a mostly CPU-shaped load. A 9B model left this machine 825 MB of reclaimable memory; the study ran on a 4B for headroom, not for quality.
One thing is still unexplained. A real agent loop on the same setup reported zero cached tokens on every round, while the scripted replay of the same shape hit the cache every round. Feeding back the model’s own text still cached; streaming made no difference; four hand-built minimal reproductions failed to cache at all. Something about when the cache engages is not understood, and a third guess seemed worse than saying so. The prefix cache is the thing to check first in any agent loop, and it fails silently.
Read next
- at-home/
- Will this model fit on my machine?
Weights are the fixed cost; the context you ask for is what pushes you over.
- MLX or llama.cpp on a Mac?
MLX decodes 1.8 times faster here, and two defaults matter more than the engine.
- explainers/
- The hit rate is the workload
The same effect on a datacenter card, where concurrency decides whether a turn's history is still in memory.
- Compute the system prompt once
What each request actually computes once the cache is addressed by prefix.
- api-bill/