<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dcgm on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/dcgm/</link><description>Recent content in Dcgm on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 29 Aug 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/dcgm/index.xml" rel="self" type="application/rss+xml"/><item><title>Measuring the power of a GPU: NVML, DCGM and the sampling errors that invalidate your watts</title><link>https://blog.lo0.es/en/posts/measuring-gpu-power-nvml-dcgm-sampling-errors/</link><pubDate>Sat, 29 Aug 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/measuring-gpu-power-nvml-dcgm-sampling-errors/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The dollar symbol is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>A GPU power sensor does not return what almost everyone believes. The NVML documentation is explicit: on Ampere except GA100, and on later architectures including &lt;strong>H100&lt;/strong>, &lt;code>nvmlDeviceGetPowerUsage&lt;/code> returns power averaged over a one-second interval, not instantaneous power. The University of Oxford study presented at SC24, covering more than 70 GPUs across 25 models checked against an external 1 mΩ meter, adds the fact that breaks most published measurements: on the A100 and H100 the sensor only samples 25 % of the time (a 25 ms window within a 101 ms period), so during the remaining 75 % the GPU may be consuming something radically different. Integrating power naively over nine real benchmarks, the mean error was 39.27 %; applying good practice brought it down to 4.89 %. The operational consequence is short: if the hardware is Volta or later, use the &lt;strong>accumulated energy counter&lt;/strong> (&lt;code>nvmlDeviceGetTotalEnergyConsumption&lt;/code>, in millijoules) and subtract, rather than integrating samples. And the GPU counter is not the bill: on measured 8× H100 nodes, the node reaches 8.4 kW against 5.6 kW of summed GPU TDP, node idle is 1.8 kW, and with an average PUE of 1.54 the NVML watts are of the order of 43 % of the watts the utility bills.&lt;/p>
&lt;hr>
&lt;h2 id="what-the-hardware-exposes">What the hardware exposes&lt;/h2>
&lt;h3 id="nvml-power">NVML: power&lt;/h3>
&lt;p>&lt;code>nvmlDeviceGetPowerUsage(device, unsigned int* power)&lt;/code> returns &lt;strong>milliwatts&lt;/strong> for the GPU &lt;em>and its associated circuitry, for example the memory&lt;/em>. It has been supported since Fermi. The note in the official documentation is the part almost nobody quotes (&lt;a href="https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html">NVML Device Queries&lt;/a>):&lt;/p>
&lt;blockquote>
&lt;p>On Fermi and Kepler the reading is accurate to ±5 % of current consumption. &lt;strong>On Ampere (except GA100) or later, the API returns power averaged over a 1 s interval. On GA100 and earlier architectures instantaneous power is returned.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>In other words, on an H100 or an L40S the usual call already delivers a one-second moving average. Anyone sampling it at 1 Hz and integrating is applying a second filter on top of an already filtered signal, without declaring it. To disambiguate, NVML exposes two separate fields accessible via &lt;code>nvmlDeviceGetFieldValues&lt;/code>: &lt;code>NVML_FI_DEV_POWER_AVERAGE&lt;/code> and &lt;code>NVML_FI_DEV_POWER_INSTANT&lt;/code>, with command-line equivalents (&lt;code>nvidia-smi --query-gpu=power.draw.average&lt;/code> and &lt;code>power.draw.instant&lt;/code>).&lt;/p>
&lt;h3 id="nvml-the-energy-counter">NVML: the energy counter&lt;/h3>
&lt;p>&lt;code>nvmlDeviceGetTotalEnergyConsumption&lt;/code> returns accumulated energy in millijoules since the last driver reload, supported on Volta or later. It is a 64-bit integer, so overflow is not a practical problem: an H100 at 700 W consumes of the order of 6.1·10¹⁰ J per year against a counter range of 1.8·10¹⁶ J. The only event that resets it is a driver reload or a system restart, and that does need watching: a &lt;code>modprobe -r nvidia&lt;/code> halfway through a campaign produces a negative delta.&lt;/p>
&lt;p>NVIDIA does not document the internal time resolution of the counter, its accumulation mechanism or its accuracy, and I have found no peer-reviewed work validating it against an external meter. The recommendation to use it, which the rest of this sheet rests on, is a structural argument (it removes quadrature error and aliasing error), not a published empirical validation.&lt;/p>
&lt;h3 id="nvml-power-limits">NVML: power limits&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Function&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Semantics&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>nvmlDeviceGetPowerManagementLimit&lt;/code>&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>Currently configured limit&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>nvmlDeviceGetPowerManagementLimitConstraints&lt;/code>&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>Legal minimum and maximum range&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>nvmlDeviceGetPowerManagementDefaultLimit&lt;/code>&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>Limit the card boots with&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>nvmlDeviceGetEnforcedPowerLimit&lt;/code>&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>Effective limit after considering &lt;strong>all&lt;/strong> limiters, including the out-of-band interface&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For an auditable measurement you have to record &lt;strong>&lt;code>EnforcedPowerLimit&lt;/code>&lt;/strong>, not &lt;code>PowerManagementLimit&lt;/code>: the BMC may be imposing an out-of-band ceiling that the latter does not reflect, and two apparently identical nodes may be operating at different ceilings.&lt;/p>
&lt;h3 id="dcgm-the-equivalent-fields">DCGM: the equivalent fields&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>ID&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Type in the exporter&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;td>155&lt;/td>
&lt;td>watts in floating point&lt;/td>
&lt;td>gauge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code>&lt;/td>
&lt;td>156&lt;/td>
&lt;td>accumulated millijoules&lt;/td>
&lt;td>counter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_MGMT_LIMIT&lt;/code>&lt;/td>
&lt;td>160&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>gauge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_ENFORCED_POWER_LIMIT&lt;/code>&lt;/td>
&lt;td>164&lt;/td>
&lt;td>mW&lt;/td>
&lt;td>gauge&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both energy fields are active in the default counter file of &lt;code>dcgm-exporter&lt;/code>. The audit conclusion is direct: a standard Prometheus panel integrating &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> is integrating a gauge that is already a one-second average of the sensor, sampled at the exporter&amp;rsquo;s &lt;code>collect-interval&lt;/code>. For energy per token the correct metric is counter 156 with &lt;code>increase()&lt;/code>.&lt;/p>
&lt;h2 id="the-real-accuracy-of-the-sensor">The real accuracy of the sensor&lt;/h2>
&lt;p>The reference is the work of Yang, Adámek and Armour (University of Oxford), published at SC24 and available as &lt;a href="https://arxiv.org/html/2312.02741v3">arXiv:2312.02741&lt;/a>: more than 70 GPUs across 25 models, including 10 H100s and 10 A100s, checked against an external meter with a 1 mΩ shunt, a 12-bit ADC and internal sampling at 34 kHz.&lt;/p>
&lt;h3 id="update-period-and-averaging-window">Update period and averaging window&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>GPU or architecture&lt;/th>
&lt;th>Update period&lt;/th>
&lt;th>Averaging window&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Kepler and Maxwell&lt;/td>
&lt;td>10-50 Hz&lt;/td>
&lt;td>logarithmic growth, ~200 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Volta (V100)&lt;/td>
&lt;td>20 ms&lt;/td>
&lt;td>10 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Turing&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>A100 (GA100)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>101 ms&lt;/strong>&lt;/td>
&lt;td>&lt;strong>25 ms&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Non-GA100 Ampere and Ada, driver earlier than March 2023&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;td>1 s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Non-GA100 Ampere and Ada, driver 530&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>H100&lt;/strong>&lt;/td>
&lt;td>&lt;strong>100 ms&lt;/strong>&lt;/td>
&lt;td>&lt;strong>25 ms&lt;/strong> with &lt;code>.instant&lt;/code>; &lt;strong>1 s&lt;/strong> with &lt;code>.average&lt;/code> and with the default field&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GH200&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;td>20 ms on the GPU, 10 ms on the Grace CPU&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two readings of this table. The first: the driver is an experimental variable, not an infrastructure detail; the averaging window changed between drivers earlier than 530, driver 530, and the later ones. Any measurement meant to be compared over time has to record the driver version.&lt;/p>
&lt;p>The second is the central finding of the paper. On the A100 and H100 the 25 ms window sits inside a 101 ms period, which gives a sensor duty cycle close to 25 %. The sensor does not average the full interval: it averages a quarter of it and the system presents the result as if it described the whole interval. In the words of the work itself, during the other 75 % of the time the GPU may be consuming radically different power and &lt;code>nvidia-smi&lt;/code> never notices.&lt;/p>
&lt;h3 id="the-steady-state-error">The steady-state error&lt;/h3>
&lt;p>The paper also quantifies the claim in the &lt;code>nvidia-smi&lt;/code> documentation, which spoke of an accuracy of ±5 watts: the real error is proportional, ±5 %, not absolute. On a GPU capable of consuming 700 W that is &lt;strong>±35 W&lt;/strong> of over- or underestimation. The errors measured against the external meter, after correcting the time offset, were −4.70 % and −4.53 % on the RTX 3090 and −5.43 % on the A100, and the residue is attributed to the tolerance of the card&amp;rsquo;s own physical shunt, which no software can correct.&lt;/p>
&lt;h3 id="the-response-to-transients">The response to transients&lt;/h3>
&lt;p>Four distinct behaviours were observed in the 10 % to 90 % rise time: an almost instantaneous real rise with the reading following on the next tick and a delay of 0 to 100 ms; a real rise of several hundred milliseconds with the reading still updating on the next tick; linear growth over a second, which corresponds to &lt;code>power.draw.average&lt;/code>; and logarithmic growth over 200 ms, only on Kepler and Maxwell. Out of this comes the most uncomfortable warning in the work: when running a short program, the measured power probably corresponds to the activity before the program.&lt;/p>
&lt;h2 id="the-two-ways-of-obtaining-energy">The two ways of obtaining energy&lt;/h2>
&lt;p>The first is to integrate samples, which is what any &lt;em>poller&lt;/em> does:&lt;/p>
$$E \approx \sum_i P(t_i) \cdot \Delta t$$
&lt;p>It accumulates five sources of error: the ±5 % of the sensor, quadrature error from the finite step, aliasing if the load has spectral content above half the sampling frequency, the fact that each sample is already a moving average, and the jitter of 0 to 100 ms between the host clock and the sensor tick.&lt;/p>
&lt;p>The second is to subtract the counter:&lt;/p>
$$E = \text{counter}(t_1) - \text{counter}(t_0)$$
&lt;p>Two calls and a subtraction. It removes quadrature, aliasing and time misalignment; the ±5 % of the sensor remains.&lt;/p>
&lt;h3 id="why-1-hz-destroys-a-200-ms-inference">Why 1 Hz destroys a 200 ms inference&lt;/h3>
&lt;p>With sampling at 1 Hz, the sampling theorem only allows components below 0.5 Hz to be reconstructed, while a 200 ms pulse has content in the region of 5 Hz. The expected number of samples inside the event is 0.2, so with probability 0.8 none falls inside it: the result is not noisy, it is undefined.&lt;/p>
&lt;p>Even sampling at the maximum useful rate, which on the A100 and H100 is about 10 Hz given the 101 ms period, a 200 ms inference produces one or two distinct sensor updates, and each has observed only 25 ms of real activity: effective coverage is around 25 % of the event. And because of the fourth transient behaviour in the previous section, the reading obtained during those 200 ms may describe what was happening before the kernel was launched.&lt;/p>
&lt;h3 id="aliasing-with-periodic-loads">Aliasing with periodic loads&lt;/h3>
&lt;p>The paper observes the phenomenon directly on the A100: with a square wave of period slightly different from 100 ms, the reading fluctuates between high and low values with a clear beat. An inference server with periodic arrivals, health checks, fixed-size batches or a scheduler tick is exactly that pathological case.&lt;/p>
&lt;p>When only instantaneous power is available, the measured good practice from the Oxford work consists of running 32 consecutive iterations or a minimum of 5 s, inserting &lt;strong>8 evenly spaced controlled delays&lt;/strong> if the window is smaller than the period, repeating over 4 separate trials with a random delay between them, and shifting the series in post-processing to synchronise it with the real activity. Over nine real benchmarks that took the mean error from 39.27 % to 4.89 %.&lt;/p>
&lt;h2 id="what-the-gpu-counter-does-not-measure">What the GPU counter does not measure&lt;/h2>
&lt;p>The scope declared by NVML is the GPU and its associated circuitry: the SXM module or the PCIe card, with its HBM and its regulators. Left out are the CPU, host DRAM, NVSwitch, network cards, storage, fans and power supply losses.&lt;/p>
&lt;p>The measured node figures come from the empirical calibration work on 8× H100 SXM5 nodes (&lt;a href="https://arxiv.org/pdf/2506.14551">arXiv:2506.14551&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quantity&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Nominal node TDP declared by the manufacturer&lt;/td>
&lt;td>10.2 kW&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Node power at idle&lt;/td>
&lt;td>1.8 kW&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maximum measured under saturating load&lt;/td>
&lt;td>8.4 kW&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Peak on real production loads&lt;/td>
&lt;td>never above 76 % of TDP&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two conversion factors come out of that, both &lt;strong>my own estimates&lt;/strong> from those figures: 8 × 700 W is 5.6 kW of GPU against 8.4 kW of node, that is, the GPU is of the order of 67 % of node power and the GPU-to-node factor is around 1.50×. Chaining that with an industry average PUE of 1.54 (Uptime Institute, 2025), the total factor from the NVML counter to the utility feed is around 2.3×; with a PUE of 1.1 it drops to 1.65×. Put in the form that matters to whoever signs the bill: the watts NVML reports are roughly 43 % of the watts the utility bills in an average datacenter.&lt;/p>
&lt;p>As a complementary reference, an 80 PLUS Titanium supply at 230 V is 96 % efficient at 50 % load and 91 % at 100 %, so supply losses add between 4 % and 9 % that is already included if you measure at the socket.&lt;/p>
&lt;h2 id="the-tax-of-keeping-a-context-open">The tax of keeping a context open&lt;/h2>
&lt;p>A GPU serving a loaded model with no traffic does not consume its nominal idle. Measurements over 335,267 production samples from 14 H100s across 18 days, plus controlled experiments, give this table:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>GPU&lt;/th>
&lt;th>Idle without a CUDA context&lt;/th>
&lt;th>Idle with a CUDA context&lt;/th>
&lt;th>Extra cost&lt;/th>
&lt;th>Percentage of TDP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>H100&lt;/td>
&lt;td>71.8 W&lt;/td>
&lt;td>121.7 W&lt;/td>
&lt;td>+49.9 W&lt;/td>
&lt;td>7.1 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100&lt;/td>
&lt;td>53.7 W&lt;/td>
&lt;td>80.0 W&lt;/td>
&lt;td>+26.3 W&lt;/td>
&lt;td>8.8 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>L40S&lt;/td>
&lt;td>35.6 W&lt;/td>
&lt;td>102.1 W&lt;/td>
&lt;td>+66.4 W&lt;/td>
&lt;td>19.0 %&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The result that changes the mental model: more than 98 % of that extra cost is produced by the open CUDA context, independently of the memory occupied. Varying allocated VRAM between 0 and 72 GB moves power by less than 1 W. The size of the loaded model does not cost watts; keeping the context open does.&lt;/p>
&lt;p>That cost is not marginal on a real platform. Over 11,791 long-running jobs, the measured split was 24 % of the time and 7 % of the energy in deep idle, 15 % of the time and 10 % of the energy idle with an active context, and 61 % of the time with 83 % of the energy in execution. On &lt;em>serving&lt;/em> loads, GPUs can spend 48 % of their energy in low-activity periods.&lt;/p>
&lt;h2 id="cpu-and-dram-rapl">CPU and DRAM: RAPL&lt;/h2>
&lt;p>To close the node you have to measure what is not GPU, and there the interface is RAPL through the kernel &lt;code>powercap&lt;/code> framework, at &lt;code>/sys/devices/virtual/powercap/intel-rapl/&lt;/code>. The hierarchy exposes &lt;code>intel-rapl:N&lt;/code> as the socket and &lt;code>intel-rapl:N:M&lt;/code> as subzones (core, uncore, dram), plus the &lt;code>psys&lt;/code> domain for the full SoC since Skylake.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File&lt;/th>
&lt;th>Content&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>energy_uj&lt;/code>&lt;/td>
&lt;td>Energy counter in microjoules; writing &amp;ldquo;0&amp;rdquo; resets it if the counter supports it&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>max_energy_range_uj&lt;/code>&lt;/td>
&lt;td>Counter range, that is, the overflow point&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>power_uw&lt;/code>&lt;/td>
&lt;td>Current power in microwatts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>name&lt;/code>&lt;/td>
&lt;td>Zone name&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The figures that condition its use come from the characterisation published in ACM TOMPECS: an update roughly every 1 ms, with jitter; an energy quantum of 61 µJ on Haswell and Skylake, 15.3 µJ on Sandy Bridge; and counter overflow in &lt;strong>52 minutes&lt;/strong> on a Haswell at 84 W, which forces polling at a far shorter period and detecting the wrap with &lt;code>max_energy_range_uj&lt;/code>. There is also a &lt;strong>thermal drift&lt;/strong> that is rarely declared: package power for the same load grows between 10 % and 12 % between 37 °C and 74 °C on Haswell. Warming up beforehand is not optional.&lt;/p>
&lt;p>Access, on the other hand, is no longer free. The &lt;strong>PLATYPUS&lt;/strong> attack (IEEE S&amp;amp;P 2021) demonstrated a purely software power side channel through RAPL, with CVE-2020-8694 and CVE-2020-8695 attached. The Linux mitigation arrived in commit &lt;code>949dd0104c49&lt;/code>, included in 5.10.0-rc4, which changed the default permissions so that only root can read &lt;code>energy_uj&lt;/code>. The microcode mitigation associated with SGX goes further and introduces random noise into the reported energy and changes the reporting frequency: on machines with that mitigation active, RAPL readings are deliberately degraded. Measuring CPU and DRAM from a container therefore requires root, a &lt;code>udev&lt;/code> rule relaxing the permissions, or a privileged daemon.&lt;/p>
&lt;h2 id="the-software-stack">The software stack&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Source of the GPU figure&lt;/th>
&lt;th>Counter or power&lt;/th>
&lt;th>Default interval&lt;/th>
&lt;th>Overhead&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Zeus&lt;/td>
&lt;td>NVML&lt;/td>
&lt;td>Counter if the architecture is Volta or later; polling otherwise&lt;/td>
&lt;td>Delimited by windows&lt;/td>
&lt;td>Less than 10 ms per call&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>dcgm-exporter&lt;/td>
&lt;td>NVML via DCGM&lt;/td>
&lt;td>Both fields available&lt;/td>
&lt;td>&lt;code>collect-interval&lt;/code> = 30,000 ms&lt;/td>
&lt;td>5-10 W extra on the server&amp;rsquo;s IPMI reading&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CodeCarbon&lt;/td>
&lt;td>NVML&lt;/td>
&lt;td>Counter&lt;/td>
&lt;td>&lt;code>measure_power_secs&lt;/code> = 15 s&lt;/td>
&lt;td>5.38 % to 46.75 % of time at 1 kHz&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scaphandre&lt;/td>
&lt;td>No GPU support; RAPL&lt;/td>
&lt;td>&lt;code>energy_uj&lt;/code> counter&lt;/td>
&lt;td>Not documented&lt;/td>
&lt;td>3.81 % to 28.38 % at 1 kHz&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kepler&lt;/td>
&lt;td>RAPL and IPMI; GPU only as an experimental option&lt;/td>
&lt;td>Hardware counters&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Four details that change the choice:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Zeus&lt;/strong> decides in code with &lt;code>nvmlDeviceGetArchitecture&lt;/code> and uses the accumulated counter on Volta or later, with a nestable window API (&lt;code>begin_window&lt;/code> / &lt;code>end_window&lt;/code>). It is the reference implementation of what this sheet recommends. Its NSDI'23 paper also documents energy reductions of 23.8 % to 75.7 % from choosing batch size and power limit well, and of 3.0 % to 31.5 % from touching only the limit, with five seconds of profiling per point enough for stable results.&lt;/li>
&lt;li>&lt;strong>Kepler&lt;/strong> has removed eBPF. The CNCF announcement of June 2026 describes the redesign towards reading &lt;code>/proc&lt;/code> and &lt;code>/sys&lt;/code>, motivated by the &lt;code>CAP_BPF&lt;/code> and &lt;code>CAP_SYSADMIN&lt;/code> permissions that blocked deployments, and by the loss of short-lived processes that underestimated the footprint. The new node metric closely follows the IPMI pattern and removes the spurious multi-kilowatt spikes of the previous version. For GPUs it remains &lt;strong>an experimental option&lt;/strong>: today it is not the tool for measuring Wh/token on an accelerator.&lt;/li>
&lt;li>&lt;strong>CodeCarbon&lt;/strong> uses the NVML counter, which is correct, but its RAM estimate is a heuristic of 5 W per module derived from the total gigabytes, and the code itself acknowledges it.&lt;/li>
&lt;li>&lt;strong>DCGM&lt;/strong> delivers profiling metrics at 1 Hz by default, and warns that collecting at higher frequencies returns zeros because it groups metrics internally.&lt;/li>
&lt;/ul>
&lt;p>On the cost of measuring, the empirical overhead study of RAPL-based tools, all forced to 1 kHz, leaves a clear ordering:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Time overhead&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CodeCarbon&lt;/td>
&lt;td>5.38 % to 46.75 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scaphandre&lt;/td>
&lt;td>3.81 % to 28.38 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Turbostat&lt;/td>
&lt;td>2.73 % to 14.37 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PowerJoular&lt;/td>
&lt;td>1.67 % to 8.88 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>perf&lt;/td>
&lt;td>−1.00 % to 4.26 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAPL read in user space&lt;/td>
&lt;td>−0.70 % to 2.93 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAPL read in kernel space&lt;/td>
&lt;td>−0.17 % to 0.99 %&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The underlying difference is that a system call costs of the order of 1.36·10⁻³ ms and an &lt;code>rdmsr&lt;/code> instruction between 2.3·10⁻⁴ and 5.6·10⁻⁴ ms, an order of magnitude less. The authors recommend matching the granularity to the phenomenon being measured instead of raising the default frequency, and limiting polling to the necessary domains.&lt;/p>
&lt;h2 id="procedure-for-making-a-whtoken-auditable">Procedure for making a Wh/token auditable&lt;/h2>
&lt;ol>
&lt;li>Fix and record the electrical state. Note &lt;code>EnforcedPowerLimit&lt;/code>, the default limit and the driver version; lock clocks or the power ceiling if the experiment requires it.&lt;/li>
&lt;li>&lt;strong>Warm up to a stable temperature.&lt;/strong> RAPL thermal drift is 10 % to 12 % between 37 °C and 74 °C, and the &lt;code>power.draw.average&lt;/code> transient takes up to a second to settle.&lt;/li>
&lt;li>Measure the idle baseline twice: without a CUDA context and with the model loaded. They are two different values and both are needed.&lt;/li>
&lt;li>Use windows of at least 5 s or 32 consecutive iterations.&lt;/li>
&lt;li>Repeat over 4 separate trials with a random delay, and add 8 evenly spaced delays if you are integrating instantaneous power with a window smaller than the period.&lt;/li>
&lt;li>&lt;strong>Prefer the accumulated counter&lt;/strong> whenever the architecture is Volta or later, watching for driver reloads.&lt;/li>
&lt;li>&lt;strong>Report dispersion.&lt;/strong> The Oxford paper itself publishes the mean and standard deviation of the error; without an interval, a Wh/token figure is not auditable.&lt;/li>
&lt;li>Declare the measurement boundary: GPU, node or utility feed. Without that label, two figures from the same facility differ by a factor of 2.3 with nobody knowing why.&lt;/li>
&lt;/ol>
&lt;h3 id="subtracting-idle-or-not">Subtracting idle, or not&lt;/h3>
&lt;p>There is no consensus, and the honest approach is to treat it as a declared decision. In production the GPU is parked with the model loaded, and that consumption shows up on the bill: subtracting it erases a real cost. To compare the marginal efficiency of two models or two kernels, by contrast, subtracting it isolates the variable. The workable way out is to report &lt;strong>both figures with labels&lt;/strong>: &lt;code>gross Wh/token&lt;/code>, which includes idle and corresponds to the bill, and &lt;code>marginal Wh/token&lt;/code>, comparable between models. What does not work is mixing them in the same table without distinguishing them.&lt;/p>
&lt;p>One estimation trap remains alive in many spreadsheets: multiplying &lt;strong>TDP by time&lt;/strong> overestimates real energy &lt;strong>by up to 4.1 times&lt;/strong>, according to the ML.ENERGY Benchmark.&lt;/p>
&lt;h2 id="power-capping-what-it-costs-and-what-it-saves">Power capping: what it costs and what it saves&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuration&lt;/th>
&lt;th>Energy saving&lt;/th>
&lt;th>Performance loss&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>V100 at 200 W against 300 W (67 % of TDP), BERT training&lt;/td>
&lt;td>~15 %&lt;/td>
&lt;td>barely any degradation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>V100 at 200 W, set of workloads&lt;/td>
&lt;td>10-20 %&lt;/td>
&lt;td>less than 5 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>V100 at 100 W (33 % of TDP)&lt;/td>
&lt;td>40-60 %&lt;/td>
&lt;td>30-40 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>A100 at 175 W against 400 W (44 % of TDP), LLaMA 65B inference&lt;/strong>&lt;/td>
&lt;td>&lt;strong>22-24 %&lt;/strong>&lt;/td>
&lt;td>&lt;strong>5-8 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The asymmetry between phases explains the result. The Microsoft measurement at ASPLOS'24 documents that the &lt;em>prompt&lt;/em> phase is short and reaches or exceeds TDP, while the token generation phase is longer and consumes less: the power ceiling penalises TTFT far more than TPOT. In their production cluster, inference uses 79 % of peak power against 97 % for training, and their measurements show that up to 20 % of power can be recovered with less than a 7 % performance loss.&lt;/p>
&lt;p>The published sweep from 200 W to 700 W on the H100 and H200 adds the nuance of where the knee is: between 500 W and 700 W each 100 W step only contributes around 10 % of performance on compute-bound loads, while the H100 holds peak memory bandwidth even at 200 W. The most stable operating zones identified are 400 W on the H100 and 500 W on the H200. That the memory subsystem is protected against the trim is the physical reason why the decode phase of an LLM, which is memory-bound, loses so little performance under a power ceiling.&lt;/p>
&lt;p>Two warnings about those figures: the H100/H200 sweep was measured with &lt;code>nvidia-smi&lt;/code> polling every 10 s, exactly the problem described above, although on long steady loads the bias averages out; and there is no publication isolating the effect of memory clock locking on Wh/token in LLM inference, so any concrete figure on that point has to be your own measurement.&lt;/p>
&lt;h2 id="reference-figures">Reference figures&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>GPU&lt;/th>
&lt;th>TDP&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>H100 SXM&lt;/td>
&lt;td>up to 700 W, configurable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>H100 NVL&lt;/td>
&lt;td>350-400 W, configurable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 80GB SXM&lt;/td>
&lt;td>400 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 80GB PCIe&lt;/td>
&lt;td>300 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 5090&lt;/td>
&lt;td>575 W, with a recommended 1000 W supply&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HGX node of 8× H100 SXM&lt;/td>
&lt;td>10.2 kW nominal&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Average power measured in inference: on an HPC cluster with vLLM on A100, the aggregate power of the GPU set ranged between 999 W and 2,983 W depending on model and parallelism, with Nemotron 70B on 8× A100 at 2,983 W, about 373 W per GPU, around 93 % of TDP.&lt;/p>
&lt;p>Energy per token, with the warning that published figures measure different boundaries and cannot be mixed without normalising by GPU count and output tokens:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source&lt;/th>
&lt;th>Figure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TokenPowerBench, H100 94 GB&lt;/td>
&lt;td>40 J per token on a standard load, more than 60 J per token in high-throughput configurations&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenPowerBench, batch effect&lt;/td>
&lt;td>−25 % of energy per token between batch 32 and 256 on a 70B model&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenPowerBench, context effect&lt;/td>
&lt;td>from 2K to 10K tokens of context on Llama 3 70B: energy ×3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenPowerBench, quantisation effect&lt;/td>
&lt;td>Llama 3 405B in FP8 against FP16: −30 % of energy per token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenPowerBench, engine effect&lt;/td>
&lt;td>TensorRT-LLM and vLLM against the Transformers engine: −25 % to −40 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Per-prompt measurement in batch&lt;/td>
&lt;td>0.0074-0.0289 Wh on 7B to 14B models; 0.0835-0.6912 Wh on 70B to 405B; single unbatched requests, between 10 and 100 times more expensive&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="cross-links-from-the-energy-track">Cross-links from the energy track&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/energy-per-token-spain-methodology-electricity-market-speculation/">C2 — Energy per token: methodology and the electricity market&lt;/a>: what to do with the joule once it is measured properly, and how the electricity price multiplies it.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/">C3 — Measuring energy in production: Kepler, DCGM and the practical stack&lt;/a>: the continuous deployment of the stack whose metrology this sheet documents.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ai-energy-benchmarking-frameworks-metrics-state-of-the-art/">C3 — Energy benchmarking: frameworks, metrics and the state of the art&lt;/a>: the full catalogue of tools and their metrics.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlperf-power-energy-efficiency-benchmark/">C4 — MLPerf Power&lt;/a>: the certified alternative, measuring at the wall with an external analyser instead of the card&amp;rsquo;s sensor.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/watt-to-carbon-pue-grid-intensity-gco2-per-token/">C6 — From watt to carbon: PUE, grid and grams of CO₂&lt;/a>: the chain that turns the measured watt into emissions and into euros.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/levers-fewer-watts-per-token-llm-inference/">C7 — Energy efficiency levers&lt;/a>: the quantified catalogue where the power ceiling of this sheet is just one more lever.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference: DCGM and vLLM metrics&lt;/a> — the same agent seen from operations, with the metrics that watch health as well as consumption.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/reproducible-harness-cost-performance-energy-experiment/">The reproducible harness: cost, performance and energy in a single auditable experiment&lt;/a> — where this procedure fits inside the bench that measures all three axes at once.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ai-datacenter-cooling-thermal-challenge/">Cooling the AI datacenter (1/4): the thermal challenge&lt;/a> — what happens to those watts after they turn entirely into heat.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>NVIDIA, &lt;em>NVML API Reference, Device Queries&lt;/em> — &lt;a href="https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html">https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>DCGM Library API, Field Identifiers&lt;/em> — &lt;a href="https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html">https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>DCGM Feature Overview&lt;/em> — &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html">https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>dcgm-exporter&lt;/em> — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>H100 Tensor Core GPU, especificaciones&lt;/em> — &lt;a href="https://www.nvidia.com/en-us/data-center/h100/">https://www.nvidia.com/en-us/data-center/h100/&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>A100 Tensor Core GPU, especificaciones&lt;/em> — &lt;a href="https://www.nvidia.com/en-us/data-center/a100/">https://www.nvidia.com/en-us/data-center/a100/&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>GeForce RTX 5090, especificaciones&lt;/em> — &lt;a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/">https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/&lt;/a>&lt;/li>
&lt;li>Yang, Adámek y Armour (Oxford), &lt;em>Part-time Power Measurements: nvidia-smi&amp;rsquo;s Lack of Attention&lt;/em> / SC24 — &lt;a href="https://arxiv.org/html/2312.02741v3">https://arxiv.org/html/2312.02741v3&lt;/a>&lt;/li>
&lt;li>Yang, &lt;em>GPU_Power_Benchmark&lt;/em>, microbenchmark del trabajo anterior — &lt;a href="https://github.com/JimZeyuYang/GPU_Power_Benchmark">https://github.com/JimZeyuYang/GPU_Power_Benchmark&lt;/a>&lt;/li>
&lt;li>ML.ENERGY, &lt;em>Measuring GPU Energy: Best Practices&lt;/em> — &lt;a href="https://ml.energy/blog/energy/measurement/measuring-gpu-energy-best-practices/">https://ml.energy/blog/energy/measurement/measuring-gpu-energy-best-practices/&lt;/a>&lt;/li>
&lt;li>You et al., &lt;em>Zeus: Understanding and Optimizing GPU Energy Consumption of DNN Training&lt;/em>, NSDI'23 — &lt;a href="https://www.usenix.org/system/files/nsdi23-you.pdf">https://www.usenix.org/system/files/nsdi23-you.pdf&lt;/a>&lt;/li>
&lt;li>Zeus, &lt;em>Measuring Energy&lt;/em> — &lt;a href="https://ml.energy/zeus/measure/">https://ml.energy/zeus/measure/&lt;/a>&lt;/li>
&lt;li>ML.ENERGY, &lt;em>The ML.ENERGY Benchmark&lt;/em> — &lt;a href="https://arxiv.org/html/2505.06371v1">https://arxiv.org/html/2505.06371v1&lt;/a>&lt;/li>
&lt;li>CNCF, &lt;em>Kepler, re-architected: improved power accuracy&lt;/em> (junio 2026) — &lt;a href="https://www.cncf.io/blog/2026/06/30/kepler-re-architected-improved-power-accuracy-and-a-community-call-to-action/">https://www.cncf.io/blog/2026/06/30/kepler-re-architected-improved-power-accuracy-and-a-community-call-to-action/&lt;/a>&lt;/li>
&lt;li>CodeCarbon, &lt;em>How Power Estimation Works&lt;/em> — &lt;a href="https://docs.codecarbon.io/latest/explanation/power-estimation/">https://docs.codecarbon.io/latest/explanation/power-estimation/&lt;/a>&lt;/li>
&lt;li>Hubblo, &lt;em>Scaphandre: cálculo del consumo por proceso&lt;/em> — &lt;a href="https://github.com/hubblo-org/scaphandre/blob/main/docs_src/explanations/how-scaph-computes-per-process-power-consumption.md">https://github.com/hubblo-org/scaphandre/blob/main/docs_src/explanations/how-scaph-computes-per-process-power-consumption.md&lt;/a>&lt;/li>
&lt;li>Linux Kernel, &lt;em>Power Capping Framework (powercap sysfs)&lt;/em> — &lt;a href="https://docs.kernel.org/power/powercap/powercap.html">https://docs.kernel.org/power/powercap/powercap.html&lt;/a>&lt;/li>
&lt;li>Intel, &lt;em>Running Average Power Limit Energy Reporting, INTEL-SA-00389&lt;/em> — &lt;a href="https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/advisory-guidance/running-average-power-limit-energy-reporting.html">https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/advisory-guidance/running-average-power-limit-energy-reporting.html&lt;/a>&lt;/li>
&lt;li>Lipp et al., &lt;em>PLATYPUS: With Great Power comes Great Leakage&lt;/em>, IEEE S&amp;amp;P 2021 — &lt;a href="https://platypusattack.com/">https://platypusattack.com/&lt;/a>&lt;/li>
&lt;li>Khan et al., &lt;em>RAPL in Action: Experiences in Using RAPL for Power Measurements&lt;/em>, ACM TOMPECS — &lt;a href="https://www.devsustainability.com/p/paper-notes-rapl-in-action">https://www.devsustainability.com/p/paper-notes-rapl-in-action&lt;/a>&lt;/li>
&lt;li>&lt;em>What Is the Cost of Energy Monitoring? An Empirical Study on the Overhead of RAPL-Based Tools&lt;/em> — &lt;a href="https://arxiv.org/html/2604.26815v1">https://arxiv.org/html/2604.26815v1&lt;/a>&lt;/li>
&lt;li>Patel et al. (Microsoft), &lt;em>Characterizing Power Management Opportunities for LLMs in the Cloud&lt;/em>, ASPLOS'24 — &lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/03/GPU_Power_ASPLOS_24.pdf">https://www.microsoft.com/en-us/research/wp-content/uploads/2024/03/GPU_Power_ASPLOS_24.pdf&lt;/a>&lt;/li>
&lt;li>MIT Lincoln Laboratory, &lt;em>Sustainable Supercomputing for AI: GPU Power Capping at HPC Scale&lt;/em> — &lt;a href="https://arxiv.org/html/2402.18593">https://arxiv.org/html/2402.18593&lt;/a>&lt;/li>
&lt;li>&lt;em>Architectural Trade-offs in the Energy-Efficient Era: power-capping NVIDIA H100 and H200&lt;/em> — &lt;a href="https://arxiv.org/html/2604.11391v1">https://arxiv.org/html/2604.11391v1&lt;/a>&lt;/li>
&lt;li>&lt;em>Empirically-Calibrated H100 Node Power Models for Datacenter Energy Analysis&lt;/em> — &lt;a href="https://arxiv.org/pdf/2506.14551">https://arxiv.org/pdf/2506.14551&lt;/a>&lt;/li>
&lt;li>&lt;em>The Model Parking Tax: Quantifying the Hidden Energy Cost of Always-On GPU Model Deployment&lt;/em> — &lt;a href="https://arxiv.org/html/2605.23918">https://arxiv.org/html/2605.23918&lt;/a>&lt;/li>
&lt;li>&lt;em>The Energy Cost of Execution-Idle in GPU Clusters&lt;/em> — &lt;a href="https://arxiv.org/html/2604.04745">https://arxiv.org/html/2604.04745&lt;/a>&lt;/li>
&lt;li>&lt;em>TokenPowerBench: Benchmarking the Power Consumption of LLM Inference&lt;/em> — &lt;a href="https://arxiv.org/pdf/2512.03024">https://arxiv.org/pdf/2512.03024&lt;/a>&lt;/li>
&lt;li>&lt;em>From Prompts to Power: Measuring the Energy Footprint of LLM Inference&lt;/em> — &lt;a href="https://arxiv.org/html/2511.05597">https://arxiv.org/html/2511.05597&lt;/a>&lt;/li>
&lt;li>&lt;em>Serving LLMs in HPC Clusters&lt;/em> — &lt;a href="https://arxiv.org/abs/2507.00418">https://arxiv.org/abs/2507.00418&lt;/a>&lt;/li>
&lt;li>Uptime Institute, &lt;em>Global Data Center Survey 2025&lt;/em> — &lt;a href="https://datacenter.uptimeinstitute.com/rs/711-RIA-145/images/2025.Annual.Survey.Report.pdf">https://datacenter.uptimeinstitute.com/rs/711-RIA-145/images/2025.Annual.Survey.Report.pdf&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>The reproducible harness: measuring cost, performance and energy in a single auditable experiment</title><link>https://blog.lo0.es/en/posts/reproducible-harness-cost-performance-energy-experiment/</link><pubDate>Tue, 16 Jun 2026 12:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/reproducible-harness-cost-performance-energy-experiment/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point, thousands with a thin space. The dollar sign is not used (on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The &amp;ldquo;data&amp;rdquo; series has produced three independent measurement axes: cost per million tokens (OpenCost + LiteLLM), performance under an SLO (GuideLLM + AIPerf) and energy per token (DCGM + Kepler). The problem is that the three have been measured in different articles, with different loads and at different times: they are not comparable with each other. This closing article describes the &lt;strong>integrated harness&lt;/strong> that runs the three axes &lt;strong>in the same experiment&lt;/strong>, on the same node (4×H100 SXM, a generic reference), with all the metadata fixed, the output in versioned JSON/CSV and an idempotent Kubernetes Job. The result is the &lt;strong>3-axis scorecard&lt;/strong> (€/1M tok, Wh/token, TTFT/ITL P99) that allows configurations to be compared on a &lt;strong>multi-objective Pareto frontier&lt;/strong> and any figure to be audited with the bench to reproduce it.&lt;/p>
&lt;hr>
&lt;h2 id="why-the-three-axes-must-be-measured-together">Why the three axes must be measured together&lt;/h2>
&lt;p>The series&amp;rsquo; opening post (&lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">The three axes&lt;/a>) established the identity:&lt;/p>
$$\text{CPM} = \frac{\text{cost/h}}{\text{throughput (tok/s)} \times 3.6 \times 10^{-3}}$$
$$\text{energy/token (Wh)} = \frac{\text{mean power (W)}}{\text{throughput (tok/s)} \times 3\,600}$$
&lt;p>Throughput is the common denominator. If it is measured in different experiments, different time, different load, different GPU temperature, then CPM and energy/token &lt;strong>do not share a denominator&lt;/strong>: they are three anecdotes, not a scorecard. The harness captures them in the same time window, over the same load, with Prometheus windows aligned to the second. Only then is the scorecard row coherent by construction.&lt;/p>
&lt;p>The second reason is reproducibility. The post &lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Bias and reproducibility in benchmarking&lt;/a> listed twelve biases that invalidate comparisons: an unpinned engine, an undeclared tokeniser, unfixed input/output lengths, missing warmup, a client outside the cluster. The harness removes all of them because the metadata are part of the Job, not of the documentation.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-of-the-integrated-bench">Architecture of the integrated bench&lt;/h2>
&lt;p>The harness has four layers. Each one is OSS, exports to Prometheus and lives in the same Kubernetes namespace:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Tool(s)&lt;/th>
&lt;th>Primary metric&lt;/th>
&lt;th>Export protocol&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Performance&lt;/strong>&lt;/td>
&lt;td>GuideLLM (SLO sweep) + AIPerf&lt;/td>
&lt;td>TTFT P99, ITL P99, goodput (tok/s)&lt;/td>
&lt;td>native JSON/CSV + &lt;code>/metrics&lt;/code> OpenMetrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cost&lt;/strong>&lt;/td>
&lt;td>OpenCost + LiteLLM proxy&lt;/td>
&lt;td>CPM (€/1M tok), cost/request&lt;/td>
&lt;td>REST API + Prometheus scrape&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Energy (GPU)&lt;/strong>&lt;/td>
&lt;td>DCGM Exporter&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> (W), &lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code> (mJ)&lt;/td>
&lt;td>Prometheus DaemonSet&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Energy (pod)&lt;/strong>&lt;/td>
&lt;td>Kepler&lt;/td>
&lt;td>&lt;code>kepler_container_joules_total&lt;/code>, energy per pod&lt;/td>
&lt;td>Prometheus DaemonSet&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>MLPerf Power is used as an &lt;strong>external comparability reference&lt;/strong>: its published results, with hardware documented in detail, allow the harness figures to be calibrated for plausibility. The in-house bench does not aim to be an MLPerf submission, but to be &lt;strong>reproducible on your own cluster&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 320" role="img" aria-label="Harness architecture: a Kubernetes Job orchestrates GuideLLM and AIPerf as load generators, OpenCost and LiteLLM measure cost, DCGM and Kepler measure energy, everything converges on Prometheus and the JSON scorecard" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.2;marker-end:url(#ah)}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3;marker-end:url(#ah)}&lt;/style>
&lt;defs>&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="10" y="10" width="800" height="300" rx="10"/>
&lt;text x="20" y="28" class="tl">Namespace: benchmark&lt;/text>
&lt;rect class="bx" x="30" y="40" width="180" height="70" rx="6"/>
&lt;text x="40" y="60" class="tl">Job benchmark-run&lt;/text>
&lt;text x="40" y="76" class="ts">GuideLLM sweep (SLO)&lt;/text>
&lt;text x="40" y="92" class="ts">AIPerf fixed concurrency&lt;/text>
&lt;rect class="bx" x="30" y="130" width="180" height="60" rx="6"/>
&lt;text x="40" y="150" class="tl">LiteLLM proxy&lt;/text>
&lt;text x="40" y="168" class="ts">token counting + CPM&lt;/text>
&lt;rect class="bx" x="30" y="210" width="180" height="60" rx="6"/>
&lt;text x="40" y="230" class="tl">DCGM DaemonSet&lt;/text>
&lt;text x="40" y="248" class="ts">POWER_USAGE, ENERGY&lt;/text>
&lt;rect class="bx" x="30" y="280" width="180" height="28" rx="6"/>
&lt;text x="40" y="298" class="tl">Kepler DaemonSet&lt;/text>
&lt;rect class="bx" x="280" y="40" width="180" height="70" rx="6"/>
&lt;text x="290" y="60" class="tl">Inference endpoint&lt;/text>
&lt;text x="290" y="78" class="ts">vLLM / SGLang&lt;/text>
&lt;text x="290" y="96" class="ts">pinned model, FP8/FP16&lt;/text>
&lt;rect class="bx" x="280" y="130" width="180" height="60" rx="6"/>
&lt;text x="290" y="150" class="tl">OpenCost&lt;/text>
&lt;text x="290" y="168" class="ts">€/GPU-h per pod/ns&lt;/text>
&lt;rect class="bx" x="540" y="60" width="160" height="80" rx="6"/>
&lt;text x="550" y="82" class="tl">Prometheus&lt;/text>
&lt;text x="550" y="100" class="ts">15 s scrape&lt;/text>
&lt;text x="550" y="116" class="ts">30-day retention&lt;/text>
&lt;rect class="bx" x="540" y="170" width="160" height="80" rx="6"/>
&lt;text x="550" y="192" class="tl">Scorecard exporter&lt;/text>
&lt;text x="550" y="210" class="ts">PromQL → JSON/CSV&lt;/text>
&lt;text x="550" y="228" class="ts">versioned in git&lt;/text>
&lt;path class="ar" d="M210,75 L280,75"/>
&lt;path class="ar" d="M460,155 L540,155"/>
&lt;path class="ar" d="M210,155 L280,155"/>
&lt;path class="dsh" d="M210,240 L540,200"/>
&lt;path class="dsh" d="M210,294 L540,220"/>
&lt;path class="ar" d="M700,100 L700,170"/>
&lt;text x="550" y="278" class="ts">one row per (model, config, hardware)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="the-kubernetes-job-the-full-yaml">The Kubernetes Job: the full YAML&lt;/h2>
&lt;p>The experiment runs as a versioned &lt;strong>Kubernetes Job&lt;/strong>. All the relevant metadata are environment variables declared in the manifest: not in ad-hoc scripts, not in external documentation. The Job is idempotent (same name = same run) and leaves traces in the pod log and in the output volume.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">batch/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bench-llama3-70b-fp8-h100x4-20260616&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">benchmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/precision&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fp8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/engine&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-0.9.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/hardware&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">h100x4-sxm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/isl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1024&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/osl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;256&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/concurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;32&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/tokenizer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">meta-llama-3-tokenizer-v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/run-id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;20260616T1200&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backoffLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Never&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serviceAccountName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bench-runner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bench-results-pvc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initContainers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Warmup: 60 s of traffic before the experiment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warmup&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/vllm-project/guidellm:0.4.2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">guidellm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">benchmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">target&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://vllm-svc.inference.svc.cluster.local:8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">rate-type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">concurrent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">rate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;4&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">max-seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;60&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">prompt_tokens=1024,output_tokens=256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GUIDELLM_ENV&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">guidellm-sweep&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/vllm-project/guidellm:0.4.2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">guidellm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">benchmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">target&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://vllm-svc.inference.svc.cluster.local:8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">rate-type&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sweep&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">max-seconds&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;120&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">prompt_tokens=1024,output_tokens=256&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">output-path&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/results/guidellm-$(BENCH_RUN_ID).json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">BENCH_RUN_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fieldRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fieldPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metadata.labels[&amp;#39;bench/run-id&amp;#39;]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aiperf-concurrent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nvcr.io/nvidia/aiperf:0.2.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">aiperf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">profile&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">url&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://vllm-svc.inference.svc.cluster.local:8000/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">meta-llama/Meta-Llama-3-70B-Instruct&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">concurrency&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;4,8,16,32&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">input-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;1024&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">output-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;256&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">num-requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;200&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">output&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/results/aiperf-$(BENCH_RUN_ID).json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">scorecard-exporter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python:3.12-slim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/scripts/export_scorecard.py&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">run-id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;$(BENCH_RUN_ID)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">prometheus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://prometheus.monitoring.svc.cluster.local:9090&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">output&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/results/scorecard-$(BENCH_RUN_ID).json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">BENCH_RUN_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fieldRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fieldPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metadata.labels[&amp;#39;bench/run-id&amp;#39;]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Job name (&lt;code>bench-llama3-70b-fp8-h100x4-20260616&lt;/code>) is the unique identifier of the run. Changing it is enough to register a variant. The labels are the metadata that the &lt;code>scorecard-exporter&lt;/code> reads to enrich the output JSON.&lt;/p>
&lt;hr>
&lt;h2 id="performance-layer-guidellm-and-aiperf">Performance layer: GuideLLM and AIPerf&lt;/h2>
&lt;h3 id="guidellm--the-slo-driven-sweep">GuideLLM — the SLO-driven sweep&lt;/h3>
&lt;p>GuideLLM (a vLLM project) generates realistic traffic patterns, synchronous, concurrent, poisson, throughput, &lt;strong>sweep&lt;/strong>, and captures full TTFT and ITL distributions (&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat Developer&lt;/a>). The &lt;code>sweep&lt;/code> mode ramps from idle to saturation over 10 rounds and identifies the &lt;strong>knee&lt;/strong>: the maximum load where goodput ≈ throughput under the declared SLO. The output is JSON/CSV with all the percentiles per round, ready to be versioned.&lt;/p>
&lt;p>For the harness, the bench&amp;rsquo;s reference SLO is:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Threshold&lt;/th>
&lt;th>Percentile&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT&lt;/td>
&lt;td>500 ms&lt;/td>
&lt;td>P99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ITL (TPOT)&lt;/td>
&lt;td>50 ms/tok&lt;/td>
&lt;td>P95&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Error rate&lt;/td>
&lt;td>0.5 %&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The Job command already appears in the YAML above. The JSON output includes, per round: rate (req/s), TTFT (P50/P95/P99), ITL (P50/P95/P99), throughput (tok/s) and goodput (tok/s). The goodput under the SLO at the knee is the value that enters the scorecard.&lt;/p>
&lt;p>For the cross-verification layer of the data and comparability with published results, see the post &lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">GuideLLM in depth&lt;/a>.&lt;/p>
&lt;h3 id="aiperf--nvidias-concurrency-profiler">AIPerf — NVIDIA&amp;rsquo;s concurrency profiler&lt;/h3>
&lt;p>AIPerf (the successor to GenAI-Perf, repository &lt;code>ai-dynamo/aiperf&lt;/code>) measures TTFT, ITL, throughput and latency distributions at fixed concurrencies (&lt;a href="https://github.com/ai-dynamo/aiperf">GitHub ai-dynamo/aiperf&lt;/a>). Where GuideLLM gives the automatic sweep up to the knee, AIPerf gives the detailed profile at specific concurrencies (4, 8, 16, 32 in the example): it allows the throughput-latency curve to be characterised point by point.&lt;/p>
&lt;p>The two are complementary: GuideLLM finds the knee automatically; AIPerf confirms it and characterises the behaviour in the neighbourhood of that knee. Both outputs go to the results volume with the same &lt;code>BENCH_RUN_ID&lt;/code>. The deep analysis of AIPerf/GenAI-Perf is in &lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">GenAI-Perf in depth&lt;/a>.&lt;/p>
&lt;p>Metrics the harness extracts from AIPerf for the scorecard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># TTFT P99 at concurrency 16 (the closest to the sweep&amp;#39;s knee):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aiperf profile ... --concurrency &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.results[] | select(.concurrency==16) | .ttft_ms.p99&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Goodput (tok/s) at concurrency 16:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aiperf profile ... --concurrency &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.results[] | select(.concurrency==16) | .output_token_throughput&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="cost-layer-opencost-and-litellm">Cost layer: OpenCost and LiteLLM&lt;/h2>
&lt;h3 id="opencost--the-denominator-in-euros-per-gpu-hour">OpenCost — the denominator in euros per GPU-hour&lt;/h3>
&lt;p>OpenCost (CNCF incubating, &lt;a href="https://opencost.io/">opencost.io&lt;/a>) allocates Kubernetes cost to namespace, label, pod and container in real time (&lt;a href="https://github.com/opencost/opencost">GitHub opencost/opencost&lt;/a>). For the harness, OpenCost answers the question: &lt;strong>how much does the &lt;code>vllm-svc&lt;/code> pod cost in euros per hour during the experiment&amp;rsquo;s window?&lt;/strong>&lt;/p>
&lt;p>The harness calls the OpenCost REST API when the experiment finishes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Cost of the inference namespace over the experiment window (1 hour)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;http://opencost.monitoring.svc.cluster.local:9003/allocation&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;window=2026-06-16T12:00:00Z,2026-06-16T13:00:00Z&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;aggregate=namespace&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;namespace=inference&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].inference.totalCost&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The returned cost (in EUR, configured with the node&amp;rsquo;s real prices) is divided by the throughput measured in that same window to obtain the CPM:&lt;/p>
$$\text{CPM} = \frac{\text{cost}_\text{namespace/h} \times 10^6}{\text{goodput (tok/s)} \times 3\,600}$$
&lt;p>The post &lt;a href="https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/">OpenCost: cost allocation in Kubernetes&lt;/a> details the price configuration and the allocation by GPU label.&lt;/p>
&lt;h3 id="litellm--the-per-request-token-counter">LiteLLM — the per-request token counter&lt;/h3>
&lt;p>LiteLLM (&lt;a href="https://www.litellm.ai/">litellm.ai&lt;/a>, &lt;a href="https://github.com/BerriAI/litellm">GitHub BerriAI/litellm&lt;/a>) acts as an OpenAI-compatible proxy with token accounting per request, model and team. In the harness, GuideLLM and AIPerf point at the LiteLLM endpoint (which in turn forwards to vLLM): every request is recorded with &lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code> and &lt;code>cost&lt;/code> (using the custom pricing configured for the on-prem node).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># litellm-config.yaml (fragment of custom on-prem pricing)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">model_list&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama3-70b-fp8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/meta-llama/Meta-Llama-3-70B-Instruct&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-svc.inference.svc.cluster.local:8000/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sk-dummy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input_cost_per_token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.00000109&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 1.09 EUR/1M tok on-prem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">output_cost_per_token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.00000109&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The LiteLLM records are exported to Prometheus as &lt;code>litellm_request_total_tokens&lt;/code> and &lt;code>litellm_spend_metric_total&lt;/code>, which the scorecard exporter consumes to compute the real CPM per token type (input vs output).&lt;/p>
&lt;hr>
&lt;h2 id="energy-layer-dcgm-and-kepler">Energy layer: DCGM and Kepler&lt;/h2>
&lt;h3 id="dcgm-exporter--gpu-power-and-energy">DCGM Exporter — GPU power and energy&lt;/h3>
&lt;p>DCGM Exporter (&lt;a href="https://github.com/NVIDIA/dcgm-exporter">GitHub NVIDIA/dcgm-exporter&lt;/a>, &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html">docs&lt;/a>) exposes GPU metrics on &lt;code>/metrics&lt;/code> for Prometheus as a DaemonSet on the GPU nodes. The two energy metrics the harness uses:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>DCGM metric&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Use in the harness&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;td>gauge&lt;/td>
&lt;td>W&lt;/td>
&lt;td>instantaneous power per GPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code>&lt;/td>
&lt;td>counter&lt;/td>
&lt;td>mJ&lt;/td>
&lt;td>energy accumulated since boot&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>To compute the energy consumed during the experiment&amp;rsquo;s window (without the idle baseline), the harness takes the difference of the counter before and after the sweep:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Deployment as a DaemonSet (fragment of the official chart)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo add gpu-helm-charts &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> https://nvidia.github.io/dcgm-exporter/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install dcgm-exporter gpu-helm-charts/dcgm-exporter &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set serviceMonitor.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set serviceMonitor.interval&lt;span class="o">=&lt;/span>15s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code> field (in mJ) allows the experiment&amp;rsquo;s energy to be computed with hardware-counter accuracy, not with an estimate:&lt;/p>
$$\text{experiment energy (J)} = \left(\text{counter}_\text{end} - \text{counter}_\text{start}\right) \times 10^{-3}$$
$$\text{Wh/token} = \frac{\text{experiment energy (J)}}{3\,600 \times \text{generated tokens}}$$
&lt;h3 id="kepler--pod-level-energy">Kepler — pod-level energy&lt;/h3>
&lt;p>Kepler (CNCF sandbox, &lt;a href="https://github.com/sustainable-computing-io/kepler">GitHub sustainable-computing-io/kepler&lt;/a>) uses eBPF to estimate energy consumption at container and pod level, exporting &lt;code>kepler_container_joules_total&lt;/code> to Prometheus (&lt;a href="https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/">Red Hat Emerging Technologies&lt;/a>). It combines RAPL (CPU/DRAM), NVML (GPU) and regression models when no sensors are available.&lt;/p>
&lt;p>In the harness, Kepler complements DCGM: DCGM gives the hardware measurement of the GPU (more precise for GPU-intensive loads such as LLM inference), while Kepler attributes the energy to the specific vLLM pod (including the node&amp;rsquo;s CPU contribution). The main metric the harness consumes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Energy of the vllm-svc pod during the experiment (J)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">increase&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nl">container_namespace&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">inference&lt;/span>&lt;span class="p">&amp;#34;,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nl">container_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">vllm&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}[&lt;/span>&lt;span class="err">${BENCH_DURATION}&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The comparative analysis of DCGM vs Kepler vs Zeus is in &lt;a href="https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/">Energy tools: deployment, precision and overhead&lt;/a>.&lt;/p>
&lt;h3 id="the-mlperf-power-reference">The MLPerf Power reference&lt;/h3>
&lt;p>MLPerf Power (&lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">MLCommons&lt;/a>, IEEE HPCA 2025 paper) establishes the protocol for measuring the energy efficiency of ML systems with high-precision external measurement. The in-house bench &lt;strong>is not an MLPerf submission&lt;/strong> (that requires external power meters and committee review), but its published results are the calibration reference: if the harness gives a result of the same order as the MLPerf submission for the same hardware with the same model, the measurement is plausible. If it differs by more than a factor of 2, there is a methodological problem. See the post &lt;a href="https://blog.lo0.es/en/posts/mlperf-power-energy-efficiency-benchmark/">MLPerf Power: energy efficiency&lt;/a> for the current reference data.&lt;/p>
&lt;hr>
&lt;h2 id="unifying-the-metrics-the-promql-queries">Unifying the metrics: the PromQL queries&lt;/h2>
&lt;p>The &lt;code>scorecard-exporter&lt;/code> is the Job container that, once GuideLLM and AIPerf have finished, collects all the metrics from Prometheus and builds the JSON of the scorecard row. The key queries:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Mean GPU power during the sweep (W) — 4×H100 node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">avg_over_time&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_POWER_USAGE&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">${BENCH_DURATION}:&lt;/span>&lt;span class="s">15s&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Total GPU energy during the sweep (mJ → convert to J)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">offset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">offset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="err">BENCH_DURATION&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Tokens generated during the sweep (from LiteLLM)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kr">increase&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">litellm_request_total_tokens&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">llama3-70b-fp8&lt;/span>&lt;span class="p">&amp;#34;,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">token_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">completion&lt;/span>&lt;span class="p">&amp;#34;}[&lt;/span>&lt;span class="err">${BENCH_DURATION}&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Cost of the inference namespace over the window (from the OpenCost API)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># → REST call at the start and at the end of the Job, difference of accrual&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>${BENCH_DURATION}&lt;/code> variable is the real duration of the sweep (in Prometheus format, e.g. &lt;code>22m&lt;/code>), which the exporter computes as &lt;code>end_ts - start_ts&lt;/code> and substitutes into every query.&lt;/p>
&lt;p>The output JSON of each run has this structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;20260616T1200&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;meta-llama/Meta-Llama-3-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;engine&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm-0.9.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;precision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fp8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hardware&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;4xH100-SXM-80GB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;isl_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;osl_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tokenizer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;meta-llama-3-tokenizer-v3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slo_ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slo_itl_p95_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bench_tool_guidellm&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.4.2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bench_tool_aiperf&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dcgm_exporter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;3.3.9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kepler&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.10.2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;performance&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;goodput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3120&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">487&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;itl_p95_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;throughput_peak_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3890&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;elbow_concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cost&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cpm_eur_1m&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.97&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gpu_cost_eur_h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">10.8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cost_window_eur&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wh_per_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.00044&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;j_per_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.58&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;power_mean_w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4924&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;energy_total_kwh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.287&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wh_per_token_pue_adjusted&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.000616&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;carbon&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;grid_intensity_gco2_kwh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;co2_per_1m_tokens_g&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">24.6&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All the fields are computed from the same time windows. The JSON is versioned in git alongside the harness code. Reproducing any scorecard row is: &lt;code>git checkout &amp;lt;run-id&amp;gt; &amp;amp;&amp;amp; kubectl apply -f job.yaml&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="the-3-axis-scorecard-table-and-interpretation">The 3-axis scorecard: table and interpretation&lt;/h2>
&lt;p>The following table illustrates how five configurations of the same model (Llama 3 70B) on the same reference node (4×H100 SXM) compare with the harness. The figures are &lt;strong>illustrative of the order of magnitude&lt;/strong> (the bench fills them in with real measurements):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Config&lt;/th>
&lt;th>Engine&lt;/th>
&lt;th>Precision&lt;/th>
&lt;th>CPM (€/1M)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;th>Wh/tok&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P95 (ms)&lt;/th>
&lt;th>CO2 /1M (g, FR)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>A&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP16&lt;/td>
&lt;td>1.64&lt;/td>
&lt;td>1,890&lt;/td>
&lt;td>0.00074&lt;/td>
&lt;td>498&lt;/td>
&lt;td>49&lt;/td>
&lt;td>29.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>B&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>0.97&lt;/td>
&lt;td>3,120&lt;/td>
&lt;td>0.00044&lt;/td>
&lt;td>487&lt;/td>
&lt;td>42&lt;/td>
&lt;td>17.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>C&lt;/strong>&lt;/td>
&lt;td>SGLang 0.4&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>0.88&lt;/td>
&lt;td>3,410&lt;/td>
&lt;td>0.00041&lt;/td>
&lt;td>421&lt;/td>
&lt;td>38&lt;/td>
&lt;td>16.4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>D&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8, ISL 512&lt;/td>
&lt;td>1.31&lt;/td>
&lt;td>2,240&lt;/td>
&lt;td>0.00056&lt;/td>
&lt;td>294&lt;/td>
&lt;td>31&lt;/td>
&lt;td>22.4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>E&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8, max-batch 128&lt;/td>
&lt;td>0.84&lt;/td>
&lt;td>3,590&lt;/td>
&lt;td>0.00040&lt;/td>
&lt;td>721&lt;/td>
&lt;td>55&lt;/td>
&lt;td>16.0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>How to read the table:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>B vs A&lt;/strong>: FP8 over FP16 cuts CPM by 41 % and energy per token by 41 % thanks to the same higher throughput. Both meet the SLO (TTFT P99 &amp;lt; 500 ms, ITL P95 &amp;lt; 50 ms). B dominates A on all three axes: it is Pareto-superior.&lt;/li>
&lt;li>&lt;strong>C vs B&lt;/strong>: SGLang gives 9 % more goodput and 9 % lower CPM with FP8. Both meet the SLO. C is Pareto-superior to B (if the engine is indifferent for the stack).&lt;/li>
&lt;li>&lt;strong>D vs B&lt;/strong>: a shorter ISL (512 tok) lowers latency (TTFT P99 294 ms vs 487 ms) but lowers goodput and raises CPM. If the use case demands TTFT &amp;lt; 300 ms, D is the candidate; if TTFT &amp;lt; 500 ms is enough, B is better on cost and energy.&lt;/li>
&lt;li>&lt;strong>E vs C&lt;/strong>: &lt;code>max-batch 128&lt;/code> raises goodput (+5 %) and lowers CPM, but the TTFT P99 breaks the SLO (721 ms &amp;gt; 500 ms) and so does the ITL P95 (55 ms &amp;gt; 50 ms). E has the best raw throughput &lt;strong>but is outside the SLO&lt;/strong>: it is not a valid candidate for the interactive chat use case.&lt;/li>
&lt;/ul>
&lt;p>The scorecard&amp;rsquo;s &lt;strong>Pareto frontier&lt;/strong>, under the declared SLO, includes only A, B, C and D (E falls out for breaking the SLO). Of those four, C dominates B which dominates A. D only enters the frontier if the use case demands TTFT P99 &amp;lt; 300 ms. The decision is not a number; it is the whole row plus the SLO.&lt;/p>
&lt;hr>
&lt;h2 id="the-multi-objective-pareto-frontier">The multi-objective Pareto frontier&lt;/h2>
&lt;p>With three minimisation metrics, CPM (€/1M tok), Wh/tok and TTFT P99 (ms), the Pareto frontier is defined as the set of configurations where none dominates another on all three axes simultaneously. Formally, config \( i \) &lt;strong>dominates&lt;/strong> config \( j \) if:&lt;/p>
$$\text{CPM}_i \leq \text{CPM}_j \;\land\; \text{Wh/tok}_i \leq \text{Wh/tok}_j \;\land\; \text{TTFT}_{i} \leq \text{TTFT}_{j}$$
&lt;p>with at least one strict inequality. The scorecard allows it to be computed over the set of runs with a simple vector operation. In the table&amp;rsquo;s example, the frontier under the SLO (with E excluded) is \(\{C, D\}\): C dominates on cost/energy/goodput; D dominates on latency. Between C and D the choice depends on the TTFT SLO of the specific use case.&lt;/p>
&lt;div class="diagram" style="max-width:700px;margin:1rem auto;">
&lt;svg viewBox="0 0 700 320" role="img" aria-label="Pareto frontier on two axes: CPM EUR per million tokens on the Y axis and goodput tok per s on the X axis; points A B C D E marked; E excluded for breaking the SLO; the Pareto curve passes through C and D" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.ts{font:11px sans-serif;fill:currentColor}.tl{font:600 12px sans-serif;fill:currentColor}.pt{fill:currentColor}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}&lt;/style>
&lt;line class="ax" x1="70" y1="30" x2="70" y2="270"/>
&lt;line class="ax" x1="70" y1="270" x2="660" y2="270"/>
&lt;text x="10" y="155" class="ts" transform="rotate(-90 10 155)">CPM (EUR/1M tok) ↑ worse&lt;/text>
&lt;text x="320" y="300" class="ts">goodput (tok/s) → better&lt;/text>
&lt;text x="70" y="26" class="ts">1.70&lt;/text>
&lt;text x="70" y="80" class="ts">1.40&lt;/text>
&lt;text x="70" y="134" class="ts">1.10&lt;/text>
&lt;text x="70" y="188" class="ts">0.90&lt;/text>
&lt;text x="70" y="242" class="ts">0.84&lt;/text>
&lt;text x="115" y="284" class="ts">1,800&lt;/text>
&lt;text x="263" y="284" class="ts">2,200&lt;/text>
&lt;text x="410" y="284" class="ts">3,100&lt;/text>
&lt;text x="520" y="284" class="ts">3,400&lt;/text>
&lt;text x="600" y="284" class="ts">3,600&lt;/text>
&lt;circle class="pt" cx="130" cy="55" r="5"/>
&lt;text x="140" y="52" class="tl">A (FP16)&lt;/text>
&lt;circle class="pt" cx="380" cy="170" r="5"/>
&lt;text x="390" y="167" class="tl">B (FP8)&lt;/text>
&lt;circle class="pt" cx="500" cy="152" r="5"/>
&lt;text x="510" y="148" class="tl">C (SGLang FP8) ★&lt;/text>
&lt;circle class="pt" cx="270" cy="195" r="5"/>
&lt;text x="280" y="192" class="tl">D (ISL 512)&lt;/text>
&lt;circle class="pt" cx="570" cy="138" r="4" opacity="0.4"/>
&lt;text x="580" y="135" class="ts" opacity="0.4">E (SLO broken)&lt;/text>
&lt;path class="dsh" d="M130,55 L270,195 L500,152"/>
&lt;text x="190" y="148" class="ts">Pareto frontier&lt;/text>
&lt;text x="72" y="286" class="ts">(A dominates only on latency; C+D on the frontier under the SLO)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="reproducible-design-the-twelve-mandatory-metadata">Reproducible design: the twelve mandatory metadata&lt;/h2>
&lt;p>The checklist that closes the bias dossier (&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Bias and reproducibility&lt;/a>). For the harness JSON to be auditable, the twelve fields must be present in every run:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Field&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Why it is mandatory&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Serving engine version&lt;/td>
&lt;td>&lt;code>vllm-0.9.1&lt;/code>&lt;/td>
&lt;td>the same model can perform differently across versions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>Model version (commit/tag)&lt;/td>
&lt;td>&lt;code>meta-llama/Meta-Llama-3-70B-Instruct@sha256:abc&lt;/code>&lt;/td>
&lt;td>avoids ambiguity between variants of the same name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Precision&lt;/td>
&lt;td>&lt;code>fp8&lt;/code>&lt;/td>
&lt;td>FP16 vs FP8 changes throughput and latency&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Tokeniser and version&lt;/td>
&lt;td>&lt;code>meta-llama-3-tokenizer-v3&lt;/code>&lt;/td>
&lt;td>the ISL/OSL in tokens depends on the tokeniser&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>ISL (input sequence length)&lt;/td>
&lt;td>&lt;code>1024&lt;/code> tok&lt;/td>
&lt;td>changes the prefill time and the knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>OSL (output sequence length)&lt;/td>
&lt;td>&lt;code>256&lt;/code> tok&lt;/td>
&lt;td>changes the decode time&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Hardware (model and count)&lt;/td>
&lt;td>&lt;code>4×H100-SXM-80GB&lt;/code>&lt;/td>
&lt;td>the basis of any comparison&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>Maximum concurrency tested&lt;/td>
&lt;td>&lt;code>32&lt;/code>&lt;/td>
&lt;td>defines the sweep&amp;rsquo;s range&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>Bench tool version&lt;/td>
&lt;td>&lt;code>guidellm-0.4.2&lt;/code>, &lt;code>aiperf-0.2.0&lt;/code>&lt;/td>
&lt;td>different versions can give different metrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>Declared SLO&lt;/td>
&lt;td>&lt;code>TTFT P99 &amp;lt; 500 ms, ITL P95 &amp;lt; 50 ms&lt;/code>&lt;/td>
&lt;td>the knee depends on the SLO; without it there is no goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11&lt;/td>
&lt;td>Warmup duration&lt;/td>
&lt;td>&lt;code>60 s&lt;/code>&lt;/td>
&lt;td>without warmup the KV cache is cold and the first rounds are biased&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12&lt;/td>
&lt;td>Harness version / run-id&lt;/td>
&lt;td>&lt;code>20260616T1200&lt;/code>&lt;/td>
&lt;td>identifies the run so it can be reproduced with &lt;code>git checkout&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A scorecard missing any of these twelve fields is an anecdote. With all twelve, it is an auditable datum. The run-id in the Job name (&lt;code>bench-llama3-70b-fp8-h100x4-20260616&lt;/code>) implicitly incorporates fields 1-3 and 7, forcing the Job name to change with every variant, which makes it impossible to overlap runs in the same namespace.&lt;/p>
&lt;hr>
&lt;h2 id="idempotence-and-versioning-of-results">Idempotence and versioning of results&lt;/h2>
&lt;p>The Kubernetes Job is idempotent: if the same manifest is applied again (same name), the Job already exists and Kubernetes does not relaunch it (&lt;code>backoffLimit: 0&lt;/code> policy). This avoids accidental duplicate runs. To relaunch, the Job has to be deleted (&lt;code>kubectl delete job &amp;lt;name&amp;gt;&lt;/code>) or the &lt;code>run-id&lt;/code> changed.&lt;/p>
&lt;p>Results are versioned with the following directory convention in the harness&amp;rsquo;s git repository:&lt;/p>
&lt;pre tabindex="0">&lt;code>results/
20260616T1200/
guidellm-20260616T1200.json
aiperf-20260616T1200.json
scorecard-20260616T1200.json
20260617T0900/
guidellm-20260617T0900.json
aiperf-20260617T0900.json
scorecard-20260617T0900.json
scorecard-aggregate.csv # all rows, for analysis and charts
&lt;/code>&lt;/pre>&lt;p>The &lt;code>scorecard-aggregate.csv&lt;/code> is the scorecard table in flat format: each row is a run, each column a field of the JSON. It is generated with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Generates the aggregate CSV from all the JSON files in results/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/aggregate_scorecards.py results/ &amp;gt; scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The CSV is what feeds the Pareto charts, the reports and the architecture decisions. It is versioned in git; its diff is the configuration change.&lt;/p>
&lt;hr>
&lt;h2 id="how-it-connects-with-the-other-posts-in-the-series">How it connects with the other posts in the series&lt;/h2>
&lt;p>The harness is not a standalone article: it is the synthesis of the twenty-eight articles. Each tool has its deep dive:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Performance&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">GuideLLM in depth&lt;/a> — sweep, SLO, goodput, knee; &lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">GenAI-Perf / AIPerf in depth&lt;/a> — concurrency, rate sweep, distribution metrics.&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/">OpenCost: cost allocation in Kubernetes&lt;/a> — allocation by namespace/label, custom on-prem prices.&lt;/li>
&lt;li>&lt;strong>Energy&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/">Energy tools: deployment, precision and overhead&lt;/a> — DCGM vs Kepler vs Zeus, instrumentation overhead, relative precision; &lt;a href="https://blog.lo0.es/en/posts/mlperf-power-energy-efficiency-benchmark/">MLPerf Power: energy efficiency&lt;/a> — the external calibration reference.&lt;/li>
&lt;li>&lt;strong>Theoretical framework&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">The three axes: cost, performance and energy&lt;/a> — the CPM/energy/throughput identity that justifies measuring them together.&lt;/li>
&lt;li>&lt;strong>Bias&lt;/strong>: &lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Bias and reproducibility in benchmarking&lt;/a> — the twelve biases the harness removes.&lt;/li>
&lt;/ul>
&lt;p>The harness closes the series&amp;rsquo; dossier because it makes the opening article&amp;rsquo;s promise possible: &amp;ldquo;when someone disputes a number in the proposal, the answer is not &amp;lsquo;a blog says so&amp;rsquo;, but &amp;rsquo;this is the bench, this is the methodology, reproduce it&amp;rsquo;&amp;rdquo;. With the run-id and the git repo, reproducing is one command.&lt;/p>
&lt;hr>
&lt;h2 id="full-flow-of-a-benchmarking-session">Full flow of a benchmarking session&lt;/h2>
&lt;p>The harness&amp;rsquo;s standard operating procedure, from start to finish:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Make sure the inference endpoint is up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc vllm-svc -n inference
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Verify that DCGM and Kepler are scraping&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s http://prometheus.monitoring.svc.cluster.local:9090/api/v1/query &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;query=DCGM_FI_DEV_POWER_USAGE&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.result | length&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Record the energy counter BEFORE the experiment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">PRE_ENERGY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl &lt;span class="nb">exec&lt;/span> -n monitoring dcgm-exporter-xxx -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -s localhost:9400/metrics &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> grep DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{sum+=$2} END{print sum}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. Launch the Job (unique name per run)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f jobs/bench-llama3-70b-fp8-h100x4-20260616.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 5. Wait for it to finish&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">complete&lt;/span> job/bench-llama3-70b-fp8-h100x4-20260616 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n benchmark --timeout&lt;span class="o">=&lt;/span>3600s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 6. Record the energy counter AFTER&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">POST_ENERGY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl &lt;span class="nb">exec&lt;/span> -n monitoring dcgm-exporter-xxx -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -s localhost:9400/metrics &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> grep DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{sum+=$2} END{print sum}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 7. Collect the results from the volume&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cp benchmark/bench-run-pod:/results/ ./results/20260616T1200/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 8. Compute the experiment&amp;#39;s energy (mJ → kWh)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/calc_energy.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --pre &lt;span class="nv">$PRE_ENERGY&lt;/span> --post &lt;span class="nv">$POST_ENERGY&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --run-id 20260616T1200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 9. Add a row to the aggregate CSV&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/aggregate_scorecards.py results/ &amp;gt; scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 10. Version it&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add results/20260616T1200/ scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;bench: llama3-70b fp8 4xH100 ISL1024 OSL256 (20260616T1200)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Steps 6-9 are candidates for automation as a second post-processing Job launched when the main one completes (with &lt;code>ownerReferences&lt;/code> or a clean-up CronJob). In the harness&amp;rsquo;s basic state, the manual steps are precisely the ones that force a review of the result before versioning it.&lt;/p>
&lt;hr>
&lt;h2 id="cost-of-the-experiment">Cost of the experiment&lt;/h2>
&lt;p>A GuideLLM sweep of 10 rounds at 120 seconds per round occupies the node for ~22-26 minutes. The AIPerf profile at 4 concurrencies (200 requests each) adds ~8-12 minutes. Total per run: ~35-40 minutes of a 4×H100 node.&lt;/p>
&lt;p>At the reference amortised cost (~10.8 €/h for 4×H100 on-prem):&lt;/p>
$$\text{cost per run} \approx 10.8 \times \frac{38}{60} \approx 6.84 \text{ EUR}$$
&lt;p>At the reference European cloud price (4 × 2.73 = 10.92 €/h):&lt;/p>
$$\text{cost per run (cloud)} \approx 10.92 \times \frac{38}{60} \approx 6.92 \text{ EUR}$$
&lt;p>A continuous benchmarking programme (10 configurations × 3 models × 2 sweeps/week) adds up to ~415 EUR/week. That is why the harness includes short sweeps (5 rounds, ~15 minutes) for CI and full sweeps for releases.&lt;/p>
&lt;hr>
&lt;h2 id="what-the-harness-does-not-measure-and-why">What the harness does not measure (and why)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Missing dimension&lt;/th>
&lt;th>Reason&lt;/th>
&lt;th>Where it is covered&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Output quality (MMLU, HumanEval)&lt;/td>
&lt;td>requires a quality evaluation bench, not a serving one&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">LLM quality benchmarks&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Training throughput&lt;/td>
&lt;td>the harness is exclusively for inference&lt;/td>
&lt;td>outside the scope of the &amp;ldquo;data&amp;rdquo; series&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Network and storage cost&lt;/td>
&lt;td>OpenCost can attribute it, but it requires additional configuration&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/">OpenCost: cost allocation&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hourly carbon intensity&lt;/td>
&lt;td>use the ElectricityMaps API + the measured energy&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">From the watt to carbon: PUE and grid mix&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client-server network latency&lt;/td>
&lt;td>the Job runs inside the cluster; WAN latency requires an external client&lt;/td>
&lt;td>document as additional metadata&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>GuideLLM · GitHub (proyecto vLLM) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&lt;/a>&lt;/li>
&lt;li>GuideLLM · PyPI — &lt;a href="https://pypi.org/project/guidellm/">https://pypi.org/project/guidellm/&lt;/a>&lt;/li>
&lt;li>Red Hat Developer · desplegar y benchmarkear vLLM con GuideLLM en Kubernetes — &lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes&lt;/a>&lt;/li>
&lt;li>Red Hat Developer · GuideLLM: evaluar despliegues LLM para inferencia real — &lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference&lt;/a>&lt;/li>
&lt;li>AIPerf · GitHub (ai-dynamo/aiperf) — &lt;a href="https://github.com/ai-dynamo/aiperf">https://github.com/ai-dynamo/aiperf&lt;/a>&lt;/li>
&lt;li>GenAI-Perf · documentación NVIDIA Triton — &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html&lt;/a>&lt;/li>
&lt;li>NVIDIA · LLM performance benchmarking con GenAI-Perf — &lt;a href="https://developer.nvidia.com/blog/llm-performance-benchmarking-measuring-nvidia-nim-performance-with-genai-perf">https://developer.nvidia.com/blog/llm-performance-benchmarking-measuring-nvidia-nim-performance-with-genai-perf&lt;/a>&lt;/li>
&lt;li>NVIDIA NIM Benchmarking · métricas (TTFT, ITL, throughput) — &lt;a href="https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html">https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html&lt;/a>&lt;/li>
&lt;li>OpenCost · web oficial — &lt;a href="https://opencost.io/">https://opencost.io/&lt;/a>&lt;/li>
&lt;li>OpenCost · GitHub (opencost/opencost) — &lt;a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost&lt;/a>&lt;/li>
&lt;li>OpenCost · CNCF blog (sandbox → incubating) — &lt;a href="https://www.cncf.io/blog/2022/12/06/opencost-a-new-cncf-sandbox-project-for-real-time-kubernetes-cost-monitoring/">https://www.cncf.io/blog/2022/12/06/opencost-a-new-cncf-sandbox-project-for-real-time-kubernetes-cost-monitoring/&lt;/a>&lt;/li>
&lt;li>LiteLLM · web oficial — &lt;a href="https://www.litellm.ai/">https://www.litellm.ai/&lt;/a>&lt;/li>
&lt;li>LiteLLM · GitHub (BerriAI/litellm) — &lt;a href="https://github.com/BerriAI/litellm">https://github.com/BerriAI/litellm&lt;/a>&lt;/li>
&lt;li>LiteLLM · Spend Tracking docs — &lt;a href="https://docs.litellm.ai/docs/proxy/cost_tracking">https://docs.litellm.ai/docs/proxy/cost_tracking&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · documentación oficial — &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html">https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · GitHub — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · métricas CSV — &lt;a href="https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/dcp-metrics-included.csv">https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/dcp-metrics-included.csv&lt;/a>&lt;/li>
&lt;li>Kepler (Kubernetes-based Efficient Power Level Exporter) · GitHub — &lt;a href="https://github.com/sustainable-computing-io/kepler">https://github.com/sustainable-computing-io/kepler&lt;/a>&lt;/li>
&lt;li>Red Hat Emerging Technologies · Introducing Kepler — &lt;a href="https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/">https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference Datacenter benchmark — &lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">https://mlcommons.org/benchmarks/inference-datacenter/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Power presentado en IEEE HPCA 2025 — &lt;a href="https://mlcommons.org/2025/03/ml-commons-power-hpca/">https://mlcommons.org/2025/03/ml-commons-power-hpca/&lt;/a>&lt;/li>
&lt;li>IEEE Xplore · MLPerf Power paper (μWatts to MWatts) — &lt;a href="https://ieeexplore.ieee.org/document/10946778/">https://ieeexplore.ieee.org/document/10946778/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 results (septiembre 2025) — &lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/&lt;/a>&lt;/li>
&lt;li>Red Hat · Efficient and reproducible LLM inference: MLPerf Inference v5.1 — &lt;a href="https://www.redhat.com/en/blog/efficient-and-reproducible-llm-inference-red-hat-mlperf-inference-v51-results">https://www.redhat.com/en/blog/efficient-and-reproducible-llm-inference-red-hat-mlperf-inference-v51-results&lt;/a>&lt;/li>
&lt;li>BentoML · Beyond tokens-per-second: cost, speed and quality in LLM inference — &lt;a href="https://www.bentoml.com/blog/beyond-tokens-per-second-how-to-balance-speed-cost-and-quality-in-llm-inference">https://www.bentoml.com/blog/beyond-tokens-per-second-how-to-balance-speed-cost-and-quality-in-llm-inference&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GPU idle: the cost that shows up on no invoice but is paid by the whole TCO</title><link>https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/</link><pubDate>Mon, 15 Jun 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The dollar sign is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The cost per token of on-premise inference is \( \text{€/GPU-hour} \div \text{throughput} \). Throughput is a direct function of the GPU&amp;rsquo;s &lt;strong>useful occupancy&lt;/strong>. On a generic node of 4×H100 SXM with a reference cost of ~11 € per GPU-hour (depreciation plus energy plus infrastructure), the cost curve over occupancy looks like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Useful occupancy&lt;/th>
&lt;th>Effective throughput (tok/s)&lt;/th>
&lt;th>Cost per 1M tokens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>20 %&lt;/td>
&lt;td>~700&lt;/td>
&lt;td>~43 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>40 %&lt;/td>
&lt;td>~1,400&lt;/td>
&lt;td>~21 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>70 %&lt;/td>
&lt;td>~2,500&lt;/td>
&lt;td>~12 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>85 % (practical ceiling)&lt;/td>
&lt;td>~3,000&lt;/td>
&lt;td>~10 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Doubling occupancy from 20 % to 40 % halves the cost per token, without buying more iron.&lt;/strong> At 70 % the cost competes with European cloud providers (~2.2 €/GPU-hour on-demand at Scaleway). At 20 % on-prem is four times dearer than renting. The lever is not the model, nor the precision: it is how many paid-for GPU-hours produce serving tokens.&lt;/p>
&lt;hr>
&lt;h2 id="1--the-fundamental-identity">1 · The fundamental identity&lt;/h2>
&lt;p>The cost per million tokens (CPM) in your own inference is not a list price. It is:&lt;/p>
$$
\text{CPM} = \frac{C_{\text{GPU}} \cdot N_{\text{GPU}}}{T_{\text{ef}} \times 3600 / 10^6}
$$
&lt;p>where \( C_{\text{GPU}} \) is the cost per GPU-hour (€/h), \( N_{\text{GPU}} \) the number of GPUs assigned to the service and \( T_{\text{ef}} \) the effective throughput (tok/s). Making the dependence on occupancy explicit:&lt;/p>
$$
T_{\text{ef}} = T_{\text{pico}} \times \rho
$$
&lt;p>where \( \rho \in [0,1] \) is the &lt;strong>useful occupancy rate&lt;/strong> (the fraction of time in which the GPU is processing real serving tokens). The resulting identity:&lt;/p>
$$
\text{CPM} = \frac{C_{\text{GPU}} \cdot N_{\text{GPU}}}{T_{\text{pico}} \times \rho \times 3600 / 10^6}
$$
&lt;p>The direct consequence: CPM is &lt;strong>inversely proportional to&lt;/strong> \( \rho \). Doubling \( \rho \) halves CPM. The numerator (cost of the iron) does not change.&lt;/p>
&lt;p>The posts &lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity-planning-inferencia-llm-on-premise&lt;/a> cover how to calculate \( C_{\text{GPU}} \) and the reference peak throughput. This article deals with \( \rho \): how to measure it, what limits it and how to raise it.&lt;/p>
&lt;hr>
&lt;h2 id="2--why-the-standard-metric-lies-dcgm_fi_dev_gpu_util">2 · Why the standard metric lies: &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/h2>
&lt;p>The field &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> (field ID 203) appears in &lt;code>nvidia-smi&lt;/code> as &amp;ldquo;GPU-Util&amp;rdquo;. Its official definition in the DCGM documentation:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;GPU Utilization&amp;rdquo; — percentage of time during which one or more kernels were executing on the GPU.&lt;/p>
&lt;/blockquote>
&lt;p>The problem for LLM inference: &lt;strong>the decode phase is memory-bound&lt;/strong>. The GPU runs a kernel reading weights from HBM token by token; therefore &lt;code>GPU_UTIL&lt;/code> registers close to 100 % even though the tensor cores are at 15 % of their capacity. The field measures &lt;em>activity&lt;/em>, not &lt;em>useful work&lt;/em>.&lt;/p>
&lt;p>The distinction is critical for FinOps: an operator who sees &lt;code>GPU_UTIL 98 %&lt;/code> assumes &amp;ldquo;saturated GPU, no room for more load&amp;rdquo;. The reality may be &amp;ldquo;the tensor cores are at 20 % and the bottleneck is HBM&amp;rdquo;, which leaves room for continuous batching or additional bin-packing.&lt;/p>
&lt;p>The metrics that measure real occupancy are those of the &lt;code>_FI_PROF_*&lt;/code> subsystem, available in DCGM 3.x with the profiling module enabled:&lt;/p>
&lt;h3 id="table-of-relevant-dcgm-fields">Table of relevant DCGM fields&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>DCGM field&lt;/th>
&lt;th>Field ID&lt;/th>
&lt;th>What it measures&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Operational note&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/td>
&lt;td>203&lt;/td>
&lt;td>% time with ≥1 active kernel&lt;/td>
&lt;td>%&lt;/td>
&lt;td>&lt;strong>Misleading&lt;/strong> in LLM decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code>&lt;/td>
&lt;td>1002&lt;/td>
&lt;td>Ratio of cycles with ≥1 active warp per SM&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Compute activity, not occupancy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;td>1003&lt;/td>
&lt;td>Resident warps / theoretical maximum per SM&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Real intra-SM parallelism&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;td>1004&lt;/td>
&lt;td>% cycles with tensor cores (HMMA) active&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>The real compute efficiency metric&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;td>1005&lt;/td>
&lt;td>% cycles with HBM transferring&lt;/td>
&lt;td>0–1&lt;/td>
&lt;td>Memory saturation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PCIE_TX_BYTES&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>TX bytes over PCIe&lt;/td>
&lt;td>bytes/s&lt;/td>
&lt;td>Useful in PCIe inference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code>&lt;/td>
&lt;td>252&lt;/td>
&lt;td>HBM used&lt;/td>
&lt;td>MiB&lt;/td>
&lt;td>VRAM budget&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Real consumption&lt;/td>
&lt;td>W&lt;/td>
&lt;td>For real energy cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Bitmap of throttle causes&lt;/td>
&lt;td>bitmap&lt;/td>
&lt;td>Detects silent degradation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>_PROF_*&lt;/code> fields require the DCGM profiling module and the right driver permissions. They are documented exhaustively in the NVIDIA DCGM field ID reference and in the sibling post &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a>.&lt;/p>
&lt;h3 id="characteristic-reading-in-llm-decode-llama-70b-fp8-on-h100">Characteristic reading in LLM decode (Llama 70B FP8 on H100)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Typical decode value&lt;/th>
&lt;th>Interpretation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/td>
&lt;td>95–99 %&lt;/td>
&lt;td>&lt;strong>A lie&lt;/strong>: there is an active kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.45–0.65&lt;/td>
&lt;td>SMs with warps 45–65 % of the time&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;td>0.30–0.55&lt;/td>
&lt;td>Resident warps at 30–55 % of the maximum&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.10–0.25&lt;/td>
&lt;td>Tensor cores active only 10–25 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;td>0.75–0.90&lt;/td>
&lt;td>HBM busy 75–90 % of the time&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reading: decode is &lt;strong>memory-bound&lt;/strong>. The model weights are loaded from HBM for each token; HBM is saturated but the tensor cores wait. All of the &amp;ldquo;utilisation&amp;rdquo; in &lt;code>nvidia-smi&lt;/code> comes from memory reads, not from compute.&lt;/p>
&lt;hr>
&lt;h2 id="3--mfu-and-hfu-occupancy-expressed-in-flops">3 · MFU and HFU: occupancy expressed in FLOPs&lt;/h2>
&lt;p>The canonical compute efficiency metric is &lt;strong>MFU (Model FLOPs Utilization)&lt;/strong>, defined in the PaLM paper (Chowdhery et al., arXiv 2204.02311, section 4):&lt;/p>
$$
\text{MFU} = \frac{T_{\text{obs}} \times C_{\text{modelo}}}{P_{\text{pico}}}
$$
&lt;p>where \( T_{\text{obs}} \) is the observed throughput (tok/s) and \( P_{\text{pico}} \) is the theoretical peak performance of the hardware (FLOP/s).&lt;/p>
&lt;p>where \( C_{\text{modelo}} \) is the number of FLOPs per token in a complete forward pass. For a dense transformer with \( P \) parameters, the usual approximation (forward plus backward = 6P FLOPs per token; forward only = 2P):&lt;/p>
$$
C_{\text{modelo}} \approx 2P \quad \text{(inference)}
$$
$$
C_{\text{modelo}} \approx 6P \quad \text{(training, forward + backward)}
$$
&lt;p>&lt;strong>HFU (Hardware FLOPs Utilization)&lt;/strong> measures the FLOPs &lt;strong>actually executed&lt;/strong> in hardware, including activation recomputation (gradient checkpointing). In training with recompute:&lt;/p>
$$
C_{\text{hardware}} \approx 8P \quad \text{(forward × 2 + backward × 4)}
$$
&lt;p>therefore HFU &amp;gt; MFU when there is recompute; they are identical without it.&lt;/p>
&lt;h3 id="typical-mfu-values">Typical MFU values&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Regime&lt;/th>
&lt;th>Hardware&lt;/th>
&lt;th>Typical MFU&lt;/th>
&lt;th>Limiting regime&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Training (large batch, BF16)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>35–50 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Training PaLM 540B&lt;/td>
&lt;td>TPU v4&lt;/td>
&lt;td>46.2 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefill inference (large batch)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>25–45 %&lt;/td>
&lt;td>compute-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode inference (bs=1)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>3–8 %&lt;/td>
&lt;td>memory-bound&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode inference (continuous batching, bs=32–64)&lt;/td>
&lt;td>H100 SXM&lt;/td>
&lt;td>15–30 %&lt;/td>
&lt;td>memory-bound, attenuated&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Decode with batch size 1 has a single-digit MFU because the hardware spends most of its time waiting for HBM to deliver weights. Raising the batch size (continuous batching) amortises the weight read across more simultaneous tokens and raises MFU.&lt;/p>
&lt;h3 id="the-roofline-model">The Roofline model&lt;/h3>
&lt;p>The roofline places each operation in the space (arithmetic intensity, throughput):&lt;/p>
&lt;div class="diagram" style="max-width:680px;margin:1rem auto;">
&lt;svg viewBox="0 0 680 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Roofline model for LLM inference: compute-bound prefill vs memory-bound decode">
&lt;style>.ax{stroke:currentColor;stroke-width:1.4;fill:none}.lbl{font:11px sans-serif;fill:currentColor}.ttl{font:700 13px sans-serif;fill:currentColor}.roof{stroke:currentColor;stroke-width:2;fill:none}.pt{fill:currentColor}&lt;/style>
&lt;text x="340" y="22" text-anchor="middle" class="ttl">Roofline: prefill vs decode on H100 SXM&lt;/text>
&lt;line x1="60" y1="270" x2="640" y2="270" class="ax"/>
&lt;line x1="60" y1="270" x2="60" y2="30" class="ax"/>
&lt;text x="350" y="298" text-anchor="middle" class="lbl">Arithmetic intensity (FLOP/byte)&lt;/text>
&lt;text x="14" y="155" text-anchor="middle" class="lbl" transform="rotate(-90 14 155)">Throughput (TFLOP/s)&lt;/text>
&lt;line x1="60" y1="270" x2="220" y2="60" class="roof"/>
&lt;line x1="220" y1="60" x2="640" y2="60" class="roof"/>
&lt;text x="230" y="52" class="lbl">~990 TFLOP/s (H100 BF16)&lt;/text>
&lt;text x="65" y="200" class="lbl" fill="currentColor">mem. bandwidth&lt;/text>
&lt;text x="65" y="213" class="lbl" fill="currentColor">~3.35 TB/s&lt;/text>
&lt;line x1="215" y1="50" x2="215" y2="270" stroke="currentColor" stroke-width="1" stroke-dasharray="4 3"/>
&lt;text x="200" y="284" class="lbl">~295&lt;/text>
&lt;text x="148" y="284" class="lbl">FLOP/byte ridge&lt;/text>
&lt;circle cx="130" cy="175" r="7" class="pt"/>
&lt;text x="138" y="171" class="lbl">decode bs=1&lt;/text>
&lt;text x="138" y="184" class="lbl">MFU ~5 %&lt;/text>
&lt;circle cx="185" cy="120" r="7" class="pt"/>
&lt;text x="193" y="116" class="lbl">decode bs=32&lt;/text>
&lt;text x="193" y="129" class="lbl">MFU ~20 %&lt;/text>
&lt;circle cx="300" cy="65" r="7" class="pt"/>
&lt;text x="308" y="61" class="lbl">prefill bs=128&lt;/text>
&lt;text x="308" y="74" class="lbl">MFU ~38 %&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Decode with batch size 1 falls in the memory-bound zone to the left of the ridge point. Raising the batch (continuous batching) moves the point to the right and up, closer to the roofline.&lt;/p>
&lt;hr>
&lt;h2 id="4--tco-sensitivity-to-occupancy">4 · TCO sensitivity to occupancy&lt;/h2>
&lt;p>Assumptions for the example: a generic node with &lt;strong>4×H100 SXM 80 GB&lt;/strong>, total node cost ~44 €/hour (5-year depreciation of ~220,000 €, energy ~4×700 W at ~0.12 €/kWh, plus rack/colocation infrastructure; see &lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> for the detail of the identity). Reference peak throughput with Llama 70B FP8 plus continuous batching: ~3,500 tok/s aggregate.&lt;/p>
$$
\text{CPM}(\rho) = \frac{44\ \text{€/h}}{3500 \times \rho \times 3600 / 10^6}
= \frac{44 \times 10^6}{3500 \times \rho \times 3600}
= \frac{3.49}{\rho}\ \text{€/1M tok}
$$
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Occupancy \( \rho \)&lt;/th>
&lt;th>Effective throughput (tok/s)&lt;/th>
&lt;th>On-prem CPM&lt;/th>
&lt;th>On-demand cloud CPM (~2.2 €/GPU-h)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>20 %&lt;/td>
&lt;td>700&lt;/td>
&lt;td>&lt;strong>~17.5 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>40 %&lt;/td>
&lt;td>1,400&lt;/td>
&lt;td>&lt;strong>~8.7 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>60 %&lt;/td>
&lt;td>2,100&lt;/td>
&lt;td>&lt;strong>~5.8 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>70 %&lt;/td>
&lt;td>2,450&lt;/td>
&lt;td>&lt;strong>~5.0 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>85 %&lt;/td>
&lt;td>2,975&lt;/td>
&lt;td>&lt;strong>~4.1 €&lt;/strong>&lt;/td>
&lt;td>~6.3 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>On-prem / cloud crossover point&lt;/strong>: with this example hardware, the on-prem cost advantage over a comparable European cloud starts at \( \rho \approx 55\text{-}60\,\% \). Below that, idle makes on-prem dearer than renting. The cloud column is flat because cloud bills per hour used, not per the throughput you extract from it.&lt;/p>
&lt;blockquote>
&lt;p>Example figures with generic hardware. The real numbers depend on the depreciation price, the local cost of energy, the model and the precision. The structure of the curve, CPM inversely proportional to \( \rho \), is universal.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="5--idle-metrics-where-occupancy-is-lost">5 · Idle metrics: where occupancy is lost&lt;/h2>
&lt;p>Before applying levers, you need to know which kind of idle dominates. Three categories:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Type of idle&lt;/th>
&lt;th>Symptom in metrics&lt;/th>
&lt;th>Usual cause&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Scheduling idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> low, &lt;code>SM_ACTIVE&lt;/code> &amp;lt; 0.05&lt;/td>
&lt;td>No requests in the queue; GPU waiting for work&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Batching idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>SM_ACTIVE&lt;/code> high, &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> low, &lt;code>DRAM_ACTIVE&lt;/code> low&lt;/td>
&lt;td>Batch too small; prefill stall between requests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Memory-bound idle&lt;/strong>&lt;/td>
&lt;td>&lt;code>DRAM_ACTIVE&lt;/code> high, &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> low&lt;/td>
&lt;td>Normal decode; HBM is the bottleneck; insufficient batch size&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Scheduling idle is the most expensive and the most directly attacked with bin-packing. Batching idle is attacked with continuous batching. Memory-bound idle in decode does not disappear entirely (it is the physics of the transformer), but it is attenuated with a larger batch size.&lt;/p>
&lt;p>The inference engine metrics that complement the diagnosis from the service side are documented in &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/">anatomia-metricas-dcgm-vllm-anomalias&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="6--levers-for-raising-occupancy">6 · Levers for raising occupancy&lt;/h2>
&lt;h3 id="table-of-lever--effect--when-it-applies">Table of lever × effect × when it applies&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>Effect on \( \rho \)&lt;/th>
&lt;th>When it applies&lt;/th>
&lt;th>Operational complexity&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Continuous batching&lt;/strong>&lt;/td>
&lt;td>High: removes idle between requests; raises decode MFU from ~5 % to ~20 %&lt;/td>
&lt;td>Always in inference; enabled by default in vLLM&lt;/td>
&lt;td>Low (engine parameter)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scheduler bin-packing&lt;/strong> (kube-scheduler / Kueue)&lt;/td>
&lt;td>High: concentrates loads on fewer nodes; frees whole nodes for shutdown or rebalancing&lt;/td>
&lt;td>Clusters with load that varies over the day&lt;/td>
&lt;td>Medium (scheduler policy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MIG&lt;/strong> (Multi-Instance GPU)&lt;/td>
&lt;td>Medium: fills GPUs with light loads that previously lived alone on a whole GPU&lt;/td>
&lt;td>Heterogeneous loads: embeddings + reranker + guardrail + large model&lt;/td>
&lt;td>High (hot repartitioning not available)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Time-slicing&lt;/strong>&lt;/td>
&lt;td>Low to medium: raises occupancy in dev/bursts; no isolation&lt;/td>
&lt;td>Consumer GPUs (RTX 5090/4090); low-risk multi-tenant dev&lt;/td>
&lt;td>Low&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MPS&lt;/strong>&lt;/td>
&lt;td>Medium: concurrent execution of multiple small processes; reduces context-switch overhead&lt;/td>
&lt;td>Many small concurrent kernels on a datacenter GPU; trust between loads&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Chunked prefill&lt;/strong>&lt;/td>
&lt;td>Medium: interleaves prefill and decode; reduces the TTFT spike and raises throughput&lt;/td>
&lt;td>Loads with a mix of short and long prompts&lt;/td>
&lt;td>Low (vLLM flag)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Quantisation&lt;/strong> (FP16→FP8→INT4)&lt;/td>
&lt;td>Indirect: raises peak throughput, which lowers CPM for the same \( \rho \)&lt;/td>
&lt;td>Models with quantised kernel support (native Hopper FP8)&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Autoscaling&lt;/strong> (KEDA)&lt;/td>
&lt;td>Keeps \( \rho \) high by scaling replicas according to the queue&lt;/td>
&lt;td>Variable and predictable load; cluster with spare capacity&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The post &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">compartir-gpu-time-slicing-mps-mig&lt;/a> details the three sharing mechanisms (time-slicing, MPS, MIG) with VRAM budgets.&lt;/p>
&lt;h3 id="bin-packing-with-kueue">Bin-packing with Kueue&lt;/h3>
&lt;p>Kueue (sigs.k8s.io/kueue) is the Kubernetes-native queue manager for GPU jobs. Its model of &lt;strong>cohorts&lt;/strong> and &lt;strong>nominal quotas&lt;/strong> allows active bin-packing: jobs accumulate in a queue and are launched only when there is a node that can take them whole, instead of fragmenting the load across partially occupied nodes.&lt;/p>
&lt;p>&lt;code>BestFit&lt;/code> packing in the &lt;code>ClusterQueue&lt;/code> is configured with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kueue.x-k8s.io/v1beta1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterQueue&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpu-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preemption&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">reclaimWithinCohort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Any&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">withinClusterQueue&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LowerPriority&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resourceGroups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">coveredResources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;nvidia.com/gpu&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">flavors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">h100-sxm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;nvidia.com/gpu&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nominalQuota&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The priority-based preemption policy ensures that production jobs displace experimentation ones when there is scarcity, keeping occupancy on the production nodes high.&lt;/p>
&lt;h3 id="mig-as-a-bin-packing-lever-inside-the-gpu">MIG as a bin-packing lever inside the GPU&lt;/h3>
&lt;p>MIG makes it possible to fill an H100 with light loads that would otherwise live alone on a whole GPU. A profile of &lt;code>3×2g.20gb + 1×1g.10gb&lt;/code> on an H100 can host simultaneously a 7B FP8 model (~14 GB of weights), two embedding services and an INT4 guardrail, all with hardware isolation. Without MIG, each of those loads would take a whole GPU with an individual \( \rho \) below 10 %.&lt;/p>
&lt;p>The profiles available on the H100 80 GB (SXM5) according to NVIDIA&amp;rsquo;s MIG User Guide:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>MIG profile&lt;/th>
&lt;th>Compute slices&lt;/th>
&lt;th>Memory&lt;/th>
&lt;th>Max. instances&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>1g.10gb&lt;/code>&lt;/td>
&lt;td>1/7 SMs&lt;/td>
&lt;td>10 GB&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>1g.20gb&lt;/code>&lt;/td>
&lt;td>1/7 SMs&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>2g.20gb&lt;/code>&lt;/td>
&lt;td>2/7 SMs&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>3g.40gb&lt;/code>&lt;/td>
&lt;td>3/7 SMs&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>4g.40gb&lt;/code>&lt;/td>
&lt;td>4/7 SMs&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>7g.80gb&lt;/code>&lt;/td>
&lt;td>7/7 SMs&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (whole GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>MIG is not available on consumer GPUs (RTX 5090, RTX 4090). Time-slicing is the only sharing option on that hardware.&lt;/p>
&lt;hr>
&lt;h2 id="7--continuous-batching-the-effect-on-mfu">7 · Continuous batching: the effect on MFU&lt;/h2>
&lt;p>Continuous batching (also called iteration-level scheduling or in-flight batching) is the mechanism with the greatest impact on occupancy in inference. The idea: instead of waiting for a complete batch to finish before launching the next, the engine evaluates the pipeline after each token and replaces completed sequences with new requests from the waiting queue.&lt;/p>
&lt;p>Quantified effect in vLLM:&lt;/p>
&lt;ul>
&lt;li>Without batching (bs=1, static): decode MFU ~3–8 %; GPU idle between requests.&lt;/li>
&lt;li>With continuous batching (dynamic bs 16–64): decode MFU ~15–30 %; the GPU almost never waits.&lt;/li>
&lt;li>On pure prefill loads with a large batch (bs=128+): prefill MFU 25–45 %; it approaches the compute roofline.&lt;/li>
&lt;/ul>
&lt;p>The vLLM parameter &lt;code>--max-num-seqs&lt;/code> controls the maximum number of sequences in the concurrent batch. Raising it increases occupancy until HBM becomes the bottleneck (watch for &lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code> &amp;gt; 90 % sustained).&lt;/p>
&lt;hr>
&lt;h2 id="8--hardware-and-scale-what-applies-to-what">8 · Hardware and scale: what applies to what&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hardware&lt;/th>
&lt;th>MIG&lt;/th>
&lt;th>MPS&lt;/th>
&lt;th>Time-slicing&lt;/th>
&lt;th>Continuous batching&lt;/th>
&lt;th>Note&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>H100 SXM / H200&lt;/td>
&lt;td>Yes (7 inst.)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>On-prem datacenter reference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A100 SXM/PCIe&lt;/td>
&lt;td>Yes (7 inst.)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Previous generation; HBM2e&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>L40S / L40&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Ada Lovelace; no MIG; good price/VRAM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 5090&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes (limited)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Consumer; no MIG; does not scale in production&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RTX 4090&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes (limited)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Consumer; 24 GB VRAM; no MIG&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The RTX 5090 and RTX 4090 illustrate the case of &lt;strong>hardware that does not scale&lt;/strong> for multi-tenant with isolation: they do not support MIG, VRAM is scarce for models above 7B with a wide KV-cache, and TDP (600 W / 450 W) is high relative to throughput. For production inference at scale, the 4×H100 SXM is the reference node of this series.&lt;/p>
&lt;hr>
&lt;h2 id="9--finops-diagnostic-flow-from-high-cpm-to-the-cause">9 · FinOps diagnostic flow: from high CPM to the cause&lt;/h2>
&lt;div class="diagram" style="max-width:720px;margin:1rem auto;">
&lt;svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="FinOps diagnostic flow from high CPM to the right lever">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dec{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 11px sans-serif;fill:currentColor}.ts{font:10px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.2;marker-end:url(#fm)}&lt;/style>
&lt;defs>&lt;marker id="fm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="270" y="10" width="180" height="42" rx="6"/>
&lt;text x="360" y="28" text-anchor="middle" class="tl">High CPM detected&lt;/text>
&lt;text x="360" y="44" text-anchor="middle" class="ts">OpenCost + LiteLLM&lt;/text>
&lt;path class="ar" d="M360,52 L360,80"/>
&lt;polygon class="dec" points="360,80 440,110 360,140 280,110"/>
&lt;text x="360" y="105" text-anchor="middle" class="tl">POWER_USAGE&lt;/text>
&lt;text x="360" y="120" text-anchor="middle" class="ts">low?&lt;/text>
&lt;path class="ar" d="M440,110 L530,110"/>
&lt;text x="537" y="114" class="ts">Yes → scheduling idle&lt;/text>
&lt;rect class="bx" x="530" y="128" width="170" height="34" rx="5"/>
&lt;text x="615" y="142" text-anchor="middle" class="ts">Bin-packing / Kueue&lt;/text>
&lt;text x="615" y="155" text-anchor="middle" class="ts">Autoscaling down&lt;/text>
&lt;path class="ar" d="M360,140 L360,168"/>
&lt;text x="270" y="114" class="ts">No&lt;/text>
&lt;polygon class="dec" points="360,168 450,198 360,228 270,198"/>
&lt;text x="360" y="193" text-anchor="middle" class="tl">PIPE_TENSOR_ACTIVE&lt;/text>
&lt;text x="360" y="208" text-anchor="middle" class="ts">low (&amp;lt;0.10)?&lt;/text>
&lt;path class="ar" d="M450,198 L530,198"/>
&lt;text x="537" y="202" class="ts">Yes → batching idle&lt;/text>
&lt;rect class="bx" x="530" y="216" width="170" height="34" rx="5"/>
&lt;text x="615" y="230" text-anchor="middle" class="ts">Continuous batching&lt;/text>
&lt;text x="615" y="243" text-anchor="middle" class="ts">Raise --max-num-seqs&lt;/text>
&lt;path class="ar" d="M360,228 L360,256"/>
&lt;text x="270" y="202" class="ts">No&lt;/text>
&lt;polygon class="dec" points="360,256 450,286 360,316 270,286"/>
&lt;text x="360" y="280" text-anchor="middle" class="tl">DRAM_ACTIVE high&lt;/text>
&lt;text x="360" y="296" text-anchor="middle" class="ts">and batch already large?&lt;/text>
&lt;path class="ar" d="M450,286 L530,286"/>
&lt;text x="537" y="290" class="ts">Yes → memory-bound&lt;/text>
&lt;rect class="bx" x="530" y="304" width="170" height="34" rx="5"/>
&lt;text x="615" y="318" text-anchor="middle" class="ts">FP8/INT4 quantisation&lt;/text>
&lt;text x="615" y="331" text-anchor="middle" class="ts">Smaller model / TP&lt;/text>
&lt;path class="ar" d="M360,316 L360,344"/>
&lt;text x="270" y="290" class="ts">No&lt;/text>
&lt;rect class="bx" x="270" y="344" width="180" height="30" rx="5"/>
&lt;text x="360" y="364" text-anchor="middle" class="ts">MIG / MPS / multi-model bin-pack&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="10--worked-example-impact-of-continuous-batching-on-cpm">10 · Worked example: impact of continuous batching on CPM&lt;/h2>
&lt;p>Starting point: Llama 70B FP8 on 4×H100 SXM, a load of 8 requests/s with 512 output tokens on average, without continuous batching (static bs = 8):&lt;/p>
&lt;ul>
&lt;li>Observed throughput: ~900 tok/s (decode dominant).&lt;/li>
&lt;li>\( \rho_{\text{efectiva}} \approx 900 / 3500 \approx 0.26 \).&lt;/li>
&lt;li>CPM: \( 44 / (900 \times 3600 / 10^6) \approx 13.6\ \text{€} \).&lt;/li>
&lt;/ul>
&lt;p>Same load, enabling continuous batching with &lt;code>--max-num-seqs 64&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Observed throughput: ~2,300 tok/s (the dynamic batch fills the gaps).&lt;/li>
&lt;li>\( \rho_{\text{efectiva}} \approx 2300 / 3500 \approx 0.66 \).&lt;/li>
&lt;li>CPM: \( 44 / (2300 \times 3600 / 10^6) \approx 5.3\ \text{€} \).&lt;/li>
&lt;/ul>
&lt;p>CPM reduction: from 13.6 € to 5.3 €, &lt;strong>–61 %&lt;/strong>, with no hardware change and without touching the model.&lt;/p>
&lt;p>The vLLM parameters relevant for raising effective throughput:&lt;/p>
&lt;pre tabindex="0">&lt;code>--max-num-seqs 64 # maximum concurrent batch
--max-num-batched-tokens 16384 # total tokens per iteration
--enable-chunked-prefill # interleaves prefill and decode
&lt;/code>&lt;/pre>&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-hour-to-cost-per-token-on-prem-vs-cloud/">coste-por-token-y-por-request&lt;/a> — the identity CPM = €/GPU-hour ÷ throughput with data from LiteLLM and OpenCost.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">observabilidad-gpu-dcgm-llm&lt;/a> — the twelve DCGM metrics and five vLLM ones that make up the cockpit.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/">anatomia-metricas-dcgm-vllm-anomalias&lt;/a> — a deep dive into the DCGM metrics with anomalies documented in production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">compartir-gpu-time-slicing-mps-mig&lt;/a> — time-slicing, MPS and MIG with worked VRAM budgets.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity-planning-inferencia-llm-on-premise&lt;/a> — sizing the cluster from peak throughput and target occupancy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/">opencost-cost-allocation-kubernetes&lt;/a> — how OpenCost calculates the \( C_{\text{GPU}} \) that goes into the CPM identity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/cloud-gpu-on-demand-reserved-spot-neoclouds-2026-prices/">Cloud GPU: a comparison of prices, commitment and sovereign neoclouds&lt;/a> — the alternative €/GPU-hour when on-premise occupancy does not justify the CAPEX: spot and reserved prices from European neoclouds.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/full-tco-on-premise-gpu-cluster-capex-gpu-hour-break-even/">TCO of an on-premise GPU cluster: depreciation, energy and infrastructure&lt;/a> — where the \( C_{\text{GPU}} \) of the identity comes from when the iron is your own: CAPEX, depreciation, energy and operations.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>NVIDIA — &lt;em>DCGM Field Identifiers reference (v3.1)&lt;/em>, lista completa de &lt;code>DCGM_FI_*&lt;/code> con field IDs y definiciones. &lt;a href="https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html">https://docs.nvidia.com/datacenter/dcgm/3.1/dcgm-api/dcgm-api-field-ids.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>GPU Profiling Metrics (Run:ai / DCGM)&lt;/em>, definiciones de &lt;code>DCGM_FI_PROF_SM_ACTIVE&lt;/code> (1002), &lt;code>SM_OCCUPANCY&lt;/code> (1003), &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> (1004). &lt;a href="https://run-ai-docs.nvidia.com/self-hosted/platform-management/monitor-performance/gpu-profiling-metrics">https://run-ai-docs.nvidia.com/self-hosted/platform-management/monitor-performance/gpu-profiling-metrics&lt;/a>&lt;/li>
&lt;li>Chowdhery et al. — &lt;em>PaLM: Scaling Language Modeling with Pathways&lt;/em> (MFU definition, sección 4). arXiv 2204.02311. &lt;a href="https://arxiv.org/abs/2204.02311">https://arxiv.org/abs/2204.02311&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Instance GPU (MIG) User Guide&lt;/em> (perfiles H100 SXM5, particionado, aislamiento). &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">https://docs.nvidia.com/datacenter/tesla/mig-user-guide/&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Supported MIG Profiles&lt;/em> (catálogo completo H100 80 GB). &lt;a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html">https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Time-Slicing GPUs in Kubernetes&lt;/em> (GPU Operator 24.9.0). &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/24.9.0/gpu-sharing.html">https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/24.9.0/gpu-sharing.html&lt;/a>&lt;/li>
&lt;li>NVIDIA — &lt;em>Multi-Process Service (MPS) Overview&lt;/em> (kernel concurrency, SM allocation). &lt;a href="https://docs.nvidia.com/deploy/mps/latest/index.html">https://docs.nvidia.com/deploy/mps/latest/index.html&lt;/a>&lt;/li>
&lt;li>Kueue — &lt;em>Overview&lt;/em> (sigs.k8s.io/kueue, bin-packing, cohorts, quotas). &lt;a href="https://kueue.sigs.k8s.io/docs/overview/">https://kueue.sigs.k8s.io/docs/overview/&lt;/a>&lt;/li>
&lt;li>vLLM Blog — &lt;em>vLLM v0.6.0: 2.7x Throughput Improvement and 5x Latency Reduction&lt;/em> (continuous batching, chunked prefill). &lt;a href="https://blog.vllm.ai/2024/09/05/perf-update.html">https://blog.vllm.ai/2024/09/05/perf-update.html&lt;/a>&lt;/li>
&lt;li>GMI Cloud — &lt;em>NVIDIA H100 GPU Pricing: 2026 Rent vs. Buy Cost Analysis&lt;/em>. &lt;a href="https://www.gmicloud.ai/en/blog/nvidia-h100-gpu-pricing-2026-rent-vs-buy-cost-analysis">https://www.gmicloud.ai/en/blog/nvidia-h100-gpu-pricing-2026-rent-vs-buy-cost-analysis&lt;/a>&lt;/li>
&lt;li>Saxena et al. — &lt;em>LLM Inference Unveiled: Survey and Roofline Model Insights&lt;/em>. arXiv 2402.16363. &lt;a href="https://arxiv.org/abs/2402.16363">https://arxiv.org/abs/2402.16363&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Measuring energy in production: Kepler, DCGM and the practical stack (accuracy and overhead)</title><link>https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/</link><pubDate>Sun, 14 Jun 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/measuring-energy-in-production-kepler-dcgm-stack/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The dollar sign is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-article-covers">What this article covers&lt;/h2>
&lt;p>Third article of the energy track (C3). C1 made the conceptual inventory of the tools;
here we get practical: how the measurement stack is deployed on Kubernetes, where each
piece takes its figures from, how accurate it is against a power meter, how much
overhead it adds, and how energy is attributed per pod and per MIG instance. It is the
equivalent of B2/B3 for energy: to stop talking about what each tool measures and start
building the bench that measures it. With commands; no recommendations, only the mechanics.&lt;/p>
&lt;hr>
&lt;h2 id="the-measurement-stack-on-kubernetes">The measurement stack on Kubernetes&lt;/h2>
&lt;p>To measure energy per workload on a GPU cluster, the practical stack has four pieces, and the
good news is that three of them you already have if you do observability:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 230" role="img" aria-label="Energy measurement stack on Kubernetes: DCGM exporter and Kepler export to Prometheus, which Grafana visualises, with the power meter as calibration" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#em)}&lt;/style>
&lt;defs>&lt;marker id="em" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="170" height="44" rx="6"/>&lt;text x="32" y="60" class="tl">DCGM exporter&lt;/text>&lt;text x="32" y="76" class="ts">GPU power&lt;/text>
&lt;rect class="bx" x="20" y="100" width="170" height="44" rx="6"/>&lt;text x="32" y="120" class="tl">Kepler (eBPF)&lt;/text>&lt;text x="32" y="136" class="ts">energy per pod/node&lt;/text>
&lt;path class="ar" d="M190,62 L250,90"/>
&lt;path class="ar" d="M190,122 L250,108"/>
&lt;rect class="bx" x="250" y="78" width="160" height="44" rx="6"/>&lt;text x="262" y="98" class="tl">Prometheus&lt;/text>&lt;text x="262" y="114" class="ts">time series&lt;/text>
&lt;path class="ar" d="M410,100 L470,100"/>
&lt;rect class="bx" x="470" y="78" width="160" height="44" rx="6"/>&lt;text x="482" y="98" class="tl">Grafana&lt;/text>&lt;text x="482" y="114" class="ts">panels · alerts&lt;/text>
&lt;rect class="dsh" x="650" y="78" width="110" height="44" rx="6"/>&lt;text x="662" y="98" class="tl">Power meter&lt;/text>&lt;text x="662" y="114" class="ts">calibration&lt;/text>
&lt;text x="20" y="180" class="ts">DCGM, Prometheus and Grafana are already there if you do GPU observability; Kepler adds per-pod attribution.&lt;/text>
&lt;text x="20" y="202" class="ts">The physical meter (point in time) calibrates the estimates: not part of the continuous stack, but the ground truth.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The new piece compared with a normal observability stack is Kepler, which adds &lt;strong>energy
attribution per pod&lt;/strong>. DCGM already gives GPU power; Prometheus stores it; Grafana draws it. It
is reused infrastructure, not a new system, in line with &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability via DCGM&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="dcgm-exporter-the-base-of-gpu-power">DCGM exporter: the base of GPU power&lt;/h2>
&lt;p>GPU power, the dominant source in LLM inference, comes from NVIDIA&amp;rsquo;s dcgm-exporter
(part of the GPU Operator), which exposes DCGM metrics to Prometheus. The key metric is
&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> (instantaneous power in W), alongside utilisation
(&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>) and memory. With that, energy is the integral of power over time
across the workload&amp;rsquo;s window. DCGM gives power at card level; to split it per pod or per
MIG instance you need a layer on top, which is Kepler.&lt;/p>
&lt;hr>
&lt;h2 id="kepler-in-depth-ebpf-and-the-sources">Kepler in depth: eBPF and the sources&lt;/h2>
&lt;p>Kepler (Kubernetes-based Efficient Power Level Exporter) is a Prometheus exporter that
measures energy at container, pod and node level, using eBPF to keep overhead low and
scientific methods to improve accuracy (&lt;a href="https://github.com/sustainable-computing-io/kepler">Kepler · GitHub&lt;/a>).
Its strength is that it combines several power sources:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source&lt;/th>
&lt;th>What it covers&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>RAPL&lt;/strong>&lt;/td>
&lt;td>CPU and DRAM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NVML&lt;/strong>&lt;/td>
&lt;td>NVIDIA GPU power&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ACPI&lt;/strong>&lt;/td>
&lt;td>platform power management&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Redfish / IPMI&lt;/strong>&lt;/td>
&lt;td>platform power (BMC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Regression models&lt;/strong>&lt;/td>
&lt;td>estimation when there is no real metric&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For the GPU, the &lt;code>dcgmEndpoint&lt;/code> parameter points at the dcgm-exporter metrics URL, which
enables power attribution per MIG instance (&lt;a href="https://github.com/sustainable-computing-io/kepler/blob/main/docs/user/configuration.md">Kepler · configuration&lt;/a>).
That is: DCGM gives the card&amp;rsquo;s power, Kepler splits it between the pods (and MIG partitions)
that use it, via eBPF and models. The result is per-pod energy metrics in Prometheus, ready
for Grafana, usable for scheduling or scaling, consumption reporting and visualisation
(&lt;a href="https://github.com/sustainable-computing-io/kepler">Kepler · GitHub&lt;/a>).&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 210" role="img" aria-label="Kepler combines RAPL, NVML/DCGM, ACPI and Redfish, and with eBPF splits the energy per pod and per MIG instance" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#km)}&lt;/style>
&lt;defs>&lt;marker id="km" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="36" width="150" height="30" rx="5"/>&lt;text x="30" y="56" class="ts">RAPL (CPU/DRAM)&lt;/text>
&lt;rect class="bx" x="20" y="76" width="150" height="30" rx="5"/>&lt;text x="30" y="96" class="ts">NVML/DCGM (GPU)&lt;/text>
&lt;rect class="bx" x="20" y="116" width="150" height="30" rx="5"/>&lt;text x="30" y="136" class="ts">ACPI · Redfish/IPMI&lt;/text>
&lt;path class="ar" d="M170,51 L230,80"/>
&lt;path class="ar" d="M170,91 L230,91"/>
&lt;path class="ar" d="M170,131 L230,102"/>
&lt;rect class="bx" x="230" y="70" width="170" height="44" rx="6"/>&lt;text x="242" y="90" class="tl">Kepler (eBPF + model)&lt;/text>&lt;text x="242" y="106" class="ts">splits the power&lt;/text>
&lt;path class="ar" d="M400,92 L460,92"/>
&lt;rect class="bx" x="460" y="70" width="180" height="44" rx="6"/>&lt;text x="472" y="90" class="tl">Energy per pod / MIG&lt;/text>&lt;text x="472" y="106" class="ts">→ Prometheus&lt;/text>
&lt;text x="20" y="180" class="ts">DCGM gives the card's power; Kepler attributes it to each pod and MIG partition, as OpenCost attributes cost.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="deployment-a-daemonset-on-top-of-prometheus">Deployment: a DaemonSet on top of Prometheus&lt;/h2>
&lt;p>The deployment is a DaemonSet (one Kepler per node), pointing at the existing Prometheus
and at the dcgm-exporter:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Helm, pointing at the dcgm-exporter for GPU attribution&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install kepler kepler/kepler &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kepler --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">dcgmEndpoint&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://dcgm-exporter:9400/metrics&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Prerequisites: a kernel with eBPF, the dcgm-exporter running (from the GPU Operator) and
Prometheus scraping. Once deployed, Kepler exposes metrics such as
&lt;code>kepler_container_joules_total&lt;/code> per pod, queried with PromQL and drawn in Grafana. Energy per
workload stops being a manual estimate and becomes a continuous time series, just like cost
with OpenCost.&lt;/p>
&lt;hr>
&lt;h2 id="promql-queries-energy-per-pod">PromQL queries: energy per pod&lt;/h2>
&lt;p>With Kepler exporting, these are the queries that build the panels. The exact names vary by
version; check your &lt;code>/metrics&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Cumulative energy per pod (joules)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Mean power per namespace over the last hour (W)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">1h&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># GPU power per node (from DCGM, W)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">node&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_POWER_USAGE&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Energy per token: power ÷ throughput (with a gateway/engine metric)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm_generation_tokens_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The last one closes the circle with the cost track: it crosses Kepler&amp;rsquo;s energy with the
engine/gateway tokens to give J/token in real time, per workload. With that in Grafana you
have energy per pod, per namespace and per token, continuously, the energy equivalent of the
OpenCost cost report.&lt;/p>
&lt;hr>
&lt;h2 id="a-calibration-procedure">A calibration procedure&lt;/h2>
&lt;p>Since Kepler estimates, it is worth calibrating once per node type. A simple procedure:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Power meter at the node input&lt;/strong> (or a PDU/BMC reading), measuring the node&amp;rsquo;s real power.&lt;/li>
&lt;li>&lt;strong>Controlled load&lt;/strong>: a stable sweep at a known power (the engine at 80 % GPU, say).&lt;/li>
&lt;li>&lt;strong>Compare&lt;/strong>: the node&amp;rsquo;s sum of &lt;code>kepler_container_joules_total&lt;/code> plus idle against the meter
reading over the same window.&lt;/li>
&lt;li>&lt;strong>Adjust&lt;/strong> Kepler&amp;rsquo;s factor or model if the deviation is noticeable, and document the
residual error.&lt;/li>
&lt;li>&lt;strong>Repeat&lt;/strong> for each node type (calibration is not transferable between different hardware).&lt;/li>
&lt;/ol>
&lt;p>With that residual error documented, Kepler&amp;rsquo;s continuous figure has a known error bar,
which is exactly what a CSRD auditor or a technical committee will ask for. Measuring without
calibrating gives a number with no credibility; calibrating once turns the continuous estimate
into a defensible figure.&lt;/p>
&lt;hr>
&lt;h2 id="accuracy-estimation-vs-power-meter">Accuracy: estimation vs power meter&lt;/h2>
&lt;p>The data honesty of the energy track: Kepler estimates (eBPF + models + counters), it does
not measure with a power meter. Accuracy depends on the source: with NVML/DCGM and RAPL
available, the figure is fairly faithful (it reads hardware counters); when it falls back on
regression models (with no real metric), it is an estimate with more error. Good practice:
calibrate against a physical power meter at least once per node type, adjust, and use
Kepler as a continuous proxy. The meter is the point-in-time ground truth; Kepler, the
continuous series calibrated against it. For a CSRD report that an auditor may challenge, that
initial calibration is what gives the continuous figure credibility.&lt;/p>
&lt;hr>
&lt;h2 id="overhead-the-cost-of-measuring">Overhead: the cost of measuring&lt;/h2>
&lt;p>Measuring energy has its own compute cost, worth budgeting for. In a Kubernetes environment,
the overhead of the monitoring system is split between the Prometheus server and the targets:
the Prometheus server consumes ~150 % of CPU (1.5 cores) and ~15 GB of RAM, while the
targets (node exporter and dcgm-exporter) together use ~35 % of CPU and ~300 MB of RAM
(&lt;a href="https://arxiv.org/pdf/2504.10702">arXiv · Container-level Energy Observability&lt;/a>). Kepler uses
eBPF precisely to keep its own overhead low. The reading: the bulk of the cost of measuring is
in Prometheus (which you probably already have for observability), not in the energy
exporters. Adding Kepler to an existing observability stack is marginal; standing up
Prometheus just for this would be expensive. That is why measuring energy is nearly free if
you already observe, and a project if you do not.&lt;/p>
&lt;hr>
&lt;h2 id="zeus-for-the-bench-and-for-optimisation-not-for-continuous-production">Zeus: for the bench and for optimisation, not for continuous production&lt;/h2>
&lt;p>Zeus (ml.energy) plays a different role: it is not a continuous production exporter, it is
the bench tool for measuring and optimising the energy of one specific workload (a
benchmark, a training run), with power capping and frequency selection. It fits into the
benchmarking flow (alongside GuideLLM/AIPerf): during a sweep, Zeus measures the energy per
configuration and lets you test the effect of power capping on performance. The practical
division: Kepler/DCGM in production (continuous per-pod attribution), Zeus on the bench
(fine measurement + optimisation of one workload), CodeCarbon if you want carbon context
integrated into an ML pipeline.&lt;/p>
&lt;hr>
&lt;h2 id="codecarbon-and-scaphandre-in-practice">CodeCarbon and Scaphandre in practice&lt;/h2>
&lt;p>The other two pieces, with their specific role:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CodeCarbon&lt;/strong> integrates inside the code of an ML pipeline (a decorator or a context
manager wrapping the training or the inference), and returns the energy and the CO₂eq of
that block, using &lt;code>nvidia-smi&lt;/code> for the GPU and estimating CPU/RAM. It fits when you want the
carbon figure per experiment or per ML job, integrated into the data flow, not at
infrastructure level. It is the closest to the power meter among the Python tools.&lt;/li>
&lt;li>&lt;strong>Scaphandre&lt;/strong> is a metrology agent in Rust that reads RAPL and gives process/VM power
with raw hardware accuracy. For CPU it is very accurate; for GPU it has to be combined with
NVML. It fits when you need the most faithful CPU metrology, or a lightweight agent
independent of Kubernetes.&lt;/li>
&lt;/ul>
&lt;p>The division with Kepler: Kepler attributes energy per pod in the cluster (the
infrastructure view); CodeCarbon gives carbon per ML job (the pipeline view);
Scaphandre gives raw CPU metrology. They do not compete; they cover different layers, and
a mature platform uses whichever matches the question.&lt;/p>
&lt;hr>
&lt;h2 id="choosing-the-stack-by-objective">Choosing the stack by objective&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Objective&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Continuous per-pod attribution (production)&lt;/td>
&lt;td>&lt;strong>Kepler + DCGM&lt;/strong>&lt;/td>
&lt;td>eBPF, per pod/MIG, into Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fine measurement + optimisation (bench)&lt;/td>
&lt;td>&lt;strong>Zeus&lt;/strong>&lt;/td>
&lt;td>measures and optimises (power capping)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Carbon in an ML pipeline&lt;/td>
&lt;td>&lt;strong>CodeCarbon&lt;/strong>&lt;/td>
&lt;td>integrated CO₂eq, close to the power meter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Raw CPU accuracy (RAPL)&lt;/td>
&lt;td>&lt;strong>Scaphandre&lt;/strong>&lt;/td>
&lt;td>hardware metrology&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Calibration / ground truth&lt;/td>
&lt;td>&lt;strong>power meter&lt;/strong>&lt;/td>
&lt;td>the standard everything is calibrated against&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>It is not &amp;ldquo;one tool&amp;rdquo;, it is a stack by objective: continuous production (Kepler/DCGM),
bench (Zeus), reporting (CodeCarbon), calibration (power meter). Mixing them according to the
phase is what gives a complete and credible energy measurement.&lt;/p>
&lt;hr>
&lt;h2 id="from-figure-to-action">From figure to action&lt;/h2>
&lt;p>Measuring is the start; the value is in acting. With energy per pod in Prometheus:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Reporting (CSRD)&lt;/strong>: the carbon footprint per workload, calibrated, ready for regulatory
reporting.&lt;/li>
&lt;li>&lt;strong>Energy/carbon-aware scheduling&lt;/strong>: move deferrable load to hours or nodes of lower
intensity (connects with the hourly carbon of the Spanish grid).&lt;/li>
&lt;li>&lt;strong>Power capping&lt;/strong>: with Zeus, trim peak power with little performance loss, lowering the W in
the numerator of J/token.&lt;/li>
&lt;li>&lt;strong>Optimisation&lt;/strong>: identify pods with poor energy efficiency (a lot of energy per token) and
attack them with quantisation or batching.&lt;/li>
&lt;/ul>
&lt;p>The measurement stack is not a panel to look at: it is the input to scheduling, optimisation
and reporting decisions. Just as OpenCost closes the cost loop, Kepler+DCGM closes the energy
one.&lt;/p>
&lt;hr>
&lt;h2 id="from-energy-to-carbon-the-final-layer">From energy to carbon: the final layer&lt;/h2>
&lt;p>The energy figure (kWh) becomes carbon by multiplying by the grid intensity, and here the
practical stack connects with the hourly source. In Spain, the intensity is published by &lt;strong>Red
Eléctrica (esios) and aggregated by ElectricityMaps&lt;/strong>, hour by hour. The pattern:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Carbon per workload: energy (kWh) × grid intensity (gCO2/kWh)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">increase&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">kepler_container_joules_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">1h&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">3.6&lt;/span>&lt;span class="nv">e6&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">on&lt;/span>&lt;span class="o">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">group_left&lt;/span>&lt;span class="o">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">grid_carbon_intensity_gco2_kwh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>where &lt;code>grid_carbon_intensity_gco2_kwh&lt;/code> is a metric you feed from esios/ElectricityMaps (an
exporter or a periodic push). With that, the Grafana panel shows &lt;strong>gCO₂ per namespace and per
token, hour by hour&lt;/strong>, not an annual average. And since Spanish intensity varies a lot (from
~80 in solar hours to ~250 on gas-heavy nights), that hourly resolution is what enables
carbon-aware scheduling: moving the deferrable work to the clean hours lowers the measured
carbon. The measurement stack does not only report carbon; with the hourly figure, it &lt;strong>allows
it to be reduced&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="energy-efficiency-kpis-to-watch">Energy efficiency KPIs to watch&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>KPI&lt;/th>
&lt;th>What it indicates&lt;/th>
&lt;th>Where from&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>J/token per model&lt;/strong>&lt;/td>
&lt;td>energy efficiency&lt;/td>
&lt;td>Kepler ÷ engine tokens&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Mean power per pod&lt;/strong>&lt;/td>
&lt;td>consumption per workload&lt;/td>
&lt;td>&lt;code>rate(kepler_container_joules_total)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>gCO₂/token (hourly)&lt;/strong>&lt;/td>
&lt;td>real footprint&lt;/td>
&lt;td>energy × grid intensity&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Idle energy&lt;/strong>&lt;/td>
&lt;td>waste&lt;/td>
&lt;td>power of pods with no traffic&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Effective PUE&lt;/strong>&lt;/td>
&lt;td>DC overhead&lt;/td>
&lt;td>total energy ÷ compute energy&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>These five turn the energy panel into a decision tool: which model is more efficient, which pod
wastes, when it pays to schedule. Without them the stack is a pretty panel; with them, it is
the input to optimisation and reporting.&lt;/p>
&lt;hr>
&lt;h2 id="example-energy-attribution-on-a-multi-tenant-cluster">Example: energy attribution on a multi-tenant cluster&lt;/h2>
&lt;p>With Kepler splitting per pod, an energy report per team on the example node (8×H100,
5.6 kW of boards, ~7.84 kW with PUE), over one month:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Team&lt;/th>
&lt;th>Energy/month (kWh)&lt;/th>
&lt;th>gCO₂ (ES grid ~160)&lt;/th>
&lt;th>Mean J/token&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A · chat-prod&lt;/td>
&lt;td>~3,100&lt;/td>
&lt;td>~496 kg&lt;/td>
&lt;td>~2.2 J/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B · batch&lt;/td>
&lt;td>~1,400&lt;/td>
&lt;td>~224 kg&lt;/td>
&lt;td>~2.8 J/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C · experimentation&lt;/td>
&lt;td>~900&lt;/td>
&lt;td>~144 kg&lt;/td>
&lt;td>~6.5 J/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>idle&lt;/td>
&lt;td>~250&lt;/td>
&lt;td>~40 kg&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>What it reveals: A is efficient (FP8, high utilisation → 2.2 J/token); C is inefficient
(6.5 J/token, underused GPU), just as it dragged the idle cost in the FinOps track. The carbon
column takes the figure into the per-team CSRD report. And the idle row (~250 kWh, ~40 kg
CO₂) is pure wasted energy. Without Kepler, that split does not exist: you would only see the
node&amp;rsquo;s total consumption, with no idea whose each watt is. With it, energy is attributable and
actionable, exactly like cost with OpenCost. In fact, the two panels together (OpenCost cost +
Kepler energy) give the complete picture of efficiency per team.&lt;/p>
&lt;hr>
&lt;h2 id="energy-and-carbon-aware-scheduling">Energy and carbon-aware scheduling&lt;/h2>
&lt;p>The highest-return action the stack enables: scheduling by energy and carbon. With energy
per pod and hourly grid intensity in Prometheus, two policies:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>By carbon&lt;/strong>: move the deferrable load (training, ingestion, overnight batch) to the
hours of lowest grid intensity. In Spain, the hours of peak solar (~80 gCO₂/kWh) against
gas-heavy nights (~250). The same workload emits ~3× less when scheduled at the right hour.&lt;/li>
&lt;li>&lt;strong>By efficiency&lt;/strong>: consolidate workloads to eliminate idle (a powered GPU with no work is
wasted energy), and direct each workload to the hardware most efficient for it.&lt;/li>
&lt;/ul>
&lt;p>Both use the same data (energy per pod + hourly intensity) that the stack already exposes, and
they connect with cluster scheduling (Kueue/Volcano). It is the difference between
measuring carbon (and reporting it) and reducing it (by scheduling). The C3 stack
enables both.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-checklist">Deployment checklist&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Step&lt;/th>
&lt;th>Action&lt;/th>
&lt;th>Verification&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>dcgm-exporter (GPU Operator)&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> in Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>Kepler DaemonSet, kernel with eBPF&lt;/td>
&lt;td>&lt;code>kepler_container_joules_total&lt;/code> per pod&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;code>dcgmEndpoint&lt;/code> pointing at the exporter&lt;/td>
&lt;td>GPU/MIG attribution works&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Grid intensity metric (esios)&lt;/td>
&lt;td>&lt;code>grid_carbon_intensity&lt;/code> in Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Calibration vs power meter (once per node type)&lt;/td>
&lt;td>error bar documented&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>Grafana panels (J/token, carbon, idle)&lt;/td>
&lt;td>KPIs visible&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Step 5 (calibration) is the one most often skipped and the one that gives the figure
credibility; step 3 (&lt;code>dcgmEndpoint&lt;/code>) is the one most often forgotten, and without it the GPU is
not attributed.&lt;/p>
&lt;hr>
&lt;h2 id="energy-by-phase-prefill-vs-decode">Energy by phase: prefill vs decode&lt;/h2>
&lt;p>A level of detail the stack allows and that guides optimisation: power is not constant
during a request. Prefill (processing the prompt) is compute-bound and draws a lot of power
in a burst; decode (generating tokens) is memory-bound and draws a lower peak but for
longer. With fine DCGM sampling that bimodal profile is visible, and it has consequences:&lt;/p>
&lt;ul>
&lt;li>A workload with long prompts (a lot of prefill) has a different power profile from one with
long generation (a lot of decode), even if they produce the same tokens.&lt;/li>
&lt;li>&lt;strong>Power capping&lt;/strong> affects each phase differently: trimming peak power mostly touches the
prefill.&lt;/li>
&lt;/ul>
&lt;p>For most cases the mean J/token is enough; to optimise energy efficiency thoroughly,
separating prefill and decode consumption (aligning the DCGM window with each phase) reveals
where the spend is. It is the energy equivalent of separating TTFT and TPOT in benchmarking,
the same principle, measuring the two phases separately because they behave differently.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-the-llm-resolution-gap">State of the art: the LLM resolution gap&lt;/h2>
&lt;p>An honest note on the limits of current tooling. The general-purpose tools (Kepler,
CodeCarbon, Scaphandre) measure well at machine, process and pod level, but they &lt;strong>lack
LLM-specific resolution&lt;/strong>: out of the box they do not distinguish prefill from decode energy,
nor energy per token by phase, nor the energy cost of a reasoning block against a normal
response. It is an open gap in the field: per-pod attribution exists, but attribution &lt;strong>per
token and per inference phase&lt;/strong> has to be built by crossing power (DCGM/Kepler) with engine
metrics (vLLM exposes tokens per phase). The 2026 state of the art is a mature stack for
infrastructure and an emerging one for LLM-specific resolution, exactly the crossing
that the reproducible harness (S4) has to build by hand. Acknowledging it avoids promising a
per-token accuracy that the general-purpose tools do not deliver on their own.&lt;/p>
&lt;hr>
&lt;h2 id="limits-and-traps-data-driven">Limits and traps (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Uncalibrated estimation.&lt;/strong> Kepler estimates; without calibrating against a power meter at
least once, the figure can carry a noticeable error, especially when it falls back on
regression models.&lt;/li>
&lt;li>&lt;strong>Forgetting the &lt;code>dcgmEndpoint&lt;/code>.&lt;/strong> Without pointing Kepler at the dcgm-exporter, GPU (and
MIG) attribution does not work; it is the key parameter for GPU.&lt;/li>
&lt;li>&lt;strong>Accounting for overhead badly.&lt;/strong> The bulk of the cost of measuring is in Prometheus; if
you stand it up just for energy, budget for it (1.5 cores, 15 GB).&lt;/li>
&lt;li>&lt;strong>Confusing bench and production.&lt;/strong> Zeus measures one workload finely; Kepler attributes
continuously in production. Using one for the other gives the wrong figure.&lt;/li>
&lt;li>&lt;strong>Misaligned windows.&lt;/strong> For energy per token, the power window and the token window must
coincide; if they do not, the J/token does not correspond.&lt;/li>
&lt;/ol>
&lt;p>With the measurement stack in place, the energy track has its instrumentation; the following
articles (C6 carbon, C8 TCO and regulation) build on this data. The foundation is this: a
continuous per-pod measurement, calibrated, on top of the observability infrastructure you
already have.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>Measuring the energy of an AI cluster in production does not require a new system: it requires
adding Kepler, per-pod attribution via eBPF, to the DCGM, Prometheus and Grafana stack you
already have for observability. That is the central idea of C3: the cost of measuring is almost
entirely in the Prometheus you already pay for, and Kepler turns it from &amp;ldquo;card power&amp;rdquo; into
&amp;ldquo;energy per pod, per team and per token&amp;rdquo;. With two cautions that separate a credible figure
from a decorative panel: calibrate once against a power meter (to have an error bar that
survives an audit) and point the &lt;code>dcgmEndpoint&lt;/code> (without which the GPU is not attributed).
Done that way, energy stops being the soft axis of sustainability and becomes an actionable
time series: the J/token per model that exposes inefficiency, the hourly carbon that enables
clean scheduling, the idle that gets recovered, and the CSRD report that holds up. For the
sovereign platform, the Kepler+DCGM stack closes the energy loop just as OpenCost closes the
cost one, and together, with GuideLLM&amp;rsquo;s goodput, they give the three faces of the dashboard
measured on the same iron, in euros, joules and grams of CO₂.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlperf-power-energy-efficiency-benchmark/">MLPerf Power&lt;/a> — the standard measurement at the wall (PTDaemon), complementary to software instrumentation (DCGM/RAPL/Kepler).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measuring-gpu-power-nvml-dcgm-sampling-errors/">Measuring the power of a GPU: NVML, DCGM and sampling errors&lt;/a> — the sensor metrology that feeds all these tools: averaging window, cumulative counter and what falls outside the counter.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-energy-leaderboards-wh-per-token/">LLM energy efficiency leaderboards&lt;/a> — where the J/token these tools produce end up: the public rankings and how to interpret them without being misled by the setup.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/watt-to-carbon-pue-grid-intensity-gco2-per-token/">From watt to carbon: PUE, grid intensity and the real cost of a token&lt;/a> — the conversion step from measured watts to gCO₂eq and to euros, applying datacenter PUE and the country&amp;rsquo;s electricity grid intensity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/levers-fewer-watts-per-token-llm-inference/">Energy efficiency levers in LLM inference&lt;/a> — the optimisations that reduce the consumption these tools measure: quantization, batching, engine and power schedulers.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Kepler · GitHub (CNCF, eBPF, por pod/nodo) — &lt;a href="https://github.com/sustainable-computing-io/kepler">https://github.com/sustainable-computing-io/kepler&lt;/a>&lt;/li>
&lt;li>Kepler · configuración (fuentes, dcgmEndpoint para MIG) — &lt;a href="https://github.com/sustainable-computing-io/kepler/blob/main/docs/user/configuration.md">https://github.com/sustainable-computing-io/kepler/blob/main/docs/user/configuration.md&lt;/a>&lt;/li>
&lt;li>CNCF · explorando Kepler (consumo de aplicaciones cloud) — &lt;a href="https://www.cncf.io/blog/2023/10/11/exploring-keplers-potentials-unveiling-cloud-application-power-consumption/">https://www.cncf.io/blog/2023/10/11/exploring-keplers-potentials-unveiling-cloud-application-power-consumption/&lt;/a>&lt;/li>
&lt;li>Red Hat · Kepler: monitorización eficiente de energía para Kubernetes — &lt;a href="https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/">https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/&lt;/a>&lt;/li>
&lt;li>arXiv · Container-level Energy Observability in Kubernetes (overhead) — &lt;a href="https://arxiv.org/pdf/2504.10702">https://arxiv.org/pdf/2504.10702&lt;/a>&lt;/li>
&lt;li>NVIDIA · dcgm-exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>Zeus Project (ml.energy) — &lt;a href="https://ml.energy/zeus/">https://ml.energy/zeus/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>OpenCost in depth: how GPU cost is allocated in Kubernetes</title><link>https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/</link><pubDate>Sun, 14 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. European references (OVH is among
the supported providers). The dollar sign is not used (on this site it is a formula
delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-article-covers">What this article covers&lt;/h2>
&lt;p>Second article in the &lt;strong>FinOps&lt;/strong> track (A2), and the first tool deep dive. The
&lt;a href="https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/">FinOps introduction&lt;/a> established that
OpenCost is the CNCF standard for cost allocation; here the box gets opened: how it allocates
cost internally, where it gets its data from, how on-prem prices are configured in euros, what its
API exposes, and where the traps are. Understanding the mechanics matters because &lt;strong>it defines what
any tool on top (Kubecost included) can and cannot do&lt;/strong>, and because a badly set base price
invalidates the whole split. No recommendations; only the mechanics and the methodology.&lt;/p>
&lt;hr>
&lt;h2 id="what-opencost-is">What OpenCost is&lt;/h2>
&lt;p>OpenCost is a vendor-neutral, Apache 2.0 project, originally built by Kubecost and donated to
the CNCF (incubating project). It is a cost allocation layer for Kubernetes: it reads
resource usage from the cluster, joins it with a price, and splits the cost across Kubernetes
dimensions down to the container (&lt;a href="https://github.com/opencost/opencost">OpenCost · GitHub&lt;/a>).
It supports dynamic pricing via the billing APIs of AWS, Azure and GCP, &lt;strong>and custom prices for
on-prem clusters&lt;/strong> (&lt;a href="https://opencost.io/docs/configuration/">OpenCost · configuration&lt;/a>).&lt;/p>
&lt;p>What it is &lt;strong>not&lt;/strong>: it is not an optimiser, nor a governance system, nor a product unit economics
tool. It is the piece that answers &amp;ldquo;how much does each namespace/pod/team cost?&amp;rdquo;, and leaves the
rest to the layers above. That is why it is the foundation of the FinOps track: without correct
allocation, there is no cost per token and no TCO model worth anything.&lt;/p>
&lt;hr>
&lt;h2 id="the-cost-model-node-level-allocation">The cost model: node-level allocation&lt;/h2>
&lt;p>The key to OpenCost is that the model works at node level. It starts from the node&amp;rsquo;s &lt;strong>resource
capacity&lt;/strong> (CPU, RAM, GPU, storage) and its &lt;strong>total price&lt;/strong>, and splits that price across the
resources. When the provider does not give explicit per-resource prices, OpenCost uses the &lt;strong>ratio
of a set of base prices&lt;/strong> (marginal rates, customisable) and &lt;strong>normalises them so that the sum of
the components equals the node&amp;rsquo;s total price&lt;/strong> (&lt;a href="https://opencost.io/docs/configuration/on-prem/">OpenCost · on-prem&lt;/a>).&lt;/p>
&lt;p>This is what makes it work on-prem: you declare the node cost (depreciated capex + opex), and
OpenCost splits it without needing a cloud invoice. The normalisation guarantees that no cost is
&amp;ldquo;lost&amp;rdquo; or &amp;ldquo;invented&amp;rdquo;: what you pay for the node is exactly what gets split across its resources.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="OpenCost cost model: total node price split and normalised across CPU, GPU, RAM and disk, and then allocated by usage to each pod" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#om)}&lt;/style>
&lt;defs>&lt;marker id="om" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="170" height="60" rx="6"/>
&lt;text x="32" y="63" class="tl">Node price&lt;/text>
&lt;text x="32" y="81" class="ts">depreciated capex&lt;/text>
&lt;text x="32" y="95" class="ts">+ opex (€/hour)&lt;/text>
&lt;path class="ar" d="M190,70 L235,70"/>
&lt;rect class="bx" x="235" y="40" width="210" height="60" rx="6"/>
&lt;text x="247" y="63" class="tl">Split + normalisation&lt;/text>
&lt;text x="247" y="81" class="ts">CPU / GPU / RAM / disk&lt;/text>
&lt;text x="247" y="95" class="ts">sum = total price&lt;/text>
&lt;path class="ar" d="M445,70 L490,70"/>
&lt;rect class="bx" x="490" y="40" width="270" height="60" rx="6"/>
&lt;text x="502" y="63" class="tl">Allocation by USAGE&lt;/text>
&lt;text x="502" y="81" class="ts">GPU half used = half the cost&lt;/text>
&lt;text x="502" y="95" class="ts">per pod / namespace / team&lt;/text>
&lt;rect class="dsh" x="20" y="130" width="740" height="96" rx="6"/>
&lt;text x="34" y="152" class="tl">What OpenCost guarantees: no cost is lost and none is invented.&lt;/text>
&lt;text x="34" y="172" class="ts">· On-prem: you declare the node cost; OpenCost splits it (no cloud invoice needed).&lt;/text>
&lt;text x="34" y="190" class="ts">· GPU is allocated by USAGE, not presence: a GPU at 50 % charges half its cost.&lt;/text>
&lt;text x="34" y="208" class="ts">· Trap: if the default base price is wrong, the WHOLE split is wrong (typical on-prem under-pricing).&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="where-the-data-comes-from-prometheus">Where the data comes from: Prometheus&lt;/h2>
&lt;p>OpenCost instruments nothing of its own: it reads from Prometheus, which is a prerequisite
of the installation (&lt;a href="https://github.com/opencost/opencost">OpenCost · GitHub&lt;/a>). The sources:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source&lt;/th>
&lt;th>What it provides&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>kube-state-metrics&lt;/code>&lt;/td>
&lt;td>state of K8s objects (pods, requests/limits)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>node-exporter&lt;/code>&lt;/td>
&lt;td>node resources and capacity&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cAdvisor&lt;/code>&lt;/td>
&lt;td>real CPU/memory usage per container&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dcgm-exporter&lt;/code> (NVIDIA)&lt;/td>
&lt;td>&lt;strong>GPU&lt;/strong> usage, memory and power&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>With those series, OpenCost automates the join between Kubernetes resource usage and the
provider price, reducing the custom PromQL you would otherwise write by hand (&lt;a href="https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/">Grafana ·
manage costs&lt;/a>).
The &lt;strong>GPU&lt;/strong> signal comes from DCGM via the &lt;code>dcgm-exporter&lt;/code> (part of the NVIDIA GPU Operator),
the same base as &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability&lt;/a>. Without
Prometheus and without DCGM exporting, OpenCost does not see the GPU.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-the-pieces">Architecture: the pieces&lt;/h2>
&lt;p>OpenCost is not a monolith; it is three responsibilities worth telling apart:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Function&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Cost model&lt;/strong>&lt;/td>
&lt;td>resolves the node price and splits/normalises it across resources&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Allocation API&lt;/strong>&lt;/td>
&lt;td>serves the allocated cost per dimension (&lt;code>/allocation&lt;/code> and endpoints)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Exporter&lt;/strong>&lt;/td>
&lt;td>publishes cost metrics on &lt;code>/metrics&lt;/code> for Prometheus/Grafana&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The data flow: Prometheus holds the usage series (from kube-state-metrics, cAdvisor,
dcgm-exporter); the cost model joins them with the resolved price; the Allocation API
serves them aggregated by whatever dimension you ask for; and the exporter returns them to
Prometheus so that Grafana can plot them. It is a cycle: metrics come in from Prometheus and cost
goes back to Prometheus enriched. That symmetry is what lets you reuse your observability stack
without standing up a new database: OpenCost lives on top of Prometheus, not beside it.&lt;/p>
&lt;p>An operational note: historical analysis depends on Prometheus retention. For cost reports
spanning months, a long-term backend behind it (Thanos, Mimir, VictoriaMetrics) is advisable, or
periodically exporting the allocations to a store of your own.&lt;/p>
&lt;hr>
&lt;h2 id="on-prem-prices-in-euros-the-piece-that-decides-everything">On-prem prices in euros: the piece that decides everything&lt;/h2>
&lt;p>On-prem, the most important figure, and the most neglected, is the &lt;strong>node price&lt;/strong>. It is configured
per provider; the options are &lt;code>alibaba&lt;/code>, &lt;code>aws&lt;/code>, &lt;code>azure&lt;/code>, &lt;code>gcp&lt;/code>, &lt;code>oracle&lt;/code>, &lt;strong>&lt;code>ovh&lt;/code>&lt;/strong> or &lt;strong>&lt;code>default&lt;/code>&lt;/strong>
(on-prem) (&lt;a href="https://opencost.io/docs/configuration/">OpenCost · configuration&lt;/a>).
For on-prem you use &lt;code>default&lt;/code>, supplying base prices in a &lt;code>default.json&lt;/code> (or by overriding the Helm
&lt;code>values.yaml&lt;/code>); anything you do not override uses the chart&amp;rsquo;s value (&lt;a href="https://opencost.io/docs/configuration/on-prem/">OpenCost · on-prem&lt;/a>).&lt;/p>
&lt;p>To set the node price in euros, the calculation is the TCO model one: depreciated capex + opex. An
example of an &lt;strong>8×H100&lt;/strong> node:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Calculation&lt;/th>
&lt;th>€/hour&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Capex (node ~240,000 €, 36 months)&lt;/td>
&lt;td>240,000 ÷ 26,280 h&lt;/td>
&lt;td>~9.13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy (5.6 kW × PUE 1.4 × 0.058 €/kWh, France)&lt;/td>
&lt;td>~0.455 €/h&lt;/td>
&lt;td>~0.46&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintenance, networking, operations&lt;/td>
&lt;td>estimate&lt;/td>
&lt;td>~1.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Node total&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~11.1 €/h&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That ~11 €/h is the number you put into OpenCost, and everything is split from it. And here is
the documented trap: with the default pricing configuration, OpenCost &lt;strong>under-prices CPU and
GPU on-prem&lt;/strong> (&lt;a href="https://github.com/opencost/opencost/issues/3781">issue #3781&lt;/a>).
If you do not adjust the base prices to your real cost, the split will be systematically low and
the cost per token you report will be unreal. &lt;strong>Configuring the node price is not optional: it is
the figure everything else hangs from.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h3 id="configuring-the-price-an-example">Configuring the price: an example&lt;/h3>
&lt;p>The base price is given in the Helm &lt;code>values.yaml&lt;/code> (or a &lt;code>default.json&lt;/code>), in euros per hour and per
unit of resource, derived from the node&amp;rsquo;s total cost:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml — on-prem prices in €/hour (8xH100 node, France)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">opencost&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">customPricing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">custom&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">costModel&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;On-prem 8xH100 node&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.030&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/CPU-hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">RAM&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.004&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GB-hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">GPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1.30&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GPU-hour ← the figure that moves cost/token the most&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0002&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GB-hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OpenCost normalises those values so that, multiplied by the node capacity, they add up to the
total price you declared (~11 €/h in the example). The number to be most careful with is the
&lt;strong>GPU&lt;/strong> one: at 1.30 €/GPU-hour, the 8 cards are ~10.4 €/h, the bulk of the node&amp;rsquo;s cost. A badly set
GPU base shifts the whole cost per token. Always review the resolved prices in
&lt;code>/allNodePricing&lt;/code> after configuring, because the default under-pricing bug bites exactly here.&lt;/p>
&lt;hr>
&lt;h2 id="allocation-by-usage-not-by-presence">Allocation: by usage, not by presence&lt;/h2>
&lt;p>The behaviour that has changed most and that matters most for GPU: in recent versions, &lt;strong>the cost
allocated to a GPU is determined by its usage, not its presence&lt;/strong>. If a GPU costing 100 €/month is
only half used, the allocated cost is ~50 € ([search]). This aligns allocation with reality,
you pay for what you use, but it has a consequence: the other half does not disappear, it is
&lt;strong>idle&lt;/strong> that somebody keeps paying for. OpenCost exposes it, and that is where the optimisation
lever comes from.&lt;/p>
&lt;p>OpenCost allocates to any Kubernetes dimension: &lt;strong>cluster, node, namespace, controller
(deployment, statefulset and so on), service, pod and container&lt;/strong>, and by &lt;strong>labels&lt;/strong> (team,
product). Access is through the Allocation API and several endpoints that expose the mechanics:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Endpoint&lt;/th>
&lt;th>What it returns&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/allocation&lt;/code>&lt;/td>
&lt;td>cost allocated by the dimension you ask for&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/costDataModel&lt;/code>&lt;/td>
&lt;td>the resolved node price&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/allNodePricing&lt;/code>&lt;/td>
&lt;td>hourly prices per node&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/pricingSourceSummary&lt;/code>&lt;/td>
&lt;td>summary of the pricing source&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/metrics&lt;/code>&lt;/td>
&lt;td>cost metrics for Prometheus&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>/metrics&lt;/code> endpoint is what turns OpenCost into a Prometheus exporter: once there, you can
write PromQL to calculate the cost and efficiency of any Kubernetes concept and build panels in
Grafana (&lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">OpenCost · exporter&lt;/a>).&lt;/p>
&lt;h3 id="allocation-api-parameters">Allocation API parameters&lt;/h3>
&lt;p>&lt;code>/allocation&lt;/code> is controlled by a handful of parameters worth knowing, because they define which
number you get:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Parameter&lt;/th>
&lt;th>What it controls&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>window&lt;/code>&lt;/td>
&lt;td>time range&lt;/td>
&lt;td>&lt;code>7d&lt;/code>, &lt;code>today&lt;/code>, specific dates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aggregate&lt;/code>&lt;/td>
&lt;td>aggregation dimension&lt;/td>
&lt;td>&lt;code>namespace&lt;/code>, &lt;code>label:team&lt;/code>, &lt;code>pod&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>accumulate&lt;/code>&lt;/td>
&lt;td>sum the range or break it per interval&lt;/td>
&lt;td>&lt;code>true&lt;/code> (total) / &lt;code>false&lt;/code> (series)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>idle&lt;/code>&lt;/td>
&lt;td>include idle cost or not&lt;/td>
&lt;td>&lt;code>true&lt;/code> / &lt;code>false&lt;/code> / &lt;code>separate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>filter&lt;/code>&lt;/td>
&lt;td>filter by namespace, label and so on&lt;/td>
&lt;td>&lt;code>namespace:&amp;quot;llm-prod&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;strong>&lt;code>idle&lt;/code>&lt;/strong> parameter is the most revealing: with &lt;code>idle=separate&lt;/code>, OpenCost returns the idle
cost as a separate row, which lets you see at a glance how much is being paid for unused
capacity. A query with &lt;code>aggregate=label:team&lt;/code> and &lt;code>idle=separate&lt;/code> over &lt;code>window=30d&lt;/code> is, literally,
the monthly chargeback report with the waste highlighted.&lt;/p>
&lt;hr>
&lt;h2 id="gpu-in-depth-dcgm-usage-mig-and-idle">GPU in depth: DCGM, usage, MIG and idle&lt;/h2>
&lt;p>The GPU is the expensive resource, so its allocation deserves detail:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Usage signal&lt;/strong>: &lt;code>dcgm-exporter&lt;/code> gives &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> (utilisation), &lt;code>DCGM_FI_DEV_FB_USED&lt;/code>
(memory), and power. OpenCost uses utilisation to allocate by usage.&lt;/li>
&lt;li>&lt;strong>MIG (Multi-Instance GPU)&lt;/strong>: an A100 is split into up to 7 isolated instances, which allows
cutting idle from 50 % to almost 0 % by sharing one card across small workloads
([search]; see &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sharing the GPU: time-slicing, MPS and MIG&lt;/a>).
The cost allocation of each MIG instance follows the same usage logic.&lt;/li>
&lt;li>&lt;strong>Idle&lt;/strong>: the reliable detection pattern combines three pieces, DCGM metrics in Prometheus, an
alert that fires when &lt;code>DCGM_FI_DEV_GPU_UTIL &amp;lt; 10&lt;/code> for more than 15 minutes, and routing of
that alert to the team that owns the namespace ([search]).&lt;/li>
&lt;/ul>
&lt;p>An attribution nuance worth knowing: MIG creates isolated instances that Kubernetes sees as
distinct resources, so OpenCost allocates them cleanly, each one to its pod. Time-slicing, by
contrast, shares the same physical GPU across several pods with no compute isolation: there,
per-pod utilisation is harder to separate, and cost attribution becomes approximate (the GPU is
split across the pods sharing it, according to their measured usage). The practical rule: if you
need exact per-team chargeback on a shared GPU, MIG gives cleaner attribution than
time-slicing, at the price of rigid fixed partitions. It is a trade-off between cost accuracy and
flexibility that is better decided before, not after, building the split.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 220" role="img" aria-label="GPU allocation by usage in OpenCost: used cost charged to the pod, idle cost flagged to the namespace owner, and MIG splitting one card across workloads" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#om2)}&lt;/style>
&lt;defs>&lt;marker id="om2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="170" height="70" rx="6"/>
&lt;text x="32" y="63" class="tl">GPU (DCGM)&lt;/text>
&lt;text x="32" y="81" class="ts">util, memory, power&lt;/text>
&lt;text x="32" y="98" class="ts">→ to Prometheus&lt;/text>
&lt;path class="ar" d="M190,75 L235,60"/>
&lt;path class="ar" d="M190,80 L235,150"/>
&lt;rect class="bx" x="235" y="34" width="240" height="44" rx="6"/>
&lt;text x="247" y="54" class="tl">USED cost → pod / team&lt;/text>
&lt;text x="247" y="70" class="ts">charged by utilisation&lt;/text>
&lt;rect class="dsh" x="235" y="128" width="240" height="44" rx="6"/>
&lt;text x="247" y="148" class="tl">IDLE cost → namespace owner&lt;/text>
&lt;text x="247" y="164" class="ts">alert util &amp;lt; 10 % &amp;gt; 15 min&lt;/text>
&lt;rect class="bx" x="520" y="80" width="240" height="50" rx="6"/>
&lt;text x="532" y="100" class="tl">MIG: 1 A100 → up to 7&lt;/text>
&lt;text x="532" y="118" class="ts">splits the card, idle 50%→~0&lt;/text>
&lt;text x="20" y="200" class="ts">Usage allocation reveals idle; MIG and scheduling recover it. OpenCost measures; other layers act.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="efficiency-the-number-that-triggers-action">Efficiency: the number that triggers action&lt;/h2>
&lt;p>With cost allocated by usage, OpenCost lets you calculate efficiency: how much of the allocated
cost corresponds to useful work against the total. A GPU at 30 % utilisation has an efficiency of
30 %: the remaining 70 % is idle cost that somebody pays without receiving work. That figure, per
team and per namespace, is what turns a dashboard into a chargeback conversation: &amp;ldquo;your namespace
has 5 GPUs allocated at 25 % efficiency; either you raise utilisation or you release three cards&amp;rdquo;.
It connects with &lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy&lt;/a>:
allocation locates the waste; scheduling and co-residency recover it.&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-the-allocation-report">Worked example: the allocation report&lt;/h2>
&lt;p>On the ~11 €/h node (8×H100), three teams share the cluster for a month. What a
&lt;code>/allocation?aggregate=label:team&amp;amp;idle=separate&amp;amp;window=30d&lt;/code> query returns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Team&lt;/th>
&lt;th>GPU-hours&lt;/th>
&lt;th>Avg. util.&lt;/th>
&lt;th>Allocated cost (€)&lt;/th>
&lt;th>Efficiency&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A · chat-prod&lt;/td>
&lt;td>2,880 (4 GPU)&lt;/td>
&lt;td>78 %&lt;/td>
&lt;td>~3,744&lt;/td>
&lt;td>high&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B · batch&lt;/td>
&lt;td>1,440 (2 GPU)&lt;/td>
&lt;td>55 %&lt;/td>
&lt;td>~1,872&lt;/td>
&lt;td>medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C · experimentation&lt;/td>
&lt;td>1,440 (2 GPU)&lt;/td>
&lt;td>22 %&lt;/td>
&lt;td>~1,872&lt;/td>
&lt;td>&lt;strong>low&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>idle&lt;/strong> (separate)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&lt;strong>~1,100&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>What the report reveals: the cost allocated to A, B and C comes out of their GPU-hours and the node
price; but the &lt;strong>idle&lt;/strong> row (~1,100 €/month) is capacity nobody used and everyone pays for in the
pro rata. And the efficiency column points at C: 2 GPUs at 22 % is ~1,560 € a month of which
only ~340 € is useful work. That is the actionable figure: C does not have a model problem, it has
a utilisation problem. Without OpenCost, those ~1,100 € of idle and C&amp;rsquo;s inefficiency stay diluted
in an aggregate invoice nobody questions. With it, they are a named row and a number in euros.&lt;/p>
&lt;hr>
&lt;h2 id="useful-promql-queries">Useful PromQL queries&lt;/h2>
&lt;p>As a Prometheus exporter, OpenCost lets you build the panels by hand. Some reference queries (exact
metric names vary by version; check in your &lt;code>/metrics&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Cost per node and hour (resolved price)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">node_total_hourly_cost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># GPU cost allocated per namespace (€/hour)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nv">container_gpu_allocation&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">on&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">node&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">group_left&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">node_gpu_hourly_cost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Average GPU utilisation per namespace (for efficiency)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">avg&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_GPU_UTIL&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Idle GPUs: utilisation &amp;lt; 10 % sustained&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nv">DCGM_FI_DEV_GPU_UTIL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The last one, wrapped in an alert with &lt;code>for: 15m&lt;/code> and routed to the namespace owner, is the
standard idle detection pattern. With these series in Grafana you have cost per team, efficiency
and waste on one dashboard, the &amp;ldquo;Inform report&amp;rdquo; of the FinOps phase in real time.&lt;/p>
&lt;hr>
&lt;h2 id="dynamic-cloud-prices-vs-custom-on-prem-prices">Dynamic cloud prices vs custom on-prem prices&lt;/h2>
&lt;p>The difference is worth understanding, because it changes how reliable the figure is:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cloud&lt;/strong> (aws/azure/gcp/oracle/ovh): OpenCost obtains the price dynamically from the
provider&amp;rsquo;s billing API, so the allocated cost reflects the real rate (including discounts, spot
and so on) without you declaring anything.&lt;/li>
&lt;li>&lt;strong>On-prem&lt;/strong> (&lt;code>default&lt;/code>/&lt;code>custom&lt;/code>): you declare the price, derived from your TCO model. The
advantage is total control; the risk is that a badly calculated price skews everything. That is
why the TCO model (depreciated capex + opex) is not an academic exercise: it is literally
OpenCost&amp;rsquo;s &lt;em>input&lt;/em>.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>&lt;code>ovh&lt;/code>&lt;/strong> provider among those supported is relevant for a European platform: if part of the
workload goes to a European sovereign cloud, OpenCost can allocate its cost with dynamic pricing,
and mix it with on-prem in the same allocation view.&lt;/p>
&lt;hr>
&lt;h2 id="from-pod-cost-to-cost-per-token">From pod cost to cost per token&lt;/h2>
&lt;p>OpenCost gets as far as &amp;ldquo;this vLLM pod cost X €/hour&amp;rdquo;. The &lt;strong>cost per token&lt;/strong>, the metric that
compares on-prem against cloud, needs one more piece: the &lt;strong>gateway&lt;/strong> (LiteLLM) that counts tokens
per request and per team, as covered in the &lt;a href="https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/">FinOps introduction&lt;/a>.
The full chain is: OpenCost gives the pod cost by usage → the gateway gives the tokens per team →
dividing them gives the cost per token per team. OpenCost is the hardware half of that
equation; without it, you would know the tokens but not their real cost. That is why A2 (allocation)
is the foundation the whole cost per token of the proposal hangs from.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-helm--prometheus">Deployment: Helm + Prometheus&lt;/h2>
&lt;p>The typical deployment is via Helm, with Prometheus as a prerequisite (for scraping and
series storage). Conceptual steps:&lt;/p>
&lt;ol>
&lt;li>Have Prometheus with &lt;code>kube-state-metrics&lt;/code>, &lt;code>node-exporter&lt;/code>, &lt;code>cAdvisor&lt;/code> and &lt;code>dcgm-exporter&lt;/code>.&lt;/li>
&lt;li>Install OpenCost with Helm, pointing it at your Prometheus.&lt;/li>
&lt;li>Configure the &lt;strong>node price&lt;/strong> in euros (&lt;code>default.json&lt;/code> / &lt;code>values.yaml&lt;/code>), the step most often
neglected.&lt;/li>
&lt;li>Expose &lt;code>/metrics&lt;/code> and build panels in Grafana with PromQL over the cost metrics.&lt;/li>
&lt;/ol>
&lt;p>As a Prometheus exporter, OpenCost lets you &lt;strong>write PromQL for the cost and efficiency of any
concept&lt;/strong> and create bespoke dashboards (&lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">OpenCost · exporter&lt;/a>).
It reuses infrastructure a GPU cluster already has for observability (DCGM, Prometheus,
Grafana): OpenCost only adds the pricing and allocation layer.&lt;/p>
&lt;hr>
&lt;h2 id="opencost-vs-kubecost-just-the-essentials">OpenCost vs Kubecost (just the essentials)&lt;/h2>
&lt;p>OpenCost is the free base; Kubecost (IBM) is the commercial product built on it. Briefly,
what the commercial one adds:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capability&lt;/th>
&lt;th>OpenCost&lt;/th>
&lt;th>Kubecost&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cost allocation (CPU/GPU/mem/PV)&lt;/td>
&lt;td>✓&lt;/td>
&lt;td>✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU via DCGM&lt;/td>
&lt;td>✓&lt;/td>
&lt;td>✓ (3.0)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Long historical retention&lt;/td>
&lt;td>depends on your Prometheus&lt;/td>
&lt;td>included&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Automatic rightsizing&lt;/td>
&lt;td>—&lt;/td>
&lt;td>✓ (Turbonomic)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Governance, alerts, enterprise RBAC&lt;/td>
&lt;td>basic&lt;/td>
&lt;td>✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Commercial support&lt;/td>
&lt;td>community&lt;/td>
&lt;td>✓ (IBM)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For most on-prem clusters with a platform team, OpenCost covers allocation; Kubecost adds value
when you want automated optimisation and enterprise features without operating it yourself. The
detail of the comparison, and when paying is worth it, is article &lt;strong>A3&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="implementation-checklist">Implementation checklist&lt;/h2>
&lt;p>For OpenCost to give a defensible per-team cost, order matters:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Step&lt;/th>
&lt;th>Action&lt;/th>
&lt;th>Verification&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Prometheus with kube-state-metrics, node-exporter, cAdvisor&lt;/td>
&lt;td>series present&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>NVIDIA GPU Operator + dcgm-exporter&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> in Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Install OpenCost (Helm) pointing at Prometheus&lt;/td>
&lt;td>&lt;code>/allocation&lt;/code> responds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;strong>Configure the node price in €&lt;/strong> (capex+opex)&lt;/td>
&lt;td>&lt;code>/allNodePricing&lt;/code> shows the real one&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Label pods/namespaces by team and product&lt;/td>
&lt;td>&lt;code>aggregate=label:team&lt;/code> splits correctly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>Grafana panels for cost, efficiency and idle&lt;/td>
&lt;td>the waste is visible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Idle alert (&lt;code>util&amp;lt;10&lt;/code> &lt;code>for: 15m&lt;/code>) to the owner&lt;/td>
&lt;td>the alert reaches the team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>(optional) long-retention backend&lt;/td>
&lt;td>months of history available&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Step 4 is the one most often skipped and the one that most invalidates the result. If you only do
one thing right, make it that one: without the correct node price in euros, steps 5 to 7 split a
wrong cost with great precision.&lt;/p>
&lt;hr>
&lt;h2 id="limits-and-traps-data-driven">Limits and traps (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Default under-pricing.&lt;/strong> With the default pricing config, OpenCost underestimates on-prem
CPU and GPU (&lt;a href="https://github.com/opencost/opencost/issues/3781">#3781&lt;/a>). Adjust the base prices
to your real cost or the whole split will come out low.&lt;/li>
&lt;li>&lt;strong>It depends on Prometheus and DCGM.&lt;/strong> Without the right series (especially &lt;code>dcgm-exporter&lt;/code>),
there is no GPU allocation. Input data quality rules.&lt;/li>
&lt;li>&lt;strong>Allocation by usage ≠ total cost.&lt;/strong> A GPU at 50 % charges half; the other half is idle that
still costs. Do not confuse &amp;ldquo;allocated cost&amp;rdquo; with &amp;ldquo;cost paid&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Prometheus retention.&lt;/strong> Historical cost analysis depends on how much your Prometheus retains;
for long series, plan a long-term backend.&lt;/li>
&lt;li>&lt;strong>Allocation, not per-token measurement.&lt;/strong> OpenCost reaches the pod; cost per token needs the
gateway (LiteLLM) above it, as covered in the FinOps introduction.&lt;/li>
&lt;/ol>
&lt;p>With OpenCost&amp;rsquo;s mechanics clear, the next article (A3) compares what Kubecost and the commercial
alternatives add, to decide which FinOps stack to adopt. But the foundation, correct allocation
with the node price properly set in euros, is this one.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>OpenCost looks like &amp;ldquo;installing a cost tool&amp;rdquo; and is, in reality, &lt;strong>declaring what your hardware is
worth and letting it split itself&lt;/strong>. That simplicity is its strength, it lives on top of the
Prometheus and DCGM you already have, with no new database, and also its trap: the split is only as
good as the node price you declare, and the default value under-prices on-prem GPU. Properly
configured in euros, OpenCost turns the cluster&amp;rsquo;s aggregate invoice into a split per team, per
product and per GPU, separates used cost from idle, and exposes the efficiency that triggers
chargeback conversations. It is the hardware half of the cost per token equation, the other half,
the tokens, is supplied by the gateway, and without this half there is no TCO model or on-prem vs
cloud comparison that holds up. For a sovereign architecture proposal, OpenCost is the piece that
makes cost stop being an intuition and become a table in euros, reproducible and auditable, with
the idle GPU named and identified. The &lt;em>Inform&lt;/em> phase of GPU FinOps starts here, and everything
else hangs on this number being right.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-chargeback-showback-multi-tenancy/">GPU chargeback and showback&lt;/a> — from OpenCost&amp;rsquo;s cost allocation to the per-team split with budgets (Kueue) and token attribution (LiteLLM).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/">GPU utilisation as a FinOps lever&lt;/a> — the cost of idle and how occupancy moves the allocated cost.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/full-tco-on-premise-gpu-cluster-capex-gpu-hour-break-even/">TCO of an on-premise GPU cluster: depreciation, energy and infrastructure&lt;/a> — the CAPEX, the depreciation and the €/GPU-hour that OpenCost needs as a base price when the node is not cloud but your own on-premise hardware.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>OpenCost · GitHub (CNCF, Apache 2.0) — &lt;a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost&lt;/a>&lt;/li>
&lt;li>OpenCost · configuración (proveedores: aws/azure/gcp/oracle/ovh/default) — &lt;a href="https://opencost.io/docs/configuration/">https://opencost.io/docs/configuration/&lt;/a>&lt;/li>
&lt;li>OpenCost · configuración on-prem (precios personalizados) — &lt;a href="https://opencost.io/docs/configuration/on-prem/">https://opencost.io/docs/configuration/on-prem/&lt;/a>&lt;/li>
&lt;li>OpenCost · exportador de Prometheus — &lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">https://opencost.io/docs/integrations/opencost-exporter/&lt;/a>&lt;/li>
&lt;li>OpenCost · issue #3781 (infra-precio CPU/GPU on-prem por defecto) — &lt;a href="https://github.com/opencost/opencost/issues/3781">https://github.com/opencost/opencost/issues/3781&lt;/a>&lt;/li>
&lt;li>NVIDIA · dcgm-exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>Grafana · manage costs (OpenCost + Kubernetes) — &lt;a href="https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/">https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GPU/LLM FinOps: frameworks, metrics and state of the art (tool by tool)</title><link>https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/</link><pubDate>Sat, 13 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The references are &lt;strong>European&lt;/strong>
(providers and prices from FR/DE/ES), as this is a sovereign proposal; when a source quotes
dollars it is marked &amp;ldquo;USD&amp;rdquo;. The dollar sign is not used (on this site it is a formula
delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-introduction-covers">What this introduction covers&lt;/h2>
&lt;p>Second article in the data series, and the first pillar &lt;em>deep dive&lt;/em>. Here the &lt;strong>FinOps&lt;/strong> tooling
for GPU/LLM infrastructure is inventoried with the detail an architecture decision needs: what
metrics each tool handles, how it allocates cost internally, what GPU support it has, under
what licence and pricing model it operates, and where its limits are. It is the natural
continuation of the &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">opening article&lt;/a>,
which established that cost per token is the unit that allows on-prem and cloud to be compared.
No recommendations: the final choice is settled in the synthesis article with the Pareto table;
here there are only facts and methodology.&lt;/p>
&lt;hr>
&lt;h2 id="why-gpu-finops-is-a-different-problem">Why GPU FinOps is a different problem&lt;/h2>
&lt;p>Classic cloud FinOps was born to split CPU, memory and storage, cheap and elastic resources. The
GPU breaks three assumptions at once, and that is why it needs its own treatment:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>It is expensive and discrete.&lt;/strong> An H100 costs on the order of 2.7 to 7 €/hour in European
cloud (Scaleway from 2.73 €/h; OVHcloud somewhat more), or its depreciated capex on-prem; it is
not a resource that gets split in trivial fractions. A 10 % allocation error across a GPU fleet
is real money.&lt;/li>
&lt;li>&lt;strong>It is easily underused.&lt;/strong> Unlike the CPU, a GPU draws its power even when idle, and
average occupancy in ungoverned clusters is notoriously low. The cost of &lt;em>idle&lt;/em>, the GPU
powered on without working, is waste number one, and it is invisible if it is not measured.&lt;/li>
&lt;li>&lt;strong>It is hard to attribute.&lt;/strong> Who owns the cost of a GPU shared by &lt;em>time-slicing&lt;/em> or MIG across
several pods from several teams? Without an allocation layer, GPU spend is a black hole nobody
claims and nobody optimises.&lt;/li>
&lt;/ol>
&lt;p>The goal of GPU FinOps is to turn that black hole into an attributed, measured and actionable
invoice: knowing what each team, each model and, in the end, each token costs.&lt;/p>
&lt;hr>
&lt;h2 id="the-finops-metrics">The FinOps metrics&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Unit&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>CPM&lt;/strong> (cost/1M tokens)&lt;/td>
&lt;td>cluster cost ÷ tokens produced&lt;/td>
&lt;td>€ / 1M tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per request&lt;/td>
&lt;td>cost charged to a complete request&lt;/td>
&lt;td>€ / req&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>€/GPU-hour&lt;/td>
&lt;td>hourly cost of a GPU (depreciated or rented)&lt;/td>
&lt;td>€/h&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Utilisation&lt;/strong>&lt;/td>
&lt;td>fraction of the GPU actually used&lt;/td>
&lt;td>% (MFU, GPU-hour util)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost of idle&lt;/td>
&lt;td>GPU powered on without useful work&lt;/td>
&lt;td>€/h wasted&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Showback&lt;/td>
&lt;td>reporting the cost to each team (without charging)&lt;/td>
&lt;td>€ / team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chargeback&lt;/td>
&lt;td>charging the cost to each team&lt;/td>
&lt;td>€ / team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost efficiency&lt;/td>
&lt;td>real cost ÷ cost if it were at 100 %&lt;/td>
&lt;td>%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The metric that closes the loop with the business is cost per token (or per request): it is the
only one that can be compared across providers, across models and against the price of an external
API. All GPU FinOps tooling exists to reach, one way or another, that number.&lt;/p>
&lt;hr>
&lt;h2 id="the-three-finops-phases-finops-foundation">The three FinOps phases (FinOps Foundation)&lt;/h2>
&lt;p>The FinOps Foundation framework organises the work in a three-phase cycle. Applied to GPU:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Phase&lt;/th>
&lt;th>Goal&lt;/th>
&lt;th>Typical GPU action&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Inform&lt;/strong>&lt;/td>
&lt;td>visibility and allocation&lt;/td>
&lt;td>measure cost per namespace/team/model/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Optimize&lt;/strong>&lt;/td>
&lt;td>reduce spend&lt;/td>
&lt;td>rightsizing, spot, quotas, switch off the idle, quantise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Operate&lt;/strong>&lt;/td>
&lt;td>continuous governance&lt;/td>
&lt;td>budgets, idle alerts, automated chargeback&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The common mistake is jumping to &lt;em>Optimize&lt;/em> without having done &lt;em>Inform&lt;/em>: GPUs get bought or
replicas get adjusted without knowing where the money really goes. Allocation (the Inform phase) is
the prerequisite for everything else, and it is where the tooling comes in.&lt;/p>
&lt;hr>
&lt;h2 id="how-cost-is-allocated-in-kubernetes-opencosts-mechanics">How cost is allocated in Kubernetes: OpenCost&amp;rsquo;s mechanics&lt;/h2>
&lt;p>OpenCost is the de facto standard, so it is worth understanding how it allocates cost,
because that defines what any tool on top can and cannot do. It is a vendor-neutral, Apache 2.0
project, originally built by Kubecost and donated to the CNCF (incubating project)
(&lt;a href="https://github.com/opencost/opencost">OpenCost · GitHub&lt;/a>).&lt;/p>
&lt;p>The model works at node level: it starts from the node&amp;rsquo;s resource capacity (CPU, RAM, GPU,
storage) and its total price, and splits that price across the resources. When the provider does
not give explicit CPU/GPU/RAM prices, OpenCost uses the ratio of a set of base prices (the
provider&amp;rsquo;s marginal rates, customisable) and &lt;strong>normalises them so that the sum of the components
equals the node&amp;rsquo;s total price&lt;/strong>
(&lt;a href="https://opencost.io/docs/configuration/on-prem/">OpenCost · on-prem&lt;/a>). This is key on-premise:
you define the node cost (depreciated capex + opex) and OpenCost splits it.&lt;/p>
&lt;p>It obtains utilisation by scraping Prometheus: &lt;code>kube-state-metrics&lt;/code>, &lt;code>node-exporter&lt;/code>
and &lt;code>cAdvisor&lt;/code> give it the real consumption per pod, and with that it allocates cost per
cluster, node, namespace, controller, service or pod (&lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">OpenCost · exporter&lt;/a>).
For the GPU, the signal comes from DCGM (via the NVIDIA GPU Operator) exported to
Prometheus, the same base as &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability&lt;/a>.
A common idle detection pattern: an alert when &lt;code>DCGM_FI_DEV_GPU_UTIL &amp;lt; 10&lt;/code>
for more than 15 minutes, routed to the team that owns the namespace.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="OpenCost cost allocation mechanics: utilisation metrics from Prometheus and DCGM, node-level pricing model, and allocation per namespace, team and pod" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#fm)}&lt;/style>
&lt;defs>&lt;marker id="fm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;text x="20" y="26" class="tl">Utilisation metrics (Prometheus)&lt;/text>
&lt;rect class="bx" x="20" y="36" width="200" height="84" rx="6"/>
&lt;text x="32" y="58" class="ts">kube-state-metrics&lt;/text>
&lt;text x="32" y="76" class="ts">node-exporter · cAdvisor&lt;/text>
&lt;text x="32" y="94" class="ts">DCGM (GPU, via GPU Operator)&lt;/text>
&lt;text x="32" y="112" class="ts">usage per pod&lt;/text>
&lt;path class="ar" d="M220,78 L265,78"/>
&lt;rect class="bx" x="265" y="36" width="210" height="84" rx="6"/>
&lt;text x="277" y="58" class="tl">Pricing model (per node)&lt;/text>
&lt;text x="277" y="78" class="ts">total node price split&lt;/text>
&lt;text x="277" y="96" class="ts">across CPU/GPU/RAM/disk&lt;/text>
&lt;text x="277" y="114" class="ts">normalised so sum = total&lt;/text>
&lt;path class="ar" d="M475,78 L520,78"/>
&lt;rect class="bx" x="520" y="36" width="240" height="84" rx="6"/>
&lt;text x="532" y="58" class="tl">Allocation&lt;/text>
&lt;text x="532" y="78" class="ts">per cluster / node / namespace&lt;/text>
&lt;text x="532" y="96" class="ts">controller / service / pod&lt;/text>
&lt;text x="532" y="114" class="ts">→ cost per team&lt;/text>
&lt;rect class="dsh" x="20" y="150" width="740" height="74" rx="6"/>
&lt;text x="34" y="172" class="tl">On-premise you define the node cost (depreciated capex + opex); OpenCost only splits it.&lt;/text>
&lt;text x="34" y="192" class="ts">GPU idle: alert if DCGM_FI_DEV_GPU_UTIL &amp;lt; 10 for &amp;gt;15 min → route to the namespace owner.&lt;/text>
&lt;text x="34" y="210" class="ts">What OpenCost allocates per resource, the layers above enrich into cost per token.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="frameworks-tool-by-tool">Frameworks, tool by tool&lt;/h2>
&lt;h3 id="opencost--the-allocation-standard-cncf-apache-20">OpenCost — the allocation standard (CNCF, Apache 2.0)&lt;/h3>
&lt;p>What it measures: allocated cost of in-cluster resources (CPU, GPU, memory, volumes) by any
Kubernetes dimension. Method: node-level pricing model + Prometheus scraping (above). GPU: yes,
via DCGM. Licence: Apache 2.0, free. It can run as a metrics exporter to Prometheus with no
further dependencies. Limit: it is the allocation layer, it brings no optimisation, governance
or product unit economics; for that you put something on top of it.&lt;/p>
&lt;h3 id="kubecost--the-commercial-product-on-opencost-ibm">Kubecost — the commercial product on OpenCost (IBM)&lt;/h3>
&lt;p>What it measures: what OpenCost does (it is built on it) plus enterprise capabilities. &lt;strong>Kubecost
3.0 (2025)&lt;/strong> added GPU monitoring via &lt;strong>NVIDIA DCGM&lt;/strong> and integration with &lt;strong>IBM
Turbonomic&lt;/strong> for automatic &lt;em>rightsizing&lt;/em>, and extended its scope from Kubernetes to cloud service
cost. IBM acquired Kubecost and integrated Kubecost/OpenCost into its FinOps Suite alongside
Cloudability and Turbonomic (&lt;a href="https://www.cloudzero.com/blog/kubecost-vs-opencost/">CloudZero · Kubecost vs OpenCost&lt;/a>).
Differentiator: rightsizing, governance, support. Limit: it is a commercial product; the bulk of
the value over free OpenCost is the optimisation and enterprise layer.&lt;/p>
&lt;h3 id="cloudzero--unit-economics-and-cost-per-product">CloudZero — unit economics and cost per product&lt;/h3>
&lt;p>What it measures: it maps cloud cost to features, products, teams and customers, not just to
resources. Method: it ingests multi-cloud billing and models it in business dimensions.
Differentiator: the deepest allocation and the unit economics approach (cost per business
unit). Limit: less focused on intra-Kubernetes mechanics than OpenCost/Kubecost; it is the
&amp;ldquo;cost to business&amp;rdquo; layer.&lt;/p>
&lt;h3 id="vantage--multi-cloud-with-many-integrations">Vantage — multi-cloud with many integrations&lt;/h3>
&lt;p>What it measures: multi-cloud cost with more than 20 native integrations (AWS, Azure, GCP,
Kubernetes, Snowflake, Datadog, OpenAI and so on). Differentiator: breadth of sources, including
the invoice from LLM providers (OpenAI), which brings it closer to end-to-end AI cost. Limit:
the depth of intra-cluster allocation is lower than that of K8s-native tools.&lt;/p>
&lt;h3 id="finout--virtual-tagging-fast-deployment">Finout — virtual tagging, fast deployment&lt;/h3>
&lt;p>What it measures: multi-cloud cost with virtual tagging: it applies cost labels &lt;strong>without
modifying the real resources&lt;/strong>, which allows spend that was not properly tagged at source to be
allocated. Differentiator: fast deployment and flexible reallocation without touching the infra.
Limit: like the others in its category, it depends on the quality of the billing data it ingests.&lt;/p>
&lt;h3 id="comparison-table">Comparison table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Scope&lt;/th>
&lt;th>GPU&lt;/th>
&lt;th>Licence / model&lt;/th>
&lt;th>Differentiator&lt;/th>
&lt;th>Layer&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>OpenCost&lt;/strong>&lt;/td>
&lt;td>Kubernetes&lt;/td>
&lt;td>Yes (DCGM)&lt;/td>
&lt;td>Apache 2.0 (CNCF), free&lt;/td>
&lt;td>allocation standard&lt;/td>
&lt;td>resource&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kubecost&lt;/strong>&lt;/td>
&lt;td>K8s + cloud&lt;/td>
&lt;td>Yes (DCGM, 3.0)&lt;/td>
&lt;td>commercial (IBM)&lt;/td>
&lt;td>rightsizing, enterprise&lt;/td>
&lt;td>resource+optim&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CloudZero&lt;/strong>&lt;/td>
&lt;td>multi-cloud&lt;/td>
&lt;td>indirect&lt;/td>
&lt;td>commercial&lt;/td>
&lt;td>unit economics per product&lt;/td>
&lt;td>business&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Vantage&lt;/strong>&lt;/td>
&lt;td>multi-cloud (20+)&lt;/td>
&lt;td>via K8s/provider&lt;/td>
&lt;td>commercial&lt;/td>
&lt;td>breadth (incl. OpenAI)&lt;/td>
&lt;td>business&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Finout&lt;/strong>&lt;/td>
&lt;td>multi-cloud&lt;/td>
&lt;td>via K8s/provider&lt;/td>
&lt;td>commercial&lt;/td>
&lt;td>virtual tagging&lt;/td>
&lt;td>business&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="pricing-models-of-commercial-tooling">Pricing models of commercial tooling&lt;/h2>
&lt;p>They are worth knowing because the cost of the FinOps tool is also FinOps:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>How it charges&lt;/th>
&lt;th>Range&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Savings-based&lt;/td>
&lt;td>% of the savings delivered&lt;/td>
&lt;td>&lt;strong>15–35 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fixed-fee&lt;/td>
&lt;td>% of annual cloud spend&lt;/td>
&lt;td>&lt;strong>1–3 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>(&lt;a href="https://www.cloudzero.com/blog/finops-tools/">CloudZero · FinOps Tools&lt;/a>). The implication: at
large cloud spend, a 1–3 % &lt;em>fixed-fee&lt;/em> can exceed the &lt;em>savings-based&lt;/em> model; at small spend with a
lot of waste, &lt;em>savings-based&lt;/em> aligns incentives. OpenCost, being free, changes the equation for
whoever has a team to operate it.&lt;/p>
&lt;hr>
&lt;h2 id="focus-the-cost-data-standard">FOCUS: the cost data standard&lt;/h2>
&lt;p>The cross-cutting problem of multi-source FinOps is that each provider bills in its own format.
&lt;strong>FOCUS&lt;/strong> (FinOps Open Cost and Usage Specification) is the open technical specification that
defines requirements so that providers produce uniform billing datasets
(&lt;a href="https://focus.finops.org/focus-specification/">FOCUS · FinOps Foundation&lt;/a>).
The committee ratified FOCUS v1.3 on 4 December 2025.&lt;/p>
&lt;p>What is relevant for this series: at &lt;strong>FinOps X 2026&lt;/strong> the focus has been put on &lt;strong>extending FOCUS
to AI workloads&lt;/strong>, with token economics pushing the specification, and the expansion requests
include AI, datacenter and SaaS/PaaS workloads (&lt;a href="https://siliconangle.com/2026/06/08/focus-specification-ai-cost-accountability-finopsx/">SiliconANGLE&lt;/a>).
In other words, the standard that normalises cloud cost is being stretched to cover AI cost per
token. For an architecture proposal, betting on tools that emit and consume FOCUS is betting on
future interoperability.&lt;/p>
&lt;hr>
&lt;h2 id="from-resource-to-token-cost-per-token-with-a-gateway">From resource to token: cost per token with a gateway&lt;/h2>
&lt;p>Resource allocation (OpenCost) gets as far as &amp;ldquo;this vLLM pod cost X €/hour&amp;rdquo;. To get to &amp;ldquo;this
request from this team cost Y&amp;rdquo; you have to intercept the inference traffic. That is where the
gateway comes in.&lt;/p>
&lt;p>Tools such as LiteLLM sit between the application and the LLM provider/engine, and &lt;strong>intercept
every request to record tokens, latency and cost in real time&lt;/strong>. Layers above (for instance
OpenLM&amp;rsquo;s AI Gateway) generate usage logs compatible with FOCUS (v1.0 to 1.3) and map spend to
team, product, customer or feature, enabling showback or chargeback
(&lt;a href="https://www.openlm.com/enable-ai-finops-with-real-time-token-attribution/">OpenLM · token attribution&lt;/a>).
This links up with &lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy of the GPU cluster&lt;/a>,
where the gateway is the piece that splits cost between tenants.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 230" role="img" aria-label="From GPU-hour to cost per token: OpenCost allocates the pod cost, the LiteLLM gateway intercepts requests and counts tokens, and cost is split per team and product" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#fm2)}&lt;/style>
&lt;defs>&lt;marker id="fm2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="160" height="58" rx="6"/>
&lt;text x="32" y="63" class="tl">OpenCost&lt;/text>
&lt;text x="32" y="82" class="ts">vLLM pod cost (€/h)&lt;/text>
&lt;path class="ar" d="M180,69 L225,69"/>
&lt;rect class="bx" x="225" y="40" width="180" height="58" rx="6"/>
&lt;text x="237" y="63" class="tl">Gateway (LiteLLM)&lt;/text>
&lt;text x="237" y="82" class="ts">intercepts · counts tokens&lt;/text>
&lt;path class="ar" d="M405,69 L450,69"/>
&lt;rect class="bx" x="450" y="40" width="150" height="58" rx="6"/>
&lt;text x="462" y="63" class="tl">FOCUS logs&lt;/text>
&lt;text x="462" y="82" class="ts">tokens · latency · cost&lt;/text>
&lt;path class="ar" d="M600,69 L645,69"/>
&lt;rect class="bx" x="645" y="40" width="115" height="58" rx="6"/>
&lt;text x="657" y="63" class="tl">Showback&lt;/text>
&lt;text x="657" y="82" class="ts">per team&lt;/text>
&lt;rect class="dsh" x="20" y="130" width="740" height="72" rx="6"/>
&lt;text x="34" y="152" class="tl">Two halves that have to be joined: allocation per resource (OpenCost) + measurement per token (gateway).&lt;/text>
&lt;text x="34" y="172" class="ts">Without the gateway you know the pod's cost, not the request's. Without OpenCost you know the tokens, not the real hardware cost.&lt;/text>
&lt;text x="34" y="190" class="ts">FOCUS is the common format that lets them be crossed with the rest of the cloud spend.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="worked-example-chargeback-of-a-multi-tenant-cluster">Worked example: chargeback of a multi-tenant cluster&lt;/h2>
&lt;p>To see the two halves joined, a split over an example node (&lt;strong>4×H100&lt;/strong>, depreciated cost
12 €/hour) shared by three teams via namespaces and MIG:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Team&lt;/th>
&lt;th>GPU-hours allocated (OpenCost)&lt;/th>
&lt;th>Hardware cost (€/h)&lt;/th>
&lt;th>Tokens/day (gateway)&lt;/th>
&lt;th>Cost/1M tok&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A · chat product&lt;/td>
&lt;td>50 %&lt;/td>
&lt;td>6.00&lt;/td>
&lt;td>8M&lt;/td>
&lt;td>~0.75 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B · nightly batch&lt;/td>
&lt;td>30 %&lt;/td>
&lt;td>3.60&lt;/td>
&lt;td>3M&lt;/td>
&lt;td>~1.20 €&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C · experimentation&lt;/td>
&lt;td>20 %&lt;/td>
&lt;td>2.40&lt;/td>
&lt;td>0.5M&lt;/td>
&lt;td>~4.80 €&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>How it comes out: OpenCost splits the node&amp;rsquo;s 12 €/h according to the GPU-hours each namespace
consumes (half for A → 6 €/h). The gateway supplies the tokens per team. Each one&amp;rsquo;s cost per
million tokens is its hardware cost divided by its output, and it reveals something neither half
would see alone: team &lt;strong>C pays 6× more per token than A&lt;/strong>, not because its model is worse, but
because its GPU is underused (a lot of allocated GPU-hours for few tokens). That 4.80 €/1M is
the chargeback signal that triggers an optimisation conversation: either C raises its utilisation,
or it releases the GPU. Without crossing allocation and tokens, that waste stays hidden in a
cluster average.&lt;/p>
&lt;hr>
&lt;h2 id="the-hidden-cost-utilisation">The hidden cost: utilisation&lt;/h2>
&lt;p>Optimisation lever number one is not changing GPU, it is stopping paying for an idle GPU. As
seen in the opening article, the electricity cost per token at 80 % utilisation is a quarter of
what it is at 20 %, and utilisation spreads all the fixed cost (capex + energy) over more tokens.
Measuring idle is, therefore, the highest-return action of the &lt;em>Optimize&lt;/em> phase:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Signal&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Typical threshold&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Idle GPU&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>&lt;/td>
&lt;td>&amp;lt; 10 % for &amp;gt; 15 min&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Unused GPU memory&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code>&lt;/td>
&lt;td>reserved but not used&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pods with no traffic&lt;/td>
&lt;td>gateway metrics&lt;/td>
&lt;td>0 requests, GPU allocated&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Allocation (OpenCost) locates whose the idle GPU is; scheduling and co-residency
(&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sharing the GPU: time-slicing, MPS and MIG&lt;/a>)
recover it. FinOps closes the loop: measure → attribute → optimise → govern.&lt;/p>
&lt;hr>
&lt;h2 id="optimize-the-saving-levers-with-data">Optimize: the saving levers (with data)&lt;/h2>
&lt;p>Once cost is allocated (Inform), the &lt;em>Optimize&lt;/em> phase has a bounded repertoire of levers. Ordered
by typical return:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>Typical saving&lt;/th>
&lt;th>Mechanism&lt;/th>
&lt;th>Cost/risk&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Recover idle&lt;/strong>&lt;/td>
&lt;td>the largest&lt;/td>
&lt;td>switch off/share the idle GPU&lt;/td>
&lt;td>requires measuring utilisation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Quantisation (FP8/INT4)&lt;/strong>&lt;/td>
&lt;td>raises throughput, lowers VRAM&lt;/td>
&lt;td>more tokens per GPU-hour&lt;/td>
&lt;td>possible quality cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Reserved commitment&lt;/strong>&lt;/td>
&lt;td>&lt;strong>20–40 %&lt;/strong> over on-demand&lt;/td>
&lt;td>reserve capacity&lt;/td>
&lt;td>less elasticity&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Spot/preemptible&lt;/strong>&lt;/td>
&lt;td>the deepest discount&lt;/td>
&lt;td>interruptible capacity&lt;/td>
&lt;td>you have to tolerate cuts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Rightsizing&lt;/strong>&lt;/td>
&lt;td>variable&lt;/td>
&lt;td>match GPU type/count to the SLO&lt;/td>
&lt;td>requires benchmarks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Autoscaling (HPA/KEDA)&lt;/strong>&lt;/td>
&lt;td>variable&lt;/td>
&lt;td>scale replicas with demand&lt;/td>
&lt;td>tuning metrics&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On commitment, the 2026 cloud data is conclusive: reserved plans give 20–40 % saving
against on-demand, and spot the deepest discount in exchange for interruptibility; the price of
the H100 fell 64–75 % between Q4 2024 and the start of 2026
(&lt;a href="https://www.spheron.network/blog/gpu-cloud-pricing-comparison-2026/">Spheron · GPU Cloud Pricing&lt;/a>).
Rightsizing and autoscaling connect with cluster scheduling: fitting the workload onto the
right GPU and scaling replicas with demand are, at the same time, performance and cost levers.
Quantisation appears here because it raises throughput, and, by the identity from the opening
article, lowers cost and energy per token at the same time.&lt;/p>
&lt;hr>
&lt;h2 id="gpu-finops-maturity">GPU FinOps maturity&lt;/h2>
&lt;p>A simple model to place where an organisation is, and what it is missing:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Level&lt;/th>
&lt;th>State&lt;/th>
&lt;th>What is missing to move up&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0 · blind&lt;/td>
&lt;td>aggregate invoice, no attribution&lt;/td>
&lt;td>instrument OpenCost + DCGM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 · visibility&lt;/td>
&lt;td>cost per namespace/team&lt;/td>
&lt;td>per-token measurement (gateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2 · unit economics&lt;/td>
&lt;td>cost per token and per product&lt;/td>
&lt;td>idle alerts and budgets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3 · optimisation&lt;/td>
&lt;td>idle recovered, commitment, rightsizing&lt;/td>
&lt;td>automated chargeback&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4 · governance&lt;/td>
&lt;td>chargeback + budgets + FOCUS&lt;/td>
&lt;td>continuous improvement&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Most organisations with GPUs are at level 0 or 1: they see a big invoice but do not know whose
each euro is. The value jump is in reaching level 2, cost per token and per product, which is
exactly where the tooling in this article stops being optional. The series&amp;rsquo; FinOps track walks that
ladder up to the full TCO model and the comparable cost/token that underpins the architecture
proposal.&lt;/p>
&lt;hr>
&lt;h2 id="the-minimum-stack-to-reach-level-2">The minimum stack to reach level 2&lt;/h2>
&lt;p>Bringing the previous pieces together into a concrete toolchain, all open source, that takes you
from blindness (level 0) to cost per token and per product (level 2):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Function&lt;/th>
&lt;th>Alternative&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>NVIDIA GPU Operator + DCGM&lt;/strong>&lt;/td>
&lt;td>exports GPU metrics (usage, memory, power)&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Prometheus&lt;/strong>&lt;/td>
&lt;td>stores the usage and cost series&lt;/td>
&lt;td>VictoriaMetrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenCost&lt;/strong>&lt;/td>
&lt;td>allocates cost per resource and dimension&lt;/td>
&lt;td>Kubecost (commercial)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Gateway (LiteLLM)&lt;/strong>&lt;/td>
&lt;td>counts tokens per team/model&lt;/td>
&lt;td>OpenLM AI Gateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Grafana&lt;/strong>&lt;/td>
&lt;td>cost, utilisation and idle panels&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>It is, deliberately, infrastructure many clusters already have for observability (DCGM,
Prometheus, Grafana); GPU FinOps reuses that base and adds OpenCost and the gateway to it. It is
not a new product, it is a layer on top of what exists.&lt;/p>
&lt;h3 id="kpis-to-watch">KPIs to watch&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>KPI&lt;/th>
&lt;th>What it indicates&lt;/th>
&lt;th>Typical target&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Average GPU utilisation&lt;/td>
&lt;td>waste&lt;/td>
&lt;td>&amp;gt;70–80 % sustained&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per 1M tokens (per model)&lt;/td>
&lt;td>economic efficiency&lt;/td>
&lt;td>compare against cloud rental&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>% of GPU-hours idle&lt;/td>
&lt;td>money thrown away&lt;/td>
&lt;td>minimise (&amp;lt;10–15 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per team/product&lt;/td>
&lt;td>attribution&lt;/td>
&lt;td>fair split, no surprises&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Deviation against budget&lt;/td>
&lt;td>governance&lt;/td>
&lt;td>alert before it is exceeded&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>These five KPIs are what turn GPU FinOps from a pretty dashboard into a decision tool: if you watch
them, you know at all times what each thing costs and where the waste is.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-2026">State of the art 2026&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Consolidation in Kubernetes&lt;/strong>: OpenCost is the CNCF allocation standard; Kubecost
(IBM) the reference commercial product, already with GPU via DCGM and rightsizing through
Turbonomic.&lt;/li>
&lt;li>&lt;strong>From resource to token&lt;/strong>: the strong platforms of 2026 track cost at token and GPU level,
combining resource allocation (OpenCost) with gateway measurement (LiteLLM); it is the only route
to a comparable cost/token for on-prem against cloud.&lt;/li>
&lt;li>&lt;strong>FOCUS v1.3&lt;/strong> (Dec 2025) as the interoperability layer, extending to AI at
FinOps X 2026: the cloud cost standard absorbs token economics.&lt;/li>
&lt;li>&lt;strong>Mature GPU FinOps&lt;/strong>: instrumentation leans on DCGM (the same base as observability), and idle
detection by a utilisation threshold is common practice.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="cost-per-token-the-bridge-to-the-business-decision">Cost per token: the bridge to the business decision&lt;/h2>
&lt;p>All the previous tooling exists to produce a number the business understands: cost per token
(or per request). It is the one that lets you answer the three questions that underpin an
architecture proposal:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Build or buy?&lt;/strong> On-prem cost/token (depreciated capex + opex, at real utilisation) against the
price of an external API or of cloud rental. Below the volume threshold (~2M tokens/day) buying
usually wins; above it, building.&lt;/li>
&lt;li>&lt;strong>How do you price a product?&lt;/strong> If a feature consumes N tokens per use and each million costs C,
the marginal cost per use is N·C/10⁶, the basis of any margin.&lt;/li>
&lt;li>&lt;strong>Where is the waste?&lt;/strong> Cost/token per team (the chargeback example) points at whoever underuses
the GPU before the aggregate invoice hides it.&lt;/li>
&lt;/ol>
&lt;p>The trap: comparing cost/token across scenarios without fixing the assumptions (utilisation,
precision, ownership model). An on-prem cost/token calculated at 80 % utilisation is not comparable
with one at 20 %, nor an FP16 with an FP8. That is why allocation (Inform) is not an end in itself:
it is the raw material of a cost model with explicit assumptions, which is what the synthesis
article turns into the &amp;ldquo;build vs buy&amp;rdquo; argument with defensible numbers. GPU FinOps is not
accounting; it is the quantitative basis of the architecture decision.&lt;/p>
&lt;hr>
&lt;h2 id="limits-and-traps-data-driven">Limits and traps (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Allocation ≠ per-token measurement.&lt;/strong> OpenCost splits the resource cost; without a gateway you
do not reach the request. They are two halves that have to be joined explicitly.&lt;/li>
&lt;li>&lt;strong>Badly set base prices = badly set allocation.&lt;/strong> On-prem, OpenCost splits the cost &lt;strong>you
declare&lt;/strong> for the node; if the depreciated capex/opex is wrong, the whole split is wrong. Input
data quality rules.&lt;/li>
&lt;li>&lt;strong>Invisible idle.&lt;/strong> Without DCGM exported to Prometheus and without utilisation alerts, waste
number one does not appear on any dashboard.&lt;/li>
&lt;li>&lt;strong>Cost of the tool.&lt;/strong> A 1–3 % &lt;em>fixed-fee&lt;/em> on large spend is real money; compare the tooling cost
with the saving it delivers (it is FinOps on top of FinOps).&lt;/li>
&lt;li>&lt;strong>Proprietary formats.&lt;/strong> Tools that do not emit/consume FOCUS tie you to their data model; in
2026 the robust bet is FOCUS interoperability.&lt;/li>
&lt;/ol>
&lt;p>The next article in the series (A2) goes into OpenCost in depth; this one fixes the map. With
allocation settled, the rest of the FinOps track builds the TCO model and the cost/token the
proposal needs.&lt;/p>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>OpenCost · GitHub (CNCF, Apache 2.0) — &lt;a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost&lt;/a>&lt;/li>
&lt;li>OpenCost · documentación on-prem (modelo de precio por nodo) — &lt;a href="https://opencost.io/docs/configuration/on-prem/">https://opencost.io/docs/configuration/on-prem/&lt;/a>&lt;/li>
&lt;li>OpenCost · exporter de Prometheus — &lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">https://opencost.io/docs/integrations/opencost-exporter/&lt;/a>&lt;/li>
&lt;li>CloudZero · Kubecost vs OpenCost (2026) — &lt;a href="https://www.cloudzero.com/blog/kubecost-vs-opencost/">https://www.cloudzero.com/blog/kubecost-vs-opencost/&lt;/a>&lt;/li>
&lt;li>CloudZero · FinOps Tools: Definitive Guide (2026) — &lt;a href="https://www.cloudzero.com/blog/finops-tools/">https://www.cloudzero.com/blog/finops-tools/&lt;/a>&lt;/li>
&lt;li>FOCUS · especificación (FinOps Foundation) — &lt;a href="https://focus.finops.org/focus-specification/">https://focus.finops.org/focus-specification/&lt;/a>&lt;/li>
&lt;li>SiliconANGLE · FOCUS y la economía de tokens de IA (FinOps X 2026) — &lt;a href="https://siliconangle.com/2026/06/08/focus-specification-ai-cost-accountability-finopsx/">https://siliconangle.com/2026/06/08/focus-specification-ai-cost-accountability-finopsx/&lt;/a>&lt;/li>
&lt;li>OpenLM · atribución de tokens en tiempo real (LiteLLM + FOCUS) — &lt;a href="https://www.openlm.com/enable-ai-finops-with-real-time-token-attribution/">https://www.openlm.com/enable-ai-finops-with-real-time-token-attribution/&lt;/a>&lt;/li>
&lt;li>Finout · Best AI Cost Observability Tools (2026) — &lt;a href="https://www.finout.io/blog/best-ai-cost-observability-tools-in-2026">https://www.finout.io/blog/best-ai-cost-observability-tools-in-2026&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Energy benchmarking in AI: frameworks, metrics and state of the art (tool by tool)</title><link>https://blog.lo0.es/en/posts/ai-energy-benchmarking-frameworks-metrics-state-of-the-art/</link><pubDate>Sat, 13 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/ai-energy-benchmarking-frameworks-metrics-state-of-the-art/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The energy and carbon
references are &lt;strong>European&lt;/strong> (France, Germany, Spain), since this is a sovereign
proposal. The dollar sign is not used (on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-introduction-covers">What this introduction covers&lt;/h2>
&lt;p>Fourth article in the data series and &lt;em>deep dive&lt;/em> on the energy axis. Measuring the watts
of an AI workload looks like a sustainability detail, but it is at once a cost lever
(electricity is 30–50 % of TCO), an axis of sovereignty (carbon per token depends on
the country) and, increasingly, a regulatory obligation (emissions reporting). This
article inventories the metrics, where each figure physically comes from, the data sheet
for each measurement framework, and how to go from watt to carbon with European figures. No
recommendations; only data and methodology, because in energy, even more than in
performance, the measurement method changes the result.&lt;/p>
&lt;hr>
&lt;h2 id="why-measuring-energy-matters-with-numbers">Why measuring energy matters (with numbers)&lt;/h2>
&lt;p>Three quantified reasons, connecting with the &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">opening article&lt;/a>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Reason&lt;/th>
&lt;th>Figure&lt;/th>
&lt;th>Implication&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Cost&lt;/strong>&lt;/td>
&lt;td>electricity = 30–50 % of TCO&lt;/td>
&lt;td>measuring energy is measuring almost half the cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Sovereignty/carbon&lt;/strong>&lt;/td>
&lt;td>France ~9× fewer gCO₂/token than Germany&lt;/td>
&lt;td>the country of the cluster is a lever&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scale/regulation&lt;/strong>&lt;/td>
&lt;td>datacenters ~460 TWh in 2025 (≈1.8 % worldwide), &amp;gt;800 TWh projected for 2028&lt;/td>
&lt;td>emissions reporting (CSRD) mandatory&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>At 700 W per H100, an 8-card node draws of the order of 5.6 kW from the boards alone, ~49,000
kWh a year. Applying PUE 1.4 that becomes ~68,700 kWh/year, and there the country decides the
bill: at the French wholesale price (~0.058 €/kWh) that is ~3,980 €/year of electricity per
node; in Germany (~0.091 €/kWh), ~6,250 €/year, 57 % more for the same iron and the same
work. Multiplied across a fleet, it is a first-order cost line, and a carbon footprint that,
in Europe, has to be measured and reported. Energy stopped being an engineering detail and
became a business and compliance variable.&lt;/p>
&lt;hr>
&lt;h2 id="the-energy-metrics">The energy metrics&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Unit&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Mean power&lt;/td>
&lt;td>average instantaneous consumption&lt;/td>
&lt;td>W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Energy per token&lt;/strong>&lt;/td>
&lt;td>energy consumed per generated token&lt;/td>
&lt;td>J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per inference&lt;/td>
&lt;td>energy per request&lt;/td>
&lt;td>µJ – Wh&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Total energy&lt;/td>
&lt;td>integral of power over time&lt;/td>
&lt;td>Wh, kWh&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PUE&lt;/strong> (Power Usage Effectiveness)&lt;/td>
&lt;td>datacenter overhead (cooling, losses)&lt;/td>
&lt;td>ratio ≥ 1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Carbon&lt;/td>
&lt;td>energy × grid intensity&lt;/td>
&lt;td>gCO₂eq&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The two base identities of the whole axis:&lt;/p>
$$\text{energy per token (J)} = \frac{\text{mean power (W)}}{\text{throughput (tok/s)}}$$
$$\text{carbon} = \text{energy (kWh)} \times \text{PUE} \times \text{grid intensity (gCO}_2\text{/kWh)}$$
&lt;p>The first has the same shape as cost per token: it shares the throughput denominator, so
raising tokens/s lowers energy per token. The second brings in the two multipliers
external to the compute, the datacenter PUE and the country&amp;rsquo;s grid intensity, which
can change carbon per token by an order of magnitude without touching the stack.&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-energy-per-token-of-an-8h100-node">Worked example: energy per token of an 8×H100 node&lt;/h2>
&lt;p>To anchor the identities with numbers, the end-to-end calculation on the example node
(8×H100, Llama 3.1 70B FP16, vLLM at 2,800 tok/s):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Step&lt;/th>
&lt;th>Calculation&lt;/th>
&lt;th>Result&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Board power&lt;/td>
&lt;td>8 × 700 W&lt;/td>
&lt;td>5,600 W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per token (boards)&lt;/td>
&lt;td>5,600 ÷ 2,800&lt;/td>
&lt;td>2.0 J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per token (with PUE 1.4)&lt;/td>
&lt;td>2.0 × 1.4&lt;/td>
&lt;td>2.8 J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per 1M tokens (boards)&lt;/td>
&lt;td>2 × 10⁶ J&lt;/td>
&lt;td>0.56 kWh&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per 1M tokens (with PUE)&lt;/td>
&lt;td>0.56 × 1.4&lt;/td>
&lt;td>0.78 kWh&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>From there come the electricity cost and the carbon, by country:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Country&lt;/th>
&lt;th>Electricity cost / 1M tok&lt;/th>
&lt;th>Carbon / 1M tok&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>France (0.058 €/kWh; ~40 gCO₂/kWh)&lt;/td>
&lt;td>~0.045 €&lt;/td>
&lt;td>~31 gCO₂&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Spain (0.054 €/kWh; ~160 gCO₂/kWh)&lt;/td>
&lt;td>~0.042 €&lt;/td>
&lt;td>~125 gCO₂&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Germany (0.091 €/kWh; ~363 gCO₂/kWh)&lt;/td>
&lt;td>~0.071 €&lt;/td>
&lt;td>~283 gCO₂&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And the link with performance: if an optimisation (FP8, say) raised throughput to
4,200 tok/s, energy per token would drop to ~1.33 J/token (boards) and everything below it in
the same proportion. Electricity cost and carbon per token fall with throughput, by the
identity. Note: these are board + PUE figures; the complete node (CPU, NICs, PSUs)
consumes somewhat more, and energy measured with a power meter can differ from energy
estimated with &lt;code>nvidia-smi&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="where-the-figure-comes-from-the-measurement-layers">Where the figure comes from: the measurement layers&lt;/h2>
&lt;p>Not every energy figure is equally reliable. The figure can come from four layers, with
increasing accuracy and decreasing context:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 240" role="img" aria-label="Layers the energy figure comes from: GPU counters NVML/DCGM, CPU RAPL, eBPF with models, and a physical power meter as the reference" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;rect class="bx" x="20" y="36" width="360" height="42" rx="6"/>
&lt;text x="32" y="55" class="tl">GPU — NVML / nvidia-smi / DCGM&lt;/text>
&lt;text x="32" y="71" class="ts">card power from a hardware counter&lt;/text>
&lt;rect class="bx" x="20" y="90" width="360" height="42" rx="6"/>
&lt;text x="32" y="109" class="tl">CPU/DRAM — RAPL&lt;/text>
&lt;text x="32" y="125" class="ts">Intel/AMD counters via /proc, perf&lt;/text>
&lt;rect class="bx" x="20" y="144" width="360" height="42" rx="6"/>
&lt;text x="32" y="163" class="tl">Per-process/pod attribution — eBPF + model&lt;/text>
&lt;text x="32" y="179" class="ts">splits the watt between workloads (Kepler)&lt;/text>
&lt;rect class="bx" x="410" y="90" width="350" height="42" rx="6"/>
&lt;text x="422" y="109" class="tl">Physical power meter (reference)&lt;/text>
&lt;text x="422" y="125" class="ts">the ground truth everything is calibrated against&lt;/text>
&lt;text x="410" y="165" class="ts">The higher the layer, the more context (carbon) and&lt;/text>
&lt;text x="410" y="181" class="ts">the less direct accuracy; the meter is the standard.&lt;/text>
&lt;text x="20" y="216" class="ts">The GPU dominates in LLM inference; RAPL covers CPU/DRAM; eBPF splits per pod; the meter calibrates.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The GPU is the dominant source in LLM inference, and its power is read from a hardware
counter (NVML/nvidia-smi, aggregated by DCGM, the same basis as
&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability&lt;/a>). RAPL gives CPU and
DRAM. The eBPF + model layer (Kepler) splits the total watt between containers and pods
without instrumenting the application. And the physical power meter is the ground truth
against which estimates are calibrated: no software estimate is better than the meter, only
more convenient.&lt;/p>
&lt;hr>
&lt;h2 id="how-it-is-instrumented-and-where-the-error-creeps-in">How it is instrumented (and where the error creeps in)&lt;/h2>
&lt;p>Four instrumentation decisions explain most of the discrepancies between energy
measurements:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Sampling rate.&lt;/strong> &lt;code>nvidia-smi&lt;/code>/DCGM read power at intervals; sampling that is too coarse
misses the peaks and underestimates the energy. It must be fine enough to capture the
power variation between prefill (high) and decode (lower).&lt;/li>
&lt;li>&lt;strong>Board vs node.&lt;/strong> GPU (board) power does not include CPU, NICs, fans or PSU losses
(typically an extra 10–20 %). Comparing a board energy figure with a complete-node one is
comparing different things.&lt;/li>
&lt;li>&lt;strong>Idle baseline.&lt;/strong> A powered GPU with no work already draws power. Is that idle attributed
to the workload or discounted? The decision changes energy per token, and idle matters: it
is real energy that someone pays for.&lt;/li>
&lt;li>&lt;strong>Time window.&lt;/strong> Energy is the integral of power over time; the measurement window has
to line up exactly with the workload (without counting warm-up or shutdown), or the number
does not correspond to the work measured.&lt;/li>
&lt;/ul>
&lt;p>These decisions, as in performance benchmarking, mean that &lt;strong>the measurement method matters
as much as the system measured&lt;/strong>. A J/token without a stated sampling rate, board/node
boundary and idle treatment is not comparable.&lt;/p>
&lt;hr>
&lt;h2 id="frameworks-tool-by-tool">Frameworks, tool by tool&lt;/h2>
&lt;h3 id="zeus-mlenergy-university-of-michigan">Zeus (ml.energy, University of Michigan)&lt;/h3>
&lt;p>What it does: measure and optimise the energy of deep learning workloads. Method: NVML
counters. Scope: NVIDIA and AMD GPUs, CPU, DRAM, Apple Silicon and NVIDIA Jetson. Licence:
OSS; academic backing (NSDI'23 paper). Differentiator: it does not only measure, it also
optimises (power capping, frequency selection) to cut energy without significant loss of
performance (&lt;a href="https://ml.energy/zeus/">Zeus Project&lt;/a>, &lt;a href="https://www.usenix.org/system/files/nsdi23-you.pdf">NSDI'23&lt;/a>).
It is the reference tool when the goal is to lower J/token, not just to measure them.&lt;/p>
&lt;h3 id="codecarbon">CodeCarbon&lt;/h3>
&lt;p>What it does: estimates the energy and the CO₂eq of compute processes. Method: reads the
GPU with &lt;code>nvidia-smi&lt;/code> and estimates CPU/RAM. Scope: CPU + GPU + RAM. Licence: OSS (Python).
Differentiator: it integrates into ML flows and CI pipelines, and it is the one that &lt;strong>comes
closest to the power meter&lt;/strong> among the Python tools (&lt;a href="https://medium.com/@prhmma/best-green-code-tools-july-2025-mainly-python-7ab415af0cd6">comparison&lt;/a>).
It brings carbon context out of the box. Limit: a high-level estimate, less exact than direct
hardware measurement.&lt;/p>
&lt;h3 id="carbontracker">CarbonTracker&lt;/h3>
&lt;p>What it does: tracks energy and carbon at GPU run time. Scope: GPU only, it measures
neither CPU nor memory. Licence: OSS. Differentiator: lightweight, easy to add to a
training/inference script. Limit: partial coverage (no CPU/DRAM), so it undercounts the total
system energy.&lt;/p>
&lt;h3 id="scaphandre">Scaphandre&lt;/h3>
&lt;p>What it does: a power metrology agent written in Rust. Method: RAPL (reads &lt;code>/proc&lt;/code>,
splits joules by CPU ticks). Scope: process or VM, with the &lt;strong>raw accuracy of the hardware
layer&lt;/strong>. Licence: OSS. Differentiator: the highest direct accuracy among the general-purpose
tools, at the cost of bringing no carbon context (it is pure metrology)
(&lt;a href="https://github.com/hubblo-org/scaphandre">Scaphandre · GitHub&lt;/a>). Limit: focused on CPU/RAPL;
for GPU it has to be combined with NVML.&lt;/p>
&lt;h3 id="kepler-cncf">Kepler (CNCF)&lt;/h3>
&lt;p>What it does: a Prometheus exporter that measures energy at container, pod and node level
in Kubernetes. Method: eBPF + ML models to estimate consumption per workload. Language:
Go + C. Licence: Apache 2.0 (the &lt;code>sustainable-computing-io&lt;/code> project) (&lt;a href="https://github.com/sustainable-computing-io/kepler">Kepler · GitHub&lt;/a>).
Differentiator: it is the cloud-native option for attributing energy per pod without
instrumenting the app, just as OpenCost attributes cost. Limit: attribution via eBPF + model
is estimation, not direct measurement; its accuracy depends on the model.&lt;/p>
&lt;h3 id="mlperf-power-mlcommons">MLPerf Power (MLCommons)&lt;/h3>
&lt;p>What it does: a standardised benchmark of energy efficiency, from µW to MW.
Maintainer: MLCommons. Differentiator: cross-vendor comparability of efficiency, with a
dataset spanning several years and workload versions (&lt;a href="https://arxiv.org/html/2410.12032v2">MLPerf Power, arXiv 2410.12032&lt;/a>).
It is to energy what MLPerf Inference is to performance: the comparison standard between
vendors.&lt;/p>
&lt;h3 id="comparison-table">Comparison table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Method&lt;/th>
&lt;th>Scope&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>Accuracy / note&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Zeus&lt;/strong>&lt;/td>
&lt;td>NVML / counters&lt;/td>
&lt;td>NV+AMD GPU, CPU, DRAM, Apple, Jetson&lt;/td>
&lt;td>OSS&lt;/td>
&lt;td>measures &lt;strong>and optimises&lt;/strong> DL energy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CodeCarbon&lt;/strong>&lt;/td>
&lt;td>nvidia-smi + estimation&lt;/td>
&lt;td>CPU+GPU+RAM, CO₂eq&lt;/td>
&lt;td>OSS (Python)&lt;/td>
&lt;td>the &lt;strong>closest to the power meter&lt;/strong> among Python tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CarbonTracker&lt;/strong>&lt;/td>
&lt;td>GPU runtime&lt;/td>
&lt;td>&lt;strong>GPU only&lt;/strong>&lt;/td>
&lt;td>OSS&lt;/td>
&lt;td>lightweight; no CPU/mem&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scaphandre&lt;/strong>&lt;/td>
&lt;td>RAPL (/proc)&lt;/td>
&lt;td>process/VM&lt;/td>
&lt;td>OSS (Rust)&lt;/td>
&lt;td>raw accuracy; no carbon context&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kepler&lt;/strong>&lt;/td>
&lt;td>eBPF + model&lt;/td>
&lt;td>container/pod/node (K8s)&lt;/td>
&lt;td>Apache 2.0 (CNCF)&lt;/td>
&lt;td>cloud-native; model-based estimation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLPerf Power&lt;/strong>&lt;/td>
&lt;td>standardised measurement&lt;/td>
&lt;td>µW to MW&lt;/td>
&lt;td>MLCommons&lt;/td>
&lt;td>cross-vendor comparability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="accuracy-who-to-believe">Accuracy: who to believe&lt;/h2>
&lt;p>The difference between tools is not a nuance: Scaphandre offers the raw accuracy of the
hardware layer (RAPL), while the Python ones such as CodeCarbon give higher-level,
carbon-aware estimates, with more context but less direct exactness; among the Python tools,
CodeCarbon is the one that comes closest to the power meter, followed by CarbonTracker, with
variability across infrastructures (&lt;a href="https://medium.com/@prhmma/best-green-code-tools-july-2025-mainly-python-7ab415af0cd6">comparison&lt;/a>).
The rule: for sizing and design, a software estimate will do; to &lt;strong>defend a figure before
a committee or an auditor, calibrate against a power meter&lt;/strong> at least once, and use the
software tool as a continuous proxy. The measured figure beats the estimated one; the rest is
convenience.&lt;/p>
&lt;hr>
&lt;h2 id="energy-leaderboards-and-benchmarks">Energy leaderboards and benchmarks&lt;/h2>
&lt;p>To compare efficiency between models without building your own test bench:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Resource&lt;/th>
&lt;th>What it provides&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>MLPerf Power&lt;/strong>&lt;/td>
&lt;td>standardised efficiency measurement, from µW to MW&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HF AI Energy Score&lt;/strong>&lt;/td>
&lt;td>comparable per-model energy efficiency ratings (&lt;a href="https://huggingface.github.io/AIEnergyScore/">HF&lt;/a>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ML.ENERGY Benchmark&lt;/strong>&lt;/td>
&lt;td>automated measurement and optimisation of inference energy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>awesome-green-ai&lt;/strong>&lt;/td>
&lt;td>curated index of Green AI resources and tools (&lt;a href="https://github.com/samuelrince/awesome-green-ai">repo&lt;/a>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>These resources give the order of magnitude of the energy per token of each model, useful for
choosing a model on efficiency at the design stage. But they do not replace measurement
on your hardware and your workload: a leaderboard measures on one specific bench, not on your
node.&lt;/p>
&lt;hr>
&lt;h2 id="from-watt-to-carbon-france-germany-spain">From watt to carbon: France, Germany, Spain&lt;/h2>
&lt;p>Here the energy axis turns into a sovereignty argument. Carbon per token comes from
multiplying the energy by the country&amp;rsquo;s grid intensity, which in Europe varies by almost
two orders of magnitude:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Country&lt;/th>
&lt;th>Wholesale (€/MWh, Mar 2026)&lt;/th>
&lt;th>Grid carbon (gCO₂/kWh)&lt;/th>
&lt;th>Profile&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>France&lt;/strong>&lt;/td>
&lt;td>~58&lt;/td>
&lt;td>&lt;strong>~20–60&lt;/strong>&lt;/td>
&lt;td>nuclear: cheap and very clean&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Spain&lt;/strong>&lt;/td>
&lt;td>~54&lt;/td>
&lt;td>&lt;strong>~150–170&lt;/strong>&lt;/td>
&lt;td>renewables + gas: cheap, medium carbon&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Germany&lt;/strong>&lt;/td>
&lt;td>~91&lt;/td>
&lt;td>&lt;strong>~363&lt;/strong> (2024 average)&lt;/td>
&lt;td>coal/gas + renewables: expensive and dirty&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Sources: wholesale prices (&lt;a href="https://tradingeconomics.com/france/electricity-price">TradingEconomics&lt;/a>),
Germany&amp;rsquo;s carbon ~363 gCO₂/kWh (2024 average, &lt;a href="https://en.wikipedia.org/wiki/Electricity_sector_in_Germany">Wikipedia&lt;/a>),
nuclear ~17–35 gCO₂e/kWh (&lt;a href="https://www.eea.europa.eu/en/analysis/indicators/greenhouse-gas-emission-intensity-of-1">EEA&lt;/a>),
Spain ~167 gCO₂/kWh (2020). Hourly intensity comes from ElectricityMaps.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 230" role="img" aria-label="From watt to carbon by European country: the same energy per token multiplied by the grid intensity of France, Spain or Germany gives very different carbon per token" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#em)}&lt;/style>
&lt;defs>&lt;marker id="em" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="90" width="190" height="50" rx="6"/>
&lt;text x="32" y="111" class="tl">0.78 kWh / 1M tokens&lt;/text>
&lt;text x="32" y="128" class="ts">(energy with PUE)&lt;/text>
&lt;path class="ar" d="M210,100 L300,55"/>
&lt;path class="ar" d="M210,115 L300,115"/>
&lt;path class="ar" d="M210,130 L300,175"/>
&lt;rect class="bx" x="300" y="36" width="200" height="40" rx="6"/>
&lt;text x="312" y="56" class="tl">France (~40 gCO₂/kWh)&lt;/text>
&lt;text x="312" y="70" class="ts">→ ~31 gCO₂ / 1M tokens&lt;/text>
&lt;rect class="bx" x="300" y="95" width="200" height="40" rx="6"/>
&lt;text x="312" y="115" class="tl">Spain (~160 gCO₂/kWh)&lt;/text>
&lt;text x="312" y="129" class="ts">→ ~125 gCO₂ / 1M tokens&lt;/text>
&lt;rect class="bx" x="300" y="154" width="200" height="40" rx="6"/>
&lt;text x="312" y="174" class="tl">Germany (~363 gCO₂/kWh)&lt;/text>
&lt;text x="312" y="188" class="ts">→ ~283 gCO₂ / 1M tokens&lt;/text>
&lt;text x="540" y="110" class="ts">Same load, same iron:&lt;/text>
&lt;text x="540" y="128" class="ts">~9× more carbon in Germany&lt;/text>
&lt;text x="540" y="146" class="ts">than in France, just from the grid.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The calculation, on the example node (0.78 kWh per million tokens, with PUE): in France
(~40 gCO₂/kWh) it is ~31 gCO₂ per million tokens; in Germany (~363 gCO₂/kWh), &lt;strong>~283
gCO₂, a factor of ~9×, without touching a line of the stack. Placing the cluster in
France or Spain is at once a cost lever, a carbon lever and a compliance lever&lt;/strong> (&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act&lt;/a>,
&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">controls ENS × 42001 × EU AI Act&lt;/a>).&lt;/p>
&lt;hr>
&lt;h2 id="energy-efficiency-levers">Energy efficiency levers&lt;/h2>
&lt;p>Once it is measured, it can be brought down. The levers, ordered by the identity J/token = W ÷
throughput (lower the numerator or raise the denominator):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>Mechanism&lt;/th>
&lt;th>Effect&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Quantisation (FP8/INT4)&lt;/strong>&lt;/td>
&lt;td>more tokens per GPU-hour, less VRAM/KV&lt;/td>
&lt;td>raises throughput → lowers J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Batching / continuous batching&lt;/strong>&lt;/td>
&lt;td>amortises the fixed power cost&lt;/td>
&lt;td>raises throughput → lowers J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Power capping&lt;/strong> (Zeus)&lt;/td>
&lt;td>limits GPU power&lt;/td>
&lt;td>lowers W with little loss of throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Efficiency-aware scheduling&lt;/strong>&lt;/td>
&lt;td>consolidate workloads, power down idle GPUs&lt;/td>
&lt;td>eliminates idle consumption&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Location (country)&lt;/strong>&lt;/td>
&lt;td>cleaner grid&lt;/td>
&lt;td>lowers carbon per token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Avoiding over-reasoning&lt;/strong>&lt;/td>
&lt;td>fewer reasoning tokens that add nothing&lt;/td>
&lt;td>less compute → less energy&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Quantisation (see &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">quantisation for inference&lt;/a>)
is the highest-return lever because it moves all three axes at once: it raises throughput,
lowers cost and lowers energy per token. Zeus&amp;rsquo;s power capping is energy-specific: it
trims peak power with little performance loss, lowering the W in the numerator. And idle,
the GPU powered but not working, is pure wasted energy that scheduling recovers.&lt;/p>
&lt;hr>
&lt;h2 id="from-design-to-production-where-energy-is-measured">From design to production: where energy is measured&lt;/h2>
&lt;p>As with the other axes, energy is measured at three moments, with different tools and
increasing accuracy:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Moment&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>What it gives&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Design&lt;/strong>&lt;/td>
&lt;td>TDP × hours (datasheet)&lt;/td>
&lt;td>ceiling estimate for sizing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Benchmark&lt;/strong>&lt;/td>
&lt;td>Zeus, MLPerf Power, power meter&lt;/td>
&lt;td>energy per token measured on a controlled bench&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Production&lt;/strong>&lt;/td>
&lt;td>Kepler (eBPF), DCGM&lt;/td>
&lt;td>continuous per-pod consumption in the cluster&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The design figure (TDP) serves to size the utility feed and the cooling, but it
overestimates (the GPU is rarely at TDP 100 % of the time). The benchmark figure is the
one used for the proposal&amp;rsquo;s cost and carbon per token, because it is measured and
reproducible. And the production figure (Kepler/DCGM) closes the loop: it checks that real
consumption matches the budget and feeds the footprint report. The three have to be traceable
to each other, which is what the harness in article S4 does.&lt;/p>
&lt;hr>
&lt;h2 id="energy-in-tco-and-european-regulation">Energy in TCO and European regulation&lt;/h2>
&lt;p>The closing of the axis: energy is not only sustainability, it is cost and compliance.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cost&lt;/strong>: being 30–50 % of TCO, every J/token improvement translates directly into euros. In
the example, moving from Germany (~0.091 €/kWh) to France (~0.058 €/kWh) lowers the
electricity share by ~37 % on top of the carbon.&lt;/li>
&lt;li>&lt;strong>Regulation (CSRD)&lt;/strong>: European companies must report their carbon footprint (scope 2).
A low-carbon grid (France) directly improves that reported figure, and measuring energy per
token stops being optional and becomes a reporting requirement.&lt;/li>
&lt;li>&lt;strong>EU AI Act&lt;/strong>: efficiency and energy consumption form part of the documentation expected of
AI systems, which makes energy measurement part of the technical file.&lt;/li>
&lt;/ul>
&lt;p>One reporting nuance worth knowing: the electricity footprint is scope 2 (indirect
emissions from purchased energy), and it can be accounted for by location (the real
intensity of the country&amp;rsquo;s grid) or by market (according to the guarantees of origin
contracted). Location-based accounting is the one that genuinely rewards putting the cluster
on a clean grid such as the French one; market-based accounting allows green energy to be
&amp;ldquo;bought&amp;rdquo; through certificates. Knowing which one your CSRD report requires changes which lever
(physical location vs contract) lowers the figure, and both start from measuring energy
per token, which is what this axis provides.&lt;/p>
&lt;p>In other words: measuring energy covers at once a first-order cost line, the carbon and
sovereignty argument, and a regulatory obligation. For a sovereign European proposal, the
energy axis is not the &amp;ldquo;nice sustainability bit&amp;rdquo;: it is one of the three columns with a number
attached.&lt;/p>
&lt;hr>
&lt;h2 id="checklist-for-a-reproducible-energy-measurement">Checklist for a reproducible energy measurement&lt;/h2>
&lt;p>For an energy or carbon figure to be defensible before a committee or an auditor, it has to
come with its context. The minimum to fix and publish:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>What to fix&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Tool + version&lt;/td>
&lt;td>each layer (RAPL, NVML, eBPF) and tool measures differently&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sampling rate&lt;/td>
&lt;td>coarse sampling misses peaks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Board vs node boundary&lt;/td>
&lt;td>±10–20 % depending on whether CPU/PSUs are included&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PUE used&lt;/td>
&lt;td>it multiplies all the compute energy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Source and time of the grid intensity&lt;/td>
&lt;td>carbon varies ~9× by country and by hour&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Idle treatment&lt;/td>
&lt;td>attributed or discounted changes the J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model, precision and workload&lt;/td>
&lt;td>energy depends on what is served and how&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Calibration vs power meter&lt;/td>
&lt;td>at least once, to validate the estimate&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The rule: if you cannot hand over this table alongside the gCO₂/token figure, the figure is
not auditable. The reproducible harness of article S4 records all these parameters so that the
energy figure is as traceable as the cost and performance ones, and so that the reported
footprint (CSRD) holds up under audit.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-2026">State of the art 2026&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>LLM-specific resolution is missing&lt;/strong>: general-purpose tools measure well at machine or
process level, but they lack LLM-specific resolution (energy per token, per prefill/decode
phase), and the simulators lack emissions modelling, an open gap in the field.&lt;/li>
&lt;li>&lt;strong>eBPF as a trend&lt;/strong> (Kepler) for attributing energy per pod without instrumenting the app,
in parallel to how OpenCost attributes cost.&lt;/li>
&lt;li>&lt;strong>Standardisation&lt;/strong> of comparison via MLPerf Power and HF AI Energy Score.&lt;/li>
&lt;li>&lt;strong>Optimisation, not only measurement&lt;/strong> (Zeus): the field is moving from &amp;ldquo;how much do I spend&amp;rdquo;
to &amp;ldquo;how do I spend less without losing performance&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="limits-and-traps-data-driven">Limits and traps (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Estimation vs measurement.&lt;/strong> An &lt;code>nvidia-smi&lt;/code> figure is not a power meter. Calibrate
against hardware at least once before defending a number.&lt;/li>
&lt;li>&lt;strong>Partial coverage.&lt;/strong> CarbonTracker measures GPU only; Scaphandre, mostly CPU/RAPL. Add the
layers or you undercount the total energy.&lt;/li>
&lt;li>&lt;strong>Forgetting PUE.&lt;/strong> Board energy does not include cooling or losses; without PUE you
underestimate the real consumption and carbon.&lt;/li>
&lt;li>&lt;strong>Carbon without a country.&lt;/strong> A gCO₂/token without a stated grid (and hour) means nothing:
it varies ~9× between France and Germany.&lt;/li>
&lt;li>&lt;strong>Confusing energy with sustainability.&lt;/strong> Here it is cost and compliance, with a number;
treating it as a &amp;ldquo;green&amp;rdquo; extra is losing one of the three columns of the proposal.&lt;/li>
&lt;/ol>
&lt;p>That closes the run of introductions: cost, performance and energy, each with its frameworks
and its methodology. The rest of the series goes tool by tool through to the dashboard that
underpins the sovereign architecture decision, with European figures, measured and
reproducible.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>Energy is the axis easiest to dismiss as &amp;ldquo;the green one&amp;rdquo; and the one that hides the most: it
is almost half the cost, it defines the footprint Europe requires to be reported, and its
carbon per token changes ~9× depending on whether the cluster sits in France or in Germany.
Measuring it properly, with the right layer, the right sampling, PUE included and the
country&amp;rsquo;s grid, turns a soft sustainability argument into a hard column of the proposal, with
a number and a source. And the sovereign conclusion stands on its own with the data in this
article: &lt;strong>an inference platform in France or Spain is at once cheaper, cleaner and compliant
with European jurisdiction than the same platform on a US hyperscaler&lt;/strong>, and that is
demonstrated with measured J/token, gCO₂/kWh by country and euros, not with intentions. The
energy axis does not decorate the proposal: it closes it.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-energy-leaderboards-wh-per-token/">LLM energy efficiency leaderboards&lt;/a> — the J/token rankings where the numbers these tools measure land: which leaderboard to use, how to read it and what biases it carries.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/watt-to-carbon-pue-grid-intensity-gco2-per-token/">From watt to carbon: PUE, grid intensity and the real cost of a token&lt;/a> — how to convert the J/token measured with Zeus or Kepler into gCO₂eq using the datacenter PUE and the country&amp;rsquo;s electricity grid intensity.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/levers-fewer-watts-per-token-llm-inference/">Energy efficiency levers in LLM inference&lt;/a> — which configuration changes reduce the J/token these tools report: quantization, batching, engine, KV cache precision.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Zeus Project (ml.energy, UMich) — &lt;a href="https://ml.energy/zeus/">https://ml.energy/zeus/&lt;/a>&lt;/li>
&lt;li>Zeus · USENIX NSDI'23 — &lt;a href="https://www.usenix.org/system/files/nsdi23-you.pdf">https://www.usenix.org/system/files/nsdi23-you.pdf&lt;/a>&lt;/li>
&lt;li>Kepler (CNCF, eBPF) · GitHub — &lt;a href="https://github.com/sustainable-computing-io/kepler">https://github.com/sustainable-computing-io/kepler&lt;/a>&lt;/li>
&lt;li>Scaphandre · GitHub — &lt;a href="https://github.com/hubblo-org/scaphandre">https://github.com/hubblo-org/scaphandre&lt;/a>&lt;/li>
&lt;li>CarbonTracker · GitHub — &lt;a href="https://github.com/saintslab/carbontracker">https://github.com/saintslab/carbontracker&lt;/a>&lt;/li>
&lt;li>MLPerf Power (arXiv 2410.12032) — &lt;a href="https://arxiv.org/html/2410.12032v2">https://arxiv.org/html/2410.12032v2&lt;/a>&lt;/li>
&lt;li>HF AI Energy Score — &lt;a href="https://huggingface.github.io/AIEnergyScore/">https://huggingface.github.io/AIEnergyScore/&lt;/a>&lt;/li>
&lt;li>awesome-green-ai — &lt;a href="https://github.com/samuelrince/awesome-green-ai">https://github.com/samuelrince/awesome-green-ai&lt;/a>&lt;/li>
&lt;li>TradingEconomics · precio electricidad Francia/Alemania/España — &lt;a href="https://tradingeconomics.com/france/electricity-price">https://tradingeconomics.com/france/electricity-price&lt;/a>&lt;/li>
&lt;li>EEA · intensidad de emisiones de la electricidad en Europa — &lt;a href="https://www.eea.europa.eu/en/analysis/indicators/greenhouse-gas-emission-intensity-of-1">https://www.eea.europa.eu/en/analysis/indicators/greenhouse-gas-emission-intensity-of-1&lt;/a>&lt;/li>
&lt;li>Electricity sector in Germany (carbono ~363 gCO₂/kWh) — &lt;a href="https://en.wikipedia.org/wiki/Electricity_sector_in_Germany">https://en.wikipedia.org/wiki/Electricity_sector_in_Germany&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Anatomy of the twelve DCGM and five vLLM metrics: analogies, documented anomalies and real cases 2024-2026</title><link>https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/</link><pubDate>Tue, 02 Jun 2026 04:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/</guid><description>&lt;blockquote>
&lt;p>This post goes deeper into the list of metrics presented in &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a>. There each metric got its G/A/R threshold and PromQL query; here each one gets its explanatory analogy and the anomaly documented in production with a referenced public case. It is the post worth having open when an alert fires and you still do not know what to do with it; the &lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">next post on runbooks&lt;/a> translates each anomaly into concrete action.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The twelve DCGM metrics (compute, memory, thermal-power, health) and the five vLLM engine metrics (concurrency, KV pool, SLO latencies) covered in the previous post paint the cluster&amp;rsquo;s cockpit, but a list without context does not teach you to diagnose. Each metric has a &lt;strong>recurring anomalous pattern&lt;/strong> documented in the public literature (academic papers, GitHub issues, OEM KBs, operator blogs) that the veteran operator recognises instantly and the junior one does not. This post develops each metric with an &lt;strong>analogy of its own&lt;/strong> that pins down which question it answers, and with the &lt;strong>statistically relevant anomaly&lt;/strong> backed by figures from documented incidents. Three examples of the calibre involved: &lt;strong>Meta&lt;/strong> published that during the training of Llama 3 405B on 16,384 H100s there were &lt;strong>419 unplanned failures in 54 days&lt;/strong>, one every 3 hours, with GPU + HBM3 accounting for 47 % of the total; the paper &lt;em>Story of Two GPUs&lt;/em> (arXiv 2503.11901) quantifies that &lt;strong>the H100 has 3.2× worse MTBE from uncorrectable ECC than the A100&lt;/strong>, attributable to the higher density of HBM3; issue &lt;strong>vllm#16300&lt;/strong> documents that on a cluster of 8×A100 80 GB &lt;strong>TP=8 delivers worse throughput than TP=4&lt;/strong> because NVLink saturation kills the partition speedup. The KBs &lt;strong>Dell 000220508&lt;/strong> and &lt;strong>Lenovo HT514380&lt;/strong> formalise the recurring &lt;em>HW Power Brake&lt;/em> case in H100 racks overcommitted at PDU level. Issue &lt;strong>vllm#25677&lt;/strong> showed &lt;em>chunked prefill&lt;/em> 10× slower than without it on Qwen3-30B-A3B (bad calibration of &lt;code>max_num_batched_tokens&lt;/code>). Issue &lt;strong>vllm#11912&lt;/strong> documents a TPOT regression from 15.7 ms to 25.7 ms crossing version 0.6.4. Each case includes a verifiable URL. The operational rule: when an alert arrives, look first at the anomalous pattern associated with the metric that fired, &lt;strong>before&lt;/strong> opening the request&amp;rsquo;s trace; 80 % of degradations match one of the documented patterns.&lt;/p>
&lt;h2 id="you-are-here-observe--the-diagnosis-layer">You are here: OBSERVE — the diagnosis layer&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Observe, diagnosis layer">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c9a8e9;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#anm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#anm)}&lt;/style>
&lt;defs>&lt;marker id="anm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: OBSERVE · each metric is a question with a typical anomaly attached&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="family-1--compute">Family 1 — Compute&lt;/h2>
&lt;h3 id="dcgm_fi_prof_sm_occupancy--is-there-parallel-work-in-the-engines">&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code> — is there parallel work in the engines?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> An industrial kitchen with 32 hobs and a single chef. The metric answers &lt;em>&amp;ldquo;how many hobs have a pan on them right now?&amp;rdquo;&lt;/em>. If half are empty, the kitchen is under-used: the orders come out one after another because the chef does not open up parallelism. If all of them are occupied but the chef is standing still watching a stopwatch, the hobs are on but nothing is being cooked (a pathological kernel saturating SMs without doing useful work).&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The best-known trap: &lt;strong>high SM occupancy does not imply real throughput&lt;/strong>. The article &lt;em>&amp;ldquo;GPU Utilization Is a Counter, Not a Cause&amp;rdquo;&lt;/em> (Ingero, May 2026) put it in one exact sentence: &lt;em>&amp;ldquo;a kernel running at 5 % of peak FLOPS for 100 ms still reads 100 % on SM_ACTIVE&amp;rdquo;&lt;/em>. In MoE workloads the effect turns pathological: overloaded experts produce the &lt;strong>Straggler Effect&lt;/strong> (paper arXiv 2503.05066). The SMs look busy while waiting for the saturated expert, and the utilisation dashboard paints green while latency goes through the roof.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Do not trust sizing or autoscaling to SM occupancy alone. Always combine it with &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> (is there useful compute?) and &lt;code>DRAM_ACTIVE&lt;/code> (is memory the bottleneck?). The normal LLM decode regime is 30–55 %, not 99 %; seeing a sustained 99 % with high TPOT is a symptom of a kernel bug or an MoE straggler.&lt;/p>
&lt;h3 id="dcgm_fi_prof_pipe_tensor_active--are-the-tensor-cores-producing">&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code> — are the tensor cores producing?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> A factory with two lines: the manual one (CUDA cores) and the automated one (tensor cores). The metric answers &lt;em>&amp;ldquo;what percentage of the time is the automated line active?&amp;rdquo;&lt;/em>. If you buy an H100 for its tensor cores and the automated line is at 5 %, you have paid for a Ferrari to do bicycle courier work.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Issue &lt;strong>vllm#20783&lt;/strong> (July 2025) was titled literally &lt;em>&amp;ldquo;Performance Anomaly: compressed-tensors shows no speedup over BF16 on H100&amp;rdquo;&lt;/em>. The operator expected 1.5–2× with FP8 quantisation and got parity with BF16; the &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> metric revealed that the FP8 path was not running on the HMMA units (the tensor unit for FP16/BF16/FP8) and was falling back to CUDA cores. Issue &lt;strong>vllm#31475&lt;/strong> documented the parallel case on MI300X: FP8 slower than BF16 because of a regression in the ROCm path. DCGM exposes separate counters per unit (&lt;code>HMMA&lt;/code> for FP16/BF16/FP8, &lt;code>IMMA&lt;/code> for INT8, &lt;code>DMMA&lt;/code> for TF32/FP32); if &lt;code>HMMA&lt;/code> is low even though the model is BF16, the engine is not using tensor cores.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Check &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> after every change of quantisation or engine version; a supposedly neutral change may have disabled the optimised path. For prefill expect 50–80 %; for decode 15–30 % is normal (decode is memory-bound, not compute-bound). A figure below 5 % in prefill means the engine is not using tensor cores.&lt;/p>
&lt;h3 id="dcgm_fi_prof_dram_active--is-the-hbm-saturated">&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code> — is the HBM saturated?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> A motorway with N lanes. The metric answers &lt;em>&amp;ldquo;what percentage of the time are all the lanes occupied moving cars?&amp;rdquo;&lt;/em>. When the tensor cores ask for data faster than the HBM delivers it, the motorway is at 95 % and the engines wait. In decode, this is the normal regime: you walk the model weights and the KV cache through for every token.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The paper &lt;em>&amp;ldquo;Mind the Memory Gap: Unveiling GPU Bottlenecks in Large-Batch LLM Inference&amp;rdquo;&lt;/em> (arXiv 2503.08311) quantifies that at contexts ≥ 128k, reading the KV cache &lt;strong>dominates the total decode time&lt;/strong> and saturates the HBM3 (3.35 TB/s on the H100). The distinctive pattern: &lt;code>DRAM_ACTIVE&lt;/code> &amp;gt; 80 % with &lt;code>PIPE_TENSOR_ACTIVE&lt;/code> around 10–20 %. Raising the batch no longer helps: the bottleneck is not FLOPS, it is bandwidth. The useful lever is compressing the KV, see &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantisation&lt;/a> for &lt;code>--kv-cache-dtype=fp8&lt;/code>, which cuts the KV footprint by roughly 50 %.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> If &lt;code>DRAM_ACTIVE &amp;gt; 95 %&lt;/code> is sustained and &lt;code>gpu_cache_usage_perc &amp;lt; 70 %&lt;/code>, &lt;strong>something other than your engine is asking for HBM&lt;/strong> (a leak in a library, another process sharing the GPU without MIG). Investigate immediately with &lt;code>nvidia-smi&lt;/code> and &lt;code>fuser /dev/nvidia*&lt;/code>.&lt;/p>
&lt;h2 id="family-2--memory">Family 2 — Memory&lt;/h2>
&lt;h3 id="dcgm_fi_dev_fb_used--how-much-vram-has-been-consumed">&lt;code>DCGM_FI_DEV_FB_USED&lt;/code> — how much VRAM has been consumed?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The fuel level in an aircraft&amp;rsquo;s tank in flight: the pilot needs to know how much is left &lt;strong>and at what rate it is being consumed&lt;/strong>, not just the instantaneous figure. An H100 at 88 % FB used and &lt;strong>stable&lt;/strong> can operate calmly; the same figure &lt;strong>rising 2 %/min&lt;/strong> announces OOM in 7 minutes.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Issue &lt;strong>dcgm-exporter#512&lt;/strong> documents a surprise relevant to MIG clusters: &lt;strong>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code> and &lt;code>DCGM_FI_DEV_FB_FREE&lt;/code> are absent on H100 GPU instances with MIG enabled&lt;/strong>, present on A100 and B200, but an exporter bug hides them on H100-MIG. Operators who assume the dashboard covers everything discover the blind spot on the day of the first OOM. Issue &lt;strong>dcgm-exporter#271&lt;/strong> documents another detail: &lt;code>FB_USED + FB_FREE&lt;/code> &lt;strong>does not always add up to a constant&lt;/strong> because there is driver-reserved overhead that shows up in the delta. The original &lt;strong>PagedAttention/vLLM&lt;/strong> paper estimated that pre-PagedAttention serving frameworks wasted &lt;strong>60–80 %&lt;/strong> of the KV cache to fragmentation; PagedAttention brought it down to under 4 %.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> On H100 MIG clusters, check that &lt;code>DCGM_FI_DEV_FB_USED&lt;/code> appears per instance before trusting alerts; if it is absent, monitor through &lt;code>nvidia-smi --query-gpu=memory.used&lt;/code> directly. Operational rule: alert on the &lt;strong>delta&lt;/strong> (a sustained rise), not just an absolute threshold.&lt;/p>
&lt;h3 id="dcgm_fi_dev_fb_free--the-absolute-complement">&lt;code>DCGM_FI_DEV_FB_FREE&lt;/code> — the absolute complement&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The &amp;ldquo;range remaining&amp;rdquo; indicator in a modern car: it complements the percentage with an absolute figure that is directly actionable.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> When an aggressive PagedAttention pool leaves &lt;code>FB_FREE&lt;/code> at small absolute values (&amp;lt; 2 GiB), any normal allocation of transient buffers (the activations of a large prefill) can push you into OOM. The classic pattern: a &amp;ldquo;green&amp;rdquo; percentage (87 %) but a &amp;ldquo;red&amp;rdquo; absolute (&amp;lt; 4 GiB free on an 80 GB H100).&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> A complementary alert with an absolute threshold: &lt;code>DCGM_FI_DEV_FB_FREE &amp;lt; 4096&lt;/code> (MiB). It is the safety net for the cases where the percentage misleads because the engine is configured with a very high &lt;code>gpu_memory_utilization&lt;/code>.&lt;/p>
&lt;h3 id="dcgm_fi_dev_nvlink_bandwidth_total--does-the-internal-bus-hold-up">&lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code> — does the internal bus hold up?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> An interstate motorway between four cities. Every car that crosses to do a tensor parallel all-reduce pays a toll and consumes width. When there are more cars than the motorway supports, the latency to reach the destination shoots up, even if each individual car is fast.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Issue &lt;strong>vllm#16300&lt;/strong> (April 2025) was titled &lt;em>&amp;ldquo;Performance degradation with tp=8 compared to tp=4 on 8×A100(80G)&amp;rdquo;&lt;/em> and documented &lt;strong>TP=8 delivering worse throughput than TP=4&lt;/strong> on the same cluster, same model, same quantisation. Root cause: tensor parallelism requires an all-reduce after each attention and MLP block; at TP=8, the communication cost between 8 GPUs (even over NVSwitch) grows faster than the speedup of partitioning the compute. The practical rule that emerges: &lt;strong>TP=4 + 2 replicas&lt;/strong> usually delivers better latency/throughput than &lt;strong>TP=8 + 1 replica&lt;/strong>, except for extremely long contexts (≥128k) where you need the aggregate VRAM. Theoretical NVLink 4.0 capacity on the H100 SXM: around 450 GB/s per GPU; typical sustained TP=4 regime: 50–150 GB/s.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> If &lt;code>NVLINK_BANDWIDTH_TOTAL &amp;gt; 90 %&lt;/code> of capacity is sustained, this is not a problem &lt;em>solvable by raising parallelism&lt;/em>. The opposite: &lt;strong>lower TP&lt;/strong>. The metric is orthogonal to the sizing from &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a>: the ceiling is not only VRAM/time, it is also the bus.&lt;/p>
&lt;h2 id="family-3--thermal-and-power">Family 3 — Thermal and power&lt;/h2>
&lt;h3 id="dcgm_fi_dev_gpu_temp--is-the-gpu-breathing">&lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code> — is the GPU breathing?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The body temperature of an elite athlete under full effort. 36–37 °C is normal; 38 °C is sustainable stress; above 39 °C the body activates protective mechanisms (sweating, slowing down) that &lt;strong>degrade performance&lt;/strong>. The GPU does the same: above a thermal threshold, it reduces its clock automatically. If it did not, it would break.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The H100 SXM5 with its 700 W TDP has thermal thresholds that are not entirely public (NVIDIA does not publish them exhaustively in the datasheet), but the behaviour is well known: above roughly 85 °C edge or roughly 95 °C HBM, the bit &lt;code>0x40 HW_THERMAL&lt;/code> appears in the clock throttle reasons. Operators on the NVIDIA developer forum report that with a &lt;strong>rack inlet temperature above 27 °C&lt;/strong>, throttling is routine. The NVIDIA HGX Platform paper states that the minimum recommended air flow is &lt;strong>&amp;gt; 1000 CFM/kW&lt;/strong>; densities above 30 kW/rack at 700 W TDP demand mandatory liquid cooling because forced air does not get there.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> If &lt;code>GPU_TEMP &amp;gt; 83 °C&lt;/code> is sustained, look first at &lt;code>CLOCK_THROTTLE_REASONS&lt;/code> (bit 0x40) and at the rack inlet temperature: this is not an engine problem, it is an air flow one. For legacy air-cooled racks, consider redistributing thermal load or installing a rear-door HX.&lt;/p>
&lt;h3 id="dcgm_fi_dev_power_usage--how-much-is-it-asking-of-the-socket">&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> — how much is it asking of the socket?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The instantaneous draw of an industrial appliance plugged into a three-phase outlet with a sized breaker. If the washing machine starts at 9 kW and the breaker is 10 kW, you live on the edge; if the washing machine &amp;ldquo;gets along&amp;rdquo; with the breaker it is because somebody sized it consciously.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> A published real measurement: an H100 SXM5 with vLLM running Llama 3.1 405B at batch=4 draws &lt;strong>around 697 W at-wall&lt;/strong> sustained (NVIDIA TDP 700 W). Now the interesting operational lever: &lt;strong>lowering &lt;code>nvidia-smi -pl&lt;/code> from 700 W to 500 W&lt;/strong> delivers around 30 % energy savings with only around 20 % loss of throughput. A cluster of 4 nodes × 8 H100 at 700 W equals around 22 kW of GPU alone; at 500 W, around 16 kW. The difference pays an entire quarter&amp;rsquo;s electricity bill in clusters operated around the clock. A three-phase 415 VAC PDU branch at 60–80 A supports around 32 kW, roughly 4 DGX H100. Legacy 208 V does not support H100 density; reference: NVIDIA DGX SuperPOD Electrical Specifications.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> A useful metric for three things: (1) detecting anomalously low workloads (unexpected idle), (2) computing per-tenant showback of real energy cost (not estimates), (3) alerting if the draw approaches the PDU branch limit. Keep &lt;strong>GPU → PDU branch → breaker&lt;/strong> mapped in the CMDB.&lt;/p>
&lt;h3 id="dcgm_fi_dev_clock_throttle_reasons--who-is-stepping-on-the-brake">&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code> — who is stepping on the brake?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The &amp;ldquo;limited mode&amp;rdquo; warning light on the dashboard of a modern car. When it lights up, the car reduces its performance automatically, but &lt;strong>it does not tell you why&lt;/strong> unless you know how to read the combination of letters. The bits of the bitmap are those letters.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> A public case formally acknowledged by two different OEMs: &lt;strong>Dell KB 000220508&lt;/strong> and &lt;strong>Lenovo HT514380&lt;/strong> address the same phenomenon: &lt;em>HW Power Brake Slowdown active&lt;/em> (bit &lt;code>0x80&lt;/code>) on the H100 SXM. The cause is not the GPU: it is the chassis PDU sending an electrical power-brake signal because the rack branch is close to the breaker limit. The operator sees throughput down 30–50 % with no XID and no ECC, and the inference engine &amp;ldquo;is healthy&amp;rdquo;; the problem is in the electrics. The NVIDIA developer forum thread on &lt;em>&amp;ldquo;HW Power Brake Slowdown&amp;rdquo;&lt;/em> corroborates the pattern. Bit &lt;code>0x40 HW_THERMAL&lt;/code> appears in badly ventilated racks; bit &lt;code>0x04 SW_POWER_CAP&lt;/code> appears if somebody left &lt;code>nvidia-smi -pl 500&lt;/code> set and nobody reverts it.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Any bit other than 0 or &lt;code>Idle&lt;/code> (bit 0x01) sustained &lt;strong>is an immediate alert&lt;/strong>. The recommended decoding: record the complete bitmap value in the log plus the attribute &lt;code>throttle.reasons.decoded=[&amp;quot;HW_THERMAL&amp;quot;, &amp;quot;HW_POWER_BRAKE&amp;quot;]&lt;/code> on the OTel span. Without that, incident response does not know what to do.&lt;/p>
&lt;h2 id="family-4--health-the-catastrophic-reports">Family 4 — Health (the catastrophic reports)&lt;/h2>
&lt;h3 id="dcgm_fi_dev_xid_errors--the-drivers-red-codes">&lt;code>DCGM_FI_DEV_XID_ERRORS&lt;/code> — the driver&amp;rsquo;s red codes&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The critical alarm lights in a nuclear control room. They do not climb gradually; they either appear or they do not. Each XID is a predefined code (XID 13 = graphics engine exception; XID 31 = MMU fault; XID 43 = stopped channel; XID 79 = GPU fallen off the bus; XID 95 = uncontained ECC), and each one has its documented procedure.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The most studied public case: &lt;strong>Meta&lt;/strong> published that during the training of Llama 3 405B on &lt;strong>16,384 H100s over 54 days&lt;/strong> there were &lt;strong>419 unplanned failures&lt;/strong>, one every 3 hours at cluster scale. GPUs accounted for 148 (35 %) plus HBM3 for 72 (17 %), almost half of all failures. The paper &lt;em>&amp;ldquo;Story of Two GPUs: Characterizing the Resilience of Hopper H100 and Ampere A100&amp;rdquo;&lt;/em> (arXiv 2503.11901) quantifies with a different dataset (2.1M GPU-hours) that the H100 has &lt;strong>3.2× worse MTBE for uncorrectable ECC than the A100&lt;/strong>. The ByteDance MegaScale paper reports that XID 79 (&amp;ldquo;GPU fallen off the bus&amp;rdquo;) co-occurs with PCIe errors in &lt;strong>43 % of cases&lt;/strong>. The NVIDIA developer forum documents persistent cases of XID 31 (MMU fault) that &lt;strong>follow the GPU when it is moved to a different PCIe slot&lt;/strong>, a hardware bug in the module, not in the backplane.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> &lt;strong>Any increment of the counter is an immediate alert&lt;/strong>: many XIDs demand a node reset or an RMA of the GPU. The XID-by-XID distinction matters: XID 13/43 is usually a software bug if it coincides with a recent change; XID 31/48/79/94/95 is usually hardware. Keep a canonical &lt;code>xid → procedure&lt;/code> table. See &lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">the runbooks&lt;/a> for the translation into concrete action.&lt;/p>
&lt;h3 id="dcgm_fi_dev_ecc_dbe_vol_total--the-errors-that-corrupt-data">&lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code> — the errors that corrupt data&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> An accounting ledger where sometimes someone erases an entry and rewrites it (corrected single-bit ECC: note a change in the margin and carry on) and sometimes someone burns two pages at once (double-bit: the information is lost, the audit has to stop).&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The paper &lt;em>&amp;ldquo;Characterizing GPU Resilience&amp;rdquo;&lt;/em> quantifies for the H100 that when XID 48 (DBE) appears, &lt;strong>the job in flight dies with 100 % probability&lt;/strong> (5 out of 5 in the dataset studied). The documented recovery: drain the node, reset, complete the row remap, equalling &lt;strong>around 19 hours of node downtime&lt;/strong>. HBM3 density explains the worse MTBE against HBM2e: there are more cells per unit area, and therefore a higher statistical probability of degradation. In Llama 3, HBM3 caused 72 of the 419 interruptions (17 %).&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Any value &amp;gt; 0 is a critical alert. The GPU must be &lt;strong>drained immediately&lt;/strong>, removed from the scheduler, fully reset, validated for row remap with &lt;code>nvidia-smi -q -d ROW_REMAPPER&lt;/code> (&lt;code>Pending: No&lt;/code>), and given an extensive smoke test before returning to the pool. If the row remap uses more than 4–8 spare pages on one GPU, plan a replacement in the next window: the degradation is progressive.&lt;/p>
&lt;h3 id="dcgm_fi_dev_retired_dbe--the-pages-marked-for-retirement">&lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code> — the pages marked for retirement&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The tiles a museum restorer marks with yellow tape because they are damaged. They pose no immediate danger (the room stays open), but the accumulation says the floor is degrading structurally and the full replacement has to be planned.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> NVIDIA documents up to &lt;strong>512 spare pages per HBM bank&lt;/strong> on the H100; the &lt;code>RETIRED_DBE&lt;/code> counter indicates how many have been used. Operators on NVIDIA forums report that above &lt;strong>4–8 retired pages on a specific GPU&lt;/strong>, the frequency of XID 48 rises. The pattern: a GPU with 6 retired pages today becomes 12 in a month, a first XID 48 two months later, and a forced drain.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> A trend metric, not an immediate alert. Record the value per GPU and review it monthly; GPUs with rising values go into the proactive replacement plan before the catastrophic failure.&lt;/p>
&lt;h2 id="the-five-vllm-engine-metrics">The five vLLM engine metrics&lt;/h2>
&lt;h3 id="vllmnum_requests_running--how-many-requests-fit-in-the-batch">&lt;code>vllm:num_requests_running&lt;/code> — how many requests fit in the batch?&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The number of cars a toll booth lets through at once. If the barrier opens for N at a time, car N+1 waits in the queue. Saturation shows up because the line does not get shorter.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Reaching the configured &lt;code>--max-num-seqs&lt;/code> and staying there is the typical symptom of a &lt;strong>cluster below its sizing&lt;/strong>; the engine admits up to the ceiling and no further. The query &lt;code>vllm:num_requests_running == max_num_seqs&lt;/code> for more than 5 minutes indicates firm saturation.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Combine it with &lt;code>num_requests_waiting&lt;/code>: if running is at the ceiling AND waiting &amp;gt; 0, you have to scale. If running is at the ceiling and waiting is 0, you are in the optimal regime (cluster used to the maximum with no queue).&lt;/p>
&lt;h3 id="vllmnum_requests_waiting--the-primary-saturation-indicator">&lt;code>vllm:num_requests_waiting&lt;/code> — the primary saturation indicator&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The visible queue in front of the toll booth. As long as it is empty, the system flows; the moment a sustained queue forms, drivers start arriving late at their destination, and TTFT goes through the roof.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> A public case in &lt;em>&amp;ldquo;11-Second Time to First Token on a Healthy vLLM Server&amp;rdquo;&lt;/em> (Medium, Ingero, 2026): a server with no XIDs, no preemption, DCGM metrics all green, but &lt;code>num_requests_waiting&lt;/code> sustained above 0 and a TTFT of &lt;strong>11 seconds&lt;/strong>. Issue &lt;strong>vllm#16985&lt;/strong> documents progressive degradation in long sessions: the queue grows slowly over hours without any other indicator moving. The root cause is not hardware, it is &lt;strong>admission control&lt;/strong>: the arrival rate exceeds the completion rate and the system does not enqueue any more, it leaves requests in &lt;code>WAITING&lt;/code>. Red Hat designates it as &lt;strong>the primary saturation metric&lt;/strong> in its tutorial &lt;em>&amp;ldquo;5 steps to triage vLLM performance&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> The primary HPA metric in KEDA, see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLM on Kubernetes&lt;/a>. Typical threshold: alert if &lt;code>avg_over_time(vllm:num_requests_waiting[5m]) &amp;gt; 5&lt;/code>. For canaries: if the queue only forms in the v2 pool, it is a regression of the new model, not cluster load.&lt;/p>
&lt;h3 id="vllmgpu_cache_usage_perc--the-kv-cache-pool">&lt;code>vllm:gpu_cache_usage_perc&lt;/code> — the KV cache pool&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The capacity of an events hall where each guest takes up a variable amount of space. The maître d&amp;rsquo; admits guests up to capacity; when a new guest arrives and there is no room, he &lt;strong>throws out the guest who has been there longest&lt;/strong> to make space for the newcomer. That is vLLM&amp;rsquo;s preempt-on-OOM.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Issue &lt;strong>vllm#5051&lt;/strong> &lt;em>&amp;ldquo;Add num_requests_preempted metric&amp;rdquo;&lt;/em> was born exactly out of operators observing degradation with no direct metric telling them how many requests were being thrown out. The official vLLM documentation confirms: &lt;em>&amp;ldquo;sustained &lt;code>gpu_cache_usage_perc&lt;/code> above 90 % indicates the server is approaching its KV cache limit and will begin preempting sequences&amp;rdquo;&lt;/em> (oldest-first). The distinctive visual pattern: a &lt;strong>sawtooth near 100 %&lt;/strong> with preemption spikes. In &lt;code>swap&lt;/code> mode, the latency of the preempted request explodes because there is a host↔device PCIe copy; in &lt;code>recompute&lt;/code> mode (the default in V1), the preempted request redoes its prefill from scratch, which pushes its TTFT to double or triple.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> If &lt;code>gpu_cache_usage_perc &amp;gt; 92 %&lt;/code> is sustained, there are &lt;strong>two levers&lt;/strong>: lower &lt;code>max_num_seqs&lt;/code> (admit less concurrency, but none of it gets preempted) or raise &lt;code>gpu_memory_utilization&lt;/code> (more pool, less VRAM for transient activations, a different risk). The choice depends on the workload. The metric that is directly missing, a preempted counter, is exported from vLLM v1.0 onwards as &lt;code>vllm:num_preemptions_total&lt;/code> (see issue #5051).&lt;/p>
&lt;h3 id="vllmtime_to_first_token_seconds--the-latency-the-client-sees">&lt;code>vllm:time_to_first_token_seconds&lt;/code> — the latency the client sees&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The time from a customer walking into a restaurant to receiving the first piece of bread at the table. Too long and the customer thinks they have been forgotten, even if the main course is going to arrive perfect.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> Three documented patterns of recurring TTFT spikes:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Badly calibrated chunked prefill.&lt;/strong> Issue &lt;strong>vllm#25677&lt;/strong> (Qwen3-30B-A3B) reported &lt;strong>prefill 10–11× slower with chunked prefill enabled&lt;/strong> than without it. Cause: a very low &lt;code>max_num_batched_tokens&lt;/code> forces small chunks that do not fill the kernels. Issue &lt;strong>vllm#7604&lt;/strong> documents an equivalent regression on Llama-3-70B v0.5.4. The lever: raise &lt;code>max_num_batched_tokens&lt;/code> to 4096–8192 for typical prompts above 2k.&lt;/li>
&lt;li>&lt;strong>Regression between engine versions.&lt;/strong> Issue &lt;strong>vllm#8819&lt;/strong> documents a regression of &lt;code>vllm:time_to_first_token_seconds_sum&lt;/code> between minor versions. Issue &lt;strong>vllm#11912&lt;/strong> reports that with a prompt of around 8000 tokens, TPOT rose from &lt;strong>15.7 ms → 25.7 ms&lt;/strong> from v0.6.4.post1 onwards with no config change, a regression confirmed and trackable only with the metric.&lt;/li>
&lt;li>&lt;strong>Long-context prefill blocking decodes.&lt;/strong> The &lt;em>&amp;ldquo;11s TTFT on healthy server&amp;rdquo;&lt;/em> case cited above: a 30k-token prefill monopolises the GPU for several seconds and the active decodes freeze. Fix: well-calibrated chunked prefill, or disaggregated serving (see &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving&lt;/a>).&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Operational implication.&lt;/strong> Do not alert only on the absolute P95; alert also on the &lt;strong>v2/v1 ratio&lt;/strong> when there is a canary (&lt;code>histogram_quantile(0.95, ..., version=&amp;quot;v2&amp;quot;) / histogram_quantile(0.95, ..., version=&amp;quot;v1&amp;quot;) &amp;gt; 1.10&lt;/code>). If TTFT grows and the queue is stable, the bottleneck is prefill: not solvable by adding replicas, but there are levers in quantisation or chunked prefill.&lt;/p>
&lt;h3 id="vllmtime_per_output_token_seconds--streaming-smoothness">&lt;code>vllm:time_per_output_token_seconds&lt;/code> — streaming smoothness&lt;/h3>
&lt;p>&lt;strong>The analogy.&lt;/strong> The speed at which the waiter brings the courses one after another after the first. If the next one takes a while, the diner notices something is wrong even though the first course arrived on time.&lt;/p>
&lt;p>&lt;strong>The documented anomaly.&lt;/strong> The distinctive pattern is the &lt;strong>abrupt step when &lt;code>gpu_cache_usage_perc&lt;/code> crosses roughly 85 %&lt;/strong>: TPOT jumps from 35 ms to 80 ms in a few seconds because the engine starts competing for the HBM with its own evictions. Issue &lt;strong>vllm#35387&lt;/strong> documents another anomalous case: &lt;strong>MTP (speculative decoding) causing a 76 % latency regression&lt;/strong> on Qwen3-Next-80B-A3B-Instruct-FP8. The TPOT metric caught it before any client complaints were filed.&lt;/p>
&lt;p>&lt;strong>Operational implication.&lt;/strong> The difference from TTFT: if TTFT grows and Queue Time is stable, you are prefill bound; if TPOT grows at a steady rate, there is pressure on the HBM (KV cache pool or swap enabled). A secondary alert on the TPOT SLO, but also watch the &lt;strong>derivative&lt;/strong>: TPOT rising 1 ms every 10 minutes is a latent regression that does not break the SLO yet but will.&lt;/p>
&lt;h2 id="the-operational-rule-read-metrics-by-family-not-in-isolation">The operational rule: read metrics by family, not in isolation&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="combined reading of metrics by family">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.c{fill:#dfe9f5;stroke:#356}.m{fill:#eef0d0;stroke:#7a3}.t{fill:#f4e3cf;stroke:#a63}.s{fill:#f6e2e2;stroke:#a33}.title{font:600 13px sans-serif;fill:#222}.h{font:700 12px sans-serif;fill:#222}.l{font:11px sans-serif;fill:#222}.n{font:italic 10px sans-serif;fill:#444}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Combinations that diagnose (each family on its own misleads)&lt;/text>
&lt;rect x="20" y="40" width="380" height="100" class="b c"/>
&lt;text x="30" y="62" class="h">COMPUTE saturated BUT memory free&lt;/text>
&lt;text x="30" y="80" class="l">SM_OCCUPANCY 95% + TENSOR_ACTIVE 75% + DRAM_ACTIVE 50%&lt;/text>
&lt;text x="30" y="98" class="l">+ FB_USED 60%&lt;/text>
&lt;text x="30" y="120" class="n">→ Prefill bound. Lever: speculative decoding,&lt;/text>
&lt;text x="30" y="134" class="n"> chunked prefill, disaggregated serving.&lt;/text>
&lt;rect x="420" y="40" width="380" height="100" class="b m"/>
&lt;text x="430" y="62" class="h">MEMORY saturated BUT compute slack&lt;/text>
&lt;text x="430" y="80" class="l">SM_OCCUPANCY 35% + TENSOR_ACTIVE 18% + DRAM_ACTIVE 92%&lt;/text>
&lt;text x="430" y="98" class="l">+ gpu_cache_usage_perc 88%&lt;/text>
&lt;text x="430" y="120" class="n">→ Decode bound + KV cache under pressure.&lt;/text>
&lt;text x="430" y="134" class="n"> Lever: FP8 KV cache, shorter context.&lt;/text>
&lt;rect x="20" y="150" width="380" height="120" class="b t"/>
&lt;text x="30" y="172" class="h">High TPOT WITHOUT saturating compute or memory&lt;/text>
&lt;text x="30" y="190" class="l">DRAM_ACTIVE 65% + FB_USED 70% + temp 78°C&lt;/text>
&lt;text x="30" y="208" class="l">+ THROTTLE_REASONS = 0x40 (HW_THERMAL)&lt;/text>
&lt;text x="30" y="230" class="n">→ Silent thermal throttle.&lt;/text>
&lt;text x="30" y="244" class="n"> Lever: check rack ventilation, not the engine.&lt;/text>
&lt;text x="30" y="258" class="n"> Classic Dell/Lenovo KB case.&lt;/text>
&lt;rect x="420" y="150" width="380" height="120" class="b s"/>
&lt;text x="430" y="172" class="h">High TTFT P95 WITHOUT throttle or queue&lt;/text>
&lt;text x="430" y="190" class="l">num_requests_waiting 0 + throttle 0 + DRAM_ACTIVE 70%&lt;/text>
&lt;text x="430" y="208" class="l">+ v2/v1 ratio = 1.4 (canary live)&lt;/text>
&lt;text x="430" y="230" class="n">→ v2 model regression in prefill.&lt;/text>
&lt;text x="430" y="244" class="n"> Lever: roll back the canary,&lt;/text>
&lt;text x="430" y="258" class="n"> review the v2 engine config.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="three-anti-patterns-of-the-novice-operator">Three anti-patterns of the novice operator&lt;/h2>
&lt;p>&lt;strong>Anti-pattern 1 — alerting only on absolute thresholds.&lt;/strong> An H100 at 87 % FB is not necessarily an alarm; an H100 at 87 % rising 2 %/min is. Alerts that fire on a fixed threshold without looking at the derivative produce twice the noise and half the value. Rule: for metrics with known dynamics (KV cache, FB, queue), alert on a &lt;strong>sustained delta&lt;/strong>, not just on the level.&lt;/p>
&lt;p>&lt;strong>Anti-pattern 2 — confusing SBE with DBE.&lt;/strong> The &lt;code>DCGM_FI_DEV_ECC_SBE_VOL_TOTAL&lt;/code> counter (single-bit, correctable) grows &lt;strong>continuously&lt;/strong> on any HBM under load; it is not an alarm, it is physics. The one that matters is &lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code> (double-bit, uncorrectable). Confusing them yields false negatives (not alerting on a real DBE) or false positives (alerting on a harmless SBE).&lt;/p>
&lt;p>&lt;strong>Anti-pattern 3 — treating SM_OCCUPANCY at 99 % as &amp;ldquo;saturated&amp;rdquo;.&lt;/strong> The LLM decode regime is memory-bound, not compute-bound; high SM occupancy with low TENSOR_ACTIVE and high DRAM_ACTIVE &lt;strong>is normal&lt;/strong>. Sizing for &amp;ldquo;GPU at 60 %&amp;rdquo; and asking for more hardware when the cluster is saturated in HBM (not in SMs) means buying twice the GPUs without gaining throughput. Rule: always read SM_OCCUPANCY together with TENSOR_ACTIVE and DRAM_ACTIVE; on its own it means nothing.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For a generic cluster of &lt;strong>4 nodes × 4×H100 SXM 80 GB with intra-node NVLink&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DCGM Exporter&lt;/strong> per node (a GPU Operator DaemonSet) emitting every 15 s; cardinality per GPU is around 80 series. A 16-GPU cluster gives roughly 1.3k base series, around 85k samples/min with a 15 s scrape.&lt;/li>
&lt;li>&lt;strong>vLLM /metrics&lt;/strong> per inference pod; each replica emits around 50 base series. For 16 replicas, around 800 additional series, around 3k samples/min.&lt;/li>
&lt;li>&lt;strong>Prometheus retention&lt;/strong>: 30 days at high resolution plus 1 year downsampled through a Thanos sidecar or Mimir. Estimated volume: 25–35 GB/day.&lt;/li>
&lt;li>&lt;strong>Alertmanager&lt;/strong>: the 6 critical alerts from the &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">previous post&lt;/a> plus derived alerts (delta, v2/v1 ratio, decoded throttle bitmap).&lt;/li>
&lt;/ul>
&lt;p>Each metric is worth exposing also as an &lt;strong>OTel attribute&lt;/strong> on the spans of the &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">GenAI tracing&lt;/a>: &lt;code>gpu.fb_used_pct&lt;/code>, &lt;code>gpu.dram_active&lt;/code>, &lt;code>gpu.throttle_reasons.decoded&lt;/code>. That lets you correlate a slow request with the state of the GPU at that instant, without jumping between dashboards.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Per-alert runbooks&lt;/strong> — the translation of each anomalous metric into concrete action (drain, reset, RMA, scaling, rollback) in the next post: &lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">Incident response runbooks&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Tail sampling for metric ↔ trace correlation&lt;/strong> — what is preserved when an alert fires, for post-mortem investigation.&lt;/li>
&lt;li>&lt;strong>Per-tenant showback&lt;/strong> combining &lt;code>vllm:request_success_total&lt;/code> × &lt;code>gen_ai.usage.*&lt;/code> × &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> to bill real energy cost.&lt;/li>
&lt;li>&lt;strong>Multi-tenant fairness metrics&lt;/strong> — when a tenant hogs the KV cache pool and how to detect it.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> — the compact list that this post goes deeper into.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">Incident response runbooks for LLM with Keep + Kafka&lt;/a> — the translation of each anomaly into action.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the other half of observability.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLM on Kubernetes&lt;/a> — &lt;code>num_requests_waiting&lt;/code> and &lt;code>gpu_cache_usage_perc&lt;/code> as primary HPA metrics.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — how the thresholds relate to the sizing.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow&lt;/a> — the v2/v1 TTFT ratio as a gate.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> — explains preempt-on-OOM and the KV pool sawtooth.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a> — underpins the calculation of &lt;code>gpu_cache_usage_perc&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Meta — &lt;em>Faulty Nvidia H100 GPUs and HBM3 memory caused half of failures during Llama 3 training&lt;/em> (Tom&amp;rsquo;s Hardware, 2024). &lt;a href="https://www.tomshardware.com/tech-industry/artificial-intelligence/faulty-nvidia-h100-gpus-and-hbm3-memory-caused-half-of-the-failures-during-llama-3-training-one-failure-every-three-hours-for-metas-16384-gpu-training-cluster">tomshardware.com&lt;/a>&lt;/li>
&lt;li>&lt;em>Story of Two GPUs: Characterizing the Resilience of Hopper H100 and Ampere A100&lt;/em>. arXiv 2503.11901. &lt;a href="https://arxiv.org/html/2503.11901v3">https://arxiv.org/html/2503.11901v3&lt;/a>&lt;/li>
&lt;li>ByteDance — &lt;em>Robust LLM Training Infrastructure at ByteDance&lt;/em>. arXiv 2509.16293. &lt;a href="https://arxiv.org/pdf/2509.16293">https://arxiv.org/pdf/2509.16293&lt;/a>&lt;/li>
&lt;li>&lt;em>Mind the Memory Gap: Unveiling GPU Bottlenecks in Large-Batch LLM Inference&lt;/em>. arXiv 2503.08311.&lt;/li>
&lt;li>&lt;em>Capacity-Aware Inference: Mitigating the Straggler Effect in Mixture of Experts&lt;/em>. arXiv 2503.05066.&lt;/li>
&lt;li>NVIDIA — &lt;em>Analyzing Xid Errors with the Xid Catalog&lt;/em> and &lt;em>Memory Error Management&lt;/em> (docs.nvidia.com/deploy).&lt;/li>
&lt;li>Dell — &lt;em>PowerEdge XE8640 with H100 - GPU Performance Issue HW Power Brake Slowdown - Active&lt;/em> (KB 000220508).&lt;/li>
&lt;li>Lenovo — &lt;em>Power brake reporting on H100 GPU&lt;/em> (HT514380).&lt;/li>
&lt;li>vLLM project — issues #5051 (preempted metric), #7604 and #25677 (chunked prefill regression), #11912 (long-prompt regression), #16300 (TP=8 worse than TP=4), #16985 (long-running degradation), #20783 (compressed-tensors no speedup), #35387 (MTP regression).&lt;/li>
&lt;li>Red Hat — &lt;em>5 steps to triage vLLM performance&lt;/em>. &lt;a href="https://developers.redhat.com/articles/2026/03/09/5-steps-triage-vllm-performance">https://developers.redhat.com/articles/2026/03/09/5-steps-triage-vllm-performance&lt;/a>&lt;/li>
&lt;li>AI21 — &lt;em>Go big or go OOM: the art of scaling vLLM&lt;/em>. &lt;a href="https://www.ai21.com/blog/scaling-vllm-without-oom/">https://www.ai21.com/blog/scaling-vllm-without-oom/&lt;/a>&lt;/li>
&lt;li>&lt;em>11-Second Time to First Token on a Healthy vLLM Server&lt;/em> (Medium, Ingero, 2026).&lt;/li>
&lt;li>NVIDIA — &lt;em>DGX SuperPOD Electrical Specifications&lt;/em> (docs.nvidia.com/dgx-superpod).&lt;/li>
&lt;/ul>
&lt;p>Sources: the full URLs are linked inline on each reference.&lt;/p></description></item><item><title>GPU observability for LLM inference: the twelve DCGM and vLLM metrics that dictate the health of your production</title><link>https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/</link><pubDate>Mon, 01 Jun 2026 15:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/</guid><description>&lt;blockquote>
&lt;p>This post complements those on &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> (the tracing layer above the metrics), &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning&lt;/a> (what was sized and what must be watched) and &lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> (the mechanism that explains several of the engine metrics).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Observability for an LLM inference cluster is built on &lt;strong>two complementary sources&lt;/strong>: the GPU hardware metrics exposed by the &lt;strong>DCGM (Data Center GPU Manager) Exporter&lt;/strong>, part of the NVIDIA GPU Operator, and the metrics from the &lt;strong>inference engine&lt;/strong> (vLLM, SGLang, TensorRT-LLM) exposed on a Prometheus-compatible &lt;code>/metrics&lt;/code>. Neither is enough on its own. The classic &lt;code>nvidia-smi&lt;/code> metric called &lt;em>GPU utilization&lt;/em> is misleading for LLMs: it reads high whenever &lt;strong>any kernel&lt;/strong> is running, without distinguishing tensor cores burning from SMs waiting on HBM. The full cockpit has &lt;strong>twelve DCGM metrics in four families&lt;/strong> (compute: &lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>, &lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>, &lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>; memory: &lt;code>DCGM_FI_DEV_FB_USED&lt;/code>, &lt;code>DCGM_FI_DEV_FB_FREE&lt;/code>, &lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>; thermal-power: &lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>, &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>, &lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>; health: &lt;code>DCGM_FI_DEV_XID_ERRORS&lt;/code>, &lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>, &lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>) and &lt;strong>five vLLM engine metrics&lt;/strong> (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:num_requests_waiting&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, &lt;code>vllm:time_to_first_token_seconds&lt;/code>, &lt;code>vllm:time_per_output_token_seconds&lt;/code>). Each one has a defensible green/amber/red threshold, a PromQL query for alerting, and at least one common false reading that confuses the junior operator. The &lt;strong>six critical alerts&lt;/strong> that any production cluster must fire are: HBM &amp;gt; 92 %, thermal or power throttle, XID error, ECC double-bit, KV cache pool &amp;gt; 95 %, and TTFT P95 outside SLO for 5 minutes. The point of having this panel: that the operator on shift diagnoses the origin of a degradation in &lt;strong>under five minutes&lt;/strong>, without opening an SSH console to the GPUs. When that holds, the cluster has moved to professional operation; until then, it is operated on intuition.&lt;/p>
&lt;h2 id="you-are-here-observe-the-other-half-of-tracing">You are here: OBSERVE (the other half of tracing)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c9a8e9;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#obm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#obm)}&lt;/style>
&lt;defs>&lt;marker id="obm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: OBSERVE · metrics (DCGM + engine) complement tracing&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;p>Tracing, already covered in &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a>, answers &lt;em>what happened in this specific request&lt;/em>. Metrics answer &lt;em>what is happening across the cluster in aggregate&lt;/em>. They are complementary: an alert on the metrics side tells you &amp;ldquo;the cluster is degrading&amp;rdquo;, tracing tells you &amp;ldquo;and here is the specific trace that proves it&amp;rdquo;. A cluster without tracing but with metrics operates; a cluster without metrics but with tracing &lt;strong>does not operate, it debugs&lt;/strong>.&lt;/p>
&lt;h2 id="the-analogy-the-cockpit-of-a-modern-aircraft">The analogy: the cockpit of a modern aircraft&lt;/h2>
&lt;p>In a modern commercial aircraft, the pilot&amp;rsquo;s instrument panel has more than 70 active indicators. If there were only one, the altimeter, say, the aircraft would fly into the ground at the first moment of low visibility. You need the altimeter &lt;strong>and&lt;/strong> the attitude indicator, &lt;strong>and&lt;/strong> the airspeed indicator, &lt;strong>and&lt;/strong> the turn indicator, &lt;strong>and&lt;/strong> the fuel gauge, &lt;strong>and&lt;/strong> the oil pressure gauges for each engine, &lt;strong>and&lt;/strong> the turbine exit temperatures. Each answers a different question. And together they cover the operational question: &lt;em>is the aircraft healthy, is it where it should be, and is it going where we want?&lt;/em>&lt;/p>
&lt;p>Observability for an LLM inference cluster works the same way. A single metric, &amp;ldquo;GPU utilization 99 %&amp;rdquo;, answers nothing useful. It is like looking only at the car&amp;rsquo;s odometer to diagnose why the engine is making a noise. The full cockpit is &lt;strong>twelve instruments on the hardware side plus five on the inference engine side&lt;/strong>, organised into families that answer different questions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compute and efficiency&lt;/strong>: &lt;em>are the tensor cores doing the work we expect, or are they waiting?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Memory&lt;/strong>: &lt;em>is there VRAM left for new requests, or are we on the edge of OOM?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Thermal and power&lt;/strong>: &lt;em>is the hardware healthy, or is it silently limiting throughput?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Health and errors&lt;/strong>: &lt;em>is there hardware degradation under way (ECC, XID, NVLink)?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Inference engine&lt;/strong>: &lt;em>is the queue growing, is the KV pool saturated, is the SLO being met?&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>The first four answer &amp;ldquo;is the GPU fine?&amp;rdquo;. The fifth answers &amp;ldquo;is it delivering the service we promised?&amp;rdquo;. The two questions are different and both must have an answer at a glance.&lt;/p>
&lt;h2 id="why-nvidia-smi-gpu-util-misleads-with-llms">Why &lt;code>nvidia-smi&lt;/code> &lt;code>GPU-Util&lt;/code> misleads with LLMs&lt;/h2>
&lt;p>The classic metric that appears in &lt;code>nvidia-smi&lt;/code> as &lt;code>GPU-Util&lt;/code> corresponds to &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>. Its official definition: &amp;ldquo;percentage of time during which one or more kernels were executing on the GPU&amp;rdquo;. The problem with LLMs: the decode phase is &lt;strong>memory-bound&lt;/strong>, not compute-bound. When the inference engine decodes token by token, the GPU spends 90 % of the time waiting for the HBM to finish delivering the model weights and the KV cache. There is a kernel running (the HBM read), so &lt;code>GPU-Util&lt;/code> reports values close to 100 %. But the tensor cores are idle: the bottleneck is memory, not compute.&lt;/p>
&lt;p>Practical result: the operator sees &amp;ldquo;GPU-Util 99 %&amp;rdquo; in Grafana and assumes &amp;ldquo;GPU saturated, no more load can go in&amp;rdquo;. But the reality may be &amp;ldquo;compute at 25 %, HBM saturated at 95 %&amp;rdquo;, which changes the operational decisions (quantisation, batch size, parallelism). The classic metric lies by oversimplifying.&lt;/p>
&lt;p>The right move is to look at the &lt;strong>three DCGM profiling metrics&lt;/strong> of the &lt;code>_FI_PROF_*&lt;/code> subsystem:&lt;/p>
&lt;ul>
&lt;li>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>: ratio of active warps over the maximum per SM. &lt;em>Is there parallel work?&lt;/em>&lt;/li>
&lt;li>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>: % of cycles with tensor cores effectively active. &lt;em>Is compute working?&lt;/em>&lt;/li>
&lt;li>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>: % of cycles with HBM transferring. &lt;em>Is memory saturated?&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>A typical decode-bound GPU running Llama 70B on an H100 shows: SM occupancy 35–55 %, tensor active 15–30 %, DRAM active 80–95 %. That is the real &amp;ldquo;saturated GPU&amp;rdquo; for LLMs. The three together tell the regimes apart; none of them alone says anything actionable.&lt;/p>
&lt;h2 id="how-they-are-wired-up-in-production">How they are wired up in production&lt;/h2>
&lt;p>The platform side is covered in &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels&lt;/a> (level 4, the GPU plane) and &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">Seven deployment phases&lt;/a> (phase F5). For the observer, the key pieces are:&lt;/p>
&lt;p>&lt;strong>NVIDIA GPU Operator.&lt;/strong> Helm manifests that deploy on each GPU node: drivers, container toolkit, MIG manager and &lt;strong>DCGM Exporter&lt;/strong>. The last one exposes &lt;code>/metrics&lt;/code> in Prometheus format with all the &lt;code>DCGM_FI_*&lt;/code> listed above. It is scraped from the cluster&amp;rsquo;s internal Prometheus.&lt;/p>
&lt;p>&lt;strong>Inference engine.&lt;/strong> vLLM exposes &lt;code>/metrics&lt;/code> on port 8000 (default) with &lt;code>vllm:*&lt;/code> metrics. SGLang exposes it too, with the &lt;code>sglang:&lt;/code> prefix. TensorRT-LLM exposes it through Triton Inference Server with the &lt;code>nv_inference:&lt;/code> prefix. The basic naming convention is similar across the three engines; the thresholds and queries in this post assume vLLM, but they translate.&lt;/p>
&lt;p>&lt;strong>ServiceMonitor / PodMonitor.&lt;/strong> The Prometheus operator resource that says what to scrape. Minimal example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">monitoring.coreos.com/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PodMonitor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-inference&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podMetricsEndpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Dashboards.&lt;/strong> The NVIDIA operator publishes reference Grafana dashboards for DCGM in &lt;code>nvidia/dcgm-exporter&lt;/code> (the official repo). vLLM publishes one in &lt;code>vllm-project/vllm&lt;/code> (the &lt;code>examples/&lt;/code> folder). Both work as a base; each team adds the panels specific to its own SLO.&lt;/p>
&lt;h2 id="the-twelve-dcgm-metrics-organised-by-family">The twelve DCGM metrics organised by family&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="twelve DCGM metrics in four families">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.fc{fill:#dfe9f5;stroke:#356}.fm{fill:#eef0d0;stroke:#7a3}.ft{fill:#f4e3cf;stroke:#a63}.fs{fill:#f6e2e2;stroke:#a33}.title{font:600 13px sans-serif;fill:#222}.fam{font:700 11px sans-serif;fill:#222}.met{font:10px monospace;fill:#222}.note{font:italic 10px sans-serif;fill:#555}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">DCGM cockpit: 12 metrics in 4 families&lt;/text>
&lt;rect x="20" y="40" width="195" height="290" class="b fc"/>
&lt;text x="117" y="60" text-anchor="middle" class="fam">COMPUTE&lt;/text>
&lt;text x="30" y="90" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="105" class="met">SM_OCCUPANCY&lt;/text>
&lt;text x="30" y="135" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="150" class="met">PIPE_TENSOR_ACTIVE&lt;/text>
&lt;text x="30" y="180" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="195" class="met">DRAM_ACTIVE&lt;/text>
&lt;text x="30" y="240" text-anchor="start" class="note">Is compute working or&lt;/text>
&lt;text x="30" y="254" text-anchor="start" class="note">waiting on HBM?&lt;/text>
&lt;rect x="220" y="40" width="195" height="290" class="b fm"/>
&lt;text x="317" y="60" text-anchor="middle" class="fam">MEMORY&lt;/text>
&lt;text x="230" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="230" y="105" class="met">FB_USED&lt;/text>
&lt;text x="230" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="230" y="150" class="met">FB_FREE&lt;/text>
&lt;text x="230" y="180" class="met">DCGM_FI_DEV_NVLINK_&lt;/text>&lt;text x="230" y="195" class="met">BANDWIDTH_TOTAL&lt;/text>
&lt;text x="230" y="240" class="note">Is there VRAM left for&lt;/text>
&lt;text x="230" y="254" class="note">new requests?&lt;/text>
&lt;rect x="420" y="40" width="195" height="290" class="b ft"/>
&lt;text x="517" y="60" text-anchor="middle" class="fam">THERMAL · POWER&lt;/text>
&lt;text x="430" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="430" y="105" class="met">GPU_TEMP&lt;/text>
&lt;text x="430" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="430" y="150" class="met">POWER_USAGE&lt;/text>
&lt;text x="430" y="180" class="met">DCGM_FI_DEV_CLOCK_&lt;/text>&lt;text x="430" y="195" class="met">THROTTLE_REASONS&lt;/text>
&lt;text x="430" y="240" class="note">Healthy hardware or&lt;/text>
&lt;text x="430" y="254" class="note">silently limiting?&lt;/text>
&lt;rect x="620" y="40" width="180" height="290" class="b fs"/>
&lt;text x="710" y="60" text-anchor="middle" class="fam">HEALTH&lt;/text>
&lt;text x="630" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="105" class="met">XID_ERRORS&lt;/text>
&lt;text x="630" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="150" class="met">ECC_DBE_VOL_TOTAL&lt;/text>
&lt;text x="630" y="180" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="195" class="met">RETIRED_DBE&lt;/text>
&lt;text x="630" y="240" class="note">Is the silicon&lt;/text>
&lt;text x="630" y="254" class="note">degrading right now?&lt;/text>
&lt;text x="410" y="350" text-anchor="middle" class="note">Each family answers a different question · none suffices alone&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="family-1--compute">Family 1 — Compute&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/strong>: ratio of active warps per SM over the maximum possible. Value between 0 and 1.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green&lt;/strong>: 0.30–0.70 (typical LLM decode regime).&lt;/li>
&lt;li>&lt;strong>Amber&lt;/strong>: &amp;lt; 0.20 sustained (batch too small, GPU under-used in parallelism).&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: 0.95 sustained with low DRAM_ACTIVE (pathological kernel saturating the SMs).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/strong>: % of cycles with tensor cores executing. The key metric for &amp;ldquo;is compute producing?&amp;rdquo;.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green in prefill&lt;/strong>: 50–80 %.&lt;/li>
&lt;li>&lt;strong>Green in decode&lt;/strong>: 15–30 % (decode is memory-bound, this is not a symptom of a problem).&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;lt; 5 % sustained in prefill, or the engine is not using the tensor cores at all (bad config, incompatible format).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/strong>: % of cycles with HBM transferring data. The key metric for detecting memory saturation.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green in decode&lt;/strong>: 60–85 %.&lt;/li>
&lt;li>&lt;strong>Amber&lt;/strong>: &amp;gt; 90 % sustained (HBM is a firm bottleneck, which explains high TPOT).&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;gt; 95 % sustained with KV cache pool &amp;lt; 70 % (something other than the engine is asking for HBM; investigate leaks).&lt;/li>
&lt;/ul>
&lt;h3 id="family-2--memory">Family 2 — Memory&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code>&lt;/strong>: Frame Buffer (HBM) used, in MiB.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green&lt;/strong>: 70–85 % of total.&lt;/li>
&lt;li>&lt;strong>Amber&lt;/strong>: 86–92 %.&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;gt; 92 % (risk of OOM on the next paged-attention allocation).&lt;/li>
&lt;/ul>
&lt;p>PromQL for the cluster-wide percentage: &lt;code>100 * sum(DCGM_FI_DEV_FB_USED) / sum(DCGM_FI_DEV_FB_TOTAL)&lt;/code>.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_FB_FREE&lt;/code>&lt;/strong>: free Frame Buffer. Complementary to the previous one; useful for absolute alerts (&lt;code>&amp;lt; 4096 MiB free&lt;/code>).&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>&lt;/strong>: aggregate NVLink bandwidth in MB/s. For TP (tensor parallel) topologies that cross GPUs over NVLink, this metric reveals whether the parallelism split is saturating the bus.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green&lt;/strong>: varies with topology. On 4×H100 SXM with NVLink 4.0, theoretical capacity is 450 GB/s per GPU. A typical TP=4 regime: 50–150 GB/s sustained.&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;gt; 90 % of capacity sustained (check whether the model would fit with lower TP or with pipeline parallel).&lt;/li>
&lt;/ul>
&lt;h3 id="family-3--thermal-and-power">Family 3 — Thermal and power&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>&lt;/strong>: die temperature in °C.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green&lt;/strong>: &amp;lt; 75 °C.&lt;/li>
&lt;li>&lt;strong>Amber&lt;/strong>: 75–82 °C.&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;gt; 83 °C (close to the H100&amp;rsquo;s automatic thermal throttle; check ventilation, air flow, rack inlet temperature).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/strong>: draw in watts. For the H100 SXM, nominal TDP is 700 W. Useful for three things: spotting an unusually low workload (suspect idle or a stall), billing real energy cost, and firing alerts if the draw approaches the PDU limit.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/strong>: encoded bitmap with the active throttle reasons. This is the metric that &lt;strong>silently explains&lt;/strong> TPOT degradations.&lt;/p>
&lt;p>Relevant bits:&lt;/p>
&lt;ul>
&lt;li>&lt;code>0x0000000000000001&lt;/code>: Idle (not a problem).&lt;/li>
&lt;li>&lt;code>0x0000000000000002&lt;/code>: App clocks setting.&lt;/li>
&lt;li>&lt;code>0x0000000000000004&lt;/code>: SW Power Cap (software limit, e.g. from &lt;code>nvidia-smi -pl&lt;/code>).&lt;/li>
&lt;li>&lt;code>0x0000000000000008&lt;/code>: HW Slowdown.&lt;/li>
&lt;li>&lt;code>0x0000000000000010&lt;/code>: Sync Boost (NVIDIA Sync).&lt;/li>
&lt;li>&lt;code>0x0000000000000020&lt;/code>: SW Thermal Slowdown (software thermal limit).&lt;/li>
&lt;li>&lt;code>0x0000000000000040&lt;/code>: HW Thermal Slowdown (hardware thermal limit, an emergency).&lt;/li>
&lt;li>&lt;code>0x0000000000000080&lt;/code>: HW Power Brake Slowdown (PSU voltage drop).&lt;/li>
&lt;li>&lt;code>0x0000000000000100&lt;/code>: Display Clock Setting.&lt;/li>
&lt;/ul>
&lt;p>Any throttle other than &lt;code>Idle&lt;/code> with a sustained value &amp;gt; 0 &lt;strong>is an alert&lt;/strong>. TPOT degradation with &lt;code>DRAM_ACTIVE&lt;/code> already high and thermal throttle active is the classic &amp;ldquo;the rack is badly ventilated, it is not the engine&amp;rsquo;s fault&amp;rdquo;.&lt;/p>
&lt;h3 id="family-4--health">Family 4 — Health&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_XID_ERRORS&lt;/code>&lt;/strong>: cumulative counter of driver XID errors. XIDs are critical event codes that NVIDIA documents exhaustively (XID 13: graphics engine exception; XID 31: GPU memory page fault; XID 43: reset channel verif error; XID 79: GPU has fallen off the bus; XID 95: uncontained ECC error; and so on). &lt;strong>Any increment is an immediate alert&lt;/strong>: many XIDs require a node reset or an RMA of the GPU.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>&lt;/strong>: volatile double-bit ECC errors (uncorrectable). Unlike single-bit ones (which ECC corrects silently and which are counted in &lt;code>DCGM_FI_DEV_ECC_SBE_*&lt;/code>), double-bit errors &lt;strong>corrupt data&lt;/strong>. Any value &amp;gt; 0 is a critical alert: the GPU must be drained and inspected.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>&lt;/strong>: physical HBM pages retired because of accumulated double-bit errors. NVIDIA retires faulty pages automatically to prevent future corruption. More than 4–8 retired pages on one GPU suggests silicon degradation: document it and plan a replacement in the next maintenance window.&lt;/p>
&lt;h2 id="the-five-inference-engine-metrics-vllm">The five inference engine metrics (vLLM)&lt;/h2>
&lt;p>The DCGM metrics answer &amp;ldquo;is the GPU healthy?&amp;rdquo;. The engine metrics answer &amp;ldquo;is the service meeting the SLO?&amp;rdquo;. Without them, you know the hardware works but you do not know whether the clients are happy.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:num_requests_running&lt;/code>&lt;/strong>: requests currently in the batch. If it reaches the configured &lt;code>--max-num-seqs&lt;/code> and does not come down, the engine is saturated on concurrency (check VRAM and rebalance through the autoscaler, see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLM on Kubernetes&lt;/a>).&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/strong>: requests queued, not yet in the batch. Any value &amp;gt; 0 sustained over minutes means the cluster is not scaling with the load. &lt;strong>This is the primary metric for HPA&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/strong>: % of the KV cache pool in use.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Green&lt;/strong>: 50–80 %.&lt;/li>
&lt;li>&lt;strong>Amber&lt;/strong>: 80–92 %.&lt;/li>
&lt;li>&lt;strong>Red&lt;/strong>: &amp;gt; 92 % (risk of &lt;strong>preempt-on-OOM&lt;/strong>: vLLM will drop requests to free memory, which raises TTFT visibly).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>vllm:time_to_first_token_seconds&lt;/code>&lt;/strong>: histogram of TTFT per request. Consumed as &lt;code>histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket[5m])))&lt;/code>. Compared against the TTFT P95 SLO, it fires the primary service alert.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:time_per_output_token_seconds&lt;/code>&lt;/strong>: histogram of TPOT. Equivalent to the previous one but for streaming smoothness. Compared against the TPOT P95 SLO, it fires the secondary alert.&lt;/p>
&lt;h2 id="the-six-alerts-that-must-page-in-production">The six alerts that must page in production&lt;/h2>
&lt;p>Any serious production cluster fires these six alerts to a channel with an on-call rotation. Without them, the SLO is met by luck, not by process.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">groups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpu-llm-critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuHbmNearOom&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w"> &lt;/span>*&lt;span class="w"> &lt;/span>&lt;span class="l">(DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL) &amp;gt; 92&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;HBM on {{ $labels.gpu }} at {{ $value }}% — OOM risk&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuThermalOrPowerThrottle&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">(DCGM_FI_DEV_CLOCK_THROTTLE_REASONS != 0) and ignoring(reason) (DCGM_FI_DEV_CLOCK_THROTTLE_REASONS != 1)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;GPU {{ $labels.gpu }} throttling (reasons={{ $value }})&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuXidErrorDetected&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">increase(DCGM_FI_DEV_XID_ERRORS[5m]) &amp;gt; 0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;XID error on GPU {{ $labels.gpu }} — investigate immediately&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuEccDoubleBit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DCGM_FI_DEV_ECC_DBE_VOL_TOTAL &amp;gt; 0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ECC double-bit on GPU {{ $labels.gpu }} — drain node&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">VllmKvCachePoolNearFull&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm:gpu_cache_usage_perc &amp;gt; 0.95&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;KV cache pool &amp;gt; 95% on {{ $labels.instance }}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">VllmTtftP95OutOfSlo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">histogram_quantile(0.95, sum by(le, instance)(rate(vllm:time_to_first_token_seconds_bucket[5m]))) &amp;gt; 1.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;TTFT P95 over SLO ({{ $value }}s &amp;gt; 1.5s)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>These six cover 80 % of the incidents that affect the SLO. The remaining 20 % demand investigation with tracing (see &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a>).&lt;/p>
&lt;h2 id="master-table-thresholds-and-queries">Master table: thresholds and queries&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Green&lt;/th>
&lt;th>Amber&lt;/th>
&lt;th>Red&lt;/th>
&lt;th>Base query (PromQL)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SM occupancy&lt;/td>
&lt;td>0.30–0.70&lt;/td>
&lt;td>0.15–0.30&lt;/td>
&lt;td>&amp;lt; 0.10 sustained&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tensor active (decode)&lt;/td>
&lt;td>15–30 %&lt;/td>
&lt;td>&amp;lt; 10 %&lt;/td>
&lt;td>&amp;lt; 3 %&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DRAM active&lt;/td>
&lt;td>60–85 %&lt;/td>
&lt;td>85–95 %&lt;/td>
&lt;td>&amp;gt; 95 % with low KV&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FB used&lt;/td>
&lt;td>70–85 %&lt;/td>
&lt;td>86–92 %&lt;/td>
&lt;td>&amp;gt; 92 %&lt;/td>
&lt;td>&lt;code>100 * DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVLink BW&lt;/td>
&lt;td>&amp;lt; 70 % cap&lt;/td>
&lt;td>70–90 % cap&lt;/td>
&lt;td>&amp;gt; 90 % cap&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU temp&lt;/td>
&lt;td>&amp;lt; 75 °C&lt;/td>
&lt;td>75–82 °C&lt;/td>
&lt;td>&amp;gt; 83 °C&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power usage&lt;/td>
&lt;td>&amp;lt; 90 % TDP&lt;/td>
&lt;td>90–98 % TDP&lt;/td>
&lt;td>&amp;gt; 98 % TDP&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throttle reasons&lt;/td>
&lt;td>0 or Idle&lt;/td>
&lt;td>App/SW&lt;/td>
&lt;td>HW Therm/Power&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>XID errors&lt;/td>
&lt;td>no change&lt;/td>
&lt;td>—&lt;/td>
&lt;td>any delta&lt;/td>
&lt;td>&lt;code>increase(DCGM_FI_DEV_XID_ERRORS[5m])&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ECC DBE&lt;/td>
&lt;td>0&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&amp;gt; 0&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Retired pages&lt;/td>
&lt;td>&amp;lt; 4&lt;/td>
&lt;td>4–8&lt;/td>
&lt;td>&amp;gt; 8&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KV cache used&lt;/td>
&lt;td>50–80 %&lt;/td>
&lt;td>80–92 %&lt;/td>
&lt;td>&amp;gt; 92 %&lt;/td>
&lt;td>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Requests waiting&lt;/td>
&lt;td>0&lt;/td>
&lt;td>1–5 sustained&lt;/td>
&lt;td>&amp;gt; 10 sustained&lt;/td>
&lt;td>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT P95&lt;/td>
&lt;td>&amp;lt; SLO&lt;/td>
&lt;td>80–100 % SLO&lt;/td>
&lt;td>&amp;gt; SLO&lt;/td>
&lt;td>see alert query above&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT P95&lt;/td>
&lt;td>&amp;lt; SLO&lt;/td>
&lt;td>80–100 % SLO&lt;/td>
&lt;td>&amp;gt; SLO&lt;/td>
&lt;td>&lt;code>histogram_quantile(0.95, sum by(le)(rate(vllm:time_per_output_token_seconds_bucket[5m])))&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="three-pitfalls-that-confuse-the-junior-operator">Three pitfalls that confuse the junior operator&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — &amp;ldquo;GPU-Util at 99 % = saturated&amp;rdquo;.&lt;/strong> As explained at the start, &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> lights up with any kernel. The right move is to look at the three &lt;code>_PROF_*&lt;/code> metrics (SM occupancy, tensor active, DRAM active) together. &lt;em>GPU util 99 % + tensor active 8 % + DRAM active 92 %&lt;/em> means &amp;ldquo;saturated by memory, not compute&amp;rdquo;; &lt;em>GPU util 99 % + tensor active 75 % + DRAM active 50 %&lt;/em> means &amp;ldquo;saturated by compute, prefill heavy&amp;rdquo;. The two situations call for different levers.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — confusing single-bit ECC (SBE) with double-bit (DBE).&lt;/strong> Single-bit errors are corrected silently and are &lt;strong>unavoidable&lt;/strong> in any HBM under load (cosmic radiation, voltage fluctuations). An SBE counter growing slowly is not an alert, it is physics. DBE is: it corrupts data. Telling the two metrics apart avoids false alarms and false negatives in equal measure.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — alerting on &lt;code>num_requests_waiting &amp;gt; 0&lt;/code> without context.&lt;/strong> An instantaneous value of 1 or 2 during a spike is normal. What matters is a &lt;strong>sustained&lt;/strong> queue: use &lt;code>for: 5m&lt;/code> with a threshold of 3–5. Without that window, the system floods the alert channel with noise.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For a generic cluster of &lt;strong>4×H100 SXM 80 GB with intra-node NVLink&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>DCGM Exporter deployed through the NVIDIA GPU Operator, one DaemonSet per GPU node.&lt;/li>
&lt;li>Internal Prometheus with 30-day retention for high-frequency metrics, 1 year for downsampled ones (Thanos/Mimir if the volume justifies it).&lt;/li>
&lt;li>Grafana with three standard dashboards: GPU hardware (DCGM), engine (vLLM), SLO (TTFT/TPOT/RPS against written targets).&lt;/li>
&lt;li>Alertmanager with an on-call rotation and rate limiting through silences grouped by node.&lt;/li>
&lt;li>Cardinality kept under control: &lt;code>gpu&lt;/code> (local id), &lt;code>node&lt;/code>, &lt;code>pod&lt;/code>, &lt;code>model&lt;/code>. Do not add &lt;code>request_id&lt;/code> or other high-cardinality labels to metrics; that is tracing&amp;rsquo;s job.&lt;/li>
&lt;/ul>
&lt;p>Estimated volume for a 16-GPU cluster scraped every 15 s: around 2 million samples/min, around 25 GB/day of raw Prometheus. Manageable with one Prometheus per cluster plus retention; if the team scales beyond 64 GPUs, consider a Thanos sidecar or VictoriaMetrics. See &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">OSS LLMOps tooling catalogue&lt;/a> for equivalent alternatives.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Tracing of LLM workloads&lt;/strong>: already covered in &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Autoscaling&lt;/strong> based on these metrics: see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLM on Kubernetes&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Incident response runbooks&lt;/strong>: how each of these alerts translates into concrete action (drain, restart, RMA, scaling, rollback).&lt;/li>
&lt;li>&lt;strong>Cost accounting&lt;/strong>: using &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> and &lt;code>vllm:request_success_total&lt;/code> for per-tenant cost showback.&lt;/li>
&lt;li>&lt;strong>Multi-tenant fairness monitoring&lt;/strong>: when several tenants share a cluster, which metrics detect that one of them is hogging the KV cache.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy on the GPU cluster: who pays for what&lt;/a> — DCGM utilisation as the cluster&amp;rsquo;s FinOps governance metric.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the other half of observability.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — what was sized and, therefore, which thresholds are defensible here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> — explains why &lt;code>num_requests_running&lt;/code>, &lt;code>num_requests_waiting&lt;/code> and &lt;code>gpu_cache_usage_perc&lt;/code> are the engine&amp;rsquo;s operational metrics.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels&lt;/a> — LLM-aware observability lives at level 4.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Seven layers of the on-premise LLM inference stack&lt;/a> — DCGM Exporter is a piece of the platform layer.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLM on Kubernetes&lt;/a> — uses these metrics as input.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-dcgm-vllm-metrics-analogies-documented-anomalies/">Anatomy of the twelve DCGM and five vLLM metrics&lt;/a> — a deeper dive with an analogy and a documented production anomaly for each metric, with figures from public incidents (Meta Llama 3, &lt;em>Story of Two GPUs&lt;/em>, vLLM issues, Dell/Lenovo KBs).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">Incident response runbooks for LLM with Keep + Kafka&lt;/a> — the translation of each critical alert into concrete action (drain, reset, RMA, rollback) with workflow YAML, a WORM Kafka schema and the fit with ISO 27035, ENS, NIS2 and EU AI Act art. 73.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/nvlink-nvswitch-nccl-cable-every-token-crosses/">NVLink, NVSwitch and NCCL: the cable every token travels through&lt;/a> — the NVLink counters (&lt;code>nvidia-smi nvlink -e&lt;/code>, TX/RX bytes per link, CRC errors) that these DCGM metrics expose: a slow all-reduce shows up in a cable error counter before it shows up in API latency.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/">GPU utilisation as a FinOps lever&lt;/a> — from the DCGM metrics (GPU_UTIL vs SM_ACTIVE, MFU) to the cost of idle and the cost per token.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>NVIDIA — &lt;em>DCGM Exporter&lt;/em> (repo &lt;code>nvidia/dcgm-exporter&lt;/code>, documented metrics and units).&lt;/li>
&lt;li>NVIDIA — &lt;em>DCGM Field Identifiers reference&lt;/em> (complete list of &lt;code>DCGM_FI_*&lt;/code>).&lt;/li>
&lt;li>NVIDIA — &lt;em>XID Errors documentation&lt;/em> (catalogue of XID codes and remediation procedures).&lt;/li>
&lt;li>NVIDIA — &lt;em>NVIDIA GPU Operator&lt;/em> (official Helm chart).&lt;/li>
&lt;li>vLLM project — &lt;code>examples/production_monitoring/&lt;/code> (reference PromQL and Grafana dashboards).&lt;/li>
&lt;li>Prometheus — &lt;em>Histogram and summary best practices&lt;/em> (for building defensible percentile queries).&lt;/li>
&lt;li>NVIDIA — &lt;em>H100 Tensor Core GPU datasheet&lt;/em> (TDP, HBM bandwidth, NVLink capacities).&lt;/li>
&lt;/ul></description></item></channel></rss>