<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dravin AI · Blog · Self-hosted LLMs</title><description>Self-hosted LLMs on the Dravin AI blog: how to serve more users per GPU, and how to run, calibrate and test typed decisions with SGLang on your own GPUs.</description><link>https://dravin.ai/blog/tags/self-hosted-llms/</link><language>en-gb</language><image><url>https://dravin.ai/apple-touch-icon.png</url><title>Dravin AI · Blog · Self-hosted LLMs</title><link>https://dravin.ai/blog/tags/self-hosted-llms/</link></image><lastBuildDate>Mon, 05 Oct 2026 18:30:00 GMT</lastBuildDate><atom:link href="https://dravin.ai/blog/tags/self-hosted-llms/rss.xml" rel="self" type="application/rss+xml"/><item><title>Typed decisions on your own GPUs</title><link>https://dravin.ai/blog/typed-decisions-on-your-own-gpus/</link><guid isPermaLink="true">https://dravin.ai/blog/typed-decisions-on-your-own-gpus/</guid><description>How SGLang&apos;s /v1/decisions turns a chat model into a classifier, how to calibrate and test its probabilities, and when to self-host.</description><pubDate>Fri, 02 Oct 2026 18:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SGLang 0.5.21’s /v1/decisions reads each label’s next-token probability from a chat model and renormalises over the labels, generating no text.&lt;/li&gt;&lt;li&gt;SGLang says those probabilities are not calibrated, and that they can shift with batching and prefix caching.&lt;/li&gt;&lt;li&gt;Fit a temperature or Platt scaling on your labelled data, test replay variance, and compare against a fine-tuned classifier.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;On 29 September 2026 OpenAI announced a Decisions API in limited preview &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[1]&lt;/a&gt;. SGLang 0.5.21 now ships &lt;code&gt;/v1/decisions&lt;/code&gt;, which turns a chat model you already serve into a classifier with a probability for every option &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[3]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. This follow-up to our post on &lt;a href=&quot;https://dravin.ai/blog/jev-and-typed-decision-models/&quot;&gt;typed decision models&lt;/a&gt; shows how the endpoint reads those probabilities out of a decoder model, why they are not calibrated, how to calibrate and test them, and when to run decisions on your own GPUs.&lt;/p&gt;
&lt;h2 id=&quot;what-openai-and-sglang-shipped&quot;&gt;What OpenAI and SGLang shipped&lt;/h2&gt;
&lt;p&gt;OpenAI’s DevDay recap says the Decisions API focuses its model on “a specific set of user-defined questions with finite pre-defined answers”, given text or images, to classify content, route requests or choose an agent’s next action &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[1]&lt;/a&gt;. Its developer account says the API runs on GPT-6 Luna &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[2]&lt;/a&gt;. The recap also notes “a broad release planned in the coming days” &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[1]&lt;/a&gt;. Unlike the other developer announcements in the recap, the Decisions API entry links to no documentation, and the recap says nothing about how it works, so nothing below describes OpenAI’s design &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;SGLang, the open-source serving engine, released 0.5.21 on PyPI on 1 October and on GitHub on 2 October &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[4]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[3]&lt;/a&gt;; the code had merged on 25 September, before DevDay &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[7]&lt;/a&gt;. The route needs no special checkpoint: any generation model served with a Jinja chat template can answer if per-request checks pass &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Two models are validated, Qwen3.8-27B and Qwen3.5-35B-A3B, each on one H200 in BF16 &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A request carries one input, as text or a JSON object or array, and a list of questions: a choice among 2 to 26 options, a score on 2 to 10 levels, or a yes or no. Release 0.5.21 has no image field &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. A second route, &lt;code&gt;/v1/systemone&lt;/code&gt;, accepts the request shape of TypeSafe’s System One API, which serves Jev, so TypeSafe’s official SDKs can point their base URL at your server, with documented exceptions &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-a-chat-model-returns-a-probability-per-option&quot;&gt;How a chat model returns a probability per option&lt;/h2&gt;
&lt;p&gt;The server writes each question as one user message: the input, a blank line, the question, one line per option with a letter label such as “A: billing”, and an instruction to answer with one label only &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[6]&lt;/a&gt;. Levels are labelled 0 to 9, and yes or no questions use the words yes and no. The message goes through the model’s chat template with thinking turned off, and the server checks that each label adds exactly one distinct token after the prompt, returning a 400 when one does not &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[6]&lt;/a&gt;. Letters keep every label to one token, whatever an option is called.&lt;/p&gt;
&lt;p&gt;For a ticket and three teams, the user message reads:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The integration keeps failing.

Question: Which team should handle this ticket?
A: billing
B: technical
C: sales
Answer with the letter of one option only.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With thinking off, Qwen’s chat template then renders a closed reasoning block, so the next token is the answer &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[6]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The model runs one prefill pass per question, the phase where it reads the prompt, and generates nothing: usage reports zero completion tokens &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. At the answer position it holds a probability for every token in its vocabulary. The server keeps only the label tokens’ log-probabilities, divides them by the request’s temperature T (default 1) and takes a softmax over them. Every log-probability carries the same vocabulary-wide normalising term, so it cancels &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[6]&lt;/a&gt;. A choice answer is the most probable option, and a score is the probability-weighted mean level &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Renormalising hides how much probability sat elsewhere, so the server also reports &lt;code&gt;label_mass&lt;/code&gt;, the raw probability of all the labels together; a low value means most of it sits outside the offered answers &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;From a typed question to a probability&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Render each question: thinking off, labels one token each&lt;/li&gt;&lt;li&gt;Prefill per question: no tokens generated&lt;/li&gt;&lt;li&gt;Softmax over labels: divided by T, plus label_mass&lt;/li&gt;&lt;li&gt;Calibrate, then threshold: on your labelled data&lt;/li&gt;&lt;/ol&gt;&lt;figcaption&gt;Steps 1 to 3 run inside SGLang 0.5.21. Step 4 is yours: the returned probabilities are not calibrated.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Questions are answered independently and batched together; each is its own prompt that repeats the input, so prompt tokens grow with the number of questions &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Laya, from our Jev post, answers every question in one forward pass.&lt;/p&gt;
&lt;p&gt;Constrained decoding reaches the same numbers. In SGLang, a one-token regex such as &lt;code&gt;(A|B|C)&lt;/code&gt; at temperature 0, with log-probabilities returned, gives the same probabilities as the &lt;code&gt;/v1/score&lt;/code&gt; path this route uses, for one-character labels in the same cache state &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. A JSON schema with enum fields gives no clean probability per option, because an enum value can span several tokens &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-the-endpoint-refuses-and-what-that-rules-out&quot;&gt;What the endpoint refuses, and what that rules out&lt;/h2&gt;
&lt;p&gt;Most of the rules protect the answer position, the one place a number is read. Qwen3.8-27B and Qwen3.5-35B-A3B think by default; the route turns thinking off, refuses requests that turn it back on, and refuses prompts that leave a reasoning block open, so the answer is never read inside the reasoning &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. That means the model answers without working anything out first. Compute arithmetic, dates and multi-step logic in code and pass the result in, as TypeSafe advises for Jev.&lt;/p&gt;
&lt;p&gt;The server also refuses built-in conversation templates, servers started with &lt;code&gt;--enable-mis&lt;/code&gt; or &lt;code&gt;--dllm-algorithm&lt;/code&gt;, and a &lt;code&gt;model&lt;/code&gt; field that names a LoRA adapter, so decisions cannot run on one &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. It does not check where a new model’s template puts the answer, so before relying on a model beyond the validated two, the docs say to inspect the prompt token ids the route can return &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A decision takes a single input, not a chat history; a conversation goes in as text or a JSON array &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. A choice stops at 26 options. &lt;code&gt;/v1/systemone&lt;/code&gt; accepts up to 255 with two-letter labels where the tokeniser and chat template allow it, as the Qwen models’ do; SGLang warns that these labels include common words with unequal priors, so answers above 26 options can depend on option order &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;why-the-probabilities-are-not-calibrated&quot;&gt;Why the probabilities are not calibrated&lt;/h2&gt;
&lt;p&gt;SGLang’s documentation says “None of these values is a calibrated probability that the decision is correct” &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Four effects pull a chat model’s label probabilities away from calibration.&lt;/p&gt;
&lt;p&gt;Post-training comes first. In a 2022 study, Anthropic found its large base models well calibrated when options were shown with letters and scored by the letter, as this route does, in prompts with a few worked examples; with no examples, which is how this route asks, calibration was weaker. Its RLHF-tuned versions looked badly miscalibrated until one temperature, T = 2.5, largely fixed them on several evaluations, though the authors warn that heavier RL training might distort calibration beyond that repair &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[9]&lt;/a&gt;. Both validated Qwen models are chat models &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, so test the same repair on yours.&lt;/p&gt;
&lt;p&gt;Letter priors add a second skew. Across 20 LLMs, a 2023 study traced a preference for particular option letters to token bias: whatever the content, the model puts more probability on some letter tokens than on others, so the letter an option lands on can move its probability &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[10]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Wording is the third. In a 2021 study of few-shot prompts, the format, the examples and even their order swung accuracy from near chance to near the state of the art. On one sentiment prompt, the default 0.5 threshold gave frequent false positives where a threshold of 0.68 would have been 94% accurate on its validation set &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[11]&lt;/a&gt;. SGLang owns the wording around your question and ships each change as a new &lt;code&gt;prompt_format_version&lt;/code&gt; &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;; the question and option text are yours, so test rewordings on labelled data.&lt;/p&gt;
&lt;p&gt;Finally, probability can sit outside the labels. The probabilities always sum to 1 over the offered labels, so a choice picks an option even when none fits. &lt;code&gt;label_mass&lt;/code&gt; shows the leak, though on yes or no questions it reads low even on clear cases, because it counts only the lowercase yes and no tokens while the model also puts probability on “Yes” and “No” &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Anthropic’s models were also reluctant to use a “none of the above” option, while their true or false judgements stayed well calibrated &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[9]&lt;/a&gt;. Of the Jev post’s two fixes, that favours a separate yes or no question before the choice.&lt;/p&gt;
&lt;h2 id=&quot;calibrating-on-your-own-server&quot;&gt;Calibrating on your own server&lt;/h2&gt;
&lt;p&gt;Fit calibration per question, on labelled examples from your own traffic kept apart from the test set.&lt;/p&gt;
&lt;p&gt;Temperature scaling fits a single number T by minimising log loss on held-out data, and never changes which option wins &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[12]&lt;/a&gt;. Here it maps onto the request’s &lt;code&gt;temperature&lt;/code&gt;, which divides the label logits before the softmax &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Fit T offline and pass it in that field: the chosen option stays put, the probabilities and so the score’s weighted mean move, and &lt;code&gt;label_mass&lt;/code&gt; does not change &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[12]&lt;/a&gt;. One temperature covers every question in a request &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[6]&lt;/a&gt;, so send questions with different temperatures separately, or apply T on your side: raise each probability returned at the default T of 1 to the power 1/T, renormalise, and recompute any score. SDK users have to do the latter, since &lt;code&gt;/v1/systemone&lt;/code&gt; refuses a temperature field &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A temperature cannot correct a lean towards one label. For yes or no, Platt scaling fits a slope and an intercept on the log-odds, and the intercept absorbs a model that says yes too often; for choices, vector scaling fits a weight and a bias per option &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[12]&lt;/a&gt;. Isotonic regression can correct any monotone distortion but overfits small sets: with fewer than about 200 to 1,000 labelled cases, Platt scaling did better for all nine learning methods in a 2005 study &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[13]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Two corrections go after label priors directly. Contextual calibration scores a content-free input such as “N/A” and rescales the outputs so that input comes out uniform &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[11]&lt;/a&gt;. PriDe estimates the letter prior by permuting options on a small sample, for example 5% of the test set, and removes it from the remaining predictions &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[10]&lt;/a&gt;. Both remove bias, and the 2021 paper separates that goal from statistical calibration &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[11]&lt;/a&gt;, so measure calibration again afterwards.&lt;/p&gt;
&lt;p&gt;Refit when you change the model or its &lt;a href=&quot;https://dravin.ai/services/#inference&quot;&gt;quantisation&lt;/a&gt;, upgrade SGLang, or receive a new &lt;code&gt;prompt_format_version&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;testing-replay-and-drift&quot;&gt;Testing replay and drift&lt;/h2&gt;
&lt;p&gt;The general evaluation is in the &lt;a href=&quot;https://dravin.ai/blog/jev-and-typed-decision-models/&quot;&gt;Jev post&lt;/a&gt;; a self-hosted endpoint adds its own tests.&lt;/p&gt;
&lt;p&gt;Start with replay variance. On the two validated models, which mix full and linear attention, SGLang saw probabilities move by up to 0.07, and &lt;code&gt;label_mass&lt;/code&gt; by up to about 0.14, between cold and prefix-cached requests and across batch compositions; the chosen option did not change in its checks &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. One general cause: batch size changes how GPU kernels split their sums, and floating-point addition is not associative, so identical inputs can score differently &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[8]&lt;/a&gt;. Send your test set several times, cold and warm, alone and mixed with chat traffic, and route a band around each threshold, at least as wide as the movement you measure, to a person. On Qwen3.5-35B-A3B, &lt;code&gt;--disable-radix-cache&lt;/code&gt; gave identical values across sequential repeats, at the cost of prefix reuse &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Keep every decision replayable. With &lt;code&gt;return_prompt_token_ids&lt;/code&gt;, an answer carries its exact prompt and label token ids, and &lt;code&gt;/v1/score&lt;/code&gt; re-scores them to the same values when temperature and cache state match &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Store them with the decision, and send &lt;code&gt;prompt_format_version&lt;/code&gt; so that an upgrade which changes the wording fails loudly instead of shifting answers &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Shuffle option order on a sample and compare, since letter priors move answers &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[10]&lt;/a&gt;. Set a &lt;code&gt;label_mass&lt;/code&gt; floor per question type from labelled data, remembering that yes or no reads low. Leave any temperature in &lt;code&gt;--preferred-sampling-params&lt;/code&gt; unset, since it can scale the probabilities and &lt;code&gt;label_mass&lt;/code&gt; when a decision shares a batch with generation or runs under speculative decoding &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;hosted-preview-or-your-own-gpus&quot;&gt;Hosted preview or your own GPUs&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://dravin.ai/services/#self-hosted&quot;&gt;Self-hosting&lt;/a&gt; makes most sense when you already serve an open model: decisions run on the same server, with no extra flag, alongside chat traffic &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;. Inputs stay on your hardware, any answer can be replayed from stored token ids, and you choose the model, its version and the calibration. The work is all prefill, so the prefix caching and chunk size levers in our &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/&quot;&gt;serving playbook&lt;/a&gt; apply, though prefix caching is also one source of the drift SGLang measured. TypeSafe’s SDKs time out after 10 seconds and retry, so raise that timeout for many questions over a long input &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Code written for Jev can switch to &lt;code&gt;/v1/systemone&lt;/code&gt; by changing its base URL. Its thresholds should not come along unchecked: the confidence that route returns is uncalibrated, and SGLang warns that thresholds tuned on another provider may not transfer &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[5]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A hosted API suits low volume, or a team that will not run GPUs; once OpenAI’s preview opens up, test it on the same labelled set.&lt;/p&gt;
&lt;p&gt;Neither replaces a trained classifier where one fits. With labelled data and a stable label set, a small &lt;a href=&quot;https://dravin.ai/services/#training&quot;&gt;fine-tuned&lt;/a&gt; model is the stronger default: a 2024 study found smaller fine-tuned models beat larger zero-shot prompted models on every text classification task it tested &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/#sources&quot;&gt;[14]&lt;/a&gt;. Our default is unchanged: &lt;a href=&quot;https://dravin.ai/services/#beyond-llms&quot;&gt;a small classifier fine-tuned on your labels, calibrated and thresholded&lt;/a&gt;, with the uncertain tail sent to an LLM or a person. A decision endpoint fits where that default cannot yet: when labels change per request, when no labelled data exists, or to draft labels, checked by people, for the classifier you train next.&lt;/p&gt;
&lt;h2 id=&quot;checklist&quot;&gt;Checklist&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Pin the SGLang version, model and quantisation, send &lt;code&gt;prompt_format_version&lt;/code&gt; with every request, and refit when any of them changes.&lt;/li&gt;
&lt;li&gt;On a new model, check the returned prompt token ids for the answer position.&lt;/li&gt;
&lt;li&gt;Leave the temperature in &lt;code&gt;--preferred-sampling-params&lt;/code&gt; unset on servers that take decisions.&lt;/li&gt;
&lt;li&gt;Fit a temperature per question on held-out labelled traffic, apply it per request or on your side, and add Platt or vector scaling if one label is over-predicted.&lt;/li&gt;
&lt;li&gt;Shuffle option order on a sample, and correct letter priors if answers move.&lt;/li&gt;
&lt;li&gt;Measure replay variance cold, warm and under load, and send a band at least that wide around each threshold to a person.&lt;/li&gt;
&lt;li&gt;Set a &lt;code&gt;label_mass&lt;/code&gt; floor per question type, and gate choices with a yes or no question where no option may fit.&lt;/li&gt;
&lt;li&gt;Store each decision’s prompt and label token ids for replay.&lt;/li&gt;
&lt;li&gt;Compare against a classifier fine-tuned on the same labels before you commit.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;sources&quot;&gt;Sources&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/index/devday-2026-recap/&quot;&gt;OpenAI, “DevDay 2026 Recap” (29 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/OpenAIDevs/status/2105003318917697873&quot;&gt;OpenAI Developers on X, Decisions API announcement (29 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sgl-project/sglang/releases/tag/v0.5.21&quot;&gt;SGLang, “v0.5.21” release notes (2 Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/sglang/0.5.21/&quot;&gt;PyPI, “sglang 0.5.21” (1 Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sgl-project/sglang/blob/v0.5.21/docs/docs/supported-models/decision_models.mdx&quot;&gt;SGLang docs, “Decision models”, at the v0.5.21 tag (2 Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sgl-project/sglang/blob/v0.5.21/python/sglang/srt/entrypoints/openai/serving_decisions.py&quot;&gt;SGLang source, “serving_decisions.py”, at the v0.5.21 tag (2 Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sgl-project/sglang/pull/41208&quot;&gt;SGLang, pull request 41208, “Add a System One compatible /v1/systemone route” (merged 25 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sgl-project/sglang/blob/v0.5.21/docs/docs/advanced_features/deterministic_inference.mdx&quot;&gt;SGLang docs, “Deterministic Inference”, at the v0.5.21 tag (2 Oct 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2207.05221&quot;&gt;Anthropic, “Language Models (Mostly) Know What They Know” (Jul 2022)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2309.03882&quot;&gt;Tsinghua University and WeChat AI, “Large Language Models Are Not Robust Multiple Choice Selectors” (ICLR 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2102.09690&quot;&gt;UC Berkeley, University of Maryland and UC Irvine, “Calibrate Before Use: Improving Few-Shot Performance of Language Models” (ICML 2021)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1706.04599&quot;&gt;Cornell University, “On Calibration of Modern Neural Networks” (ICML 2017)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doi.org/10.1145/1102351.1102430&quot;&gt;Cornell University, “Predicting Good Probabilities With Supervised Learning” (ICML 2005)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2406.08660&quot;&gt;Stanford University and University of Zurich, “Fine-Tuned ‘Small’ LLMs (Still) Significantly Outperform Zero-Shot Generative AI Models in Text Classification” (Jun 2024)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post first appeared on &lt;a href=&quot;https://dravin.ai/blog/typed-decisions-on-your-own-gpus/&quot;&gt;Dravin AI&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Typed decision models</category><category>Classification</category><category>Calibration</category><category>Self-hosted LLMs</category><author>info@dravin.ai (Dravin AI)</author></item><item><title>More users per GPU: a playbook for self-hosted LLM serving</title><link>https://dravin.ai/blog/raising-llm-serving-throughput/</link><guid isPermaLink="true">https://dravin.ai/blog/raising-llm-serving-throughput/</guid><description>What to measure first, then the levers that raise LLM serving throughput on your own GPUs, in the order we pull them and what each one trades away.</description><pubDate>Tue, 22 Sep 2026 18:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Prefill keeps the GPU’s arithmetic busy while decode waits on memory, so most of the work is giving decode a bigger batch without letting prefill stall it.&lt;/li&gt;&lt;li&gt;Measure goodput first: the request rate each GPU serves while a target share of requests meet their latency limits.&lt;/li&gt;&lt;li&gt;Pull the levers in order of cost and risk: engine settings and prompt layout, then quantisation and speculative decoding, then parallelism and disaggregation.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A model that answers well in a notebook can be too slow and too expensive once hundreds of people use it at once. Before adding GPUs, we work through engine settings, prompt layout and a few serving techniques in a fixed order, and check each change against numbers we trust.&lt;/p&gt;
&lt;p&gt;One fact sits under everything here. Prefill, where the model reads the prompt, keeps the GPU’s arithmetic busy &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[1]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[2]&lt;/a&gt;. Decode, where it writes one token at a time per request, uses little of that arithmetic and mostly waits on memory bandwidth &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[3]&lt;/a&gt;. Most throughput work comes down to giving decode a bigger batch while stopping prefill from stalling it.&lt;/p&gt;
&lt;p&gt;We serve open-weight models privately, quantised, on vLLM. Versions are current as of 23 September 2026: vLLM 0.30.0, SGLang 0.5.20 and TensorRT-LLM 1.2.1, each the latest stable release &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[4]&lt;/a&gt;. Flags and metric names below are vLLM’s unless we name another engine.&lt;/p&gt;
&lt;p&gt;The order follows cost and risk. Batching, KV cache sizing, &lt;a href=&quot;https://dravin.ai/services/#inference&quot;&gt;prefix caching&lt;/a&gt; and chunk size are configuration on a current engine, and vLLM 0.30.0 turns most of them on by default, so the work is sizing and checking them. Quantisation and speculative decoding change the arithmetic the model runs, so they need your own evals and your own load. Parallelism and disaggregation change the topology, so they come last.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Batch and KV cache sizing&lt;/td&gt;
&lt;td&gt;More requests in flight&lt;/td&gt;
&lt;td&gt;Per-user latency rises with the batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix caching and routing&lt;/td&gt;
&lt;td&gt;Skipped prefill for shared prompts&lt;/td&gt;
&lt;td&gt;Cache memory, and uneven load across replicas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk size&lt;/td&gt;
&lt;td&gt;Steady streams while long prompts load&lt;/td&gt;
&lt;td&gt;A later first token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantisation&lt;/td&gt;
&lt;td&gt;Fewer bytes per token, room for more requests&lt;/td&gt;
&lt;td&gt;Accuracy risk; formats tied to GPU generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speculative decoding&lt;/td&gt;
&lt;td&gt;Faster streams at low load&lt;/td&gt;
&lt;td&gt;Spare compute; the gain fades as the batch fills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admission control&lt;/td&gt;
&lt;td&gt;Latency targets held under load&lt;/td&gt;
&lt;td&gt;Some requests deferred or rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replicas and expert parallelism&lt;/td&gt;
&lt;td&gt;Capacity beyond one GPU group&lt;/td&gt;
&lt;td&gt;More GPUs and fast interconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disaggregation&lt;/td&gt;
&lt;td&gt;Separate control of TTFT and ITL&lt;/td&gt;
&lt;td&gt;Two pools to size, RDMA, experimental support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;measure-before-you-tune&quot;&gt;Measure before you tune&lt;/h2&gt;
&lt;p&gt;Four numbers describe a serving system.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTFT&lt;/td&gt;
&lt;td&gt;Time from sending a request to receiving the first token &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[5]&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;How long a user stares at a blank screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ITL and TPOT&lt;/td&gt;
&lt;td&gt;The gap between streamed outputs (ITL), or a request’s decode time spread over its output tokens (TPOT) &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[6]&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;How fast the answer streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens per second&lt;/td&gt;
&lt;td&gt;Output tokens per second, summed over all concurrent requests &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[5]&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What the hardware is worth to you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goodput&lt;/td&gt;
&lt;td&gt;The highest request rate per GPU served while a target share of requests meet their latency limits &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[7]&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The number to optimise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Tools disagree on whether ITL counts the first token, so NVIDIA’s guide says to “compare results only when definitions align” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[5]&lt;/a&gt;. Pick one tool and keep it.&lt;/p&gt;
&lt;p&gt;DistServe defines goodput as “the maximum request rate that can be served adhering to the SLO attainment goal (say, 90%) for each GPU provisioned” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[7]&lt;/a&gt;, where the SLO (service-level objective) is the latency limit you commit to. It leads the dashboard because raw tokens per second can keep rising while users wait longer. System throughput and per-user speed pull against each other, and most levers below trade one for the other.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vllm bench serve&lt;/code&gt; measures all of this: set &lt;code&gt;--request-rate&lt;/code&gt; and &lt;code&gt;--burstiness&lt;/code&gt; to your arrival pattern, cap load with &lt;code&gt;--max-concurrency&lt;/code&gt;, and pass your TTFT and TPOT limits in milliseconds to &lt;code&gt;--goodput&lt;/code&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[6]&lt;/a&gt;. Report p50 and p99 for each; averages hide the slow tail. Use your own prompt lengths, output lengths and shared prefixes. Published benchmarks show which levers are worth trying; your own traffic shows how much each one gives.&lt;/p&gt;
&lt;p&gt;In production, watch &lt;code&gt;vllm:num_requests_waiting&lt;/code&gt; for queueing, &lt;code&gt;vllm:kv_cache_usage_perc&lt;/code&gt; and &lt;code&gt;vllm:num_preemptions&lt;/code&gt; for memory pressure, and &lt;code&gt;vllm:prefix_cache_hits&lt;/code&gt; against &lt;code&gt;vllm:prefix_cache_queries&lt;/code&gt; for cache reuse &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[8]&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;get-the-batch-and-the-kv-cache-right&quot;&gt;Get the batch and the KV cache right&lt;/h2&gt;
&lt;p&gt;The first lever is the engine. Early servers ran a batch until its longest request finished, so new requests queued behind it. Orca introduced “iteration-level scheduling”: the scheduler decides after every decoding step, so finished requests leave and new ones join at once. On a 175B model the paper reports 36.9 times the throughput of NVIDIA FasterTransformer at the same latency &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[9]&lt;/a&gt;. Every current engine does this, under the names continuous batching or in-flight batching &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[10]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The next constraint is the KV cache, the stored keys and values for every token of every running request. For a 13B model the PagedAttention paper works it out at 800 KB per token in FP16, so a single 2,048-token request can hold 1.6 GB &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[11]&lt;/a&gt;. The general form is 2 × layers × KV heads × head dimension × bytes per element, per token. It grows linearly with context length and concurrency, so the KV cache sets the batch ceiling. Grouped-query attention, where several query heads share one key-value head, shrinks the KV heads term &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[12]&lt;/a&gt;; a model’s config lists its KV head count, so check it before you choose the model.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;em&gt;GPU memory as 48 fixed-size KV cache blocks. Static batch with one slab per request: four requests each reserve a 12-block slab for their maximum length and fill part of it; B has finished but its slab stays held until the batch ends, so queued requests wait and 17 of 48 blocks hold tokens. Continuous batching with a paged cache: requests take small scattered blocks step by step; B&apos;s blocks free after step 3, new request E takes all of them at step 4, and by step 6 41 of 48 blocks hold tokens.&lt;/em&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/&quot;&gt;See the animated diagram in the post.&lt;/a&gt;&lt;/p&gt;&lt;figcaption&gt;A slab reserves a request’s maximum length up front and holds it until the request, or the batch, ends. A paged cache adds blocks as tokens arrive and returns them the moment a request finishes.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Before PagedAttention, only 20.4 to 38.2% of KV cache memory held actual token states. Storing the cache in small pages, as an operating system pages virtual memory, brought waste close to zero and gave 2 to 4 times the throughput of FasterTransformer and Orca at the same latency &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[11]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With a current engine, the work is sizing. vLLM 0.30.0 sets &lt;code&gt;gpu_memory_utilization&lt;/code&gt; to 0.92 and picks &lt;code&gt;max_num_seqs&lt;/code&gt; and &lt;code&gt;max_num_batched_tokens&lt;/code&gt; by GPU &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[13]&lt;/a&gt;. When the cache runs out, requests are preempted and redo their prefill; the documented fixes include a higher &lt;code&gt;gpu_memory_utilization&lt;/code&gt;, lower &lt;code&gt;max_num_seqs&lt;/code&gt; or &lt;code&gt;max_num_batched_tokens&lt;/code&gt;, or more tensor or pipeline parallelism to free memory for the cache &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[14]&lt;/a&gt;. A rising &lt;code&gt;vllm:num_preemptions&lt;/code&gt; counter is the first thing we look at. On a new deployment we benchmark at the defaults first, then change one setting per run.&lt;/p&gt;
&lt;p&gt;Leave CUDA graphs on. A decode step is a series of tiny GPU jobs, and graph replay skips the CPU cost of launching each one &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[15]&lt;/a&gt;; vLLM captures graphs by default, at some cost in memory and startup time &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[16]&lt;/a&gt;. Keep &lt;code&gt;--enforce-eager&lt;/code&gt; for debugging &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[14]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The cost: a bigger batch raises tokens per second but also each user’s latency, the trade-off Sarathi-Serve is named for &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;stop-recomputing-the-same-prompt&quot;&gt;Stop recomputing the same prompt&lt;/h2&gt;
&lt;p&gt;Many workloads resend the same long prefix: a system prompt, tool definitions, a document, earlier turns of a chat. Prefix caching keeps that prefix’s KV cache, and a new request that shares it skips that part of prefill &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[17]&lt;/a&gt;. SGLang’s RadixAttention keeps prefixes in a prefix tree (a radix tree) with least-recently-used eviction &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[18]&lt;/a&gt;. Both engines turn it on by default in their current releases &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[13]&lt;/a&gt;, &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[19]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It “only reduces the time of processing the queries (the prefilling phase)” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[17]&lt;/a&gt;, so it pays most where prompts are long and answers short: &lt;a href=&quot;https://dravin.ai/services/#rag&quot;&gt;RAG&lt;/a&gt;, agents and document chat.&lt;/p&gt;
&lt;p&gt;A cache only hits on an identical token prefix. Put stable content first (system prompt, tools, documents), variable content last, and keep timestamps and request IDs out of the top. In SGLang, &lt;code&gt;--schedule-policy lpm&lt;/code&gt; reorders the queue by longest prefix match to raise the hit rate &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[20]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Routing that ignores the cache scatters requests that share a prefix across replicas, and one published benchmark shows the cost &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[21]&lt;/a&gt;. It ran 8 replicas of a 32B model on 16 H100s, with shared-prefix traffic whose cache demand was six times what one replica could hold. A router that tracked each replica’s actual cache contents produced 8,730 output tokens per second against 4,429 for random or load-only routing, and cut the 90th-percentile TTFT from over 90 seconds to about half a second; a router that estimated cache contents from its own routing history reached 6,944 tokens per second and 31 seconds &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[21]&lt;/a&gt;. SGLang’s router keeps such an estimate, an approximate copy of each worker’s prefix tree &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[22]&lt;/a&gt;, and NVIDIA Dynamo 1.5.0, an orchestration layer that runs on top of engines such as vLLM, SGLang and TensorRT-LLM, routes “based on worker load and KV cache overlap” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[23]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The cost: cache affinity fights load balance, so good routers blend a cache score with a load score, and the cache takes memory that could hold more running requests.&lt;/p&gt;
&lt;h2 id=&quot;keep-long-prompts-from-stalling-everyone&quot;&gt;Keep long prompts from stalling everyone&lt;/h2&gt;
&lt;p&gt;One 50-page document arriving mid-stream can freeze every other user’s output while its prefill runs. Sarathi-Serve’s fix is chunked prefill: cut the prompt into pieces and mix each piece into ordinary decode steps. Against the vLLM of early 2024, the paper reports 2.6 times the serving capacity for a 7B model on one A100 and up to 3.7 times for a 34B model on two A100s &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;vLLM has since made chunked prefill the default and schedules pending decodes before any prefill &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[14]&lt;/a&gt;. What remains is the dial. In vLLM’s docs, smaller &lt;code&gt;max_num_batched_tokens&lt;/code&gt; values such as 2,048 “achieve better ITL”, larger ones “achieve better time to first token”, and values above 8,192 suit throughput, “especially for smaller models on large GPUs” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[14]&lt;/a&gt;. vLLM 0.30.0’s server already defaults to 8,192 on an H100 and 2,048 on an A100 &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[13]&lt;/a&gt;. SGLang calls the same dial &lt;code&gt;--chunked-prefill-size&lt;/code&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[20]&lt;/a&gt;. Set it against your written TTFT and ITL targets, measured at p99.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;em&gt;Timeline of GPU steps on one replica with five users streaming tokens while a 30,000-token prompt arrives. Without chunking, the prompt runs as one long prefill step and all five token streams stop until it ends, so inter-token latency spikes; the new user then gets a first token. With chunked prefill, the prompt is cut into eight equal slices, each riding along with a normal decode step; the five streams keep ticking at a slightly slower rate and the new user’s first token arrives a little later. A chunk-size scale shows the trade: small chunks such as 2,048 tokens keep streams smooth and delay the first token, large chunks such as 8,192 give an earlier first token and bumpier streams.&lt;/em&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/&quot;&gt;See the animated diagram in the post.&lt;/a&gt;&lt;/p&gt;&lt;figcaption&gt;Chunked prefill cuts a long prompt into slices and runs each one alongside ordinary decode steps, so other users keep receiving tokens while it loads. The two chunk sizes marked are the examples in vLLM’s docs [14].&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;spend-fewer-bytes-per-token&quot;&gt;Spend fewer bytes per token&lt;/h2&gt;
&lt;p&gt;Decode is limited by the bytes the GPU reads per token, so storing weights and cache in fewer bits gives more tokens per second.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FP8 weights and activations&lt;/strong&gt; halve weight memory and need Ada (L40S), Hopper (H100, H200) or Blackwell (B200, GB200) GPUs &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[24]&lt;/a&gt;. One study ran more than 500,000 accuracy evaluations on an open model family at 8B, 70B and 405B, and separately benchmarked inference speed in vLLM on A6000, A100 and H100 GPUs. It found FP8 “effectively lossless across all model scales” and well-tuned INT8 within 1 to 3% &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[25]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;INT4 weight-only&lt;/strong&gt; (AWQ &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[26]&lt;/a&gt;, GPTQ &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[27]&lt;/a&gt;) cuts weight memory about four times against FP16. The same study found it the most cost-efficient when requests run one at a time; under continuous batching, 8-bit formats reached the highest peak throughput, though 4-bit stayed more efficient at low latency &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[25]&lt;/a&gt;. Weight-only 4-bit speeds up memory-bound decode, and as the batch grows the GPU turns compute-bound: on Ampere GPUs, the authors of one 4-bit kernel report close to the full 4 times speed-up per layer up to batch 16 to 32, still significant but shrinking at 64 to 128, and up to 2.8 times end to end in vLLM &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[28]&lt;/a&gt;. For a busy server, that points to an 8-bit format first, unless tight latency limits keep the batch small.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NVFP4&lt;/strong&gt; stores 4-bit values in blocks of 16 that share an FP8 scale. NVIDIA reports about 3.5 times less memory than FP16 and 1.8 times less than FP8, and 1% or less accuracy loss moving a 671B mixture-of-experts (MoE) reasoning model from FP8 to NVFP4 &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[29]&lt;/a&gt;. FP4 arithmetic needs Blackwell, though older GPUs can still load NVFP4 weights: Nemotron 3.5 Lightning ships an NVFP4 checkpoint that runs on NVIDIA’s NVFP4 kernels across Blackwell, Hopper and Ampere &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[30]&lt;/a&gt;. On the older GPUs that saves memory and bandwidth, without the FP4 arithmetic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The KV cache&lt;/strong&gt; can be quantised separately, which raises the number of requests that fit. vLLM supports an FP8 KV cache and recommends calibrated scales &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[31]&lt;/a&gt;. With Qwen3.8-27B on an RTX PRO 6000 Blackwell, the SGLang, Qwen and NVIDIA teams fitted 70 concurrent requests at 32K context with an NVFP4 KV cache, against 44 with FP8. At matched concurrency, peak-batch decode throughput rose 26 to 30%. SGLang still marks the support experimental &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[32]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The cost is accuracy and hardware lock-in. Formats are tied to GPU generations, uncalibrated KV scales are a risk, and upgrades can break checkpoints: vLLM 0.30.0 dropped GPTQ activation ordering (the “act-order” option), so check any GPTQ checkpoint before you upgrade &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[13]&lt;/a&gt;. We quantise before we add GPUs, and judge each format on the evaluation set of the product it serves.&lt;/p&gt;
&lt;h2 id=&quot;draft-tokens-while-there-is-spare-compute&quot;&gt;Draft tokens while there is spare compute&lt;/h2&gt;
&lt;p&gt;Speculative decoding lets a cheap drafter propose several tokens and the target model verify them in one pass, keeping the target model’s output distribution &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[3]&lt;/a&gt;. Leviathan and colleagues report 2 to 3 times on an 11B encoder-decoder model &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[3]&lt;/a&gt;. Each target pass yields (1 − α&lt;sup&gt;γ+1&lt;/sup&gt;) / (1 − α) tokens on average, where α is the acceptance rate and γ the number of drafted tokens: at α = 0.8 and γ = 4, about 3.4. The price is extra arithmetic: each step runs γ+1 positions through the target model in parallel, so “the number of concurrent arithmetic operations grows by a factor of γ+1”, and the method helps only when compute is spare &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[3]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So the gain shrinks as the batch fills, and vLLM aims speculation at “medium-to-low QPS, memory-bound workloads” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[33]&lt;/a&gt;. EAGLE-3, a small draft head that reads the target model’s own hidden states, reports up to 6.5 times per-request speed-up on its best task and 1.38 times throughput at batch 64 in SGLang &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[34]&lt;/a&gt;. Plan a busy server around the second number.&lt;/p&gt;
&lt;figure&gt;&lt;p id=&quot;chart-per-user-speed-up-from-eagle-3-1-by-concurrency&quot;&gt;Per-user speed-up from EAGLE 3.1, by concurrency&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Concurrency 1: 2.03&lt;/li&gt;&lt;li&gt;Concurrency 4: 1.71&lt;/li&gt;&lt;li&gt;Concurrency 16: 1.66&lt;/li&gt;&lt;/ul&gt;&lt;figcaption&gt;Speed-up in each user’s output speed against no speculation, on a trillion-parameter-class MoE in NVFP4, tensor parallel across four GB200 GPUs, coding dataset [35].&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The chart shows the speed each user sees, the number speculation targets; even so, the gain narrows from 2.03 to 1.66 times between concurrency 1 and 16 &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[35]&lt;/a&gt;. P-EAGLE, a newer drafter, shows the same pattern against EAGLE-3: on a 20B open-weight MoE on one B200, its gain fell from 1.55 times at concurrency 1 to 1.05 times at 64 on one chat benchmark, and from 1.69 to 1.25 times on a coding benchmark &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[36]&lt;/a&gt;. The EAGLE 3.1 authors note that speculative decoding “often degrades under different chat templates, long-context inputs, or out-of-distribution system prompts”, the fragility 3.1 was built to reduce &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[35]&lt;/a&gt;, so track &lt;code&gt;vllm:spec_decode_num_accepted_tokens&lt;/code&gt; against &lt;code&gt;vllm:spec_decode_num_draft_tokens&lt;/code&gt; on your own prompts &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[33]&lt;/a&gt;. vLLM can also shrink the draft as load rises: dynamic speculative decoding sets the number of drafted tokens per batch-size range, and an opt-in adaptive verification mode (DSpark drafters only) sizes verification per request from the drafter’s confidence &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[33]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Multi-token prediction (MTP) trains the model to draft its own next tokens &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[37]&lt;/a&gt;. Nemotron 3.5 Lightning has it built in &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[30]&lt;/a&gt;, so there is no separate draft model to train or host; Gemma 4 ships ready-made MTP drafters that attach to the base model and share its embeddings, so there is nothing to train &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[38]&lt;/a&gt;. NVIDIA says “MTP is best suited for medium to high concurrency, with the optimal draft length decreasing as concurrency increases” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[30]&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;protect-the-latency-target&quot;&gt;Protect the latency target&lt;/h2&gt;
&lt;p&gt;Past a certain queue depth, one more request makes every request late. Cap concurrency where goodput peaks in your benchmark, reject or defer early with a retry signal (an HTTP 429 with a Retry-After header), and give interactive traffic priority: vLLM has &lt;code&gt;--scheduling-policy priority&lt;/code&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[39]&lt;/a&gt; and SGLang has priority scheduling &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[19]&lt;/a&gt;. Nightly jobs belong in their own lane, where “achieving a large batch size is the most important thing” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[20]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The cheapest request is one the big model never sees. We route classification and extraction to the smallest model that works, which takes that traffic off the serving fleet entirely; our post on &lt;a href=&quot;https://dravin.ai/blog/jev-and-typed-decision-models/&quot;&gt;typed decision models&lt;/a&gt; covers that lever.&lt;/p&gt;
&lt;h2 id=&quot;scale-out-across-gpus&quot;&gt;Scale out across GPUs&lt;/h2&gt;
&lt;p&gt;Tensor parallelism splits every layer across GPUs and needs fast links between them; pipeline parallelism gives each GPU a run of layers; data parallelism runs whole copies of the model (replicas). vLLM advises tensor parallelism within a node, and pipeline parallelism across nodes or on GPUs without NVLink, NVIDIA’s fast GPU-to-GPU link &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[40]&lt;/a&gt;. SGLang says to “always favor data parallelism” for throughput when memory allows &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[20]&lt;/a&gt;. In practice: the smallest tensor-parallel group that holds the weights and a healthy KV cache, then more replicas.&lt;/p&gt;
&lt;p&gt;MoE models such as DeepSeek V4, Kimi K3, GLM-5.3 and Mistral Large 3 activate a small share of their weights per token. Expert parallelism places experts on separate GPUs and works best with data parallelism; since tokens spread unevenly, vLLM’s &lt;code&gt;--enable-eplb&lt;/code&gt; rebalances experts from live load statistics &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[41]&lt;/a&gt;. The SGLang team combined expert parallelism with prefill-decode disaggregation for a 671B MoE on H100s; in a decode-only test on 9 nodes of 8 GPUs, with prefill capacity assumed unlimited, it reached about 22,300 output tokens per second per node for 2,000-token inputs, about 5 times a 16-GPU tensor-parallel baseline &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[42]&lt;/a&gt;. That needs both techniques, several nodes and fast interconnect; on one node, tensor parallelism and replicas are simpler.&lt;/p&gt;
&lt;h2 id=&quot;split-prefill-and-decode-last&quot;&gt;Split prefill and decode last&lt;/h2&gt;
&lt;p&gt;The final lever runs prefill and decode on separate GPU pools. DistServe found that “adding a single prefill job to a batch of decoding requests significantly slows down both processes”, and reports 7.4 times more requests or 12.6 times tighter SLOs than colocated systems, with over 90% of requests within their latency limits &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[7]&lt;/a&gt;. Splitwise reports 1.4 times the throughput at 20% lower cost, or 2.35 times the throughput at the same cost and power &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[2]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yet vLLM’s documentation says plainly: “Disaggregated prefill DOES NOT improve throughput” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[43]&lt;/a&gt;. Both claims hold. The papers measure goodput under latency targets and cost-normalised capacity; vLLM’s statement is best read as raw tokens per second. Disaggregation leaves the GPUs as fast as they were. What it changes is interference. Chunked prefill stops the worst stalls, but every mixed step still carries a slice of someone’s prompt, which adds to ITL, and colocated phases “share their resource and parallelism settings” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[7]&lt;/a&gt;. Separate pools remove both: decode steps carry no prompt work, and each pool gets its own parallelism and batch size, so more requests meet their targets and TTFT and ITL can be tuned separately &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[43]&lt;/a&gt;. The pools are sized separately too: longer prompts call for more prefill GPUs, longer answers for more decode GPUs.&lt;/p&gt;
&lt;figure&gt;&lt;p&gt;&lt;em&gt;Prefill and decode on separate GPU pools. Prompts of varying length arrive at a router, which scores replicas by prefix overlap and load, and go to a prefill pool where each GPU runs a wide, compute-heavy prefill. Each finished prompt’s KV cache crosses an RDMA link as a stream of packets to a decode pool, where many requests stream tokens in one large, steady batch. A TTFT gauge and an ITL gauge stay inside their target bands. Longer prompts call for more prefill GPUs; longer answers call for more decode GPUs.&lt;/em&gt; &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/&quot;&gt;See the animated diagram in the post.&lt;/a&gt;&lt;/p&gt;&lt;figcaption&gt;The router scores replicas by prefix overlap and load. Prefill hands each prompt’s KV cache to the decode pool over RDMA, so a long prompt does not stall the running decode streams. Shaded bands mark the latency targets.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Conditions matter. On GB200 with a 671B reasoning MoE, TensorRT-LLM’s rate-matched curves, which assume ideal pool ratios and free KV transfer, show 1.4 to 1.8 times the output throughput per GPU at the same per-user speed, with MTP off, at 4,400 input and 1,200 output tokens, mostly at lower concurrency; on 8,192-input, 256-output traffic its real end-to-end runs fell 0 to 25% short of the rate-matched curve &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[44]&lt;/a&gt;. SGLang moves the KV cache between pools with RDMA (remote direct memory access, which copies memory between machines without going through the CPU) transfer engines, on InfiniBand or RoCE devices or AWS EFA &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[45]&lt;/a&gt;; vLLM labels the feature experimental &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[43]&lt;/a&gt; and TensorRT-LLM beta &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[10]&lt;/a&gt;. For a single model on a single GPU, Dynamo’s README says the engine alone “is probably sufficient” &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/#sources&quot;&gt;[23]&lt;/a&gt;. Reach for disaggregation with long prompts, several nodes and fast interconnect. We treat it as the last step, after chunk size, routing and replicas have been tuned against goodput.&lt;/p&gt;
&lt;h2 id=&quot;checklist&quot;&gt;Checklist&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Write the SLO down: TTFT and ITL at p99, and the share of requests that must meet them.&lt;/li&gt;
&lt;li&gt;Benchmark on your own prompt lengths, output lengths, shared prefixes and arrival pattern, and report goodput.&lt;/li&gt;
&lt;li&gt;Dashboard queue depth, KV cache usage, preemptions and prefix-cache hit rate.&lt;/li&gt;
&lt;li&gt;Run a current engine with CUDA graphs on; size memory and batch settings until preemptions stop.&lt;/li&gt;
&lt;li&gt;Put stable prompt content first, confirm prefix caching is on, and route by prefix across replicas.&lt;/li&gt;
&lt;li&gt;Set the chunk size against your TTFT and ITL targets.&lt;/li&gt;
&lt;li&gt;Try FP8 on Hopper, NVFP4 on Blackwell, INT4 weight-only at low concurrency, and a quantised KV cache; check accuracy on your own evals.&lt;/li&gt;
&lt;li&gt;Add speculative decoding where concurrency is low to medium, try a model’s built-in MTP heads at higher concurrency with a shorter draft, and watch the acceptance rate.&lt;/li&gt;
&lt;li&gt;Cap concurrency where goodput peaks, prioritise interactive traffic and give batch jobs their own lane.&lt;/li&gt;
&lt;li&gt;Use the smallest tensor-parallel group that fits, then replicas; expert parallelism with load balancing for MoE.&lt;/li&gt;
&lt;li&gt;Split prefill and decode only with long prompts, several nodes and RDMA.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;sources&quot;&gt;Sources&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2403.02310&quot;&gt;Agrawal et al., “Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve”, OSDI 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2311.18677&quot;&gt;Patel et al., “Splitwise: Efficient generative LLM inference using phase splitting”, ISCA 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2211.17192&quot;&gt;Leviathan, Kalman and Matias, “Fast Inference from Transformers via Speculative Decoding”, ICML 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PyPI release pages, checked 23 Sep 2026: &lt;a href=&quot;https://pypi.org/project/vllm/&quot;&gt;vLLM&lt;/a&gt;, &lt;a href=&quot;https://pypi.org/project/sglang/&quot;&gt;SGLang&lt;/a&gt;, &lt;a href=&quot;https://pypi.org/project/tensorrt-llm/&quot;&gt;TensorRT-LLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html&quot;&gt;NVIDIA NIM docs, “LLM benchmarking metrics”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/benchmarking/cli/&quot;&gt;vLLM docs, “Benchmark CLI”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2401.09670&quot;&gt;Zhong et al., “DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving”, OSDI 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/usage/metrics/&quot;&gt;vLLM docs, “Production Metrics”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.usenix.org/conference/osdi22/presentation/yu&quot;&gt;Yu et al., “Orca: A Distributed Serving System for Transformer-Based Generative Models”, OSDI 2022&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvidia.github.io/TensorRT-LLM/overview.html&quot;&gt;TensorRT-LLM docs, “Overview” (updated 21 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2309.06180&quot;&gt;Kwon et al., “Efficient Memory Management for Large Language Model Serving with PagedAttention”, SOSP 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2305.13245&quot;&gt;Ainslie et al., “GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints”, 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vLLM v0.30.0 (22 Sep 2026): &lt;a href=&quot;https://github.com/vllm-project/vllm/releases/tag/v0.30.0&quot;&gt;release notes&lt;/a&gt;, tagged source &lt;a href=&quot;https://github.com/vllm-project/vllm/blob/v0.30.0/vllm/config/cache.py&quot;&gt;vllm/config/cache.py&lt;/a&gt; and &lt;a href=&quot;https://github.com/vllm-project/vllm/blob/v0.30.0/vllm/engine/arg_utils.py&quot;&gt;vllm/engine/arg_utils.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/configuration/optimization/&quot;&gt;vLLM docs, “Optimization and Tuning”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pytorch.org/blog/accelerating-pytorch-with-cuda-graphs/&quot;&gt;PyTorch blog, “Accelerating PyTorch with CUDA Graphs”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/design/cuda_graphs/&quot;&gt;vLLM docs, “CUDA Graphs”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/&quot;&gt;vLLM docs, “Automatic Prefix Caching”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2312.07104&quot;&gt;Zheng et al., “SGLang: Efficient Execution of Structured Language Model Programs”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SGLang v0.5.20 (18 Sep 2026): &lt;a href=&quot;https://github.com/sgl-project/sglang/releases/tag/v0.5.20&quot;&gt;release notes&lt;/a&gt; and &lt;a href=&quot;https://docs.sglang.io/docs/advanced_features/server_arguments&quot;&gt;server arguments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sglang.io/docs/advanced_features/hyperparameter_tuning&quot;&gt;SGLang docs, “Hyperparameter Tuning”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://llm-d.ai/blog/kvcache-wins-you-can-see&quot;&gt;Prefix-aware routing benchmark, “KV-Cache Wins You Can See” (24 Sep 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lmsys.org/blog/2024-12-04-sglang-v0-4/&quot;&gt;LMSYS blog, “SGLang v0.4” (4 Dec 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ai-dynamo/dynamo&quot;&gt;NVIDIA Dynamo README, v1.5.0 (21 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/features/quantization/llm_compressor/fp8/&quot;&gt;vLLM docs, “FP8 W8A8”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2411.02355&quot;&gt;Kurtic et al., “Give Me BF16 or Give Me Death? Accuracy-Performance Trade-Offs in LLM Quantization”, ACL 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2306.00978&quot;&gt;Lin et al., “AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration”, MLSys 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2210.17323&quot;&gt;Frantar et al., “GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers”, ICLR 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2408.11743&quot;&gt;Frantar et al., mixed-precision 4-bit inference kernels for LLMs (Aug 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.nvidia.com/blog/introducing-nvfp4-for-efficient-and-accurate-low-precision-inference/&quot;&gt;NVIDIA blog, “Introducing NVFP4 for Efficient and Accurate Low-Precision Inference” (24 Jun 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.nvidia.com/blog/nvidia-nemotron-3-5-lightning-delivers-fast-accurate-specialized-task-execution-for-long-running-agents/&quot;&gt;NVIDIA blog, Nemotron 3.5 Lightning (11 Aug 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/&quot;&gt;vLLM docs, “Quantized KV Cache”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lmsys.org/blog/2026-09-16-nvfp4-kv-cache&quot;&gt;LMSYS blog, “Accelerating Long-Context and Agentic Inference with NVFP4 KV Cache” (16 Sep 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vLLM docs: &lt;a href=&quot;https://docs.vllm.ai/en/latest/features/speculative_decoding/&quot;&gt;Speculative Decoding&lt;/a&gt;, &lt;a href=&quot;https://docs.vllm.ai/en/latest/features/speculative_decoding/dynamic_speculative_decoding/&quot;&gt;Dynamic Speculative Decoding&lt;/a&gt; and &lt;a href=&quot;https://docs.vllm.ai/en/latest/features/speculative_decoding/adaptive_verification/&quot;&gt;Adaptive Verification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2503.01840&quot;&gt;Li et al., “EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test”, NeurIPS 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vllm.ai/blog/2026-05-26-eagle-3-1&quot;&gt;vLLM blog, EAGLE 3.1 (26 May 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vllm.ai/blog/2026-03-13-p-eagle&quot;&gt;vLLM blog, “P-EAGLE: Faster LLM inference with Parallel Speculative Decoding in vLLM” (13 Mar 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2404.19737&quot;&gt;Gloeckle et al., “Better &amp;amp; Faster Large Language Models via Multi-token Prediction” (Apr 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.google.dev/gemma/docs/mtp/overview&quot;&gt;Google AI for Developers, Gemma MTP overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/configuration/engine_args/&quot;&gt;vLLM docs, “Engine Arguments”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/serving/parallelism_scaling/&quot;&gt;vLLM docs, “Parallelism and Scaling”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/serving/expert_parallel_deployment/&quot;&gt;vLLM docs, “Expert Parallel Deployment”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lmsys.org/blog/2025-05-05-large-scale-ep/&quot;&gt;SGLang team: large-scale expert parallelism on 96 H100s (May 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.vllm.ai/en/latest/features/disagg_prefill/&quot;&gt;vLLM docs, “Disaggregated Prefilling (experimental)”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nvidia.github.io/TensorRT-LLM/blogs/tech_blog/blog5_Disaggregated_Serving_in_TensorRT-LLM.html&quot;&gt;TensorRT-LLM tech blog, “Disaggregated Serving in TensorRT LLM” (updated 16 Mar 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sglang.io/docs/advanced_features/pd_disaggregation&quot;&gt;SGLang docs, “PD Disaggregation”&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post first appeared on &lt;a href=&quot;https://dravin.ai/blog/raising-llm-serving-throughput/&quot;&gt;Dravin AI&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>LLM serving</category><category>Inference optimisation</category><category>Self-hosted LLMs</category><category>GPUs</category><author>info@dravin.ai (Dravin AI)</author></item></channel></rss>