arborist/docs/benchmark-matrix.md
russell@unturf.com ab8df76792
feat(#000057): CPU wattage (RAPL) in watt_bench + expanded cost/quality matrix doc
Toward fox's next goal: score the full serving stack on quality AND
cost — {qwen, hermes} × {llama.cpp, vLLM} × {3090, 4090} × {solo,
arborist}, measuring CG% + GPU watts + CPU watts + joules/answer per
cell.

watt_bench.py — adds CpuSampler (Intel RAPL package energy via
/sys/class/powercap/intel-rapl:*/energy_uj). RAPL exposes a cumulative
microjoule counter, so energy-over-window is an end-minus-start diff
(handles wrap) — more accurate than integrating instantaneous power.
Sums multi-package. energy_uj is root-only by default (PLATYPUS /
CVE-2020-8694), so it degrades to available=False when locked;
--cpu-energy-cmd 'sudo cat {path}' supplies a privileged reader when a
sudo rule exists. Each cell now reports gpu/cpu/total joules-per-
question + gpu joules-per-token; the report records cpu_rapl_available.
Verified: graceful degradation when locked; RAPL diff math (1->4 MJ uJ
= 3.0 J, exact).

benchmark-matrix.md — expands the cost section to the full 16-cell
(model × engine × GPU × arm) design, the per-cell metric set (quality +
GPU + CPU energy), the serving-stack inventory from 2026-05-20 recon
(4090=qwen/llama.cpp, 3090=hermes/vLLM — each box has one engine + one
model today), and the buildout gap (vLLM+qwen, llama.cpp+hermes, cross-
GPU models). Notes idle-floor asymmetry (hermes/3090 ~127W vs
qwen/4090 ~20W) as a real optimizer input.

Harness is ready; the serving-config buildout + RAPL perm grant are the
remaining (ops, fox-directed) prerequisites to run the full matrix.
2026-05-20 14:45:49 -04:00

7.8 KiB
Raw Blame History

#000057 benchmark test matrix

Shareable spec of the control-experiment benchmark — what we measure, how, and the cells in the matrix. Companion to the rolling results in docs/qa-modes-bench.md (Addendum 8).

The question

A small language model, asked "who is the president of France?", answers with the present-day office-holder ("Emmanuel Macron") even when the grounding corpus is a fixed ~2010 vintage (where the answer is Sarkozy). The model isn't lying — it's reciting its training-data present — but it fails to recognise it should defer to the supplied source, or abstain when it has none. The matrix isolates what fixes that: model scale, question framing, retrieval, or reasoning.

Fixture

  • 386 office-holder questions (bench/qa_questions_stale_map.json), each paired with a fixed corpus-vintage gold article (~2010-2011, verified from the artefact). The gold is the single source of truth a grounded answer must match.
  • Adversarial by construction: the corpus answer (Sarkozy) differs from the model's training-data present (Macron), so a model reciting its prior is measurably wrong against the source.

Axes

axis values what it isolates
model hermes (Hermes-3-8B, vLLM) · qwen-nothink (Qwen3.6-27B, llama.cpp, reasoning off) · qwen-think (same, reasoning on) scale (8B vs 27B) and reasoning
framing plain ("who is the president of France?") · source_relative ("According to the reference knowledge base, who is …") · as_of_corpus ("As of 2010, who was …") whether prompt framing alone fixes the drift
arm solo (model alone — measures the parametric prior) · arborist (model + retrieval over the 2010 corpus, answer_mode=claim_lattice) whether retrieval fixes it

Full cross product = 3 models × 3 framings × 2 arms = 18 cells, each over up to 386 items. (The arborist arm currently runs hermes and qwen-nothink; qwen-think is measurable but cost-prohibitive — see "Cost dimension".)

Metrics

Every (question, answer, gold) triple gets one verdict from a closed vocabulary:

verdict meaning
CORRECT_GROUNDED (CG) answer matches the gold source
WRONG (W) gold states something the answer contradicts
FABRICATED (F) answer asserts specifics absent from gold
ABSTAINED (A) answer honestly declines ("I don't have that")
JUDGE_ERROR (JE) judge could not classify deterministically

Two reads of the same verdicts:

  • accuracy — "did it say the corpus answer?" Penalises a model for knowing the current office-holder. Reported but flagged as misleading.
  • grounding-fidelity (the defensible read) — "did the model do the right conversational thing: answer when grounded, abstain when not?" W + F are failures (confident ungrounded assertion); A is success on the questions the model can't ground. CG% and abstain% are the headline numbers.

The judge

Deterministic, no LLM, no quota: bench/judge_code.py. Pipeline (first hit decides): empty/no-gold guard → explicit-abstention regex → short-answer entity-grounding fast path → NLI contradiction (θ=0.85) → lexical verifier (quote/span/entity/paraphrase) → WRONG-vs-FABRICATED tie-break on subject-in-gold. Claim-lattice JSON envelopes are unwrapped to prose before grading so the arborist arm grades on equal terms with solo.

The judge was calibrated against an Opus (SOTA) reference judge on the records Opus could grade: agreement CG 13→47 %, WRONG 56→89 %, ABSTAINED 80→95 %. The deterministic verdict is a proxy for grounding (lexical + NLI), not Opus-grade reading; JUDGE_ERROR residue is the natural input to a later LLM-batch pass. NLI runs on a GPU (currently the 4090 ai box); the rest is pure Python.

Cost dimension — for the constraint optimizer (expanded goal)

The next-goal matrix scores quality AND cost across the serving stack, not just the model. Every cell is a deployable configuration; the optimizer trades grounding-fidelity against energy to pick the config to ship.

Expanded axes (the full cost/quality matrix):

axis values
model qwen-27B · hermes-8B
engine llama.cpp · vLLM (where the model/engine combo is supported)
GPU RTX 3090 · RTX 4090
arm solo · arborist (retrieval)

Cross product = 2 model × 2 engine × 2 GPU × 2 arm = up to 16 quality+cost cells. Each cell records:

metric unit source
quality (CG% / abstain%) rate code judge
tokens / answer completion tokens API usage
latency / answer seconds wall-clock
GPU energy watts mean/peak · joules · J/answer · J/token nvidia-smi power.draw on the inference GPU
CPU energy watts mean · joules · J/answer Intel RAPL energy_uj (package sum) on the box

bench/watt_bench.py samples both GPU (nvidia-smi) and CPU (RAPL) on the inference box while driving the subset. RAPL energy_uj is root-only by default (PLATYPUS / CVE-2020-8694 mitigation) — pass --cpu-energy-cmd 'sudo cat {path}' with a sudo rule, or relax the sysfs perm, to capture CPU watts; GPU watts need no special perm.

Serving-stack inventory (recon 2026-05-20):

box host GPU engine present model present
4090 ai.foxhop.net:18888 RTX 4090, i9-14900K (32t) llama.cpp qwen-27B gguf
3090 3090-ai.foxhop.net:18888 RTX 3090, i9-12900K (24t) vLLM (venv) hermes-8B

To fill the matrix, the missing serving configs must be stood up: vLLM+qwen, llama.cpp+hermes (gguf), and each model on the other GPU. That's an infra buildout (install engines, fetch models, manage the single-slot live endpoints), tracked separately from the harness.

Why cost matters most here: reasoning chains are the headline cost finding — qwen-think spends 1300-3300 completion tokens/answer vs qwen-nothink's ~50-100 (20-50× the token cost), for a workload where arborist + qwen-nothink already lands 82 % CG. And hermes-8B on a 3090 idles at ~127 W vs qwen-27B on a 4090 at ~20 W — idle floor, model size, engine, and GPU tier all move the joules-per-grounded-answer the optimizer cares about. The metric is grounding-fidelity per joule, not per answer.

Results so far (CG%, all arms on the identical calibrated judge)

arm / model plain source_relative as_of_corpus
solo / hermes 9 % 5 % 18 %
solo / qwen-nothink 7 % 0 % 50 %
solo / qwen-think 6 % 5 % 44 %
arborist / hermes (n=40) 60 % 62 % 25 %
arborist / qwen-nothink 82 % 65 % 50 %

Headline findings: (1) retrieval dominates — no solo config approaches the arborist arms; (2) reasoning doesn't improve raw correctness (qwen-think/as_of 44 % vs nothink 50 %) and breaks honest-abstention under source_relative framing (nothink abstains 98 %, think only 62 %); (3) production answer: arborist + qwen-nothink, plain framing, reasoning off — 82 % CG. The cost axis will confirm whether reasoning's energy premium is ever justified.

Reproduce

# full matrix cell (one model, all framings, both arms)
python -m bench.control_sweep --judge code --models qwen-nothink \
    --arborist-ref qwen-nothink --n 386 --arborist-n 386 --max-workers 1

# arborist-only (solo data already collected)
python -m bench.control_sweep ... --skip-solo

# wattage subset (run ON the GPU box)
python -m bench.watt_bench --models qwen-nothink --n 20 --gpu-label 4090

Artifacts land in bench/qa_results/; scorecards via bench/score_with_code_judge.py; judge-vs-judge reconciliation via bench/analyze_judge_disagreement.py.