The Per-Step Inference Predictor

A complete mechanistic model of one served LLM engine step — and an honest account of the Vidur comparison: a memorising random forest beats us on absolute accuracy on already-profiled hardware, yet cannot make the capacity-planning decision it exists for.

Non-intrusive: zero profiling traces SKU ranking: top-1 100%, Spearman +1.00 Every number traced to results/

Llama-3.1-8B shapes · bf16 · A100-80 / H100 / H200 / L40S · NVML + Nsight-Compute + cuBLAS dispatch-log telemetry

0. Framing — a predictor that is cheap, explainable, and transferable

This deep-dive documents everything from re-examining the per-step predictor ('m2'). It answers one question: how long does one served engine step take, given only the shapes of the work? The classic FLOPs/peak ÷ bytes/peak approach needs two numbers the silicon never sustains, patched with one unexplained fitted constant. We replace that constant with a small product of factors, each a concrete post-launch GPU behaviour — and pull the SM clock out as the most important.

The honest headline. The predictor is non-intrusive: it predicts from datasheet specs plus mechanism, reading ZERO profiling traces. On a GPU you have already profiled, a baseline that memorised that table will beat it on absolute accuracy — Section 5 states that loss bluntly. But capacity-planning asks 'which hardware should I buy / route to?', and there the memoriser cannot even play: it cannot rank a GPU it has never run.

1. What the predictor computes — the full per-step equation

One engine step's total time is the boxed equation below (src/mechanism/predictor.py). Every symbol is defined before use.

(1) $$\Tstep \;=\; \max\!\big(\Tblocks,\; \Nkern\,\Tlaunch\big) \;+\; \alpha(N,T_{\mathrm{new}})\,\Sn \;+\; \Tsamp$$

$\Tblocks$ is the GPU compute body. $\Nkern\,\Tlaunch$ is the host cost of launching every kernel; the $\max$ models CPU launch overlapping the GPU body. $\alpha(N,T_{\mathrm{new}})\,\Sn$ is host overhead scaling with a sampled-token function $\Sn$. $\Tsamp$ is the fixed sampling tail. The host terms are why calibrated mode fits one host-floor scalar per trace; everything in $\Tblocks$ is mechanism.

$\Tblocks$ expands over the $N_L=32$ layers as:

(1a) $$\Tblocks \;=\; N_L\big(t_{\mathrm{proj}} + t_{\mathrm{attn}}\big) \;+\; t_{\mathrm{light}}$$

$t_{\mathrm{proj}}$ is one layer's four projection GEMMs — qkv, o, gate_up, down — and $t_{\mathrm{attn}}$ is that layer's attention; both per-layer. $t_{\mathrm{light}}$ (layernorms, RoPE, residual adds, activation, KV writes + LM head) carries its own layer count. Each operator term is itself a roofline max of a compute and a memory branch:

(1c) $$t_{\mathrm{op}} \;=\; \max\!\Big(\underbrace{\tfrac{2\,M N K}{\etac\,\Pboost}}_{\text{compute branch}},\;\; \underbrace{\tfrac{\text{bytes}(M,N,K)}{\etamem\,\mathrm{BW}}}_{\text{memory branch}}\Big)$$

For a GEMM of token count $M$, output width $N$, contraction $K$: the compute branch divides $2MNK$ FLOPs by the achieved compute rate; the memory branch divides bytes (inputs + weight + output, bf16) by achieved bandwidth. $\Pboost$ is datasheet boost TFLOP/s, $\mathrm{BW}$ peak bandwidth; $\etac$ and $\etamem$ are the two efficiency factors the rest of this page is about. The larger branch is the bottleneck. This per-operator roofline-max is all of $\Tblocks$.

Why this matters. Because $\Pboost$, $\mathrm{BW}$, and the factors inside $\etac$ come from the datasheet and a one-off micro-characterisation — not from profiling the served model — the same equation prices a GEMM on a GPU you have never run. That is what makes the SKU-ranking result in Section 5 possible. Sections 2–3 build $\etac$; Section 6 shows how getting it right per operator makes the non-intrusive prediction accurate enough to rank hardware correctly.

2. The five-factor GEMM efficiency

The compute-branch efficiency $\etac$ is the single number every roofline model hides in a fitted ceiling $\etabar$. We replace it with a product of factors, each a concrete post-launch GPU behaviour:

(2) $$\etac \;=\; \fclk \,\cdot\, \etacluster \,\cdot\, \etaactive \,\cdot\, \etaduty \qquad(\eta_{\mathrm{delivery}}\equiv 1)$$

$\fclk$ — the clock factor

Sustained clock ÷ boost clock. A measured input per operating point. The term every boost model sets to 1 — and Section 3 shows it droops 20–44%.

$\etacluster$ — SM-fill / wave quantisation

How completely the grid fills the SMs. A partial last wave idles SMs. This is the factor Section 6's η_wave fix makes per-operator.

$\etaactive$ — tensor-pipe active fraction

Cycles the tensor cores issue MMA vs stall on operand movement. Plateaus near 99% at large M under a pinned clock.

$\etaduty$ — duty / tail effect

Steady-state duty of the compute pipe across the kernel, including ramp and tail. ncu-measured per cell.

Why the decomposition earns its keep

A single ceiling works on its own card but conflates a transferable quantity (geometric efficiency) with a non-transferable one (the involuntary clock). We tested transfer with leave-one-out on 39 compute-bound GEMM cells across four SKUs. The deployable form — one per-architecture ceiling (same budget as the constant) times a derived $\etacluster$, no Nsight counter at deploy — beats the constant out of sample:

GEMM efficiency model In-sample MAPE Leave-one-out MAPE
Per-architecture constant ceiling (baseline)16.87%18.75%
Deployed min(1, AI/ridge)18.10%
FIX: η_wave × per-arch ceiling (ours)9.86%11.36%
ncu-measured tc_util (oracle upper bound)7.06%

Source: results/gemm_deployable_eta.json. Leave-one-out: 18.75% constant vs 11.36% ours — a ~7-pt gap that holds out of sample, so the win is transfer, not fitting.

3. The clock is not a constant

Of Equation (2)'s four factors, the clock $\fclk$ is the one prior work most buries. Under sustained projection-GEMM load the SM clock droops with token count by 20–44% across data-centre GPUs, so a datasheet-boost roofline mis-prices throughput by a mean 29%. Exposing the clock recovers most of that error (to 9.6%).

20–44%
qkv clock droop (4 SKUs)
29.0%
boost-roofline error (MAPE)
9.6%
error with explicit clock
−15%
L40S large-M decline

The mechanism: a power-cap step

Every data-centre card shows a piecewise-decreasing curve, not one clean step. Small $M$: low power, clock at boost. As $M$ grows, board power rises to the sustained limit; the clock falls to stay within the cap; at large $M$ it settles drooped. Throughput = $\Pboost\cdot\fclk\cdot\etanoclk$, where $\etanoclk=\etacluster\cdot\etaactive\cdot\etaduty$.

Small M — boost plateau
Low power draw; clock holds at boost (H100/H200 1980 MHz). $\fclk \approx 1$.
board power reaches the sustained cap
Power-cap knee
Power saturates the cap; the controller lowers the clock. The knee depends on the cap and arithmetic intensity.
Large M — drooped sustained clock
Clock settles 20–44% below boost. Tight-cap cards: throughput declines; well-provisioned: rising $\etanoclk$ masks it.

The numbers: n=5, randomised order, with intervals

A clock depending on token count, power, and temperature is what a single ordered sweep can fake. Each GPU's full curve was measured five times in randomised order with cooldown, aggregated into a median with 95% CI. Half-widths are tight (8–33 MHz); on the H200 boost cells reproduce with zero variance.

GPUBoost (MHz) Sustained @ M=16384 95% CI ± qkv droop Large-M throughput
A100-80GB14101125±20.420.2%rises (not tight-capped)
H200-SXM19801455±10.226.5%≈ flat (−0.6%)
H100-SXM19801380±8.330.3%rises
L40S25201425±33.343.5%declines −15%

Source: results/freq_study/clocktel_ci_summary.json + per-SKU files (n=5 randomised). Droop referenced to fixed datasheet boost.

Why some cards decline and others don't; the causal probe

The droop is universal but its throughput consequence is not. On well-provisioned cards rising $\etanoclk$ outpaces the falling clock (throughput climbs, droop masked); on the tight-cap L40S the clock overwhelms it (−15% at large M). A controlled cap sweep on a local RTX 5090 (575→400 W → 2730→2452 MHz) shows the clock tracks the cap monotonically. Honest null: that card never declined (not tight-capped enough at 575 W), so the full cap→clock→decline chain remains a bounded claim. Source: results/freq_study/cross_cap_causal_5090.json.

4. Re-examining the predictor — where we win, where we lose

The right way to judge a predictor is the full scenario matrix. The wins are decisive and cheap; the one absolute-accuracy loss — against Vidur on a GPU it has already profiled — is real, and Section 5 shows it is a loss on the wrong metric.

Scenario Ours Baseline Note
DistServe OPT-13B decode1.78%poly 2.27%real engine trace · WIN
QLM estimator MAPE0.9–4.4%native 111–123%real steps; huge margin · WIN
GEMM deployable factor (LOO)11.4%constant 18.8%transfer, not fitting · WIN
freq explicit-clock9.6%datasheet 29%clock pulled out of η̄ · WIN
Vidur SKU-ranking decision (§5)top-1 100%RF: cannot rank0 profiling rows · WIN
Vidur absolute MAPE, A100 home (§5)8.4% allRF 5.3%RF memorises its table · LOSS

Sources as listed. QLM live SLO (E3) tie; E4 neutral. The single absolute-MAPE loss is dissected in Section 5.

5. The Vidur comparison — the wrong metric, and the decision it is built for

QuestionWe use REAL measured numbers and Vidur only simulates — so why can't we beat them?

Vidur's predictor exists to make capacity-planning decisions — which SKU, engine, or parallelism is best — not absolute per-operator latency on already-profiled hardware. Judged on that purpose, the comparison inverts. Three honest parts: who is intrusive, where we genuinely lose, and the decision the memoriser cannot make.

Part 1 — non-intrusive vs intrusive

m2 predicts from datasheet specs + mechanism, ZERO profiling traces. Vidur's RF is intrusive: it interpolates its own profiled per-operator table — the model must first run on each SKU (~44k rows/GPU) before it can predict there.

Part 2 — where we genuinely lose (be blunt)

On the A100 home SKU, full per-step MAPE — ours 21.1% prefill / 7.6% decode / 8.4% all, vs RF 0.88% / 5.6% / 5.3%. We lose on absolute accuracy because the RF memorises the table that IS the ground truth, and we use none of it. Stated plainly.

Part 3 — the decision Vidur exists for

Rank {A100-80, H100, H200, L40S} by GEMM step time per workload. m2, zero profiling, gets top-1 hit-rate 100% and mean Spearman +1.00 over 13 workloads. The RF can score only its one profiled home SKU — it cannot rank hardware it has never run, so it cannot make the decision at all.

100%
m2 top-1 SKU hit-rate
+1.00
m2 mean Spearman
1 / 4
SKUs the RF can score
0
profiling rows m2 needs

The ranking is not trivial. The correct order changes across the token range — at small $M$ the bandwidth-rich H200 leads (projections are memory-bound), and the order shifts among H100/H200/A100 as the work becomes compute-bound. m2 tracks every ordering from datasheet specs alone, including the bandwidth-vs-compute crossover — exactly the judgement a capacity planner needs and a single-SKU profiler cannot supply.

Headline. m2 'loses' only on the wrong metric — absolute accuracy on hardware already profiled. On the decision Vidur is actually built for — rank unprofiled hardware before you buy or route to it — the non-intrusive mechanism wins, because the table-memorising random forest cannot even play. Section 8's cross-GPU transfer test is the full-step confirmation.

Sources: results/vidur_decision_quality.json and part2_integrations/vidur/accuracy_summary.json.

6. Why the non-intrusive prediction is accurate enough to rank correctly

Ranking hardware from specs only works if the per-step prediction is accurate enough that true orderings are not flipped by model error. We found and fixed three real GEMM-level errors eating into that margin. They are presented as the reason the decision result holds — not a standalone accuracy chase — and the last is a cautionary tale about which metric to optimise.

6.1 η_wave — per-operator SM-fill from the cuBLAS dispatch-log tile validated

SymptomThe served GEMM set η_wave→1 (full SM fill). Accurate at large M (−2%) but under-predicts small/mid-M GEMMs 10–20%: M=64 −8.4%, M=256 −13.9%, M=1024 −12.2%, M=4096 −2.6%.
Root causeAt small/mid M the grid doesn't fill the 108 SMs; the last wave is partial, so occupancy < 1. The grid depends on the actual cuBLAS output tile, which changes with M (a tile ladder).
FixRead the actual tile per (op, M) from the cuBLAS dispatch logs, build a tile-selection ladder, compute grid = ⌈M/Mt⌉·⌈N/Nt⌉, apply tail-effect η_wave = w/(w+0.5), w = grid/N_SM — one constant, not fit. Compute branch only.

The ladder is read from the logs — qkv: 64×64 from M=64, 128×128 at M=256, 256×128 at M=512, 128×256 at M=4096. Held-out: 25/36 exact tile hits, η_wave MAPE 5.77%, so it generalises. η_wave more than halves the aggregate served-projection error (8.12% → 3.64%); the principal GEMM fix. Source: results/gemm_etawave_served_fix.json.

6.2 Per-operator η̄ — one ceiling per projection, not one per GPU validated

A second, independent bias lived in the large-M compute branch. A single $\etabar=0.75$ over-predicts the narrow projections — measured per-operator as qkv 0.675, o 0.703, gate_up 0.744, down 0.748. The single 0.75 made qkv/o under-predict large-M time by 8.5%/3.9%; per-operator ceilings cut the bias toward −0.4%. Caveat: $\etabar$ and η_wave are not freely separable (the η_wave tail constant was calibrated against 0.75), so they are applied together with the down projection gated out. This is the recommended deployable stack: it improves the served-projection aggregate from 3.64% to 3.28%. Source: results/gemm_perop_etabar.json, results/gemm_full_fix_vidur.json.

6.3 Size-dependent η_mem — correct per-operator, yet it hurts the step refinement, not a step fix

The third error was a per-operator memory-branch spread a single $\etamem=0.75$ cannot match: at M=1 o-proj is −10.5% while gate_up is +10.3%. The cause is real — achieved HBM bandwidth ramps with weight-read size, so a 33 MB o-proj weight saturates HBM far less than a 235 MB gate_up. The fix η_mem(W) = 0.891 − 1.171/√(W_MB) tightens the per-operator memory-branch MAPE (M≤16) from 5.3% to 2.8%, held-out 2.9%.

The honest twist — per-op accuracy ≠ step accuracy. This fix is unambiguously correct per operator, yet it makes the per-STEP prediction slightly WORSE: in the old model the o-proj under-prediction and the gate_up over-prediction CANCEL when summed. 'Fixing' each operator breaks that lucky cancellation, so step-level decode MAPE rises (13.1% → 14.2%) and the aggregate ticks up (3.28% → 3.36%). We keep η_mem(W) as a per-operator refinement, NOT a step-level fix. The lesson: optimise the metric you actually report — the per-step time and the ranking it drives, not per-operator accuracy.

A fourth refinement — re-fitting the attention prefill constant (c_attn 2×10⁵ → 3×10⁵) — cuts prefill-attention MAPE 13.9% → 11.0% (held-out 10.9%); results/attn_prefill_refit.json. Step-level: results/gemm_full_fix_vidur.json.

7. Residuals and methodology-honesty lessons

8. The full-step confirmation: cross-GPU transfer in flight

Section 5's ranking is computed on GEMM step time. The cross-GPU transfer test (E2) is the full-step confirmation: evaluate the complete per-step predictor on a GPU the forest was never profiled on. There it must extrapolate, while our predictor only needs that architecture's datasheet constants and a cheap clock reading. E2 profiles on A100, evaluates on unseen H100/H200, comparing a 2×2 of (RF vs ours) × (home vs unseen).

Status. E2 is running on PACE. A dedicated conda environment (for sarathi-serve's torch-2.3 kernels) is being built so the serving environment is untouched; H100/H200 jobs dispatch automatically once ready. Expected: the A100-trained forest degrades off-profile while our fit-free predictor holds — the full-step version of the Section 5 ranking win.

9. Scope, boundaries, and what is next

What this is: a complete, mechanistic, non-intrusive per-step predictor with explainable trace-free constants, that wins the capacity-planning decision (rank unprofiled hardware: top-1 100%, Spearman +1.00, zero profiling rows) a memorising baseline cannot make. What it is not: a speedup, not the most accurate predictor on already-profiled hardware, and not yet a clock predictor. Honest boundaries throughout: the decline chain is proven only to cap → clock; the L40S ceiling rests on four cells; the η_mem law is correct per operator but kept out of the step model; the full-step cross-GPU confirmation is in flight. Every number traces to results/ or part2_integrations/.

Validated done

Non-intrusive SKU ranking (top-1 100%, Spearman +1.00, 0 rows); five-factor LOO transfer; clock droop (n=5, four SKUs); cap→clock causal link; η_wave (8.1%→3.6%), config D (3.3%).

In flight running

E2 cross-GPU transfer on PACE (H100/H200) — the full-step confirmation; folding config D + attention re-fit into predictor.py.

Future work open

Forecast f_clk from the cap; a tight-cap controllable card to close cap→clock→decline; a context-length term for attention; the mid-M decode roofline knee.