Docs › benchmark-protocol

Benchmark protocol

How this test bed measures things, and what it refuses to measure. Written 2026-08-04, before the boxes are back — deliberately, because a protocol designed after the first results is contaminated by them.

The governing metric is unchanged from Phase 3C: time to a correct result, with tool-call success as a gate rather than a tiebreaker. Raw tokens/second is a means. A model that decodes faster but reasons in circles is slower where it counts.


0. Preconditions — pin these or the results mean nothing

Not axes. Get one wrong and the run measures the mistake instead of the subject.

Precondition Why
preserve_thinking verified end-to-end Qwen3.5/3.6 templates discard prior-turn thinking blocks without it, and the documented failure signature is tool calls arriving with empty argument objects. Tool-selection is our stated failure mode — the same signature. An uncontrolled run measures a template bug. Acceptance test: a 5-10 turn tool loop with zero empty-arg calls, before any suite runs.
Chat-template version recorded per result Template version has been a larger source of 2026 agent variance than model choice. A result without it is not comparable across dates.
Host health verified Kernel ≥6.18.4 with CWSR exported, firmware not linux-firmware-20251125, no amdgpu-dkms. Below that ROCm reports wrong VGPR counts and crashes. Benchmarks on a sick host are noise.
Quiesce protocol No other model resident, no background crons, llama-swap quiesced for GPU-borrow windows, page cache state known. Our own decode collapse (0.12 tok/s on a 99.4%-warm turn) was caused by memory pressure from previous activity.
Output sanity gate llama-bench measures throughput and never checks the output is valid. A backend can be 24% “faster” while emitting a single garbage character — observed on gfx1151 Vulkan with Qwen3.6 (clm-0014). Before recording ANY throughput number: one real generation per (model, backend, build), eyeballed for coherence. Fail it and the throughput is discarded, not footnoted.
--load-mode none on unified memory With mmap the model file sits in page cache while the tensors are allocated again. On a discrete GPU those are separate pools; on shared memory they are the same DIMMs, so a 73 GiB model on a 122 GiB box goes to swap or dies with thousands of amdgpu: SVM mapping failed, exceeds resident system memory limit. This defect was hit FOUR separate times before being fixed properly — each occurrence patched at one call site while the others stayed broken. It is now enforced twice: LLAMA_ARG_LOAD_MODE=none on the host as a floor, and an explicit flag in every script so the setting is visible in the fingerprint. An env var alone is invisible to a reader; a flag alone is easy to omit in a new call site.
Scatter is a symptom, not an error bar Repetitions are not merely a way to average noise away. On a broken code path the same deep cell varies 5-10% between identical repetitions; on a correct one it reproduces within ~0.3% (clm-0017, where that scatter retroactively explained a 20% standard deviation that had gone unexplained for months). A cell whose repetitions disagree by more than ~3% at depth is evidence of a defective path and must be investigated before its mean is published. sweep.sh computes per-cell coefficient of variation and flags this; a clean mean over dirty samples is the most confident way to publish a wrong number.
Varied prompts under speculation Draft-free speculation (ngram-mod) shares an n-gram pool across slots within one llama-server. Run identical or near-identical content on multiple slots and they feed each other’s cache: throughput reflects the pool, not the model. A community Strix Halo run (clm-0016) measured 302-305 tok/s across 4 identical streams and 430 tok/s on same-prompt repeat, and correctly discarded both as artifacts — its trustworthy sustained figure, 121 tok/s, came from 500 varied prompts. Any multi-slot run with speculation enabled must use varied prompts, and must report the no-speculation floor alongside the speculated number — the ratio is the finding; the absolute is not comparable across configs.
Build provenance build_commit, backend, and runtime.tree on every row. With two forks now in play (con-0001, and possibly the quantized-KV fix) “which binary” is not a detail.

1. Two measurements, not one — capability and performance

Everything below rests on this split, so it comes before the axes.

Capability is what a model can do: retrieval at depth, tool calling, blind-judged reasoning. Performance is how fast it delivers that: prefill, decode, TTFT, and the cache and parallelism machinery around them. A full benchmark establishes capability, then finds the configuration that delivers the best performance without breaking it.

The two differ enormously in cost. A capability suite is hours; a llama-bench row is seconds. That asymmetry is the whole reason to separate them — it lets capability be measured rarely and performance often. What makes that legal is knowing precisely which levers can break capability, and how.

1a. Levers, classified by how they touch capability

Class Levers Behaviour Consequence
neutral speculation, mtp, ngram, cache_ram, slot_restore, disk_warmth Provably capability-preserving Sweep freely. Capability may be inherited.
binary backend, build Fine or catastrophic, nothing between Cheap guard catches it in seconds
lossy weight_quant, kv_quant A real slope Needs KL divergence + a full capability run
hazard parallel, chat_template, preserve_thinking Silently corrupts correctness Never inheritable, never assumed safe

Neutral is a proof, not an optimism. Correctly implemented speculative decoding reproduces the base model’s distribution exactly — the verification step guarantees it. So an unchanged quality score under MTP or ngram is the expected result, and mainly evidences that the implementation is not lossy. This is why clm-0016’s IFEval-strict 78.6% under ngram-mod is reassuring about the implementation rather than surprising about the technique; the more interesting half of that result is that it also held under ROCmFP4, which is a lossy lever.

Hazard is the class that breaks the clean separation, and it is not hypothetical: llama.cpp #25992 leaks responses across concurrent requests on gfx1151 HIP at --parallel > 1. That is a throughput setting silently corrupting output. Note that the community’s headline 121 tok/s sustained figure was measured at parallel 4 on HIP on exactly that silicon, and an IFEval score cannot clear the concern — contamination would surface as scattered per-prompt failures, indistinguishable from ordinary model error at that granularity.

Capability is a function of depth, not a scalar. A model fluent at 8k can fail at 200k. Every capability result carries depth_tokens; one without it is not a result.

1b. Three tiers

  1. Capability suite — expensive, thorough, run per (model, quant, template).
  2. Guard (bench/guard.sh) — cheap cliff-detector, run at every performance config. Coherence, a tool call with non-empty arguments, a shallow needle at depth, and cross-request isolation when parallel > 1. Minutes, not hours.
  3. Performance sweep — run freely once the guard passes.

The guard is what makes “best performance without breaking capability” tractable. It does not certify capability; it detects cliffs. A lossy lever cannot be cleared by a guard, and the schema enforces that: inheriting capability across a quant or backend change is a build error, not a judgement call.

1c. Publish the frontier, not the winner

“Best performance without breaking capability” implies a threshold, and thresholds are judgement calls that age badly. Every config is published carrying both its capability basis and its throughput, with risk flagged rather than the row suppressed — the same pattern already settled on for OOM. A schema that refuses to record a fast-but-degraded config is the same schema that refuses to record reality.


2. Axes — and why full factorial is the wrong instinct

Five axes interact: backend × context × quant × KV-quant × MTP. Full factorial is hundreds of runs, most of them uninformative.

Sample deliberately instead:

  • Backend × context is the primary grid. Not a fixed choice — ROCm was picked in June because Vulkan vk::DeviceLost-crashed at 128K, but the community quantized-KV work is Vulkan-side for prefill and ROCm-side for decode. The right backend may now differ by phase and by context, which is exactly the kind of thing an inherited assumption hides.
  • Quant × MTP must be measured together, as a ratio. Published Strix Halo numbers show the MTP multiplier moving opposite to intuition — 2.44x at Q8_0 against 1.81x at Q4_K_M — because decode is bandwidth-saturated, so fewer memory passes pay more the heavier the weights. Measuring either alone gives the wrong answer about both. The transferable number is the ratio, not the absolute.
  • KV-quant is now a first-class axis, not a memory-saving footnote. Community verification reports stock llama.cpp repeatedly dequantizing during inference on this silicon, with a fix worth +75% to +203% ROCm decode and q8_0 running faster than f16. If true here, “quantize the cache to save memory” becomes “quantize the cache to go faster”, which inverts the trade. See clm-0006.
  • Cold vs warm is a dimension, not noise. Every row carries served: warm|cold. A suite that measures only cold prefill misrepresents production; one that measures only warm hides the cold-start cost that dominates a first turn.

3. Tiers — cheap and broad first, expensive and narrow last

Tier 0 — capability probe. Does it load, what is the max fit context, what is peak memory. Minutes. Kills candidates before they consume eval time — and produces observed_peak_gb, the field clm-0001 says we are missing. Capture peak during Tier 0 rather than as a separate exercise; it is nearly free here and expensive later.

Tier 1 — throughput sweep. TTFT, prefill and decode tok/s across 4K → 128K → 262K, per backend. Throughput on this hardware is deterministic (N=3 stddev ≈ 0), so N=3 at ≤32K as published variance evidence, N=1 above where the number does not move.

Tier 2 — cache capability trace. The operator’s addition, and the part nobody else publishes (§3).

Tier 3 — agentic quality. Expensive, so run it only on the configuration Tier 1-2 selected, at the context band we actually operate in (>100K). N≥3 minimum, because this is the only place run-to-run variance is real. Blind-isolated judging: a separate, fresh judge per response seeing only the task, the rubric and one anonymised answer.

The R1 rule. A prescriptive scorer once ranked the eventual winner 0/15 while it was producing the highest-value output in the field — the gate was measuring entity-name luck, not reasoning. No pass/fail gate is trusted until transcripts have been read. Scores propose; transcripts decide.


4. Cache capability trace — the original contribution

Everyone publishes tokens/second. Nobody publishes what their cache actually does, and on this box cache behaviour is the difference between an 8-minute turn and a 10-second one. A complete trace per config:

Capability What to record Why it matters
Slots --parallel n, per-slot context, whether MTP forces -np 1 Doubling slots doubles KV. Almost never stated publicly, and it changes every memory figure.
In-slot prefix reuse cache_n / prompt_n on a repeat turn The 97% figure that makes the warm lane work. Measure at realistic bootstrap scale — a small-prefix test gave a misleading 40% and cost a day.
Cross-session reuse reuse when the prefix differs after the system prompt Known to fail: isolated sessions diverge immediately and cold-prefill. Worth measuring, not assuming.
--cache-ram size, hit rate, and its memory cost A host-side cache that competes with the model for the same pool.
Disk save/restore restore wall time, tokens reused, file + sidecar size ~105 ms restore, 3017 tokens reused with the checkpoint sidecar and 0 without — the whole reason con-0001 exists.
Checkpoint count --ctx-checkpoints, resident cost, save-spike size Defaulting to 32 was ~4.5 GB resident and a contributing cause of the OOM panic.
Page-cache dependence restore time cold vs warm page cache Our 105 ms restore probably relies on the file being cached. A real NVMe read is ~190 ms. Unmeasured.

This is also where the cache-wall hypothesis (clm-0007) gets tested: a measured GPU-cache knee at 32-40 MB with a ~4x read-speed drop past it, offered as a candidate explanation for our 354 → 144 tok/s prefill curve. Distinct mechanism from pool utilisation — do not conflate the two.


5. What every run must carry

Non-negotiable, or the result is not publishable: config id (never a naked number), build_commit, backend, runtime.tree, chat-template version, served: warm|cold, N and the variance where N>1, and the suite version. Plus incidents: [] — a run during a thermal event or a competing process is contaminated, and that must be visible at the point of use rather than buried in a postmortem.

Suite versioning: patch = comparable, minor = comparable with a note, major = breaks the series — stated explicitly in comparable_with, never inferred from the number. The grader is a dependency: changing the judge model silently re-baselines all history, so it is a major bump even when the test content is identical.


6. Cross-host runs — the comparison nobody has

With two boxes, run the same suite and config on both and set cross_host_pair. That isolates hardware as the only variable. It is the comparison people actually want when deciding whether a second box is worth buying, and it is almost never available because most people replace rather than overlap. Run it during the overlap window or lose it.


7. Build on existing work — what to adopt rather than invent

Checked before writing anything bespoke. Most of Tiers 0-1 already exist and are better standardised than anything we would write.

Tier Adopt Why
1 — throughput llama-bench -o json — gated on the output sanity check above Its JSON already carries build_commit, build_number, gpu_info, model_*, n_batch, n_ubatch, n_depth, type_k/type_v, n_gpu_layers, flash_attn, avg_ts, stddev_ts and the per-repetition samples — i.e. almost exactly our required provenance set, including the context-depth and KV-quant axes. Use it, do not reimplement it.
1 — served path llama-benchy — adopt as-is llama-bench measures internal C++ timing and cannot see proxy overhead, queueing or warm-lane restore. llama-benchy (MIT, actively maintained) measures the client-perceived side against any OpenAI-compatible endpoint and deliberately mirrors llama-bench’s output shape so the two sit side by side. Its --enable-prefix-caching mode is the exact primitive we need: a two-step measurement separating the cold context load from the cached-prefix reuse. Run both, publish the gap — that difference is a finding, not an inconvenience. Two caveats: it speaks only /v1, not llama.cpp’s Anthropic-compatible /v1/messages, so the Claude Code path needs a thin second script; and client-side timing conflates its own precision with our proxy hops, so sanity-check a few readings against the proxy’s own /_stats before publishing any delta. Effort: hours.
1 — determinism temperature 0, fixed seed — but do not rely on it The homebench convention, and worth setting. ⚠ It does not hold on Vulkan (clm-0011): same config, temp 0, batch 1, no speculation, three different outputs. Two consequences: N=1 is not defensible for anything output-dependent, and speculative losslessness cannot be verified by diffing outputs — which is the obvious way one would check that MTP or ngram speculation changes nothing. Use a distributional instrument (KL divergence) instead. Unverified on ROCm; check rather than assume it transfers.
3 — agentic harness-benchbuild a minimal one ourselves Checked 2026-08-05: there is no public repo. The task set, hidden graders and traces are deliberately private to keep them out of training corpora, and the work carries no OSS licence — so it cannot be adopted, only requested. (Beware a name collision: Qihoo360/harness-bench is an unrelated academic benchmark.) Its shape is still the right one and worth copying: 5-8 tasks, isolated workspace/ per cell, grading by a hidden test.sh the agent never sees. Its author also reports a real contamination case — one harness read the hidden tests in 14 runs — which is a fairness trap to design against from the start. Our question is narrower than his anyway: he compares models across harnesses; we want raw-engine versus proxy-fronted behaviour for a fixed model. Estimated 1-2 days.
3 — token economics end-to-end token accounting Current work shows the harness — how context is assembled and tools exposed — dominates token cost, with large reductions available from how tools are surfaced rather than which model is used. Relevant directly: our own tool-schema curation cut ~17.5K tokens from every prompt. Measure tokens per completed task, not just tokens per second.

The interoperability prize. Emitting llama-bench-compatible JSON makes our numbers directly comparable to everyone else’s on this silicon, and directly contributable — the standing community request is literally “reply with the numbers, with build_commit and the fa mode from the bench JSON attached”. A bespoke format would forfeit that for no gain.

What stays ours, deliberately: the cache capability trace (§3) — nobody publishes it, and on this box it is the difference between an 8-minute turn and a 10-second one; energy per correct answer, wall-metered with a stated baseline; lived production telemetry alongside controlled benchmarks, kept strictly separate; and the decision log — what displaced what, and what was rejected.


8. Task sets — adopt the standard ones, keep the bespoke ones for a reason

Distinct from §6, which is about plumbing. This is about content: which task sets to run. The rule is the same one the site is built on — the scores in the public commons are unreliable; the tasks are not. Adopting a task set while generating our own numbers against our own config fingerprint is exactly the point. It is the missing fingerprint we criticise, not the questions.

Capability Adopt Notes
Long-context recall lm-eval’s RULER port (NOT NVIDIA/RULER) + llama-perplexity --kl-divergence Four task families where NIAH has one. But verified 2026-08-05, and the plan changed: NVIDIA/RULER’s standalone pipeline is deprecated in its own README, will not import without NeMo, and its OpenAI client is hardcoded (no base_url, and a model2length lookup that KeyErrors on any local model name). It also silently dropped answer_prefix from every prompt between Jan 2025 and 21 Jul 2026 — scores from that window are not comparable to anything. Use lm-evaluation-harness’s RULER port instead: all 13 tasks, generated on the fly, points at any OpenAI endpoint with no code changes. ~2-4 hours.

⚠ RULER cannot answer the question we wanted it to answer, on this hardware. Published KV-quant effect sizes are ~1 point (NVFP4 on Ruler-64K: 95.6 / 95.5 / 94.6 for BF16 / FP8 / NVFP4; xKV 4-bit: 88.85 → 87.64). At n=25 the binomial SE near p=0.9 is ~6pp; at n=125, ~2.7pp. Any sample count we can afford resolves 5-10pp, not 1pp. And the cost is brutal: the maintainer quotes ~2 h for 128K × 500 samples on 8×H100 with vLLM batching; we have one slot, strictly serial, no batching, and every RULER sample is a distinct long prompt so prefix caching buys nothing. Full suite ≈ 6,500 sequential 128K prefills per KV config — weeks.

So use two instruments. Primary: llama-perplexity --kl-divergence, built into llama.cpp — record f16-KV logits once, replay q8_0 and q4_0 against them, and get mean KLD, ΔPPL, Δp percentiles and top-1 agreement with uncertainty bars, over hundreds of thousands of tokens, in a single forward pass with no generation. Prefill is the fast half of this box, so it is the cheapest signal-per-hour available. Secondary: a RULER-lite cliff detectorruler_vt, ruler_cwe, ruler_fwe, niah_multikey_3 at two lengths, 30-50 samples. Those four are the KV-sensitive ones; niah_single_* sits near ceiling and absorbs noise. Decision rule: if KLD(q4_0) is the same order as KLD(q8_0) and no cliff appears, ship q4_0; if KLD is 10×+ or vt/cwe fall away, we have the answer without needing resolution RULER could never give us.
Tool calling τ²-bench — adopt. MCP-Bench — do NOT adopt the harness τ²-bench (verified 2026-08-05): MIT, genuinely active (v1.0.1 Jul 2026, pushed 4 Aug), and — checked in source, not docs — it calls LiteLLM with a real tools= array, i.e. native function calling, not a text-prompted imitation. Scoring is mostly deterministic (DB state-hash diff, tool-call match against a reference trajectory, substring checks) with one LLM-judged component whose model is overridable — so hold both the judge AND --user-llm fixed across comparisons or you re-baseline silently. Effort ~2-4 hours. Domains are small: 14 / 17 / 13 tools.

MCP-Bench — rejected, on a structural finding. It does not use the native tools= parameter at all: it serialises each tool’s schema as text in the prompt and parses a JSON plan itself. So running it against llama-server never touches --jinja’s grammar-constrained path, and it cannot reproduce or validate a fix for our HTTP-400 grammar-size ceiling — that failure only occurs when a real tools=[...] array is sent. It is also unmaintained (no commits since Oct 2025, no releases), has no LICENSE file at all, and hardcodes o4-mini as judge, so it needs an OpenAI key even for an all-local test.
Function-calling breadth BFCL v4 — with care Broader agentic categories now (web search, memory read/write, format sensitivity). Two cautions: v4 scores are not yet broadly published, and v3 and v4 are not comparable — mixing them silently is the exact failure this site criticises. Record the version in the suite id or do not run it.
Knowledge / reasoning MMLU-Pro, GPQA — deliberately deprioritised Not because they are bad, but because they do not discriminate on our decision axis. Every candidate that reaches our shortlist is competent at recall; none of them fail on knowledge. Chasing these would spend eval time on a tier that has never changed a decision here. Stated so the omission is a choice, not an oversight.
Creativity / judgement stays bespoke — C1-C8 No standard set covers what Warden is actually for: proactive insight, sensor-conflict resolution, ambiguous intent, restraint under an attention budget, emotional attunement. These were written against the real job and blind-judged. Keeping them is not NIH — it is that the standard sets do not test the thing.
Coding / agent harness harness-bench shape (§6) Sandboxed cells, hidden-test grading.

⚑ Neither benchmark tests our actual failure mode, for opposite reasons. τ²-bench uses native tool-calling but its domains are far too small (13-17 tools) to approach the ceiling; MCP-Bench has large authentic schemas but bypasses native tool-calling entirely. So the grammar-ceiling probe stays bespoke — and the cheap win is to lift MCP-Bench’s mcp_servers/ corpus as raw fixture data (28 real third-party MCP schemas) and feed combinations of them into a native tools=[...] request against llama-server, scaling until HTTP 400. That extends the manual probe from 2026-07-05 (55 tools = 200, 60 = 400) with authentic schema diversity, for about an hour’s work and none of their machinery.

⚠⚠ VERIFY BEFORE SPENDING A NIGHT ON THIS — the hybrid confound. Our 122B is a hybrid/recurrent architecture; that is precisely why the checkpoint bug (con-0001) existed at all. On such models -ctk/-ctv only affect the minority of layers that use full attention — the linear-attention layers hold no KV cache to quantise. llama.cpp issue #21385 reports q4_0 KV token-identical to f16 (BLEU 1.000) on a Qwen3.5 hybrid, attributed to only 8 of 32 layers using full attention. That both predicts a tiny delta and dilutes any measurement of it. Count how many layers actually hold a quantised KV cache before designing the matrix — if it is a small minority, the honest finding is “q4_0 is near-free on this architecture, and that is an architecture result, not a quantisation result.”

Two further traps: on ROCm only matching -ctk/-ctv types hit the fused flash-attention kernel — mismatched types fall back silently to a slower path, so keep K and V identical or you are measuring something else. And the f16 arm may not fit at 128-200K beside a 122B; if f16 only fits at 64K, the three arms are not comparable at the top length and the matrix must be redesigned around what actually loads.

Operational note: run all of this against the raw llama-server, never through llama-swap or the slotpin proxy. A multi-hour sweep will evict the production warm-lane cache and wedge the single slot.

The compounding benefit: RULER at three KV quantisation levels answers two questions at once — our own “is q8_0 safe at 200K”, and the community’s open “what does cache compression cost in quality”. Same runs, two audiences, and the second one is a standing request from people who published their half of it.


8a. Live Home Assistant: schemas yes, actuation no

No benchmark touches a live Home Assistant. Not as a safety rule but a measurement one: HA state is mutable and shared, so a suite that toggles real helpers changes the world it is measuring and cannot be re-run from the same starting state. It would also silently import HA version, MCP server version, integration availability and entity naming into every result as uncontrolled variables.

We have already paid for this lesson. The R1 task in Phase 3C appeared to measure energy-dispatch reasoning. Reading 60 transcripts showed it measured whether a model happened to toggle an entity literally named switch.dishwasher and whether the mock recognised the entity IDs it guessed — zero genuine reasoning failures across the set, and it nearly selected the wrong model. Environment coupling is how a benchmark starts scoring the environment.

But schemas are not actuation, and one probe genuinely needs the real ones. The grammar ceiling is a schema-size limit; synthetic stubs are shorter and more shallowly nested than real MCP tools and would flatter the result. So:

Capture the schemas, freeze them, mock the execution.

Export the tool definitions from the HA and Apple MCP servers once, commit them as static JSON fixtures, and replay them into a real tools=[...] array (bench/grammar-ceiling.sh --schemas <dir>). Authentic size and shape, no live dependency, repeatable — and versioned, so a later HA upgrade appears as a deliberate fixture change rather than as silent drift in a result nobody re-derived.

If HA-shaped tasks are wanted, mock the environment and treat the mock as a suite dependency: versioned, frozen, and bumped like the grader. R1’s real lesson was not “do not mock” but that the mock’s fidelity became the thing being scored.

Where live HA does belong: telemetry, not benchmarks. §2.7 already separates them — benchmarks are controlled and reproducible, production telemetry is observed and workload-dependent, and mixing them destroys both. Real Warden-touching-real-HA behaviour is a counters-and-distributions question.


9. When a run fails — which levers are safe, and which invalidate the result

Benchmarks fail mid-suite. The dangerous moment is not the failure, it is the twenty seconds afterwards when you change one flag to make it complete. Some changes cost nothing but a footnote; others silently produce a number that cannot be compared to anything, including our own earlier runs.

The rule

If the change would alter the config fingerprint, it is a NEW CONFIG, not a retry.

That is not a metaphor. The fingerprint is computed over exactly these fields — build_commit, model_filename, type_k, type_v, n_gpu_layers, flash_attn, n_batch, n_ubatch (see scripts/ingest-llama-bench.mjs). The fingerprint fields ARE the invalidation boundary. Touch one to rescue a run and you have not rescued it; you have started a different experiment that happens to share a suite name.

Safe levers — declare them, then carry on

These change how much we measure, not what:

Lever Cost Condition
Reduce repeats (-r) Wider variance State N and the stddev. N=1 is legitimate where the metric is deterministic; it is not legitimate for anything judged.
Drop the deepest cell Shorter series The remaining cells stay comparable. Log what was dropped — silent truncation reads as “we covered everything”.
Raise timeouts None A cold prefill on the 122B is minutes. A timeout is a harness limit, not a property of the model.
Quiesce and re-run Time Removes contamination without touching the config. Always the first move, not the last.
Increase warmup runs Time Affects variance, not the metric, provided it is stated.
Split the matrix across sessions Bookkeeping Same config, same build. Record the dates; check for drift if the gap is long.

Invalidating levers — a new config, or nothing

Reach for any of these and the old and new numbers must never appear in the same series:

Fingerprint fields: quant · KV quant (-ctk/-ctv) · -ngl / offload split · flash attention · batch and ubatch · build commit · backend · model file or revision.

Beyond the fingerprint, equally fatal:

  • The grader model. Changing it silently re-baselines every judged result in history. A major suite bump even if the task content is byte-identical.
  • The task set. Any edit is a suite version bump; whether it stays comparable is stated in comparable_with, never inferred from the number.
  • preserve_thinking. A precondition, not a setting. Flip it and agentic results measure a template defect.
  • System prompt or bootstrap. Changes the prefix, which invalidates every cache-behaviour metric outright — reuse is measured against that prefix.
  • MTP on/off. A different decode path.

The grey zone — permitted, but the caveat travels with the number

Sometimes the only way to measure a thing at all is to compromise. That is allowed. What is not allowed is letting the compromise sit in a footnote while the number travels alone.

Situation Verdict
Higher quant does not fit, so test lower Permitted, not quant-equivalent. This is the Nemotron case: it was forced to IQ4_XS while peers ran Q4_K-class, and came last. The caveat is a first-class claim (clm-0005) precisely so it cannot be separated from the result.
Reduce context to make it fit Permitted; not comparable to a full-context result. A 32K number is not a small 128K number.
Another model resident during the run contention: true. A real-world figure, not a clean per-engine one.
Warm vs cold Both valid, never mixed. Every row carries served.
Thermal throttling suspected Do not publish the number. Log an incident, link it via run.incidents, re-run cold.

When to stop rather than push through

Three failures where continuing produces confident nonsense:

  1. Empty-argument tool calls. Stop. That is the preserve_thinking signature, and every agentic number collected past it measures the template, not the model.
  2. HTTP 400 on grammar build. Our tool surface exceeded the schema-size ceiling and the fallback is silent — the run appears to succeed against a different model entirely. Fix the tool count, do not record the run.
  3. OOM anywhere in the sweep. On unified memory this takes the whole box down and needs physical access. Stop the matrix, record the ratio at failure — that is the calibration data clm-0001 says we lack — and only then decide what to change.

10. What we deliberately do not measure

  • Completeness. Decision-relevance is the bar: what is live, what displaced what, what was rejected. A run that cannot change a decision does not need to exist.
  • Correctness in production. It is not observable there. Production yields throughput, cache behaviour and cost — not quality. Say so on the page rather than implying otherwise.
  • Output quality under KV quantisationcurrently. This is a known gap in the community work too, and the one place we already hold an answer worth contributing: NIAH at q8_0 KV, perfect retention to ~205K including the multi-needle tier.

Compute surfaces and the NPU lane (added 2026-08-13)

The NPU lane follows the SAME benchmark format as GPU/CPU work — same tiers, same record types, same energy discipline. Screening tier for NPU candidates: FIT = loads-and-serves under FastFlowLM on the NPU (device evidence required: NPU lock, power-state transition); GUARD = the standard probes over FLM’s OpenAI-compatible endpoint; SMOKE = the standard 5-task tau2 set, pinned simulator, cliff-detector semantics unchanged.

Marking rule (operator, 2026-08-13): every result is labelled with its compute surface and backend (config engine + runtime.backend). Cross-surface comparison of the same model is welcome IN COMMENTARY — “NPU prefill is x% faster, decode slower” is exactly the interesting contrast — but purpose-fit verdicts are per-surface, and no table mixes surfaces without an explicit surface column. NPU-lane candidates never inherit GPU metrics (schema-enforced).