<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Quantization on vishctl</title><link>https://vishctl.dev/tags/quantization/</link><description>Recent content in Quantization on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/quantization/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM Quantization, Simply: FP16, 8-bit, 4-bit, and What Q4_K_M Means</title><link>https://vishctl.dev/posts/llm-quantization-fp16-int8-4bit-gguf/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0800</pubDate><guid>https://vishctl.dev/posts/llm-quantization-fp16-int8-4bit-gguf/</guid><category>quantization</category><category>gguf</category><category>local-llm</category><category>inference</category><category>vllm</category><category>ai-infra</category><description>A simple, diagram-first guide to LLM quantization: what rounding a weight actually does, how much memory each bit width saves, how to read names like Q4_K_M, AWQ, and FP8, and a toy script that shows where quality falls off.</description><content:encoded><![CDATA[<p>An 8-billion-parameter model is about 16 GB as published. My laptop GPU has 8 GB. Yet models that size run on it.</p>
<p>The trick is quantization. It is the reason local LLMs exist at all, and it comes down to one idea:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Full precision = store every weight as a long, exact number (16 bits)
</span></span><span class="line"><span class="cl">Quantized      = round every weight to one of a few allowed values (8 or 4 bits)
</span></span></code></pre></div><figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph FULL["16-bit weights"]
        direction LR
        A1["8B model<br/>16 GB"] a1@--> A2["8 GB<br/>laptop GPU"]
        A2 a2@--> A3["Does not fit"]
    end

    subgraph QUANT["4-bit weights"]
        direction LR
        B1["8B model<br/>about 5 GB"] b1@--> B2["8 GB<br/>laptop GPU"]
        B2 b2@--> B3["Fits<br/>3 GB to spare"]
    end

    FULL ~~~ QUANT
    a1@{ animate: true }
    a2@{ animate: true }
    b1@{ animate: true }
    b2@{ animate: true }

    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef warn fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class A1,B1 tree;
    class A2,B2 input;
    class A3 warn;
    class B3 gpu;</div>
  <figcaption>Top: the 16-bit model is twice the size of the GPU. Bottom: the same model rounded to about 4 bits per weight fits with room for the KV cache.</figcaption>
</figure>

<p>This post explains what that rounding does, what it costs, and how to read the alphabet soup (<code>Q4_K_M</code>, AWQ, FP8) on every model download page.</p>
<h2 id="1-a-weight-is-just-a-number">1. A weight is just a number</h2>
<p>A model is a very long list of numbers called weights. An 8B model has eight billion of them.</p>
<p>Each weight is stored with some number of <strong>bits</strong>. More bits means more possible values, like a ruler with finer marks.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Bits per weight</th>
            <th scope="col">Possible values</th>
            <th scope="col">Like a ruler with</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>16</td>
            <td>65,536</td>
            <td>Marks every hair&rsquo;s width</td>
        </tr>
        <tr>
            <td>8</td>
            <td>256</td>
            <td>Millimeter marks</td>
        </tr>
        <tr>
            <td>4</td>
            <td>16</td>
            <td>Marks every few centimeters</td>
        </tr>
        <tr>
            <td>2</td>
            <td>4</td>
            <td>Four marks in total</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Quantization swaps the fine ruler for a coarse one. Every weight moves to the nearest mark that still exists.</p>
<h2 id="2-what-rounding-looks-like">2. What rounding looks like</h2>
<p>Take four weights and squeeze them into 4 bits. With 4 bits, each weight must become a whole number from -7 to 7.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart LR
    O["Original<br/>0.82"] q1@--> S["Divide by<br/>the scale<br/>6.04"]
    S q2@--> R["Round<br/>6"]
    R q3@--> D["Stored in<br/>4 bits"]
    D q4@--> B["Multiply back<br/>0.814"]

    q1@{ animate: true }
    q2@{ animate: true }
    q3@{ animate: true }
    q4@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class O input;
    class S,R tree;
    class D,B gpu;</div>
  <figcaption>One weight on its trip through 4-bit quantization. It is divided by a shared scale, rounded to a whole number, stored, and multiplied back when the model runs.</figcaption>
</figure>

<p>The <strong>scale</strong> is one shared number chosen so the largest weight lands on 7. Here that is <code>0.95 / 7 = 0.1357</code>.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Step</th>
            <th scope="col">0.82</th>
            <th scope="col">-0.31</th>
            <th scope="col">0.07</th>
            <th scope="col">-0.95</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1. Divide by the scale</td>
            <td>6.04</td>
            <td>-2.28</td>
            <td>0.52</td>
            <td>-7.00</td>
        </tr>
        <tr>
            <td>2. Round to a whole number</td>
            <td>6</td>
            <td>-2</td>
            <td>1</td>
            <td>-7</td>
        </tr>
        <tr>
            <td>3. Store (4 bits each)</td>
            <td>6</td>
            <td>-2</td>
            <td>1</td>
            <td>-7</td>
        </tr>
        <tr>
            <td>4. Multiply back at run time</td>
            <td>0.814</td>
            <td>-0.271</td>
            <td>0.136</td>
            <td>-0.950</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Compare the first and last rows. The big weights barely moved. The small one, 0.07, almost doubled to 0.136.</p>
<p>That is the whole trade. <strong>You save 12 of every 16 bits, and each weight gets slightly wrong.</strong></p>
<h2 id="3-why-the-model-still-works">3. Why the model still works</h2>
<p>A model does not depend on any single weight. Each output is a sum over thousands of them, and the rounding errors point in random directions, so many of them cancel out.</p>
<p>That tolerance has a limit. As the ruler gets coarser, the errors stop cancelling and start changing answers.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Bits per weight</th>
            <th scope="col">What usually happens</th>
            <th scope="col">Typical use</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>16</td>
            <td>The reference. No loss</td>
            <td>GPU servers with memory to spare</td>
        </tr>
        <tr>
            <td>8</td>
            <td>Very hard to tell apart from 16-bit</td>
            <td>Safe default when 16-bit does not fit</td>
        </tr>
        <tr>
            <td>5 to 6</td>
            <td>Very close to the original</td>
            <td>Local use when memory allows</td>
        </tr>
        <tr>
            <td>4</td>
            <td>Small, usually acceptable loss</td>
            <td>The popular sweet spot for local models</td>
        </tr>
        <tr>
            <td>3 and below</td>
            <td>Quality drops quickly</td>
            <td>Only when nothing else fits</td>
        </tr>
    </tbody>
  </table>
</div>
<p>These are rules of thumb, not guarantees. Coding, math, and long reasoning tend to suffer before casual chat does. Test on the kind of prompts you actually use.</p>
<p>One more rule of thumb worth knowing: <strong>for the same memory, a bigger model at 4 bits usually beats a smaller model at 16 bits.</strong></p>
<h2 id="4-how-much-memory-you-save">4. How much memory you save</h2>
<p>Size is one multiplication:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">File size = parameters x bits per weight / 8
</span></span></code></pre></div><div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Model</th>
            <th scope="col">16-bit</th>
            <th scope="col">8-bit</th>
            <th scope="col">About 4.85 bits (Q4_K_M)</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>8B</td>
            <td>16 GB</td>
            <td>8 GB</td>
            <td>About 4.9 GB</td>
        </tr>
        <tr>
            <td>14B</td>
            <td>28 GB</td>
            <td>14 GB</td>
            <td>About 8.5 GB</td>
        </tr>
        <tr>
            <td>32B</td>
            <td>64 GB</td>
            <td>32 GB</td>
            <td>About 19 GB</td>
        </tr>
        <tr>
            <td>70B</td>
            <td>140 GB</td>
            <td>70 GB</td>
            <td>About 42 GB</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Two cautions:</p>
<ul>
<li><strong>Quantizing weights does not shrink the KV cache.</strong> The cache that grows with your context is a separate cost. The <a href="https://vishctl.dev/posts/llm-gpu-memory-sizing-weights-kv-cache/">GPU memory sizing guide</a> covers both.</li>
<li><strong>File size is not the whole footprint.</strong> Leave room for the cache and the runtime, or a model that &ldquo;fits&rdquo; will crawl.</li>
</ul>
<h2 id="5-reading-the-names">5. Reading the names</h2>
<h3 id="gguf-names-such-as-q4_k_m">GGUF names such as Q4_K_M</h3>
<p>GGUF is the file format used by llama.cpp, and by tools built on it such as Ollama and LM Studio. Its quantization names have three parts:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Q4_K_M
</span></span><span class="line"><span class="cl">│  │ └─ M = medium variant (S small, M medium, L large)
</span></span><span class="line"><span class="cl">│  └─── K = the newer &#34;k-quant&#34; method
</span></span><span class="line"><span class="cl">└────── Q4 = about 4 bits per weight
</span></span></code></pre></div><p>The letter at the end says how many of the model&rsquo;s layers get extra bits. That is why the real average is a little above the headline number.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">GGUF name</th>
            <th scope="col">Average bits per weight</th>
            <th scope="col">When I would pick it</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><code>Q8_0</code></td>
            <td>About 8.5</td>
            <td>Maximum quality, memory is no concern</td>
        </tr>
        <tr>
            <td><code>Q6_K</code></td>
            <td>About 6.6</td>
            <td>Plenty of memory, want to be safe</td>
        </tr>
        <tr>
            <td><code>Q5_K_M</code></td>
            <td>About 5.7</td>
            <td>A little headroom after Q4</td>
        </tr>
        <tr>
            <td><code>Q4_K_M</code></td>
            <td>About 4.85</td>
            <td>The default almost everywhere</td>
        </tr>
        <tr>
            <td><code>Q3_K_M</code></td>
            <td>About 3.9</td>
            <td>The model will not fit any other way</td>
        </tr>
    </tbody>
  </table>
</div>
<p><code>Q4_K_M</code> is what I used in both my <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">Ollama</a> and <a href="https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/">llama.cpp</a> posts, which is why their speeds were comparable.</p>
<h3 id="names-from-the-gpu-serving-world">Names from the GPU serving world</h3>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Name</th>
            <th scope="col">What it is</th>
            <th scope="col">Where you meet it</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>FP16 or BF16</td>
            <td>The original 16-bit weights</td>
            <td>The default for vLLM and SGLang</td>
        </tr>
        <tr>
            <td>FP8</td>
            <td>8-bit floating point weights</td>
            <td>GPU serving when 16-bit is too big</td>
        </tr>
        <tr>
            <td>AWQ</td>
            <td>4-bit weights, chosen by watching real activations</td>
            <td>Pre-quantized models for GPU servers</td>
        </tr>
        <tr>
            <td>GPTQ</td>
            <td>4-bit weights, tuned layer by layer on sample data</td>
            <td>Pre-quantized models for GPU servers</td>
        </tr>
        <tr>
            <td>NVFP4</td>
            <td>NVIDIA&rsquo;s 4-bit floating point format</td>
            <td>Newer NVIDIA stacks, as in my <a href="https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/">FreeToken test</a></td>
        </tr>
        <tr>
            <td>MLX 4-bit</td>
            <td>4-bit weights in Apple&rsquo;s MLX format</td>
            <td><a href="https://vishctl.dev/posts/omlx-vs-vmlx-vs-mlx-32gb-macbook-air/">MLX-LM, oMLX, and vMLX</a> on a Mac</td>
        </tr>
    </tbody>
  </table>
</div>
<p>They all do the same job with different rounding recipes. The format you choose is mostly decided by <strong>which runtime you use</strong>, not by which one is &ldquo;best&rdquo;.</p>
<h2 id="6-why-smart-rounding-beats-simple-rounding">6. Why smart rounding beats simple rounding</h2>
<p>The walkthrough in section 2 used the simplest possible recipe: one scale for everything. Real quantizers work harder, and the toy script below shows why they have to.</p>
<p>Simple rounding has a weakness. One unusually large weight stretches the scale, and every small weight then gets squashed toward zero.</p>
<p>Modern methods fix this in a few ways:</p>
<ul>
<li><strong>Many small scales instead of one.</strong> Weights are quantized in small blocks, each with its own scale. This is the idea behind the K in <code>Q4_K_M</code>.</li>
<li><strong>Mixed precision.</strong> Sensitive layers keep more bits. This is what the S, M, and L variants control.</li>
<li><strong>Calibration.</strong> AWQ and GPTQ run sample text through the model and protect the weights that matter most for real outputs.</li>
</ul>
<p>There is also <strong>quantization-aware training</strong>, where the model is trained with rounding switched on so it learns to live with it. It gives the best quality at low bit widths, but it needs a training run. Almost every file you download was quantized after training instead.</p>
<h2 id="7-does-it-make-the-model-faster">7. Does it make the model faster?</h2>
<p>Often yes, but for a reason that surprises people.</p>
<p>Generating a token means reading every weight from memory. On laptops and CPUs, that reading is the bottleneck. A 4-bit model has a quarter of the bytes to move, so tokens come out faster.</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Hardware</th>
            <th scope="col">Effect of quantizing</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>CPU or Apple silicon</td>
            <td>Usually faster, because less memory has to be read per token</td>
        </tr>
        <tr>
            <td>Small GPU</td>
            <td>Often the only way the model fits on the GPU at all</td>
        </tr>
        <tr>
            <td>Big GPU with spare memory</td>
            <td>Smaller gain. 16-bit is already fast and well supported</td>
        </tr>
    </tbody>
  </table>
</div>
<p>On a data-center GPU the stronger argument is capacity, not speed: a smaller model leaves more memory for the KV cache, which means more concurrent users. See <a href="https://vishctl.dev/posts/llm-gpu-memory-sizing-weights-kv-cache/#3-put-it-together-how-many-requests-fit">how many requests fit</a>.</p>
<h2 id="try-it-watch-the-quality-fall-off">Try it: watch the quality fall off</h2>
<p>This script quantizes weights with the simple one-scale recipe and measures the damage. It uses only the Python standard library.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;toy_quant.py: round weights to N bits and measure the damage.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">quantize</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">bits</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">top</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">**</span> <span class="p">(</span><span class="n">bits</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>                 <span class="c1"># 4 bits -&gt; whole numbers from -7 to 7</span>
</span></span><span class="line"><span class="cl">    <span class="n">scale</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">weights</span><span class="p">)</span> <span class="o">/</span> <span class="n">top</span>
</span></span><span class="line"><span class="cl">    <span class="n">stored</span> <span class="o">=</span> <span class="p">[</span><span class="nb">round</span><span class="p">(</span><span class="n">w</span> <span class="o">/</span> <span class="n">scale</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">weights</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">stored</span><span class="p">,</span> <span class="p">[</span><span class="n">n</span> <span class="o">*</span> <span class="n">scale</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">stored</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">demo</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.82</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.31</span><span class="p">,</span> <span class="mf">0.07</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.95</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">stored</span><span class="p">,</span> <span class="n">restored</span> <span class="o">=</span> <span class="n">quantize</span><span class="p">(</span><span class="n">demo</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;original :&#34;</span><span class="p">,</span> <span class="n">demo</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;stored   :&#34;</span><span class="p">,</span> <span class="n">stored</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;restored :&#34;</span><span class="p">,</span> <span class="p">[</span><span class="nb">round</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">restored</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">random</span><span class="o">.</span><span class="n">seed</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">weights</span> <span class="o">=</span> <span class="p">[</span><span class="n">random</span><span class="o">.</span><span class="n">gauss</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">100_000</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">typical</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">weights</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">weights</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">bits</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">_</span><span class="p">,</span> <span class="n">restored</span> <span class="o">=</span> <span class="n">quantize</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">bits</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">error</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">restored</span><span class="p">))</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">weights</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">bits</span><span class="si">}</span><span class="s2"> bits: average error is </span><span class="si">{</span><span class="mi">100</span> <span class="o">*</span> <span class="n">error</span> <span class="o">/</span> <span class="n">typical</span><span class="si">:</span><span class="s2">5.1f</span><span class="si">}</span><span class="s2">% of a typical weight&#34;</span><span class="p">)</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 toy_quant.py
</span></span></code></pre></div><p>This is the output on my machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">original : [0.82, -0.31, 0.07, -0.95]
</span></span><span class="line"><span class="cl">stored   : [6, -2, 1, -7]
</span></span><span class="line"><span class="cl">restored : [0.814, -0.271, 0.136, -0.95]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">8 bits: average error is   1.0% of a typical weight
</span></span><span class="line"><span class="cl">6 bits: average error is   4.3% of a typical weight
</span></span><span class="line"><span class="cl">4 bits: average error is  19.0% of a typical weight
</span></span><span class="line"><span class="cl">3 bits: average error is  44.3% of a typical weight
</span></span><span class="line"><span class="cl">2 bits: average error is  97.0% of a typical weight
</span></span></code></pre></div><p>Two things to notice:</p>
<ul>
<li><strong>The error roughly doubles for every bit you remove.</strong> At 8 bits it is noise. At 2 bits the weights are almost entirely wrong.</li>
<li><strong>Naive 4-bit is rough: 19% error.</strong> Real 4-bit files are far better than this toy, because of the block scales, mixed precision, and calibration from section 6. That gap is exactly what those methods buy you.</li>
</ul>
<h2 id="see-which-quantization-you-are-running">See which quantization you are running</h2>
<p>Ollama prints it in the model details:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ollama show &lt;model-name&gt;
</span></span></code></pre></div><p>With llama.cpp, pick the quantization when you download by adding it after a colon:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">llama-server -hf &lt;org&gt;/&lt;model&gt;-GGUF:Q5_K_M -ngl <span class="m">99</span> --port <span class="m">8080</span>
</span></span></code></pre></div><p>To make your own from a 16-bit GGUF file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">llama-quantize model-f16.gguf model-q4_k_m.gguf Q4_K_M
</span></span></code></pre></div><p>On a GPU server, vLLM loads pre-quantized checkpoints the same way as any other model. Look for repositories whose names end in <code>-AWQ</code> or <code>-FP8</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vllm serve &lt;org&gt;/&lt;model&gt;-FP8
</span></span></code></pre></div><p>A fair test is simple: run the same ten prompts from your real work against two quantization levels and compare the answers side by side. Use the script from the <a href="https://vishctl.dev/posts/llm-inference-metrics-ttft-tpot-throughput-goodput/">inference metrics guide</a> to compare speed at the same time.</p>
<h2 id="which-one-should-you-pick">Which one should you pick?</h2>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    Q1["Where does<br/>the model run?"] p1@-->|"Laptop, CPU<br/>or Mac"| L["GGUF or MLX<br/>start at 4-bit"]
    Q1 p2@-->|"GPU server"| Q2["Does 16-bit fit<br/>with cache room?"]
    L p3@--> L2["Memory left over?<br/>step up to Q5 or Q6"]
    Q2 p4@-->|"Yes"| F["Stay on 16-bit<br/>simplest and safest"]
    Q2 p5@-->|"No"| E["Use FP8 first<br/>then AWQ if still tight"]

    p1@{ animate: true }
    p2@{ animate: true }
    p3@{ animate: true }
    p4@{ animate: true }
    p5@{ animate: true }

    classDef question fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    class Q1,Q2 question;
    class L,L2 input;
    class F,E gpu;</div>
  <figcaption>Start from where the model runs. The runtime decides the format, and the memory you have decides the bit width.</figcaption>
</figure>

<h3 id="on-a-laptop-or-mac">On a laptop or Mac:</h3>
<ul>
<li><strong>Start with <code>Q4_K_M</code></strong> (or the 4-bit MLX build). It is the default for a reason.</li>
<li><strong>Step up to <code>Q5_K_M</code> or <code>Q6_K</code></strong> if the model fits with memory to spare and you care about coding or reasoning quality.</li>
<li><strong>Prefer a bigger model at 4-bit</strong> over a smaller model at 8-bit when both fit.</li>
</ul>
<h3 id="on-a-gpu-server">On a GPU server:</h3>
<ul>
<li><strong>Stay on 16-bit</strong> if the weights and a healthy KV cache both fit. It is the best-supported path.</li>
<li><strong>Use FP8</strong> when you need room for more concurrent users.</li>
<li><strong>Use a 4-bit format such as AWQ</strong> when the model will not fit otherwise, and check quality on your own prompts first.</li>
</ul>
<h2 id="cheat-sheet">Cheat sheet</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Quantization    = round each weight to fewer bits so the model takes less memory
</span></span><span class="line"><span class="cl">Bits per weight = how fine the ruler is: 16 bits has 65,536 marks, 4 bits has 16
</span></span><span class="line"><span class="cl">Scale           = the shared number weights are divided by before rounding
</span></span><span class="line"><span class="cl">File size       = parameters x bits per weight / 8
</span></span><span class="line"><span class="cl">GGUF            = the model file format of llama.cpp, Ollama, and LM Studio
</span></span><span class="line"><span class="cl">Q4_K_M          = about 4 bits, k-quant method, medium variant; about 4.85 bits on average
</span></span><span class="line"><span class="cl">Q8_0            = about 8 bits; very close to the original
</span></span><span class="line"><span class="cl">FP8             = 8-bit floating point weights, common on GPU servers
</span></span><span class="line"><span class="cl">AWQ / GPTQ      = 4-bit methods that use sample data to protect important weights
</span></span><span class="line"><span class="cl">QAT             = quantization-aware training: learn with rounding on; best quality, needs training
</span></span><span class="line"><span class="cl">KV cache        = not shrunk by weight quantization; size it separately
</span></span><span class="line"><span class="cl">Rule of thumb   = 8-bit is safe, 4-bit is the sweet spot, below 4 falls off fast
</span></span></code></pre></div><p>For the memory math behind these choices, read <a href="https://vishctl.dev/posts/llm-gpu-memory-sizing-weights-kv-cache/">How Much GPU Memory Does an LLM Need?</a>. For plain-English definitions of nearby terms (GGUF, FP16, INT4), see the <a href="https://vishctl.dev/posts/ai-llm-glossary/">AI and LLM glossary</a>.</p>
]]></content:encoded></item></channel></rss>