<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Linear-Attention on MdJawad</title><link>https://www.mdjawad.com/tags/linear-attention/</link><description>Recent content in Linear-Attention on MdJawad</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 11 Aug 2026 13:20:33 +0000</lastBuildDate><atom:link href="https://www.mdjawad.com/tags/linear-attention/index.xml" rel="self" type="application/rss+xml"/><item><title>Kimi K3's Architecture: Kimi Delta Attention, Attention Residuals, and 896 Experts</title><link>https://www.mdjawad.com/posts/kimi-k3/</link><pubDate>Tue, 11 Aug 2026 21:06:00 +0800</pubDate><guid>https://www.mdjawad.com/posts/kimi-k3/</guid><description>Softmax costs O(n squared), so what you can afford depends only on how long the axis is. Kimi K3 drops softmax attention from three sequence layers in every four, and in the same model buys it on the depth axis. We derive the delta rule behind Kimi Delta Attention, work out why its decay carries a floor of exactly -5, unroll a residual connection to see what Attention Residuals replaces, and follow quantile balancing back to the dual it solves.</description><content:encoded><![CDATA[<h2 id="what-this-post-covers">What this post covers</h2>
<p>Moonshot AI published the Kimi K3 weights on 26 July 2026 and the technical report the following day. It is a mixture of experts with 2.78 trillion total parameters and 104.2 billion active per token, across 93 layers, with 896 routed experts and a one-million-token context window. The MXFP4 checkpoint comes to roughly 1.4 terabytes, so loading it takes a full node of accelerators.</p>
<p>Here is the idea the architecture turns on, stated up front. Letting every item on an axis see every other costs $O(n^2)$, so what that costs depends on how long the axis is. K3 moves information along three axes of very different lengths: the sequence is a million tokens, the width is 896 experts, and the depth is 93 layers grouped into 9. That difference is why K3 replaces softmax with a recurrence on three sequence layers in every four, and in the same model installs softmax attention on the depth axis where a plain residual connection used to be. Two moves in opposite directions, on two axes four orders of magnitude apart in length.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Kimi K2</th>
          <th>Kimi K3</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Total parameters</td>
          <td>1.04T</td>
          <td>2.78T</td>
      </tr>
      <tr>
          <td>Active per token</td>
          <td>32.6B</td>
          <td>104.2B</td>
      </tr>
      <tr>
          <td>Layers</td>
          <td>61</td>
          <td>93</td>
      </tr>
      <tr>
          <td>Attention</td>
          <td>61 MLA</td>
          <td>69 KDA + 24 Gated MLA</td>
      </tr>
      <tr>
          <td>Positional encoding</td>
          <td>RoPE</td>
          <td>none (NoPE)</td>
      </tr>
      <tr>
          <td>Routed experts / active</td>
          <td>384 / 8</td>
          <td>896 / 16</td>
      </tr>
      <tr>
          <td>Shared experts</td>
          <td>1</td>
          <td>2</td>
      </tr>
      <tr>
          <td>Hidden dim / MoE latent dim</td>
          <td>7,168 / n/a</td>
          <td>7,168 / 3,584</td>
      </tr>
      <tr>
          <td>Activation</td>
          <td>SwiGLU</td>
          <td>SiTU-GLU</td>
      </tr>
      <tr>
          <td>Training context</td>
          <td>128K</td>
          <td>1M</td>
      </tr>
  </tbody>
</table>
<p>The attention design was validated first on <a href="https://arxiv.org/abs/2510.26692">Kimi Linear</a>, a 48B model published in October 2025, where it cut KV-cache usage by up to 75% and delivered up to 6× the decoding throughput at a one-million-token context, with time per output token at 1M falling from 11.48 ms to 1.84 ms against full MLA. Those figures belong to the 48B model rather than to K3, which does not publish equivalents. On independent leaderboards K3 places fourth of 580 on Artificial Analysis&rsquo; index, second on Vals, and first on WebDev Arena, making it the first open-weights model to top that board.</p>
<p><strong>Who this is for.</strong> Someone who knows how a Transformer works and has met attention and mixture-of-experts before, but has not read the K3 report. Where the argument leans on something specific I link out rather than re-explain it: <a href="/posts/attention-evolution/">multi-head latent attention</a>, <a href="/posts/state-space-models-mamba/">linear attention and the Mamba lineage</a>, and <a href="/posts/rotary-positional-encoding/">rotary embeddings</a>.</p>
<p><strong>What this leaves out.</strong> There is nothing here on the vision tower, the Per-Head Muon optimiser, post-training, or what the design costs to serve, each of which would need its own post.</p>
<h2 id="the-three-axes-and-what-softmax-costs-on-each">The three axes, and what softmax costs on each</h2>
<p>Let&rsquo;s start with the architecture as a whole. Moonshot&rsquo;s overview figure lays it out along the same three axes:</p>
<p><img alt="The Kimi K3 architecture. The backbone on the right stacks three KDA layers and one Gated MLA layer per block, each attention layer followed by a Stable LatentMoE feed-forward network. Learned pseudo-queries w produce attention weights alpha that reach back over the embedding and every preceding block output. The top-left inset expands the Stable LatentMoE module with its shared and routed experts; the bottom-left inset expands the KDA module; the bottom-right pathway feeds MoonViT-V2 through a projector into the shared embedding space." loading="lazy" src="/images/posts/kimi-k3/architecture.png"></p>
<p><em>Figure 2 of the Kimi K3 technical report, reproduced without alteration. Kimi Team, <a href="https://arxiv.org/abs/2607.24653">Kimi K3: Open Frontier Intelligence</a>.</em></p>
<p>Reading the right-hand column from the bottom, the 3:1 ratio is visible directly: KDA, then a LatentMoE, three times over, then one Gated MLA and a final LatentMoE to close the block. The two boxed insets on the left are the modules we spend most of our time inside. The maroon lines fanning from every module back down to <code>Block n-1</code>, <code>Block n-2</code> and <code>Embedding</code> are Attention Residuals, which is the depth axis drawn as wiring.</p>
<p>A million tokens, 896 experts, and 93 layers grouped into 9 are three very different lengths, and they set very different prices. The sequence axis carries much the largest, since at a million tokens a single head in a single layer scores $n^2 = 10^{12}$ query-key pairs, and causal masking only halves that. Expensive as this is, it stops well short of impossible, and K3 pays it on the 24 layers where it keeps full attention. What binds is not the prefill but the decode that follows it, because every generated token has to read the whole KV cache before it can produce the next one, so time per output token climbs with context rather than holding flat.</p>
<p>Depth asks the same question of a far smaller number. K3 has 93 layers, and because Block AttnRes groups them into 9 sources the operative $n^2$ is 81 rather than 8,649. Neither figure is large enough to trouble anything, and the report is blunt about it:</p>
<blockquote>
<p>Since network depth is modest ($L \lt 100$), the $O(L^2 d)$ arithmetic of this full form is affordable.</p></blockquote>
<p>Width is where the accounting changes character rather than scale. There are 896 experts in a layer, but no expert ever attends over another: the router scores each one on its own, so the work grows linearly with the expert count and no pairwise term arises to be paid at all. What constrains the axis instead is that every expert selected costs a matrix multiply, which is why a model can afford $k$ of them and not $n$. Routing densely to all 896 would cost $E/k = 56$ times what top-$k$ costs, and a factor of 56 is a linear penalty where the sequence axis faced a quadratic one. Width stays hard top-$k$ for that reason, and the problems that make it interesting turn out to lie elsewhere.</p>
<p>Let&rsquo;s put the three side by side:</p>

<div class="kimi-axis-budget" id="kimi-axis-budget-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-axis-budget{
       
      --bg:var(--viz-bg); --bg2:var(--viz-panel); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .kimi-axis-budget *{box-sizing:border-box}
    .kimi-axis-budget .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .kimi-axis-budget .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-axis-budget .panel > *{position:relative}
    .kimi-axis-budget .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-axis-budget .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px}

    .kimi-axis-budget .axes{display:flex; flex-direction:column; gap:9px}
    .kimi-axis-budget .ax{display:grid; grid-template-columns:120px 1fr 150px; gap:14px; align-items:center; border:1px solid var(--line); border-radius:11px; padding:12px 14px; background:var(--panel2); cursor:pointer; transition:.22s; font-family:inherit; color:var(--ink); text-align:left; width:100%}
    @media(max-width:720px){.kimi-axis-budget .ax{grid-template-columns:1fr; gap:9px}}
    .kimi-axis-budget .ax:hover{border-color:var(--q)}
    .kimi-axis-budget .ax.on{border-color:color-mix(in oklab, var(--q) 55%, transparent); background:color-mix(in oklab, var(--q) 6%, transparent)}
    .kimi-axis-budget .ax .nm{font-size:14.5px; font-weight:600; line-height:1.3}
    .kimi-axis-budget .ax .nn{font-family:var(--viz-mono); font-size:10.5px; color:var(--muted); margin-top:3px}

    .kimi-axis-budget .barwrap{position:relative; height:30px; display:flex; align-items:center}
    .kimi-axis-budget .track{position:relative; width:100%; height:9px; border-radius:6px; background:var(--bg); border:1px solid var(--line); overflow:hidden}
    .kimi-axis-budget .fill{position:absolute; left:0; top:0; bottom:0; border-radius:6px; transition:width .45s cubic-bezier(.4,0,.2,1), background .3s}
    .kimi-axis-budget .tick{position:absolute; top:-5px; bottom:-5px; width:1px; background:var(--line-strong); opacity:.55}
    .kimi-axis-budget .scale{display:grid; grid-template-columns:120px 1fr 150px; gap:14px; margin-top:4px; padding:0 14px}
    @media(max-width:720px){.kimi-axis-budget .scale{grid-template-columns:1fr; padding:0 14px}}
    .kimi-axis-budget .ruler{position:relative; height:20px; grid-column:2}
    @media(max-width:720px){.kimi-axis-budget .ruler{grid-column:1}}
    .kimi-axis-budget .rt{position:absolute; top:0; transform:translateX(-50%); font-family:var(--viz-mono); font-size:9.5px; color:var(--faint); white-space:nowrap}
    .kimi-axis-budget .rcap{grid-column:2; font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--faint); margin-top:-2px}
    @media(max-width:720px){.kimi-axis-budget .rcap{grid-column:1}}

    .kimi-axis-budget .verd{font-family:var(--viz-mono); font-size:10.5px; line-height:1.5; text-align:right; color:var(--muted)}
    @media(max-width:720px){.kimi-axis-budget .verd{text-align:left}}
    .kimi-axis-budget .verd b{display:block; font-size:12px; font-weight:600}
    .kimi-axis-budget .verd.ok b{color:var(--k)}
    .kimi-axis-budget .verd.no b{color:var(--coral)}


    .kimi-axis-budget .readouts{display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:16px}
    @media(max-width:560px){.kimi-axis-budget .readouts{grid-template-columns:1fr}}
    .kimi-axis-budget .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--panel2)}
    .kimi-axis-budget .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-axis-budget .chip .num{font-size:18px; margin-top:4px; font-variant-numeric:tabular-nums}

    .kimi-axis-budget .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:15px}
    .kimi-axis-budget .btn{font-family:var(--viz-mono); font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink); background:var(--panel2); border:1px solid var(--line-strong); border-radius:9px; padding:9px 15px; cursor:pointer; transition:.15s}
    .kimi-axis-budget .btn:hover{border-color:var(--q); color:var(--ink); background:color-mix(in oklab, var(--q) 14%, var(--panel2))}
    .kimi-axis-budget button:focus-visible{outline:2px solid var(--q); outline-offset:3px}
    .kimi-axis-budget .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-axis-budget .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">Three axes · what softmax costs on each</div>
    <div class="sub">Click an axis to spend softmax attention on it. The bar is what the chosen mechanism costs, on a log scale.</div>

    <div class="axes o-axes"></div>
    <div class="scale"><div class="ruler o-ruler"></div><div class="rcap">cost per token, per layer &middot; units differ by axis</div></div>


    <div class="readouts">
      <div class="chip"><div class="lab">axes on softmax</div><div class="num o-count">1 of 3</div></div>
      <div class="chip"><div class="lab">dearest axis</div><div class="num o-total">1 × 10⁶</div></div>
      <div class="chip"><div class="lab">sequence vs depth</div><div class="num o-verdict">1.2 &times; 10&#8308;</div></div>
    </div>

    <div class="btnrow">
      <button class="btn b-k3">what K3 chose</button>
      <button class="btn b-all">softmax everywhere</button>
      <button class="btn b-none">softmax nowhere</button>
    </div>
  </div>

  <p class="note"></p>

  <script>
  (function(){
    const root = document.getElementById('kimi-axis-budget-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;

    
    
    
    const LOGMIN = 0, LOGMAX = 13;   

    const AXES = [
      { id:'seq',   name:'Sequence',  n:1000000, nlab:'n = 1,000,000 tokens',
        on:false, k3:false,
        yes:'10¹² scores<br>and a cache that<br>grows with n',
        no:'recurrence:<br>one fixed state,<br>O(1) per token' },
      { id:'depth', name:'Depth',     n:9,       nlab:'n = 93 layers, 9 AttnRes sources',
        on:true,  k3:true,
        yes:'81 scores.<br>free, and unused<br>until AttnRes',
        no:'a plus sign:<br>uniform weights,<br>unbounded sum' },
      { id:'width', name:'Width',     n:896,     nlab:'n = 896 routed experts',
        on:false, k3:false, linear:true, altN:16,
        yes:'dense: run all 896.<br>linear in n,<br>no pairwise term',
        no:'hard top-k:<br>16 of 896,<br>sparsity 56' }
    ];

    const elAxes = root.querySelector('.o-axes');
    const elCount = root.querySelector('.o-count');
    const elTotal = root.querySelector('.o-total');
    const elVerdict = root.querySelector('.o-verdict');
    const elNote = root.querySelector('.note');


    function fmt(x){
      if(x < 1000) return String(Math.round(x));
      const e = Math.floor(Math.log10(x));
      const m = x / Math.pow(10, e);
      const sup = String(e).replace(/[0-9]/g, d => '⁰¹²³⁴⁵⁶⁷⁸⁹'[+d]);
      return (Math.round(m*10)/10) + ' × 10' + sup;
    }

    const elRuler = root.querySelector('.o-ruler');
    (function buildRuler(){
      let h = '';
      for(let e = 0; e <= 12; e += 3){
        const pos = (e - LOGMIN) / (LOGMAX - LOGMIN) * 100;
        const sup = String(e).replace(/[0-9]/g, d => '\u2070\u00b9\u00b2\u00b3\u2074\u2075\u2076\u2077\u2078\u2079'[+d]);
        h += '<span class="rt" style="left:' + pos.toFixed(1) + '%">10' + sup + '</span>';
      }
      elRuler.innerHTML = h;
    })();

    function render(){
      elAxes.innerHTML = AXES.map(a => {
        
        
        const cost = a.linear ? (a.on ? a.n : a.altN)
                              : (a.on ? a.n * a.n : a.n);
        const frac = Math.max(0, Math.min(1, (Math.log10(cost) - LOGMIN) / (LOGMAX - LOGMIN)));
        const col = a.on ? 'var(--q)' : 'var(--k)';
        const aria = a.name + ' axis, ' + a.nlab + '. Softmax is currently ' + (a.on ? 'on' : 'off') +
                     ', costing ' + fmt(cost) + ' scores. Activate to toggle.';
        return '<button class="ax' + (a.on ? ' on' : '') + '" data-id="' + a.id +
          '" aria-pressed="' + a.on + '" aria-label="' + aria + '">' +
          '<div><div class="nm">' + a.name + '</div><div class="nn">' + a.nlab + '</div></div>' +
          '<div class="barwrap"><div class="track">' +
            '<div class="fill" style="width:' + (frac*100).toFixed(1) + '%; background:' + col + '"></div>' +
          '</div></div>' +
          '<div class="verd ' + (a.on ? 'no' : 'ok') + '"><b>' + (a.on ? 'softmax · ' + fmt(cost) : 'alternative') + '</b>' +
            (a.on ? a.yes : a.no) + '</div>' +
        '</button>';
      }).join('');

      const on = AXES.filter(a => a.on);
      
      
      const costFor = a => a.linear ? (a.on ? a.n : a.altN) : (a.on ? a.n*a.n : a.n);
      const worst = Math.max.apply(null, AXES.map(costFor));
      const costOf = id => costFor(AXES.find(x => x.id === id));
      elCount.textContent = on.length + ' of 3';
      elTotal.textContent = fmt(worst);
      elVerdict.textContent = fmt(costOf('seq') / costOf('depth')) + '\u00d7';

      const isK3 = AXES.every(a => a.on === a.k3);
      if(isK3){
        elNote.innerHTML = 'This is K3. Softmax runs on <b>depth</b> and nowhere else, because depth is the only all-to-all axis short enough for the n&sup2; term to stay small. The sequence axis gets a recurrence and the width axis gets hard top-k selection. Note the asymmetry the strip makes visible: the sequence axis still keeps 24 Gated MLA layers, one in every four, because giving up global content interaction entirely costs more than it saves.';
      } else if(AXES.every(a => a.on)){
        elNote.innerHTML = 'Dense mixing everywhere costs about <b>10¹² pairwise scores</b> per head per layer, essentially all of it from the sequence axis. Note that the width row barely moves: running all 896 experts is expensive in FLOPs but it is <b>linear</b> in the expert count, so it never joins the quadratic problem the other two axes have.';
      } else if(AXES.every(a => !a.on)){
        elNote.innerHTML = 'Softmax on no axis at all. That is what every Transformer before this one did, and it leaves the depth axis unused: attending over 9 block-level sources would have cost <b>81 pairwise scores</b>, which was available the whole time.';
      } else {
        elNote.innerHTML = 'Read the magnitudes off the scale rather than the bar lengths, since the axis is logarithmic: with softmax on both, the sequence axis costs about <b>ten orders of magnitude</b> more than the depth axis. The cost of softmax is a function of n alone, and depth has always had a small n.';
      }
    }

    elAxes.addEventListener('click', e => {
      const btn = e.target.closest('.ax'); if(!btn) return;
      const a = AXES.find(x => x.id === btn.dataset.id); if(!a) return;
      a.on = !a.on; render();
    });
    root.querySelector('.b-k3').addEventListener('click', () => { AXES.forEach(a => a.on = a.k3); render(); });
    root.querySelector('.b-all').addEventListener('click', () => { AXES.forEach(a => a.on = true); render(); });
    root.querySelector('.b-none').addEventListener('click', () => { AXES.forEach(a => a.on = false); render(); });

    render();
  })();
  </script>
</div>

<p>Softmax is affordable where the axis is short, and depth is much the shortest. Depth has not gone unexamined: Highway Networks put learned gates on the residual in 2015, DenseNet wired every layer to every later one in 2016, and DeepSeek-V4 ships manifold-constrained hyper-connections to the same end. What those share is that the mixing is fixed or input-gated. What AttnRes changes is the weighting, from a constant to a content-dependent softmax, which is the move the sequence axis made in 2017 and the width axis has never made at all.</p>
<h2 id="the-sequence-axis-linear-attention-and-a-fixed-size-state">The sequence axis: linear attention and a fixed-size state</h2>
<p>Let&rsquo;s start with what has to go. The quadratic score matrix is the part people name first, and it is also the part that is already solved: <a href="/posts/flash-attention/">FlashAttention</a> computes exact attention without ever materialising the $n \times n$ matrix. What has no fix of that kind is the KV cache. It is linear in context, so decoding a single token means streaming the entire history out of memory, and that happens again for every token generated. <a href="/posts/attention-evolution/">Multi-head latent attention</a> shrinks the per-token cost of that cache by a large constant factor, and a large constant factor is not enough when the thing it multiplies is $10^6$.</p>
<p>Linear attention takes the other road. Instead of keeping every past token and searching them at read time, keep a fixed-size summary and update it as you go. The state is a matrix $S_t \in \mathbb{R}^{d_k \times d_v}$, written with outer products and read with a matrix-vector product:</p>
$$S_t = S_{t-1} + k_t v_t^\top, \qquad o_t = S_t^\top q_t$$<p>This is an associative memory. Writing the pair $(k_t, v_t)$ adds a rank-one term. Reading with a query $q$ that happens to equal some stored $k_i$ returns $v_i$ plus whatever the other stored pairs contribute along that direction. The cost per token no longer depends on how many tokens came before, because there is only ever one $S$.</p>
<p>That saving has a cost, and it arrives immediately. $S$ has $d_k \times d_v$ entries and that number never grows, so past a certain point new information can only be stored by degrading what is already there. In K3, $d_k = d_v = 128$, which gives each head 16,384 numbers to work with and nothing more.</p>
<p>Before we pay for that, here is what it buys. K3 keeps 24 Gated MLA layers out of 93, and only those layers hold a cache that grows with context; the other 69 hold a fixed-size state instead:</p>

<div class="kimi-decode-cost" id="kimi-decode-cost-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-decode-cost{
      --ink:var(--viz-ink); --dim:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --panel:var(--viz-panel); --raised:var(--viz-raised); --bg2:var(--viz-bg);
      --grow:var(--viz-series-2); --fixed:var(--viz-series-1); --mark:var(--viz-series-4);
      color:var(--ink); margin:2rem 0; max-width:100%; font-family:var(--viz-sans);
    }
    .kimi-decode-cost *{box-sizing:border-box}
    .kimi-decode-cost .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; position:relative; overflow:hidden}
    .kimi-decode-cost .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--viz-grid) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--viz-grid) 1px,transparent 1px) 0 0/26px 26px; opacity:.5; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-decode-cost .panel > *{position:relative}
    .kimi-decode-cost .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-decode-cost .sub{font-size:13.5px; color:var(--muted); margin-bottom:16px}

    .kimi-decode-cost canvas{width:100%; display:block; border-radius:6px}

    .kimi-decode-cost .stack{display:flex; gap:1.5px; margin-top:16px; align-items:stretch}
    .kimi-decode-cost .ly{flex:1 1 0; height:22px; border-radius:2px; background:var(--fixed); opacity:.55}
    .kimi-decode-cost .ly.mla{background:var(--grow); opacity:1}
    .kimi-decode-cost .lykey{display:flex; gap:18px; flex-wrap:wrap; margin-top:9px; font-family:var(--viz-mono); font-size:10.5px; color:var(--muted)}
    .kimi-decode-cost .lykey span{display:flex; align-items:center; gap:6px}
    .kimi-decode-cost .sw{width:9px; height:9px; border-radius:2px; display:inline-block}

    .kimi-decode-cost .ctrl{display:flex; align-items:center; gap:12px; margin-top:18px; flex-wrap:wrap}
    .kimi-decode-cost .ctrl label{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); white-space:nowrap}
    .kimi-decode-cost input[type=range]{-webkit-appearance:none; appearance:none; flex:1; min-width:170px; height:5px; border-radius:4px; background:var(--raised); border:1px solid var(--line); outline:none}
    .kimi-decode-cost input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:20px; height:20px; border-radius:50%; background:var(--ink); border:3px solid var(--mark); cursor:pointer}
    .kimi-decode-cost input[type=range]::-moz-range-thumb{width:16px; height:16px; border-radius:50%; background:var(--ink); border:3px solid var(--mark); cursor:pointer}

    .kimi-decode-cost .readouts{display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:14px}
    @media(max-width:560px){.kimi-decode-cost .readouts{grid-template-columns:1fr}}
    .kimi-decode-cost .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--raised)}
    .kimi-decode-cost .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-decode-cost .chip .num{font-size:18px; margin-top:4px; font-variant-numeric:tabular-nums}
    .kimi-decode-cost input:focus-visible{outline:2px solid var(--mark); outline-offset:3px}
    .kimi-decode-cost .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-decode-cost .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">Growing cache · 24 of 93 layers</div>
    <div class="sub">Only the Gated MLA layers hold a cache that grows with context. The KDA layers hold a fixed-size state instead.</div>

    <canvas class="cv" height="260" role="img" aria-label="Cache footprint against context length, comparing a full-attention stack of 93 layers with K3's hybrid in which only 24 layers hold a growing cache"></canvas>

    <div class="stack o-stack"></div>
    <div class="lykey">
      <span><i class="sw" style="background:var(--grow)"></i>Gated MLA &mdash; cache grows with context (24)</span>
      <span><i class="sw" style="background:var(--fixed); opacity:.55"></i>KDA &mdash; fixed-size state (69)</span>
    </div>

    <div class="ctrl">
      <label>context length</label>
      <input type="range" class="r-n" min="16" max="1000" step="4" value="1000" aria-label="context length in thousands of tokens, 16K to 1M">
      <span class="o-n" style="font-family:var(--viz-mono); font-size:13px; min-width:62px; text-align:right">1.00M</span>
    </div>

    <div class="readouts">
      <div class="chip"><div class="lab">layers with growing cache</div><div class="num o-layers">24 of 93</div></div>
      <div class="chip"><div class="lab">cache vs full attention</div><div class="num o-frac">25.8%</div></div>
      <div class="chip"><div class="lab">reduction</div><div class="num o-red">74.2%</div></div>
    </div>
  </div>

  <p class="note">The reduction is a property of the layer counts, not of the context length: 24 of 93 layers carry a growing cache, so the growing part runs at <b>25.8%</b> of a same-depth full-attention stack whatever the context. Kimi Linear, the 48B model this design was validated on, reports up to <b>75%</b> measured, which is the same number arrived at from the other direction. The 69 KDA layers are not free, but what they hold is constant in context length rather than linear in it, which is the property that makes a million tokens tractable at all.</p>

  <script>
  (function(){
    const root = document.getElementById('kimi-decode-cost-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;

    const L_TOTAL = 93, L_MLA = 24;          
    const FRAC = L_MLA / L_TOTAL;            
    const st = { n: 1000 };                  

    const cv = root.querySelector('.cv'), ctx = cv.getContext('2d');
    const rN = root.querySelector('.r-n'), elN = root.querySelector('.o-n');
    const elFrac = root.querySelector('.o-frac'), elRed = root.querySelector('.o-red');
    const elStack = root.querySelector('.o-stack');

    (function buildStack(){
      let h = '';
      
      for(let i=0;i<L_TOTAL;i++){
        const isMLA = (i === L_TOTAL-1) || (i % 4 === 3);
        h += '<i class="ly' + (isMLA ? ' mla' : '') + '"></i>';
      }
      elStack.innerHTML = h;
    })();

    function tok(v){ return v >= 1000 ? (v/1000).toFixed(2) + 'M' : v + 'K'; }
    function css(name){ return getComputedStyle(root).getPropertyValue(name).trim(); }

    function draw(){
      const dpr = Math.max(1, window.devicePixelRatio||1);
      const w = cv.clientWidth, h = 260;
      cv.width = w*dpr; cv.height = h*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
      ctx.clearRect(0,0,w,h);

      const C = { ink:css('--viz-ink'), muted:css('--viz-ink-muted'), grid:css('--viz-grid'),
                  grow:css('--viz-series-2'), fixed:css('--viz-series-1'), mark:css('--viz-series-4') };
      const padL=52, padR=14, padT=18, padB=30, gw=w-padL-padR, gh=h-padT-padB;
      const NMAX = 1000;
      const X = n => padL + gw*(n/NMAX);
      const Y = f => padT + gh*(1-f);        

      ctx.font = '10px ' + (css('--viz-mono') || 'monospace');
      ctx.textBaseline='middle'; ctx.textAlign='right';
      for(let g=0; g<=1.0001; g+=0.25){
        const y=Y(g);
        ctx.strokeStyle=C.grid; ctx.lineWidth=1;
        ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+gw,y); ctx.stroke();
        ctx.fillStyle=C.muted; ctx.fillText((g*100).toFixed(0)+'%', padL-8, y);
      }

      
      ctx.strokeStyle=C.grow; ctx.lineWidth=2.4; ctx.setLineDash([6,4]);
      ctx.beginPath(); ctx.moveTo(X(0),Y(0)); ctx.lineTo(X(NMAX),Y(1)); ctx.stroke();
      ctx.setLineDash([]);

      
      ctx.strokeStyle=C.fixed; ctx.lineWidth=2.8;
      ctx.beginPath(); ctx.moveTo(X(0),Y(0)); ctx.lineTo(X(NMAX),Y(FRAC)); ctx.stroke();

      
      const n = st.n;
      ctx.strokeStyle=C.mark; ctx.lineWidth=1; ctx.setLineDash([3,3]);
      ctx.beginPath(); ctx.moveTo(X(n),padT); ctx.lineTo(X(n),padT+gh); ctx.stroke();
      ctx.setLineDash([]);
      [[1,C.grow],[FRAC,C.fixed]].forEach(([slope,col]) => {
        ctx.fillStyle=col;
        ctx.beginPath(); ctx.arc(X(n), Y(slope*n/NMAX), 4.5, 0, Math.PI*2); ctx.fill();
      });

      ctx.textAlign='left'; ctx.fillStyle=C.grow;
      ctx.fillText('all 93 layers full attention', padL+8, padT+9);
      ctx.fillStyle=C.fixed;
      ctx.fillText('K3 hybrid, 24 of 93', padL+8, padT+24);
      ctx.fillStyle=C.muted; ctx.textAlign='center';
      ctx.fillText('context length →', padL+gw/2, h-10);
    }

    function render(){
      rN.value = st.n;
      elN.textContent = tok(st.n);
      elFrac.textContent = (FRAC*100).toFixed(1) + '%';
      elRed.textContent = ((1-FRAC)*100).toFixed(1) + '%';
      draw();
    }

    rN.addEventListener('input', () => { st.n = +rN.value; render(); });
    window.addEventListener('resize', render);
    
    new MutationObserver(draw).observe(document.body, {attributes:true, attributeFilter:['class']});
    render();
  })();
  </script>
</div>

<p>That is the constraint the rest of this axis has to work around. Anything written early has to survive most of a million subsequent writes into the same fixed state, and every one of those writes is a chance to corrupt it.</p>
<h2 id="deriving-the-delta-rule">Deriving the delta rule</h2>
<p>The update above has an obvious failure mode. Suppose the sequence writes $(k, v_1)$ early and $(k, v_2)$ later, with the same key. The state now holds $k v_1^\top + k v_2^\top$, and reading with $q = k$ returns a blend of both values weighted by $\lVert k \rVert^2$. The memory did not update the association, it superposed two of them. Do this a few thousand times and reads return an average of everything ever written to nearby keys.</p>
<p>The fix is to treat the write as an optimisation step rather than an addition, and ask the memory to reduce its own retrieval error. We define the loss as the squared distance between what the memory currently returns for $k_t$ and what it should return:</p>
$$\mathcal{L}_t(S) = \tfrac{1}{2}\big\lVert S^\top k_t - v_t \big\rVert^2, \qquad \nabla_S \mathcal{L}_t = k_t\,\big(S^\top k_t - v_t\big)^\top$$<p>Now we take exactly one gradient step from $S_{t-1}$, with step size $\beta_t$:</p>
$$S_t = S_{t-1} - \beta_t\, k_t \big(S_{t-1}^\top k_t - v_t\big)^\top$$<p>Expanding the bracket and collecting the terms that involve $S_{t-1}$:</p>
$$\boxed{\;S_t = \big(I - \beta_t k_t k_t^\top\big)\,S_{t-1} + \beta_t k_t v_t^\top\;}$$<p>That is the delta rule, and it came out of asking a linear associative memory to perform one step of gradient descent on its own reconstruction error. Nothing else went into it. The $\beta_t k_t v_t^\top$ term is the write we already had; the new factor $\big(I - \beta_t k_t k_t^\top\big)$ is the correction. Since KDA normalises its keys, $\lVert k_t \rVert = 1$, and $I - k_t k_t^\top$ is then the orthogonal projector that removes the $k_t$ component of whatever it multiplies. So at $\beta_t = 1$ the update erases whatever the memory currently associates with $k_t$ and writes $v_t$ into the space it cleared. At $\beta_t \lt 1$ the erasure is partial, which is why the report calls $\beta_t$ the write strength.</p>
<p>Below we can write a few pairs into the memory, reusing keys, and see what the projector does:</p>

<div class="kimi-delta-memory" id="kimi-delta-memory-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-delta-memory{
       
      --bg:var(--viz-bg); --bg2:var(--viz-panel); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .kimi-delta-memory *{box-sizing:border-box}
    .kimi-delta-memory .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .kimi-delta-memory .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-delta-memory .panel > *{position:relative}
    .kimi-delta-memory .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-delta-memory .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px}

    .kimi-delta-memory .grid{display:grid; grid-template-columns:0.72fr 1fr; gap:16px}
    @media(max-width:760px){.kimi-delta-memory .grid{grid-template-columns:1fr}}
    .kimi-delta-memory .cell{border:1px solid var(--line); border-radius:11px; padding:12px; background:var(--bg)}
    .kimi-delta-memory .cl{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin-bottom:9px}
    .kimi-delta-memory canvas{width:100%; display:block; border-radius:6px}

    .kimi-delta-memory .tape{display:flex; gap:4px; margin-top:14px; flex-wrap:wrap}
    .kimi-delta-memory .wr{flex:1 1 0; min-width:52px; border:1px solid var(--line); border-radius:8px; padding:7px 4px; text-align:center; background:var(--panel2); transition:.2s; font-family:var(--viz-mono)}
    .kimi-delta-memory .wr .kk{font-size:12px; color:var(--ink-soft)}
    .kimi-delta-memory .wr .vv{font-size:10px; color:var(--faint); margin-top:2px}
    .kimi-delta-memory .wr.done{border-color:color-mix(in oklab, var(--k) 45%, transparent); background:color-mix(in oklab, var(--k) 6%, transparent)}
    .kimi-delta-memory .wr.done .kk{color:var(--k)}
    .kimi-delta-memory .wr.cur{border-color:var(--q); background:color-mix(in oklab, var(--q) 10%, transparent)}
    .kimi-delta-memory .wr.cur .kk{color:var(--q)}
    .kimi-delta-memory .wr.dup{box-shadow:inset 0 -2px 0 var(--coral)}

    .kimi-delta-memory .ctrl{display:flex; align-items:center; gap:12px; margin-top:16px; flex-wrap:wrap}
    .kimi-delta-memory .ctrl label{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); white-space:nowrap}
    .kimi-delta-memory input[type=range]{-webkit-appearance:none; appearance:none; flex:1; min-width:150px; height:5px; border-radius:4px; background:linear-gradient(90deg,var(--line),var(--line-strong)); outline:none}
    .kimi-delta-memory input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:20px; height:20px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}
    .kimi-delta-memory input[type=range]::-moz-range-thumb{width:16px; height:16px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}

    .kimi-delta-memory .readouts{display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:14px}
    @media(max-width:560px){.kimi-delta-memory .readouts{grid-template-columns:1fr}}
    .kimi-delta-memory .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--panel2)}
    .kimi-delta-memory .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-delta-memory .chip .num{font-size:18px; margin-top:4px; font-variant-numeric:tabular-nums}

    .kimi-delta-memory .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; align-items:center}
    .kimi-delta-memory .btn{font-family:var(--viz-mono); font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink); background:var(--panel2); border:1px solid var(--line-strong); border-radius:9px; padding:9px 15px; cursor:pointer; transition:.15s}
    .kimi-delta-memory .btn:hover{border-color:var(--q); color:var(--ink); background:color-mix(in oklab, var(--q) 14%, var(--panel2))}
    .kimi-delta-memory .btn.active{border-color:var(--q); color:var(--q)}
    .kimi-delta-memory button:focus-visible, .kimi-delta-memory input:focus-visible{outline:2px solid var(--q); outline-offset:3px}
    .kimi-delta-memory .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-delta-memory .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">The state S · eight writes, six keys, two rewritten</div>
    <div class="sub">Step through the sequence. Keys k₂ and k₄ are written twice, with different values the second time.</div>

    <div class="grid">
      <div class="cell">
        <div class="cl">the state S (48 × 48), teal positive · coral negative</div>
        <canvas class="cv-s" height="248" role="img" aria-label="A forty-eight by forty-eight heatmap of the recurrent state matrix, updating as key-value pairs are written"></canvas>
      </div>
      <div class="cell">
        <div class="cl">retrieval error per key · read S with q = kᵢ</div>
        <canvas class="cv-e" height="300" role="img" aria-label="A bar chart of relative retrieval error for each of the six stored keys"></canvas>
      </div>
    </div>

    <div class="tape o-tape"></div>

    <div class="ctrl">
      <label>write step</label>
      <input type="range" class="r-step" min="0" max="8" step="1" value="8" aria-label="write step, 0 to 8: how many key-value pairs have been written into the state">
      <span class="o-step" style="font-family:ui-monospace,Menlo,monospace; font-size:13px; min-width:34px; text-align:right">8</span>
    </div>

    <div class="readouts">
      <div class="chip"><div class="lab">update rule</div><div class="num o-rule" style="font-size:14px">delta rule</div></div>
      <div class="chip"><div class="lab">mean error</div><div class="num o-mean">0.00</div></div>
      <div class="chip"><div class="lab">newest key error</div><div class="num o-worst">0.00</div></div>
    </div>

    <div class="btnrow">
      <button class="btn b-delta active">projector: on</button>
      <button class="btn b-play">▶ replay writes</button>
    </div>
  </div>

  <p class="note"></p>

  <script>
  (function(){
    const root = document.getElementById('kimi-delta-memory-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;
    const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    
    
    const cssVar = n => getComputedStyle(root).getPropertyValue(n).trim();
    function palette(){
      return { q:cssVar('--viz-series-4'),  k:cssVar('--viz-series-1'),
               coral:cssVar('--viz-series-2'), violet:cssVar('--viz-series-3'),
               muted:cssVar('--viz-ink-muted'), faint:cssVar('--viz-ink-muted'),
               ink:cssVar('--viz-ink'), grid:cssVar('--viz-grid'),
               bg:cssVar('--viz-bg'), panel:cssVar('--viz-panel') };
    }
    let C = palette();
    
    function fade(hex, a){
      const h = (hex || '').replace('#','').trim();
      if(h.length < 3) return 'rgba(128,128,128,' + a + ')';
      const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
      const n = parseInt(f, 16);
      return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
    }

    const D = 48;                 
    const NKEYS = 6;

    
    let seed = 20260727;
    function rnd(){ seed = (seed*1664525 + 1013904223) >>> 0; return seed/4294967296; }
    function gauss(){ let u=0,v=0; while(u===0)u=rnd(); while(v===0)v=rnd();
      return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v); }
    function unit(){ const a=Array.from({length:D}, gauss);
      const n=Math.hypot.apply(null,a); return a.map(x=>x/n); }

    const keys = Array.from({length:NKEYS}, unit);
    
    
    const PLAN = [
      {ki:0}, {ki:1}, {ki:2}, {ki:3}, {ki:4}, {ki:5}, {ki:2, dup:true}, {ki:4, dup:true}
    ];
    PLAN.forEach(w => { w.v = unit(); });

    
    function targetFor(ki, upto){
      let t = null;
      for(let i=0;i<upto;i++) if(PLAN[i].ki===ki) t = PLAN[i].v;
      return t;
    }

    const st = { step:8, delta:true, playing:false };

    const cvS = root.querySelector('.cv-s'), cvE = root.querySelector('.cv-e');
    const sctx = cvS.getContext('2d'), ectx = cvE.getContext('2d');
    const elTape = root.querySelector('.o-tape'), elStepN = root.querySelector('.o-step');
    const elRule = root.querySelector('.o-rule'), elMean = root.querySelector('.o-mean');
    const elWorst = root.querySelector('.o-worst'), elNote = root.querySelector('.note');
    const rStep = root.querySelector('.r-step'), bDelta = root.querySelector('.b-delta'), bPlay = root.querySelector('.b-play');

    
    function buildState(upto, useDelta){
      let S = Array.from({length:D}, () => new Float64Array(D));   
      for(let t=0;t<upto;t++){
        const k = keys[PLAN[t].ki], v = PLAN[t].v, beta = 1.0;
        if(useDelta){
          
          const kTS = new Float64Array(D);
          for(let b=0;b<D;b++){ let s=0; for(let a=0;a<D;a++) s += k[a]*S[a][b]; kTS[b]=s; }
          for(let a=0;a<D;a++) for(let b=0;b<D;b++) S[a][b] -= beta*k[a]*kTS[b];
        }
        for(let a=0;a<D;a++) for(let b=0;b<D;b++) S[a][b] += beta*k[a]*v[b];
      }
      return S;
    }
    function readOut(S, q){    
      const o = new Float64Array(D);
      for(let b=0;b<D;b++){ let s=0; for(let a=0;a<D;a++) s += S[a][b]*q[a]; o[b]=s; }
      return o;
    }
    function errors(S, upto){
      return keys.map((k,ki) => {
        const tgt = targetFor(ki, upto);
        if(!tgt) return null;
        const o = readOut(S, k);
        let num=0, den=0;
        for(let b=0;b<D;b++){ num += (o[b]-tgt[b])**2; den += tgt[b]**2; }
        return Math.sqrt(num)/Math.sqrt(den);
      });
    }

    function drawState(S){
      const dpr = Math.max(1, window.devicePixelRatio||1);
      const w = cvS.clientWidth, h = 248;
      cvS.width = w*dpr; cvS.height = h*dpr; sctx.setTransform(dpr,0,0,dpr,0,0);
      sctx.clearRect(0,0,w,h);
      const pad = 8, cw = (w-pad*2)/D, ch = (h-pad*2)/D, sz = Math.min(cw,ch);
      const ox = (w - sz*D)/2, oy = (h - sz*D)/2;
      let mx = 1e-9;
      for(let a=0;a<D;a++) for(let b=0;b<D;b++) mx = Math.max(mx, Math.abs(S[a][b]));
      for(let a=0;a<D;a++) for(let b=0;b<D;b++){
        const val = S[a][b]/mx, m = Math.min(1, Math.abs(val));
        const col = val >= 0 ? C.k : C.coral;
        sctx.fillStyle = fade(col, 0.06 + 0.9*m*m);
        sctx.fillRect(ox + b*sz + 0.5, oy + a*sz + 0.5, sz-1, sz-1);
      }
      sctx.strokeStyle = fade(C.ink, .16); sctx.lineWidth = 1;
      sctx.strokeRect(ox+0.5, oy+0.5, sz*D-1, sz*D-1);
    }

    function drawErrors(errs){
      const dpr = Math.max(1, window.devicePixelRatio||1);
      const w = cvE.clientWidth, h = 300;
      cvE.width = w*dpr; cvE.height = h*dpr; ectx.setTransform(dpr,0,0,dpr,0,0);
      ectx.clearRect(0,0,w,h);
      const padL=34, padR=10, padT=16, padB=26;
      const gw = w-padL-padR, gh = h-padT-padB;
      const MAXE = 1.4;
      
      ectx.font = '10px ui-monospace,Menlo,monospace'; ectx.textAlign='right'; ectx.textBaseline='middle';
      [0,0.5,1.0].forEach(g => {
        const y = padT + gh*(1 - g/MAXE);
        ectx.strokeStyle = fade(C.muted,.14); ectx.beginPath();
        ectx.moveTo(padL,y); ectx.lineTo(padL+gw,y); ectx.stroke();
        ectx.fillStyle = C.faint; ectx.fillText(g.toFixed(1), padL-7, y);
      });
      const bw = gw/NKEYS;
      ectx.textAlign='center';
      for(let i=0;i<NKEYS;i++){
        const e = errs[i];
        const x = padL + i*bw + bw*0.22, bwid = bw*0.56;
        if(e === null){
          ectx.fillStyle = fade(C.muted,.10);
          ectx.fillRect(x, padT+gh-3, bwid, 3);
        } else {
          const hgt = Math.max(2, gh*Math.min(1, e/MAXE));
          const good = e < 0.15;
          ectx.fillStyle = good ? fade(C.k,.75) : fade(C.coral,.75);
          ectx.fillRect(x, padT+gh-hgt, bwid, hgt);
          if(e/MAXE > 1){ ectx.fillStyle = C.coral; ectx.fillText('▲', x+bwid/2, padT+5); }
        }
        ectx.fillStyle = (errs[i]===null) ? C.faint : C.muted;
        ectx.fillText('k' + '₀₁₂₃₄₅'[i], padL + i*bw + bw/2, h-11);
      }
      ectx.fillStyle = C.faint; ectx.textAlign='left';
      ectx.fillText('relative error', padL, 9);
    }

    function render(){
      const S = buildState(st.step, st.delta);
      const errs = errors(S, st.step);
      drawState(S); drawErrors(errs);

      elTape.innerHTML = PLAN.map((w,i) => {
        const cls = i < st.step ? (i === st.step-1 ? 'wr cur' : 'wr done') : 'wr';
        return '<div class="' + cls + (w.dup ? ' dup' : '') + '">' +
          '<div class="kk">k' + '₀₁₂₃₄₅'[w.ki] + '</div>' +
          '<div class="vv">' + (w.dup ? 'rewrite' : 'v' + '₁₂₃₄₅₆₇₈'[i]) + '</div></div>';
      }).join('');

      elStepN.textContent = st.step;
      rStep.value = st.step;
      elRule.textContent = st.delta ? 'delta rule' : 'plain sum';
      elRule.style.color = st.delta ? 'var(--k)' : 'var(--coral)';

      const live = errs.filter(e => e !== null);
      const mean = live.length ? live.reduce((a,b)=>a+b,0)/live.length : 0;
      elMean.textContent = mean.toFixed(2);
      elMean.style.color = mean < 0.15 ? 'var(--k)' : 'var(--coral)';
      
      
      const newestKi = st.step > 0 ? PLAN[st.step-1].ki : -1;
      const newestErr = newestKi < 0 ? null : errs[newestKi];
      elWorst.textContent = newestErr === null ? '—' : ('k' + '₀₁₂₃₄₅'[newestKi] + '  ' + newestErr.toFixed(3));
      elWorst.style.color = (newestErr !== null && newestErr < 0.01) ? 'var(--k)' : 'var(--coral)';

      if(st.delta){
        elNote.innerHTML = 'The projector buys one exact guarantee: at β = 1, reading back the key just written returns its value with <b>error 0.000</b>, because the erasure removes the old association before the new one lands. Watch the rewrites at steps 7 and 8 zero out k₂ and k₄. Older keys still drift, since each later write erases along a direction not quite orthogonal to theirs, and that residual drift is exactly what the forget gate in the next section exists to manage.';
      } else {
        elNote.innerHTML = 'Drop the projector and the update is a plain sum of outer products. Every key now returns its own value <i>plus</i> a projection of every other value stored along a correlated direction, and even the <b>most recently written key comes back wrong</b>. The rewritten keys are worst of all, because their two values are not competing for the slot, they are added together. Mean error runs roughly <b>3× higher</b> than with the projector, and at 928,000 writes into a fixed state that gap is the whole ballgame.';
      }
    }

    rStep.addEventListener('input', () => { st.step = +rStep.value; render(); });
    bDelta.addEventListener('click', () => {
      st.delta = !st.delta;
      bDelta.textContent = 'projector: ' + (st.delta ? 'on' : 'off');
      bDelta.classList.toggle('active', st.delta);
      render();
    });
    bPlay.addEventListener('click', () => {
      if(reduceMotion){ st.step = 8; render(); return; }
      if(st.playing) return;
      st.playing = true; st.step = 0; render();
      const iv = setInterval(() => {
        st.step++; render();
        if(st.step >= 8){ clearInterval(iv); st.playing = false; }
      }, 420);
    });
    window.addEventListener('resize', render);
    new MutationObserver(() => { C = palette(); render(); })
      .observe(document.body, {attributes:true, attributeFilter:['class']});
    render();
  })();
  </script>
</div>

<p>What the projector guarantees can be read straight off the update. We want to know what the memory returns for the key just written, so we set $q = k_t$:</p>
$$o_t = S_t^\top k_t = \underbrace{(1-\beta_t)\,S_{t-1}^\top k_t}_{\text{what survived the erasure}} + \;\beta_t v_t$$<p>At $\beta_t = 1$ the first term vanishes and the read returns $v_t$ exactly, which is why the newest key in the panel sits at error 0.000 however crowded the state already was. The most recent write is exact, and that is all the rule promises.</p>
<p>Older keys drift. Every subsequent write erases along a direction close to, but not quite, orthogonal to theirs, so their values degrade a little each time. With the projector switched off both problems appear at once: even the newest key comes back wrong, and mean error runs about three times higher.</p>
<p>The delta rule governs what happens to a key when we write to it and says nothing about the keys we stop writing to, which at a million tokens is most of them.</p>
<h2 id="channel-wise-decay-and-the-floor-k3-puts-under-it">Channel-wise decay, and the floor K3 puts under it</h2>
<p>So we add decay. Multiply the state by a retention factor before each write, and old content fades unless it is refreshed. KDA&rsquo;s full recurrence is the delta rule with exactly that inserted:</p>
$$S_t = \big(I - \beta_t k_t k_t^\top\big)\,\underbrace{\mathrm{Diag}(\alpha_t)}_{\text{channel-wise decay}}\,S_{t-1} + \beta_t k_t v_t^\top, \qquad \tilde{o}_t = S_t^\top q_t$$<p>The detail that matters is that $\alpha_t \in (0,1)^{d_k}$ is a vector, not a scalar. Mamba-2 and Gated DeltaNet, <a href="/posts/state-space-models-mamba/">both of which this line descends from</a>, use one decay value per head. KDA gives every one of the 128 key channels its own retention rate, computed per token from a low-rank projection of the hidden state plus a per-head bias.</p>
<p>Suppose some fact has to stay legible across a million tokens. The channel carrying it needs a per-step retention $\alpha$ with $\alpha^{N}$ still meaningfully above zero at $N = 10^6$; setting $\alpha^{N} = \tfrac{1}{2}$ and solving gives $\alpha = 2^{-10^{-6}}$, which sits <strong>less than one part in a million below 1</strong>. Meanwhile the same head is tracking things like bracket depth, the current scope, and which file it is reading, all of which should be flushed within a few tokens. A single scalar per head has to satisfy both demands with one number, and it cannot. A diagonal lets channel 47 sit at $1 - 10^{-6}$ while channel 12 sits at $0.01$ and clears itself twice a line.</p>
<p>The next change is what separates K3 from Kimi Linear. It concerns how the decay is parameterised, and the justification for it has nothing to do with modelling.</p>
<p>Chunkwise linear attention processes the sequence in chunks, parallel inside a chunk and recurrent across chunks. Inside a chunk we need the decay from each position to every later position, and the standard way to get it is to compute the cumulative decay $\Gamma^{1 \to C}$ once and divide. Concretely, from the report&rsquo;s Eq. 4, the intra-chunk term rescales keys by the reciprocal $1/\Gamma^{1\to C}$. That reciprocal is one over a product of numbers smaller than 1, and it grows without bound.</p>
<p>Kimi Linear used an unbounded log-decay, $g = -e^{A}\,\mathrm{softplus}(z)$, which lives in $(-\infty, 0)$. Under that parameterisation the reciprocal really can overflow, so Kimi Linear split each chunk into 16-token secondary tiles and computed the off-diagonal tiles as dense matrix multiplications while the diagonal tiles fell back to explicit position-pair arithmetic. The report names that fallback as the main intra-chunk bottleneck.</p>
<p>K3 changes the mapping to a scaled sigmoid with a floor:</p>
$$g_t = g_{\min}\cdot\sigma\!\left(e^{A_h} z_t\right) \in (g_{\min},\, 0), \qquad \alpha_t = \exp(g_t) \in \left(e^{g_{\min}},\, 1\right), \qquad g_{\min} = -5$$<p>Let&rsquo;s follow the arithmetic through. With $g_{\min} = -5$, every retention factor exceeds $e^{-5} \approx 6.7 \times 10^{-3}$. Cumulative log-decay across a 16-token tile therefore lies in $(-80, 0)$, so the reciprocal rescaling factor is bounded by $e^{80} \approx 5.5 \times 10^{34}$. BF16 tops out around $3.4 \times 10^{38}$. It fits, with about four orders of magnitude to spare, and it fits by construction. Every causal tile, diagonal included, then becomes a dense Tensor Core matmul, and the position-pair path is removed from the kernel entirely.</p>
<p>Dragging the floor moves both consequences together:</p>

<div class="kimi-decay-floor" id="kimi-decay-floor-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-decay-floor{
       
      --bg:var(--viz-bg); --bg2:var(--viz-panel); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .kimi-decay-floor *{box-sizing:border-box}
    .kimi-decay-floor .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .kimi-decay-floor .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-decay-floor .panel > *{position:relative}
    .kimi-decay-floor .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-decay-floor .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px}

    .kimi-decay-floor .grid{display:grid; grid-template-columns:1fr; gap:16px}
    @media(max-width:760px){.kimi-decay-floor .grid{grid-template-columns:1fr}}
    .kimi-decay-floor .cell{border:1px solid var(--line); border-radius:11px; padding:12px; background:var(--bg)}
    .kimi-decay-floor .cl{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin-bottom:9px}
    .kimi-decay-floor canvas{width:100%; display:block; border-radius:6px}

    .kimi-decay-floor .tiles{display:grid; grid-template-columns:repeat(4,1fr); gap:3px; margin-top:14px; max-width:190px}
    .kimi-decay-floor .tl{aspect-ratio:1; border-radius:3px; background:color-mix(in oklab, var(--viz-ink-muted) 10%, transparent); border:1px solid transparent; transition:.3s}
    .kimi-decay-floor .tl.off{background:color-mix(in oklab, var(--k) 30%, transparent)}
    .kimi-decay-floor .tl.diag{background:color-mix(in oklab, var(--coral) 55%, transparent); border-color:color-mix(in oklab, var(--coral) 80%, transparent)}
    .kimi-decay-floor .tl.diag.dense{background:color-mix(in oklab, var(--k) 55%, transparent); border-color:color-mix(in oklab, var(--k) 80%, transparent)}
    .kimi-decay-floor .tilewrap{display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; margin-top:6px}
    .kimi-decay-floor .tilekey{font-family:var(--viz-mono); font-size:10.5px; color:var(--muted); line-height:1.8; padding-top:12px}
    .kimi-decay-floor .tilekey i{display:inline-block; width:9px; height:9px; border-radius:2px; margin-right:6px}

    .kimi-decay-floor .ctrl{display:flex; align-items:center; gap:12px; margin-top:16px; flex-wrap:wrap}
    .kimi-decay-floor .ctrl label{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); white-space:nowrap}
    .kimi-decay-floor input[type=range]{-webkit-appearance:none; appearance:none; flex:1; min-width:160px; height:5px; border-radius:4px; background:linear-gradient(90deg,var(--line-strong),var(--line)); outline:none}
    .kimi-decay-floor input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:20px; height:20px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}
    .kimi-decay-floor input[type=range]::-moz-range-thumb{width:16px; height:16px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}

    .kimi-decay-floor .readouts{display:grid; grid-template-columns:repeat(4,1fr); gap:9px; margin-top:14px}
    @media(max-width:700px){.kimi-decay-floor .readouts{grid-template-columns:1fr 1fr}}
    @media(max-width:420px){.kimi-decay-floor .readouts{grid-template-columns:1fr}}
    .kimi-decay-floor .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--panel2)}
    .kimi-decay-floor .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-decay-floor .chip .num{font-size:17px; margin-top:4px; font-variant-numeric:tabular-nums}

    .kimi-decay-floor .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:14px}
    .kimi-decay-floor .btn{font-family:var(--viz-mono); font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink); background:var(--panel2); border:1px solid var(--line-strong); border-radius:9px; padding:9px 15px; cursor:pointer; transition:.15s}
    .kimi-decay-floor .btn:hover{border-color:var(--q); color:var(--ink); background:color-mix(in oklab, var(--q) 14%, var(--panel2))}
    .kimi-decay-floor button:focus-visible, .kimi-decay-floor input:focus-visible{outline:2px solid var(--q); outline-offset:3px}
    .kimi-decay-floor .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-decay-floor .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">The decay floor · g<sub>min</sub> and the BF16 ceiling</div>
    <div class="sub">Drag the floor. The chart is what the kernel actually has to represent, against the ceiling it has to stay under.</div>

    <div class="grid">
      <div class="cell">
        <div class="cl">reciprocal rescaling 1/&Gamma; over a 16-token tile</div>
        <canvas class="cv-r" height="250" role="img" aria-label="Worst-case reciprocal rescaling factor plotted against the decay floor, with the BF16 overflow ceiling marked"></canvas>
      </div>
    </div>

    <div class="tilewrap">
      <div>
        <div class="cl" style="margin-top:14px">causal tiles in a chunk</div>
        <div class="tiles o-tiles"></div>
      </div>
      <div class="tilekey">
        <div><i style="background:color-mix(in oklab, var(--k) 55%, transparent)"></i>dense Tensor Core matmul</div>
        <div><i style="background:color-mix(in oklab, var(--coral) 55%, transparent)"></i>explicit position-pair path</div>
      </div>
    </div>

    <div class="ctrl">
      <label>g<sub>min</sub></label>
      <input type="range" class="r-g" min="-16" max="-1" step="0.25" value="-5" aria-label="decay floor g min, from -16 to -1: the lower bound on per-step log decay">
      <span class="o-g" style="font-family:ui-monospace,Menlo,monospace; font-size:13px; min-width:44px; text-align:right">-5.00</span>
    </div>

    <div class="readouts">
      <div class="chip"><div class="lab">slowest α</div><div class="num o-amin">0.0067</div></div>
      <div class="chip"><div class="lab">worst 1/Γ</div><div class="num o-recip">5.5 × 10³⁴</div></div>
      <div class="chip"><div class="lab">BF16 headroom</div><div class="num o-head">3.8 dec</div></div>
      <div class="chip"><div class="lab">diagonal tiles</div><div class="num o-path" style="font-size:14px">Tensor Core</div></div>
    </div>

    <div class="btnrow">
      <button class="btn b-k3">K3 · g<sub>min</sub> = −5</button>
      <button class="btn b-edge">the exact edge</button>
      <button class="btn b-kl">Kimi Linear · unbounded</button>
    </div>
  </div>

  <p class="note"></p>

  <script>
  (function(){
    const root = document.getElementById('kimi-decay-floor-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;
    
    
    const cssVar = n => getComputedStyle(root).getPropertyValue(n).trim();
    function palette(){
      return { q:cssVar('--viz-series-4'),  k:cssVar('--viz-series-1'),
               coral:cssVar('--viz-series-2'), violet:cssVar('--viz-series-3'),
               muted:cssVar('--viz-ink-muted'), faint:cssVar('--viz-ink-muted'),
               ink:cssVar('--viz-ink'), grid:cssVar('--viz-grid'),
               bg:cssVar('--viz-bg'), panel:cssVar('--viz-panel') };
    }
    let C = palette();
    
    function fade(hex, a){
      const h = (hex || '').replace('#','').trim();
      if(h.length < 3) return 'rgba(128,128,128,' + a + ')';
      const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
      const n = parseInt(f, 16);
      return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
    }

    const TILE = 16;                       
    const BF16_MAX = 3.3895313892515355e38;
    const LN_BF16 = Math.log(BF16_MAX);    
    const EDGE = LN_BF16 / TILE;           

    const st = { gmin: -5 };

    const cvR = root.querySelector('.cv-r');
    const rctx = cvR.getContext('2d');
    const rG = root.querySelector('.r-g'), elG = root.querySelector('.o-g');
    const elAmin = root.querySelector('.o-amin'), elRecip = root.querySelector('.o-recip');
    const elHead = root.querySelector('.o-head'), elPath = root.querySelector('.o-path');
    const elTiles = root.querySelector('.o-tiles'), elNote = root.querySelector('.note');

    function sup(n){ return String(n).replace(/[-0-9]/g, d => (d === '-' ? '⁻' : '⁰¹²³⁴⁵⁶⁷⁸⁹'[+d])); }
    function sci(x){
      if(!isFinite(x)) return 'overflow';
      const e = Math.floor(Math.log10(x)), m = x/Math.pow(10,e);
      return (Math.round(m*10)/10) + ' × 10' + sup(e);
    }
    function setupCanvas(cv, ctx, h){
      const dpr = Math.max(1, window.devicePixelRatio||1), w = cv.clientWidth;
      cv.width = w*dpr; cv.height = h*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
      ctx.clearRect(0,0,w,h); return w;
    }


    
    function drawR(){
      const h = 250, w = setupCanvas(cvR, rctx, h);
      const padL=44, padR=12, padT=14, padB=26, gw=w-padL-padR, gh=h-padT-padB;
      const AMIN=1, AMAX=16, LMAX=120;      
      const X = a => padL + gw*(a-AMIN)/(AMAX-AMIN);
      const Y = l => padT + gh*(1-Math.min(l,LMAX)/LMAX);

      rctx.font='10px ui-monospace,Menlo,monospace';
      rctx.textAlign='right'; rctx.textBaseline='middle';
      for(let l=0;l<=LMAX;l+=30){
        const y=Y(l); rctx.strokeStyle=fade(C.muted,.13);
        rctx.beginPath(); rctx.moveTo(padL,y); rctx.lineTo(padL+gw,y); rctx.stroke();
        rctx.fillStyle=C.faint; rctx.fillText('10'+sup(l), padL-7, y);
      }

      
      const yc = Y(Math.log10(BF16_MAX));
      rctx.fillStyle=fade(C.coral,.10);
      rctx.fillRect(padL, padT, gw, yc-padT);
      rctx.strokeStyle=C.coral; rctx.lineWidth=1.5;
      rctx.beginPath(); rctx.moveTo(padL,yc); rctx.lineTo(padL+gw,yc); rctx.stroke();
      rctx.fillStyle=C.coral; rctx.textAlign='left';
      rctx.fillText('BF16 overflow', padL+6, yc-10);

      
      rctx.strokeStyle=C.k; rctx.lineWidth=2.4; rctx.beginPath();
      for(let i=0;i<=200;i++){
        const a=AMIN+(AMAX-AMIN)*i/200, l=TILE*a/Math.LN10;
        i ? rctx.lineTo(X(a),Y(l)) : rctx.moveTo(X(a),Y(l));
      }
      rctx.stroke();

      
      const a=Math.abs(st.gmin), l=TILE*a/Math.LN10;
      const over = l > Math.log10(BF16_MAX);
      rctx.fillStyle = over ? C.coral : C.q;
      rctx.beginPath(); rctx.arc(X(a),Y(l),5.5,0,Math.PI*2); rctx.fill();
      rctx.strokeStyle=fade(C.bg,.9); rctx.lineWidth=2; rctx.stroke();

      rctx.fillStyle=C.faint; rctx.textAlign='center';
      rctx.fillText('|g_min|', padL+gw/2, h-9);
    }

    function render(){
      const gmin = st.gmin, a = Math.abs(gmin);
      const amin = Math.exp(gmin);
      const lnRecip = TILE*a;
      const recip = Math.exp(lnRecip);
      const fits = lnRecip < LN_BF16;
      const headDec = (LN_BF16 - lnRecip)/Math.LN10;

      elG.textContent = gmin.toFixed(2);
      rG.value = gmin;
      elAmin.textContent = amin < 1e-4 ? amin.toExponential(1) : amin.toFixed(4);
      elRecip.textContent = sci(recip);
      elRecip.style.color = fits ? 'var(--k)' : 'var(--coral)';
      elHead.textContent = (headDec >= 0 ? headDec.toFixed(1) : '−'+Math.abs(headDec).toFixed(1)) + ' dec';
      elHead.style.color = fits ? 'var(--k)' : 'var(--coral)';
      elPath.textContent = fits ? 'Tensor Core' : 'position-pair';
      elPath.style.color = fits ? 'var(--k)' : 'var(--coral)';

      
      let html='';
      for(let r=0;r<4;r++) for(let c=0;c<4;c++){
        if(c>r) html += '<i class="tl"></i>';
        else if(c===r) html += '<i class="tl diag' + (fits ? ' dense' : '') + '"></i>';
        else html += '<i class="tl off"></i>';
      }
      elTiles.innerHTML = html;

      drawR();

      if(Math.abs(gmin + 5) < 0.13){
        elNote.innerHTML = 'This is K3. With <b>g<sub>min</sub> = −5</b> the cumulative log-decay over a 16-token tile lies in (−80, 0), so the reciprocal is bounded by e⁸⁰ ≈ 5.5 × 10³⁴ and clears BF16 by nearly four decades. Push the slider left and watch the diagonal tiles turn coral: past about −5.5 the reciprocal overflows and the kernel has to fall back to explicit position-pair arithmetic, which is exactly the bottleneck K3 was removing.';
      } else if(!fits){
        elNote.innerHTML = 'Past <b>|g<sub>min</sub>| ≈ ' + EDGE.toFixed(2) + '</b> the reciprocal rescaling factor no longer fits in BF16, and the diagonal tiles lose their dense path. This is the regime Kimi Linear lived in with its unbounded negative-softplus, and it is why it computed diagonal tiles with explicit position-pair arithmetic instead.';
      } else {
        elNote.innerHTML = 'Anywhere below the ceiling, every causal tile including the diagonal runs as a dense matmul. Note where the ceiling actually falls: <b>16 × |g<sub>min</sub>| &lt; ln(BF16<sub>max</sub>) = 88.7</b> gives |g<sub>min</sub>| &lt; ' + EDGE.toFixed(2) + ', so −5 is very close to the largest round number that fits a 16-token tile. The bound was not chosen for the model, it was chosen for the tile.';
      }
    }

    rG.addEventListener('input', () => { st.gmin = +rG.value; render(); });
    root.querySelector('.b-k3').addEventListener('click', () => { st.gmin = -5; render(); });
    root.querySelector('.b-edge').addEventListener('click', () => { st.gmin = -Math.round(EDGE*100)/100; render(); });
    root.querySelector('.b-kl').addEventListener('click', () => { st.gmin = -16; render(); });
    window.addEventListener('resize', render);
    new MutationObserver(() => { C = palette(); render(); })
      .observe(document.body, {attributes:true, attributeFilter:['class']});
    render();
  })();
  </script>
</div>

<p>Let&rsquo;s be precise about what this trade costs. A floor on the log-decay bounds how <em>fast</em> a channel may forget. It places no ceiling on how long a channel may remember, since the upper end of $\alpha$ is still an open interval approaching 1, so a channel holding a long-range fact is unaffected. What the bound removes is the ability to zero a channel in a single step, and a channel sitting at $\alpha = 6.7\times10^{-3}$ still discards 99.3% of its content per token, which clears local state about as fast as anything would need.</p>
<p>This pattern shows up repeatedly in the K3 report: the mathematics is constrained so that the kernel can get simpler, and the constraint is placed where the model was not using the range anyway. The architecture is being shaped to fit the hardware rather than the other way round.</p>
<h2 id="nope-position-from-the-recurrence">NoPE: position from the recurrence</h2>
<p>K3 applies <strong>no positional encoding at all</strong> to its 24 Gated MLA layers. No rotary embeddings, no ALiBi, nothing. Queries and keys go into global attention carrying only content.</p>
<p>That works because the KDA layers already encode position, and they do it structurally rather than by addition. A recurrence with a learned per-channel decay is inherently order-sensitive: the state at token $t$ depends on the sequence of decays applied since each earlier write, so $\alpha^{t-i}$ acts as a learned, data-dependent, multiplicative analogue of the fixed sinusoidal factors in <a href="/posts/rotary-positional-encoding/">rotary embeddings</a>. Position falls out of the dynamics instead of being injected into the representation. With three KDA layers before every MLA layer, by the time a token reaches global attention its representation is already positioned.</p>
<p>The payoff shows up in the long-context curriculum. The report extends the window in four stages, 8K to 64K during pre-training and 256K to 1M during cooldown, and it says the model &ldquo;extrapolates directly to 1M-token contexts without any positional-encoding modification, such as RoPE rescaling or interpolation.&rdquo; Every long-context model since 2023 has fought a version of this fight: your rotary base was fitted at one length, you want to serve at another, so you stretch the frequencies uniformly or unevenly and hope the model tolerates the surgery. K3 has nothing to stretch. Removing the positional encoding removed the thing that needed retuning.</p>
<p>As far as I can tell this is the strongest novelty claim available for K3: it is the first frontier-scale model to run NoPE across every layer rather than in a subset of them.</p>
<h2 id="the-depth-axis-what-a-residual-connection-computes">The depth axis: what a residual connection computes</h2>
<p>Now the other direction. In a standard residual network each layer adds its own output to the stream it received:</p>
$$h_l = h_{l-1} + f_{l-1}(h_{l-1})$$<p>Substituting the same rule for $h_{l-1}$ gives</p>
$$h_l = \underbrace{h_{l-2} + f_{l-2}(h_{l-2})}_{h_{l-1}} + f_{l-1}(h_{l-1})$$<p>and doing it again for $h_{l-2}$, and again below that, peels off one term at a time while leaving everything already peeled untouched. Carried down to the embedding, the recursion telescopes into a plain sum:</p>
$$h_l = h_0 + \sum_{i \lt l} f_i(h_i)$$<p>Each $f_i$ is still evaluated at its own input $h_i$ rather than at $h_0$, so this is an accounting identity rather than a closed form in the embedding. It tells us what the stream at layer $l$ is made of, which is the part we need.</p>
<p>Read that sum as an aggregation. Every earlier layer&rsquo;s output arrives with weight exactly 1, and that weight is not learned, does not depend on content, and is not normalised. Uniform aggregation with constant coefficients is what attention reduces to when the scores are constant and the softmax is removed, so a residual stream has been doing depth-wise linear attention since 2015. It is not usually described that way, though the family is a familiar one. Highway gates and DenseNet wiring are both weightings over depth, and what they share with a plain residual is that the weight does not depend on what the layer said.</p>
<p>Two things go wrong as $L$ grows, and the AttnRes paper names both. First, <strong>dilution</strong>: in a 93-layer sum, any individual layer&rsquo;s contribution is about 1.1% of the stream, so early information is progressively drowned by everything written after it. Second, <strong>uncontrolled growth</strong>: the sum is unnormalised, so the magnitude of $h_l$ grows with depth, and later layers have to emit ever-larger outputs to move the stream at all.</p>
<p>Neither of those is a cost problem. Attending over depth was affordable in 2015 and is free now. What was missing was a reason to want it, and dilution across 93 layers is that reason.</p>
<p>We can compare the two aggregations directly by toggling between uniform weights and learned ones:</p>

<div class="kimi-depth-attention" id="kimi-depth-attention-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-depth-attention{
       
      --bg:var(--viz-bg); --bg2:var(--viz-panel); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .kimi-depth-attention *{box-sizing:border-box}
    .kimi-depth-attention .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .kimi-depth-attention .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-depth-attention .panel > *{position:relative}
    .kimi-depth-attention .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-depth-attention .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px}

    .kimi-depth-attention .grid{display:grid; grid-template-columns:1fr 1fr; gap:16px}
    @media(max-width:760px){.kimi-depth-attention .grid{grid-template-columns:1fr}}
    .kimi-depth-attention .cell{border:1px solid var(--line); border-radius:11px; padding:12px; background:var(--bg)}
    .kimi-depth-attention .cl{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin-bottom:9px}
    .kimi-depth-attention canvas{width:100%; display:block; border-radius:6px}

    .kimi-depth-attention .toggle{display:inline-flex; border:1px solid var(--line-strong); border-radius:9px; overflow:hidden; margin-top:15px; margin-right:10px}
    .kimi-depth-attention .toggle button{font-family:var(--viz-mono); font-size:11.5px; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-soft); background:var(--panel2); border:0; padding:9px 14px; cursor:pointer; transition:.15s}
    .kimi-depth-attention .toggle button + button{border-left:1px solid var(--line-strong)}
    .kimi-depth-attention .toggle button:hover{color:var(--ink); background:var(--panel2)}
    .kimi-depth-attention .toggle button.active{background:color-mix(in oklab, var(--k) 16%, transparent); color:var(--k)}

    .kimi-depth-attention .readouts{display:grid; grid-template-columns:repeat(4,1fr); gap:9px; margin-top:15px}
    @media(max-width:700px){.kimi-depth-attention .readouts{grid-template-columns:1fr 1fr}}
    @media(max-width:420px){.kimi-depth-attention .readouts{grid-template-columns:1fr}}
    .kimi-depth-attention .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--panel2)}
    .kimi-depth-attention .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-depth-attention .chip .num{font-size:17px; margin-top:4px; font-variant-numeric:tabular-nums}
    .kimi-depth-attention button:focus-visible{outline:2px solid var(--q); outline-offset:3px}
    .kimi-depth-attention .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-depth-attention .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">Depth aggregation · α<sub>i→l</sub> and what it does to the stream</div>
    <div class="sub">Rows are the receiving layer l, columns the source i. Column 0 is the token embedding.</div>

    <div class="grid">
      <div class="cell">
        <div class="cl">the depth weight matrix</div>
        <canvas class="cv-a" height="300" role="img" aria-label="A lower-triangular heatmap of depth attention weights from each source layer to each receiving layer"></canvas>
      </div>
      <div class="cell">
        <div class="cl">stream norm ‖h<sub>l</sub>‖ and the embedding's share</div>
        <canvas class="cv-m" height="300" role="img" aria-label="Two traces against depth: the norm of the residual stream, and the fraction of it contributed by the token embedding"></canvas>
      </div>
    </div>

    <div>
      <div class="toggle t-rule">
        <button data-v="uniform" class="active">uniform (residual)</button>
        <button data-v="learned">learned (AttnRes)</button>
      </div>
      <div class="toggle t-gran">
        <button data-v="full" class="active">93 layers</button>
        <button data-v="block">8 blocks</button>
      </div>
    </div>

    <div class="readouts">
      <div class="chip"><div class="lab">sources attended</div><div class="num o-src">93</div></div>
      <div class="chip"><div class="lab">kept-alive memory</div><div class="num o-mem" style="font-size:14px">O(Ld) · 667K</div></div>
      <div class="chip"><div class="lab">embedding share at l = 93</div><div class="num o-share">1.1%</div></div>
      <div class="chip"><div class="lab">‖h‖ at l = 93</div><div class="num o-norm">9.6×</div></div>
    </div>
  </div>

  <p class="note"></p>

  <script>
  (function(){
    const root = document.getElementById('kimi-depth-attention-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;
    
    
    const cssVar = n => getComputedStyle(root).getPropertyValue(n).trim();
    function palette(){
      return { q:cssVar('--viz-series-4'),  k:cssVar('--viz-series-1'),
               coral:cssVar('--viz-series-2'), violet:cssVar('--viz-series-3'),
               muted:cssVar('--viz-ink-muted'), faint:cssVar('--viz-ink-muted'),
               ink:cssVar('--viz-ink'), grid:cssVar('--viz-grid'),
               bg:cssVar('--viz-bg'), panel:cssVar('--viz-panel') };
    }
    let C = palette();
    
    function fade(hex, a){
      const h = (hex || '').replace('#','').trim();
      if(h.length < 3) return 'rgba(128,128,128,' + a + ')';
      const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
      const n = parseInt(f, 16);
      return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
    }

    const L = 93, DMODEL = 7168, NBLOCK = 8, BLOCKSZ = 12;
    const st = { rule:'uniform', gran:'full' };

    
    let seed = 930893;
    function rnd(){ seed = (seed*1664525 + 1013904223) >>> 0; return seed/4294967296; }
    const SIM = 24;                                  
    function gauss(){ let u=0,v=0; while(u===0)u=rnd(); while(v===0)v=rnd();
      return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v); }
    const outs = [];                                 
    for(let i=0;i<=L;i++){
      const a = Array.from({length:SIM}, gauss), n = Math.hypot.apply(null,a);
      outs.push(a.map(x=>x/n));
    }

    
    
    
    function scores(l, n){
      const s = new Float64Array(n);
      for(let i=0;i<n;i++){
        const recency = 2.6*Math.exp(-(l-i)/Math.max(2, n*0.16));
        const embed = (i === 0) ? 1.9 : 0;
        s[i] = recency + embed;
      }
      return s;
    }
    function weights(l, n, rule){
      const w = new Float64Array(n);
      if(rule === 'uniform'){ for(let i=0;i<n;i++) w[i] = 1;  return w; }   
      const s = scores(l, n); let mx=-Infinity, sum=0;
      for(let i=0;i<n;i++) if(s[i]>mx) mx=s[i];
      for(let i=0;i<n;i++){ w[i]=Math.exp(s[i]-mx); sum+=w[i]; }
      for(let i=0;i<n;i++) w[i]/=sum;                                       
      return w;
    }

    const cvA = root.querySelector('.cv-a'), cvM = root.querySelector('.cv-m');
    const actx = cvA.getContext('2d'), mctx = cvM.getContext('2d');
    const elSrc = root.querySelector('.o-src'), elMem = root.querySelector('.o-mem');
    const elShare = root.querySelector('.o-share'), elNorm = root.querySelector('.o-norm');
    const elNote = root.querySelector('.note');

    function setup(cv, ctx, h){
      const dpr = Math.max(1, window.devicePixelRatio||1), w = cv.clientWidth;
      cv.width=w*dpr; cv.height=h*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
      ctx.clearRect(0,0,w,h); return w;
    }

    
    function nSlots(){ return st.gran === 'full' ? L : (NBLOCK + 1); }

    function drawMatrix(){
      const h=300, w=setup(cvA, actx, h);
      const n = nSlots(), pad = 10;
      const sz = Math.min((w-pad*2)/n, (h-pad*2)/n);
      const ox=(w-sz*n)/2, oy=(h-sz*n)/2;
      for(let l=0;l<n;l++){
        const ww = weights(l, Math.max(1,l), st.rule);
        
        let tot=0; for(let i=0;i<l;i++) tot += ww[i];
        for(let i=0;i<n;i++){
          let m;
          if(i >= l || l === 0){ m = null; }
          else { m = tot > 0 ? ww[i]/tot : 0; }
          const x = ox+i*sz, y = oy+l*sz;
          if(m === null){ actx.fillStyle=fade(C.muted,.05); }
          else {
            const inten = Math.min(1, Math.pow(m*n*0.9, 0.65));
            const col = (i===0) ? [246,183,64] : [79,216,207];
            actx.fillStyle='rgba('+col[0]+','+col[1]+','+col[2]+','+(0.05+0.92*inten)+')';
          }
          actx.fillRect(x+0.4, y+0.4, sz-0.8, sz-0.8);
        }
      }
      actx.font='10px ui-monospace,Menlo,monospace';
      actx.fillStyle=C.faint; actx.textAlign='left'; actx.textBaseline='top';
      actx.fillText('source i →', ox, oy-0.5 < 12 ? 2 : oy-13);
      actx.save(); actx.translate(ox-6, oy); actx.rotate(-Math.PI/2);
      actx.textAlign='right'; actx.fillText('← layer l', 0, 0); actx.restore();
      actx.fillStyle=C.q; actx.textAlign='center'; actx.textBaseline='bottom';
      actx.fillText('emb', ox+sz/2, oy+sz*n+13);
    }

    function traces(){
      const n = nSlots();
      const norms = [], shares = [];
      for(let l=1;l<n;l++){
        const ww = weights(l, l, st.rule);
        const acc = new Float64Array(SIM);
        let tot=0;
        for(let i=0;i<l;i++){ tot += ww[i]; for(let d=0;d<SIM;d++) acc[d] += ww[i]*outs[i][d]; }
        norms.push(Math.hypot.apply(null, Array.from(acc)));
        shares.push(tot > 0 ? ww[0]/tot : 0);
      }
      return {norms, shares};
    }

    function drawTraces(){
      const h=300, w=setup(cvM, mctx, h);
      const padL=40, padR=40, padT=16, padB=26, gw=w-padL-padR, gh=h-padT-padB;
      const {norms, shares} = traces();
      const n = norms.length;
      const NMAX = 12;
      const X = i => padL + gw*(i/(Math.max(1,n-1)));
      const YN = v => padT + gh*(1-Math.min(v,NMAX)/NMAX);
      const YS = v => padT + gh*(1-Math.min(v,1));

      mctx.font='10px ui-monospace,Menlo,monospace'; mctx.textBaseline='middle';
      for(let g=0; g<=NMAX; g+=NMAX/4){
        const y=YN(g); mctx.strokeStyle=fade(C.muted,.13);
        mctx.beginPath(); mctx.moveTo(padL,y); mctx.lineTo(padL+gw,y); mctx.stroke();
        mctx.fillStyle=C.faint; mctx.textAlign='right'; mctx.fillText(g.toFixed(0)+'×', padL-7, y);
      }
      [0,0.5,1].forEach(g => {
        mctx.fillStyle=fade(C.q,.55); mctx.textAlign='left';
        mctx.fillText((g*100).toFixed(0)+'%', padL+gw+7, YS(g));
      });

      mctx.strokeStyle=C.k; mctx.lineWidth=2.4; mctx.beginPath();
      norms.forEach((v,i)=>{ i?mctx.lineTo(X(i),YN(v)):mctx.moveTo(X(i),YN(v)); }); mctx.stroke();

      mctx.strokeStyle=C.q; mctx.lineWidth=2; mctx.setLineDash([5,4]); mctx.beginPath();
      shares.forEach((v,i)=>{ i?mctx.lineTo(X(i),YS(v)):mctx.moveTo(X(i),YS(v)); }); mctx.stroke();
      mctx.setLineDash([]);

      mctx.textAlign='left'; mctx.fillStyle=C.k; mctx.fillText('‖h_l‖', padL+6, padT+8);
      mctx.fillStyle=C.q; mctx.fillText('embedding share', padL+6, padT+22);
      mctx.fillStyle=C.faint; mctx.textAlign='center';
      mctx.fillText('depth →', padL+gw/2, h-9);

      return {norms, shares};
    }

    function render(){
      drawMatrix();
      const {norms, shares} = drawTraces();
      const n = nSlots();
      const lastShare = shares.length ? shares[shares.length-1] : 0;
      const lastNorm = norms.length ? norms[norms.length-1] : 0;

      elSrc.textContent = st.gran === 'full' ? '93' : '9';
      const kept = st.gran === 'full' ? L*DMODEL : (NBLOCK+1)*DMODEL;
      elMem.textContent = (st.gran === 'full' ? 'O(Ld) · ' : 'O(Nd) · ') + Math.round(kept/1000) + 'K';
      elMem.style.color = st.gran === 'full' ? 'var(--coral)' : 'var(--k)';
      elShare.textContent = (lastShare*100).toFixed(1) + '%';
      elShare.style.color = lastShare > 0.05 ? 'var(--k)' : 'var(--coral)';
      elNorm.textContent = lastNorm.toFixed(1) + '×';
      elNorm.style.color = lastNorm > 3 ? 'var(--coral)' : 'var(--k)';

      root.querySelectorAll('.t-rule button').forEach(b => b.classList.toggle('active', b.dataset.v === st.rule));
      root.querySelectorAll('.t-gran button').forEach(b => b.classList.toggle('active', b.dataset.v === st.gran));

      if(st.rule === 'uniform'){
        elNote.innerHTML = 'Every cell in the matrix is the same colour, which is the entire point: a plain residual gives each earlier layer weight exactly 1, regardless of what it said. The embedding\'s share falls toward <b>' + (lastShare*100).toFixed(1) + '%</b> by the top of the stack, and the stream norm climbs to <b>' + lastNorm.toFixed(1) + '×</b> a single layer output, because an unnormalised sum has nothing holding it down. Dilution and growth are two readings of the same missing softmax.';
      } else if(st.gran === 'full'){
        elNote.innerHTML = 'Softmax makes the weights competitive and, because they now sum to 1, the stream is a convex combination bounded by the largest thing in it: the norm flattens at about <b>' + lastNorm.toFixed(1) + '×</b> instead of climbing. The amber column is the token embedding holding a standing <b>' + (lastShare*100).toFixed(1) + '%</b> of the aggregate at the top of the stack. The weight pattern here is illustrative; the aggregation it feeds is computed.';
      } else {
        elNote.innerHTML = 'Block AttnRes attends over <b>9 sources rather than 93</b>: eight block summaries plus the embedding. The arithmetic was never the problem, the kept-alive memory was, and this drops it from ' + Math.round(L*DMODEL/1000) + 'K to ' + Math.round((NBLOCK+1)*DMODEL/1000) + 'K floats per token, about <b>10× less</b> to hold and to ship between pipeline stages. The bounded state is also what lets the inter-block and intra-block results merge through online softmax at inference.';
      }
    }

    root.querySelectorAll('.t-rule button').forEach(b =>
      b.addEventListener('click', () => { st.rule = b.dataset.v; render(); }));
    root.querySelectorAll('.t-gran button').forEach(b =>
      b.addEventListener('click', () => { st.gran = b.dataset.v; render(); }));
    window.addEventListener('resize', render);
    new MutationObserver(() => { C = palette(); render(); })
      .observe(document.body, {attributes:true, attributeFilter:['class']});
    render();
  })();
  </script>
</div>

<p>The magnitude trace on the right is the one to look at, because it explains why the two problems have a single fix. Uniform accumulation is an unbounded sum, so the stream grows monotonically with depth and each layer&rsquo;s relative influence shrinks whether or not it had anything useful to contribute. A softmax produces a convex combination instead: weights that are non-negative and sum to 1, which bounds the result by the largest term being combined. Normalising the weights is what fixes the dilution, and it is also what stops the growth.</p>
<p>To make that concrete: a token&rsquo;s embedding enters the stream at layer 0, and some computation near the top of the stack may still need it. Under uniform accumulation, layer 88 receives that embedding as roughly one percent of a stream into which 87 other layers have also written. What we want is for layer 88 to be able to ask for it.</p>
<h2 id="attention-residuals-one-learned-query-per-layer">Attention Residuals: one learned query per layer</h2>
<p>The mechanism is small. We give each layer $l$ a single learnable pseudo-query $w_l \in \mathbb{R}^d$, and let the keys and values be the outputs of all preceding layers, with the token embedding at index 0 so that it is always available as a source:</p>
$$k_i = v_i = \begin{cases} h_1 & i = 0 \\ f_i(h_i) & 1 \le i \le l-1\end{cases}$$<p>We score with an exponential kernel over RMS-normalised keys, then normalise across depth:</p>
$$\alpha_{i\to l} = \frac{\exp\!\big(w_l^\top\, \mathrm{RMSNorm}(k_i)\big)}{\sum_{j \lt l}\exp\!\big(w_l^\top\, \mathrm{RMSNorm}(k_j)\big)}, \qquad h_l = \sum_{i \lt l} \alpha_{i\to l}\, v_i$$<p>Three choices here are doing real work. The <strong>RMSNorm on keys</strong> exists because unnormalised residual streams grow with depth, so without it a late layer would win the attention on magnitude alone rather than on content. The <strong>softmax</strong>, rather than a sigmoid, makes the weights competitive: a layer that attends more to the embedding necessarily attends less to something else, which is what forces it to choose. And the query is <strong>one vector per layer</strong> rather than one per token, so what gets learned is a position-independent preference over depth. That is also why it costs so little.</p>
<p>There is no per-token routing over depth, and the ablations in the AttnRes paper find that multi-head depth attention makes results worse rather than better.</p>
<p>The practical problem is memory. Full AttnRes needs every preceding layer&rsquo;s output kept alive, which is $O(Ld)$ memory and, under pipeline parallelism, $O(Ld)$ cross-stage communication. The arithmetic was affordable; the memory traffic is not. <strong>Block AttnRes</strong> is the fix: K3 groups its 93 layers into 8 blocks of 12, sums ordinarily inside a block, and attends only over the block-level summaries. Counting the embedding, that leaves 9 sources rather than 93, and the memory that has to be kept alive falls by an order of magnitude with it.</p>
<p>A late layer can then put real weight on the block holding what it needs, rather than receiving it as one percent of an undifferentiated sum.</p>
<h2 id="the-width-axis-896-experts-and-the-cost-of-dispatch">The width axis: 896 experts and the cost of dispatch</h2>
<p>Sparsity on this axis, the ratio of pool to active, rises from K2&rsquo;s 48 to K3&rsquo;s 56, and the obstacle to raising it is traffic rather than parameter count. In a conventional mixture of experts, each selected expert receives the full $d$-dimensional token representation, so doubling the number of active experts doubles both the all-to-all communication and the expert weight traffic. At $d = 7168$ and 16 active experts, that communication cost is the binding constraint.</p>
<p>LatentMoE separates the model&rsquo;s width from the routed experts&rsquo; width. Shared experts keep the full-width path, while routed experts work in a compact latent space of width $\ell$, reached by a down-projection before dispatch and mapped back afterwards:</p>
$$u = \sum_{i \in T_k(x)} p_i\, E_i^{\text{routed}}(W_{\downarrow} x), \qquad y = \sum_{j} E_j^{\text{shared}}(x) + W_{\uparrow}\,\mathrm{RMSNorm}(u)$$<p>K3 sets $\ell = 3584$, exactly half the hidden dimension, so the tokens crossing the network for dispatch are half-width. The arithmetic works out exactly: K2 moved $8 \times 7168 = 57{,}344$ values per token, and K3 moves $16 \times 3584 = 57{,}344$. Twice the routing multiplicity for an identical communication bill, which is what makes 16-of-896 affordable at all.</p>
<p>Sparsity at that level amplifies two failure modes already present in the vanilla design, and the report is unusually direct about both. The routed path runs through $W_{\downarrow}$, then a gated multi-branch expert network, then $W_{\uparrow}$, which the report calls &ldquo;a chain of nearly four consecutive matrix multiplications&rdquo;; that ill-conditioned structure at 2.8 trillion parameters produces exploding activations in the routed branch. Balancing the load of nearly $10^3$ experts, meanwhile, &ldquo;exceeds the regime in which existing auxiliary-loss-free bias updates remain well behaved.&rdquo;</p>
<h3 id="bounding-the-activations">Bounding the activations</h3>
<p>The activation problem gets the smaller fix. SwiGLU multiplies two unbounded factors, so coincident large coordinates produce activation outliers, and outliers are exactly what low-precision arithmetic handles worst. The original GLU&rsquo;s sigmoid gate is bounded but loses the roughly linear positive response that makes Swish work.</p>
<p>SiTU-GLU applies a smooth cap, $\mathrm{softcap}(x,\beta) = \beta\tanh(x/\beta)$, to the linear factor of the Swish gate and independently to the up branch:</p>
$$\text{SiTU-GLU}(x) = \beta_1\tanh\!\left(\frac{W_g x}{\beta_1}\right) \odot \sigma(W_g x) \odot \beta_2\tanh\!\left(\frac{W_u x}{\beta_2}\right)$$<p>K3 uses $\beta_1 = 4$ for the gate branch and $\beta_2 = 25$ for the up branch. Three properties follow from that, all worked in Appendix B. Near the origin $\beta\tanh(z/\beta) = z + O(z^3/\beta^2)$, so SiTU-GLU matches SwiGLU to first order where most activations live. As $\beta_1,\beta_2 \to \infty$ it recovers SwiGLU pointwise, so the design is a strict generalisation. And since $\lvert\tanh\rvert \lt 1$ and $0 \lt \sigma \lt 1$, every output coordinate obeys</p>
$$\lVert \text{SiTU-GLU}(x)\rVert_\infty \le \beta_1\beta_2 = 100$$<p>A hard clamp would bound the output too, and the report explains why it was not used: the smooth cap keeps gradients nonzero away from the saturation boundary, whereas a clamp sets them to exactly zero and the unit stops learning.</p>
<p>That leaves the balancing problem, which needs more than a bound.</p>
<h2 id="quantile-balancing">Quantile balancing</h2>
<p>Mixture-of-experts models need their experts used evenly. An expert that receives too few tokens trains poorly and eventually dies; an expert that receives too many becomes a straggler that every other device waits on.</p>
<p>Some notation first, since the rest of this section leans on it. Take a batch of $m$ tokens and $n$ experts, with every token routed to $k$ of them; in K3, $n = 896$ and $k = 16$. The router emits a score $s_{i,j}$ for each token $i$ and expert $j$, and we write $s_{:,j}$ for the whole column of scores belonging to expert $j$, one per token in the batch. Let $c_j$ be that expert&rsquo;s <strong>load</strong>, meaning the number of tokens actually routed to it, and $\bar{c}$ the average load across experts. Perfect balance would put every expert at the target load</p>
$$q = \frac{mk}{n}$$<p>since the batch hands out $mk$ routing slots in total and there are $n$ experts to share them.</p>
<p>The modern approach, from DeepSeek-V3, avoids auxiliary losses and instead maintains a per-expert bias $b_j$ that is added to the router score for top-$k$ selection and omitted from the mixture weights, so it steers dispatch without touching gradients. After each step $t$, every bias moves one fixed increment $\gamma$ in the direction of its own load error:</p>
$$b_j^{(t+1)} = b_j^{(t)} + \gamma\,\mathrm{sign}\big(\bar{c} - c_j\big)$$<p>An expert sitting below the average has its bias raised, which makes it win more top-$k$ contests on the next step; one sitting above the average has its bias lowered.</p>
<p>At 384 experts $\gamma$ can be tuned. At 896 there is no good value for it: a small $\gamma$ takes too many steps to equilibrate, and during those steps some experts are receiving almost nothing to train on, while a large $\gamma$ overshoots and the loads oscillate. The underlying reason is that the update carries only the <em>direction</em> of the load error and discards its magnitude, so the step size is being asked to guess how far to move.</p>
<p>Quantile Balancing removes the step entirely. Route with top-$(k{+}1)$ rather than top-$k$: the first $k$ entries are the routes actually taken, and the $(k{+}1)$-th biased score is the cutoff $\alpha_i$ that an expert has to beat to enter token $i$&rsquo;s selection. Holding those cutoffs fixed, expert $j$ receives exactly the tokens whose margin $s_{i,j} - \alpha_i$ clears $-b_j$, so its load falls monotonically as $b_j$ falls. That makes the target load something we can solve for rather than approach. We want the bias at which exactly $q$ of the $m$ margins still clear it, and since $q/m = k/n$ that cut sits at the $(1-k/n)$-quantile of the margins:</p>
$$\hat{b}_j^{(t+1)} \leftarrow -\,\mathrm{quantile}_{1-k/n}\big(s_{:,j} - \alpha^{(t)}\big), \qquad b^{(t+1)} \leftarrow \hat{b}^{(t+1)} - \mathrm{mean}\big(\hat{b}^{(t+1)}\big)$$<p>The hat marks the raw solution before centring. Subtracting the mean from every entry removes an offset shared by all experts, which cannot change a top-$k$ ranking and so costs nothing. The update applies at the next step, so a batch is never routed with a bias derived from itself, and the bias is frozen at inference.</p>
<p>The two rules are not rivals so much as two ways at the same problem. Appendix C of the report sets up the maximum-score balanced assignment, the routing that maximises total router score while giving every expert exactly $q$ tokens, and shows that its dual has a closed-form solution: precisely the quantile above. Quantile Balancing jumps straight to it.</p>
<p>DeepSeek&rsquo;s rule turns out to be descending that same objective, one sign at a time. Its update is what you get by taking the gradient and keeping only its direction, and the gradient itself is $q - c_j$, the target load minus the observed one. <strong>The heuristic everyone shipped was already gradient descent on a problem that has an exact answer.</strong> That is why one of them needs a step size and the other does not, and why, in the report&rsquo;s phrasing, Quantile Balancing &ldquo;equilibrates within a few update steps even for nearly $10^3$ experts.&rdquo;</p>
<p>Below we can run both rules on the same imbalanced routing and look for a step size that wins:</p>

<div class="kimi-quantile-balancing" id="kimi-quantile-balancing-02ef462231f8c654f5915e4a613773a2">
  <style>
    .kimi-quantile-balancing{
       
      --bg:var(--viz-bg); --bg2:var(--viz-panel); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .kimi-quantile-balancing *{box-sizing:border-box}
    .kimi-quantile-balancing .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .kimi-quantile-balancing .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .kimi-quantile-balancing .panel > *{position:relative}
    .kimi-quantile-balancing .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .kimi-quantile-balancing .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px}

    .kimi-quantile-balancing .grid{display:grid; grid-template-columns:1.15fr 1fr; gap:16px}
    @media(max-width:760px){.kimi-quantile-balancing .grid{grid-template-columns:1fr}}
    .kimi-quantile-balancing .cell{border:1px solid var(--line); border-radius:11px; padding:12px; background:var(--bg)}
    .kimi-quantile-balancing .cl{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin-bottom:9px}
    .kimi-quantile-balancing canvas{width:100%; display:block; border-radius:6px}

    .kimi-quantile-balancing .toggle{display:inline-flex; border:1px solid var(--line-strong); border-radius:9px; overflow:hidden; margin-top:15px}
    .kimi-quantile-balancing .toggle button{font-family:var(--viz-mono); font-size:11.5px; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-soft); background:var(--panel2); border:0; padding:9px 14px; cursor:pointer; transition:.15s}
    .kimi-quantile-balancing .toggle button + button{border-left:1px solid var(--line-strong)}
    .kimi-quantile-balancing .toggle button:hover{color:var(--ink); background:var(--panel2)}
    .kimi-quantile-balancing .toggle button.active{background:color-mix(in oklab, var(--k) 16%, transparent); color:var(--k)}

    .kimi-quantile-balancing .ctrl{display:flex; align-items:center; gap:12px; margin-top:14px; flex-wrap:wrap; transition:opacity .2s}
    .kimi-quantile-balancing .ctrl.dim{opacity:.32; pointer-events:none}
    .kimi-quantile-balancing .ctrl label{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); white-space:nowrap}
    .kimi-quantile-balancing input[type=range]{-webkit-appearance:none; appearance:none; flex:1; min-width:150px; height:5px; border-radius:4px; background:linear-gradient(90deg,var(--line),var(--line-strong)); outline:none}
    .kimi-quantile-balancing input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:20px; height:20px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}
    .kimi-quantile-balancing input[type=range]::-moz-range-thumb{width:16px; height:16px; border-radius:50%; background:var(--ink); border:3px solid var(--q); cursor:pointer}

    .kimi-quantile-balancing .readouts{display:grid; grid-template-columns:repeat(4,1fr); gap:9px; margin-top:14px}
    @media(max-width:700px){.kimi-quantile-balancing .readouts{grid-template-columns:1fr 1fr}}
    @media(max-width:420px){.kimi-quantile-balancing .readouts{grid-template-columns:1fr}}
    .kimi-quantile-balancing .chip{border:1px solid var(--line); border-radius:10px; padding:10px 12px; background:var(--panel2)}
    .kimi-quantile-balancing .chip .lab{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
    .kimi-quantile-balancing .chip .num{font-size:17px; margin-top:4px; font-variant-numeric:tabular-nums}

    .kimi-quantile-balancing .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; align-items:center}
    .kimi-quantile-balancing .btn{font-family:var(--viz-mono); font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink); background:var(--panel2); border:1px solid var(--line-strong); border-radius:9px; padding:9px 15px; cursor:pointer; transition:.15s}
    .kimi-quantile-balancing .btn:hover{border-color:var(--q); color:var(--ink); background:color-mix(in oklab, var(--q) 14%, var(--panel2))}
    .kimi-quantile-balancing button:focus-visible, .kimi-quantile-balancing input:focus-visible{outline:2px solid var(--q); outline-offset:3px}
    .kimi-quantile-balancing .note{font-family:inherit; font-size:15px; color:var(--muted); font-style:italic; margin:16px 2px 0}
    @media(max-width:640px){.kimi-quantile-balancing .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">Load balancing · 256 tokens, 32 experts, top-2</div>
    <div class="sub">Target load is 16 tokens per expert. Both rules run on the same router scores.</div>

    <div class="grid">
      <div class="cell">
        <div class="cl">expert load at the current step</div>
        <canvas class="cv-l" height="250" role="img" aria-label="A bar chart of tokens routed to each of thirty-two experts, against the target load"></canvas>
      </div>
      <div class="cell">
        <div class="cl">worst-case overload across steps</div>
        <canvas class="cv-c" height="250" role="img" aria-label="Convergence traces of maximum expert overload over update steps for both balancing rules"></canvas>
      </div>
    </div>

    <div>
      <div class="toggle t-rule">
        <button data-v="sign" class="active">fixed-step sign rule</button>
        <button data-v="qb">quantile balancing</button>
      </div>
    </div>

    <div class="ctrl c-gamma">
      <label style="text-transform:none; letter-spacing:.06em">γ step size</label>
      <input type="range" class="r-g" min="0.002" max="0.20" step="0.002" value="0.02" aria-label="gamma, the fixed step size of the sign-based bias update">
      <span class="o-g" style="font-family:ui-monospace,Menlo,monospace; font-size:13px; min-width:44px; text-align:right">0.020</span>
    </div>

    <div class="ctrl">
      <label>update step</label>
      <input type="range" class="r-t" min="0" max="40" step="1" value="40" aria-label="update step, 0 to 40">
      <span class="o-t" style="font-family:ui-monospace,Menlo,monospace; font-size:13px; min-width:34px; text-align:right">40</span>
    </div>

    <div class="readouts">
      <div class="chip"><div class="lab">max overload</div><div class="num o-max">1.00×</div></div>
      <div class="chip"><div class="lab">starved experts</div><div class="num o-dead">0</div></div>
      <div class="chip"><div class="lab">steps to balance</div><div class="num o-conv">—</div></div>
      <div class="chip"><div class="lab">hyperparameters</div><div class="num o-hp" style="font-size:14px">1 (γ)</div></div>
    </div>

    <div class="btnrow">
      <button class="btn b-play">▶ replay updates</button>
    </div>
  </div>

  <p class="note"></p>

  <script>
  (function(){
    const root = document.getElementById('kimi-quantile-balancing-02ef462231f8c654f5915e4a613773a2');
    if(!root) return;
    const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    
    
    const cssVar = n => getComputedStyle(root).getPropertyValue(n).trim();
    function palette(){
      return { q:cssVar('--viz-series-4'),  k:cssVar('--viz-series-1'),
               coral:cssVar('--viz-series-2'), violet:cssVar('--viz-series-3'),
               muted:cssVar('--viz-ink-muted'), faint:cssVar('--viz-ink-muted'),
               ink:cssVar('--viz-ink'), grid:cssVar('--viz-grid'),
               bg:cssVar('--viz-bg'), panel:cssVar('--viz-panel') };
    }
    let C = palette();
    
    function fade(hex, a){
      const h = (hex || '').replace('#','').trim();
      if(h.length < 3) return 'rgba(128,128,128,' + a + ')';
      const f = h.length === 3 ? h.split('').map(c => c + c).join('') : h;
      const n = parseInt(f, 16);
      return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
    }

    const M = 256, N = 32, K = 2, STEPS = 40;
    const TARGET = M*K/N;                       

    let seed = 20260806;
    function rnd(){ seed = (seed*1664525 + 1013904223) >>> 0; return seed/4294967296; }
    function gauss(){ let u=0,v=0; while(u===0)u=rnd(); while(v===0)v=rnd();
      return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v); }

    
    
    const pop = Array.from({length:N}, (_,j) => 1.5*Math.exp(-j/6) + 0.25*gauss());
    const S = [];
    for(let i=0;i<M;i++){
      const row = new Float64Array(N);
      for(let j=0;j<N;j++) row[j] = 1/(1+Math.exp(-(pop[j] + 0.9*gauss())));   
      S.push(row);
    }

    function loadsFor(b){
      const load = new Int32Array(N);
      const idx = Array.from({length:N}, (_,j)=>j);
      for(let i=0;i<M;i++){
        const r = S[i];
        idx.sort((a,c) => (r[c]+b[c]) - (r[a]+b[a]));
        for(let t=0;t<K;t++) load[idx[t]]++;
      }
      return load;
    }
    
    function cutoffs(b){
      const a = new Float64Array(M);
      const buf = new Float64Array(N);
      for(let i=0;i<M;i++){
        const r = S[i];
        for(let j=0;j<N;j++) buf[j] = r[j]+b[j];
        const sorted = Array.from(buf).sort((x,y)=>y-x);
        a[i] = sorted[K];                                  
      }
      return a;
    }

    function runSign(gamma){
      const b = new Float64Array(N); const hist = [];
      for(let t=0;t<=STEPS;t++){
        const load = loadsFor(b);
        hist.push({b:Float64Array.from(b), load});
        let mean = 0; for(let j=0;j<N;j++) mean += load[j]; mean /= N;
        for(let j=0;j<N;j++) b[j] += gamma*Math.sign(mean - load[j]);
      }
      return hist;
    }
    function runQB(){
      const b = new Float64Array(N); const hist = [];
      const q = Math.round(TARGET);
      for(let t=0;t<=STEPS;t++){
        const load = loadsFor(b);
        hist.push({b:Float64Array.from(b), load});
        const alpha = cutoffs(b);
        const nb = new Float64Array(N);
        for(let j=0;j<N;j++){
          const marg = new Float64Array(M);
          for(let i=0;i<M;i++) marg[i] = S[i][j] - alpha[i];
          const sorted = Array.from(marg).sort((x,y)=>y-x);
          nb[j] = -sorted[Math.min(q, M-1)];               
        }
        let mn=0; for(let j=0;j<N;j++) mn += nb[j]; mn /= N;
        for(let j=0;j<N;j++) b[j] = nb[j] - mn;            
      }
      return hist;
    }

    const st = { rule:'sign', gamma:0.02, t:STEPS, playing:false };
    let HSIGN = runSign(st.gamma), HQB = runQB();

    const cvL = root.querySelector('.cv-l'), cvC = root.querySelector('.cv-c');
    const lctx = cvL.getContext('2d'), cctx = cvC.getContext('2d');
    const rG = root.querySelector('.r-g'), rT = root.querySelector('.r-t');
    const elG = root.querySelector('.o-g'), elT = root.querySelector('.o-t');
    const elMax = root.querySelector('.o-max'), elDead = root.querySelector('.o-dead');
    const elConv = root.querySelector('.o-conv'), elHp = root.querySelector('.o-hp');
    const elNote = root.querySelector('.note'), cGamma = root.querySelector('.c-gamma');

    function setup(cv, ctx, h){
      const dpr = Math.max(1, window.devicePixelRatio||1), w = cv.clientWidth;
      cv.width=w*dpr; cv.height=h*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
      ctx.clearRect(0,0,w,h); return w;
    }
    function overload(load){ let mx=0; for(let j=0;j<load.length;j++) mx=Math.max(mx,load[j]); return mx/TARGET; }
    function convStep(hist){
      for(let t=0;t<hist.length;t++) if(overload(hist[t].load) <= 1.2) return t;
      return -1;
    }

    function drawLoads(load){
      const h=250, w=setup(cvL, lctx, h);
      const padL=32, padR=10, padT=16, padB=22, gw=w-padL-padR, gh=h-padT-padB;
      const YMAX = 48;
      lctx.font='10px ui-monospace,Menlo,monospace'; lctx.textBaseline='middle'; lctx.textAlign='right';
      for(let g=0; g<=YMAX; g+=16){
        const y=padT+gh*(1-g/YMAX);
        lctx.strokeStyle=fade(C.muted,.13); lctx.beginPath();
        lctx.moveTo(padL,y); lctx.lineTo(padL+gw,y); lctx.stroke();
        lctx.fillStyle=C.faint; lctx.fillText(String(g), padL-6, y);
      }
      const yt = padT+gh*(1-TARGET/YMAX);
      lctx.strokeStyle=C.q; lctx.lineWidth=1.4; lctx.setLineDash([5,4]);
      lctx.beginPath(); lctx.moveTo(padL,yt); lctx.lineTo(padL+gw,yt); lctx.stroke(); lctx.setLineDash([]);
      lctx.fillStyle=C.q; lctx.textAlign='left'; lctx.fillText('target 16', padL+5, yt-9);

      const bw = gw/N;
      for(let j=0;j<N;j++){
        const v = load[j], hgt = Math.max(v>0?2:0, gh*Math.min(1, v/YMAX));
        const over = v > TARGET*1.2, dead = v === 0;
        lctx.fillStyle = dead ? fade(C.coral,.30) : over ? fade(C.coral,.78) : fade(C.k,.72);
        lctx.fillRect(padL + j*bw + bw*0.16, padT+gh-hgt, bw*0.68, hgt);
        if(dead){
          lctx.fillStyle=C.coral; lctx.textAlign='center';
          lctx.fillText('×', padL + j*bw + bw/2, padT+gh-8);
        }
      }
      lctx.fillStyle=C.faint; lctx.textAlign='center';
      lctx.fillText('experts →', padL+gw/2, h-7);
    }

    function drawConv(){
      const h=250, w=setup(cvC, cctx, h);
      const padL=34, padR=10, padT=16, padB=22, gw=w-padL-padR, gh=h-padT-padB;
      const YMAX=3.2;
      cctx.font='10px ui-monospace,Menlo,monospace'; cctx.textBaseline='middle'; cctx.textAlign='right';
      for(let g=1; g<=3; g++){
        const y=padT+gh*(1-g/YMAX);
        cctx.strokeStyle=fade(C.muted,.13); cctx.beginPath();
        cctx.moveTo(padL,y); cctx.lineTo(padL+gw,y); cctx.stroke();
        cctx.fillStyle=C.faint; cctx.fillText(g+'×', padL-6, y);
      }
      const y1=padT+gh*(1-1/YMAX);
      cctx.strokeStyle=fade(C.q,.5); cctx.setLineDash([4,4]); cctx.lineWidth=1;
      cctx.beginPath(); cctx.moveTo(padL,y1); cctx.lineTo(padL+gw,y1); cctx.stroke(); cctx.setLineDash([]);

      const X = t => padL + gw*t/STEPS;
      const Y = v => padT + gh*(1-Math.min(v,YMAX)/YMAX);
      [[HSIGN,fade(C.coral,.85)],[HQB,C.k]].forEach(([hist,col]) => {
        cctx.strokeStyle=col; cctx.lineWidth=2.2; cctx.beginPath();
        hist.forEach((s,t)=>{ const v=overload(s.load); t?cctx.lineTo(X(t),Y(v)):cctx.moveTo(X(t),Y(v)); });
        cctx.stroke();
      });
      const cur = st.rule==='sign' ? HSIGN : HQB;
      cctx.fillStyle=C.q;
      cctx.beginPath(); cctx.arc(X(st.t), Y(overload(cur[st.t].load)), 4.5, 0, Math.PI*2); cctx.fill();

      cctx.textAlign='right';
      cctx.fillStyle=fade(C.coral,.9); cctx.fillText('sign rule', padL+gw-6, padT+8);
      cctx.fillStyle=C.k; cctx.fillText('quantile balancing', padL+gw-6, padT+22);
      cctx.textAlign='left';
      cctx.fillStyle=C.faint; cctx.textAlign='center'; cctx.fillText('update step →', padL+gw/2, h-7);
    }

    function render(){
      const hist = st.rule === 'sign' ? HSIGN : HQB;
      const s = hist[st.t];
      drawLoads(s.load); drawConv();

      rT.value = st.t; elT.textContent = st.t;
      rG.value = st.gamma; elG.textContent = st.gamma.toFixed(3);
      cGamma.classList.toggle('dim', st.rule !== 'sign');

      const ov = overload(s.load);
      let dead = 0; for(let j=0;j<N;j++) if(s.load[j] === 0) dead++;
      elMax.textContent = ov.toFixed(2) + '×';
      elMax.style.color = ov <= 1.2 ? 'var(--k)' : 'var(--coral)';
      elDead.textContent = dead;
      elDead.style.color = dead ? 'var(--coral)' : 'var(--k)';
      const cs = convStep(hist);
      elConv.textContent = cs < 0 ? 'never' : cs;
      elConv.style.color = cs < 0 ? 'var(--coral)' : 'var(--k)';
      elHp.textContent = st.rule === 'sign' ? '1 (γ)' : 'none';
      elHp.style.color = st.rule === 'sign' ? 'var(--coral)' : 'var(--k)';

      root.querySelectorAll('.t-rule button').forEach(b => b.classList.toggle('active', b.dataset.v === st.rule));

      const csSign = convStep(HSIGN), csQB = convStep(HQB);
      if(st.rule === 'qb'){
        elNote.innerHTML = 'Quantile Balancing reaches target load in <b>' + (csQB<0?'—':csQB) + ' step' + (csQB===1?'':'s') + '</b> and stays there, with no step size to choose. It is solving for the bias that hits the target rather than walking toward it, and Appendix C shows that bias is the exact coordinate minimiser of the LP dual whose SignSGD step is the red curve. Same objective, one descends it and one solves it.';
      } else if(csSign < 0){
        elNote.innerHTML = 'At γ = ' + st.gamma.toFixed(3) + ' the sign rule never settles inside the tolerance band. ' + (st.gamma > 0.035 ? 'The step is large enough to overshoot, so the loads ring back and forth across the target indefinitely.' : 'The step is too small to close the initial gap in forty updates, so the busiest experts stay persistently overloaded while the bias inches toward them.') + ' Try to find a γ that does both jobs; the difficulty is the point, and it gets worse as the expert count grows.';
      } else {
        elNote.innerHTML = 'At γ = ' + st.gamma.toFixed(3) + ' the sign rule does eventually balance, after <b>' + csSign + ' steps</b>, against quantile balancing\'s ' + (csQB<0?'—':csQB) + '. Every one of those steps is a step during which some experts are overloaded and others are receiving nothing to train on. The sign rule carries only the direction of the load error and throws its magnitude away, which is exactly what a step size is then asked to guess.';
      }
    }

    rT.addEventListener('input', () => { st.t = +rT.value; render(); });
    rG.addEventListener('input', () => { st.gamma = +rG.value; HSIGN = runSign(st.gamma); render(); });
    root.querySelectorAll('.t-rule button').forEach(b =>
      b.addEventListener('click', () => { st.rule = b.dataset.v; render(); }));
    root.querySelector('.b-play').addEventListener('click', () => {
      if(reduceMotion){ st.t = STEPS; render(); return; }
      if(st.playing) return;
      st.playing = true; st.t = 0; render();
      const iv = setInterval(() => {
        st.t++; render();
        if(st.t >= STEPS){ clearInterval(iv); st.playing = false; }
      }, 90);
    });
    window.addEventListener('resize', render);
    new MutationObserver(() => { C = palette(); render(); })
      .observe(document.body, {attributes:true, attributeFilter:['class']});
    render();
  })();
  </script>
</div>

<p>One engineering detail makes this practical at scale. The quantile is over the whole global batch, whose margins number in the millions and are sharded across ranks and accumulation steps, so gathering them exactly is not viable. K3 estimates each expert&rsquo;s quantile from a histogram of its margins: bin locally, then a single all-reduce sums the per-rank bin counts. Because counts are additive, the pooled histogram represents the true global batch no matter how tokens were sharded, so the estimate is the whole-batch quantile up to the bin width, at a communication cost of a few hundred bins per expert. The approximation costs resolution rather than correctness: the statistic being computed is still the global one.</p>
<h2 id="where-to-get-it">Where to get it</h2>
<p>K3 is an open-weights release, so most of what is described above can be inspected directly.</p>
<ul>
<li><strong>Weights</strong>: <a href="https://huggingface.co/moonshotai/Kimi-K3">huggingface.co/moonshotai/Kimi-K3</a>, MXFP4, roughly 1.4 TB.</li>
<li><strong>KDA kernels</strong>: FlashKDA is a CUTLASS implementation of the chunkwise algorithm used in the sequence-axis sections above, auto-dispatched as a backend of <a href="https://github.com/fla-org/flash-linear-attention">flash-linear-attention</a>.</li>
<li><strong>Expert parallelism</strong>: <a href="https://github.com/MoonshotAI/MoonEP">MoonEP</a>, the balanced all-to-all layer the width axis depends on during training.</li>
<li><strong>The 48B precedent</strong>: <a href="https://arxiv.org/abs/2510.26692">Kimi Linear</a>, where this attention design was validated and where the KV-cache and throughput measurements quoted earlier come from.</li>
</ul>
<h2 id="what-the-report-does-not-say">What the report does not say</h2>
<p>Two things to keep in mind before treating any of the numbers above as settled.</p>
<p><strong>The training scale is not disclosed.</strong> Across 47 pages there is no pre-training token count, no total FLOPs, no GPU type or count, no cluster size, no MFU, no wall-clock, and no cost. The only hardware-scale figure in the paper is &ldquo;within a few hundred GPUs&rdquo;, and that refers to one co-located million-token RL experiment rather than to pre-training. Any article quoting a K3 training cost is quoting someone&rsquo;s guess.</p>
<p><strong>The headline 2.5× is an extrapolation from curves fitted far below the scale K3 was actually trained at.</strong> It is a scaling-efficiency ratio taken from fitted scaling-law curves for K2 and K3, and the x-axis of that figure spans only $10^{20}$ to $10^{21}$ FLOPs, while a 2.8-trillion-parameter production run sits orders of magnitude to the right of the plotted range. The report is straightforward about this and presents the number as an extrapolation from a hyperparameter sweep.</p>
<p>There is also a fair critique of the novelty. Sebastian Raschka&rsquo;s read is that K3 is largely the Kimi Linear architecture scaled 58×, that LatentMoE closely resembles the design in Nemotron 3 Ultra and so is convergent rather than proprietary, and that AttnRes is the one component change that is not an efficiency tweak. I think that assessment is broadly right. The engineering that matters in K3 is less the invention of the parts than the discovery of what breaks when a validated 48B design is taken to 2.8T, together with the specific repairs.</p>
<h2 id="what-i-take-from-this">What I take from this</h2>
<p>Pulling the three axes back together: on the sequence axis K3 replaces softmax with a gated delta rule over a fixed-size state on three layers in four, with a decay floor chosen so the chunkwise kernel stays inside BF16. On the depth axis it does the opposite, replacing the plain residual sum with a softmax over nine block-level sources. On the width axis it keeps hard top-$k$ and repairs the balancing rule instead, by solving the dual that the old rule had been descending.</p>
<p>Reading the report through, what stayed with me is how much of K3 is repair work rather than invention. Two of the three axes arrived largely finished: Kimi Linear had validated the hybrid attention at 48B, and Attention Residuals had been validated at the same size in its own paper. Much of what is genuinely new at 2.8T answers a specific breakage instead. SiTU-GLU is there because a chain of four matmuls in the routed path made activations explode. Quantile balancing is there because the sign rule stops behaving somewhere between 384 and 896 experts. Even the decay floor, which reads like a modelling decision, exists to delete a slow path from the kernel rather than to fix anything the model was doing. None of that is glamorous, and all of it was necessary. That is probably the honest shape of most frontier engineering.</p>
<p>The depth axis is the part I keep coming back to. Attending over 9 block-level sources costs 81 pairwise scores per token, which was affordable on 2015 hardware and is free now. Depth has never been short of mixing schemes either, from Highway gates through DenseNet to hyper-connections. What took a decade was making the weighting depend on content, and cost was never what stood in the way. I keep turning that over, because it suggests the more useful question is less often what we cannot afford than which constants we never thought to make learnable.</p>
<p>There is a larger thing here than any of the three axes. K3 sits fourth of 580 on Artificial Analysis&rsquo; index and first of 99 on WebDev Arena, the first open-weights model to lead that board, and Moonshot published the weights, the kernels, the expert-parallel layer, and 47 pages of method alongside them. Every derivation in this post exists because that report exists. None of it needed access, a partnership, or a guess.</p>
<p>That is also why the post covers so little of the model. Three sections out of 47 pages leaves out the vision tower trained from scratch, Per-Head Muon, quantisation-aware training carried through reinforcement learning, a balanced expert-parallel layer with an existence proof attached, and the 51 million sandboxes the RL environments consumed. I set out to explain Kimi K3 and got as far as the attention, the residuals, and the router. The rest is sitting there, readable, which is not something I could have written about a frontier model two years ago.</p>
<h2 id="references">References</h2>
<ol>
<li><strong>Kimi Team (2026).</strong> <a href="https://arxiv.org/abs/2607.24653">Kimi K3: Open Frontier Intelligence</a>. <em>arXiv:2607.24653.</em> The primary source for this post. Architecture in §2, pre-training in §3, infrastructure in §5, and the Quantile Balancing derivation in Appendix C.</li>
<li><strong>Kimi Team (2025).</strong> <a href="https://arxiv.org/abs/2510.26692">Kimi Linear: An Expressive, Efficient Attention Architecture</a>. <em>arXiv:2510.26692.</em> The 48B model K3&rsquo;s hybrid attention was validated on. Source for the chunkwise KDA form and the 3:1 layer ratio.</li>
<li><strong>Kimi Team (2026).</strong> <a href="https://arxiv.org/abs/2603.15031">Attention Residuals</a>. <em>arXiv:2603.15031.</em> The AttnRes paper. Source for the dilution and unbounded-growth argument, the Block AttnRes construction, and the ablations on RMSNorm, softmax, and multi-head depth attention.</li>
<li><strong>Kimi Team (2025).</strong> <a href="https://arxiv.org/abs/2507.20534">Kimi K2: Open Agentic Intelligence</a>. <em>arXiv:2507.20534.</em> The predecessor. Source for the K2 column of the architecture comparison and for the weight-clipping mechanism K3 retains.</li>
<li><strong>Elango, V. et al. (2026).</strong> <a href="https://arxiv.org/abs/2601.18089">LatentMoE: Toward Optimal Accuracy per FLOP and Parameter in Mixture of Experts</a>. <em>arXiv:2601.18089.</em> The latent-routing design K3 builds Stable LatentMoE on.</li>
<li><strong>Yang, S., Kautz, J., &amp; Hatamizadeh, A. (2025).</strong> <a href="https://arxiv.org/abs/2412.06464">Gated Delta Networks: Improving Mamba2 with Delta Rule</a>. <em>ICLR 2025.</em> The direct ancestor of KDA, and the source of the scalar-per-head decay that channel-wise gating replaces.</li>
<li><strong>Schlag, I., Irie, K., &amp; Schmidhuber, J. (2021).</strong> <a href="https://arxiv.org/abs/2102.11174">Linear Transformers Are Secretly Fast Weight Programmers</a>. <em>ICML 2021.</em> The delta rule as an update to a linear associative memory.</li>
<li><strong>Dao, T., &amp; Gu, A. (2024).</strong> <a href="https://arxiv.org/abs/2405.21060">Transformers are SSMs: Generalized Models and Efficient Algorithms Through Structured State Space Duality</a>. <em>ICML 2024.</em> Mamba-2, and the chunkwise parallel form that KDA&rsquo;s algorithm follows.</li>
<li><strong>DeepSeek-AI (2024).</strong> <a href="https://arxiv.org/abs/2405.04434">DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model</a>. <em>arXiv:2405.04434.</em> Multi-head latent attention, which K3 retains in its global layers.</li>
<li><strong>Wang, L. et al. (2024).</strong> <a href="https://arxiv.org/abs/2408.15664">Auxiliary-Loss-Free Load Balancing Strategy for Mixture-of-Experts</a>. <em>arXiv:2408.15664.</em> The origin of the fixed-step sign rule. K3&rsquo;s report cites it as deployed in the <a href="https://arxiv.org/abs/2412.19437">DeepSeek-V3 Technical Report</a>, which is the form Appendix C shows to be SignSGD on the same dual.</li>
<li><strong>Lewis, M. et al. (2021).</strong> <a href="https://arxiv.org/abs/2103.16716">BASE Layers: Simplifying Training of Large, Sparse Models</a>. <em>ICML 2021.</em> The assignment-problem view of expert load balancing that Appendix C traces its lineage to.</li>
<li><strong>Dao, T. et al. (2022).</strong> <a href="https://arxiv.org/abs/2205.14135">FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness</a>. <em>NeurIPS 2022.</em> Online softmax, which Block AttnRes reuses to merge inter-block and intra-block results.</li>
<li><strong>Shazeer, N. (2020).</strong> <a href="https://arxiv.org/abs/2002.05202">GLU Variants Improve Transformer</a>. <em>arXiv:2002.05202.</em> SwiGLU, and the unbounded product that SiTU-GLU caps.</li>
<li><strong>Jordan, K. et al. (2024).</strong> <a href="https://kellerjordan.github.io/posts/muon/">Muon: An optimizer for hidden layers in neural networks</a>. The optimiser K3 refines into its per-head variant.</li>
<li><strong>Raschka, S. (2026).</strong> <a href="https://sebastianraschka.com/blog/2026/kimi-k3-architecture-notes.html">Kimi K3 Architecture Notes</a>. The most useful sceptical read: K3 as Kimi Linear scaled 58×, LatentMoE as convergent with Nemotron 3 Ultra, and AttnRes as the one non-tweak.</li>
<li><strong>Willison, S. (2026).</strong> <a href="https://simonwillison.net/2026/Jul/16/kimi-k3/">Kimi K3</a>. Hands-on notes and the pricing jump from K2.6.</li>
<li><strong>Lambert, N. (2026).</strong> <a href="https://www.interconnects.ai/p/open-models-recap-more-on-kimi-k3">Open models recap: more on Kimi K3</a>. On where the open-to-closed gap is narrow and where it is not.</li>
</ol>
<div class="bd-subscribe">
  <div class="bd-subscribe__copy">
    <h3 class="bd-subscribe__title">Get the next deep dive</h3>
    <p class="bd-subscribe__blurb">The follow-up goes inside K3&#39;s training stack: nine domain-and-effort experts consolidated by on-policy distillation, reward hacking in kernel-optimisation environments, and fifty-one million Firecracker sandboxes.</p>
  </div>
  <form
    class="bd-subscribe__form embeddable-buttondown-form"
    action="https://buttondown.com/api/emails/embed-subscribe/jawad"
    method="post"
    target="popupwindow"
    onsubmit="window.open('https://buttondown.com/jawad', 'popupwindow')"
  >
    <input class="bd-subscribe__input" type="email" name="email" placeholder="you@example.com" aria-label="Email address" required>
    <input type="hidden" value="1" name="embed">
    <button class="bd-subscribe__btn" type="submit">Subscribe</button>
  </form>
  <p class="bd-subscribe__rss">Prefer a feed reader? <a href="/index.xml">Subscribe via RSS</a>.</p>
</div>

<style>
.bd-subscribe{
  margin:2.5rem 0;
  padding:1.5rem 1.75rem;
  border:1px solid var(--border);
  border-radius:12px;
  background:var(--entry);
}
.bd-subscribe__title{margin:0 0 .35rem;font-size:1.2rem;color:var(--primary);}
.bd-subscribe__blurb{margin:0 0 1rem;color:var(--secondary);font-size:.95rem;line-height:1.5;}
.bd-subscribe__form{display:flex;gap:.5rem;flex-wrap:wrap;}
.bd-subscribe__input{
  flex:1 1 220px;
  padding:.6rem .75rem;
  border:1px solid var(--border);
  border-radius:8px;
  background:var(--theme);
  color:var(--primary);
  font-size:.95rem;
}
.bd-subscribe__input:focus{outline:2px solid var(--tertiary);outline-offset:1px;}
.bd-subscribe__btn{
  padding:.6rem 1.2rem;
  border:0;
  border-radius:8px;
  background:var(--primary);
  color:var(--theme);
  font-weight:600;
  font-size:.95rem;
  cursor:pointer;
  transition:opacity .2s ease;
}
.bd-subscribe__btn:hover{opacity:.85;}
.bd-subscribe__rss{margin:.85rem 0 0;font-size:.82rem;color:var(--secondary);}
.bd-subscribe__rss a{color:var(--secondary);text-decoration:underline;}
</style>

]]></content:encoded></item></channel></rss>