Nameplate Analytics LLC Book a snapshot
Measurement record · 2026-09-16

The metric everyone plans capacity with, tested against the work.

We ran one H100 through a protocol registered before the instance was rented: an idle baseline, a vLLM server idle and at three request rates, and one training job in two configurations, with every serving and training window run twice and GPU utilization, five profiling fields and power sampled at one hertz. The utilization fields showed whether the GPU was busy. None of them showed how much work it did.

01 · The record

One training job, two configurations, the same reading.

Capture H100 80GB HBM3 · Lambda driver 580.105.08 DCGM 4.7.0 1 Hz 15 windows
Repeat 1 of the training pair. Amber bars are reported utilization on the percent axis. Cyan bars are real tokens per second from the training log, on their own scale where the tuned run is full length. The utilization bars are nearly identical; the work differs 26.6 times. The tuned run moved about 5.4 times as many token slots a second, from the other differences in its setup (bf16 against fp32, batch 16 against 4, and the untuned run's per-step CPU tokenization), and padding every sequence to 512 left about one in five of the untuned run's slots holding a real token, against almost all of the tuned run's. Throughput is post-hoc: it was not part of the registered analysis. Power was 577 W untuned and 597 W tuned.
Scenario Sampleskept GPUTL% GRACT% SMACT% SMOCC% TENSO% DRAMA% PowerW GPUTL / SMACT
baseline_empty300.00.00.00.00.00.072N/A
control_proftester_tenso6075.975.767.812.467.122.65641.1x
r1_vllm_idle_loaded2100.00.00.00.00.00.0116N/A
r1_vllm_rate_0.251026.727.517.92.50.618.62031.5x
r1_vllm_rate_251096.096.464.39.33.264.74341.5x
r1_vllm_saturated51085.985.670.012.124.650.85501.2x
r2_vllm_idle_loaded2100.00.00.00.00.00.0117N/A
r2_vllm_rate_0.251027.927.517.92.50.618.72041.6x
r2_vllm_rate_251096.496.464.39.33.164.74321.5x
r2_vllm_saturated51085.985.870.112.124.750.85481.2x
r1_train_untuned51099.599.590.022.61.518.95771.1x
r1_train_tuned51099.399.487.654.319.158.85971.1x
r2_train_untuned51099.599.590.022.61.518.95771.1x
r2_train_tuned51099.399.487.554.219.158.85961.1x
baseline_empty_end300.00.00.00.00.00.072N/A
dcgmi dmon -i 0 -e 203,1001,1002,1003,1004,1005,155 -d 1000 -c 600

The registered analysis, every row as the protocol computed it: the mean of each field after dropping 45 seconds at each end of the window, including the rows where GPUTL and SMACT agree. Field 203 is DCGM_FI_DEV_GPU_UTIL, 1001 to 1005 are the profiling fields GRACT, SMACT, SMOCC, TENSO and DRAMA, and 155 is power. The two saturated rows include 72 idle seconds each, because the benchmark client generated its prompts inside the window; they are published as registered and corrected post-hoc below. The protocol, deviations, results and raw run directory, with every dcgmi log, benchmark and training output and environment capture, are on GitHub.

Limits: one H100 80GB on Lambda. One serving model, Qwen2.5-7B-Instruct, with synthetic random prompts. One 0.5B training model on wikitext-2. The untuned configuration was constructed for the test (fp32, padding to 512, batch 4, per-step CPU tokenization), and no claim is made about how common it is. Single GPU, no multi-GPU traffic.

02 · Work done, post-hoc

What the GPU produced while those readings were taken.

None of this section was registered. It was decided after the run, computed by posthoc.py from the same files, and it is labeled post-hoc wherever it appears.

Training · post-hoc Qwen2.5-0.5B · wikitext-2600 s per run
Scenario GPUTLregistered % SMACTregistered % Real tokens/spost-hoc Token slots/spost-hoc
r1_train_untuned99.590.02,275.711,107
r1_train_tuned99.387.660,527.660,529
r2_train_untuned99.590.02,275.311,103
r2_train_tuned99.387.560,471.060,474

From the JSON line each run prints at the end of its window. A token slot is one position in a batch: steps times batch size times 512. The untuned run processed 3,255 steps of 4 sequences of 512 in 600.2 seconds, 11,107 slots a second, of which about one in five held a real token.

Inference · post-hoc, measured from load start Qwen2.5-7B-Instruct · vLLM 0.29.0256 output tokens per request
Scenario Load timeseconds Completedrequests Ratereq/s GPUTL% SMACT% GPUTL / SMACT
r1_vllm_rate_0.25901180.2026.617.71.50x
r1_vllm_rate_25891,1842.0196.264.31.50x
r1_vllm_saturated48312,10325.06100.081.51.23x
r2_vllm_rate_0.25901180.2027.717.71.56x
r2_vllm_rate_25891,1842.0196.664.31.50x
r2_vllm_saturated48312,15625.17100.081.71.22x

Means run from the first busy sample to 45 seconds before the end of the window; the rate is completed requests over seconds from that first sample to the end of the window. In the saturated windows traffic started at second 117, so their load time is 483 seconds against 589 and 590 for the rate windows. For the rate windows, where traffic started before the 45 second trim, this method lands within 0.2 points of the registered GPUTL and SMACT.

GPUTL read 96.0 at 2 requests a second (registered) and 100.0 from load start at saturation, with SMACT at 81.5 (post-hoc). The completed request rates were 2.01 a second over 589 seconds and 25.06 a second over 483 seconds, 12.5 times apart (post-hoc). At 0.2 requests a second the GPU completed requests at 0.8 percent of the saturated rate while GPUTL read 26.7 (registered), 27 percent of the saturated load-start reading (post-hoc).

03 · What the data supports

The utilization fields measure busy, not work.

On these loads GPUTL exceeded SMACT by 1.1 to 1.6 times in the registered rows. Measured from load start, the saturated ratio is 1.23 in repeat 1 and 1.22 in repeat 2 (post-hoc). The gap between the two fields is real and modest. The larger problem is that neither field tells you how much work was done.

In training, GPUTL read 99.5 and 99.3 and SMACT 90.0 and 87.6 for two configurations 26.6 times apart in real tokens per second: about 5.4 times in token slots per second and about 4.9 times in the share of slots holding a real token. In serving, GPUTL read 96.0 at 2 requests a second and 100.0 at saturation, about 12.5 times apart in completed requests a second (post-hoc). Over that step SMACT rose from 64.3 to 81.5, 1.27 times. At low load both fields did rise with traffic, GPUTL from 26.7 at 0.2 requests a second to 96.0 at 2, so what they failed to show was the capacity still unused once the GPU looked busy.

Idle is the case the standard field does get right. A loaded vLLM server with no traffic read 0 on every utilization field while drawing 116 and 117 watts in the two repeats, so a GPU doing nothing is visible without profiling metrics. TENSO is not a work meter either: it follows numeric precision, reading 1.5 for the fp32 run and 19.1 for the bf16 run. The two repeats agree within 1.2 points on every percentage field and within 2 watts on power.

What to collect instead

Your engineers export the profiling fields per workload, and we join them to pod identity and read them next to the throughput the workload reports:

  • SMACT and SMOCC. Streaming multiprocessor activity and occupancy. They show idle and busy, as GPUTL does, and on this run they did not measure work either.
  • TENSO. Tensor pipe activity. On this run it moved with fp32 against bf16, so it reads precision as much as work.
  • DRAMA. Memory bandwidth, which separates compute-bound from memory-bound before anyone right-sizes the wrong axis.
  • Throughput. Tokens, samples or requests per second from the workload itself. The only field in this list that counts work.

The profiling fields ship in the DCGM exporter, but turning them on is not free. DCGM_FI_PROF_SM_ACTIVE is commented out of the shipped default-counters.csv, so it takes a config change, and the exporter container needs SYS_ADMIN to read any of the profiling group at all. In a cluster with a security review, granting SYS_ADMIN to a DaemonSet is a conversation rather than a flag, and it is better to have that conversation now than to discover it mid-engagement. Throughput has to be joined in from the workload's own logs or metrics, which is the part that needs an engineer.

04 · First measurement, 2026-09-04

A Tesla T4 and a tiny repeated kernel.

Capture · 2026-09-04 Tesla T4 · g4dn.xlarge driver 595.91.07 1 Hz 242 samples
Workload GPUTLreported utilization SMACTmultiprocessors active TENSOtensor pipe active
Tiny repeated opA 64-element add launched in a tight loop. 20.0% 0.2% 0.0%
fp16 matmul4096 by 4096 half-precision GEMM, the control. 100.0% 98.1% 87.8%
dcgmi dmon -e 203,1001,1002,1003,1004,1005 -d 1000

This was the first record on this page. The workload was a tiny repeated kernel built to show the failure mode: a 64-element add occupies one of a T4's forty streaming multiprocessors, and launched in a tight loop it keeps something resident in nearly every sample window. That gap belonged to that kernel. On the realistic H100 loads above, GPUTL ran 1.1 to 1.6 times SMACT. All six fields for all 242 samples and the Terraform and workloads remain published.

05 · The join

Telemetry alone is a graph. Joined to the bill, it is a number your CFO can act on.

Hardware telemetry tells you a GPU did nothing. It does not tell you what that cost. For that, the billing side has to be right first, and the billing side has two traps that catch almost everyone.

Amortization

Commitment-covered resources report $0 unblended cost. A reserved or Savings-Plan-covered GPU node shows as free in a naive cost ranking, so the single most expensive machine in your fleet vanishes from the top of the list. Every figure we produce is amortized: the commitment is spread across the resources that actually consumed it.

Attribution

A CUR 2.0 export at hourly granularity with resource IDs, normalized across the legacy and 2.0 schemas, then joined to the cluster through provider IDs so a line item becomes a node, a node becomes a namespace, and a namespace becomes a team. Whatever will not join is reported as unattributed rather than quietly distributed, because unattributed spend is itself one of the findings.

The tooling

The analysis runs through gpuaudit, a purpose-built CLI rather than a spreadsheet with your name typed into the header. It reads Parquet and CSV, legacy CUR and CUR 2.0, a single file or a whole partitioned export prefix, with the format and dialect detected rather than configured. Map columns are read as maps, which is the detail most often got wrong and the one that fails silently: get it wrong and every tag reads as null, which quietly converts proven findings into likely ones.

Eight detectors run against billing data alone, spanning all three confidence ratings, and two more run when cluster data is supplied. Each returns a finding with its rating attached, or returns nothing rather than inventing a small number. The output is a client-ready document and a machine-readable findings file, so your engineers can check the arithmetic themselves.

A regression suite of more than 700 tests covers it, which is not a vanity number: the failure mode that matters in billing analysis is silent. Nothing raises, no column is missing, the totals just quietly come out weaker or stronger than the truth. Tests are the only thing that catches that class of bug before it reaches your report.

Cluster allocation

The allocation module reads GPU capacity and pod requests through kubectl or the Python client, maps each node to its EC2 instance through spec.providerID, and joins that to per-resource amortized cost. It ships the exact read-only RBAC the collector needs, list on nodes and pods and get on the kube-system namespace and nothing else, and the collector is a standalone shell script your team runs without installing anything, sending us the one file it writes.

This is what makes the free call's most persuasive number real: how many provisioned GPUs have no pod requesting one. If a node advertises eight and nothing asks for any, utilization is zero by definition and no telemetry argument can rescue it.

06 · Limits

What this measurement does not prove.

One H100 80GB (DCGM reports H100 80GB HBM3) on Lambda. One serving model, Qwen2.5-7B-Instruct, with synthetic random prompts. One 0.5B training model on wikitext-2. The untuned configuration was constructed for the test (fp32, padding to 512, batch 4, per-step CPU tokenization), and no claim is made about how common it is. Single GPU, no multi-GPU traffic.

What the run establishes is narrow and worth stating narrowly: on this GPU, the utilization fields did not show how much work was done. In training they read the same for two configurations of one job 26.6 times apart in throughput. In serving, GPU utilization read 96.0 percent at a completed request rate 12.5 times lower than the same GPU reached at saturation (post-hoc). It does not establish that yours are misleading, by how much, or on which workloads. Nobody can tell you that from the outside, and anyone who offers to is selling you a number they made up.

The only way to get your number is to instrument your cluster and read it next to what it produced. That is what the first call is for, and it is free.