Skip to main content

joules

Like time, but for energy. Wrap any command and get the real GPU, CPU and DRAM energy it used, 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, and how that compares with an API's price.

Illustrative output:

$ 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
$ 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)

No dependencies: only the Python standard library and your GPU driver.

Install

macOS and Linux:

curl -LsSf https://landauer-gap.vercel.app/install.sh | sh && export PATH="$HOME/.local/bin:$PATH"
joules doctor                                # what can be measured here, with a live reading from each meter

Windows 10 or 11, in PowerShell (no admin rights):

irm https://landauer-gap.vercel.app/install.ps1 | iex
joules doctor

The installers need only Python 3.9+ (no sudo, no pip) and update joules when run again. Or use (no sudo, no pip) and updates joules when run again. Or use pipx: pipx install landauer-gap (update: pipx upgrade landauer-gap). Latest from GitHub:

pipx install --force "git+https://github.com/bsharma173860-oss/d3-research#subdirectory=apps/landauer-gap/joules"

Commands

Command What it does
joules -- CMD … / joules run -- CMD … Measure a command. Its exit code passes through, so it works in CI.
joules bench ollama:MODEL Energy per token of a model served by Ollama. joules starts Ollama if it is installed but not running, and downloads the model the first time.
joules bench openai:MODEL --url http://host:8000/v1 Same for any OpenAI-compatible server: vLLM, llama.cpp, LM Studio, TGI.
joules run --track PROJECT -- CMD … Measure and save the run to your private Tracking page. Also on bench.
joules bench … --share Add the result to the public leaderboard.
joules share FILE Share a result saved earlier with --out FILE.
joules ci --budget 10 -- CMD … Energy check for a pull request: this change vs the base branch.
joules proxy --upstream URL Energy receipt 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.
joules devices List the meters joules found, and why any are missing.
joules doctor Check this machine: every meter with a live reading, GPU drivers, CPU counters, API key, connection, Ollama. The report has no key, machine name, user name or home folder, so it can be shared as it is.
joules login KEY / joules logout Save your API key on this computer, or remove it. After login every joules run and joules bench on this computer appears on your Tracking page (project default) without --track; --no-track or JOULES_NO_TRACK=1 keeps a run local. A key from --key or $LANDAUER_API_KEY (scripts, CI) tracks only with --track.

Useful options: --json / --out FILE (machine-readable result), --subtract-idle 5 (measure the idle machine first and also report energy above idle), --gpus 0,1, --price, --grid fr or --ci 55, --pue 1.2.

Tracking your runs over time

joules login lgk_…                              # once: saves your key (Settings → API keys shows this line)
joules run --track llama-ft --params 8 --tokens 2 -- python train.py
joules --track nightly -- ./train.sh            # short form

Each tracked run appears on the Tracking page within seconds: Landauer gap over time, energy per run and totals for the project. joules sends the energy, time, cost, CO₂, hardware, operations and gap, and a short label: the program and script name (python train.py) or your --label. It never sends arguments, inline code, outputs or file contents. A tracking problem is reported but never changes the command's exit code, so it is safe in CI.

Energy check on every pull request

joules ci runs a command on your checkout and on the base branch (a temporary git worktree), alternating runs so drift hits both sides, and compares the medians. It writes a Markdown summary, can post one pull request comment that it updates on each push, and fails when energy grows by more than --budget percent. A change smaller than the run-to-run spread is reported as noise, never as a failure.

joules ci --base main --runs 3 --budget 10 -- pytest -q

In GitHub Actions use the PR energy check action. On hosted runners, which have no energy counters, energy is estimated from CPU time and the comment says so; on a self-hosted runner with a GPU or RAPL it is measured. joules run --estimate uses the same estimate when a machine has no counters.

Energy receipts for AI responses

joules proxy sits in front of a model server and measures the hardware while each request runs. Point your client at the proxy instead of the server; nothing else changes.

joules proxy --upstream http://localhost:11434          # Ollama
joules proxy --upstream http://localhost:8000 --subtract-idle 5   # vLLM, measured above idle
$ curl -si localhost:8787/v1/chat/completions -d '{"model":"llama3.1:8b","messages":[…]}'
X-Energy-Joules: 41.2
X-Energy-Output-Tokens: 212
X-Energy-Joules-Per-Token: 0.194
X-Energy-CO2-Grams: 0.00423
X-Energy-Receipt: /joules/receipts/r-000017
  • Streaming responses (SSE or Ollama's NDJSON) pass through untouched and carry X-Energy-Receipt; the receipt at that address is complete when the stream ends.
  • Tokens come from the response (usage, or Ollama's eval_count). Without them, streamed pieces are counted and the receipt says tokens_exact: false.
  • Meters measure whole devices, so in each sampling interval the energy is shared equally between the requests running at that moment. With --subtract-idle receipts also carry energy above idle.
  • /joules/receipts lists the newest receipts, /joules/metrics serves Prometheus metrics, and /joules/health says what is measured.
  • --track PROJECT sends one summary per model every 5 minutes to your Tracking page: never prompts or outputs.
  • It listens on 127.0.0.1 by default. --host 0.0.0.0 exposes it, and your model server, to your network.

Energy per job on GPU clusters

joules agent runs on every node of a cluster, measures it every second and splits each device's energy between the jobs on it: NVIDIA GPUs by the GPU memory each process holds, AMD and Intel GPUs by GPU time, CPUs and DRAM by CPU time. Jobs are Slurm jobs, Kubernetes pods, containers or users' programs. GPUs a job holds but barely uses are reported as idle GPU energy.

sudo joules agent                         # http://127.0.0.1:9877/jobs and /metrics (Prometheus)
sudo joules agent --track cluster         # each finished job on your Tracking page

A Kubernetes DaemonSet, a systemd unit for Slurm nodes, Prometheus scrape and alert rules and a Grafana dashboard are in integrations/cluster-agent.

The leaderboard

joules login lgk_…                           # once; free key: landauer-gap.vercel.app → Settings → API keys
joules bench ollama:llama3.1:8b --subtract-idle 5 --share

The leaderboard ranks models and machines by measured joules per output token, using the median of everyone's runs. joules sends only the model name, hardware name and count, energy per token, tokens per second, average power, output tokens, whether idle power was subtracted, which meters were used and the joules version. It never sends prompts, outputs, host names or paths. Simulated runs are refused, and you can remove your own entries on the Leaderboard page.

Estimate vs measured, and calibration

Tell joules the size of the training job and it compares the measurement with the Landauer Gap estimate, then works out what your hardware really achieved:

$ joules run --params 1 --tokens 2 -- python finetune.py        # 1B model, 2B tokens, one H100
  estimate vs measured  17 MJ estimated (40% MFU, 80% draw) → 14.8 MJ measured (-13%)
  your hardware ran at 42.5% effective utilisation and 74% of rated power
  open in the calculator with these numbers: https://landauer-gap.vercel.app/?s=…

The link opens the website calculator with your measured utilisation and power draw, so every future estimate starts from your real hardware instead of a textbook default. If the job you describe could not fit in the measured time, joules says so instead of reporting nonsense.

What it measures, and how

Hardware Source Kind
NVIDIA GPUs NVML (driver library, via ctypes) energy counter on Volta and newer; power sampling on older cards
AMD GPUs (Instinct, Radeon) amdgpu hwmon (energy1_input, else power1_average) counter or sampling
Intel GPUs (Arc, Flex, Max) i915 / xe hwmon energy1_input energy counter
Intel and AMD CPUs, DRAM RAPL (/sys/class/powercap) counter, wraparound handled
Apple Silicon Macs powermetrics (asks for your password once; the command still runs as you) sampling: CPU + GPU + Neural Engine
Intel Macs powermetrics (asks for your password once) sampling: CPU package (cores, integrated GPU, DRAM)
Windows NVML for NVIDIA GPUs; the CPU is estimated from CPU time (Windows gives programs no CPU energy counter without a driver) GPU counter; CPU estimate, labelled

Where there is no counter (cloud VMs, CI runners, Windows CPUs), joules estimates energy from CPU time and labels every such result ESTIMATE; estimates are never added to the leaderboard.

Meters cover whole devices, so anything else running on the same GPU or CPU is included. Use --subtract-idle, and keep the machine otherwise quiet, for the cleanest numbers. Power supply losses, fans, networking and cooling are not measured; --pue adds a facility overhead.

CUDA_VISIBLE_DEVICES is respected, with indices, GPU UUIDs (as Kubernetes and Slurm set them) or MIG slices (MIG measures the whole GPU). On AMD, HIP_VISIBLE_DEVICES / ROCR_VISIBLE_DEVICES and --gpus pick cards. Reading RAPL on recent Linux kernels needs root, or sudo chmod a+r /sys/class/powercap/intel-rapl:*/energy_uj.

Tests

PYTHONPATH=src python3 -m unittest discover -s tests -v

The real sysfs layouts are rebuilt in a temporary directory (including counter wraparound), the NVML binding runs against a compiled stand-in for libnvidia-ml.so.1, and model servers are local HTTP stand-ins. JOULES_SIMULATE="gpu:H100:650,cpu:package 0:80" runs everything on constant simulated power for demos; results are always labelled SIMULATED.

Metadata

Release files for landauer-gap 0.5.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for landauer-gap 0.5.4
File Size Uploaded
landauer_gap-0.5.4.tar.gz 85.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for landauer-gap 0.5.4
File Interpreter ABI Platform
landauer_gap-0.5.4-py3-none-any.whl Python 3 none any Details

Total release size: 149.5 kB

Release files / landauer_gap-0.5.4.tar.gz

Download URL landauer_gap-0.5.4.tar.gz
Size 85.9 kB
Tags Source
SHA-256 checksum
How to use checksums
fbe8771e003002af1b16411d24f13f4b088d6d599d61b88f3f14552cbb7abcd0
BLAKE2b-256 checksum
How to use checksums
ea4e5126ce6c448d4219127194c3c4281857fd4f36eb04ebc9b0eca89e41ef44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release files / landauer_gap-0.5.4-py3-none-any.whl

Download URL landauer_gap-0.5.4-py3-none-any.whl
Size 63.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf90e010cbd82a00be96af6247d59c2bd340fd90d607745efa8db2de6075719c
BLAKE2b-256 checksum
How to use checksums
cfe6dbc3cb968930d11dc91178197fd085f57a558ac0e1646fbe796524df3480
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.4 This release

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page