Thin benchmark harness for local LLM endpoints — TTFT/prefill/decode with distribution, correlated with macOS memory pressure and thermal throttling. Same code on M1 (LM Studio) and M5 (mlx-lm), just swap the config.
  • Python 88%
  • HTML 8.4%
  • JavaScript 2.2%
  • CSS 1.3%
  • Shell 0.1%
Find a file
Johannes Kaindl d81276dad4
docs(agents): Verifikations-Befehl korrigieren -- er lief so nie durch
Der im vorigen Commit ergaenzte Einzeiler
`git rev-parse --short main origin/main github/main` bricht mit
"Needed a single revision" ab: --short nimmt nur eine Revision. Ohne --short
laeuft die Mehrfach-Form, misst dann aber die lokalen Tracking-Refs -- und die
koennen genau in dem Szenario stale sein, gegen das die Regel schuetzen soll.

Ersetzt durch `git rev-parse main` plus je ein `git ls-remote --heads <remote>
main`, das die Forge selbst befragt. Verifiziert ausgefuehrt.

Ironie mit Ansage: eine Pruefanweisung, die nie ausgefuehrt wurde, ist derselbe
ungepruefte Doku-Satz wie die Mirror-Zusage, die sie ersetzen sollte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:59:50 +02:00
.github/workflows refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
docs docs: README auf readme-spec-Tier python-cli + Tutorial und How-to-Ebene 2026-07-28 16:48:26 +02:00
packs feat(packs): buero uses curated K.-o. scope (hallucination disqualifies, quality flaws don't) 2026-06-27 12:24:59 +02:00
prompts refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
scripts docs(decisions): ADR template + dir + doc-link-check test 2026-06-28 23:28:55 +02:00
tests test(config): alle mitgelieferten Configs globben statt drei aufzuzaehlen 2026-07-28 16:58:29 +02:00
touchstone chore(release): 0.2.0 2026-06-30 14:47:10 +02:00
.editorconfig feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
.gitignore chore(gitignore): ignore .superpowers/ (brainstorm companion artifacts) 2026-06-21 00:28:22 +02:00
.pre-commit-config.yaml fix(pre-commit): Whitespace-Hooks von byte-genauen Artefakten fernhalten 2026-07-28 16:58:00 +02:00
.python-version feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
.woodpecker.yml refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
AGENTS.md docs(agents): Verifikations-Befehl korrigieren -- er lief so nie durch 2026-07-28 16:59:50 +02:00
CHANGELOG.md refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
CLA.md refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
config.embed.yaml chore: Woodpecker CI + ruff format + run-config examples 2026-06-19 22:36:41 +02:00
config.example.yaml refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
config.gemma-thinking.yaml feat(eval): answer freely by default — max_tokens optional (None = no limit) 2026-06-23 13:06:19 +02:00
config.m1.yaml feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
config.m5-lmstudio.yaml feat(config): config.m5-lmstudio.yaml (M5-64GB + LM Studio :1234) 2026-06-27 13:51:20 +02:00
config.m5.vlm.yaml chore: Woodpecker CI + ruff format + run-config examples 2026-06-19 22:36:41 +02:00
config.m5.yaml feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
config.ndeval.yaml feat(eval): qualitative use-case evaluation (eval + judge + packs) 2026-06-19 20:46:51 +02:00
CONTRIBUTING.md feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
judge.example.yaml docs(judge): record buero A/B result; bump example call_timeout_s for richer holistic call 2026-06-28 21:57:03 +02:00
LICENSE feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
LICENSE-DOCS feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
LICENSING.md refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
pyproject.toml chore(release): 0.2.0 2026-06-30 14:47:10 +02:00
queue.example.yaml fix(queue): address adversarial review (blocker + 4 major + minors) 2026-06-27 23:56:07 +02:00
README.de.md docs: README auf readme-spec-Tier python-cli + Tutorial und How-to-Ebene 2026-07-28 16:48:26 +02:00
README.md docs: README auf readme-spec-Tier python-cli + Tutorial und How-to-Ebene 2026-07-28 16:48:26 +02:00
SECURITY.md feat: Benchmark-Harness llm-ramcheck (Gerüst, alle Module, Spec-Review) 2026-06-15 13:25:37 +02:00
start-gui.sh refactor: rename tool ramcheck → touchstone (P0) 2026-06-24 17:30:32 +02:00
uv.lock chore: sync uv.lock to 0.2.0 2026-06-30 14:49:09 +02:00

llm-touchstone

🇬🇧 English · 🇩🇪 Deutsch

A thin CLI that benchmarks a local LLM through an OpenAI-compatible endpoint — recording TTFT, prefill-tok/s and decode-tok/s with their distribution while a decoupled host sampler tracks macOS memory pressure and thermal throttling, then merges both into one Markdown table (+ CSV). It also evaluates answer quality (deterministic capture + optional LLM-as-judge) and can drive the whole loop from a local web control-center. Same code on an M1 (LM Studio) and an M5 (mlx-lm) — you only swap the config.

License: AGPL-3.0 Docs: CC BY-SA 4.0 Platform

Features

  • Distribution, not the mean — TTFT as P50/P95, decode/prefill as median, consistency as CV%. On Apple Silicon latency jumps at the memory threshold; an average hides exactly that.
  • Decoupled host sampler — memory pressure, swap, power source and thermal throttling are recorded by a separate process and joined by timestamp, never estimated from the request thread.
  • Answer quality, not just speed — a use-case pack (packs/*.yaml: prompts, green/red flags, weighted dimensions, a safety knock-out) runs deterministically; a pluggable LLM-as-judge turns it into a weighted scorecard. A new use case is a new YAML, not new code.
  • System prompts as a measured axis — every prompt runs once per system-prompt variant, so the same run tells you whether your prompt actually helps.
  • Engine-agnostic by construction — the only engine-aware file is client.py. LM Studio, mlx-lm, Ollama and llama.cpp are a config swap, never a code branch.
  • Resumable everywhereeval, judge and the overnight queue all continue where they stopped; nothing is lost to an interrupted run.
  • Cross-machine comparisonaggregate rolls many bundles into one Hardware×Quality table, reporting the model delta (peak minus baseline) that actually compares across machines.
  • Overnight queue — several models sequentially, each with a fresh RAM baseline (unload → settle → eval → judge), with per-step watchdogs.
  • Optional web control-center — configure, start/stop, watch live, compare and export in the browser, as an out-of-process control-plane that never measures in its own process.

Requirements

  • macOS. The measurement core is portable, but the host sampler reads memory_pressure, pmset and powermetrics.
  • Python 3.12+ via uv (brew install uv).
  • A local OpenAI-compatible endpoint — LM Studio, mlx_lm.server, mlx-openai-server, Ollama or llama.cpp. If you don't have one yet, see uplink.jkaindl.de/llm-setup.
  • Optional — passwordless sudo for powermetrics. Without it the throttle flag stays off and throttled runs are not excluded (see How-to: set up a new machine).
  • Optional — the [gui] extra (FastAPI/uvicorn/Jinja) for the web control-center.

Install

git clone https://codeberg.org/jkaindl/llm-benchmark-harness
cd llm-benchmark-harness
uv sync
uv sync --extra gui     # optional: web control-center

Quick Start

uv sync
# Latency benchmark — M1 → LM Studio (:1234), M5 → mlx_lm.server (:8080):
uv run touchstone run    --config config.m1.yaml
uv run touchstone embed  --config config.m5.yaml   # embedding throughput (separate)
uv run touchstone report --runs ./runs             # (re)build report.md from raw.csv

# Quality evaluation — run a use-case pack, then score it:
uv run touchstone eval   --pack packs/ndassist.yaml --config config.m5.yaml
uv run touchstone judge  --bundle runs/<ts>_eval_ndassist --judge-config judge.yaml
uv run touchstone aggregate --runs ./runs          # cross-machine Hardware×Quality table

# Web control-center (optional [gui] extra):
uv sync --extra gui
uv run touchstone gui                              # configure → start → watch → evaluate → compare → export

Usage

Point endpoint.base_url at your local server, set machine/models, and run. Each run sweeps a scenario × context-bucket matrix, discards one warmup per cell, measures a separate cold-start TTFT, and writes a report.md (whose columns paste straight into a comparison note) plus a raw.csv with one row per request.

Two processes stay decoupled by design: the latency runner (requests) and the host sampler (psutil + powermetrics + memory_pressure + pmset), joined afterwards by timestamp. Memory is never estimated from the request thread.

The harness reports distribution, not the mean: TTFT as P50/P95, decode/prefill as median, and TTFT consistency as CV%. Throttled and on-battery runs are flagged and excluded from the aggregates (kept raw in the CSV).

Quality evaluation (the second half). Performance doesn't say whether a model answers well. eval runs a use-case pack (packs/*.yaml — prompts + green/red flags + weighted dimensions + a safety knock-out + system-prompt variants) deterministically on the machine under test, capturing answers and tech-specs; judge then scores them with a pluggable LLM-as-judge into a weighted scorecard (aggregate rolls many bundles into one Hardware×Quality table). Generation and judging are two decoupled phases, both incremental and resumable. A new use case is a new YAML, not new code. Two packs ship today: ndassist (Neurodivergenz-Assistent) and buero (Büro-/Wissensarbeit-Assistent).

Web control-center (touchstone gui). An optional local FastAPI server (the [gui] extra — build-free HTMX/Alpine, isolated from the measurement core) puts the whole workflow in the browser: see what each pack tests and how it's scored, configure and start/stop runs, watch them live, browse results, compare across machines, and export. It is an out-of-process control-plane — it spawns the same touchstone eval/judge subprocesses the CLI does, so the measurement loop stays decoupled and runs/ remains the single source of truth. Bound to 127.0.0.1, one measurement at a time.

macOS note: throttle detection shells out to sudo powermetrics. Allow passwordless sudo for it or the throttle flag stays off (runs are then not excluded as throttled). See AGENTS.md → Gotchas.

Documentation

Docs are in German; the entry point is docs/README.md.

License

Code: AGPL-3.0 — see LICENSE; commercial dual-license option in LICENSING.md. Documentation/text: CC BY-SA 4.0 — see LICENSE-DOCS.