<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[TheAICommit]]></title><description><![CDATA[TheAICommit]]></description><link>https://theaicommit.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 15:55:27 GMT</lastBuildDate><atom:link href="https://theaicommit.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Forced JSON Output Is Always Valid and Sometimes Wrong]]></title><description><![CDATA[Structured output deletes the tokens your schema forbids, so the JSON always parses. When the model has no legal answer it guesses.]]></description><link>https://theaicommit.hashnode.dev/why-forced-json-output-is-always-valid-and-sometimes-wrong</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/why-forced-json-output-is-always-valid-and-sometimes-wrong</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Sat, 24 Oct 2026 14:59:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/94da316c-b57e-44f4-9a11-7f15d80051b5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Engineer's view.</strong> This is a required dropdown with no "Not applicable" option. Validation always passes, so the data looks clean. But users who had no right answer picked the first item, and your reports now count them. A JSON schema enforced token by token works the same way.</p>
<p><strong>TL;DR.</strong> Forced JSON mode removes the tokens your schema forbids and rescales the rest, so the output always parses. The model still chooses among legal tokens. When it wanted something illegal, it guesses, and the guess looks as sure as a real answer.</p>
</blockquote>
<h2>Explain Like I'm 5</h2>
<p>A teacher hands out a quiz where every question has three boxes. You must tick one. There is no box for "I don't know." So when you don't know, you tick a box anyway. The teacher sees a neat page with every box ticked. The teacher cannot tell which ticks you knew and which you guessed. The fix is easy: add a fourth box that says "I don't know."</p>
<h2>The Problem</h2>
<p>You have shipped this bug before, in a system with no AI in it.</p>
<p>A signup form asks for a country. The field is required, and the dropdown has no "Other" option. Users from a missing country still have to submit. So they pick the first item in the list. Months later, an analyst asks why so many users live in Afghanistan. The validation never failed once. It only made sure every answer was a legal one.</p>
<p><strong>Structured output</strong> has the same bug. You give the model a JSON schema, and the engine guarantees the response matches it. OpenAI, Gemini, vLLM, SGLang and llama.cpp all offer this. Teams switch it on to stop parse errors, and the parse errors stop.</p>
<p>Then quieter failures start. FriendliAI ran Gemma 4 31B against a complex schema. The output filled with newlines until it hit the token limit, and the JSON never closed. In other pipelines, a field limited to three values gets one of the three, even for a document the model cannot read. The value looks as sure as any other.</p>
<p>All of these come from one rule. The engine decides which tokens are legal. The model still chooses among them, and it must choose even when it wanted to say something else.</p>
<p><strong>The fix: give the model a legal way to say what it means, bound what it can repeat, and measure how much it was forced.</strong></p>
<h2>The Fix: Give the Model a Legal Way Out</h2>
<h3>How does the engine force valid JSON?</h3>
<p>A model writes one <strong>token</strong> at a time, a word or a piece of one. At each step it gives every token in its vocabulary a probability. <strong>Constrained decoding</strong> adds one step. The engine checks the grammar built from your schema, deletes every token that would break it, and rescales the rest to sum to one. Then it samples as usual.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-24/fig-1.png" alt="One token of a forced JSON field" style="display:block;margin:0 auto" />

<h3>So what happens when the model wanted an illegal token?</h3>
<p>Its wish is deleted, and the leftovers are scaled up. Take a field that must be <code>high</code>, <code>medium</code> or <code>low</code>. On a clear invoice, those three hold 95% of the model's probability. On a blurry scan, they hold 15%, because the model wanted to write "unknown." After rescaling, the blurry scan still gets <code>high</code> 40% of the time. Both outputs look the same.</p>
<h3>Why does output sometimes fill with whitespace?</h3>
<p>JSON allows spaces and newlines between fields, so the grammar never deletes them. A little leftover whitespace cannot do this alone. Even at a 99% share each step, filling 2,048 tokens has odds of about one in a billion. The loop needs the model to be nearly certain at every step. That happens when repetition feeds itself: each newline makes the next more likely.</p>
<h3>Does the order of fields matter?</h3>
<p>The engine writes fields in the order the schema lists them, left to right. A <code>confidence</code> value written before a <code>reasoning</code> field cannot depend on that reasoning.</p>
<h2>What This Means for You</h2>
<p><strong>When this matters.</strong> You use JSON mode, a response schema or strict tool calls, and your code acts on a field's value. Agents do this constantly. A tool call is a schema the model must fill in. A routing step reads one enum. An extraction job writes rows to a database.</p>
<p><strong>How it affects you.</strong> Parse-error rates drop to zero, so dashboards built on them look healthy. The failures move into the values. A forced pick is a wrong row nobody flags. A whitespace loop is a timeout, or a bill for 2,048 tokens of nothing.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-24/fig-2.png" alt="The blurry scan: what the model wanted vs. what it wrote" style="display:block;margin:0 auto" />

<p>This is the same bug as the country dropdown. Validation guarantees that every answer is a legal one. It says nothing about whether the model had an answer.</p>
<p><strong>What to do about it.</strong> Today, open the schemas your code acts on. For every enum, ask what the model should say when it does not know. If there is no legal answer for that, add one. Next, put any reasoning field before the fields that depend on it. Whether that improves accuracy depends on the task and the model. It is cheap to test, so measure it on yours rather than assume. Then cap output length and treat a truncated response as a failure. If you host the model, switch off free whitespace and start logging how much probability the grammar removed.</p>
<h2>Implementing It</h2>
<p><strong>The change.</strong></p>
<p>Four roles touch this: whoever writes the schema, whoever writes the prompt, whoever runs the server, and whoever reads the response.</p>
<p><em>Role 1, the schema.</em> Give every field your code acts on a legal "I don't know." Put reasoning first. Bound free strings where your provider's schema subset allows <code>maxLength</code>:</p>
<pre><code class="language-python">from typing import Literal
from pydantic import BaseModel, Field

class Invoice(BaseModel):
    reasoning: str = Field(max_length=600)        # written first, read by the fields below
    vendor: str | None = Field(max_length=120)    # None is the legal "not on the page"
    total: float | None
    confidence: Literal["high", "medium", "low", "unknown"]
</code></pre>
<p>Lint every schema in CI with <code>check_schema()</code> from <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-24/code_example.py"><code>code_example.py</code></a>. It flags an enum with no way out, an unbounded string, and an answer written before its reasoning.</p>
<p><em>Role 2, the prompt.</em> Include one filled-in example of the output. FriendliAI's whitespace loop went away when the prompt carried a concrete example. The mask only removes options; the example puts probability on the right ones. Use a real record, not placeholders. An example full of <code>"string"</code> teaches the model to write <code>"string"</code>. Show an <code>unknown</code> case too, so the model sees that the way out is allowed.</p>
<p><em>Role 3, the server, if you host the model.</em> vLLM can forbid whitespace between JSON fields, which removes the loop's open door:</p>
<pre><code class="language-bash">vllm serve &lt;model&gt; \
  --structured-outputs-config '{"backend":"xgrammar","disable_any_whitespace":true}'
</code></pre>
<p>It works only with the <code>xgrammar</code> and <code>guidance</code> backends. With <code>transformers</code>, wrap your grammar processor to record how much probability it removed:</p>
<pre><code class="language-python">import torch
from transformers import LogitsProcessor

class KeptMass(LogitsProcessor):
    def __init__(self, grammar):                  # any processor that sets illegal tokens to -inf
        self.grammar, self.kept = grammar, []
    def __call__(self, input_ids, scores):
        before = torch.softmax(scores.float(), dim=-1)
        out = self.grammar(input_ids, scores.clone())
        self.kept.append((before * torch.isfinite(out)).sum(-1))
        return out
</code></pre>
<p><em>Role 4, the caller.</em> A cut-off response is a failure, not something to repair:</p>
<pre><code class="language-python">resp = client.chat.completions.create(model=MODEL, messages=msgs,
    response_format={"type": "json_schema", "json_schema": SCHEMA}, max_tokens=800)
choice = resp.choices[0]
if choice.finish_reason == "length":
    raise TruncatedOutput(resp.id)                # retry or route to review; never parse
row = Invoice.model_validate_json(choice.message.content)
</code></pre>
<p>Gemini reports the same case as <code>MAX_TOKENS</code> in <code>finishReason</code>. Count truncations per schema, because one bad schema hides inside a healthy average.</p>
<p><strong>How you know it worked.</strong> Three numbers per field, on a week of traffic. The truncation rate should be near zero. The <code>unknown</code> rate should be above zero, and roughly match how often a human finds no answer in a sample. If it is exactly zero, the model is still being forced somewhere. If you host, look at kept mass on value tokens only. Structure tokens like <code>{</code> lose mass on every request for harmless reasons. A value position that kept under half the model's probability is a guess worth reviewing.</p>
<h2>When Forcing JSON Is the Wrong Tool</h2>
<p>When the model needs room to think. A schema that starts on token one leaves no space for free reasoning. For hard tasks, let the model answer in plain text first. Then make a second, cheap call that formats the answer into JSON.</p>
<p>When your schema is huge or changes per request. The engine compiles each new grammar before the first token. A cached schema costs little, but a new one per request adds delay to every first token. Grammar engines cache compiled schemas for this reason.</p>
<p>When you use a small model on a hard schema. Forcing makes the output parse. It cannot make a weak model know the answer. Nested optional fields are where small models fill values with whatever is left.</p>
<p>When the provider's schema subset cannot express your rules. Some keywords are silently unsupported or rejected. Check the response against the full schema in your own code anyway.</p>
<p>Before you adopt it, ask:</p>
<ul>
<li>What should each field say when the model does not know?</li>
<li>Which fields does my code act on, and is anything written before its reasoning?</li>
<li>How would I notice a value the model was forced into?</li>
</ul>
<h2>Glossary</h2>
<ul>
<li><strong>token</strong> — a word or piece of a word; the model writes one at a time.</li>
<li><strong>Structured output</strong> — an API mode that guarantees the response matches a JSON schema you supply.</li>
<li><strong>Constrained decoding</strong> — deleting the tokens a grammar forbids at each step, then sampling from what is left.</li>
<li><strong>enum</strong> — a schema field limited to a fixed list of allowed values.</li>
<li><strong>kept mass</strong> — the share of the model's probability that survives the grammar's deletion at one position.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><a href="https://friendli.ai/blog/structured-output">Structured Output Requires More Than Guided Generation</a> — FriendliAI Tech &amp; Research</li>
<li><a href="https://docs.vllm.ai/en/latest/api/vllm/config/structured_outputs/">vLLM: StructuredOutputsConfig</a> — vLLM documentation</li>
<li><a href="https://blog.mlc.ai/2024/11/22/achieving-efficient-flexible-portable-structured-generation-with-xgrammar">Achieving Efficient, Flexible, and Portable Structured Generation with XGrammar</a> — MLC team (CMU and collaborators)</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://theaicommit.com/2026-10-24-why-forced-json-output-is-always-valid-and-sometimes-wrong/"><em>theaicommit.com</em></a><em>, a daily AI engineering lab for software engineers. The original has an</em> <a href="https://theaicommit.com/2026-10-24-why-forced-json-output-is-always-valid-and-sometimes-wrong/"><em>interactive version</em></a> <em>you can play with, and the full runnable code is on</em> <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-24/code_example.py"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why Split Prefill and Decode GPUs Lose Time in the Hand-Off, Not the Network]]></title><description><![CDATA[Moving the KV cache takes 10 ms, but first-token time hit 1.7 s. How vLLM's pull hand-off stalls the decode loop, and how to size decode slots and fix it.]]></description><link>https://theaicommit.hashnode.dev/why-split-prefill-and-decode-gpus-lose-time-in-the-hand-off-not-the-network</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/why-split-prefill-and-decode-gpus-lose-time-in-the-hand-off-not-the-network</guid><category><![CDATA[llm]]></category><category><![CDATA[GPU]]></category><category><![CDATA[vLLM]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Fri, 23 Oct 2026 18:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/bb9142d2-84c6-4685-9eb3-2aa5282e08f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Engineer's view.</strong> This is a Node.js server doing a synchronous parse on every upload. Each parse is quick, but it blocks the one loop every connection shares, so every response slows. With a fixed connection pool, slower responses then mean waiting for a free connection. The decode server's hand-off work behaves the same way.</p>
<p><strong>TL;DR.</strong> When prompt processing and token generation run on separate GPUs, each request's cache must cross between them. The bytes are cheap. Setup work on the generation server's loop slows every token, and that can run the server out of slots.</p>
</blockquote>
<h2>Explain Like I'm 5</h2>
<p>A bakery has one person mixing dough and another baking it. The mixer hands each bowl to the baker. Passing a bowl takes a second. But this baker stops all the ovens to write a label for every new bowl. Each label is quick. When bowls arrive fast, the ovens are stopped so often that every loaf bakes slower. Soon the oven shelves are full, and new bowls wait in line.</p>
<h2>The Problem</h2>
<p>You have shipped this bug before, in a system with no AI in it.</p>
<p>An API reads from a database through a pool of 20 connections. A release adds a small synchronous step to each query. Every query now holds its connection a little longer. At normal traffic nobody notices. At peak, all 20 connections are busy, and requests wait in line for one. The step costs milliseconds. The wait costs seconds. Nobody finds it by looking at the database, because the database is fine.</p>
<p><strong>Disaggregated serving</strong> has the same bug. It splits an LLM server in two. <strong>Prefill</strong> GPUs read the whole prompt. <strong>Decode</strong> GPUs then write the answer one token at a time. Splitting them stops long prompts from stalling other users' answers. In between, each request's <strong>KV cache</strong>, the model's notes on the prompt, moves from one pool to the other.</p>
<p>Teams blame the network when the first token is slow. The numbers say otherwise. vLLM measured Qwen3-32B on two eight-GPU H200 servers. At 8 requests per second with 16,000-token prompts, the first token took about 1,744 ms with the default hand-off. A different hand-off cut it to about 540 ms. Moving that prompt's cache takes about 10 ms. <a href="https://theaicommit.com/#2026-10-11">How to Work Out How Much GPU Memory One Request Really Needs</a> sized that cache.</p>
<p>The lost second is setup work. Part of it runs on the decode server's main loop, where it pauses every request in progress.</p>
<p><strong>The fix: keep hand-off work off the decode loop, and size decode slots with headroom. Then the split pays for itself.</strong></p>
<h2>The Fix: Keep Hand-Off Work Off the Decode Loop</h2>
<h3>Why split prefill and decode at all?</h3>
<p>Prefill is limited by arithmetic. Decode is limited by memory reads. On one server, a long prefill delays every answer being written. Splitting them lets you tune first-token time and per-token time separately. vLLM's own guide is blunt about the cost: disaggregation does not improve throughput. It buys predictable latency.</p>
<h3>How much do the bytes actually cost?</h3>
<p>Very little. Qwen3-32B stores 256 KiB per token. A 16,000-token prompt is 4.2 GB. With tensor parallelism across 8 GPUs, each GPU sends its own slice over its own links. At 50 GB/s per GPU, that takes about 10 ms.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-23/fig-1.png" alt="Where a request's first token waits" style="display:block;margin:0 auto" />

<h3>Then where does the second go?</h3>
<p>Into two places. First, pull runs its steps in series after prefill. The router relays where the blocks are. Decode allocates space. Then decode reads the blocks. Second, decode does that setup on its main loop. That loop also produces every token for every request. While it posts transfer work, nobody gets a token.</p>
<p>So the loop loses arrivals per second times setup time per request. A vLLM pull request measured 14.7 ms of posting per request at 2,000-token prompts. At 8 requests per second, the loop loses 12%. Moving that work to a thread took time per token from 6.14 ms to 5.52 ms.</p>
<h3>Why does a slower token slow the first token?</h3>
<p>Because a request holds a decode slot until its answer ends. Slots held equals arrivals times output tokens times time per token. That is <strong>Little's law</strong>. Past <code>--max-num-seqs</code>, new requests wait for a slot before their cache can land.</p>
<h3>What does push change?</h3>
<p>Decode registers its free blocks as soon as the request arrives. Prefill writes straight into them from a background thread. Nothing waits after prefill, and nothing runs on the decode loop.</p>
<h2>What This Means for You</h2>
<p><strong>When this matters.</strong> You run prefill and decode on separate GPUs with vLLM, SGLang, Dynamo or llm-d. Your prompts are long, and your traffic is high.</p>
<p><strong>How it affects you.</strong> Look at vLLM's 16,000-token run at 8 requests per second. Time per token rose from 7.0 ms with push to 10.4 ms with pull. With 128-token answers, that means 7.2 slots held with push and 10.6 with pull. The benchmark started its servers with <code>--max-num-seqs 8</code>. So pull asked for more slots than existed. That turns a few milliseconds per token into a second of waiting.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-23/fig-2.png" alt="Decode slots held at 8 requests per second, 16k prompts" style="display:block;margin:0 auto" />

<p>This is the same arithmetic as the connection pool. A small per-request cost decides nothing until the pool is nearly full. Then it decides everything. It also explains why the two modes match at low load. At 1 request per second, even pull holds barely one slot. The setup still happens, but nobody is waiting behind it.</p>
<p><strong>What to do about it.</strong> Today, compute slots held for your own traffic at peak. Use your measured time per token, not the benchmark's. Keep it under about 80% of <code>--max-num-seqs</code>. Then move hand-off work off the decode loop, and re-measure. If you cannot change the hand-off this quarter, raise <code>--max-num-seqs</code> only if decode memory allows it. More slots need more KV cache space on every decode GPU.</p>
<h2>Implementing It</h2>
<p><strong>The change.</strong></p>
<p>Three roles touch this. Whoever plans capacity sizes the decode pool. Whoever owns the serving config picks the hand-off. Whoever runs the load test proves it.</p>
<p><em>Role 1, capacity.</em> Compute the three numbers for your model and traffic. The first is usually small. The third is the one that bites:</p>
<pre><code class="language-python">def kv_bytes(tokens, layers, kv_heads, head_dim, dtype_bytes=2):
    return tokens * layers * 2 * kv_heads * head_dim * dtype_bytes

def transfer_ms(nbytes, link_gb_s, tp=1):
    return nbytes / tp / (link_gb_s * 1e9) * 1000   # each GPU sends its slice

def slots_held(qps, out_tokens, tpot_ms):
    return qps * out_tokens * tpot_ms / 1000         # Little's law

transfer_ms(kv_bytes(16_000, 64, 8, 128), 50, tp=8)  # 10.5 ms
slots_held(8, 128, 10.4)                             # 10.6, with 8 slots
</code></pre>
<p>Take <code>tpot_ms</code> from production at peak, not from a quiet benchmark. Use your real output length, too. Agents that write long answers hold slots far longer than chat does. A 2,000-token agent answer holds a slot sixteen times longer than a 128-token chat reply. So the same request rate needs sixteen times the slots.</p>
<p><em>Role 2, the serving config.</em> The connector is a setting in <code>--kv-transfer-config</code> on both servers. The vLLM post ran push with this, on the prefill side:</p>
<pre><code class="language-bash">vllm serve Qwen/Qwen3-32B --tensor-parallel-size 8 --max-num-seqs 8 --port 8100 \
  --kv-transfer-config '{"kv_connector":"NixlPushConnector","kv_buffer_device":"cuda",
    "kv_role":"kv_producer","engine_id":"prefill-engine-001","kv_rank":0,
    "kv_parallel_size":2,"kv_connector_extra_config":{"backends":["LIBFABRIC"]}}'
</code></pre>
<p>The decode server uses the same connector with <code>"kv_role":"kv_consumer"</code>, as in vLLM's NIXL guide. Mixing modes crashes the server after the first request. Push also needs a proxy that knows the prefill server's address up front. The post uses <code>disagg_proxy_pushconnector_demo.py</code>. Pull stays the right choice if you route each request to the least-loaded decode server after prefill. Push picks decode early, so it gives that up.</p>
<p><em>Role 3, measuring.</em> Sweep request rate and prompt length together, because the gap only opens at the top of both:</p>
<pre><code class="language-bash">for qps in 1 2 4 8; do for len in 1024 16384; do
  vllm bench serve --model Qwen/Qwen3-32B --request-rate $qps \
    --random-input-len $len --random-output-len 128 --num-prompts 400
done; done
</code></pre>
<p><strong>How you know it worked.</strong> Push and pull should match at low rate and short prompts. At the top of the sweep, time per token should stay near its light-load value. Watch <code>vllm:num_requests_waiting</code> on the decode server. It should stay near zero at your peak rate. If it climbs, you are out of slots, whatever the hand-off. Then split first-token time into its parts. Compare the prefill server's own first-token time with the proxy's. The difference is the hand-off plus any wait for a slot. After the change, that difference should shrink most at long prompts and high rates.</p>
<h2>When Splitting Prefill and Decode Is the Wrong Tool</h2>
<p>When you want more throughput. Splitting does not add any. It trades GPUs for steadier latency.</p>
<p>When your prompts are short. Prefill then barely disturbs decode, so there is little interference to remove. Chunked prefill on one server may be enough.</p>
<p>When you have one GPU node. The hand-off then crosses no network, but you still pay for two pools and a proxy.</p>
<p>When you need load-aware decode routing. Push chooses the decode server before prefill finishes. If your decode servers run unevenly, pull's late choice may be worth its setup cost.</p>
<p>When the decode pool is already full. No hand-off fixes too few slots. Add decode capacity first, then tune the hand-off.</p>
<p>Before you adopt it, ask:</p>
<ul>
<li>How many decode slots does my peak traffic hold, by Little's law?</li>
<li>Is my first-token time spent in prefill, in the hand-off, or in a queue?</li>
<li>Do I need to route decode after prefill, or can I choose early?</li>
</ul>
<h2>Glossary</h2>
<ul>
<li><strong>Disaggregated serving</strong> — running prompt reading and answer writing on separate GPU pools.</li>
<li><strong>Prefill</strong> — the step that reads the whole prompt and builds its cache.</li>
<li><strong>Decode</strong> — the step that writes the answer, one token at a time.</li>
<li><strong>KV cache</strong> — the model's stored notes on every token so far, kept on the GPU.</li>
<li><strong>Little's law</strong> — items in a system equal arrival rate times time each one stays.</li>
<li><strong>slot</strong> — one of the requests a decode server can generate for at once.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><a href="https://vllm.ai/blog/2026-10-14-nixl-push-connector">Push-Based KV Transfer to Improve TTFT and TPOT for Disaggregated Inference in vLLM</a> — vLLM Blog (Sunita Nadampalli, AWS, and Nicolò Lucchesi, Mistral)</li>
<li><a href="https://github.com/vllm-project/vllm/pull/45211">vLLM pull request 45211: offload NIXL READ submission to a dedicated reader thread</a> — vLLM project, GitHub</li>
<li><a href="https://haoailab.com/blogs/distserve/">Throughput is Not All You Need: Maximizing Goodput in LLM Serving using Prefill-Decode Disaggregation</a> — Hao AI Lab, UC San Diego (the DistServe authors)</li>
<li><a href="https://docs.vllm.ai/en/latest/features/disagg_prefill/">vLLM: Disaggregated Prefilling</a> — vLLM documentation</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://theaicommit.com/2026-10-23-why-split-prefill-and-decode-gpus-lose-time-in-the-hand-off/"><em>theaicommit.com</em></a><em>, a daily AI engineering lab for software engineers. The original has an</em> <a href="https://theaicommit.com/2026-10-23-why-split-prefill-and-decode-gpus-lose-time-in-the-hand-off/"><em>interactive version</em></a> <em>you can play with, and the full runnable code is on</em> <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-23/code_example.py"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Designing a per-user AI cost meter: idempotency, cache pricing and aliases]]></title><description><![CDATA[How FiscaLLM keeps per-user AI cost numbers trustworthy: idempotent events, fail-open caps, cached-token pricing, unpriced models and RevenueCat aliases.]]></description><link>https://theaicommit.hashnode.dev/designing-a-per-user-ai-cost-meter-idempotency-cache-pricing-and-aliases</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/designing-a-per-user-ai-cost-meter-idempotency-cache-pricing-and-aliases</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[meter]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Fri, 23 Oct 2026 00:06:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/aa919423-710b-438f-af4e-7ea413380d4d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI apps have a margin problem that normal SaaS metrics hide: price per user is flat, cost per user is variable. I built <a href="https://github.com/magna56/fiscallm">FiscaLLM</a> (MIT) to join the two. Most of the work wasn't the dashboard. It was making the numbers trustworthy. These are the design decisions that mattered.</p>
<h2>The shape of the system</h2>
<ul>
<li>An SDK on the developer's backend reports <code>{ userId, model, token counts, feature? }</code> after each AI call. It never sees prompts or answers.</li>
<li>A webhook (RevenueCat, Stripe beta) or a revenue API reports payments.</li>
<li>The server joins both on user ID and computes margin per user, plan and free trial.</li>
</ul>
<p>Stack: TypeScript, Next.js, Hono, Postgres (Supabase), Drizzle.</p>
<h2>1. Idempotent events</h2>
<p>Networks retry. If a batch is sent twice, cost doubles and every margin number is wrong. Each event carries a client-generated UUID and the server dedupes on it, so retries are always safe. The SDK times out, retries once, then drops the batch rather than block the app.</p>
<h2>2. Fail open</h2>
<p>A metering tool must never take down the product it measures. Tracking is batched in the background, and the optional spending cap works the same way: <code>meter.check(userId)</code> runs before an AI call and fails open, so if FiscaLLM is unreachable the call goes through. The trade-off is that caps use cost already recorded, so a user can go slightly over before being refused.</p>
<pre><code class="language-ts">const meter = createMeter({ apiKey: process.env.FISCALLM_KEY, waitUntil });
const res = await openai.chat.completions.create({ model, messages });
meter.trackOpenAI(res, { userId, feature: "chat" });
</code></pre>
<h2>3. Price cached tokens separately</h2>
<p>Providers charge cached input tokens at a different rate. Treating all input tokens the same overstates cost for apps with long, stable prompts. Cached tokens carry their own price.</p>
<h2>4. Never count an unknown model as zero</h2>
<p>A new model ID with no price entry would silently show $0 and make a user look profitable. Unknown models are flagged as <strong>unpriced</strong> instead, so the gap is visible. Built-in prices are checked against provider pages in <a href="https://github.com/magna56/fiscallm/blob/main/ACCURACY.md">ACCURACY.md</a>.</p>
<h2>5. Identity: RevenueCat aliases</h2>
<p>A user is often anonymous first, then logs in. RevenueCat reports aliases, and if you don't merge them you split one person's revenue and cost across two IDs. FiscaLLM resolves aliases so both sides land on one user.</p>
<h2>What's not solved</h2>
<ul>
<li>Needs a backend between the app and the AI provider.</li>
<li>Stripe is beta, tested on sample payloads only, USD only. No direct App Store or Play connection.</li>
<li>Prices haven't been compared with a real invoice over weeks.</li>
<li>No customers yet; the demo runs on sample data.</li>
</ul>
<h2>Try or self-host</h2>
<ul>
<li>Demo (sample data, no sign-up): <a href="https://fiscallm.com/demo?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=launch">https://fiscallm.com/demo?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=launch</a></li>
<li>Repo: <a href="https://github.com/magna56/fiscallm">https://github.com/magna56/fiscallm</a>. Self-host with <code>SELF-HOSTING.md</code> so your data never touches a third party's servers, or use the free hosted version.</li>
<li>Roadmap: <a href="https://github.com/users/magna56/projects/1">https://github.com/users/magna56/projects/1</a></li>
</ul>
<p>I'd like feedback on the design, especially from anyone running AI costs at scale.</p>
]]></content:encoded></item><item><title><![CDATA[Why Speculative Decoding Slows Hybrid Mamba Models at Large Batch Sizes]]></title><description><![CDATA[On hybrid Mamba models, speculative decoding saves a full state copy per draft token. At large batch sizes that erases the speed-up. Here's the fix in vLLM]]></description><link>https://theaicommit.hashnode.dev/why-speculative-decoding-slows-hybrid-mamba-models-at-large-batch-sizes</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/why-speculative-decoding-slows-hybrid-mamba-models-at-large-batch-sizes</guid><category><![CDATA[LLM Engineering AI Infrastructure Python GPU Optimization Performance Engineering Machine Learning Systems DevOps        Artificial Intelligence Software Engineering Programming Cloud Computing Deep Learning]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Thu, 22 Oct 2026 23:53:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/847a3e26-5b1f-4d9c-bc1e-bc5722ef4d62.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Engineer's view.</strong> This is a database that rewrote the whole table file on every transaction so it could roll back. Real databases append each change to a log, checkpoint now and then, and roll back by truncating the log. A Mamba layer can store its state the same way.</p>
<p><strong>TL;DR.</strong> A Mamba layer overwrites its state every token, so undoing a rejected guess needs a saved copy. At large batch sizes those copies cost more than speculation saves. Logging each token's inputs and checkpointing now and then makes undo a pointer move.</p>
</blockquote>
<h2>Explain Like I'm 5</h2>
<p>You are writing a story on a whiteboard, and a friend guesses your next four words. You write them in. If a guess is wrong, you must erase back to where you were. On paper you just cross out the last words. On a whiteboard that gets wiped and redrawn every word, you need a photo of the board before each guess. Taking all those photos is slow. A faster trick: photograph the board once in a while, and keep a list of the words since then.</p>
<h2>The Problem</h2>
<p>You have shipped this bug before, in a system with no AI in it.</p>
<p>A collaborative editor needs undo. The first version saves a full copy of the document before every edit. It works in testing. In production, a hundred people type into large documents at once. Each keystroke now writes megabytes, and the disk spends its time copying documents nobody will ever restore. The edits are cheap. The copies are not. The fix everyone lands on is an operation log: store each edit, which is small, and save a full snapshot now and then. Undo drops the newest edits from the log.</p>
<p>Hybrid models have the same bug inside speculative decoding. <strong>Speculative decoding</strong> lets a cheap drafter guess the next few tokens, and the big model checks them all in one pass. Wrong guesses are thrown away. <a href="https://theaicommit.com/#2026-09-30">How a Model Writes Several Words per Step</a> covered why that pays.</p>
<p>Hybrid models mix two kinds of layer. Attention layers keep a cache that only grows. A <strong>Mamba layer</strong> keeps one fixed-size state per request and overwrites it on every token. Nemotron-3-Super has 40 Mamba layers and 8 attention layers. Each Mamba state is 4 MiB per request. That is as much as one attention layer stores for about 8,000 tokens. <a href="https://theaicommit.com/#2026-10-11">How to Work Out How Much GPU Memory One Request Really Needs</a> sized that fixed part. Here it is the traffic that hurts.</p>
<p>To throw away a guess, an attention layer moves a pointer back. A Mamba layer cannot, because the old state is gone. So the server saves a full copy of the state after every draft token. With four drafts, that is four copies per layer, per request, per step.</p>
<p><strong>The fix: store each token's small inputs plus a periodic checkpoint, the way the editor stores an operation log. Then throwing away a guess is a pointer move again.</strong></p>
<h2>The Fix: Log the Inputs, Checkpoint the State</h2>
<p>New to what a model stores between tokens? Start at AI basics → <a href="https://theaicommit.com/#learn/tokens-and-sampling">Tokens and sampling</a>.</p>
<h3>Why are the copies so expensive?</h3>
<p>Because producing a token is limited by how fast the GPU reads and writes memory. The weights are read once per step and shared by the whole batch. The state is per request. So as the batch grows, state traffic grows with it and soon outweighs the weights. Every copy adds a full state's worth of writes.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-22/fig-1.png" alt="Undoing a rejected draft token, three ways" style="display:block;margin:0 auto" />

<h3>How does a log replace the state?</h3>
<p>A Mamba-2 layer updates its state with one rule: decay the old state, then add the new token's contribution. That rule can be unrolled. Today's output is the checkpoint, decayed, plus each logged token's contribution, decayed by how long ago it came. So the layer computes outputs straight from the checkpoint and the log. It never writes the state in between.</p>
<h3>When does the state get written?</h3>
<p>When the log fills. NVIDIA and Tri Dao call this <strong>ReplaySSM</strong>. With a window of 16 tokens, the full state is written once every 16 steps instead of every step. Rejected drafts are never added to the log.</p>
<h3>Does it change the answers?</h3>
<p>No. The math is the same, rearranged. <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-22/code_example.py"><code>code_example.py</code></a> checks this on random drafts and rejections. The two methods differed by under a millionth of a millionth.</p>
<h2>What This Means for You</h2>
<p><strong>When this matters.</strong> You serve a hybrid model yourself. Nemotron 3, Qwen3.5 and Qwen3-Next mix Mamba-style layers with attention. You run speculative decoding, often through the model's own built-in drafter. And your batches are large.</p>
<p><strong>How it affects you.</strong> Speculation can quietly make you slower. NVIDIA measured vLLM's speculation on these models. It stopped gaining beyond batch 128. On Qwen3.5-122B-A10B it fell below plain decoding at batch 512. Checking 8 draft tokens took nearly five times a normal step. With replay, the same check took about one normal step.</p>
<p>This is a second wall, on top of the one in the earlier article. That one was about idle compute: speculation only uses arithmetic the GPU was not using. This one is about memory traffic, and it arrives sooner, because every request in the batch pays for its own copies.</p>
<p>The rule underneath is simple to compute. Once state traffic dominates, a step with copies moves about (1 + T) / 2 times the state of a plain step, where T is the number of drafts. Speculation wins only if each step keeps more tokens than that.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-22/fig-2.png" alt="Speculation with state copies, vs plain decoding (model)" style="display:block;margin:0 auto" />

<p><strong>What to do about it.</strong> Today, find the batch size your hybrid model really runs at. Then turn speculation off above it, or turn on replay. The next section shows both.</p>
<h2>Implementing It</h2>
<p><strong>The change.</strong></p>
<p>Three roles touch this. Whoever sizes the deployment computes the break-even. Whoever owns the serving config turns replay on. Whoever runs the benchmark proves it.</p>
<p><em>Role 1, sizing.</em> Read your model's acceptance from the server metrics, then compute both sides of the rule:</p>
<pre><code class="language-python">def tokens_kept(p, T):
    """The real token, plus drafts until the first miss."""
    return sum(p**i for i in range(T + 1))

def copy_rule(p, T):
    """Above 1, speculation with state copies still wins once state traffic dominates."""
    return tokens_kept(p, T) / ((1 + T) / 2)

copy_rule(0.6, 4)   # 0.92: loses at large batch
copy_rule(0.8, 4)   # 1.34: still wins
</code></pre>
<p>Read acceptance from your own traffic, not a benchmark. Code completion keeps more drafts than open-ended chat, so the same model can sit on both sides of the line.</p>
<p>The state size per request is Mamba layers × heads × head size × state size × bytes. For Nemotron-3-Super that is 40 layers of 4 MiB, about 160 MiB per request, read every step.</p>
<p><em>Role 2, the serving config.</em> Upstream vLLM has two flags for Mamba-2 models, both off by default:</p>
<pre><code class="language-bash">vllm serve nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
  --use-replayssm --replayssm-buffer-len 16 \
  --mamba-ssm-cache-dtype float32 \
  --speculative-config '{"method": "mtp", "num_speculative_tokens": 4}' \
  --max-num-seqs 512
</code></pre>
<p>The window of 16 is the default and was best in vLLM's own sweep. Tri Dao's write-up found 8 best for Nemotron 3, so test 8, 16 and 32. Keep the state in FP32. A low-precision copy is safe for reads only. When one shared NVFP4 state fed the recurrence, Nemotron-3-Nano-4B lost 31 points on GPQA-Diamond. Qwen3.5 uses Gated DeltaNet layers. Those kernels were in the authors' vLLM fork when this was written, not upstream, so check your release notes.</p>
<p><em>Role 3, measuring.</em> Sweep batch sizes with each setup, and hold everything else fixed. Use the same prompts, the same output length and the same GPU. Run each point twice, because the first run warms caches and compiles kernels:</p>
<pre><code class="language-python">for batch in (16, 64, 128, 256, 512):
    for setup in ("plain", "spec", "replay", "replay+spec"):
        tok_s = bench(setup, max_num_seqs=batch)   # your harness, same prompts each time
        print(batch, setup, round(tok_s))
</code></pre>
<p><strong>How you know it worked.</strong> Replay alone should match plain decoding at batch 1 and beat it at large batches. NVIDIA measured 1.20 to 1.47 times per decode step at batch 256. With replay, speculation should keep gaining past batch 128 instead of flattening. Then run your eval set on both. Scores should match within noise, because the math is exact.</p>
<h2>When Replaying the Inputs Is the Wrong Tool</h2>
<p>When your batches are small. At batch 1, the weights dominate every step. State copies are a rounding error there, and NVIDIA saw no speed-up from replay.</p>
<p>When the model is pure attention. Its cache already undoes a token with a pointer. There is no state to copy.</p>
<p>When your drafts are rarely accepted. Replay makes undo cheap, but it cannot make bad guesses useful. If each step keeps barely more than the real token, fix the drafter or turn speculation off.</p>
<p>When the layer is a different kind of recurrence. The trick needs the update to unroll as decay plus a sum. Mamba-2 and Gated DeltaNet do. Check before assuming a new architecture does.</p>
<p>When you are tempted to store the state in 4 bits. That is a separate decision. A low-precision copy for reading is fine. A low-precision state that feeds the next update builds up error.</p>
<p>Before you adopt it, ask:</p>
<ul>
<li>What batch size does my hybrid model actually run at?</li>
<li>How many draft tokens does a step keep, from the server's own metrics?</li>
<li>Did my eval scores hold with replay on?</li>
</ul>
<h2>Glossary</h2>
<ul>
<li><strong>speculative decoding</strong> — a cheap drafter guesses several tokens and the big model checks them in one pass.</li>
<li><strong>Mamba layer</strong> — a layer that keeps one fixed-size state per request and updates it every token.</li>
<li><strong>state</strong> — the fixed-size matrix a Mamba layer carries from one token to the next.</li>
<li><strong>checkpoint</strong> — a full copy of the state, written once per window.</li>
<li><strong>ReplaySSM</strong> — computing outputs from a checkpoint plus a log of recent inputs, so the state is written rarely.</li>
<li><strong>acceptance</strong> — the share of draft tokens the big model keeps.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><a href="https://developer.nvidia.com/blog/replayssm-rethinking-state-storage-for-faster-hybrid-model-decoding/">ReplaySSM: Rethinking State Storage for Faster Hybrid-Model Decoding</a> — NVIDIA Technical Blog (NVIDIA with Tri Dao)</li>
<li><a href="https://tridao.me/blog/2026/replayssm/">ReplaySSM: Cache SSM Inputs, Not State</a> — Tri Dao's blog</li>
<li><a href="https://github.com/vllm-project/vllm/pull/48018">vLLM pull request 48018: ReplaySSM for Mamba2 standard decode</a> — vLLM project, GitHub</li>
<li><a href="https://github.com/Johnny-Liou/ReplaySSM">Johnny-Liou/ReplaySSM</a> — GitHub, the authors' code (Apache-2.0)</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://theaicommit.com/2026-10-22-why-speculative-decoding-slows-hybrid-mamba-models-at-large/"><em>theaicommit.com</em></a><em>, a daily AI engineering lab for software engineers. The original has an</em> <a href="https://theaicommit.com/2026-10-22-why-speculative-decoding-slows-hybrid-mamba-models-at-large/"><em>interactive version</em></a> <em>you can play with, and the full runnable code is on</em> <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-22/code_example.py"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[EngAI: AI News for Software Engineers, One Swipe at a Time]]></title><description><![CDATA[EngAI turns AI news into 60-word swipe cards for engineers: what each story means for your system, deprecations flagged with dates, and a quick daily quiz.]]></description><link>https://theaicommit.hashnode.dev/engai-ai-news-for-software-engineers-one-swipe-at-a-time</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/engai-ai-news-for-software-engineers-one-swipe-at-a-time</guid><category><![CDATA[AI, iOS, Developer Tools, Software Engineering, Productivity]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Thu, 22 Oct 2026 01:43:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/55708582-5363-408d-b73a-0e8d15595b08.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI ships faster than anyone can read it. A new model on Monday, an SDK change on Tuesday, a deprecation notice on Wednesday that breaks your build in March. Most of us follow it through newsletters and feeds written for a general audience, and then do the translation ourselves: <em>does this change anything I run?</em></p>
<p>EngAI is a free iPhone app that does that translation for you. It's built for software engineers, and it's built on top of <a href="https://theaicommit.com">The AI Commit</a>, a daily, hands-on AI engineering lab.</p>
<h2>One story per card</h2>
<p>Each card is one AI story in about 60 words. Swipe up for the next one. Categories are narrow on purpose: <strong>Models, Tools, Research and Platforms</strong>. If a story doesn't matter to someone who writes software, it doesn't get a card.</p>
<p>Every card links to the original source, so you can read the full piece when a story is worth it. A card that is vague, unsourced, or linked to a homepage counts as a failed card.</p>
<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/39/5b/5a/395b5aa3-f211-6405-e54c-77ddc48eb89e/1-agent.png/600x1334bb-60.jpg" alt="An EngAI card: a coding-agent story in about 60 words, with the For a software engineer note" style="display:block;margin:0 auto" />

<h2>"For a software engineer"</h2>
<p>This is the part nobody else writes. Under each summary, a short note says what the story means for the system you already run.</p>
<p>A new coding-agent release isn't just "Company X launched Y." The note tells you what changes for your job worker, your CI, or your API bill. It's the step between "interesting" and "do I need to do something about this?"</p>
<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/cd/d4/ee/cdd4eea3-7606-0587-bc19-6e2a923ad360/4-sphinx.png/600x1334bb-60.jpg" alt="A Sphinx card with its For a software engineer note" style="display:block;margin:0 auto" />

<h2>Deprecations and breaking changes, flagged with the date</h2>
<p>Some stories aren't news, they're deadlines. Deprecations and breaking changes get a visible label and the date they take effect, so a model retirement or an SDK change doesn't surprise you in production.</p>
<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/d1/3f/91/d13f9153-ffd9-e7f7-2686-d5e488cde163/2-deprecation.png/600x1334bb-60.jpg" alt="A deprecation card labeled with its shutdown date, Oct 23, 2026" style="display:block;margin:0 auto" />

<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/dd/6f/40/dd6f401e-62e7-b24e-ae4b-5a4ceb328846/3-breaking.png/600x1334bb-60.jpg" alt="A breaking-change card for Transformers 5.19" style="display:block;margin:0 auto" />

<h2>A quick quiz</h2>
<p>A short quiz checks what you just read. It takes under a minute, and it's the difference between scrolling past a story and remembering it in a design review.</p>
<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/31/fb/0f/31fb0f43-8856-3162-0d7e-57453765a39f/6-quiz.png/600x1334bb-60.jpg" alt="The daily quiz: four questions from today's radar" style="display:block;margin:0 auto" />

<h2>Built on The AI Commit</h2>
<p><a href="https://theaicommit.com">The AI Commit</a> publishes one hands-on lesson every weekday: a plain-English write-up, runnable Python, an interactive visualizer, and the sources behind it. Each lesson shows up in the EngAI feed as a card, so the 60-second summary links straight to the 30-minute deep dive when you want to go further.</p>
<p>Recent lessons include how to stop a coding agent and an AI reviewer from looping on one pull request, and how sorting your prompts speeds up a mixture-of-experts batch job.</p>
<h2>Made for phone moments</h2>
<p>EngAI doesn't replace your newsletters. Email is for the desk. EngAI is for the commute, the coffee line, and the five minutes between meetings.</p>
<ul>
<li><p><strong>Free</strong></p>
</li>
<li><p><strong>No account needed.</strong> Open it and start swiping.</p>
</li>
<li><p><strong>iPhone only</strong> for now</p>
</li>
</ul>
<p>When you've read everything, the app tells you you're caught up. You can turn on notifications for the next batch, at most three a day.</p>
<img src="https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/e6/c3/37/e6c33729-b3d4-2a67-f039-08e416cdae13/5-caughtup-notifications.png/600x1334bb-60.jpg" alt="All caught up: you've read today's radar" style="display:block;margin:0 auto" />

<h2>Get it</h2>
<p>Search <strong>"EngAI"</strong> on the App Store, or install it here: <a href="https://apps.apple.com/us/app/engai-for-engineers/id6817158922">EngAI for Engineers on the App Store</a>.</p>
<p>For the daily deep dives, visit <a href="https://theaicommit.com">theaicommit.com</a>.</p>
<p>I'd love feedback from engineers using it. What would make a card more useful to you? Tell me in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[How Sorting Your Prompts Speeds Up a Mixture-of-Experts Batch Job]]></title><description><![CDATA[Sort prompts by topic and a mixture-of-experts model reads fewer experts per step. 5–17% more throughput on vLLM, with code and an A/B test to verify.]]></description><link>https://theaicommit.hashnode.dev/how-sorting-your-prompts-speeds-up-a-mixture-of-experts-batch-job</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/how-sorting-your-prompts-speeds-up-a-mixture-of-experts-batch-job</guid><category><![CDATA[LLM Engineering AI Infrastructure Python GPU Optimization Performance Engineering Machine Learning Systems DevOps        Artificial Intelligence Software Engineering Programming Cloud Computing Deep Learning]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Thu, 22 Oct 2026 01:32:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad6cabdb3202cc5273cf2af/316c0ec7-f5c9-477e-96c0-88fdf6ef1988.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Engineer's view.</strong> This is a batch job that touches records in arrival order. Each record needs a different index page, so the cache keeps reloading pages. Sort by key and each page loads once per batch. A mixture-of-experts model loads expert weights the same way, so a mixed batch pays for reads nobody shares.</p>
<p><strong>TL;DR.</strong> A mixture-of-experts model reads only the experts the batch asked for, and similar prompts ask for the same ones. Sort an offline job by topic, and each step reads less, so the job finishes sooner.</p>
</blockquote>
<h2>Explain Like I'm 5</h2>
<p>A library has a hundred shelves. A helper runs to fetch books for everyone at one table. If the table holds a cook, a pilot and a poet, the helper runs to many shelves each round. If everyone at the table is a cook, they mostly want the same cooking shelf. The helper makes fewer trips, and the table gets its books sooner. Nobody reads faster. The seating just changed.</p>
<h2>The Problem</h2>
<p>You have shipped this bug before, in a system with no AI in it.</p>
<p>A nightly job processes a million records in the order they arrived. Each record looks up a row in a big table. Records from the same customer sit on the same index pages, but arrival order scatters them. So the database keeps loading a page, evicting it, and loading it again an hour later. The job is slow, and the code is fine. Someone sorts the input by customer ID, and the job runs twice as fast. Nothing about the work changed. Only the order did.</p>
<p>A mixture-of-experts model has the same bug inside every decode step. Each layer holds many small blocks of weights, called <strong>experts</strong>. Each token uses only a few of them. Qwen3-30B-A3B has 128 experts per layer, and each token picks 8. But a server runs many requests together as one batch. On each step it must read every expert that any request in the batch picked.</p>
<p>That read is the expensive part. Producing one token per request is limited by how fast the GPU reads weights from memory, not by arithmetic. A random batch of 32 Qwen requests touches about 94 of the 128 experts in a layer. Code prompts favor one set of experts, French prompts another. Mix them, and the step reads both sets.</p>
<p><strong>The fix: before an offline batch job, sort the prompts so similar ones run together. Then each step reads fewer experts.</strong> Hugging Face measured this on unchanged vLLM and got 5 to 17% more throughput.</p>
<h2>The Fix: Group Similar Prompts Before the Batch Job</h2>
<p>New to how a model produces text one token at a time? Start at AI basics → <a href="https://theaicommit.com/#learn/tokens-and-sampling">Tokens and sampling</a>.</p>
<h3>Why does the order of a list change what the GPU reads?</h3>
<p>Because the server fills its batch from the front of the list. vLLM admits waiting requests first come, first served. So neighbors in your list end up decoding side by side. Sort the list, and each batch is mostly one topic.</p>
<p>Topics really do prefer their own experts. In one layer of Qwen3-30B-A3B, a topic's 32 favorite experts took 68 to 80% of its routing decisions. Even routing would give them 25%.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-21/fig-1.png" alt="Same prompts, two orders, different reads per step" style="display:block;margin:0 auto" />

<h3>How much does one expert cost?</h3>
<p>About 2.9 microseconds per layer on an AMD MI300X. That is the time to read about 9.4 MB of expert weights. The MoE layer's time rose in a straight line with the number of distinct experts the batch touched.</p>
<h3>Why was the measured gain smaller than the reads saved?</h3>
<p>Sorted Qwen batches read 19 to 25% fewer experts, which predicts a 20 to 27% speedup. The measured gain was about half that. Only the expert layers got cheaper. Attention, sampling and scheduling cost the same as before.</p>
<h3>Do I need topic labels?</h3>
<p>No. Embed each prompt with a small embedding model, cluster the vectors with k-means, and sort by cluster. On unlabeled chat data, this still gave Qwen about 5% more throughput.</p>
<h2>What This Means for You</h2>
<p><strong>When this matters.</strong> You run offline jobs on a mixture-of-experts model that you host yourself. Think evals, synthetic data, labeling, summarizing a backlog. Qwen3 MoE, gpt-oss, DeepSeek and Mixtral are all this kind of model. The job generates many tokens per prompt, and nobody waits on any single answer.</p>
<p><strong>How it affects you.</strong> The win is GPU hours. A ten-hour job on a rented node can finish in about nine. Your outputs will also shift a little. In the Hugging Face run, 92% of greedy outputs matched between sorted and shuffled order. Two shuffled runs matched each other 90% of the time. That drift is ordinary batch nondeterminism, the effect from <a href="https://theaicommit.com/#2026-09-02">How the Same Model Gives Two Different Answers</a>. Sorting does not add new drift.</p>
<p>The gain also depends on where your batch size sits. The simulation in <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-21/code_example.py"><code>code_example.py</code></a> shows the shape: zero at batch 1, largest at small and middle batches, fading once a one-topic batch reads nearly every expert.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-21/fig-2.png" alt="Throughput gain from sorting, Qwen3-30B-A3B, 6 topics" style="display:block;margin:0 auto" />

<p><strong>What to do about it.</strong> Today, look at your next offline job's input file. If it already has a dataset, task or language column, sort by it before you submit. That costs one line. If it has no labels, add the clustering step below. Then run the A/B test in Implementing It before you trust any percentage, including the one in this article.</p>
<h2>Implementing It</h2>
<p><strong>The change.</strong></p>
<p>Three roles touch this. Whoever builds the input file groups the prompts. Whoever runs the job submits them in that order and restores the original order. Whoever owns the cluster checks the gain is real. The serving side changes nothing for an offline job.</p>
<p><em>Role 1, grouping.</em> Use labels when you have them. Otherwise, cluster embeddings. The source used Qwen3-Embedding-0.6B and 16 clusters:</p>
<pre><code class="language-python">import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans

emb = SentenceTransformer("Qwen/Qwen3-Embedding-0.6B").encode(
    prompts, normalize_embeddings=True, batch_size=64)
labels = KMeans(n_clusters=16, n_init="auto", random_state=0).fit_predict(emb)
order = np.argsort(labels, kind="stable")  # stable keeps file order inside a cluster
</code></pre>
<p>Keep the embedding model small. It runs once over the file, before the expensive model loads.</p>
<p>You may not be able to sort the whole file at once, for example when you stream it in pieces. Then sort inside each piece. At batch 32, sorting within chunks of 128 prompts gave +11.7%, and chunks of 256 gave +14.8%, against +17.3% for a full sort. Make each piece several batches long, so every batch can fill from one cluster.</p>
<p><em>Role 2, the batch job.</em> vLLM returns outputs in the order you submitted. So submit in sorted order, then put each output back where its prompt came from:</p>
<pre><code class="language-python"># before
outs = llm.generate(prompts, params)

# after
outs = llm.generate([prompts[i] for i in order], params)
results = [None] * len(prompts)
for pos, i in enumerate(order):
    results[i] = outs[pos]
</code></pre>
<p>Skip the restore step and every downstream join is wrong. You will see it as answers attached to the wrong question, not as an error.</p>
<p>Your batch size is <code>max_num_seqs</code> on the <code>LLM</code> constructor. It decides where you sit on the curve above, so record it with every result.</p>
<p><em>Role 3, measuring.</em> Run the same prompts in both orders and compare tokens per second. Force equal output lengths so only the order differs:</p>
<pre><code class="language-python">import time
params = SamplingParams(temperature=0, max_tokens=256, ignore_eos=True)

def tok_per_s(batch):
    t = time.perf_counter()
    outs = llm.generate(batch, params)
    return sum(len(o.outputs[0].token_ids) for o in outs) / (time.perf_counter() - t)
</code></pre>
<p>Alternate the runs: shuffled, sorted, shuffled, sorted. A single pair can be skewed by a warm cache. The author's full harness is <code>vllm_bench.py</code> in the sort-your-prompts repository on GitHub. It had no license file when this was written, so read it for the method.</p>
<p><strong>How you know it worked.</strong></p>
<p>The sorted runs should beat the shuffled runs on every repeat, not just on average. On a mixture-of-experts model with labeled topics, expect something near 10%. Then run the same test on a dense model of similar size as a control. It should show no gain, because a dense model reads every weight on every step. If the dense model "improves" too, your measurement is noise. Last, compare outputs. The share that differs between sorted and shuffled should match the share that differs between two shuffled runs. If it is much higher, check the restore step.</p>
<h2>When Sorting Prompts Is the Wrong Tool</h2>
<p>When someone is waiting. Grouping means holding a request until similar ones arrive. In online serving, that queueing delay costs more than the step time it saves.</p>
<p>When the model is dense. Every weight is read on every step, whatever the order. A dense model like Llama 3 gains nothing.</p>
<p>When the work is mostly prefill. A long prompt with a short answer spends its time reading the prompt. Thousands of prompt tokens touch nearly every expert anyway. A classifier that emits one label per document is this case.</p>
<p>When batches are very large. Once even a one-topic batch touches almost every expert, there is nothing left to save. On unlabeled chat data, gpt-oss-120b at batch 128 showed no measurable gain.</p>
<p>When the model has few experts. A coarse model with 32 experts per layer saved 8 to 11% of reads, against 19 to 25% for 128 experts.</p>
<p>Before you adopt it, ask:</p>
<ul>
<li><p>Is my model a mixture of experts, and is this job decode-heavy?</p>
</li>
<li><p>What batch size does my job really run at?</p>
</li>
<li><p>Did a dense control show zero gain on the same A/B test?</p>
</li>
</ul>
<h2>Glossary</h2>
<ul>
<li><p><strong>experts</strong> — small blocks of weights in a layer; each token is routed to a few of them.</p>
</li>
<li><p><strong>mixture-of-experts</strong> — a model whose layers hold many experts and use only a few per token.</p>
</li>
<li><p><strong>decode step</strong> — one round in which every request in the batch produces its next token.</p>
</li>
<li><p><strong>batch</strong> — the set of requests the server runs together on each step.</p>
</li>
<li><p><strong>k-means</strong> — a clustering method that groups vectors around a chosen number of centers.</p>
</li>
<li><p><strong>batch nondeterminism</strong> — small output changes caused only by which requests shared a batch.</p>
</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><p><a href="https://huggingface.co/blog/badaoui/sort-your-prompts">Sort your prompts: a free speed-up for MoE batch inference</a> — Hugging Face blog (Abdennacer Badaoui)</p>
</li>
<li><p><a href="https://github.com/Abdennacer-Badaoui/sort-your-prompts">Abdennacer-Badaoui/sort-your-prompts</a> — GitHub, the author's code</p>
</li>
<li><p><a href="https://huggingface.co/Qwen/Qwen3-30B-A3B">Qwen3-30B-A3B model card</a> — Qwen team, Hugging Face</p>
</li>
<li><p><a href="https://huggingface.co/blog/moe">Mixture of Experts Explained</a> — Hugging Face blog</p>
</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://theaicommit.com/2026-10-21-how-sorting-your-prompts-speeds-up-a-mixture-of-experts/"><em>theaicommit.com</em></a><em>, a daily AI engineering lab for software engineers. The original has an</em> <a href="https://theaicommit.com/2026-10-21-how-sorting-your-prompts-speeds-up-a-mixture-of-experts/"><em>interactive version</em></a> <em>you can play with, and the full runnable code is on</em> <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-21/code_example.py"><em>GitHub</em></a><em>.</em></p>
<p>#AI, #LLM, #Machine Learning, #Python, #Performance</p>
]]></content:encoded></item><item><title><![CDATA[ How to Make Your Agent Check the Records It Changed Before It Says Done]]></title><description><![CDATA[Engineer's view. This is an endpoint that returns 200 after writing the wrong value. The handler logged success, and only a look at the row shows the bug…]]></description><link>https://theaicommit.hashnode.dev/how-to-make-your-agent-check-the-records-it-changed-before-it-says-done</link><guid isPermaLink="true">https://theaicommit.hashnode.dev/how-to-make-your-agent-check-the-records-it-changed-before-it-says-done</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[#agent]]></category><category><![CDATA[claude]]></category><dc:creator><![CDATA[Theaicommit]]></dc:creator><pubDate>Tue, 20 Oct 2026 02:00:50 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Engineer's view.</strong> This is an endpoint that returns 200 after writing the wrong value. The handler logged success, and only a look at the row shows the bug. An agent's "done" is that 200: it comes from what its tools returned. Checking the records it changed is the test it never ran.</p>
<p><strong>TL;DR.</strong> An agent decides it is done from what its tools returned, and a wrong write returns success just like a right one. Check the records it actually changed against what the task required, before it closes the task.</p>
</blockquote>
<h2>Explain Like I'm 5</h2>
<p>You ask a friend to put the milk in the fridge. They say "done!" You believe them, because they did walk to the kitchen. Later you find the milk in the cupboard. Your friend was not lying. They opened a door, put the milk behind it, and the door closed fine. Nothing went wrong that they could hear. The only way to know is to open the fridge. So before you say thanks, you look inside.</p>
<h2>The Problem</h2>
<p>You have shipped this bug before, in an API.</p>
<p>A support tool calls <code>PATCH /orders/A17</code> to put an order on hold. A mapping bug sends <code>resolved</code> instead of <code>on_hold</code>. The endpoint validates the value, writes it, and returns 200. The client logs success. Your dashboard of error rates stays flat. A week later the customer calls, because nobody ever followed up. The bug was never in the transport. It was in the row, and nothing looked at the row.</p>
<p>An agent that writes to a backend has the same blind spot, at a larger scale. It decides it is done by reading its own transcript. The transcript holds what each tool returned. A write with the wrong value returns "ok" just like a right one.</p>
<p>Microsoft's ThinkingBox team measured how often this happens. Their example is a customer whose $745 appliance sat in a carrier exception for fifteen days. The agent made nine well-formed tool calls and read the refund policy correctly. Then it closed the ticket as resolved. The policy said it had to stay on hold while the exception was open.</p>
<p>That was not a rare case. Across 12 models, they ran 121,680 trials and saw 79,853 failures. Of those failures, 67% ended cleanly. The agent changed state, reported no tool error, and said it was done. A check on the transcript would have passed two thirds of the failures.</p>
<p><strong>Check the records the agent changed, not the messages it wrote. Log every write the agent makes, compare those writes to what the task requires, and refuse to close the task until they match.</strong></p>
<h2>The Fix: Check the Agent's Writes Before It Closes the Task</h2>
<p>New to how an agent decides it is finished? Start at AI basics → <a href="https://theaicommit.com/#learn/the-agent-loop">The agent loop</a>.</p>
<h3>Why doesn't the agent notice on its own?</h3>
<p>Because nothing it can see is wrong. The model reads tool results, not your database. If <code>update_ticket</code> returns <code>{"ok": true}</code>, the model has no signal that the value it sent was the wrong one. Asking it to "double-check" only makes it reread the same transcript.</p>
<p>So the check has to read state. Every write the agent makes is an effect: a table, a row, a field, and its value before and after.</p>
<h3>What does the check compare?</h3>
<p>A contract per task type. It lists the fields that must end at a given value and the fields the task may touch. Comparing the effects to the contract finds three kinds of failure:</p>
<img src="https://theaicommit.com/assets/figures/2026-10-19/fig-1.png" alt="Same clean transcript, three ways the records can be wrong" style="display:block;margin:0 auto" />

<p>In ThinkingBox's failures, 78% had a wrong field value, 43% had an extra effect, and 25% missed a required one. One run can have more than one.</p>
<h3>Where does the check go?</h3>
<p>On the tool that ends the task, such as <code>close_ticket</code>. If the check finds a problem, the tool refuses and returns the list of problems as its result. The agent then reads a real signal: which field is wrong, and what it should be. It can fix the field, or hand the ticket to a person.</p>
<h2>What This Means for You</h2>
<p><strong>When this matters.</strong> Any agent that writes to a system of record: tickets, orders, refunds, a CRM, a calendar. It matters most where a wrong write is quiet. A ticket marked resolved sends no alert. A refund issued twice shows up only at month end.</p>
<p><strong>How it affects you.</strong> Most of your agent's failures will look like successes in your logs. ThinkingBox's authors read the failures as mostly a problem of tool use and recovery, not reasoning. That is good news, because tool use is the part you control.</p>
<p>There is a second cost. A single run tells you little about the next one. One open-weight model in the study solved 94% of tasks at least once, but only 13% on all 20 tries. Claude Opus 5 solved fewer at least once, yet passed 48% on every try. The <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-19/code_example.py"><code>code_example.py</code></a> file shows why. Two simulated agents both pass about 72% of single runs. One fails a little on every task, and passes all 20 runs on none of them. The other is reliable on most tasks and hopeless on a few, and passes all 20 on 38%.</p>
<img src="https://theaicommit.com/assets/figures/2026-10-19/fig-2.png" alt="Tasks passed on all 20 runs, two agents with the same 72% single-run score" style="display:block;margin:0 auto" />

<p><strong>What to do about it.</strong> Log every write at the tool layer. Write a contract for each task type that changes records. Put the check on the closing tool. Then score your agent by how many tasks pass every run, not by its average.</p>
<h2>Implementing It</h2>
<p><strong>The change.</strong></p>
<p>Three roles touch this. Whoever writes the tools records each write. Whoever builds the agent puts the check on the closing tool. Whoever runs the evals scores every task over several clean runs.</p>
<p><em>Role 1, whoever writes the tools.</em> Route every state-changing tool through one write function. It records the effect before it returns:</p>
<pre><code class="language-python">def write(self, table, key, field, value):
    row = self.db[table].setdefault(key, {})
    self.effects.append(Effect(table, key, field, row.get(field), value))
    row[field] = value
    return "ok"            # all the model will ever see
</code></pre>
<p>Read-only tools skip it. A tool that calls another service records what it asked for, plus the id that service returned. Keep one log per run, never one per process, so two runs never share effects.</p>
<p><em>Role 2, whoever builds the agent.</em> Write the contract from your policy, not from a golden run. A ticket with an open carrier exception must end on hold:</p>
<pre><code class="language-python">HOLD = Contract(
    required={("tickets", "T9", "status"): "on_hold"},
    allowed={("tickets", "T9", "note"), ("tickets", "T9", "closed")},
)
</code></pre>
<p>The check reads final values for the required fields, and flags any effect outside both sets. Put it on the closing tool. In the OpenAI Agents SDK, a tool input guardrail runs before the tool executes and can reject the call with a message:</p>
<pre><code class="language-python">from agents import ToolGuardrailFunctionOutput
from agents.decorators import tool, tool_input_guardrail

@tool_input_guardrail
def records_match_task(data):
    problems = contract_for(current_task()).check(db, effect_log.effects)
    if problems:
        return ToolGuardrailFunctionOutput.reject_content(
            "Not closed. Fix these first:\n" + "\n".join(problems))
    return ToolGuardrailFunctionOutput.allow()

@tool(tool_input_guardrails=[records_match_task])
def close_ticket(ticket_id: str) -&gt; str: ...
</code></pre>
<p>The rejection message goes back to the model as the tool's output. Without that SDK, do the same check at the top of the tool function. Cap the retries, then escalate to a person.</p>
<p><em>Role 3, whoever runs the evals.</em> Run each task several times, each from a fresh copy of the data, and count the tasks that pass every run. The fresh copy matters: a run that starts on rows an earlier run changed can pass for the wrong reason. That number is called pass^k:</p>
<pre><code class="language-python">every = sum(all(run_task(t, fresh_db()) for _ in range(k)) for t in tasks)
print(f"pass^{k}: {every / len(tasks):.0%}")
</code></pre>
<p><strong>How you know it worked.</strong></p>
<p>Replay four runs: one right, one with a wrong value, one with an extra write, one with a missing write. A transcript check passes all four. Your gate must close the first and refuse the other three, naming the field each time. In production, count refusals by kind. A rising count of extra effects usually means the agent has a tool it should not have.</p>
<h2>When a Completion Check Is the Wrong Tool</h2>
<p>When you cannot say what the end state should be. A research agent or a drafting agent has no record to check. Review its output instead, or grade it with a rubric.</p>
<p>When the effect leaves your system. An email, a payment to a third party, or a message in someone else's queue cannot be read back or undone. A check after the fact only tells you what went wrong. Require a person's approval before those calls instead.</p>
<p>When the task types multiply faster than you can write contracts. Write contracts only for the tasks that change money, status, or anything a customer sees. Let the rest through with an effect log you review weekly.</p>
<p>When the agent only reads. A read-only agent has no effects. Its failures are wrong answers, and those belong in an answer eval.</p>
<p>The authors did not measure how much a check like this raises the pass rate, and neither did this session. Measure it on your own tasks before you promise anyone a number.</p>
<p>Before you add a gate, ask:</p>
<ul>
<li><p>Can I write down, for this task type, which fields must change and to what?</p>
</li>
<li><p>Does every write go through a layer I can log?</p>
</li>
<li><p>What happens to the customer if the gate refuses and nobody picks it up?</p>
</li>
</ul>
<h2>Glossary</h2>
<ul>
<li><p><strong>transcript</strong> — the messages and tool results an agent has seen so far in a run.</p>
</li>
<li><p><strong>effect</strong> — one write the agent made: table, row, field, and the value before and after.</p>
</li>
<li><p><strong>contract</strong> — for one task type, the fields that must end at a value and those it may touch.</p>
</li>
<li><p><strong>extra effect</strong> — a write to a field the task's contract does not allow.</p>
</li>
<li><p><strong>tool input guardrail</strong> — an Agents SDK check that runs before a tool and can reject the call.</p>
</li>
<li><p><strong>pass^k</strong> — the share of tasks an agent passes on every one of k clean runs.</p>
</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><p><a href="https://huggingface.co/blog/microsoft/thinkingbox">The Agent Said It Was Done. The Database Disagreed.</a> — Microsoft Copilot Studio team, on the Hugging Face blog</p>
</li>
<li><p><a href="https://github.com/microsoft/thinkingbox">ThinkingBox on GitHub</a> — Microsoft, MIT license</p>
</li>
<li><p><a href="https://openai.github.io/openai-agents-python/guardrails/">Guardrails: tool guardrails</a> — OpenAI, Agents SDK docs</p>
</li>
<li><p><a href="https://sierra.ai/blog/benchmarking-ai-agents">τ-Bench: Benchmarking AI agents for the real world</a> — Karthik Narasimhan, Sierra</p>
</li>
</ul>
<hr />
<p><em>Originally published at</em> <a href="https://theaicommit.com/2026-10-19-how-to-make-your-agent-check-the-records-it-changed-before/"><em>theaicommit.com</em></a><em>, a daily AI engineering lab for software engineers. The original has an</em> <a href="https://theaicommit.com/2026-10-19-how-to-make-your-agent-check-the-records-it-changed-before/"><em>interactive version</em></a> <em>you can play with, and the full runnable code is on</em> <a href="https://github.com/magna56/aI_daily_run/blob/main/2026-10-19/code_example.py"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>