<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Batch-Sizing on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/batch-sizing/</link><description>Recent content in Batch-Sizing on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 05 Jun 2026 04:00:00 +0000</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/batch-sizing/index.xml" rel="self" type="application/rss+xml"/><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>