How I Measure

Every benchmark post on this site rests on the same two machines, the same harness and the same set of rules. Rather than repeat all of it in each article, it lives here. Articles link back to this page; this page is kept current as the setup changes.

If you only read one section, read the rules. Every one of them exists because a measurement I had already published turned out to be measuring something other than what I claimed.

The two machines

Both runs use the same GPU model. Almost nothing else is the same — and that is the point. If two boxes this different produce the same bytes, the result is not an artefact of one particular machine.

WorkstationHeadless container
Roledesktop, monitor attachedLXC on a Proxmox node
GPURTX 3090, 24 GBRTX 3090, 24 GB
NVIDIA driver610.88580.126.09
CPUi7-7820X, 8C/16Ti7-1165G7, 6 threads visible
RAM128 GB24 GB
OSWindows 11 ProUbuntu 24.04 LTS
Model storeSATA SSDlocal disk, not NFS
Ollama0.32.50.32.5

One difference matters more than it looks: the attached monitor costs roughly 1.8 GB of VRAM before a single model is loaded. For any model that does not comfortably fit in 24 GB, the workstation therefore spills more onto the CPU than the headless box does. That is not a footnote — it is the single most common reason two otherwise identical runs disagree.

What gets measured

What deliberately does not get measured

Quality of prose. Helpfulness. Tone. There is no score here for “the marketing copy is good”, because I cannot measure that — and neither can a second language model asked to grade the first one. LLM-as-judge introduces a second model’s variance and dresses it up as a number. Everything scored on this site is something a script can check against a fixed answer: does the JSON parse, does the anchor resolve to an ID that exists, is the total exactly 985.77, does every claim carry a source marker.

The price of that discipline is real: these benchmarks reward instruction-following and cannot tell you which model is more pleasant to work with. I would rather report a narrow thing accurately than a broad thing vaguely.

The rules — each one from a mistake

Reserved context is not filled context

Setting num_ctx to 128k and then sending a 30-token prompt measures the reservation of the KV cache, not its use. I published “context is nearly free” on the strength of exactly that mistake and had to take it back. Every context measurement now logs the actual fill level from prompt_eval_count and states it in the result.

Warm up after every context change

Ollama reloads the model whenever num_ctx changes, so the first measured condition carries the loading cost. In one smoke test that made an empty context look slower than a full one — 85.7 against 139.2 tok/s. One discarded request after every change, then measure.

The prompt cache invalidates prefill numbers

Repeat a prompt and the cache serves it: I once recorded a prefill rate of 295,631 tok/s, which is skipped work, not speed. Filler text is varied per repetition. Where prompts unavoidably share a prefix, prefill is simply not reported. Generation throughput stays valid either way.

Fix the output length, or the comparison is not paired

With a free-running answer length, different numbers of requests fall through a filter on each host, and the two sides stop being comparable — one run was ruined by exactly this, 36 usable cells against 24. num_predict is fixed, and the prompt token counts of both hosts are checked against each other before any difference is reported. If they do not match, there is no comparison to report.

Numbers come from the raw records

Every request is written to a JSONL file as it completes. All published figures are recomputed from those files, never read off a run log or a summary. Summaries are for looking at while a run is in progress.

Read the outputs, not just the scores

A scoring routine is a piece of software and can be wrong. Two examples from this year: a check demanded bullet points the task had never asked for, dropping a correctly answering model from 100 % to 69 %. And a check looked for a result line but did not recognise it when the model wrapped it in a Markdown heading — models that had solved the task were recorded at zero. Both were found by reading the actual output. Every scoring routine is now read against real answers before it is allowed to grade a run, and every output is kept in full so that anyone can contradict the score.

Truncation is not a bad answer

A model that runs into the token ceiling has not answered badly, it has not finished. Those cells are counted and flagged separately. A model that hits the ceiling in every single run gets its own note rather than a quiet place at the bottom of the table.

CPU offload breaks reproducibility

As long as a model fits entirely in VRAM, a fixed seed at temperature 0 reproduces across machines. Once part of it runs on the CPU, that stops being true — and how much runs on the CPU depends on what else is using the card, including the desktop. Any model that does not fit is marked as such and excluded from cross-machine claims.

Line endings are not a finding

The newest rule, and a cheap lesson. Comparing outputs between the Windows host and the Linux host reported that 97 of 99 files differed — a spectacular result that was entirely my own file writing: CRLF on one side, LF on the other. Normalised, the real number was 90 of 99 identical. Harnesses now write \n explicitly. When a measurement is dramatic, suspect the measurement first.

How to read the numbers

Corrections

Where a later article contradicts an earlier one, the later one wins, and both say so. Corrections are made in the original article rather than quietly in a new one. Two published claims have already been withdrawn this way; both are named in the rules above rather than hidden.

If you think a number here is wrong, the raw outputs exist. Ask for them.