One envelope schema, every upstream eval tool, one public HF dataset.
mlx-benchmarks is the result-envelope contract and publisher for benchmarking MLX-quantized and locally-hosted LLMs on Apple Silicon. It is the thin glue between upstream evaluation tools (lm-eval, vllm benchmark_serving, agent-framework harnesses) and a single public HuggingFace dataset, with a Gradio viewer on top.
What it does
- Defines envelope v1 in
schema.json— the authoritative, versioned contract every published shard validates against. - Provides
mlx-bench-publish, a CLI that converts raw tool output into the envelope, validates it, and uploads to the HF dataset with content-addressed filenames (data/run-<timestamp>-<git_sha>-<suite>-<model_slug>.parquet). - Owns converters for
lm-eval,vllm benchmark_serving, and framework-eval (OpenAI / Qwen-Agent / smolagents / ADK). - Auto-detects runtime metadata (OS, chip, memory, Python, MLX, lm-eval versions) via
detect_system()so envelopes are fully reproducible without hand-curation. - Deploys a Gradio viewer to HF Spaces on every
mainpush touchingspace/.
How it fits
The headline metric is cumulative, not decode-only
Cumulative tokens/second = (prompt tokens + completion tokens) ÷ total wall-clock. That is the headline figure; decode-only rate is a secondary diagnostic. Decode-only throughput hides prefill. A model that reads its prompt several times faster delivers a visibly faster answer, and a decode-only number reports none of that gain — so tuning against it tunes for something the user never experiences. Cumulative counts every token the request touched against the time the user actually waited. The two metrics can rank models differently, which is the point. Report both columns; lead with cumulative.Cumulative is meaningless without its regime
Cumulative tokens/second is highly sensitive to prompt size, because prompt tokens are counted in the numerator and prefill is far faster than decode. The same model, on the same hardware, scored 551 cumulative at a 2291-token prompt and 140.9 at a ~190-token prompt. Neither is wrong; they are answers to different questions. So a cumulative figure without its regime is not a fast or slow result — it is an uninterpretable one: A stated regime is not sufficient on its own, because a regime can match on paper and still differ in practice. On 2026-08-14 a candidate read 37.1 cumulative against the incumbent’s recorded 115.2 — a regime that looked identical on every published field. Re-measuring the incumbent with the same harness, in the same session put it at 138.8, not 115.2: the harness ran hot, and the naive comparison had understated the real gap by roughly 20%. It happened to understate; it could as easily have manufactured a win. Decode-only is the most regime-stable of the three rates, which makes it the honest basis for a cross-model comparison even though cumulative is the headline. When two numbers disagree about which model is faster, check whether they were taken in the same regime before you believe either.A worked comparison, one stated regime
Isolated workers with loaded weights proven from the running command line, one model resident at a time, never through a proxy. Identical regime for all four rows: a unique filler prompt of 170–190 tokens withcached_tokens=0 asserted
per run, max_tokens=300, streaming with usage included. Cumulative is the
median of three runs, with the observed range in brackets.
All four returned a genuine
tool_calls array with
finish_reason: "tool_calls" and correctly parsed arguments.
The reasoning-first model is the cautionary row: it is competitive on every
rate and never exits thinking at max_tokens=300 — zero answer characters
across all three runs, against 1022–1193 characters of reasoning. Fast, and
produces no answer. A throughput table cannot see that; a tool-call and
answer-length assertion can. Measure what the caller receives, not only how
quickly tokens arrive.
Getting started
1
Bring up the inference stack
From the
nix-darwin flake: darwin-rebuild switch --flake .. This starts vllm-mlx + llama-swap on localhost:11434 via nix-ai. Or run vllm-mlx serve directly if you’re not on the Nix stack.2
Install and authenticate
git clone https://github.com/JacobPEvans/mlx-benchmarks && cd mlx-benchmarks && uv sync. Then export HF_TOKEN=... with write scope on the dataset namespace.3
Run a smoke benchmark
Point
lm-eval at the local endpoint:4
Publish (dry-run first)
.venv/bin/mlx-bench-publish ./run-output/<model-dir>/results_*.json --kind lm-eval --suite reasoning --dry-run validates the envelope locally against schema.json. Drop --dry-run to push to the HF dataset.5
View results
Open the HF Space viewer — it auto-loads every published shard. Or
cd space && python app.py for a local copy.Related repos
nix-ai
Packages the inference stack:
vllm-mlx LaunchAgent, llama-swap, MLX module derivations. Where models actually run.nix-darwin
macOS host config. Composes
nix-ai into the system flake so benchmarks have a reproducible environment.ai-assistant-instructions
Model routing + permission policy. Tells AI clients which models to benchmark.
Local LLM
The serving stack, tuning, and model strategy these benchmarks measure.
Source on GitHub
Schema, publisher, converters, full README,
docs/architecture.md.