← Dashboard

Cluster Carbon — Methodology

Overview

This dashboard estimates the carbon footprint of large language model (LLM) inference running on the lab's Kubernetes cluster (boettiger-lab/k8s) — one card per served model, whether it is running now or not. A model is identified by its vLLM served name and the node whose API server exports its metrics (qwen@nimbus, deepseek-v4-flash@nimbus2); a node that switches models gets a card for each, and a model scaled to zero keeps its card, with its record, for as long as Prometheus retains its data. All measurements are derived passively from existing telemetry — no instrumentation of the LLM service is required.

Key principle: Carbon = Energy × Grid Intensity. We measure GPU energy directly from hardware sensors already reporting to the cluster's Prometheus instance, then multiply by published grid carbon intensity for each node's location.

Step 1 — Measuring GPU Power

GPU power draw is read from NVIDIA DCGM Exporter (Data Center GPU Manager), which runs on every GPU node in the cluster. DCGM reads the hardware power sensor via NVML (nvmlDeviceGetPowerUsage) and exports it to Prometheus as:

DCGM_FI_DEV_POWER_USAGE{Hostname, namespace, pod, ...}  # watts, one series per GPU

Power is summed per node, and then attributed to whichever model is serving on that node at that moment. A model is "serving" while its vLLM server is exporting metrics (its vllm:num_requests_running gauge exists):

-- node GPU power, with a tensor-parallel worker's watts relabelled to its head
sum by (node) (
  label_replace(
    label_replace(DCGM_FI_DEV_POWER_USAGE{Hostname=~"cirrus|nimbus|nimbus2|nimbus4|nimbus3"},
                  "node", "$1", "Hostname", "(.*)"),
    "node", "nimbus2", "Hostname", "nimbus4")
)
-- ... joined onto the model serving there
  * on (node) group_right ()
(max by (node, namespace, model_name) (vllm:num_requests_running{...}) * 0 + 1)

The live figure wraps DCGM in avg_over_time(...[5m]) to smooth short spikes. Measurements are scraped every 30 seconds.

Power is node-total, attributed by time. DCGM attributes each GPU's watts to whichever pod its pod-resources mapping picked, which cannot be split fairly when a card is time-sliced across several tenants — and on nimbus it names an unrelated pod entirely. So the dashboard never reads per-pod power: it takes the node's total and gives it to the model serving there. Where a node's GPUs are shared (cirrus), that is an upper bound for the model. Time the node spends serving no model — cirrus's speech-to-text, say — is attributed to nothing and stays out of every total.

Tensor-parallel models span nodes. DeepSeek-V4-Flash runs TP2 across nimbus2 and nimbus4. Only the head (nimbus2) exports vLLM metrics, but both ranks draw power, so the node config lists "power_hosts": ["nimbus2", "nimbus4"] and the worker's watts are relabelled to the head before the join. A host can be claimed by only one node, so no joule is counted twice.

Limitation: We measure GPU power only. CPU, RAM, storage, and networking power are not included. For GPU-heavy inference workloads this typically represents 70–85% of total server power. A full datacenter PUE factor (usually 1.1–1.5) is also not applied. Our figures are therefore conservative underestimates of total facility energy.

Step 2 — Token Throughput (Prompt + Generation)

Token throughput is read from vLLM's built-in Prometheus metrics. Two counters are tracked separately and summed into a total token rate:

-- Output (decode) tokens generated per second (2-minute window --
-- short enough to track our bursty single-user traffic without
-- diluting a burst across mostly-idle time on either side of it):
sum by (node, namespace, model_name) (
  rate(vllm:generation_tokens_total{namespace=~"vllm",node=~"cirrus|nimbus|nimbus2|nimbus3"}[2m])
)

-- Input (prefill) tokens processed per second:
sum by (node, namespace, model_name) (
  rate(vllm:prompt_tokens_total{namespace=~"vllm",node=~"cirrus|nimbus|nimbus2|nimbus3"}[2m])
)

Why the node filter matters: every model runs in the same vllm namespace. Grouping by namespace alone would sum one node's tokens into the other's row while its watts stayed node-scoped, understating CO₂ per token. Every vLLM query here is filtered and grouped by node and namespace.

Both phases consume GPU energy: the prefill phase runs attention over every input token before the first output token is produced; the decode phase generates one output token at a time. Counting only output tokens would therefore misrepresent the true cost of inference — especially for agentic workloads where a single user-visible response may involve tens of thousands of input tokens across multiple hidden tool calls.

Why this matters for agentic AI: In agentic workloads, a single user-visible response may involve tens of thousands of input tokens across multiple hidden tool calls. Empirical studies confirm this dramatically. A 2025 measurement study (arXiv:2506.04301) found that agentic reasoning frameworks (Reflexion, LATS) consume 62–137× more energy per task than single-turn queries. A tokenomics study of multi-agent software engineering (arXiv:2601.14470) found that 53.9% of all tokens processed are input (prefill) tokens, with an input-to-output ratio of roughly 2:1. Real-world API data (OpenRouter, 2025) shows that programming prompts — the dominant agentic use case — routinely exceed 20,000 input tokens and grew ~4× in average length from early 2024 to late 2025.

If CO₂/token were computed over output tokens only, it would overstate the per-token cost by 10–20×. The dashboard uses total tokens (input + output) as the denominator, so reported CO₂/token reflects the full energy cost amortized across every token the GPU processed.

The CO₂ per token metric is only reported when total throughput (input + output) is at least 5 tok/s. Below this threshold the ratio is dominated by near-idle power draw rather than the model's efficiency under real load.

Live and aggregate views

A single toggle switches every card between two records:

How aggregates are computed

Every aggregate is a ratio of totals over the window — never an average of ratios — so a few minutes at 5 tok/s cannot outweigh hours at 3,000 tok/s. Energy integrates the attributed power above at one-minute resolution:

energy_J       = sum_over_time((attributed_power)[W:1m]) × 60
serving_s      = count_over_time((serving)[W:1m]) × 60
tokens         = increase(vllm:prompt_tokens_total[W]) + increase(vllm:generation_tokens_total[W])
mean power     = energy_J / serving_s
CO₂/token      = energy_J / 3.6e6 × intensity / tokens          (all-in)
CO₂/token work = energy_J_while_>5_tok/s / 3.6e6 × intensity / tokens   (marginal)
decode tok/s   = increase(generation_tokens) / increase(inter_token_latency_seconds_sum)
uptime         = serving_s / (window, clipped to retention)

All-in CO₂/token counts every joule the model burned while loaded, idle hosting included — what the model actually cost. Working CO₂/token counts only minutes above 5 tok/s: the marginal cost of a token when the model is busy. Both use the same token denominator. Nothing is held in memory, so a restart loses nothing and needs no backfill.

Step 3 — Carbon Intensity

Every node is in one place: Berkeley, CA. Grid carbon intensity is therefore a fixed constant rather than a lookup table — the EPA eGRID 2022 average for the CAMX subregion (California):

LocationeGRID SubregionIntensity (kg CO₂/kWh)
Berkeley, CACAMX0.198

Step 4 — Carbon Calculations

Three derived metrics are computed:

CO₂ grams per hour

CO₂_g/hr = P_watts × intensity_kg/kWh
         = P_watts × intensity   # because W × kg/kWh = W × kg/(1000W·h) × 1000 = g/h

CO₂ milligrams per token (total tokens)

total_tokens_per_sec = prompt_tokens_per_sec + generation_tokens_per_sec

CO₂_mg/token = P_watts × intensity_kg/kWh × (1e6 mg/kg) / (3.6e6 J/kWh) / total_tokens_per_sec
             = P_watts × intensity × 0.2778 / total_tokens_per_sec

The denominator is total tokens processed (prompt + generation), not output tokens alone. This is the correct normalization: the GPU expends energy on every token it touches — both the input context during prefill and each generated token during decode.

Commercial frontier comparison — per-model physics estimate

Rather than comparing against a fixed hypothetical workload, the dashboard computes a per-model, apples-to-apples power comparison: for each of our models' actual observed token throughput (prompt and generation rates), what total power would a commercial frontier model draw to serve the same tokens at the same rate?

Key principle: The comparison holds everything fixed except model size — same tokens, same traffic level, same grid. The frontier model's CO₂/token is computed using the same grid carbon intensity as the model it's compared against. This isolates the model size/energy tradeoff: a larger frontier model may produce higher-quality output, but requires substantially more hardware — and therefore more energy — to host and run. Because grid intensity is the same on both sides, the CO₂ ratio equals the watts ratio.

Commercial frontier model specification

We model a commercial frontier LLM as a ~1.5 T parameter Mixture-of-Experts (MoE) with ~300 B active parameters per token, served in FP8 on H100-80GB GPUs. This is an informed estimate, not a known architecture — commercial providers (OpenAI, Anthropic, Google) do not publicly disclose hardware configurations or per-query power measurements. The closest available ground truth is Google's measured production figure of 0.24 Wh per median Gemini prompt (arXiv:2508.15734, August 2025), covering accelerators, CPU, DRAM, and datacenter overhead. An independent bottom-up analysis estimates a median of 0.34 Wh per query (IQR: 0.18–0.67 Wh) for models exceeding 200 B parameters on H100 hardware (arXiv:2509.20241). Our physics-based idle floor of ~5,400 W for 24× H100 GPUs is consistent with these per-query figures at realistic throughput: at 50 tok/s total and 0.34 Wh/query (~700 tokens), the implied power draw is ~87 W above the idle floor, broadly consistent with our marginal energy estimates below.

ParameterValueDerivation
Total parameters~1.5 TFrontier MoE estimate (2026)
Active parameters/token~300 BTypical MoE activation ratio
Weight memory (FP8)~1.5 TB1 byte/param × 1.5 T params
Minimum GPUs24× H100-80GB1.92 TB total HBM for weights + KV cache + activations
Idle power225 W/GPU × 24 = 5,400 WH100 SXM measured idle

Marginal token energy (above idle)

PhaseMarginal energyDerivation
Prefill (input tokens)0.5 J/tokenCompute-bound: 600 GFLOPS/token, 24× H100 at 60% util → 23,700 tok/s; additional power (700−225)×24 = 11,400 W → 0.48 J/tok
Decode (output tokens)6 J/tokenMemory-bandwidth-bound at low batch (B≈1–4, matching our traffic): must load 300 GB active weights per step; 80.4 TB/s → 270 tok/s; ~3,000 W additional → ~11 J at B=1, ~4 J at B=4

Per-model comparison formula

frontier_watts = 24 × 225                                   # idle floor: 5,400 W
               + prompt_tokens_per_sec × 0.5                 # marginal prefill
               + generation_tokens_per_sec × 6.0             # marginal decode

ratio = frontier_watts / nrp_measured_watts

This formula is applied to each of our models using its actual observed prompt and generation token rates while working (tokens over working time in the selected window), against its measured working power — see where the comparison appears.

Example: nimbus at typical light traffic

nimbus measured: 11 W  (1× GB10, real DCGM reading)
Prompt rate:      40 tok/s
Generation rate:   1 tok/s

Commercial frontier equivalent:
  Idle floor:    24 × 225 = 5,400 W
  Prefill:       40 × 0.5 =    20 W
  Decode:         1 × 6.0 =     6 W
  Total:                     5,426 W

Ratio: 5,426 / 11 = 493× — nimbus uses 493× less energy for the same tokens.

Why this is fair: Both sides include hosting costs. nimbus draws ~11 W to keep its single GB10 powered; the commercial frontier would draw ~5,400 W just to keep 24× H100 GPUs powered and its models loaded in memory. The comparison reflects the real energy cost of maintaining each capability, not an idealized marginal-only estimate for one side — and it is not a judgment of "utilization" on either side: nimbus idling low is exactly what a well-designed single-user desktop-class chip should do, and a large commercial cluster's idle floor reflects it serving many concurrent users, not being poorly managed.

What this doesn't measure: our figures are GPU power only (no CPU, DRAM, PUE). Applying a typical 1.5–2× system-overhead correction to both sides would preserve the ratio. Commercial cloud PUE (~1.1–1.3) vs. research facility PUE may differ slightly but does not change the order-of-magnitude comparison.

Each card's aggregate view also shows tokens in and out and the mean tokens per request, for transparency about the workload driving the comparison.

Where the comparison appears

In the aggregate view, a card whose model did any work in the window shows one line — "~N× less energy than a frontier-scale deployment (estimate)" — computed as frontier_watts / measured_working_watts, with the frontier side fed the model's own working-time prompt and generation rates. It is informational, not a verdict, and is not colored. No per-model number on the page is colored against the others either: the models are different sizes on different hardware, and ranking the GB10s against a pair of RTX 8000s would present that as a judgment.

Energy per token (J/token)

J/token (joules per token) is a hardware- and grid-neutral measure of energy spent per token over the selected window, all-in like CO₂/token:

J_per_token = energy_J / (prompt_tokens + generation_tokens)

Unlike CO₂/token, this metric is independent of grid carbon intensity, making it directly comparable across models regardless of where they are hosted. J/token reflects batch size and workload as much as it reflects GPU health — it is not a "utilization" verdict. A personal, single-user box naturally runs higher J/token than a heavily-batched datacenter server, because personal single-stream inference is latency-bound, not throughput-bound: there's no queue of other requests to batch alongside, and there shouldn't be. Key drivers of J/token include:

Live activity metrics

Beside the carbon figures, each card reports the model's engine state and service quality, read directly from vLLM and DCGM. Engine gauges are absolute readings — a zero is as meaningful as any other value — so they are not gated on throughput. Latency percentiles are gated on request count (below):

MetricSourceNotes
Running / Queued requestsvllm:num_requests_running / vllm:num_requests_waitingLive concurrency and backlog, read as instant gauges — no averaging window
KV Cachevllm:kv_cache_usage_percInstant gauge, ×100 for display as a percentage
GPU UtilDCGM_FI_DEV_GPU_UTILCompute utilization %, averaged over 5 minutes (same smoothing window as the GPU Power tile) — a useful complement to Power: high power with low utilization suggests memory-bandwidth-bound decode, not idle waste
Requests / hourvllm:request_success_total Summed across all finished_reason values (stop, length, abort, error, repetition), rate over 15 minutes ×3600 — a genuine usage-volume counter, distinct from tokens/sec
TTFT, end-to-end latency, queue timevllm:time_to_first_token_seconds, vllm:e2e_request_latency_seconds, vllm:request_queue_time_seconds histogram_quantile over the 15-minute window (live) or the whole window (aggregate). Withheld below 20 completed requests: on bursty single-user traffic, a p95 of two requests is two data points dressed up as a statistic. Values are interpolated within vLLM's histogram buckets, so treat them as approximate
Prefix-cache hitsvllm:prefix_cache_hits_total / vllm:prefix_cache_queries_totalShare of prompt tokens served from the KV prefix cache. High on agentic traffic, where each turn resends the conversation so far
Uptime, peak concurrency, tokens per requestDerivedAggregate only: share of the window the model was serving; most requests running at once (1-minute resolution); mean prompt and generated tokens per completed request
Availability stripvllm:num_requests_running, vllm:generation_tokens_totalThe window in equal intervals (30 min for 24 h, 3 h for 7 d, 6 h for 15 d): generating, serving but idle, or not serving
Spec-decode acceptvllm:spec_decode_num_accepted_tokens_total / vllm:spec_decode_num_draft_tokens_totalSpeculative-decoding (MTP) draft-token acceptance rate, rate ratio over 15 minutes. Omitted entirely — not shown as 0% — for any model that isn't running speculative decoding, since vLLM exports no spec_decode_* series for those models at all

Why a longer window (15m) than the token-rate tiles (2m): requests complete far less often than tokens are generated on a single-user box — a 2-minute window would mostly read zero between requests. 15 minutes smooths that out while still reflecting genuinely recent activity, not a stale multi-hour average.

Cumulative CO₂ emissions (time-series view)

The backend's timeseries endpoint queries the Prometheus range API at adaptive resolution (5-minute steps for 24 h, hourly for 7 d, 2-hourly for 15 d) and returns co2_grams_per_hour at each step (computed server-side as attributed_power_watts × 0.198, our fixed grid intensity — so only GPU time spent serving an LLM is counted). The dashboard then integrates that rate client-side using the rectangle rule, running-summed over the selected window:

total_g = Σ (co2_grams_per_hour_i × step_hours)

This total only grows — it never resets within a window — so the chart climbs steadily even while the cluster is mostly idle, rather than looking flat or empty the way a tiny, nearly-constant rate does.

Limitations and Future Work

For a more complete lifecycle analysis including training, hardware manufacturing, and cooling overhead, see the references below.

References

SourceKey contribution
Luccioni, Jernite & Strubell (2024). Power Hungry Processing. ACM FAccT. Per-query energy & CO₂ measurements for open LLMs (BLOOMz, Flan-T5, etc.)
Delavande, Pierrard & Luccioni (2025). Small Talk, Big Impact. arXiv. Per-token energy decomposition: prefill vs. decode scaling; basis for J/token envelope estimates
Patterson et al. (2021). Carbon Emissions and Large Neural Network Training. arXiv. GPT-3 training energy (1,287 MWh); location & hardware efficiency analysis
Li et al. (2023). Making AI Less Thirsty. arXiv. GPT-3 inference water and energy footprint estimates
Shehabi et al. (2024). Powering Intelligence. Lawrence Berkeley National Laboratory. US AI data center energy projections; efficiency trajectories
IEA (2024). Electricity 2024. Global data center energy (460 TWh 2022, >1000 TWh by 2026); ChatGPT ≈ 10× Google search
US EPA eGRID (2022). Regional grid carbon intensity values used in this dashboard
Lottick et al. — CodeCarbon project. Open-source framework for tracking ML carbon emissions; inspiration for this work
Samsi et al. (2023). From Words to Watts. arXiv. Foundational empirical measurement of LLM inference energy on A100/V100; LLaMA-65B: 3–4 J/output token
TokenPowerBench (2025). arXiv:2512.03024. Comprehensive H100 benchmark of LLM inference power; MoE models ~2–3× more energy-efficient per token than dense equivalents
Google (2025). Measuring the environmental impact of delivering AI at Google Scale. arXiv. Only production-measured energy figure for a frontier commercial model: 0.24 Wh median per Gemini prompt (August 2025); 33× efficiency gain year-over-year
Luccioni et al. (2025). Energy Use of AI Inference: Efficiency Pathways and Test-Time Compute. arXiv. Best bottom-up estimate for frontier >200B models: 0.34 Wh/query median (IQR: 0.18–0.67); agentic 15× token multiplier → ~13× energy increase
The Cost of Dynamic Reasoning. (2025). arXiv. Empirical measurement of agentic AI energy: Reflexion/LATS workflows consume 62–137× more energy than single-turn baseline on same model
Tokenomics (2026). arXiv. Empirical tokenomics of multi-agent software engineering: 53.9% input tokens, ~2:1 input:output ratio; code review consumes 59% of all tokens
From Prompts to Power. (2025). arXiv. 32,500 empirical measurements; confirms decode phase dominates energy (~96–97% of total); output tokens 10–30× more expensive per token than input
How Hungry is AI? (2025). arXiv. GPT-4o estimates via API timing + hardware modeling: 0.42 Wh (short query) to 2.9 Wh (long query); cross-model comparison framework
Epoch AI (2024). How much energy does ChatGPT use? Transparent bottom-up estimate for GPT-4o: ~0.3 Wh per typical query; based on leaked architecture specs and H100 TDP modeling
a16z / OpenRouter (2025). State of AI: 100T Token Study. Empirical API traffic data: prompt lengths grew ~4× (2024–2025); programming prompts routinely exceed 20,000 input tokens; agentic use dominates growth

Everyday Reference Frames

Absolute CO₂ figures for a single, efficient GB10 node are small (grams, not kilograms) and don't mean much in isolation. The dashboard converts the cumulative total for the selected window (see "Cumulative CO₂ emissions" above) into a few widely-cited everyday equivalents, in the style of CodeCarbon:

ReferenceConversion factorSource
Miles driven (gas car)404 g CO₂/mileEPA, average passenger vehicle
Smartphone charges8.22 g CO₂/chargeCommonly cited estimate (a full charge is roughly 0.012 kWh)
Refrigerator runtime~150 W average drawTypical household refrigerator; converted to grams using our own 0.198 kg CO₂/kWh grid intensity, for a like-for-like "same grid, different appliance" comparison
Tree-months of absorption~1.75 kg CO₂/month/treeCommon carbon-calculator estimate — the roughest of the four; real absorption varies significantly by species, age, and region

Note: these are meant to build intuition for very small numbers, not as precise offsets.

Source Code

This dashboard is open source: github.com/boettiger-lab/nimbus-carbon-api, forked from nrp-carbon-api. The fixed carbon intensity constant is in internal/carbon/intensity.go.