<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tech_Nuggets</title>
    <description>The latest articles on DEV Community by Tech_Nuggets (@tech_nuggets).</description>
    <link>https://dev.to/tech_nuggets</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3964855%2F98c7cb4c-5bca-4836-be1e-324898055eb4.png</url>
      <title>DEV Community: Tech_Nuggets</title>
      <link>https://dev.to/tech_nuggets</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tech_nuggets"/>
    <language>en</language>
    <item>
      <title>KV cache and PagedAttention: what they do and why they matter</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Sat, 20 Jun 2026 01:36:07 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/kv-cache-and-pagedattention-what-they-do-and-why-they-matter-jce</link>
      <guid>https://dev.to/tech_nuggets/kv-cache-and-pagedattention-what-they-do-and-why-they-matter-jce</guid>
      <description>&lt;h1&gt;
  
  
  KV cache and PagedAttention: what they do and why they matter
&lt;/h1&gt;

&lt;p&gt;Your production LLM server is running behind schedule. You deployed a 70B model on four A100s with 80 GB each -- within spec, within budget -- but the time-to-first-token is creeping up as concurrent users increase. By lunch, latency is double what it was at 8 AM. You check GPU memory and find that 70% of HBM is consumed by what nvidia-smi reports as "tensor buffers," but which are actually the cached transformer states of a dozen long-running conversations that nobody cleaned up. You restart the server. It works again. By 4 PM, the same slowdown is back.&lt;/p&gt;

&lt;p&gt;This is the KV cache memory problem, and it is the single biggest operational bottleneck in production LLM serving on GPUs. This post explains what the KV cache actually stores, why it grows without bound during a conversation, and how PagedAttention -- the technique that powers vLLM -- solves it with OS-inspired memory management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the KV cache matters
&lt;/h2&gt;

&lt;p&gt;The KV cache is not optional. Every autoregressive transformer generates tokens one at a time. For token N, the attention mechanism needs the Key and Value tensors from tokens 0 through N-1. Recomputing those from scratch for every new token would be O(N^2) per step -- catastrophic for any conversation longer than a few hundred tokens. Instead, the inference engine caches the K and V tensors from prior tokens and appends to them on each step. That structure is the KV cache.&lt;/p&gt;

&lt;p&gt;The problem is its memory footprint. For a Llama 3.1 70B model with 80 layers, 8 KV heads (grouped-query attention), and a head dimension of 128, a single 4096-token sequence requires approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 (K+V) * 80 layers * 8 KV heads * 128 dim * 4096 tokens * 2 bytes (FP16)
= 1,342,177,280 bytes per sequence
= ~1.3 GB per sequence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 256 concurrent 4096-token sequences, that is 336 GB of HBM -- more than four A100s provide (320 GB total). And that is before accounting for the model weights (~140 GB for 70B in FP16), the intermediate activations, the attention scores matrix, or any batching overhead.&lt;/p&gt;

&lt;p&gt;This is the fundamental tension: the KV cache is mandatory for acceptable latency, but it consumes more memory than the model weights for any workload with meaningful concurrency or long context windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the KV cache looks like in traditional serving
&lt;/h2&gt;

&lt;p&gt;In most transformer inference implementations outside vLLM, the KV cache is a pre-allocated contiguous tensor. When a sequence starts, the framework allocates a &lt;code&gt;past_key_values&lt;/code&gt; tuple sized for the maximum sequence length (or a user-specified &lt;code&gt;max_new_tokens&lt;/code&gt;). The allocation happens up front and stays pinned until the sequence is done.&lt;/p&gt;

&lt;p&gt;Here is a simplified view of what happens during a single generation step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified attention step with a traditional contiguous KV cache
# key_cache shape: [batch, num_heads, max_seq_len, head_dim]
# value_cache shape: [batch, num_heads, max_seq_len, head_dim]
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attention_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_pos&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Slice the cache to only the valid tokens so far
&lt;/span&gt;    &lt;span class="n"&gt;past_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key_cache&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;current_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;past_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value_cache&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;current_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head_dim&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;attn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contiguous allocation means you pay the maximum possible memory cost from the very first token, even if the conversation never reaches the maximum length. This is fine for offline evaluation with fixed-length sequences, but wasteful in interactive serving where most conversations are short.&lt;/p&gt;

&lt;p&gt;Three specific inefficiencies arise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal fragmentation.&lt;/strong&gt; A sequence allocated for 4096 tokens that only uses 300 tokens wastes 93% of its allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sharing.&lt;/strong&gt; Two conversations that start with the same system prompt must each store their own copy of the K and V tensors for the shared prefix. There is no mechanism to deduplicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All-or-nothing eviction.&lt;/strong&gt; When memory runs out, the entire sequence must be evicted or swapped to CPU memory. Moving a 4096-token KV cache for a 70B model over PCIe takes tens of milliseconds, during which the GPU stalls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How PagedAttention works
&lt;/h2&gt;

&lt;p&gt;PagedAttention, introduced by the paper "Efficient Memory Management for Large Language Model Serving with PagedAttention" (Kwon, Li, Zhuang et al., 2023), applies operating-system-style virtual memory paging to the KV cache. Instead of allocating one contiguous block per sequence, the KV cache is divided into fixed-size blocks called pages -- typically 16 or 32 tokens per page. The attention kernel is modified to gather key and value data from non-contiguous physical pages during the attention computation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Virtual["Virtual KV Cache (per sequence)"]
        S1[Sequence 1\npages: A, B, C, D]
        S2[Sequence 2\npages: E, F]
        S3[Sequence 3\npages: G, H, I, J, K]
    end

    subgraph PageTable["Logical-to-Physical Mapping"]
        P0["A -&amp;gt; Frame 0"]
        P1["B -&amp;gt; Frame 3"]
        P2["C -&amp;gt; Frame 7"]
        P3["D -&amp;gt; Frame 11"]
        P4["E -&amp;gt; Frame 1"]
        P5["F -&amp;gt; Frame 4"]
        P6["G -&amp;gt; Frame 2"]
        P7["H -&amp;gt; Frame 5"]
        P8["I -&amp;gt; Frame 8"]
        P9["J -&amp;gt; Frame 9"]
        P10["K -&amp;gt; Frame 6"]
    end

    subgraph Physical["Physical Memory Frames (GPU HBM)"]
        M0[(Frame 0)]
        M1[(Frame 1)]
        M2[(Frame 2)]
        M3[(Frame 3)]
        M4[(Frame 4)]
        M5[(Frame 5)]
        M6[(Frame 6)]
        M7[(Frame 7)]
        M8[(Frame 8)]
        M9[(Frame 9)]
        M10[(Frame 10)]
        M11[(Frame 11)]
    end

    S1 --&amp;gt; P0 &amp;amp; P1 &amp;amp; P2 &amp;amp; P3
    S2 --&amp;gt; P4 &amp;amp; P5
    S3 --&amp;gt; P6 &amp;amp; P7 &amp;amp; P8 &amp;amp; P9 &amp;amp; P10

    P0 --&amp;gt; M0
    P1 --&amp;gt; M3
    P2 --&amp;gt; M7
    P3 --&amp;gt; M11
    P4 --&amp;gt; M1
    P5 --&amp;gt; M4
    P6 --&amp;gt; M2
    P7 --&amp;gt; M5
    P8 --&amp;gt; M8
    P9 --&amp;gt; M9
    P10 --&amp;gt; M6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The block manager maintains a page table that maps each sequence's logical page numbers to physical frame numbers. When the attention kernel needs the key-value data for a token at a given position, it computes which page that position falls in, reads the page table to find the physical frame, and loads the data from that frame. The layout is invisible to the model -- the attention output is mathematically identical to the contiguous case.&lt;/p&gt;

&lt;p&gt;This design unlocks three capabilities that are not available with contiguous allocation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. On-demand allocation.&lt;/strong&gt; A sequence only consumes pages as it grows. If a user asks a one-turn question that generates 150 tokens, the cache uses 10 pages (at 16 tokens per page). If another user runs a 5000-token document analysis, pages are allocated dynamically. No memory is wasted on unused capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Copy-on-write for shared prefix pages.&lt;/strong&gt; When multiple sequences share a common prefix -- the system prompt, the conversation history, a few few-shot examples -- PagedAttention maps the same physical pages into multiple virtual address spaces. The pages are marked read-only. If one sequence diverges during generation (which it always will after the first sampling step), only the page that actually changes is copied. In many chat applications, 40-60% of the tokens in a batch can be shared prefix tokens, so the memory savings are substantial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fine-grained eviction and swapping.&lt;/strong&gt; When GPU memory is exhausted, the block manager selects pages to evict based on a least-recently-used policy. Evicted pages are written to CPU DRAM. Because pages are small (16-32 tokens), the transfer granularity is fine and the PCIe bandwidth cost is amortized across many small transfers rather than one large blocking move.&lt;/p&gt;

&lt;h2&gt;
  
  
  PagedAttention vs traditional KV cache management
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Traditional contiguous KV cache&lt;/th&gt;
&lt;th&gt;PagedAttention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Allocation strategy&lt;/td&gt;
&lt;td&gt;Pre-allocate max length per sequence&lt;/td&gt;
&lt;td&gt;On-demand, one page at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory waste due to fragmentation&lt;/td&gt;
&lt;td&gt;High (allocated but unused slots)&lt;/td&gt;
&lt;td&gt;Near zero (pay for used tokens only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared prefix support&lt;/td&gt;
&lt;td&gt;None (every sequence stores its own copy)&lt;/td&gt;
&lt;td&gt;Copy-on-write page sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eviction granularity&lt;/td&gt;
&lt;td&gt;Entire sequence&lt;/td&gt;
&lt;td&gt;16-32 token pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap overhead per eviction&lt;/td&gt;
&lt;td&gt;High (full sequence over PCIe)&lt;/td&gt;
&lt;td&gt;Low (single page)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak throughput at same HBM budget&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;2-4x on mixed workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch size ceiling&lt;/td&gt;
&lt;td&gt;Limited by worst-case per-sequence allocation&lt;/td&gt;
&lt;td&gt;Limited by actual memory consumption&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The throughput gains are workload-dependent. vLLM's published benchmarks report 2-4x improvement over frameworks with contiguous allocation, with the largest gains on workloads that mix short and long sequences. For uniform-length batches, the advantage shrinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Page table overhead with very small page sizes.&lt;/strong&gt; The page table itself lives in GPU memory. With page sizes of 4-8 tokens, the metadata can consume a non-trivial fraction of HBM. vLLM defaults to 16-token pages as the practical sweet spot. If you observe lower-than-expected throughput with very long contexts, check whether your page size is too small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scheduler parameters that work against PagedAttention.&lt;/strong&gt; vLLM exposes &lt;code&gt;--max-num-batched-tokens&lt;/code&gt; and &lt;code&gt;--max-num-seqs&lt;/code&gt;, which control how many tokens and sequences are batched in a single iteration. Setting these too high wastes the batch without improving throughput. Setting them too low underutilizes the GPU. The general guidance is to start with &lt;code&gt;--max-num-seqs 256&lt;/code&gt; and &lt;code&gt;--max-num-batched-tokens 8192&lt;/code&gt; for a 70B model and tune from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prefix caching is not unconditionally beneficial.&lt;/strong&gt; vLLM's automatic prefix caching (&lt;code&gt;--enable-prefix-caching&lt;/code&gt;) computes a hash for every block of tokens. For very short prompts or rapidly rotating system prompts, the hash computation overhead can exceed the reuse benefit. Profile with and without it for your workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Interaction with KV cache quantization.&lt;/strong&gt; PagedAttention works with FP8 and INT4 KV cache quantization, but each page carries metadata that is proportionally more significant when the data per page is smaller. vLLM v0.23.0 added FP8 KV cache support for Ada Lovelace and Hopper GPUs, usable with &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;. Measure the combined effect before enabling.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;PagedAttention and vLLM are not the right choice for every deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single-user local inference.&lt;/strong&gt; If you run a model for one user on one GPU, the memory pressure that PagedAttention solves never arises. A simpler framework like llama.cpp or Hugging Face Transformers has lower overhead and fewer failure modes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sub-100ms interactive latency requirements.&lt;/strong&gt; The page-walking logic during attention adds a small but measurable overhead per token -- roughly 3-5% for 16-token pages. If your application requires consistent sub-100ms time-to-first-token, a contiguous cache with static pre-allocation gives lower tail latency (at the cost of lower throughput).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Small models on high-memory GPUs.&lt;/strong&gt; A 7B model on an A100-80GB uses about 14 GB for weights and, at 4096-token context, roughly 300 MB for the KV cache per sequence. At typical concurrency levels, the cache fits easily without paging. PagedAttention's complexity buys you nothing here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-autoregressive architectures.&lt;/strong&gt; Models that do not generate tokens left-to-right -- encoder-only models (BERT, RoBERTa), diffusion-based language models, non-causal decoders -- have no KV cache to manage. PagedAttention is specific to autoregressive decoding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Uniform-length offline evaluation.&lt;/strong&gt; If every sequence in a batch is the same length (common in evaluation benchmarks), the fragmentation and on-demand benefits of paging are minimal. The contiguous approach works fine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;KV cache&lt;/strong&gt; stores the Key and Value tensors from every previous token during autoregressive decoding. It is mandatory for acceptable latency but grows linearly with sequence length and batch size.&lt;/li&gt;
&lt;li&gt;For a Llama 3.1 70B model at 256 concurrent 4096-token sequences, the KV cache consumes approximately 336 GB of HBM -- more than four A100s can provide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PagedAttention&lt;/strong&gt; (Kwon et al., 2023) applies OS-style virtual memory paging to the KV cache: fixed-size pages, on-demand allocation, copy-on-write page sharing, and fine-grained eviction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM&lt;/strong&gt; (v0.23.0, June 2026, 83k+ GitHub stars) implements PagedAttention and achieves 2-4x throughput over contiguous-allocation frameworks on mixed workloads.&lt;/li&gt;
&lt;li&gt;Default to 16-token pages and tune &lt;code&gt;--max-num-seqs&lt;/code&gt; and &lt;code&gt;--max-num-batched-tokens&lt;/code&gt; for your model and workload.&lt;/li&gt;
&lt;li&gt;Use PagedAttention when concurrency is high, sequences vary in length, or prompts share prefixes. Skip it for single-user inference, small models, or uniform batch sizes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: vLLM vs TGI vs llama.cpp -- a practical serving benchmark for the same 70B model under realistic concurrency, comparing throughput, latency, and cost per token.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tokenization under the hood: BPE, WordPiece, SentencePiece, and Unigram compared</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Wed, 17 Jun 2026 01:10:36 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/tokenization-under-the-hood-bpe-wordpiece-sentencepiece-and-unigram-compared-4ca5</link>
      <guid>https://dev.to/tech_nuggets/tokenization-under-the-hood-bpe-wordpiece-sentencepiece-and-unigram-compared-4ca5</guid>
      <description>&lt;h1&gt;
  
  
  Tokenization under the hood: BPE, WordPiece, SentencePiece, and Unigram compared
&lt;/h1&gt;

&lt;p&gt;You deploy a chatbot. English queries average 42 tokens each. Then a Spanish-speaking user sends "¿Cómo puedo restablecer mi contraseña?" and it eats 103 tokens. Two weeks later, the same model starts outputting "Ġcon" at the edges of its generations and you cannot tell if it is a bug or a feature. The finance team flags a 40% month-over-month cost increase that no one can explain.&lt;/p&gt;

&lt;p&gt;This is what happens when tokenization is treated as invisible plumbing. Every major LLM pipeline uses one of four subword tokenization algorithms, and the choice determines vocabulary size, handling of rare words, cross-language efficiency, and inference cost. Understanding which one your model uses -- and why -- is the difference between shipping a cost-efficient product and discovering mid-quarter that your token-per-query ratio quietly doubled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Tokenization directly controls three things that hit your bottom line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inference cost.&lt;/strong&gt; LLM APIs charge by token. A model using a 32K-vocab BPE tokenizer may break "restablecer" into 8 tokens, while a 100K-vocab Unigram tokenizer handles it in 3. Over a million queries, that difference adds up to real money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vocabulary coverage.&lt;/strong&gt; Rare words, code syntax, and multilingual text stress the tokenizer. A poorly fitting vocabulary means longer sequences, which means slower generation and higher cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model behavior.&lt;/strong&gt; The tokenizer is the model's entire view of language. If your tokenizer encodes "cowboy" as ["cow", "boy"], the model learns something different than if it encodes it as ["c", "owb", "oy"]. This affects everything from spelling ability to cross-lingual transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four tokenization algorithms
&lt;/h2&gt;

&lt;p&gt;Every modern tokenizer takes raw text, optionally pre-tokenizes it into words (splitting on whitespace and punctuation), then breaks words into subword units from a fixed-size vocabulary. The difference is in how that vocabulary is built and how segmentation decisions are made.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. BPE (Byte-Pair Encoding)
&lt;/h3&gt;

&lt;p&gt;BPE was introduced in 1994 for data compression and adapted for neural machine translation by Sennrich et al. in 2016. OpenAI adopted it for GPT-2 and it remains the core of GPT-4o, Llama 3, and most modern LLMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Start with every individual character as a token. Count all adjacent token pairs, merge the most frequent pair into a new token, add it to the vocabulary, and repeat until you hit the target vocabulary size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vocabulary size goal: 16
Initial vocabulary: [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,  , ., ,]
Training corpus: "low low low low low low low low lower lowest lowest lowest lowest lowest lowest lowest"

Step 1: Count pairs -&amp;gt; ("l", "o") appears 30 times, merge -&amp;gt; "lo"
Step 2: Count pairs -&amp;gt; ("lo", "w") appears 20 times, merge -&amp;gt; "low"
Step 3: Count pairs -&amp;gt; ("low", "e") appears 10 times, merge -&amp;gt; "lowe"
Step 4: Count pairs -&amp;gt; ("lowe", "r") appears 4 times, merge -&amp;gt; "lower"
Step 5: Count pairs -&amp;gt; ("low", "e") appears 6 times... wait, "low"+"e" appears
         in "lowest" fragments, merge -&amp;gt; "lowe" already exists, so merge "lowe"+"st"
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BPE is greedy and deterministic: for any input, the segmentation is the same every time. The algorithm applies the learned merge rules in order. OpenAI's GPT-4o uses &lt;code&gt;o200k_base&lt;/code&gt; (200,096 tokens), GPT-4 used &lt;code&gt;cl100k_base&lt;/code&gt; (100,256 tokens), and GPT-2 used a 50,257-token vocabulary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who uses it:&lt;/strong&gt; GPT-4o, GPT-4, GPT-3.5, Llama 2, Llama 3 (via SentencePiece), DeepSeek, Mistral.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. WordPiece
&lt;/h3&gt;

&lt;p&gt;Google introduced WordPiece for Japanese/Korean voice search in 2012, and it powered BERT in 2018. It is often described as "BPE but with likelihood instead of frequency."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; The algorithm starts the same way as BPE -- character-level initial tokens -- but instead of counting raw frequencies, it merges the pair that maximizes the likelihood of the training data under the current vocabulary. In practice this means it picks the pair whose merge increases the corpus-likelihood the most.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compare merge candidates:
  Merge ("a", "b") -&amp;gt; new token likelihood gain: 0.0032
  Merge ("th", "e") -&amp;gt; new token likelihood gain: 0.0417
  Merge ("ing", " ") -&amp;gt; new token likelihood gain: 0.0281

WordPiece picks ("th", "e") because the probability lift is largest.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is that WordPiece tends to create tokens that are more linguistically meaningful -- common prefixes, suffixes, and root words -- compared to BPE's purely frequency-driven merges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who uses it:&lt;/strong&gt; BERT, DistilBERT, ELECTRA, and most encoder-only models from Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SentencePiece
&lt;/h3&gt;

&lt;p&gt;SentencePiece is a framework by Google (Kudo and Richardson, 2018) that wraps both BPE and Unigram tokenization. Its defining innovation: it operates &lt;strong&gt;directly on raw text without requiring a pre-tokenization step&lt;/strong&gt;. Most tokenizers need whitespace/punctuation splitting before training, which ties them to a language-specific concept of "word." SentencePiece treats the input as a raw Unicode byte sequence, making it truly language-agnostic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw text: "Hello世界"
With pre-tokenization: ["Hello", "世界"]  &amp;lt;- language-dependent
SentencePiece raw: "H", "e", "l", "l", "o", "世", "界"  &amp;lt;- no pre-tokenization needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Who uses it:&lt;/strong&gt; Llama 2, Llama 3, Gemma, T5, XLNet (in Unigram mode).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unigram Language Model
&lt;/h3&gt;

&lt;p&gt;Unigram (Kudo, 2018) flips the problem around. Instead of greedily building up a vocabulary from characters, it starts with a large vocabulary of candidate tokens and &lt;strong&gt;prunes it down&lt;/strong&gt; using a probabilistic model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Unigram models each token as an independent event and learns a probability distribution over the vocabulary. The segmentation of a word is the sequence of tokens whose probabilities multiply to the highest score.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Vocabulary:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"UN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"UNIC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"UNI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Input:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UNICORN"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Candidate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;segmentations&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;their&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scores:&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;UN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;R&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.92e-12&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;UNI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;R&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.4e-10&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;UNIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;R&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0e-9&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;best&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Unigram&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;picks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;highest-probability&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;segmentation:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;UNIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;R&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;N&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Unigram evaluates multiple candidate segmentations and chooses the best one probabilistically, it is slower to tokenize than BPE but produces more consistent token-to-meaning mappings. The probabilistic nature also enables &lt;strong&gt;subword regularization&lt;/strong&gt; -- randomly sampling alternative segmentations during training to improve robustness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who uses it:&lt;/strong&gt; T5, XLNet, ALBERT, and SentencePiece in Unigram mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Algorithm comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;BPE&lt;/th&gt;
&lt;th&gt;WordPiece&lt;/th&gt;
&lt;th&gt;SentencePiece (BPE)&lt;/th&gt;
&lt;th&gt;Unigram LM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vocabulary building&lt;/td&gt;
&lt;td&gt;Greedy merge by frequency&lt;/td&gt;
&lt;td&gt;Greedy merge by likelihood&lt;/td&gt;
&lt;td&gt;Greedy merge by frequency (same as BPE)&lt;/td&gt;
&lt;td&gt;Start big, prune by likelihood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-tokenization required&lt;/td&gt;
&lt;td&gt;Yes (whitespace/punctuation)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (raw bytes)&lt;/td&gt;
&lt;td&gt;No (raw bytes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic segmentation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (sampling possible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical vocab size&lt;/td&gt;
&lt;td&gt;32K-200K&lt;/td&gt;
&lt;td&gt;30K&lt;/td&gt;
&lt;td&gt;32K-128K&lt;/td&gt;
&lt;td&gt;32K-256K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Medium (Viterbi decoding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multilingual handling&lt;/td&gt;
&lt;td&gt;Weak (needs large vocab)&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Best (byte-level)&lt;/td&gt;
&lt;td&gt;Best (byte-level + sampling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rare word handling&lt;/td&gt;
&lt;td&gt;Decomposes to chars&lt;/td&gt;
&lt;td&gt;Decomposes to chars&lt;/td&gt;
&lt;td&gt;Decomposes to bytes&lt;/td&gt;
&lt;td&gt;Decomposes to subwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary users&lt;/td&gt;
&lt;td&gt;OpenAI, Meta, Mistral&lt;/td&gt;
&lt;td&gt;Google (BERT)&lt;/td&gt;
&lt;td&gt;Meta (Llama), Google (Gemma)&lt;/td&gt;
&lt;td&gt;Google (T5, XLNet)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Here is a Python snippet using &lt;code&gt;tiktoken&lt;/code&gt; (OpenAI's BPE tokenizer library) to see how different inputs break apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="c1"&gt;# GPT-4o uses o200k_base encoding
&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o200k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;test_strings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restablecer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Spanish
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Das ist fantastisch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# German
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;こんにちは&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# Japanese
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;def fibonacci(n): return n if n &amp;lt;= 1 else fibonacci(n-1) + fibonacci(n-2)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;test_strings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token_strs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;!r:&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token_strs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (approximate for &lt;code&gt;o200k_base&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello, world!                                    -&amp;gt; 3 tokens: ['Hello', ',', ' world']
restablecer                                      -&amp;gt; 8 tokens: ['rest', 'able', 'cer', ...]
Das ist fantastisch                              -&amp;gt; 6 tokens: ['Das', ' ist', ' fant', 'ast', 'isch', ...]
こんにちは                                         -&amp;gt; 5 tokens: ['こ', 'ん', 'に', 'ち', 'は']
def fibonacci(n): return n if n &amp;lt;= 1 else ...   -&amp;gt; 22 tokens: ['def', ' fib', 'onacci', ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the Spanish word takes 8 tokens while an analogous English word of similar length might take 3-4. This is the cost asymmetry that shows up on your monthly bill.&lt;/p&gt;

&lt;p&gt;Here is a diagram showing how a single word passes through each tokenizer type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Input: 'unbelievable'"] --&amp;gt; B["Pre-tokenization&amp;lt;br/&amp;gt;(split on space/punct)"]
    B --&amp;gt; C{"Tokenizer type?"}

    C --&amp;gt;|BPE| D["Lookup in vocab: 'un' + 'believable'&amp;lt;br/&amp;gt;If 'believable' not found:&amp;lt;br/&amp;gt;'b' + 'el' + 'ievable' ...&amp;lt;br/&amp;gt;Greedy character-level fallback"]
    C --&amp;gt;|WordPiece| E["Lookup longest prefix: 'un'&amp;lt;br/&amp;gt;Try '##believable'&amp;lt;br/&amp;gt;If not found: '##b' + '##el' + ...&amp;lt;br/&amp;gt;Likelihood-based merging"]
    C --&amp;gt;|SentencePiece| F["Byte-level segmentation&amp;lt;br/&amp;gt;No pre-tokenization&amp;lt;br/&amp;gt;BPE merge rules on raw bytes&amp;lt;br/&amp;gt;'un' + 'bel' + 'ievable'"]
    C --&amp;gt;|Unigram| G["Score all candidate segmentations&amp;lt;br/&amp;gt;Pick highest-probability path&amp;lt;br/&amp;gt;'un' + 'believ' + 'able'&amp;lt;br/&amp;gt;Probabilistic, may vary"]

    D --&amp;gt; H["Output tokens"]
    E --&amp;gt; H
    F --&amp;gt; H
    G --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Assuming all tokenizers handle multilingual text equally.&lt;/strong&gt; BPE-based tokenizers that rely on space-prefix pre-tokenization (like &lt;code&gt;cl100k_base&lt;/code&gt;) degrade significantly on CJK and Indic scripts where whitespace does not separate words. SentencePiece models handle these better because they operate at the byte level. If your user base spans non-Latin scripts, check your tokenizer's cross-language efficiency before picking a model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tying your prompt design to the wrong encoding.&lt;/strong&gt; An instruction like "Output the result as JSON" costs 5 tokens with &lt;code&gt;cl100k_base&lt;/code&gt; but 7 tokens with &lt;code&gt;o200k_base&lt;/code&gt;. Developers who craft prompts for GPT-4 and then migrate to a model with a different tokenizer silently change the prompt's token boundary handoff, which can shift output quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring the tokenizer's role in fine-tuning.&lt;/strong&gt; When you fine-tune a model, you can extend the vocabulary -- but doing so requires initializing new embedding vectors, and the model will behave unpredictably with the new tokens for the first few thousand steps. Most practitioners are better off using the existing vocabulary and handling out-of-vocabulary tokens via character-level fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "split on prefix space" trap.&lt;/strong&gt; Most BPE tokenizers add a space before each word during pre-tokenization (byte-pair encoding operates on the string " Hello" not "Hello"). This means "Hello" (capitalized, start of sentence) and "hello" (lowercase, mid-sentence) share the same token " Hello" if the space prefix is consistent. But if your text formatting changes -- removing trailing spaces, using non-standard punctuation -- you can tokenize the same semantic content into dramatically more tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting that tokenizer version matters.&lt;/strong&gt; &lt;code&gt;p50k_base&lt;/code&gt; and &lt;code&gt;cl100k_base&lt;/code&gt; and &lt;code&gt;o200k_base&lt;/code&gt; all use BPE with different pre-tokenization rules and vocab sizes. A comparison of two models' outputs is meaningless if you used different tokenizers to count their tokens. Pin your tiktoken version (&lt;code&gt;tiktoken==0.13.0&lt;/code&gt; as of June 2026) and your encoding name in every evaluation script.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When you need exact character-level control.&lt;/strong&gt; Tokenization destroys alignment between text characters and model internals. If you are building a spelling corrector, a character-level model (like ByT5 or CANINE) produces better results than any subword tokenizer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When latency is the absolute priority.&lt;/strong&gt; SentencePiece Unigram and WordPiece both require running a language model or Viterbi decoder to segment text. BPE is simpler and faster. If you are measuring single-digit millisecond TTFT budgets, use a pure BPE tokenizer and keep the vocabulary under 50K.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you are building a single-language, domain-specific model.&lt;/strong&gt; If your entire task is English medical text classification, you can build a custom BPE vocabulary (15K-20K tokens) that outperforms the general-purpose 100K vocabulary in both speed and perplexity. The general vocabularies are optimized for web-scale diversity, not domain density.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need reversible tokenization.&lt;/strong&gt; Subword tokenization is lossy. You cannot reconstruct the original string perfectly from the token IDs if the tokenizer applied normalization (lowercasing, NFKC Unicode normalization, etc.). If you need byte-level round-trips, use a byte-level tokenizer (like the one in ByT5 or CANINE).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you are benchmarking across model families.&lt;/strong&gt; Comparing GPT-4o (200K vocab, BPE) against Llama 3 (32K vocab, SentencePiece BPE) by token count is comparing apples to oranges. Always benchmark on character or byte cost, not token cost, when models use different tokenizers.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;BPE (GPT-4o, Llama 3, Mistral) builds vocabulary by merging the most frequent character pairs greedily. Deterministic, fast, but weak on multilingual text.&lt;/li&gt;
&lt;li&gt;WordPiece (BERT, ELECTRA) merges by likelihood gain rather than frequency. Produces more linguistically meaningful tokens but requires pre-tokenization.&lt;/li&gt;
&lt;li&gt;SentencePiece (Llama 3, Gemma, T5) wraps BPE and Unigram, operating on raw bytes without pre-tokenization. Best multilingual handling.&lt;/li&gt;
&lt;li&gt;Unigram (T5, XLNet) starts with a large vocabulary and prunes it by likelihood. Supports subword regularization and produces more consistent token-to-meaning alignments at the cost of slower segmentation.&lt;/li&gt;
&lt;li&gt;Tokenizer choice directly impacts inference cost: a 32K vocab English-optimized tokenizer and a 200K vocab general tokenizer will produce very different token counts for the same multilingual input.&lt;/li&gt;
&lt;li&gt;Pin your tokenizer version and encoding name when reporting any token-count metric. Differences between &lt;code&gt;cl100k_base&lt;/code&gt; and &lt;code&gt;o200k_base&lt;/code&gt; can shift token counts by 15-30% on the same text.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;When you know which tokenizer your model uses, the next question is how to prepare your data so that tokenizer wastes as few tokens as possible. That means strategic prompt design, choosing the right model for your language mix, and building evaluation pipelines that measure token efficiency alongside accuracy. We will cover token-efficient prompt engineering in the next post -- including a concrete method for estimating your per-user token consumption before you deploy.&lt;/p&gt;

</description>
      <category>tokenization</category>
      <category>llm</category>
      <category>ai</category>
      <category>nlp</category>
    </item>
    <item>
      <title>RLHF vs DPO vs IPO vs KTO: which alignment method should you use</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Tue, 16 Jun 2026 01:08:06 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/rlhf-vs-dpo-vs-ipo-vs-kto-which-alignment-method-should-you-use-ggm</link>
      <guid>https://dev.to/tech_nuggets/rlhf-vs-dpo-vs-ipo-vs-kto-which-alignment-method-should-you-use-ggm</guid>
      <description>&lt;h1&gt;
  
  
  RLHF vs DPO vs IPO vs KTO: which alignment method should you use
&lt;/h1&gt;

&lt;p&gt;You have a base model, say Llama 3.2 8B, that can write poetry in any meter and pass the bar exam. It can also generate instructions for synthesizing controlled substances, roleplay as a manipulative therapist, and explain in loving detail why your pull request is an affront to good taste. You need to align it — remove the harmful outputs while keeping the capability. Your mentor says "use RLHF." A paper on your feed says DPO is simpler. Your colleague swears by KTO because they only have thumbs-up/thumbs-down log data from production. Where do you start?&lt;/p&gt;

&lt;p&gt;Choosing an alignment method is not a theoretical debate. It is a practical decision that depends on your data, your compute budget, and the failure modes you are trying to avoid. This post compares the four dominant approaches side by side, with the actual math, the data requirements, and the sharp edges you will hit in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;The alignment method you pick determines three things that directly affect shipping timelines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data requirements.&lt;/strong&gt; Some methods need pairwise preferences (A beats B). Others work with per-sample binary scores. If you have production logs, you probably already have the latter. If you have a human annotation pipeline, you can collect the former — at a cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute budget.&lt;/strong&gt; RLHF requires training a separate reward model of comparable size to your policy model, then running PPO, which is notoriously sample-inefficient and sensitive to hyperparameters. DPO, IPO, and KTO collapse the process into a single training loop on static data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability and robustness.&lt;/strong&gt; PPO can destabilize and collapse your policy. DPO can overfit to preference noise. IPO adds a regularization term that mitigates that. KTO handles scenarios where you have no strict pairwise comparisons at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Understanding these tradeoffs is the difference between an aligned model that ships in two weeks and an alignment project that drags for three months.&lt;/p&gt;

&lt;h2&gt;
  
  
  RLHF, DPO, IPO, and KTO: how each method works
&lt;/h2&gt;

&lt;p&gt;All four methods start from the same place: a supervised fine-tuned (SFT) model and a dataset that captures human preferences. How they use that data differs fundamentally.&lt;/p&gt;

&lt;h3&gt;
  
  
  RLHF (Reinforcement Learning from Human Feedback)
&lt;/h3&gt;

&lt;p&gt;The canonical approach, popularized by OpenAI's InstructGPT paper (Ouyang et al., 2022), is a three-stage pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collect human preferences&lt;/strong&gt; — annotators rank model outputs for a set of prompts, producing pairwise preferences (chosen vs rejected).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train a reward model&lt;/strong&gt; — a separate model (usually the same architecture as the policy) is trained to predict the human preference score from a given output. It learns a scalar reward function that approximates human judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize the policy with PPO&lt;/strong&gt; — the policy model generates outputs, the reward model scores them, and PPO (Proximal Policy Optimization) updates the policy to increase the expected reward. A KL penalty keeps the policy from diverging too far from the SFT model.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified PPO update (conceptual)
# reward = reward_model.generate(policy_output) - beta * kl_divergence(policy || ref_policy)
# policy_loss = -ppo_clip(reward, old_logprobs, new_logprobs)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three-stage pipeline is expensive — each stage requires its own training run, its own GPU budget, and its own hyperparameter sweep. The reward model can learn to exploit spurious correlations (reward hacking), and PPO is sensitive to the learning rate and KL penalty coefficient. On the plus side, online PPO can in theory discover outputs that are better than any human annotation in the dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  DPO (Direct Preference Optimization)
&lt;/h3&gt;

&lt;p&gt;Rafailov et al. (2023) showed that the reward model in RLHF is strictly unnecessary. The key insight is that the Bradley-Terry preference model (the statistical model behind most reward models) has a closed-form solution that relates the optimal policy directly to the reference policy and the preference data.&lt;/p&gt;

&lt;p&gt;DPO eliminates the reward model entirely. The training loss is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L_DPO = -E[log sigmoid(beta * (log pi(y_w|x)/pi_ref(y_w|x) - log pi(y_l|x)/pi_ref(y_l|x)))]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where y_w is the chosen output, y_l is the rejected output, pi is the current policy, pi_ref is the frozen reference policy (the SFT model), and beta controls how far the policy can diverge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# DPO loss in practice (using Hugging Face TRL)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DPOTrainer&lt;/span&gt;

&lt;span class="n"&gt;dpo_trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DPOTrainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ref_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ref_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;preference_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# KL regularization strength
&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;training_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dpo_trainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DPO runs in a single training loop on a static dataset. There is no reward model, no PPO, no online generation during training. This makes it dramatically cheaper — approximately 3x less compute than RLHF for comparable results on most benchmarks.&lt;/p&gt;

&lt;p&gt;The tradeoff: DPO is an offline method. It never sees the model's own generations during training, so it can over-optimize for preferences that do not generalize. It also requires pairwise preference data — you need two outputs per prompt, one explicitly preferred over the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  IPO (Identity Preference Optimization)
&lt;/h3&gt;

&lt;p&gt;Azar et al. (2023) at DeepMind identified a subtle problem with DPO: the implicit reward parameterization in DPO can lead to the regularization term not actually constraining the policy the way it should. IPO replaces the reward parameterization with an identity mapping, providing stronger regularization.&lt;/p&gt;

&lt;p&gt;The IPO loss is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L_IPO = E[(log pi(y_w|x)/pi_ref(y_w|x) - log pi(y_l|x)/pi_ref(y_l|x) - 1/(2*tau))^2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where tau is a regularization parameter. The squared loss directly penalizes the policy when the log-likelihood gap diverges too far from the target margin. This provides a cleaner optimization landscape and better-calibrated probabilities at inference time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IPO loss (conceptual)
# margin = (log_ratio_w - log_ratio_l)
# loss = (margin - 1/(2*tau))^2  # when margin &amp;lt; 1/(2*tau), else 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IPO requires the same pairwise data as DPO. It is slightly more stable in practice, especially on noisy preference datasets where DPO can amplify annotator disagreement.&lt;/p&gt;

&lt;h3&gt;
  
  
  KTO (Kahneman-Tversky Optimization)
&lt;/h3&gt;

&lt;p&gt;Ethayarajh et al. (2024) at Contextual AI took a different tack. Inspired by prospect theory (Kahneman and Tversky, 1979), they built an alignment method that works with per-sample binary feedback — thumbs up or thumbs down — instead of pairwise preferences.&lt;/p&gt;

&lt;p&gt;The KTO loss treats gains (chosen responses) and losses (rejected responses) asymmetrically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L_KTO = -E[w(y) * (1 - sigmoid(beta * (log pi(y|x)/pi_ref(y|x) - z_ref)))]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where w(y) is a weighting factor that differs for chosen and rejected examples, and z_ref is a reference value derived from the data. The key asymmetry: losses (rejected outputs) are weighted more heavily than gains (chosen outputs), mirroring human loss aversion documented in behavioral economics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# KTO trainer in Hugging Face TRL
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;trl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KTOTrainer&lt;/span&gt;

&lt;span class="n"&gt;kto_trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KTOTrainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ref_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ref_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;binary_feedback_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# no pairs needed
&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;training_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;kto_trainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KTO's major advantage is data efficiency. Many production systems log per-output user feedback (clicks, likes, flags) without recording a pairwise comparison. KTO can train directly on this signal. The tradeoff is lower sample efficiency per annotated example — pairwise comparisons carry more information per annotation than binary labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: which method for which situation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;RLHF&lt;/th&gt;
&lt;th&gt;DPO&lt;/th&gt;
&lt;th&gt;IPO&lt;/th&gt;
&lt;th&gt;KTO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pairwise comparisons&lt;/td&gt;
&lt;td&gt;Pairwise comparisons&lt;/td&gt;
&lt;td&gt;Pairwise comparisons&lt;/td&gt;
&lt;td&gt;Binary (good/bad)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reward model needed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (separate training)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Training stages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 (SFT + RM + PPO)&lt;/td&gt;
&lt;td&gt;1 (after SFT)&lt;/td&gt;
&lt;td&gt;1 (after SFT)&lt;/td&gt;
&lt;td&gt;1 (after SFT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compute cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Highest (~3x DPO)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Online generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (PPO samples during training)&lt;/td&gt;
&lt;td&gt;No (offline)&lt;/td&gt;
&lt;td&gt;No (offline)&lt;/td&gt;
&lt;td&gt;No (offline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tricky (PPO hyperparameters)&lt;/td&gt;
&lt;td&gt;Good, can overfit to noise&lt;/td&gt;
&lt;td&gt;Better (identity regularization)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-quality RM, large compute budget&lt;/td&gt;
&lt;td&gt;Clean pair data, tight budget&lt;/td&gt;
&lt;td&gt;Noisy pair data, production stability&lt;/td&gt;
&lt;td&gt;Production logs (binary feedback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reward hacking, training collapse&lt;/td&gt;
&lt;td&gt;Overfitting on static data&lt;/td&gt;
&lt;td&gt;Slightly more complex loss&lt;/td&gt;
&lt;td&gt;Needs enough binary data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the decision flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Do you have pairwise&amp;lt;br/&amp;gt;preference data?] --&amp;gt;|Yes| B{Do you have budget&amp;lt;br/&amp;gt;for a reward model&amp;lt;br/&amp;gt;and PPO?}
    A --&amp;gt;|No / only binary feedback| C[Use KTO]
    B --&amp;gt;|Yes| D[RLHF — full pipeline&amp;lt;br/&amp;gt;highest potential ceiling]
    B --&amp;gt;|No| E{Is your preference&amp;lt;br/&amp;gt;data clean or noisy?}
    E --&amp;gt;|Clean| F[DPO — simplest&amp;lt;br/&amp;gt;single-stage training]
    E --&amp;gt;|Noisy| G[IPO — better regularization&amp;lt;br/&amp;gt;for noisy preferences]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Running DPO on binary data.&lt;/strong&gt; DPO requires pairwise preferences: a chosen output and a rejected output for the same prompt. If you concatenate unrelated good and bad outputs into pairs, DPO will learn arbitrary decision boundaries. Use KTO for binary data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring the reference model.&lt;/strong&gt; DPO, IPO, and KTO all require a frozen reference model (usually your SFT checkpoint). The loss depends on the log-ratio between the current policy and the reference. If you use a different reference model, the optimization target changes silently. Always use the same checkpoint that produced the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping SFT.&lt;/strong&gt; None of these methods work well on a raw pretrained base model. You need an SFT model that can produce reasonable completions. The alignment stage assumes the model can already generate coherent, on-task outputs — it is steering existing behavior, not teaching the model to generate text from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating beta as a free parameter.&lt;/strong&gt; The beta (or tau) parameter controls how far the aligned policy can stray from the reference. A beta too high and you get no alignment effect. A beta too low and the model unlearns general capabilities (catastrophic forgetting). Sweep it systematically — at least 3 values (e.g., 0.01, 0.1, 0.5) on a validation set before committing to a full run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming RLHF always wins.&lt;/strong&gt; On many benchmarks, DPO matches or exceeds RLHF at a fraction of the compute. The main advantage of RLHF is the online generation during PPO, which can discover novel high-reward outputs not present in the training data. For most production use cases where you already have a representative dataset, DPO/IPO/KTO are the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not use any of these methods&lt;/strong&gt; if you have fewer than a few hundred preference examples. The signal-to-noise ratio at that scale is too low. Collect at least 500–1000 examples, and prefer 5000+ for reliable results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use RLHF&lt;/strong&gt; if you are budget-constrained or shipping on a timeline under four weeks. The three-stage pipeline (SFT, reward model, PPO) with hyperparameter tuning and reward model debugging routinely takes 2–3 months for teams that are new to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use DPO or IPO&lt;/strong&gt; if your data is binary per-output feedback with no pairwise structure. You will have to fabricate pairs from unrelated outputs, which introduces noise. Use KTO instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use KTO&lt;/strong&gt; if you have clean pairwise preferences and enough compute for DPO. Pairwise comparisons carry more information per example, so DPO will converge faster with fewer total annotations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not skip evaluating your aligned model on capability benchmarks.&lt;/strong&gt; Every alignment method trades some general capability for safety. If your aligned model drops 5% on MMLU relative to the SFT checkpoint, you have likely over-regularized. Run MMLU, HellaSwag, and a task specific to your domain before and after alignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RLHF uses a trained reward model plus PPO optimization. It is the most expensive but supports online exploration. Use it when you have large compute budgets and a team that can manage the complexity.&lt;/li&gt;
&lt;li&gt;DPO eliminates the reward model and optimizes a closed-form loss on static preference pairs. It is the simplest and cheapest. Use it for clean pairwise data when compute is constrained.&lt;/li&gt;
&lt;li&gt;IPO adds identity regularization to DPO, producing more stable training on noisy preferences. Use it when your annotation quality is inconsistent.&lt;/li&gt;
&lt;li&gt;KTO works with binary per-example feedback (good/bad) instead of pairwise comparisons. Use it when you only have production logs without explicit preference pairs.&lt;/li&gt;
&lt;li&gt;All four require a strong SFT base model, a frozen reference model, and a minimum of several hundred examples. All four risk capability regression — evaluate on standard benchmarks before and after alignment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;Pairwise preference data is the gold standard for alignment, but collecting it at scale is expensive and annotator agreement is often low. Next time: how to build and maintain a preference dataset — sampling strategy, inter-annotator agreement metrics, and detecting when your annotation pipeline is quietly poisoning your model.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>alignment</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Model Context Protocol (MCP): what it is and how to build a server</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Mon, 15 Jun 2026 01:13:04 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/the-model-context-protocol-mcp-what-it-is-and-how-to-build-a-server-4fbi</link>
      <guid>https://dev.to/tech_nuggets/the-model-context-protocol-mcp-what-it-is-and-how-to-build-a-server-4fbi</guid>
      <description>&lt;h1&gt;
  
  
  The Model Context Protocol (MCP): what it is and how to build a server
&lt;/h1&gt;

&lt;p&gt;Your team's LLM-powered application talks to a search index through one custom integration, a code repository through another, a Postgres database through a chain of LangChain tools, and a file system through raw Python I/O calls. Every new data source means writing a new integration. Every integration uses a different authentication model and returns data in a different shape. The LLM application is tightly coupled to every backend it touches, and swapping one out requires changing the application code directly.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (MCP) exists to replace this bespoke plumbing with a single, standardized interface. Think of it as a USB-C port for LLM applications: one connector shape, one protocol, and any compatible server can plug into any compatible client without custom wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a standard protocol matters
&lt;/h2&gt;

&lt;p&gt;LLM-powered tools have exploded in capability over the past two years, but the integration story has not kept up. Each AI application (IDE assistant, chat client, agent framework) historically built its own connectors for databases, APIs, document stores, and code repositories. There was no shared contract. If you wanted to use a specific code search tool with two different AI assistants, you needed two separate integrations.&lt;/p&gt;

&lt;p&gt;MCP borrows its design philosophy from the Language Server Protocol (LSP), which standardized how code editors talk to language analyzers. Before LSP, each editor had its own plugin for each language. After LSP, one language server worked with every editor. MCP aims to do the same for AI tools and the data sources they need.&lt;/p&gt;

&lt;p&gt;The protocol is an open standard, originally created at Anthropic and published under the MIT license. The specification reached stable at version 2025-11-25, and the Python SDK (&lt;code&gt;mcp&lt;/code&gt; on PyPI) is at &lt;strong&gt;1.27.2&lt;/strong&gt; as of May 2026. A 2.0.0 alpha was published in June 2026 with an updated transport layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How MCP works
&lt;/h2&gt;

&lt;p&gt;MCP uses &lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt; as its message format. A client (the AI application) connects to a server (a service that provides context) over one of three transport types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdio&lt;/strong&gt;: the client spawns the server as a child process and communicates over stdin/stdout. Best for local, single-user setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE&lt;/strong&gt; (Server-Sent Events): the server runs as an HTTP endpoint, the client connects over HTTP. Works across machines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamable HTTP&lt;/strong&gt;: a newer transport that allows bidirectional streaming over HTTP. Added in the 2025-11-25 spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the conceptual architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph Client["Client (AI App)"]
        A["Host&amp;lt;br/&amp;gt;IDE / Chat / Agent"]
        B["MCP Client&amp;lt;br/&amp;gt;Protocol handler"]
    end
    subgraph Server["MCP Server"]
        C["MCP Server&amp;lt;br/&amp;gt;Protocol handler"]
        D["Resources&amp;lt;br/&amp;gt;context data"]
        E["Tools&amp;lt;br/&amp;gt;executable functions"]
        F["Prompts&amp;lt;br/&amp;gt;templated workflows"]
    end
    A &amp;lt;--&amp;gt; B
    B &amp;lt;--&amp;gt;|JSON-RPC 2.0&amp;lt;br/&amp;gt;stdio / SSE / HTTP| C
    C --&amp;gt; D
    C --&amp;gt; E
    C --&amp;gt; F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every MCP session begins with a &lt;strong&gt;capability negotiation&lt;/strong&gt; handshake. The client announces what features it supports (sampling, roots, elicitation). The server announces what features it offers (resources, tools, prompts). Both sides agree on a feature set before any data exchange happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server primitives
&lt;/h3&gt;

&lt;p&gt;Servers offer three main categories of functionality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt; expose data to the LLM. Think of them as GET endpoints in a REST API. A resource has a URI and returns content in a structured format. Example: &lt;code&gt;file:///logs/2026-06-01.txt&lt;/code&gt; returns the content of that log file. Resources are how the LLM loads context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt; are functions the LLM can invoke. Think of them as POST endpoints. A tool has a name, a description, and an input schema (JSON Schema). The LLM can call a tool to execute code, query a database, or trigger an external action. Unlike resources, tools are invoked on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompts&lt;/strong&gt; are reusable templates for LLM interactions. A prompt defines a message template with parameter slots. The client can populate the template and present the result to the user as a pre-built interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client primitives
&lt;/h3&gt;

&lt;p&gt;Clients can also offer features to servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sampling&lt;/strong&gt;: the server can request the client to generate an LLM response, enabling agentic loops where one model delegates to another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roots&lt;/strong&gt;: the server can request information about filesystem or URI boundaries, so it knows where it is allowed to operate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elicitation&lt;/strong&gt;: the server can request additional information from the user through the client's UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building an MCP server in Python
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;mcp&lt;/code&gt; package (v1.27.2) provides a high-level API called &lt;strong&gt;FastMCP&lt;/strong&gt; that makes building a server straightforward. Here is a complete server that exposes a weather tool and a greeting resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="c1"&gt;# Create an MCP server
&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weather Demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add a tool: get weather for a city
&lt;/span&gt;&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celsius&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get the current weather for a city.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# In production, call a real weather API here
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weather in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: 22 degrees &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, partly cloudy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Add a resource: city data by URI
&lt;/span&gt;&lt;span class="nd"&gt;@mcp.resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city://{name}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;city_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get information about a city.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dubai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dubai, UAE. Population: 3.6M. Timezone: UTC+4.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;london&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London, UK. Population: 8.9M. Timezone: UTC+0.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokyo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tokyo, Japan. Population: 14M. Timezone: UTC+9.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add a prompt template
&lt;/span&gt;&lt;span class="nd"&gt;@mcp.prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;travel_planning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate a travel planning prompt for a destination.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a travel assistant helping someone plan a trip to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provide practical advice on weather, transportation, and attractions.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Run with stdio transport (default)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"mcp[cli]"&lt;/span&gt;
python weather_server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server starts on stdio by default. For HTTP transport, change the last line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable-http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing with the MCP Inspector
&lt;/h3&gt;

&lt;p&gt;The official MCP Inspector is a browser-based tool for testing servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @modelcontextprotocol/inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at your server endpoint (or stdio command) and you can browse resources, invoke tools, and inspect messages without writing a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP vs the alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;th&gt;Custom API / REST&lt;/th&gt;
&lt;th&gt;LangChain Tools&lt;/th&gt;
&lt;th&gt;OpenAI function calling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standardized protocol&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (framework-specific)&lt;/td&gt;
&lt;td&gt;No (API-specific)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primitive types&lt;/td&gt;
&lt;td&gt;Resources, Tools, Prompts&lt;/td&gt;
&lt;td&gt;Endpoints only&lt;/td&gt;
&lt;td&gt;Tools only&lt;/td&gt;
&lt;td&gt;Functions only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport options&lt;/td&gt;
&lt;td&gt;stdio, SSE, Streamable HTTP&lt;/td&gt;
&lt;td&gt;HTTP only&lt;/td&gt;
&lt;td&gt;In-process only&lt;/td&gt;
&lt;td&gt;HTTP only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bidirectional&lt;/td&gt;
&lt;td&gt;Yes (sampling, roots)&lt;/td&gt;
&lt;td&gt;Request-response only&lt;/td&gt;
&lt;td&gt;Request-response only&lt;/td&gt;
&lt;td&gt;Request-response only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth model&lt;/td&gt;
&lt;td&gt;OAuth 2.1 (spec), pluggable&lt;/td&gt;
&lt;td&gt;Custom per API&lt;/td&gt;
&lt;td&gt;Custom per integration&lt;/td&gt;
&lt;td&gt;API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client independence&lt;/td&gt;
&lt;td&gt;Any MCP client&lt;/td&gt;
&lt;td&gt;One client per API&lt;/td&gt;
&lt;td&gt;LangChain only&lt;/td&gt;
&lt;td&gt;OpenAI only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The main differentiator is &lt;strong&gt;client independence&lt;/strong&gt;. A server written for MCP works with any MCP-compatible client: Claude Code, Claude Desktop, the Continue.dev VS Code extension, or a custom agent framework. Custom APIs and framework-specific tools lock you into one ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Thinking tools are free.&lt;/strong&gt; Tools execute arbitrary code on your server. Every tool invocation consumes compute and may have side effects. The LLM cannot distinguish between a cheap operation (reading a config file) and an expensive one (running a 100-row batch query). Set usage limits or implement a permission layer for destructive operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource URIs must be meaningful.&lt;/strong&gt; A resource URI is not just a label -- it is the identifier the LLM uses to request data. Using opaque URIs (&lt;code&gt;resource://abc123&lt;/code&gt;) makes it impossible for the LLM to discover resources. Use hierarchical, descriptive URIs that hint at the content structure, like &lt;code&gt;docs://project/api/reference&lt;/code&gt; or &lt;code&gt;db://customers/orders?status=pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the capability handshake.&lt;/strong&gt; If you add a new tool to an existing server and your client does not re-negotiate capabilities, the client will not know the tool exists. The capability exchange happens at connection time. Restart both sides after changing what a server offers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overloading a server.&lt;/strong&gt; An MCP server that exposes 50 tools and 200 resources becomes as hard to navigate as a REST API with 50 endpoints. Group related functionality into separate servers and let the client connect to multiple servers. Claude Desktop and other hosts already support multi-server setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming tools are always available to the LLM.&lt;/strong&gt; Tool invocation requires user consent in most host applications. The user must approve each tool call. Design your tools to be meaningful in a single invocation, because multi-step approval flows create a poor user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;MCP is the wrong choice if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are building a single-purpose script.&lt;/strong&gt; If your Python script calls one API and prints the result, MCP adds unnecessary complexity. Just use &lt;code&gt;requests&lt;/code&gt; directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need sub-millisecond latency.&lt;/strong&gt; The JSON-RPC serialization and transport overhead adds a few milliseconds per call. For latency-critical, high-frequency operations (real-time streaming inference, hardware control), use a direct connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data source has no LLM interaction.&lt;/strong&gt; MCP is designed to serve context to LLMs. If you are building a regular web application backend with no AI component, use a standard REST or gRPC API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your users are all on one framework.&lt;/strong&gt; If every consumer of your service uses LangChain and will only ever use LangChain, writing a LangChain tool directly is simpler than writing an MCP server plus a LangChain MCP adapter. MCP pays off when you have multiple client types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MCP standardizes how LLM applications connect to data sources. One server works with any MCP-compatible client.&lt;/li&gt;
&lt;li&gt;The protocol uses JSON-RPC 2.0 over stdio, SSE, or Streamable HTTP transport. Features are negotiated at connection time.&lt;/li&gt;
&lt;li&gt;Servers expose &lt;strong&gt;Resources&lt;/strong&gt; (data), &lt;strong&gt;Tools&lt;/strong&gt; (executable functions), and &lt;strong&gt;Prompts&lt;/strong&gt; (templates). Clients can offer &lt;strong&gt;Sampling&lt;/strong&gt;, &lt;strong&gt;Roots&lt;/strong&gt;, and &lt;strong&gt;Elicitation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The Python SDK &lt;code&gt;mcp&lt;/code&gt; (v1.27.2) provides FastMCP, a decorator-based API for building servers in a few lines of code.&lt;/li&gt;
&lt;li&gt;MCP pays off when you have multiple client types consuming the same data sources. For single-purpose scripts or single-framework setups, a direct integration is simpler.&lt;/li&gt;
&lt;li&gt;Use the MCP Inspector (&lt;code&gt;npx @modelcontextprotocol/inspector&lt;/code&gt;) to test servers without writing a client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: building a multi-server MCP setup that connects a code search service, a documentation index, and a database gateway into a single AI assistant, with practical trade-offs on transport selection and auth.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>llm</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Structured output from LLMs: JSON mode, function calling, and grammar-constrained decoding</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Sun, 14 Jun 2026 02:58:10 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/structured-output-from-llms-json-mode-function-calling-and-grammar-constrained-decoding-355d</link>
      <guid>https://dev.to/tech_nuggets/structured-output-from-llms-json-mode-function-calling-and-grammar-constrained-decoding-355d</guid>
      <description>&lt;h1&gt;
  
  
  Structured output from LLMs: JSON mode, function calling, and grammar-constrained decoding
&lt;/h1&gt;

&lt;p&gt;You deployed a chatbot that translates natural-language requests into API calls. A user says "book a table for four at 7pm tomorrow." Your prompt asks the LLM to emit a JSON like &lt;code&gt;{"restaurant": string, "party_size": int, "time": string, "date": string}&lt;/code&gt;. One time it returns &lt;code&gt;{"restaurant": "Olive Garden", "party_size": 4, "time": "19:00", "date": "2026-06-15"}&lt;/code&gt; -- valid JSON, everything works. The next request for "dim sum Saturday noon" produces &lt;code&gt;{"restaurant": "Dim Sum House", "party_size": 2, "time": "12:00", "date": "Saturday"}&lt;/code&gt; followed by a free-text aside: &lt;code&gt;-- also, what's the dress code?&lt;/code&gt;. Now your JSON parser throws, your downstream pipeline crashes, and your Slack channel lights up at 2 AM.&lt;/p&gt;

&lt;p&gt;The problem is fundamental: LLMs generate tokens, not data structures. Any schema you ask for is a suggestion, not a constraint. Production systems that depend on structured output need a mechanism that enforces the schema at the token level, not just at the prompt level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for production LLM applications
&lt;/h2&gt;

&lt;p&gt;Three scenarios where structured output is non-negotiable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API wrappers and function calling.&lt;/strong&gt; An LLM that calls tools on your behalf must produce arguments that match the tool's JSON Schema. A malformed argument means a runtime error from the tool, a retry, or silent failure. At scale, even a 2% malformation rate becomes a steady stream of incident alerts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data extraction and ETL pipelines.&lt;/strong&gt; You point an LLM at 10,000 support tickets and ask it to extract &lt;code&gt;{customer_id, sentiment, category, urgency}&lt;/code&gt;. If 3% of the rows have extra fields, missing fields, or non-JSON prose, your data pipeline either drops them silently or someone writes a regex band-aid that breaks later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-step agent loops.&lt;/strong&gt; An agent that calls a search tool, reads the result, then calls another tool needs each step's output to be parseable. If step 2 produces free text instead of a function call, the loop stalls. Every retry costs tokens, latency, and money.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The three approaches to structured output
&lt;/h2&gt;

&lt;p&gt;Developers today have three main ways to coerce an LLM into producing structured data. They differ in reliability, latency, and how deeply they integrate with the model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Enforcement level&lt;/th&gt;
&lt;th&gt;Latency overhead&lt;/th&gt;
&lt;th&gt;Model support&lt;/th&gt;
&lt;th&gt;Schema expressiveness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-only JSON mode&lt;/td&gt;
&lt;td&gt;None (suggestion)&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;All models&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-level JSON mode / function calling&lt;/td&gt;
&lt;td&gt;Soft (post-hoc validation + retry)&lt;/td&gt;
&lt;td&gt;0-200ms&lt;/td&gt;
&lt;td&gt;OpenAI, Anthropic, Gemini, most providers&lt;/td&gt;
&lt;td&gt;JSON Schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grammar-constrained decoding&lt;/td&gt;
&lt;td&gt;Hard (token-level)&lt;/td&gt;
&lt;td&gt;10-50ms per token&lt;/td&gt;
&lt;td&gt;Local models (llama.cpp, vLLM), Outlines, Guidance, lm-format-enforcer&lt;/td&gt;
&lt;td&gt;Any CFG, JSON Schema, regex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prompt-only is what you write when you first prototype. API-level structured output is what most teams use in production today. Grammar-constrained decoding is the emerging standard for local and self-hosted models where you control the sampling loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt-only JSON mode
&lt;/h2&gt;

&lt;p&gt;The simplest approach: tell the model to output JSON and hope it complies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a data extraction assistant.
Extract the requested fields and output ONLY valid JSON.
Do not include any explanation, markdown formatting, or extra text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works maybe 85-95% of the time with capable models, but the failure modes are maddening: trailing commas (not valid JSON but some parsers accept them), markdown code fences around the JSON, explanatory text before or after the JSON, missing closing braces, and string values that contain unescaped quotes.&lt;/p&gt;

&lt;p&gt;The fatal flaw is that prompt-only mode does not interact with the token generation process at all. If the model is partway through a field value and its next most likely token is &lt;code&gt;"fix"&lt;/code&gt; (the start of a free-text apology), it will generate that token. The prompt is just context -- it does not constrain the probability distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  API-level structured output (JSON mode and function calling)
&lt;/h2&gt;

&lt;p&gt;OpenAI introduced JSON mode in mid-2024, and the rest of the industry followed. The API takes a &lt;code&gt;response_format&lt;/code&gt; parameter with a JSON Schema. Behind the scenes, the provider uses a validator that resamples or masks tokens that would produce invalid JSON relative to the schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract: John Smith, 42, john@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;person&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is guaranteed to be valid JSON matching the schema, or the API returns an error. The 'strict' flag enforces that no extra properties are emitted.&lt;/p&gt;

&lt;p&gt;Function calling works similarly: you register tool definitions as JSON Schema objects, and the model returns a structured &lt;code&gt;tool_calls&lt;/code&gt; array. The provider handles the token-level enforcement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;book_restaurant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Book a table at a restaurant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restaurant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;party_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restaurant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;party_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model returns something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"book_restaurant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;restaurant&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Olive Garden&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;party_size&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:4,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;time&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;19:00&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-15&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anthropic Claude's tool use, Gemini's function calling, and Mistral's function calling all follow the same pattern. The schema is defined client-side, the provider validates at the token level, and the output is always parseable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grammar-constrained decoding
&lt;/h2&gt;

&lt;p&gt;For local and self-hosted models, you can push enforcement into the sampling loop itself. Grammar-constrained decoding modifies the token probability distribution at each step, zeroing out any token that would produce an invalid next character relative to a grammar or schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Using Outlines to constrain generation to a Pydantic model
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constr&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;outlines&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;constr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transformers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen2.5-7B-Instruct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract: John Smith, 42, john@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Person(name='John Smith', age=42, email='john@example.com')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outlines works by converting the JSON Schema or Pydantic model into a context-free grammar (CFG), then using that CFG to prune the token vocabulary at each generation step. Only tokens that represent valid continuations of the schema are kept.&lt;/p&gt;

&lt;p&gt;The same idea works for arbitrary grammars, not just JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Grammar-constrained generation with llama.cpp
# GBNF (Grammar-Based Negative-dFidence) format
&lt;/span&gt;&lt;span class="n"&gt;grammar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
root ::= digit+ &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; digit+
digit ::= &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama supports GBNF grammars natively. vLLM has a &lt;code&gt;--guided-decoding-backend&lt;/code&gt; flag (options: &lt;code&gt;outlines&lt;/code&gt;, &lt;code&gt;lm-format-enforcer&lt;/code&gt;, &lt;code&gt;xgrammar&lt;/code&gt;). The key insight is that grammar-constrained decoding makes structured output a sampling-time property, not a post-processing step.&lt;/p&gt;

&lt;p&gt;Here is how the token mask operates during generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Start generation] --&amp;gt; B[Get next token logits&amp;lt;br/&amp;gt;from model forward pass]
    B --&amp;gt; C[Apply grammar mask:&amp;lt;br/&amp;gt;zero-out tokens that would&amp;lt;br/&amp;gt;produce invalid structure]
    C --&amp;gt; D[Sample from masked&amp;lt;br/&amp;gt;probability distribution]
    D --&amp;gt; E{Is generation&amp;lt;br/&amp;gt;complete?}
    E --&amp;gt;|No| B
    E --&amp;gt;|Yes| F[Return valid structured&amp;lt;br/&amp;gt;output]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every token is checked against the schema before it is sampled. If the schema expects a number at position 73 and the model proposes a comma, that token is masked out and the next-best valid token is sampled instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: which method should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Prompt-only&lt;/th&gt;
&lt;th&gt;API JSON/function call&lt;/th&gt;
&lt;th&gt;Grammar-constrained&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;85-95%&lt;/td&gt;
&lt;td&gt;~99.9%&lt;/td&gt;
&lt;td&gt;&amp;gt;99.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency impact&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Negligible&lt;/td&gt;
&lt;td&gt;~10-50ms per token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with any model&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (provider-dependent)&lt;/td&gt;
&lt;td&gt;Yes (local framework)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema validation&lt;/td&gt;
&lt;td&gt;Post-hoc&lt;/td&gt;
&lt;td&gt;Token-level&lt;/td&gt;
&lt;td&gt;Token-level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging difficulty&lt;/td&gt;
&lt;td&gt;Easy (parse error)&lt;/td&gt;
&lt;td&gt;Medium (API error)&lt;/td&gt;
&lt;td&gt;Medium (grammar compile error)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best use case&lt;/td&gt;
&lt;td&gt;Prototyping, quick scripts&lt;/td&gt;
&lt;td&gt;Production API calls&lt;/td&gt;
&lt;td&gt;Self-hosted, sensitive data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nested schemas with strict mode.&lt;/strong&gt; OpenAI's strict JSON mode rejects extra properties. If your schema has &lt;code&gt;additionalProperties: true&lt;/code&gt; or relies on optional fields that the model sometimes fills with null, strict mode will return errors. Test with &lt;code&gt;strict: false&lt;/code&gt; first, then tighten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grammar compilation time.&lt;/strong&gt; Outlines and Guidance compile the schema into a state machine before generation starts. For complex schemas with deeply nested &lt;code&gt;allOf&lt;/code&gt; / &lt;code&gt;oneOf&lt;/code&gt;, this can take 2-10 seconds. Cache the compiled grammar if you reuse a schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token masking vs resampling.&lt;/strong&gt; Some implementations (early Guidance) used resampling: if the output was invalid, regenerate. This is slow and unpredictable. Prefer token-masking approaches (Outlines, xgrammar, llama.cpp GBNF) that never generate invalid tokens in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model incompatibility with grammar backends.&lt;/strong&gt; Not all Hugging Face model architectures work with Outlines' &lt;code&gt;transformers&lt;/code&gt; backend. If you hit an error about unsupported model type, switch to the &lt;code&gt;llamacpp&lt;/code&gt; backend or use vLLM's guided decoding instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Structured output is the wrong tool when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need open-ended creative text.&lt;/strong&gt; A story-writing or brainstorming session should not be grammar-constrained. The constraints reduce the model's output quality and diversity for tasks where free text is the goal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your schema changes frequently.&lt;/strong&gt; Grammar compilation and testing add overhead. If you are iterating on a schema multiple times per day, start with prompt-only JSON, then add enforcement once the schema stabilizes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your model is behind an API that does not support it.&lt;/strong&gt; Not all providers offer JSON mode or function calling. For those that do not, you are limited to prompt-only or running a local validation + retry loop, which adds latency and cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your use case tolerates occasional parse failures.&lt;/strong&gt; If a human reviews every output or the downstream system has robust error handling, the complexity of grammar-constrained decoding may not be worth it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Latency is the absolute top priority.&lt;/strong&gt; Grammar masking adds a small per-token overhead. For sub-100ms response requirements at high throughput, prompt-only with a lenient parser may be the pragmatic choice. Measure before optimizing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-only JSON&lt;/strong&gt; works ~85-95% of the time and is fine for prototyping, but will crash in production at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API-level JSON mode / function calling&lt;/strong&gt; (OpenAI, Anthropic, Gemini, Mistral) provides token-level enforcement with negligible latency overhead. Use this for production or when your provider supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grammar-constrained decoding&lt;/strong&gt; (Outlines, Guidance, llama.cpp GBNF, vLLM guided decoding) enforces schema at the sampling step. Best for self-hosted models and sensitive-data scenarios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token masking is better than resampling.&lt;/strong&gt; Prefer frameworks that mask invalid tokens rather than regenerating on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the overhead.&lt;/strong&gt; Grammar compilation and per-token masking add latency. Test with your schema and model before committing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;The pipeline that evaluated the output of grammar-constrained decoding against a test corpus of 10,000 real user requests -- how we measured reliability, what broke, and what the latency budget actually looked like in production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have a production story about structured output going wrong (or going right), the next post will compile reader experiences -- drop a comment with your war story.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Mixture of Experts (MoE): what it actually does under the hood, and when it pays off</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Sat, 13 Jun 2026 01:05:53 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/mixture-of-experts-moe-what-it-actually-does-under-the-hood-and-when-it-pays-off-alb</link>
      <guid>https://dev.to/tech_nuggets/mixture-of-experts-moe-what-it-actually-does-under-the-hood-and-when-it-pays-off-alb</guid>
      <description>&lt;h1&gt;
  
  
  Mixture of Experts (MoE): what it actually does under the hood, and when it pays off
&lt;/h1&gt;

&lt;p&gt;You deployed a 7B model in production. Response times are fine — 45 ms per token — but you want to scale to a 70B without buying four more GPUs. Someone mentions MoE: "70B performance at 7B compute." It sounds like free lunch. So you look at the Mixtral 8x7B paper, you see 45 billion parameters and a claim that each token only activates about 13 billion of them, and you wonder: &lt;em&gt;how is that physically possible, and what is the catch?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post explains the sparse MoE architecture that powers Mixtral, DeepSeek-MoE, Qwen2.5-MoE, DBRX, and Grok-1: what the router actually does, why load-balancing is the hardest problem in training them, and the three specific constraints that determine whether MoE is the right choice for your deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the distinction between total parameters and active parameters matters
&lt;/h2&gt;

&lt;p&gt;A dense transformer (like Llama 3.2) activates 100 percent of its parameters for every token. The FFN layer in each transformer block runs the same matrix multiplication for every input. This makes memory use predictable and throughput easy to model, but it also means that scaling from 7B to 70B multiplies both memory &lt;em&gt;and&lt;/em&gt; compute by 10x.&lt;/p&gt;

&lt;p&gt;MoE decouples the two. The model stores more parameters (more memory), but each token only uses a fraction of them (less compute). Here is the core trade-off expressed in numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Dense 7B&lt;/th&gt;
&lt;th&gt;Dense 70B&lt;/th&gt;
&lt;th&gt;MoE 45B (Mixtral)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total parameters&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;45B (8 experts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active per token&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;~12.9B (2 experts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute per token&lt;/td&gt;
&lt;td&gt;7B-equiv&lt;/td&gt;
&lt;td&gt;70B-equiv&lt;/td&gt;
&lt;td&gt;14B-equiv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (weights)&lt;/td&gt;
&lt;td&gt;~14 GB&lt;/td&gt;
&lt;td&gt;~140 GB&lt;/td&gt;
&lt;td&gt;~90 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (tokens/s)&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;medium-high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headline is this: &lt;strong&gt;MoE gives you better compute efficiency than a dense 70B, but you still pay the memory cost of a much larger model.&lt;/strong&gt; You cannot run Mixtral on a single consumer GPU. You need at least two 24 GB cards to fit the weights. The computational savings only show up once the model is already loaded — that is the catch that the "70B performance at 7B compute" tagline often omits.&lt;/p&gt;

&lt;h2&gt;
  
  
  How sparse MoE works in a transformer
&lt;/h2&gt;

&lt;p&gt;In a standard transformer, every layer has an FFN block (two linear projections with an activation in between). In a sparse MoE transformer, each FFN is replaced by multiple parallel "expert" FFNs plus a learned router that picks which experts to use for each token.&lt;/p&gt;

&lt;p&gt;Here is the data flow for a single token passing through one MoE layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Input token&amp;lt;br/&amp;gt;hidden states] --&amp;gt; B[Router / Gate&amp;lt;br/&amp;gt;learned linear layer]
    B --&amp;gt; C{Softmax over&amp;lt;br/&amp;gt;N experts}
    C --&amp;gt; D[Select top-k&amp;lt;br/&amp;gt;experts]
    D --&amp;gt; E1[Expert 1&amp;lt;br/&amp;gt;FFN]
    D --&amp;gt; E2[Expert 2&amp;lt;br/&amp;gt;FFN]
    D --&amp;gt; E3[...&amp;lt;br/&amp;gt;idle]
    D --&amp;gt; E4[Expert N&amp;lt;br/&amp;gt;idle]
    E1 --&amp;gt; F[Weighted sum&amp;lt;br/&amp;gt;by router scores]
    E2 --&amp;gt; F
    F --&amp;gt; G[Output token&amp;lt;br/&amp;gt;hidden states]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router is a small learned linear layer that takes the token's hidden state and outputs a score for each expert. You take the softmax over all experts, pick the k with the highest scores, run the token through only those experts, and combine the results weighted by the router scores. For Mixtral, k=2 out of 8 experts. For DeepSeek-MoE, k=6 out of 64 experts. The router itself adds negligible compute — a single matrix multiply of size (hidden_dim, n_experts).&lt;/p&gt;

&lt;h3&gt;
  
  
  The router is not just "which GPU does this go to"
&lt;/h3&gt;

&lt;p&gt;A common mental model is that the router is a load balancer that assigns tokens to experts similar to how a distributed scheduler assigns work to machines. This is misleading. The router is a &lt;strong&gt;learned differentiable gate&lt;/strong&gt; trained end-to-end with the rest of the model through backpropagation. It learns which experts specialize in which types of patterns — subject-matter expertise, syntactic structures, token positions — without any explicit supervision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expert specialization emerges, it is not designed
&lt;/h3&gt;

&lt;p&gt;When you inspect the routed outputs after training, individual experts do develop preferences. One expert in Mixtral handles arithmetic-heavy tokens disproportionately often. Another handles function words and punctuation. A third handles code syntax. But these specializations are soft, not hard: there is no constraint that says "expert 3 is the math expert." The router simply learns the assignment that minimizes the loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training an MoE model: the load-balancing problem
&lt;/h2&gt;

&lt;p&gt;The hardest part of MoE training is preventing the router from sending every token to the same two experts. If there is no corrective signal, the router quickly collapses: it sends everything to the experts that happen to initialize well, those experts get more gradient updates, they get better, the router sends even more traffic their way, and the unused experts atrophy.&lt;/p&gt;

&lt;p&gt;The standard fix is an &lt;strong&gt;auxiliary load-balancing loss&lt;/strong&gt; added to the total training loss. The most common formulation (used in Mixtral, GShard, and ST-MoE) penalizes the router for imbalance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified load-balancing loss (following the Switch Transformer formulation)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_balancing_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    router_logits: (num_tokens, num_experts) — raw router scores before softmax
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;router_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router_logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# (tokens, experts)
&lt;/span&gt;    &lt;span class="n"&gt;fraction_per_expert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router_probs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# (experts,) avg probability per expert
&lt;/span&gt;
    &lt;span class="c1"&gt;# Fraction of tokens routed to each expert
&lt;/span&gt;    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_experts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router_probs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_expert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_experts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;router_logits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tokens_per_expert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter_add_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_experts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; 
                                    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;router_logits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;load_per_expert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens_per_expert&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# (experts,) normalized token count
&lt;/span&gt;
    &lt;span class="c1"&gt;# Auxiliary loss: dot product of fraction and load
&lt;/span&gt;    &lt;span class="c1"&gt;# Minimized (zero) when all experts have equal probability AND equal load
&lt;/span&gt;    &lt;span class="n"&gt;aux_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;num_experts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fraction_per_expert&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;load_per_expert&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;aux_loss&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;num_experts&lt;/code&gt; multiplier scales the loss so it does not vanish at different expert counts. Typical &lt;code&gt;aux_loss&lt;/code&gt; coefficients are between 0.01 and 0.001. Too high and the router loses discriminative power. Too low and the expert collapse returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond the auxiliary loss: modern routing strategies
&lt;/h3&gt;

&lt;p&gt;Recent work has introduced alternatives that reduce or eliminate the auxiliary loss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek-MoE&lt;/strong&gt; uses a combination of shared experts (always-on, handles common patterns) and routed experts with top-6 selection. The shared experts cover the base computation that every token needs, so the routed experts can specialize more aggressively without collapsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen2.5-MoE&lt;/strong&gt; uses finer-grained experts (smaller intermediate size) with more of them, combined with shared experts and a "route-constrained" auxiliary loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dense-to-Sparse training&lt;/strong&gt; (DeepSpeed-MoE) starts with a dense checkpoint and incrementally sparsifies it, avoiding the collapse problem at initialization entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MoE serving: where throughput meets memory
&lt;/h2&gt;

&lt;p&gt;Serving an MoE model requires different infrastructure than a dense model. The key insight is that expert weights are &lt;em&gt;wide&lt;/em&gt; but &lt;em&gt;narrowly used&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expert parallelism&lt;/strong&gt;: place different experts on different GPUs. Since only k experts activate per token, each GPU only computes 2/k of the total expert FFN. This is the standard approach in vLLM, TGI, and SGLang for MoE models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory overhead&lt;/strong&gt;: all expert weights must be resident across the combined GPU memory. With 8 experts and 2 active per token, you need 4x the total GPU memory of the active-parameter count. For Mixtral (45B total, 12.9B active), you need ~90 GB of VRAM, which means at least 2x A100-80GB or 4x L40S.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All-to-all communication&lt;/strong&gt;: before the MoE layer, tokens must be grouped by which expert they were routed to, sent to the correct GPU, processed, and then sent back. The router dispatch and combine operations are the main latency bottleneck in MoE inference, not the expert compute itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a concrete serving comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vLLM configuration for MoE vs dense on 4x A100-80GB&lt;/span&gt;
&lt;span class="c1"&gt;# Dense 70B:&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;meta-llama/Llama-3.3-70B-Instruct&lt;/span&gt;
  &lt;span class="na"&gt;tensor_parallel_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;max_model_len&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8192&lt;/span&gt;
  &lt;span class="na"&gt;estimated throughput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~1800 tokens/s&lt;/span&gt;

&lt;span class="c1"&gt;# MoE 45B (Mixtral):&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mistralai/Mixtral-8x7B-Instruct-v0.1&lt;/span&gt;
  &lt;span class="na"&gt;tensor_parallel_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;max_model_len&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32768&lt;/span&gt;  &lt;span class="c1"&gt;# sliding window attention&lt;/span&gt;
  &lt;span class="na"&gt;estimated throughput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~3200 tokens/s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MoE throughput advantage is real but narrower than the parameter count suggests, because the dispatch overhead and the memory ceiling eat into the margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Router collapse during training.&lt;/strong&gt; Even with load-balancing loss, the router can still collapse in the first few thousand steps. Monitor the expert utilization histogram during training. If one expert receives more than 30 percent of tokens while another receives less than 5 percent, increase the auxiliary loss coefficient or switch to a different routing strategy (e.g., DeepSeek's shared-expert design).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring dispatch overhead in latency budgets.&lt;/strong&gt; The all-to-all communication in expert routing adds 5-15 ms per MoE layer depending on batch size and interconnect bandwidth. For a 32-layer model with 16 MoE layers, that is 80-240 ms of overhead before any compute happens. For latency-sensitive applications, this cost can erase the throughput gains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training on too-small batch sizes.&lt;/strong&gt; MoE models require larger batch sizes than dense models because the expert capacity constrain means that each expert sees only a fraction of the batch. A batch of 256 tokens with 8 experts and k=2 means each expert processes roughly 64 tokens. Training on small batches leads to underutilized experts and noisy gradients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using MoE for fine-tuning without adaptation.&lt;/strong&gt; Most MoE models were trained from scratch with MoE architecture. Taking a dense checkpoint and converting it to MoE (as in DeepSpeed-MoE's d2s approach) requires careful initialization and a warm-up schedule. Simple LoRA fine-tuning on an existing MoE model can break the learned routing patterns. Always evaluate the downstream task before and after fine-tuning to verify the routing did not drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measuring memory wrong.&lt;/strong&gt; The total parameter count of an MoE model determines &lt;code&gt;model.parameters()&lt;/code&gt;, but the memory you need to serve it is the sum of all experts plus the shared layers. For DeepSeek-MoE-16B, the 64 experts (each with intermediate_size 1408 at hidden_size 2048) means the expert weights alone occupy roughly 45 GB at FP16. The total 16B label refers to the active parameter count, not the storage requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;MoE is not always the right architecture for your model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need consistent latency for every request.&lt;/strong&gt; Because the router's top-k selection varies per token, and because batch composition affects which experts are active, MoE latency has higher variance than dense models. If your SLO requires 99th percentile latency under 200 ms per token, a dense model is easier to calibrate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You are deploying on a single GPU with less than 48 GB VRAM.&lt;/strong&gt; MoE models with real quality (anything above 2-3 active billion parameters) require at least two GPUs to fit the total weights. If your deployment is a single RTX 4090 or A5000, stick with dense models in the 7B-13B range.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You are building a small model under 3B parameters.&lt;/strong&gt; The overhead of the router, the auxiliary loss, and the expert parallelism infrastructure is not worth it at this scale. MoE starts to pay off when the dense baseline you are trying to beat is above 30-50B parameters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your batch size is small and latency-critical.&lt;/strong&gt; A batch of 1 (streaming chat) does not benefit from expert parallelism because the dispatch overhead dominates. The throughput advantage of MoE is most visible at batch sizes above 64.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You cannot afford the engineering complexity.&lt;/strong&gt; MoE serving requires custom kernel support (Triton or CUDA kernels for fused experts, dispatch, and combine), non-trivial CI for load-balancing validation, and integration with inference engines that are still maturing their MoE support. If your team has limited ML infrastructure, a dense model with QLoRA is the safer bet.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MoE decouples total parameters from per-token compute by routing each token to a subset of expert FFNs.&lt;/li&gt;
&lt;li&gt;Mixtral 8x7B has 45B total parameters but only activates ~13B per token, giving 70B-class compute efficiency at ~14B-class cost.&lt;/li&gt;
&lt;li&gt;The router is a learned linear layer trained end-to-end, not a scheduler. Expert specialization emerges naturally.&lt;/li&gt;
&lt;li&gt;Load-balancing loss is essential during training to prevent router collapse. Typical coefficients range from 0.01 to 0.001.&lt;/li&gt;
&lt;li&gt;Serving MoE requires expert parallelism across GPUs. Dispatch overhead is the main latency bottleneck, not the expert compute.&lt;/li&gt;
&lt;li&gt;MoE memory footprint is proportional to total parameters (all experts), not active parameters. You cannot fit Mixtral on a single 24 GB GPU.&lt;/li&gt;
&lt;li&gt;MoE pays off at large scale (target dense baseline above 30B). For small models, single-GPU deployments, or latency-sensitive applications, dense is simpler and often better.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next post:&lt;/strong&gt; structured output — how JSON mode, function calling, and grammar-constrained decoding work under the hood, and when each approach fails.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Sampling strategies compared: temperature, top-p, top-k, min-p, and what actually works in production</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Fri, 12 Jun 2026 01:12:21 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/sampling-strategies-compared-temperature-top-p-top-k-min-p-and-what-actually-works-in-2o16</link>
      <guid>https://dev.to/tech_nuggets/sampling-strategies-compared-temperature-top-p-top-k-min-p-and-what-actually-works-in-2o16</guid>
      <description>&lt;h1&gt;
  
  
  Sampling strategies compared: temperature, top-p, top-k, min-p, and what actually works in production
&lt;/h1&gt;

&lt;p&gt;You deployed a chatbot, picked temperature 0.7 because every blog post says that, and the first live user sends back screenshots of responses that drift into gibberish mid-sentence. A colleague suggests top-p 0.9. Another says top-k 50. Someone new to the team mentions min-p and claims it solves everything. You have no benchmark, no test set, and no way to tell whether any of these knobs actually fix your specific problem instead of just making the outputs shorter.&lt;/p&gt;

&lt;p&gt;This is the state of sampling parameter selection for most teams shipping LLM products. The parameters are poorly documented, they interact in non-intuitive ways, and the default values in every inference engine are tuned for general-purpose chat benchmarks, not for your use case. This post maps the four most common sampling knobs -- temperature, top-p, top-k, and min-p -- to the concrete effects they have on the output distribution, so you can pick the right one (or combination) without guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sampling parameters matter
&lt;/h2&gt;

&lt;p&gt;Every LLM generates text one token at a time by choosing from a probability distribution over the vocabulary. The raw distribution (the logits from the final transformer layer, passed through softmax) is almost never used directly. A raw distribution might assign 0.0001 probability to fifty thousand tokens and 0.3 to the top token. If you sample directly from that, you get a narrow band of high-probability continuations that sound repetitive and robotic.&lt;/p&gt;

&lt;p&gt;Sampling parameters reshape this distribution. The goal is to widen the distribution enough for creative or useful variation, but not so much that the model assigns meaningful probability to tokens that make no sense. Each parameter attacks a different failure mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temperature&lt;/strong&gt; controls the overall sharpness of the distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-p (nucleus sampling)&lt;/strong&gt; truncates the distribution to the smallest set of tokens whose cumulative probability reaches a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-k&lt;/strong&gt; keeps only the k highest-probability tokens and renormalizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min-p&lt;/strong&gt; scales a probability floor relative to the top token's probability, keeping tokens whose probability is at least that fraction of the top token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following diagram shows how each strategy transforms the same logit distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Raw logits&amp;lt;br/&amp;gt;from model] --&amp;gt; B[Softmax]
    B --&amp;gt; C[Full probability&amp;lt;br/&amp;gt;distribution]
    C --&amp;gt; D{Temperature}
    D --&amp;gt;|tau &amp;lt; 1| E[Sharpened&amp;lt;br/&amp;gt;peaks]
    D --&amp;gt;|tau &amp;gt; 1| F[Flattened&amp;lt;br/&amp;gt;tails]
    E --&amp;gt; G{Top-p / Top-k / Min-p}
    F --&amp;gt; G
    G --&amp;gt; H[Truncated&amp;lt;br/&amp;gt;distribution]
    H --&amp;gt; I[Sample&amp;lt;br/&amp;gt;next token]
    C --&amp;gt; J[Greedy argmax&amp;lt;br/&amp;gt;tau = 0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each box above is a tunable step. The order matters: temperature is applied to logits &lt;em&gt;before&lt;/em&gt; softmax, while top-p, top-k, and min-p are applied to the resulting probability distribution &lt;em&gt;after&lt;/em&gt; softmax. If you set temperature to 0 first, the later truncation parameters have no effect because the distribution is already a delta function on the argmax token.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four knobs, explained from the inside
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Temperature
&lt;/h3&gt;

&lt;p&gt;Temperature is the oldest and most widely understood parameter. It divides the logits by tau before softmax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(token_i) = exp(logit_i / tau) / sum_j exp(logit_j / tau)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When tau = 1, this is the standard softmax. When tau approaches 0, the distribution converges to a one-hot vector on the highest-probability token (greedy decoding). When tau is above 1, the distribution flattens, making low-probability tokens more likely than the raw model intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical ranges:&lt;/strong&gt; tau = 0 (deterministic, good for code generation or factual QA), tau = 0.1-0.3 (near-deterministic, useful for classification), tau = 0.6-0.9 (creative writing, conversational), tau = 1.0-1.5 (brainstorming, diverse generations). Above 1.5, the model increasingly produces incoherent text because it is assigning meaningful probability to tokens the model considers unlikely.&lt;/p&gt;

&lt;p&gt;The critical property of temperature is that it is a &lt;em&gt;distribution-wide&lt;/em&gt; transform. It does not prune any tokens; it just makes the probabilities more equal (tau &amp;gt; 1) or more unequal (tau &amp;lt; 1). This means tau &amp;gt; 1 can activate tokens that were essentially zero-probability in the raw distribution, including tokens that are misspellings, in the wrong language, or hallucinated -- because the model gave them low probability for a reason, and temperature is overriding that signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Top-p (nucleus sampling)
&lt;/h3&gt;

&lt;p&gt;Top-p, introduced by Holtzman et al. in 2019, solves a specific problem with temperature: temperature alone does not truncate the vocabulary. At tau = 0.8, the model still assigns tiny nonzero probability to thousands of tokens, and sampling from that long tail produces unexpected tokens.&lt;/p&gt;

&lt;p&gt;Top-p works by sorting tokens by probability descending, then keeping tokens from the top until their cumulative probability exceeds p. If p = 0.9, it keeps the top tokens that collectively account for 90% of the probability mass. This is adaptive: when the model is confident, top-p keeps few tokens; when uncertain, it keeps more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical ranges:&lt;/strong&gt; p = 0.8-0.95 for most generation tasks. Lower values (0.5-0.7) produce more focused outputs useful for factual QA. Values above 0.95 are close to no truncation at all. The surprising property of top-p is that it can be &lt;em&gt;less&lt;/em&gt; restrictive than top-k in high-entropy distributions, because it adapts to the distribution shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Top-k
&lt;/h3&gt;

&lt;p&gt;Top-k is the simplest truncation: keep only the k tokens with the highest probability and renormalize. A common default is k = 40 or k = 50, inherited from the early GPT-2 days.&lt;/p&gt;

&lt;p&gt;The problem with top-k is that it is static. When the distribution is peaked (model is confident), k = 50 keeps many low-probability tokens that should have been truncated. When the distribution is flat (model is uncertain), k = 50 cuts off tokens that carry meaningful probability. Top-k works acceptably when you have tuned k for a specific domain and model, but it is fragile across models and tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical ranges:&lt;/strong&gt; k = 10-50 for general generation. k = 1 is greedy (effectively tau = 0). k above 100 approaches no truncation for most models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Min-p
&lt;/h3&gt;

&lt;p&gt;Min-p, proposed by Nguyen et al. in 2024 (arXiv 2407.01082), addresses the static nature of top-k with an adaptive threshold. It works by setting a floor at (min_p * P_max), where P_max is the probability of the most likely token. Tokens below this floor are discarded, and the remaining distribution is renormalized.&lt;/p&gt;

&lt;p&gt;If min_p = 0.1 and the top token has probability 0.6, the floor is 0.06. Any token below 0.06 probability is pruned. When the model is confident (top token near 1), the floor is high and few tokens survive. When the model is uncertain (top token at 0.3), the floor drops and more tokens pass through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical ranges:&lt;/strong&gt; min_p = 0.01-0.2. Default recommendations from the paper are around 0.05-0.1 for a good balance of creativity and coherence. Values below 0.01 are close to no truncation. Values above 0.2 become very restrictive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Adaptive?&lt;/th&gt;
&lt;th&gt;Common range&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Key failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Temperature&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scales logits before softmax&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0 - 1.5&lt;/td&gt;
&lt;td&gt;Controlling randomness/creativity&lt;/td&gt;
&lt;td&gt;Enables low-probability tokens without discrimination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Top-p (nucleus)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps top tokens up to cumulative probability p&lt;/td&gt;
&lt;td&gt;Yes (adaptive count)&lt;/td&gt;
&lt;td&gt;0.8 - 0.95&lt;/td&gt;
&lt;td&gt;General generation when model confidence varies&lt;/td&gt;
&lt;td&gt;Can be too permissive in peaked distributions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Top-k&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps only k highest-probability tokens&lt;/td&gt;
&lt;td&gt;No (fixed count)&lt;/td&gt;
&lt;td&gt;10 - 50&lt;/td&gt;
&lt;td&gt;Legacy compatibility, simple tuning&lt;/td&gt;
&lt;td&gt;Static; either too restrictive or too permissive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Min-p&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps tokens with prob &amp;gt;= min_p * P_max&lt;/td&gt;
&lt;td&gt;Yes (adaptive threshold)&lt;/td&gt;
&lt;td&gt;0.01 - 0.2&lt;/td&gt;
&lt;td&gt;Production systems needing coherence + creativity&lt;/td&gt;
&lt;td&gt;Less tested at very large scales&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sampling in practice: what combinations work
&lt;/h2&gt;

&lt;p&gt;In production systems, sampling parameters are almost never used alone. The most common production recipe is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default for conversational agents:&lt;/strong&gt; temperature = 0.7, top-p = 0.9, min-p = 0.05. This gives enough randomness for natural variation while the min-p floor prevents the model from wandering into very low-probability regions. Top-k is usually turned off (set to 0 or a high value like 200) because min-p and top-p already handle truncation more adaptively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For code generation or structured output:&lt;/strong&gt; temperature = 0.1-0.2, top-p = 0.95, min-p = 0.01. The near-zero temperature forces most probability onto the top few tokens. Top-p at 0.95 ensures that when the model is truly uncertain (e.g., picking a variable name), it still has options beyond the argmax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For creative writing or brainstorming:&lt;/strong&gt; temperature = 0.9-1.1, top-p = 0.95, min-p = 0.02. Slightly elevated temperature encourages variety. The generous top-p keeps the distribution wide. The low min-p exists mainly as a safety net against the worst long-tail tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For classification or extraction:&lt;/strong&gt; temperature = 0 (greedy), no truncation parameters needed. When the output space is a fixed set of labels, any sampling at all reduces accuracy. This is the rare case where the default parameters are actually optimal.&lt;/p&gt;

&lt;p&gt;Here is a Python snippet showing how vLLM combines these parameters in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SamplingParams&lt;/span&gt;

&lt;span class="c1"&gt;# Conversational agent
&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;|im_end|&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Code generation
&lt;/span&gt;&lt;span class="n"&gt;code_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Classification (deterministic)
&lt;/span&gt;&lt;span class="n"&gt;classify_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stacking truncation parameters without understanding the interaction.&lt;/strong&gt; Top-p at 0.9 and top-k at 50 at the same time means two truncations fire sequentially. Top-p might keep 30 tokens, then top-k cuts that to 50 -- which does nothing. Or top-k keeps 50, then top-p might further trim them. The effective behavior depends on which truncation applies first. Most engines apply top-k first, then top-p, then min-p. If you set all three, you are relying on an ordering you may not remember next month. Pick at most two truncation methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting temperature above 1.5 and expecting coherence.&lt;/strong&gt; Temperature is not a creativity dial. Above 1.5, the model assigns significant probability to tokens it considers extremely unlikely. The outputs may appear creative but are actually random. If you need diverse outputs, try increasing top-p or lowering min-p instead of pushing temperature beyond 1.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using top-k as the only sampler.&lt;/strong&gt; This is the most common mistake I see in deployed services. A static k cannot adapt to the distribution. At k=50, sometimes you keep garbage and sometimes you cut off the valid tail. If you must use top-k alone, set k conservatively (10-20) and accept that you are leaving performance on the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting that temperature 0 disables all sampling.&lt;/strong&gt; If temperature is 0, the model always picks the argmax token. Top-p, top-k, and min-p have no effect because there is no distribution to truncate. If you see "temperature=0, top_p=0.95" in a config, the top_p is dead code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Applying sampling parameters incorrectly in batched inference.&lt;/strong&gt; Some inference engines share sampling parameters across all sequences in a batch. Passing a per-request temperature override that conflicts with the batch default causes silent fallback to the default. Always verify that per-request sampling overrides are actually wired through the batching layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Sampling parameters should not be the primary tool for improving output quality if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your outputs are incoherent at temperature 0.&lt;/strong&gt; Sampling parameters cannot fix a model that produces bad output even when it is maximally deterministic. If greedy decoding gives poor results, the problem is in the model, the prompt, or the training data, not in the sampling strategy. Add more examples to the prompt or improve the fine-tuning data before touching sampling parameters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need guaranteed structured output.&lt;/strong&gt; Sampling introduces nondeterminism. If the application requires valid JSON, a specific schema, or exact string matching, use constrained decoding (grammar-guided generation or JSON mode) instead of hoping the right parameters keep the output valid. Sampling parameters can reduce the rate of malformed output but cannot eliminate it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You are running a benchmark or eval.&lt;/strong&gt; Every paper and leaderboard uses greedy decoding (temperature 0) or a tightly controlled sampling procedure. If you compare a model at temperature 0.7 against another at temperature 0, you are measuring sampling strategy differences, not model quality differences. For evaluation, use deterministic settings and control for temperature as a variable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You have not measured the output quality.&lt;/strong&gt; Before tuning sampling parameters, establish a metric -- accuracy on a held-out set, human preference ratings, or a task-specific score. Without a metric, every sampling parameter change is cargo-culting. Measure first, tune second.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your application uses speculative decoding.&lt;/strong&gt; Speculative decoding's acceptance rate drops significantly at temperature 0 (greedy mode) compared to low-temperature sampling. If throughput is critical and you use speculation, the optimal temperature may be higher than you would choose for quality alone. Benchmark the throughput-quality tradeoff explicitly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temperature&lt;/strong&gt; scales logits before softmax. It is the only knob that affects the entire distribution uniformly. Use it to control randomness, from 0 (deterministic) to ~1.2 (max practical creativity).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-p&lt;/strong&gt; keeps the top tokens that cover p percent of the probability mass. It adapts to distribution shape and is the most popular general-purpose truncation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-k&lt;/strong&gt; keeps the top k tokens regardless of their probabilities. It is simple but fragile across inputs. Prefer top-p or min-p unless you have a specific reason for a fixed count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min-p&lt;/strong&gt; keeps tokens whose probability is at least a fraction of the top-token probability. It is the most adaptive truncation and works well as a safety net alongside temperature and top-p.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best production combo for most use cases:&lt;/strong&gt; temperature 0.7 + top-p 0.9 + min-p 0.05. Drop top-k entirely. For structured output, use constrained decoding instead of sampling tricks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never tune sampling parameters without a metric.&lt;/strong&gt; Greedy decoding (tau=0) is the first thing to check. If greedy fails, sampling parameters will not save you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;The MCP (Model Context Protocol) has been called the missing standard for tool integration, but the real question is what it costs in latency, reliability, and debuggability. Next post: a production-oriented walkthrough of MCP -- how tool calls flow through the protocol, where the serialization overhead lives, and what the current ecosystem actually supports.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Quantization formats compared: GGUF vs GPTQ vs AWQ vs NF4</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Thu, 11 Jun 2026 01:13:14 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/quantization-formats-compared-gguf-vs-gptq-vs-awq-vs-nf4-2mcm</link>
      <guid>https://dev.to/tech_nuggets/quantization-formats-compared-gguf-vs-gptq-vs-awq-vs-nf4-2mcm</guid>
      <description>&lt;h1&gt;
  
  
  Quantization formats compared: GGUF vs GPTQ vs AWQ vs NF4
&lt;/h1&gt;

&lt;p&gt;You just finished fine-tuning a 7B parameter model. The raw FP16 weights are 14 GB. Your target deployment is a single consumer GPU with 8 GB of VRAM, or perhaps an ARM MacBook with unified memory, or maybe a cloud instance where you pay per GB of GPU memory. The numbers do not add up. The model, as is, does not fit. You need to shrink it, and you need to shrink it in a way that does not turn it into a random-number generator.&lt;/p&gt;

&lt;p&gt;This is where weight quantization enters the picture. Reducing each parameter from 16 bits to 4 bits drops the memory footprint by 4x, from 14 GB to roughly 3.5 GB for a 7B model. The trick is how you do it, because not all 4-bit values are the same, and the trade-offs between memory, speed, accuracy, and portability are different for every format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why quantization format choice matters
&lt;/h2&gt;

&lt;p&gt;The format determines three things: which hardware can run the model, how fast inference runs, and how much accuracy you give up. These three constraints are in tension. A format optimized for CPU inference (GGUF) uses a different quantization scheme than one designed for GPU batch serving (GPTQ). A format that preserves more accuracy at the same bit-width (AWQ) may cost more to calibrate. A format designed for training (NF4 via bitsandbytes) is not the best choice for inference deployment.&lt;/p&gt;

&lt;p&gt;Choosing the wrong format means either leaving performance on the table, or worse, building a deployment pipeline around a format that the inference engine does not support. The landscape has settled into four major formats, each with a clear niche.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four formats: how they work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GGUF
&lt;/h3&gt;

&lt;p&gt;GGUF is the GGML Universal Format, created by the llama.cpp project. It is a container format that bundles model weights, tokenizer, and hyperparameters into a single file, with the weights already quantized. The quantization methods inside GGUF range from Q2_K to Q8_0, with Q4_K_M being the most popular sweet spot.&lt;/p&gt;

&lt;p&gt;GGUF quantizations use a block-wise scheme: weights are grouped into blocks (typically 32 weights per block) and each block gets its own scale and (optionally) zero-point. The K-quant variants (Q4_K_M, Q5_K_M, etc.) mix different bit-widths across different parts of the model, spending more bits on the layers that matter more.&lt;/p&gt;

&lt;p&gt;The format is designed for CPU and Apple Silicon inference. Because llama.cpp can offload some layers to GPU, GGUF also works on hybrid CPU+GPU setups, but the primary target is memory-constrained environments where a GPU is not available or not large enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPTQ
&lt;/h3&gt;

&lt;p&gt;GPTQ (GPU Post-Training Quantization) was introduced in 2023 by Frantar et al. from IST Austria. It is a weight-only quantization method that uses a second-order optimization procedure: it quantizes weights column by column, using the Hessian of the loss to adjust the remaining unquantized weights to compensate for the information lost on the already-quantized ones.&lt;/p&gt;

&lt;p&gt;The original implementation, AutoGPTQ, was archived in early 2025. The active successor is GPTQModel (v7.1.0, June 2026) from ModelCloud, which supports both Marlin and Triton kernels for fast GPU inference. GPTQ models are typically quantized to 4-bit (or occasionally 3-bit and 8-bit) and are stored in Hugging Face-compatible safetensors format with a &lt;code&gt;quantize_config.json&lt;/code&gt; metadata file.&lt;/p&gt;

&lt;p&gt;GPTQ requires a GPU to run. The Marlin kernel (int4 x fp16) achieves near-lossless throughput on NVIDIA GPUs, making GPTQ the default choice for serving quantized models on datacenter GPUs.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWQ
&lt;/h3&gt;

&lt;p&gt;AWQ (Activation-Aware Weight Quantization) was introduced by Lin et al. from MIT in 2024. The key insight is that not all weights are equally important -- the ones corresponding to large activation magnitudes have a disproportionate impact on output quality. AWQ identifies these "salient" weight channels by analyzing a small calibration dataset and protects them by scaling them up before quantization, then scaling the output back down during inference.&lt;/p&gt;

&lt;p&gt;The implementation is AutoAWQ (v0.2.9, May 2025). Like GPTQ, AWQ targets GPU inference and produces Hugging Face-compatible weights. AWQ tends to produce slightly lower perplexity than GPTQ at the same bit-width, especially at 4-bit, though the gap is small (typically within 0.1 perplexity points).&lt;/p&gt;

&lt;h3&gt;
  
  
  NF4
&lt;/h3&gt;

&lt;p&gt;NF4 (NormalFloat4) is a quantization data type introduced as part of the QLoRA paper (Dettmers et al., 2023). It is not a container format or a quantization algorithm per se -- it is a 4-bit data type that assumes the weights follow a normal distribution and uses a normalized float mapping that allocates more quantization levels near zero.&lt;/p&gt;

&lt;p&gt;NF4 is implemented in the bitsandbytes library (v0.49.2, February 2026) and is the default 4-bit type for QLoRA fine-tuning in the Hugging Face ecosystem. Unlike the other three formats, NF4 is primarily used for training (parameter-efficient fine-tuning) rather than inference deployment. You use NF4 to load a model in 4-bit during training, but you typically export to a different format for serving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;GGUF&lt;/th&gt;
&lt;th&gt;GPTQ&lt;/th&gt;
&lt;th&gt;AWQ&lt;/th&gt;
&lt;th&gt;NF4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary use case&lt;/td&gt;
&lt;td&gt;CPU / Apple Silicon inference&lt;/td&gt;
&lt;td&gt;GPU inference serving&lt;/td&gt;
&lt;td&gt;GPU inference serving&lt;/td&gt;
&lt;td&gt;QLoRA fine-tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container format&lt;/td&gt;
&lt;td&gt;Single .gguf file&lt;/td&gt;
&lt;td&gt;safetensors + config.json&lt;/td&gt;
&lt;td&gt;safetensors + config.json&lt;/td&gt;
&lt;td&gt;Not a standalone format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantization method&lt;/td&gt;
&lt;td&gt;Block-wise K-quants&lt;/td&gt;
&lt;td&gt;Hessian-based, column-by-column&lt;/td&gt;
&lt;td&gt;Activation-aware saliency scaling&lt;/td&gt;
&lt;td&gt;Normal-distribution optimized float&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical bit-width&lt;/td&gt;
&lt;td&gt;2-8 bits (Q4_K_M most common)&lt;/td&gt;
&lt;td&gt;4-bit (3/8 also supported)&lt;/td&gt;
&lt;td&gt;4-bit&lt;/td&gt;
&lt;td&gt;4-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU inference&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU inference&lt;/td&gt;
&lt;td&gt;Partial (layer offload)&lt;/td&gt;
&lt;td&gt;Yes (Marlin kernel)&lt;/td&gt;
&lt;td&gt;Yes (Triton kernel)&lt;/td&gt;
&lt;td&gt;Yes (training only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Silicon&lt;/td&gt;
&lt;td&gt;Native (Metal)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calibration data needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (128-512 samples)&lt;/td&gt;
&lt;td&gt;Yes (128-512 samples)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy at 4-bit&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference engine&lt;/td&gt;
&lt;td&gt;llama.cpp, Ollama, LM Studio&lt;/td&gt;
&lt;td&gt;vLLM, TGI, HF Transformers, GPTQModel&lt;/td&gt;
&lt;td&gt;vLLM, TGI, HF Transformers&lt;/td&gt;
&lt;td&gt;HF Transformers (training)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest version&lt;/td&gt;
&lt;td&gt;b9592 (llama.cpp, Jun 2026)&lt;/td&gt;
&lt;td&gt;GPTQModel v7.1.0 (Jun 2026)&lt;/td&gt;
&lt;td&gt;AutoAWQ v0.2.9 (May 2025)&lt;/td&gt;
&lt;td&gt;bitsandbytes 0.49.2 (Feb 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quantization at a glance: the pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[FP16 model&amp;lt;br/&amp;gt;16-bit weights] --&amp;gt; B{Which format?}
    B --&amp;gt;|CPU / Apple| C[GGUF quantization&amp;lt;br/&amp;gt;llama.cpp]
    B --&amp;gt;|GPU serving| D[GPTQ quantization&amp;lt;br/&amp;gt;GPTQModel]
    B --&amp;gt;|GPU serving| E[AWQ quantization&amp;lt;br/&amp;gt;AutoAWQ]
    B --&amp;gt;|QLoRA training| F[NF4 loading&amp;lt;br/&amp;gt;bitsandbytes]
    C --&amp;gt; G[Single .gguf file&amp;lt;br/&amp;gt;ready to run]
    D --&amp;gt; H[safetensors + config&amp;lt;br/&amp;gt;load with vLLM/TGI]
    E --&amp;gt; I[safetensors + config&amp;lt;br/&amp;gt;load with vLLM/TGI]
    F --&amp;gt; J[4-bit training&amp;lt;br/&amp;gt;export to deploy format]
    G --&amp;gt; K[llama.cpp / Ollama / LM Studio]
    H --&amp;gt; L[vLLM / TGI / Transformers]
    I --&amp;gt; L
    J --&amp;gt; B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram shows the branching decision. The critical fork is between CPU/Apple Silicon and GPU serving, because the format choice there determines the entire downstream toolchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treating all 4-bit as equivalent.&lt;/strong&gt; A 4-bit GPTQ model is not the same quality as a 4-bit GGUF Q4_K_M or a 4-bit NF4 model. The quantization method, calibration data, and block size all affect final perplexity. Always compare within the same family, and use perplexity as a relative guide, not an absolute one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming you need calibration data for every format.&lt;/strong&gt; GPTQ and AWQ both require a small calibration dataset (typically 128 samples from the training distribution). GGUF and NF4 do not. If you are quantizing a model for which you do not have representative sample data, GGUF is the simpler path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quantizing for GPU, then trying to run on CPU.&lt;/strong&gt; A GPTQ model uses GPU-only kernels. There is no CPU fallback. If you download a GPTQ model from Hugging Face and try to run it with llama.cpp, it will not work. Similarly, GGUF models run poorly (or not at all) in vLLM. The format and the runtime are coupled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building an AWQ model with a stale version.&lt;/strong&gt; AutoAWQ v0.2.9 (May 2025) is the latest release, but HF Transformers v5.11.0 (June 2026) also includes native AWQ loading via &lt;code&gt;transformers.AwqConfig&lt;/code&gt;. If you use the Transformers integration, you do not need the standalone AutoAWQ library. Check which path is supported by your inference engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using NF4 for deployment.&lt;/strong&gt; NF4 is not a format designed for fast inference. The bitsandbytes 4-bit dequantization path is slow compared to the dedicated kernels in GPTQ (Marlin) or AWQ (Triton). Use NF4 for QLoRA training, then re-quantize to GPTQ or GGUF for deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use each format
&lt;/h2&gt;

&lt;p&gt;Do not use GGUF if you are serving a high-throughput API on NVIDIA GPUs. The CPU fallback path of llama.cpp is slower than GPTQ's Marlin kernel at batch sizes above 1.&lt;/p&gt;

&lt;p&gt;Do not use GPTQ if your deployment target is a MacBook, a Raspberry Pi, or any non-NVIDIA GPU. GPTQ kernels are NVIDIA CUDA-only. For Apple Silicon, use GGUF. For AMD GPUs, check if ROCm-based GPTQ kernels are available (limited support as of mid-2026).&lt;/p&gt;

&lt;p&gt;Do not use AWQ if you cannot provide a representative calibration dataset. AWQ relies on activation statistics from real data. A mismatch between calibration data and deployment data degrades the saliency detection and can increase accuracy loss.&lt;/p&gt;

&lt;p&gt;Do not use NF4 for anything beyond training. It is a storage format for the QLoRA paper, not a deployment format. If you see a model on Hugging Face labeled "NF4", it was likely uploaded as a training checkpoint, not a serving artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;There are four mainstream LLM weight quantization formats: GGUF, GPTQ, AWQ, and NF4. Each targets a different deployment scenario.&lt;/li&gt;
&lt;li&gt;GGUF (llama.cpp) is for CPU and Apple Silicon inference. It is a self-contained single-file format with no calibration step.&lt;/li&gt;
&lt;li&gt;GPTQ (GPTQModel v7.1.0) is for NVIDIA GPU serving. It uses Hessian-based quantization and the Marlin kernel for fast inference.&lt;/li&gt;
&lt;li&gt;AWQ (AutoAWQ v0.2.9) is also for NVIDIA GPU serving. It uses activation-aware saliency scaling and achieves slightly better perplexity than GPTQ at the same bit-width.&lt;/li&gt;
&lt;li&gt;NF4 (bitsandbytes) is for QLoRA fine-tuning, not inference deployment. Use it to train, then re-quantize for serving.&lt;/li&gt;
&lt;li&gt;Choose your format based on your hardware (CPU vs NVIDIA GPU vs Apple Silicon) before considering bit-width or accuracy metrics. The runtime determines the format.&lt;/li&gt;
&lt;li&gt;Calibration data is required for GPTQ and AWQ, but not for GGUF and NF4.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;Now that you know which format to use, the next question is: how fast will a quantized model actually run on your hardware? The next post breaks down tokens-per-second for each format across consumer GPUs, Apple Silicon, and CPU configurations, with concrete benchmarks you can use to size your deployment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have a quantized model deployment story -- or a horror story about picking the wrong format -- the comments are the place to share it. The next post will include community-sourced numbers from exactly these stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>quantization</category>
      <category>mlops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Flash Attention: what it does and why it matters</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Wed, 10 Jun 2026 11:20:09 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/flash-attention-what-it-does-and-why-it-matters-59b8</link>
      <guid>https://dev.to/tech_nuggets/flash-attention-what-it-does-and-why-it-matters-59b8</guid>
      <description>&lt;h1&gt;
  
  
  Flash Attention: what it does and why it matters
&lt;/h1&gt;

&lt;p&gt;Your training job is paying for an A100 at $3/hour. The loss is going down, gradients are flowing, and the model's loss curve looks textbook-logarithmic. But if you profile the step time and look at what the GPU is actually doing, you'll see something alarming: the GPU compute units are idle 40-60% of the time. The bottleneck isn't arithmetic -- it's memory bandwidth. The GPU's HBM (high-bandwidth memory, 1.5-2 TB/s on an A100) cannot keep up with how fast the compute units want to consume data. And the single biggest chunk of memory traffic in any transformer training or inference run is the attention computation, which naively reads and writes the full N x N attention matrix to HBM for every forward pass.&lt;/p&gt;

&lt;p&gt;Flash Attention exists to solve that one problem: it eliminates the redundant HBM traffic by fusing the attention computation into tiles that stay entirely inside the GPU's SRAM (the fast, on-chip memory, roughly 20 MB on an A100). The result is a 2-4x end-to-end speedup on attention-bound workloads, at zero loss of precision, with no model changes required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why attention memory costs matter
&lt;/h2&gt;

&lt;p&gt;A standard self-attention layer on a single head works with three matrices Q, K, V, each of shape (N, d) where N is the sequence length and d is the head dimension. The naive computation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute S = Q @ K^T -- shape (N, N)&lt;/li&gt;
&lt;li&gt;Compute P = softmax(S, dim=-1) -- shape (N, N)&lt;/li&gt;
&lt;li&gt;Compute O = P @ V -- shape (N, d)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical cost is that S and P are each N x N entries. For a 4096-token sequence with d=128, that's 16 million entries per head. At FP16, that's 32 MB per head. With 32 heads, the full N x N matrix across all heads would be 1 GB -- far larger than the ~20 MB of SRAM on a single A100 GPU. The standard implementation writes this 1 GB to HBM (slow), reads it back for softmax (HBM read), writes the result back (HBM write), then reads it again for the V multiplication.&lt;/p&gt;

&lt;p&gt;Flash Attention avoids materializing this N x N matrix entirely by tiling the softmax computation across blocks small enough to fit in SRAM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Flash Attention actually does
&lt;/h2&gt;

&lt;p&gt;The core insight from Tri Dao and the Stanford group (2022) was that the attention computation is IO-bound, not compute-bound, and the dominant cost is moving data between HBM and SRAM. On an A100, SRAM bandwidth is roughly 20 TB/s (compute units to SRAM), while HBM bandwidth is ~2 TB/s. A 10x difference. If the computation can be structured to stay in SRAM, it wins.&lt;/p&gt;

&lt;p&gt;The mechanism is algorithmically straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Block the Q, K, V matrices&lt;/strong&gt; into tiles small enough to fit in SRAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute a partial softmax&lt;/strong&gt; for each block, using the online softmax algorithm (safe softmax that can be updated incrementally).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accumulate partial results&lt;/strong&gt; into the output, keeping per-block rescaling statistics in registers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the final output&lt;/strong&gt; to HBM once per layer, instead of multiple reads/writes per head.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a classic tiling technique, but applied to the attention-specific problem where the softmax is a global normalization -- you cannot naively sum over tiles because softmax requires a denominator over the full row. The paper's key algorithmic contribution is an online-safe softmax that lets each tile compute a local softmax and then correct the running output as new tiles arrive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode for one Flash Attention forward pass block
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flash_attention_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V_block&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Q_block: (B_r, d), K_block: (B_c, d), V_block: (B_c, d)
&lt;/span&gt;    &lt;span class="c1"&gt;# B_r and B_c are tile sizes chosen to fit in SRAM
&lt;/span&gt;
    &lt;span class="c1"&gt;# Initialize running maximum and denominator
&lt;/span&gt;    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;inf&lt;/span&gt;   &lt;span class="c1"&gt;# row-wise max for numerical stability
&lt;/span&gt;    &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;    &lt;span class="c1"&gt;# sum of exp(x - m) for the running normalization
&lt;/span&gt;    &lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B_r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt; &lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Q_block&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;K_tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;        &lt;span class="c1"&gt;# local attention scores (B_r, B_c)
&lt;/span&gt;        &lt;span class="n"&gt;m_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rowmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# update running max
&lt;/span&gt;        &lt;span class="n"&gt;l_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rowsum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_new&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;l_new&lt;/span&gt;    &lt;span class="c1"&gt;# local softmax
&lt;/span&gt;        &lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m_new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;l_new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;V_tile&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l_new&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;O&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The algorithm reads Q, K, V from HBM once, processes them tile by tile in SRAM, and writes O to HBM once. Compare to the naive approach: for a sequence of length N, the standard implementation reads and writes the N x N attention matrix to HBM, which is O(N^2 d) HBM traffic. Flash Attention reduces this to O(N^2 d / M) where M is the SRAM size -- a reduction proportional to SRAM capacity.&lt;/p&gt;

&lt;p&gt;The following diagram shows how the tiling skips the materialization of the full attention matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph SRAM["GPU SRAM (~20 MB)"]
        QB[Q tile&amp;lt;br/&amp;gt;(B_r x d)]
        KB[K tile&amp;lt;br/&amp;gt;(B_c x d)]
        VB[V tile&amp;lt;br/&amp;gt;(B_c x d)]
        ST[Partial S = QB @ KB^T&amp;lt;br/&amp;gt;(B_r x B_c)]
        OT[Partial O accumulator&amp;lt;br/&amp;gt;(B_r x d)]
    end
    subgraph HBM["GPU HBM (~40-80 GB)"]
        QF[Full Q&amp;lt;br/&amp;gt;(N x d)]
        KF[Full K&amp;lt;br/&amp;gt;(N x d)]
        VF[Full V&amp;lt;br/&amp;gt;(N x d)]
        OF[Full O&amp;lt;br/&amp;gt;(N x d)]
    end

    QF --&amp;gt;|read once| QB
    KF --&amp;gt;|read once&amp;lt;br/&amp;gt;tile by tile| KB
    VF --&amp;gt;|read once&amp;lt;br/&amp;gt;tile by tile| VB
    KB --&amp;gt; ST
    VB --&amp;gt;|partial products| OT
    OT --&amp;gt;|write once| OF

    style SRAM fill:#1e293b,stroke:#38bdf8,color:#e2e8f0
    style HBM fill:#0f172a,stroke:#64748b,color:#94a3b8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each arrow from HBM to SRAM is a slow DMA transfer. The naive implementation makes O(N) of these per row and per head. Flash Attention makes exactly two passes over K and V (read and tile-by-tile process), then writes O once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flash Attention v1 vs v2 vs v3
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Key improvements&lt;/th&gt;
&lt;th&gt;Speedup vs naive&lt;/th&gt;
&lt;th&gt;GPU focus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Tiling + online softmax, O(N^2) avoidance&lt;/td&gt;
&lt;td&gt;2x&lt;/td&gt;
&lt;td&gt;A100 (Ampere)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Reduced non-matmul ops, better parallelism, non-power-of-2 lengths supported&lt;/td&gt;
&lt;td&gt;2-3.5x&lt;/td&gt;
&lt;td&gt;A100, H100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2024-2025&lt;/td&gt;
&lt;td&gt;WGMMA (warp-group matrix multiply-accumulate) for H100 Tensor Cores, async pipelining, FP8 support&lt;/td&gt;
&lt;td&gt;3-7x&lt;/td&gt;
&lt;td&gt;H100/B200 (Hopper)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flash Attention v2 removed a significant number of non-matrix-multiply instructions that creation of the mask and scaling required. This matters because Tensor Cores are most efficient when the workload is pure matrix multiplication, and any extra elementwise operations reduce utilization. The v2 paper reported that a single forward pass on a 65M-parameter model went from 6.5ms (PyTorch standard) to 2.6ms (Flash Attention v2).&lt;/p&gt;

&lt;p&gt;Flash Attention v3, published in 2024, targets the H100's Hopper architecture. It uses the WGMMA instruction (warp-group MMA), which lets the GPU overlap data movement with computation during the tiled softmax pass. The synchronous SRAM reads of v1/v2 are replaced with asynchronous copies that hide latency. Additionally, v3 introduces FP8 support that cuts data movement in half again for the score computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Flash Attention is used today
&lt;/h2&gt;

&lt;p&gt;Flash Attention is integrated into virtually every major LLM framework. The most common path is through PyTorch's &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; (SDPA), which has shipped the flash-attention backend since PyTorch 2.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn.functional&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="c1"&gt;# This automatically uses Flash Attention if conditions are met:
# - CUDA GPU
# - dtype is half-precision (FP16 or BF16)
# - head_dim is a multiple of 8
# - (v2+) Sequence length doesn't have restrictions on being power of 2
&lt;/span&gt;&lt;span class="n"&gt;attn_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaled_dot_product_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attn_mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dropout_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_causal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to import &lt;code&gt;flash_attn&lt;/code&gt; directly in most cases. PyTorch's SDPA dispatches automatically to the best available backend: Flash Attention if available, otherwise memory-efficient attention, and falls back to the naive implementation.&lt;/p&gt;

&lt;p&gt;For direct access, the &lt;code&gt;flash-attn&lt;/code&gt; package on PyPI provides the &lt;code&gt;FlashAttention&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flash-attn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs a prebuilt wheel matching your CUDA and PyTorch combination (PyPI wheels are available starting with v2.8.x). If no wheel exists for your configuration, building from source takes about 15 minutes and requires a CUDA compiler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flash_attn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;flash_attn_func&lt;/span&gt;

&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flash_attn_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dropout_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;softmax_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;causal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;flash_attn_func&lt;/code&gt; API gives you direct control over the backend parameters and is the path used by vLLM, Hugging Face &lt;code&gt;transformers&lt;/code&gt;, and torch.compile paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The is_causal / padding interaction.&lt;/strong&gt; If you use a causal mask AND a separate padding mask (for batched sequences of different lengths), the interaction between them is non-trivial. Flash Attention should handle it, but passing &lt;code&gt;attn_mask&lt;/code&gt; with both a causal mask and individual padding requires careful construction. The safest approach is to leave &lt;code&gt;causal=True&lt;/code&gt; and pad to the same length, or use a per-batch mask that is the full N x N with -inf in the right places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Head dimension limits.&lt;/strong&gt; Flash Attention has historically had constraints on head dimension. v1 required head_dim &amp;lt;= 128. v2 increased this to head_dim &amp;lt;= 256. v3 supports up to 256. If your model uses head_dim=96 or head_dim=64, you are fine. If you are experimenting with head_dim=512 (rare but seen in some vision transformers), Flash Attention cannot accelerate that attention computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CUDA graph compatibility.&lt;/strong&gt; Flash Attention uses a variable amount of shared memory depending on the tile size, which can cause issues with CUDA graph capture. If you are using &lt;code&gt;torch.compile&lt;/code&gt; with &lt;code&gt;mode="reduce-overhead"&lt;/code&gt;, test that the Flash Attention kernel does not prevent graph capture. v2.8.x has improved this, but the interaction is not guaranteed across all PyTorch versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AMD GPUs and non-CUDA backends.&lt;/strong&gt; Flash Attention is a CUDA kernel. It does not run on AMD ROCm out of the box. The ROCm ecosystem has an alternative implementation called &lt;code&gt;triton&lt;/code&gt;-based Flash Attention, but it has different performance characteristics and is not a drop-in replacement. If you are on AMD GPUs, benchmark before assuming parity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic fallback in SDPA can hide problems.&lt;/strong&gt; Because PyTorch's SDPA silently falls back to the naive implementation if Flash Attention conditions are unmet, you can accidentally get different kernels on different GPU types and not notice. Always log which SDPA backend was selected if you care about reproducible performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Flash Attention is the wrong optimization if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your bottleneck is the MLP layers, not attention.&lt;/strong&gt; For inference workloads where batch size is 1 and sequence length is short (under 512 tokens), the attention compute is a small fraction of total time. The MLP projections dominate. Optimizing attention gives you a 5-10% speedup instead of 2-4x. Profile first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You are on CPU inference.&lt;/strong&gt; Flash Attention requires a CUDA-capable GPU. CPUs use entirely different attention paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You need integer-only attention (e.g., quantized KV cache on CPU/edge devices).&lt;/strong&gt; Flash Attention is implemented in CUDA and expects FP16/BF16 data. Quantized attention kernels (MatMul-free LLMs, etc.) use different algorithms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You are training a small model for quick iteration.&lt;/strong&gt; If your model takes 30 seconds per epoch, optimizing attention will not move the bottleneck. The overhead of importing and configuring Flash Attention (not large, but nonzero) is wasted effort.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your sequence length is extremely long (100K+ tokens).&lt;/strong&gt; For very long sequences, the memory-efficient attention in SDPA (which is Flash Attention for normal lengths) may still require an HBM pass that makes the tiling less effective. The Ring Attention / DeepSpeed Ulysses / Stripe Attention approaches are better suited above 100K tokens because they shard across GPUs instead of within a single GPU's SRAM.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Flash Attention tiles the Q, K, V matrices into blocks that fit in GPU SRAM, computing the softmax online without ever materializing the full N x N attention matrix in HBM.&lt;/li&gt;
&lt;li&gt;v2.8.3.post1 is the current stable release (June 2026). v2 improved parallelism and removed length restrictions. v3 added H100-specific WGMMA instructions and FP8 support.&lt;/li&gt;
&lt;li&gt;The speedup is 2-4x on A100-class GPUs, 3-7x on H100, at zero precision loss, with no model architecture changes required.&lt;/li&gt;
&lt;li&gt;You get it automatically through PyTorch &lt;code&gt;F.scaled_dot_product_attention&lt;/code&gt; or directly via the &lt;code&gt;flash_attn&lt;/code&gt; package.&lt;/li&gt;
&lt;li&gt;Watch for head_dim limits (max 256 in v2/v3), CUDA graph compatibility, and the silent SDPA backend fallback that can hide performance regressions.&lt;/li&gt;
&lt;li&gt;Do not use Flash Attention if your bottleneck is not attention, you are on CPU/AMD, or you have extreme sequence lengths that require inter-GPU sharding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: a practical comparison of sampling strategies -- temperature, top-p, top-k, min-p, and what actually produces better output quality in production systems.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>deeplearning</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Flash Attention: what it does and why it matters</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Wed, 10 Jun 2026 09:58:51 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/flash-attention-what-it-does-and-why-it-matters-o27</link>
      <guid>https://dev.to/tech_nuggets/flash-attention-what-it-does-and-why-it-matters-o27</guid>
      <description>&lt;h1&gt;
  
  
  Flash Attention: what it does and why it matters
&lt;/h1&gt;

&lt;p&gt;You have a single H100 with 80 GB of VRAM. The Llama 3.1 70B model fits — barely, at 140 GB in FP16, so you're running at 4-bit quantization and have maybe 5–8 GB of KV cache space left for a long-context workload. The model is fast enough at 8K context, so you push it to 32K for a RAG pipeline. It's still fine. Then you push it to 128K for a document-summary task, and suddenly the attention layer alone is spending 3 seconds per forward pass, 85% of which is just &lt;em&gt;moving data between HBM and SRAM&lt;/em&gt;, not doing math. The CUDA kernel occupancy graph tells the story: green compute bars are tiny, grey memory-stall bars are huge. The GPU is bandwidth-bound, and vanilla attention is the cause.&lt;/p&gt;

&lt;p&gt;Flash Attention is the algorithm that fixes this by restructuring the attention computation itself — not approximate, not sparse, not quantized, just &lt;em&gt;IO-aware&lt;/em&gt;. Here is what it does, how the three versions differ, and where it stops helping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters in practice
&lt;/h2&gt;

&lt;p&gt;The attention mechanism is the core of every transformer: compute a similarity matrix S = Q K^T, normalize it with softmax P = softmax(S), and use it as weights over values O = P V. The problem is that for sequence length N and head dimension d, the S and P matrices are N×N, and writing them to GPU HBM (high-bandwidth memory) and reading them back is the bottleneck, not the matrix multiplies themselves.&lt;/p&gt;

&lt;p&gt;For N = 32K and d = 128 (a single GPT-style head), S is 1 GB. At HBM bandwidth of 2 TB/s on an H100, moving that matrix out and back costs ~1 ms per layer. Across 80 layers and both forward and backward passes, that adds up to 150+ ms per step, and you haven't done a single useful ALU operation yet — just memory shuffling. At 128K context, the per-layer HBM traffic for vanilla attention hits ~16 GB, and the memory wall dominates.&lt;/p&gt;

&lt;p&gt;Flash Attention eliminates almost all of the intermediate HBM traffic by &lt;em&gt;tiling&lt;/em&gt; the Q, K, V matrices into blocks that fit in on-chip SRAM (192 KB on A100, 256 KB on H100), performing the entire softmax + weighted sum inside SRAM, and only writing the final output O back to HBM. The result: 2–4× faster attention for typical long-context workloads, up to 10× for very long sequences, with &lt;em&gt;bit-exact&lt;/em&gt; output for FP16/BF16 and tiny relative error in FP8.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the algorithm works
&lt;/h2&gt;

&lt;p&gt;The core insight is that softmax over a sub-block can be recomputed from the running statistics. You don't need the full N×N matrix — you can process Q, K, V in blocks, compute local softmax within each block, maintain an online estimate of the softmax denominator, and merge the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph HBM["HBM (main memory)"]
        Q["Q (N × d)"]
        K["K (N × d)"]
        V["V (N × d)"]
        O["O (N × d)"]
    end
    subgraph SRAM["SRAM (on-chip, ~192 KB)"]
        Qi["Q_block (Bc × d)"]
        Kj["K_block (Br × d)"]
        Vj["V_block (Br × d)"]
        Sij["S_block (Bc × Br)"]
        Pij["P_block (Bc × Br)"]
        Oi["O_block accumulator"]
        mi["Row max&amp;lt;br/&amp;gt;m_i"]
        li["Row sum&amp;lt;br/&amp;gt;ℓ_i"]
    end
    Q --&amp;gt;|tile| Qi
    K --&amp;gt;|tile| Kj
    V --&amp;gt;|tile| Vj
    Qi --&amp;gt; Sij
    Kj --&amp;gt; Sij
    Sij --&amp;gt; Pij
    Pij --&amp;gt; Oi
    Oi -.-&amp;gt;|write| O
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The algorithm for each attention head proceeds as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Divide Q into blocks&lt;/strong&gt; of size Bc that fit in SRAM alongside one block each of K and V.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divide K and V into blocks&lt;/strong&gt; of size Br.&lt;/li&gt;
&lt;li&gt;For each Q block i and each K/V block j:

&lt;ul&gt;
&lt;li&gt;Load Q_i and K_j, V_j into SRAM.&lt;/li&gt;
&lt;li&gt;Compute S_ij = Q_i K_j^T in SRAM.&lt;/li&gt;
&lt;li&gt;Compute local softmax: m_ij = rowmax(S_ij), P_ij = exp(S_ij - m_ij), ℓ_ij = rowsum(P_ij).&lt;/li&gt;
&lt;li&gt;Update global running max m_i = max(m_i, m_ij).&lt;/li&gt;
&lt;li&gt;Update global running sum ℓ_i = exp(m_i_prev - m_i) · ℓ_i + exp(m_ij - m_i) · ℓ_ij.&lt;/li&gt;
&lt;li&gt;Correct and accumulate output: O_i = O_i · exp(m_i_prev - m_i) / (ℓ_i / ℓ_i_prev) + (P_ij V_j) / ℓ_i.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the final O_i&lt;/strong&gt; back to HBM after all K/V blocks have been processed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical property: the output is &lt;em&gt;identical&lt;/em&gt; to vanilla attention in FP16/BF16, because softmax over the full sequence is exactly reconstructed from the block-level statistics. The algorithm does not approximate — it rearranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flash Attention 1 → 2 → 3
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Vanilla&lt;/th&gt;
&lt;th&gt;Flash Attn v1&lt;/th&gt;
&lt;th&gt;Flash Attn v2&lt;/th&gt;
&lt;th&gt;Flash Attn v3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Dao et al., 2022&lt;/td&gt;
&lt;td&gt;Dao et al., 2023&lt;/td&gt;
&lt;td&gt;Shah + Dao, 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU target&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;A100 (Ampere)&lt;/td&gt;
&lt;td&gt;A100 + H100&lt;/td&gt;
&lt;td&gt;H100/H200 (Hopper)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HBM traffic per step&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(N² d)&lt;/td&gt;
&lt;td&gt;O(N² d / M)&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forward speed vs vanilla&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1×&lt;/td&gt;
&lt;td&gt;2–3×&lt;/td&gt;
&lt;td&gt;3–4×&lt;/td&gt;
&lt;td&gt;4–6×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backward speed vs vanilla&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1×&lt;/td&gt;
&lt;td&gt;2–3×&lt;/td&gt;
&lt;td&gt;4–5×&lt;/td&gt;
&lt;td&gt;6–8×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Precision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FP32/BF16&lt;/td&gt;
&lt;td&gt;FP16/BF16&lt;/td&gt;
&lt;td&gt;FP16/BF16&lt;/td&gt;
&lt;td&gt;FP8/BF16/FP16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;standard&lt;/td&gt;
&lt;td&gt;FP16 only&lt;/td&gt;
&lt;td&gt;BF16 + FP16&lt;/td&gt;
&lt;td&gt;FP8 + BF16 + FP16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core technique&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Tiling + recompute&lt;/td&gt;
&lt;td&gt;Improved block scheduling&lt;/td&gt;
&lt;td&gt;Async WGMMA + FP8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CUDA features used&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;standard&lt;/td&gt;
&lt;td&gt;MMA (Tensor Core)&lt;/td&gt;
&lt;td&gt;MMA + better occupancy&lt;/td&gt;
&lt;td&gt;WGMMA + async copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓ (Dao-AILab)&lt;/td&gt;
&lt;td&gt;✓ (Dao-AILab)&lt;/td&gt;
&lt;td&gt;✓ (Dao-AILab)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flash Attention v1&lt;/strong&gt; (NeurIPS 2022, the paper that started it): Introduced the tiling scheme, proved the IO complexity result (O(N² d / M) HBM accesses vs O(N² d) for vanilla), and showed that the algorithm is exact for FP16. Forward pass is 2–3× faster than PyTorch's &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; on A100s. The backward pass uses the same tiling approach but recomputes S and P from the stored Q, K, V tiles rather than materializing the full gradient matrices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flash Attention v2&lt;/strong&gt; (2023): Redesigned the work distribution. In v1, each thread block processes one Q-block and iterates over all K/V blocks (SPMD-style). In v2, the parallelism is over different Q-blocks independently, and within each block the softmax reduction is fused with the output accumulation. This halves the number of global atomics and improves occupancy. v2 is roughly 2× faster than v1 on both A100 and H100, and it's the version that made Flash Attention a default in Hugging Face Transformers and PyTorch 2.x.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flash Attention v3&lt;/strong&gt; (2024–2025, Hopper-specific): Taps the H100's WGMMA (warp-group matrix multiply-accumulate) instructions and asynchronous TMA (tensor memory accelerator) copies. v3 overlaps SRAM data transfers with computation via async copies: while the current block is computing attention, the next block's K, V tiles are being fetched in the background. The FP8 path uses the H100's 2× faster FP8 Tensor Cores (1.97 PFLOPS vs 989 TFLOPS for FP16) with stochastic rounding. v3 delivers 4–6× speedup over vanilla attention and is the recommended default for Hopper GPUs with sequence lengths above 8K.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it in practice
&lt;/h2&gt;

&lt;p&gt;Flash Attention 3 is included in the &lt;code&gt;flash-attn&lt;/code&gt; PyPI package (v3.1.2 as of May 2026). Installation is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flash-attn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is straightforward once the package is installed. The main entry points are functions, not a module that auto-patches your model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flash_attn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;flash_attn_func&lt;/span&gt;

&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# (batch, heads, seqlen, headdim) → (batch, seqlen, heads, headdim)
&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flash_attn_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dropout_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;softmax_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;causal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# out shape: (1, 4096, 32, 128) — same as input layout
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most users, the easiest path is PyTorch's &lt;code&gt;torch.nn.functional.scaled_dot_product_attention&lt;/code&gt;, which detects Flash Attention through the &lt;code&gt;torch.backends.cuda.sdp_kernel&lt;/code&gt; context manager and dispatches to it automatically when the input dtype, layout, and GPU support it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_flash_sdp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# on by default in PyTorch 2.x
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sdp_kernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;enable_flash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enable_math&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enable_mem_efficient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scaled_dot_product_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_causal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dispatch check is reliable on A100 and H100 with BF16/FP16 inputs and head dimensions of 64 or 128. For FP8, you need H100 and &lt;code&gt;flash_attn_func&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;FA3 also integrates with Hugging Face models via &lt;code&gt;attn_implementation="flash_attention_2"&lt;/code&gt; in &lt;code&gt;from_pretrained&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta-llama/Llama-3.1-8B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flash_attention_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This swaps the attention module during model loading and is the path most training pipelines use today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Head dimension must be 64 or 128 (v1/v2) or up to 256 (v3).&lt;/strong&gt; This is a hardware constraint from Tensor Core layout requirements. Models with unusual head dims (e.g., 80 in some older architectures) will silently fall back to vanilla attention with no error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FP8 has higher numerical error on outlier-heavy models.&lt;/strong&gt; Flash Attention 3's FP8 path pre-scales K and V row-wise and accumulates in FP16, but extremely spiky attention patterns (e.g., models trained without attention dropout) can amplify the relative error. Compare the output distribution on a few samples before trusting FP8 for your use case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all GPUs support all versions.&lt;/strong&gt; FA1 needs A100-class Tensor Cores (it won't run on V100). FA2 runs on Ampere and newer. FA3 requires Hopper (H100/H200) — SM 90 kernels will not load on Ada Lovelace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory gains are less visible with very short sequences.&lt;/strong&gt; At N &amp;lt; 512, the overhead of block iteration and the SRAM management cost can make Flash Attention &lt;em&gt;slower&lt;/em&gt; than a well-tuned vanilla kernel. PyTorch's sdp_kernel handles this by falling back automatically, but if you call &lt;code&gt;flash_attn_func&lt;/code&gt; directly at short context, benchmark first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropout in attention is not free.&lt;/strong&gt; FA supports attention dropout via a separate random mask, but because it recomputes S and P in the backward pass, the dropout rng state must be stored per block. In practice, most modern LLMs don't use attention dropout, so this rarely matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Flash Attention is the wrong tool if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your GPU is compute-bound, not memory-bound.&lt;/strong&gt; On very small batch sizes with short contexts, the attention operation's HBM traffic is small enough that the GPU's Tensor Cores are the bottleneck, not the memory system. Flash Attention's tiling adds per-block overhead that can regress performance at N &amp;lt; 512 on high-end GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need exact FP32 attention for research or numerical experiments.&lt;/strong&gt; Flash Attention is exact for FP16/BF16 (bitwise identical to the unfused computation), but in FP32 it would be slower than vanilla because the tiling overhead is not amortized. For most LLM work this doesn't matter — BF16 is the training standard — but it's worth flagging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your model uses an unusual attention variant.&lt;/strong&gt; ALiBi, xPos, linear attention (Mamba-style), and sliding-window attention have their own fused kernels that may not compose with Flash Attention's tiling. Flash Attention works for standard softmax attention with optional causal masking and ALiBi, but not for every recent variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're on a production inference stack that already uses prefix caching.&lt;/strong&gt; Flash Attention and prefix caching both sit in the attention layer, and they compose — but only if your serving engine (vLLM / SGLang) has implemented the combined kernel. As of v0.22, vLLM does not fuse FA3 with its prefix-caching kernel. You get one or the other, not both simultaneously (though this is a known work-in-progress).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flash Attention&lt;/strong&gt; tiles the Q, K, V matrices into SRAM-sized blocks, computes softmax on each block, and merges the results using online statistics. The output is &lt;strong&gt;bit-exact&lt;/strong&gt; in FP16/BF16 — not approximate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original insight:&lt;/strong&gt; standard attention is HBM-bandwidth-bound, not compute-bound. Reducing HBM round-trips from O(N² d) to O(N² d / M) is where the speedup comes from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v1&lt;/strong&gt; (NeurIPS 2022) proved the concept on A100s. &lt;strong&gt;v2&lt;/strong&gt; (2023) doubled performance with better parallelism. &lt;strong&gt;v3&lt;/strong&gt; (2025) adds FP8 and async copies, reaching 4–6× vs vanilla on H100s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use it through&lt;/strong&gt; PyTorch 2.x &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; (auto-dispatch) or Hugging Face &lt;code&gt;attn_implementation="flash_attention_2"&lt;/code&gt; for the easiest path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip it&lt;/strong&gt; for sequences under 512 tokens, FP32 research, or unusual attention variants that don't use standard softmax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: Mixture of Experts (MoE) — what practitioners need to know about routing, load balancing, and the engineering decisions behind Mixtral and DeepSeek-V3.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>deeplearning</category>
      <category>transformers</category>
    </item>
    <item>
      <title>LoRA and QLoRA fine-tuning: what they actually do under the hood</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:52:04 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/lora-and-qlora-fine-tuning-what-they-actually-do-under-the-hood-3a03</link>
      <guid>https://dev.to/tech_nuggets/lora-and-qlora-fine-tuning-what-they-actually-do-under-the-hood-3a03</guid>
      <description>&lt;h1&gt;
  
  
  LoRA and QLoRA fine-tuning: what they actually do under the hood
&lt;/h1&gt;

&lt;p&gt;You spent three weeks curating a dataset of legal contract summaries: 12,000 pairs of dense legalese and plain-English counterparts. The model you picked -- a 7B parameter instruction-tuned Llama -- understands your prompts but produces summaries that read like a junior associate who memorized Blackstone but never saw a real merger clause. You reach for full fine-tuning, the obvious move. Then &lt;code&gt;torch.cuda.OutOfMemoryError&lt;/code&gt; hits at step 20 on your RTX 4090. You try gradient checkpointing. You try a smaller batch. You try half-precision. Still OOM. Your colleague says "just use LoRA" and walks off, as if that explains anything.&lt;/p&gt;

&lt;p&gt;This is the gap this post fills. You do not need another high-level "LoRA is a PEFT method" post. You need the math and the trade-offs that let you decide between LoRA, QLoRA, and full fine-tuning for your specific hardware and quality requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why parameter-efficient fine-tuning exists
&lt;/h2&gt;

&lt;p&gt;The cost of full fine-tuning is straightforward: a model with P parameters requires storing, at minimum, the model weights (2P bytes for fp16), the optimizer states (8P bytes for Adam), and the gradients (2P bytes). For Llama 3 8B with fp16 parameters, that is roughly 16 GB for weights plus 64 GB for optimizer state plus 16 GB for gradients -- 96 GB total. An RTX 4090 has 24 GB. A single A100-80 has exactly enough, barely, with no room for a batch size above 1.&lt;/p&gt;

&lt;p&gt;Parameter-efficient fine-tuning (PEFT) avoids this by keeping the vast majority of the model frozen and training only a tiny set of added parameters. The key insight is that the weight update during fine-tuning, delta W, has low intrinsic rank -- you can approximate it as a product of two much smaller matrices.&lt;/p&gt;

&lt;h2&gt;
  
  
  LoRA: low-rank adaptation
&lt;/h2&gt;

&lt;p&gt;The LoRA paper (Hu et al., 2021, arXiv 2106.09685) proposed freezing the pretrained weight matrix W in R^(d x d) and learning a low-rank decomposition:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W' = W + BA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;where B in R^(d x r), A in R^(r x d), and r &amp;lt;&amp;lt; d (typically r = 8 or r = 16). Instead of updating d^2 parameters per layer, you update 2dr. For d = 4096 (a common hidden dimension) and r = 8, that is 65,536 parameters per layer instead of 16,777,216 -- a reduction of roughly 256x.&lt;/p&gt;

&lt;p&gt;During the forward pass, the computation becomes:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h = xW' = xW + xBA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The first term uses frozen weights (no gradient needed). The second term is the adapter path. Only A and B receive gradient updates. The original W stays intact, which means you can swap adapters in and out at inference time with zero overhead: just add the adapter weights to W (or compute h = xW + xBA on the fly).&lt;/p&gt;

&lt;p&gt;Here is what the architecture looks like for a single Transformer attention layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph Forward pass
        X[Input x] --&amp;gt; W[W frozen&amp;lt;br/&amp;gt;d x d]
        X --&amp;gt; B_adapt[B d x r]
        B_adapt --&amp;gt; A_adapt[A r x d]
        W --&amp;gt; ADD[Add]
        A_adapt --&amp;gt; ADD
        ADD --&amp;gt; OUT[Output h]
    end

    subgraph Gradient flow
        OUT --&amp;gt; GRAD_B[Gradients flow&amp;lt;br/&amp;gt;to B and A only]
        GRAD_B --&amp;gt; NO[No gradient&amp;lt;br/&amp;gt;through W]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, LoRA is applied to the query and value projection matrices in each attention head. You can also extend it to key, output, and the feed-forward layers. Empirically, setting r = 8 on Q and V covers most of the benefit; doubling r beyond 16 rarely beats full fine-tuning by more than a trivial margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  QLoRA: adding 4-bit quantization
&lt;/h2&gt;

&lt;p&gt;QLoRA (Dettmers et al., 2023, arXiv 2305.14314) asked: what if instead of storing W in fp16, we stored it in 4 bits and still trained adapters on top? The result is a method that can fine-tune a 65B model on a single 48 GB GPU -- something that was previously impossible.&lt;/p&gt;

&lt;p&gt;QLoRA makes three specific contributions that work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NF4 data type.&lt;/strong&gt; NormalFloat4 is a quantization scheme designed for normally distributed weights. It maps the 4-bit values to the quantiles of a normal distribution, so the discretization error is minimized exactly where most weight values fall. Informally, NF4 allocates more of its 16 representable values around zero and fewer in the tails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double quantization.&lt;/strong&gt; The quantization constants (scale and offset) themselves take space. QLoRA quantizes these constants from fp32 to fp8, saving another 0.5 bits per parameter. The total is ~4.5 bits per parameter for the base model -- about 3.5 GB for a 7B model instead of 14 GB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paged optimizers.&lt;/strong&gt; When GPU memory runs out during a long training run, the optimizer states are paged to CPU RAM and fetched back as needed. This prevents the OOM crash but can slow training; it is a safety net, not a performance feature.&lt;/p&gt;

&lt;p&gt;During training, QLoRA dequantizes the 4-bit weights on the fly for each forward pass, computes the LoRA adapter contribution, and backpropagates only through the low-rank matrices. The dequantized weights never have their gradients computed, which is the whole source of memory savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Full fine-tuning&lt;/th&gt;
&lt;th&gt;LoRA (fp16)&lt;/th&gt;
&lt;th&gt;QLoRA (4-bit base + LoRA)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base model memory&lt;/td&gt;
&lt;td&gt;16 GB (7B, fp16)&lt;/td&gt;
&lt;td&gt;16 GB (frozen)&lt;/td&gt;
&lt;td&gt;~3.5 GB (NF4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapter memory&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2 GB (r=8, all layers)&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimizer state&lt;/td&gt;
&lt;td&gt;~32 GB (Adam)&lt;/td&gt;
&lt;td&gt;~4 GB (only adapters)&lt;/td&gt;
&lt;td&gt;~4 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total VRAM needed&lt;/td&gt;
&lt;td&gt;~56 GB&lt;/td&gt;
&lt;td&gt;~22 GB&lt;/td&gt;
&lt;td&gt;~9.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qual. vs full FT&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;On par or within 0.5%&lt;/td&gt;
&lt;td&gt;Within 1-2% on most benchmarks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-task support&lt;/td&gt;
&lt;td&gt;One copy per task&lt;/td&gt;
&lt;td&gt;One base + N adapters&lt;/td&gt;
&lt;td&gt;One base + N adapters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training speed (7B, A100)&lt;/td&gt;
&lt;td&gt;1.0x baseline&lt;/td&gt;
&lt;td&gt;~1.4x faster&lt;/td&gt;
&lt;td&gt;~0.8x slower (dequant overhead)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speed trade-off is worth calling out explicitly: QLoRA trains slower than LoRA because every forward pass must dequantize the base weights. On a 7B model with a single A100, LoRA is roughly 1.4x faster than full fine-tuning (less data movement), while QLoRA is about 0.8x the speed of full fine-tuning (dequantization overhead). The memory savings are enormous though, which is why QLoRA dominates the conversation for consumer-grade GPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rank selection is not magic.&lt;/strong&gt; Setting r = 256 everywhere will not automatically improve results. Higher rank means more trainable parameters but also more noise in the gradient signal. The original LoRA paper found that a rank of 1 already captures meaningful adaptation for many tasks. Start with r = 8 on Q and V, evaluate, and only increase rank on layers that underfit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adapter merge at scale.&lt;/strong&gt; You can merge LoRA weights into W at inference time by computing W' = W + BA for each layer and discarding A and B. This eliminates the adapter inference overhead. But if you have 50 adapters for 50 different clients, you now need 50 copies of the full weights -- trading compute for storage. The right design depends on which resource you have more of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QLoRA is not free.&lt;/strong&gt; The NF4 dequantization adds numerical noise. On most tasks the quality loss is within the noise floor (1-2% on MMLU, roughly 0.5% on domain-specific benchmarks). But if you are tuning a model for a precision-critical task such as medical diagnosis or code correctness verification, the trade-off may swing back to full-precision LoRA or full fine-tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bitsandbytes versions matter.&lt;/strong&gt; QLoRA depends on the bitsandbytes library for its CUDA quantization kernels. As of June 2026, bitsandbytes is at v0.49.2 and PEFT is at v0.19.1. The API changed between v0.43 and v0.44 -- if you are using an older PEFT, pin to a compatible bitsandbytes version. A version mismatch silently falls back to CPU quantization, which runs orders of magnitude slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling the LoRA alpha.&lt;/strong&gt; The LoRA scaling factor alpha / r controls the magnitude of the adapter update. A common mistake is setting alpha too low (adapter contribution vanishes) or too high (training destabilizes). The paper recommends alpha = 2r as a starting point. Double-check this if your loss curve looks flat after 200 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;LoRA and QLoRA are the wrong choice when:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need to change the model's internal representations fundamentally.&lt;/strong&gt; If you are adding new knowledge that the base model does not have (a new language, a new domain with very different token statistics), low-rank updates may not have enough capacity. Continued pretraining or full fine-tuning will capture the distribution shift more effectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inference latency is your binding constraint and you serve from CPU.&lt;/strong&gt; LoRA merges into the weights easily on GPU, but on CPU with on-the-fly adapter computation, the extra matrix multiply for BA adds latency. You can merge ahead of time, but then every adapter becomes a separate weight file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You are fine-tuning a model smaller than 1B parameters.&lt;/strong&gt; The memory savings of PEFT are less dramatic on small models. A 350M-parameter model consumes roughly 1.4 GB in fp16 -- the adapter overhead of LoRA starts to be a significant fraction of total parameters. A simple full fine-tuning pass may fit with gradient checkpointing and a reasonable batch size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need deterministic training across hardware.&lt;/strong&gt; The quantization paths in QLoRA introduce non-determinism from the dequantization kernel. If you need perfectly reproducible training runs (for auditing or compliance), stick with full-precision LoRA or full fine-tuning with a fixed seed and deterministic CUDA backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LoRA approximates the fine-tuning weight update as a product of two low-rank matrices (B in d x r, A in r x d), reducing trainable parameters by 100x-1000x per layer with minimal quality loss.&lt;/li&gt;
&lt;li&gt;QLoRA quantizes the frozen base model to 4-bit NF4, then trains LoRA adapters on top. A 65B model fits on a single 48 GB GPU.&lt;/li&gt;
&lt;li&gt;The practical memory equation for a 7B model: full fine-tuning ~56 GB, LoRA ~22 GB, QLoRA ~9.5 GB.&lt;/li&gt;
&lt;li&gt;Start with r = 8 on Q and V projection layers. Increase rank only if you see clear underfitting on your validation set.&lt;/li&gt;
&lt;li&gt;QLoRA trains slower than LoRA (dequantization overhead) but uses roughly half the memory. Pick based on whether you are GPU-bound or time-bound.&lt;/li&gt;
&lt;li&gt;Keep bitsandbytes and PEFT versions in sync. A version mismatch causes silent CPU fallback and catastrophic slowdown.&lt;/li&gt;
&lt;li&gt;Do not use LoRA/QLoRA for small models (under 1B), for injecting fundamentally new knowledge, or for CPU-latency-sensitive serving where merge-ahead is impractical.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;We covered how to adapt an existing model efficiently. The next step is knowing when that adaptation has actually worked -- and that means evaluation. Next post: building a reliable evaluation pipeline that catches regressions before they ship, with or without a labeled test set.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you are deciding between LoRA and QLoRA for a project right now, the key variable is your GPU budget. 24 GB or less? QLoRA. 48 GB or more? LoRA with a larger rank or full fine-tuning with LoRA on the side for rapid iteration. The code to make either choice work is a single &lt;code&gt;pip install&lt;/code&gt; away.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>lora</category>
      <category>qlora</category>
      <category>finetuning</category>
      <category>llm</category>
    </item>
    <item>
      <title>Prefix caching at scale: when it saves you 80% of prefill cost, and the eviction policies that quietly turn it into 5%</title>
      <dc:creator>Tech_Nuggets</dc:creator>
      <pubDate>Sun, 07 Jun 2026 01:09:57 +0000</pubDate>
      <link>https://dev.to/tech_nuggets/prefix-caching-at-scale-when-it-saves-you-80-of-prefill-cost-and-the-eviction-policies-that-5e8</link>
      <guid>https://dev.to/tech_nuggets/prefix-caching-at-scale-when-it-saves-you-80-of-prefill-cost-and-the-eviction-policies-that-5e8</guid>
      <description>&lt;h1&gt;
  
  
  Prefix caching at scale: when it saves you 80% of prefill cost, and the eviction policies that quietly turn it into 5%
&lt;/h1&gt;

&lt;p&gt;Your chatbot deploys 70B Llama on 8x H100s. Steady-state TTFT sits around 180 ms for short prompts, and the team is fine with that. Then you turn on a RAG feature: every request sends a 6,000-token context stuffed with retrieved documents, plus a short system prompt, plus the user's question. TTFT jumps to 1.4 seconds. p99 hits 2.1 s. A surprising share of those tokens are &lt;em&gt;the same&lt;/em&gt; on every request — the system prompt, the same 6k retrieved chunks for the top queries, the tool definitions. The model is recomputing the same attention state over and over, then throwing it away. This is the problem prefix caching solves, and last week's post on KV cache quantization closed with it as the next topic — because the two features compose: a quantized prefix cache is cheaper to keep warm than a BF16 one, and the saved memory buys you either more concurrent users or a longer shared prefix.&lt;/p&gt;

&lt;p&gt;Here's what prefix caching actually is, how vLLM and SGLang implement it differently, and where production deployments quietly lose most of the benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters in practice
&lt;/h2&gt;

&lt;p&gt;A modern LLM serving stack has two phases per request: &lt;strong&gt;prefill&lt;/strong&gt; (process the entire prompt to build the KV cache) and &lt;strong&gt;decode&lt;/strong&gt; (generate one token at a time, attending against the growing cache). For long-context workloads, prefill dominates. On a 70B Llama-3 with 8k of input, prefill accounts for roughly 70–85% of TTFT — decode is fast in comparison.&lt;/p&gt;

&lt;p&gt;Most "long input" workloads are not actually long and unique on every request. They're long and &lt;strong&gt;repetitive&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG pipelines.&lt;/strong&gt; The same retrieved chunks hit the same top queries. The system prompt and tool schema are byte-for-byte identical across every request. The user question is the only variable part, and it's tiny.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-turn chat.&lt;/strong&gt; Each turn is a strict prefix extension of the previous one. Round 2 shares everything except the latest assistant message and the new user turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent loops.&lt;/strong&gt; The same tool schema, planning prompt, and few-shot examples get prepended every step. Only the latest tool result differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-document QA.&lt;/strong&gt; Users repeatedly ask questions about the same 200-page PDF. The document is the prefix; the question is the suffix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefix caching is the optimization that says: &lt;em&gt;if the first N tokens of this request match a request I already processed, hand me back the KV cache for those N tokens instead of recomputing them.&lt;/em&gt; In the textbook case, the model output is bit-identical to a no-cache run, but prefill drops to a fraction of the cost. The reported "80% prefill saved" numbers come from RAG with 90%+ prefix overlap. The 5% numbers come from workloads where the prefix rarely matches, or the cache is constantly evicted before reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "prefix caching" actually is
&lt;/h2&gt;

&lt;p&gt;The high-level idea is simple. The implementation has three decisions that drive the rest of the system: &lt;strong&gt;what unit do you hash on&lt;/strong&gt;, &lt;strong&gt;how do you look it up&lt;/strong&gt;, and &lt;strong&gt;what do you do when the cache is full&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[New request&amp;lt;br/&amp;gt;tokens 0..N-1] --&amp;gt; B[Tokenize &amp;amp;&amp;lt;br/&amp;gt;split into blocks]
    B --&amp;gt; C[Hash each block&amp;lt;br/&amp;gt;tokens + parent hash]
    C --&amp;gt; D{Lookup in&amp;lt;br/&amp;gt;block table}
    D -- hit --&amp;gt; E[Reuse KV blocks&amp;lt;br/&amp;gt;skip prefill]
    D -- miss --&amp;gt; F[Compute KV&amp;lt;br/&amp;gt;for that block]
    F --&amp;gt; G[Insert block&amp;lt;br/&amp;gt;into table]
    E --&amp;gt; H[Continue with&amp;lt;br/&amp;gt;remaining prefill]
    G --&amp;gt; H
    H --&amp;gt; I[Decode normally&amp;lt;br/&amp;gt;+ append new blocks]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things matter. First, prefix caching is &lt;strong&gt;prefix-only&lt;/strong&gt;: you can only skip the leading tokens, never a middle substring. If two requests share tokens 1000–2000 but differ on 0–999, you reuse nothing. Second, the cache is &lt;strong&gt;block-grained&lt;/strong&gt;, not token-grained. A request has to match a whole block (default 16 tokens) to get a hit. A request that diverges at token 14,003 of a 14,016-token shared prefix still recomputes almost everything. Third, prefix caching &lt;strong&gt;does not change decoding&lt;/strong&gt; — every saved token is a saved prefill token.&lt;/p&gt;

&lt;h2&gt;
  
  
  How vLLM does it: hash-based blocks
&lt;/h2&gt;

&lt;p&gt;vLLM's &lt;strong&gt;Automatic Prefix Caching (APC)&lt;/strong&gt; is block-based and content-addressed. Each KV-cache block (default 16 tokens) is keyed by a hash of three things: the parent block's hash, the tokens in the block, and a small set of "extra hashes" for LoRA adapter IDs, multimodal input hashes, and per-tenant cache salts.&lt;/p&gt;

&lt;p&gt;The block-size choice is the lever most teams miss. A small block (4–8 tokens) gives finer reuse — a divergence only kills the divergent block. A large block (32–64 tokens) cuts hash-table overhead and improves batching, but wastes more work on partial-prefix misses. The 16-token default is a reasonable middle for chat; for RAG with 4k–8k chunks, 16 or 32 is common.&lt;/p&gt;

&lt;p&gt;The hash function got a security upgrade in v0.11 (April 2026). Before that, the default used Python's &lt;code&gt;hash()&lt;/code&gt; of the serialized block — a salted SipHash, randomized per process, fine for collision avoidance but non-reproducible across restarts. As of v0.22.1, the default is &lt;code&gt;sha256&lt;/code&gt;, with a new &lt;code&gt;--prefix-caching-hash-algo&lt;/code&gt; CLI flag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Hash&lt;/th&gt;
&lt;th&gt;Serialization&lt;/th&gt;
&lt;th&gt;Reproducible&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sha256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pickle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Default. Secure, but pickle is Python-version-sensitive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sha256_cbor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cbor2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Recommended for multi-process or multi-language tiers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xxhash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;xxHash 128-bit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pickle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Faster, non-cryptographic. Multi-tenant risk must be assessed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xxhash_cbor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;xxHash 128-bit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cbor2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Fastest with reproducibility. Same caveat.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The multi-tenant caveat is the one to take seriously. If you serve multiple customers out of one engine and your hash function is non-cryptographic, a deliberate collision in a crafted prompt can evict another tenant's cache, or — in pathological cases — substitute their KV blocks with attacker-controlled values. If you don't control the prompts, stay on &lt;code&gt;sha256&lt;/code&gt; or &lt;code&gt;sha256_cbor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A typical vLLM deploy turns APC on at serve time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve meta-llama/Meta-Llama-3-70B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-prefix-caching&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prefix-caching-hash-algo&lt;/span&gt; sha256_cbor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 32768 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.92
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APC is a server-level decision, not per-request — correct, because the cache is a shared resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SGLang does it: a radix tree
&lt;/h2&gt;

&lt;p&gt;SGLang keeps a &lt;strong&gt;radix tree&lt;/strong&gt; of cached prefixes. Each node represents a shared prefix across one or more requests; each leaf is a request-specific tail. The engine traverses the tree per request, reuses the longest matching prefix, and forks new branches where requests diverge.&lt;/p&gt;

&lt;p&gt;The practical differences that matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Match granularity is one token, not one block.&lt;/strong&gt; SGLang reuses down to a single divergent token, recovering more of the cache than vLLM's block-level scheme on chatty workloads with mid-prompt variations (an inserted tool result). The trade is per-token tree-walk overhead per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction is LRU on nodes, not blocks.&lt;/strong&gt; When memory pressure forces a prune, the whole subtree under the coldest node goes. Faster than vLLM's per-block LRU but coarser — a cold tail can take a warm subtree with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-LoRA / multimodal.&lt;/strong&gt; SGLang stores per-request metadata at the leaves, so different LoRA adapters and image inputs sit naturally on different branches. vLLM achieves the same via the "extra hashes" component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most RAG and chat workloads, the two implementations deliver comparable hit rates. SGLang tends to win on many short shared prefixes (per-token matching helps); vLLM tends to win on very long shared prefixes (block-hash lookups are O(1) with a tiny constant).&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get at the metric level
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Median prefill saved&lt;/th&gt;
&lt;th&gt;TTFT reduction&lt;/th&gt;
&lt;th&gt;Caveat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAG with 6k static context&lt;/td&gt;
&lt;td&gt;88–94%&lt;/td&gt;
&lt;td&gt;70–85%&lt;/td&gt;
&lt;td&gt;Hit rate near 1.0 if the retrieved set is stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-turn chat, 8 turns&lt;/td&gt;
&lt;td&gt;60–80% (avg)&lt;/td&gt;
&lt;td&gt;30–55%&lt;/td&gt;
&lt;td&gt;First turn is a miss; later turns reuse aggressively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-doc QA on a single PDF&lt;/td&gt;
&lt;td&gt;92–97% after first query&lt;/td&gt;
&lt;td&gt;75–90%&lt;/td&gt;
&lt;td&gt;First query is a miss, all subsequent reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-ended Q&amp;amp;A (no shared prefix)&lt;/td&gt;
&lt;td&gt;0–5%&lt;/td&gt;
&lt;td&gt;0–5%&lt;/td&gt;
&lt;td&gt;Don't bother enabling it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-using agent loop&lt;/td&gt;
&lt;td&gt;40–70% per step&lt;/td&gt;
&lt;td&gt;20–45%&lt;/td&gt;
&lt;td&gt;Tool result insertion breaks prefix mid-prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hit rate — the fraction of blocks already in the cache when a request arrived — is the single most useful number to instrument. If you turn on APC and your hit rate is below 30%, something is wrong: prefixes don't match, or the cache is being evicted before reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eviction is a silent killer.&lt;/strong&gt; vLLM evicts blocks under GPU memory pressure with LRU. A mix of long-prefix and short-prefix traffic often evicts long-prefix blocks first (they take more slots), and they're the only ones whose loss actually hurts. Raise &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; from 0.85 to 0.92 and the working set of cached prefixes typically doubles. Monitor &lt;strong&gt;cache hit rate after 60 seconds of warmup&lt;/strong&gt; — a rate that decays over the day is an eviction problem, not a workload problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA and multimodal mix badly if you forget the salt.&lt;/strong&gt; vLLM's block hash includes LoRA IDs and image hashes; swap adapters at request time and you get cache thrash. Same for image inputs that vary per request — caching the multimodal prefix is essentially useless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix caching does not save decode.&lt;/strong&gt; A common dashboard mistake is to credit the entire speedup to APC. Decode time is unchanged. If your workload is decode-bound, APC helps very little.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash algorithm migrations are not transparent.&lt;/strong&gt; Changing &lt;code&gt;--prefix-caching-hash-algo&lt;/code&gt; between deploys makes the new engine see zero hits until it warms back up. One-time cost, but a real incident if unexpected. Bake the algo into your Helm chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-replica cache sharing is hard.&lt;/strong&gt; vLLM's APC lives in GPU memory; each replica has its own cache. A request landing on a cold replica pays full prefill. Disaggregated architectures (vLLM v0.22's &lt;code&gt;kv_connector&lt;/code&gt;, SGLang's &lt;code&gt;DistServe&lt;/code&gt;) can route prefix-matched requests to warm replicas, but that needs explicit config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "first request after restart" problem.&lt;/strong&gt; A rolling deploy invalidates the entire cache. The first 30–60 seconds after each deploy are prefill-bound. Schedule rolling deploys during low-traffic windows, or pre-warm with a synthetic-traffic sidecar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Prefix caching is the wrong choice (or a wasted flag) if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your prompts have no shared structure.&lt;/strong&gt; Open-ended completion APIs, code-gen on a fresh repo per request, single-turn Q&amp;amp;A with no system prompt — there's nothing to reuse. Hit rate near zero, and you're paying hash-table overhead for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're under a strict determinism SLO that includes cache state.&lt;/strong&gt; A cache hit and a cache miss produce the same output &lt;em&gt;for the same model and same prompt&lt;/em&gt;, but float-rounding in the attention kernel can give a divergent token at extreme depths. If you need bit-exact reproducibility across requests, disable APC and accept the prefill cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't budget enough GPU memory for the working set.&lt;/strong&gt; A cache that misses more than it hits is worse than no cache: you spent memory on entries that never get reused, pushing decode batch sizes down. Measure first, enable second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your traffic is dominated by mid-prompt insertions.&lt;/strong&gt; Agent loops, multi-modal chat with per-turn image insertion, RAG with dynamic chunk re-ordering — these frequently insert new tokens mid-prompt, breaking the prefix. SGLang's per-token matching recovers more here, but workloads that are 50%+ mid-prompt insertions still see sub-30% hit rates in either engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're already prefill-bound on a single giant request.&lt;/strong&gt; A 100k-token analysis pass per request, one request at a time, will hit a 100% miss on the first call and a 100% hit on the second &lt;em&gt;if it ever comes&lt;/em&gt;. The amortized win depends entirely on whether those requests repeat, and most one-shot analytics workloads don't repeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefix caching&lt;/strong&gt; reuses the KV cache for the leading tokens of a request when a previous request already computed them. It only affects prefill; decode is unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM's Automatic Prefix Caching (APC)&lt;/strong&gt; is a content-addressed block store. Each block is hashed by parent hash + block tokens + LoRA/multimodal/salt extras. Default block size is 16 tokens. Default hash since v0.22.1 is SHA-256, with &lt;code&gt;sha256_cbor&lt;/code&gt;, &lt;code&gt;xxhash&lt;/code&gt;, and &lt;code&gt;xxhash_cbor&lt;/code&gt; available via &lt;code&gt;--prefix-caching-hash-algo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SGLang uses a radix tree&lt;/strong&gt; of token-level prefixes, which gives finer-grained matching at the cost of per-request tree-walk overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The win is real but workload-shaped.&lt;/strong&gt; RAG with a stable retrieved set: 88–94% prefill saved. Multi-turn chat: 60–80% averaged. Open-ended Q&amp;amp;A: 0–5%. Measure your hit rate before you trust the marketing numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction is the silent killer.&lt;/strong&gt; Long-prefix blocks get evicted first under memory pressure. Size the cache budget explicitly and monitor hit rate over the day, not just at startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't enable it on open-ended workloads, on a multi-tenant engine with a non-cryptographic hash, or when you can't afford the working-set memory.&lt;/strong&gt; Measure first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: structured output at the decoding layer — JSON mode vs grammar-constrained decoding vs function calling, where the three diverge in latency and reliability, and the failure modes that show up only in production.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>infrastructure</category>
      <category>vllm</category>
    </item>
  </channel>
</rss>
