EcoCompute · Notes

It was never the format. It's the kernel.

2026-08-31 · Hongping Zhang

MLPerf Client v2.0 shipped with signed Windows and macOS builds, agentic tasks, and a much wider hardware matrix — a lot of people can now benchmark a local LLM on their own machine without building anything. What comes out is speed: time to first token, tokens per second. Its power path is a different animal, needing a Yokogawa analyzer, SPEC PTDaemon and a second machine, so in practice a home run gives you throughput and nothing about joules.

We wrote a page about that gap last week, and it ended with a promise: our own energy numbers were measured with transformers + bitsandbytes NF4, while MLPerf Client's NVIDIA path is llama.cpp with a GGUF Q4_0 file, so the two could not be read against each other; putting a GGUF path next to ours was "the next thing on our list". This note is that measurement. It came back more interesting than we expected.

The result. On one RTX 4090, decoding Llama-3.1-8B-Instruct, GGUF Q4_0 under llama.cpp costs 63.6% less energy per token than GGUF F16 under the same binary, for +5.6% perplexity. On the same card, bitsandbytes LLM.int8() costs 106% more energy per token than its FP16 baseline. Both are weight-only quantization of a 7–8B model on one GPU. The bit width does not tell you the sign of the energy effect. The kernel does.

What we ran

One rented RTX 4090 (Ada, 24 GB, 450 W limit), one llama.cpp build (b10643-192067b72, CUDA 12.x), one model, three GGUF files:

Each file was run at -n 64 and -n 576, three times each, batch 1, -ngl 99 -fa 1 --no-mmap -c 2048 --temp 0 --seed 1234, with GPU package power sampled from NVML at 10 Hz around the whole llama-cli process. Eighteen runs. Quality is a separate llama.cpp perplexity pass over the WikiText-2 raw test set, 564 chunks at context 512, which touches the energy figure not at all.

The two token counts are the point. A raw process energy total includes loading the weights — 16 GB for F16 against 4.7 GB for Q4_0 — which hands the quantized arm a structural head start that has nothing to do with decoding. Subtracting the 64-token run from the 576-token run leaves the energy of exactly 512 additional decoded tokens, with load and prefill cancelling out. Every number below is that difference.

The numbers

llama.cpp armmJ / tokentokens / Jtok/sdecode powerΔenergy vs F16perplexityΔppl
F1648150.20863.8307 W7.3260
Q4_0 (ours)17540.570168.6296 W−63.6%7.7364+5.60%
Q4_0 (MLPerf Client v2.0 file)17950.557172.6310 W−62.7%7.7366+5.61%

Decode-only, by differencing the 576- and 64-token runs; n = 3 per cell. SD of the differenced energy is 7–52 J (0.8–2.1% of the value). Perplexity is ±0.047–0.050 (1σ) on 564 WikiText-2 chunks; absolute values depend on the corpus and tokenizer and are not comparable with published WikiText numbers — only the within-row delta is. Raw rows: rtx4090_llamacpp_gguf_2026-08-31.csv, aggregates in the .summary.csv beside it.

The saving is throughput. It is not lower power.

Look at the decode power column before the energy column. All three arms pull 296–310 W while decoding — within a few percent of each other, on a card capped at 450 W. The GPU is about equally busy in all three cases. What changes is that Q4_0 moves a quarter of the bytes per token and runs at 2.6–2.7× the tokens per second. Energy per token is power divided by throughput, so the entire 63.6% is the throughput term.

That is the same mechanism, running the other way, that we reported for INT8: there too package power barely moved (74–91 W), and throughput fell to 0.63–0.72× of FP16, so energy per token went up. Which is the whole argument of this note:

Three weight-only quantizations of a 7-8B model on one RTX 4090: bitsandbytes LLM.int8() costs 105.8% more decode energy per token than FP16, bitsandbytes NF4 saves 39.0%, and llama.cpp GGUF Q4_0 saves 63.6%. Their perplexity costs, +1.19%, +12.06% and +5.60%, rank them differently again.
Three weight-only quantizations of a 7–8B model, all on an RTX 4090 (Ada), each against an FP16 baseline measured in its own session. Energy spans +106% to −64%; the quality cost orders them differently again. Regenerate with python3 build/make_kernel_figure.py.

Three implementations of "keep the activations, shrink the weights", one card, and the answer to "does quantizing save energy?" is +106%, −39% and −64% depending only on which kernel executes the result. LLM.int8() pays for its outlier-preserving mixed-precision decomposition in decode throughput; llama.cpp's Q4_0 path is a memory-bandwidth win on a bandwidth-bound workload and collects nearly the full ratio. A number like "4-bit" or "8-bit" predicts the memory footprint. It does not predict the sign of the energy effect, and anyone quoting an energy saving without naming the runtime is quoting a property of their kernel.

The quality axis does not rescue the ranking either. Ordered by energy the winner is llama.cpp Q4_0; ordered by perplexity damage it is LLM.int8() at +1.19%, which is also the worst on energy. There is no single "efficiency" ordering here, which is why this site publishes the two columns side by side and refuses to fuse them into one score.

A zero result worth publishing: our Q4_0 and MLPerf Client's are the same file, in effect

The two Q4_0 arms differ by 0.0002 in final perplexity — 7.7364 against 7.7366. Chunk by chunk across all 564, the largest absolute difference is 0.0003, and 472 of them agree to three decimals. On energy they differ by 1.4–2.3%, which is inside our run-to-run spread, and the sign of that gap flips depending on whether the cold-start runs discussed below are excluded. Both variants are in the summary CSV, deliberately.

So: we are not claiming our quantization is more efficient than the one MLPerf Client ships. It isn't, within this measurement. What the comparison buys is provenance — the numbers in the table are not an artifact of one particular GGUF file, and anyone who quantizes Llama-3.1-8B to Q4_0 themselves should land where the shipped file lands.

What this does not show

If you are running MLPerf Client v2.0 this week

The two things measure different quantities and compose rather well on one machine. Run the signed client for speed on the workloads MLCommons has defined; run the container for the joules and the perplexity of a precision pair you choose, with the FP16 baseline measured in the same session:

docker run --rm --gpus all -v "$PWD/out:/out" \
  ghcr.io/hongping-zh/ecocompute-mlcube:latest \
  --model Qwen/Qwen2-7B --precision NF4 --output_dir /out

Keep the boundary in view when you put the two reports side by side: client tokens/second is a whole-system, vendor-optimized number; our joules are GPU-package only. And the container's current backend is transformers + bitsandbytes, so it reproduces the method of this note, not the llama.cpp rows — a GGUF backend, which is what would let anyone reproduce the table above with one command, is now the top of our list. All eighteen runs are in the per-run CSV linked above, with the undifferenced process energy, the first and peak power sample and a cold_start flag on each, so you can redo the arithmetic differently; drop your own result on the replications page, especially if it disagrees.