WattOps Start free
← Blog

How to measure MLX energy per token on Apple Silicon

Guide · Last updated 21 October 2026 · 6 min read

On this pageWhat joules reads on a MacInstallWith the MLX server, in one commandIn your own scriptOr wrap the whole commandMake the number mean somethingCompare two models fairlyPut your Mac on the leaderboardWhat the number covers, and how to check itOther ways to measureNext

A Mac running a model with MLX uses very little power, and that is the point of running it there. To know how little, and to compare models, quantisations and Macs fairly, measure the energy the chip actually used and divide it by the tokens it actually produced.

What joules reads on a Mac

macOS keeps its power meters behind powermetrics, which only an administrator can run. joules asks for your password once (macOS remembers it for a few minutes) and runs only powermetrics with it, never your script. It takes a reading every 200 ms of the chip's combined power: CPU + GPU + Neural Engine, the figure powermetrics reports as "Combined Power". Energy is each reading's average power times its 200 ms, counted only for the part that falls inside what you measured.

It does not see the screen, the SSD, the charger or the rest of the board. That is the right number for comparing models on the same Mac; for what the whole Mac draws from the wall, see the end of this page.

Install

curl -LsSf https://wattops.dev/install.sh | sh && export PATH="$HOME/.local/bin:$PATH"
pip install mlx-lm
joules doctor

Or pipx install landauer-gap. joules doctor asks for your password and shows a live reading from the chip, so you know the meter works before you measure anything.

With the MLX server, in one command

mlx_lm.server speaks the OpenAI API, so joules bench can measure it like any other server:

mlx_lm.server --model mlx-community/Llama-3.2-3B-Instruct-4bit --port 8080

In a second terminal:

joules bench openai:mlx-community/Llama-3.2-3B-Instruct-4bit --url http://localhost:8080/v1 --subtract-idle 5

joules measures the idle Mac for 5 seconds, sends one warm-up request that is not counted (so loading the model is left out), then measures five requests (--runs changes it) of up to 256 tokens (--max-tokens) and divides the energy above idle by the output tokens the server reports. It prints joules per token, tokens per second and the average power. If it says the server returned no output tokens, your version of mlx-lm does not report token counts in its replies; use the script below instead.

In your own script

To measure exactly the generation and nothing else, wrap it in a joules.Session and count the tokens as they come:

import joules
from mlx_lm import load, stream_generate

MODEL = "mlx-community/Llama-3.2-3B-Instruct-4bit"
model, tokenizer = load(MODEL)
messages = [{"role": "user", "content": "Explain in about 200 words why data centres measure power usage effectiveness."}]
prompt = tokenizer.apply_chat_template(messages, add_generation_prompt=True)

def generate(n=256):
    tokens = 0
    for _ in stream_generate(model, tokenizer, prompt, max_tokens=n):
        tokens += 1           # one response per generated token
    return tokens

generate(32)                  # warm-up, not measured

with joules.Session() as s:
    for _ in range(5):
        with s.phase("generate") as p:
            tokens = generate()
        print(f"{p.energy_j:.1f} J for {tokens} tokens: {p.energy_j / tokens:.3f} J per token, {tokens / p.duration_s:.1f} tokens/s")
    print(s.summary()["generate"]["energy_j"])   # median and spread over the five

Run it from Terminal, not from a notebook, so joules can ask for the password.

Or wrap the whole command

No code at all:

joules -- mlx_lm.generate --model mlx-community/Llama-3.2-3B-Instruct-4bit --prompt "Explain power usage effectiveness." --max-tokens 256

mlx_lm.generate prints how many tokens it generated; divide the energy joules prints by that. This counts loading the model too, so it reads higher than the two ways above. It is fine for a quick look, not for a comparison.

Make the number mean something

Compare two models fairly

To compare a 4-bit and an 8-bit model, or two models, load both and alternate them in the same session, so heat and background activity hit both equally:

import joules
from mlx_lm import load, stream_generate

MODELS = ["mlx-community/Llama-3.2-3B-Instruct-4bit", "mlx-community/Llama-3.2-3B-Instruct-8bit"]
loaded = {m: load(m) for m in MODELS}
tokens = {m: 0 for m in MODELS}

def generate(m, n=256):
    model, tokenizer = loaded[m]
    prompt = tokenizer.apply_chat_template([{"role": "user", "content": "Explain power usage effectiveness in 200 words."}],
                                           add_generation_prompt=True)
    return sum(1 for _ in stream_generate(model, tokenizer, prompt, max_tokens=n))

for m in MODELS:
    generate(m, 32)                                   # warm-up, not measured

with joules.Session() as s:
    for _ in range(5):                                # A B A B ...
        for m in MODELS:
            with s.phase(m):
                tokens[m] += generate(m)
    for m, p in s.summary().items():
        print(f"{m}: {p['energy_j_total'] / tokens[m]:.3f} J per token over {p['count']} runs, spread {p['energy_j']['spread_pct']:.1f}%")

To compare two whole commands instead, joules compare "python a.py" "python b.py" alternates them and reports the medians with a p-value; it counts each command's model loading too.

Put your Mac on the leaderboard

The open leaderboard ranks machines against each other on the same model, from real measurements. Add yours with --share:

joules login                                      # once, with an API key from your account
joules bench openai:mlx-community/Llama-3.2-3B-Instruct-4bit --url http://localhost:8080/v1 --share

--share measures idle first and sends only the numbers: the model name, the chip, energy per token, tokens per second and power. Never the prompt, the output or your machine's name. A Mac's row says its meter is the whole chip, so it is never ranked as if it were a GPU alone.

What the number covers, and how to check it

The number is the chip's combined CPU, GPU and Neural Engine power as powermetrics reports it. The screen, SSD, charger and the rest of the board are not in it. To see how it compares with what the Mac draws from the socket, put a plug-in energy meter between the wall and the charger, set the screen to its lowest brightness with the battery full, and let joules do the arithmetic: joules calibrate --seconds 600 --wall-wh <the meter's Wh over those 10 minutes> for a meter that shows a running total, or joules calibrate --wall-csv log.csv for one that logs readings every second. It prints the share of the wall reading the chip's meter saw.

Other ways to measure

joules is not the only way. Zeus, the Python library from the ML.ENERGY group at the University of Michigan, reads Apple Silicon's energy too. You can run sudo powermetrics --samplers cpu_power,gpu_power yourself and add up the readings. A plug-in wall meter is the only one of these that sees the whole Mac. What joules adds is one command around any program or server (joules -- …, joules bench), the same tool and the same numbers on NVIDIA, AMD and Intel machines, joules compare for a fair A/B, and the open leaderboard.

Next