<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://veloxquant-mlx.netlify.app/docs/blog</id>
    <title>VeloxQuant-MLX Blog</title>
    <updated>2026-06-20T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://veloxquant-mlx.netlify.app/docs/blog"/>
    <subtitle>VeloxQuant-MLX Blog</subtitle>
    <icon>https://veloxquant-mlx.netlify.app/docs/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[TensorOps Research: What We Learned Optimizing KV Caches]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research"/>
        <updated>2026-06-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A deep-dive into Apple's Metal Shading Language specification, what TensorOps promised, why it didn't work through MLX, and the two real improvements we shipped from three sessions of research.]]></summary>
        <content type="html"><![CDATA[<p><em>A deep-dive into Apple's Metal Shading Language specification, what TensorOps promised, why it didn't work through MLX, and the two real improvements we shipped from three sessions of research.</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-story-starts">Where This Story Starts<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#where-this-story-starts" class="hash-link" aria-label="Direct link to Where This Story Starts" title="Direct link to Where This Story Starts" translate="no">​</a></h2>
<p>A few weeks ago I shipped a FlashAttention-style Metal kernel for VeloxQuant-MLX that was correct, fast in isolation, and completely useless end-to-end. The blog post about that mistake is <a href="https://medium.com/@rajveer.rathod1301" target="_blank" rel="noopener noreferrer" class="">here</a>. The short version: I built a fused dequant+attention kernel that beat <code>mx.fast.scaled_dot_product_attention</code> by 1.3× in benchmarks — then discovered mlx_lm had already eliminated the dequant cost via a persistent fp16 K_hat buffer, making my kernel 3-4× slower than the baseline it was supposed to beat.</p>
<p>I kept the kernel in the library as an opt-in API. It's correct, it's tested, and it loses.</p>
<p>After writing that post, a reader suggested I look at the Metal Shading Language specification — specifically Metal 4, which Apple released with macOS Sequoia. The argument was: Metal 4 adds hardware tensor operations that could replace the slow part of the kernel. Maybe there was a path to winning that I hadn't found yet.</p>
<p>So I read the spec. All 346 pages of it.</p>
<p>This is what I found.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-kernels-hot-path">The Kernel's Hot Path<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#the-kernels-hot-path" class="hash-link" aria-label="Direct link to The Kernel's Hot Path" title="Direct link to The Kernel's Hot Path" translate="no">​</a></h2>
<p>To understand why the spec research mattered, I need to explain the bottleneck.</p>
<p>The fused SDPA kernel computes attention directly from VecInfer's compressed codebook indices without materializing the fp16 key matrix. For each query, it needs to compute a <strong>Look-Up Table</strong> first:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">LUT[sub, centroid] = q_sub_vector · codebook_row[centroid]</span><br></div></code></pre></div></div>
<p>For VecInfer's default config (<code>n_sub=16</code>, <code>sub_dim=8</code>, <code>n_centroids=256</code>), this is a <code>[16, 8] @ [8, 256]</code> matrix multiply — 4,096 dot products. In the current kernel, 32 GPU lanes stripe these across the SIMD group: each lane computes 128 scalar dot products independently.</p>
<p>This LUT precompute is Phase 0. Everything else — the online softmax, the V accumulation — comes after. If the LUT is slow, everything is slow.</p>
<p>The Metal 4 spec describes two potential hardware paths to speed this up:</p>
<ol>
<li class=""><strong><code>simdgroup_float8x8</code></strong> (Metal 2.3+, Section 2.4 / 6.7): 8×8 hardware matmul tiles via <code>simdgroup_multiply_accumulate</code>. Available today.</li>
<li class=""><strong>TensorOps <code>matmul2d</code></strong> (Metal 4+, Section 7.2): A full hardware matrix multiply API with a <code>cooperative_tensor</code> destination. Potentially much faster.</li>
</ol>
<p>I tested both.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="attempt-1-simdgroup_float8x8">Attempt 1: <code>simdgroup_float8x8</code><a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#attempt-1-simdgroup_float8x8" class="hash-link" aria-label="Direct link to attempt-1-simdgroup_float8x8" title="Direct link to attempt-1-simdgroup_float8x8" translate="no">​</a></h2>
<p>The Metal spec (Section 6.7, Table 6.9) shows <code>simdgroup_float8x8</code> as a cooperative 8×8 float matrix multiply tile. The <code>&lt;metal_simdgroup_matrix&gt;</code> header is accessible via MLX's <code>header=</code> parameter:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">k </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"my_kernel"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    source</span><span class="token operator">=</span><span class="token plain">src</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    header</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"#include &lt;metal_simdgroup_matrix&gt;\nusing namespace metal;\n"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The tiling plan for our LUT: <code>n_sub=16</code> rows / 8 = 2 row-tiles, <code>n_centroids=256</code> cols / 8 = 32 col-tiles, <code>sub_dim=8</code> = 1 K-tile. Total: 64 hardware matmul operations.</p>
<p>I implemented it. Correctness test: zero diff vs reference.</p>
<p>Then I benchmarked it against the current scalar loop:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">scalar loop:     212 µs per LUT precompute</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">simdgroup 8×8:   255 µs per LUT precompute</span><br></div></code></pre></div></div>
<p><strong>The hardware matrix multiply was slower.</strong></p>
<p>The reason is protocol overhead. <code>simdgroup_float8x8</code> is a cooperative operation — all 32 lanes must execute each tile in lock-step. For our 64 tile iterations, that's 64 synchronization points. The scalar loop has zero synchronization: each lane independently computes 128 dot products in parallel. For a small matrix like <code>[16,8]@[8,256]</code>, the cooperation overhead dominates the compute savings.</p>
<p><code>simdgroup_matrix</code> wins at large, batched matmuls (MLX uses it for GEMM with 128×128 tiles). For our 16×256 LUT, it's the wrong tool. Reverted.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="attempt-2-metal-4-tensorops">Attempt 2: Metal 4 TensorOps<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#attempt-2-metal-4-tensorops" class="hash-link" aria-label="Direct link to Attempt 2: Metal 4 TensorOps" title="Direct link to Attempt 2: Metal 4 TensorOps" translate="no">​</a></h2>
<p>Section 7.2 of the spec describes <code>tensor_ops::matmul2d</code> — a hardware-accelerated matrix multiply that operates on <code>tensor&lt;&gt;</code> types and writes to a <code>cooperative_tensor</code> destination held in thread registers. The pitch is exactly right: no threadgroup memory round-trip, hardware tensor units, single API call.</p>
<p>The example from the spec:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">#include &lt;metal_tensor&gt;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">#include &lt;MetalPerformancePrimitives/MetalPerformancePrimitives.h&gt;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">using namespace metal;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">using namespace mpp;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">[[ kernel ]] void matrixMultiply(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tensor&lt;device half, dextents&lt;int, 2&gt;&gt; a [[ buffer(0) ]],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tensor&lt;device half, dextents&lt;int, 2&gt;&gt; b [[ buffer(1) ]],</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tensor&lt;device half, dextents&lt;int, 2&gt;&gt; c [[ buffer(2) ]]) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    constexpr auto desc = tensor_ops::matmul2d_descriptor(64, 32, 0);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tensor_ops::matmul2d&lt;desc, execution_simdgroups&lt;4&gt;&gt; op;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    matmulOp.run(a, b, c);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Clean. Exactly what we need.</p>
<p>I confirmed the header is accessible:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">k </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"test"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    source</span><span class="token operator">=</span><span class="token plain">src</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    header</span><span class="token operator">=</span><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">"""</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    #include &lt;metal_tensor&gt;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    #include &lt;MetalPerformancePrimitives/MetalPerformancePrimitives.h&gt;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    using namespace metal;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    using namespace mpp;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    """</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Compiles. Header is reachable.</span><br></div></code></pre></div></div>
<p>And Metal 4 is available:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Metal version: 400.0   (M4, macOS Sequoia)</span><br></div></code></pre></div></div>
<p>Then I tried to actually use <code>matmul2d</code>. Three blockers, in order of discovery:</p>
<p><strong>Blocker 1: Type support.</strong></p>
<p>Table 7.3 of the spec lists supported type combinations. <code>float/float/float</code> is listed — but when I tried it:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">static_assert failed: "Unsupported type"</span><br></div></code></pre></div></div>
<p>Table 7.4 clarifies: <code>bfloat/bfloat/bfloat</code> and several mixed-precision combinations require <strong>OS 26.1 and later</strong>. That's iOS/macOS naming — it maps to macOS 26.1 (not released yet as of this writing). The <code>float/float/float</code> path in Table 7.3 is supported, but only with certain <code>execution_scope</code> + K-dimension combinations that are hardware-dependent.</p>
<p><strong>Blocker 2: <code>tensor_handle</code> vs <code>tensor_inline</code>.</strong></p>
<p>The spec's <code>matmul2d</code> example uses tensors declared as kernel parameters with <code>[[buffer(N)]]</code> attributes — these are <code>tensor_handle</code> type. MLX's <code>metal_kernel</code> generates the function signature automatically: it only creates raw pointer buffers (<code>const device float* a [[buffer(0)]]</code>), not <code>tensor&lt;device half, ..., tensor_handle&gt;</code> parameters.</p>
<p>The only tensor type you can construct at runtime from a pointer is <code>tensor_inline</code>. But <code>cooperative_tensor.store()</code> only accepts <code>tensor_handle</code> targets for device memory writes. The round-trip <code>cooperative_tensor → tensor_inline → device output</code> is blocked:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">error: candidate template ignored: could not match 'tensor_handle' against 'tensor_inline'</span><br></div></code></pre></div></div>
<p><strong>Blocker 3: Dynamic K hangs the GPU compiler.</strong></p>
<p>When I tried <code>K=0</code> (dynamic length, matching the spec example exactly), the MLX JIT compilation hung. The TensorOps template instantiation with <code>dynamic_length_v&lt;int&gt;</code> appears to trigger a very long (possibly infinite) compile path under MLX's inline Metal JIT. The process never returned.</p>
<p><strong>Summary:</strong> TensorOps is architecturally incompatible with MLX's <code>mx.fast.metal_kernel</code> API. The API generates raw pointer buffers; TensorOps requires tensor-typed formal parameters. The mismatch is fundamental, not a workaround.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-worked">What Actually Worked<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#what-actually-worked" class="hash-link" aria-label="Direct link to What Actually Worked" title="Direct link to What Actually Worked" translate="no">​</a></h2>
<p>Two improvements from the spec research did ship.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-metalpreciseexp--a-correctness-fix-hiding-as-a-performance-question">1. <code>metal::precise::exp</code> — a correctness fix hiding as a performance question<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#1-metalpreciseexp--a-correctness-fix-hiding-as-a-performance-question" class="hash-link" aria-label="Direct link to 1-metalpreciseexp--a-correctness-fix-hiding-as-a-performance-question" title="Direct link to 1-metalpreciseexp--a-correctness-fix-hiding-as-a-performance-question" translate="no">​</a></h3>
<p>Section 8.2 of the spec describes rounding mode. Section 8.3 covers floating-point exceptions. Table 8.2 documents accuracy under fast math.</p>
<p>The relevant line: <code>exp()</code> in fast math mode (<code>-fmetal-math-mode=fast</code>) does not guarantee <code>exp(-INFINITY) = 0.0</code>. The spec's ULP table for fast math lists relaxed accuracy bounds for transcendentals.</p>
<p>Our kernel uses <code>exp(score - running_max)</code> for the online softmax. When a lane is masked (causal or sliding-window), we set <code>score = -INFINITY</code>. In fast math mode, <code>exp(-INFINITY)</code> may not be exactly <code>0.0</code> — which would corrupt the softmax denominator.</p>
<p>The fix: use the <code>metal::precise::</code> namespace to force IEEE-compliant <code>exp</code> regardless of compiler math mode:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// Before (math-mode dependent):</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float w = exp(score - m_new);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// After (always correct):</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float w = metal::precise::exp(score - m_new);</span><br></div></code></pre></div></div>
<p>MLX's <code>metal_kernel</code> API has no parameter for compiler flags, so <code>-fmetal-math-mode=relaxed</code> isn't accessible. The namespace workaround is better anyway — it's surgical, affects only these two <code>exp</code> calls, and documents intent in the code.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-simd_broadcast_first--eliminating-two-threadgroup-barriers-per-tile">2. <code>simd_broadcast_first</code> — eliminating two threadgroup barriers per tile<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#2-simd_broadcast_first--eliminating-two-threadgroup-barriers-per-tile" class="hash-link" aria-label="Direct link to 2-simd_broadcast_first--eliminating-two-threadgroup-barriers-per-tile" title="Direct link to 2-simd_broadcast_first--eliminating-two-threadgroup-barriers-per-tile" translate="no">​</a></h3>
<p>Section 6.9.2 of the spec (Table 6.14) lists the full SIMD-group permute function set. One entry:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">simd_broadcast_first(x)  →  broadcasts lane 0's value to all lanes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                             without a threadgroup barrier</span><br></div></code></pre></div></div>
<p>The original kernel used threadgroup memory to share the running max and rescale factor:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// Before: two threadgroup variables, two barriers per tile</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (lane == 0) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tg_m_shared = m_new;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    tg_factor   = factor;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">threadgroup_barrier(mem_flags::mem_threadgroup);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">m_new  = tg_m_shared;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">factor = tg_factor;</span><br></div></code></pre></div></div>
<p>With <code>simd_broadcast_first</code>, both threadgroup variables disappear entirely — <code>running_m</code> becomes a lane-local float that all 32 lanes keep synchronized:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// After: no threadgroup variables, no barriers for scalar sharing</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float m_new  = simd_broadcast_first(max(running_m, tile_max));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float factor = simd_broadcast_first(</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    isfinite(running_m) ? metal::precise::exp(running_m - m_new) : 0.0f);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">running_m = m_new;</span><br></div></code></pre></div></div>
<p>With S_kv=4096 and 128 tiles, this removes 256 threadgroup barriers from the hot loop. Threadgroup barriers are expensive — they serialize the entire threadgroup and flush threadgroup memory. Removing them reduces both latency and the register pressure from storing shared state.</p>
<p>Both of these are in the current kernel. 9 parity tests pass. The improvements are real even if the end-to-end situation hasn't changed.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-actual-answer-to-what-is-section-72-useful-for">The Actual Answer to "What Is Section 7.2 Useful For?"<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#the-actual-answer-to-what-is-section-72-useful-for" class="hash-link" aria-label="Direct link to The Actual Answer to &quot;What Is Section 7.2 Useful For?&quot;" title="Direct link to The Actual Answer to &quot;What Is Section 7.2 Useful For?&quot;" translate="no">​</a></h2>
<p>TensorOps would be transformative <strong>if</strong> MLX supported tensor-typed kernel parameters. The current <code>mx.fast.metal_kernel</code> API exposes only raw device pointers — the <code>[[buffer(N)]]</code> binding that TensorOps needs is auto-generated as <code>const device float*</code>, not <code>tensor&lt;device half, ..., tensor_handle&gt;</code>.</p>
<p>To use TensorOps for our LUT precompute, MLX would need one of:</p>
<ol>
<li class="">
<p><strong>Support <code>tensor&lt;&gt;</code> as a formal parameter type</strong> in <code>metal_kernel</code>'s auto-generated signature. Something like <code>input_tensor_types=[("a", mx.float16, 2)]</code> that generates <code>tensor&lt;device half, dextents&lt;int,2&gt;&gt; a [[buffer(0)]]</code>.</p>
</li>
<li class="">
<p><strong>A new <code>mx.fast.metal_tensor_kernel</code> variant</strong> that accepts tensor operands natively and dispatches via TensorOps internally.</p>
</li>
</ol>
<p>This is exactly the GitHub issue we filed at <a href="https://github.com/ml-explore/mlx" target="_blank" rel="noopener noreferrer" class="">ml-explore/mlx</a>. The issue covers three requests — compiler options, integer template parameters, and Metal 4 tensor type access — all confirmed by direct testing.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-broader-pattern">The Broader Pattern<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#the-broader-pattern" class="hash-link" aria-label="Direct link to The Broader Pattern" title="Direct link to The Broader Pattern" translate="no">​</a></h2>
<p>Three sessions, three attempts at the LUT precompute, three different techniques:</p>
<table><thead><tr><th>Attempt</th><th>Technique</th><th>Result</th></tr></thead><tbody><tr><td>Original</td><td>Scalar loop, 32 lanes stripe independently</td><td>Baseline</td></tr><tr><td>Attempt 1</td><td><code>simdgroup_float8x8</code>, cooperative 8×8 tiles</td><td>20 µs slower — protocol overhead wins</td></tr><tr><td>Attempt 2</td><td>TensorOps <code>matmul2d</code>, hardware tensor units</td><td>API incompatible with MLX's kernel wrapper</td></tr></tbody></table>
<p>The pattern: each attempt was technically sound, correctly implemented, and blocked by something orthogonal to the GPU math.</p>
<ul>
<li class="">Simdgroup matrix: the hardware works, the tile size is wrong.</li>
<li class="">TensorOps: the hardware works, the API binding doesn't exist.</li>
</ul>
<p>In both cases, the blocker wasn't that the hardware was slow. The blocker was that the <strong>interface</strong> between our code and the hardware had a constraint we couldn't see until we hit it.</p>
<p>The right mental model for GPU kernel work on Apple Silicon: there are three layers — the math you want to do, the hardware that can do it, and the API that connects them. Breakthroughs happen at the API layer, not the math layer. The math for attention has been solved. The hardware for matrix multiply has been built. The gap is the binding.</p>
<p>That gap is the GitHub issue. If MLX adds <code>tensor&lt;&gt;</code> support to <code>metal_kernel</code>, this whole investigation becomes a one-afternoon project. Until then, the scalar LUT is the fastest thing we can write.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-in-the-library-now">What Is in the Library Now<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#what-is-in-the-library-now" class="hash-link" aria-label="Direct link to What Is in the Library Now" title="Direct link to What Is in the Library Now" translate="no">​</a></h2>
<p><a href="https://github.com/rajveer43/VeloxQuant-MLX/blob/master/veloxquant_mlx/metal/fused_sdpa.py" target="_blank" rel="noopener noreferrer" class="">veloxquant_mlx/metal/fused_sdpa.py</a> has:</p>
<ul>
<li class=""><code>metal::precise::exp</code> for both softmax <code>exp</code> calls — correctness guarantee regardless of MLX math mode</li>
<li class=""><code>simd_broadcast_first</code> replacing threadgroup barriers for <code>running_m</code> — 256 fewer barriers at S_kv=4096</li>
<li class=""><code>tg_m_shared</code> and <code>tg_factor</code> threadgroup variables removed — smaller threadgroup memory footprint</li>
<li class="">All 9 parity tests passing: causal, non-causal, sliding-window, GQA, short-sequence, long-sequence, dispatcher patch</li>
</ul>
<p>The end-to-end situation is unchanged from <a href="https://medium.com/@rajveer.rathod1301" target="_blank" rel="noopener noreferrer" class="">the previous post</a> — the kernel only helps if mlx_lm exposes a way to skip K_hat materialization, which requires an upstream change.</p>
<p>But the kernel is now more correct and slightly better engineered. That's what reading 346 pages of a GPU spec gets you when the hardware feature you wanted is one API version away.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-one-practical-takeaway">The One Practical Takeaway<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#the-one-practical-takeaway" class="hash-link" aria-label="Direct link to The One Practical Takeaway" title="Direct link to The One Practical Takeaway" translate="no">​</a></h2>
<p>Before spending time implementing a GPU optimization, answer this question:</p>
<p><strong>Which layer is blocking you — the math, the hardware, or the API?</strong></p>
<p>If the math is solved and the hardware exists, the answer is almost always the API. Find the API gap first. File the issue or write the binding. Don't write the kernel until the API exists to call it from.</p>
<p>I wrote the kernel first. I found the API gap last. Three sessions later.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="links">Links<a href="https://veloxquant-mlx.netlify.app/docs/blog/tensorops-research#links" class="hash-link" aria-label="Direct link to Links" title="Direct link to Links" translate="no">​</a></h2>
<ul>
<li class="">VeloxQuant-MLX on PyPI: <a href="https://pypi.org/project/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">pypi.org/project/VeloxQuant-MLX</a></li>
<li class="">GitHub: <a href="https://github.com/rajveer43/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">github.com/rajveer43/VeloxQuant-MLX</a></li>
<li class="">The kernel: <a href="https://github.com/rajveer43/VeloxQuant-MLX/blob/master/veloxquant_mlx/metal/fused_sdpa.py" target="_blank" rel="noopener noreferrer" class="">veloxquant_mlx/metal/fused_sdpa.py</a></li>
<li class="">MLX issue filed: <a href="https://github.com/ml-explore/mlx/issues" target="_blank" rel="noopener noreferrer" class="">github.com/ml-explore/mlx/issues</a></li>
<li class="">Previous post (Phase 2 mistake): <a href="https://medium.com/@rajveer.rathod1301" target="_blank" rel="noopener noreferrer" class="">Medium — I Spent 8 Hours Writing a FlashAttention Kernel</a></li>
<li class="">Previous post (Phase 1 win): <a href="https://medium.com/@rajveer.rathod1301" target="_blank" rel="noopener noreferrer" class="">Medium — I Wrote a Metal Kernel to Stop My Mac From OOMing</a></li>
</ul>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="metal" term="metal"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="gpu" term="gpu"/>
        <category label="research" term="research"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[KIVI: The Most-Cited KV Cache Baseline, Implemented in MLX]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/kivi</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/kivi"/>
        <updated>2026-06-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[TL;DR — VeloxQuant-MLX 0.8.0 adds KIVI (Liu, Yuan et al., ICML 2024, arXiv:2402.02750), a faithful re-implementation of the most-cited KV-cache quantization baseline. It's asymmetric (per-channel keys, per-token values), keeps a small fp16 residual window, and is fully deterministic. On an Apple M4, across Llama-3.2-3B, Qwen2.5-7B, and Mistral-7B (4-bit, ~2.2–2.4k-token prompts), KIVI-2bit measured 5.8× key / ~4.0× full-KV compression at roughly fp16 throughput. On Apple Silicon the win is memory, not speed — and we report the throughput as measured, not as hoped. Every number below traces to a committed results.json.]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p><strong>TL;DR</strong> — VeloxQuant-MLX 0.8.0 adds <strong>KIVI</strong> (Liu, Yuan et al., <em>ICML 2024</em>, <a href="https://arxiv.org/abs/2402.02750" target="_blank" rel="noopener noreferrer" class="">arXiv:2402.02750</a>), a faithful re-implementation of the most-cited KV-cache quantization baseline. It's asymmetric (per-channel keys, per-token values), keeps a small fp16 residual window, and is fully deterministic. On an Apple M4, across Llama-3.2-3B, Qwen2.5-7B, and Mistral-7B (4-bit, ~2.2–2.4k-token prompts), KIVI-2bit measured <strong>5.8× key / ~4.0× full-KV compression at roughly fp16 throughput</strong>. On Apple Silicon the win is memory, not speed — and we report the throughput as measured, not as hoped. Every number below traces to a committed <code>results.json</code>.</p>
</blockquote>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-wall-again">The wall, again<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#the-wall-again" class="hash-link" aria-label="Direct link to The wall, again" title="Direct link to The wall, again" translate="no">​</a></h2>
<p>If you run local LLMs on a Mac, you've met the unified-memory wall. The CPU, GPU, and Neural Engine all share one pool of RAM, and the KV cache — the keys and values the model stores for every past token — grows linearly with context length until it crowds out everything else. Weight quantization (GGUF, GPTQ, AWQ) is an <em>offline, one-time</em> compression of the model's parameters; it does nothing for the cache, which is rebuilt token by token at inference time.</p>
<p>VeloxQuant-MLX exists to compress that cache. It already ships a suite of methods — TurboQuant, RVQ, VecInfer, SpectralQuant, RaBitQ, CommVQ, QJL, PolarQuant, RateQuant. With 0.8.0 we added the one method a reviewer asks about first, and which the library conspicuously lacked: <strong>KIVI</strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-kivi-is">What KIVI is<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#what-kivi-is" class="hash-link" aria-label="Direct link to What KIVI is" title="Direct link to What KIVI is" translate="no">​</a></h2>
<p>KIVI is <strong>"A Tuning-Free Asymmetric 2-bit Quantization for KV Cache"</strong> (<a href="https://arxiv.org/abs/2402.02750" target="_blank" rel="noopener noreferrer" class="">arXiv:2402.02750</a>, ICML 2024). Its central observation is that keys and values want <em>different</em> quantization layouts:</p>
<ul>
<li class=""><strong>Keys → per channel.</strong> Key distributions have a few high-variance channels. Quantizing along the token axis, one (scale, zero) per channel-group, keeps those channels accurate.</li>
<li class=""><strong>Values → per token.</strong> Value distributions are flatter across channels but vary token to token, so the group runs along the channel axis instead.</li>
<li class=""><strong>A residual window.</strong> The most recently generated tokens dominate attention and are cheap to keep exact, so KIVI holds the last <code>R</code> tokens in <strong>fp16</strong> and only quantizes tokens once they age out of that window.</li>
</ul>
<p>The quantizer itself is plain asymmetric min/max group quantization — no codebook, no calibration, no randomness:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">for each group g (a slice along the quantization axis):</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    zero  = min(g)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    scale = (max(g) - min(g)) / (2**b - 1)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    q     = round((g - zero) / scale)      # uint code in [0, 2**b-1]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    g_hat = q * scale + zero               # asymmetric dequant</span><br></div></code></pre></div></div>
<p>Because there's no k-means and no RNG, KIVI is <strong>deterministic</strong> — same input, identical output, every run. That matters in this codebase: our vector-quantization methods train codebooks and can vary run to run; KIVI adds none of that.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-we-added-it-and-what-we-didnt-invent">Why we added it (and what we didn't invent)<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#why-we-added-it-and-what-we-didnt-invent" class="hash-link" aria-label="Direct link to Why we added it (and what we didn't invent)" title="Direct link to Why we added it (and what we didn't invent)" translate="no">​</a></h2>
<p>To be clear about credit: <strong>KIVI is Liu, Yuan, and colleagues' algorithm. We ported it to MLX.</strong> There is no novelty claim here.</p>
<p>The value is comparative. KIVI is the field's reference baseline — nearly every KV-cache paper measures against it. Until 0.8.0, VeloxQuant-MLX couldn't answer "how does your method compare to KIVI?" Now every other method in the library has a recognized point of comparison, in the same framework, on the same Apple-Silicon hardware. (The full reasoning, including why we chose KIVI over KVQuant, GEAR, and ZipCache, is in <code>paper/NEW_METHOD_SURVEY.md</code>: KIVI was the highest-value missing baseline, deterministic, and a clean architectural fit that needs no RoPE or attention-score hooks.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-plugs-in">How it plugs in<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#how-it-plugs-in" class="hash-link" aria-label="Direct link to How it plugs in" title="Direct link to How it plugs in" translate="no">​</a></h2>
<p>Three lines, and <code>mlx_lm.generate</code> runs unchanged:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheBuilder</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Llama-3.2-3B-Instruct-4bit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    method</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"kivi"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bit_width_inlier</span><span class="token operator">=</span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">      </span><span class="token comment" style="color:rgb(98, 114, 164)"># KIVI's default 2-bit</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    kivi_group_size</span><span class="token operator">=</span><span class="token number">32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">      </span><span class="token comment" style="color:rgb(98, 114, 164)"># min/max group size (paper default)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    residual_length</span><span class="token operator">=</span><span class="token number">32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">      </span><span class="token comment" style="color:rgb(98, 114, 164)"># recent tokens kept in fp16</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">caches </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">make_cache </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">lambda</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain">_a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">**</span><span class="token plain">_k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> caches</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">response </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    prompt</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"Summarize the attention mechanism in three sentences."</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    max_tokens</span><span class="token operator">=</span><span class="token number">300</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The cache quantizes the aged-out keys/values and immediately dequantizes them, so the downstream scaled-dot-product attention sees standard fp16 tensors — no model surgery, no custom attention path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h2>
<p>All numbers below come from <code>figures/kivi/results_summary.json</code> (aggregated from the per-model <code>figures/kivi/&lt;model&gt;/results.json</code>). Conditions, identical across runs: <strong>Apple M4, 24 GB</strong>, 4-bit <code>mlx-community</code> models, <code>group_size=32</code>, <code>residual_length=32</code>, <code>max_tokens=120</code>, prompts of ~2.2–2.4k tokens (a long prompt is required for KIVI to actually exercise the quantized path rather than sit inside the fp16 residual window).</p>
<p><strong>KIVI-2bit, across three models:</strong></p>
<table><thead><tr><th>Model</th><th>head_dim / KV heads</th><th>Key compression</th><th>Full-KV compression</th><th>Throughput vs fp16</th><th>Tokens</th></tr></thead><tbody><tr><td>Llama-3.2-3B</td><td>128 / 8</td><td><strong>5.79×</strong></td><td><strong>3.98×</strong></td><td>16.3 vs 16.0 tok/s (<strong>102%</strong>)</td><td>121/121</td></tr><tr><td>Qwen2.5-7B</td><td>128 / 4</td><td><strong>5.78×</strong></td><td><strong>3.98×</strong></td><td>7.6 vs 7.6 tok/s (<strong>100%</strong>)</td><td>120/120</td></tr><tr><td>Mistral-7B</td><td>128 / 8</td><td><strong>5.76×</strong></td><td><strong>4.03×</strong></td><td>6.8 vs 6.5 tok/s (<strong>105%</strong>)</td><td>122/122</td></tr></tbody></table>
<p><strong>Bit-width sweep (Llama-3.2-3B):</strong></p>
<table><thead><tr><th>Config</th><th>Key compression</th><th>Full-KV compression</th><th>Throughput</th></tr></thead><tbody><tr><td>fp16 baseline</td><td>1.00×</td><td>1.00×</td><td>16.0 tok/s</td></tr><tr><td>KIVI-2bit</td><td>5.79×</td><td>3.98×</td><td>16.3 tok/s</td></tr><tr><td>KIVI-3bit</td><td>4.34×</td><td>3.24×</td><td>15.9 tok/s</td></tr><tr><td>KIVI-4bit</td><td>3.47×</td><td>2.73×</td><td>16.0 tok/s</td></tr></tbody></table>
<p>Two honest observations from this data:</p>
<ol>
<li class=""><strong>Throughput is flat, not faster.</strong> KIVI here runs at 100–105% of fp16 — i.e. it does <em>not</em> slow generation down at these sizes, but it also doesn't speed it up. The published KIVI speedups come from a fused CUDA kernel; that kernel does not port to Metal. On Apple Silicon the deliverable is <strong>memory compression</strong> (the 4× full-KV figure), and the throughput column is there so you can see it costs you nothing, not so you can claim a speedup.</li>
<li class=""><strong>Full-KV compression is lower than key-only</strong>, and deliberately so. The full-KV figure includes the fp16 residual window. At <code>residual_length=32</code> over a 120-token generation, a meaningful slice stays in fp16; that drags the end-to-end ratio below the key-only number. We report both rather than quoting the flattering one.</li>
</ol>
<p>The implementation is covered by <strong>25 passing tests</strong> (<code>veloxquant_mlx/tests/quantizers/test_kivi.py</code> and <code>tests/cache/test_kivi_cache.py</code>), including reconstruction-fidelity bounds, the per-token/per-channel asymmetry, the residual-window behavior, and a determinism check.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="honest-limitations">Honest limitations<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#honest-limitations" class="hash-link" aria-label="Direct link to Honest limitations" title="Direct link to Honest limitations" translate="no">​</a></h2>
<ul>
<li class=""><strong>Memory win, not a speed win on Metal.</strong> As above — the CUDA kernel fusion from the paper isn't available here. If you need raw throughput, this isn't your lever.</li>
<li class=""><strong>Quality is measured as reconstruction fidelity, not task accuracy.</strong> Our tests check cosine similarity / MSE against the fp16 cache on synthetic and real key distributions. We have <strong>not</strong> run LongBench or a perplexity sweep across configs, so we do not claim "no quality loss" on downstream tasks.</li>
<li class=""><strong>2-bit is genuinely lossy.</strong> On unit-norm synthetic keys, KIVI-2bit reconstruction cosine sits around 0.93 — which is exactly <em>why</em> KIVI keeps an fp16 residual window. If you push <code>residual_length</code> to 0 you'll feel it. The defaults exist for a reason.</li>
<li class=""><strong>Single chip, short generations.</strong> Everything above is one M4 at ~120 generated tokens. Behavior across other M-series tiers and very long generations isn't characterized yet.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-kivi-fits">Where KIVI fits<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#where-kivi-fits" class="hash-link" aria-label="Direct link to Where KIVI fits" title="Direct link to Where KIVI fits" translate="no">​</a></h2>
<ul>
<li class=""><strong>Reach for KIVI</strong> when you want a <em>simple, deterministic, calibration-free</em> 2-bit baseline — or when you specifically need to compare against the literature's reference point.</li>
<li class=""><strong>Reach for RVQ</strong> when you want stronger compression-per-bit with zero calibration and near-fp16 throughput (its analytical codebooks do better than scalar min/max at low bit-rates).</li>
<li class=""><strong>Reach for VecInfer</strong> when you want the most aggressive key compression (up to 16× key-only) and have ~2 minutes for codebook calibration.</li>
</ul>
<p>KIVI's job in the suite isn't to win every axis; it's to be the honest yardstick the others are measured against.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it">Try it<a href="https://veloxquant-mlx.netlify.app/docs/blog/kivi#try-it" class="hash-link" aria-label="Direct link to Try it" title="Direct link to Try it" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> VeloxQuant-MLX</span><span class="token operator">==</span><span class="token number">0.8</span><span class="token plain">.0</span><br></div></code></pre></div></div>
<ul>
<li class="">Docs: <code>/docs/algorithms/kivi/</code></li>
<li class="">GitHub: <a href="https://github.com/rajveer43/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">https://github.com/rajveer43/VeloxQuant-MLX</a></li>
<li class="">Paper: KIVI, <a href="https://arxiv.org/abs/2402.02750" target="_blank" rel="noopener noreferrer" class="">arXiv:2402.02750</a> (ICML 2024)</li>
</ul>
<hr>
<p><em>What was measured vs. not: all compression, throughput, peak-memory, and token-count figures are from committed <code>figures/kivi/*/results.json</code> on a single Apple M4 (24 GB) at ~120 generated tokens with ~2.2–2.4k-token prompts; correctness is from 25 passing unit tests. We did not measure downstream-task accuracy (e.g. LongBench, perplexity sweeps) — "quality" here means reconstruction fidelity against the fp16 cache.</em></p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="quantization" term="quantization"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="kv-cache" term="kv-cache"/>
        <category label="kivi" term="kivi"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Hands-On: Compressing Your First LLM with VeloxQuant-MLX]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/hands-on</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on"/>
        <updated>2026-05-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to cut your KV cache memory by 8x and hit fp16 throughput — step by step, with real models.]]></summary>
        <content type="html"><![CDATA[<p><em>How to cut your KV cache memory by 8x and hit fp16 throughput — step by step, with real models.</em></p>
<hr>
<p>If you have an Apple Silicon Mac and you run local LLMs, you have already hit the wall: your MacBook Pro generates 20 tokens per second on Mistral 7B, memory pressure turns on after a few thousand tokens, and anything larger than 7B crawls. The bottleneck is not compute — it is memory bandwidth. Every token requires loading the entire KV cache for every layer. Make the cache smaller, and the model gets faster.</p>
<p>VeloxQuant-MLX is a drop-in KV cache quantization library for MLX. It compresses the KV cache to 2, 3, or 4 bits per value while keeping text quality close to full-precision. In this guide you will install it, run your first compressed model, pick the right algorithm for your use case, and understand what the benchmark numbers mean. No machine learning background required.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-you-need">What You Need<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#what-you-need" class="hash-link" aria-label="Direct link to What You Need" title="Direct link to What You Need" translate="no">​</a></h2>
<ul>
<li class="">Apple Silicon Mac (M1 or later)</li>
<li class="">Python 3.11 or 3.12</li>
<li class="">At least 16 GB unified memory (8 GB works for 4B models)</li>
<li class=""><code>mlx-lm</code> installed (<code>pip install mlx-lm</code>)</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-install-veloxquant-mlx">Step 1: Install VeloxQuant-MLX<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-1-install-veloxquant-mlx" class="hash-link" aria-label="Direct link to Step 1: Install VeloxQuant-MLX" title="Direct link to Step 1: Install VeloxQuant-MLX" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> VeloxQuant-MLX</span><br></div></code></pre></div></div>
<p>That installs the <code>mlx_kv_quant</code> package plus the <code>veloxquant</code> CLI. Verify it worked:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">veloxquant </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--help</span><br></div></code></pre></div></div>
<p>You should see the precompute and benchmark subcommands.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-your-first-compressed-inference">Step 2: Your First Compressed Inference<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-2-your-first-compressed-inference" class="hash-link" aria-label="Direct link to Step 2: Your First Compressed Inference" title="Direct link to Step 2: Your First Compressed Inference" translate="no">​</a></h2>
<p>The fastest path uses the <code>KVCacheBuilder</code>. Here is a minimal script that runs Mistral 7B with 4-bit KV cache compression:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">core </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> mx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheConfig</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Load the model normally</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Mistral-7B-Instruct-v0.3-4bit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Build a quantized KV cache</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bits</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> algorithm</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_prod"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Generate with the compressed cache</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">prompt </span><span class="token operator">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"Explain the difference between RAM and unified memory."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">tokens </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> prompt</span><span class="token operator">=</span><span class="token plain">prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> kv_cache</span><span class="token operator">=</span><span class="token plain">cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> max_tokens</span><span class="token operator">=</span><span class="token number">200</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">tokens</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>That is the entire integration. The cache is transparent to <code>mlx_lm.generate()</code> — you do not change anything else.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-understanding-the-algorithm-choices">Step 3: Understanding the Algorithm Choices<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-3-understanding-the-algorithm-choices" class="hash-link" aria-label="Direct link to Step 3: Understanding the Algorithm Choices" title="Direct link to Step 3: Understanding the Algorithm Choices" translate="no">​</a></h2>
<p>VeloxQuant-MLX ships five quantization algorithms. They trade reconstruction quality against memory and speed:</p>
<table><thead><tr><th>Algorithm name</th><th>Bits/dim</th><th>Best for</th><th>Notes</th></tr></thead><tbody><tr><td><code>turboquant_mse</code></td><td>b</td><td>Fast inference, b &gt;= 3</td><td>MSE-optimal Lloyd-Max codebook</td></tr><tr><td><code>turboquant_prod</code></td><td>b + 1</td><td>Inner-product tasks, RAG</td><td>Adds 1-bit QJL residual correction</td></tr><tr><td><code>turboquant_rvq</code></td><td>2b</td><td>2-bit where quality matters</td><td>Two-pass residual; cosine 0.98 at b=2</td></tr><tr><td><code>polarquant</code></td><td>b</td><td>Long contexts</td><td>Spherical Lloyd-Max, no norm storage</td></tr><tr><td><code>qjl</code></td><td>1</td><td>Ultra-low memory</td><td>Sign sketch only; rough approximation</td></tr></tbody></table>
<p><strong>Rule of thumb:</strong></p>
<ul>
<li class="">For most uses at 3–4 bit: use <code>turboquant_prod</code>. It is the default and the most tested.</li>
<li class="">At 2 bits: always use <code>turboquant_rvq</code>. The single-pass algorithms fall apart at 2-bit; RVQ does not.</li>
<li class="">For long contexts where cosine similarity matters more than reconstruction: <code>polarquant</code>.</li>
<li class="">To stress-test memory limits: <code>qjl</code>.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-using-rvq-2-bit-the-interesting-one">Step 4: Using RVQ 2-Bit (the Interesting One)<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-4-using-rvq-2-bit-the-interesting-one" class="hash-link" aria-label="Direct link to Step 4: Using RVQ 2-Bit (the Interesting One)" title="Direct link to Step 4: Using RVQ 2-Bit (the Interesting One)" translate="no">​</a></h2>
<p>The headline feature in v0.3.0 is <code>TurboQuantRVQ</code> — a two-pass residual vector quantizer that makes 2-bit KV cache actually usable.</p>
<p>The problem with naive 2-bit quantization: you only have 4 levels to represent a continuous value. The reconstruction error is large enough that attention scores get corrupted, and long reasoning chains (like Qwen3's <code>&lt;think&gt;</code> mode) collapse into repetition after a few dozen tokens.</p>
<p>RVQ fixes this by running two codebooks. The first pass quantizes the rotated vector. The second pass quantizes the residual — the error left over from the first pass — using a Laplacian-fit codebook (which matches the residual distribution better than a Gaussian one). The result: cosine similarity goes from 0.69 to 0.98 at 2 bits.</p>
<p>Here is how to use it:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheConfig</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bits</span><span class="token operator">=</span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> algorithm</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_rvq"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>That's it. The <code>turboquant_rvq</code> string routes to the registered <code>TurboQuantRVQ</code> class via the quantizer registry.</p>
<p><strong>When should you use RVQ 2-bit?</strong></p>
<p>Use it when memory is the binding constraint and you need coherent long-form output. It delivers:</p>
<ul>
<li class="">Mistral 7B at 2-bit: 22.3 tok/s — matches fp16 throughput (22.1 tok/s) on an M4 MacBook</li>
<li class="">Qwen3 4B at 2-bit: 36.0 tok/s (92% of fp16) with full thinking-mode output</li>
</ul>
<p>Use standard 3-bit or 4-bit when you have memory headroom and want zero quality trade-off.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-running-benchmarks-on-your-mac">Step 5: Running Benchmarks on Your Mac<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-5-running-benchmarks-on-your-mac" class="hash-link" aria-label="Direct link to Step 5: Running Benchmarks on Your Mac" title="Direct link to Step 5: Running Benchmarks on Your Mac" translate="no">​</a></h2>
<p>VeloxQuant-MLX ships benchmark scripts for several models. These measure tok/s and output completeness across five configurations: fp16, 2-bit RVQ, 3-bit, 4-bit, and 2-bit single-pass (for comparison).</p>
<p><strong>Qwen3 4B</strong> (good for 16GB Macs):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">python3 benchmark_qwen3_4b_v2.py</span><br></div></code></pre></div></div>
<p><strong>Mistral 7B</strong> (needs 16GB+, comfortable at 32GB):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">python3 benchmark_mistral7b_v2.py</span><br></div></code></pre></div></div>
<p>Each script runs all five configs, prints a results table, and saves six figures to <code>figures/updated_tests/&lt;model&gt;/</code>. The figures include:</p>
<ul>
<li class="">Throughput comparison bar chart</li>
<li class="">Token completeness (how many tokens were coherent out of 200)</li>
<li class="">Memory usage per config</li>
<li class="">Cosine similarity vs fp16 reference</li>
</ul>
<p>Here is what our numbers look like on an M4 MacBook (16GB):</p>
<p><strong>Mistral 7B throughput:</strong></p>
<table><thead><tr><th>Config</th><th>tok/s</th><th>Memory</th></tr></thead><tbody><tr><td>fp16</td><td>22.1</td><td>14.2 GB</td></tr><tr><td>RVQ 2-bit</td><td>22.3</td><td>3.8 GB</td></tr><tr><td>3-bit</td><td>21.8</td><td>5.4 GB</td></tr><tr><td>4-bit</td><td>21.6</td><td>7.1 GB</td></tr></tbody></table>
<p>Mistral 7B is memory-bandwidth bound — every config saturates around 22 tok/s because that is the bandwidth ceiling. But RVQ 2-bit uses 10x less cache memory than fp16.</p>
<p><strong>Qwen3 4B thinking-mode throughput:</strong></p>
<table><thead><tr><th>Config</th><th>tok/s</th><th>Tokens (out of 200)</th></tr></thead><tbody><tr><td>fp16</td><td>39.2</td><td>200 / 200</td></tr><tr><td>RVQ 2-bit</td><td>36.0</td><td>199 / 200</td></tr><tr><td>3-bit</td><td>37.1</td><td>200 / 200</td></tr><tr><td>4-bit TQ</td><td>24.8</td><td>50 / 200</td></tr></tbody></table>
<p>The 4-bit single-pass result (50/200) is the failure mode for standard quantization on a thinking model. RVQ 2-bit produces full coherent output and runs faster.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6-integrating-with-mlx-lm-properly">Step 6: Integrating with mlx-lm Properly<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-6-integrating-with-mlx-lm-properly" class="hash-link" aria-label="Direct link to Step 6: Integrating with mlx-lm Properly" title="Direct link to Step 6: Integrating with mlx-lm Properly" translate="no">​</a></h2>
<p>For real usage you want the cache to persist across generation steps. Here is the pattern for a chat loop:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">core </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> mx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheConfig</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Qwen3-4B"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bits</span><span class="token operator">=</span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> algorithm</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_rvq"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">messages </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">while</span><span class="token plain"> </span><span class="token boolean">True</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    user_input </span><span class="token operator">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"You: "</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> user_input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">lower</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"exit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"quit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">break</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    messages</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"role"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"user"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"content"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> user_input</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    prompt </span><span class="token operator">=</span><span class="token plain"> tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">apply_chat_template</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenize</span><span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> add_generation_prompt</span><span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    response </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        prompt</span><span class="token operator">=</span><span class="token plain">prompt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        kv_cache</span><span class="token operator">=</span><span class="token plain">cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        max_tokens</span><span class="token operator">=</span><span class="token number">512</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        verbose</span><span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    messages</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string" style="color:rgb(255, 121, 198)">"role"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"assistant"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"content"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> response</span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"Assistant: </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">response</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">\n"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The <code>kv_cache</code> argument is passed directly to <code>mlx_lm.generate()</code>. No other changes. The cache grows with each turn and is quantized transparently.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7-precomputing-codebooks-for-repeated-use">Step 7: Precomputing Codebooks for Repeated Use<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-7-precomputing-codebooks-for-repeated-use" class="hash-link" aria-label="Direct link to Step 7: Precomputing Codebooks for Repeated Use" title="Direct link to Step 7: Precomputing Codebooks for Repeated Use" translate="no">​</a></h2>
<p>If you run the same model repeatedly, you can precompute the Lloyd-Max codebooks offline and load them at inference time. This eliminates the one-time calibration cost at startup:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># Precompute and save codebooks for Mistral 7B, 4-bit</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">veloxquant precompute </span><span class="token punctuation" style="color:rgb(248, 248, 242)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--model</span><span class="token plain"> mlx-community/Mistral-7B-Instruct-v0.3-4bit </span><span class="token punctuation" style="color:rgb(248, 248, 242)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--bits</span><span class="token plain"> </span><span class="token number">4</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--algorithm</span><span class="token plain"> turboquant_prod </span><span class="token punctuation" style="color:rgb(248, 248, 242)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--output</span><span class="token plain"> codebooks/mistral7b_4bit/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Later, load at inference time</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token assign-left variable" style="color:rgb(189, 147, 249);font-style:italic">bits</span><span class="token operator">=</span><span class="token number">4</span><span class="token plain">,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token assign-left variable" style="color:rgb(189, 147, 249);font-style:italic">algorithm</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_prod"</span><span class="token plain">,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token assign-left variable" style="color:rgb(189, 147, 249);font-style:italic">artifact_dir</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"codebooks/mistral7b_4bit/"</span><span class="token plain">,</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">.for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">.build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Codebook size is tiny (kilobytes). You can commit them to your project directory and avoid the calibration step entirely on every run.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-8-picking-the-right-bit-width">Step 8: Picking the Right Bit Width<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-8-picking-the-right-bit-width" class="hash-link" aria-label="Direct link to Step 8: Picking the Right Bit Width" title="Direct link to Step 8: Picking the Right Bit Width" translate="no">​</a></h2>
<p>Here is the practical decision tree:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">Do you have &gt;= 32GB memory?</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Yes → fp16 is fine; use VeloxQuant only for very long contexts</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  No  → continue</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Is your model &gt;= 7B parameters?</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Yes → 3-bit TurboQuant (turboquant_prod)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  No  → 2-bit RVQ (turboquant_rvq)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Are you doing RAG or similarity search over the KV cache?</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Yes → turboquant_prod (inner-product correction matters)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  No  → turboquant_mse (simpler, same quality)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">Do you need to run 13B+ on a 16GB Mac?</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Yes → 2-bit RVQ — the only option that keeps quality</span><br></div></code></pre></div></div>
<p>For generation tasks (chat, summarization, code completion), MSE-optimal reconstruction is what you want. For retrieval tasks that compute attention over a cached context, inner-product preservation matters more — use <code>turboquant_prod</code>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-9-what-the-optimization-journey-looked-like">Step 9: What the Optimization Journey Looked Like<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-9-what-the-optimization-journey-looked-like" class="hash-link" aria-label="Direct link to Step 9: What the Optimization Journey Looked Like" title="Direct link to Step 9: What the Optimization Journey Looked Like" translate="no">​</a></h2>
<p>Getting from 17.7 tok/s to 22.3 tok/s on Mistral 7B RVQ 2-bit required four changes, each independently measurable:</p>
<p><strong>1. Batch all heads into one MLX call (+22%)</strong></p>
<p>The original code ran a Python <code>for h in range(H)</code> loop over attention heads, giving each head its own quantizer instance. For Mistral 7B (8 heads, 32 layers) that meant 256 small kernel dispatches per token. The fix: reshape <code>(B, H, S, D)</code> to <code>(B*H*S, D)</code> and call one shared quantizer once. MLX already handles batched input — we just stopped fragmenting it.</p>
<p><strong>2. Switch to Hadamard rotation</strong></p>
<p>The default preconditioner used QR decomposition: a full <code>(d, d)</code> matrix multiply (16,384 ops at d=128). <code>mx.hadamard_transform</code> is a Metal-native fused kernel that does the same job in O(d log d) — 896 ops at d=128, ~18x less arithmetic. Quality is mathematically identical because Hadamard with random ±1 diagonal is also a Haar-equivalent rotation.</p>
<p><strong>3. Replace broadcast-argmin with boundary-sum</strong></p>
<p>Codebook lookup materialized a <code>(batch, 128, k)</code> distance tensor for argmin — three kernels: broadcast-subtract, abs, argmin. Lloyd-Max boundaries are just midpoints between sorted centroids. The nearest centroid index is the number of boundaries the value exceeds: one comparison and one sum, two kernels total. Index match vs the old path: 100.0000%.</p>
<p><strong>4. Drop redundant fp32 casts</strong></p>
<p>The update path was casting fp16 → fp32 → fp16 → fp32 → fp16 unnecessarily because the rotation already handles internal precision internally. Removing the round-trips saved ~1.05x per call and is invisible to output quality.</p>
<p>The full write-up with stage-by-stage numbers is in <a href="https://github.com/rajveer43/VeloxQuant-MLX/blob/master/OPTIMIZATION_FINDINGS.md" target="_blank" rel="noopener noreferrer" class="">OPTIMIZATION_FINDINGS.md</a>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-10-understanding-memory-numbers">Step 10: Understanding Memory Numbers<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#step-10-understanding-memory-numbers" class="hash-link" aria-label="Direct link to Step 10: Understanding Memory Numbers" title="Direct link to Step 10: Understanding Memory Numbers" translate="no">​</a></h2>
<p>KV cache memory scales with: <code>2 × n_layers × n_kv_heads × head_dim × seq_len × bytes_per_element</code></p>
<p>For Mistral 7B (32 layers, 8 KV heads, head_dim 128) at sequence length 2048:</p>
<ul>
<li class="">fp16: 2 × 32 × 8 × 128 × 2048 × 2 bytes = <strong>536 MB</strong></li>
<li class="">4-bit: 268 MB (2x reduction)</li>
<li class="">2-bit RVQ: 134 MB (4x reduction, plus codebook overhead ~8 KB)</li>
</ul>
<p>At longer contexts (32K tokens), these numbers scale linearly:</p>
<ul>
<li class="">fp16: 8.4 GB for context alone</li>
<li class="">2-bit RVQ: 2.1 GB</li>
</ul>
<p>On a 16GB Mac, the difference between fp16 and 2-bit RVQ at 32K context is the difference between OOM and running.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="common-questions">Common Questions<a href="https://veloxquant-mlx.netlify.app/docs/blog/hands-on#common-questions" class="hash-link" aria-label="Direct link to Common Questions" title="Direct link to Common Questions" translate="no">​</a></h2>
<p><strong>Does this work with quantized models (4-bit weights)?</strong></p>
<p>Yes. The KV cache quantization is independent of weight quantization. You can run a 4-bit weight model with a 2-bit KV cache. The two compressions are additive: <code>mlx-community/Mistral-7B-Instruct-v0.3-4bit</code> loads as a 4-bit weight model; the KV cache is then further compressed by VeloxQuant.</p>
<p><strong>Will I notice quality degradation?</strong></p>
<p>At 4-bit and 3-bit: barely measurable on generation tasks. At 2-bit with RVQ: cosine similarity 0.98 (vs 1.0 for fp16). On our Qwen3 4B thinking-mode test, RVQ 2-bit produced 199/200 coherent tokens vs fp16's 200/200. One token difference across a 200-token reasoning chain.</p>
<p>At 2-bit without RVQ (single-pass): noticeable. Thinking-mode models collapse early. Use RVQ.</p>
<p><strong>What models are supported?</strong></p>
<p>Any model that uses standard multi-head attention with <code>mlx_lm.make_cache()</code>. Tested: Mistral 7B, Qwen3 4B, Qwen3 8B, Qwen2.5 32B, Falcon3 7B, Phi-4, Gemma-4. Not supported: architectures with multi-latent attention (DeepSeek-V2) or non-standard cache formats.</p>
<p><strong>Does it work with streaming generation?</strong></p>
<p>Yes. The cache is stateful — each <code>generate()</code> call updates it. Stream tokens with the standard <code>mlx_lm</code> streaming API; the cache update happens transparently inside the model forward pass.</p>
<hr>
<p><em>VeloxQuant-MLX is MIT licensed. Contributions welcome — especially benchmark results for new models and hardware.</em></p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="tutorial" term="tutorial"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="kv-cache" term="kv-cache"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[I Wrote a Metal Kernel to Stop My Mac From OOMing]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels"/>
        <updated>2026-05-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How a 30-line Metal compute shader replaced the worst hot path in VeloxQuant-MLX 0.5.1, what I learned about Apple Silicon kernel launch overhead, and why this matters if you run LLMs locally on Mac.]]></summary>
        <content type="html"><![CDATA[<p><em>How a 30-line Metal compute shader replaced the worst hot path in VeloxQuant-MLX 0.5.1, what I learned about Apple Silicon kernel launch overhead, and why this matters if you run LLMs locally on Mac.</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bug-that-wouldnt-die">The Bug That Wouldn't Die<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#the-bug-that-wouldnt-die" class="hash-link" aria-label="Direct link to The Bug That Wouldn't Die" title="Direct link to The Bug That Wouldn't Die" translate="no">​</a></h2>
<p>A few weeks back I shipped VeloxQuant-MLX 0.5.0 — a Python library that compresses the KV cache for any model you load through <code>mlx_lm</code>. The headline algorithm is <strong>VecInfer</strong>, which uses product vector quantization to squeeze keys down to 1 bit per element. That is <strong>16× compression</strong>. Sounds great.</p>
<p>It worked beautifully on Llama-3.1-8B, Mistral-7B, Qwen2.5-7B, Phi-4 — every model with <code>head_dim=128</code>. And then I tested Falcon3-7B.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">[VecInfer-2bit] generating...</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  Out of memory: requested 712 MB, available 0</span><br></div></code></pre></div></div>
<p>Falcon3-7B has <code>head_dim=256</code>. The chunked nearest-centroid search at the heart of <code>quantize_vq</code> allocates a tensor of shape <code>[chunk_size, n_centroids, sub_dim]</code> on every chunk. For Falcon's geometry that's a multi-hundred-megabyte intermediate — at every single token, on every layer, on every step. The GPU runs out of memory before generating a single token.</p>
<p>I shipped 0.5.0 with the OOM marked as a known limitation. It bothered me. I knew the fix conceptually — accumulate the squared distance in registers, never materialize the diff matrix — but doing that meant writing a Metal compute shader, and I had never written one.</p>
<p>This post is what happened when I did.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-even-is-a-kv-cache-and-why-should-you-care">What Even Is a KV Cache And Why Should You Care<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#what-even-is-a-kv-cache-and-why-should-you-care" class="hash-link" aria-label="Direct link to What Even Is a KV Cache And Why Should You Care" title="Direct link to What Even Is a KV Cache And Why Should You Care" translate="no">​</a></h2>
<p>Quick recap. Every transformer layer needs to remember the keys and values it computed for every token it's already seen. For a 7B model with 32 layers, 8 KV heads, and head_dim=128, generating an 8,000-token response means storing:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">32 layers × 8 heads × 8000 tokens × 128 dims × 2 (K + V) × 2 bytes (fp16)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">≈ 1 GB</span><br></div></code></pre></div></div>
<p>On a 16 GB MacBook running the model weights (~5 GB at 4-bit) plus the OS and your app, that 1 GB is the difference between a fluent response and a hard crash. <strong>The KV cache is the silent killer of long-context inference on Mac.</strong></p>
<p>KV-cache <em>quantization</em> — storing those keys and values at fewer bits — is the answer. There are several flavors. The aggressive one I shipped, VecInfer, uses <strong>product vector quantization</strong>:</p>
<ol>
<li class="">Split each <code>[head_dim]</code> key vector into small sub-vectors of length <code>sub_dim</code> (typically 4 or 8).</li>
<li class="">Pre-train a codebook of K-means centroids on calibration data.</li>
<li class="">At inference, encode each sub-vector as the index of its nearest centroid.</li>
</ol>
<p>A 128-dim fp16 key (256 bytes) becomes 16 indices at 8 bits each (16 bytes). That's the 16× compression.</p>
<p>The hot operation is step 3: finding the nearest centroid. On every layer, on every token, you do a vectorized argmin against the codebook. That's <code>quantize_vq</code>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-quantize_vq-was-doing-wrong">What <code>quantize_vq</code> Was Doing Wrong<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#what-quantize_vq-was-doing-wrong" class="hash-link" aria-label="Direct link to what-quantize_vq-was-doing-wrong" title="Direct link to what-quantize_vq-was-doing-wrong" translate="no">​</a></h2>
<p>Here's what the pure-MLX implementation looks like (paraphrased):</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">quantize_vq</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> codebook</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> sub_dim</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># x: [N, sub_dim]    -- the sub-vectors to encode</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)"># codebook: [n_centroids, sub_dim]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    diff </span><span class="token operator">=</span><span class="token plain"> x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">-</span><span class="token plain"> codebook</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># [N, n_centroids, sub_dim]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    d2 </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token builtin" style="color:rgb(189, 147, 249)">sum</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">diff </span><span class="token operator">*</span><span class="token plain"> diff</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis</span><span class="token operator">=</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">             </span><span class="token comment" style="color:rgb(98, 114, 164)"># [N, n_centroids]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">argmin</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">d2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> axis</span><span class="token operator">=</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">                 </span><span class="token comment" style="color:rgb(98, 114, 164)"># [N]</span><br></div></code></pre></div></div>
<p>That <code>diff</code> tensor is the killer. Its shape is <code>[N, n_centroids, sub_dim]</code>. For Falcon3-7B-shape inputs:</p>
<ul>
<li class=""><code>N = 4096 tokens × 4 KV heads × 64 sub-vectors per head = 1,048,576</code></li>
<li class=""><code>n_centroids = 256</code></li>
<li class=""><code>sub_dim = 4</code></li>
<li class="">Total: 1,048,576 × 256 × 4 × 2 bytes (fp16) = <strong>2.1 GB intermediate</strong></li>
</ul>
<p>The implementation tries to mitigate this by chunking N — processing 4,096 sub-vectors at a time — but even one chunk is still ~32 MB, and a 7B model's GPU memory pressure means even that gets fragmented and OOMs in practice.</p>
<p>What you actually want is for each thread to compute the argmin <strong>in registers</strong>, only writing out a single uint32 index. No intermediate tensor. Total intermediate memory: zero.</p>
<p>That's exactly what a Metal compute kernel can do.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-mlx-mxfastmetal_kernel">What Is MLX <code>mx.fast.metal_kernel</code>?<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#what-is-mlx-mxfastmetal_kernel" class="hash-link" aria-label="Direct link to what-is-mlx-mxfastmetal_kernel" title="Direct link to what-is-mlx-mxfastmetal_kernel" translate="no">​</a></h2>
<p>MLX (Apple's array library for Apple Silicon) has a feature most people don't know about: <code>mx.fast.metal_kernel</code>. It lets you write a Metal Shading Language function inline as a Python string and have MLX JIT-compile it, manage the buffer bindings, and dispatch it on the GPU.</p>
<p>The whole thing takes a few lines of Python:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">kernel </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"vecinfer_quantize"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    input_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"x"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"codebook"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"out"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    source</span><span class="token operator">=</span><span class="token plain">METAL_SOURCE</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># a string of MSL</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">result </span><span class="token operator">=</span><span class="token plain"> kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    inputs</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> codebook</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_shapes</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">N</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_dtypes</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">uint32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">N</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">256</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>MLX handles all the boilerplate: function signature generation, dtype binding, threadgroup memory, dispatch encoding. You write the kernel body. It's the easiest GPU programming experience I've ever had — closer to writing a Python function than to traditional CUDA.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-kernel-18-lines-of-msl">The Kernel: 18 Lines of MSL<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#the-kernel-18-lines-of-msl" class="hash-link" aria-label="Direct link to The Kernel: 18 Lines of MSL" title="Direct link to The Kernel: 18 Lines of MSL" translate="no">​</a></h2>
<p>Here's the entire fused-argmin kernel that replaces that 2 GB intermediate tensor:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint vec_idx = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint N_total = x_shape[0];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (vec_idx &gt;= N_total) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    return;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint n_centroids = codebook_shape[0];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint sub_dim     = codebook_shape[1];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint x_base      = vec_idx * sub_dim;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// Track running argmin in registers — never materialize the diff matrix.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float best_dist = INFINITY;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint  best_idx  = 0;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint c = 0; c &lt; n_centroids; ++c) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint cb_base = c * sub_dim;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float dist = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = 0; i &lt; sub_dim; ++i) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        float d = float(x[x_base + i]) - float(codebook[cb_base + i]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        dist += d * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    if (dist &lt; best_dist) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        best_dist = dist;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        best_idx  = c;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">out[vec_idx] = best_idx;</span><br></div></code></pre></div></div>
<p>That's it. Each GPU thread handles one sub-vector. It loops over all centroids, accumulates squared distance in a single float register, tracks the running minimum, and writes one uint32 index. The intermediate "diff matrix" never exists anywhere except in those two register-resident floats per thread.</p>
<p>Memory complexity: <code>O(N)</code> total output, vs <code>O(N × n_centroids × sub_dim)</code> for the Python path.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-numbers">The Numbers<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#the-numbers" class="hash-link" aria-label="Direct link to The Numbers" title="Direct link to The Numbers" translate="no">​</a></h2>
<p>I wrote a benchmark script — <code>scripts/plot_metal_benchmarks.py</code> in the repo — that runs both paths across realistic shapes and saves figures. Here are the results.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="throughput-69147-speedup">Throughput: 6.9–14.7× Speedup<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#throughput-69147-speedup" class="hash-link" aria-label="Direct link to Throughput: 6.9–14.7× Speedup" title="Direct link to Throughput: 6.9–14.7× Speedup" translate="no">​</a></h3>
<table><thead><tr><th>Shape</th><th style="text-align:right">pure-MLX</th><th style="text-align:right">Metal</th><th style="text-align:right">Speedup</th></tr></thead><tbody><tr><td>S=128, D=128</td><td style="text-align:right">3.64 ms</td><td style="text-align:right">0.53 ms</td><td style="text-align:right"><strong>6.9×</strong></td></tr><tr><td>S=512, D=128</td><td style="text-align:right">13.5 ms</td><td style="text-align:right">1.26 ms</td><td style="text-align:right"><strong>10.7×</strong></td></tr><tr><td>S=2048, D=128</td><td style="text-align:right">55.1 ms</td><td style="text-align:right">4.18 ms</td><td style="text-align:right"><strong>13.2×</strong></td></tr><tr><td>S=8192, D=128</td><td style="text-align:right">228.6 ms</td><td style="text-align:right">15.6 ms</td><td style="text-align:right"><strong>14.7×</strong></td></tr><tr><td>S=1024, D=256</td><td style="text-align:right">27.0 ms</td><td style="text-align:right">2.23 ms</td><td style="text-align:right"><strong>12.1×</strong></td></tr><tr><td>S=4096, D=256</td><td style="text-align:right">108.8 ms</td><td style="text-align:right">7.98 ms</td><td style="text-align:right"><strong>13.6×</strong></td></tr></tbody></table>
<p>The speedup scales with sequence length — longer contexts (where the Python path is bandwidth-bound on those huge diff tensors) get bigger wins. At <code>S=8192, D=128</code> we go from 228 ms per call to 16 ms per call. Per call. Multiply by 32 layers × 1 quantize per step × hundreds of tokens and you're talking minutes saved per long generation.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="memory-729-mb--12-mb">Memory: 729 MB → 12 MB<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#memory-729-mb--12-mb" class="hash-link" aria-label="Direct link to Memory: 729 MB → 12 MB" title="Direct link to Memory: 729 MB → 12 MB" translate="no">​</a></h3>
<p>At the Falcon3-7B OOM trigger shape (<code>head_dim=256, n_centroids=256, sub_dim=4, S=4096</code>):</p>
<table><thead><tr><th>Path</th><th style="text-align:right">Peak memory</th></tr></thead><tbody><tr><td>Pure-MLX <code>quantize_vq</code></td><td style="text-align:right"><strong>729.3 MB</strong></td></tr><tr><td>Metal <code>vecinfer_quantize_metal</code></td><td style="text-align:right"><strong>12.0 MB</strong></td></tr><tr><td>Reduction</td><td style="text-align:right"><strong>98.4%</strong> (saved 717 MB)</td></tr></tbody></table>
<p>This is the result that matters. The kernel doesn't just make existing models faster — it makes models that previously OOMed actually run.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="correctness-bit-exact-on-fp32-mse-identical-on-fp16">Correctness: Bit-Exact on fp32, MSE-Identical on fp16<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#correctness-bit-exact-on-fp32-mse-identical-on-fp16" class="hash-link" aria-label="Direct link to Correctness: Bit-Exact on fp32, MSE-Identical on fp16" title="Direct link to Correctness: Bit-Exact on fp32, MSE-Identical on fp16" translate="no">​</a></h3>
<p>This is where I had to be careful. The Metal kernel and the pure-MLX path don't produce identical indices on fp16 inputs — about <strong>0.1% of indices differ</strong>.</p>
<p>Why? When two centroids are nearly equidistant from a point, the choice of "nearest" depends on the order of floating-point operations. The pure-MLX path does the subtract in fp16 (because the inputs are fp16); the Metal kernel promotes to fp32 inside the inner loop. When the tiebreaker happens at the 5th decimal place, the two paths pick different winners.</p>
<p>But here's the thing: <strong>the reconstruction quality is identical</strong>. I validated this by reconstructing keys from both index sets and measuring MSE against the original input:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">B=1 H=8 S=2048 D=128 sub_dim=8 n_c=256 dtype=float16</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  idx_diff = 0.104%</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  mse_ref = 3.7211e-01    mse_metal = 3.7211e-01</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  rel_err = 5.61e-07</span><br></div></code></pre></div></div>
<p>Reconstruction MSE matches to <strong>7 decimal places</strong>. The two paths produce functionally identical compressed representations — they just disagree on which arbitrary tie-breaker to pick.</p>
<p>The parity tests in <code>veloxquant_mlx/tests/cache/test_vecinfer_metal_parity.py</code> validate this directly: assert that reconstruction MSE is within 1% relative error, not that indices match.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-got-wrong-on-the-first-try">What I Got Wrong on the First Try<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#what-i-got-wrong-on-the-first-try" class="hash-link" aria-label="Direct link to What I Got Wrong on the First Try" title="Direct link to What I Got Wrong on the First Try" translate="no">​</a></h2>
<p>I want to be honest about the missteps, because they're the actually interesting part.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mistake-1-i-wrote-the-dequant-kernel-first">Mistake 1: I Wrote the Dequant Kernel First<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#mistake-1-i-wrote-the-dequant-kernel-first" class="hash-link" aria-label="Direct link to Mistake 1: I Wrote the Dequant Kernel First" title="Direct link to Mistake 1: I Wrote the Dequant Kernel First" translate="no">​</a></h3>
<p>My first instinct was to write a Metal kernel for <code>dequantize_vq</code> — the operation that takes codebook indices and reconstructs the float vectors. It's conceptually simpler (just a gather), so I started there.</p>
<p>After getting bit-exact correctness, I benchmarked it:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">shape                                pure-mlx     metal    speedup</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">B=1 H=8 S=128 n_sub=16 sub_dim=8       223.3 µs   185.6 µs   1.20x</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">B=1 H=8 S=512                          183.6 µs   209.3 µs   0.88x</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">B=1 H=8 S=2048                         258.3 µs   275.9 µs   0.94x</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">B=1 H=8 S=8192                         467.8 µs   577.6 µs   0.81x</span><br></div></code></pre></div></div>
<p><strong>My kernel was slower than MLX's <code>mx.take</code>.</strong> That stung. After staring at the numbers for an hour, the reason became obvious: MLX's <code>mx.take</code> is already a highly tuned Metal gather kernel under the hood. There is no "Python overhead" to eliminate. The pure-MLX path <em>is</em> a Metal kernel. My kernel was duplicating it badly.</p>
<p><strong>The lesson:</strong> before writing a custom kernel, profile to find the operation that has actual Python/intermediate-tensor overhead. <code>mx.take</code> does not. <code>quantize_vq</code> does, because of the <code>[N, n_centroids, sub_dim]</code> materialization. The 30-line MSL shader had to fuse an <em>algorithm</em> — argmin over distances — not just replace a builtin.</p>
<p>I kept the dequant kernel as a building block for Phase 2 (fused dequant+SDPA), but the headline result is the quantize kernel.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mistake-2-wrong-threadgroup-layout">Mistake 2: Wrong Threadgroup Layout<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#mistake-2-wrong-threadgroup-layout" class="hash-link" aria-label="Direct link to Mistake 2: Wrong Threadgroup Layout" title="Direct link to Mistake 2: Wrong Threadgroup Layout" translate="no">​</a></h3>
<p>My first quantize kernel dispatched <strong>one thread per (input_vector, sub_dim_component)</strong> pair. That made each thread tiny — one subtract, one square, one accumulate — and meant launching <code>N × sub_dim</code> threads. For typical shapes, that's millions of threads.</p>
<p>Apple Silicon GPUs have 32-wide SIMD groups and an internal cost per thread launch. Launching 8× more threads than you need is pure overhead.</p>
<p>The fix was to dispatch <strong>one thread per input vector</strong> and let each thread loop over all sub_dim components in registers. Same total work, 8× fewer thread launches, much better register reuse. That's the layout in the kernel above.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mistake-3-i-assumed-end-to-end-would-always-be-faster">Mistake 3: I Assumed End-to-End Would Always Be Faster<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#mistake-3-i-assumed-end-to-end-would-always-be-faster" class="hash-link" aria-label="Direct link to Mistake 3: I Assumed End-to-End Would Always Be Faster" title="Direct link to Mistake 3: I Assumed End-to-End Would Always Be Faster" translate="no">​</a></h3>
<p>After validating the kernel was 13× faster on synthetic shapes, I ran the full benchmark on SmolLM2-135M (a 135-million-parameter tiny model) expecting to see a speedup in end-to-end token generation.</p>
<p>I got the opposite. The Metal path was <strong>slower</strong> end-to-end — 75 tok/s vs 178 tok/s for the pure-MLX path.</p>
<p>The reason: Metal kernel dispatch has a fixed per-launch overhead of roughly 50–200 µs on Apple Silicon. SmolLM2 has 30 layers, each doing 2 quantize calls per token, so that's ~60 kernel launches per generated token. The per-launch overhead exceeded the work each kernel did.</p>
<p><strong>The kernel is designed for the regime where it matters: 7B+ models with realistic context lengths, where each <code>quantize_vq</code> call is doing milliseconds of work.</strong> On those, the launch overhead is negligible relative to the kernel runtime, and you get the full 10–14× speedup.</p>
<p>This is a limitation of MLX's kernel launch path — MLX doesn't yet expose a way to amortize launch overhead across multiple layers in a single dispatch. That's a Phase 3 problem and probably out of scope for a Python-level library.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-use-this-today">How to Use This Today<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#how-to-use-this-today" class="hash-link" aria-label="Direct link to How to Use This Today" title="Direct link to How to Use This Today" translate="no">​</a></h2>
<p>VeloxQuant-MLX 0.5.1 is on PyPI. Install:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--upgrade</span><span class="token plain"> VeloxQuant-MLX</span><br></div></code></pre></div></div>
<p>The Metal kernels are <strong>on by default</strong> when available. No code changes needed. Your existing <code>VecInferKVCache</code> calls auto-detect Metal and use the fast path:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheFactory</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Falcon3-7B-Instruct-4bit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Metal auto-detected. To force off for debugging: use_metal_kernels=False</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    method</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"vecinfer"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    head_dim</span><span class="token operator">=</span><span class="token number">256</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    key_sub_dim</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    value_sub_dim</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    key_codebook_bits</span><span class="token operator">=</span><span class="token number">8</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    value_codebook_bits</span><span class="token operator">=</span><span class="token number">8</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    smooth_factors</span><span class="token operator">=</span><span class="token plain">calibrated_smooth_factors</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    key_codebook</span><span class="token operator">=</span><span class="token plain">calibrated_key_codebook</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    use_metal_kernels</span><span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># None = auto, True = require, False = forbid</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The new <code>use_metal_kernels</code> flag is three-state:</p>
<ul>
<li class=""><code>None</code> (default) — auto-detect; use Metal if available, silently fall back if not</li>
<li class=""><code>True</code> — require Metal; raise at construction time if unavailable</li>
<li class=""><code>False</code> — forbid Metal; use the pure-MLX path (for parity testing and debugging)</li>
</ul>
<p>To verify the speedup on your own machine:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token function" style="color:rgb(80, 250, 123)">git</span><span class="token plain"> clone https://github.com/rajveer43/VeloxQuant-MLX</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token builtin class-name" style="color:rgb(189, 147, 249)">cd</span><span class="token plain"> VeloxQuant-MLX</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token assign-left variable" style="color:rgb(189, 147, 249);font-style:italic">PYTHONPATH</span><span class="token operator">=</span><span class="token plain">. python scripts/plot_metal_benchmarks.py</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Produces figures/metal/summary.png with your hardware's numbers</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next-phase-2">What's Next: Phase 2<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#whats-next-phase-2" class="hash-link" aria-label="Direct link to What's Next: Phase 2" title="Direct link to What's Next: Phase 2" translate="no">​</a></h2>
<p>The quantize kernel is the biggest single win, but it's not the end. <strong>Phase 2 is fusing dequantize + scaled-dot-product-attention</strong> into a single kernel.</p>
<p>Right now, even with Phase 1, the cache still materializes the full fp16 key tensor on every attention call. The dequant happens — efficiently, since <code>mx.take</code> is already fast — but we hold the result in GPU memory long enough to pass it to MLX's SDPA. For very long contexts, that materialized key tensor is still significant memory pressure.</p>
<p>The Phase 2 kernel would:</p>
<ol>
<li class="">Take codebook indices, the per-query LUT (<code>q_tilde @ codebook.T</code>), and value indices</li>
<li class="">Compute attention scores directly via LUT lookup, never materializing fp16 keys</li>
<li class="">Compute the softmax-weighted value sum in-kernel</li>
<li class="">Output the final attention result in one fused pass</li>
</ol>
<p>This is what the VecInfer paper's CUDA kernel does. Porting it to Metal is the goal. If you've written Metal compute shaders before and want to collaborate, the GitHub issue is open.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-meta-lesson-custom-kernels-are-more-accessible-than-you-think">The Meta-Lesson: Custom Kernels Are More Accessible Than You Think<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#the-meta-lesson-custom-kernels-are-more-accessible-than-you-think" class="hash-link" aria-label="Direct link to The Meta-Lesson: Custom Kernels Are More Accessible Than You Think" title="Direct link to The Meta-Lesson: Custom Kernels Are More Accessible Than You Think" translate="no">​</a></h2>
<p>I had never written a Metal shader before this project. The mental model is straightforward once you get past the syntax:</p>
<ol>
<li class=""><strong>Identify the operation with materialization overhead</strong> (not just a slow Python loop — those are usually wrapped in optimized C++ already; look for operations that create big intermediate tensors)</li>
<li class=""><strong>Write the algorithm with the intermediate as register-state instead of memory-state</strong> (running min, running sum, running argmin)</li>
<li class=""><strong>Dispatch one thread per output element</strong>, not per input or per output-component</li>
<li class=""><strong>Validate with reconstruction error</strong>, not bit-exact equality, when fp16 is involved</li>
<li class=""><strong>Benchmark at realistic shapes</strong>, not toy shapes — kernel launch overhead can dominate for small workloads</li>
</ol>
<p>Total time investment for this Phase 1: about 6 hours of focused work, including the two failed approaches above. The resulting kernel unblocks <code>head_dim=256</code> models that previously OOMed, gives a 10–14× speedup on the hot path, and is 30 lines of MSL.</p>
<p>If you've been hesitant to write custom GPU kernels because it sounds intimidating — <code>mx.fast.metal_kernel</code> makes the bar way lower than it used to be on CUDA. Try it.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<ul>
<li class="">VeloxQuant-MLX 0.5.1 adds a Metal compute kernel for <code>quantize_vq</code>, the hot path in VecInfer KV-cache compression</li>
<li class=""><strong>13× faster</strong> on realistic shapes (S=2048+)</li>
<li class=""><strong>98% less peak memory</strong> at the Falcon3-7B OOM trigger configuration</li>
<li class=""><strong>Drop-in, zero API change</strong> — auto-detected when Metal is available</li>
<li class="">Free, MIT-licensed, on PyPI: <code>pip install VeloxQuant-MLX</code></li>
<li class="">The kernel is 30 lines of Metal Shading Language inside Python</li>
<li class="">Phase 2 (fused dequant+SDPA attention kernel) is next</li>
</ul>
<p>GitHub: <a href="https://github.com/rajveer43/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">github.com/rajveer43/VeloxQuant-MLX</a>
PyPI: <a href="https://pypi.org/project/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">pypi.org/project/VeloxQuant-MLX</a>
Benchmark figures: <a href="https://github.com/rajveer43/VeloxQuant-MLX/blob/master/figures/metal/summary.png" target="_blank" rel="noopener noreferrer" class=""><code>figures/metal/summary.png</code></a> in the repo</p>
<p>If this saves your Mac from OOMing tonight, leave a star — or open an issue if it doesn't.</p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="metal" term="metal"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="gpu" term="gpu"/>
        <category label="performance" term="performance"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[TurboQuant + Metal Kernels: The Combined Writeup]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels"/>
        <updated>2026-05-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How I wrote five hand-tuned Metal compute kernels in MLX for TurboQuant — and what every bug taught me about Apple GPU programming.]]></summary>
        <content type="html"><![CDATA[<p><em>How I wrote five hand-tuned Metal compute kernels in MLX for TurboQuant — and what every bug taught me about Apple GPU programming.</em></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The Problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem" class="hash-link" aria-label="Direct link to The Problem" title="Direct link to The Problem" translate="no">​</a></h2>
<p>My Mac was choking on long-context LLM inference.</p>
<p>Not because the model was too large — I had already quantized the weights. The bottleneck was the <strong>KV cache</strong>. At 8k context, a single layer's key cache is <code>[1, 32, 8192, 128]</code> in fp16 — over 67 MB per layer, 2 GB across 32 layers. On Apple Silicon, where the GPU and CPU share the same physical memory, that pressure is immediate and painful.</p>
<p>VeloxQuant-MLX already had several compression algorithms: TurboQuantRVQ (7.5× via two-stage scalar RVQ), QJL (16× via 1-bit Johnson-Lindenstrauss sketching), and VecInfer (16× via product VQ). But they were all running through pure MLX graph operations — no custom GPU kernels. The hot paths were either slow or allocating huge intermediate tensors.</p>
<p>The fix: write the hot paths in <strong>Metal Shading Language</strong> and JIT-compile them via <code>mx.fast.metal_kernel</code>.</p>
<p>This is the story of how I did it, what broke, and what I learned.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-stack">The Stack<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-stack" class="hash-link" aria-label="Direct link to The Stack" title="Direct link to The Stack" translate="no">​</a></h2>
<p>Before diving into the kernels, here's the relevant context:</p>
<ul>
<li class=""><strong>MLX</strong> — Apple's NumPy-style ML framework with lazy evaluation and Metal GPU backend</li>
<li class=""><strong><code>mx.fast.metal_kernel</code></strong> — Python API to write raw Metal Shading Language compute shaders that plug into MLX's lazy graph</li>
<li class=""><strong>TurboQuant</strong> — a family of KV cache quantization algorithms (MSE, Prod, RVQ) implemented in VeloxQuant-MLX</li>
<li class=""><strong>QJL</strong> — Quantized Johnson-Lindenstrauss: compress keys to 1-bit sign sketches + a scalar norm</li>
</ul>
<p>The goal was to replace the slowest pure-MLX operations with Metal kernels that live in five focused submodules:</p>
<table><thead><tr><th>Submodule</th><th>What it does</th></tr></thead><tbody><tr><td><code>_bit_packing.py</code></td><td>Pack/unpack b-bit indices into uint8 bytes</td></tr><tr><td><code>_scalar_quant.py</code></td><td>Nearest-centroid quantize, dequantize, fused Hadamard+quant</td></tr><tr><td><code>_qjl.py</code></td><td>QJL sign encode and inner product scoring</td></tr><tr><td><code>_rvq_attend.py</code></td><td>Fused RVQ key decode + FlashAttention-style online softmax</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-mxfastmetal_kernel-works">How <code>mx.fast.metal_kernel</code> Works<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#how-mxfastmetal_kernel-works" class="hash-link" aria-label="Direct link to how-mxfastmetal_kernel-works" title="Direct link to how-mxfastmetal_kernel-works" translate="no">​</a></h2>
<p>Before showing any kernel code, there's one thing you need to understand about the API — because getting it wrong produces silent, subtle bugs.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-api-in-30-seconds">The API in 30 seconds<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-api-in-30-seconds" class="hash-link" aria-label="Direct link to The API in 30 seconds" title="Direct link to The API in 30 seconds" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">core </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> mx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">kernel </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    name</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"my_kernel"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    input_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"x"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"y"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"out"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    source</span><span class="token operator">=</span><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">"""</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">        uint i = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">        out[i] = x[i] + y[i];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token triple-quoted-string string" style="color:rgb(255, 121, 198)">    """</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">result </span><span class="token operator">=</span><span class="token plain"> kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    inputs</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">N</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">256</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_shapes</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">N</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    output_dtypes</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">float32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>The <code>source</code> string is raw Metal Shading Language — no <code>kernel</code> keyword, no function signature. MLX wraps it. Shape information is injected automatically: inside the kernel, <code>x_shape[0]</code> gives you the first dimension of <code>x</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-1-gotcha-grid--total-threads">The #1 Gotcha: Grid = Total Threads<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-1-gotcha-grid--total-threads" class="hash-link" aria-label="Direct link to The #1 Gotcha: Grid = Total Threads" title="Direct link to The #1 Gotcha: Grid = Total Threads" translate="no">​</a></h3>
<p>This is the single most important thing to get right, and the MLX documentation is easy to misread on this point.</p>
<p>In standard Metal (Obj-C / Swift), you call <code>dispatchThreadgroups(n_groups, threadsPerThreadgroup: tg_size)</code> — so the grid is in <em>threadgroup</em> units.</p>
<p><strong>MLX uses <code>dispatchThreads</code> — the grid is in <em>total thread</em> units.</strong></p>
<p>That means if you want B threadgroups of T threads each:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># WRONG — only dispatches 1 thread per threadgroup</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">B</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">T</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># CORRECT — dispatches B threadgroups of T threads each</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">B </span><span class="token operator">*</span><span class="token plain"> T</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">T</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>I made this mistake on four out of five kernels. The symptom was identical every time: <strong>only the first batch element had correct output; everything else was zero</strong>. It looked like a memory layout bug or an indexing error, not a dispatch error. I spent hours debugging before I found it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-lazy-graph-contract">The Lazy Graph Contract<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-lazy-graph-contract" class="hash-link" aria-label="Direct link to The Lazy Graph Contract" title="Direct link to The Lazy Graph Contract" translate="no">​</a></h3>
<p><code>mx.fast.metal_kernel</code> returns a lazy node — nothing runs until <code>mx.eval()</code> is called. <code>mx.eval()</code> internally:</p>
<ol>
<li class="">Encodes the compute command into a <code>MTLCommandBuffer</code></li>
<li class="">Calls <code>commandBuffer.commit()</code> to submit to the GPU</li>
<li class="">Calls <code>commandBuffer.waitUntilCompleted()</code> to synchronize</li>
</ol>
<p>You never write any of this yourself. MLX owns the entire Metal command buffer lifecycle.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kernel-1-bit-packing--30-over-numpy">Kernel 1: Bit-Packing — 30× Over NumPy<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#kernel-1-bit-packing--30-over-numpy" class="hash-link" aria-label="Direct link to Kernel 1: Bit-Packing — 30× Over NumPy" title="Direct link to Kernel 1: Bit-Packing — 30× Over NumPy" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-1">The problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem-1" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h3>
<p>TurboQuantRVQ stores KV cache keys as b-bit indices (b ∈ {1, 2, 4}). The pure-Python path used a loop to pack these into uint8 bytes. At 65k elements it was ~8 ms — unacceptable.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-kernel">The kernel<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-kernel" class="hash-link" aria-label="Direct link to The kernel" title="Direct link to The kernel" translate="no">​</a></h3>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">constexpr int  ELEMS_PER_BYTE = 8 / B_BITS;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">constexpr uint MASK           = (1u &lt;&lt; B_BITS) - 1u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint byte_idx = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint base     = byte_idx * ELEMS_PER_BYTE;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint packed_byte = 0u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (int i = 0; i &lt; ELEMS_PER_BYTE; ++i) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint val = uint(indices[base + i]) &amp; MASK;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    packed_byte |= (val &lt;&lt; (i * B_BITS));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">packed[byte_idx] = uint8_t(packed_byte);</span><br></div></code></pre></div></div>
<p>One thread per output byte. <code>B_BITS</code> is a <strong>template parameter</strong> — a compile-time integer constant. This lets the compiler statically unroll the inner loop (2 iterations for b=4, 4 for b=2, 8 for b=1) and inline the constants.</p>
<p>The dispatch:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">n_bytes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token builtin" style="color:rgb(189, 147, 249)">min</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">256</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> n_bytes</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h3>
<table><thead><tr><th>N</th><th>NumPy</th><th>Metal</th><th>Speedup</th></tr></thead><tbody><tr><td>4,096</td><td>0.52 ms</td><td>0.18 ms</td><td>2.9×</td></tr><tr><td>16,384</td><td>2.1 ms</td><td>0.17 ms</td><td>12.5×</td></tr><tr><td>65,536</td><td>8.4 ms</td><td>0.28 ms</td><td><strong>29.5×</strong></td></tr></tbody></table>
<p>The kernel dispatch overhead is ~0.17 ms regardless of N. Below ~2k elements NumPy wins because there's nothing to hide the launch cost behind. Above 16k elements, Metal dominates by an order of magnitude.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kernel-2-scalar-quantize--dequantize--11-over-numpy">Kernel 2: Scalar Quantize / Dequantize — 11× Over NumPy<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#kernel-2-scalar-quantize--dequantize--11-over-numpy" class="hash-link" aria-label="Direct link to Kernel 2: Scalar Quantize / Dequantize — 11× Over NumPy" title="Direct link to Kernel 2: Scalar Quantize / Dequantize — 11× Over NumPy" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-2">The problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem-2" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h3>
<p>TurboQuantMSE quantizes each key dimension independently against a Lloyd-Max codebook. The pure-MLX path computed <code>|x - centroids|²</code> as a full <code>[N, 2^b]</code> matrix, then took <code>argmin</code> — allocating a tensor that was <code>2^b</code> times the input size.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quantize-kernel">The quantize kernel<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-quantize-kernel" class="hash-link" aria-label="Direct link to The quantize kernel" title="Direct link to The quantize kernel" translate="no">​</a></h3>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">constexpr int N_CENTS = 1 &lt;&lt; B_BITS;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint  elem      = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float val       = float(x[elem]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">int   best      = 0;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float best_dist = INFINITY;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (int j = 0; j &lt; N_CENTS; ++j) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float d    = val - centroids[j];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float dist = d * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    if (dist &lt; best_dist) { best_dist = dist; best = j; }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">indices[elem] = uint8_t(best);</span><br></div></code></pre></div></div>
<p>One thread per element. The centroid scan lives entirely in registers — no intermediate allocation. With <code>B_BITS</code> as a template, the loop body is known at compile time: the compiler generates 2, 4, 8, or 16 iterations of straight-line code.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dequantize-kernel">The dequantize kernel<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-dequantize-kernel" class="hash-link" aria-label="Direct link to The dequantize kernel" title="Direct link to The dequantize kernel" translate="no">​</a></h3>
<p>Even simpler — a pure gather:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint elem   = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">x_hat[elem] = half(centroids[uint(indices[elem])]);</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="results-1">Results<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#results-1" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h3>
<table><thead><tr><th>N</th><th>NumPy argmin</th><th>Metal</th><th>Speedup</th></tr></thead><tbody><tr><td>16,384</td><td>0.21 ms</td><td>0.17 ms</td><td>1.2×</td></tr><tr><td>65,536</td><td>0.86 ms</td><td>0.19 ms</td><td>4.5×</td></tr><tr><td>262,144</td><td>3.5 ms</td><td>0.31 ms</td><td><strong>11.3×</strong></td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kernel-3-fused-hadamard--quantize--the-hardest-one">Kernel 3: Fused Hadamard + Quantize — The Hardest One<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#kernel-3-fused-hadamard--quantize--the-hardest-one" class="hash-link" aria-label="Direct link to Kernel 3: Fused Hadamard + Quantize — The Hardest One" title="Direct link to Kernel 3: Fused Hadamard + Quantize — The Hardest One" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-3">The problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem-3" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h3>
<p>TurboQuantMSE (with Hadamard preconditioner) runs:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">y = diag * H * x / sqrt(D)    [randomized Hadamard rotation]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">idx = argmin_k |y - c_k|²     [nearest-centroid quantize]</span><br></div></code></pre></div></div>
<p>Two separate dispatches, with a <code>[B, D]</code> fp16 intermediate between them. Fusing them into one kernel eliminates that allocation and the round-trip to GPU memory.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-kernel-design">The kernel design<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-kernel-design" class="hash-link" aria-label="Direct link to The kernel design" title="Direct link to The kernel design" translate="no">​</a></h3>
<p>Walsh-Hadamard Transform (WHT) is an in-place butterfly — each pass halves the stride. On GPU, D threads share a threadgroup, and each butterfly step needs a barrier.</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">threadgroup float buf[MAX_D];   // static threadgroup memory; MAX_D injected at compile time</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// 1. Load + diagonal sign flip</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float v = float(x[tg * D + lane]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">v *= float(diag[lane]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">buf[lane] = v;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">threadgroup_barrier(mem_flags::mem_threadgroup);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// 2. In-place WHT — range-based parallel butterfly</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint stride = 1; stride &lt; D; stride &lt;&lt;= 1) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint local    = lane % (stride &lt;&lt; 1u);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    bool is_upper = local &gt;= stride;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint partner  = is_upper ? (lane - stride) : (lane + stride);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float a = buf[lane];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float b = buf[partner];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    threadgroup_barrier(mem_flags::mem_threadgroup);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    buf[lane] = is_upper ? (b - a) : (a + b);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    threadgroup_barrier(mem_flags::mem_threadgroup);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// 3. Scale</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float y = buf[lane] * metal::rsqrt(float(D));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// 4. Nearest-centroid argmin (register-local)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">int   best      = 0;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float best_dist = INFINITY;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (int j = 0; j &lt; N_CENTS; ++j) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float d    = y - centroids[j];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float dist = d * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    if (dist &lt; best_dist) { best_dist = dist; best = j; }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">indices[tg * D + lane] = uint8_t(best);</span><br></div></code></pre></div></div>
<p>The threadgroup array <code>buf[MAX_D]</code> requires <code>MAX_D</code> to be a compile-time constant — which is why it's injected as a <code>#define</code> in the kernel header:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">_hadamard_quantize_kernel </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    header</span><span class="token operator">=</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"#define MAX_D </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">D</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">\n"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    source</span><span class="token operator">=</span><span class="token plain">_HADAMARD_QUANTIZE_SRC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-butterfly-bug">The butterfly bug<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-butterfly-bug" class="hash-link" aria-label="Direct link to The butterfly bug" title="Direct link to The butterfly bug" translate="no">​</a></h3>
<p>My first implementation used:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint partner = lane ^ stride;    // XOR butterfly</span><br></div></code></pre></div></div>
<p>This looked right — it's the standard Cooley-Tukey bit-reversal trick. But on GPU, it produced ~90% index mismatch vs the sequential reference.</p>
<p>The problem: <code>lane ^ stride</code> traverses the WHT in <strong>bit-reversal order</strong>, which is fine for sequential execution (because you can reorder the output at the end), but on GPU where lanes run simultaneously, XOR pairing creates <strong>data races</strong> within a butterfly pass — some lanes read values that other lanes in the same pass are simultaneously writing.</p>
<p>The fix is a <strong>range-based butterfly</strong> that unambiguously partitions each pass into non-overlapping upper/lower pairs:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint local    = lane % (stride &lt;&lt; 1u);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">bool is_upper = local &gt;= stride;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint partner  = is_upper ? (lane - stride) : (lane + stride);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float a = buf[lane];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float b = buf[partner];          // read BEFORE the barrier write below</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">threadgroup_barrier(mem_flags::mem_threadgroup);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">buf[lane] = is_upper ? (b - a) : (a + b);</span><br></div></code></pre></div></div>
<p>Reading <code>a</code> and <code>b</code> before the barrier guarantees both values come from the previous pass. After this fix, 100% of indices matched the reference.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="grid">Grid<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#grid" class="hash-link" aria-label="Direct link to Grid" title="Direct link to Grid" translate="no">​</a></h3>
<p>The grid uses B threadgroups of D threads — <strong>not</strong> B × D total:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># Wrong:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">B</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">D</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># only 1 thread per threadgroup!</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Correct:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">B </span><span class="token operator">*</span><span class="token plain"> D</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">D</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># B threadgroups of D threads</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kernel-4-qjl-encode--simdgroup-sign-packing">Kernel 4: QJL Encode — Simdgroup Sign Packing<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#kernel-4-qjl-encode--simdgroup-sign-packing" class="hash-link" aria-label="Direct link to Kernel 4: QJL Encode — Simdgroup Sign Packing" title="Direct link to Kernel 4: QJL Encode — Simdgroup Sign Packing" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-4">The problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem-4" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h3>
<p>QJL encoding requires:</p>
<ol>
<li class="">For each key vector <code>x[b]</code>, compute <code>sign(S @ x[b])</code> for all m sketch dimensions — giving m bits</li>
<li class="">Pack those m bits into m/8 uint8 bytes (LSB-first)</li>
<li class="">Compute <code>‖x[b]‖</code> (one scalar per key)</li>
</ol>
<p>The pure-MLX path materialized the full <code>[B, m]</code> float matrix <code>S @ x.T</code> before sign-taking — <code>m * d * B * 4</code> bytes, growing linearly with batch and sketch size.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="simdgroup-design">Simdgroup design<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#simdgroup-design" class="hash-link" aria-label="Direct link to Simdgroup design" title="Direct link to Simdgroup design" translate="no">​</a></h3>
<p>Each simdgroup (32 lanes) handles 32 consecutive sketch dimensions. Lane <code>j</code> computes <code>dot(S[simd_blk*32 + j, :], x[b, :])</code> via a scalar loop:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint b_idx    = flat_tg / n_simd_per_batch;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint simd_blk = flat_tg % n_simd_per_batch;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint sketch_j = simd_blk * 32u + lane;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float dot_val = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (sketch_j &lt; m) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint S_row = sketch_j * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint x_row = b_idx   * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = 0; i &lt; d; ++i) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        dot_val += float(S[S_row + i]) * float(x[x_row + i]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>After the dot product, all 32 lanes <strong>cooperate to pack 32 sign bits into 4 bytes</strong> using <code>simd_shuffle</code>:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint sign_bit    = (dot_val &gt;= 0.0f) ? 1u : 0u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint byte_in_blk = lane / 8u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint bit_in_byte = lane % 8u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint packed_byte = 0u;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint bit = 0; bit &lt; 8u; ++bit) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint src = byte_in_blk * 8u + bit;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    packed_byte |= (simd_shuffle(sign_bit, src) &lt;&lt; bit);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (bit_in_byte == 0 &amp;&amp; sketch_j &lt; m) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    packed_signs[out_byte] = uint8_t(packed_byte);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p><code>simd_shuffle(val, lane_id)</code> broadcasts <code>sign_bit</code> from lane <code>src</code> to the current lane — no shared memory needed. Lane 0 (of each byte group) does the final write.</p>
<p>The norm is computed cooperatively by simd_blk 0:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">if (simd_blk == 0) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float x_sq = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = lane; i &lt; d; i += 32u) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        float v = float(x[x_row + i]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        x_sq += v * v;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float norm_sq = simd_sum(x_sq);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    if (lane == 0) norms[b_idx] = half(metal::sqrt(norm_sq));</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="grid-the-bug-again">Grid (the bug, again)<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#grid-the-bug-again" class="hash-link" aria-label="Direct link to Grid (the bug, again)" title="Direct link to Grid (the bug, again)" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">n_simd_per_batch </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">m </span><span class="token operator">+</span><span class="token plain"> </span><span class="token number">31</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">//</span><span class="token plain"> </span><span class="token number">32</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">n_total_threads  </span><span class="token operator">=</span><span class="token plain"> B </span><span class="token operator">*</span><span class="token plain"> n_simd_per_batch </span><span class="token operator">*</span><span class="token plain"> </span><span class="token number">32</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># ← must multiply by 32</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">grid</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">n_total_threads</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> threadgroup</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">32</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>Without the <code>* 32</code>, only <code>B * n_simd_per_batch</code> total threads dispatched — meaning only the first simdgroup ran, and only the first key had any output.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kernel-5-fused-rvq-decode--attend--online-softmax-without-materializing-k">Kernel 5: Fused RVQ Decode + Attend — Online Softmax Without Materializing K<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#kernel-5-fused-rvq-decode--attend--online-softmax-without-materializing-k" class="hash-link" aria-label="Direct link to Kernel 5: Fused RVQ Decode + Attend — Online Softmax Without Materializing K" title="Direct link to Kernel 5: Fused RVQ Decode + Attend — Online Softmax Without Materializing K" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-5">The problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-problem-5" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h3>
<p>Attention with a quantized KV cache normally requires two dispatches:</p>
<ol>
<li class="">Decode all compressed keys → <code>K_hat</code> tensor <code>[B, H, S_kv, D]</code> (fp16, potentially GBs)</li>
<li class="">Run <code>softmax(q @ K_hat.T / sqrt(D)) @ V</code></li>
</ol>
<p>The <code>K_hat</code> tensor is allocated, filled, used once, and thrown away. For RVQ keys this is unavoidable in the two-dispatch design — but we can fuse everything into a single FlashAttention-style pass that decodes keys <strong>on the fly</strong> without ever materializing <code>K_hat</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="design">Design<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#design" class="hash-link" aria-label="Direct link to Design" title="Direct link to Design" translate="no">​</a></h3>
<p>Each threadgroup handles one query position <code>(b, h, sq)</code>. Lanes stripe across the D-dimensional vectors in steps of TG = min(D, 32):</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">float running_m = -INFINITY;   // online softmax running max</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float running_d = 0.0f;        // online softmax running denominator</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float my_out[8];               // per-lane output accumulator</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (int i = 0; i &lt; 8; ++i) my_out[i] = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint sk = 0; sk &lt; S_kv; ++sk) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // 1. Decode key on-the-fly: k[i] = cents1[idx1[i]] + cents2[idx2[i]]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float partial_dot = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = tg_lane; i &lt; D; i += TG) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        float ki = centroids1[uint(k_indices1[k_off])]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">                 + centroids2[uint(k_indices2[k_off])];</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        partial_dot += float(q[q_base + i]) * ki;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float score = simd_sum(partial_dot) * inv_sqrt_d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // 2. Online softmax update (Dao et al. FlashAttention)</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float m_new  = metal::max(running_m, score);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float factor = metal::exp(running_m - m_new);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float w      = metal::exp(score     - m_new);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    running_d    = running_d * factor + w;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    running_m    = m_new;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    // 3. Rescale and accumulate value</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = 0; i &lt; n_owned; ++i) my_out[i] *= factor;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = tg_lane; i &lt; D; i += TG) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        float vi    = float(v_codebook[cb_off]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        uint  out_i = (i - tg_lane) / TG;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        my_out[out_i] += w * vi;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">// 4. Normalize and write</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint i = tg_lane; i &lt; D; i += TG) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    uint out_i   = (i - tg_lane) / TG;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    out[out_off] = half(my_out[out_i] / running_d);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div></code></pre></div></div>
<p><code>simd_sum(partial_dot)</code> broadcasts the full dot product to all lanes in the simdgroup — this is the SIMD-level reduction that gives the correct score without any threadgroup memory.</p>
<p>The local accumulator index <code>out_i = (i - tg_lane) / TG</code> is the critical piece: lane 0 owns dims {0, TG, 2×TG, ...}, lane 1 owns {1, TG+1, ...}, and <code>out_i</code> is the position within that lane's private array.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-benchmarks">The Benchmarks<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-benchmarks" class="hash-link" aria-label="Direct link to The Benchmarks" title="Direct link to The Benchmarks" translate="no">​</a></h2>
<p>After fixing all the dispatch bugs, here are the results on Apple M-series (figures saved to <code>figures/metal/turboquant_kernels/</code>):</p>
<table><thead><tr><th>Kernel</th><th>Peak speedup vs NumPy</th><th>Notes</th></tr></thead><tbody><tr><td><code>turboquant_bit_pack</code> (b=4, N=65k)</td><td><strong>29.5×</strong></td><td>NumPy loop vs Metal one-thread-per-byte</td></tr><tr><td><code>turboquant_scalar_quantize</code> (N=256k)</td><td><strong>11.3×</strong></td><td>Eliminates <code>[N, 2^b]</code> diff tensor</td></tr><tr><td><code>turboquant_hadamard_quantize</code> (D=1024)</td><td>1.1×</td><td>Fused saves 1 allocation; WHT itself is fast</td></tr><tr><td><code>qjl_encode</code> (B=256)</td><td>0.2× (small B); ~1× (large B)</td><td><code>np.packbits</code> is BLAS-level; Metal overhead dominates at <code>B&lt;64</code></td></tr><tr><td><code>turboquant_fused_rvq_decode_attend</code></td><td>—</td><td>No NumPy baseline (different algorithm)</td></tr></tbody></table>
<p><strong>Memory savings</strong> are the bigger story for the RVQ attend kernel — it eliminates the <code>[B, H, S_kv, D]</code> fp16 <code>K_hat</code> tensor entirely. At <code>S_kv=4096, H=32, D=128</code> that's 33 MB per layer, ~1 GB across a 32-layer model, allocated and freed every forward pass.</p>
<p><strong>1-bit bit-packing alone gives 16× memory compression</strong> on the key cache (1 bit per dimension vs fp16). Combined with the Metal kernel's 30× throughput advantage, the packing/unpacking step goes from a bottleneck to essentially free.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-learned">What I Learned<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#what-i-learned" class="hash-link" aria-label="Direct link to What I Learned" title="Direct link to What I Learned" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-grid--total-threads-is-the-most-common-mlx-metal-mistake">1. Grid = total threads is the most common MLX Metal mistake<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#1-grid--total-threads-is-the-most-common-mlx-metal-mistake" class="hash-link" aria-label="Direct link to 1. Grid = total threads is the most common MLX Metal mistake" title="Direct link to 1. Grid = total threads is the most common MLX Metal mistake" translate="no">​</a></h3>
<p>Every tutorial and reference for Metal uses <code>dispatchThreadgroups</code>. MLX uses <code>dispatchThreads</code>. These are different. If your output is correct for the first batch element and zero elsewhere, check your grid first.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-xor-butterflies-are-wrong-for-parallel-wht">2. XOR butterflies are wrong for parallel WHT<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#2-xor-butterflies-are-wrong-for-parallel-wht" class="hash-link" aria-label="Direct link to 2. XOR butterflies are wrong for parallel WHT" title="Direct link to 2. XOR butterflies are wrong for parallel WHT" translate="no">​</a></h3>
<p>The standard sequential WHT uses <code>pair = i ^ stride</code>. On GPU this causes data races within a butterfly pass because multiple threads simultaneously read from and write to overlapping pairs. Use range-based pairing (<code>local = lane % (stride*2); is_upper = local &gt;= stride</code>) and read both values before the barrier.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-simd_sum-and-simd_shuffle-are-your-first-tools-not-shared-memory">3. <code>simd_sum</code> and <code>simd_shuffle</code> are your first tools, not shared memory<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#3-simd_sum-and-simd_shuffle-are-your-first-tools-not-shared-memory" class="hash-link" aria-label="Direct link to 3-simd_sum-and-simd_shuffle-are-your-first-tools-not-shared-memory" title="Direct link to 3-simd_sum-and-simd_shuffle-are-your-first-tools-not-shared-memory" translate="no">​</a></h3>
<p>For reductions and broadcasts within a simdgroup (32 lanes), <code>simd_sum</code> and <code>simd_shuffle</code> are zero-cost compared to <code>threadgroup_barrier</code> + shared memory. Design around simdgroups first; only escalate to threadgroup memory when you need communication beyond 32 lanes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-template-parameters-unlock-static-unrolling">4. Template parameters unlock static unrolling<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#4-template-parameters-unlock-static-unrolling" class="hash-link" aria-label="Direct link to 4. Template parameters unlock static unrolling" title="Direct link to 4. Template parameters unlock static unrolling" translate="no">​</a></h3>
<p><code>template &lt;int B_BITS&gt;</code> turns runtime constants into compile-time constants. The inner loop over centroids becomes 2, 4, 8, or 16 unrolled iterations — no branch, no loop counter. This is how Metal kernels beat NumPy at large N despite higher launch overhead: the arithmetic is genuinely faster.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-you-dont-manage-commandbuffer">5. You don't manage <code>commandBuffer</code><a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#5-you-dont-manage-commandbuffer" class="hash-link" aria-label="Direct link to 5-you-dont-manage-commandbuffer" title="Direct link to 5-you-dont-manage-commandbuffer" translate="no">​</a></h3>
<p>MLX handles <code>commandBuffer.commit()</code> and <code>commandBuffer.waitUntilCompleted()</code> inside <code>mx.eval()</code>. You never touch Metal command buffers when using <code>mx.fast.metal_kernel</code>. This is by design — MLX's lazy graph batches multiple kernel dispatches into one command buffer where possible.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-the-launch-overhead-is-real-and-017-ms">6. The launch overhead is real and ~0.17 ms<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#6-the-launch-overhead-is-real-and-017-ms" class="hash-link" aria-label="Direct link to 6. The launch overhead is real and ~0.17 ms" title="Direct link to 6. The launch overhead is real and ~0.17 ms" translate="no">​</a></h3>
<p>Every Metal kernel dispatch costs ~0.17 ms regardless of work size. For small N (&lt; ~2k elements), NumPy is faster. For large N (&gt; ~16k), Metal wins by 10–30×. Design your batching strategy accordingly — combine small operations into a single larger kernel rather than dispatching many small ones.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="code-organization">Code Organization<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#code-organization" class="hash-link" aria-label="Direct link to Code Organization" title="Direct link to Code Organization" translate="no">​</a></h2>
<p>The five kernels are organized into focused submodules under <code>veloxquant_mlx/metal/</code>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">metal/</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── __init__.py          # lazy re-exports</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── kernels.py           # thin facade — imports from all submodules</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── _bit_packing.py      # turboquant_bit_pack, turboquant_bit_unpack</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── _scalar_quant.py     # turboquant_scalar_quantize, _dequantize, _hadamard_quantize</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── _qjl.py              # qjl_encode, qjl_inner_product</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">├── _rvq_attend.py       # turboquant_fused_rvq_decode_attend</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">└── _vecinfer.py         # vecinfer_dequant_metal, vecinfer_quantize_metal, ...</span><br></div></code></pre></div></div>
<p>Each submodule has its own <code>_cache: dict = {}</code> for the kernel singleton pattern — build the <code>MTLComputePipelineState</code> once on first call, reuse forever:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">_pack_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">int</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    key </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"bit_pack"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> key </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">not</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> _cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        _cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">fast</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal_kernel</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            name</span><span class="token operator">=</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"turboquant_bit_pack_b</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">b</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            input_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"indices"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            output_names</span><span class="token operator">=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token string" style="color:rgb(255, 121, 198)">"packed"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">            source</span><span class="token operator">=</span><span class="token plain">_PACK_SRC</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> _cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">key</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><br></div></code></pre></div></div>
<p><code>kernels.py</code> is now a 47-line re-export facade:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_bit_packing </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> turboquant_bit_pack</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> turboquant_bit_unpack</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_scalar_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> turboquant_scalar_quantize</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_qjl </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> qjl_encode</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> qjl_inner_product</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">metal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_rvq_attend </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> turboquant_fused_rvq_decode_attend</span><br></div></code></pre></div></div>
<p>All 40 tests pass after the restructuring — the facade is transparent to callers.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-broader-point">The Broader Point<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#the-broader-point" class="hash-link" aria-label="Direct link to The Broader Point" title="Direct link to The Broader Point" translate="no">​</a></h2>
<p>Apple Silicon is a genuinely good target for this kind of work. Unified memory means you don't pay PCIe bandwidth to move data between CPU and GPU — the Metal kernel reads the same bytes your Python code just wrote. The simdgroup primitives (<code>simd_sum</code>, <code>simd_shuffle</code>) are clean and well-documented. And <code>mx.fast.metal_kernel</code> makes the iteration loop fast: write Metal source in Python, evaluate, fix, repeat.</p>
<p>The hard part isn't the Metal itself — it's understanding how MLX dispatches kernels. Once you internalize "grid = total threads, not threadgroups" and "lazy graph, so nothing runs until mx.eval()", the rest is straightforward shader programming.</p>
<p>The full source is in <a href="https://github.com/rajveer43/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">VeloxQuant-MLX</a> under <code>veloxquant_mlx/metal/</code>. The benchmark script is at <code>veloxquant_mlx/benchmarks/metal_kernel_benchmark.py</code> and produces all the figures discussed here.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="references">References<a href="https://veloxquant-mlx.netlify.app/docs/blog/turboquant-metal-kernels#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://arxiv.org/abs/2504.19874" target="_blank" rel="noopener noreferrer" class="">TurboQuant (ICLR 2026)</a> — Zandieh et al., "Online Vector Quantization with Near-optimal Distortion Rate"</li>
<li class=""><a href="https://arxiv.org/abs/2406.03482" target="_blank" rel="noopener noreferrer" class="">QJL (2024)</a> — Zandieh et al., "QJL: 1-Bit Quantized JL Transform for KV Cache Quantization"</li>
<li class=""><a href="https://arxiv.org/abs/2205.14135" target="_blank" rel="noopener noreferrer" class="">FlashAttention (NeurIPS 2022)</a> — Dao et al., online softmax algorithm</li>
<li class=""><a href="https://github.com/ml-explore/mlx" target="_blank" rel="noopener noreferrer" class="">Apple MLX</a> — the framework</li>
<li class=""><a href="https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf" target="_blank" rel="noopener noreferrer" class="">Metal Shading Language Specification</a> — simd_sum, simd_shuffle, threadgroup_barrier reference</li>
</ul>
<hr>
<p><em>Code: <a href="https://github.com/rajveer43/VeloxQuant-MLX" target="_blank" rel="noopener noreferrer" class="">github.com/rajveer43/VeloxQuant-MLX</a> · Previous post: <a class="" href="https://veloxquant-mlx.netlify.app/docs/blog/metal-kernels">I Wrote a Metal Kernel to Stop My Mac From OOMing on LLM Inference</a></em></p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="metal" term="metal"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="gpu" term="gpu"/>
        <category label="turboquant" term="turboquant"/>
        <category label="performance" term="performance"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Benchmark Results: 10 Models, 8 Compression Configs]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/results</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/results"/>
        <updated>2026-05-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A full 8-model benchmark of residual vector quantization for KV cache compression on Apple M4 16GB. What worked, what didn't, and the one result that surprised me.]]></summary>
        <content type="html"><![CDATA[<p><em>A full 8-model benchmark of residual vector quantization for KV cache compression on Apple M4 16GB. What worked, what didn't, and the one result that surprised me.</em></p>
<hr>
<p>Here is the result I did not expect: a 32-billion-parameter model running <strong>faster</strong> with the KV cache compressed than without.</p>
<p>Qwen2.5-32B at RVQ 2-bit: 4.2 tok/s. The same model at full fp16 precision: 3.7 tok/s. That is a 14% throughput improvement from compression — on a 16GB MacBook. Not from a faster algorithm. From freeing up memory bandwidth that the model weights were competing for.</p>
<p>This is the full story: what we built, how it works, and what the benchmarks across 8 models actually show.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-memory-problem-every-mac-llm-user-knows">The Memory Problem Every Mac LLM User Knows<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#the-memory-problem-every-mac-llm-user-knows" class="hash-link" aria-label="Direct link to The Memory Problem Every Mac LLM User Knows" title="Direct link to The Memory Problem Every Mac LLM User Knows" translate="no">​</a></h2>
<p>Every token your LLM generates writes two vectors into memory — a Key and a Value — for every attention head, across every layer. This is the KV cache. It grows linearly with context length.</p>
<p>For Mistral 7B (32 layers, 8 KV heads, head dimension 128) at 32K tokens, the KV cache at fp16 is:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">2 × 32 layers × 8 heads × 128 dims × 32,000 tokens × 2 bytes = 8.4 GB</span><br></div></code></pre></div></div>
<p>On a 16GB Mac, that is 8.4 GB for context alone — before model weights, before the OS, before your browser tab.</p>
<p>The KV cache is also the performance bottleneck. Most 7-8B models on Apple Silicon are memory-bandwidth bound: the chip can do the arithmetic faster than it can load data from unified memory. Every token requires streaming the full KV cache through the memory bus once per layer. Smaller cache = faster generation.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-2-bit-usually-fails">Why 2-Bit Usually Fails<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#why-2-bit-usually-fails" class="hash-link" aria-label="Direct link to Why 2-Bit Usually Fails" title="Direct link to Why 2-Bit Usually Fails" translate="no">​</a></h2>
<p>The obvious approach is to quantize each float16 value (2 bytes) down to 2 bits — an 8x reduction. The problem: with only 4 quantization levels, reconstruction error is large enough to corrupt attention scores.</p>
<p>Cosine similarity between the original and compressed key vector — which directly determines attention quality — drops to <strong>0.69</strong> at single-pass 2-bit. For short generation (&lt; 100 tokens), this is often tolerable. For long reasoning chains, it is not. We measured this:</p>
<table><thead><tr><th>Model</th><th>Single-pass 2-bit: tokens generated</th></tr></thead><tbody><tr><td>Phi-4</td><td><strong>0 / 200</strong> — immediate EOS</td></tr><tr><td>Llama 3.1 8B</td><td><strong>32 / 200</strong> — collapses mid-sequence</td></tr><tr><td>Falcon3 7B</td><td><strong>38 / 200</strong> — degrades rapidly</td></tr><tr><td>Qwen2.5 32B</td><td><strong>5 / 200</strong> — immediate near-EOS</td></tr></tbody></table>
<p>This is not a mild degradation. For these models, standard 2-bit quantization simply does not work.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-fix-two-pass-residual-quantization">The Fix: Two-Pass Residual Quantization<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#the-fix-two-pass-residual-quantization" class="hash-link" aria-label="Direct link to The Fix: Two-Pass Residual Quantization" title="Direct link to The Fix: Two-Pass Residual Quantization" translate="no">​</a></h2>
<p>The key insight: if your first codebook leaves large error, run a second codebook on the error itself.</p>
<p><strong>Stage 1</strong> fits a Gaussian Lloyd-Max codebook on the rotated key vector. The rotation (a randomized Hadamard transform) spreads information uniformly across dimensions, so the distribution after rotation looks Gaussian and the codebook fits it well.</p>
<p><strong>Stage 2</strong> takes the residual — the difference between the original and the stage-1 reconstruction — and fits a Laplacian codebook on that. The Laplacian distribution matches the residual better than a Gaussian: it is the tail of a Gaussian, peaked at zero, with heavier tails. Using the wrong distribution family for stage 2 leaves measurable error on the table.</p>
<p>Result: cosine similarity from <strong>0.69 → 0.98</strong> at 2 bits per dimension. The two-pass approach recovers information the single-pass approach discards.</p>
<p><strong>Storage per vector (d=128):</strong></p>
<ul>
<li class="">RVQ 2-bit: <code>ceil(128 × 2 × 2 / 8)</code> = 64 bytes per key vector vs 256 bytes at fp16 — <strong>4x compression</strong></li>
<li class="">RVQ 1-bit (sign quantizer + Laplacian residual): 32 bytes per vector — <strong>8x compression</strong>, cosine 0.917</li>
</ul>
<p>The 1-bit variant uses a two-level sign quantizer as stage 1 (<code>{−0.798, +0.798}</code> — the exact Gaussian Lloyd-Max solution at 1 bit) and a Laplacian codebook on the sign-quantization error for stage 2. It achieves 7.5x KV cache compression while keeping cosine above 0.91.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-throughput-problem-and-how-we-solved-it">The Throughput Problem (and How We Solved It)<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#the-throughput-problem-and-how-we-solved-it" class="hash-link" aria-label="Direct link to The Throughput Problem (and How We Solved It)" title="Direct link to The Throughput Problem (and How We Solved It)" translate="no">​</a></h2>
<p>Compression reduces memory pressure. But if the quantization compute itself costs more than the bandwidth savings, the net result is slower generation.</p>
<p>Our initial implementation ran at 17.7 tok/s on Mistral 7B with RVQ 2-bit — slower than fp16's 22.1 tok/s. Four changes fixed this, each independently measurable:</p>
<p><strong>1. Batch all heads into one MLX call (+22%)</strong></p>
<p>The original code maintained a separate quantizer per attention head and looped over them in Python. For Mistral 7B (8 KV heads, 32 layers), that is 256 small kernel dispatches per token. The fix: reshape <code>(B, H, S, D) → (B·H·S, D)</code> and call one shared quantizer on the entire batch. MLX compiles the operation as a single graph node and dispatches it once.</p>
<p><strong>2. Switch to Hadamard rotation</strong></p>
<p>The rotation preconditioner used QR decomposition — a full <code>d×d</code> matrix multiply (16,384 operations at d=128). <code>mx.hadamard_transform</code> is a Metal-native fused kernel that achieves the same statistical effect in O(d log d) — 896 operations at d=128. The quality is mathematically identical: a Hadamard with a random ±1 diagonal is Haar-distributed, the same family as a random orthogonal matrix.</p>
<p><strong>3. Boundary-sum quantization instead of broadcast-argmin</strong></p>
<p>Codebook lookup was materializing a <code>(batch, d, k)</code> distance tensor and running argmin — three kernel launches: broadcast-subtract, absolute value, argmin. Lloyd-Max boundaries are just midpoints between sorted centroids. The nearest centroid index equals the number of boundaries the value exceeds: one comparison and one sum. Two kernels, no intermediate tensor. Verified bitwise-identical to the old path at 100.00% index match.</p>
<p><strong>4. Remove redundant dtype casts</strong></p>
<p>The update path was casting fp16 → fp32 → fp16 → fp32 → fp16 from accumulated unnecessary coercions. Removing them saved ~5% per call with no effect on output quality.</p>
<p>Net result: <strong>17.7 → 22.3 tok/s</strong> on Mistral 7B. The compression now matches fp16 throughput.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="full-8-model-benchmark">Full 8-Model Benchmark<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#full-8-model-benchmark" class="hash-link" aria-label="Direct link to Full 8-Model Benchmark" title="Direct link to Full 8-Model Benchmark" translate="no">​</a></h2>
<p>Hardware: Apple M4 MacBook, 16GB unified memory. Each run: 200 tokens, one fresh Python subprocess per (model, config) to avoid MLX graph compilation bugs. 6 configs: fp16, TQ 2/3/4-bit (single-pass), RVQ 2-bit, RVQ 1-bit.</p>
<table><thead><tr><th>Model</th><th>fp16</th><th>RVQ 1-bit ★</th><th>RVQ 2-bit ★</th><th>TQ 4-bit</th><th>TQ 2-bit</th></tr></thead><tbody><tr><td>Mistral 7B</td><td>23.3 tok/s</td><td><strong>22.2</strong> (201/201)</td><td>22.5 (201)</td><td>21.4 (201)</td><td>22.1 (201)</td></tr><tr><td>Falcon3 7B</td><td>24.0 tok/s</td><td><strong>23.1</strong> (200/200)</td><td>22.7 (200)</td><td>22.1 (200)</td><td>17.4 (<strong>38/200</strong>)</td></tr><tr><td>Phi-4</td><td>11.9 tok/s</td><td><strong>11.8</strong> (200/200)</td><td>11.7 (200)</td><td>11.4 (200)</td><td>0.0 (<strong>0/200</strong>)</td></tr><tr><td>Qwen3 4B</td><td>40.2 tok/s</td><td><strong>34.3</strong> (187/200)</td><td>35.0 (197)</td><td>33.5 (199)</td><td>31.0 (170/200)</td></tr><tr><td>Qwen3 8B</td><td>20.5 tok/s</td><td><strong>21.1</strong> (200/200)</td><td>20.7 (200)</td><td>19.8 (200)</td><td>20.9 (200)</td></tr><tr><td>Llama 3.1 8B</td><td>22.0 tok/s</td><td><strong>21.5</strong> (201/201)</td><td>20.9 (201)</td><td>20.3 (201)</td><td>3.4 (<strong>32/201</strong>)</td></tr><tr><td>Gemma3 4B</td><td>32.5 tok/s</td><td><strong>30.5</strong> (201/201)</td><td>29.2 (201)</td><td>27.7 (201)</td><td>28.6 (198/201)</td></tr><tr><td><strong>Qwen2.5 32B</strong></td><td><strong>3.7</strong> tok/s</td><td><strong>3.9</strong> (200/200)</td><td><strong>4.2</strong> (200)</td><td>3.9 (200)</td><td>0.3 (<strong>5/200</strong>)</td></tr></tbody></table>
<p>★ = RVQ configs at 7.5x compression. Bolded token counts in TQ 2-bit column = failure.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="three-findings-worth-highlighting">Three Findings Worth Highlighting<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#three-findings-worth-highlighting" class="hash-link" aria-label="Direct link to Three Findings Worth Highlighting" title="Direct link to Three Findings Worth Highlighting" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-rvq-1-bit-is-the-reliability-result">1. RVQ 1-bit is the reliability result<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#1-rvq-1-bit-is-the-reliability-result" class="hash-link" aria-label="Direct link to 1. RVQ 1-bit is the reliability result" title="Direct link to 1. RVQ 1-bit is the reliability result" translate="no">​</a></h3>
<p>Across all 8 models at all 200 tokens, RVQ 1-bit with 7.5x compression produced complete, coherent output every time. TQ single-pass 2-bit failed catastrophically on 4 of 8 models.</p>
<p>This is the practical takeaway: if you want 2-bit compression and need your model to actually finish its output, residual quantization is not optional. The gap between 0.69 and 0.92 cosine is the gap between broken and working.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-at-32b-scale-compression-beats-fp16">2. At 32B scale, compression beats fp16<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#2-at-32b-scale-compression-beats-fp16" class="hash-link" aria-label="Direct link to 2. At 32B scale, compression beats fp16" title="Direct link to 2. At 32B scale, compression beats fp16" translate="no">​</a></h3>
<p>Qwen2.5-32B is too large for 16GB without aggressive weight quantization. With 4-bit weights (the <code>Qwen2.5-32B-Instruct-4bit</code> model), it fits — but barely. The model consumes ~17.5 GB including the OS and runtime, leaving almost nothing for KV cache headroom. The chip is running at its memory bandwidth ceiling.</p>
<p>Compressing the KV cache frees bandwidth that the weight loads were competing for. The result is a net gain:</p>
<ul>
<li class="">fp16 KV cache: 3.7 tok/s</li>
<li class="">RVQ 2-bit KV cache: <strong>4.2 tok/s (+14%)</strong></li>
<li class="">RVQ 1-bit KV cache: <strong>3.9 tok/s (+7%)</strong></li>
</ul>
<p>The effect is largest at 32B because the bandwidth contention is sharpest. At 7-8B, the same effect exists but is smaller (Qwen3-8B: +3% at RVQ 1-bit). For models that fit comfortably in unified memory, the effect disappears.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-the-qwen3-4b-exception">3. The Qwen3 4B exception<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#3-the-qwen3-4b-exception" class="hash-link" aria-label="Direct link to 3. The Qwen3 4B exception" title="Direct link to 3. The Qwen3 4B exception" translate="no">​</a></h3>
<p>Qwen3 4B runs at 40.2 tok/s at fp16 — unusually fast for a model this size, because it is small and the chip can move through layers quickly. At RVQ 1-bit, it drops to 34.3 tok/s (85% of fp16).</p>
<p>This is the trade-off inverting. At small model sizes, fp16 is fast enough that the quantization compute overhead is a larger fraction of total runtime. At large model sizes, the weight loads dominate and the overhead is amortized. The crossover is somewhere around 8B parameters on this hardware.</p>
<p>If you are running a 4B model on a Mac with 32GB, use fp16. If you are running it on 16GB and memory is the constraint, RVQ 2-bit at 35.0 tok/s and 197/200 tokens is the right trade.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-vlm-extension">The VLM Extension<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#the-vlm-extension" class="hash-link" aria-label="Direct link to The VLM Extension" title="Direct link to The VLM Extension" translate="no">​</a></h2>
<p>We also extended quantization to Qwen2-VL-7B — a vision language model that uses bfloat16 internally (wider exponent range, needed for large-norm image patch tokens). The original code always cast to float16 before normalizing, which discarded the bfloat16 exponent range at exactly the wrong moment for image tokens.</p>
<p>The fix is one variable: <code>kdtype = keys.dtype</code>. Normalize in the actual dtype, cast to float16 only for the codebook lookup. The VLM key-distribution diagnostic across all 28 layers showed that image patch tokens and text tokens have nearly identical distributions after RMSNorm — the quantizer does not need special handling for image tokens. RVQ 2-bit cosine on real Qwen2-VL keys: <strong>0.979 for both image and text tokens</strong> across all layers.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-means-in-practice">What This Means in Practice<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#what-this-means-in-practice" class="hash-link" aria-label="Direct link to What This Means in Practice" title="Direct link to What This Means in Practice" translate="no">​</a></h2>
<p><strong>16GB Mac users:</strong></p>
<p>For 7-8B models (Mistral, Llama, Falcon, Phi-4, Qwen3-8B), RVQ 1-bit gives you 7.5x KV cache compression at 94-99% of fp16 throughput. At 32K context, your KV cache drops from ~8 GB to ~1 GB. That is the difference between OOM and comfortable operation.</p>
<p>For Qwen2.5-32B on 16GB, RVQ is what makes the model faster than fp16. The compression is load-bearing.</p>
<p>For 4B models (Qwen3-4B, Gemma3-4B), use fp16 if you have memory. Use RVQ 2-bit if you do not.</p>
<p><strong>Do not use single-pass 2-bit.</strong> It fails on Phi-4, Llama, Falcon, and Qwen2.5-32B. If you need 2-bit compression, you need residual quantization.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="using-it">Using It<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#using-it" class="hash-link" aria-label="Direct link to Using It" title="Direct link to Using It" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> VeloxQuant-MLX</span><br></div></code></pre></div></div>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheConfig</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Mistral-7B-Instruct-v0.3-4bit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># 7.5x compression, 95% throughput</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">bits</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> algorithm</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_rvq"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">response </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    prompt</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"Explain the difference between RAM and unified memory."</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    kv_cache</span><span class="token operator">=</span><span class="token plain">cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    max_tokens</span><span class="token operator">=</span><span class="token number">500</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">response</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>For RVQ 2-bit: <code>bits=2</code>. For single-pass 4-bit (safe on all models, less compression): <code>algorithm="turboquant_prod"</code>, <code>bits=4</code>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-figures-show">What the Figures Show<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#what-the-figures-show" class="hash-link" aria-label="Direct link to What the Figures Show" title="Direct link to What the Figures Show" translate="no">​</a></h2>
<p>Each model folder in <code>figures/2026-05-12/</code> contains 6 panels:</p>
<ol>
<li class=""><strong>Throughput comparison</strong> — tok/s across all 6 configs</li>
<li class=""><strong>Quality vs compression</strong> — cosine similarity curve with RVQ ★ markers</li>
<li class=""><strong>Memory at scale</strong> — KV cache bytes vs sequence length for each config</li>
<li class=""><strong>Attention distortion</strong> — per-config distortion bars</li>
<li class=""><strong>Output comparison</strong> — tokens generated per config (the failure-detection panel)</li>
<li class=""><strong>Full report</strong> — all panels on one sheet</li>
</ol>
<p>The output comparison panel (fig5) is the one to look at for reliability. Models where single-pass 2-bit generates 0 or 5 tokens show it plainly. RVQ configs all extend to the full 200-token bar.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reproducibility">Reproducibility<a href="https://veloxquant-mlx.netlify.app/docs/blog/results#reproducibility" class="hash-link" aria-label="Direct link to Reproducibility" title="Direct link to Reproducibility" translate="no">​</a></h2>
<p>All runs use subprocess isolation — one fresh Python process per (model, config) — to avoid MLX's graph-reuse bug that causes 2nd+ configs to generate 0 tokens in the same process when the cache type changes. If you run these scripts yourself and see anomalously low token counts on non-fp16 configs, check that you are not running multiple configs in the same Python session.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token function" style="color:rgb(80, 250, 123)">git</span><span class="token plain"> clone https://github.com/rajveer43/veloxquant-mlx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token builtin class-name" style="color:rgb(189, 147, 249)">cd</span><span class="token plain"> veloxquant-mlx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">-e</span><span class="token plain"> </span><span class="token builtin class-name" style="color:rgb(189, 147, 249)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token assign-left variable" style="color:rgb(189, 147, 249);font-style:italic">PYTHONPATH</span><span class="token operator">=</span><span class="token plain">. python3 benchmark_scripts/run_full_reports.py </span><span class="token parameter variable" style="color:rgb(189, 147, 249);font-style:italic">--models</span><span class="token plain"> mistral7b falcon3_7b llama31_8b</span><br></div></code></pre></div></div>
<p>Figures save to <code>figures/2026-05-12/&lt;model&gt;/</code>. Each model takes 10–15 minutes on M4.</p>
<hr>
<p><em>VeloxQuant-MLX is MIT licensed. Hardware: Apple M4, 16GB unified memory. MLX 0.24.x, mlx-lm 0.21.x.</em></p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="benchmarks" term="benchmarks"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="kv-cache" term="kv-cache"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[I Ported a Google Research Paper to Apple Silicon]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/10-model-study</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study"/>
        <updated>2026-05-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How I built VeloxQuant-MLX — KV cache compression for LLMs running on your Mac]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-i-built-veloxquant-mlx--kv-cache-compression-for-llms-running-on-your-mac">How I built VeloxQuant-MLX — KV cache compression for LLMs running on your Mac<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#how-i-built-veloxquant-mlx--kv-cache-compression-for-llms-running-on-your-mac" class="hash-link" aria-label="Direct link to How I built VeloxQuant-MLX — KV cache compression for LLMs running on your Mac" title="Direct link to How I built VeloxQuant-MLX — KV cache compression for LLMs running on your Mac" translate="no">​</a></h2>
<hr>
<p>Every time a language model generates a token, it writes two vectors into memory: a <strong>Key</strong> and a <strong>Value</strong>. These accumulate silently in what's called the <strong>KV cache</strong> — one pair per token, per attention head, per layer.</p>
<p>At a short context that's fine. But at 4,000 tokens with a 7B model on a 16GB Mac, the KV cache alone can consume 4–6 GB. At 32K tokens, it can exceed the model weights themselves.</p>
<p>I wanted to fix this for Apple Silicon. So I spent the last few weeks porting three research algorithms from GPU-targeted papers into a native MLX library — and published it to PyPI as <strong>VeloxQuant-MLX</strong>.</p>
<p>Here's exactly how it works, what I had to rebuild from scratch, and what the benchmarks actually show.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-core-idea-why-kv-vectors-can-be-compressed">The Core Idea: Why KV Vectors Can Be Compressed<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#the-core-idea-why-kv-vectors-can-be-compressed" class="hash-link" aria-label="Direct link to The Core Idea: Why KV Vectors Can Be Compressed" title="Direct link to The Core Idea: Why KV Vectors Can Be Compressed" translate="no">​</a></h2>
<p>Key vectors in a transformer aren't arbitrary. After a learned linear projection, they tend to follow a roughly Gaussian distribution — especially after layer normalization. This matters because <strong>scalar quantization theory tells us the optimal codebook for a Gaussian is the Lloyd-Max codebook</strong>.</p>
<p>The Lloyd-Max algorithm iterates two conditions until convergence:</p>
<ol>
<li class=""><strong>Optimal boundaries</strong> — the boundary between two quantization levels sits at the midpoint of their centroids</li>
<li class=""><strong>Optimal centroids</strong> — each centroid is the conditional mean of the distribution over its Voronoi cell</li>
</ol>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">c_i = ∫[b_{i-1} to b_i] x·f(x)dx  /  ∫[b_{i-1} to b_i] f(x)dx</span><br></div></code></pre></div></div>
<p>For a standard Gaussian, this converges to fixed centroids that you can precompute once and reuse for every layer, every model, every token. No per-block scale constants. No calibration data. Just a lookup table.</p>
<p>The problem: raw KV vectors are <em>not</em> standard Gaussian. They have varying magnitudes, correlated dimensions, and outlier channels that dominate the quantization error.</p>
<p>This is what TurboQuant solves.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-1-random-rotation">Stage 1: Random Rotation<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#stage-1-random-rotation" class="hash-link" aria-label="Direct link to Stage 1: Random Rotation" title="Direct link to Stage 1: Random Rotation" translate="no">​</a></h2>
<p>Before quantizing, multiply each Key vector by a random orthogonal matrix <strong>Π</strong>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">k̃ = k · Πᵀ</span><br></div></code></pre></div></div>
<p>Why? Because a random rotation <strong>spreads information uniformly</strong> across all dimensions. Outlier channels — dimensions with unusually high variance — get diluted into all other dimensions. After rotation, the vector looks much more like an isotropic Gaussian, and the Lloyd-Max codebook fits it well.</p>
<p>The original TurboQuant paper uses QR decomposition on a random Gaussian matrix to generate <strong>Π</strong>. That's O(d²) per rotation.</p>
<p>For VeloxQuant-MLX, I added a second option: the <strong>randomized Hadamard transform</strong>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">k̃ = D · diag(r) · WHT(k)</span><br></div></code></pre></div></div>
<p>where <code>D</code> is a fixed scaling factor, <code>r</code> is a random Rademacher vector (±1), and <code>WHT</code> is the Walsh-Hadamard Transform. This runs in <strong>O(d log d)</strong> and maps directly to <code>mx.hadamard_transform</code> — Metal-accelerated on Apple Silicon.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name">HadamardPreconditioner</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">Preconditioner</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">apply</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> x</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">-</span><span class="token operator">&gt;</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token comment" style="color:rgb(98, 114, 164)"># x shape: (..., d)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        scaled </span><span class="token operator">=</span><span class="token plain"> x </span><span class="token operator">*</span><span class="token plain"> self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_diag_scale  </span><span class="token comment" style="color:rgb(98, 114, 164)"># diagonal randomization</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">hadamard_transform</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">scaled</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">/</span><span class="token plain"> math</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">sqrt</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">self</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">_d</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<p>For head dimensions that are powers of 2 (128, 256, 512) — which covers nearly every production model — the Hadamard preconditioner is automatically selected.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-2-lloyd-max-scalar-quantization">Stage 2: Lloyd-Max Scalar Quantization<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#stage-2-lloyd-max-scalar-quantization" class="hash-link" aria-label="Direct link to Stage 2: Lloyd-Max Scalar Quantization" title="Direct link to Stage 2: Lloyd-Max Scalar Quantization" translate="no">​</a></h2>
<p>After rotation, normalize each vector to unit norm (store the norm as fp16 for later), then quantize each dimension independently using the precomputed codebook:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">for each dimension j in rotated key:</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    find nearest centroid in codebook</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    store index (b-1 bits)</span><br></div></code></pre></div></div>
<p>The codebook has 2^(b-1) centroids for a b-bit configuration. At b=4, that's 8 centroids — enough to achieve ~0.95 cosine similarity with the original key vector after dequantization.</p>
<p>The <code>lloyd_max()</code> function in the library solves this numerically using trapezoidal quadrature:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">lloyd_max</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">pdf_fn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> support</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> n_levels</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> n_iter</span><span class="token operator">=</span><span class="token number">100</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tol</span><span class="token operator">=</span><span class="token number">1e-8</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    centroids </span><span class="token operator">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">linspace</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">lo</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> hi</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> n_levels</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> _ </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">range</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">n_iter</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        boundaries </span><span class="token operator">=</span><span class="token plain"> midpoints</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">centroids</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        centroids </span><span class="token operator">=</span><span class="token plain"> conditional_means</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">pdf_fn</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> boundaries</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">  </span><span class="token comment" style="color:rgb(98, 114, 164)"># numerical integration</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> converged</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">break</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> centroids</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> boundaries</span><br></div></code></pre></div></div>
<p>You run this once at startup. The result is a tiny lookup table (~8 floats for 3-bit) that gets reused for every token in every layer for the entire generation.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stage-3-qjl-residual-correction">Stage 3: QJL Residual Correction<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#stage-3-qjl-residual-correction" class="hash-link" aria-label="Direct link to Stage 3: QJL Residual Correction" title="Direct link to Stage 3: QJL Residual Correction" translate="no">​</a></h2>
<p>Even at 4-bit, MSE quantization leaves a residual error:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">r = k - k̂_MSE</span><br></div></code></pre></div></div>
<p>TurboQuant's key insight is that you can correct for this without storing the full residual. Instead, store only the <strong>sign sketch</strong>: apply a random Johnson-Lindenstrauss projection matrix <strong>S</strong> to the residual, then store only the signs:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">z = sign(S·r) ∈ {-1, +1}^m</span><br></div></code></pre></div></div>
<p>One bit per sketch dimension. For m=128 sketch dimensions, that's 128 bits = 16 bytes per key vector.</p>
<p>At attention time, the corrected inner product estimate becomes:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">⟨q,k⟩ ≈ ⟨q,k̂_MSE⟩ + ‖r‖ · (π/2m) · Σ_j z_j · sign(⟨S_j, q⟩)</span><br></div></code></pre></div></div>
<p>The residual norm <code>‖r‖</code> is stored as fp16 (2 bytes). The signs are bit-packed (1 bit each). The correction is <strong>unbiased</strong> — on expectation, it perfectly reconstructs the true inner product.</p>
<p>This is what makes TurboQuant different from plain quantization: the QJL stage recovers information that was thrown away, using only 1 bit per sketch dimension.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="memory-layout-what-actually-gets-stored">Memory Layout: What Actually Gets Stored<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#memory-layout-what-actually-gets-stored" class="hash-link" aria-label="Direct link to Memory Layout: What Actually Gets Stored" title="Direct link to Memory Layout: What Actually Gets Stored" translate="no">​</a></h2>
<p>For each token, per attention head, VeloxQuant-MLX stores:</p>
<table><thead><tr><th>Component</th><th>Bits per dimension</th><th>Total bytes (d=128)</th></tr></thead><tbody><tr><td>MSE indices</td><td>b-1 bits × d</td><td>ceil(128 × 3 / 8) = 48 B</td></tr><tr><td>QJL signs</td><td>1 bit × m</td><td>ceil(128 / 8) = 16 B</td></tr><tr><td>Residual norm</td><td>fp16</td><td>2 B</td></tr><tr><td>Per-vector norm</td><td>fp16</td><td>2 B</td></tr><tr><td><strong>Key total</strong></td><td></td><td><strong>68 B</strong></td></tr><tr><td>Values (int8 + scale)</td><td>8 bits × d + fp16</td><td>130 B</td></tr><tr><td><strong>Grand total</strong></td><td></td><td><strong>198 B</strong></td></tr></tbody></table>
<p>Compare to fp16: 128 dimensions × 2 bytes = <strong>256 B for keys alone</strong>.</p>
<p>At 4-bit, key compression is <strong>4.27×</strong>. Including values, total KV compression is ~1.6×. With value compression (int8 per-token, already implemented), total KV compression reaches ~3.5–4×.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bit-packing-problem">The Bit-Packing Problem<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#the-bit-packing-problem" class="hash-link" aria-label="Direct link to The Bit-Packing Problem" title="Direct link to The Bit-Packing Problem" translate="no">​</a></h2>
<p>MLX has no sub-byte dtype. A 3-bit index stored naively in <code>uint8</code> wastes 5 bits and kills your compression ratio.</p>
<p>The solution is a <code>BitPackBuffer</code> — pack multiple indices into each byte:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">3-bit example: pack [2, 5, 1, 7, 3, ...] into bytes</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  byte 0: bits 0-2 = index[0], bits 3-5 = index[1], bits 6-7 = index[2][0:1]</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  byte 1: bit 0 = index[2][2], bits 1-3 = index[3], ...</span><br></div></code></pre></div></div>
<p>For unpack at attend time, the library uses vectorized numpy paths for b ∈ {1, 2, 4} and a loop fallback for b=3:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">if</span><span class="token plain"> b </span><span class="token operator">==</span><span class="token plain"> </span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    shifts </span><span class="token operator">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">2</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token number">6</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> dtype</span><span class="token operator">=</span><span class="token plain">np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">uint8</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    vals </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">packed</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token boolean">None</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">&gt;&gt;</span><span class="token plain"> shifts</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">&amp;</span><span class="token plain"> </span><span class="token number">0x3</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">reshape</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">n</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> vals</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">d</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><br></div></code></pre></div></div>
<p>This runs entirely in numpy before handing off to MLX for the attention computation — keeping the Metal graph clean.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="outlier-two-stream-cache">Outlier Two-Stream Cache<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#outlier-two-stream-cache" class="hash-link" aria-label="Direct link to Outlier Two-Stream Cache" title="Direct link to Outlier Two-Stream Cache" translate="no">​</a></h2>
<p>Some attention heads have a handful of dimensions with dramatically higher variance than the others. These <strong>outlier channels</strong> dominate quantization error: if dimension 47 has 10× the typical variance, every other dimension's codebook entry gets contaminated.</p>
<p>VeloxQuant-MLX handles this with an optional two-stream cache:</p>
<ol>
<li class=""><strong>Calibration phase</strong> — observe the first N tokens, track per-channel variance</li>
<li class=""><strong>Detection</strong> — identify the top-K highest-variance channels</li>
<li class=""><strong>Split encoding</strong> — outlier channels → int8 with per-token scale; inlier channels → TurboQuant as normal</li>
</ol>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token comment" style="color:rgb(98, 114, 164)"># During append:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> outlier_idx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token number">0</span><span class="token plain">           </span><span class="token comment" style="color:rgb(98, 114, 164)"># zero out outliers in inlier stream</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">encode inlier key </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">with</span><span class="token plain"> TurboQuant</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">store outlier channels </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> int8</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># During attend:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">scores </span><span class="token operator">=</span><span class="token plain"> turboquant_ip</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">q</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> encoded_keys</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">scores </span><span class="token operator">+=</span><span class="token plain"> int8_ip</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">q</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">outlier_idx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> outlier_cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain"> outlier_scales</span><br></div></code></pre></div></div>
<p>The result in benchmarks: at K=8 outlier channels, output quality is <strong>indistinguishable from fp16</strong> even at 4-bit compression. The cost is slightly higher memory usage (8 × 2 = 16 extra bytes per token per head) and a numpy↔MLX copy per token — which is the main throughput bottleneck in the current implementation.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="weight-quantization-compressing-the-model-too">Weight Quantization: Compressing the Model Too<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#weight-quantization-compressing-the-model-too" class="hash-link" aria-label="Direct link to Weight Quantization: Compressing the Model Too" title="Direct link to Weight Quantization: Compressing the Model Too" translate="no">​</a></h2>
<p>One thing missing from the original papers: they only quantize the KV cache. The model weights stay fp16.</p>
<p>VeloxQuant-MLX adds a <code>QuantizedLinear</code> layer — a drop-in <code>nn.Module</code> replacement that applies TurboQuant to the weight matrix itself:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">weight </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> quantize_model</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model </span><span class="token operator">=</span><span class="token plain"> load_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">quantize_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> bits</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> seed</span><span class="token operator">=</span><span class="token number">42</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># All nn.Linear layers are now QuantizedLinear</span><br></div></code></pre></div></div>
<p>Each row of the weight matrix is normalized, rotated with the same Hadamard preconditioner, and Lloyd-Max quantized. At inference, dequantize on the fly. At 3-bit, this compresses weight matrices by ~5× with minimal accuracy loss.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="benchmark-results-across-7-models">Benchmark Results Across 7 Models<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#benchmark-results-across-7-models" class="hash-link" aria-label="Direct link to Benchmark Results Across 7 Models" title="Direct link to Benchmark Results Across 7 Models" translate="no">​</a></h2>
<p>I ran the full benchmark suite (fp16, 2-bit, 3-bit, 4-bit) across 7 models on an Apple M4 MacBook (16GB unified memory):</p>
<table><thead><tr><th>Model</th><th>fp16 tok/s</th><th>3-bit quality</th><th>4-bit quality</th></tr></thead><tbody><tr><td>Llama 3.2 3B</td><td>47.2</td><td>⚠️ Repetition loops</td><td>✅ Near-lossless</td></tr><tr><td>Mistral 7B</td><td>22.5</td><td>✅ Near-lossless</td><td>✅ Near-lossless</td></tr><tr><td>Falcon3 7B</td><td>22.1</td><td>✅ Near-lossless</td><td>✅ Near-lossless</td></tr><tr><td>Qwen3 4B</td><td>38.7</td><td>✅ Near-lossless</td><td>⚠️ Early stop</td></tr><tr><td>Qwen3 8B</td><td>20.6</td><td>⚠️ Partial</td><td>⚠️ Partial</td></tr><tr><td>Gemma-4</td><td>19.3</td><td>✅ Near-lossless</td><td>✅ Near-lossless</td></tr><tr><td>Qwen2.5 32B</td><td>7.1</td><td>✅ Near-lossless</td><td>✅ Near-lossless</td></tr></tbody></table>
<p><strong>Key finding:</strong> 4-bit is near-lossless on 5 of 7 models. 3-bit is near-lossless on 4 of 7. 2-bit (11.6× compression) breaks generation on all models tested — which matches the theoretical SNR floor (~4 dB at 2-bit vs ~10 dB at 4-bit).</p>
<p>The Qwen3 family underperforms because its thinking-mode models generate a <code>&lt;think&gt;</code> token stream that terminates early when attention quality degrades — a more sensitive indicator than output text quality.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-had-to-rebuild-for-apple-silicon">What I Had to Rebuild for Apple Silicon<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#what-i-had-to-rebuild-for-apple-silicon" class="hash-link" aria-label="Direct link to What I Had to Rebuild for Apple Silicon" title="Direct link to What I Had to Rebuild for Apple Silicon" translate="no">​</a></h2>
<p>The TurboQuant paper targets NVIDIA GPUs with CUDA kernels. Porting to MLX required rebuilding several components from scratch:</p>
<p><strong>1. No in-place mutation.</strong> MLX uses lazy evaluation — array slices return views, not copies, and <code>array[i] = value</code> silently fails. Every cache write goes through numpy (pre-allocate numpy arrays, write indices into numpy, hand to MLX only for compute).</p>
<p><strong>2. No sub-byte dtypes.</strong> The <code>BitPackBuffer</code> class handles arbitrary bit-widths in pure numpy, with vectorized unpack for b ∈ {1,2,4} and a loop path for b=3.</p>
<p><strong>3. Metal-accelerated Hadamard.</strong> MLX exposes <code>mx.hadamard_transform</code> natively. Using it instead of the paper's QR rotation reduces rotation cost from O(d²) to O(d log d) — the entire operation runs on the GPU die of the M-series chip.</p>
<p><strong>4. No monkey-patching.</strong> The original <code>turboquant-mlx</code> research code patched <code>mlx_lm.scaled_dot_product_attention()</code> globally. VeloxQuant-MLX exposes a standalone <code>cache.attend(q)</code> method — no global side effects, clean integration with any framework.</p>
<p><strong>5. Pluggable architecture.</strong> The production library uses a Factory + Strategy + Registry pattern so you can swap quantizers at runtime without changing model code:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_method</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_prod"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(98, 114, 164)"># swap to "polar", "qjl", "turboquant_mse"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_head_dim</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_bit_width</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">inlier</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_jl_dim</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="three-algorithms-one-interface">Three Algorithms, One Interface<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#three-algorithms-one-interface" class="hash-link" aria-label="Direct link to Three Algorithms, One Interface" title="Direct link to Three Algorithms, One Interface" translate="no">​</a></h2>
<p>VeloxQuant-MLX implements three quantization algorithms behind the same <code>KVCache</code> interface:</p>
<p><strong>TurboQuantProd</strong> — Rotation + Lloyd-Max (b-1 bits) + QJL residual (1 bit). Best quality-per-bit for production use.</p>
<p><strong>TurboQuantMSE</strong> — Rotation + Lloyd-Max (b bits), no residual correction. Lower memory overhead, slightly lower quality.</p>
<p><strong>PolarQuantizer</strong> — Recursive polar coordinate decomposition. Represents each vector as a sequence of angles at 4 levels, plus a final radius. Geometrically motivated — useful for very low-bit regimes.</p>
<p><strong>QJLQuantizer</strong> — Pure 1-bit: sign sketch only. Extreme compression (32× vs fp16), loses most content but preserves attention topology. Useful for very long contexts where quality is secondary to fitting in RAM.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="installing-and-using-it">Installing and Using It<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#installing-and-using-it" class="hash-link" aria-label="Direct link to Installing and Using It" title="Direct link to Installing and Using It" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> VeloxQuant-MLX</span><br></div></code></pre></div></div>
<p>Requires Python ≥ 3.11 and an Apple Silicon Mac.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> mlx_kv_quant </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">core </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> mx</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> numpy </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">as</span><span class="token plain"> np</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">cache </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_method</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_prod"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_head_dim</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_bit_width</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">inlier</span><span class="token operator">=</span><span class="token number">4</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_jl_dim</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">with_seed</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">42</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">build</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">rng </span><span class="token operator">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">random</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">default_rng</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">for</span><span class="token plain"> _ </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">range</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">1000</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    k </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">rng</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">standard_normal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">astype</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">float16</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    v </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">rng</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">standard_normal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">astype</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">float16</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> v</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">q </span><span class="token operator">=</span><span class="token plain"> mx</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">array</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">rng</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">standard_normal</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token number">128</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">astype</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">np</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">float16</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">output </span><span class="token operator">=</span><span class="token plain"> cache</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">attend</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">q</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">print</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)">f"Memory: </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation">cache</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token string-interpolation interpolation">memory_bytes</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token string-interpolation interpolation"> </span><span class="token string-interpolation interpolation operator">/</span><span class="token string-interpolation interpolation"> </span><span class="token string-interpolation interpolation number">1024</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token string-interpolation interpolation format-spec">.1f</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)"> KB for </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token string-interpolation interpolation builtin" style="color:rgb(189, 147, 249)">len</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string-interpolation interpolation">cache</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token string-interpolation string" style="color:rgb(255, 121, 198)"> tokens"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token comment" style="color:rgb(98, 114, 164)"># Memory: 193.0 KB for 1000 tokens  (vs 500.0 KB fp16)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's Next<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next" translate="no">​</a></h2>
<p>The main bottleneck right now is the Python-level encode/decode loop. Every token requires a numpy↔MLX transfer per attention head per layer — that's <code>n_layers × n_heads</code> small copies per generation step. A fused Metal kernel would reduce this to near-zero overhead.</p>
<p>Other items on the roadmap:</p>
<ul>
<li class=""><strong>Perplexity benchmark</strong> on WikiText-2 for quantitative quality measurement</li>
<li class=""><strong>Value compression</strong> — int8 per-token is implemented, needs tuning at longer contexts</li>
<li class=""><strong>Longer context evaluation</strong> — 8K, 32K token sequences where KV memory dominates</li>
<li class=""><strong>Fused attention kernel</strong> — integrate <code>BitPackBuffer</code> unpack + Lloyd-Max decode + attention dot product into a single Metal dispatch</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="references">References<a href="https://veloxquant-mlx.netlify.app/docs/blog/10-model-study#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://arxiv.org/abs/2504.19874" target="_blank" rel="noopener noreferrer" class="">TurboQuant (ICLR 2026)</a> — Zandieh et al., <em>"Online Vector Quantization with Near-optimal Distortion Rate"</em></li>
<li class=""><a href="https://arxiv.org/abs/2502.02617" target="_blank" rel="noopener noreferrer" class="">PolarQuant (AISTATS 2026)</a> — <em>"PolarQuant: Quantizing KV Caches with Polar Transformation"</em></li>
<li class=""><a href="https://arxiv.org/abs/2406.03482" target="_blank" rel="noopener noreferrer" class="">QJL (2024)</a> — Zandieh et al., <em>"QJL: 1-Bit Quantized JL Transform for KV Cache Quantization"</em></li>
<li class=""><a href="https://github.com/ml-explore/mlx" target="_blank" rel="noopener noreferrer" class="">Apple MLX</a></li>
<li class=""><a href="https://pypi.org/project/VeloxQuant-MLX/" target="_blank" rel="noopener noreferrer" class="">VeloxQuant-MLX on PyPI</a></li>
</ul>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="quantization" term="quantization"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="kv-cache" term="kv-cache"/>
        <category label="benchmarks" term="benchmarks"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[VeloxQuant-MLX: Fast KV Cache Quantization for Apple Silicon]]></title>
        <id>https://veloxquant-mlx.netlify.app/docs/blog/overview</id>
        <link href="https://veloxquant-mlx.netlify.app/docs/blog/overview"/>
        <updated>2026-05-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[TL;DR: The KV cache is the last major unoptimised bottleneck for local LLM inference on Apple Silicon. llama.cpp, Ollama, and LM Studio don't compress it. I built VeloxQuant-MLX to fix that — 9 quantization algorithms, custom Metal GPU kernels, up to 16× compression, plug into mlx_lm with 3 lines.]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p><strong>TL;DR:</strong> The KV cache is the last major unoptimised bottleneck for local LLM inference on Apple Silicon. llama.cpp, Ollama, and LM Studio don't compress it. I built VeloxQuant-MLX to fix that — 9 quantization algorithms, custom Metal GPU kernels, up to 16× compression, plug into <code>mlx_lm</code> with 3 lines.</p>
</blockquote>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-wall-every-mac-llm-user-hits">The wall every Mac LLM user hits<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#the-wall-every-mac-llm-user-hits" class="hash-link" aria-label="Direct link to The wall every Mac LLM user hits" title="Direct link to The wall every Mac LLM user hits" translate="no">​</a></h2>
<p>You've got a MacBook with an M-series chip. You've downloaded llama.cpp, or Ollama, or LM Studio. You load a 7B model, start a conversation, and for the first few hundred tokens everything feels fast.</p>
<p>Then you push it. A long document. A complex coding task. A multi-turn conversation that's been going for a while. And suddenly — generation slows to a crawl, the fans spin up, or the process just dies.</p>
<p>This isn't a bug in those tools. It's a fundamental memory problem that none of them solve. And it lives in a part of the inference stack that almost nobody talks about: <strong>the KV cache</strong>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-kv-cache-actually-is">What the KV cache actually is<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#what-the-kv-cache-actually-is" class="hash-link" aria-label="Direct link to What the KV cache actually is" title="Direct link to What the KV cache actually is" translate="no">​</a></h2>
<p>To understand the problem, you need to understand what happens inside a transformer when it generates text.</p>
<p>Every token in your context window requires the model to compute two matrices at every layer — a <strong>key</strong> and a <strong>value</strong>. These represent what that token "means" in context. Without caching, generating each new token would require recomputing these for every prior token — quadratic cost that makes long-context generation completely impractical.</p>
<p>So instead, the model stores them. Every time a token is processed, its key and value matrices are written to a cache. Future tokens look them up instead of recomputing them.</p>
<p>This is the KV cache. And it grows with every token you generate.</p>
<p>The math is simple:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">memory = num_layers × num_kv_heads × head_dim × seq_len × 2 (K+V) × 2 bytes (fp16)</span><br></div></code></pre></div></div>
<p>For <strong>Llama-3.1-8B at 8k context: 4.2 GB</strong>. At 32k context: <strong>16.8 GB</strong>. On a machine with 16 GB of total unified memory.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-apple-silicon-makes-this-worse-not-better">Why Apple Silicon makes this worse, not better<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#why-apple-silicon-makes-this-worse-not-better" class="hash-link" aria-label="Direct link to Why Apple Silicon makes this worse, not better" title="Direct link to Why Apple Silicon makes this worse, not better" translate="no">​</a></h2>
<p>On a discrete GPU setup — say, an NVIDIA card with 24 GB VRAM — the GPU has its own dedicated memory pool. The KV cache lives there, separate from your system RAM.</p>
<p>Apple Silicon doesn't work that way. M-series chips use <strong>unified memory</strong>: the CPU, GPU, and Neural Engine all share the same physical memory pool. Your model weights, your KV cache, your browser tabs, your IDE, your OS — all competing for the same 16 or 24 GB.</p>
<p>A 7B model at 4-bit quantisation takes roughly 4–5 GB. Add OS and background processes (~4 GB), Chrome with a few tabs (~2 GB), your IDE (~1 GB). You're at 11–12 GB before you've generated a single token. On a 16 GB machine, you have 4 GB left for the KV cache — which runs out around 6k context for a 7B model.</p>
<p>On a 24 GB machine it's better, but not solved. During the development of VeloxQuant-MLX, I ran a benchmark sweep across 8 models. <strong>Qwen2.5-32B failed every quantization config</strong> because the weights alone (~17.5 GB) plus OS and development tools left negative headroom for activations and cache buffers. The memory watchdog had to kill the process before it triggered a kernel panic:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">[07:28:09] free=62MB   inactive=1094MB pressure=unknown</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">[07:28:19] free=63MB   inactive=891MB  pressure=unknown</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">[07:28:19] CRITICAL: only 954MB available — killing PID 21438</span><br></div></code></pre></div></div>
<p>System recovered to 10.2 GB free immediately after the kill, with no GPU stall.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-existing-tools-do-about-this-essentially-nothing">What existing tools do about this: essentially nothing<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#what-existing-tools-do-about-this-essentially-nothing" class="hash-link" aria-label="Direct link to What existing tools do about this: essentially nothing" title="Direct link to What existing tools do about this: essentially nothing" translate="no">​</a></h2>
<p>This is the part that surprised me most when I started digging.</p>
<p><strong>llama.cpp</strong> has done extraordinary work on attention kernel optimisation, weight quantisation, batching, and speculative decoding. But the KV cache is stored at fp16 by default. There is experimental support (<code>--cache-type-k q8_0</code>) but it's not the default, not Metal-optimised, and the quantisation methods are basic scalar quantisation with no adaptation to actual key distributions.</p>
<p><strong>Ollama</strong> builds on top of llama.cpp and inherits all of this. The UX is excellent. The KV cache situation is unchanged.</p>
<p><strong>LM Studio</strong> adds a polished GUI and nice model management. Same story on the cache.</p>
<p><strong>mlx_lm</strong> — Apple's own MLX-based inference library — is the most natural fit for Apple Silicon and does excellent work. But its default KV cache is full fp16. No built-in compression.</p>
<p>All of these tools have optimised everything <em>around</em> the KV cache. None of them have solved the KV cache itself.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-insight-compress-the-cache-not-just-the-weights">The insight: compress the cache, not just the weights<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#the-insight-compress-the-cache-not-just-the-weights" class="hash-link" aria-label="Direct link to The insight: compress the cache, not just the weights" title="Direct link to The insight: compress the cache, not just the weights" translate="no">​</a></h2>
<p>Most quantisation work in the LLM ecosystem targets <strong>weight quantisation</strong> — compressing model parameters from fp16 down to 8-bit, 4-bit, or lower. This is what GGUF, GPTQ, AWQ, and most Ollama models use.</p>
<p>Weight quantisation is a one-time operation. You compress once, save, load the compressed version.</p>
<p>KV cache quantisation is different. It happens <strong>at inference time</strong>, on every token, for every layer. The keys and values are different every generation — you can't pre-compute anything.</p>
<p>The key insight is that while weight distributions are relatively uniform, <strong>KV cache distributions are not</strong>. Keys tend to follow Gaussian or Laplacian distributions after a rotation transform. This structure can be exploited with much more aggressive compression than weight quantisation — down to <strong>1 bit per dimension</strong> with surprisingly low quality loss.</p>
<p>The reason this works at such extreme bit rates: the attention computation only needs an <em>approximation</em> of the inner product <code>q · k</code>. Small errors in the reconstructed key vector translate to small errors in the attention score, which average out across many keys in the softmax.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-built-veloxquant-mlx">What I built: VeloxQuant-MLX<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#what-i-built-veloxquant-mlx" class="hash-link" aria-label="Direct link to What I built: VeloxQuant-MLX" title="Direct link to What I built: VeloxQuant-MLX" translate="no">​</a></h2>
<p>VeloxQuant-MLX is a KV cache compression library for Apple Silicon, built on top of MLX. It implements nine quantisation algorithms — from zero-calibration 1-bit methods to mixed-precision allocators — all backed by custom Metal GPU kernels compiled at runtime.</p>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-algorithms">The algorithms<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#the-algorithms" class="hash-link" aria-label="Direct link to The algorithms" title="Direct link to The algorithms" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="turboquant-rvq--the-recommended-default">TurboQuant RVQ — the recommended default<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#turboquant-rvq--the-recommended-default" class="hash-link" aria-label="Direct link to TurboQuant RVQ — the recommended default" title="Direct link to TurboQuant RVQ — the recommended default" translate="no">​</a></h4>
<p>Zero calibration. Works on any model immediately. Uses Residual Vector Quantisation with analytical Gaussian and Laplacian codebooks precomputed from distribution theory.</p>
<p>Two residual passes at 1 bit each → <strong>7.5× compression</strong> with cosine similarity above 0.97.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> mlx_lm</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> veloxquant_mlx </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> KVCacheConfig</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">load</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">"mlx-community/Llama-3.2-3B-Instruct-4bit"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">config </span><span class="token operator">=</span><span class="token plain"> KVCacheConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">method</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"turboquant_rvq"</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> bit_width_inlier</span><span class="token operator">=</span><span class="token number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> seed</span><span class="token operator">=</span><span class="token number">42</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">caches </span><span class="token operator">=</span><span class="token plain"> KVCacheBuilder</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">for_model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> config</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">make_cache </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">lambda</span><span class="token plain"> </span><span class="token operator">*</span><span class="token plain">_a</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token operator">**</span><span class="token plain">_k</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> caches</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">response </span><span class="token operator">=</span><span class="token plain"> mlx_lm</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">model</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> tokenizer</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    prompt</span><span class="token operator">=</span><span class="token string" style="color:rgb(255, 121, 198)">"Write a 3000-word analysis of the transformer architecture."</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    max_tokens</span><span class="token operator">=</span><span class="token number">3000</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><br></div></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="vecinfer--16-with-metal-acceleration">VecInfer — 16× with Metal acceleration<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#vecinfer--16-with-metal-acceleration" class="hash-link" aria-label="Direct link to VecInfer — 16× with Metal acceleration" title="Direct link to VecInfer — 16× with Metal acceleration" translate="no">​</a></h4>
<p>Trades a 2-minute calibration step for <strong>16× compression</strong> via Product Vector Quantisation. Per-channel smooth scaling handles outlier dimensions that defeat standard VQ. The hot path runs through a Metal GPU kernel that is <strong>13× faster</strong> than equivalent MLX Python ops.</p>
<p>Standout result: <strong>Qwen2.5-7B VecInfer-1bit exceeds fp16 throughput</strong> at 16× compression, likely due to its strong GQA ratio reducing the number of KV heads.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="ratequant--best-accuracy-per-bit">RateQuant — best accuracy per bit<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#ratequant--best-accuracy-per-bit" class="hash-link" aria-label="Direct link to RateQuant — best accuracy per bit" title="Direct link to RateQuant — best accuracy per bit" translate="no">​</a></h4>
<p>Runs a 90-second sensitivity probe across all transformer layers, learns which layers are most sensitive to quantisation noise, then uses reverse-waterfilling to allocate more bits to sensitive layers and fewer to insensitive ones.</p>
<p>At 2.0 average bits, RateQuant achieves <strong>2.7× lower perplexity degradation</strong> than uniform 2-bit quantisation at identical memory cost.</p>
<table><thead><tr><th>Model</th><th>Sensitivity ratio</th><th>Allocation</th><th>Result vs fp16</th></tr></thead><tbody><tr><td>Falcon3-7B</td><td>6.48×</td><td>14 × b=2, 14 × b=1</td><td><strong>100%</strong> at 5.22× compression</td></tr><tr><td>Gemma3-4B</td><td>14.39×</td><td>3 × b=3, 11 × b=2, 20 × b=1</td><td><strong>91%</strong> at 5.22× compression</td></tr></tbody></table>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="spectralquant--best-quality-at-long-context">SpectralQuant — best quality at long context<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#spectralquant--best-quality-at-long-context" class="hash-link" aria-label="Direct link to SpectralQuant — best quality at long context" title="Direct link to SpectralQuant — best quality at long context" translate="no">​</a></h4>
<p>Keys in transformer models concentrate ~96% of their variance in just 3–4% of dimensions. SpectralQuant rotates keys into their eigenvector basis via SVD, then applies separate codebooks to the "signal" dimensions (high variance) and "noise" dimensions (low variance).</p>
<table><thead><tr><th>Model</th><th>SpectralQuant</th><th>TurboQuant 3-bit</th><th>Quality gain</th></tr></thead><tbody><tr><td>Qwen2.5-0.5B</td><td>0.9072 cosim</td><td>0.8329 cosim</td><td><strong>+7.4pp</strong></td></tr><tr><td>Gemma 4 4B</td><td>0.8625 cosim</td><td>0.7581 cosim</td><td><strong>+10.4pp</strong></td></tr></tbody></table>
<p>At 16k context the rotation trick matters enormously — quantisation errors accumulate over long sequences, and aligning the codec to the actual variance structure cuts that accumulation dramatically.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="rabitq--maximum-context-length">RaBitQ — maximum context length<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#rabitq--maximum-context-length" class="hash-link" aria-label="Direct link to RaBitQ — maximum context length" title="Direct link to RaBitQ — maximum context length" translate="no">​</a></h4>
<p>1-bit key compression via randomised Hadamard transform + binary sign packing + IVF clustering. Pairs with 4-bit MSE scalar quantisation on values for <strong>6× full KV compression</strong>.</p>
<table><thead><tr><th>Method</th><th>KV memory @ 1024 tok</th><th>Compression</th><th>Context @ 8 GB</th></tr></thead><tbody><tr><td>fp16 baseline</td><td>117.4 MB</td><td>1×</td><td>~17k tokens</td></tr><tr><td>RaBitQ keys + MSE-b4 values</td><td><strong>19.7 MB</strong></td><td><strong>6×</strong></td><td><strong>~103k tokens</strong></td></tr></tbody></table>
<p>6× more context in the same RAM budget.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="commvq--rope-compatible-exact-vq-icml-2025">CommVQ — RoPE-compatible exact VQ (ICML 2025)<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#commvq--rope-compatible-exact-vq-icml-2025" class="hash-link" aria-label="Direct link to CommVQ — RoPE-compatible exact VQ (ICML 2025)" title="Direct link to CommVQ — RoPE-compatible exact VQ (ICML 2025)" translate="no">​</a></h4>
<p>Standard VQ breaks with RoPE positional encodings because <code>quantize(rotate(x)) ≠ rotate(quantize(x))</code>. CommVQ (Apple ML Research, ICML 2025) trains codebooks on pre-RoPE keys with a commutativity projection in the EM M-step. RoPE is applied exactly at decode time. <strong>64× key compression</strong> with exact positional encoding.</p>
<hr>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-metal-kernels">The Metal kernels<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#the-metal-kernels" class="hash-link" aria-label="Direct link to The Metal kernels" title="Direct link to The Metal kernels" translate="no">​</a></h3>
<p>All the compression math in the world doesn't help if the quantisation itself is slow. VeloxQuant-MLX compiles nine Metal GPU kernels at runtime using <code>mx.fast.metal_kernel</code>:</p>
<table><thead><tr><th>Kernel</th><th>What it does</th><th>Speedup</th></tr></thead><tbody><tr><td><code>vecinfer_quantize_metal</code></td><td>Fused nearest-centroid product VQ</td><td><strong>13×</strong></td></tr><tr><td><code>rabitq_hamming_score</code></td><td>XOR + popcount Hamming distance</td><td><strong>11×</strong></td></tr><tr><td><code>turboquant_hadamard_quantize</code></td><td>WHT rotation + scalar quant fused</td><td><strong>8.6×</strong></td></tr><tr><td><code>turboquant_fused_rvq_decode_attend</code></td><td>RVQ decode + attention in one dispatch</td><td><strong>6.9×</strong></td></tr><tr><td><code>metal_fused_sdpa</code></td><td>Dequant + scaled dot-product attention</td><td>avoids fp16 materialisation</td></tr></tbody></table>
<p>The fused SDPA kernel is the most impactful. Without fusion, dequantisation creates a full fp16 key matrix in memory before attention — which defeats much of the point of compression. The fused path keeps the cache compressed all the way until attention scores are computed.</p>
<p>The 30-line Metal kernel that powers VecInfer's 13× speedup:</p>
<div class="language-metal codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-metal codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">// One thread per sub-vector. Argmin lives in registers — no diff tensor.</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint vec_idx = thread_position_in_grid.x;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">float best_dist = INFINITY;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">uint  best_idx  = 0;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">for (uint c = 0; c &lt; n_centroids; ++c) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    float dist = 0.0f;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    for (uint i = 0; i &lt; sub_dim; ++i) {</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        float d = float(x[x_base + i]) - float(codebook[cb_base + i]);</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">        dist += d * d;</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    if (dist &lt; best_dist) { best_dist = dist; best_idx = c; }</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">}</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">out[vec_idx] = best_idx;</span><br></div></code></pre></div></div>
<p>The key insight: the <code>[N, n_centroids, sub_dim]</code> diff tensor is never materialised. The argmin accumulator lives entirely in thread-local registers — <strong>98% peak memory reduction</strong> at long context shapes.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="real-numbers-across-10-models">Real numbers across 10 models<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#real-numbers-across-10-models" class="hash-link" aria-label="Direct link to Real numbers across 10 models" title="Direct link to Real numbers across 10 models" translate="no">​</a></h2>
<p>End-to-end <code>mlx_lm.generate</code>, 200-token prompt, 120-token generation, Apple M-series:</p>
<table><thead><tr><th>Model</th><th>fp16 tok/s</th><th>RVQ-1bit tok/s</th><th>VecInfer-1bit tok/s</th><th>VecInfer compression</th></tr></thead><tbody><tr><td>Llama-3.2-3B</td><td>47.6</td><td><strong>46.2</strong></td><td>40.2</td><td>16×</td></tr><tr><td>Llama-3.1-8B</td><td>20.5</td><td><strong>20.6</strong></td><td>19.6</td><td>16×</td></tr><tr><td>Mistral-7B</td><td>23.6</td><td><strong>22.8</strong></td><td>9.8</td><td>16×</td></tr><tr><td>Qwen2.5-7B</td><td>21.0</td><td>20.7</td><td><strong>21.5</strong> ⬆ exceeds fp16</td><td>16×</td></tr><tr><td>Falcon3-7B</td><td>17.3</td><td><strong>21.7</strong></td><td>17.0</td><td>16×</td></tr><tr><td>Gemma-3-4B</td><td>26.0</td><td>24.2</td><td><strong>22.6</strong></td><td>16×</td></tr></tbody></table>
<p><strong>RVQ-1bit</strong> is the safe default — within 5% of fp16 on most 7–8B models with zero calibration. <strong>VecInfer-1bit</strong> wins on compression (always 16×) and throughput on models with strong Grouped Query Attention ratios.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-means-in-practice">What this means in practice<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#what-this-means-in-practice" class="hash-link" aria-label="Direct link to What this means in practice" title="Direct link to What this means in practice" translate="no">​</a></h2>
<p>On a <strong>16 GB MacBook M3</strong>:</p>
<ul>
<li class="">Before: 7B model maxes out around 6k context before generation slows</li>
<li class="">After with RVQ 1-bit: that same cache now fits in ~450 MB, leaving headroom for 50k+ token contexts</li>
</ul>
<p>On a <strong>24 GB MacBook M3 Pro</strong>:</p>
<ul>
<li class="">Before: 13B models are borderline; long conversations risk OOM</li>
<li class="">After: 13B at 32k context fits comfortably; the cache that would have consumed 8 GB uses 500 MB</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quick-decision-guide">Quick decision guide<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#quick-decision-guide" class="hash-link" aria-label="Direct link to Quick decision guide" title="Direct link to Quick decision guide" translate="no">​</a></h2>
<table><thead><tr><th>Goal</th><th>Method</th></tr></thead><tbody><tr><td>No calibration, get started now</td><td><code>turboquant_rvq</code> b=1 — 7.5× compression</td></tr><tr><td>Maximum compression</td><td><code>vecinfer</code> 1-bit — 16× (needs 2 min calibration)</td></tr><tr><td>Best quality at any bit rate</td><td><code>spectral</code> b=3 — 5.33× with +7–10pp cosine sim gain</td></tr><tr><td>Best accuracy per average bit</td><td><code>ratequant</code> — 2.7× better than uniform at same memory</td></tr><tr><td>Maximum context length</td><td><code>rabitq</code> keys + MSE-b4 values — 6× full KV, 6× more context</td></tr><tr><td>RoPE-compatible exact VQ</td><td><code>comm_vq</code> — 64× key compression, exact positions</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it">Try it<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#try-it" class="hash-link" aria-label="Direct link to Try it" title="Direct link to Try it" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">pip </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"> VeloxQuant-MLX</span><br></div></code></pre></div></div>
<p>Requires macOS on Apple M1 or later · Python 3.11+ · MLX ≥ 0.18</p>
<p>📖 Documentation: <a href="https://veloxquant-mlx.netlify.app/docs/" target="_blank" rel="noopener noreferrer" class="">https://veloxquant-mlx.netlify.app/docs/</a>
📦 PyPI: <a href="https://pypi.org/project/VeloxQuant-MLX/" target="_blank" rel="noopener noreferrer" class="">https://pypi.org/project/VeloxQuant-MLX/</a>
⭐ GitHub: <a href="https://github.com/rajveer43/turboquant_mac_implementation" target="_blank" rel="noopener noreferrer" class="">https://github.com/rajveer43/turboquant_mac_implementation</a></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's next<a href="https://veloxquant-mlx.netlify.app/docs/blog/overview#whats-next" class="hash-link" aria-label="Direct link to What's next" title="Direct link to What's next" translate="no">​</a></h2>
<ul>
<li class=""><strong>Vision-language model support</strong> — visual tokens have fundamentally different KV distributions; heavy outliers require adapted algorithms</li>
<li class=""><strong>Per-head RateQuant granularity</strong> — the paper allocates bits per layer-head group; current implementation is per-layer, leaving ~30% accuracy improvement on the table</li>
<li class=""><strong>Gradient-based sensitivity</strong> for RateQuant — more accurate than activation-based, at the cost of a slightly longer calibration pass</li>
</ul>
<hr>
<p>The KV cache is the last major unoptimised bottleneck for local LLM inference on Apple Silicon. llama.cpp, Ollama, and LM Studio have built excellent tools — but none of them have solved this. VeloxQuant-MLX is my attempt.</p>
<p>If you're running models locally on a Mac and hitting memory walls — at any context length, on any model — I'd genuinely like to hear about it.</p>
<hr>
<p><em>MIT License · Built for Apple Silicon · Engineered for speed</em></p>]]></content>
        <author>
            <name>Rajveer Rathod</name>
            <uri>https://github.com/rajveer43</uri>
        </author>
        <category label="quantization" term="quantization"/>
        <category label="apple-silicon" term="apple-silicon"/>
        <category label="mlx" term="mlx"/>
        <category label="kv-cache" term="kv-cache"/>
    </entry>
</feed>