<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Benchmarking on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/benchmarking/</link><description>Recent content in Benchmarking on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 29 Aug 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/benchmarking/index.xml" rel="self" type="application/rss+xml"/><item><title>MLPerf Inference: how to read it, what LoadGen enforces and what comparability it offers</title><link>https://blog.lo0.es/en/posts/mlperf-inference-reading-loadgen-comparability/</link><pubDate>Sat, 29 Aug 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/mlperf-inference-reading-loadgen-comparability/</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>MLPerf Inference is maintained by MLCommons on a six-monthly cadence. It imposes three things no home-made benchmark has: a &lt;strong>mandatory load generator&lt;/strong> (LoadGen, compiled from a tagged revision and unmodified), model-equivalence rules that bound what can be optimised, and an &lt;strong>audit process&lt;/strong> that reviews up to two submissions per round. The four scenarios (SingleStream, MultiStream, Server, Offline) have a minimum duration of 600 s and different metrics: Server reports the sustained Poisson rate and is the only one with a latency constraint. For the LLM benchmarks that constraint is not a request latency but two simultaneous SLOs at the 99th percentile: TTFT and TPOT, with per-model figures ranging from 2000 ms / 200 ms on Llama 2 70B to 450 ms / 40 ms on its Interactive variant. Quality is required relative to the reference model (99 % or 99.9 %) and the &lt;strong>generation length&lt;/strong> is audited as well (Llama 2 70B: more than 90 % of 294.45 tokens per sample). Three reading rules sum up the real comparability: results are only compared within the same version, division, category and scenario; per-accelerator is not an official MLCommons metric, it is derived; and neither cost nor energy enters the primary metric, to the point that round v5.1 had 2 power submissions against 27 participating organisations.&lt;/p>
&lt;hr>
&lt;h2 id="what-it-is-and-how-it-is-organised">What it is and how it is organised&lt;/h2>
&lt;p>MLPerf Inference is maintained by the &lt;strong>MLCommons Association&lt;/strong> through the MLPerf Inference Working Group, whose participation is limited to MLCommons members and affiliates (&lt;a href="https://mlcommons.org/working-groups/benchmarks/inference/">MLPerf Inference Working Group&lt;/a>). The declared cadence for publishing results is roughly six months.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Round&lt;/th>
&lt;th>Results published&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>v4.0&lt;/td>
&lt;td>27 March 2024&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v4.1&lt;/td>
&lt;td>28 August 2024&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v5.0&lt;/td>
&lt;td>2 April 2025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v5.1&lt;/td>
&lt;td>9 September 2025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v6.0&lt;/td>
&lt;td>1 April 2026&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v6.1&lt;/td>
&lt;td>in preparation as of this data sheet&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The suite splits into two system categories: &lt;strong>Datacenter&lt;/strong> and &lt;strong>Edge&lt;/strong>, the latter defined in the rules as everything that is not datacenter (&lt;a href="https://github.com/mlcommons/inference_policies/blob/master/inference_rules.adoc">inference_rules.adoc&lt;/a>). A Datacenter system has two requirements that do not apply in Edge:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Mandatory ECC&lt;/strong> on DRAM and HBM, active during all performance and accuracy runs. There is no requirement on SRAM.&lt;/li>
&lt;li>&lt;strong>Mandatory networking&lt;/strong> since round v3.0, with a minimum bandwidth computed from the achieved throughput. For Llama 3.1 405B the minimum ingress is &lt;code>throughput × 20000 × dtype_size&lt;/code> bytes/s; for Llama 2 70B the minimum egress is &lt;code>throughput × 1024 × dtype_size&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Edge additionally allows &lt;strong>inferred results&lt;/strong>: a MultiStream derived from SingleStream counts as 8 times the p99 latency; an Offline derived from MultiStream, 8000 divided by the mean latency in milliseconds.&lt;/p>
&lt;h2 id="the-four-scenarios">The four scenarios&lt;/h2>
&lt;p>This is the normative table, transcribed from the rules:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scenario&lt;/th>
&lt;th>Query generation&lt;/th>
&lt;th>Duration&lt;/th>
&lt;th>Samples/query&lt;/th>
&lt;th>Latency constraint&lt;/th>
&lt;th>Tail latency&lt;/th>
&lt;th>Reported metric&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SingleStream&lt;/td>
&lt;td>The next query is issued when the SUT completes the previous one&lt;/td>
&lt;td>600 s&lt;/td>
&lt;td>1&lt;/td>
&lt;td>None&lt;/td>
&lt;td>90 %&lt;/td>
&lt;td>p90 latency with early stopping&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Server / Interactive&lt;/td>
&lt;td>LoadGen issues queries following a &lt;strong>Poisson distribution&lt;/strong>&lt;/td>
&lt;td>600 s&lt;/td>
&lt;td>1&lt;/td>
&lt;td>Benchmark-specific&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>Maximum Poisson parameter supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Offline&lt;/td>
&lt;td>All samples are delivered at the start in &lt;strong>a single query&lt;/strong>&lt;/td>
&lt;td>1 query, 600 s&lt;/td>
&lt;td>At least 24,576&lt;/td>
&lt;td>None&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>Measured throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MultiStream&lt;/td>
&lt;td>The next query is issued when the SUT completes the previous one&lt;/td>
&lt;td>600 s&lt;/td>
&lt;td>&lt;strong>8&lt;/strong>&lt;/td>
&lt;td>None&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>p99 query latency with early stopping&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Four operational clarifications that change how each number is interpreted:&lt;/p>
&lt;ul>
&lt;li>In &lt;strong>Server&lt;/strong>, LoadGen does not measure a latency: it runs a binary search over the target rate. For a given value it generates queries at that QPS with Poisson arrivals; if the run fails the latency gate, it lowers the value and repeats. The final published metric is, per the official FAQ, &lt;strong>scheduled samples per second&lt;/strong>, not completed samples nor the target entered by the user.&lt;/li>
&lt;li>In &lt;strong>MultiStream&lt;/strong>, the latency of a query is the &lt;strong>maximum&lt;/strong> of the latencies of its samples, and the 8 samples of each query are contiguous in the order in which they were loaded. The value &lt;code>multi_stream_samples_per_query = 8&lt;/code> is fixed in the official configuration.&lt;/li>
&lt;li>In &lt;strong>Offline&lt;/strong> there is no latency constraint at all. The rules themselves forbid techniques that only work in fixed-length experiments &lt;em>except in the Offline scenario&lt;/em>, which is a normative acknowledgement that an Offline result says nothing about behaviour under an SLO.&lt;/li>
&lt;li>The percentiles in the table are the &lt;strong>lower theoretical bound&lt;/strong> for runs with very many queries. With early stopping, the percentile actually computed is somewhat above the nominal one, as a penalty for processing few queries.&lt;/li>
&lt;/ul>
&lt;p>The mandatory scenarios depend on the benchmark. In Datacenter, the summarisation, question answering, text generation and VLM tasks require &lt;code>(Server | Interactive), Offline&lt;/code>, that is, the submitter chooses between Server and Interactive. Reasoning and recommendation require &lt;code>Server, Offline&lt;/code>. Medical segmentation, node classification, speech-to-text and RAG require only &lt;code>Offline&lt;/code>.&lt;/p>
&lt;h2 id="loadgen-what-it-measures-and-what-it-does-not">LoadGen: what it measures and what it does not&lt;/h2>
&lt;p>LoadGen is the module that generates the load and computes the metrics. Its use is mandatory for all submissions, and it must be compiled from a tagged and approved revision of the repository, unaltered; the README is explicit that local modifications of the C++ library are not accepted.&lt;/p>
&lt;p>Its four declared responsibilities are generating the queries according to the scenario, tracking the latency of each query, validating the accuracy of the results and computing the final metrics. Latency is defined as the time from when LoadGen was scheduled to pass a query to the system under test until it receives the response.&lt;/p>
&lt;p>Equally important is what it declares &lt;strong>out of scope&lt;/strong>: LoadGen does not know the model, does not know the data formats, cannot score accuracy and does not know the scenario constraints of the MLPerf rules. The consequence is written in its own README: being model-agnostic, it cannot enforce the MLPerf requirements, for example the target percentiles and latencies. Those limits are injected from &lt;code>mlperf.conf&lt;/code> and &lt;code>user.conf&lt;/code>. The justification for making it mandatory appears in the founding ISCA 2020 paper: it establishes a clear boundary between the components that belong to the submitter and those that belong to MLPerf, and it measures the performance of the complete system instead of that of a single piece.&lt;/p>
&lt;h3 id="duration-query-count-and-early-stopping">Duration, query count and early stopping&lt;/h3>
&lt;p>The minimum duration is 600,000 ms in all four scenarios. The minimum number of queries comes from a confidence interval table that the rules publish:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tail percentile&lt;/th>
&lt;th>Confidence&lt;/th>
&lt;th>Margin of error&lt;/th>
&lt;th>Inferences&lt;/th>
&lt;th>Rounded&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>90 %&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>0.50 %&lt;/td>
&lt;td>23,886&lt;/td>
&lt;td>24,576&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>95 %&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>0.25 %&lt;/td>
&lt;td>50,425&lt;/td>
&lt;td>57,344&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>97 %&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>0.15 %&lt;/td>
&lt;td>85,811&lt;/td>
&lt;td>90,112&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>99 %&lt;/td>
&lt;td>99 %&lt;/td>
&lt;td>0.05 %&lt;/td>
&lt;td>262,742&lt;/td>
&lt;td>270,336&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The submission checker applies &lt;code>Server: 270336&lt;/code>, &lt;code>SingleStream: 1024&lt;/code> and &lt;code>MultiStream: 662&lt;/code>. For Offline, the minimum is set per benchmark: 24,576 on Llama 2 70B, 15,000 on Mixtral 8x7B, 13,368 on Llama 3.1 8B, 8,313 on Llama 3.1 405B, 4,388 on DeepSeek-R1, 1,633 on Whisper and 43 on 3D UNet.&lt;/p>
&lt;p>The &lt;strong>early stopping&lt;/strong> mechanism allows runs to be shortened while keeping the statistical guarantee. With tolerance &lt;code>d = 0&lt;/code> and confidence &lt;code>c = 0.99&lt;/code>, the algorithm resolves by binary search the smallest number of queries below the latency threshold that satisfies the criterion, given the number of queries observed above it.&lt;/p>
&lt;p>LoadGen&amp;rsquo;s &lt;strong>modes&lt;/strong> are &lt;code>SubmissionRun&lt;/code> (accuracy followed by performance), &lt;code>AccuracyOnly&lt;/code>, &lt;code>PerformanceOnly&lt;/code> and &lt;code>FindPeakPerformance&lt;/code>. The last, applicable only to Server, takes &lt;code>target_qps&lt;/code> as a lower bound if it passes, estimates the upper bound at double and doubles it until failure, and then does a binary search. In performance mode LoadGen selects queries uniformly at random with replacement from a set of size QSL; in accuracy mode it uses a copy of the validation dataset, each sample exactly once. One accuracy run must be executed for every performance result submitted, and the same code in both modes.&lt;/p>
&lt;p>The &lt;strong>seeds&lt;/strong> are announced four weeks before the deadline and the mandatory generator is Mersenne Twister 19937. They change every round: in v5.1, &lt;code>qsl_rng_seed = 1780908523862526354&lt;/code>; in v5.0 it was &lt;code>6023615788873153749&lt;/code>.&lt;/p>
&lt;h3 id="the-compliance-tests">The compliance tests&lt;/h3>
&lt;p>They are activated by placing an &lt;code>audit.config&lt;/code> file in the working directory, whose parameters &lt;strong>override&lt;/strong> those of &lt;code>mlperf.conf&lt;/code> and &lt;code>user.conf&lt;/code>. Their logs are mandatory in the submission package. The declared purpose is to detect anomalies, not to diagnose their cause.&lt;/p>
&lt;p>For the LLM benchmarks the required test is &lt;strong>TEST06&lt;/strong>, specifically against the end-of-sequence token &lt;em>exploit&lt;/em>. It runs 100 samples and demands three conditions: that the first token reported separately matches the first token of the model&amp;rsquo;s output, that the output ends with zero or a single EOS token, and that the number of tokens reported matches the real length of the generated sequence.&lt;/p>
&lt;h2 id="divisions-categories-and-what-can-be-touched">Divisions, categories and what can be touched&lt;/h2>
&lt;h3 id="the-three-divisions">The three divisions&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Closed&lt;/strong>: requires preprocessing, postprocessing and model equivalent to the reference implementation. It allows calibration for quantisation and &lt;strong>does not allow retraining&lt;/strong>. It is the only one that can use the MLPerf name unqualified.&lt;/li>
&lt;li>&lt;strong>Open&lt;/strong>: allows arbitrary preprocessing, postprocessing and model, including retraining. The accuracy, latency and scenario constraints &lt;strong>do not apply&lt;/strong>: instead, the accuracy obtained and the latency constraints under which the performance was obtained must be reported. The model may have any origin, be quantised in any way and be sparsified in any way. It must use the same validation dataset as the corresponding Closed and use all of it.&lt;/li>
&lt;li>&lt;strong>Network&lt;/strong>: inherits all the Closed requirements, applies only to Datacenter, and separates the LoadGen node from the system under test through a &lt;em>fabric&lt;/em>. The submitter&amp;rsquo;s dispatch library cannot preprocess, postprocess, &lt;strong>batch&lt;/strong>, pad or &lt;strong>cache&lt;/strong>. Bus interconnects are forbidden by name (PCIe, CXL, CCIX, HyperTransport, NVLink, QPI, UPI, ICI) and only Ethernet, IEEE 802.11, InfiniBand and 3GPP are admitted, with a requirement to work chassis to chassis at more than ten metres.&lt;/li>
&lt;/ul>
&lt;h3 id="the-availability-categories">The availability categories&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Hardware&lt;/th>
&lt;th>Software&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Available in cloud&lt;/td>
&lt;td>Available for cloud rental&lt;/td>
&lt;td>Available&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Available on premise&lt;/td>
&lt;td>Available for purchase&lt;/td>
&lt;td>Available&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Preview&lt;/td>
&lt;td>Must be available for the next submission, or the one after 140 days, whichever is longer&lt;/td>
&lt;td>Available except what is needed for substantially new hardware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RDI&lt;/td>
&lt;td>Does not meet the above&lt;/td>
&lt;td>Does not meet the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Available demands four cumulative conditions: price available, having been rented or shipped to at least one third party, public evidence of availability and reasonable availability to additional third parties on the submission date. A Preview result obliges a resubmission as Available with equal or better performance, tolerating up to &lt;strong>2 % degradation&lt;/strong> from noise; if there is no resubmission, the Preview result is &lt;strong>marked invalid&lt;/strong>. RDI components cannot be submitted as Available until the cycle after the next one, or 221 days, whichever is longer.&lt;/p>
&lt;h3 id="what-closed-allows-and-forbids">What Closed allows and forbids&lt;/h3>
&lt;p>MLPerf delivers the weights in fp16 or fp32. The central rule is that the submitter may perform purely mathematical and reproducible quantisation, using only the calibration data and the tensors of the delivered model, to any numerical format that reaches the required quality, and that the method must be described publicly at a level that allows it to be reproduced. The test against back doors is elegant: the description of the quantisation method must be &lt;em>much smaller&lt;/em> than the non-zero weights it produces.&lt;/p>
&lt;p>&lt;strong>Allowed&lt;/strong>, among other things: any framework or runtime, arbitrary data layout, varying the matrix multiplication algorithm, mathematically equivalent transformations, polynomial approximations of transcendentals, processing queries out of order within what the scenario admits, replacing dense operations with mathematically equivalent sparse operations, hand-picking different precisions per operation, fusing and unfusing, dynamic batching, and mixtures of experts combining weights with different quantisation.&lt;/p>
&lt;p>&lt;strong>Forbidden&lt;/strong>: replacing or supplementing weights wholesale, discarding non-zero weights, including pruning, caching queries or responses, coalescing identical queries, modifying weights during the timed portion, quantisation algorithms of size comparable to the weights they produce, hardcoding the total number of queries, using knowledge of the LoadGen implementation to predict peaks or troughs in the Server scenario, changing the number of search beams, and incorporating statistics from the performance or accuracy sets.&lt;/p>
&lt;p>Two clarifications that matter to anyone serving LLMs in production: the &lt;strong>KV cache&lt;/strong> is allowed just as in the reference model provided it is not applied &lt;strong>across queries&lt;/strong>; PagedAttention is admitted if the block is reused only within the batch; continuous or dynamic batching is allowed; and KV cache entries are treated as activations for quantisation purposes, and cannot be pruned. &lt;strong>Speculative decoding&lt;/strong> is only admitted in the benchmark and scenario combinations explicitly listed, with the reference head at the same precision, and implementations that artificially manipulate the acceptance rate are forbidden.&lt;/p>
&lt;h2 id="the-slos-almost-nobody-cites-ttft-and-tpot">The SLOs almost nobody cites: TTFT and TPOT&lt;/h2>
&lt;p>This is the point where an MLPerf table is most often misread. In the LLM benchmarks of the Server scenario, the constraint is not a request latency: the rules set &lt;code>target_latency = 0&lt;/code> and activate &lt;code>use_token_latencies = 1&lt;/code>, so that the gate is formed by &lt;strong>two simultaneous metrics&lt;/strong>, both evaluated at the &lt;strong>99th percentile&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT&lt;/strong> (&lt;em>time to first token&lt;/em>): latency of the first token.&lt;/li>
&lt;li>&lt;strong>TPOT&lt;/strong> (&lt;em>time per output token&lt;/em>): mean interval between all generated tokens.&lt;/li>
&lt;/ul>
&lt;p>The figures are in &lt;code>mlperf.conf&lt;/code> and change per round and per benchmark:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark (Server scenario)&lt;/th>
&lt;th>TTFT&lt;/th>
&lt;th>TPOT&lt;/th>
&lt;th>Round&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>llama2-70b&lt;/code>&lt;/td>
&lt;td>2000 ms&lt;/td>
&lt;td>200 ms&lt;/td>
&lt;td>v5.0, v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llama2-70b-interactive&lt;/code>&lt;/td>
&lt;td>&lt;strong>450 ms&lt;/strong>&lt;/td>
&lt;td>&lt;strong>40 ms&lt;/strong>&lt;/td>
&lt;td>v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mixtral-8x7b&lt;/code>&lt;/td>
&lt;td>2000 ms&lt;/td>
&lt;td>200 ms&lt;/td>
&lt;td>v5.0, v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llama3_1-405b&lt;/code>&lt;/td>
&lt;td>6000 ms&lt;/td>
&lt;td>175 ms&lt;/td>
&lt;td>v5.0, v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llama3_1-405b-interactive&lt;/code>&lt;/td>
&lt;td>4500 ms&lt;/td>
&lt;td>80 ms&lt;/td>
&lt;td>v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llama3_1-8b&lt;/code>&lt;/td>
&lt;td>2000 ms&lt;/td>
&lt;td>100 ms&lt;/td>
&lt;td>v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llama3_1-8b-interactive&lt;/code>&lt;/td>
&lt;td>500 ms&lt;/td>
&lt;td>30 ms&lt;/td>
&lt;td>v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>deepseek-r1&lt;/code>&lt;/td>
&lt;td>2000 ms&lt;/td>
&lt;td>80 ms&lt;/td>
&lt;td>v5.1, v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>deepseek-r1-interactive&lt;/code>&lt;/td>
&lt;td>1500 ms&lt;/td>
&lt;td>15 ms&lt;/td>
&lt;td>v6.x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gpt-oss-120b&lt;/code>&lt;/td>
&lt;td>3000 ms&lt;/td>
&lt;td>80 ms&lt;/td>
&lt;td>v6.x&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A TPOT of 40 ms is equivalent to 25 tokens per second per user, which is the order of magnitude of a conversational interface; one of 200 ms is equivalent to 5 tokens per second, which is a batch use case dressed up as a service. Two results for the same model under the Server and Interactive labels are not the same measurement. Two documented discrepancies between primary sources remain: the &lt;code>llama2-70b-interactive&lt;/code> variant does not appear in the &lt;code>mlperf.conf&lt;/code> tagged as v5.0 despite having been announced in that round, and in the v6.x cycle the TPOT of &lt;code>gpt-oss-120b-interactive&lt;/code> appears as 15 ms in &lt;code>mlperf.conf&lt;/code> and as 20 ms in the text of the rules.&lt;/p>
&lt;p>For contrast, the constraints of the non-LLM benchmarks in the Server scenario are conventional request latencies: ResNet-50 15 ms, RetinaNet 100 ms, BERT 130 ms, DLRMv2 60 ms, DLRMv3 80 ms, RNN-T 1000 ms, GPT-J and Stable Diffusion XL 20,000 ms.&lt;/p>
&lt;h2 id="quality-the-99--and-the-token-count">Quality: the 99 % and the token count&lt;/h2>
&lt;p>The quality targets are expressed relative to the reference model, never in absolute terms, and each benchmark demands one variant, the other or both. In the current Datacenter suite, 3D UNet, Llama 3.1 8B and Whisper ask for 99 % and 99.9 % of FP32, which generates two distinct results; Llama 2 70B asks only for 99.9 %; Llama 3.1 405B, Mixtral, DeepSeek-R1, GPT-OSS-120B, Qwen3-VL, RGAT and WAN-2.2 ask only for 99 %; and E2E-RAG is the exception at 97 %. In the checker this materialises as separate benchmarks (&lt;code>bert-99&lt;/code> and &lt;code>bert-99.9&lt;/code>), each with its computed numerical threshold.&lt;/p>
&lt;p>On top of that metric there is a second LLM-specific constraint that is cited very rarely: the generation length is audited as well.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark&lt;/th>
&lt;th>Length constraint&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 2 70B&lt;/td>
&lt;td>tokens per sample &amp;gt; 90 % of 294.45&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3.1 405B&lt;/td>
&lt;td>tokens per sample between 90 % and 110 % of 684.68&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mixtral 8x7B&lt;/td>
&lt;td>tokens per sample between 90 % and 110 % of 144.84&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3.1 8B&lt;/td>
&lt;td>total generated length &amp;gt; 90 % of 8,167,644&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The FAQ settles the obvious shortcut: reducing the maximum output length below the reference is not allowed, and truncating tokens to improve performance or reach the accuracy target &lt;strong>is not permitted&lt;/strong>. The inference parameters are fixed in Closed (&lt;code>max_new_tokens = 1024&lt;/code> on Llama 2 70B, &lt;code>20000&lt;/code> on Llama 3.1 405B and DeepSeek-R1), as is the decoding algorithm: greedy search in the Llama family, Mixtral and DeepSeek-R1; sampling with &lt;code>temperature = 1.0&lt;/code> and &lt;code>top_p = 1.0&lt;/code> in GPT-OSS-120B and Qwen3-VL. Accuracy is reported to &lt;strong>five significant figures&lt;/strong> with round-half-to-even.&lt;/p>
&lt;h2 id="the-current-datacenter-suite">The current Datacenter suite&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Area&lt;/th>
&lt;th>Task&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Dataset&lt;/th>
&lt;th>QSL&lt;/th>
&lt;th>Required quality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Vision&lt;/td>
&lt;td>Medical segmentation&lt;/td>
&lt;td>3D UNet&lt;/td>
&lt;td>KiTS 2019&lt;/td>
&lt;td>42&lt;/td>
&lt;td>99 % and 99.9 % of FP32 (DICE 0.86330)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>Summarisation&lt;/td>
&lt;td>Llama 3.1 8B&lt;/td>
&lt;td>CNN DailyMail v3.0.0&lt;/td>
&lt;td>13,368&lt;/td>
&lt;td>99 % and 99.9 % of FP32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>Question answering&lt;/td>
&lt;td>Llama 2 70B&lt;/td>
&lt;td>OpenOrca&lt;/td>
&lt;td>24,576&lt;/td>
&lt;td>99.9 % of FP32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>Text generation&lt;/td>
&lt;td>Llama 3.1 405B&lt;/td>
&lt;td>LongBench, Ruler, GovReport&lt;/td>
&lt;td>8,313&lt;/td>
&lt;td>99 % of FP16&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>QA, maths and code&lt;/td>
&lt;td>Mixtral 8x7B&lt;/td>
&lt;td>OpenOrca, GSM8K, MBXP&lt;/td>
&lt;td>15,000&lt;/td>
&lt;td>99 % of FP16&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>Reasoning&lt;/td>
&lt;td>DeepSeek-R1&lt;/td>
&lt;td>&lt;code>mlperf_deepseek_r1&lt;/code>&lt;/td>
&lt;td>4,388&lt;/td>
&lt;td>99 % of FP16 (exact match 81.9132 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>QA, maths and code&lt;/td>
&lt;td>GPT-OSS-120B&lt;/td>
&lt;td>AIME25, GPQA Diamond, LiveCodeBench v6&lt;/td>
&lt;td>6,396&lt;/td>
&lt;td>99 % of 83.13 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vision&lt;/td>
&lt;td>Vision-language model&lt;/td>
&lt;td>Qwen3-VL-235B-A22B&lt;/td>
&lt;td>Shopify product catalogue&lt;/td>
&lt;td>48,289&lt;/td>
&lt;td>99 % of BF16 (hierarchical F1 0.7824)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Commerce&lt;/td>
&lt;td>Recommendation&lt;/td>
&lt;td>DLRMv3&lt;/td>
&lt;td>Synthetic Streaming 100B&lt;/td>
&lt;td>34,996&lt;/td>
&lt;td>99.9 % of FP32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Generative&lt;/td>
&lt;td>Text to video&lt;/td>
&lt;td>WAN-2.2-T2V-A14B&lt;/td>
&lt;td>VBench prompts&lt;/td>
&lt;td>248&lt;/td>
&lt;td>99 % of BF16 (VBench 69.7752)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Graphs&lt;/td>
&lt;td>Node classification&lt;/td>
&lt;td>RGAT&lt;/td>
&lt;td>IGBH&lt;/td>
&lt;td>788,379&lt;/td>
&lt;td>99 % of FP32 (72.86 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Audio&lt;/td>
&lt;td>Speech to text&lt;/td>
&lt;td>Whisper&lt;/td>
&lt;td>LibriSpeech&lt;/td>
&lt;td>1,633&lt;/td>
&lt;td>99 % and 99.9 % of FP32 (WER 2.0671 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Language&lt;/td>
&lt;td>End-to-end RAG&lt;/td>
&lt;td>E2E-RAG&lt;/td>
&lt;td>FRAMES&lt;/td>
&lt;td>824&lt;/td>
&lt;td>97 % of FP32&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;strong>E2E-RAG&lt;/strong> benchmark deserves attention from anyone designing a RAG platform, because it fixes the whole pipeline: a FAISS HNSW index with mandatory parameters &lt;code>M = 32&lt;/code>, &lt;code>efConstruction = 200&lt;/code> and &lt;code>efSearch = 100&lt;/code>; e5-base-v2 embeddings; ColBERTv2 reranking; generation and query rewriting with GPT-OSS-120B; evaluation with Llama 3.1 8B as judge; and a maximum of 5 retrieval iterations, mandatory for all submitters. Model loading is not timed; building the vector database is.&lt;/p>
&lt;p>The definition of &lt;em>sample&lt;/em> changes per model and is a frequent source of confusion: in the LLMs it is a sequence, in DLRMv3 it is a request with a user history and &lt;strong>2048 candidates&lt;/strong>, in WAN-2.2 it is a pair of positive and negative prompts, and in PointPainting it is five images and a lidar point cloud.&lt;/p>
&lt;h2 id="how-to-read-a-results-row">How to read a results row&lt;/h2>
&lt;p>The columns of the official table in the Closed division are &lt;em>Submitter, Software, System, Benchmark Results, Processor/Count, Details, Accelerator/Count&lt;/em> and &lt;em>Code&lt;/em>. The Open division adds &lt;em>Model Used&lt;/em> and &lt;em>Notes&lt;/em>. Rows with power measurement add &lt;em>System Power&lt;/em> in Server and Offline, or &lt;em>Energy Per Stream&lt;/em> in the stream scenarios. Any citation must use the identifier in the format &lt;code>major-version.minor-version.entry.benchmark&lt;/code>, of the style &lt;code>5.1-0053&lt;/code>, and carry a footnote with suite, version, division, benchmark, scenario, date and source.&lt;/p>
&lt;h3 id="per-accelerator-is-not-an-official-metric">Per-accelerator is not an official metric&lt;/h3>
&lt;p>&lt;strong>There is no per-accelerator column.&lt;/strong> It is a derived metric, and the MLCommons messaging guidelines are blunt: any comparison based on a different or derived metric, such as power, cost, model size or accuracy, must make the basis of comparison clear in the text and in a footnote, and secondary and derived metrics cannot be presented as official or verified MLPerf metrics. NVIDIA itself states this with the standard formula in its round blogs: per-GPU performance is not a primary MLPerf Inference metric and is computed by dividing the reported throughput by the number of reported accelerators. The same applies to combining results from several benchmarks: MLCommons allows it but &lt;strong>does not endorse it&lt;/strong>, and the composite cannot be presented as an official result.&lt;/p>
&lt;h3 id="the-comparison-rules">The comparison rules&lt;/h3>
&lt;ul>
&lt;li>MLPerf results can only be compared &lt;strong>with compatible MLPerf results&lt;/strong>: same benchmark, same scenario and compatible versions per the normative compatibility table.&lt;/li>
&lt;li>MLPerf results cannot be compared with non-MLPerf results.&lt;/li>
&lt;li>When comparing, any difference in version, division, category, verification status, scenario or number of chips must be clearly identified. When comparing Open with Closed, it must be identified in what sense the Open result would not qualify as Closed.&lt;/li>
&lt;li>Submitters cannot publish results of a version before its official date; non-submitters must wait &lt;strong>two weeks&lt;/strong> from that date.&lt;/li>
&lt;li>The sanctions regime goes as far as barring an infringer from submitting results in the future and marking their results as non-compliant permanently in the database, with a deadline of &lt;strong>three working days&lt;/strong> to withdraw the infringing content.&lt;/li>
&lt;/ul>
&lt;h3 id="how-much-audit-sits-behind-a-row">How much audit sits behind a row&lt;/h3>
&lt;p>In each round up to two submissions are audited: one chosen at random from all of them and zero or one chosen by the review committee. And there is a constraint that conditions the reading of the whole table: only Available submissions in the Closed division are auditable. Preview, RDI and Open fall outside the process. There is an exemption from the random audit if the system is equivalent to another already audited and neither the aggregate performance nor the per-accelerator performance differ by more than 10 % from those of the previous audit. The deadlines are 28 days to select an auditor, 30 for the report after signing the confidentiality agreements and around 90 days for the complete process, with &lt;strong>two days&lt;/strong> of hardware access for the auditor. The rules close with three sentences that define the standard of proof: results that cannot be replicated are not valid results, benchmark detection is not allowed and input-based optimisation is not allowed either.&lt;/p>
&lt;h2 id="the-recent-rounds-in-figures">The recent rounds, in figures&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Round&lt;/th>
&lt;th>Organisations&lt;/th>
&lt;th>Performance results&lt;/th>
&lt;th>Power results&lt;/th>
&lt;th>New benchmarks&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>v4.0&lt;/td>
&lt;td>23&lt;/td>
&lt;td>more than 8,500&lt;/td>
&lt;td>900&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v4.1&lt;/td>
&lt;td>22&lt;/td>
&lt;td>964&lt;/td>
&lt;td>31&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v5.0&lt;/td>
&lt;td>23&lt;/td>
&lt;td>17,457&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v5.1&lt;/td>
&lt;td>27&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>2&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v6.0&lt;/td>
&lt;td>24&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>5 of 11 Datacenter tests&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>These counts do not form a time series: the counting criteria differ between press releases and none documents the criterion, to the point that v4.0 and v4.1 differ by a factor close to nine.&lt;/p>
&lt;p>The published performance jumps are comparable within each announcement. Between v4.0 and v5.0, twelve months apart, the number of Llama 2 70B submissions multiplied by 2.5, the &lt;strong>median&lt;/strong> score doubled and the best score was &lt;strong>3.3 times faster&lt;/strong>. Between v5.0 and v5.1, in six months, the best systems improved &lt;strong>by up to 50 %&lt;/strong> in some scenarios. Round v6.0 changed axis and its press release talks about scale rather than speed: 30 % more multi-node submissions, 10 % of the systems with more than ten nodes against 2 % in the previous round, and a system larger than 72 nodes and 288 accelerators, quadrupling the previous maximum node count.&lt;/p>
&lt;p>Hardware that debuted in each round: MI300X, TPU v6e, Xeon Granite Rapids and B200 in v4.1; MI325X, Xeon 6980P, GB200 and Jetson AGX Thor in v5.0; MI355X, Intel Arc Pro B60, GB300 and RTX Pro 6000 Blackwell Server Edition in v5.1.&lt;/p>
&lt;h2 id="what-mlperf-inference-does-not-measure">What MLPerf Inference does not measure&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Cost.&lt;/strong> There is no cost or TCO metric, and cost is explicitly classified as a derived metric that cannot be presented as official.&lt;/li>
&lt;li>&lt;strong>Energy, except as an optional extension.&lt;/strong> MLPerf Power is regulated in a separate document, requires measuring at the wall with the power group&amp;rsquo;s workflow and PTDaemon, and forbids any other method. Its real adoption is marginal: 2 power submissions in v5.1 against 27 participating organisations. In addition, MLCommons bounds what that figure means: the measured power is only valid for the benchmark it accompanies, and any other reference to power, such as a TDP setting or a power supply rating, is neither measured nor validated by MLCommons. The messaging rule forbids submitters from publishing per-watt normalised comparisons using any proxy other than the measured power.&lt;/li>
&lt;li>Configurations not optimised by the vendor. The bias is acknowledged in the audit rule itself, which contemplates submissions whose performance is not consistent with the known characteristics of the hardware, or where the committee lacks visibility into how it was achieved, or where hardware and software are not reasonably available to the public.&lt;/li>
&lt;li>&lt;strong>Barrier to entry.&lt;/strong> Only MLCommons members and &lt;em>test partners&lt;/em> can submit results for review. Anyone measuring with the same code without submitting must label every figure as &lt;strong>unverified&lt;/strong> with the corresponding legend.&lt;/li>
&lt;/ul>
&lt;h2 id="usage-flow-for-an-on-premise-platform">Usage flow for an on-premise platform&lt;/h2>
&lt;p>MLPerf Inference is useful in a purchasing decision if it is used for what it is, a test bench with written rules, and not as a ranking.&lt;/p>
&lt;ol>
&lt;li>Filter by division and category before looking at any number: keep Closed and Available. A Preview or RDI result describes hardware that cannot yet be bought, and neither of the two is auditable.&lt;/li>
&lt;li>Choose the scenario that matches the load pattern: Server or Interactive if there are users waiting, Offline if the work is batch. An Offline result does not authorise promising a latency.&lt;/li>
&lt;li>Read the SLO before the throughput: in any LLM benchmark, the number of queries per second only means something alongside its TTFT/TPOT pair. The same machine publishes very different figures under Server and under Interactive.&lt;/li>
&lt;li>Check the quality variant: 99 % and 99.9 % are different results for the same model, and the second usually costs performance.&lt;/li>
&lt;li>Translate to your own configuration carefully: dividing by the number of accelerators is a derived metric, not an official one, and it has to be declared as such.&lt;/li>
&lt;li>Close the gap MLPerf leaves open with your own measurement: cost, energy and behaviour under real traffic. That is exactly the function of the reproducible harness and of the track B bench tools, and the reason no platform decision stands on a public table alone.&lt;/li>
&lt;/ol>
&lt;h2 id="cross-links-from-the-benchmarking-track">Cross-links from the benchmarking track&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">B1/B2 — LLM inference benchmarking: frameworks, metrics and state of the art&lt;/a>: the TTFT, TPOT and goodput metrics outside the MLPerf corset, and which tool measures each one.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">B2 — LLM benchmark tool catalogue&lt;/a>: the tools with which you reproduce at home what MLPerf standardises.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">B3 — GuideLLM and SLO validation under load&lt;/a>: the practical equivalent of LoadGen&amp;rsquo;s binary search in the Server scenario.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">B6 — Measurement bias and reproducibility&lt;/a>: why LoadGen exists, told from the side of what happens when there is none.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">B8 — Serving engines compared on the Pareto frontier&lt;/a>: the comparison MLPerf does not make, with engines instead of systems.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlperf-power-energy-efficiency-benchmark/">C4 — MLPerf Power&lt;/a>: the energy extension of this same machinery, with its certified measurement at the wall.&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/reproducible-harness-cost-performance-energy-experiment/">The reproducible harness: cost, performance and energy in a single auditable experiment&lt;/a> — how to build at home the auditable equivalent of a submission, measuring on top of that what MLPerf leaves out.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/slo-to-number-of-gpus-sizing-justifying-inference-hardware/">Sizing and justifying the GPU investment&lt;/a> — the step from a throughput figure under an SLO to the number of accelerators and the return.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>MLCommons, &lt;em>MLPerf Inference Working Group&lt;/em> — &lt;a href="https://mlcommons.org/working-groups/benchmarks/inference/">https://mlcommons.org/working-groups/benchmarks/inference/&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference Rules (inference_rules.adoc)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference_policies/blob/master/inference_rules.adoc">https://github.com/mlcommons/inference_policies/blob/master/inference_rules.adoc&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference Power Measurement (power_measurement.adoc)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference_policies/blob/master/power_measurement.adoc">https://github.com/mlcommons/inference_policies/blob/master/power_measurement.adoc&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Submission Rules (submission_rules.adoc)&lt;/em> — &lt;a href="https://github.com/mlcommons/policies/blob/master/submission_rules.adoc">https://github.com/mlcommons/policies/blob/master/submission_rules.adoc&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Results Messaging Guidelines&lt;/em> — &lt;a href="https://github.com/mlcommons/policies/blob/master/MLPerf_Results_Messaging_Guidelines.adoc">https://github.com/mlcommons/policies/blob/master/MLPerf_Results_Messaging_Guidelines.adoc&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>LoadGen README&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/master/loadgen/README.md">https://github.com/mlcommons/inference/blob/master/loadgen/README.md&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>loadgen/test_settings.h&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/master/loadgen/test_settings.h">https://github.com/mlcommons/inference/blob/master/loadgen/test_settings.h&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>loadgen/mlperf.conf (master)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/master/loadgen/mlperf.conf">https://github.com/mlcommons/inference/blob/master/loadgen/mlperf.conf&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>loadgen/mlperf.conf (tag v5.1)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/v5.1/loadgen/mlperf.conf">https://github.com/mlcommons/inference/blob/v5.1/loadgen/mlperf.conf&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>loadgen/mlperf.conf (tag v5.0)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/v5.0/loadgen/mlperf.conf">https://github.com/mlcommons/inference/blob/v5.0/loadgen/mlperf.conf&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>tools/submission/submission_checker.py (tag v5.0)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/v5.0/tools/submission/submission_checker.py">https://github.com/mlcommons/inference/blob/v5.0/tools/submission/submission_checker.py&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>compliance/nvidia TEST06 README (tag v5.1)&lt;/em> — &lt;a href="https://github.com/mlcommons/inference/blob/v5.1/compliance/nvidia/TEST06/README.md">https://github.com/mlcommons/inference/blob/v5.1/compliance/nvidia/TEST06/README.md&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>Benchmark results: Inference Datacenter&lt;/em> — &lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">https://mlcommons.org/benchmarks/inference-datacenter/&lt;/a>&lt;/li>
&lt;li>Reddi et al., &lt;em>MLPerf Inference Benchmark&lt;/em>, ISCA 2020 — &lt;a href="https://arxiv.org/abs/1911.02549">https://arxiv.org/abs/1911.02549&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference v5.0 Benchmark Results&lt;/em> (abril 2025) — &lt;a href="https://www.businesswire.com/news/home/20250402313932/en/MLCommons-Releases-New-MLPerf-Inference-v5.0-Benchmark-Results">https://www.businesswire.com/news/home/20250402313932/en/MLCommons-Releases-New-MLPerf-Inference-v5.0-Benchmark-Results&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference v5.1 Benchmark Results&lt;/em> (septiembre 2025) — &lt;a href="https://www.globenewswire.com/news-release/2025/09/09/3147136/0/en/MLCommons-Releases-New-MLPerf-Inference-v5-1-Benchmark-Results.html">https://www.globenewswire.com/news-release/2025/09/09/3147136/0/en/MLCommons-Releases-New-MLPerf-Inference-v5-1-Benchmark-Results.html&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference v6.0 Benchmark Results&lt;/em> (abril 2026) — &lt;a href="https://www.globenewswire.com/news-release/2026/04/01/3266801/0/en/MLCommons-Releases-New-MLPerf-Inference-v6-0-Benchmark-Results.html">https://www.globenewswire.com/news-release/2026/04/01/3266801/0/en/MLCommons-Releases-New-MLPerf-Inference-v6-0-Benchmark-Results.html&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Inference v4.1 Benchmark Results&lt;/em> (agosto 2024) — &lt;a href="https://www.businesswire.com/news/home/20240828886616/en/New-MLPerf-Inference-v4.1-Benchmark-Results-Highlight-Rapid-Hardware-and-Software-Innovations-in-Generative-AI-Systems">https://www.businesswire.com/news/home/20240828886616/en/New-MLPerf-Inference-v4.1-Benchmark-Results-Highlight-Rapid-Hardware-and-Software-Innovations-in-Generative-AI-Systems&lt;/a>&lt;/li>
&lt;li>NVIDIA Developer Blog, &lt;em>NVIDIA Blackwell delivers massive performance leaps in MLPerf Inference v5.0&lt;/em> — &lt;a href="https://developer.nvidia.com/blog/nvidia-blackwell-delivers-massive-performance-leaps-in-mlperf-inference-v5-0/">https://developer.nvidia.com/blog/nvidia-blackwell-delivers-massive-performance-leaps-in-mlperf-inference-v5-0/&lt;/a>&lt;/li>
&lt;li>NVIDIA Developer Blog, &lt;em>NVIDIA Blackwell Ultra sets new inference records in MLPerf debut&lt;/em> — &lt;a href="https://developer.nvidia.com/blog/nvidia-blackwell-ultra-sets-new-inference-records-in-mlperf-debut/">https://developer.nvidia.com/blog/nvidia-blackwell-ultra-sets-new-inference-records-in-mlperf-debut/&lt;/a>&lt;/li>
&lt;li>MLCommons, &lt;em>MLPerf Automotive&lt;/em> (octubre 2025) — &lt;a href="https://arxiv.org/html/2510.27065v1">https://arxiv.org/html/2510.27065v1&lt;/a>&lt;/li>
&lt;li>Tschand et al., &lt;em>MLPerf Power: Benchmarking the Energy Efficiency of Machine Learning Systems from Microwatts to Megawatts&lt;/em> — &lt;a href="https://arxiv.org/abs/2410.12032">https://arxiv.org/abs/2410.12032&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>Measurement bias and reproducibility: why two benchmarks of the same model give figures that differ by up to 7×</title><link>https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/</link><pubDate>Tue, 16 Jun 2026 07:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. The mathematical symbol is
reserved for formulas: amounts are expressed in € or USD. Thousands with a thin space
(\(1\,000\)).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Running the same Llama-3-70B-Instruct model on the same 4×H100 SXM 80 GB node with two
different tools can produce throughput results that differ by &lt;strong>7.2×&lt;/strong> without the engine
changing a single line of code. The cause is not the engine: it is the &lt;strong>measurement method&lt;/strong>. At
1,000 QPS, a single-process asyncio client (vLLM bench, SGLang bench, genai-perf
pre-AIPerf) processed &lt;strong>75,574 tokens&lt;/strong> against the &lt;strong>545,733 tokens&lt;/strong> of a multi-process
client, both measuring the same endpoint
(&lt;a href="https://arxiv.org/abs/2605.24217">arXiv 2605.24217&lt;/a>). The bias is not random: it is
systematic, directional and reproducible. This article catalogues the sources of bias, quantifies
their magnitude and describes the minimum harness that turns a number into an auditable datum.&lt;/p>
&lt;hr>
&lt;h2 id="the-sources-of-bias-a-catalogue-with-magnitudes">The sources of bias: a catalogue with magnitudes&lt;/h2>
&lt;p>Each source of bias moves the published number in a given direction and with a
characteristic magnitude. The table below orders the sources from largest to smallest impact
observed in the literature:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source of bias&lt;/th>
&lt;th>Direction of the bias&lt;/th>
&lt;th>Documented magnitude&lt;/th>
&lt;th>Reference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Single-process client saturation&lt;/strong> (Python GIL, asyncio)&lt;/td>
&lt;td>understates throughput; overstates latency&lt;/td>
&lt;td>up to &lt;strong>7.2×&lt;/strong> fewer tokens processed at 1,000 QPS&lt;/td>
&lt;td>arXiv 2605.24217&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tokenizer different from the model&amp;rsquo;s&lt;/strong> (LLMPerf uses a universal LlamaTokenizer)&lt;/td>
&lt;td>tok/s not comparable across different vocabularies&lt;/td>
&lt;td>variable; up to &lt;strong>33 %&lt;/strong> of client overhead in the TPS denominator&lt;/td>
&lt;td>NVIDIA blog (TPS = client overhead ÷ total duration)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ignore_eos missing or inconsistent OSL&lt;/strong>&lt;/td>
&lt;td>underestimates throughput; artificially shortens the real OSL&lt;/td>
&lt;td>up to premature termination; an unrealistic fixed-length benchmark&lt;/td>
&lt;td>vLLM bench docs; NVIDIA fundamental concepts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Unrealistic ISL/OSL distribution&lt;/strong> (fixed length instead of a realistic distribution)&lt;/td>
&lt;td>an optimistic knee that does not resemble production&lt;/td>
&lt;td>the knee shifts; prefill understated with short prompts&lt;/td>
&lt;td>AIPerf sequence-length-distributions docs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Missing warmup / warm prefix cache&lt;/strong>&lt;/td>
&lt;td>overstates TTFT (artificially low on the first requests)&lt;/td>
&lt;td>TTFT underestimated by an already populated KV cache&lt;/td>
&lt;td>vLLM prefix caching docs; arXiv 2605.24217&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>concurrency&lt;/code> vs &lt;code>request-rate&lt;/code>&lt;/strong> as the load mode&lt;/td>
&lt;td>concurrency saturates symmetrically; request-rate can build a queue&lt;/td>
&lt;td>ITL overestimated if the queue grows without a ceiling with request-rate&lt;/td>
&lt;td>NVIDIA fundamentals; AIPerf docs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Measurement window too short&lt;/strong>&lt;/td>
&lt;td>does not capture the steady state; includes ramp-up&lt;/td>
&lt;td>throughput overestimated; latency underestimated&lt;/td>
&lt;td>meta-metrics arXiv 2508.10251&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Thermal state and GPU clocks&lt;/strong>&lt;/td>
&lt;td>thermal throttling reduces throughput without prior stabilisation&lt;/td>
&lt;td>temperature &amp;gt; 75 °C triggers throttling; a difference of ~3 W of stable power&lt;/td>
&lt;td>arXiv 2604.09048&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Warm prefix cache between runs&lt;/strong>&lt;/td>
&lt;td>overstates TTFT; unrealistic TTFT if the cache is pre-populated from the previous run&lt;/td>
&lt;td>a severe effect in multi-prompt evaluations with shared prefixes&lt;/td>
&lt;td>arXiv 2605.24217&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Mean instead of percentiles&lt;/strong>&lt;/td>
&lt;td>hides the latency tail&lt;/td>
&lt;td>P99 can be 4–6× the median at high concurrency&lt;/td>
&lt;td>NVIDIA fundamentals; Anyscale docs&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="the-dominant-bias-single-process-client-saturation">The dominant bias: single-process client saturation&lt;/h2>
&lt;p>The largest bias is not in the engine but in the &lt;strong>client that generates the load&lt;/strong>. The
single-process tools (vLLM bench, SGLang bench, genai-perf before its replacement by
AIPerf on 15 April 2026) use a single Python process with asyncio to manage the
concurrent requests. The Python Global Interpreter Lock (GIL) prevents a single process from
using more than one CPU core at a time, which introduces a bottleneck on the
client side that becomes critical at high concurrency.&lt;/p>
&lt;p>The mathematical effect: as the request rate rises, the client fails to dispatch
requests at the configured pace, accumulates queueing time on the client side, and that
time gets recorded as engine latency. The result is that TTFT and TPOT
&lt;strong>appear inflated&lt;/strong> and throughput &lt;strong>appears depressed&lt;/strong>, without the engine having changed
anything (&lt;a href="https://arxiv.org/abs/2605.24217">arXiv 2605.24217&lt;/a>).&lt;/p>
&lt;p>The documented discrepancy at 1,000 QPS:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Client architecture&lt;/th>
&lt;th>Tokens processed at 1,000 QPS&lt;/th>
&lt;th>Ratio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single-process (asyncio, Python GIL)&lt;/td>
&lt;td>75,574&lt;/td>
&lt;td>1× (baseline)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-process (distributed load)&lt;/td>
&lt;td>545,733&lt;/td>
&lt;td>&lt;strong>7.2×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Note: both clients point at the &lt;strong>same endpoint&lt;/strong>; the difference is entirely attributable
to the benchmark client, not to the engine.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 220" role="img" aria-label="Client saturation diagram: a single-process client builds a queue on the client side and reports inflated latency; a multi-process client removes the bottleneck and measures the real engine" 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(#bm2)}&lt;/style>
&lt;defs>&lt;marker id="bm2" 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="22" class="tl">SINGLE-PROCESS (vLLM bench, SGLang bench, genai-perf pre-AIPerf)&lt;/text>
&lt;rect class="bx" x="20" y="32" width="140" height="44" rx="5"/>
&lt;text x="30" y="52" class="ts">1 Python proc.&lt;/text>
&lt;text x="30" y="67" class="ts">asyncio + GIL&lt;/text>
&lt;rect class="dsh" x="200" y="32" width="130" height="44" rx="5"/>
&lt;text x="210" y="52" class="ts">client queue&lt;/text>
&lt;text x="210" y="67" class="ts">(≠ engine queue)&lt;/text>
&lt;path class="ar" d="M160,54 L198,54"/>
&lt;path class="ar" d="M330,54 L375,54"/>
&lt;rect class="bx" x="375" y="32" width="110" height="44" rx="5"/>
&lt;text x="385" y="58" class="ts">Engine (vLLM…)&lt;/text>
&lt;text x="498" y="48" class="ts">TTFT/TPOT&lt;/text>
&lt;text x="498" y="63" class="ts">overestimated&lt;/text>
&lt;text x="498" y="78" class="ts">TPS underestimated&lt;/text>
&lt;text x="20" y="112" class="tl">MULTI-PROCESS (AIPerf, GuideLLM)&lt;/text>
&lt;rect class="bx" x="20" y="122" width="140" height="44" rx="5"/>
&lt;text x="30" y="142" class="ts">N processes&lt;/text>
&lt;text x="30" y="157" class="ts">no shared GIL&lt;/text>
&lt;path class="ar" d="M160,144 L375,144"/>
&lt;rect class="bx" x="375" y="122" width="110" height="44" rx="5"/>
&lt;text x="385" y="148" class="ts">Engine (vLLM…)&lt;/text>
&lt;text x="498" y="138" class="ts">measures the engine;&lt;/text>
&lt;text x="498" y="153" class="ts">7.2× more throughput&lt;/text>
&lt;text x="498" y="168" class="ts">captured&lt;/text>
&lt;text x="20" y="210" class="ts">The client architecture determines what gets measured: the client bottleneck or the engine's.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="tokenizer-bias-toks-are-not-comparable-across-vocabularies">Tokenizer bias: tok/s are not comparable across vocabularies&lt;/h2>
&lt;p>Tokens are not universal. Each model has its own tokenizer with its own
vocabulary. Llama-3 uses a vocabulary of 128,256 tokens; Gemma uses 256,128; earlier
models used 32,000–50,000. The same English text produces a different number of tokens
depending on the model&amp;rsquo;s tokenizer.&lt;/p>
&lt;p>The direct consequence for benchmarking: if the tool measures tok/s with a
tokenizer different from that of the model being served, the token count, and therefore the throughput in
tok/s and the cost per token, are biased. LLMPerf (archived in December 2025) used
&lt;strong>LlamaTokenizer&lt;/strong> universally for all models, which guaranteed internal consistency
in the leaderboard but made the tok/s &lt;strong>not comparable&lt;/strong> with measurements of other
models with different vocabularies.&lt;/p>
&lt;p>The TPS formula in LLMPerf additionally included the benchmark&amp;rsquo;s full denominator, the time
spent generating prompts, preparing requests and storing responses, which
NVIDIA estimated at up to &lt;strong>33 % of the total duration&lt;/strong> at concurrency 1. This makes
LLMPerf&amp;rsquo;s TPS systematically lower than that of GenAI-Perf/AIPerf for the same system,
without the engine being worse:&lt;/p>
$$\text{TPS}_{\text{LLMPerf}} = \frac{\text{output tokens}}{T_{\text{end}} - T_{\text{start}}}$$
$$\text{TPS}_{\text{GenAI-Perf}} = \frac{\text{output tokens}}{T_y - T_x}$$
&lt;p>Where \(T_{\text{start}}\) and \(T_{\text{end}}\) include the client overheads, while
\(T_x\) and \(T_y\) are the instant of the first request and that of the last token received,
respectively (&lt;a href="https://developer.nvidia.com/blog/llm-benchmarking-fundamental-concepts/">NVIDIA · Fundamental Concepts&lt;/a>).&lt;/p>
&lt;p>The operational rule: &lt;strong>always count tokens with the tokenizer of the model being served&lt;/strong>, not with a
proxy tokenizer.&lt;/p>
&lt;hr>
&lt;h2 id="the-bias-of-ignore_eos-and-an-inconsistent-osl">The bias of ignore_eos and an inconsistent OSL&lt;/h2>
&lt;p>Most LLM models generate a special end-of-sequence token (EOS) when
they consider the response complete. If the benchmark does not set &lt;code>ignore_eos=True&lt;/code>, the real
output length (OSL) varies from request to request according to the model&amp;rsquo;s natural length
distribution, which produces:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Inconsistent OSL&lt;/strong>: two runs with the same seed can produce different OSL
distributions if the model varies its natural output.&lt;/li>
&lt;li>&lt;strong>Spurious comparison&lt;/strong>: a &amp;ldquo;faster&amp;rdquo; model may be so simply because it generates
shorter responses (it hits EOS sooner), not because it has more real throughput.&lt;/li>
&lt;li>&lt;strong>Underestimated throughput&lt;/strong>: if the benchmark expects OSL=256 but the model stops at
OSL=80 on average, the throughput in tok/s appears higher but measures less work.&lt;/li>
&lt;/ol>
&lt;p>The &lt;code>ignore_eos&lt;/code> parameter (or &lt;code>--ignore-eos&lt;/code> in vLLM bench) instructs the engine to ignore the
EOS token and continue until reaching &lt;code>max_tokens&lt;/code>. It is &lt;strong>mandatory&lt;/strong> for the OSL to be the
configured one rather than the model&amp;rsquo;s natural one, and for two runs to be comparable
(&lt;a href="https://docs.vllm.ai/en/latest/benchmarking/cli/">vLLM benchmark CLI docs&lt;/a>).&lt;/p>
&lt;hr>
&lt;h2 id="the-bias-of-the-islosl-distribution">The bias of the ISL/OSL distribution&lt;/h2>
&lt;p>The distribution of input (ISL) and output (OSL) lengths determines what proportion of the
compute time goes to prefill (costly in TTFT) and how much to decode (costly in
ITL). A benchmark with a fixed length, say ISL=128, OSL=128, produces results that
do not resemble any real traffic.&lt;/p>
&lt;p>Real use cases have very different distributions:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Use case&lt;/th>
&lt;th>Typical ISL (tokens)&lt;/th>
&lt;th>Typical OSL (tokens)&lt;/th>
&lt;th>Dominated by&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Translation&lt;/td>
&lt;td>500–2,000&lt;/td>
&lt;td>500–2,000&lt;/td>
&lt;td>balanced&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Generation (code, email)&lt;/td>
&lt;td>~100&lt;/td>
&lt;td>~1,000&lt;/td>
&lt;td>decode (long OSL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Summarisation / RAG&lt;/td>
&lt;td>~1,000&lt;/td>
&lt;td>~100&lt;/td>
&lt;td>prefill (long ISL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reasoning (CoT)&lt;/td>
&lt;td>~100&lt;/td>
&lt;td>1,000–10,000&lt;/td>
&lt;td>very long decode&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A short-ISL benchmark with a model optimised for prefill will give an artificially
low TTFT and an artificially high throughput. The knee of the sweep shifts: with ISL=64 the
system admits more concurrency without breaking the TTFT SLO; with ISL=1,024 prefill saturates
earlier and the knee appears earlier. Using the wrong distribution means sizing for traffic
that does not exist.&lt;/p>
&lt;p>AIPerf introduces &lt;strong>sequence distributions&lt;/strong> with configurable per-component variance
to reproduce realistic traffic mixes
(&lt;a href="https://docs.nvidia.com/aiperf/tutorials/datasets-inputs/sequence-length-distributions-for-advanced-benchmarking">AIPerf · Sequence Length Distributions&lt;/a>):&lt;/p>
&lt;pre tabindex="0">&lt;code>--sequence-distribution &amp;#34;64|10,32|8:70;256|40,128|20:20;1024|100,512|50:10&amp;#34;
&lt;/code>&lt;/pre>&lt;p>This creates 70 % of requests with \(\text{ISL} \sim \mathcal{N}(64, 10)\) and
\(\text{OSL} \sim \mathcal{N}(32, 8)\), 20 % with medium ISL/OSL, and 10 % with
long ISL/OSL, far more faithful to real chatbot traffic than a fixed length.&lt;/p>
&lt;hr>
&lt;h2 id="the-bias-of-warmup-and-the-prefix-cache">The bias of warmup and the prefix cache&lt;/h2>
&lt;p>Two related but distinct sources of bias:&lt;/p>
&lt;p>&lt;strong>Missing warmup.&lt;/strong> The first requests of a benchmark hit a &amp;ldquo;cold&amp;rdquo; engine: the
GPU is in a low-clock state, the KV cache is empty, and the operating system may
be paging memory. The TTFT of the first requests is structurally higher than
that of the steady state. If the benchmark does not discard a warmup period, the mean
TTFT includes these outliers and overestimates the real production latency. Some frameworks
(GenAI-Perf/AIPerf) use a &lt;strong>sliding window&lt;/strong> that excludes the ramp-up and
ramp-down requests; others do not.&lt;/p>
&lt;p>&lt;strong>Warm prefix cache between runs.&lt;/strong> The prefix KV cache (prefix cache or prompt
cache) stores the computed results of repeated prompt prefixes. If the
benchmark runs multiple consecutive runs with the same prompts, the second and
subsequent runs find the KV cache already populated and report an artificially
low TTFT, that of the decode, not of the prefill. For a baseline benchmark, the prefix cache must
be cold; for one that simulates production with repeated prompts, warm. The distinction
must be made explicit
(&lt;a href="https://arxiv.org/abs/2605.24217">arXiv 2605.24217&lt;/a>; &lt;a href="https://docs.vllm.ai/en/stable/design/prefix_caching/">vLLM · Automatic Prefix Caching&lt;/a>).&lt;/p>
&lt;hr>
&lt;h2 id="the-bias-of-concurrency-vs-request-rate">The bias of &lt;code>concurrency&lt;/code> vs &lt;code>request-rate&lt;/code>&lt;/h2>
&lt;p>The two load control modes produce different latency distributions for the
same system:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th>Semantics&lt;/th>
&lt;th>When to use&lt;/th>
&lt;th>Risk&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>concurrency N&lt;/strong>&lt;/td>
&lt;td>keeps exactly N requests in flight; as soon as one finishes, it launches another&lt;/td>
&lt;td>measuring the system under a fixed concurrency load&lt;/td>
&lt;td>over-represents sustained load; does not simulate real arrivals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>request-rate r&lt;/strong> (constant or Poisson)&lt;/td>
&lt;td>launches one request every \(1/r\) seconds (constant) or with interarrival \(\sim \text{Exp}(1/r)\) (Poisson)&lt;/td>
&lt;td>simulating real traffic (random arrivals)&lt;/td>
&lt;td>if the engine cannot absorb r req/s, the queue grows without a ceiling&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>NVIDIA recommends the &lt;strong>concurrency&lt;/strong> mode for most capacity benchmarks
(&lt;a href="https://developer.nvidia.com/blog/llm-benchmarking-fundamental-concepts/">NVIDIA · LLM Benchmarking Fundamental Concepts&lt;/a>). The request-rate mode is more faithful for online traffic
(a Poisson distribution of arrivals), but if the rate exceeds the engine&amp;rsquo;s capacity the queue
grows indefinitely and the latency metrics include queueing time that can
dominate the TTFT, mixing queue behaviour with engine behaviour.&lt;/p>
&lt;p>A system measured at concurrency=16 and another at request-rate=16 req/s &lt;strong>are not under the
same load&lt;/strong>: fixed concurrency guarantees 16 simultaneous requests; the rate sets
the arrival pace but not the instantaneous concurrency. Comparing their results without adjustment
is incorrect.&lt;/p>
&lt;hr>
&lt;h2 id="the-bias-of-the-measurement-window-and-the-thermal-state">The bias of the measurement window and the thermal state&lt;/h2>
&lt;p>&lt;strong>Window too short.&lt;/strong> A 30-second benchmark may measure the system&amp;rsquo;s
ramp-up, not the steady state. The recommendation of arXiv 2508.10251 is that the measurement
window cover at least 3-5× the system&amp;rsquo;s ramp time under load, and that the
metrics be computed only over the steady window, excluding warmup and cooldown.&lt;/p>
&lt;p>&lt;strong>Thermal state of the GPU.&lt;/strong> Datacenter GPUs (H100 SXM, A100) operate with thermal
throttling above ~75 °C. If the GPU has not reached its steady-state temperature before the
benchmark, the first measurements correspond to a higher-clock state than the
sustainable one. Controlled experiments document that power must stabilise within a
range of &lt;strong>3 W&lt;/strong> for at least 30 seconds before the measurements are representative
(&lt;a href="https://arxiv.org/html/2604.09048v1">arXiv 2604.09048 · Watt Counts&lt;/a>). The effect is
especially severe in prefill benchmarks (TTFT), where the high clocks of the cold
state produce an artificially low TTFT.&lt;/p>
&lt;p>For a reproducible benchmark on 4×H100 SXM 80 GB: before measuring, run sustained load
for at least 2–3 minutes until &lt;code>nvidia-smi&lt;/code> reports a stable temperature and stable
power (variation &amp;lt; 3 W over 30 s). Only then start the measurement window.&lt;/p>
&lt;hr>
&lt;h2 id="how-the-tools-compute-itl-differently">How the tools compute ITL differently&lt;/h2>
&lt;p>ITL (Inter-Token Latency) appears in every tool but its formula varies, and the
differences are not small:&lt;/p>
&lt;p>&lt;strong>GenAI-Perf / AIPerf:&lt;/strong>&lt;/p>
$$\text{ITL} = \frac{\text{e2e latency} - \text{TTFT}}{\text{output tokens} - 1}$$
&lt;p>TTFT is excluded from the numerator and the denominator discounts the first token. ITL is a
pure &lt;strong>decode&lt;/strong> metric, with no prefill contamination.&lt;/p>
&lt;p>&lt;strong>LLMPerf&lt;/strong> (archived Dec. 2025):&lt;/p>
$$\text{ITL}_{\text{LLMPerf}} = \frac{\text{e2e latency}}{\text{output tokens}}$$
&lt;p>TTFT &lt;strong>is included&lt;/strong> in the numerator. For short sequences (OSL &amp;lt; 50 tokens), TTFT
can represent 50–80 % of the e2e_latency, so that LLMPerf&amp;rsquo;s ITL measures
mainly prefill, not decode. Two systems with the same decode but different
prefill will show different ITLs in LLMPerf even if they are identical in decode speed.&lt;/p>
&lt;p>&lt;strong>vLLM bench (benchmark_serving.py):&lt;/strong>&lt;/p>
&lt;p>It computes ITL as the mean of the intervals between consecutive tokens of the output stream,
including intra-request variance. It can reveal decode jitter that the others average away.&lt;/p>
&lt;p>The practical consequence: &lt;strong>never cross a GenAI-Perf ITL with an LLMPerf one&lt;/strong> as if
they were the same metric. They are different formulas over the same signal.&lt;/p>
&lt;hr>
&lt;h2 id="comparability-across-tools-why-their-numbers-do-not-cross">Comparability across tools: why their numbers do not cross&lt;/h2>
&lt;p>The following table summarises the methodological differences that make one tool&amp;rsquo;s numbers
&lt;strong>structurally incompatible&lt;/strong> with another&amp;rsquo;s without an explicit adjustment:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimension&lt;/th>
&lt;th>vLLM bench&lt;/th>
&lt;th>LLMPerf (archived)&lt;/th>
&lt;th>GenAI-Perf / AIPerf&lt;/th>
&lt;th>GuideLLM&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Client architecture&lt;/strong>&lt;/td>
&lt;td>single-process asyncio&lt;/td>
&lt;td>single-process&lt;/td>
&lt;td>&lt;strong>multi-process&lt;/strong>&lt;/td>
&lt;td>&lt;strong>multi-process&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ITL includes TTFT&lt;/strong>&lt;/td>
&lt;td>no&lt;/td>
&lt;td>&lt;strong>yes&lt;/strong>&lt;/td>
&lt;td>no&lt;/td>
&lt;td>no&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPS denominator&lt;/strong>&lt;/td>
&lt;td>\(T_y - T_x\)&lt;/td>
&lt;td>\(T_{\text{end}} - T_{\text{start}}\) (overhead included)&lt;/td>
&lt;td>\(T_y - T_x\)&lt;/td>
&lt;td>per round&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tokenizer&lt;/strong>&lt;/td>
&lt;td>that of the model served&lt;/td>
&lt;td>universal LlamaTokenizer&lt;/td>
&lt;td>that of the model served&lt;/td>
&lt;td>that of the model served&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Warmup / sliding window&lt;/strong>&lt;/td>
&lt;td>not automatic&lt;/td>
&lt;td>no&lt;/td>
&lt;td>&lt;strong>yes (sliding window)&lt;/strong>&lt;/td>
&lt;td>per round&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ignore_eos by default&lt;/strong>&lt;/td>
&lt;td>no&lt;/td>
&lt;td>no&lt;/td>
&lt;td>explicitly recommended&lt;/td>
&lt;td>configurable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ISL/OSL distribution&lt;/strong>&lt;/td>
&lt;td>manual parameter&lt;/td>
&lt;td>manual parameter&lt;/td>
&lt;td>distributions with variance&lt;/td>
&lt;td>configurable &lt;code>--data&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Primary load mode&lt;/strong>&lt;/td>
&lt;td>concurrency&lt;/td>
&lt;td>concurrency (drained batches)&lt;/td>
&lt;td>concurrency (recommended)&lt;/td>
&lt;td>sweep + poisson&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Structured output&lt;/strong>&lt;/td>
&lt;td>basic JSON&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>JSON + CSV&lt;/td>
&lt;td>JSON + HTML + CSV&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>An LLMPerf result and a GenAI-Perf result for the same endpoint can differ in TTFT,
ITL and TPS &lt;strong>simultaneously&lt;/strong> and in all of them for methodological reasons, not because of engine
differences. The only way to cross them is to run both tools on the same system
under the same conditions and compute the empirical conversion factor, which in practice
amounts to re-measuring with a single tool.&lt;/p>
&lt;hr>
&lt;h2 id="the-honest-harness-a-reproducibility-checklist">The honest harness: a reproducibility checklist&lt;/h2>
&lt;p>A benchmark whose number can be defended in an audit has to come accompanied by
all the metadata that allow it to be reproduced. The minimum to demand, organised by category:&lt;/p>
&lt;h3 id="hardware">Hardware&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metadata&lt;/th>
&lt;th>Example 4×H100&lt;/th>
&lt;th>Effect if not fixed&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPU (model, variant, count)&lt;/td>
&lt;td>4× NVIDIA H100 SXM 80 GB&lt;/td>
&lt;td>H100 PCIe vs SXM differ in memory BW and NVLink&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU-GPU interconnect&lt;/td>
&lt;td>NVLink 4.0&lt;/td>
&lt;td>tensor parallelism depends on the interconnect BW&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CPU, RAM, CPU-GPU bandwidth&lt;/td>
&lt;td>2× Intel Xeon 8480+, 512 GB DDR5, PCIe 5.0&lt;/td>
&lt;td>the tokenisation bottleneck can be on the CPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Thermal state at the start&lt;/td>
&lt;td>GPU temperature &amp;lt; 65 °C, stable power ± 3 W&lt;/td>
&lt;td>throttling alters TPS and TTFT by up to ~15 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU clocks&lt;/td>
&lt;td>without &lt;code>nvidia-smi -pm 1&lt;/code> they can vary&lt;/td>
&lt;td>a difference of up to ~10 % in throughput&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="software-and-model">Software and model&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metadata&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Effect if not fixed&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Engine + version + flags&lt;/td>
&lt;td>vLLM 0.8.4, &lt;code>--tensor-parallel-size 4 --gpu-memory-utilization 0.90&lt;/code>&lt;/td>
&lt;td>each version changes the scheduler and the KV cache management&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model + precision&lt;/td>
&lt;td>Llama-3-70B-Instruct, FP8&lt;/td>
&lt;td>FP16 vs FP8 differ by ~1.4–1.8× in throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenizer used for counting&lt;/td>
&lt;td>tokenizer of the model served (HF tokenizer)&lt;/td>
&lt;td>a universal LlamaTokenizer biases tok/s across vocabularies&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Generation seed&lt;/td>
&lt;td>&lt;code>--seed 42&lt;/code>&lt;/td>
&lt;td>without a fixed seed, the OSL varies run-to-run&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ignore_eos&lt;/code>&lt;/td>
&lt;td>&lt;code>True&lt;/code>&lt;/td>
&lt;td>without it, the OSL varies with the prompt content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sampling parameters&lt;/td>
&lt;td>&lt;code>temperature=1.0, top_p=0.95&lt;/code>&lt;/td>
&lt;td>greedy vs sampling affect logit speed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="load">Load&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metadata&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Effect if not fixed&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Bench tool + version&lt;/td>
&lt;td>AIPerf 0.2.1&lt;/td>
&lt;td>each version changes formulas and warmup&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ISL/OSL distribution&lt;/td>
&lt;td>\(\mathcal{N}(512, 64)\) ISL, \(\mathcal{N}(128, 20)\) OSL&lt;/td>
&lt;td>changing the distribution moves the knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Load mode&lt;/td>
&lt;td>concurrency, sweep 1–64&lt;/td>
&lt;td>concurrency vs request-rate: different distributions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Concurrency levels&lt;/td>
&lt;td>1, 2, 4, 8, 16, 32, 64 (full sweep, beyond the knee)&lt;/td>
&lt;td>without passing the knee, the safe capacity is unknown&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Duration per point&lt;/td>
&lt;td>120 s minimum per level&lt;/td>
&lt;td>short windows capture ramp-up, not the steady state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Warmup&lt;/td>
&lt;td>30 s excluded from the metric computation&lt;/td>
&lt;td>without warmup, the metrics include the cold state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefix cache treatment&lt;/td>
&lt;td>cold (flush between runs) or warm (declared explicitly)&lt;/td>
&lt;td>warm cache: unrealistic TTFT for a baseline&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="slo-and-reported-metrics">SLO and reported metrics&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metadata&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Effect if not declared&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Declared SLO&lt;/td>
&lt;td>TTFT P99 &amp;lt; 500 ms, ITL P95 &amp;lt; 50 ms&lt;/td>
&lt;td>goodput depends on the SLO; without an SLO there is no goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reported percentiles&lt;/td>
&lt;td>P50, P95, P99 (not just the mean)&lt;/td>
&lt;td>the mean hides the tail; irrelevant for an SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput vs goodput&lt;/td>
&lt;td>goodput under the SLO, not raw throughput&lt;/td>
&lt;td>catalogue throughput can be 5–10× the goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Total number of requests&lt;/td>
&lt;td>≥ 1,000 per concurrency level&lt;/td>
&lt;td>small samples: high variance in percentiles&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="methodological-defence-of-the-datum-before-an-audit">Methodological defence of the datum before an audit&lt;/h2>
&lt;p>A throughput number presented without the data sheet above is not a datum: it is an anecdote.
The validation pattern for a technical audit:&lt;/p>
&lt;p>&lt;strong>1. Traceability of the tool.&lt;/strong> The tool and its version must be pinnable to a
Git commit or a container image hash. AIPerf and GuideLLM export JSON with
version metadata; vLLM bench omits them by default and they have to be captured manually.&lt;/p>
&lt;p>&lt;strong>2. Reproducibility of the environment.&lt;/strong> The engine deployment script (or the Kubernetes
manifest) and the exact benchmark command must be enough for a third party to
reproduce the number on the same hardware. GuideLLM exports the benchmark file as an
authoritative record of the session: configuration, metadata, per-benchmark statistics and
request entries with individual timings
(&lt;a href="https://github.com/vllm-project/guidellm">GitHub vllm-project/guidellm&lt;/a>).&lt;/p>
&lt;p>&lt;strong>3. Verification of the steady state.&lt;/strong> The metrics must come from the benchmark&amp;rsquo;s
steady window (warmup excluded). The throughput vs concurrency curve must
show the knee, the point where throughput saturates and latency blows up, and extend
beyond it. Without a visible knee, the reported capacity may be the client&amp;rsquo;s limit, not
the engine&amp;rsquo;s.&lt;/p>
&lt;p>&lt;strong>4. Goodput, not raw throughput.&lt;/strong> The auditable throughput is the goodput under the declared
SLO, not the peak throughput. A system that reports 4,000 tok/s but whose goodput
under TTFT P99 &amp;lt; 500 ms is 1,800 tok/s has a real capacity of 1,800 tok/s for
interactive use cases.&lt;/p>
&lt;p>&lt;strong>5. Internal comparability.&lt;/strong> If two engines or two configurations are compared, the
tool, the load distribution, the SLO, the hardware and the warmup treatment
must be &lt;strong>identical&lt;/strong>. Any difference in these dimensions contaminates the comparison.&lt;/p>
&lt;p>The goodput formula as an auditable metric:&lt;/p>
$$\text{goodput} = \text{throughput} \times \Pr[\text{TTFT} \leq \text{SLO}_{\text{TTFT}}] \times \Pr[\text{ITL} \leq \text{SLO}_{\text{ITL}}]$$
&lt;p>Where the probabilities are estimated over the empirical distribution of the run. An engine
with a throughput of 4,000 tok/s and \(\Pr[\text{TTFT} \leq 500\,\text{ms}] = 0.45\) has
a goodput of \(4{,}000 \times 0.45 = 1{,}800\) tok/s.&lt;/p>
&lt;hr>
&lt;h2 id="a-numerical-example-the-same-node-four-different-measurements">A numerical example: the same node, four different measurements&lt;/h2>
&lt;p>Reference hardware: 4×H100 SXM 80 GB, model Llama-3-70B-Instruct FP8, tensor
parallel 4, SLO TTFT P99 &amp;lt; 500 ms.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark configuration&lt;/th>
&lt;th>Reported throughput&lt;/th>
&lt;th>Real goodput&lt;/th>
&lt;th>Factor vs goodput&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>vLLM bench, concurrency=32, no ignore_eos, no warmup, fixed OSL=64&lt;/td>
&lt;td>5,800 tok/s&lt;/td>
&lt;td>~1,200 tok/s (short OSL inflates TPS)&lt;/td>
&lt;td>&lt;strong>4.8×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLMPerf, concurrency=32, LlamaTokenizer, overhead included in the denominator&lt;/td>
&lt;td>3,100 tok/s&lt;/td>
&lt;td>~1,600 tok/s (ITL includes TTFT)&lt;/td>
&lt;td>&lt;strong>1.9×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AIPerf, concurrency=32, ignore_eos, sliding window, model&amp;rsquo;s tokenizer&lt;/td>
&lt;td>3,400 tok/s&lt;/td>
&lt;td>3,350 tok/s&lt;/td>
&lt;td>&lt;strong>1.01×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GuideLLM, sweep 1–64, poisson, realistic ISL/OSL distribution, declared SLO&lt;/td>
&lt;td>3,330 tok/s goodput (knee at round 6)&lt;/td>
&lt;td>3,330 tok/s&lt;/td>
&lt;td>&lt;strong>1.0× (honest baseline)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The range: from 1,200 tok/s to 5,800 tok/s for the same engine, the same hardware and the same
model. The maximum factor is &lt;strong>4.8×&lt;/strong>. The cause is not the engine; it is the
instrumentation decisions.&lt;/p>
&lt;hr>
&lt;h2 id="decomposing-the-total-bias">Decomposing the total bias&lt;/h2>
&lt;p>The total observable bias between two tools is the multiplicative composition of the
individual biases. For a comparison of single-process vLLM bench vs multi-process GuideLLM
on the same system:&lt;/p>
$$\text{total factor} \approx \underbrace{f_{\text{client}}}_{\leq 7.2\times} \times \underbrace{f_{\text{tokenizer}}}_{1.0{-}1.3\times} \times \underbrace{f_{\text{ignore-eos}}}_{1.0{-}2.0\times} \times \underbrace{f_{\text{ISL/OSL}}}_{1.0{-}1.5\times} \times \underbrace{f_{\text{warmup}}}_{1.0{-}1.2\times}$$
&lt;p>The client saturation factor (\(\leq 7.2\times\)) dominates, but the other factors
multiply. Under adverse conditions (single-process client + different tokenizer + no
ignore_eos + unrealistic OSL + no warmup), the compound bias can exceed &lt;strong>15–20×&lt;/strong> for
the same system.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-2026-what-has-changed">State of the art 2026: what has changed&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>genai-perf → AIPerf migration&lt;/strong> (15 Apr 2026): NVIDIA retired genai-perf and replaced it
with AIPerf, which is multi-process with automatic saturation detection, configurable
ISL/OSL distributions and a built-in sliding window. The migration removes the single-process
client bias from NVIDIA&amp;rsquo;s official tooling.&lt;/li>
&lt;li>&lt;strong>LLMPerf archived&lt;/strong> (Dec. 2025): the &lt;code>ray-project/llmperf&lt;/code> repository is archived and
in read-only mode. The historical results of the LLMPerf leaderboard are comparable
only among themselves; they must not be crossed with modern measurements without adjustment.&lt;/li>
&lt;li>&lt;strong>GuideLLM 0.5.x&lt;/strong> (2025–2026): a complete architectural refactor, multimodal support, and
authoritative JSON export with all the session metadata. It is the OSS standard for
SLO-driven evaluation.&lt;/li>
&lt;li>&lt;strong>arXiv 2605.24217&lt;/strong> (May 2026): the first formal characterisation of systematic measurement
bias in production LLM benchmarks, with a mathematical demonstration of the GIL effect and
a proposal for a multi-process harness.&lt;/li>
&lt;li>&lt;strong>arXiv 2508.10251&lt;/strong>: meta-metrics and good practice for system-level performance
benchmarking; it establishes the minimum dimensions of the parameter space that must be
declared for a result to be reproducible.&lt;/li>
&lt;li>&lt;strong>MLPerf Inference v5.1&lt;/strong> (Sep. 2025): 27 participants, a participation record. The
MLPerf rules require declaring the exact code, the dataset and the hardware, and admit
public review; it is the only industry benchmark with a formal review process for
reproducibility.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="cross-links-from-the-benchmarking-track">Cross-links from the benchmarking track&lt;/h2>
&lt;p>The biases described here apply to every tool in the catalogue. For the complete
data sheet of each tool:&lt;/p>
&lt;ul>
&lt;li>Full catalogue: &lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">LLM benchmarking tools, data sheet by data sheet&lt;/a>&lt;/li>
&lt;li>GuideLLM and SLO validation: &lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">GuideLLM in depth: validating the SLO under load&lt;/a>&lt;/li>
&lt;li>AIPerf (ex genai-perf) and saturation detection: &lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">NVIDIA GenAI-Perf in depth&lt;/a>&lt;/li>
&lt;li>State of the art of frameworks: &lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">LLM benchmarking: frameworks and state of the art&lt;/a>&lt;/li>
&lt;li>Engine decision on the Pareto axis: &lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">Serving engines compared: the Pareto frontier&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>arXiv 2605.24217 · Identifying and Mitigating Systemic Measurement Bias in Production LLM Inference Benchmarks — &lt;a href="https://arxiv.org/abs/2605.24217">https://arxiv.org/abs/2605.24217&lt;/a>&lt;/li>
&lt;li>arXiv 2508.10251 · Meta-Metrics and Best Practices for System-Level Inference Performance Benchmarking — &lt;a href="https://arxiv.org/pdf/2508.10251">https://arxiv.org/pdf/2508.10251&lt;/a>&lt;/li>
&lt;li>arXiv 2604.09048 · Watt Counts: Energy-Aware Benchmark for Sustainable LLM Inference on Heterogeneous GPU Architectures — &lt;a href="https://arxiv.org/html/2604.09048v1">https://arxiv.org/html/2604.09048v1&lt;/a>&lt;/li>
&lt;li>NVIDIA Technical Blog · LLM Inference Benchmarking: Fundamental Concepts (ITL, TPS, ISL/OSL, ignore_eos, concurrency vs request-rate) — &lt;a href="https://developer.nvidia.com/blog/llm-benchmarking-fundamental-concepts/">https://developer.nvidia.com/blog/llm-benchmarking-fundamental-concepts/&lt;/a>&lt;/li>
&lt;li>NVIDIA AIPerf Docs · Sequence Length Distributions for Advanced Benchmarking — &lt;a href="https://docs.nvidia.com/aiperf/tutorials/datasets-inputs/sequence-length-distributions-for-advanced-benchmarking">https://docs.nvidia.com/aiperf/tutorials/datasets-inputs/sequence-length-distributions-for-advanced-benchmarking&lt;/a>&lt;/li>
&lt;li>NVIDIA AIPerf · Request Rate with Max Concurrency — &lt;a href="https://docs.nvidia.com/aiperf/tutorials/load-patterns-scheduling/request-rate-with-max-concurrency">https://docs.nvidia.com/aiperf/tutorials/load-patterns-scheduling/request-rate-with-max-concurrency&lt;/a>&lt;/li>
&lt;li>NVIDIA AIPerf · Comprehensive LLM Benchmarking Guide — &lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/&lt;/a>&lt;/li>
&lt;li>vLLM Documentation · Benchmark CLI (ignore_eos, métricas disponibles) — &lt;a href="https://docs.vllm.ai/en/latest/benchmarking/cli/">https://docs.vllm.ai/en/latest/benchmarking/cli/&lt;/a>&lt;/li>
&lt;li>vLLM Documentation · Automatic Prefix Caching — &lt;a href="https://docs.vllm.ai/en/stable/design/prefix_caching/">https://docs.vllm.ai/en/stable/design/prefix_caching/&lt;/a>&lt;/li>
&lt;li>GitHub · ray-project/llmperf (archivado dic. 2025) — &lt;a href="https://github.com/ray-project/llmperf">https://github.com/ray-project/llmperf&lt;/a>&lt;/li>
&lt;li>GitHub · vllm-project/guidellm (exportación JSON autoritativa, metadatos de sesión) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&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>MLCommons · MLPerf Inference v5.1 results (récord 27 participantes, reproducibilidad formal) — &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>arXiv 2502.16721 · Speed and Conversational LLMs: Not All Is About Tokens per Second (tokenizer incompatibilidad y tok/s entre vocabularios) — &lt;a href="https://arxiv.org/pdf/2502.16721">https://arxiv.org/pdf/2502.16721&lt;/a>&lt;/li>
&lt;li>Medium · LLM Inference Benchmarking (genAI-perf y vLLM, discrepancia 7,2×) — &lt;a href="https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e">https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>LLM quality benchmarks: the contamination trap and the OSS tools for not fooling yourself</title><link>https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/</link><pubDate>Tue, 16 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a full stop. The dollar sign is not used
(on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>Post &lt;strong>B7&lt;/strong> of the benchmarking track. The &lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">B1 introduction&lt;/a>
established the distinction between the three axes (performance, quality, energy). The &lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">B2 tool catalogue&lt;/a>
covered the performance axis (TTFT, ITL, goodput). This article covers the &lt;strong>quality&lt;/strong> axis:
what each benchmark measures, how it is run, and why the leaderboard numbers do not always
say what they appear to say. The &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLMOps eval pipeline&lt;/a>
and &lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> cover the system&amp;rsquo;s internal
evaluation layer; this article covers the evaluation of the &lt;strong>base model&lt;/strong> with standard
academic benchmarks.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An inference engine with an 80 ms TTFT serving a low-quality model is worth nothing: the platform&amp;rsquo;s Pareto front has two independent axes. In 2025–2026, the most widely used static benchmarks (MMLU, HumanEval) have crossed the saturation threshold (HumanEval &amp;gt; 93% in frontier models, MMLU-Pro &amp;gt; 75% in the top-5), which invalidates their ability to discriminate at the high end. The main cause is not that the models are better: it is data contamination, with 18% n-gram overlap detected in Llama-2-70B against evaluation datasets. The OSS tools active in 2026 are &lt;strong>lm-evaluation-harness&lt;/strong> (EleutherAI, MIT, 60+ tasks, the ecosystem&amp;rsquo;s de facto engine), &lt;strong>HELM&lt;/strong> (Stanford, Apache 2.0, in maintenance since June 2026), &lt;strong>LiveBench&lt;/strong> (MIT, 1,000 questions refreshed monthly to avoid contamination), &lt;strong>lighteval&lt;/strong> (Hugging Face, MIT, 1,000+ tasks, vLLM support), and &lt;strong>LMArena/Chatbot Arena&lt;/strong> (Bradley-Terry over millions of human comparisons). OpenLLM Leaderboard v2 was archived in 2025 and replaced by the &lt;strong>OpenEvals&lt;/strong> ecosystem (200+ specialised leaderboards). Reproducing an eval number requires pinning: harness version, model version, prompt template version and number of shots.&lt;/p>
&lt;hr>
&lt;h2 id="quality--performance-the-independent-pareto-axis">Quality ≠ performance: the independent Pareto axis&lt;/h2>
&lt;p>The performance axis (TTFT, ITL, throughput) measures &lt;strong>how long&lt;/strong> the engine takes to produce tokens.
The quality axis measures &lt;strong>how correct&lt;/strong> those tokens are. They are orthogonal: a small model
served with vLLM at 3,000 tok/s can score 42% on MMLU-Pro; a frontier model may need
4× more GPU and score 72%. The product throughput × quality is the only number that matters for
a platform in production.&lt;/p>
&lt;div class="diagram" style="max-width:700px;margin:1rem auto;">
&lt;svg viewBox="0 0 700 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pareto space of quality versus performance with four quadrants">
&lt;style>
.ax{fill:none;stroke:currentColor;stroke-width:1.4}
.tl{font:600 13px sans-serif;fill:currentColor}
.ts{font:11px sans-serif;fill:currentColor}
.dot{stroke:currentColor;stroke-width:1.2}
.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#arh)}
&lt;/style>
&lt;defs>
&lt;marker id="arh" 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;line class="ax" x1="60" y1="260" x2="660" y2="260"/>
&lt;line class="ax" x1="60" y1="260" x2="60" y2="30"/>
&lt;path class="ar" d="M650,260 L665,260"/>
&lt;path class="ar" d="M60,40 L60,25"/>
&lt;text x="670" y="264" class="ts">Performance (tok/s)&lt;/text>
&lt;text x="65" y="22" class="ts">Quality (MMLU-Pro %)&lt;/text>
&lt;text x="62" y="258" class="ts">0&lt;/text>
&lt;text x="55" y="155" class="ts" text-anchor="end">50&lt;/text>
&lt;text x="55" y="80" class="ts" text-anchor="end">72&lt;/text>
&lt;text x="200" y="270" class="ts">1000&lt;/text>
&lt;text x="460" y="270" class="ts">3000&lt;/text>
&lt;line class="ax" x1="58" y1="155" x2="62" y2="155"/>
&lt;line class="ax" x1="58" y1="80" x2="62" y2="80"/>
&lt;line class="ax" x1="200" y1="258" x2="200" y2="262"/>
&lt;line class="ax" x1="460" y1="258" x2="460" y2="262"/>
&lt;circle class="dot" cx="200" cy="155" r="10" fill="none"/>
&lt;text x="212" y="150" class="ts">Small fast model&lt;/text>
&lt;text x="212" y="163" class="ts">(42% / 3,000 tok/s) — useless if the task demands it&lt;/text>
&lt;circle class="dot" cx="460" cy="80" r="10" fill="none"/>
&lt;text x="472" y="75" class="ts">Optimised frontier model&lt;/text>
&lt;text x="472" y="88" class="ts">(72% / 3,000 tok/s) — Pareto zone&lt;/text>
&lt;circle class="dot" cx="180" cy="80" r="10" fill="none"/>
&lt;text x="192" y="75" class="ts">Slow frontier&lt;/text>
&lt;text x="192" y="88" class="ts">(72% / 800 tok/s)&lt;/text>
&lt;circle class="dot" cx="500" cy="200" r="10" fill="none"/>
&lt;text x="512" y="195" class="ts">Fast engine, bad model&lt;/text>
&lt;text x="512" y="208" class="ts">(35% / 4,000 tok/s) — unusable&lt;/text>
&lt;text x="330" y="300" class="tl" text-anchor="middle">Useful zone: quality ≥ task threshold ∧ performance ≥ production SLO&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The operational consequence: select model and engine &lt;strong>first by quality&lt;/strong> (does it clear the minimum threshold for the task?) and &lt;strong>then by performance&lt;/strong> (does it meet the SLO within the GPU budget?). Inverting the order, picking the fastest engine and fitting the model into it, is the most frequent mistake in initial sizing.&lt;/p>
&lt;hr>
&lt;h2 id="reference-benchmarks-what-each-one-measures">Reference benchmarks: what each one measures&lt;/h2>
&lt;p>The table below collects the standard academic benchmarks in 2026, the capability each one measures, its test set size and the current saturation level in frontier models.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark&lt;/th>
&lt;th>Capability measured&lt;/th>
&lt;th>Format&lt;/th>
&lt;th>Test set&lt;/th>
&lt;th>Frontier saturation 2026&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>MMLU&lt;/strong>&lt;/td>
&lt;td>Multidisciplinary knowledge (57 subjects)&lt;/td>
&lt;td>4-choice MCQ&lt;/td>
&lt;td>14,079&lt;/td>
&lt;td>High: top-5 &amp;gt; 88%; insufficient separation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MMLU-Pro&lt;/strong>&lt;/td>
&lt;td>MMLU with 10 options, multistep reasoning&lt;/td>
&lt;td>10-choice MCQ&lt;/td>
&lt;td>12,032&lt;/td>
&lt;td>Medium: top-5 between 72–77%; still separates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GSM8K&lt;/strong>&lt;/td>
&lt;td>Primary-school maths reasoning (word problems)&lt;/td>
&lt;td>answer generation&lt;/td>
&lt;td>1,319&lt;/td>
&lt;td>Very high: top models &amp;gt; 97%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MATH&lt;/strong>&lt;/td>
&lt;td>Competition maths (algebra, geometry, calculus, etc.)&lt;/td>
&lt;td>LaTeX generation&lt;/td>
&lt;td>5,000&lt;/td>
&lt;td>Medium-high: top models 85–92%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GPQA Diamond&lt;/strong>&lt;/td>
&lt;td>PhD-level scientific reasoning (biology, physics, chemistry)&lt;/td>
&lt;td>4-choice MCQ&lt;/td>
&lt;td>198&lt;/td>
&lt;td>Emerging: top &amp;gt; 90% in 2026; still separates in 60–90%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>BBH&lt;/strong> (BIG-Bench Hard)&lt;/td>
&lt;td>23 tasks where earlier models fell below human level&lt;/td>
&lt;td>generation&lt;/td>
&lt;td>6,511&lt;/td>
&lt;td>Medium: useful for complex reasoning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>IFEval&lt;/strong>&lt;/td>
&lt;td>Verifiable instruction following (format, length, language)&lt;/td>
&lt;td>generation + rules&lt;/td>
&lt;td>541&lt;/td>
&lt;td>Low: notable separation between models&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HumanEval&lt;/strong>&lt;/td>
&lt;td>Python code generation (164 problems with signature)&lt;/td>
&lt;td>generation + execution&lt;/td>
&lt;td>164&lt;/td>
&lt;td>Very high: top &amp;gt; 93%; saturated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MBPP&lt;/strong>&lt;/td>
&lt;td>Code generation (500 problems without a function signature)&lt;/td>
&lt;td>generation + execution&lt;/td>
&lt;td>500&lt;/td>
&lt;td>High: top models 85–92%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MuSR&lt;/strong>&lt;/td>
&lt;td>Multistep reasoning over long texts&lt;/td>
&lt;td>generation&lt;/td>
&lt;td>~1,000&lt;/td>
&lt;td>Low: useful for extended reasoning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TruthfulQA&lt;/strong>&lt;/td>
&lt;td>Truthfulness: resistance to asserting popular myths&lt;/td>
&lt;td>generation + MCQ&lt;/td>
&lt;td>817&lt;/td>
&lt;td>Medium: visible separation between families&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Note on saturation:&lt;/strong> a saturated benchmark does not distinguish models at the high end; it is only valuable as a &lt;em>floor check&lt;/em> (confirming the model has not regressed to the level of three years ago). For the frontier zone, the useful benchmarks in 2026 are MMLU-Pro, GPQA Diamond, IFEval and MuSR.&lt;/p>
&lt;hr>
&lt;h2 id="oss-evaluation-tools-data-sheets">OSS evaluation tools: data sheets&lt;/h2>
&lt;h3 id="lm-evaluation-harness-eleutherai">lm-evaluation-harness (EleutherAI)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/EleutherAI/lm-evaluation-harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>EleutherAI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active version (Jun 2026)&lt;/td>
&lt;td>0.4.x (&lt;code>main&lt;/code> branch)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Available tasks&lt;/td>
&lt;td>60+ benchmarks, hundreds of subtasks and variants&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>Hugging Face Transformers, vLLM, litellm, OpenAI-compatible API, GGUF&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Task format&lt;/td>
&lt;td>YAML + Python; each task defines dataset, template, metric and number of shots&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>MCQ accuracy, exact match, pass@k on code, perplexity, BLEU/ROUGE&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>&lt;code>lm_eval --model hf --model_args pretrained=&amp;lt;model&amp;gt; --tasks mmlu,gsm8k --num_fewshot 5 --output_path results/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON with per-task metrics + configuration metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Use in the ecosystem&lt;/td>
&lt;td>Official engine of the OpenLLM Leaderboard (v1 and v2); de facto reference in research&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The harness is the industry standard for reproducible evaluation. Its strength is the breadth of tasks and the normalisation of the prompt format. Its limitation: the tasks are static, the datasets do not change between harness versions, which exposes it to the contamination problem if the model was trained on the same data.&lt;/p>
&lt;p>Full invocation with vLLM as the backend:&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">lm_eval &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model_args &lt;span class="nv">pretrained&lt;/span>&lt;span class="o">=&lt;/span>meta-llama/Llama-3.1-70B-Instruct,dtype&lt;span class="o">=&lt;/span>bfloat16 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tasks mmlu_pro,gpqa_diamond,ifeval,humaneval &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num_fewshot &lt;span class="m">5&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> --batch_size auto &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --output_path ./eval_results/ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --log_samples
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="helm-stanford-crfm">HELM (Stanford CRFM)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/stanford-crfm/helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Official site&lt;/td>
&lt;td>crfm.stanford.edu/helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>Stanford CRFM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Status (Jun 2026)&lt;/td>
&lt;td>&lt;strong>Maintenance mode&lt;/strong> since 1 June 2026&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>Accuracy, calibration, robustness, fairness, toxicity, efficiency (latency/cost)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Task format&lt;/td>
&lt;td>Python + YAML; scenarios with multiple simultaneous metrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>API (OpenAI, Anthropic, Cohere, etc.) + local models via Hugging Face&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Active variants&lt;/td>
&lt;td>HELM Classic, HELM Capabilities, HELM Lite, MedHELM, HELM Safety&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Installation&lt;/td>
&lt;td>&lt;code>pip install crfm-helm&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>&lt;code>helm-run --conf-paths run_specs.conf --suite v1 --max-eval-instances 1000&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON + web UI with a reproducible leaderboard and per-prompt transparency&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>HELM is the tool that introduced &lt;strong>holistic&lt;/strong> evaluation (multiple metrics over the same scenario instead of a single number). In 2026, with HELM in maintenance mode, the ecosystem has fragmented into specialised leaderboards; but HELM remains the methodological reference for evaluations that need fairness and toxicity as well as accuracy.&lt;/p>
&lt;p>&lt;strong>HELM Capabilities&lt;/strong> (published March 2025): an active leaderboard with full prompt transparency, reproducible with the HELM framework. Available at crfm.stanford.edu/helm/capabilities.&lt;/p>
&lt;hr>
&lt;h3 id="livebench">LiveBench&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/LiveBench/LiveBench&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Official site&lt;/td>
&lt;td>livebench.ai&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>NYU, UT Austin, UC Santa Barbara&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Paper&lt;/td>
&lt;td>arXiv:2406.19314&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Size&lt;/td>
&lt;td>1,000 active questions (monthly refresh, same total size)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Categories&lt;/td>
&lt;td>6: math, coding, reasoning, language, instruction following, data analysis&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tasks&lt;/td>
&lt;td>18 subtasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>General capabilities over questions with objectively verifiable answers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anti-contamination&lt;/td>
&lt;td>Questions generated from recent sources: arXiv papers, news, IMDb, new datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scoring&lt;/td>
&lt;td>No LLM-as-judge: automatically verifiable answer (exact ground truth)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cadence&lt;/td>
&lt;td>New questions monthly; old ones retired to avoid accumulated memorisation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>LiveBench is the architectural answer to the contamination problem: if the benchmark changes every month with questions based on sources published &lt;strong>after&lt;/strong> the cutoff of the model under evaluation, memorisation of the training set cannot inflate the result. The limitation is size (1,000 questions) and that it does not cover every domain with the same depth as MMLU.&lt;/p>
&lt;p>Execution:&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">git clone https://github.com/LiveBench/LiveBench
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> LiveBench
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install -e .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/gen_model_answer.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> --model-path meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model-id llama3-70b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/gen_ground_truth_judgment.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> --model-id llama3-70b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python livebench/show_livebench_result.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="lighteval-hugging-face">lighteval (Hugging Face)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/huggingface/lighteval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>MIT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer&lt;/td>
&lt;td>Hugging Face (Leaderboard &amp;amp; Evals Team)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Available tasks&lt;/td>
&lt;td>1,000+ tasks across multiple domains and languages&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported backends&lt;/td>
&lt;td>Hugging Face Accelerate (CPU/GPU/multi-GPU), Nanotron, vLLM, TGI, OpenAI-compatible endpoints&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What it measures&lt;/td>
&lt;td>Accuracy, BERT score, BLEU, exact match; same as the harness + HF&amp;rsquo;s own tasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Differentiator&lt;/td>
&lt;td>Designed to be the engine behind HF&amp;rsquo;s leaderboards; easy extension with custom tasks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Installation&lt;/td>
&lt;td>&lt;code>pip install lighteval&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>How it is invoked&lt;/td>
&lt;td>`lighteval accelerate &amp;ndash;model_args &amp;ldquo;pretrained=&lt;model>&amp;rdquo; &amp;ndash;tasks &amp;ldquo;lighteval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Output&lt;/td>
&lt;td>JSON + direct integration with the Hugging Face Hub to publish results&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>lighteval is the internal successor to lm-evaluation-harness in the HF ecosystem: more modern, better vLLM and distributed backend support, and designed to run without leaving the HF stack. For teams already using the Hub to manage models, it is the lowest-friction option.&lt;/p>
&lt;hr>
&lt;h3 id="openllm-leaderboard-v2-archived-2025--openevals">OpenLLM Leaderboard v2 (archived 2025) → OpenEvals&lt;/h3>
&lt;p>The &lt;strong>OpenLLM Leaderboard v2&lt;/strong> evaluated more than 13,000 models between June 2024 and its closure in 2025, using six fixed benchmarks: &lt;strong>IFEval, MuSR, GPQA, MATH, BBH and MMLU-Pro&lt;/strong>, all run with lm-evaluation-harness. Its closure was a direct consequence of the saturation and gaming problem: as the benchmarks became well known, models were optimised for them.&lt;/p>
&lt;p>&lt;strong>Successor: OpenEvals&lt;/strong> (huggingface.co/spaces/OpenEvals/find-a-leaderboard)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspect&lt;/th>
&lt;th>OpenLLM Leaderboard v2&lt;/th>
&lt;th>OpenEvals&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Architecture&lt;/td>
&lt;td>Single centralised leaderboard&lt;/td>
&lt;td>200+ specialised community leaderboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Benchmarks&lt;/td>
&lt;td>Fixed (6 tasks)&lt;/td>
&lt;td>Variable per leaderboard (math, code, medical, safety, etc.)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Models evaluated&lt;/td>
&lt;td>13,000+&lt;/td>
&lt;td>Distributed across leaderboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Eval engine&lt;/td>
&lt;td>lm-evaluation-harness&lt;/td>
&lt;td>lighteval (most) or harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Status&lt;/td>
&lt;td>Archived with an updated UI (Mar 2025)&lt;/td>
&lt;td>Active; you can create your own leaderboard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Advantage&lt;/td>
&lt;td>Historical consistency&lt;/td>
&lt;td>Specialisation, speed of adaptation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The archived collection is available for reference at &lt;code>huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025&lt;/code>.&lt;/p>
&lt;hr>
&lt;h3 id="lmarena--chatbot-arena">LMArena / Chatbot Arena&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Data&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Site&lt;/td>
&lt;td>lmarena.ai (rebrand of LMSys Chatbot Arena, January 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Methodology&lt;/td>
&lt;td>Anonymous pairwise comparisons: the user votes for the answer they prefer without knowing the model&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Statistical model&lt;/td>
&lt;td>&lt;strong>Bradley-Terry&lt;/strong> (maximum likelihood over the full history of match-ups; replaced classic Elo for greater stability)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Accumulated votes&lt;/td>
&lt;td>&amp;gt; 6 million (Jun 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Variants&lt;/td>
&lt;td>Overall, Coding, Math, Hard Prompts, Multimodal — the specialised ones are more reliable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence/access&lt;/td>
&lt;td>Free (web); vote data available under a CC licence&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Limitations&lt;/td>
&lt;td>Overall is biased by labs&amp;rsquo; selective disclosure and by format gaming; Bradley-Terry was recalibrated in mid-2025 (Style Control), shifting some models by ±20–40 Elo&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>LMArena is the only benchmark based on human preference at scale. Its advantage: it captures real perceived quality, not just MCQ accuracy. Its limitation: it is hard to reproduce on-premise (it requires a user interface and a panel of human voters). Useful as an external validation signal, not as a CI gate.&lt;/p>
&lt;hr>
&lt;h2 id="decision-table-which-tool-for-which-objective">Decision table: which tool for which 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>Reproduce a standard academic eval&lt;/td>
&lt;td>lm-evaluation-harness&lt;/td>
&lt;td>De facto engine; maximum compatibility with papers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Holistic evaluation (accuracy + fairness + toxicity)&lt;/td>
&lt;td>HELM (+ HELM Capabilities)&lt;/td>
&lt;td>The only framework measuring every axis at once&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Avoid contamination with recent questions&lt;/td>
&lt;td>LiveBench&lt;/td>
&lt;td>Monthly refresh; verifiable answers without a judge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Native integration with HF Hub and vLLM&lt;/td>
&lt;td>lighteval&lt;/td>
&lt;td>vLLM backend, 1,000+ tasks, direct publishing to the Hub&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Compare against the public state of the art (OSS)&lt;/td>
&lt;td>OpenEvals + lighteval&lt;/td>
&lt;td>The successor ecosystem to the OpenLLM Leaderboard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Human preference at scale&lt;/td>
&lt;td>LMArena&lt;/td>
&lt;td>Bradley-Terry over millions of real comparisons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Custom domain eval (medical, legal, etc.)&lt;/td>
&lt;td>lighteval with a custom task&lt;/td>
&lt;td>Simpler to extend than the harness&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fast CI gate (&amp;lt;10 min)&lt;/td>
&lt;td>lm-eval harness (subset)&lt;/td>
&lt;td>Individual tasks with &lt;code>--tasks ifeval,gpqa&lt;/code> + vLLM batching&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="the-data-contamination-trap">The data contamination trap&lt;/h2>
&lt;h3 id="what-it-is">What it is&lt;/h3>
&lt;p>A model that has seen a benchmark&amp;rsquo;s &lt;strong>test data&lt;/strong> during pretraining memorises answers instead of generalising. The benchmark measures memorisation, not capability. This is called &lt;strong>data contamination&lt;/strong> or &lt;strong>benchmark leakage&lt;/strong>.&lt;/p>
&lt;h3 id="scale-of-the-problem-20242025-data">Scale of the problem (2024–2025 data)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Finding&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Figure&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>N-gram overlap in Llama-2-70B&lt;/td>
&lt;td>&amp;ldquo;Data Contamination or Genuine Generalization?&amp;rdquo; (2025)&lt;/td>
&lt;td>18.1% overlap with eval datasets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Out-of-distribution drop in contaminated large models&lt;/td>
&lt;td>Ibid.&lt;/td>
&lt;td>−9.4 pp on reformulated prompts vs the original test&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HumanEval saturated&lt;/td>
&lt;td>Artificial Analysis / multiple 2026 reports&lt;/td>
&lt;td>Top models &amp;gt; 93%; separation ≤ 2 pp between the best 5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPQA Diamond: appearance of indirect contamination&lt;/td>
&lt;td>mindstudio.ai analysis 2026&lt;/td>
&lt;td>&amp;ldquo;Data laundering&amp;rdquo;: questions leaked via online forums&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="detection-mechanisms">Detection mechanisms&lt;/h3>
&lt;p>&lt;strong>1. N-gram overlap (the classic method)&lt;/strong>&lt;/p>
&lt;p>Compute the fraction of test set n-grams that appear in the model&amp;rsquo;s pretraining corpus. A practical threshold: if more than 13% of the test&amp;rsquo;s 8-grams match the corpus, the result is compromised.&lt;/p>
\[
\text{contamination ratio} = \frac{|\{g \in \text{test n-grams}\} \cap \{g \in \text{train n-grams}\}|}{|\text{test n-grams}|}
\]
&lt;p>Limit: it only works if you have access to the pretraining corpus. For closed models, it is not applicable.&lt;/p>
&lt;p>&lt;strong>2. Canary strings (decoy insertion)&lt;/strong>&lt;/p>
&lt;p>Unique random strings are inserted into the evaluation dataset while it is being created. If the model reproduces those strings without having seen them in context, it confirms that it memorised them from pretraining. The technique was proposed in &amp;ldquo;Extracting Training Data from Large Language Models&amp;rdquo; (Carlini et al., 2021) and adopted in recent benchmarks.&lt;/p>
&lt;p>&lt;strong>3. Perturbation and reformulation&lt;/strong>&lt;/p>
&lt;p>Semantically equivalent variants of the test are generated (paraphrasing, changing proper nouns, translation and back-translation). A model that generalises keeps its accuracy; a model that memorises drops. The average drop detected in contaminated models is 9–15 pp on equivalent reformulated prompts.&lt;/p>
&lt;p>&lt;strong>4. LiveBench (monthly refresh)&lt;/strong>&lt;/p>
&lt;p>The architectural solution: if the questions are generated from sources published &lt;strong>after&lt;/strong> the model&amp;rsquo;s cutoff (recent arXiv papers, news, new data), memorisation from pretraining cannot help. The model has to reason over information it could never have seen.&lt;/p>
&lt;p>&lt;strong>5. Kernel divergence analysis&lt;/strong> (Savelka et al., 2025)&lt;/p>
&lt;p>A more sophisticated statistical method: it compares the embedding distribution of the test set with the distribution of the training corpus. An abnormally low divergence indicates that the benchmark and the training corpus come from the same source.&lt;/p>
&lt;h3 id="why-static-leaderboards-saturate">Why static leaderboards saturate&lt;/h3>
&lt;p>The cycle is predictable:&lt;/p>
&lt;ol>
&lt;li>A new benchmark is published (MMLU in 2021, GPQA in 2023).&lt;/li>
&lt;li>The labs use it as an &lt;em>evaluation target&lt;/em> during pretraining and fine-tuning.&lt;/li>
&lt;li>Performance on that benchmark rises quickly, faster than real capability.&lt;/li>
&lt;li>The benchmark stops discriminating at the high end.&lt;/li>
&lt;li>The community needs a new benchmark.&lt;/li>
&lt;/ol>
&lt;p>The average time between the publication of a benchmark and its saturation in frontier models has gone from ~36 months (MMLU: 2021–2024) to ~18 months (HumanEval: 2021–2023, GPQA: 2023–2025). Saturation is accelerating.&lt;/p>
&lt;hr>
&lt;h2 id="honest-methodology-reproducibility-and-sources-of-variance">Honest methodology: reproducibility and sources of variance&lt;/h2>
&lt;h3 id="the-four-parameters-that-must-be-pinned">The four parameters that must be pinned&lt;/h3>
&lt;p>An eval result is only reproducible if it declares:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Parameter&lt;/th>
&lt;th>Example&lt;/th>
&lt;th>Why it matters&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Harness version&lt;/td>
&lt;td>&lt;code>lm-evaluation-harness==0.4.3&lt;/code>&lt;/td>
&lt;td>Changes to the prompt template between versions invalidate the comparison&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Exact model version&lt;/td>
&lt;td>&lt;code>meta-llama/Llama-3.1-70B-Instruct@sha256:abc…&lt;/code>&lt;/td>
&lt;td>Silent model updates on the Hub change the results&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prompt template&lt;/td>
&lt;td>&lt;code>mmlu_pro_cot_0shot&lt;/code> vs &lt;code>mmlu_pro_5shot&lt;/code>&lt;/td>
&lt;td>The difference between 0-shot and 5-shot can be 5–12 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Number of shots&lt;/td>
&lt;td>&lt;code>--num_fewshot 5&lt;/code>&lt;/td>
&lt;td>The published standard varies by benchmark; 0-shot and 5-shot are not comparable&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="few-shot-vs-zero-shot-the-numerical-difference">Few-shot vs zero-shot: the numerical difference&lt;/h3>
&lt;p>Sensitivity to the number of shots varies dramatically by benchmark:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Benchmark&lt;/th>
&lt;th>Typical 0-shot&lt;/th>
&lt;th>Typical 5-shot&lt;/th>
&lt;th>Difference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>MMLU-Pro&lt;/td>
&lt;td>62%&lt;/td>
&lt;td>72%&lt;/td>
&lt;td>+10 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPQA Diamond&lt;/td>
&lt;td>55%&lt;/td>
&lt;td>65%&lt;/td>
&lt;td>+10 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GSM8K&lt;/td>
&lt;td>70%&lt;/td>
&lt;td>82%&lt;/td>
&lt;td>+12 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IFEval&lt;/td>
&lt;td>68%&lt;/td>
&lt;td>71%&lt;/td>
&lt;td>+3 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HumanEval&lt;/td>
&lt;td>75%&lt;/td>
&lt;td>80%&lt;/td>
&lt;td>+5 pp&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Publishing an MMLU-Pro result in 0-shot without saying so, when the leaderboard standard is 5-shot, inflates the comparison by ~10 pp. Two results from the same model published with different numbers of shots &lt;strong>are not comparable&lt;/strong>.&lt;/p>
&lt;h3 id="prompt-sensitivity">Prompt sensitivity&lt;/h3>
&lt;p>Minor perturbations in the prompt template produce variations of up to ±8 pp on MCQ and ±15 pp on generation tasks. Sources of variance identified in the literature:&lt;/p>
&lt;ul>
&lt;li>Order of the options in MCQ (position bias): up to ±4 pp.&lt;/li>
&lt;li>Wording of the question (paraphrasing): ±3–8 pp.&lt;/li>
&lt;li>Presence or absence of &amp;ldquo;Let&amp;rsquo;s think step by step&amp;rdquo;: ±5–12 pp on reasoning.&lt;/li>
&lt;li>Language of the system instructions: ±3–10 pp in non-multilingual models.&lt;/li>
&lt;/ul>
&lt;h3 id="why-two-evals-give-different-results">Why two evals give different results&lt;/h3>
&lt;p>A common scenario: the paper reports MMLU-Pro 74.3% and the reproduction gives 71.8%. Frequent causes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Cause&lt;/th>
&lt;th>Typical magnitude&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Different harness version (changed template)&lt;/td>
&lt;td>1–4 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model with a silent update&lt;/td>
&lt;td>0.5–3 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Different context truncation&lt;/td>
&lt;td>0.5–2 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sampling seed (for generation)&lt;/td>
&lt;td>0.3–1.5 pp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenisation with a chat template vs without one&lt;/td>
&lt;td>2–8 pp&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The sum of these sources explains gaps of 2–6 pp that are not noise but systematic protocol differences.&lt;/p>
&lt;h3 id="the-confidence-interval-formula-for-comparing-two-models">The confidence interval formula for comparing two models&lt;/h3>
&lt;p>To know whether the difference between model A (accuracy \(p_A\)) and model B (\(p_B\)) over a test set of size \(n\) is statistically significant:&lt;/p>
\[
\Delta p \pm 1.96 \cdot \sqrt{\frac{p_A(1-p_A) + p_B(1-p_B)}{n}}
\]
&lt;p>Applied to GPQA Diamond (\(n = 198\)), if \(p_A = 0.90\) and \(p_B = 0.88\):&lt;/p>
\[
\Delta p = 0.02 \quad;\quad \text{margin} = 1.96 \cdot \sqrt{\frac{0.09 + 0.1056}{198}} \approx 1.96 \cdot 0.0314 \approx 0.062
\]
&lt;p>The 95% confidence interval is \([{-0.042},\ {+0.082}]\): it &lt;strong>crosses zero&lt;/strong>. A difference of 2 pp over 198 questions is not statistically distinguishable from noise. To distinguish 90% from 88% with 95% confidence you need:&lt;/p>
\[
n \approx \frac{(1.96)^2 \cdot (p_A(1-p_A) + p_B(1-p_B))}{(\Delta p)^2} \approx \frac{3.84 \cdot 0.1956}{0.0004} \approx 1{,}878 \text{ questions}
\]
&lt;p>GPQA Diamond is not that size. Differences of 1–2 pp on GPQA Diamond are statistically invisible.&lt;/p>
&lt;hr>
&lt;h2 id="the-quality-axis-in-the-scorecard">The quality axis in the scorecard&lt;/h2>
&lt;p>The platform&amp;rsquo;s scorecard has three columns: performance (goodput under SLO), quality (score on a reference benchmark), energy (J/token). The quality column is built like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Content&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Primary benchmark&lt;/td>
&lt;td>MMLU-Pro (5-shot) — separates in the 60–77% zone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reasoning benchmark&lt;/td>
&lt;td>GPQA Diamond (0-shot) — separates in the 55–90% zone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Instruction benchmark&lt;/td>
&lt;td>IFEval (0-shot prompt-level accuracy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code benchmark&lt;/td>
&lt;td>MBPP (0-shot, pass@1) — less saturated than HumanEval&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anti-contamination&lt;/td>
&lt;td>LiveBench global score (monthly)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tool&lt;/td>
&lt;td>lm-evaluation-harness (primary) + lighteval (secondary, HF Hub)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pinned version&lt;/td>
&lt;td>&lt;code>harness==0.4.3&lt;/code>, &lt;code>lighteval==0.6.x&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shots&lt;/td>
&lt;td>Declared explicitly per benchmark&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reproducibility&lt;/td>
&lt;td>Output JSON stored with the model SHA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Frequency&lt;/td>
&lt;td>Per model release; not in the CI of every PR (GPU-hour cost)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A full eval over 4 benchmarks (MMLU-Pro 12,032 + GPQA 198 + IFEval 541 + MBPP 500) with a 70B model on a generic 2×H100 node takes on the order of &lt;strong>4–8 hours&lt;/strong> depending on batch size and number of shots. The GPU cost is real: at ~5 €/h per amortised card, a full eval costs on the order of &lt;strong>40–80 €&lt;/strong>. It is not run on every commit; it is run on every model release or major adapter release.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLM evals: the layer after tracing&lt;/a> — the system&amp;rsquo;s internal evaluation layer (golden dataset, CI gate, calibrated judge). A necessary complement to the academic benchmarks in this article.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge: fundamentals&lt;/a> — the LLM judge that evaluates outputs in production; different from an academic benchmark, but sharing the need for calibration and reproducibility.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/">Evaluating a RAG: RAGAS and the golden dataset&lt;/a> — a specialisation of evaluation for RAG systems; faithfulness and context precision do not appear in the academic benchmarks in this article.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">Benchmark tool catalogue: data sheet by data sheet&lt;/a> — the performance axis (TTFT, ITL, goodput); the complementary half of this article in the scorecard.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">LLM benchmarking: state of the art&lt;/a> — the B1 introduction with the three axes and the track&amp;rsquo;s general methodology.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>EleutherAI · lm-evaluation-harness — &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness">https://github.com/EleutherAI/lm-evaluation-harness&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM — &lt;a href="https://github.com/stanford-crfm/helm">https://github.com/stanford-crfm/helm&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM Capabilities (mar-2025) — &lt;a href="https://crfm.stanford.edu/2025/03/20/helm-capabilities.html">https://crfm.stanford.edu/2025/03/20/helm-capabilities.html&lt;/a>&lt;/li>
&lt;li>Stanford CRFM · HELM (sitio oficial) — &lt;a href="https://crfm.stanford.edu/helm/">https://crfm.stanford.edu/helm/&lt;/a>&lt;/li>
&lt;li>LiveBench · paper arXiv:2406.19314 — &lt;a href="https://arxiv.org/abs/2406.19314">https://arxiv.org/abs/2406.19314&lt;/a>&lt;/li>
&lt;li>LiveBench · sitio oficial — &lt;a href="https://livebench.ai">https://livebench.ai&lt;/a>&lt;/li>
&lt;li>LiveBench · GitHub — &lt;a href="https://github.com/LiveBench/LiveBench">https://github.com/LiveBench/LiveBench&lt;/a>&lt;/li>
&lt;li>NYU Center for Data Science · LiveBench blog — &lt;a href="https://nyudatascience.medium.com/livebench-challenging-language-models-with-contamination-free-questions-999b52967ec8">https://nyudatascience.medium.com/livebench-challenging-language-models-with-contamination-free-questions-999b52967ec8&lt;/a>&lt;/li>
&lt;li>Hugging Face · lighteval — &lt;a href="https://github.com/huggingface/lighteval">https://github.com/huggingface/lighteval&lt;/a>&lt;/li>
&lt;li>Hugging Face · OpenLLM Leaderboard (archivado) — &lt;a href="https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard">https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard&lt;/a>&lt;/li>
&lt;li>Hugging Face · OpenEvals (sucesor) — &lt;a href="https://huggingface.co/spaces/OpenEvals/find-a-leaderboard">https://huggingface.co/spaces/OpenEvals/find-a-leaderboard&lt;/a>&lt;/li>
&lt;li>Hugging Face · Archived Open LLM Leaderboard 2024-2025 — &lt;a href="https://huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025">https://huggingface.co/collections/OpenEvals/archived-open-llm-leaderboard-2024-2025&lt;/a>&lt;/li>
&lt;li>LMArena · Chatbot Arena — &lt;a href="https://lmarena.ai">https://lmarena.ai&lt;/a>&lt;/li>
&lt;li>LMSys · blog Chatbot Arena Elo — &lt;a href="https://www.lmsys.org/blog/2023-05-03-arena/">https://www.lmsys.org/blog/2023-05-03-arena/&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;A Statistical Framework for Ranking LLM-Based Chatbots&amp;rdquo; · arXiv:2412.18407 — &lt;a href="https://arxiv.org/pdf/2412.18407">https://arxiv.org/pdf/2412.18407&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;Data Contamination or Genuine Generalization?&amp;rdquo; (2025) — &lt;a href="https://www.suaspress.org/ojs/index.php/AJNS/article/view/v2n2a03">https://www.suaspress.org/ojs/index.php/AJNS/article/view/v2n2a03&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;When Benchmarks Leak: Inference-Time Decontamination&amp;rdquo; (2025) · arXiv:2601.19334 — &lt;a href="https://arxiv.org/pdf/2601.19334">https://arxiv.org/pdf/2601.19334&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;How Contaminated Is Your Benchmark?&amp;rdquo; (2025) · arXiv:2502.00678 — &lt;a href="https://arxiv.org/pdf/2502.00678">https://arxiv.org/pdf/2502.00678&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;When AI Benchmarks Plateau&amp;rdquo; (2026) · arXiv:2602.16763 — &lt;a href="https://arxiv.org/pdf/2602.16763">https://arxiv.org/pdf/2602.16763&lt;/a>&lt;/li>
&lt;li>GPQA · arXiv:2311.12022 — &lt;a href="https://arxiv.org/abs/2311.12022">https://arxiv.org/abs/2311.12022&lt;/a>&lt;/li>
&lt;li>GPQA Diamond leaderboard · Artificial Analysis — &lt;a href="https://artificialanalysis.ai/evaluations/gpqa-diamond">https://artificialanalysis.ai/evaluations/gpqa-diamond&lt;/a>&lt;/li>
&lt;li>&amp;ldquo;The Emperor&amp;rsquo;s New Clothes in Benchmarking?&amp;rdquo; (2025) · arXiv:2503.16402 — &lt;a href="https://arxiv.org/pdf/2503.16402">https://arxiv.org/pdf/2503.16402&lt;/a>&lt;/li>
&lt;li>lm-evaluation-harness · CLI reference — &lt;a href="https://lm-evaluation-harness.readthedocs.io/running_evals/cli_reference/">https://lm-evaluation-harness.readthedocs.io/running_evals/cli_reference/&lt;/a>&lt;/li>
&lt;li>crfm-helm · PyPI — &lt;a href="https://pypi.org/project/crfm-helm/">https://pypi.org/project/crfm-helm/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>LLM serving engines compared on the Pareto frontier: vLLM, SGLang, TRT-LLM and Dynamo</title><link>https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/</link><pubDate>Tue, 16 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/</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). Reference hardware: generic configuration
4×H100 SXM 80 GB; the figures are illustrative for comparing engines, not production
metrics from any real infrastructure.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Four OSS engines dominate on-premise LLM serving in 2026: &lt;strong>vLLM&lt;/strong>, &lt;strong>SGLang&lt;/strong>,
&lt;strong>NVIDIA TensorRT-LLM&lt;/strong> and &lt;strong>NVIDIA Dynamo&lt;/strong>. None is superior on every axis. In
raw throughput with a shared prompt, SGLang beats vLLM by 29 % on Llama-3.3-70B
FP8 over H100 (16,200 vs 12,500 tok/s). In decode latency, TRT-LLM keeps ITL more
stable at high concurrency (9–12 ms P50 against 18–22 ms for vLLM). Dynamo is not an
inference engine: it is the &lt;strong>orchestration layer&lt;/strong> on top of the previous three for
multi-node clusters, and it reports up to 7× additional throughput in disaggregated serving on Blackwell.
The right choice depends on three variables: &lt;strong>load pattern&lt;/strong> (batch/interactive),
&lt;strong>deployment scale&lt;/strong> (single node/multi-node) and &lt;strong>operational constraints&lt;/strong> (OSS
maturity, ease of operation). The Pareto decision table is at the end of the article.&lt;/p>
&lt;hr>
&lt;h2 id="framing-the-article-what-this-b8-is-and-is-not">Framing the article: what this B8 is and is not&lt;/h2>
&lt;p>This article (B8 of the benchmarking pillar) takes for granted the metrics from the
&lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">introduction to benchmarking&lt;/a> and
the tool catalogue from &lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">the card-by-card piece&lt;/a>.
It applies those concepts to engine selection. The protocol from &lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">GuideLLM for SLO
validation&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">AIPerf in depth&lt;/a>
detail how to run the harness; here the result is used. The fundamentals of
&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">continuous batching&lt;/a>,
&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated serving&lt;/a>,
&lt;a href="https://blog.lo0.es/en/posts/vllm-attention-backend-flashattention-flashinfer-prefill-decode-asymmetry/">the attention backend&lt;/a> and
&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">quantisation&lt;/a> are assumed known.&lt;/p>
&lt;hr>
&lt;h2 id="card-for-each-engine">Card for each engine&lt;/h2>
&lt;h3 id="vllm">vLLM&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Reference version&lt;/td>
&lt;td>v0.9.x (June 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer / governance&lt;/td>
&lt;td>vLLM Project (Linux Foundation AI), led by UC Berkeley + Red Hat&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>NVIDIA (Ampere/Hopper/Blackwell), AMD ROCm, Intel Gaudi, TPU, CPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/vllm-project/vllm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API&lt;/td>
&lt;td>OpenAI-compatible (completions, chat, embeddings)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Origin: PagedAttention (Kwon et al., 2023). The first implementation of continuous batching
with a paged KV cache. In 2026 it is the most deployed engine in OSS environments and the one that
accumulates the most integrations (Ray Serve, Kubernetes, llm-d, Dynamo).&lt;/p>
&lt;h3 id="sglang">SGLang&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Reference version&lt;/td>
&lt;td>v0.4.x (June 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer / governance&lt;/td>
&lt;td>LMSYS Org (Berkeley, CMU, UCSD)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>NVIDIA (Ampere/Hopper/Blackwell), AMD ROCm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/sgl-project/sglang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API&lt;/td>
&lt;td>OpenAI-compatible + SGLang runtime API&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Origin: the SGLang paper, 2024 (Zheng et al.). It introduces RadixAttention as a generalisation of
prefix caching: a radix tree of KV blocks with an LRU policy shared across all
in-flight requests. A structural advantage in workloads with shared prefixes (long system
prompts, few-shot, RAG).&lt;/p>
&lt;h3 id="nvidia-tensorrt-llm-trt-llm">NVIDIA TensorRT-LLM (TRT-LLM)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Reference version&lt;/td>
&lt;td>v0.18.x (June 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer / governance&lt;/td>
&lt;td>NVIDIA (proprietary in practice, OSS in name)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>NVIDIA only: Ampere (A100), Hopper (H100/H200/GH200), Ada (L40/L40S), Blackwell (B200/GB200)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/NVIDIA/TensorRT-LLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API&lt;/td>
&lt;td>Python + C++ runtime; integrates with Triton Inference Server&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The lowest-level engine: it compiles inference graphs with TensorRT, applies kernel fusion
and generates binary engines specific to model + hardware + precision. Maximum performance
on NVIDIA, with no portability to other hardware. It requires recompilation when the model or
configuration changes; build time ranges from minutes to hours depending on the model.&lt;/p>
&lt;h3 id="nvidia-dynamo">NVIDIA Dynamo&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Reference version&lt;/td>
&lt;td>v1.1.1 (May 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintainer / governance&lt;/td>
&lt;td>NVIDIA + OSS community (ai-dynamo org on GitHub)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>NVIDIA (through the underlying engine: vLLM, SGLang or TRT-LLM)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Repository&lt;/td>
&lt;td>github.com/ai-dynamo/dynamo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Role&lt;/td>
&lt;td>Orchestration layer, NOT an inference engine&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dynamo &lt;strong>does not run inference directly&lt;/strong>: it is the scheduler and router that
coordinates pools of workers (vLLM, SGLang, TRT-LLM) across multiple nodes. Implemented in Rust
(performance) + Python (extensibility). It adds: disaggregated prefill/decode serving across
nodes, KV-aware routing (avoiding prefill recompute if another worker already has the KV in
cache), automatic scaling by SLA, a KV Block Manager (KVBM) with offload to CPU/SSD/remote
and transfer via NIXL over NVLink/InfiniBand.&lt;/p>
&lt;h3 id="honourable-mentions-lmdeploy-and-hf-tgi">Honourable mentions: LMDeploy and HF TGI&lt;/h3>
&lt;p>&lt;strong>LMDeploy&lt;/strong> (OpenMMLab/InternLM, Apache 2.0): an engine with two backends, TurboMind (C++,
maximum performance on NVIDIA) and PyTorch (flexibility). KV cache blocking and persistent
batching; 4-bit inference up to 2.4× faster than FP16 in its own benchmark. A narrower
ecosystem; no AMD support.&lt;/p>
&lt;p>&lt;strong>HF TGI&lt;/strong> (Hugging Face, Apache 2.0): the reference engine for the HuggingFace ecosystem.
TGI v3.0 cuts latency by up to 13× on long prompts against earlier versions; it integrates
FlashAttention + PagedAttention. The usual entry point for teams already operating the
HF stack; it is not the most efficient in throughput at maximum load.&lt;/p>
&lt;hr>
&lt;h2 id="capability-matrix">Capability matrix&lt;/h2>
&lt;p>The table uses this notation: &lt;strong>Yes&lt;/strong> = supported and stable, &lt;strong>Beta&lt;/strong> = available but not
recommended for production, &lt;strong>No&lt;/strong> = not available or only with a manual workaround.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capability&lt;/th>
&lt;th>vLLM&lt;/th>
&lt;th>SGLang&lt;/th>
&lt;th>TRT-LLM&lt;/th>
&lt;th>Dynamo (via backend)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Continuous batching&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (delegates to the engine)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PagedAttention&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RadixAttention / prefix caching&lt;/strong>&lt;/td>
&lt;td>Yes (chunked prefill + prefix)&lt;/td>
&lt;td>Yes (native RadixAttention)&lt;/td>
&lt;td>Yes (prefix caching)&lt;/td>
&lt;td>Yes + KV-aware routing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Speculative decoding&lt;/strong>&lt;/td>
&lt;td>Yes (n-gram, EAGLE, DFlash, suffix)&lt;/td>
&lt;td>Yes (draft+verify)&lt;/td>
&lt;td>Yes (EAGLE-3, MTP)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FP8 quantisation&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MXFP4/NVFP4 quantisation&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Yes (Blackwell)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>INT4 AWQ quantisation&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GPTQ quantisation&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Not native&lt;/td>
&lt;td>Yes (via vLLM)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>INT8 quantisation (SmoothQuant)&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Structured output&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (compressed FSM)&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Dynamic multi-LoRA&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Disaggregated prefill/decode&lt;/strong>&lt;/td>
&lt;td>Beta (experimental)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (first class)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Chunked prefill&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Multimodal support (VLM)&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Multi-node tensor parallelism&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (+ pipeline across nodes)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenAI API compatible&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (via Triton)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Non-NVIDIA hardware&lt;/strong>&lt;/td>
&lt;td>Yes (AMD/Intel/TPU)&lt;/td>
&lt;td>Yes (AMD)&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Sources for the matrix: docs.vllm.ai, docs.sglang.io, nvidia.github.io/TensorRT-LLM,
docs.nvidia.com/dynamo, github.com/ai-dynamo/dynamo (feature matrix, June 2026).&lt;/p>
&lt;hr>
&lt;h2 id="why-they-are-not-compared-lightly-the-variables-that-break-a-comparison">Why they are not compared lightly: the variables that break a comparison&lt;/h2>
&lt;p>Before the table of figures, the four decisions that invalidate any cross-engine benchmark:&lt;/p>
&lt;p>&lt;strong>1. A different harness.&lt;/strong> A benchmark run with each engine&amp;rsquo;s native micro-bench (vllm bench
serve vs SGLang bench) measures the client, not the engine. The discrepancy can be up to 7.2× at
high concurrency. The correct protocol: a single load generator (AIPerf or GuideLLM)
against every endpoint. The engine is the variable, the tool is the constant.&lt;/p>
&lt;p>&lt;strong>2. Different ISL/OSL.&lt;/strong> Input Sequence Length and Output Sequence Length determine the
prefill/decode ratio and therefore which engine is favoured. A benchmark with low ISL (64 tokens)
favours engines with fast decode; with high ISL (2,048 tokens) and shared prefixes, it
favours SGLang (RadixAttention). Fixing ISL and OSL is mandatory for the comparison
to be valid.&lt;/p>
&lt;p>&lt;strong>3. Different precision.&lt;/strong> FP16 and FP8 are not comparable: FP8 gives between 1.5× and 2× more
throughput on Hopper. Comparing vLLM FP16 with TRT-LLM FP8 says nothing about the engine.&lt;/p>
&lt;p>&lt;strong>4. Not reporting P99.&lt;/strong> Maximum throughput is reached at the point where TTFT P99 already
violates the SLO. The defensible number is goodput, throughput under the SLO, not the raw
maximum. The end-to-end latency formula:&lt;/p>
$$\text{latency}_{e2e} \approx \text{TTFT} + (N_{\text{out}} - 1) \times \text{TPOT}$$
&lt;p>where \(N_{\text{out}}\) is the number of output tokens, makes it clear that a high TTFT with low TPOT
has the same effect as the inverse case only for one specific \(N_{\text{out}}\):
both must be reported separately.&lt;/p>
&lt;p>The complete fair-comparison protocol is in
&lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">the tools article&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="performance-on-the-pareto-frontier-illustrative-figures">Performance on the Pareto frontier: illustrative figures&lt;/h2>
&lt;p>Reference configuration for every row: &lt;strong>4×H100 SXM 80 GB NVLink&lt;/strong>,
model &lt;strong>Llama-3.1-70B-Instruct FP8&lt;/strong>, harness &lt;strong>AIPerf&lt;/strong> (formerly genai-perf, the multi-process successor),
dataset &lt;strong>ShareGPT&lt;/strong> (realistic length distribution), mean ISL ~512 tok, mean OSL
~256 tok, SLO TTFT P99 &amp;lt; 500 ms. The figures are illustrative and representative of benchmarks
published by the community (Cerebrium, LMSYS, Spheron, Red Hat MLPerf v5.1); they are not measurements
from any real infrastructure. Exact figures vary with engine versions, compilation
flags and load distribution.&lt;/p>
&lt;h3 id="throughput-vs-latency-pareto-table">Throughput vs latency: Pareto table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Engine&lt;/th>
&lt;th>Config&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>TTFT P50 (ms)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Goodput (@SLO)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM v0.9&lt;/strong>&lt;/td>
&lt;td>FP8, chunked prefill&lt;/td>
&lt;td>12,500&lt;/td>
&lt;td>160&lt;/td>
&lt;td>420&lt;/td>
&lt;td>18&lt;/td>
&lt;td>~96 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang v0.4&lt;/strong>&lt;/td>
&lt;td>FP8, RadixAttention&lt;/td>
&lt;td>16,200&lt;/td>
&lt;td>140&lt;/td>
&lt;td>390&lt;/td>
&lt;td>21&lt;/td>
&lt;td>~97 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TRT-LLM v0.18&lt;/strong>&lt;/td>
&lt;td>FP8, compiled engine&lt;/td>
&lt;td>14,800&lt;/td>
&lt;td>190&lt;/td>
&lt;td>480&lt;/td>
&lt;td>10&lt;/td>
&lt;td>~93 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>vLLM + Dynamo&lt;/strong>&lt;/td>
&lt;td>FP8, disagg. P/D 2+2&lt;/td>
&lt;td>18,500&lt;/td>
&lt;td>120&lt;/td>
&lt;td>310&lt;/td>
&lt;td>19&lt;/td>
&lt;td>~98 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang + Dynamo&lt;/strong>&lt;/td>
&lt;td>FP8, disagg. P/D 2+2&lt;/td>
&lt;td>21,000&lt;/td>
&lt;td>110&lt;/td>
&lt;td>280&lt;/td>
&lt;td>22&lt;/td>
&lt;td>~99 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TRT-LLM + Dynamo&lt;/strong>&lt;/td>
&lt;td>FP8, disagg. P/D 2+2&lt;/td>
&lt;td>22,500&lt;/td>
&lt;td>130&lt;/td>
&lt;td>350&lt;/td>
&lt;td>11&lt;/td>
&lt;td>~97 %&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reading notes:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;Disagg. P/D 2+2&amp;rdquo; = 2 prefill GPUs + 2 decode GPUs (the same 4×H100 budget).&lt;/li>
&lt;li>Goodput falls against raw throughput when the latency tail exceeds the SLO of
500 ms P99. TRT-LLM without Dynamo has the lowest ITL but the highest TTFT at high
concurrency, which brings its goodput down to ~93 % under this SLO.&lt;/li>
&lt;li>Dynamo adds ~30–40 % of effective throughput over 4 GPUs thanks to disaggregated serving
and KV-aware routing, in the scenario with reusable prefixes (ShareGPT).&lt;/li>
&lt;li>In workloads without shared prefixes (short ISL, unique prompts), Dynamo&amp;rsquo;s advantage
shrinks to ~10–15 % over the base engine.&lt;/li>
&lt;/ul>
&lt;h3 id="saturation-point-concurrency-sweep-vllm-fp8-4h100">Saturation point (concurrency sweep, vLLM FP8, 4×H100)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrency&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>210&lt;/td>
&lt;td>14&lt;/td>
&lt;td>5,200&lt;/td>
&lt;td>5,200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>370&lt;/td>
&lt;td>18&lt;/td>
&lt;td>9,800&lt;/td>
&lt;td>9,800&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>420&lt;/td>
&lt;td>18&lt;/td>
&lt;td>12,500&lt;/td>
&lt;td>12,200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>48&lt;/td>
&lt;td>680&lt;/td>
&lt;td>26&lt;/td>
&lt;td>13,800&lt;/td>
&lt;td>7,400&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>64&lt;/td>
&lt;td>1,200&lt;/td>
&lt;td>41&lt;/td>
&lt;td>14,100&lt;/td>
&lt;td>2,100&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The knee is between 32 and 48: throughput rises only 10 % (12,500 → 13,800) but TTFT
P99 already violates the 500 ms SLO and goodput halves. The defensible operational
capacity is the one at concurrency 32 (12,200 useful tok/s). Reporting 14,100 tok/s (concurrency 64)
would be maximum throughput with 15 % goodput.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 260" role="img" aria-label="Pareto frontier of LLM engines: throughput on the X axis, TTFT P99 on the Y axis. Each engine occupies a different point in the space, and Dynamo shifts the points up and to the right." xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.cv{fill:none;stroke:currentColor;stroke-width:1.5}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}.tl{font:700 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.pt{r:6;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="30" x2="60" y2="210"/>
&lt;line class="ax" x1="60" y1="210" x2="740" y2="210"/>
&lt;text x="20" y="125" class="ts" transform="rotate(-90 20 125)">TTFT P99 (ms) ↑ lower is better&lt;/text>
&lt;text x="360" y="235" class="ts" text-anchor="middle">Throughput (tok/s) → higher is better&lt;/text>
&lt;line class="dsh" x1="60" y1="80" x2="740" y2="80"/>
&lt;text x="66" y="76" class="ts">SLO 500 ms&lt;/text>
&lt;circle class="pt" cx="290" cy="108"/>
&lt;text x="298" y="103" class="tl">vLLM&lt;/text>
&lt;text x="298" y="116" class="ts">12,500 tok/s · 420 ms&lt;/text>
&lt;circle class="pt" cx="380" cy="98"/>
&lt;text x="388" y="93" class="tl">SGLang&lt;/text>
&lt;text x="388" y="106" class="ts">16,200 tok/s · 390 ms&lt;/text>
&lt;circle class="pt" cx="340" cy="118"/>
&lt;text x="348" y="113" class="tl">TRT-LLM&lt;/text>
&lt;text x="348" y="126" class="ts">14,800 tok/s · 480 ms&lt;/text>
&lt;circle class="pt" cx="480" cy="72"/>
&lt;text x="488" y="67" class="tl">vLLM+Dynamo&lt;/text>
&lt;text x="488" y="80" class="ts">18,500 tok/s · 310 ms&lt;/text>
&lt;circle class="pt" cx="580" cy="62"/>
&lt;text x="588" y="57" class="tl">SGLang+Dynamo&lt;/text>
&lt;text x="588" y="70" class="ts">21,000 tok/s · 280 ms&lt;/text>
&lt;circle class="pt" cx="660" cy="85"/>
&lt;text x="554" y="165" class="tl">TRT-LLM+Dynamo&lt;/text>
&lt;text x="554" y="177" class="ts">22,500 tok/s · 350 ms&lt;/text>
&lt;line class="dsh" x1="660" y1="85" x2="610" y2="160"/>
&lt;path class="cv" d="M290,108 Q380,80 480,72 Q580,60 660,85" fill="none"/>
&lt;text x="66" y="210" class="ts">0&lt;/text>
&lt;text x="290" y="225" class="ts">12 K&lt;/text>
&lt;text x="380" y="225" class="ts">16 K&lt;/text>
&lt;text x="480" y="225" class="ts">18.5 K&lt;/text>
&lt;text x="580" y="225" class="ts">21 K&lt;/text>
&lt;text x="660" y="225" class="ts">22.5 K&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="fair-comparison-methodology-the-harness-as-the-constant">Fair-comparison methodology: the harness as the constant&lt;/h2>
&lt;p>The protocol that makes numbers comparable across different engines. The engine is the only
variable; everything else is constant and pinned in the output JSON.&lt;/p>
&lt;h3 id="variables-that-are-fixed-constants-of-the-experiment">Variables that are fixed (constants of the experiment)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Value fixed in the reference experiment&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Load tool&lt;/td>
&lt;td>AIPerf v2.x (multi-process)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>Llama-3.1-70B-Instruct&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Precision&lt;/td>
&lt;td>FP8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hardware&lt;/td>
&lt;td>4×H100 SXM 80 GB NVLink&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dataset&lt;/td>
&lt;td>ShareGPT (realistic ISL/OSL distribution)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLO&lt;/td>
&lt;td>TTFT P99 &amp;lt; 500 ms, ITL P50 &amp;lt; 30 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Warm-up&lt;/td>
&lt;td>200 requests discarded before measuring&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sweep&lt;/td>
&lt;td>concurrencies 1, 4, 8, 16, 32, 48, 64&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Primary metric&lt;/td>
&lt;td>goodput (useful tok/s under SLO)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="harness-steps">Harness steps&lt;/h3>
&lt;ol>
&lt;li>Deploy the engine with the exact config (pinned flags, fixed version).&lt;/li>
&lt;li>Warm-up: 200 requests; discard the results.&lt;/li>
&lt;li>Concurrency sweep: &lt;code>aiperf profile --concurrency-range 1:64:step&lt;/code> with AIPerf.&lt;/li>
&lt;li>Extend the sweep past the knee (until goodput falls below 50 %).&lt;/li>
&lt;li>Collect the JSON: TTFT P50/P99, ITL P50, raw throughput, goodput.&lt;/li>
&lt;li>Change &lt;strong>only the engine&lt;/strong>; repeat steps 1–5.&lt;/li>
&lt;li>Compare the goodput column across rows.&lt;/li>
&lt;/ol>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">sweep with GuideLLM&lt;/a> is equivalent
for SLO validation; AIPerf is preferable for engine comparisons because its
automatic &lt;code>estimatedCapacity&lt;/code> normalises the saturation point. Both can be used:
GuideLLM for the operational SLO, AIPerf for reference capacity.&lt;/p>
&lt;h3 id="reproducible-output-format">Reproducible output format&lt;/h3>
&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;harness&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;aiperf&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;harness_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.x&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;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Llama-3.1-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &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_NVLink&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;dataset&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sharegpt&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&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;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 class="nt">&amp;#34;itl_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&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;motor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;motor_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.9.x&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;concurrency_sweep&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">48&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">64&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;results_at_knee&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;concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">32&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_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">210&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">420&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_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;throughput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">12500&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">12200&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>This versioned JSON is the auditable datum: anyone can reproduce the figure with the same
tool, version, model, hardware and load.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-2026-relative-maturity">State of the art 2026: relative maturity&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimension&lt;/th>
&lt;th>vLLM&lt;/th>
&lt;th>SGLang&lt;/th>
&lt;th>TRT-LLM&lt;/th>
&lt;th>Dynamo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Production maturity&lt;/strong>&lt;/td>
&lt;td>High (3+ years)&lt;/td>
&lt;td>Medium-high (2 years)&lt;/td>
&lt;td>High (NVIDIA)&lt;/td>
&lt;td>Medium (1.0 GA Mar 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Release cadence&lt;/strong>&lt;/td>
&lt;td>Weekly&lt;/td>
&lt;td>Weekly&lt;/td>
&lt;td>Monthly&lt;/td>
&lt;td>Fortnightly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Integration ecosystem&lt;/strong>&lt;/td>
&lt;td>Very broad (Ray, k8s, llm-d, Dynamo, NIM)&lt;/td>
&lt;td>Broad (Dynamo, k8s)&lt;/td>
&lt;td>NVIDIA-centric (Triton, NIM)&lt;/td>
&lt;td>NVIDIA-centric&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Ease of operation&lt;/strong>&lt;/td>
&lt;td>High (pip install, OpenAI API)&lt;/td>
&lt;td>High&lt;/td>
&lt;td>Medium (requires compiling engines)&lt;/td>
&lt;td>Medium-low (multi-node, etcd/NATS)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Non-NVIDIA hardware&lt;/strong>&lt;/td>
&lt;td>Yes (AMD, Intel, TPU)&lt;/td>
&lt;td>Yes (AMD)&lt;/td>
&lt;td>No&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Disaggregated serving&lt;/strong>&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Stable&lt;/td>
&lt;td>Stable&lt;/td>
&lt;td>First class&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Dynamic multi-LoRA&lt;/strong>&lt;/td>
&lt;td>Stable&lt;/td>
&lt;td>Stable&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Stable (via vLLM/SGLang)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Structured output&lt;/strong>&lt;/td>
&lt;td>Stable&lt;/td>
&lt;td>Stable (FSM)&lt;/td>
&lt;td>Beta&lt;/td>
&lt;td>Stable (via the engine)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OSS community support&lt;/strong>&lt;/td>
&lt;td>Very high (&amp;gt;30 K GitHub stars)&lt;/td>
&lt;td>High (~20 K stars)&lt;/td>
&lt;td>High (~10 K stars)&lt;/td>
&lt;td>Growing (6.8 K stars)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="pareto-decision-table-when-to-choose-each-engine">Pareto decision table: when to choose each engine&lt;/h2>
&lt;p>No prose; the decision as a table of criteria and outcome.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Selection criterion&lt;/th>
&lt;th>Recommended engine&lt;/th>
&lt;th>Quantitative justification&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single node, quick prototype, team with no LLM serving experience&lt;/td>
&lt;td>&lt;strong>vLLM&lt;/strong>&lt;/td>
&lt;td>pip install + OpenAI API in minutes; the broadest community support&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Workload with long shared prefixes (RAG, few-shot, system prompts &amp;gt; 512 tok)&lt;/td>
&lt;td>&lt;strong>SGLang&lt;/strong>&lt;/td>
&lt;td>RadixAttention: up to 6.4× more throughput vs a baseline without prefix caching&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maximum throughput on NVIDIA, no portability, team with TRT experience&lt;/td>
&lt;td>&lt;strong>TRT-LLM&lt;/strong>&lt;/td>
&lt;td>Fused kernels + native FP8: ITL 9–12 ms vs 18–22 ms for vLLM on the same hardware&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-node (&amp;gt; 8 GPUs), mixed prefill-intensive/decode-intensive traffic&lt;/td>
&lt;td>&lt;strong>Dynamo + SGLang&lt;/strong>&lt;/td>
&lt;td>Disaggregated P/D: up to 7× additional throughput on Blackwell; 2× TTFT with KV-aware routing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AMD ROCm or Intel Gaudi hardware&lt;/td>
&lt;td>&lt;strong>vLLM&lt;/strong>&lt;/td>
&lt;td>The only SOTA engine with stable non-NVIDIA support&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Structured output + high concurrency&lt;/td>
&lt;td>&lt;strong>SGLang&lt;/strong>&lt;/td>
&lt;td>Compressed FSM: guided decode with no appreciable overhead&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stable dynamic multi-LoRA in production&lt;/td>
&lt;td>&lt;strong>vLLM or SGLang&lt;/strong>&lt;/td>
&lt;td>TRT-LLM multi-LoRA still in beta (Jun 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hugging Face ecosystem, models &amp;lt; 13B&lt;/td>
&lt;td>&lt;strong>HF TGI v3&lt;/strong>&lt;/td>
&lt;td>Direct HF Hub integration; competitive latency on small models&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maximum energy efficiency (J/token) on NVIDIA&lt;/td>
&lt;td>&lt;strong>TRT-LLM&lt;/strong>&lt;/td>
&lt;td>Lower-overhead kernels → fewer J/token at iso-throughput&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="quantisation-effect-on-throughput-and-quality">Quantisation: effect on throughput and quality&lt;/h2>
&lt;p>The four main options in order of decreasing speed and increasing quality:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Format&lt;/th>
&lt;th>Relative throughput (H100)&lt;/th>
&lt;th>Quality degradation (perplexity)&lt;/th>
&lt;th>Engines with stable support&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>NVFP4 / MXFP4&lt;/strong>&lt;/td>
&lt;td>~2.2× vs FP16&lt;/td>
&lt;td>1–3 % on MMLU&lt;/td>
&lt;td>TRT-LLM (Blackwell), vLLM (experimental)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FP8 (W8A8)&lt;/strong>&lt;/td>
&lt;td>~1.7× vs FP16&lt;/td>
&lt;td>&amp;lt; 1 % on MMLU&lt;/td>
&lt;td>vLLM, SGLang, TRT-LLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>INT4 AWQ&lt;/strong>&lt;/td>
&lt;td>~1.5× vs FP16&lt;/td>
&lt;td>1–2 % on MMLU&lt;/td>
&lt;td>vLLM, SGLang, TRT-LLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GPTQ (INT4)&lt;/strong>&lt;/td>
&lt;td>~1.4× vs FP16&lt;/td>
&lt;td>1–3 % on MMLU&lt;/td>
&lt;td>vLLM, SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FP16 (baseline)&lt;/strong>&lt;/td>
&lt;td>1.0×&lt;/td>
&lt;td>0 %&lt;/td>
&lt;td>All&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>FP8 is the de facto standard on Hopper for production: 1.7× throughput with degradation
below 1 %. The article on &lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">quantisation in depth&lt;/a>
develops the quality/speed trade-off by format and model architecture.&lt;/p>
&lt;hr>
&lt;h2 id="disaggregated-serving-when-the-overhead-is-worth-it">Disaggregated serving: when the overhead is worth it&lt;/h2>
&lt;p>Separating prefill/decode into independent pools introduces transfer latency for
KV tensors between nodes (NIXL over NVLink or InfiniBand). The overhead pays off when:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Condition&lt;/th>
&lt;th>Effect of disaggregated serving&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Mean ISL &amp;gt; 1,024 tokens&lt;/td>
&lt;td>Prefill dominates; separate pools stop it blocking decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prefill/decode ratio &amp;gt; 3:1 in time&lt;/td>
&lt;td>The decode pool sits idle waiting for prefill&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mixed traffic (prefill burst + decode queue)&lt;/td>
&lt;td>Independent scaling of each pool by SLA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;lt; 512 tokens ISL, unique prompts&lt;/td>
&lt;td>Transfer overhead exceeds the benefit; an aggregated engine is enough&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dynamo quantifies the break-even point: disaggregated serving pays off when the
transfer time for KV tensors (a function of ISL and NVLink/IB bandwidth) is
lower than the time the decode worker would spend waiting for the prefill worker in aggregated mode. On
4×H100 with an ISL of 512 tok, break-even occurs at a prefill/decode ratio &amp;gt; 2.5:1. The
article on &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated serving&lt;/a> develops
the full formula.&lt;/p>
&lt;hr>
&lt;h2 id="mlperf-inference-v51-cross-vendor-reference-figures">MLPerf Inference v5.1: cross-vendor reference figures&lt;/h2>
&lt;p>MLPerf Inference v5.1 (September 2025) is the standard for cross-vendor comparability.
Server scenario (Llama-3.1-70B, tokens/s):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Submitter&lt;/th>
&lt;th>Hardware&lt;/th>
&lt;th>Scenario&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Red Hat (vLLM)&lt;/td>
&lt;td>1×H100 80 GB&lt;/td>
&lt;td>Server&lt;/td>
&lt;td>5,103&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Red Hat (vLLM)&lt;/td>
&lt;td>1×L40S&lt;/td>
&lt;td>Server&lt;/td>
&lt;td>1,207&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVIDIA (TRT-LLM)&lt;/td>
&lt;td>8×H100 SXM&lt;/td>
&lt;td>Server&lt;/td>
&lt;td>~52,000 (estimated)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The MLPerf figures are not directly comparable with the internal benchmarks above
(a different harness, MLPerf&amp;rsquo;s Server scenario with a Poisson distribution and fixed SLOs). They are
useful for comparing vendors under the same rules; not for sizing a specific case.
For that, run your own sweep with AIPerf/GuideLLM against the target endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="variables-the-benchmark-does-not-capture">Variables the benchmark does not capture&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Omitted variable&lt;/th>
&lt;th>Effect in production&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Network latency (gateway, load balancer)&lt;/td>
&lt;td>Can add 20–100 ms to the TTFT measured on the server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Engine compilation time (TRT-LLM)&lt;/td>
&lt;td>Hours per model×precision×hardware; penalises cold starts and CI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Weight loading time on the GPU&lt;/td>
&lt;td>Relevant in autoscaling; ModelExpress (Dynamo) cuts it 7×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Operational cost of the system (Dynamo vs a simple engine)&lt;/td>
&lt;td>Dynamo requires etcd/NATS, a Rust runtime, a more specialised team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Response quality&lt;/td>
&lt;td>Performance does not imply quality; measure with lm-evaluation-harness (article B7)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy (J/token)&lt;/td>
&lt;td>Does not come out of a throughput benchmark; requires DCGM running alongside&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>vLLM · documentación oficial — &lt;a href="https://docs.vllm.ai/en/latest/">https://docs.vllm.ai/en/latest/&lt;/a>&lt;/li>
&lt;li>vLLM · blog de rendimiento (v0.6.0, 2,7× throughput) — &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>vLLM · disaggregated prefill (experimental) — &lt;a href="https://docs.vllm.ai/en/latest/features/disagg_prefill/">https://docs.vllm.ai/en/latest/features/disagg_prefill/&lt;/a>&lt;/li>
&lt;li>SGLang · documentación oficial — &lt;a href="https://docs.sglang.io/">https://docs.sglang.io/&lt;/a>&lt;/li>
&lt;li>SGLang · paper RadixAttention (LMSYS, 2024) — &lt;a href="https://arxiv.org/pdf/2312.07104">https://arxiv.org/pdf/2312.07104&lt;/a>&lt;/li>
&lt;li>SGLang · blog LMSYS sobre Llama-3 serving — &lt;a href="https://www.lmsys.org/blog/2024-07-25-sglang-llama3/">https://www.lmsys.org/blog/2024-07-25-sglang-llama3/&lt;/a>&lt;/li>
&lt;li>NVIDIA TensorRT-LLM · GitHub — &lt;a href="https://github.com/NVIDIA/TensorRT-LLM">https://github.com/NVIDIA/TensorRT-LLM&lt;/a>&lt;/li>
&lt;li>NVIDIA TensorRT-LLM · overview y release notes — &lt;a href="https://nvidia.github.io/TensorRT-LLM/overview.html">https://nvidia.github.io/TensorRT-LLM/overview.html&lt;/a>&lt;/li>
&lt;li>NVIDIA TensorRT-LLM · quantización — &lt;a href="https://nvidia.github.io/TensorRT-LLM/features/quantization.html">https://nvidia.github.io/TensorRT-LLM/features/quantization.html&lt;/a>&lt;/li>
&lt;li>NVIDIA Dynamo · GitHub (ai-dynamo/dynamo) — &lt;a href="https://github.com/ai-dynamo/dynamo">https://github.com/ai-dynamo/dynamo&lt;/a>&lt;/li>
&lt;li>NVIDIA Dynamo · documentación oficial — &lt;a href="https://docs.nvidia.com/dynamo/latest">https://docs.nvidia.com/dynamo/latest&lt;/a>&lt;/li>
&lt;li>NVIDIA Dynamo · disaggregated serving — &lt;a href="https://docs.dynamo.nvidia.com/dynamo/design-docs/disaggregated-serving">https://docs.dynamo.nvidia.com/dynamo/design-docs/disaggregated-serving&lt;/a>&lt;/li>
&lt;li>Cerebrium · benchmark vLLM vs SGLang vs TRT-LLM (Llama 3.1-70B) — &lt;a href="https://cerebrium.ai/blog/benchmarking-vllm-sglang-tensorrt-for-llama-3-1-api">https://cerebrium.ai/blog/benchmarking-vllm-sglang-tensorrt-for-llama-3-1-api&lt;/a>&lt;/li>
&lt;li>Spheron · vLLM vs TRT-LLM vs SGLang H100 benchmarks 2026 — &lt;a href="https://www.spheron.network/blog/vllm-vs-tensorrt-llm-vs-sglang-benchmarks/">https://www.spheron.network/blog/vllm-vs-tensorrt-llm-vs-sglang-benchmarks/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 results — &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 · MLPerf Inference v5.1 con vLLM — &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>arXiv 2605.24217 · sesgo sistemático en benchmarks de inferencia LLM — &lt;a href="https://arxiv.org/html/2605.24217">https://arxiv.org/html/2605.24217&lt;/a>&lt;/li>
&lt;li>IETF Draft · LLM Benchmarking Methodology (enero 2026) — &lt;a href="https://www.ietf.org/archive/id/draft-gaikwad-llm-benchmarking-methodology-00.html">https://www.ietf.org/archive/id/draft-gaikwad-llm-benchmarking-methodology-00.html&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GenAI-Perf in depth: LLM inference profiling with real data</title><link>https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/</link><pubDate>Mon, 15 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N EUR)&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>GenAI-Perf (the &lt;code>genai-perf&lt;/code> package, part of NVIDIA&amp;rsquo;s Triton ecosystem) is the reference LLM
inference profiler for OpenAI-compatible endpoints. Installable with &lt;code>pip install genai-perf&lt;/code>.
A single &lt;code>genai-perf profile&lt;/code> command produces a console table with TTFT, ITL, request latency
(avg/min/max/P75/P90/P99), output sequence length, output token throughput and request
throughput, plus &lt;code>profile_export_genai_perf.json&lt;/code> and &lt;code>.csv&lt;/code> artefacts. The &lt;code>analyze&lt;/code>
subcommand automatically sweeps concurrencies from 1 to 256 (powers of 2) and generates a
summary &lt;code>analyze_export_genai_perf.csv&lt;/code>. GenAI-Perf was declared &lt;strong>retired in April 2026&lt;/strong>
(replaced by AIPerf); it remains the reference benchmark for historical runs and documented
comparisons, and the NVIDIA tool with the most public documentation for LLMs before 2026.&lt;/p>
&lt;hr>
&lt;h2 id="what-genai-perf-is-and-where-it-fits">What GenAI-Perf is and where it fits&lt;/h2>
&lt;p>GenAI-Perf is a command-line tool for measuring the throughput and latency of generative AI
models served through an inference server. It is part of the
&lt;code>triton-inference-server/perf_analyzer&lt;/code> GitHub repository and is distributed as a pip package
(&lt;code>genai-perf&lt;/code>) and inside the Triton SDK container
(&lt;code>nvcr.io/nvidia/tritonserver:YY.MM-py3-sdk&lt;/code>).&lt;/p>
&lt;p>&lt;strong>Position in the NVIDIA ecosystem:&lt;/strong>&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 200" role="img" aria-label="NVIDIA ecosystem: Triton Inference Server and TensorRT-LLM as engines, NIM as a microservice, GenAI-Perf as an endpoint profiler, AIPerf as the successor" 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(#am)}&lt;/style>
&lt;defs>&lt;marker id="am" 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="60" width="140" height="44" rx="6"/>&lt;text x="90" y="79" text-anchor="middle" class="tl">Inference engine&lt;/text>&lt;text x="90" y="95" text-anchor="middle" class="ts">Triton + TRT-LLM / vLLM&lt;/text>
&lt;path class="ar" d="M160,82 L200,82"/>
&lt;rect class="bx" x="200" y="60" width="120" height="44" rx="6"/>&lt;text x="260" y="79" text-anchor="middle" class="tl">Endpoint&lt;/text>&lt;text x="260" y="95" text-anchor="middle" class="ts">OpenAI-compatible&lt;/text>
&lt;path class="ar" d="M320,82 L360,82"/>
&lt;rect class="bx" x="360" y="60" width="140" height="44" rx="6"/>&lt;text x="430" y="79" text-anchor="middle" class="tl">GenAI-Perf&lt;/text>&lt;text x="430" y="95" text-anchor="middle" class="ts">profiler (until Apr 2026)&lt;/text>
&lt;path class="ar" d="M500,82 L540,82"/>
&lt;rect class="bx" x="540" y="60" width="140" height="44" rx="6"/>&lt;text x="610" y="79" text-anchor="middle" class="tl">AIPerf&lt;/text>&lt;text x="610" y="95" text-anchor="middle" class="ts">successor (Apr 2026+)&lt;/text>
&lt;text x="20" y="155" class="ts">GenAI-Perf works against any endpoint compatible with the OpenAI API (vLLM, NIM, Triton, SGLang, TGI…).&lt;/text>
&lt;text x="20" y="172" class="ts">AIPerf is the official successor from April 2026 (GitHub: ai-dynamo/aiperf).&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>Relationship with perf_analyzer:&lt;/strong> GenAI-Perf internally uses Triton&amp;rsquo;s &lt;code>perf_analyzer&lt;/code>
binary to generate load and measure latencies. perf_analyzer is the low-level load generator;
GenAI-Perf is the high-level layer that adds prompt synthesis, OpenAI payload construction and
the LLM-specific metrics (TTFT, ITL, sequence lengths).&lt;/p>
&lt;p>&lt;strong>Installation:&lt;/strong>&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"># Option 1: pip (requires CUDA 12, Ubuntu 24.04, Python 3.10+)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pip install genai-perf
&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"># Option 2: Triton SDK container (recommended for reproducibility)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">RELEASE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;25.01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -it --net&lt;span class="o">=&lt;/span>host --gpus&lt;span class="o">=&lt;/span>all &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> nvcr.io/nvidia/tritonserver:25.01-py3-sdk
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Inside the container:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">genai-perf --help
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="metrics-genai-perf-produces">Metrics GenAI-Perf produces&lt;/h2>
&lt;p>The metrics cover the full cycle of a streaming generation request. All of them are reported
with avg, min, max, P75, P90 and P99 except the throughput ones (avg only).&lt;/p>
&lt;h3 id="metrics-glossary">Metrics glossary&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Symbol&lt;/th>
&lt;th>Exact definition (GenAI-Perf)&lt;/th>
&lt;th>Unit&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Time to First Token&lt;/strong>&lt;/td>
&lt;td>TTFT&lt;/td>
&lt;td>Time from sending the request to receiving the first token (includes queueing, prefill and network)&lt;/td>
&lt;td>ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Inter-Token Latency&lt;/strong>&lt;/td>
&lt;td>ITL (= TPOT)&lt;/td>
&lt;td>\(\frac{e2e\_latency - TTFT}{output\_tokens - 1}\) — decode phase only, excludes the first token&lt;/td>
&lt;td>ms/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Request Latency&lt;/strong>&lt;/td>
&lt;td>e2e_latency&lt;/td>
&lt;td>\(e2e = TTFT + Generation\_time\) — from the first request to the last response&lt;/td>
&lt;td>ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Output Token Throughput&lt;/strong>&lt;/td>
&lt;td>OTT&lt;/td>
&lt;td>\(\frac{total\_output\_tokens}{T_y - T_x}\) where \(T_x\) = first request, \(T_y\) = last token received&lt;/td>
&lt;td>tok/s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Request Throughput&lt;/strong>&lt;/td>
&lt;td>RPS&lt;/td>
&lt;td>\(\frac{total\_completed\_requests}{T_y - T_x}\)&lt;/td>
&lt;td>req/s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Input Sequence Length&lt;/strong>&lt;/td>
&lt;td>ISL&lt;/td>
&lt;td>Mean length in tokens of the input prompt&lt;/td>
&lt;td>tokens&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Output Sequence Length&lt;/strong>&lt;/td>
&lt;td>OSL&lt;/td>
&lt;td>Mean length in tokens of the generated response&lt;/td>
&lt;td>tokens&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Methodological note on ITL versus LLMPerf:&lt;/strong> GenAI-Perf excludes TTFT from the ITL
calculation (pure decode only). LLMPerf includes TTFT in its ITL mean. The numbers are not
directly comparable.&lt;/p>
&lt;p>&lt;strong>Methodological note on TPS versus LLMPerf:&lt;/strong> GenAI-Perf measures throughput between the
first request and the last token received (\(T_y - T_x\)). LLMPerf uses the total duration of
the benchmark including prompt generation and response storage; in a concurrency-1 scenario,
that difference can amount to up to 33% in the reported throughput figure.&lt;/p>
&lt;h3 id="console-output-table-real-example-trt-llm-backend">Console output table (real example, TRT-LLM backend)&lt;/h3>
&lt;p>The table GenAI-Perf prints for a profile with ISL=200, OSL=100, concurrency=1:&lt;/p>
&lt;pre tabindex="0">&lt;code> NVIDIA GenAI-Perf | LLM Metrics
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Statistic ┃ avg ┃ min ┃ max ┃ p99 ┃ p90 ┃ p75 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ Time to first token (ms) │ 13.68 │ 11.07 │ 21.50 │ 18.81 │ 14.29 │ 13.97 │
│ Inter token latency (ms) │ 1.86 │ 1.28 │ 2.11 │ 2.11 │ 2.01 │ 1.95 │
│ Request latency (ms) │ 203.70 │ 180.33 │ 228.30 │ 225.45 │ 216.48 │ 211.72 │
│ Output sequence length │ 103.46 │ 95.00 │ 134.00 │ 122.96 │ 108.00 │ 104.75 │
│ Input sequence length │ 200.00 │ 200.00 │ 200.00 │ 200.00 │ 200.00 │ 200.00 │
│ Output token throughput (per sec) │ 504.02 │ N/A │ N/A │ N/A │ N/A │ N/A │
│ Request throughput (per sec) │ 4.87 │ N/A │ N/A │ N/A │ N/A │ N/A │
└───────────────────────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘
&lt;/code>&lt;/pre>&lt;p>&lt;em>(Source: docs.nvidia.com/deeplearning/triton-inference-server — LLM tutorial)&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="invocation-against-openai-compatible-endpoints">Invocation against OpenAI-compatible endpoints&lt;/h2>
&lt;h3 id="profile-mode-one-load-point">&lt;code>profile&lt;/code> mode: one load point&lt;/h3>
&lt;p>The &lt;code>profile&lt;/code> subcommand measures a fixed operating point (concurrency or request-rate,
synthetic ISL/OSL).&lt;/p>
&lt;p>&lt;strong>Example 1: vLLM endpoint (chat, streaming)&lt;/strong>&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. Bring up the vLLM server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -it --net&lt;span class="o">=&lt;/span>host --rm --gpus&lt;span class="o">=&lt;/span>all &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> vllm/vllm-openai:latest &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dtype float16
&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. Profile with GenAI-Perf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">genai-perf profile &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type chat &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --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> --synthetic-input-tokens-mean &lt;span class="m">1024&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> --synthetic-input-tokens-stddev &lt;span class="m">128&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> --output-tokens-mean &lt;span class="m">256&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> --output-tokens-stddev &lt;span class="m">0&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> --output-tokens-mean-deterministic &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --request-count &lt;span class="m">200&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> --warmup-request-count &lt;span class="m">20&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> --tokenizer meta-llama/Llama-3.1-70B-Instruct
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Example 2: completions endpoint (no streaming)&lt;/strong>&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">genai-perf profile &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --concurrency &lt;span class="m">32&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> --synthetic-input-tokens-mean &lt;span class="m">512&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> --synthetic-input-tokens-stddev &lt;span class="m">64&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> --output-tokens-mean &lt;span class="m">128&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> --request-count &lt;span class="m">200&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> --warmup-request-count &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Example 3: NVIDIA NIM (TRT-LLM backend endpoint, via Triton)&lt;/strong>&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">genai-perf profile &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta/llama-3.1-70b-instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --backend tensorrtllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --concurrency &lt;span class="m">8&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> --synthetic-input-tokens-mean &lt;span class="m">2048&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> --synthetic-input-tokens-stddev &lt;span class="m">0&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> --output-tokens-mean &lt;span class="m">512&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> --output-tokens-mean-deterministic &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --request-count &lt;span class="m">100&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> --warmup-request-count &lt;span class="m">10&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> --generate-plots
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Example 4: request-rate instead of concurrency (Poisson)&lt;/strong>&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">genai-perf profile &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type chat &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --request-rate 5.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --synthetic-input-tokens-mean &lt;span class="m">1024&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> --output-tokens-mean &lt;span class="m">256&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> --request-count &lt;span class="m">300&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> --warmup-request-count &lt;span class="m">30&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="main-flags-and-what-they-do">Main flags and what they do&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag&lt;/th>
&lt;th>Typical values&lt;/th>
&lt;th>Function&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>--endpoint-type&lt;/code>&lt;/td>
&lt;td>&lt;code>chat&lt;/code>, &lt;code>completions&lt;/code>, &lt;code>embeddings&lt;/code>&lt;/td>
&lt;td>Type of OpenAI-compatible endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--streaming&lt;/code>&lt;/td>
&lt;td>(boolean)&lt;/td>
&lt;td>Enables SSE streaming; required to measure real TTFT and ITL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--concurrency&lt;/code>&lt;/td>
&lt;td>1, 8, 16, 32, 64&lt;/td>
&lt;td>Number of concurrent requests maintained; GenAI-Perf guarantees N active at all times&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--request-rate&lt;/code>&lt;/td>
&lt;td>1.0, 5.0, 10.0&lt;/td>
&lt;td>Constant arrival rate (req/s); does not guarantee N active&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--synthetic-input-tokens-mean&lt;/code>&lt;/td>
&lt;td>128–8192&lt;/td>
&lt;td>Mean of synthetic ISL tokens&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--synthetic-input-tokens-stddev&lt;/code>&lt;/td>
&lt;td>0–512&lt;/td>
&lt;td>Standard deviation of the ISL (0 = fixed)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--output-tokens-mean&lt;/code>&lt;/td>
&lt;td>64–2048&lt;/td>
&lt;td>Mean of target OSL tokens&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--output-tokens-mean-deterministic&lt;/code>&lt;/td>
&lt;td>(boolean)&lt;/td>
&lt;td>Sets the minimum output tokens = target mean (more precise with TRT-LLM)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--request-count&lt;/code>&lt;/td>
&lt;td>100–2000&lt;/td>
&lt;td>Number of requests to benchmark&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--warmup-request-count&lt;/code>&lt;/td>
&lt;td>10–50&lt;/td>
&lt;td>Warm-up requests discarded from the metrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--generate-plots&lt;/code>&lt;/td>
&lt;td>(boolean)&lt;/td>
&lt;td>Generates PNG plots of TTFT vs ISL, ITL vs token position, etc.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--tokenizer&lt;/code>&lt;/td>
&lt;td>HF model id&lt;/td>
&lt;td>Tokenizer for counting tokens; mandatory when ISL/OSL matter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--backend&lt;/code>&lt;/td>
&lt;td>&lt;code>tensorrtllm&lt;/code>, &lt;code>vllm&lt;/code>&lt;/td>
&lt;td>Triton backend (for serving directly via Triton without an OpenAI endpoint)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--extra-inputs&lt;/code>&lt;/td>
&lt;td>&lt;code>ignore_eos:true&lt;/code>&lt;/td>
&lt;td>Extra request parameters; &lt;code>ignore_eos:true&lt;/code> guarantees a consistent OSL&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Note on &lt;code>--concurrency&lt;/code> versus &lt;code>--request-rate&lt;/code>:&lt;/strong> NVIDIA recommends using &lt;code>--concurrency&lt;/code>.
With &lt;code>--request-rate&lt;/code>, if the rate exceeds the engine&amp;rsquo;s throughput, the queue grows without
bound and the metrics stop being stable. With &lt;code>--concurrency&lt;/code>, there are always exactly N
active requests, which gives a clean latency-throughput curve.&lt;/p>
&lt;h3 id="output-artefacts">Output artefacts&lt;/h3>
&lt;p>GenAI-Perf dumps all results into an &lt;code>artifacts/&lt;/code> directory:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Artefact&lt;/th>
&lt;th>Format&lt;/th>
&lt;th>Content&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>profile_export_genai_perf.json&lt;/code>&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>Full metrics (avg, min, max, P75, P90, P99) + CLI arguments used&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>profile_export_genai_perf.csv&lt;/code>&lt;/td>
&lt;td>CSV&lt;/td>
&lt;td>Exported console tables, ready to import into Excel/pandas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>profile_export.json&lt;/code>&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>Raw perf_analyzer data (per-request traces)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>inputs.json&lt;/code>&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>Synthetic payloads sent (for reproducibility)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PNG plots&lt;/td>
&lt;td>PNG&lt;/td>
&lt;td>TTFT analysis, Request latency, TTFT vs ISL, ITL vs position, ISL vs OSL (with &lt;code>--generate-plots&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="the-analyze-subcommand-automatic-concurrency-sweep">The &lt;code>analyze&lt;/code> subcommand: automatic concurrency sweep&lt;/h2>
&lt;p>The &lt;code>analyze&lt;/code> subcommand sweeps multiple values of a parameter in a single command and
generates a summary CSV.&lt;/p>
&lt;h3 id="concurrency-sweep-the-most-used">Concurrency sweep (the most used)&lt;/h3>
&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"># Sweeps concurrencies 1, 2, 4, 8, 16, 32, 64, 128, 256&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">genai-perf analyze &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type chat &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-type concurrency &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-range 1:256 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --synthetic-input-tokens-mean &lt;span class="m">1024&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> --output-tokens-mean &lt;span class="m">256&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> --request-count &lt;span class="m">200&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> --warmup-request-count &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="request-rate-sweep">Request-rate sweep&lt;/h3>
&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"># Sweeps 2, 4, 6, 8, 10, 12 req/s&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">genai-perf analyze &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type chat &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-type request_rate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-list 2,4,6,8,10,12 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --synthetic-input-tokens-mean &lt;span class="m">1024&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> --output-tokens-mean &lt;span class="m">256&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> --request-count &lt;span class="m">200&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> --warmup-request-count &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="isl-sweep-to-study-the-effect-of-prefill">ISL sweep (to study the effect of prefill)&lt;/h3>
&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">genai-perf analyze &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -m meta-llama/Llama-3.1-70B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint-type chat &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --streaming &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-type input_sequence_length &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --sweep-list 256,512,1024,2048,4096 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --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> --output-tokens-mean &lt;span class="m">256&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> --request-count &lt;span class="m">200&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> --warmup-request-count &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="artefact-structure-of-analyze">Artefact structure of &lt;code>analyze&lt;/code>&lt;/h3>
&lt;p>For a sweep with &lt;code>--sweep-type concurrency --sweep-range 1:32&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>artifacts/
llama70b-openai-chat-concurrency1/
inputs.json
profile_export.json
profile_export_genai_perf.json
profile_export_genai_perf.csv
llama70b-openai-chat-concurrency2/
...
llama70b-openai-chat-concurrency4/
...
[...]
analyze_export_genai_perf.csv ← summary of every scenario
checkpoint.json ← allows interrupted sweeps to be resumed
&lt;/code>&lt;/pre>&lt;h3 id="summary-csv-format">Summary CSV format&lt;/h3>
&lt;pre tabindex="0">&lt;code class="language-csv" data-lang="csv">Config Name,Concurrency,ISL,p99 TTFT (ms),p99 ITL (ms),p99 Request Latency (ms),Avg. OTT (tok/s),RPS
llama70b_run_0,1,1024,45.2,6.8,1823.4,128.3,0.55
llama70b_run_1,4,1024,48.1,7.1,1901.2,510.7,2.18
llama70b_run_2,8,1024,61.3,7.9,2142.5,989.2,4.12
llama70b_run_3,16,1024,98.7,9.4,2891.3,1741.6,6.88
llama70b_run_4,32,1024,284.1,14.2,4320.8,2134.2,7.14
llama70b_run_5,64,1024,892.4,28.7,8741.2,2251.8,7.23
&lt;/code>&lt;/pre>&lt;p>The summary CSV also includes a second table with GPU metrics (P99 power, energy, utilisation,
memory) when they are captured via Triton&amp;rsquo;s telemetry URLs
(&lt;code>--server-metrics-urls http://localhost:8002/metrics&lt;/code>).&lt;/p>
&lt;hr>
&lt;h2 id="comparison-table-genai-perf-versus-guidellm-versus-llmperf-versus-vllm-bench">Comparison table: GenAI-Perf versus GuideLLM versus LLMPerf versus vLLM bench&lt;/h2>
&lt;p>The choice of tool changes the number. They are not comparable with each other without
adjustment.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimension&lt;/th>
&lt;th>&lt;strong>GenAI-Perf&lt;/strong>&lt;/th>
&lt;th>&lt;strong>GuideLLM&lt;/strong>&lt;/th>
&lt;th>&lt;strong>LLMPerf&lt;/strong>&lt;/th>
&lt;th>&lt;strong>vLLM bench serve&lt;/strong>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Origin / maintainer&lt;/strong>&lt;/td>
&lt;td>NVIDIA / Triton team (retired Apr 2026)&lt;/td>
&lt;td>Red Hat / vLLM project&lt;/td>
&lt;td>Anyscale / Ray&lt;/td>
&lt;td>vLLM project&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Class&lt;/strong>&lt;/td>
&lt;td>multi-process load generator&lt;/td>
&lt;td>multi-process load generator&lt;/td>
&lt;td>multi-process load generator&lt;/td>
&lt;td>single-process micro-bench&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Supported endpoints&lt;/strong>&lt;/td>
&lt;td>OpenAI-compatible + KServe + native Triton&lt;/td>
&lt;td>OpenAI-compatible&lt;/td>
&lt;td>OpenAI-compatible&lt;/td>
&lt;td>native vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Main load mode&lt;/strong>&lt;/td>
&lt;td>fixed concurrency N (recommended) or constant request-rate&lt;/td>
&lt;td>synchronous, concurrent, poisson, throughput, &lt;strong>automatic sweep&lt;/strong>&lt;/td>
&lt;td>batches of N concurrent (with a draining period at the end)&lt;/td>
&lt;td>request-rate, concurrency&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Draining period&lt;/strong>&lt;/td>
&lt;td>NO — guarantees N active at all times&lt;/td>
&lt;td>NO (poisson)&lt;/td>
&lt;td>YES — at the end of each batch the system empties, concurrency drops to 0&lt;/td>
&lt;td>N/A&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Automatic sweep&lt;/strong>&lt;/td>
&lt;td>&lt;code>analyze&lt;/code> (concurrency, request-rate, ISL, OSL)&lt;/td>
&lt;td>&lt;code>--rate-type sweep&lt;/code> (idle to saturation, 10 rounds)&lt;/td>
&lt;td>manual (several runs)&lt;/td>
&lt;td>&lt;code>vllm bench sweep serve&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLM metrics&lt;/strong>&lt;/td>
&lt;td>TTFT, ITL, e2e latency, OTT, RPS, ISL/OSL&lt;/td>
&lt;td>TTFT, ITL/TPOT, throughput, goodput under SLO&lt;/td>
&lt;td>TTFT, ITL (includes TTFT in the mean), TPS (total benchmark duration)&lt;/td>
&lt;td>TTFT, TPOT, throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ITL difference&lt;/strong>&lt;/td>
&lt;td>excludes TTFT: \((e2e - TTFT) / (N_{tok}-1)\)&lt;/td>
&lt;td>excludes TTFT (same as GenAI-Perf)&lt;/td>
&lt;td>includes TTFT in the mean&lt;/td>
&lt;td>excludes TTFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPS difference&lt;/strong>&lt;/td>
&lt;td>\(total\_tokens / (T_y - T_x)\)&lt;/td>
&lt;td>throughput + goodput under SLO&lt;/td>
&lt;td>\(total\_tokens / (T_{end} - T_{start})\) — up to 33% lower&lt;/td>
&lt;td>\(total\_tokens / (T_y - T_x)\)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Goodput / SLO&lt;/strong>&lt;/td>
&lt;td>NO&lt;/td>
&lt;td>YES (the key differentiator)&lt;/td>
&lt;td>NO&lt;/td>
&lt;td>NO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Exports&lt;/strong>&lt;/td>
&lt;td>JSON + CSV + PNG + checkpoint&lt;/td>
&lt;td>JSON + YAML + CSV + interactive HTML&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>console / JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Built-in warm-up&lt;/strong>&lt;/td>
&lt;td>YES (&lt;code>--warmup-request-count&lt;/code>)&lt;/td>
&lt;td>YES (warm-up requests)&lt;/td>
&lt;td>NOT native&lt;/td>
&lt;td>YES&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Real datasets&lt;/strong>&lt;/td>
&lt;td>OpenOrca, CNN DailyMail, JSONL file, moon_cake&lt;/td>
&lt;td>file with a traffic distribution&lt;/td>
&lt;td>JSONL file&lt;/td>
&lt;td>sharegpt, random&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GPU telemetry&lt;/strong>&lt;/td>
&lt;td>YES (via Triton&amp;rsquo;s server-metrics-urls → summary CSV)&lt;/td>
&lt;td>NOT native (integrate DCGM separately)&lt;/td>
&lt;td>NO&lt;/td>
&lt;td>NO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>When to choose it&lt;/strong>&lt;/td>
&lt;td>fine profiling of one operating point, comparing configs with the same harness, integration with NIM/Triton&lt;/td>
&lt;td>SLO-driven sweep, finding the knee, sizing replicas&lt;/td>
&lt;td>quick endpoint validation in the Ray ecosystem&lt;/td>
&lt;td>iterating on vLLM flags&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Fair-comparison rule:&lt;/strong> never cross numbers from different tools for the same conclusion. If
vLLM and TRT-LLM are being compared, use GenAI-Perf against both, with the same ISL/OSL, the
same concurrency and the same &lt;code>--warmup-request-count&lt;/code>. The tool is the constant; the engine is
the variable.&lt;/p>
&lt;hr>
&lt;h2 id="profiling-on-4h100-sxm-80-gb-a-full-sweep-example">Profiling on 4×H100 SXM 80 GB: a full sweep example&lt;/h2>
&lt;p>Generic reference hardware: 4×H100 SXM 80 GB NVLink, model Llama-3.1-70B-Instruct FP16,
ISL=1024 tokens, OSL=256 tokens, vLLM endpoint.&lt;/p>
&lt;h3 id="illustrative-result-of-a-concurrency-sweep-164">Illustrative result of a concurrency sweep (1→64)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrency&lt;/th>
&lt;th>TTFT P50 (ms)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>ITL P99 (ms)&lt;/th>
&lt;th>OTT (tok/s)&lt;/th>
&lt;th>RPS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>38&lt;/td>
&lt;td>45&lt;/td>
&lt;td>6.4&lt;/td>
&lt;td>7.1&lt;/td>
&lt;td>130&lt;/td>
&lt;td>0.51&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>40&lt;/td>
&lt;td>51&lt;/td>
&lt;td>6.8&lt;/td>
&lt;td>8.2&lt;/td>
&lt;td>510&lt;/td>
&lt;td>2.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>46&lt;/td>
&lt;td>62&lt;/td>
&lt;td>7.3&lt;/td>
&lt;td>9.4&lt;/td>
&lt;td>980&lt;/td>
&lt;td>3.83&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>68&lt;/td>
&lt;td>98&lt;/td>
&lt;td>8.9&lt;/td>
&lt;td>12.1&lt;/td>
&lt;td>1,720&lt;/td>
&lt;td>6.72&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>145&lt;/td>
&lt;td>284&lt;/td>
&lt;td>13.7&lt;/td>
&lt;td>21.4&lt;/td>
&lt;td>2,130&lt;/td>
&lt;td>8.32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>48&lt;/td>
&lt;td>380&lt;/td>
&lt;td>740&lt;/td>
&lt;td>22.3&lt;/td>
&lt;td>38.6&lt;/td>
&lt;td>2,280&lt;/td>
&lt;td>8.90&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>64&lt;/td>
&lt;td>890&lt;/td>
&lt;td>1,820&lt;/td>
&lt;td>41.2&lt;/td>
&lt;td>78.3&lt;/td>
&lt;td>2,340&lt;/td>
&lt;td>9.14&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>How to read it: throughput saturates around concurrency 48–64, while TTFT P99 already exceeds
500 ms from concurrency 32 onwards. The &lt;strong>knee&lt;/strong> (the last point with TTFT P99 &amp;lt; 500 ms) is at
concurrency 16 with 1,720 useful tok/s. Anyone reporting &amp;ldquo;2,340 tok/s&amp;rdquo; (concurrency 64) is
describing the maximum throughput, at a point where the system meets no interactive chat SLO
at all.&lt;/p>
&lt;h3 id="formulas-for-the-key-metrics">Formulas for the key metrics&lt;/h3>
$$
\text{ITL} = \frac{e2e\_latency - TTFT}{N_{output\_tokens} - 1}
$$
$$
\text{OTT} = \frac{\sum output\_tokens}{T_y - T_x}
$$
$$
\text{RPS} = \frac{N_{requests}}{T_y - T_x}
$$
&lt;p>where \(T_x\) is the timestamp of the first request sent and \(T_y\) the timestamp of the last
token received from the last request.&lt;/p>
&lt;hr>
&lt;h2 id="synthetic-islosl-profiling-what-to-configure-for-each-use-case">Synthetic ISL/OSL profiling: what to configure for each use case&lt;/h2>
&lt;p>The ISL/OSL parameters determine which engine of the system is stressed: a long ISL stresses
prefill (KV-cache, TTFT); a long OSL stresses decode (ITL, memory bandwidth).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Use case&lt;/th>
&lt;th>Typical ISL&lt;/th>
&lt;th>Typical OSL&lt;/th>
&lt;th>&lt;code>--synthetic-input-tokens-mean&lt;/code>&lt;/th>
&lt;th>&lt;code>--output-tokens-mean&lt;/code>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Short interactive chat&lt;/td>
&lt;td>~300 tok&lt;/td>
&lt;td>~100 tok&lt;/td>
&lt;td>300&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code copilot&lt;/td>
&lt;td>~800 tok&lt;/td>
&lt;td>~200 tok&lt;/td>
&lt;td>800&lt;/td>
&lt;td>200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Document summarisation&lt;/td>
&lt;td>~2000 tok&lt;/td>
&lt;td>~256 tok&lt;/td>
&lt;td>2000&lt;/td>
&lt;td>256&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAG with long context&lt;/td>
&lt;td>~4096 tok&lt;/td>
&lt;td>~512 tok&lt;/td>
&lt;td>4096&lt;/td>
&lt;td>512&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch without latency&lt;/td>
&lt;td>~512 tok&lt;/td>
&lt;td>~1024 tok&lt;/td>
&lt;td>512&lt;/td>
&lt;td>1024&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>--extra-inputs ignore_eos:true&lt;/code> option disables the engine&amp;rsquo;s EOS token, forcing exactly
the requested tokens to be generated. Essential for measuring a consistent OSL on TRT-LLM;
optional on vLLM.&lt;/p>
&lt;hr>
&lt;h2 id="honest-methodology-how-not-to-measure-badly">Honest methodology: how not to measure badly&lt;/h2>
&lt;h3 id="1-warm-up-is-mandatory">1. Warm-up is mandatory&lt;/h3>
&lt;p>Without warm-up, the first requests include the KV-cache loading latency, the engine&amp;rsquo;s
autotuning and the initialisations. GenAI-Perf applies a sliding window to detect stability
(max/min ratio within a margin over the last 3 measurements), but the explicit
&lt;code>--warmup-request-count&lt;/code> discards the first N requests from the metrics. Recommended minimum
value: 10 requests (or 5% of the total if the total is large).&lt;/p>
&lt;h3 id="2-the-concurrency-knee-extend-the-sweep-until-you-see-it">2. The concurrency knee: extend the sweep until you see it&lt;/h3>
&lt;p>Throughput saturates once the engine&amp;rsquo;s effective max batch size is reached. If the sweep stops
before saturation is reached, the &amp;ldquo;maximum capacity&amp;rdquo; reported is the last concurrency tested,
not the real knee. The rule: the sweep must reach a point where TTFT P99 has doubled relative
to the low-load value (or where OTT stops growing by more than 5% between steps). Only then is
the knee visible.&lt;/p>
&lt;p>For a sweep with &lt;code>--sweep-range 1:256&lt;/code>, GenAI-Perf sweeps 1, 2, 4, 8, 16, 32, 64, 128, 256
(powers of 2). On most 70B models over 4×H100, saturation occurs between concurrency 32 and
64; the range 1:128 covers the knee with room to spare.&lt;/p>
&lt;h3 id="3-concurrency-versus-request-rate-choosing-correctly">3. Concurrency versus request-rate: choosing correctly&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th>Behaviour&lt;/th>
&lt;th>When to use&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>--concurrency N&lt;/code>&lt;/td>
&lt;td>Always N active requests; when one finishes, the next is sent&lt;/td>
&lt;td>Clean latency-throughput curve; comparison between configs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--request-rate R&lt;/code>&lt;/td>
&lt;td>1 request is sent every 1/R seconds; the queue can grow without bound&lt;/td>
&lt;td>Simulating traffic with arrivals at a constant rate&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>NVIDIA recommends &lt;code>--concurrency&lt;/code> for benchmarking. With &lt;code>--request-rate&lt;/code>, if R exceeds the
engine&amp;rsquo;s maximum throughput, the queue grows indefinitely and the metrics become unstable.&lt;/p>
&lt;h3 id="4-single-process-versus-multi-process-bias">4. Single-process versus multi-process bias&lt;/h3>
&lt;p>GenAI-Perf is multi-process: it maintains N concurrent requests using perf_analyzer&amp;rsquo;s internal
manager. There is no risk of client saturation. This sets it apart from &lt;code>vllm bench serve&lt;/code>
(single-process), which can saturate on the client at high concurrency and give inflated
throughput metrics.&lt;/p>
&lt;h3 id="5-ignore_eos-and-an-inconsistent-osl">5. &lt;code>ignore_eos&lt;/code> and an inconsistent OSL&lt;/h3>
&lt;p>Without &lt;code>--extra-inputs ignore_eos:true&lt;/code>, the engine may generate fewer tokens than requested
(if the model produces an EOS earlier). This makes the real OSL vary between requests and the
ITL become inconsistent between runs. For reproducible benchmarks with a controlled OSL: always
enable &lt;code>ignore_eos:true&lt;/code>.&lt;/p>
&lt;h3 id="6-the-correct-tokenizer">6. The correct tokenizer&lt;/h3>
&lt;p>The &lt;code>--tokenizer&lt;/code> flag must point to the model being served. If it is omitted, GenAI-Perf uses
a default tokenizer that can give different ISL/OSL in the real model&amp;rsquo;s tokens, making the
throughput numbers in tok/s not comparable between models with different vocabularies.&lt;/p>
&lt;h3 id="7-one-run--a-data-point-three-or-more--a-number">7. One run = a data point; three or more = a number&lt;/h3>
&lt;p>Variance between runs under identical conditions can be 5–10% on tail latency metrics (P99). A
single profile is not a defensible number; the mean of three runs with the same config is.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 170" role="img" aria-label="Flow of a reproducible benchmark with GenAI-Perf: deploy engine, warm up, concurrency sweep beyond the knee, collect JSON, compare" 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(#bm)}&lt;/style>
&lt;defs>&lt;marker id="bm" 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="50" width="120" height="44" rx="6"/>&lt;text x="80" y="69" text-anchor="middle" class="tl">1 · Engine&lt;/text>&lt;text x="80" y="85" text-anchor="middle" class="ts">pinned config&lt;/text>
&lt;path class="ar" d="M140,72 L163,72"/>
&lt;rect class="bx" x="163" y="50" width="120" height="44" rx="6"/>&lt;text x="223" y="69" text-anchor="middle" class="tl">2 · Warm-up&lt;/text>&lt;text x="223" y="85" text-anchor="middle" class="ts">≥10 requests&lt;/text>
&lt;path class="ar" d="M283,72 L306,72"/>
&lt;rect class="bx" x="306" y="50" width="130" height="44" rx="6"/>&lt;text x="371" y="69" text-anchor="middle" class="tl">3 · Sweep&lt;/text>&lt;text x="371" y="85" text-anchor="middle" class="ts">1:256, past the knee&lt;/text>
&lt;path class="ar" d="M436,72 L459,72"/>
&lt;rect class="bx" x="459" y="50" width="130" height="44" rx="6"/>&lt;text x="524" y="69" text-anchor="middle" class="tl">4 · Artefacts&lt;/text>&lt;text x="524" y="85" text-anchor="middle" class="ts">versioned JSON + CSV&lt;/text>
&lt;path class="ar" d="M589,72 L612,72"/>
&lt;rect class="bx" x="612" y="50" width="130" height="44" rx="6"/>&lt;text x="677" y="69" text-anchor="middle" class="tl">5 · Compare&lt;/text>&lt;text x="677" y="85" text-anchor="middle" class="ts">P99 at the knee&lt;/text>
&lt;text x="20" y="128" class="ts">Pin: genai-perf version, engine version, model, precision, ISL/OSL, hardware, tokenizer.&lt;/text>
&lt;text x="20" y="145" class="ts">Without that metadata in the JSON, the number is neither reproducible nor comparable.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="connection-with-the-cost-and-energy-track">Connection with the cost and energy track&lt;/h2>
&lt;p>GenAI-Perf can capture GPU metrics in the same summary CSV if
&lt;code>--server-metrics-urls http://localhost:8002/metrics&lt;/code> is passed (Triton&amp;rsquo;s metrics endpoint,
which exposes power and utilisation via DCGM). The &lt;code>analyze&lt;/code> summary CSV then includes a second
table with P99 power (W), energy (MJ), utilisation (%) and memory (GB) per GPU and per
scenario.&lt;/p>
&lt;p>With that data, a single sweep yields the three axes of the scorecard:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Metric&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Performance&lt;/td>
&lt;td>GenAI-Perf&lt;/td>
&lt;td>OTT (tok/s), TTFT P99, ITL P99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>DCGM via server-metrics-urls&lt;/td>
&lt;td>Power (W) → \(J/token = W / OTT\)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost&lt;/td>
&lt;td>node price&lt;/td>
&lt;td>EUR/hour → \(EUR/token = EUR_{hour} / (OTT \times 3600)\)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The formula for cost per million tokens:&lt;/p>
$$
CPM = \frac{EUR_{hour}}{OTT \times 3{.}6 \times 10^3}
$$
&lt;p>where \(OTT\) is the output token throughput in tok/s and \(CPM\) is the cost per million
tokens in EUR.&lt;/p>
&lt;p>It connects with &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability with DCGM&lt;/a> for
the power capture, and with &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">anatomy of an LLM request&lt;/a>
for the breakdown of phases (prefill versus decode) that TTFT and ITL quantify.&lt;/p>
&lt;hr>
&lt;h2 id="status-2026-genai-perf--aiperf">Status 2026: GenAI-Perf → AIPerf&lt;/h2>
&lt;p>NVIDIA announced in April 2026 that GenAI-Perf moves to passive maintenance mode (no new
features) and that the active successor is &lt;strong>AIPerf&lt;/strong> (&lt;code>github.com/ai-dynamo/aiperf&lt;/code>). AIPerf
adds automatic detection of the saturation point (&lt;code>estimatedCapacity&lt;/code>) and is integrated into
the NVIDIA Dynamo pipeline.&lt;/p>
&lt;p>For new benchmarks in production: migrate to AIPerf. To reproduce historical runs documented
with GenAI-Perf, or to use NVIDIA&amp;rsquo;s NIM benchmarking artefacts (which use GenAI-Perf): stay on
GenAI-Perf 25.x.&lt;/p>
&lt;p>The article &lt;a href="https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/">LLM benchmark tools, one by one&lt;/a>
covers the full comparison including AIPerf. The article
&lt;a href="https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/">GuideLLM: SLO validation under load&lt;/a> details
the SLO-driven sweep that GenAI-Perf does not do natively. For the general methodology context:
&lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">LLM benchmarking frameworks, state of the art&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Measurement bias and reproducibility&lt;/a> — the configuration traps that invalidate a GenAI-Perf sweep even when the commands are correct: insufficient warm-up, synthetic versus real dataset, shared environment.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">LLM serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — where the numbers measured with GenAI-Perf land: the goodput-latency Pareto frontier across the four main engines.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>NVIDIA · GenAI-Perf — README oficial — &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 · Tutorial LLM con GenAI-Perf (comandos y tablas de salida reales) — &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/docs/tutorial.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/docs/tutorial.html&lt;/a>&lt;/li>
&lt;li>NVIDIA · GenAI-Perf Analyze subcommand (sweep, CSV resumen, checkpoint) — &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/docs/analyze.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/docs/analyze.html&lt;/a>&lt;/li>
&lt;li>NVIDIA · NIM LLM Benchmarking — Métricas (definiciones TTFT, ITL, TPS, RPS, fórmulas exactas) — &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>NVIDIA · NIM LLM Benchmarking — Parámetros y buenas prácticas (ISL/OSL, concurrencia vs request-rate, ignore_eos) — &lt;a href="https://docs.nvidia.com/nim/benchmarking/llm/latest/parameters.html">https://docs.nvidia.com/nim/benchmarking/llm/latest/parameters.html&lt;/a>&lt;/li>
&lt;li>NVIDIA Technical Blog · Measuring Generative AI Model Performance Using NVIDIA GenAI-Perf and an OpenAI-Compatible API — &lt;a href="https://developer.nvidia.com/blog/measuring-generative-ai-model-performance-using-nvidia-genai-perf-and-an-openai-compatible-api/">https://developer.nvidia.com/blog/measuring-generative-ai-model-performance-using-nvidia-genai-perf-and-an-openai-compatible-api/&lt;/a>&lt;/li>
&lt;li>GitHub · triton-inference-server/perf_analyzer (genai-perf) — &lt;a href="https://github.com/triton-inference-server/perf_analyzer/blob/main/genai-perf/README.md">https://github.com/triton-inference-server/perf_analyzer/blob/main/genai-perf/README.md&lt;/a>&lt;/li>
&lt;li>GitHub · ray-project/llmperf (métricas y diferencias metodológicas) — &lt;a href="https://github.com/ray-project/llmperf">https://github.com/ray-project/llmperf&lt;/a>&lt;/li>
&lt;li>PyPI · nvidia-genai-perf-eval — &lt;a href="https://pypi.org/project/nvidia-genai-perf-eval/">https://pypi.org/project/nvidia-genai-perf-eval/&lt;/a>&lt;/li>
&lt;li>Macnica · Benchmarking LLM Applications Part 1: What is GenAI-Perf? — &lt;a href="https://www.macnica.co.jp/en/business/semiconductor/articles/nvidia/145977/">https://www.macnica.co.jp/en/business/semiconductor/articles/nvidia/145977/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GuideLLM in depth: validating the SLO under load and sizing from the knee</title><link>https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/</link><pubDate>Sun, 14 Jun 2026 04:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/guidellm-validating-slo-under-load-sizing-knee/</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 &lt;strong>benchmarking&lt;/strong> track (B3). B2 covered the tool catalogue; here we go into
&lt;strong>GuideLLM&lt;/strong> in depth, because it answers the operational question that sizes a platform: &lt;strong>how far
can I load this engine without breaking the SLO?&lt;/strong> It is not a catalogue benchmark (&amp;ldquo;X tokens/s&amp;rdquo;), it
is an &lt;strong>SLO-driven sweep&lt;/strong> that finds the &lt;strong>knee&lt;/strong>, the safe capacity, and from which the real replica
count and cost per token come out. We will look at its load modes, how the SLO is defined, how to read
the output and how to turn the knee into sizing and into euros. With real commands; no recommendations,
only the mechanics.&lt;/p>
&lt;hr>
&lt;h2 id="why-guidellm">Why GuideLLM&lt;/h2>
&lt;p>GuideLLM (a vLLM project) generates &lt;strong>realistic, configurable traffic patterns&lt;/strong> and captures &lt;strong>full
distributions&lt;/strong> of TTFT, ITL and end-to-end behaviour, for &lt;strong>SLO-driven&lt;/strong> evaluation, with reproducible
sweeps that identify the safe operating range
(&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat&lt;/a>).
Unlike a micro-benchmark, it &lt;strong>measures the engine, not the client&lt;/strong> (multi-process load generation), and
unlike MLPerf, it &lt;strong>sizes your specific case&lt;/strong> (your model, your SLO, your load). It is the middle tool:
not for tuning a flag, not for comparing vendors, but for &lt;strong>deciding how many GPUs you need&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="the-load-modes-rate-types">The load modes (rate-types)&lt;/h2>
&lt;p>GuideLLM offers several modes through &lt;code>--rate-type&lt;/code>, and picking the right one is half the work:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>--rate-type&lt;/code>&lt;/th>
&lt;th>What it does&lt;/th>
&lt;th>When&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>synchronous&lt;/strong>&lt;/td>
&lt;td>one request at a time&lt;/td>
&lt;td>baseline latency, no concurrency&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>concurrent&lt;/strong>&lt;/td>
&lt;td>keeps N fixed simultaneous requests&lt;/td>
&lt;td>measuring at a specific concurrency&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>poisson&lt;/strong>&lt;/td>
&lt;td>requests per second following Poisson&lt;/td>
&lt;td>simulating real traffic (random arrivals)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>throughput&lt;/strong>&lt;/td>
&lt;td>saturates the engine to the maximum&lt;/td>
&lt;td>raw peak capacity&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>sweep&lt;/strong>&lt;/td>
&lt;td>automatic ramp from idle to maximum&lt;/td>
&lt;td>&lt;strong>finding the knee&lt;/strong> (the most useful one)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In &lt;code>concurrent&lt;/code> mode, &lt;code>--rate &amp;quot;1,2,4&amp;quot;&lt;/code> launches three benchmarks at those concurrencies
(&lt;a href="https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794">Medium · GuideLLM on OpenShift&lt;/a>).
The &lt;strong>poisson&lt;/strong> mode is the most realistic for online inference (requests do not arrive at regular
intervals), and the &lt;strong>sweep&lt;/strong> is the one that automates the search for the saturation point.&lt;/p>
&lt;hr>
&lt;h2 id="the-automatic-sweep-from-idle-to-the-knee">The automatic sweep: from idle to the knee&lt;/h2>
&lt;p>The flagship mode. When running a &lt;strong>sweep&lt;/strong> profile, GuideLLM &lt;strong>automatically increases the request rate
from idle to maximum throughput across 10 rounds&lt;/strong>, and produces an &lt;strong>interactive HTML report&lt;/strong> with
detailed latency and throughput (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat · GuideLLM on Kubernetes&lt;/a>).
During the sweep, &lt;strong>on detecting saturation it identifies the previous iteration, the knee, and returns
it as estimated capacity&lt;/strong>; if it does not detect saturation, the sweep has to be &lt;strong>extended beyond the
knee&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="The GuideLLM sweep: 10 rounds from idle to maximum, throughput saturates and latency blows up, marking the knee as the safe capacity under SLO" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.cv{fill:none;stroke:currentColor;stroke-width:1.6}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="40" x2="60" y2="200"/>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;text x="20" y="120" class="ts" transform="rotate(-90 20 120)">metric&lt;/text>
&lt;text x="330" y="228" class="ts">sweep rounds (idle → maximum) →&lt;/text>
&lt;path class="cv" d="M60,190 C200,150 300,118 410,110 C520,104 620,100 700,98"/>
&lt;text x="600" y="92" class="ts">throughput (saturates)&lt;/text>
&lt;path class="cv" d="M60,182 C260,178 360,168 430,148 C520,116 600,66 700,44"/>
&lt;text x="600" y="40" class="ts">P99 latency (blows up)&lt;/text>
&lt;line class="dsh" x1="430" y1="40" x2="430" y2="200"/>
&lt;text x="392" y="56" class="tl">knee&lt;/text>
&lt;text x="345" y="216" class="ts">estimated capacity (safe under SLO)&lt;/text>
&lt;text x="60" y="245" class="ts">The sweep runs 10 rounds; the knee is the last round before P99 breaks the SLO. It must be extended PAST the knee to see it.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="a-worked-example-reading-a-10-round-sweep">A worked example: reading a 10-round sweep&lt;/h2>
&lt;p>An illustrative output from a sweep on a 70B on 8×H100 (SLO: TTFT P99 &amp;lt; 500 ms), per round:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Round&lt;/th>
&lt;th>Rate (req/s)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2&lt;/td>
&lt;td>95&lt;/td>
&lt;td>19&lt;/td>
&lt;td>480&lt;/td>
&lt;td>480&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>8&lt;/td>
&lt;td>140&lt;/td>
&lt;td>20&lt;/td>
&lt;td>1,900&lt;/td>
&lt;td>1,900&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>14&lt;/td>
&lt;td>240&lt;/td>
&lt;td>22&lt;/td>
&lt;td>3,100&lt;/td>
&lt;td>3,060&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>17&lt;/td>
&lt;td>460&lt;/td>
&lt;td>24&lt;/td>
&lt;td>3,400&lt;/td>
&lt;td>3,330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>20&lt;/td>
&lt;td>760&lt;/td>
&lt;td>31&lt;/td>
&lt;td>3,700&lt;/td>
&lt;td>2,520&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>26&lt;/td>
&lt;td>1,500&lt;/td>
&lt;td>48&lt;/td>
&lt;td>3,950&lt;/td>
&lt;td>900&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>30&lt;/td>
&lt;td>2,400&lt;/td>
&lt;td>71&lt;/td>
&lt;td>4,000&lt;/td>
&lt;td>380&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>How to read it: up to &lt;strong>round 6&lt;/strong> (17 req/s) the TTFT P99 meets the SLO (460 ms) and goodput ≈ throughput
(3,330 ≈ 3,400). In &lt;strong>round 7&lt;/strong> P99 already breaks (760 ms) and goodput &lt;strong>drops&lt;/strong> to 2,520. The &lt;strong>knee&lt;/strong>
sits between 6 and 7: the safe capacity is that of round 6, &lt;strong>~3,330 tok/s of goodput&lt;/strong>. Rounds 9 and 10
give more raw throughput (3,950, 4,000) but with goodput of 900 and 380; the system is &amp;ldquo;performing&amp;rdquo; by
mostly serving requests that miss the target. Whoever reports &amp;ldquo;4,000 tok/s&amp;rdquo; is describing round 10, where
the system is broken. The defensible number is &lt;strong>3,330 tok/s under TTFT P99 &amp;lt; 500 ms&lt;/strong>, and that is the
one that goes into the sizing. Note as well that the sweep had to reach round 10 (P99 of 2,400 ms) to
&lt;strong>see&lt;/strong> where it broke: that is why it has to be extended beyond the knee.&lt;/p>
&lt;hr>
&lt;h2 id="defining-the-slo-the-number-that-decides-the-knee">Defining the SLO: the number that decides the knee&lt;/h2>
&lt;p>The &amp;ldquo;knee&amp;rdquo; is not absolute: it depends on the &lt;strong>SLO&lt;/strong> you define. A typical online inference SLO:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Example threshold&lt;/th>
&lt;th>What it protects&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>TTFT P99&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 500 ms&lt;/td>
&lt;td>the wait for the first token (interactivity)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPOT / ITL P95&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 50 ms/token&lt;/td>
&lt;td>the perceived &amp;ldquo;typing speed&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Error rate&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 0.1 %&lt;/td>
&lt;td>reliability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Goodput&lt;/strong>, the throughput that meets that SLO, is the metric that defines the knee: the safe capacity
is the maximum load where goodput ≈ throughput. Changing the SLO moves the knee: a stricter TTFT SLO
(200 ms) gives an earlier knee (less safe capacity) than a loose one (1 s). That is why &lt;strong>the SLO is
fixed before the sweep&lt;/strong>, and reported alongside the result: a capacity &amp;ldquo;of 3,330 tok/s&amp;rdquo; without saying
under which SLO means nothing.&lt;/p>
&lt;hr>
&lt;h2 id="execution-the-command">Execution: the command&lt;/h2>
&lt;p>An SLO-driven sweep against a vLLM endpoint:&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">guidellm benchmark &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --target &lt;span class="s2">&amp;#34;http://vllm:8000&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> --rate-type sweep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-seconds &lt;span class="m">120&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 &lt;span class="s2">&amp;#34;prompt_tokens=1024,output_tokens=256&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> --output-path resultados.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key parameters: &lt;code>--rate-type sweep&lt;/code> (the automatic ramp), &lt;code>--data&lt;/code> (the length distribution of the load,
which must resemble your real traffic), &lt;code>--max-seconds&lt;/code> (duration per round), &lt;code>--target&lt;/code> (the endpoint).
For the SLO, GuideLLM allows the latency constraints that define goodput to be set. The output goes to
&lt;code>--output-path&lt;/code> in &lt;strong>JSON, YAML or CSV&lt;/strong>, in addition to the interactive HTML report.&lt;/p>
&lt;hr>
&lt;h2 id="defining-the-load---data-decides-the-result">Defining the load: &lt;code>--data&lt;/code> decides the result&lt;/h2>
&lt;p>The &lt;code>--data&lt;/code> parameter (the distribution of prompt and output lengths) changes the knee as much as the
SLO does. Three forms, from least to most faithful:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Load&lt;/th>
&lt;th>&lt;code>--data&lt;/code>&lt;/th>
&lt;th>Fidelity&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Fixed length&lt;/td>
&lt;td>&lt;code>prompt_tokens=1024,output_tokens=256&lt;/code>&lt;/td>
&lt;td>low: real traffic is not fixed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Synthetic distribution&lt;/td>
&lt;td>length ranges&lt;/td>
&lt;td>medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Real traces&lt;/td>
&lt;td>a dataset of your traffic&lt;/td>
&lt;td>high: the knee that applies&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The trap: a sweep with short fixed-length prompts gives an optimistic knee that &lt;strong>does not resemble
production&lt;/strong>, where long prompts dominate the prefill cost and bring the knee forward. To size properly,
feed GuideLLM the &lt;strong>real distribution&lt;/strong> of your traffic (prompt and output lengths measured in
production). The knee of a sweep is only as realistic as the load you put into it; with an unrealistic
&lt;code>--data&lt;/code>, you are sizing for traffic that does not exist.&lt;/p>
&lt;p>The arrival profile matters too: &lt;code>--rate-type poisson&lt;/code> simulates random arrivals (like real online
traffic), while &lt;code>concurrent&lt;/code> keeps N fixed (like a batch). An online service measured with &lt;code>concurrent&lt;/code>
can give a knee different from the real one; for interactive inference, &lt;strong>poisson&lt;/strong> is the honest profile.&lt;/p>
&lt;hr>
&lt;h2 id="the-output-what-to-read">The output: what to read&lt;/h2>
&lt;p>GuideLLM produces &lt;strong>standardised, exportable&lt;/strong> reports for dashboards, analysis and regression tracking,
in JSON, YAML and CSV (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).
What matters to read, per sweep round:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT&lt;/strong> (P50, P95, P99) — the latency to the first token.&lt;/li>
&lt;li>&lt;strong>ITL/TPOT&lt;/strong> (P50, P95, P99) — between tokens.&lt;/li>
&lt;li>&lt;strong>Throughput&lt;/strong> (req/s and tok/s) — the raw figure.&lt;/li>
&lt;li>&lt;strong>Goodput&lt;/strong> — the part that meets the SLO (the honest number).&lt;/li>
&lt;/ul>
&lt;p>The interactive HTML report lets you see the &lt;strong>full distributions&lt;/strong> (not just means), which is where the
latency tail that a mean hides becomes visible. For sizing and comparison, the JSON is what gets versioned
and fed into the reproducible harness.&lt;/p>
&lt;hr>
&lt;h2 id="from-the-knee-to-sizing-and-cost">From the knee to sizing and cost&lt;/h2>
&lt;p>This is where GuideLLM connects with the rest of the series. The &lt;strong>replica count&lt;/strong> and the &lt;strong>cost per
token&lt;/strong> come out of the knee:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 200" role="img" aria-label="From the GuideLLM knee to sizing: goodput per replica, target load, replica count and cost 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(#gm)}&lt;/style>
&lt;defs>&lt;marker id="gm" 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="60" width="160" height="48" rx="6"/>&lt;text x="32" y="80" class="tl">Knee (GuideLLM)&lt;/text>&lt;text x="32" y="97" class="ts">goodput per replica&lt;/text>
&lt;path class="ar" d="M180,84 L225,84"/>
&lt;rect class="bx" x="225" y="60" width="160" height="48" rx="6"/>&lt;text x="237" y="80" class="tl">Target load&lt;/text>&lt;text x="237" y="97" class="ts">peak tok/s (SLO)&lt;/text>
&lt;path class="ar" d="M385,84 L430,84"/>
&lt;rect class="bx" x="430" y="60" width="160" height="48" rx="6"/>&lt;text x="442" y="80" class="tl">Replica count&lt;/text>&lt;text x="442" y="97" class="ts">load ÷ goodput&lt;/text>
&lt;path class="ar" d="M590,84 L635,84"/>
&lt;rect class="bx" x="635" y="60" width="125" height="48" rx="6"/>&lt;text x="647" y="80" class="tl">Cost/token&lt;/text>&lt;text x="647" y="97" class="ts">€/h ÷ goodput&lt;/text>
&lt;text x="20" y="150" class="ts">The goodput at the knee (not raw throughput) is the denominator of sizing and of cost per token.&lt;/text>
&lt;text x="20" y="172" class="ts">Sizing with raw throughput overstates goodput and breaks the SLO in production.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The calculation: if the knee gives a &lt;strong>goodput of 3,330 tok/s per replica&lt;/strong> and your peak target load is
&lt;strong>20,000 tok/s&lt;/strong>, you need &lt;strong>6 replicas&lt;/strong> (20,000 ÷ 3,330 ≈ 6.0). And the cost per token comes from the
replica cost (from OpenCost, ~11 €/h) divided by its goodput: ~0.92 €/1M tokens. It is the bridge to
&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a> and to cost per token:
&lt;strong>the correct denominator is the goodput at the knee&lt;/strong>, not catalogue throughput. Sizing with raw
throughput leaves you short of useful capacity and breaks the SLO in production.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-as-a-kubernetes-job">Deployment as a Kubernetes Job&lt;/h2>
&lt;p>GuideLLM runs as a &lt;strong>Kubernetes Job inside the cluster&lt;/strong> to benchmark models served by the orchestration
platform (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).
This matters for fidelity: the load generator runs &lt;strong>inside the cluster&lt;/strong>, on the same network as the
endpoint, so it measures the engine without the network latency of an external client. The Job is
parameterised with the target, the rate-type and the load, and dumps the JSON to a volume or a store for
versioning. Running it as a Job also makes automation easier (a CronJob for periodic benchmarking, or
triggered from CI).&lt;/p>
&lt;hr>
&lt;h2 id="slo-validation-in-ci-catching-regressions">SLO validation in CI: catching regressions&lt;/h2>
&lt;p>The most valuable use in the medium term: a &lt;strong>Job that runs a short sweep on every change&lt;/strong> (a vLLM
release, a config change) and &lt;strong>compares the goodput against the baseline&lt;/strong>. If goodput drops by more than
a threshold or the knee moves earlier, the pipeline fails. Since GuideLLM exports standardised JSON for
&lt;strong>regression tracking&lt;/strong>, comparing two runs is trivial. That way a performance regression, which is a cost
and capacity regression, is caught at the commit, not in production. The full sweep is not needed on every
commit: a short sweep covering the knee is enough to catch the regression; the exhaustive one is for
releases.&lt;/p>
&lt;hr>
&lt;h2 id="comparing-configurations-with-sweeps">Comparing configurations with sweeps&lt;/h2>
&lt;p>GuideLLM shines for &lt;strong>deciding between configurations&lt;/strong> by running the same sweep against each one. The
fair protocol: same load (&lt;code>--data&lt;/code>), same SLO, same hardware, varying only the config. Examples of
decisions that a sweep settles with data:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Decision&lt;/th>
&lt;th>What to compare&lt;/th>
&lt;th>What the knee reveals&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>FP16 vs FP8&lt;/td>
&lt;td>two deployments of the same model&lt;/td>
&lt;td>FP8 usually gives a higher knee (more goodput)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>max-num-seqs&lt;/code>&lt;/td>
&lt;td>different values of the flag&lt;/td>
&lt;td>the one that maximises goodput under the SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM vs SGLang&lt;/td>
&lt;td>two engines, same model&lt;/td>
&lt;td>which engine gives more goodput at your SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KV size/precision&lt;/td>
&lt;td>KV cache configs&lt;/td>
&lt;td>the effect on capacity&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each sweep gives a knee; the highest knee under the same SLO wins. It is the controlled experiment that
fills the scorecard row (article B8): not &amp;ldquo;X is faster&amp;rdquo; in the abstract, but &amp;ldquo;X gives Y tok/s more goodput
under this specific SLO, which translates into Z fewer replicas and W € less per million tokens&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h2 id="watching-the-three-axes-during-the-sweep">Watching the three axes during the sweep&lt;/h2>
&lt;p>A trick that connects with the cost and energy track: &lt;strong>while the sweep runs, capture the GPU metrics with
DCGM&lt;/strong>. That way each round yields the goodput (GuideLLM), the average power (DCGM → J/token) and the cost
(node price → €/token) at once. In a single sweep campaign you measure the three axes of the scorecard for
every operating point, and they come out coherent by construction (same instant, same load). Instead of an
isolated performance benchmark, you get the &lt;strong>complete row&lt;/strong>, cost, performance, energy, for the knee,
which is exactly what the proposal needs. Align the time windows (the DCGM power and the GuideLLM metrics
must cover the same interval) or the J/token does not correspond to the measured goodput.&lt;/p>
&lt;hr>
&lt;h2 id="interpreting-the-distributions-not-just-the-knee">Interpreting the distributions, not just the knee&lt;/h2>
&lt;p>The knee is the headline, but the GuideLLM HTML report gives &lt;strong>full distributions&lt;/strong>, and there is
information there that the knee summarises away. Three additional readings:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The shape of the tail.&lt;/strong> Two configs with the same P99 can have different tails: one with P99 at
500 ms and P99.9 at 600 ms is stable; another with P99 at 500 ms and P99.9 at 3,000 ms has a long tail
that will hit some users severely. The mean, and even P99, hide it; the distribution shows it.&lt;/li>
&lt;li>&lt;strong>The TTFT/ITL split.&lt;/strong> Seeing the two distributions separately tells you whether the bottleneck is
prefill (high TTFT) or decode (high ITL), which steers the optimisation (more concurrency vs chunked
prefill, and so on).&lt;/li>
&lt;li>&lt;strong>The spread across rounds.&lt;/strong> If goodput varies a lot between rounds at the same rate, the system is
unstable under load, something a single capacity number does not reveal.&lt;/li>
&lt;/ul>
&lt;p>The knee sizes; the distributions diagnose. For a proposal, the knee is the datum; for operating and
optimising, the distributions are where you see what to fix.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-in-the-reproducible-harness">GuideLLM in the reproducible harness&lt;/h2>
&lt;p>GuideLLM fits as the load engine of the reproducible harness (article S4). The pattern:&lt;/p>
&lt;ol>
&lt;li>A &lt;strong>script/Job&lt;/strong> that deploys the engine with the pinned config, warms up, runs the sweep and dumps the
JSON with all the metadata (model, version, hardware, &lt;code>--data&lt;/code>, SLO).&lt;/li>
&lt;li>&lt;strong>Named by date and config&lt;/strong> to version the runs.&lt;/li>
&lt;li>A &lt;strong>store&lt;/strong> (a git repo of JSONs or a bucket) to reproduce and compare.&lt;/li>
&lt;li>&lt;strong>DCGM&lt;/strong> capturing in parallel to add the energy to each point.&lt;/li>
&lt;/ol>
&lt;p>That way &amp;ldquo;reproduce the knee of this config&amp;rdquo; is one command, not an afternoon, and comparing two releases
is a JSON diff. It is what turns benchmarking from a one-off activity into a continuous capability, and
what lets the capacity figure in the proposal come with the bench to reproduce it.&lt;/p>
&lt;hr>
&lt;h2 id="the-cost-of-a-sweep-in-euros">The cost of a sweep (in euros)&lt;/h2>
&lt;p>A 10-round sweep at ~2 minutes per round occupies the node for ~20–30 minutes. At an amortised cost of
~11 €/h, that is &lt;strong>~4–5.5 € per full sweep&lt;/strong> of a model. Not much per run, but a continuous benchmarking
programme (every release, every config, several models) adds up; hence the value of automating the sweep as
a reproducible Job and running &lt;strong>short&lt;/strong> sweeps in CI (only around the knee), reserving the exhaustive one
for releases. The cost of measuring is part of the cost of the platform, and small next to the cost of
sizing badly.&lt;/p>
&lt;hr>
&lt;h2 id="the-slo-by-use-case-the-knee-moves-with-it">The SLO by use case: the knee moves with it&lt;/h2>
&lt;p>Since the SLO defines the knee, different use cases give different capacities &lt;strong>on the same hardware&lt;/strong>.
Worth keeping in mind when sizing:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Use case&lt;/th>
&lt;th>Typical SLO&lt;/th>
&lt;th>Effect on the knee&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Interactive chat&lt;/td>
&lt;td>TTFT P99 &amp;lt; 500 ms, ITL &amp;lt; 50 ms&lt;/td>
&lt;td>early knee (latency rules)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code copilot&lt;/td>
&lt;td>TTFT P99 &amp;lt; 300 ms&lt;/td>
&lt;td>even earlier knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch / summarisation&lt;/td>
&lt;td>no TTFT SLO, maximise throughput&lt;/td>
&lt;td>late knee (almost pure throughput)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent (multi-step)&lt;/td>
&lt;td>bounded per-step latency&lt;/td>
&lt;td>depends on the number of steps&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The same GPU serves &lt;strong>more&lt;/strong> batch load than interactive chat load, because the batch SLO is loose. That is
why sizing requires &lt;strong>one sweep per load profile&lt;/strong>: there is no &amp;ldquo;the node&amp;rsquo;s capacity&amp;rdquo;, there is &amp;ldquo;the node&amp;rsquo;s
capacity for this SLO&amp;rdquo;. Mixing loads with different SLOs in the same pool without separating them is a
classic source of broken SLOs, with batch saturating and interactive chat paying for it. The fix connects
with scheduling: separate pools or priorities by SLO.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-vs-the-others-when-to-use-which">GuideLLM vs the others: when to use which&lt;/h2>
&lt;p>To place it against the tools from B2:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Question&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;How far can I load without breaking the SLO?&amp;rdquo;&lt;/td>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>SLO-driven sweep, the knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What is the endpoint&amp;rsquo;s maximum capacity?&amp;rdquo;&lt;/td>
&lt;td>AIPerf&lt;/td>
&lt;td>automatic saturation detection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Does my vLLM flag change help?&amp;rdquo;&lt;/td>
&lt;td>vllm bench serve&lt;/td>
&lt;td>fast, native to the engine&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Which hardware/engine is better in the abstract?&amp;rdquo;&lt;/td>
&lt;td>read MLPerf&lt;/td>
&lt;td>cross-vendor comparability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>GuideLLM is the tool for &lt;strong>sizing under an SLO&lt;/strong>: when the question is operational (how many replicas, what
cost per token at my SLO), it is the most direct answer. AIPerf and GuideLLM overlap (both multi-process);
the practical difference is that GuideLLM is more oriented to the SLO and the reproducible report, and
AIPerf to maximum capacity with automatic detection. Many teams use both; what must not be done is to
compare a result from one with a result from the other as if they were the same measurement.&lt;/p>
&lt;hr>
&lt;h2 id="checklist-for-a-defensible-sweep">Checklist for a defensible sweep&lt;/h2>
&lt;p>For the knee of a sweep to support a sizing decision:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Step&lt;/th>
&lt;th>Check&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Realistic load&lt;/td>
&lt;td>&lt;code>--data&lt;/code> with your traffic&amp;rsquo;s distribution (or poisson)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Declared SLO&lt;/td>
&lt;td>TTFT/TPOT with a fixed percentile and threshold&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Extended sweep&lt;/td>
&lt;td>reaches the point where P99 breaks (past the knee)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Goodput, not throughput&lt;/td>
&lt;td>capacity is the goodput under the SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Job in the cluster&lt;/td>
&lt;td>no network latency from an external client&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Versioned output&lt;/td>
&lt;td>JSON with all the metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DCGM in parallel&lt;/td>
&lt;td>energy per point, optional but recommended&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you can tick the seven boxes, the knee is an auditable datum; if any is missing, it is an anecdote. The
sizing in the proposal hangs on these seven being green.&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>Not extending the sweep beyond the knee.&lt;/strong> If saturation is not detected, the estimated capacity is
the last round tested, not the real knee. Extend it until P99 breaks.&lt;/li>
&lt;li>&lt;strong>Unrealistic load.&lt;/strong> A fixed-length &lt;code>--data&lt;/code> does not resemble your traffic; use the real distribution
(or poisson) for a knee that applies.&lt;/li>
&lt;li>&lt;strong>Reporting throughput, not goodput.&lt;/strong> The knee is defined by goodput under the SLO; raw throughput
overestimates capacity.&lt;/li>
&lt;li>&lt;strong>Undeclared SLO.&lt;/strong> A capacity without the SLO it was measured under is neither comparable nor
defensible.&lt;/li>
&lt;li>&lt;strong>Client outside the cluster.&lt;/strong> Running GuideLLM from outside puts network latency into the measurement;
run it as a Job inside the cluster.&lt;/li>
&lt;/ol>
&lt;p>With GuideLLM mastered, you have the tool that turns performance into an actionable datum: the knee, the
sizing and the cost per token, all from a reproducible sweep. The next article (B4) goes into NVIDIA&amp;rsquo;s
AIPerf; this one closes SLO validation, which is what sizes the proposal.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>GuideLLM answers the only performance question that sizes a platform: &lt;strong>how far can I load without breaking
the SLO?&lt;/strong> And it answers it with the right discipline: a sweep from idle to saturation that finds the knee,
defined by goodput under a declared SLO, over a load that resembles the real one. Everything the proposal
needs comes out of that knee: the replica count, the cost per token (with goodput as the denominator, not
catalogue throughput) and, if you capture DCGM during the sweep, the energy per token as well. The mistake
that invalidates the exercise is the usual one: reporting peak throughput instead of goodput, measuring with
an unrealistic load, or not extending the sweep until you see where it breaks. Done properly, as a Job
inside the cluster, with the real distribution, the SLO fixed and the output in versioned JSON, GuideLLM
turns performance from a marketing figure into a reproducible datum that survives an audit and sizes a
sovereign architecture with numbers. The knee is not the fastest point; it is the point where your platform
still delivers what it promised.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">GenAI-Perf in depth&lt;/a> — NVIDIA&amp;rsquo;s profiler and how it compares with GuideLLM data sheet by data sheet (rate-types, concurrency sweep, metrics).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">LLM serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — after validating the SLOs with GuideLLM, this comparison decides which engine meets the goodput-latency Pareto frontier.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Measurement bias and reproducibility&lt;/a> — the setup errors that make a concurrency sweep non-reproducible, even when the tool is correctly configured.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Red Hat · 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>Red Hat · 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>GuideLLM · PyPI — &lt;a href="https://pypi.org/project/guidellm/">https://pypi.org/project/guidellm/&lt;/a>&lt;/li>
&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>Medium · GuideLLM en OpenShift (rate-types, ejemplo) — &lt;a href="https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794">https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>LLM benchmark tool catalogue: the practical deep dive</title><link>https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/</link><pubDate>Sun, 14 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-benchmark-tool-catalogue-practical-deep-dive/</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="what-this-article-covers">What this article covers&lt;/h2>
&lt;p>Second article in the &lt;strong>benchmarking&lt;/strong> track (B2). The &lt;a href="https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/">B1 introduction&lt;/a>
pinned the metrics and the methodology; this article is the &lt;strong>practical manual&lt;/strong>: for each
tool, what it measures, how it is actually invoked (with the command), what output format
it produces and when to choose it. The goal is that, after reading it, you know what to run to
measure your engine and you can reproduce the number. No universal recommendations; only the
mechanics of each tool and the criterion for choosing.&lt;/p>
&lt;hr>
&lt;h2 id="how-to-choose-the-tool-map">How to choose: the tool map&lt;/h2>
&lt;p>Recalling the split from B1, the tools are ordered on two axes: &lt;strong>micro-bench&lt;/strong>
(single-process, for tuning an engine) versus &lt;strong>load generator&lt;/strong> (multi-process, for measuring
real capacity), and &lt;strong>engine-native&lt;/strong> versus &lt;strong>endpoint-agnostic&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="Map of benchmark tools on two axes: single-process micro-bench versus multi-process load generator, and engine-native versus endpoint-agnostic" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.bx{fill:none;stroke:currentColor;stroke-width:1.2}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;line class="ax" x1="390" y1="40" x2="390" y2="200"/>
&lt;text x="70" y="216" class="ts">micro-bench (single-process)&lt;/text>
&lt;text x="560" y="216" class="ts">load generator (multi-process)&lt;/text>
&lt;text x="400" y="52" class="ts">↑ endpoint-agnostic&lt;/text>
&lt;text x="400" y="195" class="ts">↓ engine-native&lt;/text>
&lt;rect class="bx" x="150" y="150" width="150" height="34" rx="5"/>&lt;text x="225" y="171" text-anchor="middle" class="ts">vLLM bench / SGLang bench&lt;/text>
&lt;rect class="bx" x="470" y="70" width="120" height="34" rx="5"/>&lt;text x="530" y="91" text-anchor="middle" class="ts">AIPerf&lt;/text>
&lt;rect class="bx" x="470" y="115" width="120" height="34" rx="5"/>&lt;text x="530" y="136" text-anchor="middle" class="ts">GuideLLM&lt;/text>
&lt;rect class="bx" x="610" y="93" width="110" height="34" rx="5"/>&lt;text x="665" y="114" text-anchor="middle" class="ts">LLMPerf&lt;/text>
&lt;rect class="bx" x="150" y="60" width="150" height="34" rx="5"/>&lt;text x="225" y="81" text-anchor="middle" class="ts">MLPerf (standard suite)&lt;/text>
&lt;text x="60" y="240" class="ts">To tune a specific engine: micro-bench. To measure capacity under SLO: load generator. To compare vendors: MLPerf.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="datasets-the-synthetic-load-matters-as-much-as-the-tool">Datasets: the synthetic load matters as much as the tool&lt;/h2>
&lt;p>Before the profiles, one point that changes results without anyone noticing: &lt;strong>what load you
feed it&lt;/strong>. The tools accept different kinds of dataset, and each one measures something
different:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dataset&lt;/th>
&lt;th>What it simulates&lt;/th>
&lt;th>Bias it introduces&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>random&lt;/strong> (fixed lengths)&lt;/td>
&lt;td>controlled uniform load&lt;/td>
&lt;td>unrealistic: traffic is not fixed length&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>sharegpt&lt;/strong> (real conversations)&lt;/td>
&lt;td>realistic prompt distribution&lt;/td>
&lt;td>the de facto standard for comparison&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>your own traces&lt;/strong>&lt;/td>
&lt;td>your real traffic&lt;/td>
&lt;td>the most faithful, but specific to your case&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The trap: a benchmark with short, fixed-length prompts gives a very high throughput that
looks nothing like production, where lengths vary and long prompts dominate the prefill
cost. For a defensible figure, use &lt;strong>sharegpt&lt;/strong> (comparability) or, better, &lt;strong>traces from your
own traffic&lt;/strong> (fidelity). And always declare the length distribution (prompt/output) alongside
the number, because two benchmarks with different datasets are not comparable even if they
use the same tool.&lt;/p>
&lt;hr>
&lt;h2 id="vllm-bench-serve">vLLM bench serve&lt;/h2>
&lt;p>What it measures: TTFT, TPOT, throughput and latencies of the vLLM server under a
synthetic load. Class: micro-bench (single-process). It is the tool for &lt;strong>tuning vLLM&lt;/strong> and
seeing the effect of its optimisations (&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">decode&lt;/a>,
&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">prefill&lt;/a>).&lt;/p>
&lt;p>Typical invocation (&lt;a href="https://docs.vllm.ai/en/latest/cli/bench/serve/">vLLM · benchmark CLI&lt;/a>):&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">vllm bench serve &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --backend vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint /v1/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dataset-name sharegpt &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-prompts &lt;span class="m">1000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key parameters: &lt;code>--num-prompts&lt;/code> (load), &lt;code>--dataset-name&lt;/code> (sharegpt, random, etc.),
&lt;code>--request-rate&lt;/code> (requests/s). The output is a console summary with TTFT, TPOT, throughput
and percentiles, and it can be dumped to JSON. To sweep concurrencies there is &lt;strong>&lt;code>vllm bench sweep serve&lt;/code>&lt;/strong>, which automates the sweep (&lt;a href="https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/">vLLM · sweep&lt;/a>).&lt;/p>
&lt;p>Limit: single-process, it saturates on the client at high concurrency; less flexible than
GuideLLM in datasets and load patterns. Use it to iterate quickly on the vLLM config, not to
measure maximum capacity at scale.&lt;/p>
&lt;hr>
&lt;h2 id="sglang-bench">SGLang bench&lt;/h2>
&lt;p>What it measures: the equivalent for the &lt;strong>SGLang&lt;/strong> engine (TTFT, TPOT, throughput). Class:
micro-bench. Use: tuning SGLang and comparing it against itself across configurations. The
mechanics are analogous to &lt;code>vllm bench serve&lt;/code>: a synthetic load, console/JSON output with the
same metrics. If you are evaluating SGLang against vLLM, do not compare them with their
respective micro-benches (same class but different implementations): use an endpoint-agnostic
load generator (GuideLLM/AIPerf) against both endpoints, so that the tool is not the variable.&lt;/p>
&lt;hr>
&lt;h2 id="aiperf-nvidia-formerly-genai-perf">AIPerf (NVIDIA, formerly genai-perf)&lt;/h2>
&lt;p>What it measures: TTFT, ITL, throughput and latency against any compatible endpoint (vLLM,
NIM, TGI, SGLang). Class: &lt;strong>multi-process&lt;/strong> load generator. It is the successor to genai-perf
(retired on 15 Apr 2026). Its distinguishing feature: during the sweep it &lt;strong>detects GPU
saturation&lt;/strong> and returns the previous iteration as &lt;code>estimatedCapacity&lt;/code>, which is why the sweep
has to extend beyond the knee
(&lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">AIPerf&lt;/a>).&lt;/p>
&lt;p>Output: structured metrics (JSON) with TTFT/ITL distributions and the &lt;code>estimatedCapacity&lt;/code>. Use
it when you want the real capacity of an endpoint, whatever the engine, with automatic
detection of the saturation point.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-vllm-project">GuideLLM (vLLM project)&lt;/h2>
&lt;p>What it measures: full distributions of TTFT, ITL and end-to-end behaviour, for &lt;strong>SLO-driven&lt;/strong>
evaluation. Class: multi-process load generator. It is the recommended tool for benchmarking
vLLM servers in production: more flexible than &lt;code>vllm bench serve&lt;/code> in dataset loading, request
format and traffic patterns, with live progress and automatic report generation
(&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).&lt;/p>
&lt;p>Typical invocation:&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">guidellm benchmark &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --target &lt;span class="s2">&amp;#34;http://localhost:8000&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> --rate-type throughput &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-requests &lt;span class="m">1000&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 &lt;span class="s2">&amp;#34;samples=1000,prompt_tokens=1024,output_tokens=256&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key parameters: &lt;code>--rate-type&lt;/code> (&lt;code>synchronous&lt;/code>, &lt;code>concurrent&lt;/code>, &lt;code>throughput&lt;/code>, or rate-based),
&lt;code>--data&lt;/code> (the load specification: number of samples and prompt/output lengths), &lt;code>--target&lt;/code>
(the endpoint). It generates reproducible sweeps to find the safe operating range under
SLO, with full distributions (not just means). Output: a report with percentiles and, often,
exportable to JSON/HTML. It is the default option for answering &amp;ldquo;how hard can I load this
engine without breaking the SLO?&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h2 id="llmperf-anyscaleray">LLMPerf (Anyscale/Ray)&lt;/h2>
&lt;p>What it measures: throughput and latency at the inference level. Class: load generator. Use:
endpoint validation, historically very widespread in the Ray/Anyscale ecosystem. It is a solid
and well-known option, though less focused on distributions and sweeps than GuideLLM/AIPerf.
It fits if you already operate on Ray or want a simple tool to validate an endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="inference-benchmarker-hugging-face">inference-benchmarker (Hugging Face)&lt;/h2>
&lt;p>What it measures: latency and throughput of inference endpoints, aimed at producing comparable
reports. Class: load generator. Use: an OSS alternative inside the Hugging Face ecosystem,
useful if you already work with TGI or the HF stack. Like the others in its class, its value
lies in measuring real capacity with distributed load; the choice between this one, GuideLLM
and AIPerf usually comes down to the ecosystem you already operate in rather than to deep
differences in what they measure.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-versus-aiperf-which-of-the-two-load-generators">GuideLLM versus AIPerf: which of the two load generators&lt;/h2>
&lt;p>They are the two serious multi-process load options, and they overlap a great deal. The
practical differences that tip the choice:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>GuideLLM&lt;/th>
&lt;th>AIPerf&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Origin&lt;/td>
&lt;td>vLLM project (Red Hat)&lt;/td>
&lt;td>NVIDIA (successor to genai-perf)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Focus&lt;/td>
&lt;td>&lt;strong>SLO&lt;/strong>-driven evaluation, reproducible sweeps&lt;/td>
&lt;td>real capacity, automatic &lt;strong>estimatedCapacity&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Load patterns&lt;/td>
&lt;td>synchronous, concurrent, rate-based&lt;/td>
&lt;td>sweep with saturation detection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Report&lt;/td>
&lt;td>live progress + automatic report&lt;/td>
&lt;td>structured metrics (JSON)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ecosystem&lt;/td>
&lt;td>vLLM / OpenShift AI&lt;/td>
&lt;td>NVIDIA NIM / Triton / vLLM&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In practice: if your question is &amp;ldquo;how hard can I load it without breaking the SLO?&amp;rdquo;,
&lt;strong>GuideLLM&lt;/strong> answers it more directly with its SLO-driven sweeps; if your question is &amp;ldquo;what is
the maximum capacity of this endpoint?&amp;rdquo;, &lt;strong>AIPerf&lt;/strong> gives it with its automatic knee
detection. Many teams use both: GuideLLM for the operational SLO, AIPerf for the reference
capacity. What you must not do is compare a GuideLLM result with an AIPerf one as if they
were the same measurement: although both are multi-process, their sweep methodology differs;
pick one for a given comparison and stick to it.&lt;/p>
&lt;hr>
&lt;h2 id="mlperf-inference-mlcommons">MLPerf Inference (MLCommons)&lt;/h2>
&lt;p>What it measures: performance under standardised scenarios (Offline, Server, Interactive)
with strict rules. Unlike the previous ones, MLPerf is not &amp;ldquo;run&amp;rdquo; for your day-to-day case:
it is a vendor comparison suite whose results are read (published by NVIDIA, AMD, Intel and
others). Use it to compare hardware and engines against each other under identical rules, not
to size your specific workload. For that, GuideLLM/AIPerf against your endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="practical-comparison-table">Practical comparison table&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Class&lt;/th>
&lt;th>Base command&lt;/th>
&lt;th>Output&lt;/th>
&lt;th>When&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vllm bench serve&lt;/strong>&lt;/td>
&lt;td>micro&lt;/td>
&lt;td>&lt;code>vllm bench serve&lt;/code>&lt;/td>
&lt;td>console/JSON&lt;/td>
&lt;td>tune vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>vllm bench sweep&lt;/strong>&lt;/td>
&lt;td>micro (sweep)&lt;/td>
&lt;td>&lt;code>vllm bench sweep serve&lt;/code>&lt;/td>
&lt;td>console/JSON&lt;/td>
&lt;td>sweep concurrency in vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang bench&lt;/strong>&lt;/td>
&lt;td>micro&lt;/td>
&lt;td>bench from the SGLang repo&lt;/td>
&lt;td>console/JSON&lt;/td>
&lt;td>tune SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AIPerf&lt;/strong>&lt;/td>
&lt;td>load&lt;/td>
&lt;td>&lt;code>aiperf profile …&lt;/code>&lt;/td>
&lt;td>JSON + estimatedCapacity&lt;/td>
&lt;td>real capacity, multi-endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>load&lt;/td>
&lt;td>&lt;code>guidellm benchmark …&lt;/code>&lt;/td>
&lt;td>report + JSON/HTML&lt;/td>
&lt;td>SLO, reproducible sweep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLMPerf&lt;/strong>&lt;/td>
&lt;td>load&lt;/td>
&lt;td>Ray/LLMPerf script&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>validate endpoint (Ray)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLPerf&lt;/strong>&lt;/td>
&lt;td>suite&lt;/td>
&lt;td>(results are read)&lt;/td>
&lt;td>official results&lt;/td>
&lt;td>compare vendors&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="summary-what-you-run-for-each-question">Summary: what you run for each question&lt;/h2>
&lt;p>So as not to get lost in the catalogue, the direct mapping from question to tool:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Your question&lt;/th>
&lt;th>Tool&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;Does my vLLM config change help?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>vllm bench serve&lt;/code> (+ sweep)&lt;/td>
&lt;td>fast, engine-native&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;How hard can I load without breaking the SLO?&amp;rdquo;&lt;/td>
&lt;td>GuideLLM&lt;/td>
&lt;td>SLO-driven sweeps&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What is the maximum capacity of the endpoint?&amp;rdquo;&lt;/td>
&lt;td>AIPerf&lt;/td>
&lt;td>automatic knee detection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;vLLM or SGLang for my workload?&amp;rdquo;&lt;/td>
&lt;td>GuideLLM/AIPerf against both&lt;/td>
&lt;td>same tool, engine as the variable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Which hardware/engine is better in the abstract?&amp;rdquo;&lt;/td>
&lt;td>read MLPerf&lt;/td>
&lt;td>cross-vendor comparability&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Is there a regression in this release?&amp;rdquo;&lt;/td>
&lt;td>short sweep in CI against a baseline&lt;/td>
&lt;td>continuous detection&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The rule underlying the whole table: &lt;strong>micro-bench to iterate on an engine, load generator to
measure capacity and decide, MLPerf to compare vendors&lt;/strong>. And, for any comparison between
systems, the same tool for every candidate. If these three categories are clear to you, the
specific choice is secondary.&lt;/p>
&lt;hr>
&lt;h2 id="step-by-step-benchmark-methodology">Step-by-step benchmark methodology&lt;/h2>
&lt;p>A reliable run is not &amp;ldquo;launching the command&amp;rdquo;: it is a procedure. The steps, with the load
tool (GuideLLM/AIPerf) against your endpoint:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 170" role="img" aria-label="Benchmark flow: deploy the engine, warm up, sweep concurrency, collect metrics and compare" 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(#wm)}&lt;/style>
&lt;defs>&lt;marker id="wm" 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="50" width="120" height="44" rx="6"/>&lt;text x="80" y="69" text-anchor="middle" class="tl">1 · Deploy&lt;/text>&lt;text x="80" y="85" text-anchor="middle" class="ts">engine + config&lt;/text>
&lt;path class="ar" d="M140,72 L165,72"/>
&lt;rect class="bx" x="165" y="50" width="120" height="44" rx="6"/>&lt;text x="225" y="69" text-anchor="middle" class="tl">2 · Warm up&lt;/text>&lt;text x="225" y="85" text-anchor="middle" class="ts">discard warm-up&lt;/text>
&lt;path class="ar" d="M285,72 L310,72"/>
&lt;rect class="bx" x="310" y="50" width="120" height="44" rx="6"/>&lt;text x="370" y="69" text-anchor="middle" class="tl">3 · Sweep&lt;/text>&lt;text x="370" y="85" text-anchor="middle" class="ts">go past the knee&lt;/text>
&lt;path class="ar" d="M430,72 L455,72"/>
&lt;rect class="bx" x="455" y="50" width="120" height="44" rx="6"/>&lt;text x="515" y="69" text-anchor="middle" class="tl">4 · Collect&lt;/text>&lt;text x="515" y="85" text-anchor="middle" class="ts">JSON with percentiles&lt;/text>
&lt;path class="ar" d="M575,72 L600,72"/>
&lt;rect class="bx" x="600" y="50" width="120" height="44" rx="6"/>&lt;text x="660" y="69" text-anchor="middle" class="tl">5 · Compare&lt;/text>&lt;text x="660" y="85" text-anchor="middle" class="ts">goodput vs SLO&lt;/text>
&lt;text x="20" y="130" class="ts">Pin model, precision, hardware, dataset and SLO before step 1; record them in the output of step 4.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;ol>
&lt;li>&lt;strong>Deploy&lt;/strong> the engine with the exact config to be measured (model, precision, flags),
pinned.&lt;/li>
&lt;li>&lt;strong>Warm up&lt;/strong>: send a few requests and discard them, so that the prefix cache and the
autotuning do not inflate the first numbers.&lt;/li>
&lt;li>&lt;strong>Sweep&lt;/strong>: sweep rising concurrencies (1, 8, 16, 24, 32…) beyond the knee, to see where
latency takes off.&lt;/li>
&lt;li>&lt;strong>Collect&lt;/strong> the output in JSON with percentiles (TTFT/ITL/throughput/goodput) and the
metadata.&lt;/li>
&lt;li>&lt;strong>Compare&lt;/strong>: read the goodput under the SLO, not the maximum throughput.&lt;/li>
&lt;/ol>
&lt;p>Skipping step 2 (warm-up) or not going past the knee in step 3 are the two mistakes that bias
the result the most.&lt;/p>
&lt;hr>
&lt;h2 id="comparing-two-engines-the-fair-protocol">Comparing two engines: the fair protocol&lt;/h2>
&lt;p>If the goal is to choose between vLLM and SGLang (or TRT-LLM), the protocol that avoids false
conclusions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Same load tool&lt;/strong> (GuideLLM or AIPerf) against both endpoints, never each engine&amp;rsquo;s own
micro-bench.&lt;/li>
&lt;li>&lt;strong>Same dataset and length distribution.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Same hardware and precision.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Same SLO&lt;/strong> to compute the goodput of both.&lt;/li>
&lt;li>Vary only the engine; everything else fixed.&lt;/li>
&lt;/ul>
&lt;p>Only then is the difference you measure down to the engine and not to the tool, the dataset or
the hardware. It is the controlled experiment that supports the scorecard row (article B8).&lt;/p>
&lt;hr>
&lt;h2 id="output-format-and-comparability">Output format and comparability&lt;/h2>
&lt;p>What you record alongside the number is what makes it comparable. A useful output includes,
in JSON so it can be versioned:&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;tool&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;guidellm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;x.y.z&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;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Llama-3.1-70B&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;precision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;FP16&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;8xH100 SXM NVLink&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;load&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;prompt_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 class="nt">&amp;#34;output_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 class="nt">&amp;#34;concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">16&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;results&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;ttft_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">180&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">460&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_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">22&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;throughput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3400&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;goodput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3330&lt;/span>&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>Saving this for every run turns a benchmark into an auditable datum: anyone can reproduce the
figure with the same tool, version, model, hardware and load. It is the material of the
reproducible harness (article S4), and the difference between a defensible number and a
console screenshot.&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-reading-the-output-of-a-sweep">Worked example: reading the output of a sweep&lt;/h2>
&lt;p>An illustrative output from a GuideLLM sweep on a 70B on 8×H100 (SLO: P99 TTFT &amp;lt; 500 ms), just
as you would read it:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrency&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>240&lt;/td>
&lt;td>20&lt;/td>
&lt;td>2,100&lt;/td>
&lt;td>2,100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>460&lt;/td>
&lt;td>22&lt;/td>
&lt;td>3,400&lt;/td>
&lt;td>3,330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>24&lt;/td>
&lt;td>980&lt;/td>
&lt;td>31&lt;/td>
&lt;td>3,900&lt;/td>
&lt;td>2,420&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>1,800&lt;/td>
&lt;td>54&lt;/td>
&lt;td>4,000&lt;/td>
&lt;td>800&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>How to read it: the &lt;strong>knee&lt;/strong> sits between 16 and 24. At concurrency 16, P99 (460 ms) meets the
SLO and goodput (3,330 tok/s) ≈ throughput. At 24, throughput rises little (3,400 → 3,900) but
P99 already violates the SLO and goodput falls to 2,420. At 32, throughput is at its
maximum (4,000) but goodput collapses to 800: the system &amp;ldquo;performs well&amp;rdquo; while serving requests
that do not comply. The defensible capacity is the one at concurrency 16 (3,330 useful
tok/s), and that is the number that goes into &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a>
and into cost per token. Anyone reporting &amp;ldquo;4,000 tok/s&amp;rdquo; is describing the point where the
system no longer meets its SLO.&lt;/p>
&lt;hr>
&lt;h2 id="automating-the-harness">Automating the harness&lt;/h2>
&lt;p>A manual run does not scale to a benchmarking programme. The minimum automation:&lt;/p>
&lt;ul>
&lt;li>An &lt;strong>idempotent script&lt;/strong> that deploys the engine, warms it up, runs the sweep and saves the
JSON with all the metadata (model, version, hardware, dataset, SLO).&lt;/li>
&lt;li>&lt;strong>Naming by date and config&lt;/strong>, to version the runs and compare over time.&lt;/li>
&lt;li>A &lt;strong>results store&lt;/strong> (a git repo of JSONs, or a bucket) so that anyone can reproduce and
compare.&lt;/li>
&lt;/ul>
&lt;p>The goal is that reproducing a number is one command, not an afternoon. It is the basis of the
harness in article S4, and what turns benchmarking from a one-off activity into a continuous
platform capability.&lt;/p>
&lt;hr>
&lt;h2 id="ci-integration-continuous-benchmarking">CI integration: continuous benchmarking&lt;/h2>
&lt;p>The next level is measuring on every change: a CI job that, when the engine or the config
is updated, launches a short sweep against a test environment and &lt;strong>compares against the
baseline&lt;/strong>. If goodput falls by more than a threshold, the pipeline fails. That way a
performance regression is caught at the commit, not in production. Watch out for two things:
benchmarking in CI consumes GPU-hours (budget for it) and it needs a stable environment (same
hardware) for the comparison to be valid. You do not need the full sweep on every commit: a
short sweep covering the knee is enough to detect regressions; the exhaustive sweep is for
releases.&lt;/p>
&lt;hr>
&lt;h2 id="the-cost-of-measuring-in-euros">The cost of measuring (in euros)&lt;/h2>
&lt;p>Benchmarking consumes GPU-hours, and that has a cost worth budgeting for. A serious sweep
on an 8×H100 node can occupy the cards for a couple of hours; at an amortised cost of ~11 €/h,
that is ~22 € per full sweep, more if you sweep several models and precisions. It is not much
per run, but a continuous benchmarking programme (every release, every config change) adds up.
The practical rule: automate the harness so that each run is cheap and reproducible, and
measure what you are going to use to decide, not for the sake of exhaustiveness. The cost of
measuring is part of the cost of the platform, small next to the cost of serving but real.&lt;/p>
&lt;hr>
&lt;h2 id="observing-during-the-benchmark-one-run-three-axes">Observing during the benchmark: one run, three axes&lt;/h2>
&lt;p>A trick that saves work and connects the series: &lt;strong>while the sweep runs, capture the GPU
metrics too&lt;/strong>. With DCGM exporting to Prometheus during the run, you record at the same time:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>Source during the sweep&lt;/th>
&lt;th>Metric&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Performance&lt;/td>
&lt;td>the tool (GuideLLM/AIPerf)&lt;/td>
&lt;td>TTFT, ITL, throughput, goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>DCGM&lt;/td>
&lt;td>power (W) → J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost&lt;/td>
&lt;td>node price (OpenCost)&lt;/td>
&lt;td>€/hour → CPM per point&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That way, from a single run you get the three numbers for the same operating point: at
concurrency 16, the goodput (3,330 tok/s), the average power (from DCGM, which divided by the
throughput gives J/token) and the cost per token (with the node price). Instead of three
separate campaigns, you measure the three axes at once, and they end up &lt;strong>coherent by
construction&lt;/strong> because they correspond to the same instant and the same load. It is what the
harness in article S4 does, and the reason to export DCGM during the benchmark even if you are
only after performance: energy and cost come almost free if you capture them in the same
window.&lt;/p>
&lt;p>The methodological warning: align the time windows. The DCGM power figures and the tool&amp;rsquo;s
metrics have to cover exactly the same interval (no warm-up, no shutdown), or the J/token does
not correspond to the throughput measured. Same window, same three numbers.&lt;/p>
&lt;hr>
&lt;h2 id="the-benchmark-is-also-finops-and-energy">The benchmark is also FinOps and energy&lt;/h2>
&lt;p>An idea that connects this article with the rest of the series: &lt;strong>measuring performance is, in
fact, measuring cost and energy&lt;/strong>. By the identity of the &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">opening article&lt;/a>,
goodput is the denominator of cost per token and of energy per token. When a sweep reveals
that config A gives 3,330 tok/s of goodput and config B gives 4,000, you are not only measuring
speed: you are measuring that B costs fewer euros and fewer watts per token. That is why the
output JSON of a benchmark should be accompanied by the iron cost (from OpenCost) to compute
the real CPM of every point in the sweep: throughput × node price = cost per token.
Benchmarking is not an isolated axis; it is the tool that, indirectly, moves the cost and the
energy of the platform the most, and the one that fills the performance column of the scorecard
with numbers that translate directly into euros.&lt;/p>
&lt;p>The operational consequence: do not benchmark performance in a vacuum. Every run you save with
its throughput and its goodput should be crossable with the node cost (€/hour) and the energy
(J/token) to give the three faces of the same operating point. That is how the benchmarking
track feeds the FinOps and energy tracks instead of living apart.&lt;/p>
&lt;hr>
&lt;h2 id="state-of-the-art-2026-and-limits">State of the art 2026 and limits&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Migration to multi-process&lt;/strong>: AIPerf (formerly genai-perf) and GuideLLM consolidate the
measurement of real capacity; micro-benches are left for tuning engines.&lt;/li>
&lt;li>&lt;strong>GuideLLM as the OSS standard&lt;/strong> for SLO-driven evaluation with an automatic report.&lt;/li>
&lt;li>&lt;strong>Careful with comparing across classes&lt;/strong>: a micro-bench and a load generator are not
comparable; pin the class and the tool.&lt;/li>
&lt;li>&lt;strong>Version and dataset matter&lt;/strong>: the same command with a different dataset or version gives a
different number; pin them.&lt;/li>
&lt;li>&lt;strong>MLPerf does not size your case&lt;/strong>: it compares vendors, it does not replace a sweep against
your workload.&lt;/li>
&lt;/ul>
&lt;p>With the practical catalogue covered, the next article in the track (B3) goes into GuideLLM
and SLO validation under load in depth. The tool is the means; the reproducible datum is the
end.&lt;/p>
&lt;h2 id="mistakes-that-invalidate-a-benchmark">Mistakes that invalidate a benchmark&lt;/h2>
&lt;p>To finish, the list of what turns a run into rubbish, by frequency:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mistake&lt;/th>
&lt;th>Effect&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Comparing different classes&lt;/td>
&lt;td>up to 7× of spurious difference&lt;/td>
&lt;td>same tool for everyone&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Not discarding the warm-up&lt;/td>
&lt;td>artificially low TTFT&lt;/td>
&lt;td>warm up and discard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Not going past the knee&lt;/td>
&lt;td>you do not know the safe capacity&lt;/td>
&lt;td>extend the sweep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Unrealistic dataset (fixed length)&lt;/td>
&lt;td>throughput that does not apply&lt;/td>
&lt;td>sharegpt or your own traces&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reporting the mean, not P99&lt;/td>
&lt;td>hides the tail&lt;/td>
&lt;td>percentiles always&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenizer foreign to the model&lt;/td>
&lt;td>biased tok/s and cost per token&lt;/td>
&lt;td>count with the model&amp;rsquo;s own&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Not pinning version/config&lt;/td>
&lt;td>irreproducible&lt;/td>
&lt;td>save everything in the JSON&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Any one of these is enough for the number not to be defensible. A benchmark is only as good as
its methodology: the tool matters less than running it properly and recording everything.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>The catalogue of benchmark tools boils down to a simple decision (micro-bench to tune an
engine, load generator to measure capacity, MLPerf to compare vendors) and to a discipline that
weighs more than the choice: &lt;strong>the method&lt;/strong>. The same &lt;code>guidellm benchmark&lt;/code> gives a golden datum
or a useless one depending on the dataset, the warm-up, how far the sweep goes and what you
record in the output. For a sovereign architecture proposal, performance only counts if it
comes with its command, its version, its load and its goodput under SLO, and, crossed with cost
in euros and energy per token, it becomes the scorecard column that decides which engine and
which configuration hold up the platform. You choose the tool in five minutes; the methodology
is what makes the number survive an audit.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/genai-perf-llm-inference-profiling-in-depth/">GenAI-Perf in depth&lt;/a> — an extended profile of the NVIDIA profiler: metrics (TTFT/TPOT/ISL/OSL), invocation against an OpenAI-compatible endpoint and comparison with GuideLLM/LLMPerf.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">LLM serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — once goodput has been measured with these tools, here is which engine wins at each point of the Pareto frontier.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Measurement bias and reproducibility&lt;/a> — the sources of bias that invalidate results even when the tool is correctly configured: warm-up, real versus synthetic dataset, shared environment.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>vLLM · CLI de benchmark (&lt;code>bench serve&lt;/code>) — &lt;a href="https://docs.vllm.ai/en/latest/cli/bench/serve/">https://docs.vllm.ai/en/latest/cli/bench/serve/&lt;/a>&lt;/li>
&lt;li>vLLM · &lt;code>bench sweep serve&lt;/code> — &lt;a href="https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/">https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/&lt;/a>&lt;/li>
&lt;li>Red Hat · 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>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>NVIDIA AIPerf · guía de benchmarking — &lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/&lt;/a>&lt;/li>
&lt;li>Medium · Benchmarking LLM Serving Performance (guía) — &lt;a href="https://medium.com/@kimdoil1211/benchmarking-llm-serving-performance-a-comprehensive-guide-db94b1bfe8cf">https://medium.com/@kimdoil1211/benchmarking-llm-serving-performance-a-comprehensive-guide-db94b1bfe8cf&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Cost, performance and energy: the three axes of LLM inference and the identity that ties them together</title><link>https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/</link><pubDate>Sat, 13 Jun 2026 03:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. Cost and energy references are
European (France, Germany, Spain), because the aim is to defend a sovereign European
architecture; when a source quotes prices in dollars it is flagged as &amp;ldquo;USD&amp;rdquo;. The dollar
symbol is not used (on this site it is a formula delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-series-is">What this series is&lt;/h2>
&lt;p>This is the first instalment of a series of &lt;strong>28 data articles&lt;/strong> (not opinion pieces)
about the three dimensions that decide whether a sovereign European LLM platform is
sustainable: &lt;strong>cost (FinOps)&lt;/strong>, &lt;strong>performance (benchmarking)&lt;/strong> and &lt;strong>energy&lt;/strong>. The aim
is to have a body of verifiable figures, with their methodology, to support architecture
decisions with numbers rather than intuition. This first article sets the
frame: what each axis measures, &lt;strong>how they link to one another&lt;/strong>, an end-to-end worked
example with European data and the 2026 references that will be used as a base.&lt;/p>
&lt;p>The quantitative thesis, in one sentence: the three axes &lt;strong>are not independent&lt;/strong>. Throughput
(tokens per second) is the common denominator of cost per token and energy per token, and
energy is, on top of that, between &lt;strong>30 % and 50 %&lt;/strong> of the cost itself. Optimising one moves
the other two. And in Europe, where the cluster sits (France, Germany, Spain) changes
both the electricity cost and the carbon per token at once.&lt;/p>
&lt;hr>
&lt;h2 id="the-three-axes-and-their-metrics">The three axes and their metrics&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>Question&lt;/th>
&lt;th>Key metrics&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Tool family&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cost (FinOps)&lt;/td>
&lt;td>How much does each unit of work cost?&lt;/td>
&lt;td>cost/1M tokens (CPM), cost/request, €/GPU-hour, utilisation&lt;/td>
&lt;td>€/1M tok, €/h&lt;/td>
&lt;td>OpenCost, Kubecost, CloudZero&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Performance&lt;/td>
&lt;td>How much work per second and at what latency?&lt;/td>
&lt;td>TTFT, TPOT/ITL, throughput, goodput, P50/P99&lt;/td>
&lt;td>tok/s, ms&lt;/td>
&lt;td>GuideLLM, AIPerf, LLMPerf, MLPerf Inference&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>How many watts per unit of work?&lt;/td>
&lt;td>energy/token, average power, PUE, carbon&lt;/td>
&lt;td>J/tok, Wh, kWh, gCO₂&lt;/td>
&lt;td>Zeus, CodeCarbon, MLPerf Power, Kepler&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each axis has its own in-depth introductory article in this same batch; here only how they
fit together matters. It is worth breaking each one down a little more before crossing them.&lt;/p>
&lt;h3 id="cost-axis-in-detail">Cost axis in detail&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Why it matters&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CPM (cost/1M tokens)&lt;/td>
&lt;td>cluster cost ÷ tokens produced&lt;/td>
&lt;td>the comparable unit on-prem vs cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per request&lt;/td>
&lt;td>cost attributed to one complete request&lt;/td>
&lt;td>billing by real usage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>€/GPU-hour&lt;/td>
&lt;td>hourly cost of a GPU (amortised or rented)&lt;/td>
&lt;td>the basis of any calculation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Utilisation&lt;/td>
&lt;td>fraction of the GPU actually used&lt;/td>
&lt;td>the lever that moves everything else&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fixed vs variable cost&lt;/td>
&lt;td>amortised capex vs energy/operations&lt;/td>
&lt;td>defines the break-even point&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="performance-axis-in-detail">Performance axis in detail&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Dominant phase&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT&lt;/td>
&lt;td>latency to the first token&lt;/td>
&lt;td>prefill&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT / ITL&lt;/td>
&lt;td>time between output tokens&lt;/td>
&lt;td>decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput&lt;/td>
&lt;td>tokens or requests per second of the system&lt;/td>
&lt;td>both&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Goodput&lt;/td>
&lt;td>throughput that meets the SLO (not the raw figure)&lt;/td>
&lt;td>the honest number&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P95 / P99&lt;/td>
&lt;td>latency percentiles (not the mean)&lt;/td>
&lt;td>the real experience&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="energy-axis-in-detail">Energy axis in detail&lt;/h3>
&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>Average power&lt;/td>
&lt;td>average instantaneous consumption&lt;/td>
&lt;td>W&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per token&lt;/td>
&lt;td>energy per generated token&lt;/td>
&lt;td>J/token&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>PUE&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;hr>
&lt;h2 id="the-identity-that-ties-the-three-axes-together-throughput">The identity that ties the three axes together: throughput&lt;/h2>
&lt;p>The &lt;strong>cost per million tokens (CPM)&lt;/strong> is the cluster&amp;rsquo;s cost divided by what it
produces:&lt;/p>
$$\text{CPM} = \frac{\text{cluster cost per hour}}{\text{throughput (tok/s)} \times 3600 / 10^6}$$
&lt;p>&lt;strong>Energy per token&lt;/strong> has exactly the same form, with power in the
numerator:&lt;/p>
$$\text{energy per token (J)} = \frac{\text{average power (W)}}{\text{throughput (tok/s)}}$$
&lt;p>The two share the &lt;strong>same denominator&lt;/strong>: throughput. Hence the key operational consequence:
&lt;strong>every performance improvement that raises tokens/s lowers both cost per token and
energy per token at the same time&lt;/strong>. These are not three separate optimisations; throughput moves them together.
That is why performance benchmarking (axis 2) is, at bottom, also a FinOps tool and an
energy efficiency tool.&lt;/p>
&lt;p>The CPM↔throughput relationship is direct: the higher the throughput, the lower the CPM. It is the identity on
which the whole cost analysis of the series is built.&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-unit-economics-of-an-8h100-node-in-europe">Worked example: unit economics of an 8×H100 node in Europe&lt;/h2>
&lt;p>To see the identity in action, an end-to-end calculation on an example node
(8×H100 SXM, Llama 3.1 70B FP16 served with vLLM), with European prices. Order-of-magnitude
figures; the methodology is what matters.&lt;/p>
&lt;p>&lt;strong>Starting data:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Parameter&lt;/th>
&lt;th>Value&lt;/th>
&lt;th>Source&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>European cloud rental (Scaleway, per H100)&lt;/td>
&lt;td>2.73 €/h&lt;/td>
&lt;td>&lt;a href="https://www.scaleway.com/en/h100/">Scaleway&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>→ 8×H100 node on-demand&lt;/td>
&lt;td>~21.84 €/h&lt;/td>
&lt;td>8 × 2.73&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Amortised on-prem cost (estimate)&lt;/td>
&lt;td>~10–13 €/h&lt;/td>
&lt;td>capex 24–36 months + opex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput&lt;/td>
&lt;td>2,800 tok/s&lt;/td>
&lt;td>reference benchmark&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power per H100 (TDP)&lt;/td>
&lt;td>up to 700 W&lt;/td>
&lt;td>&lt;a href="https://www.nvidia.com/en-us/data-center/h100/">NVIDIA&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reference PUE&lt;/td>
&lt;td>1.4&lt;/td>
&lt;td>typical of a datacenter&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Cost per million tokens.&lt;/strong> At 2,800 tok/s, hourly throughput is
2,800 × 3,600 / 10⁶ = 10.08 million tokens/hour. Therefore:&lt;/p>
$$\text{CPM (cloud)} = \frac{21{.}84}{10{.}08} \approx 2{.}17 \text{ €/1M tok}$$
&lt;p>With the amortised on-prem cost (~11 €/h) it drops to &lt;strong>~1.09 €/1M tok&lt;/strong>, the difference between
renting from a provider and amortising your own iron.&lt;/p>
&lt;p>&lt;strong>Energy per token.&lt;/strong> The power of the 8 cards is 8 × 700 = 5,600 W (board only).
Over 2,800 tok/s:&lt;/p>
$$\text{energy per token} = \frac{5,600}{2,800} = 2 \text{ J/token (board)}$$
&lt;p>Over a million tokens that is 2 × 10⁶ J = 0.56 kWh (GPU only). Applying PUE 1.4: ~0.78
kWh per million tokens. At French wholesale prices (~0.058 €/kWh) that is ~0.045 € of
electricity per million tokens; in Germany (~0.091 €/kWh), ~0.071 €, some 57 % more, because of
the grid, not the model.&lt;/p>
&lt;p>&lt;strong>The link.&lt;/strong> If a serving optimisation raised throughput from 2,800 to 4,200 tok/s
(+50 %), the CPM would fall to ~1.45 €/1M tok and energy per token to ~1.33 J, both at
once, through the same denominator. That is the identity, with numbers.&lt;/p>
&lt;hr>
&lt;h2 id="energy-is-inside-the-cost">Energy is &lt;em>inside&lt;/em> the cost&lt;/h2>
&lt;p>The second link: energy is not an axis separate from cost, it is &lt;strong>a large component of
it&lt;/strong>. 2026 reference data:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Figure&lt;/th>
&lt;th>Value&lt;/th>
&lt;th>Source&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Electricity as a share of TCO (5 years)&lt;/td>
&lt;td>30–40 % (&amp;gt;50 % at high density 24/7)&lt;/td>
&lt;td>&lt;a href="https://www.spheron.network/blog/ai-inference-power-electricity-cost-2026/">Spheron&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Day-ahead wholesale France (Mar-2026)&lt;/td>
&lt;td>~58 €/MWh (0.058 €/kWh)&lt;/td>
&lt;td>&lt;a href="https://tradingeconomics.com/france/electricity-price">TradingEconomics&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Day-ahead wholesale Spain&lt;/td>
&lt;td>~54 €/MWh (0.054 €/kWh)&lt;/td>
&lt;td>&lt;a href="https://tradingeconomics.com/spain/electricity-price">TradingEconomics&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Day-ahead wholesale Germany&lt;/td>
&lt;td>~91 €/MWh (0.091 €/kWh)&lt;/td>
&lt;td>&lt;a href="https://www.tradingeconomics.com/germany/electricity-price">TradingEconomics&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Electricity cost/token at 80 % util&lt;/td>
&lt;td>¼ of that at 20 % util&lt;/td>
&lt;td>&lt;a href="https://www.spheron.network/blog/ai-inference-power-electricity-cost-2026/">Spheron&lt;/a>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A methodological warning: the &lt;strong>wholesale&lt;/strong> price (day-ahead) is not what a company pays; &lt;strong>industrial
retail&lt;/strong> adds grid charges and taxes (roughly ×1.5–2). But the relative
ordering holds: &lt;strong>France and Spain, cheap; Germany, expensive.&lt;/strong>&lt;/p>
&lt;p>The third link, utilisation, closes the triangle: power is consumed even when the
GPU is idle, so spreading the fixed cost (energy included) over more tokens (more
throughput, more occupancy) &lt;strong>lowers all three axes at once&lt;/strong>. The electricity cost per token at
80 % utilisation is a quarter of what it is at 20 %.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 300" role="img" aria-label="The three axes (cost, performance, energy) linked by throughput at the centre; energy is also a component of cost" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 13px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.2;stroke-dasharray:4 3}&lt;/style>
&lt;rect class="bx" x="300" y="120" width="180" height="60" rx="8"/>
&lt;text x="390" y="146" text-anchor="middle" class="tl">THROUGHPUT&lt;/text>
&lt;text x="390" y="164" text-anchor="middle" class="ts">tokens/s · common denominator&lt;/text>
&lt;rect class="bx" x="40" y="40" width="200" height="64" rx="8"/>
&lt;text x="56" y="63" class="tl">Cost (FinOps)&lt;/text>
&lt;text x="56" y="81" class="ts">CPM = cost/h ÷ throughput&lt;/text>
&lt;text x="56" y="97" class="ts">€/1M tok · utilisation&lt;/text>
&lt;rect class="bx" x="540" y="40" width="200" height="64" rx="8"/>
&lt;text x="556" y="63" class="tl">Performance&lt;/text>
&lt;text x="556" y="81" class="ts">TTFT · TPOT · goodput&lt;/text>
&lt;text x="556" y="97" class="ts">raise tok/s -> lower the others&lt;/text>
&lt;rect class="bx" x="290" y="220" width="200" height="60" rx="8"/>
&lt;text x="306" y="243" class="tl">Energy&lt;/text>
&lt;text x="306" y="261" class="ts">J/tok = W ÷ throughput&lt;/text>
&lt;line class="ar" x1="240" y1="92" x2="300" y2="135"/>
&lt;line class="ar" x1="540" y1="92" x2="480" y2="135"/>
&lt;line class="ar" x1="390" y1="180" x2="390" y2="220"/>
&lt;line class="ar" x1="140" y1="104" x2="320" y2="220"/>
&lt;text x="150" y="170" class="ts">energy = 30-50 % of cost (TCO)&lt;/text>
&lt;text x="40" y="295" class="ts">Improving throughput lowers cost/token and energy/token at once; raising utilisation spreads fixed cost over more tokens.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="sensitivity-analysis-what-moves-the-cost-per-token">Sensitivity analysis: what moves the cost per token&lt;/h2>
&lt;p>Four levers dominate the CPM, and it is worth knowing their elasticity before defending
any figure:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Lever&lt;/th>
&lt;th>Change&lt;/th>
&lt;th>Effect on CPM&lt;/th>
&lt;th>Mechanism&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Throughput&lt;/td>
&lt;td>+50 % tok/s&lt;/td>
&lt;td>−33 % CPM&lt;/td>
&lt;td>direct denominator&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Utilisation&lt;/td>
&lt;td>20 % → 80 %&lt;/td>
&lt;td>up to −75 % on the electricity portion&lt;/td>
&lt;td>spreads fixed cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Location (energy)&lt;/td>
&lt;td>Germany → France&lt;/td>
&lt;td>lowers the electricity portion (~37 % less €/kWh)&lt;/td>
&lt;td>wholesale FR &amp;lt; DE&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Amortise vs rent&lt;/td>
&lt;td>21.84 → 11 €/h&lt;/td>
&lt;td>−50 % CPM&lt;/td>
&lt;td>removes the provider&amp;rsquo;s margin&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quantisation (FP8/INT4)&lt;/td>
&lt;td>+throughput, −VRAM/KV&lt;/td>
&lt;td>lowers CPM and J/token&lt;/td>
&lt;td>same identity&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reading: the CPM &lt;strong>is not a number, it is a function&lt;/strong> of utilisation, throughput, location
(energy price) and ownership model. Any on-prem vs cloud comparison that does not
fix those assumptions is not comparable, and that is exactly the job of the synthesis article.&lt;/p>
&lt;hr>
&lt;h2 id="simplified-tco-model-on-premise">Simplified TCO model (on-premise)&lt;/h2>
&lt;p>The real on-premise cost is not the GPU alone: it is &lt;strong>amortised capex + opex&lt;/strong>, over a hardware
cycle of 24–36 months. Reference structure of an inference node:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Typical weight&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPUs&lt;/td>
&lt;td>capex&lt;/td>
&lt;td>the bulk&lt;/td>
&lt;td>amortise over 24–36 months&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Server, network, BMC&lt;/td>
&lt;td>capex&lt;/td>
&lt;td>medium&lt;/td>
&lt;td>NICs, switching, out-of-band management&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Electricity&lt;/td>
&lt;td>opex&lt;/td>
&lt;td>30–50 % of TCO&lt;/td>
&lt;td>depending on country (FR/ES cheap, DE expensive) and PUE&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cooling&lt;/td>
&lt;td>opex&lt;/td>
&lt;td>inside the PUE&lt;/td>
&lt;td>PUE 1.2–1.6 typical&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintenance and support&lt;/td>
&lt;td>opex&lt;/td>
&lt;td>medium&lt;/td>
&lt;td>spares, contracts, RMA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Staff and operations&lt;/td>
&lt;td>opex&lt;/td>
&lt;td>variable&lt;/td>
&lt;td>the hidden cost that cloud rental includes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The difference between cloud rental (21.84 €/h in the example) and the real amortised cost
(~10–13 €/h) is this breakdown: the provider charges a margin for absorbing capex,
cooling and operations. The break-even point appears when sustained volume
exceeds &lt;strong>~2M tokens/day&lt;/strong>, with typical amortisation in &lt;strong>6–12 months&lt;/strong>
(&lt;a href="https://www.spheron.network/blog/ai-inference-cost-economics-2026/">Spheron&lt;/a>). Below
that threshold, renting usually wins; above it, amortising. Article A8 develops the
full model and synthesis S3 connects it with sizing.&lt;/p>
&lt;hr>
&lt;h2 id="reference-the-price-of-a-gpu-hour-in-european-sovereign-cloud">Reference: the price of a GPU-hour in European sovereign cloud&lt;/h2>
&lt;p>The other side of the on-prem comparison is the price of renting the GPU. For a
sovereign European platform, what matters is not AWS/GCP/Azure (subject to the &lt;strong>US CLOUD
Act&lt;/strong>), but the European providers:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Provider&lt;/th>
&lt;th>H100 (€/hour)&lt;/th>
&lt;th>Location&lt;/th>
&lt;th>Sovereignty&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Scaleway&lt;/td>
&lt;td>from 2.73&lt;/td>
&lt;td>Paris, Warsaw&lt;/td>
&lt;td>EU jurisdiction, exempt from US CLOUD Act&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OVHcloud&lt;/td>
&lt;td>+40–45 % vs Scaleway&lt;/td>
&lt;td>France, Germany&lt;/td>
&lt;td>EU jurisdiction, exempt from US CLOUD Act&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hyperscalers (reference)&lt;/td>
&lt;td>3–7 USD&lt;/td>
&lt;td>global&lt;/td>
&lt;td>subject to US CLOUD Act&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The data: Scaleway offers the H100 from &lt;strong>2.73 €/h&lt;/strong> in European datacenters (Paris, Warsaw),
with models and data under EU jurisdiction, immune to extraterritorial regulation; OVHcloud
serves it from France and Germany, ~40–45 % more expensive than Scaleway as of March 2026. Both are
&lt;strong>exempt from the US CLOUD Act&lt;/strong>, the foundational sovereignty requirement for data subject to
GDPR (&lt;a href="https://www.scaleway.com/en/h100/">Scaleway&lt;/a>, &lt;a href="https://deploybase.ai/articles/ovhcloud-gpu-pricing">DeployBase&lt;/a>).&lt;/p>
&lt;p>The implication for the proposal: the comparison is not only cost, it is &lt;strong>cost + sovereignty&lt;/strong>. An
on-prem cluster of your own or a European cloud keep the data under EU jurisdiction; a
US hyperscaler does not, however cheap the GPU-hour comes out. That is the leg that the
cost axis on its own does not capture, and that synthesis S2 quantifies.&lt;/p>
&lt;hr>
&lt;h2 id="energy-and-carbon-reference-france-germany-spain">Energy and carbon reference: France, Germany, Spain&lt;/h2>
&lt;p>Here the choice of European country changes both the electricity cost and the carbon per
token. It is the most important table for a sovereign proposal:&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>France&lt;/td>
&lt;td>~58&lt;/td>
&lt;td>~20–60&lt;/td>
&lt;td>nuclear: cheap and very clean&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Spain&lt;/td>
&lt;td>~54&lt;/td>
&lt;td>~150–170&lt;/td>
&lt;td>renewables + gas: cheap, medium carbon&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Germany&lt;/td>
&lt;td>~91&lt;/td>
&lt;td>~363 (2024 average)&lt;/td>
&lt;td>coal/gas + renewables: expensive and dirty&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Sources: day-ahead wholesale prices (&lt;a href="https://tradingeconomics.com/france/electricity-price">TradingEconomics&lt;/a>),
German carbon ~363 gCO₂/kWh (2024 average, &lt;a href="https://en.wikipedia.org/wiki/Electricity_sector_in_Germany">Statista via 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). The PUE multiplies all of that energy: at 1.4, for every kWh of
GPU the datacenter spends 1.4 kWh with cooling included.&lt;/p>
&lt;p>The calculation, on the example node (0.78 kWh per million tokens, with PUE): in
France (~40 gCO₂/kWh) it is &lt;strong>~31 gCO₂ per million tokens&lt;/strong>; in Germany (~363
gCO₂/kWh), &lt;strong>~283 gCO₂ per million&lt;/strong>, a factor of &lt;strong>~9×&lt;/strong>. That is, the same load
served in France emits of the order of &lt;strong>9× less carbon per token&lt;/strong> than in Germany, and
costs ~37 % less in electricity, &lt;strong>without touching a single line of the stack&lt;/strong>, purely because of the
country&amp;rsquo;s grid. &lt;strong>Siting the cluster in France
or Spain is at once a cost lever, a carbon lever and a compliance lever.&lt;/strong> Hourly grid
intensity is obtained from ElectricityMaps, the reference source for these
calculations.&lt;/p>
&lt;p>Scale context: global datacenters consumed &lt;strong>~460 TWh in 2025&lt;/strong> (≈1.8 % of the world&amp;rsquo;s
electricity), and the IEA projects passing &lt;strong>800 TWh in 2028&lt;/strong>, which is why energy
efficiency per token stops being a detail and becomes a design constraint,
and why carbon enters the European regulatory equation.&lt;/p>
&lt;hr>
&lt;h2 id="sovereignty-and-regulation-the-axis-that-does-not-appear-in-the-cpm">Sovereignty and regulation: the axis that does not appear in the CPM&lt;/h2>
&lt;p>For a European platform there is a fourth factor that the three numerical axes &lt;strong>do not
capture&lt;/strong>, and that can invalidate the lowest cost figure: jurisdiction. Three
fronts that turn &amp;ldquo;the cheapest GPU-hour&amp;rdquo; into &amp;ldquo;the cheapest GPU-hour &lt;em>that complies&lt;/em>&amp;rdquo;:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Front&lt;/th>
&lt;th>What it requires&lt;/th>
&lt;th>Architectural implication&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GDPR + US CLOUD Act&lt;/td>
&lt;td>personal data under a jurisdiction accessible to US authorities is exposed&lt;/td>
&lt;td>European cloud (Scaleway/OVHcloud) or on-prem keep the data in the EU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>EU AI Act&lt;/td>
&lt;td>obligations for high-risk systems (traceability, control, documentation)&lt;/td>
&lt;td>on-prem control makes auditability and compliance easier&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CSRD / emissions reporting&lt;/td>
&lt;td>reporting the carbon footprint (scope 2)&lt;/td>
&lt;td>a low-carbon grid (France) improves the reported figure&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The key point for the proposal: a US hyperscaler can offer a competitive
GPU-hour, but it &lt;strong>puts the data under the US CLOUD Act&lt;/strong>, which for data subject to GDPR
is a compliance risk, not a saving. That is why this series&amp;rsquo; comparison is not made
in USD against US providers: it is made in euros, against options that keep
&lt;strong>data sovereignty&lt;/strong>, on-prem of your own or European cloud. Cost, energy and
carbon are measurable axes; sovereignty is the constraint that decides &lt;strong>which options even
enter the table&lt;/strong>. This links with &lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">the mapping of the EU AI Act onto the on-premise
architecture&lt;/a> and with &lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">the technical
controls ENS × ISO 42001 × EU AI Act&lt;/a>.&lt;/p>
&lt;p>The operational conclusion: &lt;strong>France and Spain not only come out cheaper and cleaner than
Germany; being EU jurisdiction, they meet the sovereignty constraint that a US
hyperscaler does not meet at any price.&lt;/strong> Three advantages that reinforce each other, and that the
proposal can defend with the same table.&lt;/p>
&lt;hr>
&lt;h2 id="methodology-how-to-read-and-not-fool-yourself-with-these-figures">Methodology: how to read (and not fool yourself with) these figures&lt;/h2>
&lt;p>Five warnings that apply to all the data in this series:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Board vs node.&lt;/strong> The GPU&amp;rsquo;s power (700 W per H100) is the floor; the complete
node (CPU, NICs, power supplies, fans) consumes more. Always compare the same boundary.&lt;/li>
&lt;li>&lt;strong>Wholesale vs retail.&lt;/strong> The day-ahead price is not what the company pays; industrial
retail adds grid charges and taxes. Use whichever matches your contract.&lt;/li>
&lt;li>&lt;strong>Measured vs estimated.&lt;/strong> A wattmeter figure and an &lt;code>nvidia-smi&lt;/code> estimate are not
the same thing; the difference is quantified in the energy articles.&lt;/li>
&lt;li>&lt;strong>Amortisation assumptions.&lt;/strong> The on-prem cost/hour depends on the hardware cycle (24 vs
36 months) and on the assumed utilisation; changing those assumptions moves the CPM more than
any serving optimisation.&lt;/li>
&lt;li>&lt;strong>Comparability.&lt;/strong> Two CPM or J/token figures are only comparable if model,
precision, context length, load &lt;strong>and country&lt;/strong> match. Most public
comparisons (and in USD) do not fix those assumptions, which is why the series reproduces the ones that matter
with its own harness and European data.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="the-combined-scorecard">The combined scorecard&lt;/h2>
&lt;p>The destination of the series is a single board where each model/configuration/location is
scored on all the axes at once, to decide on a &lt;strong>Pareto frontier&lt;/strong> rather than on a
single number. An &lt;strong>illustrative&lt;/strong> example (order-of-magnitude figures) on the same
8×H100 node:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Candidate&lt;/th>
&lt;th>CPM (€/1M)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;th>Energy (J/tok)&lt;/th>
&lt;th>Carbon (gCO₂/1M)&lt;/th>
&lt;th>Sovereignty&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A · 70B FP16, on-prem France&lt;/td>
&lt;td>~1.09&lt;/td>
&lt;td>~2,800&lt;/td>
&lt;td>~2.0&lt;/td>
&lt;td>low (grid ~40 gCO₂/kWh)&lt;/td>
&lt;td>EU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B · 70B FP8, on-prem France&lt;/td>
&lt;td>~0.75&lt;/td>
&lt;td>~4,000&lt;/td>
&lt;td>~1.4&lt;/td>
&lt;td>lower&lt;/td>
&lt;td>EU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C · Scaleway on-demand&lt;/td>
&lt;td>~2.17&lt;/td>
&lt;td>~2,800&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>low (FR)&lt;/td>
&lt;td>EU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>D · US hyperscaler&lt;/td>
&lt;td>~3.0&lt;/td>
&lt;td>~2,800&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>depends on region&lt;/td>
&lt;td>not EU&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reading: &lt;strong>B dominates A&lt;/strong> on the three intrinsic axes thanks to FP8. C (European cloud) is
more expensive per token but shifts capex and operations, while keeping sovereignty. D
(hyperscaler) may be competitive on cost but &lt;strong>loses sovereignty&lt;/strong>, an axis that for
GDPR data is not negotiable. No isolated number decides: the choice comes from &lt;strong>seeing
the whole row&lt;/strong>.&lt;/p>
&lt;p>The trap the board avoids: &lt;strong>optimising a single axis&lt;/strong>. Each cell will be filled in by an
article in the series with measured data.&lt;/p>
&lt;hr>
&lt;h2 id="where-each-axis-is-measured-from-design-to-production">Where each axis is measured: from design to production&lt;/h2>
&lt;p>Each axis is measured at three different moments, with different tools and increasing
precision. Confusing them is a common source of figures that do not add up:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>Design (estimate)&lt;/th>
&lt;th>Benchmark (controlled)&lt;/th>
&lt;th>Production (continuous)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cost&lt;/td>
&lt;td>datasheet, GPU-hour price&lt;/td>
&lt;td>CPM measured over synthetic load&lt;/td>
&lt;td>OpenCost/Kubecost per team and token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Performance&lt;/td>
&lt;td>theoretical throughput (roofline)&lt;/td>
&lt;td>GuideLLM/AIPerf with SLO&lt;/td>
&lt;td>OTel/Langfuse over real traffic&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>TDP × hours&lt;/td>
&lt;td>Zeus/MLPerf Power on the bench&lt;/td>
&lt;td>Kepler/DCGM per pod in the cluster&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The rule: a design figure serves for sizing, but not for defending a
proposal; for that you need a reproducible benchmark figure, and to operate, a
continuous production one. When someone disputes a number in the proposal, the answer is not
&amp;ldquo;a blog says so&amp;rdquo;, but &amp;ldquo;here is the bench, here is the methodology, reproduce it&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h2 id="glossary-of-units-and-conversions">Glossary of units and conversions&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quantity&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Conversion / reference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Energy&lt;/td>
&lt;td>joule (J), watt-hour (Wh)&lt;/td>
&lt;td>1 Wh = 3,600 J; 1 kWh = 3.6 MJ = 3,600,000 J&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power&lt;/td>
&lt;td>watt (W)&lt;/td>
&lt;td>1 W = 1 J/s; H100 ≈ 700 W TDP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy per token&lt;/td>
&lt;td>J/token&lt;/td>
&lt;td>= average power (W) ÷ throughput (tok/s)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput&lt;/td>
&lt;td>tok/s&lt;/td>
&lt;td>tokens generated per second by the system&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per token&lt;/td>
&lt;td>€/1M tok (CPM)&lt;/td>
&lt;td>= cost/hour ÷ (tok/s × 3,600 / 10⁶)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Latency&lt;/td>
&lt;td>ms&lt;/td>
&lt;td>TTFT (first token), TPOT/ITL (between tokens)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PUE&lt;/td>
&lt;td>ratio (≥1)&lt;/td>
&lt;td>total DC energy ÷ compute energy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Carbon&lt;/td>
&lt;td>gCO₂eq&lt;/td>
&lt;td>= energy (kWh) × PUE × grid intensity (gCO₂/kWh)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A useful conversion to keep in your head: at a constant 700 W, an H100 consumes ~0.7 kWh per hour, ~16.8
kWh a day and ~6,130 kWh a year (board only, no PUE). Multiplied by the PUE and your country&amp;rsquo;s
energy price (FR ~0.058, ES ~0.054, DE ~0.091 €/kWh wholesale), that number is the basis
of the electricity portion of the TCO.&lt;/p>
&lt;hr>
&lt;h2 id="how-the-rest-of-the-series-fits-together">How the rest of the series fits together&lt;/h2>
&lt;p>The 28 articles group into four tracks; this is the map:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Track&lt;/th>
&lt;th>What it contributes&lt;/th>
&lt;th>Closes with&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>FinOps (8)&lt;/td>
&lt;td>measuring and allocating cost, TCO model&lt;/td>
&lt;td>cost/token comparable on-prem vs European cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM benchmarking (8)&lt;/td>
&lt;td>measuring performance reproducibly&lt;/td>
&lt;td>Pareto frontier of engines&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy (8)&lt;/td>
&lt;td>measuring watts and carbon per token&lt;/td>
&lt;td>energy in the TCO and in European regulation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Synthesis (4)&lt;/td>
&lt;td>crossing the three axes&lt;/td>
&lt;td>scorecard, on-prem vs cloud, sizing, harness&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The three articles that follow this one in the batch introduce each axis in depth:
their frameworks and the state of the art. From there, each article adds a layer of data
to the final scorecard.&lt;/p>
&lt;p>The thread running through all 28 instalments is the one in this article: &lt;strong>cost, performance and energy
are a single problem with three faces, tied together by throughput and by the energy that lives
inside the cost, and in Europe, by the country where the cluster sits&lt;/strong>. Anyone who optimises one
face while ignoring the others will end up with a pretty number and a platform that does not hold up.
The work of the series is to produce the figures, measured, dated, European and reproducible,
that allow a sovereign architecture to be defended before any committee with the only answer
that closes a technical debate: the data and the bench to reproduce 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-serving-engines-pareto-frontier-comparison/">Comparison of serving engines (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — the synthesis of the performance axis: which engine wins at each point of the goodput-latency frontier with measured data.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/">LLM quality benchmarks: MMLU, MT-Bench, HELM and the leaderboards that matter&lt;/a> — the quality dimension that complements performance: how public leaderboards let you calibrate the right model for each SLO.&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> — the synthesis of the energy axis: the J/token rankings and how to read them without being fooled by the benchmark&amp;rsquo;s setup.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/cloud-gpu-on-demand-reserved-spot-neoclouds-2026-prices/">Cloud GPU: comparison of prices, commitment and sovereign neoclouds&lt;/a> — the cost axis piece for cloud or hybrid architectures: on-demand, spot and reserved prices from European providers in 2026.&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 the on-premise GPU cluster: amortisation, energy and infrastructure&lt;/a> — the cost axis piece for your own architectures: the real €/GPU-hour with CAPEX, energy and operations broken down.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Measurement bias and reproducibility in LLM benchmarks&lt;/a> — the methodological traps that make two benchmarks of the same engine give incompatible results: the basis for trusting this series&amp;rsquo; data.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/watt-to-carbon-pue-grid-intensity-gco2-per-token/">From the watt to carbon: PUE, grid intensity and the real cost of a token&lt;/a> — how to convert the GPU&amp;rsquo;s watts into gCO₂eq and into electricity euros, closing the loop between the energy axis and the cost axis.&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 move all three axes at once: quantisation, batching and engine as levers that reduce J/token, €/token and improve goodput simultaneously.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/reproducible-harness-cost-performance-energy-experiment/">The reproducible harness for measuring cost, performance and energy&lt;/a> — the test bench that makes this article&amp;rsquo;s methodology concrete: how to produce the data for the three axes with a single reproducible experiment.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Scaleway · H100 GPU instance (€/h price, EU sovereignty) — &lt;a href="https://www.scaleway.com/en/h100/">https://www.scaleway.com/en/h100/&lt;/a>&lt;/li>
&lt;li>DeployBase · OVHcloud GPU Pricing (European sovereignty) — &lt;a href="https://deploybase.ai/articles/ovhcloud-gpu-pricing">https://deploybase.ai/articles/ovhcloud-gpu-pricing&lt;/a>&lt;/li>
&lt;li>TradingEconomics · electricity price France / Germany / Spain — &lt;a href="https://tradingeconomics.com/france/electricity-price">https://tradingeconomics.com/france/electricity-price&lt;/a>&lt;/li>
&lt;li>EEA · emission intensity of electricity in Europe — &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 (carbon ~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;li>Spheron · AI Inference Power &amp;amp; Cost Economics (2026) — &lt;a href="https://www.spheron.network/blog/ai-inference-cost-economics-2026/">https://www.spheron.network/blog/ai-inference-cost-economics-2026/&lt;/a>&lt;/li>
&lt;li>NVIDIA · H100 (TDP/specs) — &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;/ul></description></item><item><title>LLM inference benchmarking: frameworks, metrics and the state of the art (tool by tool)</title><link>https://blog.lo0.es/en/posts/llm-inference-benchmarking-frameworks-metrics-state-of-the-art/</link><pubDate>Sat, 13 Jun 2026 02:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-inference-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. Performance is not very
sensitive to the country, but its associated cost (cost per token) is expressed in € and
links back to the &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">opening article&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-introduction-covers">What this introduction covers&lt;/h2>
&lt;p>Third article in the data series, a deep dive into the performance axis. Measuring the
performance of an inference engine looks trivial (&amp;ldquo;how many tokens per second?&amp;rdquo;) and it is
exactly where people fool themselves most: two tools can report results that differ by a
factor of 7 for the same system. This article inventories the metrics that matter and &lt;strong>how
they are defined&lt;/strong>, why the architecture of the tool biases the figure, how the saturation
point is found with a concurrency &lt;em>sweep&lt;/em>, and the profile of each framework. No
recommendations: the choice of engine is settled in the Pareto article (B8); here there are
only the facts and the methodology, because &lt;strong>a benchmark without a published methodology is
not comparable&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="the-performance-metrics">The performance metrics&lt;/h2>
&lt;p>There is no single performance metric, there are five, and mixing them is the first source
of error:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Unit&lt;/th>
&lt;th>Dominant phase&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>TTFT&lt;/strong> (Time To First Token)&lt;/td>
&lt;td>time from sending the prompt to the first token&lt;/td>
&lt;td>ms&lt;/td>
&lt;td>prefill&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPOT / ITL&lt;/strong> (Time Per Output Token / Inter-Token Latency)&lt;/td>
&lt;td>average time between output tokens once generation has started&lt;/td>
&lt;td>ms/token&lt;/td>
&lt;td>decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Request throughput&lt;/strong>&lt;/td>
&lt;td>complete request-response cycles per second at the concurrency tested&lt;/td>
&lt;td>req/s&lt;/td>
&lt;td>both&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Token throughput&lt;/strong>&lt;/td>
&lt;td>total tokens (input + output) per second across all concurrent requests&lt;/td>
&lt;td>tok/s&lt;/td>
&lt;td>both&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Goodput&lt;/strong>&lt;/td>
&lt;td>share of requests that &lt;strong>meet the defined SLO&lt;/strong>&lt;/td>
&lt;td>useful tok/s&lt;/td>
&lt;td>both&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>P50 / P95 / P99&lt;/strong>&lt;/td>
&lt;td>latency percentiles (not the mean)&lt;/td>
&lt;td>ms&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Precise definitions, because every tool computes them its own way (&lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">Anyscale · latency and
throughput metrics&lt;/a>): TTFT is
what a user waits before seeing the first character, dominated by prefill compute; ITL is
the average time between successive output tokens and sets the perceived &amp;ldquo;typing speed&amp;rdquo; of
the answer; request throughput is complete cycles per second; token throughput is
total tokens (input plus output) per second across all concurrent requests.&lt;/p>
&lt;h3 id="the-latency-decomposition">The latency decomposition&lt;/h3>
&lt;p>The total latency of a request with \(N\) output tokens decomposes as:&lt;/p>
$$\text{latency} \approx \text{TTFT} + (N-1)\times \text{TPOT}$$
&lt;p>That is why TTFT and TPOT are reported separately: a single mean latency hides very
different profiles. A system with high TTFT and low TPOT (expensive prefill, fast decode) and
another one the other way round can have the same mean latency for one particular length, yet
behave in opposite ways when the response size changes. For an interactive chat experience
TTFT and TPOT rule; for a batch of long summaries, token throughput does. Measuring the mean
hides both realities.&lt;/p>
&lt;h3 id="goodput-the-honest-metric">Goodput: the honest metric&lt;/h3>
&lt;p>Raw throughput (TPS, RPS) says how much work the system does; &lt;strong>goodput&lt;/strong> says how much
of that work meets your quality-of-service standards (SLO)
(&lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">Anyscale&lt;/a>). An engine can boast
10,000 aggregate tok/s, but if half the requests violate the P99 TTFT SLO, its goodput is
5,000. The figure you defend in a proposal is the &lt;strong>goodput&lt;/strong>, not the catalogue throughput:
it is the only one that translates into satisfied users and an honest cost per token.&lt;/p>
&lt;hr>
&lt;h2 id="how-each-metric-is-instrumented-and-where-the-error-creeps-in">How each metric is instrumented (and where the error creeps in)&lt;/h2>
&lt;p>Before comparing numbers it helps to know where each clock starts and stops, because two
tools can call different things &amp;ldquo;TTFT&amp;rdquo;:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Client-side versus server-side TTFT.&lt;/strong> The TTFT measured by the client includes network
latency and gateway queueing; the one measured by the server does not. For an engine
comparison the server-side figure is the one that matters; for user experience, the
client-side one. Mixing the two invalidates the comparison.&lt;/li>
&lt;li>&lt;strong>Streaming is required.&lt;/strong> TTFT and ITL can only be measured if the response arrives as a
&lt;em>stream&lt;/em> (token by token). If the tool measures complete responses, there is no real TTFT:
there is total latency in disguise.&lt;/li>
&lt;li>&lt;strong>Token counting.&lt;/strong> Throughput in tok/s depends on which tokenizer counts the tokens.
If the tool uses a tokenizer different from the model&amp;rsquo;s, the token count (and therefore the
tok/s and the cost per token) is biased. You have to count with the tokenizer of the model
being served.&lt;/li>
&lt;li>&lt;strong>Warm-up and prefix cache.&lt;/strong> The first requests of a benchmark benefit from a warm prefix
cache and give artificially low TTFT; the warm-up has to be discarded or the result
inflates reality.&lt;/li>
&lt;/ul>
&lt;p>These four instrumentation decisions explain a good share of the discrepancies between tools.
A performance figure that does not say where the clock is measured and with which tokenizer
is not comparable, however precise it may look.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture-of-the-tool-biases-the-figure">The architecture of the tool biases the figure&lt;/h2>
&lt;p>Here is the trap that invalidates half the published benchmarks. Tools split into two classes
by the architecture of the client that generates the load, and that architecture
determines whether the measurement is reliable at high concurrency:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Single-process micro-bench&lt;/strong> (vLLM bench, SGLang bench, genai-perf): a Python client with
asyncio in a single process. Useful for quick experiments on one engine, but the
single-process architecture introduces a bottleneck on the client side that &lt;strong>biases the
data at high concurrency&lt;/strong> (&lt;a href="https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e">genAI-perf and vLLM&lt;/a>):
the client cannot generate enough load and you measure the limit of the client, not the
limit of the engine.&lt;/li>
&lt;li>&lt;strong>Multi-process load&lt;/strong> (GuideLLM, AIPerf): they spread load generation across several
processes, avoiding that limit. This is the class that has emerged for measuring at real
scale.&lt;/li>
&lt;/ul>
&lt;p>The size of the bias is enormous: at 1,000 QPS, a single-process benchmark processed
75,574 tokens against the 545,733 tokens of a distributed architecture, a
discrepancy of 7.2× in measurement capacity for the same system ([search]). Anyone
comparing two engines with tools from different classes is not comparing the engines: they
are comparing the benchmark clients.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 210" role="img" aria-label="Two classes of benchmark tool: a single-process client that saturates at high concurrency versus a multi-process load generator that measures the engine" 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(#bm)}&lt;/style>
&lt;defs>&lt;marker id="bm" 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">Single-process (vLLM bench, SGLang bench, genai-perf)&lt;/text>
&lt;rect class="bx" x="20" y="36" width="150" height="40" rx="6"/>
&lt;text x="32" y="62" class="ts">1 asyncio client&lt;/text>
&lt;path class="ar" d="M170,58 L215,58"/>
&lt;rect class="dsh" x="215" y="36" width="120" height="40" rx="6"/>
&lt;text x="227" y="56" class="ts">client bottleneck&lt;/text>
&lt;text x="227" y="71" class="ts">bias at high conc.&lt;/text>
&lt;path class="ar" d="M335,58 L380,58"/>
&lt;rect class="bx" x="380" y="36" width="110" height="40" rx="6"/>
&lt;text x="392" y="62" class="ts">engine (vLLM…)&lt;/text>
&lt;text x="510" y="52" class="ts">you measure the client,&lt;/text>
&lt;text x="510" y="68" class="ts">not the engine&lt;/text>
&lt;text x="20" y="118" class="tl">Multi-process (GuideLLM, AIPerf)&lt;/text>
&lt;rect class="bx" x="20" y="128" width="150" height="46" rx="6"/>
&lt;text x="32" y="148" class="ts">N load processes&lt;/text>
&lt;text x="32" y="164" class="ts">(real load)&lt;/text>
&lt;path class="ar" d="M170,151 L380,151"/>
&lt;rect class="bx" x="380" y="131" width="110" height="40" rx="6"/>
&lt;text x="392" y="155" class="ts">engine (vLLM…)&lt;/text>
&lt;text x="510" y="145" class="ts">you measure the engine;&lt;/text>
&lt;text x="510" y="161" class="ts">7.2× more capacity&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="frameworks-one-by-one">Frameworks, one by one&lt;/h2>
&lt;h3 id="vllm-bench-and-sglang-bench--engine-micro-bench">vLLM bench and SGLang bench — engine micro-bench&lt;/h3>
&lt;p>What they measure: TTFT, TPOT and throughput of the engine itself (vLLM or SGLang). Class:
single-process micro-bench. Use: quick experiments to tune one engine and see the effect of
its optimisations (see &lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">decode&lt;/a> and
&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">prefill&lt;/a>). Limit: they saturate on the client at
high concurrency; they are no good for measuring real capacity at scale.&lt;/p>
&lt;h3 id="aiperf--the-nvidia-one-formerly-genai-perf-multi-process">AIPerf — the NVIDIA one (formerly genai-perf), multi-process&lt;/h3>
&lt;p>What it measures: TTFT, ITL, throughput and latency against &lt;strong>vLLM, NIM, TGI and any
compatible endpoint&lt;/strong>. Class: multi-process load. State-of-the-art datum: NVIDIA &lt;strong>retired
genai-perf and replaced it with AIPerf on 15 April 2026&lt;/strong>. During the &lt;em>sweep&lt;/em>, AIPerf
detects GPU saturation and identifies the previous iteration, returning it as
&lt;code>estimatedCapacity&lt;/code>; if it detects no saturation, &lt;code>estimatedCapacity&lt;/code> is the last iteration
tested, which is why the sweep has to extend beyond the knee
(&lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">AIPerf&lt;/a>).&lt;/p>
&lt;h3 id="guidellm--from-the-vllm-project-slo-oriented">GuideLLM — from the vLLM project, SLO-oriented&lt;/h3>
&lt;p>What it measures: full distributions of TTFT, ITL and end-to-end behaviour, for
SLO-driven evaluation. Class: multi-process load. Differentiator: it generates realistic,
configurable traffic patterns in synchronous, concurrent and rate-based modes, including
reproducible sweeps to identify safe operating ranges
(&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat · GuideLLM&lt;/a>,
&lt;a href="https://github.com/vllm-project/guidellm">GuideLLM · GitHub&lt;/a>). It is the tool for answering
&amp;ldquo;how hard can I load this engine without breaking the SLO?&amp;rdquo;.&lt;/p>
&lt;h3 id="llmperf--the-anyscaleray-classic">LLMPerf — the Anyscale/Ray classic&lt;/h3>
&lt;p>What it measures: throughput and latency at the inference level. Use: endpoint validation,
historically very widespread. Class: load generator. Limit: less focused on distributions and
sweeps than GuideLLM/AIPerf.&lt;/p>
&lt;h3 id="mlperf-inference--the-industry-standard">MLPerf Inference — the industry standard&lt;/h3>
&lt;p>What it measures: performance under standardised scenarios with strict rules, for
comparability across vendors. Maintainer: &lt;strong>MLCommons&lt;/strong>. It is the gold standard of
cross-vendor comparability; it is expanded on below.&lt;/p>
&lt;h3 id="comparison-table">Comparison table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Class&lt;/th>
&lt;th>What it measures&lt;/th>
&lt;th>Maintainer&lt;/th>
&lt;th>When to use it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM bench&lt;/strong>&lt;/td>
&lt;td>single-process micro&lt;/td>
&lt;td>TTFT, TPOT, throughput of vLLM&lt;/td>
&lt;td>vLLM (OSS)&lt;/td>
&lt;td>tune vLLM, quick experiments&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang bench&lt;/strong>&lt;/td>
&lt;td>single-process micro&lt;/td>
&lt;td>metrics of the SGLang engine&lt;/td>
&lt;td>SGLang (OSS)&lt;/td>
&lt;td>tune SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AIPerf&lt;/strong> (formerly genai-perf)&lt;/td>
&lt;td>multi-process load&lt;/td>
&lt;td>TTFT, ITL, throughput; estimatedCapacity&lt;/td>
&lt;td>NVIDIA (OSS)&lt;/td>
&lt;td>real capacity, multi-endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>multi-process load&lt;/td>
&lt;td>distributions, SLO, sweeps&lt;/td>
&lt;td>vLLM (OSS)&lt;/td>
&lt;td>validate SLO, find the knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLMPerf&lt;/strong>&lt;/td>
&lt;td>load&lt;/td>
&lt;td>throughput and latency&lt;/td>
&lt;td>Anyscale/Ray (OSS)&lt;/td>
&lt;td>endpoint validation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLPerf Inference&lt;/strong>&lt;/td>
&lt;td>standard suite&lt;/td>
&lt;td>server/offline/interactive scenarios&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="the-concurrency-sweep-finding-the-knee">The concurrency sweep: finding the knee&lt;/h2>
&lt;p>The most useful measurement for sizing is not a number, it is a curve: how latency and
throughput change as concurrency rises. Raising concurrency keeps the GPU busier and raises
RPS, but past a certain point it sends TTFT, ITL and end-to-end latency through the roof
([search]). The goal of the &lt;em>sweep&lt;/em> is to find the &lt;strong>knee&lt;/strong>: the maximum concurrency where
throughput still rises without latency breaking the SLO.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 260" role="img" aria-label="Concurrency sweep curve: throughput rises and saturates while latency spikes beyond the knee, which marks the safe capacity under SLO" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.cv{fill:none;stroke:currentColor;stroke-width:1.6}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="40" x2="60" y2="200"/>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;text x="20" y="120" class="ts" transform="rotate(-90 20 120)">metric&lt;/text>
&lt;text x="360" y="228" class="ts">concurrency →&lt;/text>
&lt;path class="cv" d="M60,190 C200,150 300,120 400,112 C520,104 620,100 700,98"/>
&lt;text x="600" y="92" class="ts">throughput (saturates)&lt;/text>
&lt;path class="cv" d="M60,180 C260,176 360,170 430,150 C520,120 600,70 700,46"/>
&lt;text x="600" y="40" class="ts">latency (spikes)&lt;/text>
&lt;line class="dsh" x1="430" y1="40" x2="430" y2="200"/>
&lt;text x="392" y="56" class="tl">knee&lt;/text>
&lt;text x="362" y="216" class="ts">safe capacity under SLO&lt;/text>
&lt;text x="60" y="250" class="ts">Before the knee, raising concurrency gives more throughput for free. After it, latency breaks the SLO with barely any gain.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>That is why AIPerf extends the sweep beyond the knee: only by seeing where latency takes
off can it return the safe capacity (&lt;code>estimatedCapacity&lt;/code>). This curve is the raw material of
&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a>: the knee
yields the replica count and the cost per token at the target load.&lt;/p>
&lt;hr>
&lt;h3 id="worked-example-reading-a-sweep">Worked example: reading a sweep&lt;/h3>
&lt;p>An illustrative sweep on an example node (a 70B on 8×H100, SLO of P99 TTFT &amp;lt; 500 ms), to
see how the knee is read:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrency&lt;/th>
&lt;th>RPS&lt;/th>
&lt;th>TTFT P50 (ms)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>Token tput (tok/s)&lt;/th>
&lt;th>Goodput&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2&lt;/td>
&lt;td>80&lt;/td>
&lt;td>110&lt;/td>
&lt;td>350&lt;/td>
&lt;td>100 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>14&lt;/td>
&lt;td>110&lt;/td>
&lt;td>240&lt;/td>
&lt;td>2,100&lt;/td>
&lt;td>100 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>22&lt;/td>
&lt;td>180&lt;/td>
&lt;td>460&lt;/td>
&lt;td>3,400&lt;/td>
&lt;td>98 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>24&lt;/td>
&lt;td>26&lt;/td>
&lt;td>320&lt;/td>
&lt;td>980&lt;/td>
&lt;td>3,900&lt;/td>
&lt;td>62 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>27&lt;/td>
&lt;td>540&lt;/td>
&lt;td>1,800&lt;/td>
&lt;td>4,000&lt;/td>
&lt;td>20 %&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Reading: up to concurrency ~16 throughput grows and P99 stays under the SLO (goodput
~100 %). Between 16 and 24 sits the &lt;strong>knee&lt;/strong>: throughput barely rises any more (3,400 →
3,900 tok/s) but P99 takes off (460 → 980 ms) and goodput collapses (98 % → 62 %). At
concurrency 32 raw throughput is at its maximum (4,000 tok/s) but goodput is 20 %: the
system &amp;ldquo;performs well&amp;rdquo; while serving mostly requests that violate the SLO. The defensible
safe capacity is the one at concurrency ~16, not the one at maximum throughput. This is the
number that goes into &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a>
and into cost per token: at 3,400 useful tok/s, not 4,000 raw tok/s.&lt;/p>
&lt;hr>
&lt;h2 id="mlperf-inference-the-comparability-standard">MLPerf Inference: the comparability standard&lt;/h2>
&lt;p>To compare across vendors and engines with identical rules there is MLPerf Inference
(MLCommons). The datacenter category focuses on two scenarios plus an optional one
(&lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">MLCommons · datacenter&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scenario&lt;/th>
&lt;th>What it simulates&lt;/th>
&lt;th>Metric&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Offline&lt;/strong>&lt;/td>
&lt;td>raw throughput processing the whole dataset in batch&lt;/td>
&lt;td>maximum throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Server&lt;/strong>&lt;/td>
&lt;td>interactive environment: requests one at a time following Poisson at a mean RPS&lt;/td>
&lt;td>RPS under TTFT and TPOT limits&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Interactive&lt;/strong> (optional)&lt;/td>
&lt;td>like server but with &lt;strong>stricter latency limits&lt;/strong>&lt;/td>
&lt;td>RPS under a hard SLO&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The Server scenario is the realistic one for online inference: the load generator sends
requests following a Poisson distribution and demands that specific TTFT and TPOT bounds
be met. &lt;strong>MLPerf Inference v5.0&lt;/strong> (April 2025) introduced a large-scale 405B benchmark
and a low-latency interactive 70B one, offering language benchmarks at every scale (7B to
405B), architectural diversity (including MoE) and scenarios
(&lt;a href="https://mlcommons.org/2025/04/llm-inference-v5/">MLCommons · v5.0&lt;/a>); &lt;strong>v5.1&lt;/strong> (September
2025) widened the results with record participation
(&lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">MLCommons · v5.1&lt;/a>).&lt;/p>
&lt;p>The value of MLPerf is comparability: everyone measures the same thing under the same
rules. Its limit is that those rules may not match your workload (your length distribution,
your specific SLO), so it serves to compare hardware and engines against each other, not
necessarily to size your case. For that, your own sweep.&lt;/p>
&lt;hr>
&lt;h2 id="measurement-bias-and-reproducibility">Measurement bias and reproducibility&lt;/h2>
&lt;p>That two benchmarks give very different results for the same system is no accident:
systematic measurement bias in production benchmarks is characterised in the literature
(&lt;a href="https://arxiv.org/html/2605.24217">arXiv 2605.24217&lt;/a>), and there is work dedicated to the
meta-metrics and good practices of system-level performance benchmarking
(&lt;a href="https://arxiv.org/pdf/2508.10251">arXiv 2508.10251&lt;/a>). The most common sources of bias:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source of bias&lt;/th>
&lt;th>Effect&lt;/th>
&lt;th>Mitigation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single-process client&lt;/td>
&lt;td>underestimates throughput at high concurrency&lt;/td>
&lt;td>use multi-process load&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Unrealistic length distribution&lt;/td>
&lt;td>results that do not apply to your traffic&lt;/td>
&lt;td>use realistic traces&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Measuring the mean instead of percentiles&lt;/td>
&lt;td>hides the latency tail&lt;/td>
&lt;td>report P95/P99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Uncontrolled warm-up&lt;/td>
&lt;td>the prefix cache inflates the first results&lt;/td>
&lt;td>discard the warm-up&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Not pinning engine/model version&lt;/td>
&lt;td>irreproducible&lt;/td>
&lt;td>pin everything and publish it&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The methodological conclusion of this article: &lt;strong>a benchmark without a published methodology
is not comparable&lt;/strong>. For a performance figure to support a proposal it has to come with the
tool, its version, the model and precision, the load distribution and the SLO. Synthesis
article S4 builds a reproducible harness that pins all of that.&lt;/p>
&lt;hr>
&lt;h2 id="checklist-for-a-reproducible-benchmark">Checklist for a reproducible benchmark&lt;/h2>
&lt;p>For a performance figure to be defensible before a committee, it has to come with everything
that allows it to be reproduced. The minimum published alongside the result:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>What to pin&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Tool + version&lt;/td>
&lt;td>each one measures differently; the version changes behaviour&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model + precision (FP16/FP8/INT4)&lt;/td>
&lt;td>precision changes throughput and quality&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hardware (GPU, count, interconnect)&lt;/td>
&lt;td>an 8×H100 NVLink is not an 8×H100 PCIe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Engine + version + flags&lt;/td>
&lt;td>vLLM/SGLang/TRT-LLM and their configuration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Length distribution (in/out)&lt;/td>
&lt;td>real traffic is not fixed length&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Concurrency levels of the sweep&lt;/td>
&lt;td>you have to go past the knee&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLO (which percentile, which threshold)&lt;/td>
&lt;td>it defines the goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Warm-up handling&lt;/td>
&lt;td>discard it or it biases the result&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenizer used for counting&lt;/td>
&lt;td>it affects tok/s and cost per token&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The practical rule: if you cannot hand over this table alongside the figure, the figure is
not a datum, it is an anecdote. The reproducible harness of article S4 automates the recording
of all these parameters so that anyone, including whoever challenges the proposal, can
reproduce the exact number.&lt;/p>
&lt;hr>
&lt;h2 id="performance--quality">Performance ≠ quality&lt;/h2>
&lt;p>One warning that avoids the most expensive mistake: these tools measure &lt;strong>speed and
throughput, not accuracy&lt;/strong>. An engine can be blisteringly fast while serving a model that
answers badly. Quality is measured with another family of tools
(&lt;strong>lm-evaluation-harness&lt;/strong>, &lt;strong>HELM&lt;/strong>, task &lt;em>leaderboards&lt;/em>) which is another axis of the
scorecard (article B7). Confusing &amp;ldquo;fast&amp;rdquo; with &amp;ldquo;good&amp;rdquo; is how you build a platform that serves
bad answers very quickly. On the final Pareto frontier, performance and quality are two
distinct axes that must be looked at together, never one instead of the other.&lt;/p>
&lt;p>The other family of tools, for reference (expanded in B7):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>What it measures&lt;/th>
&lt;th>Common trap&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>lm-evaluation-harness&lt;/strong>&lt;/td>
&lt;td>accuracy on hundreds of standardised tasks&lt;/td>
&lt;td>contamination of the test dataset&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HELM&lt;/strong>&lt;/td>
&lt;td>holistic evaluation (accuracy, robustness, bias, efficiency)&lt;/td>
&lt;td>heavy to run&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LiveBench / dynamic leaderboards&lt;/strong>&lt;/td>
&lt;td>tasks that rotate to avoid contamination&lt;/td>
&lt;td>comparability over time&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The data point: contamination, the model having seen the test during training, inflates
quality metrics just as warm-up inflates performance ones. That is why dynamic leaderboards
rotate their questions. Quality and performance share that lesson: the measurement method
biases the result as much as the system being measured.&lt;/p>
&lt;hr>
&lt;h2 id="the-latency-versus-throughput-trade-off">The latency versus throughput trade-off&lt;/h2>
&lt;p>A property the sweep reveals and which is worth keeping in mind: &lt;strong>latency and throughput
pull in opposite directions&lt;/strong>. Batching groups requests to amortise the cost of moving the
weights out of VRAM, which raises throughput, but each request waits for the batch to form,
which raises individual latency. There are two operating regimes, and the benchmark
serves to place you in the right one:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Regime&lt;/th>
&lt;th>Optimises&lt;/th>
&lt;th>Configuration&lt;/th>
&lt;th>Case&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Latency&lt;/strong>&lt;/td>
&lt;td>low TTFT/TPOT&lt;/td>
&lt;td>small batch, low concurrency&lt;/td>
&lt;td>interactive chat, copilots&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Throughput&lt;/strong>&lt;/td>
&lt;td>maximum tok/s&lt;/td>
&lt;td>large batch, high concurrency&lt;/td>
&lt;td>overnight batch, ingestion&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is no single &amp;ldquo;best&amp;rdquo; point: there is the best point for your SLO. A benchmark that
reports only maximum throughput is describing the throughput regime and ignoring whether that
point meets the latency your case needs. That is why &lt;strong>goodput&lt;/strong>, throughput under the SLO,
is the metric that reconciles the two regimes: it measures how much throughput you get
without stepping outside acceptable latency. The sweep traverses the curve between both
regimes; your SLO marks where on that curve your system sits.&lt;/p>
&lt;hr>
&lt;h2 id="the-connection-with-cost-and-energy">The connection with cost and energy&lt;/h2>
&lt;p>Performance is not an isolated axis: by the identity of the &lt;a href="https://blog.lo0.es/en/posts/cost-performance-energy-three-axes-llm-inference/">opening article&lt;/a>,
throughput is the denominator of cost per token and of energy per token. A sweep that finds a
knee at 4,000 tok/s rather than 2,800 is not just &amp;ldquo;faster&amp;rdquo;: it lowers the CPM from ~1.09 to
~0.76 €/1M tok and the energy per token in the same proportion, on the same iron. That is
why performance benchmarking is the tool that, indirectly, moves cost the most: every goodput
improvement translates into euros and into watts per token. The number that connects the
three axes is &lt;strong>goodput&lt;/strong>, the throughput that meets the SLO, not catalogue throughput.&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>genai-perf → AIPerf migration&lt;/strong> (15 Apr 2026): NVIDIA consolidates its benchmarking into
a multi-process tool with saturation detection.&lt;/li>
&lt;li>&lt;strong>GuideLLM&lt;/strong> as the OSS standard for SLO-driven evaluation with reproducible sweeps.&lt;/li>
&lt;li>&lt;strong>MLPerf Inference v5.0/v5.1&lt;/strong> extends to 405B, interactive 70B and MoE, with record
participation: cross-vendor comparability is maturing.&lt;/li>
&lt;li>&lt;strong>Measurement bias characterised&lt;/strong>: the community accepts that the measurement method
matters as much as the system measured; the emphasis on reproducibility and meta-metrics is
growing.&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>Comparing tools from different classes.&lt;/strong> A single-process micro-bench and a
multi-process load generator are not comparable; the difference can be 7×. Pin the class.&lt;/li>
&lt;li>&lt;strong>Catalogue throughput instead of goodput.&lt;/strong> The honest number is the one that meets the
SLO.&lt;/li>
&lt;li>&lt;strong>Means instead of percentiles.&lt;/strong> The mean hides the tail; report P95/P99.&lt;/li>
&lt;li>&lt;strong>Not extending the sweep beyond the knee.&lt;/strong> Without seeing where latency takes off you do
not know the safe capacity.&lt;/li>
&lt;li>&lt;strong>Confusing performance with quality.&lt;/strong> They are distinct axes; fast is not good.&lt;/li>
&lt;li>&lt;strong>Not pinning versions.&lt;/strong> Engine, model, precision and load left unpinned = irreproducible
= not defensible.&lt;/li>
&lt;/ol>
&lt;p>The next article in the track (B2) goes into the tool catalogue in depth; this one pins the
metrics and the methodology. With performance measured reproducibly, the scorecard can cross
it with cost (in €) and energy for the final decision.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>Performance benchmarking looks like the most &amp;ldquo;objective&amp;rdquo; of the three axes (they are tokens
per second, after all) and it is exactly where the most manipulation happens, almost always
without bad intent: a single-process tool here, a mean instead of a P99 there, a catalogue
throughput instead of the goodput. The difference between a marketing number and a defensible
datum lies not in the engine measured but in the &lt;strong>methodology&lt;/strong>: the class of tool, where
the clock is measured, with which tokenizer, how far the sweep goes and which SLO defines the
goodput. For a sovereign architecture proposal, performance only counts if it is delivered
with that reproducibility record, and, crossed with cost in euros and energy per token, it
becomes the column of the Pareto frontier that decides which engine and which configuration
hold up the platform. The number you defend is not the highest one: it is the &lt;strong>reproducible
goodput&lt;/strong>.&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-serving-engines-pareto-frontier-comparison/">LLM serving engines compared (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — choosing the engine after measuring goodput: from the methodology of this article to the Pareto frontier.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlperf-inference-reading-loadgen-comparability/">MLPerf Inference: how to read it and what comparability it offers&lt;/a> — the industry standard: the four scenarios, the TTFT/TPOT SLOs of the Server scenario and the rules for comparing results.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/measurement-bias-reproducibility-llm-benchmarks/">Measurement bias and reproducibility&lt;/a> — sources of systematic bias in performance benchmarks and how to control them to obtain defensible data.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>Anyscale · métricas de latencia y throughput de LLM — &lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">https://docs.anyscale.com/llm/serving/benchmarking/metrics&lt;/a>&lt;/li>
&lt;li>Red Hat · 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>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>NVIDIA AIPerf · guía de benchmarking (ex genai-perf) — &lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference datacenter (escenarios) — &lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">https://mlcommons.org/benchmarks/inference-datacenter/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.0 (405B + 70B interactivo) — &lt;a href="https://mlcommons.org/2025/04/llm-inference-v5/">https://mlcommons.org/2025/04/llm-inference-v5/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 — &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>arXiv 2605.24217 · sesgo sistemático de medición en benchmarks de inferencia LLM — &lt;a href="https://arxiv.org/html/2605.24217">https://arxiv.org/html/2605.24217&lt;/a>&lt;/li>
&lt;li>arXiv 2508.10251 · meta-métricas y buenas prácticas de benchmarking de rendimiento — &lt;a href="https://arxiv.org/pdf/2508.10251">https://arxiv.org/pdf/2508.10251&lt;/a>&lt;/li>
&lt;li>Medium · LLM Inference Benchmarking (genAI-perf y vLLM) — &lt;a href="https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e">https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Batch sizing in vLLM: the two-hour grid search worth weeks of hardware</title><link>https://blog.lo0.es/en/posts/vllm-batch-sizing-two-hour-grid-search/</link><pubDate>Fri, 05 Jun 2026 04:00:00 +0000</pubDate><guid>https://blog.lo0.es/en/posts/vllm-batch-sizing-two-hour-grid-search/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>&lt;code>max-num-seqs&lt;/code> and &lt;code>max-num-batched-tokens&lt;/code> are the two dials that control how much work vLLM processes on each scheduler iteration. Their default values are calibrated to be safe on any hardware, not to maximise throughput on yours. A systematic grid search over 25 configurations, runnable in two hours, identifies the combination that, for your specific workload and hardware, can double throughput without changing a single line of the model or adding a GPU. The OTel metrics that confirm you found the optimum are &lt;code>vllm:num_waiting_seqs&lt;/code>, &lt;code>vllm:num_preemptions_total&lt;/code> and &lt;code>vllm:time_per_output_token_seconds&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="the-analogy">The analogy&lt;/h2>
&lt;p>An industrial kitchen with one chef and ten stoves. If the maître d&amp;rsquo; only sends one order at a time, the chef works at 10% of capacity. If he sends a hundred simultaneous orders but there are ingredients for only twenty, the chef spends half his time waiting for restocking. The optimum is the point where every stove is lit and the resupply never runs dry.&lt;/p>
&lt;p>&lt;code>max-num-seqs&lt;/code> is how many orders the chef can have in preparation at once. &lt;code>max-num-batched-tokens&lt;/code> is how many ingredients he can process in a single movement of the wok. Getting either of them wrong leaves stoves empty.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-the-defaults-are-not-for-your-hardware">The problem: the defaults are not for your hardware&lt;/h2>
&lt;p>In vLLM V1 (≥ 0.6), the defaults are:&lt;/p>
&lt;pre tabindex="0">&lt;code>max-num-seqs = 1024 (V1) / 256 (V0)
max-num-batched-tokens = 8192
&lt;/code>&lt;/pre>&lt;p>These values guarantee that vLLM starts on any GPU without OOM. They do not guarantee optimal throughput. The reason: the optimal point depends on three variables vLLM does not know at start-up:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>The length distribution of your real workload&lt;/strong> — a RAG system with 2K-token prompts needs a different budget from a chat with 50-token messages.&lt;/li>
&lt;li>&lt;strong>VRAM available for KV cache&lt;/strong> — determined by the model, the quantisation and &lt;code>--gpu-memory-utilization&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Real expected concurrency&lt;/strong> — how many simultaneous users arrive at the 95th percentile.&lt;/li>
&lt;/ol>
&lt;p>The interaction between these three factors makes it impossible for a universal default to be optimal in specific cases.&lt;/p>
&lt;hr>
&lt;h2 id="the-arithmetic-that-matters">The arithmetic that matters&lt;/h2>
&lt;p>On each iteration, vLLM&amp;rsquo;s scheduler decides which tokens to process. The total budget available per step is &lt;code>max-num-batched-tokens&lt;/code>. That budget is split between:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Decode tokens&lt;/strong>: 1 for each active request in the generation phase. With 64 requests in decode, 64 tokens of budget are consumed.&lt;/li>
&lt;li>&lt;strong>Prefill tokens&lt;/strong> (in chunks): the rest of the budget goes to processing new prompts.&lt;/li>
&lt;/ul>
$$\text{prefill tokens per step} = \text{max num batched tokens} - \text{num requests decode}$$
&lt;p>If &lt;code>max-num-batched-tokens = 8192&lt;/code> and you have 512 requests in decode, each step can only process &lt;code>8192 - 512 = 7680&lt;/code> prefill tokens. With 2000-token prompts, that is ~3.8 new prompts per iteration.&lt;/p>
&lt;p>The problem appears when &lt;code>max-num-seqs&lt;/code> is very high relative to the available KV cache. Each active request in decode occupies KV cache blocks. If the blocks run out, vLLM performs &lt;strong>preemption&lt;/strong>: it pauses a request, frees its KV cache and re-queues it. Each preemption costs additional latency for the paused request and complexity for the scheduler.&lt;/p>
$$\text{KV budget} = \frac{\text{free VRAM} \times \text{gpu memory utilization}}{\text{bytes per token} \times \text{max model len}}$$
&lt;p>For a Qwen2.5-14B on an RTX 4090 with Q4_K_M (9 GB of model, 15 GB free):&lt;/p>
$$\text{KV budget} = \frac{15 \times 0.92 \times 10^9}{40\,000} \approx 345\,000 \text{ tokens}$$
&lt;p>With &lt;code>max-model-len = 8192&lt;/code>, the maximum number of simultaneous requests with a full context is:&lt;/p>
$$\text{real max seqs} = \frac{345\,000}{8192} \approx 42 \text{ requests}$$
&lt;p>Setting &lt;code>max-num-seqs = 1024&lt;/code> with those numbers guarantees constant preemptions. The optimum is at 40-50.&lt;/p>
&lt;hr>
&lt;h2 id="the-grid-search-methodology">The grid search: methodology&lt;/h2>
&lt;h3 id="step-1-measure-the-real-workload">Step 1: measure the real workload&lt;/h3>
&lt;p>Before hunting for the optimum, you need to know your traffic&amp;rsquo;s percentiles. From Langfuse or the vLLM logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Extract the length distribution from Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">langfuse&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Langfuse&lt;/span>&lt;span class="p">()&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="n">traces&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fetch_traces&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">limit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">prompt_lens&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">input_tokens&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">t&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">traces&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">input_tokens&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output_lens&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">output_tokens&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">t&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">traces&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">output_tokens&lt;/span>&lt;span class="p">]&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="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Prompt p50=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">prompt_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> p95=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">prompt_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">95&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> p99=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">prompt_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">99&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&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="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Output p50=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">50&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> p95=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">95&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> p99=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">percentile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output_lens&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">99&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.0f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2-work-out-the-kv-budget">Step 2: work out the KV budget&lt;/h3>
&lt;p>Run once with &lt;code>--dry-run&lt;/code>, or read vLLM&amp;rsquo;s start-up log:&lt;/p>
&lt;pre tabindex="0">&lt;code>INFO: # GPU blocks: 4521, # CPU blocks: 512
&lt;/code>&lt;/pre>&lt;p>Each block is 16 tokens. &lt;code>4521 × 16 = 72,336 tokens&lt;/code> of total KV budget.&lt;/p>
&lt;h3 id="step-3-the-grid">Step 3: the grid&lt;/h3>
&lt;p>With the KV budget known and the p95 of prompt/output length:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># grid_search_batch.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">json&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">time&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="n">MODEL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Qwen/Qwen2.5-14B-Instruct-AWQ&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">PROMPT_LEN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">512&lt;/span> &lt;span class="c1"># p50 of your workload&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">OUTPUT_LEN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">256&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CONCURRENCY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">32&lt;/span> &lt;span class="c1"># simultaneous users expected at peak&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="n">seqs_values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">32&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">128&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">512&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tokens_values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">4096&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8192&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">16384&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">32768&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">65536&lt;/span>&lt;span class="p">]&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="n">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">seqs&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">seqs_values&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">tokens&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">tokens_values&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cmd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;vllm.entrypoints.benchmark_throughput&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="s2">&amp;#34;--model&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">MODEL&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;--max-num-seqs&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">seqs&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;--max-num-batched-tokens&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tokens&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;--num-prompts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;200&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="s2">&amp;#34;--input-len&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PROMPT_LEN&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;--output-len&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OUTPUT_LEN&lt;/span>&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;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">capture_output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">300&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Parse throughput from the output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">out&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">splitlines&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="s2">&amp;#34;Throughput&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tps&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">float&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">()[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s2">&amp;#34;seqs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">seqs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tokens&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tps&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tps&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;seqs=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">seqs&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> tokens=&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">tokens&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> → &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">tps&lt;/span>&lt;span class="si">:&lt;/span>&lt;span class="s2">.1f&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> tok/s&amp;#34;&lt;/span>&lt;span class="p">)&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"># Save for analysis&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;grid_results.json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;w&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dump&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">results&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>25 configurations × ~5 min = &lt;strong>~2 hours&lt;/strong>. Real execution time, not waiting time.&lt;/p>
&lt;h3 id="step-4-read-the-surface">Step 4: read the surface&lt;/h3>
&lt;p>The result is a 5×5 throughput matrix. The typical shape:&lt;/p>
&lt;pre tabindex="0">&lt;code>max-num-batched-tokens → 4K 8K 16K 32K 64K
max-num-seqs ↓
32 180 310 380 390 385 ← max-num-seqs too low
64 185 350 480 510 508 ← optimal point for this workload
128 182 340 450 480 475
256 178 320 400 410 402 ← KV cache runs out, preemptions
512 170 290 360 370 368 ← high preemptions
&lt;/code>&lt;/pre>&lt;p>The optimum in this example: &lt;code>max-num-seqs=64, max-num-batched-tokens=32768&lt;/code>. Above that, preemptions cancel out the concurrency gain.&lt;/p>
&lt;hr>
&lt;h2 id="confirmation-with-otel">Confirmation with OTel&lt;/h2>
&lt;p>Once the optimal configuration is deployed, three Prometheus metrics confirm it is well calibrated:&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"># 1. Queued requests — should stay close to 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="c1"># If it grows steadily: max-num-seqs too low or max-num-batched-tokens insufficient&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">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_waiting_seqs&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"># 2. Preemptions — should be 0 or very occasional (&amp;lt;1/min)&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"># If it grows: max-num-seqs too high for the available KV cache&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">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">num_preemptions_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="mi">60&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"># 3. ITL (inter-token latency) — should be stable, with no spikes&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"># Bimodality = badly calibrated batch size (some requests outside the CUDA graph bucket)&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">histogram_quantile&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="mf">0.99&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">vllm&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="nv">time_per_output_token_seconds_bucket&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 optimal configuration produces:&lt;/p>
&lt;ul>
&lt;li>&lt;code>num_waiting_seqs&lt;/code> ≈ 0 under normal operation&lt;/li>
&lt;li>&lt;code>num_preemptions_total&lt;/code> stable (not growing)&lt;/li>
&lt;li>&lt;code>time_per_output_token&lt;/code> unimodal&lt;/li>
&lt;/ul>
&lt;p>If &lt;code>num_waiting_seqs&lt;/code> is high with &lt;code>gpu_cache_usage_perc&lt;/code> low: raise &lt;code>max-num-batched-tokens&lt;/code> to process prefills faster. If &lt;code>num_preemptions_total&lt;/code> grows: lower &lt;code>max-num-seqs&lt;/code> or enable FP8 KV cache to free up blocks.&lt;/p>
&lt;hr>
&lt;h2 id="reference-configurations-by-profile">Reference configurations by profile&lt;/h2>
&lt;p>Based on the grid search for mid-range hardware (generic 4×H100, 14B-70B model):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">Profile&lt;/th>
&lt;th style="text-align:right">Prompt p50&lt;/th>
&lt;th style="text-align:right">Output p50&lt;/th>
&lt;th style="text-align:right">max-num-seqs&lt;/th>
&lt;th style="text-align:right">max-num-batched-tokens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">Conversational chat&lt;/td>
&lt;td style="text-align:right">150 tok&lt;/td>
&lt;td style="text-align:right">300 tok&lt;/td>
&lt;td style="text-align:right">256&lt;/td>
&lt;td style="text-align:right">16384&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Enterprise RAG&lt;/td>
&lt;td style="text-align:right">1500 tok&lt;/td>
&lt;td style="text-align:right">200 tok&lt;/td>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">32768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Coding (completion)&lt;/td>
&lt;td style="text-align:right">800 tok&lt;/td>
&lt;td style="text-align:right">500 tok&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">32768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Summarisation&lt;/td>
&lt;td style="text-align:right">2500 tok&lt;/td>
&lt;td style="text-align:right">400 tok&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">65536&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">Batch processing&lt;/td>
&lt;td style="text-align:right">4000 tok&lt;/td>
&lt;td style="text-align:right">800 tok&lt;/td>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">65536&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>None of these is universal. They are starting points for the grid search on your real hardware and workload.&lt;/p>
&lt;hr>
&lt;h2 id="when-not-to-touch-the-defaults">When not to touch the defaults&lt;/h2>
&lt;p>If your system sits below 50% KV cache utilisation (&lt;code>vllm:gpu_cache_usage_perc &amp;lt; 0.50&lt;/code>) under real demand and with no &lt;code>num_waiting_seqs&lt;/code>, the defaults are enough for your current load. The grid search contributes more when you are close to maximum capacity or when you want to extract the full performance of fixed hardware.&lt;/p>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">&lt;code>max-num-batched-tokens&lt;/code> is the budget chunked prefill uses to interleave decode; this article covers the tuning of that parameter&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">&lt;code>max-num-seqs&lt;/code> interacts directly with &lt;code>gpu-memory-utilization&lt;/code> and the KV cache capacity for decode&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/instrumenting-vllm-otel-measuring-optimisations/">the &lt;code>num_waiting_seqs&lt;/code>, &lt;code>num_preemptions_total&lt;/code> and &lt;code>time_per_output_token&lt;/code> metrics configured in the full OTel pipeline&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">the KV budget formula that determines the real maximum of &lt;code>max-num-seqs&lt;/code> for your hardware&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">hardware sizing starts from the optimal throughput this grid search determines&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="in-this-same-series">In this same series&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">the second free optimisation: taking the prefix cache hit rate from 15% to 75%&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/fp8-end-to-end-enable-measure-quality-decide/">FP8 in weights and KV cache: +40-60% throughput measured before and after with an eval suite&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/one-big-replica-or-many-small-ones-platform-decision/">TP=4×1 vs TP=2×2: when the crossover point changes the platform decision&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.vllm.ai/en/stable/configuration/optimization/">vLLM Optimization and Tuning — documentación oficial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://medium.com/@mahernaija/tuning-vllm-for-maximum-throughput-a-research-engineers-field-guide-21f341d71248">Tuning vLLM for Maximum Throughput (2026)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://rocm.docs.amd.com/en/latest/how-to/rocm-for-ai/inference-optimization/vllm-optimization.html">vLLM V1 performance optimization — ROCm&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anyscale.com/llm/serving/parameter-tuning">Anyscale: Tune parameters for LLMs&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>