joules: like time, but for energy
Put joules in front of any command and get the real energy it used, read from the hardware's own meters: GPU, CPU and DRAM, what that cost, its carbon, and how far it sits above the Landauer limit of physics. Benchmark a local model and get joules per token, against what an API charges. Open source (MIT), Python standard library only, no dependencies, no account needed.
Install
macOS and Linux (no sudo, no pip; needs Python 3.9 or newer):
curl -LsSf https://wattops.dev/install.sh | sh && export PATH="$HOME/.local/bin:$PATH"
joules doctor
Windows 10 or 11, in PowerShell (no admin rights):
irm https://wattops.dev/install.ps1 | iex
joules doctor
Or with pipx: pipx install landauer-gap (update with pipx upgrade landauer-gap). joules doctor says what this machine can measure, with a live reading from each meter.
One command
$ joules -- python train.py
joules · python train.py · 2.31 h · exit 0
GPU 0 NVIDIA H100 80GB HBM3 4.91 MJ 590 W avg nvml counter
CPU package 0 612 kJ 74 W avg rapl counter
DRAM (package 0) 98.7 kJ 12 W avg rapl counter
──────────────────────────────────────────────────────────
total 5.62 MJ = 1.56 kWh
cost 0.156 at 0.1/kWh · CO₂ 577 g at 370 g/kWh
The command's exit code passes through, so joules works in scripts and CI. Add --params 8 --tokens 2 (billions) to a training run and it also reports the Landauer gap, your effective utilisation and power draw, and a link that opens the calculator with your hardware's real numbers. (Illustrative output; the numbers depend on the machine.)
Is local cheaper than the API?
$ joules bench ollama:llama3.1:8b --api-price 0.20
per token 3.74 J · 1.04 kWh per 1M tokens
electricity 0.104 per 1M output tokens at 0.1/kWh
vs API 0.2 per 1M → electricity is 1.9× cheaper than the API (hardware cost not included)
joules bench works with Ollama and with any OpenAI-compatible server (vLLM, llama.cpp, LM Studio, TGI) through openai:MODEL --url http://host:8000/v1. --subtract-idle 5 measures the idle machine first and reports the energy above it. --share adds your result to the open leaderboard of measured joules per token: medians of everyone's runs, no names.
What it measures
| Hardware | Meter | Who can read it |
|---|---|---|
| NVIDIA GPUs | NVML energy counter, or sampled power on older cards | Any user |
| AMD GPUs | amdgpu energy or power through hwmon | Any user on most distributions |
| Intel GPUs | hwmon energy | Any user on most distributions |
| CPU and DRAM (Intel, AMD) | RAPL counters | Root, or one chmod a+r that joules doctor prints |
| Apple Silicon | powermetrics | sudo, asked once |
| Anything else | Estimated from CPU time, and the output says so | Any user |
On NVIDIA GPUs every measurement also reads the clocks, temperature, power limit, busy and memory-controller percentages and the driver's throttle reasons, and prints what they mean: compute-bound · throttled 62% of the time (thermal). Throttled means below the maximum clock for a reason other than being idle, which is performance you paid for and did not get.
The cheapest token
A GPU's power rises faster than its speed as the clock climbs, so a lower power limit often buys a large energy saving for a small delay (the Zeus result from the ML.ENERGY group, 2023). joules tune measures it on your card instead of assuming it:
$ sudo joules tune ollama:llama3.1:8b
limit W tok/s J/token GPU W avg
100% 700 41.2 3.21 612
85% 595 40.8 2.74 522
70% 490 38.9 2.33 451
55% 385 31.0 2.19 359
best: 70% of the limit uses 27% less energy per token and is 6% slower (within --max-slowdown 10%)
set it with: sudo nvidia-smi -pl 490 (per GPU; nvidia-smi -pl 700 puts it back)
(Illustrative output.) The limit is always put back when tune ends.
Meters measure whole devices, so other load on the machine is included; --subtract-idle helps. Power supply losses, fans and the screen are not measured; --pue adds a facility overhead. Every limit is in the output, never hidden.
Commands
| Command | What it does |
|---|---|
joules -- CMD … | Measure a command. Exit code passes through. |
joules bench ollama:MODEL | Energy per token of a local model. Starts Ollama and pulls the model if needed. |
joules ci --budget 10 -- CMD … | Energy check for a pull request: this change against the base branch, as a PR comment. |
joules proxy --upstream URL | An energy receipt (X-Energy-Joules headers) on every response of a model server. |
joules agent | Energy per job on a GPU server or cluster node: Slurm jobs, Kubernetes pods, containers, users. Prometheus metrics. |
joules doctor | What this machine can measure, with a live reading from each meter. Shareable: no key, host name or user in it. |
joules tune ollama:MODEL | Try several GPU power limits and report the one with the cheapest token that is at most 10% slower. Puts the limit back. Needs sudo. |
joules calibrate --wall-wh X | Compare the hardware meters with a wall-plug meter over the same time, and record what they cover. |
joules login KEY | Save a WattOps API key so --track and --share work from any terminal. |
What it sends
Nothing, unless you ask. --track sends a run's energy, time, cost, hardware and a short label (the program and script name) to your private Tracking page. --share sends a benchmark's model, hardware and energy per token to the leaderboard. Never prompts, outputs, arguments, code, file paths or file contents. The full list is on the security and data flow page.
Keep your runs over time
A free WattOps account adds a Tracking page: every tracked run, the Landauer gap over time, energy per project, CSV export, and an energy check for every pull request.
joules login lgk_… # once; Settings → API keys shows this line
joules run --track llama-ft --params 8 --tokens 2 -- python train.py
Create a free account · PyPI: landauer-gap · PR energy check action
Running a fleet?
The paid plans are for GPU servers and clusters: joules agent on every node, idle GPUs found and priced, energy and cost per job, team and lab, and audit-ready energy and carbon reports. See Live or book a demo.