<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Haifeng Jin</title>
  <subtitle>Haifeng Jin's Blog</subtitle>
  <link href="https://haifengjin.com/feed.xml" rel="self"/>
  <link href="https://haifengjin.com/"/>
  <updated>2026-09-06T00:00:00+00:00</updated>
  <id>https://haifengjin.com/</id>
  <author>
    <name>Haifeng Jin</name>
  </author>
  <entry>
    <title>The Anatomy of an LLM</title>
    <link href="https://haifengjin.com/the-anatomy-of-an-llm/"/>
    <id>https://haifengjin.com/the-anatomy-of-an-llm/</id>
    <published>2026-09-06T00:00:00+00:00</published>
    <updated>2026-09-06T00:00:00+00:00</updated>
    <summary>Every tensor operation explained with code and diagrams</summary>
    <content type="html"><![CDATA[<p>In this article, we walk through every tensor operation in a modern large
language model (LLM). For an ML systems engineer, mastering the exact compute
graph is a must-have for optimizing training and inference performance, whereas
the statistical theory and modeling assumptions are often secondary. When both
are taught together, the abstract math often obscures the mechanical reality.</p>
<p>The same thing happens on the implementation side. Squeezing code into as few
lines as possible makes it cryptic, while piling on hardware-specific
performance optimizations quickly buries the core architecture under complex
engineering tricks. That is why our implementation does not optimize for lines
of code or raw runtime performance. Instead, it optimizes strictly for
readability at the compute level, while remaining mathematically equivalent to
heavily optimized production systems.</p>
<p>To reduce cognitive load, this article keeps the math to a strict minimum,
using visual diagrams and clean Python code to trace how every tensor moves
through the model. All code is available in the open-source
<a href="https://github.com/haifeng-jin/readable-llm">readable-llm</a> repository, which
implements every operation in pure Python with zero dependencies. At the end
of the article, we also put everything together in a
<a href="/the-anatomy-of-an-llm/graph/">complete computation graph</a> so you can visually
explore the end-to-end flow.</p>
<h2>LLMs have evolved</h2>
<p>Over the past few years, the Transformer architecture has evolved significantly
beyond the original "Attention Is All You Need" paper<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup>.
Several key techniques have been introduced and widely adopted as the modern standard,
including the decoder-only architecture<sup id="cite-2-a"><a href="#ref-2" class="citation-link">2</a></sup>, Mixture of Experts (MoE)<sup id="cite-3-a"><a href="#ref-3" class="citation-link">3</a></sup>,
Grouped-Query Attention (GQA)<sup id="cite-4-a"><a href="#ref-4" class="citation-link">4</a></sup>, Root Mean Square Normalization (RMSNorm)<sup id="cite-5-a"><a href="#ref-5" class="citation-link">5</a></sup>,
and Rotary Position Embedding (RoPE)<sup id="cite-6-a"><a href="#ref-6" class="citation-link">6</a></sup>. The architecture we walk through here
adopts all of these modern techniques.</p>
<h2>pipeline</h2>
<p>Let's start with an end-to-end abstraction of an LLM pipeline in the following
figure. We input a question, and the pipeline appends the answer to the string.
Note that we simplified the text by omitting special tokens and chat template
markers here (such as <code>&lt;bos&gt;</code>, <code>&lt;start_of_turn&gt;</code>, and <code>&lt;eos&gt;</code>), which models use
to mark the start and end of questions and responses.</p>
<p>Throughout the article, we use a single input prompt instance with no batch
dimension for simplicity. Most operations process this sequence directly, except
for a few cases in the MoE module where we further split that down to a single
token.</p>
<p>Notice that <code>pipeline</code> is drawn as a capsule. Throughout this article, a capsule
represents a module with internal structure and states that we will further
break down.</p>
<figure class="svg-figure"><svg viewBox="-60 -35 600 245" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="22.8" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="22.8" y="81">What is</tspan>
    <tspan x="22.8" y="100">1+1?</tspan>
  </text>
  <line x1="105" y1="100" x2="141.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="141.5,95.5 141.5,104.5 154.25,100" fill="var(--text-color)" />
  <rect x="154.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="182.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 182.25 100)">pipeline</text>
  <!-- Casual annotation -->
  <g class="casual-annotation">
    <!-- Casual circle around entire capsule (curved organic loop, no straight lines) -->
    <path d="M 182,1 C 158,1 139,22 137,65 C 135,110 138,150 148,178 C 157,198 173,201 184,200 C 200,199 217,186 224,155 C 229,125 229,80 224,45 C 220,22 210,8 195,3 C 185,-0.5 174,1 168,5"
          fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
    <!-- Dotted arrow to the up-right -->
    <path d="M 222,48 Q 225,26 238,18" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
    <line x1="238" y1="18" x2="234.7" y2="24.7" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <line x1="238" y1="18" x2="230.7" y2="19.6" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <!-- Handwriting note -->
    <g transform="rotate(-3 340 22)">
      <text x="245" y="2" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">a capsule is a module with</text>
      <text x="245" y="22" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">internal structure and states</text>
      <text x="245" y="42" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">that we will further break down</text>
    </g>
  </g>
  <line x1="210.25" y1="100" x2="246.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="246.75,95.5 246.75,104.5 259.5,100" fill="var(--text-color)" />
  <text x="274.5" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="274.5" y="81">What is</tspan>
    <tspan x="274.5" y="100">1+1?</tspan>
    <tspan x="274.5" y="119">It's 2.</tspan>
  </text>
</svg><figcaption>The end-to-end pipeline receives a raw text prompt and returns the completed text response.</figcaption></figure>

<p>As shown in the figure below, the pipeline breaks down into three parts:
<code>tokenizer.encode</code>, <code>model.generate</code>, and <code>tokenizer.decode</code>. Let's walk through them one by one.
Notice the small capsule in the top-right corner of the diagram, which shows
which module we are currently breaking down.</p>
<figure class="svg-figure"><svg viewBox="-8 -50 600 270" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="495" y="-35" width="85" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="537.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">pipeline</text>
  <!-- Casual annotation -->
  <g class="casual-annotation">
    <!-- Circle around top right label -->
    <path d="M 535,-38 C 564,-39 584,-34 584,-23 C 584,-12 562,-7 537,-7 C 505,-7 488,-12 489,-23 C 490,-34 510,-38 540,-38"
          fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
    <!-- Dotted arched arrow pointing left to note -->
    <path d="M 488,-23 Q 466,-30 445,-23" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
    <line x1="445" y1="-23" x2="450.4" y2="-28.2" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <line x1="445" y1="-23" x2="452.4" y2="-22.0" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <!-- Handwriting note to the left -->
    <g transform="rotate(-2 355 -23)">
      <text x="435" y="-33" text-anchor="end" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">shows which module</text>
      <text x="435" y="-13" text-anchor="end" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">we are zooming into</text>
    </g>
  </g>
  <text x="22.8" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="22.8" y="81">What is</tspan>
    <tspan x="22.8" y="100">1+1?</tspan>
  </text>
  <line x1="105" y1="100" x2="141.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="141.5,95.5 141.5,104.5 154.25,100" fill="var(--text-color)" />
  <rect x="154.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="182.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 182.25 100)">tokenizer.encode</text>
  <line x1="210.25" y1="100" x2="246.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="246.75,95.5 246.75,104.5 259.5,100" fill="var(--text-color)" />
  <rect x="259.5" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="287.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 287.5 100)">model.generate</text>
  <line x1="315.5" y1="100" x2="352" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="352,95.5 352,104.5 364.75,100" fill="var(--text-color)" />
  <rect x="364.75" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="392.75" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 392.75 100)">tokenizer.decode</text>
  <line x1="420.75" y1="100" x2="457.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="457.25,95.5 457.25,104.5 470,100" fill="var(--text-color)" />
  <text x="485" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="485" y="81">What is</tspan>
    <tspan x="485" y="100">1+1?</tspan>
    <tspan x="485" y="119">It's 2.</tspan>
  </text>
</svg><figcaption>The pipeline capsule breaks down into three sequential submodules.</figcaption></figure>

<p>Here is what <code>pipeline</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">pipeline</span><span class="p">(</span><span class="n">prompt</span><span class="p">,</span> <span class="n">tokenizer</span><span class="p">,</span> <span class="n">model</span><span class="p">):</span>
    <span class="c1"># input_ids: [seq_len]</span>
    <span class="n">input_ids</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
    <span class="c1"># output_ids: [total_seq_len]</span>
    <span class="n">output_ids</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">input_ids</span><span class="p">)</span>
    <span class="c1"># output_text: string</span>
    <span class="n">output_text</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">output_ids</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">output_text</span>
</code></pre></div>

<h2>tokenizer.encode</h2>
<p>The job of a tokenizer is to convert between text strings and lists of integer
token IDs. Let's see how that works in practice.</p>
<p>In the figure below, <code>tokenizer.encode</code> turns the input text into a list of token
IDs. To keep things simple, we can imagine that each word or punctuation mark is
its own token, though modern tokenizers usually split text into subword pieces.
The tokenizer first chops the string into individual tokens (like <code>['What', ' is', ' 1', '+', '1', '?']</code>),
and then looks each one up in a dictionary that maps tokens to unique integers.</p>
<figure class="svg-figure"><svg viewBox="-127.5 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="22.8" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="22.8" y="81">What is</tspan>
    <tspan x="22.8" y="100">1+1?</tspan>
  </text>
  <line x1="105" y1="100" x2="141.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="141.5,95.5 141.5,104.5 154.25,100" fill="var(--text-color)" />
  <rect x="154.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="182.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 182.25 100)">tokenizer.encode</text>
  <line x1="210.25" y1="100" x2="246.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="246.75,95.5 246.75,104.5 259.5,100" fill="var(--text-color)" />
  <rect x="259.5" y="28" width="56" height="144"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259.5" y1="52" x2="315.5" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259.5" y1="76" x2="315.5" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259.5" y1="100" x2="315.5" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259.5" y1="124" x2="315.5" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259.5" y1="148" x2="315.5" y2="148" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="287.5" y="40" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="287.5" y="64" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="287.5" y="88" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="287.5" y="112" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <text x="287.5" y="136" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
  <text x="287.5" y="160" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">30</text>
</svg><figcaption>tokenizer.encode splits the prompt string into tokens and maps each token to an integer ID.</figcaption></figure>

<p>The lookup table (or vocabulary) maps each token string to its assigned ID. It
contains all the possible tokens that the LLM can speak:</p>
<div class="highlight"><pre><span></span><code><span class="n">vocab</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"+"</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
    <span class="s2">"1"</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span>
    <span class="s2">"?"</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
    <span class="s2">" is"</span><span class="p">:</span> <span class="mi">318</span><span class="p">,</span>
    <span class="s2">" 1"</span><span class="p">:</span> <span class="mi">352</span><span class="p">,</span>
    <span class="s2">"What"</span><span class="p">:</span> <span class="mi">1867</span><span class="p">,</span>
    <span class="c1"># ...</span>
<span class="p">}</span>
</code></pre></div>

<p>Notice why the two <code>1</code>s in our prompt get different IDs (<code>352</code> and <code>16</code>):
tokenizers treat whitespace as part of the token, so <code>' 1'</code> (with a
leading space) and <code>'1'</code> (without) get distinct entries. That is also why
<code>' is'</code> carries its space. Decoding is then a plain string join, with no need
to guess where the spaces went.</p>
<p>Here is what <code>tokenizer.encode</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Tokenizer</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">vocab</span><span class="p">):</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">vocab</span> <span class="o">=</span> <span class="n">vocab</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">inv_vocab</span> <span class="o">=</span> <span class="p">{</span><span class="n">token_id</span><span class="p">:</span> <span class="n">token</span> <span class="k">for</span> <span class="n">token</span><span class="p">,</span> <span class="n">token_id</span> <span class="ow">in</span> <span class="n">vocab</span><span class="o">.</span><span class="n">items</span><span class="p">()}</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">encode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">):</span>
        <span class="n">tokens</span> <span class="o">=</span> <span class="n">split_tokens</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
        <span class="c1"># input_ids: [seq_len]</span>
        <span class="n">input_ids</span> <span class="o">=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">vocab</span><span class="p">[</span><span class="n">token</span><span class="p">]</span> <span class="k">for</span> <span class="n">token</span> <span class="ow">in</span> <span class="n">tokens</span><span class="p">]</span>
        <span class="k">return</span> <span class="n">input_ids</span>
</code></pre></div>

<p>Notice that <code>__init__</code> also builds <code>self.inv_vocab</code>. This is an inverted
dictionary that flips the keys and values, mapping each integer token ID back to
its string token. We don't need it inside <code>encode</code>, but it will be used in
<code>tokenizer.decode</code> to turn generated token IDs back into text.</p>
<p>We use <code>seq_len</code> (sequence length) to describe the number of tokens in this sequence.
This dimension will appear in tensor shapes throughout the rest of the model.</p>
<h2>tokenizer.decode</h2>
<p>Similarly, <code>tokenizer.decode</code> takes the full ID sequence (the original prompt
plus any newly generated tokens) and converts it back into text, using the
inverted lookup table <code>inv_vocab</code> we built earlier:</p>
<figure class="svg-figure"><svg viewBox="-132.5 -10 600 220" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="20" y="4" width="56" height="192"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="28" x2="76" y2="28" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="52" x2="76" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="76" x2="76" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="100" x2="76" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="124" x2="76" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="148" x2="76" y2="148" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="172" x2="76" y2="172" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="48" y="16" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="48" y="40" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="48" y="64" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="48" y="88" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <text x="48" y="112" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
  <text x="48" y="136" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">30</text>
  <text x="48" y="160" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">632</text>
  <text x="48" y="184" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--gray-text-color)">...</text>
  <line x1="76" y1="100" x2="112.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,95.5 112.5,104.5 125.25,100" fill="var(--text-color)" />
  <rect x="125.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="153.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 153.25 100)">tokenizer.decode</text>
  <line x1="181.25" y1="100" x2="217.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="217.75,95.5 217.75,104.5 230.5,100" fill="var(--text-color)" />
  <text x="245.5" y="100" text-anchor="start" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)">
    <tspan x="245.5" y="81">What is</tspan>
    <tspan x="245.5" y="100">1+1?</tspan>
    <tspan x="245.5" y="119">It's 2.</tspan>
  </text>
</svg><figcaption>tokenizer.decode takes the full sequence of token IDs and converts them back into text.</figcaption></figure>

<p>Here is what <code>tokenizer.decode</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Tokenizer</span><span class="p">:</span>
    <span class="c1"># ...</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">decode</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token_ids</span><span class="p">):</span>
        <span class="c1"># token_ids: [seq_len]</span>
        <span class="k">return</span> <span class="s2">""</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">inv_vocab</span><span class="p">[</span><span class="n">token_id</span><span class="p">]</span> <span class="k">for</span> <span class="n">token_id</span> <span class="ow">in</span> <span class="n">token_ids</span><span class="p">)</span>
</code></pre></div>

<h2>model.generate</h2>
<p><code>model.generate</code> is the module that generates new token IDs and appends them
to the sequence, as shown in the figure below.</p>
<figure class="svg-figure"><svg viewBox="-132.5 -10 600 220" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="20" y="28" width="56" height="144"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="52" x2="76" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="76" x2="76" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="100" x2="76" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="124" x2="76" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="148" x2="76" y2="148" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="48" y="40" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="48" y="64" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="48" y="88" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="48" y="112" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <text x="48" y="136" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
  <text x="48" y="160" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">30</text>
  <line x1="76" y1="100" x2="112.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,95.5 112.5,104.5 125.25,100" fill="var(--text-color)" />
  <rect x="125.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="153.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 153.25 100)">model.generate</text>
  <line x1="181.25" y1="100" x2="217.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="217.75,95.5 217.75,104.5 230.5,100" fill="var(--text-color)" />
  <rect x="230.5" y="4" width="56" height="192"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="28" x2="286.5" y2="28" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="52" x2="286.5" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="76" x2="286.5" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="100" x2="286.5" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="124" x2="286.5" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="148" x2="286.5" y2="148" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="172" x2="286.5" y2="172" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="258.5" y="16" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="258.5" y="40" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="258.5" y="64" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="258.5" y="88" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <text x="258.5" y="112" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
  <text x="258.5" y="136" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">30</text>
  <text x="258.5" y="160" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">632</text>
  <text x="258.5" y="184" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--gray-text-color)">...</text>
</svg><figcaption>model.generate takes the prompt token IDs and returns the complete token sequence.</figcaption></figure>

<p>Those new tokens are generated one by one by <code>greedy_sampler</code>. With each step,
the input sequence grows by one token as the newly predicted ID is appended:</p>
<figure class="svg-figure"><svg viewBox="-20 -66 600 340" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Sequence 1 -->
  <rect x="0" y="64" width="56" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="0" y1="88" x2="56" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="0" y1="112" x2="56" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="28" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="28" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="28" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <!-- Sequence 1 -> greedy_sampler 1 -->
  <line x1="56" y1="100" x2="155.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="155.25,95.5 155.25,104.5 168,100" fill="var(--text-color)" />
  <!-- Branch dot 1 (at x=112, 56px from Box 1 and 56px from Capsule 1) -->
  <circle cx="112" cy="100" r="3.5" fill="var(--text-color)" />
  <!-- Top route: Sequence 1 -> Sequence 2 (radius 28, y=-46 is 56px above capsule y=10) -->
  <path d="M 112 100 L 112 -18 A 28 28 0 0 1 140 -46 L 252 -46 A 28 28 0 0 1 280 -18 L 280 39.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="275.5,39.25 284.5,39.25 280,52" fill="var(--text-color)" />
  <!-- greedy_sampler 1 (x=168..224, width=56, height=180, rx=28, ry=28) -->
  <rect x="168" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="196" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 196 100)">greedy_sampler</text>
  <!-- greedy_sampler 1 -> next_token_id 1 (down) -->
  <line x1="196" y1="190" x2="196" y2="217.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="191.5,217.25 200.5,217.25 196,230" fill="var(--text-color)" />
  <!-- next_token_id 1 box (x=168..224, width=56) -->
  <rect x="168" y="230" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="196" y="242" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <!-- next_token_id 1 -> Sequence 2 (turn up, radius 28) -->
  <path d="M 224 242 L 252 242 A 28 28 0 0 0 280 214 L 280 160.75"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="275.5,160.75 284.5,160.75 280,148" fill="var(--text-color)" />
  <!-- Sequence 2 (x=252..308, width=56, center=280) -->
  <rect x="252" y="52" width="56" height="96"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="76" x2="308" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="100" x2="308" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="124" x2="308" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="280" y="64" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="280" y="88" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="280" y="112" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="280" y="136" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <!-- Sequence 2 -> greedy_sampler 2 -->
  <line x1="308" y1="100" x2="407.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="407.25,95.5 407.25,104.5 420,100" fill="var(--text-color)" />
  <!-- Branch dot 2 (at x=364, 56px from Box 2 and 56px from Capsule 2) -->
  <circle cx="364" cy="100" r="3.5" fill="var(--text-color)" />
  <!-- Top route: Sequence 2 -> Sequence 3 (radius 28, y=-46 is 56px above capsule y=10) -->
  <path d="M 364 100 L 364 -18 A 28 28 0 0 1 392 -46 L 504 -46 A 28 28 0 0 1 532 -18 L 532 27.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="527.5,27.25 536.5,27.25 532,40" fill="var(--text-color)" />
  <!-- greedy_sampler 2 (x=420..476, width=56, height=180, rx=28, ry=28) -->
  <rect x="420" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="448" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 448 100)">greedy_sampler</text>
  <!-- greedy_sampler 2 -> next_token_id 2 (down) -->
  <line x1="448" y1="190" x2="448" y2="217.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="443.5,217.25 452.5,217.25 448,230" fill="var(--text-color)" />
  <!-- next_token_id 2 box (x=420..476, width=56) -->
  <rect x="420" y="230" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="448" y="242" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
  <!-- next_token_id 2 -> Sequence 3 (turn up, radius 28) -->
  <path d="M 476 242 L 504 242 A 28 28 0 0 0 532 214 L 532 172.75"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="527.5,172.75 536.5,172.75 532,160" fill="var(--text-color)" />
  <!-- Sequence 3 (x=504..560, width=56, center=532) -->
  <rect x="504" y="40" width="56" height="120"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="504" y1="64" x2="560" y2="64" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="504" y1="88" x2="560" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="504" y1="112" x2="560" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="504" y1="136" x2="560" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="532" y="52" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="532" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="532" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <text x="532" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <text x="532" y="148" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">16</text>
</svg><figcaption>Inside model.generate, greedy_sampler predicts one token ID per step, which is appended to the sequence to form the input for the next step.</figcaption></figure>

<p>At each step, <code>model.generate</code> calls <code>greedy_sampler</code> to predict the next token,
appends that token to the sequence, and feeds the longer sequence back in. This
loop continues until the model produces an end-of-sequence token or reaches a
maximum length limit.</p>
<p>Here is what the generation loop looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="n">MAX_NEW_TOKENS</span> <span class="o">=</span> <span class="mi">128</span>
<span class="n">EOS_TOKEN_ID</span> <span class="o">=</span> <span class="mi">2</span>

<span class="k">class</span><span class="w"> </span><span class="nc">Model</span><span class="p">:</span>
    <span class="c1"># ...</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">generate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_ids</span><span class="p">):</span>
        <span class="c1"># input_ids: [seq_len]</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">MAX_NEW_TOKENS</span><span class="p">):</span>
            <span class="c1"># next_token_id: int</span>
            <span class="n">next_token_id</span> <span class="o">=</span> <span class="n">greedy_sampler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_ids</span><span class="p">)</span>
            <span class="c1"># input_ids: [seq_len + 1]</span>
            <span class="n">input_ids</span> <span class="o">=</span> <span class="n">input_ids</span> <span class="o">+</span> <span class="p">[</span><span class="n">next_token_id</span><span class="p">]</span>
            <span class="k">if</span> <span class="n">next_token_id</span> <span class="o">==</span> <span class="n">EOS_TOKEN_ID</span><span class="p">:</span>
                <span class="k">break</span>
        <span class="c1"># input_ids: [total_seq_len]</span>
        <span class="k">return</span> <span class="n">input_ids</span>
</code></pre></div>

<h2>greedy_sampler</h2>
<p>We use greedy sampling here for simplicity. In practice, modern LLMs typically
rely on stochastic sampling techniques like temperature scaling, top-$p$
(nucleus) sampling, and top-$k$ sampling to generate more diverse and creative
responses instead of always picking the single most probable token.</p>
<p>In a single call to <code>greedy_sampler</code>, <code>model.predict</code> turns <code>input_ids</code>
into <code>logits</code>, and then <code>argmax</code> (marked as <strong>A</strong> in the diagram) picks the
highest-scoring token ID. While capsules represent modules with internal
structure and states, circles in our diagrams represent pure functions with no
internal states:</p>
<figure class="svg-figure"><svg viewBox="-20 -40 600 280" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="460" y="-35" width="120" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="520" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">greedy_sampler</text>
  <!-- input_ids (x=62..118, center=90) -->
  <text x="90" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">input_ids</text>
  <text x="37" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 37 100)">seq_len</text>
  <rect x="62" y="64" width="56" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="62" y1="88" x2="118" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="62" y1="112" x2="118" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="90" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="90" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="90" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <!-- Arrow 1: input_ids -> model.predict (length 56, x=118..174) -->
  <line x1="118" y1="100" x2="161.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="161.25,95.5 161.25,104.5 174,100" fill="var(--text-color)" />
  <!-- model.predict (x=174..230, center=202, width=56) -->
  <rect x="174" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="202" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 202 100)">model.predict</text>
  <!-- Arrow 2: model.predict -> logits (length 56, x=230..286) -->
  <line x1="230" y1="100" x2="273.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="273.25,95.5 273.25,104.5 286,100" fill="var(--text-color)" />
  <!-- logits (x=286..310, center=298, width=24, each cell 24x24 square) -->
  <text x="298" y="18" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="298" y="37" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <rect x="286" y="52" width="24" height="96"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="286" y1="76" x2="310" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="286" y1="100" x2="310" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="286" y1="124" x2="310" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow 3: logits -> argmax (length 56, x=310..366) -->
  <line x1="310" y1="100" x2="353.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="353.25,95.5 353.25,104.5 366,100" fill="var(--text-color)" />
  <!-- argmax circle (cx=376, cy=100, r=10, width=20, x=366..386) -->
  <circle cx="376" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="376" y="100" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">A</text>
  <!-- Arrow 4: argmax -> next_token_id (length 56, x=386..442) -->
  <line x1="386" y1="100" x2="429.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="429.25,95.5 429.25,104.5 442,100" fill="var(--text-color)" />
  <!-- next_token_id (x=442..498, center=470, width=56) -->
  <text x="470" y="73" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">next_token_id</text>
  <rect x="442" y="88" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="470" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">10</text>
  <!-- Legend -->
  <g class="legend">
    <circle cx="255" cy="225" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="255" y="225" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">A</text>
    <text x="273" y="225" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">argmax</text>
  </g>
  <!-- Casual annotation -->
  <g class="casual-annotation">
    <!-- Circle around Circle A in diagram -->
    <path d="M 376,83 C 387,82 395,90 395,100 C 395,110 387,118 376,118 C 364,118 356,110 356,100 C 356,89 365,83 377,83 C 384,83 390,87 392,93"
          fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
    <!-- Dotted arrow down curving right -->
    <path d="M 376,118 Q 377,140 385,160" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
    <line x1="385" y1="160" x2="379.4" y2="155.0" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <line x1="385" y1="160" x2="386.0" y2="152.6" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <!-- Handwriting note -->
    <g transform="rotate(2 440 175)">
      <text x="395" y="155" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">these circles are</text>
      <text x="395" y="175" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">pure functions with</text>
      <text x="395" y="195" text-anchor="start" dominant-baseline="central" font-size="18"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">no internal states</text>
    </g>
  </g>
</svg><figcaption>In a single step of greedy_sampler, model.predict turns input_ids into logits (scores), and argmax selects the highest-scoring next_token_id.</figcaption></figure>

<p>Each number in the <code>logits</code> vector represents the score for the token associated
with that index. So the size of the vector is <code>vocab_size</code>, exactly one score
for each token in the vocabulary. For example, <code>logits[318]</code> is the score for
token ID 318, which corresponds to the string <code>' is'</code> in our vocabulary. <code>argmax</code>
simply picks whichever token ID has the highest score.</p>
<p>Here is what <code>argmax</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">argmax</span><span class="p">(</span><span class="n">logits</span><span class="p">):</span>
    <span class="c1"># logits: [vocab_size]</span>
    <span class="c1"># max_idx: int</span>
    <span class="n">max_idx</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">logits</span><span class="p">)):</span>
        <span class="k">if</span> <span class="n">logits</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">logits</span><span class="p">[</span><span class="n">max_idx</span><span class="p">]:</span>
            <span class="n">max_idx</span> <span class="o">=</span> <span class="n">i</span>
    <span class="k">return</span> <span class="n">max_idx</span>
</code></pre></div>

<p>Putting it together, here is what <code>greedy_sampler</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">greedy_sampler</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">input_ids</span><span class="p">):</span>
    <span class="c1"># input_ids: [seq_len]</span>
    <span class="c1"># logits: [vocab_size]</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">predict</span><span class="p">(</span><span class="n">input_ids</span><span class="p">)</span>
    <span class="c1"># next_token_id: int</span>
    <span class="n">next_token_id</span> <span class="o">=</span> <span class="n">argmax</span><span class="p">(</span><span class="n">logits</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">next_token_id</span>
</code></pre></div>

<h2>model.predict</h2>
<p>Now, we are ready to further break down <code>model.predict</code> into smaller components.
<code>model.predict</code> is the step that actually calls the neural network model. It
runs one forward pass of it.</p>
<p>It turns the <code>input_ids</code> into the logits for the next token across the
vocabulary. It breaks down into three parts: <code>embedding</code>, <code>decoder</code>,
and <code>lm_head</code>, as shown in the figure below.</p>
<figure class="svg-figure"><svg viewBox="-10 -40 600 260" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="465" y="-35" width="115" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="522.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">model.predict</text>
  <text x="77" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">input_ids</text>
  <text x="34" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 34 100)">seq_len</text>
  <rect x="49" y="64" width="56" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="49" y1="88" x2="105" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="49" y1="112" x2="105" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="77" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="77" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="77" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <line x1="105" y1="100" x2="141.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="141.5,95.5 141.5,104.5 154.25,100" fill="var(--text-color)" />
  <rect x="154.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="182.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 182.25 100)">embedding</text>
  <line x1="210.25" y1="100" x2="246.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="246.75,95.5 246.75,104.5 259.5,100" fill="var(--text-color)" />
  <rect x="259.5" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="287.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 287.5 100)">decoder</text>
  <line x1="315.5" y1="100" x2="352" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="352,95.5 352,104.5 364.75,100" fill="var(--text-color)" />
  <rect x="364.75" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="392.75" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 392.75 100)">lm_head</text>
  <line x1="420.75" y1="100" x2="457.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="457.25,95.5 457.25,104.5 470,100" fill="var(--text-color)" />
  <text x="482" y="37" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="509" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 509 100)">vocab_size</text>
  <rect x="470" y="52" width="24" height="96"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="470" y1="76" x2="494" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="470" y1="100" x2="494" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="470" y1="124" x2="494" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict breaks down into three core submodules.</figcaption></figure>

<p>Here is what <code>model.predict</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Model</span><span class="p">:</span>
    <span class="c1"># ...</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">predict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_ids</span><span class="p">):</span>
        <span class="c1"># input_ids: [seq_len]</span>
        <span class="c1"># embed_out: [seq_len, hidden_size]</span>
        <span class="n">embed_out</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">embedding</span><span class="p">(</span><span class="n">input_ids</span><span class="p">)</span>
        <span class="c1"># decoder_out: [seq_len, hidden_size]</span>
        <span class="n">decoder_out</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">decoder</span><span class="p">(</span><span class="n">embed_out</span><span class="p">)</span>
        <span class="c1"># logits: [vocab_size]</span>
        <span class="n">logits</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">lm_head</span><span class="p">(</span><span class="n">decoder_out</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">logits</span>
</code></pre></div>

<p>Now, we will tackle the three components one by one. We will explore <code>embedding</code>
and <code>lm_head</code> first before diving into the more complex <code>decoder</code>.</p>
<h2>embedding</h2>
<p>Zooming into the <code>embedding</code> module, it takes <code>input_ids</code> as input and maps each
integer ID to a vector of floating-point numbers, producing the matrix
<code>embed_out</code>:</p>
<figure class="svg-figure"><svg viewBox="-130 0 600 220" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="48" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">input_ids</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="56" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="76" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="76" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="48" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="48" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="48" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <line x1="76" y1="100" x2="112.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,95.5 112.5,104.5 125.25,100" fill="var(--text-color)" />
  <rect x="125.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="153.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 153.25 100)">embedding</text>
  <line x1="181.25" y1="100" x2="217.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="217.75,95.5 217.75,104.5 230.5,100" fill="var(--text-color)" />
  <text x="266.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embed_out</text>
  <text x="266.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="317.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 317.5 100)">seq_len</text>
  <rect x="230.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="88" x2="302.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="230.5" y1="112" x2="302.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="254.5" y1="64" x2="254.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="278.5" y1="64" x2="278.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M316.53,163.48 Q308.77,137.51 266.5,100" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
  <line x1="266.5" y1="100" x2="270.98" y2="111.13" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <line x1="266.5" y1="100" x2="277.91" y2="103.72" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <text x="288" y="182" text-anchor="middle" dominant-baseline="central" font-size="22"
        font-family="var(--handwriting-font)" fill="var(--hue-6)"
        transform="rotate(-15 288 182)">end of backprop</text>
</svg><figcaption>The embedding module maps integer token IDs to dense vectors, producing the 2D embed_out matrix.</figcaption></figure>

<p>Note that <code>embed_out</code> is the end of the backward pass during training. Because
token IDs are discrete integers, gradients cannot backpropagate any further
into the input text. Gradients update the weights in <code>embedding_table</code>, and
backpropagation stops there.</p>
<p><code>embedding</code> is a token-wise operation, which means it processes each token in
isolation. No token depends on the value or position of other tokens to pass
through this module. As shown in the figure below, we can conceptually split the
sequence into individual tokens, process each one independently, and combine the
vectors back together to form <code>embed_out</code>:</p>
<figure class="svg-figure"><svg viewBox="-45 -85 600 310" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="48" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">input_ids</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="56" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="76" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="76" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="48" y="76" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <text x="48" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <text x="48" y="124" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <path d="M76,76 C94.25,76 94.25,68 112.5,68" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,63.5 112.5,72.5 125.25,68" fill="var(--text-color)" />
  <line x1="76" y1="100" x2="112.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,95.5 112.5,104.5 125.25,100" fill="var(--text-color)" />
  <path d="M76,124 C94.25,124 94.25,132 112.5,132" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="112.5,127.5 112.5,136.5 125.25,132" fill="var(--text-color)" />
  <text x="153.25" y="41" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">token_id</text>
  <rect x="125.25" y="56" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="153.25" y="68" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">1867</text>
  <rect x="125.25" y="88" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="153.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">318</text>
  <rect x="125.25" y="120" width="56" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="153.25" y="132" text-anchor="middle" dominant-baseline="central" font-size="16" font-family="var(--main-font)" fill="var(--text-color)">352</text>
  <line x1="181.25" y1="68" x2="208.5" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="208.5,63.5 208.5,72.5 221.25,68" fill="var(--text-color)" />
  <line x1="181.25" y1="100" x2="208.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="208.5,95.5 208.5,104.5 221.25,100" fill="var(--text-color)" />
  <line x1="181.25" y1="132" x2="208.5" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="208.5,127.5 208.5,136.5 221.25,132" fill="var(--text-color)" />
  <text x="231.25" y="-72" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embedding_table</text>
  <text x="231.25" y="-53" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="282.25" y="-2" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 282.25 -2)">vocab_size</text>
  <rect x="195.25" y="-38" width="72" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="195.25" y1="-14" x2="267.25" y2="-14" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="195.25" y1="10" x2="267.25" y2="10" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="219.25" y1="-38" x2="219.25" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="243.25" y1="-38" x2="243.25" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="231.25" y1="34" x2="231.25" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="226.75,45.25 235.75,45.25 231.25,58" fill="var(--text-color)" />
  <path d="M231.25,34 A66,66 0 0,0 226.92,90.99" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="218.09,80.75 226.48,77.48 226.92,90.99" fill="var(--text-color)" />
  <path d="M231.25,34 A98,98 0 0,0 226.70,123.10" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="217.38,113.31 225.59,109.62 226.70,123.10" fill="var(--text-color)" />
  <circle cx="231.25" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="231.25" y="68" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">L</text>
  <circle cx="231.25" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="231.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">L</text>
  <circle cx="231.25" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="231.25" y="132" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">L</text>
  <line x1="241.25" y1="68" x2="268.5" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.5,63.5 268.5,72.5 281.25,68" fill="var(--text-color)" />
  <line x1="241.25" y1="100" x2="268.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.5,95.5 268.5,104.5 281.25,100" fill="var(--text-color)" />
  <line x1="241.25" y1="132" x2="268.5" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.5,127.5 268.5,136.5 281.25,132" fill="var(--text-color)" />
  <text x="317.25" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="281.25" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="305.25" y1="56" x2="305.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="329.25" y1="56" x2="329.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="281.25" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="305.25" y1="88" x2="305.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="329.25" y1="88" x2="329.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="281.25" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="305.25" y1="120" x2="305.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="329.25" y1="120" x2="329.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M353.25,68 C371.875,68 371.875,76 390.5,76" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="390.5,71.5 390.5,80.5 403.25,76" fill="var(--text-color)" />
  <line x1="353.25" y1="100" x2="390.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="390.5,95.5 390.5,104.5 403.25,100" fill="var(--text-color)" />
  <path d="M353.25,132 C371.875,132 371.875,124 390.5,124" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="390.5,119.5 390.5,128.5 403.25,124" fill="var(--text-color)" />
  <text x="439.25" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embed_out</text>
  <text x="439.25" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="490.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 490.25 100)">seq_len</text>
  <rect x="403.25" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="403.25" y1="88" x2="475.25" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="403.25" y1="112" x2="475.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="427.25" y1="64" x2="427.25" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="451.25" y1="64" x2="451.25" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="455" y="-80" width="90" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="500" y="-68" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">embedding</text>
  <!-- Legend -->
  <g class="legend">
    <rect x="173.00" y="185.00" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="201.00" y="195.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="279.00" cy="195.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="279.00" y="195.00" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">L</text>
    <text x="297.00" y="195.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">lookup</text>
  </g>
</svg><figcaption>model.predict -> embedding<br>Inside embedding, each token ID selects its corresponding row from embedding_table via lookup (circled L) to form embed_out.</figcaption></figure>

<p>A few quick notes here:
1. Splitting into individual tokens is purely for illustration. Real hardware
   does not loop through tokens one by one in Python; modern frameworks look up
   the entire sequence at once in parallel via batched tensor indexing
   (<code>embedding_table[input_ids]</code>).
2. Most operations in an LLM are token-wise. The attention layer in the decoder
   blocks is the sole exception where tokens actually interact with one another.
3. The blue box represents learnable weights. Here, <code>embedding_table</code> holds the
   model parameters updated during training. We will use this same blue style
   for all weights throughout the rest of the diagrams to distinguish them from
   intermediate activations (shown with white backgrounds).</p>
<p>During the lookup, each token ID indexes into <code>embedding_table</code> of shape
<code>[vocab_size, hidden_size]</code>, where each row is an embedding vector corresponding
to one specific token ID.</p>
<p>How large is <code>vocab_size</code> in practice? Early models (such as LLaMA 1 and 2) used
smaller vocabularies of 32K tokens. Modern open-weights models have shifted
toward much larger vocabularies to represent multilingual text and code more
compactly.</p>
<p>Throughout the rest of the article, we quote real numbers from two reference
points: Qwen 3.6 and the much larger Kimi K3 2.8T. Qwen 3.6 ships in two
flavors, and the difference matters for the numbers we quote. The <strong>dense</strong> 27B
runs every one of its parameters for every single token. The <strong>sparse</strong> 35B is a
Mixture of Experts model: it carries more parameters in total, but each token
only activates a small slice of them, so the compute per token stays close to
that of a much smaller model.</p>
<p>We use the dense 27B wherever the number does not depend on MoE, and switch to
the sparse 35B once we reach the expert-specific dimensions.</p>
<p>Interestingly, model parameter count does not dictate <code>vocab_size</code>: Qwen 3.6 27B
uses a <code>vocab_size</code> of 248K, while Kimi K3 2.8T uses 164K. A larger vocabulary
compresses text into fewer tokens, but it also means <code>embedding_table</code> has 150K
to 250K rows.</p>
<p>Each embedding vector has a length of <code>hidden_size</code>, an important dimension used
throughout the model. Looking up every token ID in the sequence turns the
<code>[seq_len]</code> list of IDs into a <code>[seq_len, hidden_size]</code> matrix.</p>
<p>Here is what <code>lookup</code> and the <code>embedding</code> module look like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">lookup</span><span class="p">(</span><span class="n">token_id</span><span class="p">,</span> <span class="n">embedding_table</span><span class="p">):</span>
    <span class="c1"># token_id: int</span>
    <span class="c1"># embedding_table: [vocab_size, hidden_size]</span>
    <span class="c1"># token_vec: [hidden_size]</span>
    <span class="n">token_vec</span> <span class="o">=</span> <span class="n">embedding_table</span><span class="p">[</span><span class="n">token_id</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">token_vec</span>

<span class="k">def</span><span class="w"> </span><span class="nf">embedding</span><span class="p">(</span><span class="n">input_ids</span><span class="p">,</span> <span class="n">embedding_table</span><span class="p">):</span>
    <span class="c1"># input_ids: [seq_len]</span>
    <span class="c1"># embedding_table: [vocab_size, hidden_size]</span>
    <span class="c1"># embed_out: [seq_len, hidden_size]</span>
    <span class="n">embed_out</span> <span class="o">=</span> <span class="p">[</span><span class="n">lookup</span><span class="p">(</span><span class="n">token_id</span><span class="p">,</span> <span class="n">embedding_table</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_id</span> <span class="ow">in</span> <span class="n">input_ids</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">embed_out</span>
</code></pre></div>

<h2>lm_head</h2>
<p>After learning about <code>embedding</code>, whose output is fed into <code>decoder</code>,
we turn to <code>lm_head</code>. It takes the output of the decoder stack (<code>decoder_out</code>)
and converts it into <code>logits</code>, as shown in the figure below:</p>
<figure class="svg-figure"><svg viewBox="-125 0 600 220" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">lm_head</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="258.5" y="37" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="285.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 285.5 100)">vocab_size</text>
  <rect x="246.5" y="52" width="24" height="96"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="76" x2="270.5" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="100" x2="270.5" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="124" x2="270.5" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M308.53,156.48 Q300.77,127.51 258.5,88" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
  <line x1="258.5" y1="88" x2="262.98" y2="99.13" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <line x1="258.5" y1="88" x2="269.91" y2="91.72" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <text x="280" y="175" text-anchor="middle" dominant-baseline="central" font-size="22"
        font-family="var(--handwriting-font)" fill="var(--hue-6)"
        transform="rotate(-15 280 175)">start of backprop</text>
</svg><figcaption>The lm_head module takes the final hidden states and predicts next-token logits across the vocabulary.</figcaption></figure>

<p>Just as <code>embed_out</code> marks the end of backpropagation, the logits mark its
start.  During training, these logits are compared against the actual next
token (the ground truth) to compute the loss. Backpropagation begins at this
loss, flowing gradients backward through <code>lm_head</code>, across all decoder blocks,
and finally into <code>embedding_table</code> to update all the learnable weights.</p>
<p>During inference, the model does not compute a loss; instead, <code>greedy_sampler</code>
uses the logits directly to pick the next token.</p>
<p>You might notice that the input shape of <code>decoder</code> is identical to its
output shape. In other words, the tensor shape <code>[seq_len, hidden_size]</code> remains
the same after passing through all the decoder blocks.</p>
<p>How large is <code>hidden_size</code> in practice? Unlike <code>vocab_size</code>, which is largely
independent of model scale, <code>hidden_size</code> grows directly with parameter count.
Smaller models around 7B or 8B typically use a <code>hidden_size</code> of 4,096. Moving up
the scale, Qwen 3.6 27B uses 5,120, while Kimi K3 2.8T reaches 7,168.
Wider vectors give the model more capacity to hold rich
representations for each token as they flow through the stack.</p>
<p>Looking inside <code>lm_head</code>, it breaks down into three sequential parts: <code>rms_norm</code>,
<code>logits_matmul</code>, and <code>slice</code>:</p>
<figure class="svg-figure"><svg viewBox="-20 -40 600 260" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="485" y="-35" width="85" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="527.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">lm_head</text>
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">rms_norm</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <rect x="246.5" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="274.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 274.5 100)">logits_matmul</text>
  <line x1="302.5" y1="100" x2="339" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="339,95.5 339,104.5 351.75,100" fill="var(--text-color)" />
  <rect x="351.75" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="379.75" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 379.75 100)">slice</text>
  <line x1="407.75" y1="100" x2="444.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="444.25,95.5 444.25,104.5 457,100" fill="var(--text-color)" />
  <text x="469" y="37" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="496" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 496 100)">vocab_size</text>
  <rect x="457" y="52" width="24" height="96"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="457" y1="76" x2="481" y2="76" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="457" y1="100" x2="481" y2="100" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="457" y1="124" x2="481" y2="124" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> lm_head<br>lm_head breaks down into three sequential operations.</figcaption></figure>

<p>Here is what <code>lm_head</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">lm_head</span><span class="p">(</span><span class="n">decoder_out</span><span class="p">,</span> <span class="n">gamma</span><span class="p">,</span> <span class="n">embedding_table_T</span><span class="p">):</span>
    <span class="c1"># decoder_out: [seq_len, hidden_size]</span>
    <span class="c1"># gamma: [hidden_size]</span>
    <span class="c1"># embedding_table_T: [hidden_size, vocab_size]</span>

    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="n">rms_out</span> <span class="o">=</span> <span class="n">rms_norm</span><span class="p">(</span><span class="n">decoder_out</span><span class="p">,</span> <span class="n">gamma</span><span class="p">)</span>
    <span class="c1"># all_logits: [seq_len, vocab_size]</span>
    <span class="n">all_logits</span> <span class="o">=</span> <span class="n">logits_matmul</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">embedding_table_T</span><span class="p">)</span>
    <span class="c1"># logits: [vocab_size]</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">slice_last</span><span class="p">(</span><span class="n">all_logits</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">logits</span>
</code></pre></div>

<p>Let's walk through these three operations one by one.</p>
<h2>rms_norm</h2>
<p>Before projecting <code>decoder_out</code> to vocabulary logits, <code>lm_head</code> first
normalizes the input using <code>rms_norm</code> (short for Root Mean Square Normalization,
commonly written as RMSNorm). Because normalization does not change tensor
dimensions, the input and output shapes are identical:</p>
<figure class="svg-figure"><svg viewBox="-115 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">rms_norm</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> lm_head<br>rms_norm normalizes the input activations while preserving the original tensor shape.</figcaption></figure>

<p>Zooming into <code>rms_norm</code>: it is a token-wise operation, meaning each row of the
input matrix is normalized independently:</p>
<figure class="svg-figure"><svg viewBox="-20 -40 600 265" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M92,76 C110.25,76 110.25,68 128.5,68" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,63.5 128.5,72.5 141.25,68" fill="var(--text-color)" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <path d="M92,124 C110.25,124 110.25,132 128.5,132" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,127.5 128.5,136.5 141.25,132" fill="var(--text-color)" />
  <text x="177.25" y="41" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">token_vec</text>
  <text x="177.25" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="141.25" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="56" x2="165.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="56" x2="189.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="88" x2="165.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="88" x2="189.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="120" x2="165.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="120" x2="189.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="213.25" y1="68" x2="249.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,63.5 249.75,72.5 262.5,68" fill="var(--text-color)" />
  <line x1="213.25" y1="100" x2="249.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,95.5 249.75,104.5 262.5,100" fill="var(--text-color)" />
  <line x1="213.25" y1="132" x2="249.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,127.5 249.75,136.5 262.5,132" fill="var(--text-color)" />
  <rect x="236.5" y="10" width="72" height="24"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="260.5" y1="10" x2="260.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="284.5" y1="10" x2="284.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="272.5" y="-24" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gamma</text>
  <text x="272.5" y="-5" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <line x1="272.5" y1="34" x2="272.5" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.0,45.25 277.0,45.25 272.5,58" fill="var(--text-color)" />
  <path d="M272.5,34 A66,66 0 0,0 268.17,90.99" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="259.34,80.75 267.73,77.48 268.17,90.99" fill="var(--text-color)" />
  <path d="M272.5,34 A98,98 0 0,0 267.95,123.10" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="258.63,113.31 266.84,109.62 267.95,123.10" fill="var(--text-color)" />
  <circle cx="272.5" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="68" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">N</text>
  <circle cx="272.5" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">N</text>
  <circle cx="272.5" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="132" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">N</text>
  <line x1="282.5" y1="68" x2="309.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="309.75,63.5 309.75,72.5 322.5,68" fill="var(--text-color)" />
  <line x1="282.5" y1="100" x2="309.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="309.75,95.5 309.75,104.5 322.5,100" fill="var(--text-color)" />
  <line x1="282.5" y1="132" x2="309.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="309.75,127.5 309.75,136.5 322.5,132" fill="var(--text-color)" />
  <text x="358.5" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="322.5" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="346.5" y1="56" x2="346.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="370.5" y1="56" x2="370.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="322.5" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="346.5" y1="88" x2="346.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="370.5" y1="88" x2="370.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="322.5" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="346.5" y1="120" x2="346.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="370.5" y1="120" x2="370.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M394.5,68 C413.125,68 413.125,76 431.75,76" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="431.75,71.5 431.75,80.5 444.5,76" fill="var(--text-color)" />
  <line x1="394.5" y1="100" x2="431.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="431.75,95.5 431.75,104.5 444.5,100" fill="var(--text-color)" />
  <path d="M394.5,132 C413.125,132 413.125,124 431.75,124" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="431.75,119.5 431.75,128.5 444.5,124" fill="var(--text-color)" />
  <text x="480.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="480.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="531.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 531.5 100)">seq_len</text>
  <rect x="444.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="444.5" y1="88" x2="516.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="444.5" y1="112" x2="516.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="468.5" y1="64" x2="468.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="492.5" y1="64" x2="492.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="490" y="-35" width="80" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="530" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">rms_norm</text>
  <!-- Legend -->
  <g class="legend">
    <rect x="191.00" y="185.00" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="219.00" y="195.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="297.00" cy="195.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="297.00" y="195.00" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">N</text>
    <text x="315.00" y="195.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">norm_token</text>
  </g>
</svg><figcaption>model.predict -> lm_head -> rms_norm<br>Inside rms_norm, each token vector is scaled by its root-mean-square norm (circled N) and multiplied elementwise by gamma.</figcaption></figure>

<p>Here, <code>gamma</code> is the only learnable weight. It has a shape of <code>[hidden_size]</code>,
matching the dimension of each token vector. It is multiplied element-wise with
each token vector.</p>
<p>RMSNorm is an improved version of the earlier LayerNorm. It keeps the rescaling
and drops the mean subtraction, which turned out to matter little in practice.</p>
<p>Here is the Python code for <code>norm_token</code>, which takes a single token vector and
the learnable weight <code>gamma</code>:</p>
<div class="highlight"><pre><span></span><code><span class="n">EPS</span> <span class="o">=</span> <span class="mf">1e-6</span>

<span class="k">def</span><span class="w"> </span><span class="nf">norm_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">gamma</span><span class="p">):</span>
    <span class="c1"># token_vec: [hidden_size]</span>
    <span class="n">sum_of_squares</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">token_vec</span><span class="p">:</span>
        <span class="n">sum_of_squares</span> <span class="o">+=</span> <span class="n">v</span> <span class="o">**</span> <span class="mi">2</span>
    <span class="n">rms</span> <span class="o">=</span> <span class="p">(</span><span class="n">sum_of_squares</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">token_vec</span><span class="p">)</span> <span class="o">+</span> <span class="n">EPS</span><span class="p">)</span> <span class="o">**</span> <span class="mf">0.5</span>

    <span class="c1"># output: [hidden_size]</span>
    <span class="n">output</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">token_vec</span><span class="p">)):</span>
        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">token_vec</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">/</span> <span class="n">rms</span> <span class="o">*</span> <span class="n">gamma</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">output</span>

<span class="k">def</span><span class="w"> </span><span class="nf">rms_norm</span><span class="p">(</span><span class="n">tensor</span><span class="p">,</span> <span class="n">gamma</span><span class="p">):</span>
    <span class="c1"># tensor: [seq_len, hidden_size]</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="n">rms_out</span> <span class="o">=</span> <span class="p">[</span><span class="n">norm_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">gamma</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">tensor</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">rms_out</span>
</code></pre></div>

<p>This <code>rms_norm</code> module is used throughout the model; we will see it again soon
in the attention and feed-forward blocks.</p>
<h2>logits_matmul</h2>
<p>The output of <code>rms_norm</code> feeds directly into <code>logits_matmul</code>, which performs a
matrix multiplication to produce <code>all_logits</code>:</p>
<figure class="svg-figure"><svg viewBox="-119 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">logits_matmul</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="294.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">all_logits</text>
  <text x="294.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <text x="357.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 357.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="342.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="342.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="318.5" y1="64" x2="318.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> lm_head<br>logits_matmul projects normalized hidden states to vocabulary dimension scores for all tokens.</figcaption></figure>

<p>You may notice that the output tensor still has the same number of tokens
(<code>seq_len</code>), but the length of each token vector has expanded from
<code>hidden_size</code> to <code>vocab_size</code>. Each row in <code>all_logits</code> is now a vector of
logits, containing an unnormalized prediction score for every token in the
vocabulary (as we introduced in <code>greedy_sampler</code>).</p>
<p>Zooming into <code>logits_matmul</code>: like <code>embedding</code> and <code>rms_norm</code>, this is a
token-wise operation. The input matrix splits into individual token vectors.
Each token vector multiplies the weight matrix <code>embedding_table.T</code>, and the
resulting vectors are combined back into <code>all_logits</code>:</p>
<figure class="svg-figure"><svg viewBox="-20 -85 640 305" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Splitting arrows into 3 token vectors -->
  <path d="M92,76 C110.25,76 110.25,68 128.5,68" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,63.5 128.5,72.5 141.25,68" fill="var(--text-color)" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <path d="M92,124 C110.25,124 110.25,132 128.5,132" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,127.5 128.5,136.5 141.25,132" fill="var(--text-color)" />
  <!-- 3 Token Vectors -->
  <text x="177.25" y="41" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">token_vec</text>
  <text x="177.25" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="141.25" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="56" x2="165.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="56" x2="189.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="88" x2="165.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="88" x2="189.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="120" x2="165.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="120" x2="189.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrows from token vectors to matmul circles -->
  <line x1="213.25" y1="68" x2="249.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,63.5 249.75,72.5 262.5,68" fill="var(--text-color)" />
  <line x1="213.25" y1="100" x2="249.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,95.5 249.75,104.5 262.5,100" fill="var(--text-color)" />
  <line x1="213.25" y1="132" x2="249.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,127.5 249.75,136.5 262.5,132" fill="var(--text-color)" />
  <!-- Weight Matrix: embedding_table.T (vocab_size=96, hidden_size=72) -->
  <text x="272.5" y="-72" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embedding_table.T</text>
  <text x="272.5" y="-53" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <text x="336.5" y="-2" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 336.5 -2)">hidden_size</text>
  <rect x="224.5" y="-38" width="96" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="224.5" y1="-14" x2="320.5" y2="-14" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="224.5" y1="10" x2="320.5" y2="10" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="248.5" y1="-38" x2="248.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="272.5" y1="-38" x2="272.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="296.5" y1="-38" x2="296.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrows from embedding_table.T down to matmul circles -->
  <line x1="272.5" y1="34" x2="272.5" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.0,45.25 277.0,45.25 272.5,58" fill="var(--text-color)" />
  <path d="M272.5,34 A66,66 0 0,0 268.17,90.99" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="259.34,80.75 267.73,77.48 268.17,90.99" fill="var(--text-color)" />
  <path d="M272.5,34 A98,98 0 0,0 267.95,123.10" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="258.63,113.31 266.84,109.62 267.95,123.10" fill="var(--text-color)" />
  <!-- 3 Matmul Circles (cx=272.5) -->
  <circle cx="272.5" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="60.93" x2="279.57" y2="75.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="75.07" x2="279.57" y2="60.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="272.5" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="92.93" x2="279.57" y2="107.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="107.07" x2="279.57" y2="92.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="272.5" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="124.93" x2="279.57" y2="139.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="139.07" x2="279.57" y2="124.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Arrows from matmul circles to output logit vectors -->
  <line x1="282.5" y1="68" x2="319.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,63.5 319.75,72.5 332.5,68" fill="var(--text-color)" />
  <line x1="282.5" y1="100" x2="319.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,95.5 319.75,104.5 332.5,100" fill="var(--text-color)" />
  <line x1="282.5" y1="132" x2="319.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,127.5 319.75,136.5 332.5,132" fill="var(--text-color)" />
  <!-- 3 Output Logit Vectors (vocab_size=96, 4 cells x 24) -->
  <text x="380.5" y="41" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="380.5" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <rect x="332.5" y="56" width="96" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="56" x2="356.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="56" x2="380.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404.5" y1="56" x2="404.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="332.5" y="88" width="96" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="88" x2="356.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="88" x2="380.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404.5" y1="88" x2="404.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="332.5" y="120" width="96" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="120" x2="356.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="120" x2="380.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404.5" y1="120" x2="404.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Converging arrows from logit vectors to all_logits matrix -->
  <path d="M428.5,68 C447.125,68 447.125,76 465.75,76" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="465.75,71.5 465.75,80.5 478.5,76" fill="var(--text-color)" />
  <line x1="428.5" y1="100" x2="465.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="465.75,95.5 465.75,104.5 478.5,100" fill="var(--text-color)" />
  <path d="M428.5,132 C447.125,132 447.125,124 465.75,124" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="465.75,119.5 465.75,128.5 478.5,124" fill="var(--text-color)" />
  <!-- Output Matrix: all_logits (vocab_size=96, seq_len=72) -->
  <text x="526.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">all_logits</text>
  <text x="526.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <text x="589.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 589.5 100)">seq_len</text>
  <rect x="478.5" y="64" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="478.5" y1="88" x2="574.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="478.5" y1="112" x2="574.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="502.5" y1="64" x2="502.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="526.5" y1="64" x2="526.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="550.5" y1="64" x2="550.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Badge -->
  <rect x="474" y="-80" width="110" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="529" y="-68" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">logits_matmul</text>
  <!-- Legend -->
  <g class="legend">
    <rect x="235.00" y="180.00" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="263.00" y="190.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="341.00" cy="190.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="333.93" y1="182.93" x2="348.07" y2="197.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="333.93" y1="197.07" x2="348.07" y2="182.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="359.00" y="190.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
  </g>
</svg><figcaption>model.predict -> lm_head -> logits_matmul<br>Inside logits_matmul, a matrix multiplication (circled X) projects each token vector against embedding_table.T.</figcaption></figure>

<p>Notice that the learnable weight matrix is the transpose of <code>embedding_table</code>.
In the <code>embedding</code> step, the table mapped discrete token IDs into dense vectors
of length <code>hidden_size</code>. Multiplying by its transpose does the reverse,
projecting each token vector from shape <code>[hidden_size]</code> back to <code>[vocab_size]</code>.
Reusing the embedding weights here is called <strong>weight tying</strong>, which saves a
significant number of parameters. When weights are untied, models use a
separate projection matrix of shape <code>[hidden_size, vocab_size]</code> instead, but the
computation is identical.</p>
<p>Because multiplying a vector by a matrix is used in several places throughout
the model, we write a general <code>matmul</code> helper in plain Python. Here is what
<code>matmul</code> and <code>logits_matmul</code> look like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">matmul</span><span class="p">(</span><span class="n">vec</span><span class="p">,</span> <span class="n">matrix</span><span class="p">):</span>
    <span class="c1"># vec: [in_dim]</span>
    <span class="c1"># matrix: [in_dim, out_dim]</span>
    <span class="n">in_dim</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">vec</span><span class="p">)</span>
    <span class="n">out_dim</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">matrix</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
    <span class="n">output</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">out_dim</span><span class="p">):</span>
        <span class="n">dot_product</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">vec</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">*</span> <span class="n">matrix</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="n">col</span><span class="p">]</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">in_dim</span><span class="p">))</span>
        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dot_product</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">output</span>

<span class="k">def</span><span class="w"> </span><span class="nf">logits_matmul</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">embedding_table_T</span><span class="p">):</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="c1"># embedding_table_T: [hidden_size, vocab_size]</span>

    <span class="c1"># all_logits: [seq_len, vocab_size]</span>
    <span class="n">all_logits</span> <span class="o">=</span> <span class="p">[</span><span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">embedding_table_T</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">rms_out</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">all_logits</span>
</code></pre></div>

<h2>slice</h2>
<p><code>slice</code> is the last step in <code>lm_head</code>, as well as the final operation inside
<code>model.predict</code>.</p>
<p>As we saw earlier, the output of <code>model.predict</code> is a single 1D logits vector of
shape <code>[vocab_size]</code>. But <code>logits_matmul</code> produces <code>all_logits</code>, a 2D matrix
containing <code>seq_len</code> logit vectors. The job of <code>slice</code> is simple: it extracts
the very last vector from the matrix and returns it, as shown in the figure
below:</p>
<figure class="svg-figure"><svg viewBox="-67.5 -20 600 170" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="458.5" y="-15" width="64" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="490.5" y="-3" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">slice</text>
  <text x="68" y="9" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">all_logits</text>
  <text x="68" y="111" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <text x="5" y="60" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 60)">seq_len</text>
  <rect x="20" y="24" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="48" x2="116" y2="48" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="72" x2="116" y2="72" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="24" x2="44" y2="96" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="24" x2="68" y2="96" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="24" x2="92" y2="96" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M116,48 C141.625,48 141.625,28 167.25,28" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="167.25,23.5 167.25,32.5 180,28" fill="var(--text-color)" />
  <path d="M116,84 C141.625,84 141.625,104 167.25,104" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="167.25,99.5 167.25,108.5 180,104" fill="var(--text-color)" />
  <rect x="180" y="4" width="96" height="48"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="180" y1="28" x2="276" y2="28" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="4" x2="204" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="4" x2="228" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="4" x2="252" y2="52" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="180" y="92" width="96" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="92" x2="204" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="92" x2="228" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="92" x2="252" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="228" y="131" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <line x1="276" y1="104" x2="339.25" y2="104" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="339.25,99.5 339.25,108.5 352,104" fill="var(--text-color)" />
  <text x="400" y="77" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">logits</text>
  <text x="400" y="131" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">vocab_size</text>
  <rect x="352" y="92" width="96" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="376" y1="92" x2="376" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="400" y1="92" x2="400" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="424" y1="92" x2="424" y2="116" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> lm_head -> slice<br>slice extracts the final row of all_logits corresponding to the last token position.</figcaption></figure>

<p>That final vector provides the scores for the sampler to pick the next token.
Because slicing the last row of a 2D list in Python only requires index <code>-1</code>,
the code is straightforward:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">slice_last</span><span class="p">(</span><span class="n">all_logits</span><span class="p">):</span>
    <span class="c1"># all_logits: [seq_len, vocab_size]</span>
    <span class="c1"># logits: [vocab_size]</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">all_logits</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">logits</span>
</code></pre></div>

<h2>decoder</h2>
<p>So far, we have explored everything before and after <code>decoder</code>:
<code>embedding</code> at the input and <code>lm_head</code> at the output. Now, we are ready to dive
into the core and most complex part of the Transformer architecture.</p>
<p>As shown in the figure below, the input and output shapes of the module are
identical, <code>[seq_len, hidden_size]</code>:</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embed_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">decoder</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>The decoder processes token embeddings through stacked transformer layers.</figcaption></figure>

<p>Zooming into <code>decoder</code>: it consists of a stack of identical decoder
blocks chained in series, with each block's output feeding directly into the
next:</p>
<figure class="svg-figure"><svg viewBox="-19 -40 600 260" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="491" y="-35" width="80" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="531" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">decoder</text>
  <text x="85" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">embed_out</text>
  <text x="85" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="34" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 34 100)">seq_len</text>
  <rect x="49" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="49" y1="88" x2="121" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="49" y1="112" x2="121" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="73" y1="64" x2="73" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="97" y1="64" x2="97" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="121" y1="100" x2="136.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="136.25,95.5 136.25,104.5 149,100" fill="var(--text-color)" />
  <rect x="149" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="177" y="100" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 177 100)">decoder_block 0</text>
  <line x1="205" y1="100" x2="220.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="220.25,95.5 220.25,104.5 233,100" fill="var(--text-color)" />
  <rect x="233" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="261" y="100" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 261 100)">decoder_block 1</text>
  <line x1="289" y1="100" x2="305" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <circle cx="314" cy="100" r="2.5" fill="var(--text-color)" />
  <circle cx="323" cy="100" r="2.5" fill="var(--text-color)" />
  <circle cx="332" cy="100" r="2.5" fill="var(--text-color)" />
  <line x1="340" y1="100" x2="344.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="344.25,95.5 344.25,104.5 357,100" fill="var(--text-color)" />
  <rect x="357" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="385" y="100" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 385 100)">decoder_block N-1</text>
  <line x1="413" y1="100" x2="428.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="428.25,95.5 428.25,104.5 441,100" fill="var(--text-color)" />
  <text x="477" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out</text>
  <text x="477" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="528" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 528 100)">seq_len</text>
  <rect x="441" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="441" y1="88" x2="513" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="441" y1="112" x2="513" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="465" y1="64" x2="465" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="489" y1="64" x2="489" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder<br>The decoder consists of a sequential stack of identical decoder_block layers.</figcaption></figure>

<p>As a reference, Qwen 3.6 27B has 64 decoder blocks, and Kimi K3 2.8T has 93 decoder
blocks.</p>
<p>Because each individual block preserves tensor dimensions, the sequence retains
its <code>[seq_len, hidden_size]</code> shape from the first block to the last.</p>
<p>Here is what <code>decoder</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">decoder</span><span class="p">(</span><span class="n">embed_out</span><span class="p">,</span> <span class="n">decoder_blocks</span><span class="p">):</span>
    <span class="c1"># embed_out: [seq_len, hidden_size]</span>
    <span class="c1"># decoder_blocks: list of decoder_block layers</span>

    <span class="c1"># decoder_out: [seq_len, hidden_size]</span>
    <span class="n">decoder_out</span> <span class="o">=</span> <span class="n">embed_out</span>
    <span class="k">for</span> <span class="n">decoder_block</span> <span class="ow">in</span> <span class="n">decoder_blocks</span><span class="p">:</span>
        <span class="n">decoder_out</span> <span class="o">=</span> <span class="n">decoder_block</span><span class="p">(</span><span class="n">decoder_out</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">decoder_out</span>
</code></pre></div>

<h2>decoder_block</h2>
<p>Zooming into a single <code>decoder_block</code>: each block contains two sub-layers with
residual connections: grouped query attention (<code>gqa_block</code>) and mixture of
experts (<code>moe_block</code>). The input passes through <code>gqa_block</code> and is added back to
itself element-wise via a residual connection, and that result passes through
<code>moe_block</code> and is added back to itself element-wise:</p>
<figure class="svg-figure"><svg viewBox="0 -40 600 330" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="475" y="-35" width="115" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="532.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">decoder_block</text>
  <text x="96" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_in_i</text>
  <text x="96" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="44" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 44 100)">seq_len</text>
  <rect x="60" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="88" x2="132" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="112" x2="132" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="84" y1="64" x2="84" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="108" y1="64" x2="108" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="132" y1="100" x2="183.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="183.25,95.5 183.25,104.5 196,100" fill="var(--text-color)" />
  <circle cx="164" cy="100" r="3" fill="var(--text-color)" />
  <rect x="196" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="224" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 224 100)">gqa_block</text>
  <line x1="252" y1="100" x2="261.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="261.25,95.5 261.25,104.5 274,100" fill="var(--text-color)" />
  <path d="M164,100 L164,197 A28,28 0 0,0 192,225 L256,225 A28,28 0 0,0 284,197 L284,122.75" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="279.5,122.75 288.5,122.75 284,110" fill="var(--text-color)" />
  <circle cx="284" cy="100" r="10" fill="none" stroke="var(--text-color)" stroke-width="2" />
  <line x1="274" y1="100" x2="294" y2="100" stroke="var(--text-color)" stroke-width="2" />
  <line x1="284" y1="90" x2="284" y2="110" stroke="var(--text-color)" stroke-width="2" />
  <line x1="294" y1="100" x2="335.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="335.25,95.5 335.25,104.5 348,100" fill="var(--text-color)" />
  <circle cx="316" cy="100" r="3" fill="var(--text-color)" />
  <rect x="348" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="376" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 376 100)">moe_block</text>
  <line x1="404" y1="100" x2="413.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="413.25,95.5 413.25,104.5 426,100" fill="var(--text-color)" />
  <path d="M316,100 L316,197 A28,28 0 0,0 344,225 L408,225 A28,28 0 0,0 436,197 L436,122.75" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="431.5,122.75 440.5,122.75 436,110" fill="var(--text-color)" />
  <circle cx="436" cy="100" r="10" fill="none" stroke="var(--text-color)" stroke-width="2" />
  <line x1="426" y1="100" x2="446" y2="100" stroke="var(--text-color)" stroke-width="2" />
  <line x1="436" y1="90" x2="436" y2="110" stroke="var(--text-color)" stroke-width="2" />
  <line x1="446" y1="100" x2="455.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="455.25,95.5 455.25,104.5 468,100" fill="var(--text-color)" />
  <text x="504" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">decoder_out_i</text>
  <text x="504" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="556" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 556 100)">seq_len</text>
  <rect x="468" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="468" y1="88" x2="540" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="468" y1="112" x2="540" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="492" y1="64" x2="492" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="516" y1="64" x2="516" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Legend -->
  <g class="legend">
    <circle cx="245.00" cy="258.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="235.00" y1="258.00" x2="255.00" y2="258.00" stroke="var(--text-color)" stroke-width="2" />
    <line x1="245.00" y1="248.00" x2="245.00" y2="268.00" stroke="var(--text-color)" stroke-width="2" />
    <text x="263.00" y="258.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">element-wise add</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block<br>decoder_block combines a gqa_block and a moe_block, each followed by an elementwise residual addition.</figcaption></figure>

<p>Because the residual additions are element-wise, the tensor shape
<code>[seq_len, hidden_size]</code> remains unchanged through both <code>gqa_block</code> and
<code>moe_block</code>.</p>
<p>Here is what a single <code>decoder_block</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">add</span><span class="p">(</span><span class="n">tensor_a</span><span class="p">,</span> <span class="n">tensor_b</span><span class="p">):</span>
    <span class="c1"># tensor_a: [seq_len, hidden_size]</span>
    <span class="c1"># tensor_b: [seq_len, hidden_size]</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tensor_a</span><span class="p">)</span>
    <span class="n">hidden_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tensor_a</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>

    <span class="c1"># output: [seq_len, hidden_size]</span>
    <span class="n">output</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">hidden_size</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">)]</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">hidden_size</span><span class="p">):</span>
            <span class="n">output</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">tensor_a</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span> <span class="o">+</span> <span class="n">tensor_b</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">j</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">output</span>

<span class="k">def</span><span class="w"> </span><span class="nf">decoder_block</span><span class="p">(</span><span class="n">decoder_in</span><span class="p">,</span> <span class="n">gqa_block</span><span class="p">,</span> <span class="n">moe_block</span><span class="p">):</span>
    <span class="c1"># decoder_in: [seq_len, hidden_size]</span>

    <span class="c1"># gqa_out: [seq_len, hidden_size]</span>
    <span class="n">gqa_out</span> <span class="o">=</span> <span class="n">gqa_block</span><span class="p">(</span><span class="n">decoder_in</span><span class="p">)</span>
    <span class="n">residual_1</span> <span class="o">=</span> <span class="n">add</span><span class="p">(</span><span class="n">decoder_in</span><span class="p">,</span> <span class="n">gqa_out</span><span class="p">)</span>

    <span class="c1"># moe_out: [seq_len, hidden_size]</span>
    <span class="n">moe_out</span> <span class="o">=</span> <span class="n">moe_block</span><span class="p">(</span><span class="n">residual_1</span><span class="p">)</span>

    <span class="c1"># decoder_out: [seq_len, hidden_size]</span>
    <span class="n">decoder_out</span> <span class="o">=</span> <span class="n">add</span><span class="p">(</span><span class="n">residual_1</span><span class="p">,</span> <span class="n">moe_out</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">decoder_out</span>
</code></pre></div>

<h2>moe_block</h2>
<p><code>gqa_block</code> runs first inside the block, but <code>moe_block</code> is the simpler of the
two, so let's start there. As shown in the figure below, the input and
output shapes of the module are identical, <code>[seq_len, hidden_size]</code>:</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">moe_block_in</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">moe_block</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">moe_block_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block<br>The moe_block computes mixture-of-experts feedforward updates.</figcaption></figure>

<p>Zooming into <code>moe_block</code>: it consists of three steps in sequence: normalizing
tokens with <code>rms_norm</code> (which we introduced earlier in <code>lm_head</code>), computing
routing weights with <code>router</code>, and passing them to <code>moe</code> to compute the weighted
expert outputs:</p>
<figure class="svg-figure"><svg viewBox="-52 -40 600 295" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="443" y="-35" width="95" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="490.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">moe_block</text>
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">moe_block_in</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="115.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="115.25,95.5 115.25,104.5 128,100" fill="var(--text-color)" />
  <rect x="128" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="156" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 156 100)">rms_norm</text>
  <line x1="184" y1="100" x2="207.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="207.25,95.5 207.25,104.5 220,100" fill="var(--text-color)" />
  <rect x="220" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="248" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 248 100)">router</text>
  <line x1="276" y1="100" x2="299.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="299.25,95.5 299.25,104.5 312,100" fill="var(--text-color)" />
  <rect x="312" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="340" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 340 100)">moe</text>
  <line x1="368" y1="100" x2="391.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="391.25,95.5 391.25,104.5 404,100" fill="var(--text-color)" />
  <text x="440" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">moe_block_out</text>
  <text x="440" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="492" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 492 100)">seq_len</text>
  <rect x="404" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404" y1="88" x2="476" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404" y1="112" x2="476" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="428" y1="64" x2="428" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="452" y1="64" x2="452" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Branch from rms_norm output to bottom of moe -->
  <circle cx="202" cy="100" r="3" fill="var(--text-color)" />
  <path d="M202,100 L202,211 A24,24 0 0,0 226,235 L316,235 A24,24 0 0,0 340,211 L340,202.75" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="335.5,202.75 344.5,202.75 340,190" fill="var(--text-color)" />
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block<br>Inside moe_block, inputs are normalized by rms_norm, routed by router, and processed by the moe module.</figcaption></figure>

<p>Here is what <code>moe_block</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">moe_block</span><span class="p">(</span><span class="n">moe_in</span><span class="p">,</span> <span class="n">rms_norm</span><span class="p">,</span> <span class="n">router</span><span class="p">,</span> <span class="n">moe</span><span class="p">):</span>
    <span class="c1"># moe_in: [seq_len, hidden_size]</span>

    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="n">rms_out</span> <span class="o">=</span> <span class="n">rms_norm</span><span class="p">(</span><span class="n">moe_in</span><span class="p">)</span>

    <span class="c1"># top_weights: [seq_len, num_experts]</span>
    <span class="n">top_weights</span> <span class="o">=</span> <span class="n">router</span><span class="p">(</span><span class="n">rms_out</span><span class="p">)</span>

    <span class="c1"># moe_out: [seq_len, hidden_size]</span>
    <span class="n">moe_out</span> <span class="o">=</span> <span class="n">moe</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">top_weights</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">moe_out</span>
</code></pre></div>

<p>A new dimension shows up in <code>top_weights</code>: <code>num_experts</code>. This represents the
total number of expert networks available in the layer. We will dive into how
tokens route to these experts in the next few sections.</p>
<p>Notice that <code>moe_block</code> normalizes its input with <code>rms_norm</code> right at the
start, before computing routing weights and expert outputs and after the start
of branching out the residual connection. This arrangement is known as
<strong>Pre-LN</strong> (pre-layer normalization).</p>
<p>This technique works hand in hand with the residual connection. Without Pre-LN,
the tensor between the end of one residual connection and the start of the next
would be normalized. Every token would have to pass through that normalization
at each choke point, with no way around it.</p>
<p>With Pre-LN, normalization happens inside the branch between the start and end
of the residual connection. This moves normalization off the model's main choke
points and places it onto a parallel path, leaving the residual stream untouched.</p>
<h2>router</h2>
<p>The normalized matrix is fed into <code>router</code>, which outputs a matrix called <code>top_weights</code>
with shape <code>[seq_len, num_experts]</code> as shown in the figure below:</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">router</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">top_weights</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="258.5" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="282.5" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="306.5" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="258.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.7</text>
  <text x="282.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="306.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <text x="258.5" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="282.5" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="306.5" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block<br>The router selects the top experts and calculates their gating weights for each token.</figcaption></figure>

<p><code>num_experts</code> is an important hyperparameter here: it sets the number of
subnetworks, also known as experts, in each MoE module. This is where we switch
to the sparse Qwen 3.6 35B, which has 256 experts, while Kimi K3 2.8T uses 896
experts per MoE module.</p>
<p>Zooming into <code>router</code>: it is again a token-wise operation. The input matrix splits into
individual token vectors. Each token vector passes through <code>route_token</code> (circled as <strong>R</strong>)
along with the weight matrix <code>w_router</code> to compute its expert routing weights.
The output row vectors are then stacked back into the matrix <code>top_weights</code>:</p>
<figure class="svg-figure"><svg viewBox="-20 -85 600 310" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="490" y="-80" width="70" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="525" y="-68" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">router</text>
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <path d="M92,76 C110.25,76 110.25,68 128.5,68" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,63.5 128.5,72.5 141.25,68" fill="var(--text-color)" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <path d="M92,124 C110.25,124 110.25,132 128.5,132" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,127.5 128.5,136.5 141.25,132" fill="var(--text-color)" />
  <text x="177.25" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="141.25" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="56" x2="165.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="56" x2="189.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="88" x2="165.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="88" x2="189.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="120" x2="165.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="120" x2="189.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="213.25" y1="68" x2="249.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,63.5 249.75,72.5 262.5,68" fill="var(--text-color)" />
  <line x1="213.25" y1="100" x2="249.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,95.5 249.75,104.5 262.5,100" fill="var(--text-color)" />
  <line x1="213.25" y1="132" x2="249.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,127.5 249.75,136.5 262.5,132" fill="var(--text-color)" />
  <text x="272.5" y="-72" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">w_router</text>
  <text x="272.5" y="-53" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <text x="323.5" y="-2" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 323.5 -2)">hidden_size</text>
  <rect x="236.5" y="-38" width="72" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="236.5" y1="-14" x2="308.5" y2="-14" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="236.5" y1="10" x2="308.5" y2="10" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="260.5" y1="-38" x2="260.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="284.5" y1="-38" x2="284.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="272.5" y1="34" x2="272.5" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.0,45.25 277.0,45.25 272.5,58" fill="var(--text-color)" />
  <path d="M272.5,34 A66,66 0 0,0 268.17,90.99" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="259.34,80.75 267.73,77.48 268.17,90.99" fill="var(--text-color)" />
  <path d="M272.5,34 A98,98 0 0,0 267.95,123.10" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="258.63,113.31 266.84,109.62 267.95,123.10" fill="var(--text-color)" />
  <circle cx="272.5" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="68" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">R</text>
  <circle cx="272.5" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">R</text>
  <circle cx="272.5" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="272.5" y="132" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">R</text>
  <line x1="282.5" y1="68" x2="319.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,63.5 319.75,72.5 332.5,68" fill="var(--text-color)" />
  <line x1="282.5" y1="100" x2="319.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,95.5 319.75,104.5 332.5,100" fill="var(--text-color)" />
  <line x1="282.5" y1="132" x2="319.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,127.5 319.75,136.5 332.5,132" fill="var(--text-color)" />
  <rect x="332.5" y="56" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="56" x2="356.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="56" x2="380.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="344.5" y="68" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="368.5" y="68" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="392.5" y="68" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <rect x="332.5" y="88" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="88" x2="356.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="88" x2="380.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="344.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.7</text>
  <text x="368.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="392.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <rect x="332.5" y="120" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="120" x2="356.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="120" x2="380.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="344.5" y="132" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="368.5" y="132" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="392.5" y="132" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="368.5" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <path d="M404.5,68 C422.75,68 422.75,76 441.25,76" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,71.5 441.25,80.5 454,76" fill="var(--text-color)" />
  <line x1="404.5" y1="100" x2="441.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,95.5 441.25,104.5 454,100" fill="var(--text-color)" />
  <path d="M404.5,132 C422.75,132 422.75,124 441.25,124" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,119.5 441.25,128.5 454,124" fill="var(--text-color)" />
  <text x="490" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">top_weights</text>
  <text x="490" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <text x="542" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 542 100)">seq_len</text>
  <rect x="454" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="454" y1="88" x2="526" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="454" y1="112" x2="526" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="478" y1="64" x2="478" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="502" y1="64" x2="502" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="466" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="490" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="514" y="76" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="466" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.7</text>
  <text x="490" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="514" y="100" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <text x="466" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="490" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="514" y="124" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <g class="legend">
    <rect x="180" y="185" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="208" y="195" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="286" cy="195" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="286" y="195" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">R</text>
    <text x="304" y="195" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">route_token</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block -> router<br>Inside router, matrix multiplication and softmax score all experts to produce the top experts and weights.</figcaption></figure>

<p>Inside <code>route_token</code>, we see another important hyperparameter: <code>TOP_K</code>. In an MoE
module, we do not activate all <code>num_experts</code> for every token. Instead, we only
select the top ones. <code>TOP_K</code> is the number of experts we select for each token;
we will see how they compute outputs in the next few sections.</p>
<p>As a reference, Qwen 3.6 35B activates 9 out of 256 experts per token, while Kimi
K3 2.8T activates 16 out of 896 experts per token.</p>
<p>In <code>route_token</code>, we first multiply the token vector by the weight matrix
<code>w_router</code> to produce a logits vector, where each value corresponds to one
expert.</p>
<p>Then, we construct the <code>top_weights</code> vector. We pick the <code>TOP_K</code> highest scores
from the logits vector, set the ones not in <code>TOP_K</code> to 0, and normalize the
selected scores with a <code>softmax</code> function so they sum to 1.</p>
<p>For example, with <code>TOP_K = 2</code>, suppose a token produces raw logits <code>[1.2, 1.6, 0.3]</code>
for 3 experts. The top two scores are <code>1.2</code> (expert 0) and <code>1.6</code> (expert 1). Normalizing
them with softmax yields approximately <code>.4</code> and <code>.6</code>. Setting the unselected expert 2 to 0
gives the final vector <code>[.4, .6, 0]</code>, matching the first row of <code>top_weights</code> in the diagram
above.</p>
<p>Here is what <code>route_token</code> and <code>router</code> look like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">math</span>

<span class="n">TOP_K</span> <span class="o">=</span> <span class="mi">2</span>

<span class="k">def</span><span class="w"> </span><span class="nf">softmax</span><span class="p">(</span><span class="n">scores</span><span class="p">):</span>
    <span class="c1"># scores: list of scalars</span>
    <span class="n">max_score</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">scores</span><span class="p">)</span>
    <span class="n">exp_scores</span> <span class="o">=</span> <span class="p">[</span><span class="n">math</span><span class="o">.</span><span class="n">exp</span><span class="p">(</span><span class="n">s</span> <span class="o">-</span> <span class="n">max_score</span><span class="p">)</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">scores</span><span class="p">]</span>
    <span class="n">sum_exp</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">exp_scores</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">s</span> <span class="o">/</span> <span class="n">sum_exp</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">exp_scores</span><span class="p">]</span>

<span class="k">def</span><span class="w"> </span><span class="nf">route_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_router</span><span class="p">):</span>
    <span class="c1"># token_vec: [hidden_size]</span>
    <span class="c1"># w_router:  [hidden_size, num_experts]</span>

    <span class="c1"># logits: [num_experts]</span>
    <span class="c1"># example value: [1.2, 1.6, 0.3]</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_router</span><span class="p">)</span>

    <span class="n">num_experts</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">logits</span><span class="p">)</span>

    <span class="c1"># indices: [num_experts]</span>
    <span class="c1"># example value: [0, 1, 2]</span>
    <span class="n">indices</span> <span class="o">=</span> <span class="p">[</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_experts</span><span class="p">)]</span>

    <span class="c1"># sorted_indices: [num_experts]</span>
    <span class="c1"># example value: [1, 0, 2]</span>
    <span class="n">sorted_indices</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">indices</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">logits</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>

    <span class="c1"># top_indices: [TOP_K]</span>
    <span class="c1"># example value: [1, 0]</span>
    <span class="n">top_indices</span> <span class="o">=</span> <span class="n">sorted_indices</span><span class="p">[:</span><span class="n">TOP_K</span><span class="p">]</span>

    <span class="c1"># top_logits: [TOP_K]</span>
    <span class="c1"># example value: [1.6, 1.2]</span>
    <span class="n">top_logits</span> <span class="o">=</span> <span class="p">[</span><span class="n">logits</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">top_indices</span><span class="p">]</span>

    <span class="c1"># top_probs: [TOP_K]</span>
    <span class="c1"># example value: [0.6, 0.4]</span>
    <span class="n">top_probs</span> <span class="o">=</span> <span class="n">softmax</span><span class="p">(</span><span class="n">top_logits</span><span class="p">)</span>

    <span class="c1"># top_weights: [num_experts]</span>
    <span class="c1"># example value: [0.4, 0.6, 0]</span>
    <span class="n">top_weights</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">]</span> <span class="o">*</span> <span class="n">num_experts</span>
    <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">TOP_K</span><span class="p">):</span>
        <span class="n">top_weights</span><span class="p">[</span><span class="n">top_indices</span><span class="p">[</span><span class="n">k</span><span class="p">]]</span> <span class="o">=</span> <span class="n">top_probs</span><span class="p">[</span><span class="n">k</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">top_weights</span>

<span class="k">def</span><span class="w"> </span><span class="nf">router</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">w_router</span><span class="p">):</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="c1"># top_weights: [seq_len, num_experts]</span>
    <span class="n">top_weights</span> <span class="o">=</span> <span class="p">[</span><span class="n">route_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_router</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">rms_out</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">top_weights</span>
</code></pre></div>

<h2>moe</h2>
<p><code>moe</code> takes the normalized tensor <code>rms_out</code> and the router output <code>top_weights</code>
as inputs, producing <code>moe_out</code> with the exact same shape as <code>rms_out</code>:</p>
<figure class="svg-figure"><svg viewBox="-65 -15 600 280" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Top input: rms_out -->
  <text x="66" y="5" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="66" y="107" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="14" y="56" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 14 56)">seq_len</text>
  <rect x="30" y="20" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="30" y1="44" x2="102" y2="44" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="30" y1="68" x2="102" y2="68" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="54" y1="20" x2="54" y2="92" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="78" y1="20" x2="78" y2="92" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Bottom input: top_weights -->
  <text x="66" y="141" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">top_weights</text>
  <text x="66" y="243" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <text x="14" y="192" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 14 192)">seq_len</text>
  <rect x="30" y="156" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="30" y1="180" x2="102" y2="180" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="30" y1="204" x2="102" y2="204" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="54" y1="156" x2="54" y2="228" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="78" y1="156" x2="78" y2="228" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="42" y="168" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="66" y="168" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="90" y="168" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="42" y="192" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.7</text>
  <text x="66" y="192" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="90" y="192" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <text x="42" y="216" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="66" y="216" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="90" y="216" text-anchor="middle" dominant-baseline="central" font-size="13" font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <!-- Curved arrows into moe -->
  <path d="M102,56 C153,56 153,84 191.25,84" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="191.25,79.5 191.25,88.5 204,84" fill="var(--text-color)" />
  <path d="M102,192 C153,192 153,164 191.25,164" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="191.25,159.5 191.25,168.5 204,164" fill="var(--text-color)" />
  <!-- moe capsule -->
  <rect x="204" y="34" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="232" y="124" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 232 124)">moe</text>
  <!-- Output arrow -->
  <line x1="260" y1="124" x2="349.25" y2="124" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="349.25,119.5 349.25,128.5 362,124" fill="var(--text-color)" />
  <!-- Output: moe_out -->
  <text x="398" y="73" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">moe_out</text>
  <text x="398" y="175" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="450" y="124" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 450 124)">seq_len</text>
  <rect x="362" y="88" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="362" y1="112" x2="434" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="362" y1="136" x2="434" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="386" y1="88" x2="386" y2="160" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="410" y1="88" x2="410" y2="160" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block<br>The moe module processes token representations through sparse combinations of expert sub-networks.</figcaption></figure>

<p>To make this easy to follow, we split <code>rms_out</code> and <code>top_weights</code> into individual
token rows. Taking token 0 as an example, <code>rms_out_0</code> and <code>top_weights_0</code> are the
corresponding rows. We also split <code>top_weights_0</code> into three scalar weights, one
for each expert. Here is how they work together to produce <code>moe_out_0</code>:</p>
<figure class="svg-figure"><svg viewBox="0 0 600 395" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="465" y="15" width="120" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="525" y="27" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">moe (1 token)</text>
  <!-- ==================== LEFT TOP: rms_out_0 ==================== -->
  <text x="56" y="85" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">rms_out_0</text>
  <text x="56" y="135" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="20" y="98" width="72" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="98" x2="44" y2="122" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="98" x2="68" y2="122" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Black dot branching from rms_out_0 to 3 experts -->
  <line x1="92" y1="110" x2="115" y2="110" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="115" cy="110" r="3" fill="var(--text-color)" />
  <!-- Branch up to expert_0 -->
  <path d="M115,110 L115,94 A16,16 0 0,1 131,78 L137.25,78"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="137.25,73.5 137.25,82.5 150,78" fill="var(--text-color)" />
  <!-- Branch straight to expert_1 -->
  <line x1="115" y1="110" x2="137.25" y2="110" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="137.25,105.5 137.25,114.5 150,110" fill="var(--text-color)" />
  <!-- Branch down to expert_2 -->
  <path d="M115,110 L115,126 A16,16 0 0,0 131,142 L137.25,142"
        fill="none" stroke="var(--gray-text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="137.25,137.5 137.25,146.5 150,142" fill="var(--gray-text-color)" />
  <!-- ==================== EXPERT CAPSULES (x=150..222, width=72, height=24, rx=12) ==================== -->
  <rect x="150" y="66" width="72" height="24" rx="12" ry="12"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="186" y="78" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="600"
        font-family="var(--main-font)" fill="var(--text-color)">expert_0</text>
  <rect x="150" y="98" width="72" height="24" rx="12" ry="12"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="186" y="110" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="600"
        font-family="var(--main-font)" fill="var(--text-color)">expert_1</text>
  <rect x="150" y="130" width="72" height="24" rx="12" ry="12"
        fill="var(--background-color)" stroke="var(--gray-text-color)" stroke-width="2" />
  <text x="186" y="142" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="600"
        font-family="var(--main-font)" fill="var(--gray-text-color)">expert_2</text>
  <!-- ==================== EXPERT HORIZONTAL ARROWS INTO MATMUL CIRCLES ==================== -->
  <!-- Horizontal line meets arc at touching point (340, y); arrowhead is from 340 to 352.75 -->
  <line x1="222" y1="78" x2="340" y2="78"
        stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="340,73.5 340,82.5 352.75,78" fill="var(--text-color)" />
  <line x1="222" y1="110" x2="340" y2="110"
        stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="340,105.5 340,114.5 352.75,110" fill="var(--text-color)" />
  <line x1="222" y1="142" x2="340" y2="142"
        stroke="var(--gray-text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="340,137.5 340,146.5 352.75,142" fill="var(--gray-text-color)" />
  <!-- ==================== MATMUL CIRCLES (left edge=352.75, cx=362.75) ==================== -->
  <circle cx="362.75" cy="78" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="355.68" y1="70.93" x2="369.82" y2="85.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="355.68" y1="85.07" x2="369.82" y2="70.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="362.75" cy="110" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="355.68" y1="102.93" x2="369.82" y2="117.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="355.68" y1="117.07" x2="369.82" y2="102.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="362.75" cy="142" r="10" fill="var(--background-color)" stroke="var(--gray-text-color)" stroke-width="2" />
  <line x1="355.68" y1="134.93" x2="369.82" y2="149.07" stroke="var(--gray-text-color)" stroke-width="2" />
  <line x1="355.68" y1="149.07" x2="369.82" y2="134.93" stroke="var(--gray-text-color)" stroke-width="2" />
  <!-- ==================== ARROWS FROM CIRCLES TO ELEMENT-WISE ADD CIRCLE ==================== -->
  <path d="M372.75,78 L423.5,78 A9.25,9.25 0 0,1 432.75,87.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="428.25,87.25 437.25,87.25 432.75,100" fill="var(--text-color)" />
  <line x1="372.75" y1="110" x2="410" y2="110" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="410,105.5 410,114.5 422.75,110" fill="var(--text-color)" />
  <path d="M372.75,142 L423.5,142 A9.25,9.25 0 0,0 432.75,132.75"
        fill="none" stroke="var(--gray-text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="428.25,132.75 437.25,132.75 432.75,120" fill="var(--gray-text-color)" />
  <!-- ==================== FINAL ELEMENT-WISE ADD CIRCLE (cx=432.75, cy=110) ==================== -->
  <circle cx="432.75" cy="110" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="422.75" y1="110" x2="442.75" y2="110" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <line x1="432.75" y1="100" x2="432.75" y2="120" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <!-- Arrow from Add Circle to moe_out_0 -->
  <line x1="442.75" y1="110" x2="478" y2="110" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="478,105.5 478,114.5 490.75,110" fill="var(--text-color)" />
  <!-- ==================== OUTPUT: moe_out_0 ==================== -->
  <text x="526.75" y="85" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">moe_out_0</text>
  <text x="526.75" y="135" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="490.75" y="98" width="72" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="514.75" y1="98" x2="514.75" y2="122" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="538.75" y1="98" x2="538.75" y2="122" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- ==================== THREE CONCENTRIC 90-DEGREE ARCS ==================== -->
  <!-- Arc 0 (radius 104): Cell 0 (x=236) touching at (340, 78) at back of arrow -->
  <path d="M236,182 A104,104 0 0,1 340,78"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <!-- Arc 1 (radius 72): Cell 1 (x=268) touching at (340, 110) at back of arrow -->
  <path d="M268,182 A72,72 0 0,1 340,110"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <!-- Arc 2 (radius 40): Cell 2 (x=300) touching at (340, 142) at back of arrow -->
  <path d="M300,182 A40,40 0 0,1 340,142"
        fill="none" stroke="var(--gray-text-color)" stroke-width="2" stroke-linecap="round" />
  <!-- 3 single cells (y=182..206, separated by 8px space) -->
  <!-- Cell 0: .4 (center x=236) -->
  <rect x="224" y="182" width="24" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="236" y="194" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <!-- Cell 1: .6 (center x=268) -->
  <rect x="256" y="182" width="24" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="268" y="194" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <!-- Cell 2: 0 (center x=300) -->
  <rect x="288" y="182" width="24" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="300" y="194" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <!-- ==================== ARROWS FROM top_weights_0 TO SPLIT CELLS ==================== -->
  <path d="M244,260 C244,239.375 236,239.375 236,218.75"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="231.5,218.75 240.5,218.75 236,206" fill="var(--text-color)" />
  <line x1="268" y1="260" x2="268" y2="218.75"
        stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="263.5,218.75 272.5,218.75 268,206" fill="var(--text-color)" />
  <path d="M292,260 C292,239.375 300,239.375 300,218.75"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,218.75 304.5,218.75 300,206" fill="var(--text-color)" />
  <!-- ==================== top_weights_0 (DIRECTLY UNDER SPLIT CELLS) ==================== -->
  <rect x="232" y="260" width="72" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="256" y1="260" x2="256" y2="284" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="280" y1="260" x2="280" y2="284" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="244" y="272" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="268" y="272" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="292" y="272" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <!-- Name and shape under vector -->
  <text x="268" y="300" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">top_weights_0</text>
  <text x="268" y="318" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">num_experts</text>
  <!-- ==================== LEGEND ==================== -->
  <g class="legend">
    <circle cx="180" cy="365" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="172.93" y1="357.93" x2="187.07" y2="372.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="172.93" y1="372.07" x2="187.07" y2="357.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="195" y="365" text-anchor="start" dominant-baseline="central" font-size="12"
          font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
    <circle cx="280" cy="365" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="270" y1="365" x2="290" y2="365" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
    <line x1="280" y1="355" x2="280" y2="375" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
    <text x="296" y="365" text-anchor="start" dominant-baseline="central" font-size="12"
          font-family="var(--main-font)" fill="var(--text-color)">element-wise add</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block -> moe<br>Processing a single token in moe: the token vector passes through its selected experts and is combined using routing weights.</figcaption></figure>

<p>First, <code>rms_out_0</code> feeds into all three experts (<code>expert_0</code>, <code>expert_1</code>, and
<code>expert_2</code>), and each expert produces an output vector of shape <code>[hidden_size]</code>.
Next, each output vector is multiplied by its corresponding scalar from
<code>top_weights_0</code>. This is an element-wise scalar-vector multiplication, scaling
every element in the vector. Finally, an element-wise add combines the three
scaled vectors into <code>moe_out_0</code>.</p>
<p>In this example, <code>expert_2</code> is not activated. Its output is multiplied by 0,
producing an all-zero vector that contributes nothing to the element-wise add.
Its weight was set to 0 during routing because it was not among the <code>TOP_K</code>
experts for this token.</p>
<p>Here is what <code>moe_token</code> and <code>moe</code> look like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">moe_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">top_weights</span><span class="p">,</span> <span class="n">experts</span><span class="p">):</span>
    <span class="c1"># token_vec: [hidden_size]</span>
    <span class="c1"># top_weights: [num_experts]</span>
    <span class="n">hidden_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">token_vec</span><span class="p">)</span>

    <span class="c1"># moe_out: [hidden_size]</span>
    <span class="n">moe_out</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">]</span> <span class="o">*</span> <span class="n">hidden_size</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">experts</span><span class="p">)):</span>
        <span class="c1"># expert_out: [hidden_size]</span>
        <span class="n">expert_out</span> <span class="o">=</span> <span class="n">experts</span><span class="p">[</span><span class="n">i</span><span class="p">](</span><span class="n">token_vec</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">hidden_size</span><span class="p">):</span>
            <span class="n">moe_out</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">+=</span> <span class="n">top_weights</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">expert_out</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">moe_out</span>

<span class="k">def</span><span class="w"> </span><span class="nf">moe</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">top_weights</span><span class="p">,</span> <span class="n">experts</span><span class="p">):</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="c1"># top_weights: [seq_len, num_experts]</span>

    <span class="c1"># moe_out: [seq_len, hidden_size]</span>
    <span class="n">moe_out</span> <span class="o">=</span> <span class="p">[</span>
        <span class="n">moe_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">weights</span><span class="p">,</span> <span class="n">experts</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">token_vec</span><span class="p">,</span> <span class="n">weights</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">top_weights</span><span class="p">)</span>
    <span class="p">]</span>
    <span class="k">return</span> <span class="n">moe_out</span>
</code></pre></div>

<p>We can use the following mental model to understand how the MoE module works
across the full sequence. Each token activates its own top-$k$ experts, and the
selected experts can differ from token to token. Tokens are only routed to these
activated experts, and their outputs are weighted and added together
element-wise to produce <code>moe_out</code>:</p>
<figure class="svg-figure"><svg class="moe-interactive" viewBox="0 0 600 480" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <style>
    .token-0, .token-1, .token-2 {
      transition: opacity 0.25s ease-in-out;
      cursor: pointer;
    }
    .moe-interactive:has(.token-0:hover) .token-1,
    .moe-interactive:has(.token-0:hover) .token-2 {
      opacity: 0.15;
    }
    .moe-interactive:has(.token-1:hover) .token-0,
    .moe-interactive:has(.token-1:hover) .token-2 {
      opacity: 0.15;
    }
    .moe-interactive:has(.token-2:hover) .token-0,
    .moe-interactive:has(.token-2:hover) .token-1 {
      opacity: 0.15;
    }
  </style>
  <!-- Badge -->
  <rect x="425" y="15" width="150" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="500" y="27" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">moe (mental model)</text>
  <!-- ==================== ROW 1: ARROWS (drawn before boxes in z-axis) ==================== -->
  <!-- Diverging arrows from rms_out (starts at 110, inside matrix) -->
  <g class="token-0">
    <path d="M110,74 C121.625,74 121.625,66 131.25,66"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,61.5 131.25,70.5 144,66" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <line x1="110" y1="98" x2="131.25" y2="98"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,93.5 131.25,102.5 144,98" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <path d="M110,122 C121.625,122 121.625,130 131.25,130"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,125.5 131.25,134.5 144,130" fill="var(--hue-4)" />
  </g>
  <!-- Routing from rms_out vectors directly turning down to top of experts (starts at 214, inside vector) -->
  <g class="token-0">
    <!-- Yellow (v0 at 66): goes right to 272 (expert_0 left) and 368 (expert_1 left) -->
    <line x1="214" y1="66" x2="352" y2="66"
          stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <path d="M352,66 A16,16 0 0,1 368,82 L368,149.25"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="363.5,149.25 372.5,149.25 368,162" fill="var(--hue-6)" />
    <circle cx="272" cy="66" r="3.5" fill="var(--hue-6)" />
    <line x1="272" y1="66" x2="272" y2="149.25"
          stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="267.5,149.25 276.5,149.25 272,162" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <!-- Pink (v1 at 98): goes right to 304 (expert_0 right) and 496 (expert_2 right) -->
    <line x1="214" y1="98" x2="480" y2="98"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <path d="M480,98 A16,16 0 0,1 496,114 L496,149.25"
          fill="none" stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="491.5,149.25 500.5,149.25 496,162" fill="var(--hue-3)" />
    <circle cx="304" cy="98" r="3.5" fill="var(--hue-3)" />
    <line x1="304" y1="98" x2="304" y2="149.25"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="299.5,149.25 308.5,149.25 304,162" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <!-- Green (v2 at 130): goes right to 400 (expert_1 right) and 464 (expert_2 left) -->
    <line x1="214" y1="130" x2="448" y2="130"
          stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <path d="M448,130 A16,16 0 0,1 464,146 L464,149.25"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="459.5,149.25 468.5,149.25 464,162" fill="var(--hue-4)" />
    <circle cx="400" cy="130" r="3.5" fill="var(--hue-4)" />
    <line x1="400" y1="130" x2="400" y2="149.25"
          stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="395.5,149.25 404.5,149.25 400,162" fill="var(--hue-4)" />
  </g>
  <!-- ==================== ROW 1: BOXES (drawn on top of arrows in z-axis) ==================== -->
  <!-- rms_out matrix -->
  <text x="76.0" y="47" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="76.0" y="149" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="25" y="98.0" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 25 98.0)">seq_len</text>
  <rect x="40" y="62" width="72" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="86" x2="112" y2="86" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="110" x2="112" y2="110" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="64" y1="62" x2="64" y2="134" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="88" y1="62" x2="88" y2="134" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- 3 rms_out vectors (fill=bg) -->
  <g class="token-0">
    <rect x="144" y="54.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-6)" stroke-width="1.5" />
    <line x1="168" y1="54.0" x2="168" y2="78.0" stroke="var(--hue-6)" stroke-width="1.5" />
    <line x1="192" y1="54.0" x2="192" y2="78.0" stroke="var(--hue-6)" stroke-width="1.5" />
  </g>
  <g class="token-1">
    <rect x="144" y="86.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-3)" stroke-width="1.5" />
    <line x1="168" y1="86.0" x2="168" y2="110.0" stroke="var(--hue-3)" stroke-width="1.5" />
    <line x1="192" y1="86.0" x2="192" y2="110.0" stroke="var(--hue-3)" stroke-width="1.5" />
  </g>
  <g class="token-2">
    <rect x="144" y="118.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-4)" stroke-width="1.5" />
    <line x1="168" y1="118.0" x2="168" y2="142.0" stroke="var(--hue-4)" stroke-width="1.5" />
    <line x1="192" y1="118.0" x2="192" y2="142.0" stroke="var(--hue-4)" stroke-width="1.5" />
  </g>
  <!-- ==================== OUTGOING LINES FROM EXPERTS (drawn below capsules in z-axis) ==================== -->
  <g class="token-0">
    <!-- Top: expert_0 left (272) down to (272, c0_cy - cr) -->
    <line x1="272" y1="192" x2="272" y2="267.25"
          stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="267.5,267.25 276.5,267.25 272,280" fill="var(--hue-6)" />
    <!-- Right: expert_1 left (368) left to (282, c0_cy) -->
    <path d="M368,192 L368,274 A16,16 0 0,1 352,290 L294.75,290"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="294.75,285.5 294.75,294.5 282,290" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <!-- Left: expert_0 right (304) down to (304, c1_cy - 9.25), turns right to (326, c1_cy) -->
    <path d="M304,192 L304,312.75 A9.25,9.25 0 0,0 313.25,322"
          fill="none" stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="313.25,317.5 313.25,326.5 326,322" fill="var(--hue-3)" />
    <!-- Right: expert_2 right (496) down to (496, c1_cy - 16), turns left to (346, c1_cy) -->
    <path d="M496,192 L496,306 A16,16 0 0,1 480,322 L358.75,322"
          fill="none" stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="358.75,317.5 358.75,326.5 346,322" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <!-- Left: expert_1 right (400) down to (400, c2_cy - 9.25), turns right to (422, c2_cy) -->
    <path d="M400,192 L400,344.75 A9.25,9.25 0 0,0 409.25,354"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="409.25,349.5 409.25,358.5 422,354" fill="var(--hue-4)" />
    <!-- Right: expert_2 left (464) down to (464, c2_cy - 9.25), turns left to (442, c2_cy) -->
    <path d="M464,192 L464,344.75 A9.25,9.25 0 0,1 454.75,354"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="454.75,349.5 454.75,358.5 442,354" fill="var(--hue-4)" />
  </g>
  <!-- ==================== ROW 2: ARROWS (drawn before boxes in z-axis) ==================== -->
  <!-- Diverging arrows from top_probs (starts at 86, inside matrix) -->
  <g class="token-0">
    <path d="M86,202.0 C108.625,202.0 108.625,194 131.25,194"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,189.5 131.25,198.5 144,194" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <line x1="86" y1="226" x2="131.25" y2="226"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,221.5 131.25,230.5 144,226" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <path d="M86,250.0 C108.625,250.0 108.625,258 131.25,258"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="131.25,253.5 131.25,262.5 144,258" fill="var(--hue-4)" />
  </g>
  <!-- Routing from top_probs vectors to circles (starts at 190, inside vector) -->
  <g class="token-0">
    <!-- Left: W0 (240) right to (262, c0_cy) - ALIGNED TO BOTTOM EDGE OF EXPERTS (194) -->
    <path d="M190,194 L224,194 A16,16 0 0,1 240,210 L240,280.75 A9.25,9.25 0 0,0 249.25,290"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="249.25,285.5 249.25,294.5 262,290" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <!-- Top: W1 (336) straight down to (336, c1_cy - cr) -->
    <path d="M190,226 L320,226 A16,16 0 0,1 336,242 L336,299.25"
          fill="none" stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="331.5,299.25 340.5,299.25 336,312" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <!-- Top: W2 (432) straight down to (432, c2_cy - cr) -->
    <path d="M190,258 L416,258 A16,16 0 0,1 432,274 L432,331.25"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="427.5,331.25 436.5,331.25 432,344" fill="var(--hue-4)" />
  </g>
  <!-- ==================== ROW 2: BOXES (drawn on top of arrows in z-axis) ==================== -->
  <!-- top_probs matrix -->
  <text x="64" y="175.0" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">top_probs</text>
  <text x="64" y="277.0" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">top_k</text>
  <text x="25" y="226.0" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 25 226.0)">seq_len</text>
  <rect x="40" y="190.0" width="48" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="214.0" x2="88" y2="214.0" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="238.0" x2="88" y2="238.0" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="64" y1="190.0" x2="64" y2="262.0" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="52" y="202" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="76" y="202" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="52" y="226" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.7</text>
  <text x="76" y="226" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <text x="52" y="250" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <text x="76" y="250" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <!-- 3 top_probs vectors (fill=bg) -->
  <g class="token-0">
    <rect x="144" y="182.0" width="48" height="24"
          fill="var(--background-color)" stroke="var(--hue-6)" stroke-width="1.5" />
    <line x1="168" y1="182.0" x2="168" y2="206.0" stroke="var(--hue-6)" stroke-width="1.5" />
    <text x="156" y="194" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-6)">.6</text>
    <text x="180" y="194" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-6)">.4</text>
  </g>
  <g class="token-1">
    <rect x="144" y="214.0" width="48" height="24"
          fill="var(--background-color)" stroke="var(--hue-3)" stroke-width="1.5" />
    <line x1="168" y1="214.0" x2="168" y2="238.0" stroke="var(--hue-3)" stroke-width="1.5" />
    <text x="156" y="226" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-3)">.7</text>
    <text x="180" y="226" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-3)">.3</text>
  </g>
  <g class="token-2">
    <rect x="144" y="246.0" width="48" height="24"
          fill="var(--background-color)" stroke="var(--hue-4)" stroke-width="1.5" />
    <line x1="168" y1="246.0" x2="168" y2="270.0" stroke="var(--hue-4)" stroke-width="1.5" />
    <text x="156" y="258" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-4)">.5</text>
    <text x="180" y="258" text-anchor="middle" dominant-baseline="central" font-size="13"
          font-family="var(--main-font)" fill="var(--hue-4)">.5</text>
  </g>
  <!-- Circles and paths to output vectors -->
  <g class="token-0">
    <circle cx="272" cy="290" r="10"
            fill="var(--background-color)" stroke="var(--hue-6)" stroke-width="2" />
    <line x1="262" y1="290" x2="282" y2="290"
          stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <line x1="272" y1="280" x2="272" y2="300"
          stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <circle cx="272" cy="290" r="3" fill="var(--hue-6)" />
    <!-- Bottom of Circle 0 going down and turning left at next row (322) to Vector 0 -->
    <path d="M272,300 L272,306 A16,16 0 0,1 256,322 L228.75,322"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="228.75,317.5 228.75,326.5 216,322" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <circle cx="336" cy="322" r="10"
            fill="var(--background-color)" stroke="var(--hue-3)" stroke-width="2" />
    <line x1="326" y1="322" x2="346" y2="322"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <line x1="336" y1="312" x2="336" y2="332"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <circle cx="336" cy="322" r="3" fill="var(--hue-3)" />
    <!-- Bottom of Circle 1 going down and turning left at next row (354) to Vector 1 -->
    <path d="M336,332 L336,338 A16,16 0 0,1 320,354 L228.75,354"
          fill="none" stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="228.75,349.5 228.75,358.5 216,354" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <circle cx="432" cy="354" r="10"
            fill="var(--background-color)" stroke="var(--hue-4)" stroke-width="2" />
    <line x1="422" y1="354" x2="442" y2="354"
          stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <line x1="432" y1="344" x2="432" y2="364"
          stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <circle cx="432" cy="354" r="3" fill="var(--hue-4)" />
    <!-- Bottom of Circle 2 going down and turning left at next row (386) to Vector 2 -->
    <path d="M432,364 L432,370 A16,16 0 0,1 416,386 L228.75,386"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="228.75,381.5 228.75,390.5 216,386" fill="var(--hue-4)" />
  </g>
  <!-- ==================== ROW 3: ARROWS (drawn before boxes in z-axis) ==================== -->
  <!-- Converging arrows from 3 vectors into moe_out matrix (starts at 146, inside vector) -->
  <g class="token-0">
    <path d="M146,322 C134.375,322 134.375,330.0 124.75,330.0"
          fill="none" stroke="var(--hue-6)" stroke-width="2" stroke-linecap="round" />
    <polygon points="124.75,325.5 124.75,334.5 112,330.0" fill="var(--hue-6)" />
  </g>
  <g class="token-1">
    <line x1="146" y1="354" x2="124.75" y2="354"
          stroke="var(--hue-3)" stroke-width="2" stroke-linecap="round" />
    <polygon points="124.75,349.5 124.75,358.5 112,354" fill="var(--hue-3)" />
  </g>
  <g class="token-2">
    <path d="M146,386 C134.375,386 134.375,378.0 124.75,378.0"
          fill="none" stroke="var(--hue-4)" stroke-width="2" stroke-linecap="round" />
    <polygon points="124.75,373.5 124.75,382.5 112,378.0" fill="var(--hue-4)" />
  </g>
  <!-- ==================== ROW 3: BOXES (drawn on top of arrows in z-axis) ==================== -->
  <!-- 3 OUTPUT VECTORS (fill=bg) -->
  <g class="token-0">
    <rect x="144" y="310.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-6)" stroke-width="1.5" />
    <line x1="168" y1="310.0" x2="168" y2="334.0" stroke="var(--hue-6)" stroke-width="1.5" />
    <line x1="192" y1="310.0" x2="192" y2="334.0" stroke="var(--hue-6)" stroke-width="1.5" />
  </g>
  <g class="token-1">
    <rect x="144" y="342.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-3)" stroke-width="1.5" />
    <line x1="168" y1="342.0" x2="168" y2="366.0" stroke="var(--hue-3)" stroke-width="1.5" />
    <line x1="192" y1="342.0" x2="192" y2="366.0" stroke="var(--hue-3)" stroke-width="1.5" />
  </g>
  <g class="token-2">
    <rect x="144" y="374.0" width="72" height="24"
          fill="var(--background-color)" stroke="var(--hue-4)" stroke-width="1.5" />
    <line x1="168" y1="374.0" x2="168" y2="398.0" stroke="var(--hue-4)" stroke-width="1.5" />
    <line x1="192" y1="374.0" x2="192" y2="398.0" stroke="var(--hue-4)" stroke-width="1.5" />
  </g>
  <!-- moe_out MATRIX (fill=bg) -->
  <text x="76.0" y="303.0" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">moe_out</text>
  <text x="76.0" y="405.0" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="25" y="354.0" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 25 354.0)">seq_len</text>
  <rect x="40" y="318.0" width="72" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="342.0" x2="112" y2="342.0" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="366.0" x2="112" y2="366.0" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="64" y1="318.0" x2="64" y2="390.0" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="88" y1="318.0" x2="88" y2="390.0" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- ==================== EXPERT CAPSULES (DRAWN ON TOP IN Z-AXIS) ==================== -->
  <rect x="252.0" y="162" width="72" height="32" rx="16" ry="16"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="288" y="178.0" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">expert_0</text>
  <rect x="348.0" y="162" width="72" height="32" rx="16" ry="16"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="384" y="178.0" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">expert_1</text>
  <rect x="444.0" y="162" width="72" height="32" rx="16" ry="16"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="480" y="178.0" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">expert_2</text>
  <!-- ==================== LEGEND ==================== -->
  <g class="legend">
    <circle cx="250" cy="448" r="10"
            fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="240" y1="448" x2="260" y2="448"
          stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
    <line x1="250" y1="438" x2="250" y2="458"
          stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
    <circle cx="250" cy="448" r="3" fill="var(--text-color)" />
    <text x="268" y="448" text-anchor="start" dominant-baseline="central" font-size="12"
          font-family="var(--main-font)" fill="var(--text-color)">weighted add</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block -> moe<br>The sequence-level mental model of moe: tokens are dispatched across experts and aggregated into moe_out.</figcaption></figure>

<p>This also gives us a neat way to think about the dense models we mentioned
earlier: a dense model is just one big expert that is always activated for every
token.</p>
<h2>expert</h2>
<p>Now, we are ready to peek into the last module in MoE: <code>expert</code>.
Each expert is a <strong>SwiGLU</strong> (Swish Gated Linear Unit) feed-forward network.
The architecture of an <code>expert</code> is shown in the following figure.
Again, for better illustration, we only use a single token as input here:</p>
<figure class="svg-figure"><svg viewBox="0 -75 600 550" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="435" y="-62" width="145" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="507.5" y="-50" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">expert_0 (1 token)</text>
  <!-- Input: rms_out_0 [hidden_size = 3 cells] -->
  <text x="106.0" y="145" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out_0</text>
  <text x="106.0" y="209" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="70.0" y="168" width="72" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="94.0" y1="168" x2="94.0" y2="192" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="118.0" y1="168" x2="118.0" y2="192" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Branching path from rms_out_0 into matmuls -->
  <line x1="142.0" y1="180" x2="184.0" y2="180" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="184.0" cy="180" r="3" fill="var(--text-color)" />
  <line x1="184.0" y1="180" x2="184.0" y2="157.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="179.5,157.25 188.5,157.25 184.0,145" fill="var(--text-color)" />
  <line x1="184.0" y1="180" x2="184.0" y2="202.75" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="179.5,202.75 188.5,202.75 184.0,215" fill="var(--text-color)" />
  <!-- Top Weight: w_gate [hidden_size=3, inter_size=4], center=184.0 -->
  <text x="184.0" y="-25" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">w_gate</text>
  <text x="184.0" y="-7" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">inter_size</text>
  <text x="121.0" y="44" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 121.0 44)">hidden_size</text>
  <rect x="136.0" y="8" width="96" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="160.0" y1="8" x2="160.0" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="184.0" y1="8" x2="184.0" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="208.0" y1="8" x2="208.0" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="136.0" y1="32" x2="232.0" y2="32" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="136.0" y1="56" x2="232.0" y2="56" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from w_gate down to top matmul -->
  <line x1="184.0" y1="80" x2="184.0" y2="112.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="179.5,112.25 188.5,112.25 184.0,125" fill="var(--text-color)" />
  <!-- Top Matmul Circle (cx=184.0, cy=135) -->
  <circle cx="184.0" cy="135" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="176.93" y1="127.93" x2="191.07" y2="142.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="176.93" y1="142.07" x2="191.07" y2="127.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Arrow from top matmul to top intermediate vector -->
  <line x1="194.0" y1="135" x2="213.25" y2="135" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="213.25,130.5 213.25,139.5 226.0,135" fill="var(--text-color)" />
  <!-- Top Intermediate Vector [inter_size = 4 cells] at x=226.0..322.0, y=123..147 -->
  <text x="274.0" y="108" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">inter_size</text>
  <rect x="226.0" y="123" width="96" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="250.0" y1="123" x2="250.0" y2="147" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="274.0" y1="123" x2="274.0" y2="147" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="298.0" y1="123" x2="298.0" y2="147" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Bottom Weight: w_up [hidden_size=3, inter_size=4], center=184.0 -->
  <rect x="136.0" y="280" width="96" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="160.0" y1="280" x2="160.0" y2="352" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="184.0" y1="280" x2="184.0" y2="352" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="208.0" y1="280" x2="208.0" y2="352" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="136.0" y1="304" x2="232.0" y2="304" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="136.0" y1="328" x2="232.0" y2="328" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="121.0" y="316" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 121.0 316)">hidden_size</text>
  <text x="184.0" y="367" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">inter_size</text>
  <text x="184.0" y="386" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">w_up</text>
  <!-- Arrow from w_up up to bottom matmul -->
  <line x1="184.0" y1="280" x2="184.0" y2="247.75" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="179.5,247.75 188.5,247.75 184.0,235" fill="var(--text-color)" />
  <!-- Bottom Matmul Circle (cx=184.0, cy=225) -->
  <circle cx="184.0" cy="225" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="176.93" y1="217.93" x2="191.07" y2="232.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="176.93" y1="232.07" x2="191.07" y2="217.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Arrow from bottom matmul to bottom intermediate vector -->
  <line x1="194.0" y1="225" x2="213.25" y2="225" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="213.25,220.5 213.25,229.5 226.0,225" fill="var(--text-color)" />
  <!-- Bottom Intermediate Vector [inter_size = 4 cells] at x=226.0..322.0, y=213..237 -->
  <text x="274.0" y="252" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">inter_size</text>
  <rect x="226.0" y="213" width="96" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="250.0" y1="213" x2="250.0" y2="237" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="274.0" y1="213" x2="274.0" y2="237" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="298.0" y1="213" x2="298.0" y2="237" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from top intermediate vector into S (silu) circle (cx=364.0, cy=135) -->
  <line x1="322.0" y1="135" x2="341.25" y2="135" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="341.25,130.5 341.25,139.5 354.0,135" fill="var(--text-color)" />
  <circle cx="364.0" cy="135" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="364.0" y="135" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">S</text>
  <!-- Arrow from S circle down into merge circle (cx=364.0, cy=180) -->
  <line x1="364.0" y1="145" x2="364.0" y2="157.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="359.5,157.25 368.5,157.25 364.0,170" fill="var(--text-color)" />
  <!-- Arrow from bottom intermediate vector turning up into merge circle -->
  <path d="M322.0,225 L348.0,225 A16,16 0 0,0 364.0,209 L364.0,202.75" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="359.5,202.75 368.5,202.75 364.0,190" fill="var(--text-color)" />
  <!-- Merge Circle (Hadamard product dot), center=364.0, cy=180 -->
  <circle cx="364.0" cy="180" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="364.0" cy="180" r="3" fill="var(--text-color)" />
  <!-- Arrow from merge circle to down-projection matmul -->
  <line x1="374.0" y1="180" x2="393.25" y2="180" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="393.25,175.5 393.25,184.5 406.0,180" fill="var(--text-color)" />
  <!-- Down Weight: w_down [inter_size=4, hidden_size=3], center=416.0 -->
  <text x="416.0" y="-25" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">w_down</text>
  <text x="416.0" y="-7" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="365.0" y="56" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 365.0 56)">inter_size</text>
  <rect x="380.0" y="8" width="72" height="96"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404.0" y1="8" x2="404.0" y2="104" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="428.0" y1="8" x2="428.0" y2="104" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.0" y1="32" x2="452.0" y2="32" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.0" y1="56" x2="452.0" y2="56" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.0" y1="80" x2="452.0" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from w_down down to down-projection matmul -->
  <line x1="416.0" y1="104" x2="416.0" y2="157.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="411.5,157.25 420.5,157.25 416.0,170" fill="var(--text-color)" />
  <!-- Down Matmul Circle (cx=416.0, cy=180) -->
  <circle cx="416.0" cy="180" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="408.93" y1="172.93" x2="423.07" y2="187.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="408.93" y1="187.07" x2="423.07" y2="172.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Arrow from down matmul to expert_0_out_0 -->
  <line x1="426.0" y1="180" x2="445.25" y2="180" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="445.25,175.5 445.25,184.5 458.0,180" fill="var(--text-color)" />
  <!-- Output: expert_0_out_0 [hidden_size = 3 cells] at x=458.0..530.0 -->
  <text x="494.0" y="145" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">expert_0_out_0</text>
  <text x="494.0" y="209" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="458.0" y="168" width="72" height="24"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="482.0" y1="168" x2="482.0" y2="192" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="506.0" y1="168" x2="506.0" y2="192" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Casual annotation -->
  <g class="casual-annotation">
    <path d="M 362,118 C 344,118 342,138 342,158 C 342,180 346,205 364,205 C 382,205 386,182 386,158 C 386,134 380,118 366,117 C 356,116 350,121 348,127"
          fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
    <path d="M 374,201 Q 386,230 405,250" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
    <line x1="405" y1="250" x2="397.6" y2="248.5" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <line x1="405" y1="250" x2="403.4" y2="242.9" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
    <g transform="rotate(1 475 285)">
      <text x="475" y="265" text-anchor="middle" dominant-baseline="central" font-size="17"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">when intermediate tensors</text>
      <text x="475" y="285" text-anchor="middle" dominant-baseline="central" font-size="17"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">are not shown, their shape is</text>
      <text x="475" y="305" text-anchor="middle" dominant-baseline="central" font-size="17"
            font-family="var(--handwriting-font)" fill="var(--hue-6)">unchanged from the input</text>
    </g>
  </g>
  <!-- Legend -->
  <g class="legend">
    <rect x="88.0" y="435.00" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="116.0" y="445.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="194.0" cy="445.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="186.93" y1="437.93" x2="201.07" y2="452.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="186.93" y1="452.07" x2="201.07" y2="437.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="212.0" y="445.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
    <circle cx="290.0" cy="445.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="290.0" y="445.00" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold" font-family="var(--main-font)" fill="var(--text-color)">S</text>
    <text x="308.0" y="445.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">silu</text>
    <circle cx="366.0" cy="445.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <circle cx="366.0" cy="445.00" r="3" fill="var(--text-color)" />
    <text x="384.0" y="445.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">element-wise multiply</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> moe_block -> moe -> expert<br>Inside an expert, a token passes through gate and up projections, silu activation (circled S), and down projection.</figcaption></figure>

<p>Each <code>expert</code> uses three weight matrices (<code>w_gate</code>, <code>w_up</code>, and <code>w_down</code>) to do the matmuls.</p>
<p><code>silu</code> (marked as <strong>S</strong> in the diagram) is the SiLU (Swish) activation function.
Both <code>silu</code> and the element-wise multiplication that follows operate on vectors
element by element without changing their dimensions. Whenever we omit
intermediate output tensors between operations in our diagrams, it means their
shape stays exactly the same as the incoming input tensor, which is
<code>[inter_size]</code> here.</p>
<p>The gate is what makes this a SwiGLU rather than a plain feed-forward network,
with <code>w_up</code> producing the values and <code>w_gate</code> one multiplier for each. Why that
beats the simpler alternatives is still not well understood, and the paper that
introduced these variants is refreshingly blunt about having no explanation
for it<sup id="cite-7-a"><a href="#ref-7" class="citation-link">7</a></sup>.</p>
<p>Here is the Python code for <code>silu</code>:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">math</span>

<span class="k">def</span><span class="w"> </span><span class="nf">silu</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">x</span> <span class="o">/</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">+</span> <span class="n">math</span><span class="o">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">x</span><span class="p">))</span>
</code></pre></div>

<p>Using our <code>matmul</code> helper, here is what <code>expert_token</code> and <code>expert</code> look like in
Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">expert_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_gate</span><span class="p">,</span> <span class="n">w_up</span><span class="p">,</span> <span class="n">w_down</span><span class="p">):</span>
    <span class="c1"># token_vec: [hidden_size]</span>
    <span class="c1"># w_gate, w_up: [hidden_size, inter_size]</span>
    <span class="c1"># w_down: [inter_size, hidden_size]</span>

    <span class="c1"># x_gate: [inter_size]</span>
    <span class="n">x_gate</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_gate</span><span class="p">)</span>

    <span class="c1"># x_up: [inter_size]</span>
    <span class="n">x_up</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_up</span><span class="p">)</span>

    <span class="c1"># x_act: [inter_size]</span>
    <span class="n">x_act</span> <span class="o">=</span> <span class="p">[</span><span class="n">silu</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">x_gate</span><span class="p">]</span>

    <span class="c1"># x_inter: [inter_size]</span>
    <span class="n">inter_size</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">x_act</span><span class="p">)</span>
    <span class="n">x_inter</span> <span class="o">=</span> <span class="p">[</span><span class="n">x_act</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">x_up</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">inter_size</span><span class="p">)]</span>

    <span class="c1"># x_down: [hidden_size]</span>
    <span class="n">x_down</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">x_inter</span><span class="p">,</span> <span class="n">w_down</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">x_down</span>

<span class="k">def</span><span class="w"> </span><span class="nf">expert</span><span class="p">(</span><span class="n">tensor</span><span class="p">,</span> <span class="n">w_gate</span><span class="p">,</span> <span class="n">w_up</span><span class="p">,</span> <span class="n">w_down</span><span class="p">):</span>
    <span class="c1"># tensor: [seq_len, hidden_size]</span>
    <span class="c1"># output: [seq_len, hidden_size]</span>
    <span class="n">output</span> <span class="o">=</span> <span class="p">[</span><span class="n">expert_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_gate</span><span class="p">,</span> <span class="n">w_up</span><span class="p">,</span> <span class="n">w_down</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">tensor</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">output</span>
</code></pre></div>

<p>As a reference for the intermediate dimension, Qwen 3.6 35B uses an <code>inter_size</code>
of 512 per expert, while Kimi K3 2.8T uses 3,584. In MoE models with
many experts, this inner dimension is often kept relatively narrow for each
individual expert so that activating multiple experts per token remains
computationally efficient.</p>
<h2>gqa_block</h2>
<p>So far, we have covered <code>moe_block</code>, which forms the second half of a single <code>decoder_block</code>.
The first half is <code>gqa_block</code>.
Here, <code>gqa</code> stands for <strong>Grouped-Query Attention</strong> (GQA), a modern evolution of the
multi-head attention mechanism introduced in the original Transformer paper. We will
see how it works in detail in the next few sections. As shown in the following
figure, the input and output shapes are identical (<code>[seq_len, hidden_size]</code>):</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_in</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">gqa_block</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block<br>The gqa_block computes grouped-query attention.</figcaption></figure>

<p>Zooming into <code>gqa_block</code>: it consists of three steps in sequence: <code>rms_norm</code>, <code>gqa</code>, and <code>out_matmul</code>:</p>
<figure class="svg-figure"><svg viewBox="-52 -40 600 260" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <rect x="443" y="-35" width="95" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="490.5" y="-23" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">gqa_block</text>
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_in</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="115.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="115.25,95.5 115.25,104.5 128,100" fill="var(--text-color)" />
  <rect x="128" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="156" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 156 100)">rms_norm</text>
  <line x1="184" y1="100" x2="207.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="207.25,95.5 207.25,104.5 220,100" fill="var(--text-color)" />
  <rect x="220" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="248" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 248 100)">gqa</text>
  <line x1="276" y1="100" x2="299.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="299.25,95.5 299.25,104.5 312,100" fill="var(--text-color)" />
  <rect x="312" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="340" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 340 100)">out_matmul</text>
  <line x1="368" y1="100" x2="391.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="391.25,95.5 391.25,104.5 404,100" fill="var(--text-color)" />
  <text x="440" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_out</text>
  <text x="440" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="492" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 492 100)">seq_len</text>
  <rect x="404" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404" y1="88" x2="476" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404" y1="112" x2="476" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="428" y1="64" x2="428" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="452" y1="64" x2="452" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block<br>Inside gqa_block, the input passes through rms_norm, the gqa attention mechanism, and out_matmul.</figcaption></figure>

<p>Here is what <code>gqa_block</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">gqa_block</span><span class="p">(</span><span class="n">gqa_block_in</span><span class="p">,</span> <span class="n">rms_norm</span><span class="p">,</span> <span class="n">gqa</span><span class="p">,</span> <span class="n">out_matmul</span><span class="p">):</span>
    <span class="c1"># gqa_block_in: [seq_len, hidden_size]</span>

    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="n">rms_out</span> <span class="o">=</span> <span class="n">rms_norm</span><span class="p">(</span><span class="n">gqa_block_in</span><span class="p">)</span>

    <span class="c1"># gqa_out: [seq_len, hidden_size]</span>
    <span class="n">gqa_out</span> <span class="o">=</span> <span class="n">gqa</span><span class="p">(</span><span class="n">rms_out</span><span class="p">)</span>

    <span class="c1"># gqa_block_out: [seq_len, hidden_size]</span>
    <span class="n">gqa_block_out</span> <span class="o">=</span> <span class="n">out_matmul</span><span class="p">(</span><span class="n">gqa_out</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">gqa_block_out</span>
</code></pre></div>

<p>Notice that each sub-module inside <code>gqa_block</code> shares the exact same input and output shape (<code>[seq_len, hidden_size]</code>).</p>
<p>Just like <code>moe_block</code>, <code>gqa_block</code> follows the Pre-LN design. It normalizes its
input with <code>rms_norm</code> after the residual connection has branched out. This keeps
normalization off the choke point, leaving the main residual stream unnormalized as
it passes through the block.</p>
<p>Because <code>rms_norm</code> is identical to what we explored in <code>lm_head</code> and <code>moe_block</code>,
we can jump straight into <code>out_matmul</code> and <code>gqa</code>.</p>
<h2>out_matmul</h2>
<p>Let's look at <code>out_matmul</code> first. It is the simpler of the two remaining components:
just a standard projection that maps the concatenated attention output back to the
residual stream. <code>out_matmul</code> takes the output of <code>gqa</code> (<code>gqa_out</code>) and produces
<code>gqa_block_out</code> with the exact same shape:</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">out_matmul</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block<br>out_matmul projects the concatenated attention group outputs back to the hidden size dimension.</figcaption></figure>

<p>Zooming into <code>out_matmul</code>: like <code>rms_norm</code> and <code>logits_matmul</code>, this is a token-wise
operation. The input matrix splits into individual token vectors, each multiplying
the weight matrix <code>w_matmul</code>, and the resulting vectors combine back into <code>gqa_block_out</code>:</p>
<figure class="svg-figure"><svg viewBox="-20 -85 640 305" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Input Matrix: gqa_out [seq_len=72, hidden_size=72] -->
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Splitting arrows into 3 token vectors -->
  <path d="M92,76 C110.25,76 110.25,68 128.5,68" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,63.5 128.5,72.5 141.25,68" fill="var(--text-color)" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <path d="M92,124 C110.25,124 110.25,132 128.5,132" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,127.5 128.5,136.5 141.25,132" fill="var(--text-color)" />
  <!-- 3 Token Vectors -->
  <text x="177.25" y="41" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">token_vec</text>
  <text x="177.25" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="141.25" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="56" x2="165.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="56" x2="189.25" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="88" x2="165.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="88" x2="189.25" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="141.25" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="165.25" y1="120" x2="165.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="189.25" y1="120" x2="189.25" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrows from token vectors to matmul circles -->
  <line x1="213.25" y1="68" x2="249.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,63.5 249.75,72.5 262.5,68" fill="var(--text-color)" />
  <line x1="213.25" y1="100" x2="249.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,95.5 249.75,104.5 262.5,100" fill="var(--text-color)" />
  <line x1="213.25" y1="132" x2="249.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="249.75,127.5 249.75,136.5 262.5,132" fill="var(--text-color)" />
  <!-- Weight Matrix: w_matmul (hidden_size=72, hidden_size=72) -->
  <text x="272.5" y="-72" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">w_matmul</text>
  <text x="272.5" y="-53" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="323.5" y="-2" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 323.5 -2)">hidden_size</text>
  <rect x="236.5" y="-38" width="72" height="72"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="236.5" y1="-14" x2="308.5" y2="-14" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="236.5" y1="10" x2="308.5" y2="10" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="260.5" y1="-38" x2="260.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="284.5" y1="-38" x2="284.5" y2="34" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrows from w_matmul down to matmul circles -->
  <line x1="272.5" y1="34" x2="272.5" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="268.0,45.25 277.0,45.25 272.5,58" fill="var(--text-color)" />
  <path d="M272.5,34 A66,66 0 0,0 268.17,90.99" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="259.34,80.75 267.73,77.48 268.17,90.99" fill="var(--text-color)" />
  <path d="M272.5,34 A98,98 0 0,0 267.95,123.10" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="258.63,113.31 266.84,109.62 267.95,123.10" fill="var(--text-color)" />
  <!-- 3 Matmul Circles (cx=272.5) -->
  <circle cx="272.5" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="60.93" x2="279.57" y2="75.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="75.07" x2="279.57" y2="60.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="272.5" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="92.93" x2="279.57" y2="107.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="107.07" x2="279.57" y2="92.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="272.5" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="124.93" x2="279.57" y2="139.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="265.43" y1="139.07" x2="279.57" y2="124.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Arrows from matmul circles to output vectors -->
  <line x1="282.5" y1="68" x2="319.75" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,63.5 319.75,72.5 332.5,68" fill="var(--text-color)" />
  <line x1="282.5" y1="100" x2="319.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,95.5 319.75,104.5 332.5,100" fill="var(--text-color)" />
  <line x1="282.5" y1="132" x2="319.75" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.75,127.5 319.75,136.5 332.5,132" fill="var(--text-color)" />
  <!-- 3 Output Token Vectors (hidden_size=72, 3 cells x 24) -->
  <text x="368.5" y="159" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <rect x="332.5" y="56" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="56" x2="356.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="56" x2="380.5" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="332.5" y="88" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="88" x2="356.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="88" x2="380.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="332.5" y="120" width="72" height="24"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356.5" y1="120" x2="356.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380.5" y1="120" x2="380.5" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Converging arrows from token vectors to gqa_block_out matrix -->
  <path d="M404.5,68 C422.75,68 422.75,76 441.25,76" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,71.5 441.25,80.5 454,76" fill="var(--text-color)" />
  <line x1="404.5" y1="100" x2="441.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,95.5 441.25,104.5 454,100" fill="var(--text-color)" />
  <path d="M404.5,132 C422.75,132 422.75,124 441.25,124" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.25,119.5 441.25,128.5 454,124" fill="var(--text-color)" />
  <!-- Output Matrix: gqa_block_out (hidden_size=72, seq_len=72) -->
  <text x="490" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_block_out</text>
  <text x="490" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="542" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 542 100)">seq_len</text>
  <rect x="454" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="454" y1="88" x2="526" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="454" y1="112" x2="526" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="478" y1="64" x2="478" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="502" y1="64" x2="502" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Badge -->
  <rect x="470" y="-80" width="105" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="522.5" y="-68" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">out_matmul</text>
  <!-- Legend -->
  <g class="legend">
    <rect x="235.00" y="180.00" width="20" height="20" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="263.00" y="190.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="341.00" cy="190.00" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="333.93" y1="182.93" x2="348.07" y2="197.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="333.93" y1="197.07" x2="348.07" y2="182.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="359.00" y="190.00" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block -> out_matmul<br>Inside out_matmul, matrix multiplication (circled X) multiplies gqa_out by the output projection weights w_matmul.</figcaption></figure>

<p>Because each token vector simply multiplies <code>w_matmul</code>, we can reuse the <code>matmul</code>
helper subroutine from the <code>expert</code> section:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">out_matmul</span><span class="p">(</span><span class="n">gqa_out</span><span class="p">,</span> <span class="n">w_matmul</span><span class="p">):</span>
    <span class="c1"># gqa_out: [seq_len, hidden_size]</span>
    <span class="c1"># w_matmul: [hidden_size, hidden_size]</span>

    <span class="c1"># gqa_block_out: [seq_len, hidden_size]</span>
    <span class="n">gqa_block_out</span> <span class="o">=</span> <span class="p">[</span><span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_matmul</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">gqa_out</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">gqa_block_out</span>
</code></pre></div>

<h2>gqa</h2>
<p><code>gqa</code> takes the normalized tokens <code>rms_out</code> and produces <code>gqa_out</code>, which feeds directly into <code>out_matmul</code>.
Its input and output share the exact same shape, <code>[seq_len, hidden_size]</code>:</p>
<figure class="svg-figure"><svg viewBox="-131 0 600 200" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <text x="56" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="56" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 5 100)">seq_len</text>
  <rect x="20" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="88" x2="92" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="20" y1="112" x2="92" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="44" y1="64" x2="44" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="64" x2="68" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="100" x2="128.5" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="128.5,95.5 128.5,104.5 141.25,100" fill="var(--text-color)" />
  <rect x="141.25" y="10" width="56" height="180" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="169.25" y="100" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 169.25 100)">gqa</text>
  <line x1="197.25" y1="100" x2="233.75" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="233.75,95.5 233.75,104.5 246.5,100" fill="var(--text-color)" />
  <text x="282.5" y="49" text-anchor="middle" dominant-baseline="central" font-size="14" font-family="var(--main-font)" fill="var(--text-color)">gqa_out</text>
  <text x="282.5" y="151" text-anchor="middle" dominant-baseline="central" font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="333.5" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 333.5 100)">seq_len</text>
  <rect x="246.5" y="64" width="72" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="88" x2="318.5" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="246.5" y1="112" x2="318.5" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="270.5" y1="64" x2="270.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="294.5" y1="64" x2="294.5" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block<br>The gqa module splits attention computation into independent head groups.</figcaption></figure>

<p>Zooming into <code>gqa</code>: the attention computation is divided into independent head groups
(<code>group_0</code>, <code>group_1</code>, and <code>group_2</code>). To clearly illustrate how the dimensions
and projections work in this module and the upcoming <code>group_0</code> section, we
expand the visual representation of <code>hidden_size</code> from 3 cells to 12 cells.</p>
<p>Each group receives the full <code>rms_out</code> matrix of shape <code>[seq_len,
hidden_size]</code>. Inside each group, the tokens are projected into Query, Key, and
Value representations, and the group produces an output matrix of shape
<code>[seq_len, head_dim]</code> (where <code>head_dim = hidden_size // num_groups</code>). Finally,
the outputs from all groups are concatenated back along the column dimension to
form <code>gqa_out</code>:</p>
<figure class="svg-figure"><svg viewBox="0 0 600 660" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="495" y="15" width="65" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="527.5" y="27" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">gqa</text>
  <!-- Casual annotation: rotated larger handwriting text -->
  <g transform="rotate(-9 105 56)">
    <text x="20" y="34" text-anchor="start" dominant-baseline="central" font-size="18"
          font-family="var(--handwriting-font)" fill="var(--hue-6)">expanded from 3</text>
    <text x="20" y="56" text-anchor="start" dominant-baseline="central" font-size="18"
          font-family="var(--handwriting-font)" fill="var(--hue-6)">(in previous figures) to 12</text>
    <text x="20" y="78" text-anchor="start" dominant-baseline="central" font-size="18"
          font-family="var(--handwriting-font)" fill="var(--hue-6)">for better illustration</text>
  </g>
  <!-- Casual circle around hidden_size at (300, 66) -->
  <path d="M 292,56 C 330,55 341,62 339,66 C 337,71 312,76 298,76 C 275,76 259,70 261,65 C 263,60 285,56 312,55"
        fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
  <!-- Dotted arrow pointing from circle to note -->
  <path d="M 260,65 Q 225,44 192,54" fill="none" stroke="var(--hue-6)" stroke-width="1.5" stroke-dasharray="5 4" stroke-linecap="round" />
  <line x1="192" y1="54" x2="201" y2="49" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <line x1="192" y1="54" x2="199" y2="59" stroke="var(--hue-6)" stroke-width="1.5" stroke-linecap="round" />
  <!-- Input Matrix: rms_out [seq_len=3, hidden_size=12] -->
  <text x="300" y="50" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="300" y="66" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="140" y="116" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 140 116)">seq_len</text>
  <!-- rms_out grid: 12 cols x 3 rows, x: 156..444, y: 80..152 -->
  <rect x="156" y="80" width="288" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="104" x2="444" y2="104" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="128" x2="444" y2="128" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="180" y1="80" x2="180" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="80" x2="204" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="80" x2="228" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="80" x2="252" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="80" x2="276" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="80" x2="300" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="324" y1="80" x2="324" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="80" x2="348" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="372" y1="80" x2="372" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="396" y1="80" x2="396" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="420" y1="80" x2="420" y2="152" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Black dot junction split directly to the 3 group capsules -->
  <line x1="300" y1="152" x2="300" y2="182" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="300" cy="182" r="3" fill="var(--text-color)" />
  <!-- Left branch to group_0 (x=116) -->
  <path d="M300,182 L132,182 A16,16 0 0,0 116,198 L116,211.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="111.5,211.25 120.5,211.25 116,224" fill="var(--text-color)" />
  <!-- Center branch straight down to group_1 (x=300) -->
  <line x1="300" y1="182" x2="300" y2="211.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,211.25 304.5,211.25 300,224" fill="var(--text-color)" />
  <!-- Right branch to group_2 (x=484) -->
  <path d="M300,182 L468,182 A16,16 0 0,1 484,198 L484,211.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="479.5,211.25 488.5,211.25 484,224" fill="var(--text-color)" />
  <!-- 3 Group Capsules (y: 240..350) -->
  <rect x="88" y="224" width="56" height="110" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="116" y="279" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 116 279)">group_0</text>
  <rect x="272" y="224" width="56" height="110" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="300" y="279" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 300 279)">group_1</text>
  <rect x="456" y="224" width="56" height="110" rx="28" ry="28"
        fill="none" stroke="var(--text-color)" stroke-width="2" />
  <text x="484" y="279" text-anchor="middle" dominant-baseline="central" font-size="16"
        font-family="var(--main-font)" fill="var(--text-color)"
        transform="rotate(-90 484 279)">group_2</text>
  <!-- Arrows from capsules to output slices (y=350 to y=374) -->
  <line x1="116" y1="334" x2="116" y2="361.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="111.5,361.25 120.5,361.25 116,374" fill="var(--text-color)" />
  <line x1="300" y1="334" x2="300" y2="361.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,361.25 304.5,361.25 300,374" fill="var(--text-color)" />
  <line x1="484" y1="334" x2="484" y2="361.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="479.5,361.25 488.5,361.25 484,374" fill="var(--text-color)" />
  <!-- 3 Output Slices [seq_len=3, head_dim=4] (x: 68..164, 252..348, 436..532; y: 400..472) -->
  <!-- Out Slice 0 (center x=116) -->
  <text x="116" y="387" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">head_dim</text>
  <rect x="68" y="400" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="424" x2="164" y2="424" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="68" y1="448" x2="164" y2="448" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="92" y1="400" x2="92" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="116" y1="400" x2="116" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="140" y1="400" x2="140" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="52" y="436" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 52 436)">seq_len</text>
  <!-- Out Slice 1 (center x=300) -->
  <text x="300" y="387" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">head_dim</text>
  <rect x="252" y="400" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="424" x2="348" y2="424" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="448" x2="348" y2="448" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="400" x2="276" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="400" x2="300" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="324" y1="400" x2="324" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="236" y="436" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 236 436)">seq_len</text>
  <!-- Out Slice 2 (center x=484) -->
  <text x="484" y="387" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">head_dim</text>
  <rect x="436" y="400" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="436" y1="424" x2="532" y2="424" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="436" y1="448" x2="532" y2="448" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="460" y1="400" x2="460" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="484" y1="400" x2="484" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="508" y1="400" x2="508" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="420" y="436" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 420 436)">seq_len</text>
  <!-- Converging arrows from slices to gqa_out columns -->
  <!-- Left branch from group_0 out (x=116) to col 0..3 center (x=204) -->
  <path d="M116,472 L116,484 A16,16 0 0,0 132,500 L188,500 A16,16 0 0,1 204,516 L204,524.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="199.5,524.25 208.5,524.25 204,537" fill="var(--text-color)" />
  <!-- Center branch from group_1 out (x=300) straight to col 4..7 center (x=300) -->
  <line x1="300" y1="472" x2="300" y2="524.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,524.25 304.5,524.25 300,537" fill="var(--text-color)" />
  <!-- Right branch from group_2 out (x=484) to col 8..11 center (x=396) -->
  <path d="M484,472 L484,484 A16,16 0 0,1 468,500 L412,500 A16,16 0 0,0 396,516 L396,524.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="391.5,524.25 400.5,524.25 396,537" fill="var(--text-color)" />
  <!-- Output Matrix: gqa_out [seq_len=3, hidden_size=12] -->
  <rect x="156" y="537" width="288" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="561" x2="444" y2="561" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="585" x2="444" y2="585" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="180" y1="537" x2="180" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="537" x2="204" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="537" x2="228" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="537" x2="252" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="537" x2="276" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="537" x2="300" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="324" y1="537" x2="324" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="537" x2="348" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="372" y1="537" x2="372" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="396" y1="537" x2="396" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="420" y1="537" x2="420" y2="609" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="140" y="573" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 140 573)">seq_len</text>
  <text x="300" y="623" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="300" y="641" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">gqa_out</text>
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block -> gqa<br>Inside gqa, inputs are processed in parallel across attention groups and concatenated into gqa_out.</figcaption></figure>

<p>Here is what <code>gqa</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">gqa</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">groups</span><span class="p">):</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">rms_out</span><span class="p">)</span>

    <span class="c1"># Each group computes attention on the full rms_out input</span>
    <span class="c1"># group_out: [seq_len, head_dim]</span>
    <span class="n">group_outs</span> <span class="o">=</span> <span class="p">[</span><span class="n">group</span><span class="p">(</span><span class="n">rms_out</span><span class="p">)</span> <span class="k">for</span> <span class="n">group</span> <span class="ow">in</span> <span class="n">groups</span><span class="p">]</span>

    <span class="c1"># Concatenate group outputs back along the column dimension</span>
    <span class="c1"># gqa_out: [seq_len, hidden_size]</span>
    <span class="n">gqa_out</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="c1"># Iterate over the rows of gqa_out</span>
    <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">):</span>
        <span class="n">row</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="c1"># Iterate over groups to concatenate the outputs</span>
        <span class="k">for</span> <span class="n">out</span> <span class="ow">in</span> <span class="n">group_outs</span><span class="p">:</span>
            <span class="n">row</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">out</span><span class="p">[</span><span class="n">t</span><span class="p">])</span>
        <span class="n">gqa_out</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">gqa_out</span>
</code></pre></div>

<p>Here we introduce a new hyperparameter, <code>num_groups</code>, as well as <code>head_dim</code>, which
depends on it: <code>head_dim = hidden_size // num_groups</code>. As a reference, Qwen 3.6 27B
has 4 groups, while Kimi K3 2.8T uses a different attention architecture entirely.</p>
<h2>group_0</h2>
<p>Zooming into <code>group_0</code>: it takes the full <code>rms_out</code> matrix and projects it into
query (<code>q</code>), key (<code>k</code>), and value (<code>v</code>) tensors with matmul using the learned
weights <code>w_q</code>, <code>w_k</code>, and <code>w_v</code>. Note that these matmuls are also token-wise operations.</p>
<p>Next, <code>q</code> and <code>k</code> pass through <code>rope</code> (circled as <strong>P</strong>), while <code>v</code> remains unchanged.
Then, all three branches feed into <code>attention_head</code> (circled as <strong>H</strong>) to
produce the final output, <code>group_0_out</code> of shape <code>[seq_len, head_dim]</code>:</p>
<figure class="svg-figure"><svg viewBox="0 -20 600 964" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="490" y="3" width="80" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="530" y="15" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">group_0</text>
  <!-- ==================== TOP INPUT: rms_out [seq_len=3, hidden_size=12] ==================== -->
  <text x="300" y="15" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">rms_out</text>
  <text x="300" y="33" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">hidden_size</text>
  <text x="140" y="84" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 140 84)">seq_len</text>
  <!-- Grid: 12 cols x 3 rows = 288px x 72px, centered at x=300 (x=156..444, y=48..120) -->
  <rect x="156" y="48" width="288" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Horizontal grid lines -->
  <line x1="156" y1="72" x2="444" y2="72" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="96" x2="444" y2="96" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Vertical grid lines (11 interior lines at intervals of 24px) -->
  <line x1="180" y1="48" x2="180" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="48" x2="204" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="48" x2="228" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="48" x2="252" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="48" x2="276" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="48" x2="300" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="324" y1="48" x2="324" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="48" x2="348" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="372" y1="48" x2="372" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="396" y1="48" x2="396" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="420" y1="48" x2="420" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Vertical line from bottom of input vector down to intersection -->
  <line x1="300" y1="120" x2="300" y2="150" stroke="var(--text-color)" stroke-width="2" />
  <!-- Black junction dot at intersection -->
  <circle cx="300" cy="150" r="3" fill="var(--text-color)" />
  <!-- ==================== ARROWS BREAKING INTO 3 FROM BLACK DOT ==================== -->
  <!-- Left branch to Q matmul (x=156) -->
  <path d="M300,150 L172,150 A16,16 0 0,0 156,166 L156,313.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="151.5,313.25 160.5,313.25 156,326" fill="var(--text-color)" />
  <!-- Center branch straight down to K matmul (x=300) -->
  <line x1="300" y1="150" x2="300" y2="313.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,313.25 304.5,313.25 300,326" fill="var(--text-color)" />
  <!-- Right branch to V matmul (x=444) -->
  <path d="M300,150 L428,150 A16,16 0 0,1 444,166 L444,313.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="439.5,313.25 448.5,313.25 444,326" fill="var(--text-color)" />
  <!-- ==================== Q STREAM (LEFT, STACK OF 2) ==================== -->
  <!-- w_q weights: Stack of 2 pieces of [hidden_size=12, d_head=2] matrices, y=192..480 -->
  <text x="80" y="155" text-anchor="middle" dominant-baseline="central" font-size="12"
        font-family="var(--main-font)" fill="var(--text-color)">w_q</text>
  <text x="80" y="169" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="36" y="336" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 36 336)">hidden_size</text>
  <!-- Shape annotation "q_heads" for w_q stack: center aligned with middle of z-stack -->
  <text x="110" y="482" text-anchor="middle" dominant-baseline="central"
        font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-45 110 482)">q_heads</text>
  <!-- Layer 1 (back: +8, -8, x=60..108, y=184..472) -->
  <rect x="60" y="184" width="48" height="288"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="60" y="184" width="48" height="288"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="84" y1="184" x2="84" y2="472" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="208" x2="108" y2="208" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="232" x2="108" y2="232" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="256" x2="108" y2="256" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="280" x2="108" y2="280" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="304" x2="108" y2="304" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="328" x2="108" y2="328" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="352" x2="108" y2="352" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="376" x2="108" y2="376" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="400" x2="108" y2="400" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="424" x2="108" y2="424" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="60" y1="448" x2="108" y2="448" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Layer 0 (front: x=52..100, y=192..480) -->
  <rect x="52" y="192" width="48" height="288"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="52" y="192" width="48" height="288"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="76" y1="192" x2="76" y2="480" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="216" x2="100" y2="216" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="240" x2="100" y2="240" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="264" x2="100" y2="264" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="288" x2="100" y2="288" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="312" x2="100" y2="312" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="336" x2="100" y2="336" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="360" x2="100" y2="360" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="384" x2="100" y2="384" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="408" x2="100" y2="408" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="432" x2="100" y2="432" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="52" y1="456" x2="100" y2="456" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from w_q to Q matmul circle (starts at x=108, ends at x=146, y=336) -->
  <line x1="108" y1="336" x2="133.25" y2="336" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="133.25,331.5 133.25,340.5 146,336" fill="var(--text-color)" />
  <!-- Q Matmul Circle (cx=156, cy=336, r=10) -->
  <circle cx="156" cy="336" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="148.93" y1="328.93" x2="163.07" y2="343.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="148.93" y1="343.07" x2="163.07" y2="328.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Stacked Q Vectors: 2 pieces along z-axis, y=528..600 -->
  <!-- Layer 1 (back: +8, -8, x=140..188, y=520..592) -->
  <rect x="140" y="520" width="48" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="140" y1="544" x2="188" y2="544" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="140" y1="568" x2="188" y2="568" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="164" y1="520" x2="164" y2="592" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Layer 0 (front: x=132..180, y=528..600) -->
  <rect x="132" y="528" width="48" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="132" y1="552" x2="180" y2="552" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="132" y1="576" x2="180" y2="576" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="156" y1="528" x2="156" y2="600" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from Q matmul down towards Q stack -->
  <line x1="156" y1="346" x2="156" y2="481.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="151.5,481.25 160.5,481.25 156,494" fill="var(--text-color)" />
  <!-- Shape annotation "d_head" on top of Q stack -->
  <text x="156" y="507" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <!-- q label on left (y=564) -->
  <text x="116" y="564" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="700"
        font-family="var(--main-font)" fill="var(--text-color)">q</text>
  <!-- Shape annotation "seq_len" on right of Q stack -->
  <text x="204" y="556" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 204 556)">seq_len</text>
  <!-- Shape annotation "q_heads" for Q stack: center aligned with middle of z-stack -->
  <text x="190" y="602" text-anchor="middle" dominant-baseline="central"
        font-size="11" font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-45 190 602)">q_heads</text>
  <!-- ==================== K STREAM (CENTER, SINGLE) ==================== -->
  <!-- w_k weight: [hidden_size=12, d_head=2], y=192..480 -->
  <text x="228" y="165" text-anchor="middle" dominant-baseline="central" font-size="12"
        font-family="var(--main-font)" fill="var(--text-color)">w_k</text>
  <text x="228" y="179" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="190" y="336" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 190 336)">hidden_size</text>
  <rect x="204" y="192" width="48" height="288"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="228" y1="192" x2="228" y2="480" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="216" x2="252" y2="216" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="240" x2="252" y2="240" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="264" x2="252" y2="264" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="288" x2="252" y2="288" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="312" x2="252" y2="312" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="336" x2="252" y2="336" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="360" x2="252" y2="360" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="384" x2="252" y2="384" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="408" x2="252" y2="408" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="432" x2="252" y2="432" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="204" y1="456" x2="252" y2="456" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from w_k to K matmul circle (starts at x=252, ends at x=290) -->
  <line x1="252" y1="336" x2="277.25" y2="336" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="277.25,331.5 277.25,340.5 290,336" fill="var(--text-color)" />
  <!-- K Matmul Circle (cx=300, cy=336, r=10) -->
  <circle cx="300" cy="336" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="292.93" y1="328.93" x2="307.07" y2="343.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="292.93" y1="343.07" x2="307.07" y2="328.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Single K Vector (x=276..324, y=528..600) -->
  <rect x="276" y="528" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="552" x2="324" y2="552" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="576" x2="324" y2="576" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="528" x2="300" y2="600" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from K matmul down towards single K vector -->
  <line x1="300" y1="346" x2="300" y2="489.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,489.25 304.5,489.25 300,502" fill="var(--text-color)" />
  <!-- Shape annotation "d_head" on top of K -->
  <text x="300" y="515" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <!-- k label on left (y=564) -->
  <text x="260" y="564" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="700"
        font-family="var(--main-font)" fill="var(--text-color)">k</text>
  <!-- Shape annotation "seq_len" on right of K -->
  <text x="340" y="564" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 340 564)">seq_len</text>
  <!-- ==================== V STREAM (RIGHT, SINGLE) ==================== -->
  <!-- w_v weight: [hidden_size=12, d_head=2], y=192..480 -->
  <text x="372" y="165" text-anchor="middle" dominant-baseline="central" font-size="12"
        font-family="var(--main-font)" fill="var(--text-color)">w_v</text>
  <text x="372" y="179" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="334" y="336" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 334 336)">hidden_size</text>
  <rect x="348" y="192" width="48" height="288"
        fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="372" y1="192" x2="372" y2="480" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="216" x2="396" y2="216" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="240" x2="396" y2="240" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="264" x2="396" y2="264" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="288" x2="396" y2="288" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="312" x2="396" y2="312" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="336" x2="396" y2="336" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="360" x2="396" y2="360" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="384" x2="396" y2="384" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="408" x2="396" y2="408" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="432" x2="396" y2="432" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="348" y1="456" x2="396" y2="456" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from w_v to V matmul circle (starts at x=396, ends at x=434) -->
  <line x1="396" y1="336" x2="421.25" y2="336" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="421.25,331.5 421.25,340.5 434,336" fill="var(--text-color)" />
  <!-- V Matmul Circle (cx=444, cy=336, r=10) -->
  <circle cx="444" cy="336" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="436.93" y1="328.93" x2="451.07" y2="343.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="436.93" y1="343.07" x2="451.07" y2="328.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- Single V Vector (x=420..468, y=528..600) -->
  <rect x="420" y="528" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="420" y1="552" x2="468" y2="552" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="420" y1="576" x2="468" y2="576" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="444" y1="528" x2="444" y2="600" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Arrow from V matmul down towards single V vector -->
  <line x1="444" y1="346" x2="444" y2="489.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="439.5,489.25 448.5,489.25 444,502" fill="var(--text-color)" />
  <!-- Shape annotation "d_head" on top of V -->
  <text x="444" y="515" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <!-- v label on left (y=564) -->
  <text x="404" y="564" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="700"
        font-family="var(--main-font)" fill="var(--text-color)">v</text>
  <!-- Shape annotation "seq_len" on right of V -->
  <text x="484" y="564" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 484 564)">seq_len</text>
  <!-- ==================== DOWNSTREAM: ROPE & ATTENTION HEADS ==================== -->
  <!-- Arrow from Q (y=600) to P_Q (cy=648) -->
  <line x1="156" y1="600" x2="156" y2="625.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="151.5,625.25 160.5,625.25 156,638" fill="var(--text-color)" />
  <!-- P_Q Circle (cx=156, cy=648, r=10) -->
  <circle cx="156" cy="648" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="156" y="648" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">P</text>
  <!-- Arrow from P_Q (cy=648) to Heads circle H (cy=696) -->
  <path d="M156,658 L156,680 A16,16 0 0,0 172,696 L277.25,696" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="277.25,691.5 277.25,700.5 290,696" fill="var(--text-color)" />
  <!-- Arrow from K (y=600) to P_K (cy=648) -->
  <line x1="300" y1="600" x2="300" y2="625.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,625.25 304.5,625.25 300,638" fill="var(--text-color)" />
  <!-- P_K Circle (cx=300, cy=648, r=10) -->
  <circle cx="300" cy="648" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="300" y="648" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">P</text>
  <!-- Arrow from P_K (cy=648) straight down into top of H circle (cy=696) -->
  <line x1="300" y1="658" x2="300" y2="673.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,673.25 304.5,673.25 300,686" fill="var(--text-color)" />
  <!-- Arrow from V (y=600) into right of H circle (cy=696, does NOT pass through P) -->
  <path d="M444,600 L444,680 A16,16 0 0,1 428,696 L322.75,696" fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="322.75,700.5 322.75,691.5 310,696" fill="var(--text-color)" />
  <!-- Heads Circle (cx=300, cy=696, r=10) -->
  <circle cx="300" cy="696" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="300" y="696" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">H</text>
  <!-- ==================== DIRECT ARROW FROM H (cy=696) TO group_0_out (y=744) ==================== -->
  <line x1="300" y1="706" x2="300" y2="731.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="295.5,731.25 304.5,731.25 300,744" fill="var(--text-color)" />
  <!-- ==================== FINAL OUTPUT VECTOR: [seq_len=3, head_dim=4] at y=744..816 ==================== -->
  <text x="236" y="780" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 236 780)">seq_len</text>
  <rect x="252" y="744" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="768" x2="348" y2="768" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="252" y1="792" x2="348" y2="792" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="276" y1="744" x2="276" y2="816" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="300" y1="744" x2="300" y2="816" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="324" y1="744" x2="324" y2="816" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="300" y="830" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">head_dim</text>
  <text x="300" y="848" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">group_0_out</text>
  <!-- ==================== LEGEND ==================== -->
  <g class="legend">
    <rect x="120" y="888" width="24" height="24" fill="var(--hue-2)" fill-opacity="0.35" stroke="var(--text-color)" stroke-width="1.5" />
    <text x="152" y="900" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">weights</text>
    <circle cx="225" cy="900" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="217.93" y1="892.93" x2="232.07" y2="907.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="217.93" y1="907.07" x2="232.07" y2="892.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="240" y="900" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
    <circle cx="310" cy="900" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="310" y="900" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold"
          font-family="var(--main-font)" fill="var(--text-color)">P</text>
    <text x="325" y="900" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">rope</text>
    <circle cx="380" cy="900" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="380" y="900" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="bold"
          font-family="var(--main-font)" fill="var(--text-color)">H</text>
    <text x="395" y="900" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">attention_head</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block -> gqa -> group_0<br>Inside group_0, inputs project to q, k, and v, apply rotary embedding (circled P), and enter multi-head attention (circled H).</figcaption></figure>

<p>As you can see, <code>w_q</code> has an extra depth axis (the z-axis in our diagram), so its
output also has a z-axis. We call each matrix along this depth axis a head. In
<code>group_0</code>, we have one <code>k</code> head and one <code>v</code> head, but multiple <code>q</code> heads. The
length of this z-axis is the number of query heads, <code>q_heads</code>.</p>
<p>This introduces another hyperparameter: <code>d_head</code>, the length of each token vector
in <code>q</code>, <code>k</code>, and <code>v</code>. Once you choose <code>d_head</code>, the number of query heads <code>q_heads</code>
is decided as well. That is because <code>attention_head</code> concatenates all <code>q_heads</code>
outputs along their channel dimension into the group output <code>head_dim</code>:</p>
<p>$$
\text{head_dim} = \text{q_heads} \times \text{d_head}
$$</p>
<p>In production configurations, architects usually define this in reverse: they
pick the total number of attention heads first and derive
<code>d_head = hidden_size // num_attention_heads</code>.</p>
<p>For comparison, Qwen 3.6 27B uses 6 query heads per group.</p>
<p>Here is what <code>group_0</code> looks like in Python:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">group_0</span><span class="p">(</span><span class="n">rms_out</span><span class="p">,</span> <span class="n">w_q</span><span class="p">,</span> <span class="n">w_k</span><span class="p">,</span> <span class="n">w_v</span><span class="p">):</span>
    <span class="c1"># rms_out: [seq_len, hidden_size]</span>
    <span class="c1"># w_q: [q_heads, hidden_size, d_head]</span>
    <span class="c1"># w_k: [hidden_size, d_head]</span>
    <span class="c1"># w_v: [hidden_size, d_head]</span>

    <span class="c1"># Project inputs to q, k, v token-wise using our matmul helper</span>
    <span class="c1"># q: [q_heads, seq_len, d_head]</span>
    <span class="c1"># k: [seq_len, d_head]</span>
    <span class="c1"># v: [seq_len, d_head]</span>
    <span class="n">q</span> <span class="o">=</span> <span class="p">[[</span><span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">rms_out</span><span class="p">]</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">w_q</span><span class="p">]</span>
    <span class="n">k</span> <span class="o">=</span> <span class="p">[</span><span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_k</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">rms_out</span><span class="p">]</span>
    <span class="n">v</span> <span class="o">=</span> <span class="p">[</span><span class="n">matmul</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">w_v</span><span class="p">)</span> <span class="k">for</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="n">rms_out</span><span class="p">]</span>

    <span class="c1"># Encode positions with rotary embedding</span>
    <span class="c1"># rope is token-wise and preserves tensor shape</span>
    <span class="n">q</span> <span class="o">=</span> <span class="n">rope</span><span class="p">(</span><span class="n">q</span><span class="p">)</span>
    <span class="n">k</span> <span class="o">=</span> <span class="n">rope</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>

    <span class="c1"># Compute multi-head attention and concatenate heads</span>
    <span class="c1"># group_0_out: [seq_len, head_dim]</span>
    <span class="n">group_0_out</span> <span class="o">=</span> <span class="n">attention_head</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">group_0_out</span>
</code></pre></div>

<p>Even though <code>rope</code> and <code>attention_head</code> are stateless functions, both are central
to how attention works. Let's dive into them one by one.</p>
<h2>rope</h2>
<p><code>rope</code> (circled as <strong>P</strong>) is short for <strong>Rotary Position Embedding</strong>.
Earlier transformer architectures injected position information right at the
start of the model. Modern LLMs instead move position encoding directly
into the attention layer. Notice that only <code>q</code> and <code>k</code> go through <code>rope</code>,
while <code>v</code> skips <code>rope</code> entirely.</p>
<p><code>rope</code> is a token-wise operation: each token vector is processed independently
based on its position index, without mixing information across different tokens
in the sequence.</p>
<p>In <code>group_0</code>, <code>k</code> is a 2D matrix (<code>[seq_len, d_head]</code>), while <code>q</code> is a 3D tensor
with multiple query heads (<code>[q_heads, seq_len, d_head]</code>). The entry-point <code>rope</code>
function inspects the input shape and routes the tensor to either <code>rope_2d</code> or
<code>rope_3d</code>:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">rope</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="c1"># x: [seq_len, d_head] or [q_heads, seq_len, d_head]</span>
    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span> <span class="nb">list</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">rope_3d</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">rope_2d</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div>

<p>Both functions handle tensor dimensions by calling a token-level subroutine,
<code>rope_token</code>. <code>rope_2d</code> processes each token vector across the sequence, while
<code>rope_3d</code> passes each head matrix into <code>rope_2d</code>:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">rope_2d</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="c1"># x: [seq_len, d_head]</span>
    <span class="c1"># return: [seq_len, d_head]</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">rope_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">pos</span><span class="p">)</span> <span class="k">for</span> <span class="n">pos</span><span class="p">,</span> <span class="n">token_vec</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">x</span><span class="p">)]</span>

<span class="k">def</span><span class="w"> </span><span class="nf">rope_3d</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="c1"># x: [q_heads, seq_len, d_head]</span>
    <span class="c1"># return: [q_heads, seq_len, d_head]</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">rope_2d</span><span class="p">(</span><span class="n">head</span><span class="p">)</span> <span class="k">for</span> <span class="n">head</span> <span class="ow">in</span> <span class="n">x</span><span class="p">]</span>
</code></pre></div>

<p>Inside <code>rope_token</code>, the vector is processed two numbers at a time. It steps
through consecutive pairs along the token vector, passing each pair together
with its token position to <code>rope_pair</code>:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">rope_token</span><span class="p">(</span><span class="n">token_vec</span><span class="p">,</span> <span class="n">pos</span><span class="p">):</span>
    <span class="c1"># token_vec: [d_head]</span>
    <span class="c1"># pos: scalar</span>
    <span class="n">d_head</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">token_vec</span><span class="p">)</span>
    <span class="c1"># output: [d_head]</span>
    <span class="n">output</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="c1"># Iterate over consecutive coordinate pairs: (x0, x1), (x2, x3), ...</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">d_head</span><span class="p">,</span> <span class="mi">2</span><span class="p">):</span>
        <span class="n">r0</span><span class="p">,</span> <span class="n">r1</span> <span class="o">=</span> <span class="n">rope_pair</span><span class="p">(</span><span class="n">token_vec</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">token_vec</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">],</span> <span class="n">pos</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">d_head</span><span class="p">)</span>
        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">r0</span><span class="p">)</span>
        <span class="n">output</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">r1</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">output</span>
</code></pre></div>

<p>Finally, <code>rope_pair</code> operates on two individual numbers. It updates the pair
based on the coordinates of <code>x0</code> and <code>x1</code> on the matrix (token position <code>pos</code>
and the dimension index <code>i</code>), returning two modified values. The operations here
are all on scalars. Because the angle is proportional to position, the two
rotations cancel down to a difference in the later dot product, so attention
sees relative position. Feel free to dive deeper on your own.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">math</span>

<span class="k">def</span><span class="w"> </span><span class="nf">rope_pair</span><span class="p">(</span><span class="n">x0</span><span class="p">,</span> <span class="n">x1</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">d_head</span><span class="p">):</span>
    <span class="c1"># The variables in this function are all scalars.</span>
    <span class="n">freq</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="p">(</span><span class="mi">10000</span> <span class="o">**</span> <span class="p">(</span><span class="n">i</span> <span class="o">/</span> <span class="n">d_head</span><span class="p">))</span>
    <span class="n">angle</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">*</span> <span class="n">freq</span>
    <span class="n">cos_val</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">angle</span><span class="p">)</span>
    <span class="n">sin_val</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">sin</span><span class="p">(</span><span class="n">angle</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">x0</span> <span class="o">*</span> <span class="n">cos_val</span> <span class="o">-</span> <span class="n">x1</span> <span class="o">*</span> <span class="n">sin_val</span><span class="p">,</span> <span class="n">x0</span> <span class="o">*</span> <span class="n">sin_val</span> <span class="o">+</span> <span class="n">x1</span> <span class="o">*</span> <span class="n">cos_val</span>
</code></pre></div>

<p>Because <code>rope</code> preserves tensor shapes, <code>q</code> and <code>k</code> emerge with the exact same
dimensions they had before entering <code>rope</code>, ready for <code>attention_head</code>.</p>
<h2>attention_head</h2>
<p>The attention head circled as <strong>H</strong> takes the query tensor <code>q</code>, key <code>k</code>, and value <code>v</code>.
This is the only operation in the LLM that is not a token-wise operation.
We have to pass in all the tokens at once instead of one by one.
This is the most complex function we need to explain in this article.</p>
<p>Remember that the <code>q</code> tensor has a z-axis of length <code>q_heads</code>?
In the <code>attention_head</code> function, we loop over that dimension, calling the
subroutine <code>dot_product_attention</code> (marked as <strong>D</strong> in the figure below), which
processes a single 2D query matrix alongside the shared key and value matrices.
Notice in the figure that <code>q</code> is the only tensor that splits. Every head reads
the same <code>k</code> and <code>v</code>. We then concatenate the outputs back into one tensor of
shape <code>[seq_len, head_dim]</code>, marked as <strong>C</strong>:</p>
<figure class="svg-figure"><svg viewBox="-66 -20 600 450" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="396" y="3" width="128" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="460" y="15" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">attention_head</text>
  <!-- ==================== LEFT INPUT: q [q_heads=2, seq_len=3, d_head=2] ==================== -->
  <text x="64" y="124" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">q</text>
  <text x="64" y="140" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="24" y="196" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 24 196)">seq_len</text>
  <text x="98" y="242" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-45 98 242)">q_heads</text>
  <!-- Layer 1 (back: +8, -8) -->
  <rect x="48" y="152" width="48" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="72" y1="152" x2="72" y2="224" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="48" y1="176" x2="96" y2="176" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="48" y1="200" x2="96" y2="200" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- Layer 0 (front) -->
  <rect x="40" y="160" width="48" height="72"
        fill="var(--background-color)" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="64" y1="160" x2="64" y2="232" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="184" x2="88" y2="184" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="40" y1="208" x2="88" y2="208" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- ==================== ONE QUERY HEAD PER LAYER ==================== -->
  <path d="M96,188 C141.625,188 141.625,160 187.25,160"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="187.25,155.5 187.25,164.5 200,160" fill="var(--text-color)" />
  <path d="M88,196 C137.625,196 137.625,232 187.25,232"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="187.25,227.5 187.25,236.5 200,232" fill="var(--text-color)" />
  <!-- ==================== SHARED KEY (TOP) ==================== -->
  <text x="210" y="36" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="172" y="84" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">k</text>
  <text x="248" y="84" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 248 84)">seq_len</text>
  <rect x="186" y="48" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="210" y1="48" x2="210" y2="120" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="186" y1="72" x2="234" y2="72" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="186" y1="96" x2="234" y2="96" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="210" y1="120" x2="210" y2="137.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="205.5,137.25 214.5,137.25 210,150" fill="var(--text-color)" />
  <path d="M210,120 A67.27,67.27 0 0,0 193.53,216.46"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="190.50,219.79 196.55,213.12 202.11,225.86" fill="var(--text-color)" />
  <!-- ==================== SHARED VALUE (BOTTOM) ==================== -->
  <text x="210" y="358" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="172" y="308" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">v</text>
  <text x="248" y="308" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 248 308)">seq_len</text>
  <rect x="186" y="272" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="210" y1="272" x2="210" y2="344" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="186" y1="296" x2="234" y2="296" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="186" y1="320" x2="234" y2="320" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="210" y1="272" x2="210" y2="254.75" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="205.5,254.75 214.5,254.75 210,242" fill="var(--text-color)" />
  <path d="M210,272 A67.27,67.27 0 0,0 226.47,175.54"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="229.50,172.21 223.45,178.88 217.89,166.14" fill="var(--text-color)" />
  <!-- ==================== DOT PRODUCT ATTENTION ==================== -->
  <circle cx="210" cy="160" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="210" y="160" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">D</text>
  <circle cx="210" cy="232" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="210" y="232" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">D</text>
  <!-- ==================== CONCAT ==================== -->
  <path d="M220,160 L282,160 A8,8 0 0,1 290,168 L290,173.25"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="285.5,173.25 294.5,173.25 290,186" fill="var(--text-color)" />
  <path d="M220,232 L282,232 A8,8 0 0,0 290,224 L290,218.75"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="285.5,218.75 294.5,218.75 290,206" fill="var(--text-color)" />
  <circle cx="290" cy="196" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="290" y="196" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">C</text>
  <!-- ==================== OUTPUT: out [seq_len=3, head_dim=4] ==================== -->
  <line x1="300" y1="196" x2="319.25" y2="196" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="319.25,191.5 319.25,200.5 332,196" fill="var(--text-color)" />
  <rect x="332" y="160" width="96" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="356" y1="160" x2="356" y2="232" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="380" y1="160" x2="380" y2="232" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="404" y1="160" x2="404" y2="232" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="332" y1="184" x2="428" y2="184" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="332" y1="208" x2="428" y2="208" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="444" y="196" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 444 196)">seq_len</text>
  <text x="380" y="246" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">head_dim</text>
  <text x="380" y="264" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">out</text>
  <!-- ==================== LEGEND ==================== -->
  <g class="legend">
    <circle cx="67" cy="400" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="67" y="400" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
          font-family="var(--main-font)" fill="var(--text-color)">D</text>
    <text x="82" y="400" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">dot_product_attention</text>
    <circle cx="271" cy="400" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="271" y="400" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
          font-family="var(--main-font)" fill="var(--text-color)">C</text>
    <text x="286" y="400" text-anchor="start" dominant-baseline="central" font-size="12" font-family="var(--main-font)" fill="var(--text-color)">concat along d_head</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block -> gqa -> group_0 -> attention_head<br>The query tensor splits into one matrix per head, while the same key and value matrices feed every head.</figcaption></figure>

<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">attention_head</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">):</span>
    <span class="c1"># q: [q_heads, seq_len, d_head]</span>
    <span class="c1"># k: [seq_len, d_head]</span>
    <span class="c1"># v: [seq_len, d_head]</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>

    <span class="c1"># Compute attention for each query head</span>
    <span class="c1"># head_outs: [q_heads, seq_len, d_head]</span>
    <span class="n">head_outs</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">q_head</span> <span class="ow">in</span> <span class="n">q</span><span class="p">:</span>
        <span class="c1"># head_out: [seq_len, d_head]</span>
        <span class="n">head_out</span> <span class="o">=</span> <span class="n">dot_product_attention</span><span class="p">(</span><span class="n">q_head</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
        <span class="n">head_outs</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">head_out</span><span class="p">)</span>

    <span class="c1"># Concatenate all head outputs</span>
    <span class="c1"># out: [seq_len, head_dim]</span>
    <span class="n">out</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">):</span>
        <span class="c1"># token_out: [head_dim]</span>
        <span class="c1"># head_dim == q_heads * d_head</span>
        <span class="n">token_out</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">head_out</span> <span class="ow">in</span> <span class="n">head_outs</span><span class="p">:</span>
            <span class="c1"># head_out: [seq_len, d_head]</span>
            <span class="n">token_out</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">head_out</span><span class="p">[</span><span class="n">t</span><span class="p">])</span>
        <span class="n">out</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">token_out</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">out</span>
</code></pre></div>

<p>Zooming into <code>dot_product_attention</code>: this subroutine was also used by
traditional multi-head attention (<strong>MHA</strong>) before GQA was introduced. It takes
care of one Q, K, and V head. The function first transposes <code>k</code> into <code>k_T</code> so
that we can compute dot products between <code>q_head</code> and the keys with <code>matmul</code>.
Because more operations follow the dot products, we iterate over the tokens
(rows) in <code>q_head</code> and handle them one by one. For each token, it delegates the
computation to a token-level subroutine, <code>attention_token</code>, and appends the
resulting vector to <code>head_out</code>.</p>
<p>The figure below unrolls that loop and shows all three tokens at once. Every row
of <code>q_head</code> takes its own trip through the same steps, and the shared <code>k_T</code> and
<code>v</code> feed all of them. Stack the resulting attention weights and you get a
triangle, because a token is only allowed to look at itself and the tokens
before it:</p>
<figure class="svg-figure"><svg viewBox="-5 -78 600 418" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 2rem auto; max-width: 100%; width: 600px; height: auto;">
  <!-- Badge -->
  <rect x="409" y="-70" width="171" height="24" rx="12" ry="12"
        fill="var(--text-color)" stroke="none" />
  <text x="494.5" y="-58" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="700"
        font-family="var(--main-font)" fill="var(--background-color)">dot_product_attention</text>
  <!-- ==================== INPUT: q_head [seq_len=3, d_head=2] ==================== -->
  <text x="59" y="49" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">q_head</text>
  <text x="59" y="151" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="20" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 20 100)">seq_len</text>
  <rect x="35" y="64" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="59" y1="64" x2="59" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="35" y1="88" x2="83" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="35" y1="112" x2="83" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- one row per token, fanning into the three matmul circles -->
  <path d="M83,76 C96.125,76 96.125,68 109.25,68"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="109.25,63.5 109.25,72.5 122,68" fill="var(--text-color)" />
  <line x1="83" y1="100" x2="109.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="109.25,95.5 109.25,104.5 122,100" fill="var(--text-color)" />
  <path d="M83,124 C96.125,124 96.125,132 109.25,132"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="109.25,127.5 109.25,136.5 122,132" fill="var(--text-color)" />
  <!-- ==================== SHARED KEYS (TOP): k_T [d_head=2, seq_len=3] ==================== -->
  <text x="132" y="-62" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">k_T</text>
  <text x="132" y="-43" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">seq_len</text>
  <text x="183" y="-4" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 183 -4)">d_head</text>
  <rect x="96" y="-28" width="72" height="48"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="120" y1="-28" x2="120" y2="20" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="144" y1="-28" x2="144" y2="20" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="96" y1="-4" x2="168" y2="-4" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="132" y1="20" x2="132" y2="45.25" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="127.5,45.25 136.5,45.25 132,58" fill="var(--text-color)" />
  <path d="M132,20 A50,50 0 0,1 146.53,82.73"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="143.17,79.74 149.90,85.72 139.36,93.23" fill="var(--text-color)" />
  <path d="M132,20 A65,65 0 0,1 149.21,117.28"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="146.36,113.80 152.06,120.76 140.20,126.28" fill="var(--text-color)" />
  <!-- ==================== MATMUL WITH THE KEYS ==================== -->
  <circle cx="132" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="60.93" x2="139.07" y2="75.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="75.07" x2="139.07" y2="60.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="132" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="92.93" x2="139.07" y2="107.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="107.07" x2="139.07" y2="92.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="132" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="124.93" x2="139.07" y2="139.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="124.93" y1="139.07" x2="139.07" y2="124.93" stroke="var(--text-color)" stroke-width="2" />
  <line x1="142" y1="68" x2="174.25" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="174.25,63.5 174.25,72.5 187,68" fill="var(--text-color)" />
  <line x1="142" y1="100" x2="174.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="174.25,95.5 174.25,104.5 187,100" fill="var(--text-color)" />
  <line x1="142" y1="132" x2="174.25" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="174.25,127.5 174.25,136.5 187,132" fill="var(--text-color)" />
  <!-- ==================== dot_products [seq_len] per token ==================== -->
  <text x="223" y="41" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">dot_products</text>
  <text x="223" y="159" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">seq_len</text>
  <rect x="187" y="56" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="211" y1="56" x2="211" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="235" y1="56" x2="235" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="187" y="88" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="211" y1="88" x2="211" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="235" y1="88" x2="235" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <rect x="187" y="120" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="211" y1="120" x2="211" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="235" y1="120" x2="235" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="259" y1="68" x2="271.25" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="271.25,63.5 271.25,72.5 284,68" fill="var(--text-color)" />
  <line x1="259" y1="100" x2="271.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="271.25,95.5 271.25,104.5 284,100" fill="var(--text-color)" />
  <line x1="259" y1="132" x2="271.25" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="271.25,127.5 271.25,136.5 284,132" fill="var(--text-color)" />
  <!-- ==================== SCALE, SOFTMAX, ZERO-PAD ==================== -->
  <circle cx="294" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="294" y="68" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">&#963;</text>
  <circle cx="294" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="294" y="100" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">&#963;</text>
  <circle cx="294" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <text x="294" y="132" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
        font-family="var(--main-font)" fill="var(--text-color)">&#963;</text>
  <line x1="304" y1="68" x2="316.25" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="316.25,63.5 316.25,72.5 329,68" fill="var(--text-color)" />
  <line x1="304" y1="100" x2="316.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="316.25,95.5 316.25,104.5 329,100" fill="var(--text-color)" />
  <line x1="304" y1="132" x2="316.25" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="316.25,127.5 316.25,136.5 329,132" fill="var(--text-color)" />
  <!-- ==================== weights [seq_len] per token: the causal triangle ==================== -->
  <text x="365" y="41" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">weights</text>
  <text x="365" y="159" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">seq_len</text>
  <rect x="329" y="56" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="353" y1="56" x2="353" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="377" y1="56" x2="377" y2="80" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="341" y="68" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">1</text>
  <text x="365" y="68" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <text x="389" y="68" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <rect x="329" y="88" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="353" y1="88" x2="353" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="377" y1="88" x2="377" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="341" y="100" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.6</text>
  <text x="365" y="100" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.4</text>
  <text x="389" y="100" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--gray-text-color)">0</text>
  <rect x="329" y="120" width="72" height="24" fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="353" y1="120" x2="353" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="377" y1="120" x2="377" y2="144" stroke="var(--text-color)" stroke-width="1.5" />
  <text x="341" y="132" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.2</text>
  <text x="365" y="132" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.3</text>
  <text x="389" y="132" text-anchor="middle" dominant-baseline="central" font-size="13"
        font-family="var(--main-font)" fill="var(--text-color)">.5</text>
  <line x1="401" y1="68" x2="433.25" y2="68" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="433.25,63.5 433.25,72.5 446,68" fill="var(--text-color)" />
  <line x1="401" y1="100" x2="433.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="433.25,95.5 433.25,104.5 446,100" fill="var(--text-color)" />
  <line x1="401" y1="132" x2="433.25" y2="132" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="433.25,127.5 433.25,136.5 446,132" fill="var(--text-color)" />
  <!-- ==================== SHARED VALUES (BOTTOM): v [seq_len=3, d_head=2] ==================== -->
  <text x="456" y="286" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">v</text>
  <text x="456" y="267" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="495" y="216" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 495 216)">seq_len</text>
  <rect x="432" y="180" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="456" y1="180" x2="456" y2="252" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="432" y1="204" x2="480" y2="204" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="432" y1="228" x2="480" y2="228" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="456" y1="180" x2="456" y2="154.75" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="451.5,154.75 460.5,154.75 456,142" fill="var(--text-color)" />
  <path d="M456,180 A50,50 0 0,1 441.47,117.27"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="444.83,120.26 438.10,114.28 448.64,106.77" fill="var(--text-color)" />
  <path d="M456,180 A65,65 0 0,1 438.79,82.72"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="441.64,86.20 435.94,79.24 447.80,73.72" fill="var(--text-color)" />
  <!-- ==================== MATMUL WITH THE VALUES ==================== -->
  <circle cx="456" cy="68" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="60.93" x2="463.07" y2="75.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="75.07" x2="463.07" y2="60.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="456" cy="100" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="92.93" x2="463.07" y2="107.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="107.07" x2="463.07" y2="92.93" stroke="var(--text-color)" stroke-width="2" />
  <circle cx="456" cy="132" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="124.93" x2="463.07" y2="139.07" stroke="var(--text-color)" stroke-width="2" />
  <line x1="448.93" y1="139.07" x2="463.07" y2="124.93" stroke="var(--text-color)" stroke-width="2" />
  <!-- ==================== OUTPUT: head_out [seq_len=3, d_head=2] ==================== -->
  <path d="M466,68 C479.125,68 479.125,76 492.25,76"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="492.25,71.5 492.25,80.5 505,76" fill="var(--text-color)" />
  <line x1="466" y1="100" x2="492.25" y2="100" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="492.25,95.5 492.25,104.5 505,100" fill="var(--text-color)" />
  <path d="M466,132 C479.125,132 479.125,124 492.25,124"
        fill="none" stroke="var(--text-color)" stroke-width="2" stroke-linecap="round" />
  <polygon points="492.25,119.5 492.25,128.5 505,124" fill="var(--text-color)" />
  <text x="529" y="49" text-anchor="middle" dominant-baseline="central" font-size="14"
        font-family="var(--main-font)" fill="var(--text-color)">head_out</text>
  <text x="529" y="151" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)">d_head</text>
  <text x="568" y="100" text-anchor="middle" dominant-baseline="central" font-size="11"
        font-family="var(--main-font)" fill="var(--gray-text-color)"
        transform="rotate(-90 568 100)">seq_len</text>
  <rect x="505" y="64" width="48" height="72"
        fill="none" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="529" y1="64" x2="529" y2="136" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="505" y1="88" x2="553" y2="88" stroke="var(--text-color)" stroke-width="1.5" />
  <line x1="505" y1="112" x2="553" y2="112" stroke="var(--text-color)" stroke-width="1.5" />
  <!-- ==================== LEGEND ==================== -->
  <g class="legend">
    <circle cx="143" cy="322" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <text x="143" y="322" dy="0.36em" text-anchor="middle" font-size="14" font-weight="bold"
          font-family="var(--main-font)" fill="var(--text-color)">&#963;</text>
    <text x="158" y="322" text-anchor="start" dominant-baseline="central" font-size="12"
          font-family="var(--main-font)" fill="var(--text-color)">scale, softmax, pad with zeros</text>
    <circle cx="403" cy="322" r="10" fill="var(--background-color)" stroke="var(--text-color)" stroke-width="2" />
    <line x1="395.93" y1="314.93" x2="410.07" y2="329.07" stroke="var(--text-color)" stroke-width="2" />
    <line x1="395.93" y1="329.07" x2="410.07" y2="314.93" stroke="var(--text-color)" stroke-width="2" />
    <text x="418" y="322" text-anchor="start" dominant-baseline="central" font-size="12"
          font-family="var(--main-font)" fill="var(--text-color)">matmul</text>
  </g>
</svg><figcaption>model.predict -> decoder -> decoder_block -> gqa_block -> gqa -> group_0 -> attention_head -> dot_product_attention<br>Each token attends only to itself and the tokens before it, so the attention weights form a triangle.</figcaption></figure>

<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">dot_product_attention</span><span class="p">(</span><span class="n">q_head</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">):</span>
    <span class="c1"># q_head: [seq_len, d_head]</span>
    <span class="c1"># k: [seq_len, d_head]</span>
    <span class="c1"># v: [seq_len, d_head]</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
    <span class="n">d_head</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">k</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>

    <span class="c1"># Transpose k to compute dot products with q: [d_head, seq_len]</span>
    <span class="n">k_T</span> <span class="o">=</span> <span class="p">[[</span><span class="n">k</span><span class="p">[</span><span class="n">row</span><span class="p">][</span><span class="n">col</span><span class="p">]</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">seq_len</span><span class="p">)]</span> <span class="k">for</span> <span class="n">col</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">d_head</span><span class="p">)]</span>

    <span class="c1"># head_out: [seq_len, d_head]</span>
    <span class="n">head_out</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">q_token</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">q_head</span><span class="p">):</span>
        <span class="c1"># token_out: [d_head]</span>
        <span class="n">token_out</span> <span class="o">=</span> <span class="n">attention_token</span><span class="p">(</span><span class="n">q_token</span><span class="p">,</span> <span class="n">k_T</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
        <span class="n">head_out</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">token_out</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">head_out</span>
</code></pre></div>

<p>Finally, <code>attention_token</code> performs scaled dot-product attention for a single
token. It first computes dot products with <code>k_T</code> to produce a vector of shape
<code>[seq_len]</code>. Those scores are then scaled and converted to attention weights,
<code>weights</code>, before multiplying with <code>v</code>.</p>
<p>To keep generation autoregressive, only the first <code>i + 1</code> values in <code>weights</code>
(from position 0 up to and including current position <code>i</code>) are non-zero.
The remaining future positions are padded with zeros:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">math</span>

<span class="k">def</span><span class="w"> </span><span class="nf">attention_token</span><span class="p">(</span><span class="n">q_token</span><span class="p">,</span> <span class="n">k_T</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">i</span><span class="p">):</span>
    <span class="c1"># q_token: [d_head]</span>
    <span class="c1"># k_T: [d_head, seq_len]</span>
    <span class="c1"># v: [seq_len, d_head]</span>
    <span class="n">d_head</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">q_token</span><span class="p">)</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>

    <span class="c1"># Compute raw dot products against all keys: [seq_len]</span>
    <span class="n">dot_products</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">q_token</span><span class="p">,</span> <span class="n">k_T</span><span class="p">)</span>

    <span class="c1"># Scale scores up to the current token position i: [i + 1]</span>
    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dot_products</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">/</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">d_head</span><span class="p">))</span>

    <span class="c1"># weights: [seq_len]</span>
    <span class="c1"># Pad zeros for future tokens to make length [seq_len]</span>
    <span class="n">weights</span> <span class="o">=</span> <span class="n">softmax</span><span class="p">(</span><span class="n">scores</span><span class="p">)</span> <span class="o">+</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="n">seq_len</span> <span class="o">-</span> <span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>

    <span class="c1"># Compute weighted sum of values: [d_head]</span>
    <span class="n">token_out</span> <span class="o">=</span> <span class="n">matmul</span><span class="p">(</span><span class="n">weights</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">token_out</span>
</code></pre></div>

<h2>Putting it all together</h2>
<p>We put everything together in <a href="/the-anatomy-of-an-llm/graph/">this figure</a> to
show the end-to-end workflow of <code>model.predict</code>. You can hover over any tensor
to see its name and shape, or any operation to see its Python code. In the
code popups, we used NumPy operations without explicit token-wise iterations to
make them concise and easier to read. You can bookmark the page and come back to
it anytime you need a quick reference.</p>
<p>You can also explore the complete, runnable Python implementation in the
<a href="https://github.com/haifeng-jin/readable-llm">readable-llm</a> repository on GitHub.
To check that the architecture actually works, we trained a 0.000005B model
(4,596 parameters) on a single sentence. It can only answer the one prompt
<code>"What is 1+1?"</code>, which is all we needed. The training code is in the repo too,
if you want to see how those weights were produced.</p>
<h2>Where to go next</h2>
<p>What we built here is a foundation. Most models you will run into are variations
on the same shape, and they get much easier to read once you know the baseline.
From here you can branch off in whatever direction interests you: other
architectures, the theory behind why any of this works, or the performance
engineering that separates code written to be read from code written to be fast.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="attention-is-all-you-need" class="csl-entry">Vaswani, A., &#38; others. (2017). Attention is all you need. <i>NeurIPS</i>. <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-2"><div class="csl-bib-body">
  <div data-csl-entry-id="gpt" class="csl-entry">Radford, A., Narasimhan, K., Salimans, T., &#38; Sutskever, I. (2018). Improving language understanding by generative pre-training. <i>OpenAI</i>. <a href="#cite-2-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-3"><div class="csl-bib-body">
  <div data-csl-entry-id="moe" class="csl-entry">Shazeer, N., &#38; others. (2017). Outrageously large neural networks: The sparsely-gated mixture-of-experts layer. <i>ICLR</i>. <a href="#cite-3-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-4"><div class="csl-bib-body">
  <div data-csl-entry-id="gqa" class="csl-entry">Ainslie, J., &#38; others. (2023). GQA: Training generalized multi-query transformer models from multi-head checkpoints. <i>EMNLP</i>. <a href="#cite-4-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-5"><div class="csl-bib-body">
  <div data-csl-entry-id="rmsnorm" class="csl-entry">Zhang, B., &#38; Sennrich, R. (2019). Root mean square layer normalization. <i>NeurIPS</i>. <a href="#cite-5-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-6"><div class="csl-bib-body">
  <div data-csl-entry-id="rope" class="csl-entry">Su, J., &#38; others. (2024). RoFormer: Enhanced transformer with rotary position embedding. <i>Neurocomputing</i>. <a href="#cite-6-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-7"><div class="csl-bib-body">
  <div data-csl-entry-id="swiglu" class="csl-entry">Shazeer, N. (2020). GLU variants improve transformer. <i>arXiv Preprint arXiv:2002.05202</i>. <a href="#cite-7-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>Intelligence Goodput</title>
    <link href="https://haifengjin.com/intelligence-goodput/"/>
    <id>https://haifengjin.com/intelligence-goodput/</id>
    <published>2025-08-20T00:00:00+00:00</published>
    <updated>2025-08-20T00:00:00+00:00</updated>
    <summary>A metric to measure the speed of intelligence</summary>
    <content type="html"><![CDATA[<p>Processing speed has long been recognized as an important metric in human cognitive
ability<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup> and intelligence scale measurement<sup id="cite-2-a"><a href="#ref-2" class="citation-link">2,3,4</a></sup>.
It reflects the fundamental capacity to perform daily tasks as a human,
including reading comprehension, communication, and driving. Both the quality of work and the time required are essential to its evaluation.</p>
<p>To measure the intelligence scale of artificial intelligence (AI), various
benchmark datasets have been designed, analogous to tests for humans.
For example, the MMLU dataset<sup id="cite-5-a"><a href="#ref-5" class="citation-link">5</a></sup> consists of multiple-choice questions to
test multi-language understanding, the GPQA dataset<sup id="cite-6-a"><a href="#ref-6" class="citation-link">6</a></sup> consists of
graduate-level questions on a variety of subjects, and the MATH dataset<sup id="cite-7-a"><a href="#ref-7" class="citation-link">7</a></sup>
comprises competition math problems.</p>
<p>However, when experiments are conducted to test AI on these datasets,
unlike tests for humans, only the quality of work is emphasized, while the time
aspect of the test is typically ignored. Such an approach does not satisfy the
requirements of AI in real-world applications.</p>
<p>From a pragmatic perspective, many tasks are time-sensitive, such as autonomous
driving and customer service. In these applications, the output of
intelligence is required within a limited time window. Even in the early days of deep
learning, there was an implicit assumption of a time limit. When AlphaGo<sup id="cite-8-a"><a href="#ref-8" class="citation-link">8</a></sup>
defeated the best human Go player in 2016, it adhered to the same rules,
including time constraints, as the human player.</p>
<h2>Implications of test-time scaling</h2>
<p>With the advent of the test-time scaling law<sup id="cite-9-a"><a href="#ref-9" class="citation-link">9</a></sup>, the scores
achieved on these AI benchmark datasets alone can no longer fully measure intelligence.</p>
<p>According to the test-time scaling law, a small model can solve harder problems by spending
more tokens "thinking", while a larger model can solve the same problem with
fewer tokens. By analogy to a human IQ test, it is as if one person spent
more time and used a long sheet of scratch paper to finish the test, while
another person used very little time and no scratch paper at all. If both
achieve the same number of correct answers, it is more rational to conclude that
the person who completed the test faster possesses higher intelligence, likely
employing a more efficient method.</p>
<p>That smaller and larger models can answer the same question correctly does not
imply they have the same intelligence level; rather, they approached the problem
differently. Therefore, without any constraint on the output, whether in terms of
the number of tokens or output speed, it is impossible to measure the actual
intelligence level of AI solely based on correctness.</p>
<p>From a user's perspective, estimating the time required for AI to complete tasks
has become increasingly challenging. Prior to the advent of test-time scaling,
models typically used a similar number of tokens to solve each problem. With
test-time scaling, models are incentivized to generate more tokens for certain
problems, leading to greater variability. Users now have access to metrics such
as tokens per second for the APIs they utilize, but lack information about the
total number of tokens needed for a given task. As a result, they can no longer
reliably estimate the time required for task completion.</p>
<h2>Why speed was ignored</h2>
<p>If processing speed is so important to intelligence, why was it historically ignored?
The reasons are twofold. First, AI was not sufficiently advanced. Early systems
could not pass the Turing test<sup id="cite-10-a"><a href="#ref-10" class="citation-link">10</a></sup>, solve math or coding problems, or
perform in-depth reading or writing. Given that AI was in a primitive stage, the
focus was on enabling AI to perform new tasks rather than on the speed of task
completion. Second, AI applications were all task-specific. The AI for X-ray image
processing<sup id="cite-11-a"><a href="#ref-11" class="citation-link">11</a></sup> was entirely different from the AI used for recommendation
systems<sup id="cite-12-a"><a href="#ref-12" class="citation-link">12</a></sup>. The applicability of AI was measured
case by case by application builders, without the need for a unified
processing speed metric.</p>
<p>However, with the advent of large language models (LLMs)<sup id="cite-13-a"><a href="#ref-13" class="citation-link">13</a></sup>, AI has become
substantially more capable and generalizable, with applications spanning from medical
diagnosis<sup id="cite-14-a"><a href="#ref-14" class="citation-link">14</a></sup> to recommendation systems<sup id="cite-15-a"><a href="#ref-15" class="citation-link">15</a></sup>. Consequently,
it is now both rational and timely to establish a unified metric for AI processing speed.</p>
<h2>A mental shift for evaluating AI</h2>
<p>Additionally, there has been a significant shift in how application builders
interface with models as summarized in Table 1. Initially, developers focused on training
application-specific deep learning models, beginning with the advent of AlexNet<sup id="cite-16-a"><a href="#ref-16" class="citation-link">16</a></sup>.
With the emergence of ChatGPT<sup id="cite-13-b"><a href="#ref-13" class="citation-link">13</a></sup>, the paradigm shifted toward pretraining
large language models (LLMs). The public release of Llama<sup id="cite-17-a"><a href="#ref-17" class="citation-link">17</a></sup> enabled
post-training and fine-tuning of open models. As the capabilities of open models
advanced, such as the Qwen series<sup id="cite-18-a"><a href="#ref-18" class="citation-link">18</a></sup>, it became increasingly feasible to
serve an open-weight model "as is" without additional fine-tuning. More
recently, the introduction of DeepSeek<sup id="cite-19-a"><a href="#ref-19" class="citation-link">19</a></sup>, which substantially reduced
token costs, has made it more cost-effective to utilize hosted APIs from major
AI service providers rather than maintaining proprietary infrastructure.</p>
<p><strong>Table 1.</strong> Paradigm shifts of AI usages</p>
<table>
<thead>
<tr>
<th>Paradigms</th>
<th>Defining Moments</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deep Learning</td>
<td>AlexNet</td>
</tr>
<tr>
<td>Pretraining LLMs</td>
<td>ChatGPT</td>
</tr>
<tr>
<td>Open Model Fine-tuning</td>
<td>Llama</td>
</tr>
<tr>
<td>Open Model Serving</td>
<td>Qwen</td>
</tr>
<tr>
<td>Hosted APIs</td>
<td>DeepSeek</td>
</tr>
</tbody>
</table>
<p>Therefore, when evaluating the intelligence of AI, a shift is required from
evaluating static models, pure mathematical constructs defined by neural
architectures and parameters, to evaluating hosted AI services or APIs. It is
necessary to assess their processing speed for applicability to time-sensitive tasks.</p>
<h2>Existing metrics</h2>
<p>There are established metrics to evaluate the speed of LLMs, such as time to
first token (TTFT) and tokens per second (TPS)<sup id="cite-20-a"><a href="#ref-20" class="citation-link">20</a></sup>. These are suitable
metrics for serving language models, but there are two major limitations when
considering them as general metrics for AI processing speed.</p>
<p>Unlike the time-limit for an IQ test, these metrics focus on the number
of tokens rather than the actual useful information produced by intelligence. For
example, a simple program without any intelligence can output random tokens at
a very high speed.</p>
<p>Therefore, a new metric is needed that measures the intelligent
portion of the output rather than the total volume of tokens produced per unit time.</p>
<h2>Intelligence goodput</h2>
<p>I propose intelligence goodput as a metric for measuring the processing speed of AI.</p>
<p><em>Intelligence goodput</em> (IG) is a measurement indicating the
maximum amount of intelligent information that an AI service 
can produce in a given amount of time, formally expressed as:</p>
<p>$$
G=\frac{I}{t}
$$</p>
<p>where $G$ is the intelligence goodput, $I$ is the amount of intelligent
information, and $t$ is the total time spent.</p>
<p>It is important to note that intelligence goodput primarily measures the output
speed of AI, rather than input, for two reasons. First, the main impact of
intelligence goodput is in human-AI interaction, which will be discussed
further in the next section. Second, AI's processing speed is mainly bounded by
output, not input. Thus, it is more meaningful to track AI progress with a
metric that measures its bottleneck.</p>
<p>Despite the formal definition of intelligence goodput, certain ambiguities
remain in practical measurement. First, "intelligent information" is not an
unambiguous term. Defining the amount of intelligence contained in the output of
an AI model remains a challenging task. Second, time is also subject to multiple
interpretations in computer science.</p>
<p>Regarding time measurement, the two popular choices are
CPU time and wall time. Wall time is preferred, as intelligence goodput is
primarily relevant for human-AI interactions and applications. End-to-end
latency is more informative than technical details such as CPU time.</p>
<p>To address the ambiguity in "intelligent information" in the
definition, a method for calculating intelligence goodput
are proposed. We divide the score achieved by AI on benchmark
datasets by the time used to produce the answer.
In this way, we delegate the challenging task of measuring intelligence to the existing benchmarks.
It can be formally expressed as follows.</p>
<p>Let $S = \{s_1, s_2, \dots, s_n\}$ be a set of scores from $n$ individual AI
benchmarks normalized to the same range, and $W = \{w_1, w_2, \dots, w_n\}$ be a
corresponding set of weights, where $w_i$ represents the relative importance of
each benchmark. The total time expended across all
assessments is denoted by $t$. Intelligence goodput $G$ can be defined
as:</p>
<p>$$
G = \frac{\sum_\limits{i=1}^{n} w_i s_i}{\sum_\limits{i=1}^{n} w_i \cdot t}
$$</p>
<h2>Experiments</h2>
<p>Several high-quality models from leading AI providers were evaluated. Source data, including benchmark scores, tokens per second (TPS), and total tokens generated while running the benchmarks, were collected from Artificial Analysis<sup id="cite-21-a"><a href="#ref-21" class="citation-link">21</a></sup>.</p>
<p>We aim to answer three questions:
* Do models use very different numbers of tokens to complete the same tasks?
* Can users roughly estimate per-task time from TPS?
* Does intelligence goodput (IG) provide a better estimate of per-task time?
To answer these questions, we conduct three measurements.</p>
<p>First, we compare the total tokens used by different models on the same benchmark suite. The results are shown in Figure 1. The bar length represents the number of tokens generated to complete the tasks by each model.</p>
<figure class="svg-figure"><?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="1008pt" height="576pt" viewBox="0 0 1008 576" xmlns="http://www.w3.org/2000/svg" version="1.1">
 <metadata>
  <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
   <cc:Work>
    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
    <dc:date>2025-11-04T02:21:29.215447</dc:date>
    <dc:format>image/svg+xml</dc:format>
    <dc:creator>
     <cc:Agent>
      <dc:title>Matplotlib v3.10.0, https://matplotlib.org/</dc:title>
     </cc:Agent>
    </dc:creator>
   </cc:Work>
  </rdf:RDF>
 </metadata>
 <defs>
  <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}
  g[id^="text_"] { fill: var(--text-color); }
  g[id^="text_"] use { fill: var(--text-color); }</style>
 </defs>
 <g id="svg_goodput_verbosity_1_figure_1">
  <g id="svg_goodput_verbosity_1_patch_1">
   <path d="M 0 576 
L 1008 576 
L 1008 0 
L 0 0 
L 0 576 
z
" style="fill: none; opacity: 0"/>
  </g>
  <g id="svg_goodput_verbosity_1_axes_1">
   <g id="svg_goodput_verbosity_1_patch_2">
    <path d="M 159.62 547.52 
L 987.1 547.52 
L 987.1 39.6 
L 159.62 39.6 
L 159.62 547.52 
z
" style="fill: none; opacity: 0"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_3">
    <path d="M 159.62 524.432727 
L 947.69619 524.432727 
L 947.69619 470.109733 
L 159.62 470.109733 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_4">
    <path d="M 159.62 456.528984 
L 814.189862 456.528984 
L 814.189862 402.205989 
L 159.62 402.205989 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_5">
    <path d="M 159.62 388.625241 
L 710.495627 388.625241 
L 710.495627 334.302246 
L 159.62 334.302246 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_6">
    <path d="M 159.62 320.721497 
L 619.76317 320.721497 
L 619.76317 266.398503 
L 159.62 266.398503 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_7">
    <path d="M 159.62 252.817754 
L 551.713828 252.817754 
L 551.713828 198.494759 
L 159.62 198.494759 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_8">
    <path d="M 159.62 184.914011 
L 450.611949 184.914011 
L 450.611949 130.591016 
L 159.62 130.591016 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_9">
    <path d="M 159.62 117.010267 
L 426.632657 117.010267 
L 426.632657 62.687273 
L 159.62 62.687273 
z
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: var(--gray-text-color)"/>
   </g>
   <g id="svg_goodput_verbosity_1_matplotlib.axis_1">
    <g id="svg_goodput_verbosity_1_xtick_1">
     <g id="svg_goodput_verbosity_1_line2d_1">
      <path d="M 159.62 547.52 
L 159.62 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_2"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_2">
     <g id="svg_goodput_verbosity_1_line2d_3">
      <path d="M 289.237794 547.52 
L 289.237794 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_4"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_3">
     <g id="svg_goodput_verbosity_1_line2d_5">
      <path d="M 418.855589 547.52 
L 418.855589 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_6"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_4">
     <g id="svg_goodput_verbosity_1_line2d_7">
      <path d="M 548.473383 547.52 
L 548.473383 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_8"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_5">
     <g id="svg_goodput_verbosity_1_line2d_9">
      <path d="M 678.091178 547.52 
L 678.091178 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_10"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_6">
     <g id="svg_goodput_verbosity_1_line2d_11">
      <path d="M 807.708972 547.52 
L 807.708972 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_12"/>
    </g>
    <g id="svg_goodput_verbosity_1_xtick_7">
     <g id="svg_goodput_verbosity_1_line2d_13">
      <path d="M 937.326767 547.52 
L 937.326767 39.6 
" clip-path="url(#svg_goodput_verbosity_1_pc99c5802a0)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_verbosity_1_line2d_14"/>
    </g>
    <g id="svg_goodput_verbosity_1_text_1">
     <!-- Tokens used to run benchmarks (Million Tokens) -->
     <g transform="translate(379.505937 562.232188) scale(0.14 -0.14)">
      <defs>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-54" d="M 147 4666 
L 3706 4666 
L 3706 4134 
L 2247 4134 
L 2247 0 
L 1613 0 
L 1613 4134 
L 147 4134 
L 147 4666 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-6f" d="M 1925 3097 
Q 1488 3097 1263 2756 
Q 1038 2416 1038 1747 
Q 1038 1081 1263 739 
Q 1488 397 1925 397 
Q 2366 397 2591 739 
Q 2816 1081 2816 1747 
Q 2816 2416 2591 2756 
Q 2366 3097 1925 3097 
z
M 1925 3584 
Q 2653 3584 3039 3112 
Q 3425 2641 3425 1747 
Q 3425 850 3040 379 
Q 2656 -91 1925 -91 
Q 1197 -91 812 379 
Q 428 850 428 1747 
Q 428 2641 812 3112 
Q 1197 3584 1925 3584 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-6b" d="M 738 4863 
L 1331 4863 
L 1331 2047 
L 2841 3500 
L 3541 3500 
L 2163 2181 
L 3756 0 
L 3053 0 
L 1759 1806 
L 1331 1403 
L 1331 0 
L 738 0 
L 738 4863 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-65" d="M 3475 1894 
L 3475 1613 
L 984 1613 
L 984 1594 
Q 984 1022 1282 709 
Q 1581 397 2125 397 
Q 2400 397 2700 484 
Q 3000 572 3341 750 
L 3341 178 
Q 3013 44 2708 -23 
Q 2403 -91 2119 -91 
Q 1303 -91 843 398 
Q 384 888 384 1747 
Q 384 2584 834 3084 
Q 1284 3584 2034 3584 
Q 2703 3584 3089 3131 
Q 3475 2678 3475 1894 
z
M 2900 2063 
Q 2888 2569 2661 2833 
Q 2434 3097 2009 3097 
Q 1594 3097 1325 2822 
Q 1056 2547 1006 2059 
L 2900 2063 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-6e" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 3500 
L 1184 3500 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-73" d="M 3041 3378 
L 3041 2816 
Q 2794 2959 2544 3031 
Q 2294 3103 2034 3103 
Q 1644 3103 1451 2976 
Q 1259 2850 1259 2591 
Q 1259 2356 1403 2240 
Q 1547 2125 2119 2016 
L 2350 1972 
Q 2778 1891 2998 1647 
Q 3219 1403 3219 1013 
Q 3219 494 2850 201 
Q 2481 -91 1825 -91 
Q 1566 -91 1281 -36 
Q 997 19 666 128 
L 666 722 
Q 988 556 1281 473 
Q 1575 391 1838 391 
Q 2219 391 2428 545 
Q 2638 700 2638 978 
Q 2638 1378 1872 1531 
L 1847 1538 
L 1631 1581 
Q 1134 1678 906 1908 
Q 678 2138 678 2534 
Q 678 3038 1018 3311 
Q 1359 3584 1991 3584 
Q 2272 3584 2531 3532 
Q 2791 3481 3041 3378 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-20" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-75" d="M 609 1325 
L 609 3494 
L 1184 3494 
L 1184 1325 
Q 1184 853 1351 631 
Q 1519 409 1869 409 
Q 2275 409 2490 695 
Q 2706 981 2706 1516 
L 2706 3494 
L 3284 3494 
L 3284 0 
L 2706 0 
L 2706 525 
Q 2553 222 2289 65 
Q 2025 -91 1672 -91 
Q 1134 -91 871 261 
Q 609 613 609 1325 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-64" d="M 2681 3053 
L 2681 4863 
L 3256 4863 
L 3256 0 
L 2681 0 
L 2681 441 
Q 2538 181 2298 45 
Q 2059 -91 1747 -91 
Q 1113 -91 748 401 
Q 384 894 384 1759 
Q 384 2613 750 3098 
Q 1116 3584 1747 3584 
Q 2063 3584 2303 3448 
Q 2544 3313 2681 3053 
z
M 991 1747 
Q 991 1078 1203 737 
Q 1416 397 1831 397 
Q 2247 397 2464 740 
Q 2681 1084 2681 1747 
Q 2681 2413 2464 2755 
Q 2247 3097 1831 3097 
Q 1416 3097 1203 2756 
Q 991 2416 991 1747 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-74" d="M 1919 4494 
L 1919 3500 
L 3225 3500 
L 3225 3053 
L 1919 3053 
L 1919 1153 
Q 1919 766 2066 612 
Q 2213 459 2578 459 
L 3225 459 
L 3225 0 
L 2522 0 
Q 1875 0 1609 259 
Q 1344 519 1344 1153 
L 1344 3053 
L 409 3053 
L 409 3500 
L 1344 3500 
L 1344 4494 
L 1919 4494 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-72" d="M 3609 2778 
Q 3425 2922 3234 2987 
Q 3044 3053 2816 3053 
Q 2278 3053 1993 2715 
Q 1709 2378 1709 1741 
L 1709 0 
L 1131 0 
L 1131 3500 
L 1709 3500 
L 1709 2816 
Q 1853 3188 2151 3386 
Q 2450 3584 2859 3584 
Q 3072 3584 3256 3531 
Q 3441 3478 3609 3366 
L 3609 2778 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-62" d="M 2869 1747 
Q 2869 2416 2656 2756 
Q 2444 3097 2028 3097 
Q 1609 3097 1393 2755 
Q 1178 2413 1178 1747 
Q 1178 1084 1393 740 
Q 1609 397 2028 397 
Q 2444 397 2656 737 
Q 2869 1078 2869 1747 
z
M 1178 3053 
Q 1316 3309 1558 3446 
Q 1800 3584 2119 3584 
Q 2750 3584 3112 3098 
Q 3475 2613 3475 1759 
Q 3475 894 3111 401 
Q 2747 -91 2113 -91 
Q 1800 -91 1561 45 
Q 1322 181 1178 441 
L 1178 0 
L 603 0 
L 603 4863 
L 1178 4863 
L 1178 3053 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-63" d="M 3316 178 
Q 3084 44 2839 -23 
Q 2594 -91 2338 -91 
Q 1525 -91 1067 396 
Q 609 884 609 1747 
Q 609 2609 1067 3096 
Q 1525 3584 2338 3584 
Q 2591 3584 2831 3518 
Q 3072 3453 3316 3316 
L 3316 2713 
Q 3088 2916 2858 3006 
Q 2628 3097 2338 3097 
Q 1797 3097 1506 2747 
Q 1216 2397 1216 1747 
Q 1216 1100 1508 748 
Q 1800 397 2338 397 
Q 2638 397 2875 489 
Q 3113 581 3316 775 
L 3316 178 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-68" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 4863 
L 1184 4863 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-6d" d="M 2113 3144 
Q 2219 3369 2383 3476 
Q 2547 3584 2778 3584 
Q 3200 3584 3373 3257 
Q 3547 2931 3547 2028 
L 3547 0 
L 3022 0 
L 3022 2003 
Q 3022 2744 2939 2923 
Q 2856 3103 2638 3103 
Q 2388 3103 2295 2911 
Q 2203 2719 2203 2003 
L 2203 0 
L 1678 0 
L 1678 2003 
Q 1678 2753 1589 2928 
Q 1500 3103 1269 3103 
Q 1041 3103 952 2911 
Q 863 2719 863 2003 
L 863 0 
L 341 0 
L 341 3500 
L 863 3500 
L 863 3200 
Q 966 3388 1120 3486 
Q 1275 3584 1472 3584 
Q 1709 3584 1867 3475 
Q 2025 3366 2113 3144 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-61" d="M 2194 1759 
L 2003 1759 
Q 1500 1759 1245 1582 
Q 991 1406 991 1056 
Q 991 741 1181 566 
Q 1372 391 1709 391 
Q 2184 391 2456 720 
Q 2728 1050 2731 1631 
L 2731 1759 
L 2194 1759 
z
M 3309 1997 
L 3309 0 
L 2731 0 
L 2731 519 
Q 2547 206 2267 57 
Q 1988 -91 1588 -91 
Q 1053 -91 734 211 
Q 416 513 416 1019 
Q 416 1603 808 1906 
Q 1200 2209 1959 2209 
L 2731 2209 
L 2731 2300 
Q 2728 2719 2518 2908 
Q 2309 3097 1850 3097 
Q 1556 3097 1256 3012 
Q 956 2928 672 2766 
L 672 3341 
Q 991 3463 1283 3523 
Q 1575 3584 1850 3584 
Q 2284 3584 2592 3456 
Q 2900 3328 3091 3072 
Q 3209 2916 3259 2686 
Q 3309 2456 3309 1997 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-28" d="M 2766 4856 
Q 2350 4144 2145 3436 
Q 1941 2728 1941 2009 
Q 1941 1294 2145 584 
Q 2350 -125 2766 -844 
L 2266 -844 
Q 1794 -100 1562 604 
Q 1331 1309 1331 2009 
Q 1331 2706 1562 3412 
Q 1794 4119 2266 4856 
L 2766 4856 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-4d" d="M 269 4666 
L 1113 4666 
L 1919 2291 
L 2731 4666 
L 3578 4666 
L 3578 0 
L 2994 0 
L 2994 4122 
L 2163 1663 
L 1684 1663 
L 850 4122 
L 850 0 
L 269 0 
L 269 4666 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-69" d="M 800 3500 
L 2272 3500 
L 2272 447 
L 3413 447 
L 3413 0 
L 556 0 
L 556 447 
L 1697 447 
L 1697 3053 
L 800 3053 
L 800 3500 
z
M 1697 4863 
L 2272 4863 
L 2272 4134 
L 1697 4134 
L 1697 4863 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-6c" d="M 1997 1269 
Q 1997 881 2139 684 
Q 2281 488 2559 488 
L 3231 488 
L 3231 0 
L 2503 0 
Q 1988 0 1705 331 
Q 1422 663 1422 1269 
L 1422 4447 
L 500 4447 
L 500 4897 
L 1997 4897 
L 1997 1269 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_verbosity_1_DejaVuSansMono-29" d="M 1088 4856 
L 1588 4856 
Q 2059 4119 2290 3412 
Q 2522 2706 2522 2009 
Q 2522 1306 2290 600 
Q 2059 -106 1588 -844 
L 1088 -844 
Q 1503 -119 1708 590 
Q 1913 1300 1913 2009 
Q 1913 2722 1708 3431 
Q 1503 4141 1088 4856 
z
" transform="scale(0.015625)"/>
      </defs>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-54"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6b" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(180.615234 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(301.025391 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-75" transform="translate(421.435547 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(481.640625 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(541.845703 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-64" transform="translate(602.050781 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(662.255859 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-74" transform="translate(722.460938 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(782.666016 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(842.871094 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-72" transform="translate(903.076172 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-75" transform="translate(963.28125 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(1023.486328 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(1083.691406 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-62" transform="translate(1143.896484 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(1204.101562 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(1264.306641 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-63" transform="translate(1324.511719 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-68" transform="translate(1384.716797 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6d" transform="translate(1444.921875 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-61" transform="translate(1505.126953 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-72" transform="translate(1565.332031 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6b" transform="translate(1625.537109 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(1685.742188 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(1745.947266 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-28" transform="translate(1806.152344 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-4d" transform="translate(1866.357422 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(1926.5625 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6c" transform="translate(1986.767578 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6c" transform="translate(2046.972656 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(2107.177734 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(2167.382812 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(2227.587891 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(2287.792969 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-54" transform="translate(2347.998047 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(2408.203125 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6b" transform="translate(2468.408203 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(2528.613281 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(2588.818359 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(2649.023438 0)"/>
      <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-29" transform="translate(2709.228516 0)"/>
     </g>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_matplotlib.axis_2">
    <g id="svg_goodput_verbosity_1_ytick_1">
     <g id="svg_goodput_verbosity_1_line2d_15"/>
     <g id="svg_goodput_verbosity_1_text_2">
      <!-- Grok 4 -->
      <g transform="translate(104.049375 502.627324) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-47" d="M 3450 384 
Q 3197 150 2880 29 
Q 2563 -91 2194 -91 
Q 1306 -91 812 545 
Q 319 1181 319 2328 
Q 319 3472 819 4111 
Q 1319 4750 2209 4750 
Q 2503 4750 2772 4667 
Q 3041 4584 3291 4416 
L 3291 3769 
Q 3038 4009 2772 4123 
Q 2506 4238 2209 4238 
Q 1594 4238 1286 3761 
Q 978 3284 978 2328 
Q 978 1356 1276 889 
Q 1575 422 2194 422 
Q 2403 422 2561 470 
Q 2719 519 2847 622 
L 2847 1875 
L 2169 1875 
L 2169 2394 
L 3450 2394 
L 3450 384 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-34" d="M 2297 4091 
L 825 1625 
L 2297 1625 
L 2297 4091 
z
M 2194 4666 
L 2925 4666 
L 2925 1625 
L 3547 1625 
L 3547 1113 
L 2925 1113 
L 2925 0 
L 2297 0 
L 2297 1113 
L 319 1113 
L 319 1709 
L 2194 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-72" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6b" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34" transform="translate(301.025391 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_2">
     <g id="svg_goodput_verbosity_1_line2d_16"/>
     <g id="svg_goodput_verbosity_1_text_3">
      <!-- Gemini 2.5 Pro -->
      <g transform="translate(36.621875 434.72358) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-32" d="M 1166 531 
L 3309 531 
L 3309 0 
L 475 0 
L 475 531 
Q 1059 1147 1496 1619 
Q 1934 2091 2100 2284 
Q 2413 2666 2522 2902 
Q 2631 3138 2631 3384 
Q 2631 3775 2401 3997 
Q 2172 4219 1772 4219 
Q 1488 4219 1175 4116 
Q 863 4013 513 3803 
L 513 4441 
Q 834 4594 1145 4672 
Q 1456 4750 1759 4750 
Q 2444 4750 2861 4386 
Q 3278 4022 3278 3431 
Q 3278 3131 3139 2831 
Q 3000 2531 2688 2169 
Q 2513 1966 2180 1606 
Q 1847 1247 1166 531 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-2e" d="M 1528 953 
L 2316 953 
L 2316 0 
L 1528 0 
L 1528 953 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-35" d="M 647 4666 
L 3009 4666 
L 3009 4134 
L 1222 4134 
L 1222 2988 
Q 1356 3038 1492 3061 
Q 1628 3084 1766 3084 
Q 2491 3084 2916 2656 
Q 3341 2228 3341 1497 
Q 3341 759 2895 334 
Q 2450 -91 1678 -91 
Q 1306 -91 998 -41 
Q 691 9 447 109 
L 447 750 
Q 734 594 1025 517 
Q 1316 441 1619 441 
Q 2141 441 2423 716 
Q 2706 991 2706 1497 
Q 2706 1997 2414 2275 
Q 2122 2553 1600 2553 
Q 1347 2553 1106 2495 
Q 866 2438 647 2322 
L 647 4666 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-50" d="M 1247 4147 
L 1247 2394 
L 1978 2394 
Q 2416 2394 2661 2625 
Q 2906 2856 2906 3272 
Q 2906 3688 2662 3917 
Q 2419 4147 1978 4147 
L 1247 4147 
z
M 616 4666 
L 1978 4666 
Q 2759 4666 3162 4311 
Q 3566 3956 3566 3272 
Q 3566 2581 3164 2228 
Q 2763 1875 1978 1875 
L 1247 1875 
L 1247 0 
L 616 0 
L 616 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-50" transform="translate(662.255859 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-72" transform="translate(722.460938 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(782.666016 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_3">
     <g id="svg_goodput_verbosity_1_line2d_17"/>
     <g id="svg_goodput_verbosity_1_text_4">
      <!-- GPT-5 High -->
      <g transform="translate(70.335625 366.78265) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-2d" d="M 1113 2009 
L 2741 2009 
L 2741 1497 
L 1113 1497 
L 1113 2009 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-48" d="M 428 4666 
L 1063 4666 
L 1063 2753 
L 2791 2753 
L 2791 4666 
L 3425 4666 
L 3425 0 
L 2791 0 
L 2791 2222 
L 1063 2222 
L 1063 0 
L 428 0 
L 428 4666 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-67" d="M 2681 1778 
Q 2681 2425 2470 2761 
Q 2259 3097 1856 3097 
Q 1434 3097 1212 2761 
Q 991 2425 991 1778 
Q 991 1131 1214 792 
Q 1438 453 1863 453 
Q 2259 453 2470 793 
Q 2681 1134 2681 1778 
z
M 3256 225 
Q 3256 -563 2884 -969 
Q 2513 -1375 1791 -1375 
Q 1553 -1375 1293 -1331 
Q 1034 -1288 775 -1203 
L 775 -634 
Q 1081 -778 1331 -847 
Q 1581 -916 1791 -916 
Q 2256 -916 2468 -662 
Q 2681 -409 2681 141 
L 2681 166 
L 2681 556 
Q 2544 263 2306 119 
Q 2069 -25 1728 -25 
Q 1116 -25 750 465 
Q 384 956 384 1778 
Q 384 2603 750 3093 
Q 1116 3584 1728 3584 
Q 2066 3584 2300 3450 
Q 2534 3316 2681 3034 
L 2681 3488 
L 3256 3488 
L 3256 225 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-48" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-67" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-68" transform="translate(541.845703 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_4">
     <g id="svg_goodput_verbosity_1_line2d_18"/>
     <g id="svg_goodput_verbosity_1_text_5">
      <!-- Gemini 2.5 Flash -->
      <g transform="translate(19.765 298.916094) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-46" d="M 728 4666 
L 3475 4666 
L 3475 4134 
L 1363 4134 
L 1363 2759 
L 3278 2759 
L 3278 2228 
L 1363 2228 
L 1363 0 
L 728 0 
L 728 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-46" transform="translate(662.255859 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6c" transform="translate(722.460938 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-61" transform="translate(782.666016 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(842.871094 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-68" transform="translate(903.076172 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_5">
     <g id="svg_goodput_verbosity_1_line2d_19"/>
     <g id="svg_goodput_verbosity_1_text_6">
      <!-- Grok 4 Fast -->
      <g transform="translate(61.907187 231.01235) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-72" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6b" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-46" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-61" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-73" transform="translate(541.845703 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-74" transform="translate(602.050781 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_6">
     <g id="svg_goodput_verbosity_1_line2d_20"/>
     <g id="svg_goodput_verbosity_1_text_7">
      <!-- GPT-5 Medium -->
      <g transform="translate(53.47875 163.108607) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-47"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-4d" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-64" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-69" transform="translate(541.845703 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-75" transform="translate(602.050781 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6d" transform="translate(662.255859 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_verbosity_1_ytick_7">
     <g id="svg_goodput_verbosity_1_line2d_21"/>
     <g id="svg_goodput_verbosity_1_text_8">
      <!-- Claude 4.5 Sonnet -->
      <g transform="translate(11.336562 95.204864) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-43" d="M 3353 166 
Q 3113 38 2859 -26 
Q 2606 -91 2322 -91 
Q 1425 -91 929 543 
Q 434 1178 434 2328 
Q 434 3472 932 4111 
Q 1431 4750 2322 4750 
Q 2606 4750 2859 4686 
Q 3113 4622 3353 4494 
L 3353 3847 
Q 3122 4038 2856 4138 
Q 2591 4238 2322 4238 
Q 1706 4238 1400 3763 
Q 1094 3288 1094 2328 
Q 1094 1372 1400 897 
Q 1706 422 2322 422 
Q 2597 422 2861 522 
Q 3125 622 3353 813 
L 3353 166 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_verbosity_1_DejaVuSansMono-53" d="M 3163 4506 
L 3163 3866 
Q 2875 4050 2586 4144 
Q 2297 4238 2003 4238 
Q 1556 4238 1297 4030 
Q 1038 3822 1038 3469 
Q 1038 3159 1208 2996 
Q 1378 2834 1844 2725 
L 2175 2650 
Q 2831 2497 3131 2169 
Q 3431 1841 3431 1275 
Q 3431 609 3018 259 
Q 2606 -91 1819 -91 
Q 1491 -91 1159 -20 
Q 828 50 494 191 
L 494 863 
Q 853 634 1173 528 
Q 1494 422 1819 422 
Q 2297 422 2562 636 
Q 2828 850 2828 1234 
Q 2828 1584 2645 1768 
Q 2463 1953 2009 2053 
L 1672 2131 
Q 1022 2278 728 2575 
Q 434 2872 434 3372 
Q 434 3997 854 4373 
Q 1275 4750 1972 4750 
Q 2241 4750 2537 4689 
Q 2834 4628 3163 4506 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-43"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6c" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-61" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-75" transform="translate(180.615234 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-64" transform="translate(240.820312 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(301.025391 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34" transform="translate(421.435547 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-53" transform="translate(662.255859 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6f" transform="translate(722.460938 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(782.666016 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-6e" transform="translate(842.871094 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-65" transform="translate(903.076172 0)"/>
       <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-74" transform="translate(963.28125 0)"/>
      </g>
     </g>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_patch_10">
    <path d="M 159.62 547.52 
L 159.62 39.6 
" style="fill: none; stroke: var(--text-color); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: square"/>
   </g>
   <g id="svg_goodput_verbosity_1_patch_11">
    <path d="M 159.62 547.52 
L 987.1 547.52 
" style="fill: none"/>
   </g>
   <g id="svg_goodput_verbosity_1_text_9">
    <!-- 121.6 -->
    <g style="fill: var(--text-color)" transform="translate(952.69619 501.171542) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-31" d="M 844 531 
L 1825 531 
L 1825 4097 
L 769 3859 
L 769 4434 
L 1819 4666 
L 2450 4666 
L 2450 531 
L 3419 531 
L 3419 0 
L 844 0 
L 844 531 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-36" d="M 3097 4563 
L 3097 3981 
Q 2900 4097 2678 4158 
Q 2456 4219 2216 4219 
Q 1616 4219 1306 3767 
Q 997 3316 997 2438 
Q 1147 2750 1412 2917 
Q 1678 3084 2022 3084 
Q 2697 3084 3067 2670 
Q 3438 2256 3438 1497 
Q 3438 741 3056 325 
Q 2675 -91 1984 -91 
Q 1172 -91 794 492 
Q 416 1075 416 2328 
Q 416 3509 870 4129 
Q 1325 4750 2188 4750 
Q 2419 4750 2650 4701 
Q 2881 4653 3097 4563 
z
M 1972 2591 
Q 1569 2591 1337 2300 
Q 1106 2009 1106 1497 
Q 1106 984 1337 693 
Q 1569 403 1972 403 
Q 2391 403 2603 679 
Q 2816 956 2816 1497 
Q 2816 2041 2603 2316 
Q 2391 2591 1972 2591 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-32" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-36" transform="translate(240.820312 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_10">
    <!-- 101.0 -->
    <g style="fill: var(--text-color)" transform="translate(819.189862 433.267799) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-30" d="M 1509 2344 
Q 1509 2516 1629 2641 
Q 1750 2766 1919 2766 
Q 2094 2766 2219 2641 
Q 2344 2516 2344 2344 
Q 2344 2169 2220 2047 
Q 2097 1925 1919 1925 
Q 1744 1925 1626 2044 
Q 1509 2163 1509 2344 
z
M 1925 4250 
Q 1484 4250 1267 3775 
Q 1050 3300 1050 2328 
Q 1050 1359 1267 884 
Q 1484 409 1925 409 
Q 2369 409 2586 884 
Q 2803 1359 2803 2328 
Q 2803 3300 2586 3775 
Q 2369 4250 1925 4250 
z
M 1925 4750 
Q 2672 4750 3055 4137 
Q 3438 3525 3438 2328 
Q 3438 1134 3055 521 
Q 2672 -91 1925 -91 
Q 1178 -91 797 521 
Q 416 1134 416 2328 
Q 416 3525 797 4137 
Q 1178 4750 1925 4750 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-30" transform="translate(240.820312 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_11">
    <!-- 85.0 -->
    <g style="fill: var(--text-color)" transform="translate(715.495627 365.364056) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-38" d="M 1925 2216 
Q 1503 2216 1273 1980 
Q 1044 1744 1044 1313 
Q 1044 881 1276 642 
Q 1509 403 1925 403 
Q 2350 403 2579 639 
Q 2809 875 2809 1313 
Q 2809 1741 2576 1978 
Q 2344 2216 1925 2216 
z
M 1375 2478 
Q 972 2581 745 2862 
Q 519 3144 519 3541 
Q 519 4097 897 4423 
Q 1275 4750 1925 4750 
Q 2578 4750 2956 4423 
Q 3334 4097 3334 3541 
Q 3334 3144 3107 2862 
Q 2881 2581 2478 2478 
Q 2947 2375 3195 2062 
Q 3444 1750 3444 1253 
Q 3444 622 3041 265 
Q 2638 -91 1925 -91 
Q 1213 -91 811 264 
Q 409 619 409 1247 
Q 409 1747 657 2061 
Q 906 2375 1375 2478 
z
M 1147 3481 
Q 1147 3106 1347 2909 
Q 1547 2713 1925 2713 
Q 2306 2713 2506 2909 
Q 2706 3106 2706 3481 
Q 2706 3863 2507 4063 
Q 2309 4263 1925 4263 
Q 1547 4263 1347 4061 
Q 1147 3859 1147 3481 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-38"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-30" transform="translate(180.615234 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_12">
    <!-- 71.0 -->
    <g style="fill: var(--text-color)" transform="translate(624.76317 297.460312) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-37" d="M 434 4666 
L 3372 4666 
L 3372 4397 
L 1703 0 
L 1044 0 
L 2669 4134 
L 434 4134 
L 434 4666 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-37"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-30" transform="translate(180.615234 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_13">
    <!-- 60.5 -->
    <g style="fill: var(--text-color)" transform="translate(556.713828 229.556569) scale(0.14 -0.14)">
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-36"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-35" transform="translate(180.615234 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_14">
    <!-- 44.9 -->
    <g style="fill: var(--text-color)" transform="translate(455.611949 161.652826) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_verbosity_1_DejaVuSansMono-39" d="M 1863 2069 
Q 2266 2069 2495 2359 
Q 2725 2650 2725 3163 
Q 2725 3675 2495 3965 
Q 2266 4256 1863 4256 
Q 1444 4256 1231 3979 
Q 1019 3703 1019 3163 
Q 1019 2619 1230 2344 
Q 1441 2069 1863 2069 
z
M 738 97 
L 738 678 
Q 934 563 1156 502 
Q 1378 441 1619 441 
Q 2219 441 2526 892 
Q 2834 1344 2834 2222 
Q 2688 1909 2422 1742 
Q 2156 1575 1813 1575 
Q 1138 1575 767 1990 
Q 397 2406 397 3169 
Q 397 3922 776 4336 
Q 1156 4750 1850 4750 
Q 2663 4750 3041 4165 
Q 3419 3581 3419 2328 
Q 3419 1150 2964 529 
Q 2509 -91 1644 -91 
Q 1416 -91 1184 -42 
Q 953 6 738 97 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-39" transform="translate(180.615234 0)"/>
    </g>
   </g>
   <g id="svg_goodput_verbosity_1_text_15">
    <!-- 41.2 -->
    <g style="fill: var(--text-color)" transform="translate(431.632657 93.749083) scale(0.14 -0.14)">
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-34"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-31" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-2e" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_verbosity_1_DejaVuSansMono-32" transform="translate(180.615234 0)"/>
    </g>
   </g>
  </g>
 </g>
 <defs>
  <clipPath id="svg_goodput_verbosity_1_pc99c5802a0">
   <rect x="159.62" y="39.6" width="827.48" height="507.92"/>
  </clipPath>
 </defs>
</svg></figure>
<p><strong>Figure 1</strong>: Model output tokens running benchmarks.</p>
<p>Token usage varies substantially across models. The highest (Grok 4) uses nearly three times the tokens of the lowest (Claude 4.5 Sonnet). Therefore, models can require very different token budgets for the same workload.</p>
<p>Second, we compare model speed measured in TPS with the actual wall-clock time to complete the same benchmark suite. The results are shown in Figure 2, where each point is a model; the x-axis is TPS, and the y-axis is days to finish all tasks.</p>
<figure class="svg-figure"><?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="1008pt" height="576pt" viewBox="0 0 1008 576" xmlns="http://www.w3.org/2000/svg" version="1.1">
 <metadata>
  <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
   <cc:Work>
    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
    <dc:date>2025-11-04T02:21:28.113354</dc:date>
    <dc:format>image/svg+xml</dc:format>
    <dc:creator>
     <cc:Agent>
      <dc:title>Matplotlib v3.10.0, https://matplotlib.org/</dc:title>
     </cc:Agent>
    </dc:creator>
   </cc:Work>
  </rdf:RDF>
 </metadata>
 <defs>
  <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}
  g[id^="text_"] { fill: var(--text-color); }
  g[id^="text_"] use { fill: var(--text-color); }</style>
 </defs>
 <g id="svg_goodput_speed_2_figure_1">
  <g id="svg_goodput_speed_2_patch_1">
   <path d="M 0 576 
L 1008 576 
L 1008 0 
L 0 0 
L 0 576 
z
" style="fill: none; opacity: 0"/>
  </g>
  <g id="svg_goodput_speed_2_axes_1">
   <g id="svg_goodput_speed_2_patch_2">
    <path d="M 50.4 523.84 
L 997.2 523.84 
L 997.2 40.24 
L 50.4 40.24 
L 50.4 523.84 
z
" style="fill: none; opacity: 0"/>
   </g>
   <g id="svg_goodput_speed_2_matplotlib.axis_1">
    <g id="svg_goodput_speed_2_xtick_1">
     <g id="svg_goodput_speed_2_line2d_1"/>
     <g id="svg_goodput_speed_2_text_1">
      <!-- 50 -->
      <g transform="translate(141.884178 544.552187) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_speed_2_DejaVuSansMono-35" d="M 647 4666 
L 3009 4666 
L 3009 4134 
L 1222 4134 
L 1222 2988 
Q 1356 3038 1492 3061 
Q 1628 3084 1766 3084 
Q 2491 3084 2916 2656 
Q 3341 2228 3341 1497 
Q 3341 759 2895 334 
Q 2450 -91 1678 -91 
Q 1306 -91 998 -41 
Q 691 9 447 109 
L 447 750 
Q 734 594 1025 517 
Q 1316 441 1619 441 
Q 2141 441 2423 716 
Q 2706 991 2706 1497 
Q 2706 1997 2414 2275 
Q 2122 2553 1600 2553 
Q 1347 2553 1106 2495 
Q 866 2438 647 2322 
L 647 4666 
z
" transform="scale(0.015625)"/>
        <path id="svg_goodput_speed_2_DejaVuSansMono-30" d="M 1509 2344 
Q 1509 2516 1629 2641 
Q 1750 2766 1919 2766 
Q 2094 2766 2219 2641 
Q 2344 2516 2344 2344 
Q 2344 2169 2220 2047 
Q 2097 1925 1919 1925 
Q 1744 1925 1626 2044 
Q 1509 2163 1509 2344 
z
M 1925 4250 
Q 1484 4250 1267 3775 
Q 1050 3300 1050 2328 
Q 1050 1359 1267 884 
Q 1484 409 1925 409 
Q 2369 409 2586 884 
Q 2803 1359 2803 2328 
Q 2803 3300 2586 3775 
Q 2369 4250 1925 4250 
z
M 1925 4750 
Q 2672 4750 3055 4137 
Q 3438 3525 3438 2328 
Q 3438 1134 3055 521 
Q 2672 -91 1925 -91 
Q 1178 -91 797 521 
Q 416 1134 416 2328 
Q 416 3525 797 4137 
Q 1178 4750 1925 4750 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_xtick_2">
     <g id="svg_goodput_speed_2_line2d_2"/>
     <g id="svg_goodput_speed_2_text_2">
      <!-- 100 -->
      <g transform="translate(327.257464 544.552187) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_speed_2_DejaVuSansMono-31" d="M 844 531 
L 1825 531 
L 1825 4097 
L 769 3859 
L 769 4434 
L 1819 4666 
L 2450 4666 
L 2450 531 
L 3419 531 
L 3419 0 
L 844 0 
L 844 531 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-31"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_xtick_3">
     <g id="svg_goodput_speed_2_line2d_3"/>
     <g id="svg_goodput_speed_2_text_3">
      <!-- 150 -->
      <g transform="translate(516.844969 544.552187) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-31"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_xtick_4">
     <g id="svg_goodput_speed_2_line2d_4"/>
     <g id="svg_goodput_speed_2_text_4">
      <!-- 200 -->
      <g transform="translate(706.432474 544.552187) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_speed_2_DejaVuSansMono-32" d="M 1166 531 
L 3309 531 
L 3309 0 
L 475 0 
L 475 531 
Q 1059 1147 1496 1619 
Q 1934 2091 2100 2284 
Q 2413 2666 2522 2902 
Q 2631 3138 2631 3384 
Q 2631 3775 2401 3997 
Q 2172 4219 1772 4219 
Q 1488 4219 1175 4116 
Q 863 4013 513 3803 
L 513 4441 
Q 834 4594 1145 4672 
Q 1456 4750 1759 4750 
Q 2444 4750 2861 4386 
Q 3278 4022 3278 3431 
Q 3278 3131 3139 2831 
Q 3000 2531 2688 2169 
Q 2513 1966 2180 1606 
Q 1847 1247 1166 531 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_xtick_5">
     <g id="svg_goodput_speed_2_line2d_5"/>
     <g id="svg_goodput_speed_2_text_5">
      <!-- 250 -->
      <g transform="translate(896.019979 544.552187) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_text_6">
     <!-- Generation speed (tokens/s) -->
     <g transform="translate(410.016094 562.175937) scale(0.14 -0.14)">
      <defs>
       <path id="svg_goodput_speed_2_DejaVuSansMono-47" d="M 3450 384 
Q 3197 150 2880 29 
Q 2563 -91 2194 -91 
Q 1306 -91 812 545 
Q 319 1181 319 2328 
Q 319 3472 819 4111 
Q 1319 4750 2209 4750 
Q 2503 4750 2772 4667 
Q 3041 4584 3291 4416 
L 3291 3769 
Q 3038 4009 2772 4123 
Q 2506 4238 2209 4238 
Q 1594 4238 1286 3761 
Q 978 3284 978 2328 
Q 978 1356 1276 889 
Q 1575 422 2194 422 
Q 2403 422 2561 470 
Q 2719 519 2847 622 
L 2847 1875 
L 2169 1875 
L 2169 2394 
L 3450 2394 
L 3450 384 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-65" d="M 3475 1894 
L 3475 1613 
L 984 1613 
L 984 1594 
Q 984 1022 1282 709 
Q 1581 397 2125 397 
Q 2400 397 2700 484 
Q 3000 572 3341 750 
L 3341 178 
Q 3013 44 2708 -23 
Q 2403 -91 2119 -91 
Q 1303 -91 843 398 
Q 384 888 384 1747 
Q 384 2584 834 3084 
Q 1284 3584 2034 3584 
Q 2703 3584 3089 3131 
Q 3475 2678 3475 1894 
z
M 2900 2063 
Q 2888 2569 2661 2833 
Q 2434 3097 2009 3097 
Q 1594 3097 1325 2822 
Q 1056 2547 1006 2059 
L 2900 2063 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-6e" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 3500 
L 1184 3500 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-72" d="M 3609 2778 
Q 3425 2922 3234 2987 
Q 3044 3053 2816 3053 
Q 2278 3053 1993 2715 
Q 1709 2378 1709 1741 
L 1709 0 
L 1131 0 
L 1131 3500 
L 1709 3500 
L 1709 2816 
Q 1853 3188 2151 3386 
Q 2450 3584 2859 3584 
Q 3072 3584 3256 3531 
Q 3441 3478 3609 3366 
L 3609 2778 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-61" d="M 2194 1759 
L 2003 1759 
Q 1500 1759 1245 1582 
Q 991 1406 991 1056 
Q 991 741 1181 566 
Q 1372 391 1709 391 
Q 2184 391 2456 720 
Q 2728 1050 2731 1631 
L 2731 1759 
L 2194 1759 
z
M 3309 1997 
L 3309 0 
L 2731 0 
L 2731 519 
Q 2547 206 2267 57 
Q 1988 -91 1588 -91 
Q 1053 -91 734 211 
Q 416 513 416 1019 
Q 416 1603 808 1906 
Q 1200 2209 1959 2209 
L 2731 2209 
L 2731 2300 
Q 2728 2719 2518 2908 
Q 2309 3097 1850 3097 
Q 1556 3097 1256 3012 
Q 956 2928 672 2766 
L 672 3341 
Q 991 3463 1283 3523 
Q 1575 3584 1850 3584 
Q 2284 3584 2592 3456 
Q 2900 3328 3091 3072 
Q 3209 2916 3259 2686 
Q 3309 2456 3309 1997 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-74" d="M 1919 4494 
L 1919 3500 
L 3225 3500 
L 3225 3053 
L 1919 3053 
L 1919 1153 
Q 1919 766 2066 612 
Q 2213 459 2578 459 
L 3225 459 
L 3225 0 
L 2522 0 
Q 1875 0 1609 259 
Q 1344 519 1344 1153 
L 1344 3053 
L 409 3053 
L 409 3500 
L 1344 3500 
L 1344 4494 
L 1919 4494 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-69" d="M 800 3500 
L 2272 3500 
L 2272 447 
L 3413 447 
L 3413 0 
L 556 0 
L 556 447 
L 1697 447 
L 1697 3053 
L 800 3053 
L 800 3500 
z
M 1697 4863 
L 2272 4863 
L 2272 4134 
L 1697 4134 
L 1697 4863 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-6f" d="M 1925 3097 
Q 1488 3097 1263 2756 
Q 1038 2416 1038 1747 
Q 1038 1081 1263 739 
Q 1488 397 1925 397 
Q 2366 397 2591 739 
Q 2816 1081 2816 1747 
Q 2816 2416 2591 2756 
Q 2366 3097 1925 3097 
z
M 1925 3584 
Q 2653 3584 3039 3112 
Q 3425 2641 3425 1747 
Q 3425 850 3040 379 
Q 2656 -91 1925 -91 
Q 1197 -91 812 379 
Q 428 850 428 1747 
Q 428 2641 812 3112 
Q 1197 3584 1925 3584 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-20" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-73" d="M 3041 3378 
L 3041 2816 
Q 2794 2959 2544 3031 
Q 2294 3103 2034 3103 
Q 1644 3103 1451 2976 
Q 1259 2850 1259 2591 
Q 1259 2356 1403 2240 
Q 1547 2125 2119 2016 
L 2350 1972 
Q 2778 1891 2998 1647 
Q 3219 1403 3219 1013 
Q 3219 494 2850 201 
Q 2481 -91 1825 -91 
Q 1566 -91 1281 -36 
Q 997 19 666 128 
L 666 722 
Q 988 556 1281 473 
Q 1575 391 1838 391 
Q 2219 391 2428 545 
Q 2638 700 2638 978 
Q 2638 1378 1872 1531 
L 1847 1538 
L 1631 1581 
Q 1134 1678 906 1908 
Q 678 2138 678 2534 
Q 678 3038 1018 3311 
Q 1359 3584 1991 3584 
Q 2272 3584 2531 3532 
Q 2791 3481 3041 3378 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-70" d="M 1172 441 
L 1172 -1331 
L 594 -1331 
L 594 3500 
L 1172 3500 
L 1172 3053 
Q 1316 3313 1555 3448 
Q 1794 3584 2106 3584 
Q 2741 3584 3102 3093 
Q 3463 2603 3463 1734 
Q 3463 881 3100 395 
Q 2738 -91 2106 -91 
Q 1788 -91 1548 45 
Q 1309 181 1172 441 
z
M 2859 1747 
Q 2859 2416 2648 2756 
Q 2438 3097 2022 3097 
Q 1603 3097 1387 2755 
Q 1172 2413 1172 1747 
Q 1172 1084 1387 740 
Q 1603 397 2022 397 
Q 2438 397 2648 737 
Q 2859 1078 2859 1747 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-64" d="M 2681 3053 
L 2681 4863 
L 3256 4863 
L 3256 0 
L 2681 0 
L 2681 441 
Q 2538 181 2298 45 
Q 2059 -91 1747 -91 
Q 1113 -91 748 401 
Q 384 894 384 1759 
Q 384 2613 750 3098 
Q 1116 3584 1747 3584 
Q 2063 3584 2303 3448 
Q 2544 3313 2681 3053 
z
M 991 1747 
Q 991 1078 1203 737 
Q 1416 397 1831 397 
Q 2247 397 2464 740 
Q 2681 1084 2681 1747 
Q 2681 2413 2464 2755 
Q 2247 3097 1831 3097 
Q 1416 3097 1203 2756 
Q 991 2416 991 1747 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-28" d="M 2766 4856 
Q 2350 4144 2145 3436 
Q 1941 2728 1941 2009 
Q 1941 1294 2145 584 
Q 2350 -125 2766 -844 
L 2266 -844 
Q 1794 -100 1562 604 
Q 1331 1309 1331 2009 
Q 1331 2706 1562 3412 
Q 1794 4119 2266 4856 
L 2766 4856 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-6b" d="M 738 4863 
L 1331 4863 
L 1331 2047 
L 2841 3500 
L 3541 3500 
L 2163 2181 
L 3756 0 
L 3053 0 
L 1759 1806 
L 1331 1403 
L 1331 0 
L 738 0 
L 738 4863 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-2f" d="M 2778 4666 
L 3372 4666 
L 916 -594 
L 319 -594 
L 2778 4666 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-29" d="M 1088 4856 
L 1588 4856 
Q 2059 4119 2290 3412 
Q 2522 2706 2522 2009 
Q 2522 1306 2290 600 
Q 2059 -106 1588 -844 
L 1088 -844 
Q 1503 -119 1708 590 
Q 1913 1300 1913 2009 
Q 1913 2722 1708 3431 
Q 1503 4141 1088 4856 
z
" transform="scale(0.015625)"/>
      </defs>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(180.615234 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-72" transform="translate(240.820312 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(301.025391 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-74" transform="translate(361.230469 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(421.435547 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(481.640625 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(541.845703 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(662.255859 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-70" transform="translate(722.460938 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(782.666016 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(842.871094 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-64" transform="translate(903.076172 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(963.28125 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-28" transform="translate(1023.486328 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-74" transform="translate(1083.691406 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(1143.896484 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6b" transform="translate(1204.101562 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(1264.306641 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(1324.511719 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(1384.716797 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2f" transform="translate(1444.921875 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(1505.126953 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-29" transform="translate(1565.332031 0)"/>
     </g>
    </g>
   </g>
   <g id="svg_goodput_speed_2_matplotlib.axis_2">
    <g id="svg_goodput_speed_2_ytick_1">
     <g id="svg_goodput_speed_2_line2d_6">
      <path d="M 50.4 475.173395 
L 997.2 475.173395 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_7"/>
     <g id="svg_goodput_speed_2_text_7">
      <!-- 5 -->
      <g transform="translate(36.971563 480.529489) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_2">
     <g id="svg_goodput_speed_2_line2d_8">
      <path d="M 50.4 416.534892 
L 997.2 416.534892 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_9"/>
     <g id="svg_goodput_speed_2_text_8">
      <!-- 10 -->
      <g transform="translate(28.543125 421.890986) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-31"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_3">
     <g id="svg_goodput_speed_2_line2d_10">
      <path d="M 50.4 357.896388 
L 997.2 357.896388 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_11"/>
     <g id="svg_goodput_speed_2_text_9">
      <!-- 15 -->
      <g transform="translate(28.543125 363.252482) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-31"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_4">
     <g id="svg_goodput_speed_2_line2d_12">
      <path d="M 50.4 299.257885 
L 997.2 299.257885 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_13"/>
     <g id="svg_goodput_speed_2_text_10">
      <!-- 20 -->
      <g transform="translate(28.543125 304.613979) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_5">
     <g id="svg_goodput_speed_2_line2d_14">
      <path d="M 50.4 240.619382 
L 997.2 240.619382 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_15"/>
     <g id="svg_goodput_speed_2_text_11">
      <!-- 25 -->
      <g transform="translate(28.543125 245.975475) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_6">
     <g id="svg_goodput_speed_2_line2d_16">
      <path d="M 50.4 181.980878 
L 997.2 181.980878 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_17"/>
     <g id="svg_goodput_speed_2_text_12">
      <!-- 30 -->
      <g transform="translate(28.543125 187.336972) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_speed_2_DejaVuSansMono-33" d="M 2425 2497 
Q 2884 2375 3128 2064 
Q 3372 1753 3372 1288 
Q 3372 644 2939 276 
Q 2506 -91 1741 -91 
Q 1419 -91 1084 -31 
Q 750 28 428 141 
L 428 769 
Q 747 603 1056 522 
Q 1366 441 1672 441 
Q 2191 441 2469 675 
Q 2747 909 2747 1350 
Q 2747 1756 2469 1995 
Q 2191 2234 1716 2234 
L 1234 2234 
L 1234 2753 
L 1716 2753 
Q 2150 2753 2394 2943 
Q 2638 3134 2638 3475 
Q 2638 3834 2411 4026 
Q 2184 4219 1766 4219 
Q 1488 4219 1191 4156 
Q 894 4094 569 3969 
L 569 4550 
Q 947 4650 1242 4700 
Q 1538 4750 1766 4750 
Q 2447 4750 2855 4408 
Q 3263 4066 3263 3500 
Q 3263 3116 3048 2859 
Q 2834 2603 2425 2497 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-33"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_7">
     <g id="svg_goodput_speed_2_line2d_18">
      <path d="M 50.4 123.342375 
L 997.2 123.342375 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_19"/>
     <g id="svg_goodput_speed_2_text_13">
      <!-- 35 -->
      <g transform="translate(28.543125 128.698468) scale(0.14 -0.14)">
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-33"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_ytick_8">
     <g id="svg_goodput_speed_2_line2d_20">
      <path d="M 50.4 64.703871 
L 997.2 64.703871 
" clip-path="url(#svg_goodput_speed_2_p6760884bbf)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_goodput_speed_2_line2d_21"/>
     <g id="svg_goodput_speed_2_text_14">
      <!-- 40 -->
      <g transform="translate(28.543125 70.059965) scale(0.14 -0.14)">
       <defs>
        <path id="svg_goodput_speed_2_DejaVuSansMono-34" d="M 2297 4091 
L 825 1625 
L 2297 1625 
L 2297 4091 
z
M 2194 4666 
L 2925 4666 
L 2925 1625 
L 3547 1625 
L 3547 1113 
L 2925 1113 
L 2925 0 
L 2297 0 
L 2297 1113 
L 319 1113 
L 319 1709 
L 2194 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-34"/>
       <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_goodput_speed_2_text_15">
     <!-- Time to finish all benchmarks (days) -->
     <g transform="translate(21.631563 433.751875) rotate(-90) scale(0.14 -0.14)">
      <defs>
       <path id="svg_goodput_speed_2_DejaVuSansMono-54" d="M 147 4666 
L 3706 4666 
L 3706 4134 
L 2247 4134 
L 2247 0 
L 1613 0 
L 1613 4134 
L 147 4134 
L 147 4666 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-6d" d="M 2113 3144 
Q 2219 3369 2383 3476 
Q 2547 3584 2778 3584 
Q 3200 3584 3373 3257 
Q 3547 2931 3547 2028 
L 3547 0 
L 3022 0 
L 3022 2003 
Q 3022 2744 2939 2923 
Q 2856 3103 2638 3103 
Q 2388 3103 2295 2911 
Q 2203 2719 2203 2003 
L 2203 0 
L 1678 0 
L 1678 2003 
Q 1678 2753 1589 2928 
Q 1500 3103 1269 3103 
Q 1041 3103 952 2911 
Q 863 2719 863 2003 
L 863 0 
L 341 0 
L 341 3500 
L 863 3500 
L 863 3200 
Q 966 3388 1120 3486 
Q 1275 3584 1472 3584 
Q 1709 3584 1867 3475 
Q 2025 3366 2113 3144 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-66" d="M 3322 4863 
L 3322 4384 
L 2669 4384 
Q 2359 4384 2239 4257 
Q 2119 4131 2119 3809 
L 2119 3500 
L 3322 3500 
L 3322 3053 
L 2119 3053 
L 2119 0 
L 1544 0 
L 1544 3053 
L 609 3053 
L 609 3500 
L 1544 3500 
L 1544 3744 
Q 1544 4319 1808 4591 
Q 2072 4863 2631 4863 
L 3322 4863 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-68" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 4863 
L 1184 4863 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-6c" d="M 1997 1269 
Q 1997 881 2139 684 
Q 2281 488 2559 488 
L 3231 488 
L 3231 0 
L 2503 0 
Q 1988 0 1705 331 
Q 1422 663 1422 1269 
L 1422 4447 
L 500 4447 
L 500 4897 
L 1997 4897 
L 1997 1269 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-62" d="M 2869 1747 
Q 2869 2416 2656 2756 
Q 2444 3097 2028 3097 
Q 1609 3097 1393 2755 
Q 1178 2413 1178 1747 
Q 1178 1084 1393 740 
Q 1609 397 2028 397 
Q 2444 397 2656 737 
Q 2869 1078 2869 1747 
z
M 1178 3053 
Q 1316 3309 1558 3446 
Q 1800 3584 2119 3584 
Q 2750 3584 3112 3098 
Q 3475 2613 3475 1759 
Q 3475 894 3111 401 
Q 2747 -91 2113 -91 
Q 1800 -91 1561 45 
Q 1322 181 1178 441 
L 1178 0 
L 603 0 
L 603 4863 
L 1178 4863 
L 1178 3053 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-63" d="M 3316 178 
Q 3084 44 2839 -23 
Q 2594 -91 2338 -91 
Q 1525 -91 1067 396 
Q 609 884 609 1747 
Q 609 2609 1067 3096 
Q 1525 3584 2338 3584 
Q 2591 3584 2831 3518 
Q 3072 3453 3316 3316 
L 3316 2713 
Q 3088 2916 2858 3006 
Q 2628 3097 2338 3097 
Q 1797 3097 1506 2747 
Q 1216 2397 1216 1747 
Q 1216 1100 1508 748 
Q 1800 397 2338 397 
Q 2638 397 2875 489 
Q 3113 581 3316 775 
L 3316 178 
z
" transform="scale(0.015625)"/>
       <path id="svg_goodput_speed_2_DejaVuSansMono-79" d="M 2681 1125 
Q 2538 759 2316 163 
Q 2006 -663 1900 -844 
Q 1756 -1088 1540 -1209 
Q 1325 -1331 1038 -1331 
L 575 -1331 
L 575 -850 
L 916 -850 
Q 1169 -850 1312 -703 
Q 1456 -556 1678 56 
L 325 3500 
L 934 3500 
L 1972 763 
L 2994 3500 
L 3603 3500 
L 2681 1125 
z
" transform="scale(0.015625)"/>
      </defs>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-54"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(180.615234 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(240.820312 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-74" transform="translate(301.025391 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(361.230469 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(421.435547 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-66" transform="translate(481.640625 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(541.845703 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(602.050781 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(662.255859 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(722.460938 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-68" transform="translate(782.666016 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(842.871094 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(903.076172 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6c" transform="translate(963.28125 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6c" transform="translate(1023.486328 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(1083.691406 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-62" transform="translate(1143.896484 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(1204.101562 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(1264.306641 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-63" transform="translate(1324.511719 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-68" transform="translate(1384.716797 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6d" transform="translate(1444.921875 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(1505.126953 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-72" transform="translate(1565.332031 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6b" transform="translate(1625.537109 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(1685.742188 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(1745.947266 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-28" transform="translate(1806.152344 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-64" transform="translate(1866.357422 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(1926.5625 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-79" transform="translate(1986.767578 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(2046.972656 0)"/>
      <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-29" transform="translate(2107.177734 0)"/>
     </g>
    </g>
   </g>
   <g id="svg_goodput_speed_2_patch_3">
    <path d="M 50.4 523.84 
L 50.4 40.24 
" style="fill: none; stroke: var(--text-color); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: square"/>
   </g>
   <g id="svg_goodput_speed_2_patch_4">
    <path d="M 50.4 523.84 
L 997.2 523.84 
" style="fill: none; stroke: var(--text-color); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: square"/>
   </g>
   <g id="svg_goodput_speed_2_text_16">
    <!-- Grok 4 Fast -->
    <g style="fill: var(--text-color)" transform="translate(832.492073 511.858182) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_speed_2_DejaVuSansMono-46" d="M 728 4666 
L 3475 4666 
L 3475 4134 
L 1363 4134 
L 1363 2759 
L 3278 2759 
L 3278 2228 
L 1363 2228 
L 1363 0 
L 728 0 
L 728 4666 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-72" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6b" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-34" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-46" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-74" transform="translate(602.050781 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_17">
    <!-- Gemini 2.5 Flash -->
    <g style="fill: var(--text-color)" transform="translate(809.308636 487.028133) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_speed_2_DejaVuSansMono-2e" d="M 1528 953 
L 2316 953 
L 2316 0 
L 1528 0 
L 1528 953 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-46" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6c" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(782.666016 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-73" transform="translate(842.871094 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-68" transform="translate(903.076172 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_18">
    <!-- GPT-5 Medium -->
    <g style="fill: var(--text-color)" transform="translate(493.986624 479.648104) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_speed_2_DejaVuSansMono-50" d="M 1247 4147 
L 1247 2394 
L 1978 2394 
Q 2416 2394 2661 2625 
Q 2906 2856 2906 3272 
Q 2906 3688 2662 3917 
Q 2419 4147 1978 4147 
L 1247 4147 
z
M 616 4666 
L 1978 4666 
Q 2759 4666 3162 4311 
Q 3566 3956 3566 3272 
Q 3566 2581 3164 2228 
Q 2763 1875 1978 1875 
L 1247 1875 
L 1247 0 
L 616 0 
L 616 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_speed_2_DejaVuSansMono-2d" d="M 1113 2009 
L 2741 2009 
L 2741 1497 
L 1113 1497 
L 1113 2009 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_speed_2_DejaVuSansMono-4d" d="M 269 4666 
L 1113 4666 
L 1919 2291 
L 2731 4666 
L 3578 4666 
L 3578 0 
L 2994 0 
L 2994 4122 
L 2163 1663 
L 1684 1663 
L 850 4122 
L 850 0 
L 269 0 
L 269 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_speed_2_DejaVuSansMono-75" d="M 609 1325 
L 609 3494 
L 1184 3494 
L 1184 1325 
Q 1184 853 1351 631 
Q 1519 409 1869 409 
Q 2275 409 2490 695 
Q 2706 981 2706 1516 
L 2706 3494 
L 3284 3494 
L 3284 0 
L 2706 0 
L 2706 525 
Q 2553 222 2289 65 
Q 2025 -91 1672 -91 
Q 1134 -91 871 261 
Q 609 613 609 1325 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-4d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-64" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-75" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6d" transform="translate(662.255859 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_19">
    <!-- Gemini 2.5 Pro -->
    <g style="fill: var(--text-color)" transform="translate(562.238126 435.930713) scale(0.14 -0.14)">
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-50" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-72" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(782.666016 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_20">
    <!-- GPT-5 High -->
    <g style="fill: var(--text-color)" transform="translate(444.693873 431.510551) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_speed_2_DejaVuSansMono-48" d="M 428 4666 
L 1063 4666 
L 1063 2753 
L 2791 2753 
L 2791 4666 
L 3425 4666 
L 3425 0 
L 2791 0 
L 2791 2222 
L 1063 2222 
L 1063 0 
L 428 0 
L 428 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_speed_2_DejaVuSansMono-67" d="M 2681 1778 
Q 2681 2425 2470 2761 
Q 2259 3097 1856 3097 
Q 1434 3097 1212 2761 
Q 991 2425 991 1778 
Q 991 1131 1214 792 
Q 1438 453 1863 453 
Q 2259 453 2470 793 
Q 2681 1134 2681 1778 
z
M 3256 225 
Q 3256 -563 2884 -969 
Q 2513 -1375 1791 -1375 
Q 1553 -1375 1293 -1331 
Q 1034 -1288 775 -1203 
L 775 -634 
Q 1081 -778 1331 -847 
Q 1581 -916 1791 -916 
Q 2256 -916 2468 -662 
Q 2681 -409 2681 141 
L 2681 166 
L 2681 556 
Q 2544 263 2306 119 
Q 2069 -25 1728 -25 
Q 1116 -25 750 465 
Q 384 956 384 1778 
Q 384 2603 750 3093 
Q 1116 3584 1728 3584 
Q 2066 3584 2300 3450 
Q 2534 3316 2681 3034 
L 2681 3488 
L 3256 3488 
L 3256 225 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-48" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-69" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-67" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-68" transform="translate(541.845703 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_21">
    <!-- Claude 4.5 Sonnet -->
    <g style="fill: var(--text-color)" transform="translate(198.230116 430.605635) scale(0.14 -0.14)">
     <defs>
      <path id="svg_goodput_speed_2_DejaVuSansMono-43" d="M 3353 166 
Q 3113 38 2859 -26 
Q 2606 -91 2322 -91 
Q 1425 -91 929 543 
Q 434 1178 434 2328 
Q 434 3472 932 4111 
Q 1431 4750 2322 4750 
Q 2606 4750 2859 4686 
Q 3113 4622 3353 4494 
L 3353 3847 
Q 3122 4038 2856 4138 
Q 2591 4238 2322 4238 
Q 1706 4238 1400 3763 
Q 1094 3288 1094 2328 
Q 1094 1372 1400 897 
Q 1706 422 2322 422 
Q 2597 422 2861 522 
Q 3125 622 3353 813 
L 3353 166 
z
" transform="scale(0.015625)"/>
      <path id="svg_goodput_speed_2_DejaVuSansMono-53" d="M 3163 4506 
L 3163 3866 
Q 2875 4050 2586 4144 
Q 2297 4238 2003 4238 
Q 1556 4238 1297 4030 
Q 1038 3822 1038 3469 
Q 1038 3159 1208 2996 
Q 1378 2834 1844 2725 
L 2175 2650 
Q 2831 2497 3131 2169 
Q 3431 1841 3431 1275 
Q 3431 609 3018 259 
Q 2606 -91 1819 -91 
Q 1491 -91 1159 -20 
Q 828 50 494 191 
L 494 863 
Q 853 634 1173 528 
Q 1494 422 1819 422 
Q 2297 422 2562 636 
Q 2828 850 2828 1234 
Q 2828 1584 2645 1768 
Q 2463 1953 2009 2053 
L 1672 2131 
Q 1022 2278 728 2575 
Q 434 2872 434 3372 
Q 434 3997 854 4373 
Q 1275 4750 1972 4750 
Q 2241 4750 2537 4689 
Q 2834 4628 3163 4506 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-43"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6c" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-61" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-75" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-64" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-34" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-53" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(782.666016 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6e" transform="translate(842.871094 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-65" transform="translate(903.076172 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-74" transform="translate(963.28125 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_text_22">
    <!-- Grok 4 -->
    <g style="fill: var(--text-color)" transform="translate(103.436364 52.221818) scale(0.14 -0.14)">
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-47"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-72" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-6b" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-20" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_goodput_speed_2_DejaVuSansMono-34" transform="translate(301.025391 0)"/>
    </g>
   </g>
   <g id="svg_goodput_speed_2_PathCollection_1">
    <defs>
     <path id="svg_goodput_speed_2_m037f29e639" d="M 0 5 
C 1.326016 5 2.597899 4.473168 3.535534 3.535534 
C 4.473168 2.597899 5 1.326016 5 0 
C 5 -1.326016 4.473168 -2.597899 3.535534 -3.535534 
C 2.597899 -4.473168 1.326016 -5 0 -5 
C -1.326016 -5 -2.597899 -4.473168 -3.535534 -3.535534 
C -4.473168 -2.597899 -5 -1.326016 -5 0 
C -5 1.326016 -4.473168 2.597899 -3.535534 3.535534 
C -2.597899 4.473168 -1.326016 5 0 5 
z
" style="stroke: var(--gray-text-color)"/>
    </defs>
    <g clip-path="url(#svg_goodput_speed_2_p6760884bbf)">
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="935.204886" y="501.858182" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="954.163636" y="497.028133" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="483.986624" y="489.648104" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="552.238126" y="445.930713" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="434.693873" y="441.510551" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="188.230116" y="440.605635" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_goodput_speed_2_m037f29e639" x="93.436364" y="62.221818" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
    </g>
   </g>
  </g>
 </g>
 <defs>
  <clipPath id="svg_goodput_speed_2_p6760884bbf">
   <rect x="50.4" y="40.24" width="946.8" height="483.6"/>
  </clipPath>
 </defs>
</svg></figure>
<p><strong>Figure 2</strong>: Total time running all benchmarks vs. TPS.</p>
<p>From Figure 2, we observe weak correlation between TPS and total time. Therefore, TPS alone is an unreliable predictor of per-task or per-suite wall-clock time.</p>
<p>Third, we compute intelligence goodput for each model and analyze the results. The experimental results are shown in Table 2, which reports the intelligence index (I; normalized and averaged benchmark scores), Time (days to run all benchmarks), and intelligence goodput (IG). For details on the methodology for computing the intelligence index, please visit Artificial Analysis<sup id="cite-21-b"><a href="#ref-21" class="citation-link">21</a></sup>.</p>
<p><strong>Table 2.</strong> Intelligence Goodput Results</p>
<table>
<thead>
<tr>
<th style="text-align: left;">Models</th>
<th style="text-align: right;">I</th>
<th style="text-align: right;">Time</th>
<th style="text-align: right;">IG</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Grok 4 Fast</td>
<td style="text-align: right;">60</td>
<td style="text-align: right;">2.7</td>
<td style="text-align: right;">254.88</td>
</tr>
<tr>
<td style="text-align: left;">GPT-5 Medium</td>
<td style="text-align: right;">66</td>
<td style="text-align: right;">3.8</td>
<td style="text-align: right;">202.85</td>
</tr>
<tr>
<td style="text-align: left;">Gemini 2.5 Flash</td>
<td style="text-align: right;">54</td>
<td style="text-align: right;">3.1</td>
<td style="text-align: right;">199.27</td>
</tr>
<tr>
<td style="text-align: left;">GPT-5 High</td>
<td style="text-align: right;">68</td>
<td style="text-align: right;">7.9</td>
<td style="text-align: right;">100.00</td>
</tr>
<tr>
<td style="text-align: left;">Gemini 2.5 Pro</td>
<td style="text-align: right;">60</td>
<td style="text-align: right;">7.5</td>
<td style="text-align: right;">92.67</td>
</tr>
<tr>
<td style="text-align: left;">Claude 4.5 Sonnet</td>
<td style="text-align: right;">63</td>
<td style="text-align: right;">7.9</td>
<td style="text-align: right;">91.75</td>
</tr>
<tr>
<td style="text-align: left;">Grok 4</td>
<td style="text-align: right;">65</td>
<td style="text-align: right;">40.2</td>
<td style="text-align: right;">18.71</td>
</tr>
</tbody>
</table>
<p>From the results, we make two observations:</p>
<ul>
<li>
<p>Among models with similar intelligence, IG differentiates by time-to-complete. For example, Grok 4 Fast and Gemini 2.5 Pro both have I = 60, but their times differ a lot (2.7d vs 7.5d). Consequently, Grok 4 Fast has much higher IG and completes tasks significantly faster.</p>
</li>
<li>
<p>Among models with similar total time, IG reflects differences in correctness.
GPT-5 High and Claude 4.5 Sonnet both finish in about 8 days, but GPT-5 High
has a higher I (68 vs 63), meaning it completed more tasks correctly in the
same time, which is reflected in their IGs (100.00 vs 91.75).</p>
</li>
</ul>
<p>In both cases, no matter it is different time-to-complete for similar correct
answers or number of correct answers in a fixed time window, IG gives a better
estimation on the per-task time.</p>
<h2>Discussions</h2>
<p>Benchmark datasets score only the final answer, disregarding intermediate outputs
such as the reasoning process. This approach effectively isolates the intelligent
portion of the total output, aligning with the definition of intelligence goodput.</p>
<p>For AI application developers working with time-bounded tasks, the tokens-per-second
metric provides limited insight, as the number of tokens required to complete a
task remains unknown beforehand. In contrast, intelligence goodput offers a more
informative measure of how much useful work can be accomplished per unit time.</p>
<p>Furthermore, incorporating intelligence goodput as an optimization target during
model training may help mitigate the verbosity problem commonly observed in LLMs.
This problem manifests as unnecessarily long chains of thought that repeatedly
revisit the same logical steps. Since longer reasoning processes result in slower
time-to-answer and consequently lower intelligence goodput, optimizing for this
metric naturally incentivizes more concise and efficient reasoning.</p>
<h2>Limitations</h2>
<p>The proposed intelligence goodput metric has two primary limitations.</p>
<p>First, it is currently limited to text-based outputs. While AI models are
increasingly multimodal, benchmark datasets for evaluating the intelligence of
outputs in other modalities, such as images, remain underdeveloped. Such outputs
are typically assessed for real-world fidelity and artistic merit rather than
intelligence.</p>
<p>Second, the metric is computationally expensive to measure. Calculating
intelligence goodput requires significant engineering effort to build evaluation
infrastructure capable of running APIs through comprehensive benchmark datasets.
Additionally, the cost of API token consumption for executing these benchmarks
can be substantial.</p>
<p>Third, tokens spent on incorrect answers are wasted. This property may bias intelligence goodput toward higher-accuracy models because lower-accuracy models consume tokens on wrong answers that do not increase the intelligence index, thereby lowering their IG.</p>
<p>However, in some settings this penalty is desirable. For example, in customer service, an incorrect answer may prompt the customer to re-prompt the AI before receiving a correct answer. From this perspective, the tokens and time spent on wrong attempts are part of the true time to a correct answer. Intelligence goodput appropriately includes this cost in the denominator.</p>
<h2>Conclusions</h2>
<p>This article introduces the concept of intelligence goodput a metric that
measures the processing speed of AI services by quantifying the rate at
which they produce intelligent information. The article advocates for a new
evaluation paradigm that focuses on dynamic, served AI systems, emphasizing the
crucial interplay between hardware, software, and models.</p>
<p>Ultimately, the most important takeaway is that when evaluating AI for
performing tasks traditionally performed by humans, the evaluation criteria
should mirror those used for humans. The distinction between assessing human and
AI performance for a given task will become increasingly blurred. Innovative
approaches are required to integrate processing speed into comprehensive
intelligence assessments for AI.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="wjiv" class="csl-entry">Woodcock, R. W., &#38; others. (1989). <i>Woodcock-Johnson tests of cognitive ability</i>. DLM Teaching Resources. <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-2"><div class="csl-bib-body">
  <div data-csl-entry-id="processing-speed" class="csl-entry">Fry, A. F., &#38; others. (1996). Processing speed, working memory, and fluid intelligence: Evidence for a developmental cascade. <i>Psychological Science</i>. <a href="#cite-2-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-3"><div class="csl-bib-body">
  <div data-csl-entry-id="wais" class="csl-entry">Lichtenberger, E. O., &#38; others. (2012). <i>Essentials of WAIS-IV assessment</i>. John Wiley &#38; Sons. <a href="#cite-3-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-4"><div class="csl-bib-body">
  <div data-csl-entry-id="wisc" class="csl-entry">Flanagan, D. P., &#38; others. (2017). <i>Essentials of WISC-V assessment</i>. John Wiley &#38; Sons. <a href="#cite-4-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-5"><div class="csl-bib-body">
  <div data-csl-entry-id="mmlu" class="csl-entry">Hendrycks, D., &#38; others. (2021). Measuring Massive Multitask Language Understanding. <i>ICLR</i>. <a href="#cite-5-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-6"><div class="csl-bib-body">
  <div data-csl-entry-id="gpqa" class="csl-entry">Rein, D., &#38; others. (2024). GPQA: A Graduate-Level Google-Proof Q&#38;A Benchmark. <i>COLM</i>. <a href="#cite-6-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-7"><div class="csl-bib-body">
  <div data-csl-entry-id="math" class="csl-entry">Hendrycks, D., &#38; others. (2021). Measuring Mathematical Problem Solving With the MATH Dataset. <i>NeurIPS</i>. <a href="#cite-7-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-8"><div class="csl-bib-body">
  <div data-csl-entry-id="alphago" class="csl-entry">Silver, D., &#38; others. (2016). Mastering the game of Go with deep neural networks and tree search. <i>Nature</i>. <a href="#cite-8-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-9"><div class="csl-bib-body">
  <div data-csl-entry-id="test-time-scaling" class="csl-entry">Snell, C., &#38; others. (2025). Scaling LLM test-time compute optimally can be more effective than scaling model parameters. <i>ICLR</i>. <a href="#cite-9-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-10"><div class="csl-bib-body">
  <div data-csl-entry-id="turing-test" class="csl-entry">Turing, A. M. (2009). <i>Computing machinery and intelligence</i>. Springer. <a href="#cite-10-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-11"><div class="csl-bib-body">
  <div data-csl-entry-id="x-ray" class="csl-entry">Çallı, E., &#38; others. (2021). Deep learning for chest X-ray analysis: A survey. <i>Medical Image Analysis</i>. <a href="#cite-11-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-12"><div class="csl-bib-body">
  <div data-csl-entry-id="neural-collaborative-filtering" class="csl-entry">He, X., &#38; others. (2017). Neural collaborative filtering. <i>The Web Conference</i>. <a href="#cite-12-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-13"><div class="csl-bib-body">
  <div data-csl-entry-id="chatgpt" class="csl-entry">Brown, T., &#38; others. (2020). Language models are few-shot learners. <i>NeurIPS</i>. <a href="#cite-13-a" class="reference-back-link">↩</a> <a href="#cite-13-b" class="reference-back-link">↩</a></div>
</div></li><li id="ref-14"><div class="csl-bib-body">
  <div data-csl-entry-id="diagnosis" class="csl-entry">Ullah, E., &#38; others. (2024). Challenges and barriers of using large language models (LLM) such as ChatGPT for diagnostic medicine with a focus on digital pathology–a recent scoping review. <i>Diagnostic Pathology</i>. <a href="#cite-14-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-15"><div class="csl-bib-body">
  <div data-csl-entry-id="llm-recommendation" class="csl-entry">Wu, X., &#38; others. (2024). Could small language models serve as recommenders? Towards data-centric cold-start recommendation. <i>The Web Conference</i>. <a href="#cite-15-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-16"><div class="csl-bib-body">
  <div data-csl-entry-id="alexnet" class="csl-entry">Krizhevsky, A., &#38; others. (2012). Imagenet classification with deep convolutional neural networks. <i>NeurIPS</i>. <a href="#cite-16-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-17"><div class="csl-bib-body">
  <div data-csl-entry-id="llama" class="csl-entry">Touvron, H., &#38; others. (2023). Llama: Open and efficient foundation language models. <i>arXiv Preprint arXiv:2302.13971</i>. <a href="#cite-17-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-18"><div class="csl-bib-body">
  <div data-csl-entry-id="qwen" class="csl-entry">Bai, J., &#38; others. (2023). Qwen technical report. <i>arXiv Preprint arXiv:2309.16609</i>. <a href="#cite-18-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-19"><div class="csl-bib-body">
  <div data-csl-entry-id="deepseek" class="csl-entry">Guo, D., &#38; others. (2025). DeepSeek-R1: Incentivizing reasoning capability in llms via reinforcement learning. <i>arXiv Preprint arXiv:2501.12948</i>. <a href="#cite-19-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-20"><div class="csl-bib-body">
  <div data-csl-entry-id="distserve" class="csl-entry">Zhong, Y., &#38; others. (2024). DistServe: disaggregating prefill and decoding for goodput-optimized large language model serving. <i>OSDI</i>. <a href="#cite-20-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-21"><div class="csl-bib-body">
  <div data-csl-entry-id="artificial-analysis" class="csl-entry">Artificial Analysis, Inc. (2024). <i>AI Model &#38; API Providers Analysis</i>. <a href="https://artificialanalysis.ai/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-21-a" class="reference-back-link">↩</a> <a href="#cite-21-b" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>Intelligence Bandwidth</title>
    <link href="https://haifengjin.com/intelligence-bandwidth/"/>
    <id>https://haifengjin.com/intelligence-bandwidth/</id>
    <published>2025-08-19T00:00:00+00:00</published>
    <updated>2025-08-19T00:00:00+00:00</updated>
    <summary>A metric to track the exponential growth of AI</summary>
    <content type="html"><![CDATA[<p>Using AI today is like using an old computer in command line from the early 1990s.
Everything is text based, you type in the command and wait for the response. On
modern social platforms, people read articles with illustrative images and watch
long-form, short-form, and live video. By contrast, most AI interactions remain
predominantly text-based.</p>
<p>With the emergence of multimodal AI, such as image
generation<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1,2,3</a></sup> and video generation<sup id="cite-4-a"><a href="#ref-4" class="citation-link">4,5,6</a></sup>, it’s
reasonable to expect that both human-AI interaction<sup id="cite-7-a"><a href="#ref-7" class="citation-link">7</a></sup> and AI-AI
interaction<sup id="cite-8-a"><a href="#ref-8" class="citation-link">8</a></sup> will become multimodal. Images may even appear in a model’s
chain of thought, approximating aspects of visual reasoning in the human brain.</p>
<h2>Analogy to the internet</h2>
<p>This invites a comparison between AI's trajectory and the internet's. Viewed
through the lens of network bandwidth, increases in throughput enabled richer
content formats<sup id="cite-9-a"><a href="#ref-9" class="citation-link">9</a></sup>, evolving from text-forward sites like
early Twitter<sup id="cite-10-a"><a href="#ref-10" class="citation-link">10</a></sup> to video-centric platforms like YouTube<sup id="cite-11-a"><a href="#ref-11" class="citation-link">11</a></sup>.
This transformation fundamentally changed how people use the internet.</p>
<p>Nielsen's law<sup id="cite-12-a"><a href="#ref-12" class="citation-link">12</a></sup> states that internet bandwidth increases by 50%
annually. This predictive framework enabled anticipation of when the internet
would become fast enough for users to easily download images or stream videos,
thereby forecasting the emergence and popularization of platforms such as
Pinterest<sup id="cite-13-a"><a href="#ref-13" class="citation-link">13</a></sup> and YouTube<sup id="cite-11-b"><a href="#ref-11" class="citation-link">11</a></sup>.</p>
<p>If bandwidth served as a progress proxy for the web, can we identify an
analogous metric to track the rate of AI output? Moreover, if we can observe a
growth pattern in such a metric, we may be able to predict the timeline for new
modalities of human-AI interaction in the future.</p>
<h2>Intelligence bandwidth</h2>
<p>I propose intelligence bandwidth as a metric to measure the output rate of AI
services. It quantifies the number of bits in the raw outputs of an AI model,
with the unit expressed as KB/s.</p>
<p><em>Intelligence bandwidth</em> is formally expressed as:</p>
<p>$$
B=\frac{D}{t}
$$</p>
<p>where $B$ is the intelligence bandwidth, $D$ is the total size of output data
from AI measured in KB, and $t$ is the total time spent.</p>
<p>The measurement methodology is straightforward. For any generative AI service, whether
it produces text, images, or videos, the output can be saved to disk. Intelligence
bandwidth is then calculated by dividing the storage size of the output by the
time the service took to generate that content.</p>
<h2>Measuring models across modalities</h2>
<p>With intelligence bandwith, we can project all the GenAI models to the same axis, KB/s.
All historically significant generative AI models are measured and plotted in a single figure. The models covered
include large language models, image generators, and video generators.
Most of the data
presented in this section is collected from Artificial
Analysis<sup id="cite-14-a"><a href="#ref-14" class="citation-link">14</a></sup>.</p>
<p>The experimental results are shown in Figure 1, where the X-axis is the release
date of the models, and the Y-axis is the intelligence bandwidth of the models
measured in kilobytes per second. The modality of the models is indicated by
different colors as shown in the legend.</p>
<figure class="svg-figure"><?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="1008pt" height="576pt" viewBox="0 0 1008 576" xmlns="http://www.w3.org/2000/svg" version="1.1">
 <metadata>
  <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
   <cc:Work>
    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
    <dc:date>2025-08-20T04:11:47.276042</dc:date>
    <dc:format>image/svg+xml</dc:format>
    <dc:creator>
     <cc:Agent>
      <dc:title>Matplotlib v3.10.0, https://matplotlib.org/</dc:title>
     </cc:Agent>
    </dc:creator>
   </cc:Work>
  </rdf:RDF>
 </metadata>
 <defs>
  <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}
  g[id^="text_"] { fill: var(--text-color); }
  g[id^="text_"] use { fill: var(--text-color); }</style>
 </defs>
 <g id="svg_intelligence_1_figure_1">
  <g id="svg_intelligence_1_patch_1">
   <path d="M 0 576 
L 1008 576 
L 1008 0 
L 0 0 
z
" style="fill: none"/>
  </g>
  <g id="svg_intelligence_1_axes_1">
   <g id="svg_intelligence_1_patch_2">
    <path d="M 61.56 541.52 
L 903.3775 541.52 
L 903.3775 99.128 
L 61.56 99.128 
z
" style="fill: none"/>
   </g>
   <g id="svg_intelligence_1_matplotlib.axis_1">
    <g id="svg_intelligence_1_xtick_1">
     <g id="svg_intelligence_1_line2d_1"/>
     <g id="svg_intelligence_1_text_1">
      <!-- 2023 -->
      <g transform="translate(262.332081 562.232188) scale(0.14 -0.14)">
       <defs>
        <path id="svg_intelligence_1_DejaVuSansMono-32" d="M 1166 531 
L 3309 531 
L 3309 0 
L 475 0 
L 475 531 
Q 1059 1147 1496 1619 
Q 1934 2091 2100 2284 
Q 2413 2666 2522 2902 
Q 2631 3138 2631 3384 
Q 2631 3775 2401 3997 
Q 2172 4219 1772 4219 
Q 1488 4219 1175 4116 
Q 863 4013 513 3803 
L 513 4441 
Q 834 4594 1145 4672 
Q 1456 4750 1759 4750 
Q 2444 4750 2861 4386 
Q 3278 4022 3278 3431 
Q 3278 3131 3139 2831 
Q 3000 2531 2688 2169 
Q 2513 1966 2180 1606 
Q 1847 1247 1166 531 
z
" transform="scale(0.015625)"/>
        <path id="svg_intelligence_1_DejaVuSansMono-30" d="M 1509 2344 
Q 1509 2516 1629 2641 
Q 1750 2766 1919 2766 
Q 2094 2766 2219 2641 
Q 2344 2516 2344 2344 
Q 2344 2169 2220 2047 
Q 2097 1925 1919 1925 
Q 1744 1925 1626 2044 
Q 1509 2163 1509 2344 
z
M 1925 4250 
Q 1484 4250 1267 3775 
Q 1050 3300 1050 2328 
Q 1050 1359 1267 884 
Q 1484 409 1925 409 
Q 2369 409 2586 884 
Q 2803 1359 2803 2328 
Q 2803 3300 2586 3775 
Q 2369 4250 1925 4250 
z
M 1925 4750 
Q 2672 4750 3055 4137 
Q 3438 3525 3438 2328 
Q 3438 1134 3055 521 
Q 2672 -91 1925 -91 
Q 1178 -91 797 521 
Q 416 1134 416 2328 
Q 416 3525 797 4137 
Q 1178 4750 1925 4750 
z
" transform="scale(0.015625)"/>
        <path id="svg_intelligence_1_DejaVuSansMono-33" d="M 2425 2497 
Q 2884 2375 3128 2064 
Q 3372 1753 3372 1288 
Q 3372 644 2939 276 
Q 2506 -91 1741 -91 
Q 1419 -91 1084 -31 
Q 750 28 428 141 
L 428 769 
Q 747 603 1056 522 
Q 1366 441 1672 441 
Q 2191 441 2469 675 
Q 2747 909 2747 1350 
Q 2747 1756 2469 1995 
Q 2191 2234 1716 2234 
L 1234 2234 
L 1234 2753 
L 1716 2753 
Q 2150 2753 2394 2943 
Q 2638 3134 2638 3475 
Q 2638 3834 2411 4026 
Q 2184 4219 1766 4219 
Q 1488 4219 1191 4156 
Q 894 4094 569 3969 
L 569 4550 
Q 947 4650 1242 4700 
Q 1538 4750 1766 4750 
Q 2447 4750 2855 4408 
Q 3263 4066 3263 3500 
Q 3263 3116 3048 2859 
Q 2834 2603 2425 2497 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-33" transform="translate(180.615234 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_xtick_2">
     <g id="svg_intelligence_1_line2d_2"/>
     <g id="svg_intelligence_1_text_2">
      <!-- 2024 -->
      <g transform="translate(504.806345 562.232188) scale(0.14 -0.14)">
       <defs>
        <path id="svg_intelligence_1_DejaVuSansMono-34" d="M 2297 4091 
L 825 1625 
L 2297 1625 
L 2297 4091 
z
M 2194 4666 
L 2925 4666 
L 2925 1625 
L 3547 1625 
L 3547 1113 
L 2925 1113 
L 2925 0 
L 2297 0 
L 2297 1113 
L 319 1113 
L 319 1709 
L 2194 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-34" transform="translate(180.615234 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_xtick_3">
     <g id="svg_intelligence_1_line2d_3"/>
     <g id="svg_intelligence_1_text_3">
      <!-- 2025 -->
      <g transform="translate(747.944922 562.232188) scale(0.14 -0.14)">
       <defs>
        <path id="svg_intelligence_1_DejaVuSansMono-35" d="M 647 4666 
L 3009 4666 
L 3009 4134 
L 1222 4134 
L 1222 2988 
Q 1356 3038 1492 3061 
Q 1628 3084 1766 3084 
Q 2491 3084 2916 2656 
Q 3341 2228 3341 1497 
Q 3341 759 2895 334 
Q 2450 -91 1678 -91 
Q 1306 -91 998 -41 
Q 691 9 447 109 
L 447 750 
Q 734 594 1025 517 
Q 1316 441 1619 441 
Q 2141 441 2423 716 
Q 2706 991 2706 1497 
Q 2706 1997 2414 2275 
Q 2122 2553 1600 2553 
Q 1347 2553 1106 2495 
Q 866 2438 647 2322 
L 647 4666 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(120.410156 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(180.615234 0)"/>
      </g>
     </g>
    </g>
   </g>
   <g id="svg_intelligence_1_matplotlib.axis_2">
    <g id="svg_intelligence_1_ytick_1">
     <g id="svg_intelligence_1_line2d_4">
      <path d="M 61.56 521.622158 
L 903.3775 521.622158 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_5"/>
     <g id="svg_intelligence_1_text_4">
      <!-- 0 -->
      <g transform="translate(48.131562 526.978252) scale(0.14 -0.14)">
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_ytick_2">
     <g id="svg_intelligence_1_line2d_6">
      <path d="M 61.56 446.305875 
L 903.3775 446.305875 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_7"/>
     <g id="svg_intelligence_1_text_5">
      <!-- 50 -->
      <g transform="translate(39.703125 451.661969) scale(0.14 -0.14)">
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_ytick_3">
     <g id="svg_intelligence_1_line2d_8">
      <path d="M 61.56 370.989592 
L 903.3775 370.989592 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_9"/>
     <g id="svg_intelligence_1_text_6">
      <!-- 100 -->
      <g transform="translate(31.274687 376.345686) scale(0.14 -0.14)">
       <defs>
        <path id="svg_intelligence_1_DejaVuSansMono-31" d="M 844 531 
L 1825 531 
L 1825 4097 
L 769 3859 
L 769 4434 
L 1819 4666 
L 2450 4666 
L 2450 531 
L 3419 531 
L 3419 0 
L 844 0 
L 844 531 
z
" transform="scale(0.015625)"/>
       </defs>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-31"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_ytick_4">
     <g id="svg_intelligence_1_line2d_10">
      <path d="M 61.56 295.673309 
L 903.3775 295.673309 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_11"/>
     <g id="svg_intelligence_1_text_7">
      <!-- 150 -->
      <g transform="translate(31.274687 301.029403) scale(0.14 -0.14)">
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-31"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_ytick_5">
     <g id="svg_intelligence_1_line2d_12">
      <path d="M 61.56 220.357026 
L 903.3775 220.357026 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_13"/>
     <g id="svg_intelligence_1_text_8">
      <!-- 200 -->
      <g transform="translate(31.274687 225.71312) scale(0.14 -0.14)">
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
    <g id="svg_intelligence_1_ytick_6">
     <g id="svg_intelligence_1_line2d_14">
      <path d="M 61.56 145.040743 
L 903.3775 145.040743 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke: var(--gray-text-color); stroke-opacity: 0.7; stroke-width: 0.5; stroke-linecap: square"/>
     </g>
     <g id="svg_intelligence_1_line2d_15"/>
     <g id="svg_intelligence_1_text_9">
      <!-- 250 -->
      <g transform="translate(31.274687 150.396836) scale(0.14 -0.14)">
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(60.205078 0)"/>
       <use xlink:href="#svg_intelligence_1_DejaVuSansMono-30" transform="translate(120.410156 0)"/>
      </g>
     </g>
    </g>
   </g>
   <g id="svg_intelligence_1_line2d_16">
    <path d="M 99.824432 499.027273 
L 107.55462 498.522418 
L 115.284808 498.006281 
L 123.014997 497.478613 
L 130.745185 496.939154 
L 138.475373 496.387642 
L 146.205561 495.823806 
L 153.93575 495.247373 
L 161.665938 494.65806 
L 169.396126 494.055579 
L 177.126314 493.439637 
L 184.856503 492.809932 
L 192.586691 492.166157 
L 200.316879 491.507998 
L 208.047067 490.835133 
L 215.777256 490.147233 
L 223.507444 489.443964 
L 231.237632 488.72498 
L 238.96782 487.989932 
L 246.698008 487.23846 
L 254.428197 486.470197 
L 262.158385 485.684769 
L 269.888573 484.88179 
L 277.618761 484.060871 
L 285.34895 483.221608 
L 293.079138 482.363594 
L 300.809326 481.486408 
L 308.539514 480.589623 
L 316.269703 479.672799 
L 323.999891 478.735491 
L 331.730079 477.777239 
L 339.460267 476.797577 
L 347.190456 475.796025 
L 354.920644 474.772095 
L 362.650832 473.725286 
L 370.38102 472.655087 
L 378.111209 471.560976 
L 385.841397 470.442419 
L 393.571585 469.298869 
L 401.301773 468.129767 
L 409.031962 466.934543 
L 416.76215 465.712614 
L 424.492338 464.463382 
L 432.222526 463.186237 
L 439.952715 461.880556 
L 447.682903 460.545701 
L 455.413091 459.18102 
L 463.143279 457.785847 
L 470.873468 456.359501 
L 478.603656 454.901285 
L 486.333844 453.410486 
L 494.064032 451.886378 
L 501.794221 450.328215 
L 509.524409 448.735237 
L 517.254597 447.106665 
L 524.984785 445.441705 
L 532.714974 443.739544 
L 540.445162 441.99935 
L 548.17535 440.220273 
L 555.905538 438.401445 
L 563.635727 436.541977 
L 571.365915 434.640962 
L 579.096103 432.69747 
L 586.826291 430.710554 
L 594.55648 428.679243 
L 602.286668 426.602544 
L 610.016856 424.479444 
L 617.747044 422.308906 
L 625.477233 420.089869 
L 633.207421 417.821251 
L 640.937609 415.501944 
L 648.667797 413.130814 
L 656.397986 410.706704 
L 664.128174 408.22843 
L 671.858362 405.694783 
L 679.58855 403.104524 
L 687.318739 400.456388 
L 695.048927 397.749083 
L 702.779115 394.981287 
L 710.509303 392.151648 
L 718.239492 389.258784 
L 725.96968 386.301282 
L 733.699868 383.277698 
L 741.430056 380.186556 
L 749.160244 377.026346 
L 756.890433 373.795524 
L 764.620621 370.492514 
L 772.350809 367.115703 
L 780.080997 363.66344 
L 787.811186 360.134041 
L 795.541374 356.525781 
L 803.271562 352.836899 
L 811.00175 349.065594 
L 818.731939 345.210023 
L 826.462127 341.268304 
L 834.192315 337.238512 
L 841.922503 333.118679 
L 849.652692 328.906793 
L 857.38288 324.600798 
L 865.113068 320.19859 
" clip-path="url(#svg_intelligence_1_pf298a9178c)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: var(--gray-text-color); stroke-width: 2"/>
   </g>
   <g id="svg_intelligence_1_patch_3">
    <path d="M 61.56 541.52 
L 61.56 99.128 
" style="fill: none; stroke: var(--text-color); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: square"/>
   </g>
   <g id="svg_intelligence_1_patch_4">
    <path d="M 61.56 541.52 
L 903.3775 541.52 
" style="fill: none; stroke: var(--text-color); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: square"/>
   </g>
   <g id="svg_intelligence_1_PathCollection_1">
    <defs>
     <path id="svg_intelligence_1_m80cfef98f3" d="M 0 5 
C 1.326016 5 2.597899 4.473168 3.535534 3.535534 
C 4.473168 2.597899 5 1.326016 5 0 
C 5 -1.326016 4.473168 -2.597899 3.535534 -3.535534 
C 2.597899 -4.473168 1.326016 -5 0 -5 
C -1.326016 -5 -2.597899 -4.473168 -3.535534 -3.535534 
C -4.473168 -2.597899 -5 -1.326016 -5 0 
C -5 1.326016 -4.473168 2.597899 -3.535534 3.535534 
C -2.597899 4.473168 -1.326016 5 0 5 
z
" style="stroke: var(--text-color)"/>
    </defs>
    <g clip-path="url(#svg_intelligence_1_pf298a9178c)">
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="99.824432" y="488.803489" style="fill: var(--text-color); stroke: var(--text-color)"/>
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="460.546419" y="471.009616" style="fill: var(--text-color); stroke: var(--text-color)"/>
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="744.208093" y="489.186884" style="fill: var(--text-color); stroke: var(--text-color)"/>
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="779.416684" y="298.613366" style="fill: var(--text-color); stroke: var(--text-color)"/>
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="847.840929" y="119.236727" style="fill: var(--text-color); stroke: var(--text-color)"/>
     <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="865.113068" y="338.357507" style="fill: var(--text-color); stroke: var(--text-color)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_PathCollection_2">
    <defs>
     <path id="svg_intelligence_1_m9f9fa6714d" d="M 0 5 
C 1.326016 5 2.597899 4.473168 3.535534 3.535534 
C 4.473168 2.597899 5 1.326016 5 0 
C 5 -1.326016 4.473168 -2.597899 3.535534 -3.535534 
C 2.597899 -4.473168 1.326016 -5 0 -5 
C -1.326016 -5 -2.597899 -4.473168 -3.535534 -3.535534 
C -4.473168 -2.597899 -5 -1.326016 -5 0 
C -5 1.326016 -4.473168 2.597899 -3.535534 3.535534 
C -2.597899 4.473168 -1.326016 5 0 5 
z
" style="stroke: var(--gray-text-color)"/>
    </defs>
    <g clip-path="url(#svg_intelligence_1_pf298a9178c)">
     <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="257.266625" y="521.079881" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="326.355183" y="521.411273" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="609.352543" y="520.884059" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="690.398735" y="520.05558" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="834.554668" y="518.895709" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_PathCollection_3">
    <defs>
     <path id="svg_intelligence_1_maacfe09141" d="M 0 5 
C 1.326016 5 2.597899 4.473168 3.535534 3.535534 
C 4.473168 2.597899 5 1.326016 5 0 
C 5 -1.326016 4.473168 -2.597899 3.535534 -3.535534 
C 2.597899 -4.473168 1.326016 -5 0 -5 
C -1.326016 -5 -2.597899 -4.473168 -3.535534 -3.535534 
C -4.473168 -2.597899 -5 -1.326016 -5 0 
C -5 1.326016 -4.473168 2.597899 -3.535534 3.535534 
C -2.597899 4.473168 -1.326016 5 0 5 
z
" style="stroke: var(--gray-text-color)"/>
    </defs>
    <g clip-path="url(#svg_intelligence_1_pf298a9178c)">
     <use xlink:href="#svg_intelligence_1_maacfe09141" x="857.141312" y="410.56378" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_10">
    <!-- DALLE-2 -->
    <g style="fill: var(--text-color)" transform="translate(109.824432 478.803489) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-44" d="M 1363 519 
Q 2159 519 2475 911 
Q 2791 1303 2791 2328 
Q 2791 3363 2477 3755 
Q 2163 4147 1363 4147 
L 1063 4147 
L 1063 519 
L 1363 519 
z
M 1375 4666 
Q 2444 4666 2950 4097 
Q 3456 3528 3456 2328 
Q 3456 1134 2950 567 
Q 2444 0 1375 0 
L 428 0 
L 428 4666 
L 1375 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-41" d="M 1925 4109 
L 1259 1722 
L 2591 1722 
L 1925 4109 
z
M 1544 4666 
L 2309 4666 
L 3738 0 
L 3084 0 
L 2741 1216 
L 1106 1216 
L 769 0 
L 116 0 
L 1544 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-4c" d="M 672 4666 
L 1306 4666 
L 1306 531 
L 3559 531 
L 3559 0 
L 672 0 
L 672 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-45" d="M 616 4666 
L 3384 4666 
L 3384 4134 
L 1247 4134 
L 1247 2753 
L 3291 2753 
L 3291 2222 
L 1247 2222 
L 1247 531 
L 3444 531 
L 3444 0 
L 616 0 
L 616 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-2d" d="M 1113 2009 
L 2741 2009 
L 2741 1497 
L 1113 1497 
L 1113 2009 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-44"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-41" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-4c" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-4c" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-45" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(361.230469 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_11">
    <!-- Gpt-3.5 -->
    <g style="fill: var(--text-color)" transform="translate(267.266625 511.079881) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-47" d="M 3450 384 
Q 3197 150 2880 29 
Q 2563 -91 2194 -91 
Q 1306 -91 812 545 
Q 319 1181 319 2328 
Q 319 3472 819 4111 
Q 1319 4750 2209 4750 
Q 2503 4750 2772 4667 
Q 3041 4584 3291 4416 
L 3291 3769 
Q 3038 4009 2772 4123 
Q 2506 4238 2209 4238 
Q 1594 4238 1286 3761 
Q 978 3284 978 2328 
Q 978 1356 1276 889 
Q 1575 422 2194 422 
Q 2403 422 2561 470 
Q 2719 519 2847 622 
L 2847 1875 
L 2169 1875 
L 2169 2394 
L 3450 2394 
L 3450 384 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-70" d="M 1172 441 
L 1172 -1331 
L 594 -1331 
L 594 3500 
L 1172 3500 
L 1172 3053 
Q 1316 3313 1555 3448 
Q 1794 3584 2106 3584 
Q 2741 3584 3102 3093 
Q 3463 2603 3463 1734 
Q 3463 881 3100 395 
Q 2738 -91 2106 -91 
Q 1788 -91 1548 45 
Q 1309 181 1172 441 
z
M 2859 1747 
Q 2859 2416 2648 2756 
Q 2438 3097 2022 3097 
Q 1603 3097 1387 2755 
Q 1172 2413 1172 1747 
Q 1172 1084 1387 740 
Q 1603 397 2022 397 
Q 2438 397 2648 737 
Q 2859 1078 2859 1747 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-74" d="M 1919 4494 
L 1919 3500 
L 3225 3500 
L 3225 3053 
L 1919 3053 
L 1919 1153 
Q 1919 766 2066 612 
Q 2213 459 2578 459 
L 3225 459 
L 3225 0 
L 2522 0 
Q 1875 0 1609 259 
Q 1344 519 1344 1153 
L 1344 3053 
L 409 3053 
L 409 3500 
L 1344 3500 
L 1344 4494 
L 1919 4494 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-2e" d="M 1528 953 
L 2316 953 
L 2316 0 
L 1528 0 
L 1528 953 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-70" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-74" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-33" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2e" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(361.230469 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_12">
    <!-- GPT-4 -->
    <g style="fill: var(--text-color)" transform="translate(336.355183 511.411273) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-50" d="M 1247 4147 
L 1247 2394 
L 1978 2394 
Q 2416 2394 2661 2625 
Q 2906 2856 2906 3272 
Q 2906 3688 2662 3917 
Q 2419 4147 1978 4147 
L 1247 4147 
z
M 616 4666 
L 1978 4666 
Q 2759 4666 3162 4311 
Q 3566 3956 3566 3272 
Q 3566 2581 3164 2228 
Q 2763 1875 1978 1875 
L 1247 1875 
L 1247 0 
L 616 0 
L 616 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-54" d="M 147 4666 
L 3706 4666 
L 3706 4134 
L 2247 4134 
L 2247 0 
L 1613 0 
L 1613 4134 
L 147 4134 
L 147 4666 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-34" transform="translate(240.820312 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_13">
    <!-- DALLE-3 -->
    <g style="fill: var(--text-color)" transform="translate(470.546419 461.009616) scale(0.14 -0.14)">
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-44"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-41" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-4c" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-4c" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-45" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-33" transform="translate(361.230469 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_14">
    <!-- GPT-4o -->
    <g style="fill: var(--text-color)" transform="translate(619.352543 510.884059) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-6f" d="M 1925 3097 
Q 1488 3097 1263 2756 
Q 1038 2416 1038 1747 
Q 1038 1081 1263 739 
Q 1488 397 1925 397 
Q 2366 397 2591 739 
Q 2816 1081 2816 1747 
Q 2816 2416 2591 2756 
Q 2366 3097 1925 3097 
z
M 1925 3584 
Q 2653 3584 3039 3112 
Q 3425 2641 3425 1747 
Q 3425 850 3040 379 
Q 2656 -91 1925 -91 
Q 1197 -91 812 379 
Q 428 850 428 1747 
Q 428 2641 812 3112 
Q 1197 3584 1925 3584 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-34" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6f" transform="translate(301.025391 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_15">
    <!-- GPT-o1-mini -->
    <g style="fill: var(--text-color)" transform="translate(700.398735 510.05558) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-6d" d="M 2113 3144 
Q 2219 3369 2383 3476 
Q 2547 3584 2778 3584 
Q 3200 3584 3373 3257 
Q 3547 2931 3547 2028 
L 3547 0 
L 3022 0 
L 3022 2003 
Q 3022 2744 2939 2923 
Q 2856 3103 2638 3103 
Q 2388 3103 2295 2911 
Q 2203 2719 2203 2003 
L 2203 0 
L 1678 0 
L 1678 2003 
Q 1678 2753 1589 2928 
Q 1500 3103 1269 3103 
Q 1041 3103 952 2911 
Q 863 2719 863 2003 
L 863 0 
L 341 0 
L 341 3500 
L 863 3500 
L 863 3200 
Q 966 3388 1120 3486 
Q 1275 3584 1472 3584 
Q 1709 3584 1867 3475 
Q 2025 3366 2113 3144 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-69" d="M 800 3500 
L 2272 3500 
L 2272 447 
L 3413 447 
L 3413 0 
L 556 0 
L 556 447 
L 1697 447 
L 1697 3053 
L 800 3053 
L 800 3500 
z
M 1697 4863 
L 2272 4863 
L 2272 4134 
L 1697 4134 
L 1697 4863 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-6e" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 3500 
L 1184 3500 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-50" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-54" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6f" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-31" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6e" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(602.050781 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_16">
    <!-- Grok-2.x -->
    <g style="fill: var(--text-color)" transform="translate(754.208093 479.186884) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-72" d="M 3609 2778 
Q 3425 2922 3234 2987 
Q 3044 3053 2816 3053 
Q 2278 3053 1993 2715 
Q 1709 2378 1709 1741 
L 1709 0 
L 1131 0 
L 1131 3500 
L 1709 3500 
L 1709 2816 
Q 1853 3188 2151 3386 
Q 2450 3584 2859 3584 
Q 3072 3584 3256 3531 
Q 3441 3478 3609 3366 
L 3609 2778 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-6b" d="M 738 4863 
L 1331 4863 
L 1331 2047 
L 2841 3500 
L 3541 3500 
L 2163 2181 
L 3756 0 
L 3053 0 
L 1759 1806 
L 1331 1403 
L 1331 0 
L 738 0 
L 738 4863 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-78" d="M 3494 3500 
L 2241 1825 
L 3616 0 
L 2950 0 
L 1925 1403 
L 903 0 
L 238 0 
L 1613 1825 
L 359 3500 
L 997 3500 
L 1925 2234 
L 2847 3500 
L 3494 3500 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-72" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6b" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2e" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-78" transform="translate(421.435547 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_17">
    <!-- Imagen-3 -->
    <g style="fill: var(--text-color)" transform="translate(789.416684 288.613366) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-49" d="M 628 4666 
L 3219 4666 
L 3219 4134 
L 2241 4134 
L 2241 531 
L 3219 531 
L 3219 0 
L 628 0 
L 628 531 
L 1606 531 
L 1606 4134 
L 628 4134 
L 628 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-61" d="M 2194 1759 
L 2003 1759 
Q 1500 1759 1245 1582 
Q 991 1406 991 1056 
Q 991 741 1181 566 
Q 1372 391 1709 391 
Q 2184 391 2456 720 
Q 2728 1050 2731 1631 
L 2731 1759 
L 2194 1759 
z
M 3309 1997 
L 3309 0 
L 2731 0 
L 2731 519 
Q 2547 206 2267 57 
Q 1988 -91 1588 -91 
Q 1053 -91 734 211 
Q 416 513 416 1019 
Q 416 1603 808 1906 
Q 1200 2209 1959 2209 
L 2731 2209 
L 2731 2300 
Q 2728 2719 2518 2908 
Q 2309 3097 1850 3097 
Q 1556 3097 1256 3012 
Q 956 2928 672 2766 
L 672 3341 
Q 991 3463 1283 3523 
Q 1575 3584 1850 3584 
Q 2284 3584 2592 3456 
Q 2900 3328 3091 3072 
Q 3209 2916 3259 2686 
Q 3309 2456 3309 1997 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-67" d="M 2681 1778 
Q 2681 2425 2470 2761 
Q 2259 3097 1856 3097 
Q 1434 3097 1212 2761 
Q 991 2425 991 1778 
Q 991 1131 1214 792 
Q 1438 453 1863 453 
Q 2259 453 2470 793 
Q 2681 1134 2681 1778 
z
M 3256 225 
Q 3256 -563 2884 -969 
Q 2513 -1375 1791 -1375 
Q 1553 -1375 1293 -1331 
Q 1034 -1288 775 -1203 
L 775 -634 
Q 1081 -778 1331 -847 
Q 1581 -916 1791 -916 
Q 2256 -916 2468 -662 
Q 2681 -409 2681 141 
L 2681 166 
L 2681 556 
Q 2544 263 2306 119 
Q 2069 -25 1728 -25 
Q 1116 -25 750 465 
Q 384 956 384 1778 
Q 384 2603 750 3093 
Q 1116 3584 1728 3584 
Q 2066 3584 2300 3450 
Q 2534 3316 2681 3034 
L 2681 3488 
L 3256 3488 
L 3256 225 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-65" d="M 3475 1894 
L 3475 1613 
L 984 1613 
L 984 1594 
Q 984 1022 1282 709 
Q 1581 397 2125 397 
Q 2400 397 2700 484 
Q 3000 572 3341 750 
L 3341 178 
Q 3013 44 2708 -23 
Q 2403 -91 2119 -91 
Q 1303 -91 843 398 
Q 384 888 384 1747 
Q 384 2584 834 3084 
Q 1284 3584 2034 3584 
Q 2703 3584 3089 3131 
Q 3475 2678 3475 1894 
z
M 2900 2063 
Q 2888 2569 2661 2833 
Q 2434 3097 2009 3097 
Q 1594 3097 1325 2822 
Q 1056 2547 1006 2059 
L 2900 2063 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-49"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-67" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6e" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-33" transform="translate(421.435547 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_18">
    <!-- Gemini-2.5-Flash -->
    <g style="fill: var(--text-color)" transform="translate(844.554668 508.895709) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-46" d="M 728 4666 
L 3475 4666 
L 3475 4134 
L 1363 4134 
L 1363 2759 
L 3278 2759 
L 3278 2228 
L 1363 2228 
L 1363 0 
L 728 0 
L 728 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-6c" d="M 1997 1269 
Q 1997 881 2139 684 
Q 2281 488 2559 488 
L 3231 488 
L 3231 0 
L 2503 0 
Q 1988 0 1705 331 
Q 1422 663 1422 1269 
L 1422 4447 
L 500 4447 
L 500 4897 
L 1997 4897 
L 1997 1269 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-73" d="M 3041 3378 
L 3041 2816 
Q 2794 2959 2544 3031 
Q 2294 3103 2034 3103 
Q 1644 3103 1451 2976 
Q 1259 2850 1259 2591 
Q 1259 2356 1403 2240 
Q 1547 2125 2119 2016 
L 2350 1972 
Q 2778 1891 2998 1647 
Q 3219 1403 3219 1013 
Q 3219 494 2850 201 
Q 2481 -91 1825 -91 
Q 1566 -91 1281 -36 
Q 997 19 666 128 
L 666 722 
Q 988 556 1281 473 
Q 1575 391 1838 391 
Q 2219 391 2428 545 
Q 2638 700 2638 978 
Q 2638 1378 1872 1531 
L 1847 1538 
L 1631 1581 
Q 1134 1678 906 1908 
Q 678 2138 678 2534 
Q 678 3038 1018 3311 
Q 1359 3584 1991 3584 
Q 2272 3584 2531 3532 
Q 2791 3481 3041 3378 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-68" d="M 3284 2169 
L 3284 0 
L 2706 0 
L 2706 2169 
Q 2706 2641 2540 2862 
Q 2375 3084 2022 3084 
Q 1619 3084 1401 2798 
Q 1184 2513 1184 1978 
L 1184 0 
L 609 0 
L 609 4863 
L 1184 4863 
L 1184 2975 
Q 1338 3275 1600 3429 
Q 1863 3584 2222 3584 
Q 2756 3584 3020 3232 
Q 3284 2881 3284 2169 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-46" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6c" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(782.666016 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-73" transform="translate(842.871094 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-68" transform="translate(903.076172 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_19">
    <!-- Gemini-2.5-Flash -->
    <g style="fill: var(--text-color)" transform="translate(857.840929 109.236727) scale(0.14 -0.14)">
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-47"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6e" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-32" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2e" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-35" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-46" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6c" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(782.666016 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-73" transform="translate(842.871094 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-68" transform="translate(903.076172 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_20">
    <!-- Veo-3 -->
    <g style="fill: var(--text-color)" transform="translate(867.141312 400.56378) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-56" d="M 1925 531 
L 3022 4666 
L 3675 4666 
L 2309 0 
L 1544 0 
L 178 4666 
L 831 4666 
L 1925 531 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-56"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6f" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-33" transform="translate(240.820312 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_21">
    <!-- Imagen-4-Ultra -->
    <g style="fill: var(--text-color)" transform="translate(875.113068 328.357507) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-55" d="M 459 1791 
L 459 4666 
L 1094 4666 
L 1094 1503 
Q 1094 1163 1112 1017 
Q 1131 872 1178 794 
Q 1278 609 1467 515 
Q 1656 422 1925 422 
Q 2197 422 2384 515 
Q 2572 609 2675 794 
Q 2722 872 2740 1015 
Q 2759 1159 2759 1497 
L 2759 4666 
L 3391 4666 
L 3391 1791 
Q 3391 1075 3302 773 
Q 3213 472 2994 275 
Q 2788 91 2522 0 
Q 2256 -91 1925 -91 
Q 1597 -91 1331 0 
Q 1066 91 856 275 
Q 641 469 550 776 
Q 459 1084 459 1791 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-49"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-67" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6e" transform="translate(301.025391 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(361.230469 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-34" transform="translate(421.435547 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2d" transform="translate(481.640625 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-55" transform="translate(541.845703 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6c" transform="translate(602.050781 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-74" transform="translate(662.255859 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-72" transform="translate(722.460938 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(782.666016 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_text_22">
    <!-- (KB/s) -->
    <g transform="translate(10.989375 96.216438) scale(0.14 -0.14)">
     <defs>
      <path id="svg_intelligence_1_DejaVuSansMono-28" d="M 2766 4856 
Q 2350 4144 2145 3436 
Q 1941 2728 1941 2009 
Q 1941 1294 2145 584 
Q 2350 -125 2766 -844 
L 2266 -844 
Q 1794 -100 1562 604 
Q 1331 1309 1331 2009 
Q 1331 2706 1562 3412 
Q 1794 4119 2266 4856 
L 2766 4856 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-4b" d="M 428 4666 
L 1063 4666 
L 1063 2591 
L 3034 4666 
L 3775 4666 
L 1959 2759 
L 3828 0 
L 3066 0 
L 1544 2338 
L 1063 1825 
L 1063 0 
L 428 0 
L 428 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-42" d="M 1153 2228 
L 1153 519 
L 1900 519 
Q 2450 519 2684 711 
Q 2919 903 2919 1344 
Q 2919 1800 2672 2014 
Q 2425 2228 1900 2228 
L 1153 2228 
z
M 1153 4147 
L 1153 2741 
L 1888 2741 
Q 2344 2741 2548 2916 
Q 2753 3091 2753 3481 
Q 2753 3834 2551 3990 
Q 2350 4147 1888 4147 
L 1153 4147 
z
M 519 4666 
L 1900 4666 
Q 2616 4666 3003 4356 
Q 3391 4047 3391 3481 
Q 3391 3053 3186 2806 
Q 2981 2559 2572 2497 
Q 3031 2428 3292 2104 
Q 3553 1781 3553 1281 
Q 3553 647 3137 323 
Q 2722 0 1900 0 
L 519 0 
L 519 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-2f" d="M 2778 4666 
L 3372 4666 
L 916 -594 
L 319 -594 
L 2778 4666 
z
" transform="scale(0.015625)"/>
      <path id="svg_intelligence_1_DejaVuSansMono-29" d="M 1088 4856 
L 1588 4856 
Q 2059 4119 2290 3412 
Q 2522 2706 2522 2009 
Q 2522 1306 2290 600 
Q 2059 -106 1588 -844 
L 1088 -844 
Q 1503 -119 1708 590 
Q 1913 1300 1913 2009 
Q 1913 2722 1708 3431 
Q 1503 4141 1088 4856 
z
" transform="scale(0.015625)"/>
     </defs>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-28"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-4b" transform="translate(60.205078 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-42" transform="translate(120.410156 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-2f" transform="translate(180.615234 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-73" transform="translate(240.820312 0)"/>
     <use xlink:href="#svg_intelligence_1_DejaVuSansMono-29" transform="translate(301.025391 0)"/>
    </g>
   </g>
   <g id="svg_intelligence_1_legend_1">
    <g id="svg_intelligence_1_PathCollection_4">
     <g>
      <use xlink:href="#svg_intelligence_1_m80cfef98f3" x="350.669687" y="52.406388" style="fill: var(--text-color); stroke: var(--text-color)"/>
     </g>
    </g>
    <g id="svg_intelligence_1_text_23">
     <!-- Image -->
     <g transform="translate(375.869687 56.081388) scale(0.14 -0.14)">
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-49"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6d" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-61" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-67" transform="translate(180.615234 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(240.820312 0)"/>
     </g>
    </g>
    <g id="svg_intelligence_1_PathCollection_5">
     <g>
      <use xlink:href="#svg_intelligence_1_m9f9fa6714d" x="460.011875" y="52.406388" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     </g>
    </g>
    <g id="svg_intelligence_1_text_24">
     <!-- Text -->
     <g transform="translate(485.211875 56.081388) scale(0.14 -0.14)">
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-54"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-78" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-74" transform="translate(180.615234 0)"/>
     </g>
    </g>
    <g id="svg_intelligence_1_PathCollection_6">
     <g>
      <use xlink:href="#svg_intelligence_1_maacfe09141" x="560.925625" y="52.406388" style="fill: var(--gray-text-color); stroke: var(--gray-text-color)"/>
     </g>
    </g>
    <g id="svg_intelligence_1_text_25">
     <!-- Video -->
     <g transform="translate(586.125625 56.081388) scale(0.14 -0.14)">
      <defs>
       <path id="svg_intelligence_1_DejaVuSansMono-64" d="M 2681 3053 
L 2681 4863 
L 3256 4863 
L 3256 0 
L 2681 0 
L 2681 441 
Q 2538 181 2298 45 
Q 2059 -91 1747 -91 
Q 1113 -91 748 401 
Q 384 894 384 1759 
Q 384 2613 750 3098 
Q 1116 3584 1747 3584 
Q 2063 3584 2303 3448 
Q 2544 3313 2681 3053 
z
M 991 1747 
Q 991 1078 1203 737 
Q 1416 397 1831 397 
Q 2247 397 2464 740 
Q 2681 1084 2681 1747 
Q 2681 2413 2464 2755 
Q 2247 3097 1831 3097 
Q 1416 3097 1203 2756 
Q 991 2416 991 1747 
z
" transform="scale(0.015625)"/>
      </defs>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-56"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-69" transform="translate(60.205078 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-64" transform="translate(120.410156 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-65" transform="translate(180.615234 0)"/>
      <use xlink:href="#svg_intelligence_1_DejaVuSansMono-6f" transform="translate(240.820312 0)"/>
     </g>
    </g>
   </g>
  </g>
 </g>
 <defs>
  <clipPath id="svg_intelligence_1_pf298a9178c">
   <rect x="61.56" y="99.128" width="841.8175" height="442.392"/>
  </clipPath>
 </defs>
</svg></figure>
<p><strong>Figure 1</strong>: Intelligence bandwidth (KB/s) over time.</p>
<p>Key observations from the experimental results include:
1. Most language models are between 0 KB/s and 3 KB/s.</p>
<ol>
<li>
<p>Image generators exhibit an exponential growth rate.</p>
</li>
<li>
<p>The video generator, Veo3<sup id="cite-5-b"><a href="#ref-5" class="citation-link">5</a></sup>, currently exhibits an even lower
intelligence bandwidth than the state-of-the-art image generators. This is
primarily attributable to less mature serving technologies for video models
compared to those for large language models and image generators. As serving
efficiency for video generators improves, substantial growth in their
intelligence bandwidth is anticipated in the near future.</p>
</li>
<li>
<p>The Gemini 2.5 Flash<sup id="cite-15-a"><a href="#ref-15" class="citation-link">15</a></sup> image generator is an outlier, primarily
because it is optimized for low latency and usability rather than best quality
and fidelity.</p>
</li>
</ol>
<h2>Jin's law</h2>
<p>Effective metrics can help reveal new laws to predict the future, analogous to the number of transistors
and Moore's law<sup id="cite-16-a"><a href="#ref-16" class="citation-link">16</a></sup>, FLOPS and Huang's Law, or the internet bandwidth and
Nielsen's law<sup id="cite-12-b"><a href="#ref-12" class="citation-link">12</a></sup>. Similarly, a robust metric for AI output rate can facilitate
the discovery of macro-level trends in AI development.</p>
<p>We now
assess the validity of intelligence bandwidth as such a metric by examining
whether it supports the formulation of a predictive law for future AI growth.</p>
<p>The dotted curve in Figure 1 represents the estimated
growth of intelligence bandwidth. The prediction of the growth rate is based
primarily on Imagen 4, the state-of-the-art high-quality image generator, rather
than a model balanced between speed and quality, such as Gemini 2.5 Flash<sup id="cite-15-b"><a href="#ref-15" class="citation-link">15</a></sup>. The
growth rate of intelligence bandwidth is summarized in a simple law named after
the author's surname, presented as follows.</p>
<p><strong>Jin's law</strong>: The intelligence bandwidth (KB/s) of the best generative AI service
available to the public doubles every year.</p>
<p>The formal definition of this law is as follows. Let $B(t)$ be the
intelligence bandwidth of the best hosted AI model available to the public at
time $t$ and $B_0$ ​be the initial intelligence bandwidth at time $t_0$. The law
can be expressed as:</p>
<p>$$
B(t)=B_0 \cdot 2^{(t−t_0 ​)/T}
$$</p>
<p>where $T$ is the doubling period. The best estimate is $T=1$, the unit of which
is year.</p>
<p>In Jin's law, intelligence bandwidth is defined by the modality exhibiting the
highest KB/s measurement. Currently, image generators are at the forefront of
this growth. As advancements in models and serving technologies for image
generation reach a plateau, it is anticipated that video generators will become
the primary drivers of further increases in intelligence bandwidth.</p>
<p>We only measure the best GenAI services. Any model or service that is not
well-received by the public is not considered by Jin's law.</p>
<p>We only measure the public available GenAI services. Any service that is not
public accessible is not considered by Jin's law.</p>
<h2>Impact on human-AI interaction</h2>
<p>To use Jin's law to predict the future of human-AI interaction, we first need to
recap a few basics on this subject.</p>
<p>Current human-AI interactions are still
predominantly text-based. This is possible because the output speed of AI has exceeded
human reading speed, which is approximately 238 words per minute<sup id="cite-17-a"><a href="#ref-17" class="citation-link">17</a></sup>.
This is enabled by state-of-the-art serving technologies, which can generate
14,000 words per minute. Similarly, speech generation speed is far beyond
human listening speed<sup id="cite-18-a"><a href="#ref-18" class="citation-link">18</a></sup>.</p>
<p>To clarify the prerequisites for enabling real-time human-AI interaction within
a given modality, it is helpful to examine the specific conditions that must be
satisfied.
For self-paced media formats such as text
and images, individuals typically consume content at their maximum perceptual
speed. Once the intelligence bandwidth exceeds the human perceptual threshold,
real-time interaction in that modality becomes feasible. In contrast, for
fixed-speed media formats such as audio and video, users generally adhere to the
inherent playback speed. Thus, as long as the AI generation speed surpasses the
fixed playback rate of these media formats, real-time interaction in those
modalities is achievable.</p>
<p>Research in
multi-modal AI continues to address the bottlenecks of other modalities.
With the increase of the intelligence bandwidth of image generation, visual
illustrations will become integrated into AI responses.
AI may also be able to perform visual reasoning akin to humans on a
whiteboard, and iteratively refine graphical designs as designers do on paper.
As the intelligence bandwidth of leading AI models continues to increase, it is
expected that video illustrations and real-time generated environment
interactions, such as those demonstrated by Genie 3<sup id="cite-19-a"><a href="#ref-19" class="citation-link">19</a></sup>, will become feasible.
In a speculative future, AI could generate entire worlds that users can
interact with in real time, an idealistic scenario enabled by the cognitive
capabilities of AI.</p>
<p>Achieving such increases in intelligence bandwidth requires not only advances in
AI models as static collections of neural architectures and parameters, but also
significant improvements in AI hardware and machine learning systems. In this
vision, hardware, software, and models are no longer orthogonal, but are
deeply integrated to enable superintelligence.</p>
<h2>Predictions</h2>
<p>Based on Jin's law, two predictions about human-AI
interaction in the near future are made as an example of how to use the law to
predict the exponential growth of AI in the future:</p>
<ol>
<li>
<p><strong>Images will soon be used in AI interactions.</strong> The latency of the Gemini
2.5 Flash<sup id="cite-15-c"><a href="#ref-15" class="citation-link">15</a></sup> image generator is lower than that of large language
model responses. Consequently, large language models may soon incorporate images
to provide enhanced illustrations in their outputs. Currently, it takes only 4.6
seconds to generate an image. If this speed doubles within a year, it is likely
that applications will emerge in which images become a primary mode of
interaction and illustration.</p>
</li>
<li>
<p><strong>Real-time video interaction will be widely available in three years.</strong>
Given the intelligence bandwidth of models in 2025, it is currently possible to
generate 8 seconds of video in 50 to 60 seconds. If generation speed increases
by a factor of 7 to 8, real-time video generation will become feasible.
Achieving an 8-fold increase corresponds to approximately $\log_2(8) = 3$ years.
While some uncertainty remains in this prediction, video generators are
presently below the projected growth curve and possess significant potential for
accelerated improvement as serving technologies advance.</p>
</li>
</ol>
<p>There are many other implications that can be derived from Jin's law. It is hoped that this law
will guide AI application developers in identifying optimal time windows to bring
products to market, and policymakers in enacting regulations at
appropriate times to maximize development while minimizing harm.</p>
<h2>Limitations</h2>
<p>There are two main limitations to this work. </p>
<p>First, the accuracy of the estimated doubling period $T$ in Jin's law is
uncertain. As the field of AI is still in its early stages, the current
estimation is based on a limited number of available data points.</p>
<p>Second, the exponential growth described by Jin's law represents an idealized
scenario. Just like the idea of "the end of Moore's Law", Jin's law would not
live for ever. In practice, growth may be constrained by factors such as energy
supply limitations or economic pressures, particularly if the AI sector
experiences a market correction.</p>
<h2>Conclusions</h2>
<p>The article introduces the concept of intelligence bandwidth, a metric that
measures the processing speed of AI services. The article observes an exponential growth
trend in this metric across various modalities and formulates Jin's law, which
states that the intelligence bandwidth of the best publicly available AI doubles
annually. This law provides a predictive framework, forecasting the imminent
integration of real-time images into human-AI text interactions and the
widespread availability of real-time video generation within three years.</p>
<h2>Further reading</h2>
<p>Intelligence bandwidth measures only the total size of AI output, whereas the
proportion of intelligent information contained within that output can vary
significantly. To address this limitation, it is also important to measure the
rate at which an AI service produces genuinely intelligent information. I
propose another metric to evaluate this aspect, named intelligence goodput,
which is discussed in detail in <a href="https://haifengjin.com/intelligence-goodput/">this companion article</a>.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="vae" class="csl-entry">Kingma, D. P., &#38; others. (2014). Auto-encoding variational Bayes. <i>ICLR</i>. <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-2"><div class="csl-bib-body">
  <div data-csl-entry-id="gan" class="csl-entry">Goodfellow, I. J., &#38; others. (2014). Generative adversarial nets. <i>NeurIPS</i>. <a href="#cite-2-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-3"><div class="csl-bib-body">
  <div data-csl-entry-id="stable-diffusion" class="csl-entry">Rombach, R., &#38; others. (2022). High-resolution image synthesis with latent diffusion models. <i>CVPR</i>. <a href="#cite-3-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-4"><div class="csl-bib-body">
  <div data-csl-entry-id="veo2" class="csl-entry">Oord, Aäron van den, &#38; others. (2024). <i>State-of-the-art video and image generation with Veo 2 and Imagen 3</i>. <a href="https://blog.google/technology/google-labs/video-image-generation-update-december-2024/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-4-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-5"><div class="csl-bib-body">
  <div data-csl-entry-id="veo3" class="csl-entry">Hume, T., &#38; others. (2025). <i>Meet Flow: AI-powered filmmaking with Veo 3</i>. <a href="https://blog.google/technology/ai/google-flow-veo-ai-filmmaking-tool/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-5-a" class="reference-back-link">↩</a> <a href="#cite-5-b" class="reference-back-link">↩</a></div>
</div></li><li id="ref-6"><div class="csl-bib-body">
  <div data-csl-entry-id="sora" class="csl-entry">OpenAI. (2024). <i>Sora: System Card</i>. <a href="https://openai.com/index/sora-system-card/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-6-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-7"><div class="csl-bib-body">
  <div data-csl-entry-id="haii" class="csl-entry">Amershi, S., &#38; others. (2019). Guidelines for human-AI interaction. <i>CHI</i>. <a href="#cite-7-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-8"><div class="csl-bib-body">
  <div data-csl-entry-id="a2a" class="csl-entry">Surapaneni, R., &#38; others. (2025). <i>Announcing the Agent2Agent Protocol (A2A)</i>. <a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-8-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-9"><div class="csl-bib-body">
  <div data-csl-entry-id="moores-law-traffic" class="csl-entry">Coffman, K. G., &#38; others. (2002). Internet growth: Is there a “Moore’s Law” for data traffic? <i>Handbook of Massive Data Sets</i>. <a href="#cite-9-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-10"><div class="csl-bib-body">
  <div data-csl-entry-id="twitter" class="csl-entry">Murthy, D. (2018). <i>Twitter</i>. Polity Press Cambridge. <a href="#cite-10-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-11"><div class="csl-bib-body">
  <div data-csl-entry-id="youtube" class="csl-entry">Snickars, P., &#38; Vonderau, P. (2009). <i>The YouTube reader</i>. Kungliga biblioteket. <a href="#cite-11-a" class="reference-back-link">↩</a> <a href="#cite-11-b" class="reference-back-link">↩</a></div>
</div></li><li id="ref-12"><div class="csl-bib-body">
  <div data-csl-entry-id="nielsens-law" class="csl-entry">Nielsen, J. (1998). <i>Nielsen’s law of internet bandwidth</i>. <a href="http://www.useit.com/alertbox/980405.html" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-12-a" class="reference-back-link">↩</a> <a href="#cite-12-b" class="reference-back-link">↩</a></div>
</div></li><li id="ref-13"><div class="csl-bib-body">
  <div data-csl-entry-id="pinterest" class="csl-entry">Gilbert, E., &#38; others. (2013). “ I need to try this”? A statistical overview of Pinterest. <i>CHI</i>. <a href="#cite-13-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-14"><div class="csl-bib-body">
  <div data-csl-entry-id="artificial-analysis" class="csl-entry">Artificial Analysis, Inc. (2024). <i>AI Model &#38; API Providers Analysis</i>. <a href="https://artificialanalysis.ai/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-14-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-15"><div class="csl-bib-body">
  <div data-csl-entry-id="gemini-2-5" class="csl-entry">Team, G. (2025). Gemini 2.5: Pushing the frontier with advanced reasoning, multimodality, long context, and next generation agentic capabilities. <i>arXiv Preprint arXiv:2507.06261</i>. <a href="#cite-15-a" class="reference-back-link">↩</a> <a href="#cite-15-b" class="reference-back-link">↩</a> <a href="#cite-15-c" class="reference-back-link">↩</a></div>
</div></li><li id="ref-16"><div class="csl-bib-body">
  <div data-csl-entry-id="moores-law" class="csl-entry">Moore, G. E., &#38; others. (1965). <i>Cramming more components onto integrated circuits</i>. <a href="#cite-16-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-17"><div class="csl-bib-body">
  <div data-csl-entry-id="reading-speed" class="csl-entry">Brysbaert, M. (2019). How many words do we read per minute? A review and meta-analysis of reading rate. <i>Journal of Memory and Language</i>. <a href="#cite-17-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-18"><div class="csl-bib-body">
  <div data-csl-entry-id="listening-speed" class="csl-entry">Kuperman, V., &#38; others. (2021). A lingering question addressed: Reading rate and most efficient listening rate are highly similar. <i>Journal of Experimental Psychology: Human Perception and Performance</i>. <a href="#cite-18-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-19"><div class="csl-bib-body">
  <div data-csl-entry-id="genie3" class="csl-entry">Parker-Holder, J., &#38; others. (2025). <i>Genie 3: A new frontier for world models</i>. <a href="https://deepmind.google/discover/blog/genie-3-a-new-frontier-for-world-models/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-19-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>Build an ML Framework from Scratch</title>
    <link href="https://haifengjin.com/build-an-ml-framework-from-scratch/"/>
    <id>https://haifengjin.com/build-an-ml-framework-from-scratch/</id>
    <published>2025-04-27T00:00:00+00:00</published>
    <updated>2025-04-27T00:00:00+00:00</updated>
    <summary>An introduction to the basics of ML frameworks</summary>
    <content type="html"><![CDATA[<p>I have always wondered how TensorFlow, PyTorch, or JAX work. However,
their codebases are super complex and unreadable. To find out, I built a
machine learning framework from scratch by myself to learn the basics of
machine learning frameworks.</p>
<p>It only took about 800 lines of Python and C++ code altogether to build the
framework. The following is a basic example of what it is capable of, but you
are also welcome to check out <a href="https://github.com/haifeng-jin/readable-ml-framework/blob/main/examples/classification_example_with_readable_ml_framework.ipynb">the end-to-end classification
example</a>.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">framework</span>

<span class="n">x</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">Tensor</span><span class="o">.</span><span class="n">from_numpy</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">]],</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">Tensor</span><span class="o">.</span><span class="n">from_numpy</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="mf">4.0</span><span class="p">],</span> <span class="p">[</span><span class="mf">5.0</span><span class="p">]],</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span>
<span class="n">z</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>  <span class="c1"># Expected: [[2*4 + 3*5]] = [[23.0]]</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>

<span class="n">loss</span><span class="o">.</span><span class="n">backward</span><span class="p">()</span>  <span class="c1"># Compute the gradients of loss w.r.t. tensor x, y, and z.</span>

<span class="nb">print</span><span class="p">(</span><span class="s2">"x.grad:"</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">grad</span><span class="o">.</span><span class="n">numpy</span><span class="p">())</span>  <span class="c1"># Expected: [[4.0, 5.0]]</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"y.grad:"</span><span class="p">,</span> <span class="n">y</span><span class="o">.</span><span class="n">grad</span><span class="o">.</span><span class="n">numpy</span><span class="p">())</span>  <span class="c1"># Expected: [[2.0], [3.0]]</span>
</code></pre></div>

<p>The codebase is thoroughly commented, which brings it up to around 2000 lines
of code. Everyone can easily read the source code to understand how an ML
framework works and feel more confident about building a new one. This project
is named the "Readable ML Framework" and is available on
<a href="https://github.com/haifeng-jin/readable-ml-framework">GitHub</a>.</p>
<p>This article dives into all the core concepts in ML frameworks citing the
implementations from the repo. We will also discuss other important elements of
ML frameworks beyond the project.</p>
<h2>Why care about ML frameworks?</h2>
<p>ML framework lies in the center of the ML infra. Understanding ML framework
gives you the bigger picture by connecting all the dots, including
CUDA/Triton kernels, GPU/TPU, data pipelining, ML compilers, distributed
training and so on.</p>
<h2>How did I come up with this idea?</h2>
<p>It all started with a question from a friend. He asked me if I knew how to
build a project like TensorFlow from scratch. The answer was absolutely no at
the time. It planted a seed in my mind that grew my curiosity for ML
frameworks through the years. Five years later, I built this project to learn
more about ML frameworks myself.</p>
<p>Having spent years working on machine learning infrastructure, many people
would think that I knew ML frameworks very well, but I did not. Then, I
started to realize that if I did not know much about it, others would not
either unless they worked on building an ML framework directly.</p>
<p>The next question I asked myself was how hard could it be to build a basic ML
framework. To find out, I read the source code of PyTorch. Here are my
<a href="https://github.com/haifeng-jin/pytorch-source">notes</a>. I found it was not
that hard if we got rid of all the complex features.</p>
<p>So, I decided to build one with almost no features except tensor operations and
automatic gradient computation. This "bare-bones" style for educational
projects is largely inspired by <a href="https://github.com/jamiebuilds/the-super-tiny-compiler">the super tiny
compiler</a> project,
which helped me learn what a compiler is.</p>
<h2>Python C++ interfacing</h2>
<p>Let's start with the number one issue that turns the code readers away from the
ML frameworks, Python C++ interfacing. Whenever I wanted to track down a tensor
operation (We call it op/ops in the rest of the article for short) from its
Python interface to its actual implementation in C++, I got lost. It could take
a super complex system to bind all the C++ functions to Python functions. We
call this one-to-one mapping between Python functions and C++ functions
bindings.</p>
<p>In PyTorch, it indeed developed a system called <code>torchgen</code> to generate the
Python C++ bindings. TensorFlow and JAX uses Pybind11, which is a third-party
library to simplify the code for Python C++ bindings. For simplicity and the
readability of the code, we use Pybind11 for our framework.</p>
<p>It works as follows.
* <strong>Write some C++ functions/classes.</strong>
* <strong>Map the functions/classes to Python objects with Pybind11.</strong>  Here is an
  example from
  <a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/core/python_bind.cpp"><code>python_bind.py</code></a>
  of using Pybind11 to present the C++ functions as Python functions.</p>
<div class="highlight"><pre><span></span><code><span class="c1">// "core" is the Python submodule name.</span>
<span class="c1">// Its Python import path is framework.core</span>
<span class="c1">// "m" is the module object.</span>
<span class="n">PYBIND11_MODULE</span><span class="p">(</span><span class="n">core</span><span class="p">,</span><span class="w"> </span><span class="n">m</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="p">...</span>

<span class="w">    </span><span class="c1">// Create a Python submodule named ops under framework.core.</span>
<span class="w">    </span><span class="k">auto</span><span class="w"> </span><span class="n">ops_module</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">m</span><span class="p">.</span><span class="n">def_submodule</span><span class="p">(</span><span class="s">"ops"</span><span class="p">);</span>

<span class="w">    </span><span class="c1">// Bind C++ function ops::matmul to</span>
<span class="w">    </span><span class="c1">// Python function framework.core.ops.matmul().</span>
<span class="w">    </span><span class="n">ops_module</span><span class="p">.</span><span class="n">def</span><span class="p">(</span>
<span class="w">        </span><span class="s">"matmul"</span><span class="p">,</span><span class="w">                  </span><span class="c1">// Function name in Python.</span>
<span class="w">        </span><span class="o">&amp;</span><span class="n">ops</span><span class="o">::</span><span class="n">matmul</span><span class="p">,</span><span class="w">              </span><span class="c1">// Pointer to the C++ function.</span>
<span class="w">        </span><span class="s">"(m, k), (k, n) -&gt; (m, n)"</span><span class="w"> </span><span class="c1">// Docstring of the function in Python.</span>
<span class="w">    </span><span class="p">);</span>
<span class="w">    </span><span class="p">...</span>
<span class="p">}</span>
</code></pre></div>

<ul>
<li><strong>Define the Python module as a C++ extension in <a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/setup.py"><code>setup.py</code></a></strong>.</li>
</ul>
<div class="highlight"><pre><span></span><code><span class="c1"># Define the extension module</span>
<span class="n">ext_modules</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">Pybind11Extension</span><span class="p">(</span>
        <span class="c1"># The Python import path to the C++ module.</span>
        <span class="s2">"framework.core"</span><span class="p">,</span>
        <span class="c1"># Pass in all the C++ files needed to compile the module. We put all</span>
        <span class="c1"># the .cpp and .h files under framework/core dir.</span>
        <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">"framework/core/**/*.cpp"</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
        <span class="c1"># Tell setuptools where to find the pybind11 header files while</span>
        <span class="c1"># compiling the C++ extension module.</span>
        <span class="n">include_dirs</span><span class="o">=</span><span class="p">[</span><span class="n">pybind11</span><span class="o">.</span><span class="n">get_include</span><span class="p">()],</span>
    <span class="p">),</span>
<span class="p">]</span>

<span class="c1"># Setup function</span>
<span class="n">setuptools</span><span class="o">.</span><span class="n">setup</span><span class="p">(</span>
    <span class="o">...</span>
    <span class="c1"># Add the C++ extension module to the package.</span>
    <span class="n">ext_modules</span><span class="o">=</span><span class="n">ext_modules</span><span class="p">,</span>
    <span class="o">...</span>
<span class="p">)</span>
</code></pre></div>

<p>That is it! With the above, we can call the C++ function as a Python function from Python code. </p>
<p>When we build the project, for example, with <code>pip install</code>, the setuptools
will compile the C++ code into a <code>.so</code> file in Linux, which is binary file. It
is also known as a shared object or shared library. When we call the
corresponding Python function in Python, it will automatically convert the
arguments of Python types to the parameters of C++ types, and call the C++
function.</p>
<p>In our framework, it is easy to track down an op. Just look at
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/core/python_bind.cpp"><code>python_bind.py</code></a>
and see which C++ function it is bound to.</p>
<h3>Why mix Python with C++?</h3>
<p>The simple answer is we want the speed of C++ and the simplicity of Python programming.
There are parts of an ML framework that have to be Python and parts that have to be C++/CUDA.</p>
<p>The user-facing APIs have to be in Python. Python is the programming language of
machine learning and AI. All the research scientists and machine learning
engineers love Python. It has an amazing ecosystem of machine learning
libraries. For the optimal user experience, the frameworks have to have
Python APIs.</p>
<p>The underlying data structure of tensors and the tensor ops have to be
implemented in C++/CUDA for speed performance. Python is slow, especially with
its Global Interpreter Lock (GIL) to prevent parallel programming. So we need
C++/CUDA to write efficient data structures for tensors and parallel tensor
ops.</p>
<p>Note there are also ongoing efforts to include the merits of both languages in
one programming language, like the &#128293; <a href="https://www.modular.com/mojo">Mojo programming
language</a>.</p>
<h2>Tensor</h2>
<p>Tensor is the most essential data structure used in an ML framework. To be more
efficient and easier to explicitly manage the memory allocation, they are
usually implemented in C++/CUDA.</p>
<p>In modern ML frameworks, tensors are split into the data and the view. The data
contains the actual values in the tensor occupying a large amount of memory.
The view is how these data are presented to the ops or users.</p>
<p>The data can be a contiguous chunk of memory of floats or integers. The view
can contain the shape of the tensor, the order of the axis, the step size to
iterate the data, and so on.</p>
<p>This data-view split allows many ops to be super efficient. For example, if you
want to transpose a tensor or matrix, you only need to create a new view
indexing the values differently. No copy or modification to the data is needed.</p>
<p>For simplicity, we chose a simple split strategy to implement the <code>Tensor</code>
class in our framework. The data part is always a flattened C++ vector. The view
only contains the shape of the tensor. Data and shape are the two members of
the <code>Tensor</code> class.</p>
<p>For example, the following 2x2 matrix can be represented as <code>shape=[2,2]</code> and <code>data = [1,2,3,4]</code>.
$$
\begin{bmatrix}
  1 & 2 \\
  3 & 4
\end{bmatrix}
$$</p>
<p>Here is our <code>Tensor</code> class.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Tensor</span><span class="w"> </span><span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="w">    </span><span class="cm">/**</span>
<span class="cm">     * @brief The shape of the tensor, represented as a vector of size_t.</span>
<span class="cm">     *</span>
<span class="cm">     * Each element in the vector corresponds to the size of a dimension.</span>
<span class="cm">     * For example, a tensor with shape {2, 3} would be a 2x3 matrix.</span>
<span class="cm">     */</span>
<span class="w">    </span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">size_t</span><span class="o">&gt;</span><span class="w"> </span><span class="n">shape</span><span class="p">;</span>

<span class="w">    </span><span class="cm">/**</span>
<span class="cm">     * @brief The underlying data of the tensor, stored as a contiguous vector</span>
<span class="cm">     * of floats.</span>
<span class="cm">     *</span>
<span class="cm">     * The elements are typically ordered according to a row-major (C-style)</span>
<span class="cm">     * layout, though this class itself doesn't enforce a specific layout.</span>
<span class="cm">     */</span>
<span class="w">    </span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span><span class="w"> </span><span class="n">data</span><span class="p">;</span>

<span class="w">    </span><span class="p">...</span>
<span class="p">}</span>
</code></pre></div>

<p>Read
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/main/framework/core/tensor.h"><code>tensor.h</code></a>
and
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/main/framework/core/tensor.cpp"><code>tensor.cpp</code></a>
for more details.</p>
<h2>Tensor operations</h2>
<p>Implementing the tensor operations is the most challenging part of building our ML framework. We
had to make sure they were parallelized correctly. Let's take a look at
matrix multiplication, one of the most important ops we implemented, and how
backward functions work in general.</p>
<h3>Matrix multiplication</h3>
<p>Matrix multiplication is important because it is so widely used in neural
networks. Optimizing it has a high ROI. The technique we used here is called
tiling.</p>
<p>Let's say we have two tensors (matrices) <code>x</code> and <code>y</code>. Also, we have allocated
memory for the output tensor. Then, <code>output[i,j]</code> is the inner product of the
<code>i</code>th row of <code>x</code> and the <code>j</code>th column of <code>y</code>.</p>
<p>Tiling is a technique that chops large matrices into small blocks that can be
processed in parallel. You split <code>x</code>
into row blocks and <code>y</code> into column blocks, and hand each row-block/column-block
pair to its own task. A task assigned <code>x[a:b,:]</code> and <code>y[:,c:d]</code> produces
<code>output[a:b,c:d]</code>. If the shared dimension is also long, you can chop it too,
computing partial inner products on shorter slices and summing them up
afterward. The sub-blocks produced by all the tasks are then combined (tiled)
into the final output matrix.</p>
<p>There is even fully optimized hardware just for matrix multiplication, such as
the <a href="https://www.nvidia.com/en-us/data-center/tensor-cores/">Tensor Cores</a> in
NVIDIA GPUs and the
<a href="https://cloud.google.com/blog/products/ai-machine-learning/an-in-depth-look-at-googles-first-tensor-processing-unit-tpu">MXUs</a>
in Google's TPUs. To use them, you chop the matrix, feed the blocks to the
ASIC, and tile the output back together.</p>
<h3>Backward functions</h3>
<p>A quick recap of forward and backward functions. The forward function is the
function of the op itself, for example, softmax, matmul, and relu. It computes the
output of the op given the input to the op. The backward function is to compute
the gradients w.r.t. the input given the gradients w.r.t. the output. The
backward function may also access the input and output of the forward function.</p>
<p>The backward function is useful for the neural network optimizers during
backward propagation to compute the gradients of loss w.r.t all the weights of
the neural network.</p>
<p>The following figure shows how gradients are computed. In the forward function,
we compute z using x and y. In the backward function, we compute the x.grad,
y.grad using x, y, and z.grad, where x.grad, y.grad, z.grad are the gradients
of loss w.r.t. x, y, z.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="-60.0 0.0 650 252.29133858267716" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_framework_backward_function_1_p.0"><path d="m0 0l518.57745 0l0 252.29134l-518.57745 0l0 -252.29134z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_framework_backward_function_1_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l518.57745 0l0 252.29134l-518.57745 0z" fill-rule="evenodd"/><path fill="var(--gray-text-color)" d="m308.35696 172.26509l0 0c0 -13.254837 10.745148 -24.0 24.0 -24.0l89.82675 0l0 0c6.365204 0 12.469696 2.5285645 16.970581 7.029434c4.5008545 4.5008698 7.029419 10.605377 7.029419 16.970566l0 0l0 0c0 13.254837 -10.745148 24.0 -24.0 24.0l-89.82675 0c-13.254852 0 -24.0 -10.745163 -24.0 -24.0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m35.850395 70.49606l0 0c0 -9.906338 8.03067 -17.937008 17.937008 -17.937008l0 0c4.757187 0 9.319538 1.8897858 12.683376 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030674 17.937004 -17.937008 17.937004l0 0c-9.906338 0 -17.937008 -8.03067 -17.937008 -17.937004z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m35.850395 70.49606l0 0c0 -9.906338 8.03067 -17.937008 17.937008 -17.937008l0 0c4.757187 0 9.319538 1.8897858 12.683376 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030674 17.937004 -17.937008 17.937004l0 0c-9.906338 0 -17.937008 -8.03067 -17.937008 -17.937004z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m125.267715 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m125.267715 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m80.56693 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937004 -17.937012l0 0c4.757187 0 9.319542 1.8897858 12.68338 5.2536316c3.3638382 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.030678 17.937012 -17.937012 17.937012l0 0c-9.906334 0 -17.937004 -8.03067 -17.937004 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m80.56693 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937004 -17.937012l0 0c4.757187 0 9.319542 1.8897858 12.68338 5.2536316c3.3638382 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.030678 17.937012 -17.937012 17.937012l0 0c-9.906334 0 -17.937004 -8.03067 -17.937004 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m35.834644 52.54331l35.87402 0l0 35.874012l-35.87402 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m49.112274 77.20032l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m125.25197 52.55118l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m140.92023 80.567566l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m80.55118 154.32808l35.874016 0l0 35.874023l-35.874016 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m94.56319 178.98509l0 -1.671875l5.75 -7.0625l-5.59375 0l0 -1.53125l7.484375 0l0 1.6875l-5.859375 7.046875l6.0625 0l0 1.53125l-7.84375 0z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m404.0735 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m404.0735 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m404.05774 154.33595l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m419.72598 182.35234l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m314.65616 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.363861 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m314.65616 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.363861 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m314.6404 154.32808l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m327.91806 178.98509l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m314.63254 70.49606l0 0c0 -9.906338 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.253628c3.3638306 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.03067 17.937004 -17.937012 17.937004l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937004z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m404.04987 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.253628c3.3638306 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m359.3491 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.7571716 0 9.319519 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.253601 7.926193 5.253601 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936981 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m314.6168 52.54331l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m327.89444 77.20032l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m404.03412 52.55118l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m419.70236 80.567566l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m359.33334 154.32808l35.873993 0l0 35.874023l-35.873993 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m373.34534 178.98509l0 -1.671875l5.75 -7.0625l-5.59375 0l0 -1.53125l7.484375 0l0 1.6875l-5.859375 7.046875l6.0625 0l0 1.53125l-7.84375 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m53.771652 88.41732c0 16.477684 11.181107 24.715202 22.362206 32.95536c11.181107 8.240158 22.362206 16.482956 22.362206 32.965897" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m53.771652 88.41732c0 16.477684 11.181107 24.715202 22.362206 32.95536c5.5905533 4.120079 11.181107 8.240814 15.374016 13.391907c1.0482254 1.2877655 2.0091019 2.6399384 2.860794 4.072586c0.4258423 0.7163391 0.8243866 1.452774 1.1929016 2.2113647c0.1842575 0.37928772 0.36101532 0.7641144 0.52991486 1.1547241l0.1465683 0.3496704" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m92.99361 143.17455l4.9522934 8.292465l1.5366135 -9.535675z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m143.18898 88.42519c0 16.475739 -11.173233 24.715256 -22.346458 32.951477c-11.173233 8.236214 -22.346458 16.46914 -22.346458 32.938286" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m143.18898 88.42519c0 16.475739 -11.173233 24.715256 -22.346458 32.951477c-5.5866165 4.1181107 -11.173233 8.23539 -15.36319 13.381798c-1.047493 1.2866058 -2.0076904 2.6375122 -2.85878 4.068863c-0.4255371 0.71565247 -0.82379913 1.4514313 -1.1920624 2.2093048c-0.18412781 0.37893677 -0.36076355 0.7634125 -0.529541 1.1536713l-0.14233398 0.3394928" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m97.51229 141.9075l1.5345535 9.5359955l4.9540863 -8.291382z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m332.5538 88.41732c0 14.96183 11.181091 22.443344 22.362213 29.923653c11.181091 7.480316 22.362213 14.959442 22.362213 29.918869" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m335.29068 100.10105l0.19760132 0.37973785c0.36853027 0.68855286 0.76708984 1.3570251 1.1929321 2.0072327c0.8516846 1.3004227 1.812561 2.5277786 2.8607788 3.6966858c4.1929016 4.6756516 9.783478 8.416107 15.374023 12.156265c11.181091 7.480316 22.362213 14.959442 22.362213 29.918869" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m338.50708 99.34762l-5.286438 -8.0835495l-1.1463623 9.590416z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m421.97113 88.42519c0 14.960037 -11.173218 22.43975 -22.346466 29.920067c-11.173218 7.480316 -22.346436 14.961227 -22.346436 29.922455" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m419.23523 100.109146l-0.19650269 0.3778839c-0.36828613 0.6884613 -0.7665405 1.3568344 -1.1921082 2.0069427c-0.8510437 1.3002243 -1.8112793 2.5274048 -2.8587646 3.6961517c-4.1899414 4.6749725 -9.776581 8.414978 -15.36319 12.155136c-11.173218 7.4803085 -22.346436 14.961227 -22.346436 29.922455" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m422.4517 100.86231l-1.1471558 -9.590317l-5.2857666 8.084z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m254.03055 211.71129l260.03152 0l0 37.16536l-260.03152 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m264.28055 234.79129l0 -1.203125l2.859375 0l0 -5.65625l-2.5 0l0 -1.203125l3.8125 0l0 6.859375l2.71875 0l0 1.203125l-6.890625 0zm3.359375 -9.515625q-0.484375 0 -0.765625 -0.25q-0.265625 -0.25 -0.265625 -0.671875q0 -0.4375 0.265625 -0.6875q0.28125 -0.265625 0.765625 -0.265625q0.484375 0 0.765625 0.265625q0.28125 0.25 0.28125 0.6875q0 0.421875 -0.28125 0.671875q-0.28125 0.25 -0.765625 0.25zm5.5281677 9.515625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm8.793762 2.640625l0 -10.703125l1.328125 0l0 1.546875l0.265625 0l-0.265625 0.296875q0 -0.921875 0.609375 -1.453125q0.609375 -0.53125 1.59375 -0.53125q1.21875 0 1.9375 0.796875q0.734375 0.796875 0.734375 2.1875l0 2.359375q0 0.921875 -0.328125 1.59375q-0.328125 0.671875 -0.921875 1.046875q-0.59375 0.359375 -1.421875 0.359375q-0.96875 0 -1.59375 -0.53125q-0.609375 -0.546875 -0.609375 -1.453125l0.265625 0.3125l-0.296875 0l0.03125 1.828125l0 2.34375l-1.328125 0zm3.09375 -3.640625q0.84375 0 1.3125 -0.5q0.484375 -0.5 0.484375 -1.4375l0 -2.1875q0 -0.9375 -0.484375 -1.4375q-0.46875 -0.5 -1.3125 -0.5q-0.796875 0 -1.28125 0.515625q-0.484375 0.515625 -0.484375 1.421875l0 2.1875q0 0.921875 0.484375 1.4375q0.484375 0.5 1.28125 0.5zm8.731293 1.140625q-1.390625 0 -2.234375 -0.828125q-0.828125 -0.828125 -0.828125 -2.25l0 -5.125l1.328125 0l0 5.125q0 0.921875 0.46875 1.421875q0.46875 0.5 1.265625 0.5q0.828125 0 1.296875 -0.5q0.484375 -0.5 0.484375 -1.421875l0 -5.125l1.3125 0l0 5.125q0 1.421875 -0.84375 2.25q-0.84375 0.828125 -2.25 0.828125zm9.700012 -0.140625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.6531677 0.125q-0.8125 0 -1.421875 -0.25q-0.609375 -0.265625 -0.96875 -0.734375q-0.359375 -0.46875 -0.4375 -1.125l1.328125 0q0.078125 0.4375 0.46875 0.6875q0.390625 0.234375 1.03125 0.234375l0.625 0q0.78125 0 1.15625 -0.3125q0.375 -0.3125 0.375 -0.84375q0 -0.5 -0.34375 -0.796875q-0.34375 -0.3125 -1.015625 -0.40625l-1.078125 -0.1875q-1.1875 -0.203125 -1.765625 -0.734375q-0.578125 -0.53125 -0.578125 -1.5625q0 -1.078125 0.6875 -1.671875q0.6875 -0.59375 2.0625 -0.59375l0.5625 0q1.15625 0 1.859375 0.546875q0.703125 0.53125 0.84375 1.453125l-1.3125 0q-0.09375 -0.375 -0.453125 -0.59375q-0.34375 -0.234375 -0.9375 -0.234375l-0.5625 0q-0.75 0 -1.09375 0.28125q-0.34375 0.265625 -0.34375 0.828125q0 0.5 0.296875 0.734375q0.296875 0.234375 0.921875 0.328125l1.078125 0.1875q1.3125 0.203125 1.90625 0.765625q0.59375 0.546875 0.59375 1.609375q0 1.109375 -0.71875 1.75q-0.703125 0.640625 -2.140625 0.640625l-0.625 0zm18.72818 -0.125q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.903137 0.125q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm18.056305 1.203125q-0.984375 0 -1.59375 -0.53125q-0.59375 -0.53125 -0.59375 -1.453125l0.265625 0.3125l-0.265625 0l0 1.53125l-1.328125 0l0 -10.703125l1.328125 0l0 2.34375l-0.03125 1.84375l0.296875 0l-0.265625 0.296875q0 -0.90625 0.609375 -1.4375q0.609375 -0.546875 1.578125 -0.546875q1.21875 0 1.953125 0.8125q0.734375 0.796875 0.734375 2.1875l0 2.359375q0 1.375 -0.734375 2.1875q-0.734375 0.796875 -1.953125 0.796875zm-0.4375 -1.140625q0.84375 0 1.328125 -0.5q0.484375 -0.5 0.484375 -1.4375l0 -2.1875q0 -0.9375 -0.484375 -1.4375q-0.484375 -0.5 -1.328125 -0.5q-0.796875 0 -1.28125 0.515625q-0.46875 0.515625 -0.46875 1.421875l0 2.1875q0 0.921875 0.46875 1.4375q0.484375 0.5 1.28125 0.5zm8.059418 1.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm9.231262 1.03125q-0.96875 0 -1.703125 -0.359375q-0.71875 -0.375 -1.109375 -1.0625q-0.390625 -0.703125 -0.390625 -1.65625l0 -2.1875q0 -0.96875 0.390625 -1.65625q0.390625 -0.6875 1.109375 -1.046875q0.734375 -0.375 1.703125 -0.375q1.390625 0 2.234375 0.75q0.859375 0.734375 0.90625 2.03125l-1.328125 0q-0.03125 -0.78125 -0.515625 -1.1875q-0.46875 -0.421875 -1.296875 -0.421875q-0.859375 0 -1.375 0.5q-0.515625 0.484375 -0.515625 1.390625l0 2.203125q0 0.921875 0.515625 1.421875q0.515625 0.484375 1.375 0.484375q0.828125 0 1.296875 -0.421875q0.484375 -0.421875 0.515625 -1.1875l1.328125 0q-0.046875 1.296875 -0.90625 2.046875q-0.84375 0.734375 -2.234375 0.734375zm5.7312927 -0.140625l0 -10.703125l1.328125 0l0 5.96875l1.546875 0l2.34375 -3.328125l1.515625 0l-2.71875 3.84375l2.75 4.21875l-1.53125 0l-2.34375 -3.59375l-1.5625 0l0 3.59375l-1.328125 0zm9.200012 0l-1.28125 -8.0625l1.171875 0l0.796875 5.71875q0.046875 0.328125 0.09375 0.703125q0.0625 0.359375 0.09375 0.609375q0.03125 -0.25 0.09375 -0.609375q0.0625 -0.375 0.109375 -0.703125l0.890625 -5.71875l1.265625 0l0.890625 5.71875q0.046875 0.328125 0.109375 0.703125q0.0625 0.359375 0.09375 0.609375q0.03125 -0.25 0.09375 -0.609375q0.0625 -0.375 0.109375 -0.703125l0.84375 -5.71875l1.109375 0l-1.34375 8.0625l-1.453125 0l-0.875 -5.5625q-0.046875 -0.421875 -0.125 -0.828125q-0.078125 -0.40625 -0.109375 -0.640625q-0.015625 0.234375 -0.078125 0.640625q-0.0625 0.40625 -0.140625 0.828125l-0.90625 5.5625l-1.453125 0zm10.684418 0.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm6.403137 0.890625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0zm11.090668 0.140625q-1.203125 0 -1.9375 -0.796875q-0.734375 -0.8125 -0.734375 -2.1875l0 -2.359375q0 -1.390625 0.734375 -2.1875q0.734375 -0.8125 1.9375 -0.8125q1.0 0 1.59375 0.546875q0.609375 0.53125 0.609375 1.4375l-0.265625 -0.296875l0.296875 0l-0.03125 -1.84375l0 -2.34375l1.328125 0l0 10.703125l-1.328125 0l0 -1.53125l-0.265625 0l0.265625 -0.3125q0 0.921875 -0.609375 1.453125q-0.59375 0.53125 -1.59375 0.53125zm0.453125 -1.140625q0.8125 0 1.28125 -0.5q0.46875 -0.515625 0.46875 -1.4375l0 -2.1875q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.328125 0.5q-0.484375 0.5 -0.484375 1.4375l0 2.1875q0 0.9375 0.484375 1.4375q0.484375 0.5 1.328125 0.5zm16.431305 1.0l0 -6.046875l-2.34375 0l0 -1.203125l2.34375 0l0 -1.390625q0 -0.953125 0.59375 -1.5q0.609375 -0.5625 1.671875 -0.5625l2.359375 0l0 1.203125l-2.359375 0q-0.453125 0 -0.703125 0.21875q-0.234375 0.203125 -0.234375 0.640625l0 1.390625l3.296875 0l0 1.203125l-3.296875 0l0 6.046875l-1.328125 0zm9.950012 0.140625q-1.390625 0 -2.234375 -0.828125q-0.828125 -0.828125 -0.828125 -2.25l0 -5.125l1.328125 0l0 5.125q0 0.921875 0.46875 1.421875q0.46875 0.5 1.265625 0.5q0.828125 0 1.296875 -0.5q0.484375 -0.5 0.484375 -1.421875l0 -5.125l1.3125 0l0 5.125q0 1.421875 -0.84375 2.25q-0.84375 0.828125 -2.25 0.828125zm5.7625427 -0.140625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm11.903137 0.140625q-0.96875 0 -1.703125 -0.359375q-0.71875 -0.375 -1.109375 -1.0625q-0.390625 -0.703125 -0.390625 -1.65625l0 -2.1875q0 -0.96875 0.390625 -1.65625q0.390625 -0.6875 1.109375 -1.046875q0.734375 -0.375 1.703125 -0.375q1.390625 0 2.234375 0.75q0.859375 0.734375 0.90625 2.03125l-1.328125 0q-0.03125 -0.78125 -0.515625 -1.1875q-0.46875 -0.421875 -1.296875 -0.421875q-0.859375 0 -1.375 0.5q-0.515625 0.484375 -0.515625 1.390625l0 2.203125q0 0.921875 0.515625 1.421875q0.515625 0.484375 1.375 0.484375q0.828125 0 1.296875 -0.421875q0.484375 -0.421875 0.515625 -1.1875l1.328125 0q-0.046875 1.296875 -0.90625 2.046875q-0.84375 0.734375 -2.234375 0.734375zm9.621918 -0.140625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm4.762512 0l0 -1.203125l2.859375 0l0 -5.65625l-2.5 0l0 -1.203125l3.8125 0l0 6.859375l2.71875 0l0 1.203125l-6.890625 0zm3.359375 -9.515625q-0.484375 0 -0.765625 -0.25q-0.265625 -0.25 -0.265625 -0.671875q0 -0.4375 0.265625 -0.6875q0.28125 -0.265625 0.765625 -0.265625q0.484375 0 0.765625 0.265625q0.28125 0.25 0.28125 0.6875q0 0.421875 -0.28125 0.671875q-0.28125 0.25 -0.765625 0.25zm8.575043 9.640625q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm5.746887 1.0625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m37.771652 211.71129l77.98425 0l0 37.16536l-77.98425 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m52.052902 234.79129q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.9031525 0.140625q-0.953125 0 -1.671875 -0.375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.625l0 -2.1875q0 -0.9375 0.375 -1.625q0.390625 -0.703125 1.09375 -1.078125q0.71875 -0.375 1.671875 -0.375q0.953125 0 1.65625 0.375q0.71875 0.375 1.109375 1.078125q0.390625 0.6875 0.390625 1.625l0 1.421875l-5.015625 0l0 0.765625q0 0.953125 0.484375 1.46875q0.484375 0.5 1.375 0.5q0.765625 0 1.234375 -0.265625q0.46875 -0.28125 0.578125 -0.8125l1.3125 0q-0.125 1.0 -0.984375 1.59375q-0.859375 0.59375 -2.140625 0.59375zm1.875 -4.734375l0 -0.53125q0 -0.953125 -0.484375 -1.46875q-0.46875 -0.515625 -1.390625 -0.515625q-0.890625 0 -1.375 0.515625q-0.484375 0.515625 -0.484375 1.46875l0 0.421875l3.828125 0l-0.09375 0.109375zm3.8719025 4.59375l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm11.590652 0.125q-0.8125 0 -1.421875 -0.25q-0.609375 -0.265625 -0.96875 -0.734375q-0.359375 -0.46875 -0.4375 -1.125l1.328125 0q0.078125 0.4375 0.46875 0.6875q0.390625 0.234375 1.03125 0.234375l0.625 0q0.78125 0 1.15625 -0.3125q0.375 -0.3125 0.375 -0.84375q0 -0.5 -0.34375 -0.796875q-0.34375 -0.3125 -1.015625 -0.40625l-1.078125 -0.1875q-1.1875 -0.203125 -1.765625 -0.734375q-0.578125 -0.53125 -0.578125 -1.5625q0 -1.078125 0.6875 -1.671875q0.6875 -0.59375 2.0625 -0.59375l0.5625 0q1.15625 0 1.859375 0.546875q0.703125 0.53125 0.84375 1.453125l-1.3125 0q-0.09375 -0.375 -0.453125 -0.59375q-0.34375 -0.234375 -0.9375 -0.234375l-0.5625 0q-0.75 0 -1.09375 0.28125q-0.34375 0.265625 -0.34375 0.828125q0 0.5 0.296875 0.734375q0.296875 0.234375 0.921875 0.328125l1.078125 0.1875q1.3125 0.203125 1.90625 0.765625q0.59375 0.546875 0.59375 1.609375q0 1.109375 -0.71875 1.75q-0.703125 0.640625 -2.140625 0.640625l-0.625 0zm9.043777 0q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm6.0281525 1.0625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0z" fill-rule="nonzero"/><path fill="var(--gray-text-color)" d="m211.77034 230.27821l0 0c0 -6.1403656 4.9777527 -11.118103 11.118118 -11.118103l22.425186 0l0 0c2.9487152 0 5.776642 1.1713562 7.8616943 3.2564087c2.0850525 2.0850525 3.256424 4.9129944 3.256424 7.8616943l0 0l0 0c0 6.1403656 -4.9777527 11.118118 -11.118118 11.118118l-22.425186 0l0 0c-6.1403656 0 -11.118118 -4.9777527 -11.118118 -11.118118z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m15.535433 230.27916l0 0c0 -6.1403656 4.977747 -11.118118 11.118111 -11.118118l0 0c2.9487057 0 5.77664 1.1713715 7.8616924 3.256424c2.0850487 2.0850525 3.2564163 4.912979 3.2564163 7.8616943l0 0c0 6.1403656 -4.977745 11.118103 -11.118109 11.118103l0 0c-6.1403637 0 -11.118111 -4.9777374 -11.118111 -11.118103z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m15.535433 230.27916l0 0c0 -6.1403656 4.977747 -11.118118 11.118111 -11.118118l0 0c2.9487057 0 5.77664 1.1713715 7.8616924 3.256424c2.0850487 2.0850525 3.2564163 4.912979 3.2564163 7.8616943l0 0c0 6.1403656 -4.977745 11.118103 -11.118109 11.118103l0 0c-6.1403637 0 -11.118111 -4.9777374 -11.118111 -11.118103z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m121.35171 230.29347l0 0c0 -6.149063 4.977745 -11.133865 11.118111 -11.133865l0 0c2.9487 0 5.776642 1.1730347 7.8616943 3.261032c2.0850525 2.0880127 3.2564087 4.9199524 3.2564087 7.8728333l0 0c0 6.149063 -4.9777374 11.13385 -11.118103 11.13385l0 0c-6.1403656 0 -11.118111 -4.984787 -11.118111 -11.13385z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m143.58792 211.71129l77.98425 0l0 37.16536l-77.98425 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m154.9473 237.43192l0 -1.203125l2.5625 0q0.625 0 0.90625 -0.28125q0.28125 -0.28125 0.28125 -0.859375l0 -1.03125l0.03125 -1.46875l-0.265625 0l0.25 -0.21875q0 0.890625 -0.59375 1.40625q-0.578125 0.5 -1.5625 0.5q-1.25 0 -1.96875 -0.8125q-0.703125 -0.8125 -0.703125 -2.1875l0 -1.703125q0 -1.375 0.703125 -2.171875q0.71875 -0.8125 1.96875 -0.8125q0.984375 0 1.5625 0.515625q0.59375 0.5 0.59375 1.390625l-0.25 -0.21875l0.25 0l0 -1.546875l1.3125 0l0 8.359375q0 1.078125 -0.671875 1.703125q-0.671875 0.640625 -1.859375 0.640625l-2.546875 0zm2.015625 -4.296875q0.8125 0 1.28125 -0.515625q0.46875 -0.515625 0.46875 -1.421875l0 -1.53125q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.3125 0.5q-0.453125 0.5 -0.453125 1.4375l0 1.53125q0 0.9375 0.453125 1.4375q0.46875 0.5 1.3125 0.5zm6.0437775 1.65625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0zm10.856277 0.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm8.700027 1.03125q-1.203125 0 -1.9375 -0.796875q-0.734375 -0.8125 -0.734375 -2.1875l0 -2.359375q0 -1.390625 0.734375 -2.1875q0.734375 -0.8125 1.9375 -0.8125q1.0 0 1.59375 0.546875q0.609375 0.53125 0.609375 1.4375l-0.265625 -0.296875l0.296875 0l-0.03125 -1.84375l0 -2.34375l1.328125 0l0 10.703125l-1.328125 0l0 -1.53125l-0.265625 0l0.265625 -0.3125q0 0.921875 -0.609375 1.453125q-0.59375 0.53125 -1.59375 0.53125zm0.453125 -1.140625q0.8125 0 1.28125 -0.5q0.46875 -0.515625 0.46875 -1.4375l0 -2.1875q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.328125 0.5q-0.484375 0.5 -0.484375 1.4375l0 2.1875q0 0.9375 0.484375 1.4375q0.484375 0.5 1.328125 0.5z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m0 0l206.11023 0l0 42.015747l-206.11023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m10.765625 26.919998l0 -13.640625l8.03125 0l0 1.53125l-6.375 0l0 4.53125l5.890625 0l0 1.53125l-5.859375 0l0 6.046875l-1.6875 0zm15.021881 0.15625q-1.234375 0 -2.125 -0.46875q-0.890625 -0.46875 -1.390625 -1.359375q-0.5 -0.890625 -0.5 -2.09375l0 -2.734375q0 -1.21875 0.5 -2.09375q0.5 -0.890625 1.390625 -1.34375q0.890625 -0.46875 2.125 -0.46875q1.234375 0 2.125 0.46875q0.90625 0.453125 1.390625 1.34375q0.5 0.875 0.5 2.078125l0 2.75q0 1.203125 -0.5 2.09375q-0.484375 0.890625 -1.390625 1.359375q-0.890625 0.46875 -2.125 0.46875zm0 -1.5q1.109375 0 1.71875 -0.609375q0.609375 -0.625 0.609375 -1.8125l0 -2.734375q0 -1.1875 -0.609375 -1.796875q-0.609375 -0.625 -1.71875 -0.625q-1.078125 0 -1.703125 0.625q-0.625 0.609375 -0.625 1.796875l0 2.734375q0 1.1875 0.625 1.8125q0.625 0.609375 1.703125 0.609375zm7.678131 1.34375l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm11.412506 0l-1.640625 -10.265625l1.484375 0l1.03125 7.28125q0.0625 0.40625 0.125 0.890625q0.0625 0.46875 0.09375 0.78125q0.046875 -0.3125 0.125 -0.78125q0.078125 -0.484375 0.140625 -0.890625l1.140625 -7.28125l1.609375 0l1.125 7.28125q0.0625 0.40625 0.140625 0.890625q0.09375 0.46875 0.125 0.78125q0.03125 -0.3125 0.109375 -0.78125q0.09375 -0.484375 0.15625 -0.890625l1.0625 -7.28125l1.421875 0l-1.71875 10.265625l-1.84375 0l-1.109375 -7.09375q-0.078125 -0.515625 -0.171875 -1.03125q-0.09375 -0.53125 -0.125 -0.828125q-0.03125 0.296875 -0.109375 0.828125q-0.078125 0.515625 -0.171875 1.03125l-1.15625 7.09375l-1.84375 0zm13.600006 0.1875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm8.162506 1.140625l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm14.115631 0.1875q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625zm18.590637 1.265625l0 -13.640625l8.03125 0l0 1.53125l-6.375 0l0 4.53125l5.890625 0l0 1.53125l-5.859375 0l0 6.046875l-1.6875 0zm15.006256 0.1875q-1.765625 0 -2.84375 -1.046875q-1.0625 -1.0625 -1.0625 -2.875l0 -6.53125l1.6875 0l0 6.53125q0 1.15625 0.59375 1.8125q0.59375 0.640625 1.625 0.640625q1.046875 0 1.640625 -0.640625q0.609375 -0.65625 0.609375 -1.8125l0 -6.53125l1.6875 0l0 6.53125q0 1.8125 -1.09375 2.875q-1.078125 1.046875 -2.84375 1.046875zm7.334381 -0.1875l0 -10.265625l1.671875 0l0 1.96875l0.359375 0l-0.359375 0.390625q0 -1.21875 0.71875 -1.875q0.734375 -0.671875 2.03125 -0.671875q1.546875 0 2.453125 0.953125q0.921875 0.953125 0.921875 2.59375l0 6.90625l-1.6875 0l0 -6.71875q0 -1.09375 -0.59375 -1.6875q-0.578125 -0.59375 -1.578125 -0.59375q-1.046875 0 -1.65625 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 6.53125l-1.671875 0zm15.146881 0.1875q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125zm12.240631 -0.1875q-1.328125 0 -2.0625 -0.703125q-0.734375 -0.71875 -0.734375 -2.0l0 -6.03125l-2.890625 0l0 -1.53125l2.890625 0l0 -2.890625l1.671875 0l0 2.890625l4.109375 0l0 1.53125l-4.109375 0l0 6.03125q0 1.171875 1.125 1.171875l2.796875 0l0 1.53125l-2.796875 0zm6.053131 0l0 -1.53125l3.640625 0l0 -7.203125l-3.171875 0l0 -1.53125l4.859375 0l0 8.734375l3.453125 0l0 1.53125l-8.78125 0zm4.296875 -12.109375q-0.609375 0 -0.96875 -0.3125q-0.359375 -0.328125 -0.359375 -0.859375q0 -0.5625 0.359375 -0.890625q0.359375 -0.328125 0.96875 -0.328125q0.625 0 0.96875 0.328125q0.359375 0.328125 0.359375 0.890625q0 0.53125 -0.359375 0.859375q-0.34375 0.3125 -0.96875 0.3125zm10.912506 12.265625q-1.234375 0 -2.125 -0.46875q-0.890625 -0.46875 -1.390625 -1.359375q-0.5 -0.890625 -0.5 -2.09375l0 -2.734375q0 -1.21875 0.5 -2.09375q0.5 -0.890625 1.390625 -1.34375q0.890625 -0.46875 2.125 -0.46875q1.234375 0 2.125 0.46875q0.90625 0.453125 1.390625 1.34375q0.5 0.875 0.5 2.078125l0 2.75q0 1.203125 -0.5 2.09375q-0.484375 0.890625 -1.390625 1.359375q-0.890625 0.46875 -2.125 0.46875zm0 -1.5q1.109375 0 1.71875 -0.609375q0.609375 -0.625 0.609375 -1.8125l0 -2.734375q0 -1.1875 -0.609375 -1.796875q-0.609375 -0.625 -1.71875 -0.625q-1.078125 0 -1.703125 0.625q-0.625 0.609375 -0.625 1.796875l0 2.734375q0 1.1875 0.625 1.8125q0.625 0.609375 1.703125 0.609375zm7.318756 1.34375l0 -10.265625l1.671875 0l0 1.96875l0.359375 0l-0.359375 0.390625q0 -1.21875 0.71875 -1.875q0.734375 -0.671875 2.03125 -0.671875q1.546875 0 2.453125 0.953125q0.921875 0.953125 0.921875 2.59375l0 6.90625l-1.6875 0l0 -6.71875q0 -1.09375 -0.59375 -1.6875q-0.578125 -0.59375 -1.578125 -0.59375q-1.046875 0 -1.65625 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 6.53125l-1.671875 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m270.79004 0l215.49606 0l0 42.015747l-215.49606 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m281.5244 26.919998l0 -13.625l3.859375 0q1.875 0 2.921875 0.9375q1.0625 0.9375 1.0625 2.59375q0 0.9375 -0.40625 1.625q-0.390625 0.6875 -1.109375 1.0625q-0.71875 0.375 -1.71875 0.359375l0 -0.28125q1.078125 -0.015625 1.875 0.40625q0.796875 0.421875 1.234375 1.234375q0.4375 0.796875 0.4375 1.90625q0 1.15625 -0.484375 2.015625q-0.484375 0.84375 -1.390625 1.3125q-0.890625 0.453125 -2.140625 0.453125l-4.140625 0zm1.640625 -1.515625l2.3125 0q1.171875 0 1.859375 -0.625q0.6875 -0.625 0.6875 -1.703125q0 -1.125 -0.6875 -1.8125q-0.6875 -0.703125 -1.859375 -0.703125l-2.3125 0l0 4.84375zm0 -6.296875l2.203125 0q1.078125 0 1.703125 -0.578125q0.625 -0.578125 0.625 -1.578125q0 -0.984375 -0.625 -1.5625q-0.609375 -0.578125 -1.6875 -0.578125l-2.21875 0l0 4.296875zm12.521881 8.0q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm11.756256 1.328125q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125zm7.287506 -0.1875l0 -13.625l1.6875 0l0 7.59375l1.96875 0l2.984375 -4.234375l1.921875 0l-3.4375 4.890625l3.5 5.375l-1.953125 0l-2.984375 -4.578125l-2.0 0l0 4.578125l-1.6875 0zm11.725006 0l-1.640625 -10.265625l1.484375 0l1.03125 7.28125q0.0625 0.40625 0.125 0.890625q0.0625 0.46875 0.09375 0.78125q0.046875 -0.3125 0.125 -0.78125q0.078125 -0.484375 0.140625 -0.890625l1.140625 -7.28125l1.609375 0l1.125 7.28125q0.0625 0.40625 0.140625 0.890625q0.09375 0.46875 0.125 0.78125q0.03125 -0.3125 0.109375 -0.78125q0.09375 -0.484375 0.15625 -0.890625l1.0625 -7.28125l1.421875 0l-1.71875 10.265625l-1.84375 0l-1.109375 -7.09375q-0.078125 -0.515625 -0.171875 -1.03125q-0.09375 -0.53125 -0.125 -0.828125q-0.03125 0.296875 -0.109375 0.828125q-0.078125 0.515625 -0.171875 1.03125l-1.15625 7.09375l-1.84375 0zm13.600006 0.1875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm8.162506 1.140625l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm14.115631 0.1875q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625zm18.590637 1.265625l0 -13.640625l8.03125 0l0 1.53125l-6.375 0l0 4.53125l5.890625 0l0 1.53125l-5.859375 0l0 6.046875l-1.6875 0zm15.006256 0.1875q-1.765625 0 -2.84375 -1.046875q-1.0625 -1.0625 -1.0625 -2.875l0 -6.53125l1.6875 0l0 6.53125q0 1.15625 0.59375 1.8125q0.59375 0.640625 1.625 0.640625q1.046875 0 1.640625 -0.640625q0.609375 -0.65625 0.609375 -1.8125l0 -6.53125l1.6875 0l0 6.53125q0 1.8125 -1.09375 2.875q-1.078125 1.046875 -2.84375 1.046875zm7.334381 -0.1875l0 -10.265625l1.671875 0l0 1.96875l0.359375 0l-0.359375 0.390625q0 -1.21875 0.71875 -1.875q0.734375 -0.671875 2.03125 -0.671875q1.546875 0 2.453125 0.953125q0.921875 0.953125 0.921875 2.59375l0 6.90625l-1.6875 0l0 -6.71875q0 -1.09375 -0.59375 -1.6875q-0.578125 -0.59375 -1.578125 -0.59375q-1.046875 0 -1.65625 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 6.53125l-1.671875 0zm15.146881 0.1875q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125zm12.240631 -0.1875q-1.328125 0 -2.0625 -0.703125q-0.734375 -0.71875 -0.734375 -2.0l0 -6.03125l-2.890625 0l0 -1.53125l2.890625 0l0 -2.890625l1.671875 0l0 2.890625l4.109375 0l0 1.53125l-4.109375 0l0 6.03125q0 1.171875 1.125 1.171875l2.796875 0l0 1.53125l-2.796875 0zm6.053131 0l0 -1.53125l3.640625 0l0 -7.203125l-3.171875 0l0 -1.53125l4.859375 0l0 8.734375l3.453125 0l0 1.53125l-8.78125 0zm4.296875 -12.109375q-0.609375 0 -0.96875 -0.3125q-0.359375 -0.328125 -0.359375 -0.859375q0 -0.5625 0.359375 -0.890625q0.359375 -0.328125 0.96875 -0.328125q0.625 0 0.96875 0.328125q0.359375 0.328125 0.359375 0.890625q0 0.53125 -0.359375 0.859375q-0.34375 0.3125 -0.96875 0.3125zm10.912506 12.265625q-1.234375 0 -2.125 -0.46875q-0.890625 -0.46875 -1.390625 -1.359375q-0.5 -0.890625 -0.5 -2.09375l0 -2.734375q0 -1.21875 0.5 -2.09375q0.5 -0.890625 1.390625 -1.34375q0.890625 -0.46875 2.125 -0.46875q1.234375 0 2.125 0.46875q0.90625 0.453125 1.390625 1.34375q0.5 0.875 0.5 2.078125l0 2.75q0 1.203125 -0.5 2.09375q-0.484375 0.890625 -1.390625 1.359375q-0.890625 0.46875 -2.125 0.46875zm0 -1.5q1.109375 0 1.71875 -0.609375q0.609375 -0.625 0.609375 -1.8125l0 -2.734375q0 -1.1875 -0.609375 -1.796875q-0.609375 -0.625 -1.71875 -0.625q-1.078125 0 -1.703125 0.625q-0.625 0.609375 -0.625 1.796875l0 2.734375q0 1.1875 0.625 1.8125q0.625 0.609375 1.703125 0.609375zm7.318756 1.34375l0 -10.265625l1.671875 0l0 1.96875l0.359375 0l-0.359375 0.390625q0 -1.21875 0.71875 -1.875q0.734375 -0.671875 2.03125 -0.671875q1.546875 0 2.453125 0.953125q0.921875 0.953125 0.921875 2.59375l0 6.90625l-1.6875 0l0 -6.71875q0 -1.09375 -0.59375 -1.6875q-0.578125 -0.59375 -1.578125 -0.59375q-1.046875 0 -1.65625 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 6.53125l-1.671875 0z" fill-rule="nonzero"/></g></svg></figure>

<p>To put it in math, if the forward function is $\mathbf{z}=f(\mathbf{x},
\mathbf{y})$. The backward function is to compute $\partial L / \partial
\mathbf{x}$ and $\partial L / \partial \mathbf{y}$ where $L$ is the loss. We
can compute them using the chain rule as follows.</p>
<p>$$
\frac{\partial L}{\partial \mathbf{x}} = \frac{\partial L}{\partial \mathbf{z}} \cdot \frac{\partial \mathbf{z}}{\partial \mathbf{x}}
$$
$$
\frac{\partial L}{\partial \mathbf{y}} = \frac{\partial L}{\partial \mathbf{z}} \cdot \frac{\partial \mathbf{z}}{\partial \mathbf{y}}
$$</p>
<p>In practice, $\mathbf{x}$, $\mathbf{y}$, $\mathbf{z}$, and $\frac{\partial
L}{\partial \mathbf{z}}$ are known at the time we call the backward function.
We need to compute $\frac{\partial L}{\partial \mathbf{x}}$ using them. As a
straightforward way to compute it, we only need to compute $\frac{\partial
\mathbf{z}}{\partial \mathbf{x}}$, and $\frac{\partial \mathbf{z}}{\partial
\mathbf{y}}$, which are computed using $\mathbf{x}$ and $\mathbf{y}$.</p>
<p>For a simplified example, given $z = f(x, y) = x \times y$, $\partial L /
\partial z = 3$, $x=4$, $y=5$, $z=20$ are scalars, we have
$$
\frac{\partial z}{\partial x} = y, \\
\frac{\partial z}{\partial y} = x.
$$
So, we can compute the gradients as follows.
$$
\frac{\partial L}{\partial x} = \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial x} = \frac{\partial L}{\partial z} \cdot y = 3\cdot 5 = 15
$$
$$
\frac{\partial L}{\partial y} = \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial y} = \frac{\partial L}{\partial z} \cdot x = 3\cdot 4 = 12
$$</p>
<p>Note that in the example above, the output of the forward function, $z$, is not
directly involved.  However, we may still need the output in some cases as a
trick for performance (running speed) optimization, which is where
recomputing comes in.</p>
<h3>Recomputing</h3>
<p>Some backward functions need the forward function's output, not just its
input, to compute gradients. That leaves two ways to implement them: store the
output when the forward function runs and reuse it in the backward function,
or discard it and recompute it from the input when the backward function
needs it.</p>
<p>Why recompute the output if we already have it saved somewhere? The memory it's
saved in could be slow to load from. Recomputing the output and writing it to
a local fast-but-small memory can sometimes even be faster than loading it
from a big-and-slow memory.</p>
<p>The same trade-off shows up at a much larger scale in training deep neural
networks. Normally, every layer's outputs, called activations, are kept in
memory during the forward pass so the backward pass can load them instead of
recomputing them. If we feel recomputing is actually faster for some of the
layers, we do not save those tensors during the forward pass, and recompute
them on demand during the backward pass instead. This technique of only
keeping a subset of activations and recomputing the rest on demand is known
as <strong>activation checkpointing</strong>. It substantially cuts peak memory usage,
helping fit large model training workloads into limited memory.</p>
<p>Please refer to
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/core/ops.h"><code>ops.h</code></a>
and
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/core/ops.cpp"><code>ops.cpp</code></a>
for the ops implementations.</p>
<h2>Automatic differentiation</h2>
<p>Automatic differentiation, also known as autodiff or autograd, is a mechanism
to compute the gradients of the loss w.r.t. the weights of the neural network
automatically. The user only defines the forward pass. The framework records
the ops performed on each tensor and computes the gradients. This is a core
feature of an ML framework.</p>
<p>Before we dive into the implementation, There are a few things we need to
clarify.</p>
<h3>Compute graph</h3>
<p>The following figure shows the compute graphs of the forward pass and the
backward pass of a super simple neural network, whose inputs are <code>a</code>, <code>b</code>, 
<code>d</code>, and the output is <code>e</code>.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="-60 -20 650 400" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_framework_backward_2_p.0"><path d="m0 0l518.2756 0l0 370.07086l-518.2756 0l0 -370.07086z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_framework_backward_2_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l518.2756 0l0 370.07086l-518.2756 0z" fill-rule="evenodd"/><path fill="var(--gray-text-color)" d="m316.35696 172.26509l0 0c0 -13.254837 10.745148 -24.0 24.0 -24.0l89.82675 0l0 0c6.365204 0 12.469696 2.5285645 16.970581 7.029434c4.5008545 4.5008698 7.029419 10.605377 7.029419 16.970566l0 0l0 0c0 13.254837 -10.745148 24.0 -24.0 24.0l-89.82675 0c-13.254852 0 -24.0 -10.745163 -24.0 -24.0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m19.850393 70.49606l0 0c0 -9.906338 8.030672 -17.937008 17.93701 -17.937008l0 0c4.757187 0 9.319538 1.8897858 12.68338 5.253628c3.3638382 3.363842 5.253628 7.926193 5.253628 12.68338l0 0c0 9.906334 -8.030674 17.937004 -17.937008 17.937004l0 0c-9.906338 0 -17.93701 -8.03067 -17.93701 -17.937004z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m19.850393 70.49606l0 0c0 -9.906338 8.030672 -17.937008 17.93701 -17.937008l0 0c4.757187 0 9.319538 1.8897858 12.68338 5.253628c3.3638382 3.363842 5.253628 7.926193 5.253628 12.68338l0 0c0 9.906334 -8.030674 17.937004 -17.937008 17.937004l0 0c-9.906338 0 -17.93701 -8.03067 -17.93701 -17.937004z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m109.267715 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m109.267715 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.253628c3.3638458 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m64.56693 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937004 -17.937012l0 0c4.757187 0 9.319542 1.8897858 12.68338 5.2536316c3.3638382 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.030678 17.937012 -17.937012 17.937012l0 0c-9.906334 0 -17.937004 -8.03067 -17.937004 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m64.56693 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937004 -17.937012l0 0c4.757187 0 9.319542 1.8897858 12.68338 5.2536316c3.3638382 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.030678 17.937012 -17.937012 17.937012l0 0c-9.906334 0 -17.937004 -8.03067 -17.937004 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m153.98425 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536163 7.926193 5.2536163 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936996 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m153.98425 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757187 0 9.319534 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536163 7.926193 5.2536163 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936996 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m109.27559 274.9685l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757187 0 9.319534 1.889801 12.68338 5.2536316c3.3638458 3.3638306 5.2536316 7.926178 5.2536316 12.68338l0 0c0 9.906342 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m109.27559 274.9685l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757187 0 9.319534 1.889801 12.68338 5.2536316c3.3638458 3.3638306 5.2536316 7.926178 5.2536316 12.68338l0 0c0 9.906342 -8.030685 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m19.834646 52.54331l35.874016 0l0 35.874012l-35.874016 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m36.8779 77.38782q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m109.25197 52.55118l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m127.7796 77.39569q-1.265625 0 -2.03125 -0.671875q-0.765625 -0.6875 -0.765625 -1.859375l0.34375 0.390625l-0.34375 0l0 1.953125l-1.671875 0l0 -13.625004l1.671875 0l0 2.9843788l-0.03125 2.34375l0.375 0l-0.34375 0.390625q0 -1.15625 0.765625 -1.84375q0.78125 -0.703125 2.03125 -0.703125q1.5625 0 2.484375 1.03125q0.9375 1.03125 0.9375 2.796875l0 3.0q0 1.765625 -0.9375 2.796875q-0.921875 1.015625 -2.484375 1.015625zm-0.546875 -1.453125q1.0625 0 1.671875 -0.625q0.609375 -0.640625 0.609375 -1.84375l0 -2.796875q0 -1.1875 -0.609375 -1.828125q-0.609375 -0.640625 -1.671875 -0.640625q-1.03125 0 -1.640625 0.65625q-0.609375 0.65625 -0.609375 1.8125l0 2.796875q0 1.15625 0.609375 1.8125q0.609375 0.65625 1.640625 0.65625z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m64.55118 154.32808l35.874016 0l0 35.874023l-35.874016 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m82.56319 179.17259q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m153.9685 154.33595l35.87401 0l0 35.874023l-35.87401 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m171.30864 179.18047q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m109.25984 257.01575l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m127.193726 281.86026q-1.203125 0 -2.109375 -0.484375q-0.90625 -0.484375 -1.40625 -1.375q-0.5 -0.890625 -0.5 -2.0625l0 -2.796875q0 -1.1875 0.5 -2.0625q0.5 -0.890625 1.40625 -1.375q0.90625 -0.484375 2.109375 -0.484375q1.21875 0 2.125 0.484375q0.90625 0.484375 1.390625 1.375q0.5 0.875 0.5 2.0625l0 1.8125l-6.375 0l0 0.984375q0 1.21875 0.609375 1.859375q0.609375 0.640625 1.75 0.640625q0.96875 0 1.5625 -0.34375q0.609375 -0.34375 0.734375 -1.03125l1.6875 0q-0.171875 1.28125 -1.265625 2.046875q-1.09375 0.75 -2.71875 0.75zm2.375 -6.03125l0 -0.6875q0 -1.21875 -0.609375 -1.859375q-0.609375 -0.65625 -1.765625 -0.65625q-1.140625 0 -1.75 0.65625q-0.609375 0.640625 -0.609375 1.859375l0 0.546875l4.859375 0l-0.125 0.140625z" fill-rule="nonzero"/><path fill="var(--gray-text-color)" d="m361.0971 274.9685l0 0c0 -13.254822 10.745178 -24.0 24.0 -24.0l89.82678 0l0 0c6.365204 0 12.469696 2.5285645 16.97055 7.0294495c4.500885 4.5008545 7.0294495 10.605347 7.0294495 16.97055l0 0l0 0c0 13.254822 -10.745178 24.0 -24.0 24.0l-89.82678 0c-13.254822 0 -24.0 -10.745178 -24.0 -24.0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m367.35696 274.9882l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.7571716 0 9.319519 1.889801 12.68338 5.2536316c3.3638306 3.3638306 5.253601 7.926178 5.253601 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936981 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m367.35696 274.9882l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.7571716 0 9.319519 1.889801 12.68338 5.2536316c3.3638306 3.3638306 5.253601 7.926178 5.253601 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936981 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m456.7743 274.99606l0 0c0 -9.906342 8.03067 -17.937012 17.936981 -17.937012l0 0c4.757202 0 9.31955 1.889801 12.68338 5.2536316c3.363861 3.3638306 5.2536316 7.926178 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906311 0 -17.936981 -8.03067 -17.936981 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m456.7743 274.99606l0 0c0 -9.906342 8.03067 -17.937012 17.936981 -17.937012l0 0c4.757202 0 9.31955 1.889801 12.68338 5.2536316c3.363861 3.3638306 5.2536316 7.926178 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906311 0 -17.936981 -8.03067 -17.936981 -17.937012z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m412.0735 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m412.0735 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m412.05774 154.33595l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m430.58536 179.18047q-1.265625 0 -2.03125 -0.671875q-0.765625 -0.6875 -0.765625 -1.859375l0.34375 0.390625l-0.34375 0l0 1.953125l-1.671875 0l0 -13.625l1.671875 0l0 2.984375l-0.03125 2.34375l0.375 0l-0.34375 0.390625q0 -1.15625 0.765625 -1.84375q0.78125 -0.703125 2.03125 -0.703125q1.5625 0 2.484375 1.03125q0.9375 1.03125 0.9375 2.796875l0 3.0q0 1.765625 -0.9375 2.796875q-0.921875 1.015625 -2.484375 1.015625zm-0.546875 -1.453125q1.0625 0 1.671875 -0.625q0.609375 -0.640625 0.609375 -1.84375l0 -2.796875q0 -1.1875 -0.609375 -1.828125q-0.609375 -0.640625 -1.671875 -0.640625q-1.03125 0 -1.640625 0.65625q-0.609375 0.65625 -0.609375 1.8125l0 2.796875q0 1.15625 0.609375 1.8125q0.609375 0.65625 1.640625 0.65625z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m322.65616 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.363861 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m322.65616 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.363861 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m367.34122 257.03543l35.873993 0l0 35.874023l-35.873993 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m385.3532 281.87994q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m456.75854 257.0433l35.873993 0l0 35.874023l-35.873993 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m474.09866 281.88782q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m322.6404 154.32808l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m339.6837 179.17259q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m412.05774 274.9685l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.757202 0 9.31955 1.889801 12.68338 5.2536316c3.3638306 3.3638306 5.2536316 7.926178 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m412.042 257.01575l35.874023 0l0 35.874023l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m429.97586 281.86026q-1.203125 0 -2.109375 -0.484375q-0.90625 -0.484375 -1.40625 -1.375q-0.5 -0.890625 -0.5 -2.0625l0 -2.796875q0 -1.1875 0.5 -2.0625q0.5 -0.890625 1.40625 -1.375q0.90625 -0.484375 2.109375 -0.484375q1.21875 0 2.125 0.484375q0.90625 0.484375 1.390625 1.375q0.5 0.875 0.5 2.0625l0 1.8125l-6.375 0l0 0.984375q0 1.21875 0.609375 1.859375q0.609375 0.640625 1.75 0.640625q0.96875 0 1.5625 -0.34375q0.609375 -0.34375 0.734375 -1.03125l1.6875 0q-0.171875 1.28125 -1.265625 2.046875q-1.09375 0.75 -2.71875 0.75zm2.375 -6.03125l0 -0.6875q0 -1.21875 -0.609375 -1.859375q-0.609375 -0.65625 -1.765625 -0.65625q-1.140625 0 -1.75 0.65625q-0.609375 0.640625 -0.609375 1.859375l0 0.546875l4.859375 0l-0.125 0.140625z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m322.63254 70.49606l0 0c0 -9.906338 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.253628c3.3638306 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.03067 17.937004 -17.937012 17.937004l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937004z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m412.04987 70.50394l0 0c0 -9.906334 8.03067 -17.937008 17.937012 -17.937008l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.253628c3.3638306 3.363842 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906334 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906342 0 -17.937012 -8.030678 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m367.3491 172.28084l0 0c0 -9.906342 8.03067 -17.937012 17.937012 -17.937012l0 0c4.7571716 0 9.319519 1.8897858 12.68338 5.2536316c3.3638306 3.3638458 5.253601 7.926193 5.253601 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.936981 17.937012l0 0c-9.906342 0 -17.937012 -8.03067 -17.937012 -17.937012z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m456.76642 172.28871l0 0c0 -9.906326 8.03067 -17.937012 17.936981 -17.937012l0 0c4.757202 0 9.31955 1.8897858 12.68338 5.2536316c3.363861 3.3638458 5.2536316 7.926193 5.2536316 12.68338l0 0c0 9.906342 -8.03067 17.937012 -17.937012 17.937012l0 0c-9.906311 0 -17.936981 -8.03067 -17.936981 -17.937012z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m456.75067 154.33595l35.873993 0l0 35.874023l-35.873993 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m474.0908 179.18047q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m322.6168 52.54331l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m339.66006 77.38782q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m412.03412 52.55118l35.874023 0l0 35.874012l-35.874023 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m430.56174 77.39569q-1.265625 0 -2.03125 -0.671875q-0.765625 -0.6875 -0.765625 -1.859375l0.34375 0.390625l-0.34375 0l0 1.953125l-1.671875 0l0 -13.625004l1.671875 0l0 2.9843788l-0.03125 2.34375l0.375 0l-0.34375 0.390625q0 -1.15625 0.765625 -1.84375q0.78125 -0.703125 2.03125 -0.703125q1.5625 0 2.484375 1.03125q0.9375 1.03125 0.9375 2.796875l0 3.0q0 1.765625 -0.9375 2.796875q-0.921875 1.015625 -2.484375 1.015625zm-0.546875 -1.453125q1.0625 0 1.671875 -0.625q0.609375 -0.640625 0.609375 -1.84375l0 -2.796875q0 -1.1875 -0.609375 -1.828125q-0.609375 -0.640625 -1.671875 -0.640625q-1.03125 0 -1.640625 0.65625q-0.609375 0.65625 -0.609375 1.8125l0 2.796875q0 1.15625 0.609375 1.8125q0.609375 0.65625 1.640625 0.65625z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m367.33334 154.32808l35.873993 0l0 35.874023l-35.873993 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m385.34534 179.17259q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m37.771652 88.41732c0 16.477684 11.181103 24.715202 22.362206 32.95536c11.181107 8.240158 22.362206 16.482956 22.362206 32.965897" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m37.771652 88.41732c0 16.477684 11.181103 24.715202 22.362206 32.95536c5.5905533 4.120079 11.181107 8.240814 15.374016 13.391907c1.0482254 1.2877655 2.0091019 2.6399384 2.860794 4.072586c0.4258423 0.7163391 0.8243866 1.452774 1.1929016 2.2113647c0.1842575 0.37928772 0.36101532 0.7641144 0.52991486 1.1547241l0.14657593 0.3496704" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m76.99361 143.17455l4.9522934 8.292465l1.5366135 -9.535675z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m127.18898 88.42519c0 16.475739 -11.173233 24.715256 -22.346458 32.951477c-11.173233 8.236214 -22.346458 16.46914 -22.346458 32.938286" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m127.18898 88.42519c0 16.475739 -11.173233 24.715256 -22.346458 32.951477c-5.5866165 4.1181107 -11.173233 8.23539 -15.36319 13.381798c-1.047493 1.2866058 -2.0076904 2.6375122 -2.85878 4.068863c-0.4255371 0.71565247 -0.82380676 1.4514313 -1.1920624 2.2093048c-0.18412781 0.37893677 -0.36075592 0.7634125 -0.529541 1.1536713l-0.14233398 0.3394928" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m81.51229 141.9075l1.5345535 9.5359955l4.9540863 -8.291382z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m82.48819 190.2021c0 16.70346 11.181099 25.056519 22.362206 33.40692c11.181099 8.350388 22.362206 16.698105 22.362206 33.396225" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m82.48819 190.2021c0 16.703445 11.181099 25.056534 22.362206 33.40692c5.5905533 4.1752014 11.181099 8.349716 15.374016 13.567703c1.0482254 1.3045044 2.0091019 2.6742249 2.8607864 4.125473c0.4258423 0.7256317 0.8243942 1.4716339 1.1929016 2.2400665c0.18426514 0.38420105 0.36101532 0.7740326 0.5299225 1.169693l0.20545197 0.496521" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m121.76595 245.81387l4.9108276 8.317093l1.5842056 -9.527878z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m171.90552 190.20998c0 16.701447 -11.181107 25.05252 -22.362213 33.40291c-11.181107 8.350388 -22.362206 16.700119 -22.362206 33.400238" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m171.90552 190.20998c0 16.701462 -11.181107 25.052505 -22.362213 33.40291c-5.5905457 4.175186 -11.181107 8.35022 -15.374008 13.56897c-1.048233 1.3046875 -2.0091095 2.6746063 -2.860794 4.126068c-0.4258423 0.72572327 -0.8243866 1.4718475 -1.1929016 2.240387c-0.18426514 0.38426208 -0.36102295 0.77415466 -0.5299225 1.1698914l-0.20605469 0.49804688" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m126.13207 244.61101l1.5847015 9.5278015l4.910408 -8.317337z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m340.5538 88.41732c0 14.96183 11.181091 22.443344 22.362213 29.923653c11.181091 7.480316 22.362213 14.959442 22.362213 29.918869" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m343.29068 100.10105l0.19760132 0.37973785c0.36853027 0.68855286 0.76708984 1.3570251 1.1929016 2.0072327c0.8517151 1.3004227 1.8125916 2.5277786 2.8608093 3.6966858c4.1929016 4.6756516 9.783478 8.416107 15.374023 12.156265c11.181091 7.480316 22.362213 14.959442 22.362213 29.918869" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m346.50708 99.34762l-5.286438 -8.0835495l-1.1463623 9.590416z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m429.97113 88.42519c0 14.960037 -11.173218 22.43975 -22.346466 29.920067c-11.173218 7.480316 -22.346436 14.961227 -22.346436 29.922455" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m427.23523 100.109146l-0.19650269 0.3778839c-0.36828613 0.6884613 -0.7665405 1.3568344 -1.1921082 2.0069427c-0.8510437 1.3002243 -1.8112793 2.5274048 -2.8587646 3.6961517c-4.1899414 4.6749725 -9.776581 8.414978 -15.36319 12.155136c-11.173218 7.4803085 -22.346436 14.961227 -22.346436 29.922455" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m430.4517 100.86231l-1.1471558 -9.590317l-5.2857666 8.084z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m385.27036 190.2021c0 15.191711 11.188965 22.788925 22.37793 30.383423c11.188995 7.5944824 22.37796 15.1862335 22.37796 30.372482" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m387.92703 201.90433l0.00894165 0.018508911c0.08843994 0.17736816 0.17874146 0.35342407 0.27093506 0.5282135c0.3687439 0.699173 0.7675781 1.3779297 1.1937256 2.038147c0.85232544 1.3204346 1.8138428 2.566681 2.8628235 3.7535706c4.195862 4.747589 9.790344 8.545517 15.384827 12.342758c11.188995 7.5944977 22.37796 15.1862335 22.37796 30.372482" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m391.14853 201.17296l-5.230896 -8.119614l-1.2120972 9.582336z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m474.68765 190.20998c0 15.189575 -11.165344 22.784683 -22.330688 30.379166c-11.165375 7.5944824 -22.330719 15.18837 -22.330719 30.37674" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m472.03522 201.91316l-0.0075683594 0.015731812c-0.08822632 0.17732239 -0.17837524 0.35336304 -0.27035522 0.5281067c-0.36798096 0.6990509 -0.7659912 1.3776855 -1.1912231 2.037796c-0.85046387 1.3202057 -1.8099976 2.5662231 -2.8567505 3.7529297c-4.1870117 4.7467804 -9.769684 8.544159 -15.352356 12.341415c-11.165375 7.5944824 -22.330719 15.18837 -22.330719 30.37674" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m475.257 202.64334l-1.2155762 -9.581879l-5.227936 8.121506z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m254.03055 323.71127l260.03152 0l0 37.165375l-260.03152 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m264.28055 346.7913l0 -1.203125l2.859375 0l0 -5.65625l-2.5 0l0 -1.203125l3.8125 0l0 6.859375l2.71875 0l0 1.203125l-6.890625 0zm3.359375 -9.515625q-0.484375 0 -0.765625 -0.25q-0.265625 -0.25 -0.265625 -0.671875q0 -0.4375 0.265625 -0.6875q0.28125 -0.265625 0.765625 -0.265625q0.484375 0 0.765625 0.265625q0.28125 0.25 0.28125 0.6875q0 0.421875 -0.28125 0.671875q-0.28125 0.25 -0.765625 0.25zm5.5281677 9.515625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm8.793762 2.640625l0 -10.703125l1.328125 0l0 1.546875l0.265625 0l-0.265625 0.296875q0 -0.921875 0.609375 -1.453125q0.609375 -0.53125 1.59375 -0.53125q1.21875 0 1.9375 0.796875q0.734375 0.796875 0.734375 2.1875l0 2.359375q0 0.921875 -0.328125 1.59375q-0.328125 0.671875 -0.921875 1.046875q-0.59375 0.359375 -1.421875 0.359375q-0.96875 0 -1.59375 -0.53125q-0.609375 -0.546875 -0.609375 -1.453125l0.265625 0.3125l-0.296875 0l0.03125 1.828125l0 2.34375l-1.328125 0zm3.09375 -3.640625q0.84375 0 1.3125 -0.5q0.484375 -0.5 0.484375 -1.4375l0 -2.1875q0 -0.9375 -0.484375 -1.4375q-0.46875 -0.5 -1.3125 -0.5q-0.796875 0 -1.28125 0.515625q-0.484375 0.515625 -0.484375 1.421875l0 2.1875q0 0.921875 0.484375 1.4375q0.484375 0.5 1.28125 0.5zm8.731293 1.140625q-1.390625 0 -2.234375 -0.828125q-0.828125 -0.828125 -0.828125 -2.25l0 -5.125l1.328125 0l0 5.125q0 0.921875 0.46875 1.421875q0.46875 0.5 1.265625 0.5q0.828125 0 1.296875 -0.5q0.484375 -0.5 0.484375 -1.421875l0 -5.125l1.3125 0l0 5.125q0 1.421875 -0.84375 2.25q-0.84375 0.828125 -2.25 0.828125zm9.700012 -0.140625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.6531677 0.125q-0.8125 0 -1.421875 -0.25q-0.609375 -0.265625 -0.96875 -0.734375q-0.359375 -0.46875 -0.4375 -1.125l1.328125 0q0.078125 0.4375 0.46875 0.6875q0.390625 0.234375 1.03125 0.234375l0.625 0q0.78125 0 1.15625 -0.3125q0.375 -0.3125 0.375 -0.84375q0 -0.5 -0.34375 -0.796875q-0.34375 -0.3125 -1.015625 -0.40625l-1.078125 -0.1875q-1.1875 -0.203125 -1.765625 -0.734375q-0.578125 -0.53125 -0.578125 -1.5625q0 -1.078125 0.6875 -1.671875q0.6875 -0.59375 2.0625 -0.59375l0.5625 0q1.15625 0 1.859375 0.546875q0.703125 0.53125 0.84375 1.453125l-1.3125 0q-0.09375 -0.375 -0.453125 -0.59375q-0.34375 -0.234375 -0.9375 -0.234375l-0.5625 0q-0.75 0 -1.09375 0.28125q-0.34375 0.265625 -0.34375 0.828125q0 0.5 0.296875 0.734375q0.296875 0.234375 0.921875 0.328125l1.078125 0.1875q1.3125 0.203125 1.90625 0.765625q0.59375 0.546875 0.59375 1.609375q0 1.109375 -0.71875 1.75q-0.703125 0.640625 -2.140625 0.640625l-0.625 0zm18.72818 -0.125q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.903137 0.125q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm18.056305 1.203125q-0.984375 0 -1.59375 -0.53125q-0.59375 -0.53125 -0.59375 -1.453125l0.265625 0.3125l-0.265625 0l0 1.53125l-1.328125 0l0 -10.703125l1.328125 0l0 2.34375l-0.03125 1.84375l0.296875 0l-0.265625 0.296875q0 -0.90625 0.609375 -1.4375q0.609375 -0.546875 1.578125 -0.546875q1.21875 0 1.953125 0.8125q0.734375 0.796875 0.734375 2.1875l0 2.359375q0 1.375 -0.734375 2.1875q-0.734375 0.796875 -1.953125 0.796875zm-0.4375 -1.140625q0.84375 0 1.328125 -0.5q0.484375 -0.5 0.484375 -1.4375l0 -2.1875q0 -0.9375 -0.484375 -1.4375q-0.484375 -0.5 -1.328125 -0.5q-0.796875 0 -1.28125 0.515625q-0.46875 0.515625 -0.46875 1.421875l0 2.1875q0 0.921875 0.46875 1.4375q0.484375 0.5 1.28125 0.5zm8.059418 1.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm9.231262 1.03125q-0.96875 0 -1.703125 -0.359375q-0.71875 -0.375 -1.109375 -1.0625q-0.390625 -0.703125 -0.390625 -1.65625l0 -2.1875q0 -0.96875 0.390625 -1.65625q0.390625 -0.6875 1.109375 -1.046875q0.734375 -0.375 1.703125 -0.375q1.390625 0 2.234375 0.75q0.859375 0.734375 0.90625 2.03125l-1.328125 0q-0.03125 -0.78125 -0.515625 -1.1875q-0.46875 -0.421875 -1.296875 -0.421875q-0.859375 0 -1.375 0.5q-0.515625 0.484375 -0.515625 1.390625l0 2.203125q0 0.921875 0.515625 1.421875q0.515625 0.484375 1.375 0.484375q0.828125 0 1.296875 -0.421875q0.484375 -0.421875 0.515625 -1.1875l1.328125 0q-0.046875 1.296875 -0.90625 2.046875q-0.84375 0.734375 -2.234375 0.734375zm5.7312927 -0.140625l0 -10.703125l1.328125 0l0 5.96875l1.546875 0l2.34375 -3.328125l1.515625 0l-2.71875 3.84375l2.75 4.21875l-1.53125 0l-2.34375 -3.59375l-1.5625 0l0 3.59375l-1.328125 0zm9.200012 0l-1.28125 -8.0625l1.171875 0l0.796875 5.71875q0.046875 0.328125 0.09375 0.703125q0.0625 0.359375 0.09375 0.609375q0.03125 -0.25 0.09375 -0.609375q0.0625 -0.375 0.109375 -0.703125l0.890625 -5.71875l1.265625 0l0.890625 5.71875q0.046875 0.328125 0.109375 0.703125q0.0625 0.359375 0.09375 0.609375q0.03125 -0.25 0.09375 -0.609375q0.0625 -0.375 0.109375 -0.703125l0.84375 -5.71875l1.109375 0l-1.34375 8.0625l-1.453125 0l-0.875 -5.5625q-0.046875 -0.421875 -0.125 -0.828125q-0.078125 -0.40625 -0.109375 -0.640625q-0.015625 0.234375 -0.078125 0.640625q-0.0625 0.40625 -0.140625 0.828125l-0.90625 5.5625l-1.453125 0zm10.684418 0.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm6.403137 0.890625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0zm11.090668 0.140625q-1.203125 0 -1.9375 -0.796875q-0.734375 -0.8125 -0.734375 -2.1875l0 -2.359375q0 -1.390625 0.734375 -2.1875q0.734375 -0.8125 1.9375 -0.8125q1.0 0 1.59375 0.546875q0.609375 0.53125 0.609375 1.4375l-0.265625 -0.296875l0.296875 0l-0.03125 -1.84375l0 -2.34375l1.328125 0l0 10.703125l-1.328125 0l0 -1.53125l-0.265625 0l0.265625 -0.3125q0 0.921875 -0.609375 1.453125q-0.59375 0.53125 -1.59375 0.53125zm0.453125 -1.140625q0.8125 0 1.28125 -0.5q0.46875 -0.515625 0.46875 -1.4375l0 -2.1875q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.328125 0.5q-0.484375 0.5 -0.484375 1.4375l0 2.1875q0 0.9375 0.484375 1.4375q0.484375 0.5 1.328125 0.5zm16.431305 1.0l0 -6.046875l-2.34375 0l0 -1.203125l2.34375 0l0 -1.390625q0 -0.953125 0.59375 -1.5q0.609375 -0.5625 1.671875 -0.5625l2.359375 0l0 1.203125l-2.359375 0q-0.453125 0 -0.703125 0.21875q-0.234375 0.203125 -0.234375 0.640625l0 1.390625l3.296875 0l0 1.203125l-3.296875 0l0 6.046875l-1.328125 0zm9.950012 0.140625q-1.390625 0 -2.234375 -0.828125q-0.828125 -0.828125 -0.828125 -2.25l0 -5.125l1.328125 0l0 5.125q0 0.921875 0.46875 1.421875q0.46875 0.5 1.265625 0.5q0.828125 0 1.296875 -0.5q0.484375 -0.5 0.484375 -1.421875l0 -5.125l1.3125 0l0 5.125q0 1.421875 -0.84375 2.25q-0.84375 0.828125 -2.25 0.828125zm5.7625427 -0.140625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm11.903137 0.140625q-0.96875 0 -1.703125 -0.359375q-0.71875 -0.375 -1.109375 -1.0625q-0.390625 -0.703125 -0.390625 -1.65625l0 -2.1875q0 -0.96875 0.390625 -1.65625q0.390625 -0.6875 1.109375 -1.046875q0.734375 -0.375 1.703125 -0.375q1.390625 0 2.234375 0.75q0.859375 0.734375 0.90625 2.03125l-1.328125 0q-0.03125 -0.78125 -0.515625 -1.1875q-0.46875 -0.421875 -1.296875 -0.421875q-0.859375 0 -1.375 0.5q-0.515625 0.484375 -0.515625 1.390625l0 2.203125q0 0.921875 0.515625 1.421875q0.515625 0.484375 1.375 0.484375q0.828125 0 1.296875 -0.421875q0.484375 -0.421875 0.515625 -1.1875l1.328125 0q-0.046875 1.296875 -0.90625 2.046875q-0.84375 0.734375 -2.234375 0.734375zm9.621918 -0.140625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm4.762512 0l0 -1.203125l2.859375 0l0 -5.65625l-2.5 0l0 -1.203125l3.8125 0l0 6.859375l2.71875 0l0 1.203125l-6.890625 0zm3.359375 -9.515625q-0.484375 0 -0.765625 -0.25q-0.265625 -0.25 -0.265625 -0.671875q0 -0.4375 0.265625 -0.6875q0.28125 -0.265625 0.765625 -0.265625q0.484375 0 0.765625 0.265625q0.28125 0.25 0.28125 0.6875q0 0.421875 -0.28125 0.671875q-0.28125 0.25 -0.765625 0.25zm8.575043 9.640625q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm5.746887 1.0625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m37.771652 323.71127l77.98425 0l0 37.165375l-77.98425 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m52.052902 346.7913q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm7.9031525 0.140625q-0.953125 0 -1.671875 -0.375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.625l0 -2.1875q0 -0.9375 0.375 -1.625q0.390625 -0.703125 1.09375 -1.078125q0.71875 -0.375 1.671875 -0.375q0.953125 0 1.65625 0.375q0.71875 0.375 1.109375 1.078125q0.390625 0.6875 0.390625 1.625l0 1.421875l-5.015625 0l0 0.765625q0 0.953125 0.484375 1.46875q0.484375 0.5 1.375 0.5q0.765625 0 1.234375 -0.265625q0.46875 -0.28125 0.578125 -0.8125l1.3125 0q-0.125 1.0 -0.984375 1.59375q-0.859375 0.59375 -2.140625 0.59375zm1.875 -4.734375l0 -0.53125q0 -0.953125 -0.484375 -1.46875q-0.46875 -0.515625 -1.390625 -0.515625q-0.890625 0 -1.375 0.515625q-0.484375 0.515625 -0.484375 1.46875l0 0.421875l3.828125 0l-0.09375 0.109375zm3.8719025 4.59375l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0zm11.590652 0.125q-0.8125 0 -1.421875 -0.25q-0.609375 -0.265625 -0.96875 -0.734375q-0.359375 -0.46875 -0.4375 -1.125l1.328125 0q0.078125 0.4375 0.46875 0.6875q0.390625 0.234375 1.03125 0.234375l0.625 0q0.78125 0 1.15625 -0.3125q0.375 -0.3125 0.375 -0.84375q0 -0.5 -0.34375 -0.796875q-0.34375 -0.3125 -1.015625 -0.40625l-1.078125 -0.1875q-1.1875 -0.203125 -1.765625 -0.734375q-0.578125 -0.53125 -0.578125 -1.5625q0 -1.078125 0.6875 -1.671875q0.6875 -0.59375 2.0625 -0.59375l0.5625 0q1.15625 0 1.859375 0.546875q0.703125 0.53125 0.84375 1.453125l-1.3125 0q-0.09375 -0.375 -0.453125 -0.59375q-0.34375 -0.234375 -0.9375 -0.234375l-0.5625 0q-0.75 0 -1.09375 0.28125q-0.34375 0.265625 -0.34375 0.828125q0 0.5 0.296875 0.734375q0.296875 0.234375 0.921875 0.328125l1.078125 0.1875q1.3125 0.203125 1.90625 0.765625q0.59375 0.546875 0.59375 1.609375q0 1.109375 -0.71875 1.75q-0.703125 0.640625 -2.140625 0.640625l-0.625 0zm9.043777 0q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm6.0281525 1.0625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0z" fill-rule="nonzero"/><path fill="var(--gray-text-color)" d="m211.77034 342.27823l0 0c0 -6.140381 4.9777527 -11.118134 11.118118 -11.118134l22.425186 0l0 0c2.9487152 0 5.776642 1.1713867 7.8616943 3.2564392c2.0850525 2.0850525 3.256424 4.912964 3.256424 7.8616943l0 0l0 0c0 6.1403503 -4.9777527 11.118103 -11.118118 11.118103l-22.425186 0l0 0c-6.1403656 0 -11.118118 -4.9777527 -11.118118 -11.118103z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m15.535433 342.27914l0 0c0 -6.1403503 4.977747 -11.118103 11.118111 -11.118103l0 0c2.9487057 0 5.77664 1.1713867 7.8616924 3.2564087c2.0850487 2.0850525 3.2564163 4.9129944 3.2564163 7.8616943l0 0c0 6.140381 -4.977745 11.118134 -11.118109 11.118134l0 0c-6.1403637 0 -11.118111 -4.9777527 -11.118111 -11.118134z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m15.535433 342.27914l0 0c0 -6.1403503 4.977747 -11.118103 11.118111 -11.118103l0 0c2.9487057 0 5.77664 1.1713867 7.8616924 3.2564087c2.0850487 2.0850525 3.2564163 4.9129944 3.2564163 7.8616943l0 0c0 6.140381 -4.977745 11.118134 -11.118109 11.118134l0 0c-6.1403637 0 -11.118111 -4.9777527 -11.118111 -11.118134z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m121.35171 342.29346l0 0c0 -6.149048 4.977745 -11.13385 11.118111 -11.13385l0 0c2.9487 0 5.776642 1.1730347 7.8616943 3.2610474c2.0850525 2.0879822 3.2564087 4.919922 3.2564087 7.8728027l0 0c0 6.1490784 -4.9777374 11.133881 -11.118103 11.133881l0 0c-6.1403656 0 -11.118111 -4.9848022 -11.118111 -11.133881z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m143.58792 323.71127l77.98425 0l0 37.165375l-77.98425 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m154.9473 349.43192l0 -1.203125l2.5625 0q0.625 0 0.90625 -0.28125q0.28125 -0.28125 0.28125 -0.859375l0 -1.03125l0.03125 -1.46875l-0.265625 0l0.25 -0.21875q0 0.890625 -0.59375 1.40625q-0.578125 0.5 -1.5625 0.5q-1.25 0 -1.96875 -0.8125q-0.703125 -0.8125 -0.703125 -2.1875l0 -1.703125q0 -1.375 0.703125 -2.171875q0.71875 -0.8125 1.96875 -0.8125q0.984375 0 1.5625 0.515625q0.59375 0.5 0.59375 1.390625l-0.25 -0.21875l0.25 0l0 -1.546875l1.3125 0l0 8.359375q0 1.078125 -0.671875 1.703125q-0.671875 0.640625 -1.859375 0.640625l-2.546875 0zm2.015625 -4.296875q0.8125 0 1.28125 -0.515625q0.46875 -0.515625 0.46875 -1.421875l0 -1.53125q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.3125 0.5q-0.453125 0.5 -0.453125 1.4375l0 1.53125q0 0.9375 0.453125 1.4375q0.46875 0.5 1.3125 0.5zm6.0437775 1.65625l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0zm10.856277 0.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm8.700027 1.03125q-1.203125 0 -1.9375 -0.796875q-0.734375 -0.8125 -0.734375 -2.1875l0 -2.359375q0 -1.390625 0.734375 -2.1875q0.734375 -0.8125 1.9375 -0.8125q1.0 0 1.59375 0.546875q0.609375 0.53125 0.609375 1.4375l-0.265625 -0.296875l0.296875 0l-0.03125 -1.84375l0 -2.34375l1.328125 0l0 10.703125l-1.328125 0l0 -1.53125l-0.265625 0l0.265625 -0.3125q0 0.921875 -0.609375 1.453125q-0.59375 0.53125 -1.59375 0.53125zm0.453125 -1.140625q0.8125 0 1.28125 -0.5q0.46875 -0.515625 0.46875 -1.4375l0 -2.1875q0 -0.90625 -0.46875 -1.421875q-0.46875 -0.515625 -1.28125 -0.515625q-0.84375 0 -1.328125 0.5q-0.484375 0.5 -0.484375 1.4375l0 2.1875q0 0.9375 0.484375 1.4375q0.484375 0.5 1.328125 0.5z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m0 0l165.00787 0l0 42.015747l-165.00787 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m10.765625 26.919998l0 -13.640625l8.03125 0l0 1.53125l-6.375 0l0 4.53125l5.890625 0l0 1.53125l-5.859375 0l0 6.046875l-1.6875 0zm15.021881 0.15625q-1.234375 0 -2.125 -0.46875q-0.890625 -0.46875 -1.390625 -1.359375q-0.5 -0.890625 -0.5 -2.09375l0 -2.734375q0 -1.21875 0.5 -2.09375q0.5 -0.890625 1.390625 -1.34375q0.890625 -0.46875 2.125 -0.46875q1.234375 0 2.125 0.46875q0.90625 0.453125 1.390625 1.34375q0.5 0.875 0.5 2.078125l0 2.75q0 1.203125 -0.5 2.09375q-0.484375 0.890625 -1.390625 1.359375q-0.890625 0.46875 -2.125 0.46875zm0 -1.5q1.109375 0 1.71875 -0.609375q0.609375 -0.625 0.609375 -1.8125l0 -2.734375q0 -1.1875 -0.609375 -1.796875q-0.609375 -0.625 -1.71875 -0.625q-1.078125 0 -1.703125 0.625q-0.625 0.609375 -0.625 1.796875l0 2.734375q0 1.1875 0.625 1.8125q0.625 0.609375 1.703125 0.609375zm7.678131 1.34375l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm11.412506 0l-1.640625 -10.265625l1.484375 0l1.03125 7.28125q0.0625 0.40625 0.125 0.890625q0.0625 0.46875 0.09375 0.78125q0.046875 -0.3125 0.125 -0.78125q0.078125 -0.484375 0.140625 -0.890625l1.140625 -7.28125l1.609375 0l1.125 7.28125q0.0625 0.40625 0.140625 0.890625q0.09375 0.46875 0.125 0.78125q0.03125 -0.3125 0.109375 -0.78125q0.09375 -0.484375 0.15625 -0.890625l1.0625 -7.28125l1.421875 0l-1.71875 10.265625l-1.84375 0l-1.109375 -7.09375q-0.078125 -0.515625 -0.171875 -1.03125q-0.09375 -0.53125 -0.125 -0.828125q-0.03125 0.296875 -0.109375 0.828125q-0.078125 0.515625 -0.171875 1.03125l-1.15625 7.09375l-1.84375 0zm13.600006 0.1875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm8.162506 1.140625l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm14.115631 0.1875q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625zm18.543762 1.265625l0 -13.625l4.375 0q1.28125 0 2.203125 0.5q0.9375 0.5 1.453125 1.390625q0.515625 0.890625 0.515625 2.125q0 1.21875 -0.515625 2.125q-0.515625 0.90625 -1.453125 1.40625q-0.921875 0.484375 -2.203125 0.484375l-2.703125 0l0 5.59375l-1.671875 0zm1.671875 -7.109375l2.703125 0q1.109375 0 1.765625 -0.671875q0.671875 -0.6875 0.671875 -1.828125q0 -1.15625 -0.671875 -1.828125q-0.65625 -0.671875 -1.765625 -0.671875l-2.703125 0l0 5.0zm12.506256 7.296875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm11.365631 1.296875q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0zm11.193756 0q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m302.79004 0l165.00787 0l0 42.015747l-165.00787 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m313.5244 26.919998l0 -13.625l3.859375 0q1.875 0 2.921875 0.9375q1.0625 0.9375 1.0625 2.59375q0 0.9375 -0.40625 1.625q-0.390625 0.6875 -1.109375 1.0625q-0.71875 0.375 -1.71875 0.359375l0 -0.28125q1.078125 -0.015625 1.875 0.40625q0.796875 0.421875 1.234375 1.234375q0.4375 0.796875 0.4375 1.90625q0 1.15625 -0.484375 2.015625q-0.484375 0.84375 -1.390625 1.3125q-0.890625 0.453125 -2.140625 0.453125l-4.140625 0zm1.640625 -1.515625l2.3125 0q1.171875 0 1.859375 -0.625q0.6875 -0.625 0.6875 -1.703125q0 -1.125 -0.6875 -1.8125q-0.6875 -0.703125 -1.859375 -0.703125l-2.3125 0l0 4.84375zm0 -6.296875l2.203125 0q1.078125 0 1.703125 -0.578125q0.625 -0.578125 0.625 -1.578125q0 -0.984375 -0.625 -1.5625q-0.609375 -0.578125 -1.6875 -0.578125l-2.21875 0l0 4.296875zm12.521881 8.0q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm11.756256 1.328125q-1.234375 0 -2.15625 -0.46875q-0.921875 -0.46875 -1.4375 -1.34375q-0.5 -0.890625 -0.5 -2.109375l0 -2.796875q0 -1.234375 0.5 -2.109375q0.515625 -0.875 1.4375 -1.34375q0.921875 -0.46875 2.15625 -0.46875q1.765625 0 2.84375 0.953125q1.09375 0.953125 1.140625 2.59375l-1.671875 0q-0.0625 -0.984375 -0.671875 -1.515625q-0.59375 -0.53125 -1.640625 -0.53125q-1.09375 0 -1.75 0.625q-0.65625 0.625 -0.65625 1.78125l0 2.8125q0 1.15625 0.65625 1.796875q0.65625 0.625 1.75 0.625q1.046875 0 1.640625 -0.53125q0.609375 -0.546875 0.671875 -1.515625l1.671875 0q-0.046875 1.640625 -1.140625 2.59375q-1.078125 0.953125 -2.84375 0.953125zm7.287506 -0.1875l0 -13.625l1.6875 0l0 7.59375l1.96875 0l2.984375 -4.234375l1.921875 0l-3.4375 4.890625l3.5 5.375l-1.953125 0l-2.984375 -4.578125l-2.0 0l0 4.578125l-1.6875 0zm11.725006 0l-1.640625 -10.265625l1.484375 0l1.03125 7.28125q0.0625 0.40625 0.125 0.890625q0.0625 0.46875 0.09375 0.78125q0.046875 -0.3125 0.125 -0.78125q0.078125 -0.484375 0.140625 -0.890625l1.140625 -7.28125l1.609375 0l1.125 7.28125q0.0625 0.40625 0.140625 0.890625q0.09375 0.46875 0.125 0.78125q0.03125 -0.3125 0.109375 -0.78125q0.09375 -0.484375 0.15625 -0.890625l1.0625 -7.28125l1.421875 0l-1.71875 10.265625l-1.84375 0l-1.109375 -7.09375q-0.078125 -0.515625 -0.171875 -1.03125q-0.09375 -0.53125 -0.125 -0.828125q-0.03125 0.296875 -0.109375 0.828125q-0.078125 0.515625 -0.171875 1.03125l-1.15625 7.09375l-1.84375 0zm13.600006 0.1875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm8.162506 1.140625l0 -10.265625l1.640625 0l0 1.96875l0.390625 0l-0.546875 1.203125q0 -1.65625 0.71875 -2.5q0.71875 -0.859375 2.140625 -0.859375q1.625 0 2.578125 1.03125q0.96875 1.015625 0.96875 2.75l0 0.609375l-1.6875 0l0 -0.46875q0 -1.21875 -0.59375 -1.859375q-0.578125 -0.640625 -1.65625 -0.640625q-1.0625 0 -1.671875 0.65625q-0.609375 0.65625 -0.609375 1.84375l0 6.53125l-1.671875 0zm14.115631 0.1875q-1.53125 0 -2.484375 -1.015625q-0.9375 -1.03125 -0.9375 -2.796875l0 -3.0q0 -1.765625 0.9375 -2.796875q0.9375 -1.03125 2.484375 -1.03125q1.265625 0 2.03125 0.703125q0.765625 0.6875 0.765625 1.84375l-0.328125 -0.390625l0.359375 0l-0.03125 -2.34375l0 -2.984375l1.6875 0l0 13.625l-1.6875 0l0 -1.953125l-0.328125 0l0.328125 -0.390625q0 1.171875 -0.765625 1.859375q-0.765625 0.671875 -2.03125 0.671875zm0.5625 -1.453125q1.046875 0 1.640625 -0.65625q0.59375 -0.65625 0.59375 -1.8125l0 -2.796875q0 -1.15625 -0.59375 -1.8125q-0.59375 -0.65625 -1.640625 -0.65625q-1.0625 0 -1.6875 0.640625q-0.609375 0.640625 -0.609375 1.828125l0 2.796875q0 1.203125 0.609375 1.84375q0.625 0.625 1.6875 0.625zm18.543762 1.265625l0 -13.625l4.375 0q1.28125 0 2.203125 0.5q0.9375 0.5 1.453125 1.390625q0.515625 0.890625 0.515625 2.125q0 1.21875 -0.515625 2.125q-0.515625 0.90625 -1.453125 1.40625q-0.921875 0.484375 -2.203125 0.484375l-2.703125 0l0 5.59375l-1.671875 0zm1.671875 -7.109375l2.703125 0q1.109375 0 1.765625 -0.671875q0.671875 -0.6875 0.671875 -1.828125q0 -1.15625 -0.671875 -1.828125q-0.65625 -0.671875 -1.765625 -0.671875l-2.703125 0l0 5.0zm12.506256 7.296875q-1.625 0 -2.5625 -0.828125q-0.921875 -0.828125 -0.921875 -2.25q0 -1.4375 0.921875 -2.25q0.9375 -0.828125 2.53125 -0.828125l3.15625 0l0 -1.03125q0 -0.953125 -0.5625 -1.453125q-0.5625 -0.515625 -1.609375 -0.515625q-0.921875 0 -1.546875 0.390625q-0.609375 0.375 -0.6875 1.015625l-1.6875 0q0.171875 -1.3125 1.25 -2.09375q1.078125 -0.796875 2.703125 -0.796875q1.78125 0 2.796875 0.921875q1.03125 0.90625 1.03125 2.484375l0 7.046875l-1.640625 0l0 -1.953125l-0.28125 0l0.28125 -0.28125q0 1.109375 -0.875 1.765625q-0.859375 0.65625 -2.296875 0.65625zm0.40625 -1.328125q1.203125 0 1.953125 -0.59375q0.765625 -0.59375 0.765625 -1.53125l0 -1.453125l-3.125 0q-0.84375 0 -1.328125 0.46875q-0.484375 0.46875 -0.484375 1.265625q0 0.859375 0.578125 1.359375q0.59375 0.484375 1.640625 0.484375zm11.365631 1.296875q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0zm11.193756 0q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0z" fill-rule="nonzero"/></g></svg></figure>

<p>In the graph, each node is a tensor, and each edge or two edges pointing to the
same output tensor is an op.</p>
<p>To put the compute shown in the figure in code, it should look like the following.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">forward</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">d</span><span class="p">):</span>
    <span class="n">c</span> <span class="o">=</span> <span class="n">ops</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
    <span class="n">e</span> <span class="o">=</span> <span class="n">ops</span><span class="o">.</span><span class="n">multiply</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">e</span>

<span class="c1"># Run if we call e.backward().</span>
<span class="k">def</span><span class="w"> </span><span class="nf">backward</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">e</span><span class="p">):</span>
    <span class="n">c</span><span class="o">.</span><span class="n">grad</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">grad</span> <span class="o">=</span> <span class="n">ops</span><span class="o">.</span><span class="n">multiply_backward</span><span class="p">(</span><span class="n">output_grad</span><span class="o">=</span><span class="n">e</span><span class="o">.</span><span class="n">grad</span><span class="p">,</span> <span class="n">inputs</span><span class="o">=</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">))</span>
    <span class="n">a</span><span class="o">.</span><span class="n">grad</span><span class="p">,</span> <span class="n">b</span><span class="o">.</span><span class="n">grad</span> <span class="o">=</span> <span class="n">ops</span><span class="o">.</span><span class="n">add_backward</span><span class="p">(</span><span class="n">output_grad</span><span class="o">=</span><span class="n">c</span><span class="o">.</span><span class="n">grad</span><span class="p">,</span> <span class="n">inputs</span><span class="o">=</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">))</span>
</code></pre></div>

<p>In the forward pass, we compute the output <code>e</code> from the inputs. In the backward
pass, we compute the gradients of <code>e</code> w.r.t. each tensor (i.e. <code>a.grad</code>,
<code>b.grad</code>, <code>c.grad</code>, <code>d.grad</code>).</p>
<p>The user defines the forward pass of the neural network. We need to
automatically figure out the backward pass when the user calls <code>e.backward()</code>,
or, in most cases, <code>loss.backward()</code>.</p>
<h3>Tracing</h3>
<p>Tracing refers to the process of extracting the compute graph from the
user-defined forward pass. It is called tracing because it is the process of
tracing the paths of tensors flowing in the compute graph.</p>
<p>There are many different ways to do this, but we implemented the simplest one.
We just record the op history on every tensor, namely <code>OpRecord</code>. We use this
information to reconstruct the compute graph from the very last output tensor,
which, in most cases, is the loss.</p>
<p>What information do we store in an <code>OpRecord</code>?  In the example above, we
just record <code>c</code> and <code>d</code> in <code>e.op_record</code>, and record <code>a</code> and <code>b</code> in
<code>c.op_record</code>. So, we record the input tensors of the op in the <code>OpRecord</code> of
the output tensor. This information should be good enough for us to
reconstruct the entire compute graph by starting a breadth-first search (BFS)
from the last output tensor.</p>
<h3>Backward propagation</h3>
<p>During backward propagation, we want to compute the gradients w.r.t. all the
tensors. So, every time, we just call the backward function of the op with the
gradients and the input tensors of the op. So, we need one more piece of
information in the <code>OpRecord</code>, the backward function of the op to call.</p>
<p>Following is our implementation of the <code>OpRecord</code>.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">OpRecord</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">func_backward</span><span class="p">,</span> <span class="n">input_tensors</span><span class="p">,</span> <span class="n">output_tensor</span><span class="p">):</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">func_backward</span> <span class="o">=</span> <span class="n">func_backward</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">input_tensors</span> <span class="o">=</span> <span class="n">input_tensors</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">output_tensor</span> <span class="o">=</span> <span class="n">output_tensor</span>
</code></pre></div>

<p>So, it only has 3 attributes:</p>
<ul>
<li><code>func_backward</code>. The backward function to call. We need it because we need to
  know which function to call during backward propagation.</li>
<li><code>input_tensors</code>. The input tensors to the forward operation. We need it to
  trace the compute graph and for the backward function call.</li>
<li><code>output_tensor</code>. The output tensor of the operation. We need it for the
  backward function call because we need the gradients w.r.t. the output
  tensor, i.e., <code>output_tensor.grad</code>.</li>
</ul>
<p>Here are the steps to do it.</p>
<ul>
<li>During the forward pass, assign an <code>OpRecord</code> object to the <code>.op_record</code>
  attribute of the <code>Tensor</code> when we produce the <code>Tensor</code> with an op.</li>
<li>When <code>loss.backward()</code> is called, use the <code>OpRecord</code>s to trace back the
  entire compute graph.</li>
<li>Sort the nodes in the compute graph in reverse topological order.</li>
<li>Call the backward function stored in the <code>.op_record</code> of every tensor
  following the sorted order, which starts from the loss node.</li>
</ul>
<p>Please refer to
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/ops/op_record.py"><code>op_record.py</code></a>,
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/ops/forward.py"><code>forward.py</code></a>,
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/ops/backward.py"><code>backward.py</code></a>,
<a href="https://github.com/haifeng-jin/readable-ml-framework/blob/v0.0.1/framework/autograd.py"><code>autograd.py</code></a>
for more details, for example, why we need topological sorting.</p>
<h2>End-to-end workflow</h2>
<p>So far, we have reviewed all the major components of the framework. Let's
connect the dots by describing an end-to-end workflow.</p>
<p>Let's look at the code example at the beginning of this article.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">framework</span>

<span class="n">x</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">Tensor</span><span class="o">.</span><span class="n">from_numpy</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">]],</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">Tensor</span><span class="o">.</span><span class="n">from_numpy</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="mf">4.0</span><span class="p">],</span> <span class="p">[</span><span class="mf">5.0</span><span class="p">]],</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span>
<span class="n">z</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>  <span class="c1"># Expected: [[2*4 + 3*5]] = [[23.0]]</span>
<span class="n">loss</span> <span class="o">=</span> <span class="n">framework</span><span class="o">.</span><span class="n">ops</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>

<span class="n">loss</span><span class="o">.</span><span class="n">backward</span><span class="p">()</span>  <span class="c1"># Compute the gradients of loss w.r.t. tensor x, y, and z.</span>

<span class="nb">print</span><span class="p">(</span><span class="s2">"x.grad:"</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">grad</span><span class="o">.</span><span class="n">numpy</span><span class="p">())</span>  <span class="c1"># Expected: [[4.0, 5.0]]</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"y.grad:"</span><span class="p">,</span> <span class="n">y</span><span class="o">.</span><span class="n">grad</span><span class="o">.</span><span class="n">numpy</span><span class="p">())</span>  <span class="c1"># Expected: [[2.0], [3.0]]</span>
</code></pre></div>

<p>First, we constructed two tensors <code>x</code> and <code>y</code> from NumPy arrays. The
<code>framework.Tensor</code> is a Python class, it uses the C++ implementation of the
tensor, <code>framework.core.Tensor</code>, as its attribute. The NumPy array will be
passed to the constructor of the C++ class. The argument type conversion from
Python types to C++ types is handled by Pybind11.</p>
<p>Then, we call two ops <code>matmul</code> and <code>sum</code> on them to get the single value tensor,
the loss. This defines the forward pass of the model. <code>framework.ops.matmul</code>
and <code>framework.ops.sum</code> are just Python wrappers of the C++ implementation,
<code>framework.core.ops</code>. Before calling the C++ code, the Python wrapper stores
<code>x</code>, <code>y</code>, and <code>matmul_backward()</code> in <code>z.op_record</code>. It also does something
similar to <code>loss.op_record</code>.</p>
<p>Finally, when <code>loss.backward()</code> is called, the autograd module trace and build
the compute graph using the op records, and compute the gradients calling the
backward functions of the ops.</p>
<p>So far, we have gone through all the features we have implemented in our ML
framework. The main challenges we faced were the Python C++ interfacing and ops
implementations.</p>
<p>If you follow everything we introduced, you can explore the
<a href="https://github.com/haifeng-jin/readable-ml-framework">heavily commented
codebase</a> and try to
understand all the code by yourself. You will feel confident to implement a
framework by yourself. I encourage you to do so to better understand all the
details.</p>
<h2>Other features</h2>
<p>In the rest of the article, we will explore the important features that are not
included in our framework. We will explain what they are, why they are
important, and how they can be added to our framework.</p>
<h3>GPU backend</h3>
<p>A GPU backend of an ML framework should allow you to run your models on GPU for
faster speed. The implementation requires adding a new C++ extension module to
the Python library. This new module should implement a new <code>Tensor</code> class and
all the ops using CUDA, a programming language similar to C++ but for GPUs
specifically. The Python code of the framework should dynamically route to
different backends based on the user configuration.</p>
<p>It is not hard to implement the GPU backend, but extremely challenging if you
want it to run fast. There are a lot of tricks in CUDA kernel implementation. A
CUDA kernel is a function that is executed on a GPU making use of its parallel
processing capabilities. </p>
<h3>More precisions</h3>
<p>In our framework, we only support <code>float32</code> as the data type for all the
tensors. However, people may want to use a different precision like <code>bfloat16</code>,
<code>int8</code>, and so on. This allows the user to control the compute precision on a
finer-grained level to better address their speed and power requirements.</p>
<p>The implementation of this feature is mainly about abstraction. To reduce code
duplication, we need to make code for the ops general enough for more data types
and also leave space for custom optimization for certain data types.</p>
<h3>More tensor types</h3>
<p>In many applications, people may find sparse tensors and ragged tensors useful.
For example, sparse tensors are widely used in the embedding lookup tables of
recommendation systems. Ragged tensors, which means a dimension of the model
may have different lengths, for example, the length of the input sentences to
a natural language processing model.</p>
<p>To implement these, we need to make the <code>Tensor</code> class of the framework
extensible, and make the ops more supportive for different types of tensors.
We may even need to introduce new hardware feature support to better process
sparse tensors, for example, making use of the <a href="https://cloud.google.com/tpu/docs/system-architecture-tpu-vm#sparsecore">sparse
core</a>
of Google TPUs.</p>
<h3>Data pipelining</h3>
<p>If not careful, the data pipeline can become the performance bottleneck of the
overall ML process. The data pipelining module needs to load the data in
advance to feed the model.</p>
<p>The implementation is rather orthogonal to what we have introduced above. It
should prefetch the data on the compute device, for example, the HBM of GPUs
before the model actually requests that data for computation.</p>
<h3>ML compiler</h3>
<p>ML compiler is a big topic. We will dig more into the ML compilers in a future
blog post. The ML compiler is different from a traditional programming language
compiler, it does not compile any programming language, but optimizes the
compute graph and lowers it down to execute on a GPU/TPU.</p>
<p>Here is an example of an optimization that an ML compiler can do. We use
softmax to convert logits $\mathbf{z}$ into probability $\mathbf{p}$ and use
categorical cross-entropy as the loss function to get the loss value $L$, shown
as follows.</p>
<p>The softmax function:
$$
\mathbf{p} = \text{softmax}(\mathbf{z})
$$
$$
p_i = \text{softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}
$$</p>
<p>The categorical cross-entropy loss:
$$
L = - \sum_{i=1}^{K} y_i \log(p_i)
$$</p>
<p>where $\mathbf{y} = (y_1, y_2, \cdots, y_K)$ is the one-hot encoded label.</p>
<p>It would be time-consuming to call the backward function of all the ops
involved (exp, log, divide, multiply, sum) one by one to get the gradients of
the loss $L$ w.r.t. the logits $\mathbf{z}$ denoted as $\partial L/\partial
\mathbf{z}$.</p>
<p>However, if you derive it by hand, you can find that $\partial L/\partial
\mathbf{z}$ can be simplified to $\mathbf{p} - \mathbf{y}$.</p>
<p>$$
\frac{\partial L}{\partial \mathbf{z}} = \mathbf{p} - \mathbf{y}
$$</p>
<p>The ML compiler should have a built-in rule to capture this pattern of a long
chain of backward function in the compute graph and optimize it to
$\mathbf{p}-\mathbf{y}$.</p>
<p>Regarding the implementation of an ML compiler, it takes tremendous effort to
build a good one. It is far beyond what a solo project can do. It should
capture the compute graph through tracing, and serialize the compute graph to some
intermediate representations (IR). The compiler takes the IR as input and
optimizes it to output another IR. After multiple levels of such optimization,
the IR should be lowered down to the hardware to execute the computation.</p>
<h3>Distributed training</h3>
<p>Finally, here is the killer feature of an ML framework. It is almost impossible
to do distributed training without an ML framework. It distributes the compute
and memory usage across multiple machines and multiple GPUs. It allows the
users to have an experience similar to running the model on a single machine.</p>
<p>There are many different distribution techniques in machine learning. We will
also have a future post dedicated to this topic.</p>
<h2>Conclusions</h2>
<p>So far, we have gone through the basics of building an ML framework from
scratch including Python and C++ interfacing, tensor data structures, tensor
operations, and automatic differentiation.</p>
<p>As you can see, it is not difficult to build a simple working ML framework.
The challenge is to make it efficient enough for all the different
scenarios. It may include running on different hardware, on different numbers of
machines, with different data types and tensor types. To support these
scenarios, it may require a good ML compiler, distribution features, optimized
op implementations, and so on.</p>
<p>After reading this article, hope you are more confident about your knowledge on
ML frameworks now.</p>]]></content>
  </entry>
  <entry>
    <title>Llama Is Open-Source, But Why?</title>
    <link href="https://haifengjin.com/llama-is-open-source-but-why/"/>
    <id>https://haifengjin.com/llama-is-open-source-but-why/</id>
    <published>2024-06-25T00:00:00+00:00</published>
    <updated>2024-06-25T00:00:00+00:00</updated>
    <summary>An analysis of Meta's open-source large model strategy</summary>
    <content type="html"><![CDATA[<p>Training a large language model can cost millions of dollars. Why would Meta spend so much money training a model and letting everyone use it for free?</p>
<p>This article analyzes Meta's GenAI and large model strategy to understand
the considerations of open-sourcing their large models. We also discuss
how this wave of open-source models is similar to and different from
traditional open-source software.</p>
<p>DISCLAIMER: Whether the Llama models are genuinely open-source falls outside the scope of this article. All information is from public sources.</p>
<h2>The illusion of proprietary models</h2>
<p>If Meta made its models open-source, how could they make money? People would
just build their own services instead of paying for the service provided
by Meta, for example, the chatbot on <a href="https://www.meta.ai/">meta.ai</a>, an
API based on Llama, or services for fine-tuning and serving the model.</p>
<p>However, you can never prevent people from building their own solutions. Keeping
your models proprietary would not help you achieve this goal. Regardless of
whether you make your models open-source, others, like
<a href="https://docs.mistral.ai/#open-source">Mistral AI</a>,
<a href="https://github.com/QwenLM/Qwen2">Alibaba</a>, and even
<a href="https://blog.google/technology/developers/gemma-open-models/">Google</a>,
made their models open-source.</p>
<p>The users are building their models based on the best open-source models. 
Therefore, it doesn't matter whether your model is open-source or not unless it is always better than the best open-source models in the long run.
I don't think many companies are so confident about that.</p>
<p>So, now, you only have two choices: to be the first and the leader of open-source models, or to be a follower by releasing your models later.</p>
<h2>Why be the leader of open-source models?</h2>
<p>Being the leader of open-source models has many benefits, but the most important is attracting talent.</p>
<p>The war of GenAI is a talent competition bottlenecked by computing power.
How much computing power you get largely depends on two things: your cash
flow, and your relationship with Nvidia. However, how many talents you
have is another story.</p>
<p>According to Elon Musk<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup>, Google had
two-thirds of the AI talent pool in the early 2010s. To counter Google's power, they founded
OpenAI. Then, some of the best people left OpenAI and founded Anthropic
to focus on AI safety. So, these three companies have the best and the
most AI experts in the job market. Everyone else is super hungry
for more AI experts.</p>
<p>Being the leader of open-source models would help Meta bridge this gap of AI experts. Open-source models attract talent in two different ways.</p>
<p><strong>First, the AI experts want to work for Meta.</strong> It is super cool to have
the whole world use the model you built. It gives you so much exposure
for your work, amplifies your professional impact, and benefits your
future career. So, many talented people would like to work for them.</p>
<p><strong>Second, the AI experts in the community do the work for Meta for
free.</strong> Right after the release of Llama, people started to experiment
with it. They help you develop new serving technologies to reduce costs,
fine-tune your models to discover new applications and scrutinize your
model to discover vulnerabilities to make it safer. For example,
according to <a href="https://www.semianalysis.com/p/google-we-have-no-moat-and-neither">this
article</a>,
they did instruction tuning, quantization, quality improvements, human
evals, multimodality, and RLHF for Llama within a month after its initial
release. Delegating this work to the community saves Meta huge amounts of
computing and human resources.</p>
<h2>Iterate fast with the community.</h2>
<p>With open-source models, Meta can iterate quickly with the community by directly incorporating their newly developed methods.</p>
<p>How much would it cost Google to adopt a new method from the community? The process consists of two phases: implementation and evaluation. First, they need to reimplement the method for Gemini. This involves rewriting the code in JAX, which requires a fair amount of engineering resources. During the evaluation, they need to run a list of benchmarks on it, which requires a lot of computing power. Most importantly, it takes time. It stopped them from iterating on the latest technologies when they were first available.</p>
<p>Conversely, if Meta wants to adopt a new method from the community, it will cost them nothing. The community has done the experiments and benchmarks on the Llama model directly, so not much further evaluation is needed. The code is written in PyTorch. They can just copy and paste it into their system.</p>
<p>Llama built a flywheel between Meta and the community. Meta brings in the latest technology from the community and rolls out its next-generation model to the community. PyTorch is the common language they speak.</p>
<h2>Can they still make money?</h2>
<p>The model is open-source. Wouldn't people just build their own service? Why would they want to pay Meta for a service built on an open-source model? Of course, they will. The service is difficult to build even with an open-source model.</p>
<p>How do you fine-tune and align the model to your specific application? How do you balance between the service cost and the model quality? Are you aware of all the tricks to fully utilize your GPUs?</p>
<p>The people who know the answers to these questions are expensive to hire. Even with enough people, it is hard to get the computing power to fine-tune and serve the model. Imagine how hard it is to build <a href="https://www.meta.ai/">Meta AI</a> from the open-source Llama model. I would expect hundreds of employees and GPUs to be involved.</p>
<p>So, it is likely that people will still pay for Meta's GenAI service if they have any in the future.</p>
<h2>It's just like open-source software, but not quite.</h2>
<p>The situation is very similar to traditional open-source software. The “<a href="/tensorflow-is-open-source-but-why/">free code paid service</a>” framework still applies. The code or the model is free to attract more users to the ecosystem. With a larger ecosystem, the owner collects more benefits. The service built upon the free code is for profit.</p>
<p>However, it is also NOT like open-source software. The main difference can be summarized as low user retention and a new type of ecosystem.</p>
<h2>Low user retention</h2>
<p>Open-source models have lower user retention. Migrating to a new model is much easier than to new software.</p>
<p>It is hard to migrate software. PyTorch and HuggingFace have established a strong ecosystem for deep learning frameworks and model pools. Imagine how hard it would be to shift their dominance even slightly if you created a new deep learning framework or model pool to compete with them.</p>
<p>A good example is JAX. It has better support for large-scale distributed training, but it is hard to onboard users to JAX because it has a smaller ecosystem and community. It lacks a helpful community to support users with issues. Moreover, the engineering cost of migrating the entire infra to a new framework is too high for most companies.</p>
<p>Open-source models do not have these problems. They are easy to migrate and require almost no user support. Therefore, it is easy for people to shift to the latest and best models. To maintain your leadership in open-source models, you must constantly release new models at the top of the leaderboard. This is also noted as a downside or challenge to be the leader in open-source models.</p>
<h2>A new type of ecosystem</h2>
<p>Open-source models create a new type of ecosystem. Unlike open-source software, which creates ecosystems of contributors and new software built upon them, open-source models create ecosystems of fine-tuned and quantized models, which can be seen as forks of the original model.</p>
<p>As a result, an open-source foundational model doesn't have to be super good at every specific task because users would fine-tune it for their applications with domain-specific data. The most important feature of a foundational model is to meet the deployment requirements of the users, such as low latency in inferencing or being small enough to fit an end device.</p>
<p>This is why Llama has multiple sizes for each version. For example, Llama-3 has three versions: 8B, 70B, and 400B. They want to ensure they cover all deployment scenarios.</p>
<h2>Summary</h2>
<p>Even if Meta did not open-source their model, others would. So, it would be wise for Meta to open-source it early and lead the open-source models. Then, Meta can iterate quickly with the community to improve its models and catch up with OpenAI and Google.</p>
<p>When open-sourcing your model, there is no need to worry about people not using your service since there is still a huge gap between the foundational model and a well-built service.</p>
<p>Open-source models are similar to open-source software in that they all follow the “free code paid service” framework but differ in user retention rate and the type of ecosystem they create.</p>
<p>In the future, I would expect to see more open-source models from more companies. Unlike the deep learning frameworks converged on PyTorch, open-source models will remain diverse and competitive for a long time.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="musk-interview" class="csl-entry">Musk, E. (2023). <i>Interview on Google and OpenAI AI talent</i>. YouTube. <a href="https://youtu.be/2BfMuHDfGJI?t=2748" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>TPUs Are Not for Sale, But Why?</title>
    <link href="https://haifengjin.com/tpus-are-not-for-sale-but-why/"/>
    <id>https://haifengjin.com/tpus-are-not-for-sale-but-why/</id>
    <published>2024-04-30T00:00:00+00:00</published>
    <updated>2024-04-30T00:00:00+00:00</updated>
    <summary>An analysis of Google's unique approach to AI hardware</summary>
    <content type="html"><![CDATA[<p>Nvidia's stock price has skyrocketed because of its GPU's dominance in the AI hardware market. However, at the same time, TPUs, well-known AI hardware from Google, are not for sale. You can only rent virtual machines on Google Cloud to use them. Why did Google not join the game of selling AI hardware?</p>
<p>DISCLAIMER: The views expressed in this article are solely those of the author and do not necessarily reflect the opinions or viewpoints of Google or its affiliates. The entirety of the information presented within this article is sourced exclusively from publicly available materials.</p>
<h2>A popular theory</h2>
<p>One popular theory I heard is that Google wants to attract more customers to its cloud services. If they sell it to other cloud service providers, they are less competitive in the cloud service market.</p>
<p>According to cloud service customers, this theory does not make much sense. No corporate-level customer wants to be locked to one specific cloud service provider. They want to be flexible enough to move to another whenever needed. Otherwise, if the provider increases the price, they can do nothing about it.</p>
<p>If they are locked to Google Cloud for using TPUs, they would rather not use it. This is why many customers don't want to use TPUs. They only started to feel less locked in recently when OpenXLA, an intermediate software to access TPUs, supported more frameworks like PyTorch.</p>
<p>So, using TPUs to attract customers to Google Cloud is not a valid reason for not selling them. Then, what is the real reason? To answer this question better, we must look into how Google started the TPU project.</p>
<h2>Why did Google start the TPU project?</h2>
<p>The short answer is for proprietary usage. There was a time when GPUs could not meet the computing requirements for AI hardware.</p>
<p>Let's try to estimate when the TPU project was started. Given it was first announced to the public in 2016<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup>, it would be a fair guess that it started around 2011. If that is true, they started the project pretty early since we did not see a significant improvement in computer vision until 2012 by AlexNet.</p>
<p>With this timeline, we know GPUs were less potent than today when the project started. Google saw this AI revolution early and wanted faster hardware for large-scale computing. Their only choice is to build a new solution for it.</p>
<p>That was why Google started this project, but there are more questions. Why were GPUs not good enough back in the day? What potential improvements did Google see that are significant enough to start their new hardware project?</p>
<p>The answer lies in the microarchitecture of GPUs and TPUs. Let's examine the design of the cores on GPUs and TPUs.</p>
<h2>The design idea of GPUs</h2>
<p>First, let's do a quick recap of the background knowledge of CPUs. When an instruction comes, it is decoded by the instruction decoder and fed into the arithmetic logic unit (ALU) together with data from the registers. The ALU does all the computing and returns the results to one of the registers. If you have multiple cores in the CPU, they can work in parallel.</p>
<p>What is a GPU? It is short for the graphics processing unit. It was designed for graphics computing and later discovered suitable for machine learning. Most of the operations in a GPU are matrix operations, which could run in parallel. This also means there are not many operations they need to support compared with a CPU.</p>
<p>The more specialized the chip is for a given task, the faster it is on the task.</p>
<p>The key idea of the GPU's initial design was to have a feature-reduced CPU with smaller but more cores for faster parallel computing. The number of instructions supported on a GPU is much less than on a CPU, which makes the area taken by a single core on a chip much smaller. This way, they can pack more cores onto the chip for large-scale parallel computing.</p>
<p>Why do fewer features mean a smaller area on the chip? In software, more features mean more code. In hardware, all features are implemented using logical circuits instead of code. More features mean the circuit is more complex. For example, a CPU must implement more instructions on the chip.</p>
<p>Smaller also means faster. A simpler design of the logic gates leads to a shorter cycle time.</p>
<h2>The design idea of TPUs</h2>
<p>TPU further developed this idea of specialized chips for deep learning. The defining feature of a TPU is its matrix-multiply unit (MXU). Since matrix multiplication is the most frequent operation in deep learning, TPU builds a specialized core for it, the MXU.</p>
<p>This is even more specialized than a GPU core, capable of many matrix operations, while the MXU only does one thing: matrix multiplication.</p>
<p>It works quite differently from a traditional CPU/GPU core. We usually refer to
these processors with new architectures as application-specific integrated
circuits (ASICs).</p>
<p>In an MXU, all the dynamics and
generality are removed. It has a grid of nodes all connected together. Each node
only does multiplication and addition in a predefined manner. The results are
directly pushed to the next node for the next multiplication and addition. So,
everything is predefined and fixed.</p>
<p>Here is a simple example to explain how it works. Every matrix multiplication is
decomposed into a few vector inner-product computations.  We have $\mathbf{x}$
and $\mathbf{y}$ as the input of the MXU, where</p>
<p>$$
\mathbf{x} = \begin{bmatrix} x_1,\ x_2,\ x_3\end{bmatrix},\ 
\mathbf{y} = \begin{bmatrix} y_1 \\ y_2 \\ y_3\end{bmatrix}.
$$</p>
<p>We load the float values onto the registers and multiply the corresponding value pairs together to get</p>
<p>$$
\mathbf{z} = \begin{bmatrix} z_1,\ z_2,\ z_3\end{bmatrix}=\begin{bmatrix} x_1y_1,\ x_2y_2,\ x_3y_3\end{bmatrix}.\ 
$$</p>
<p>Then, we accumulate the values in $\mathbf{z}$ with</p>
<p>$$
\mathbf{s}=\begin{bmatrix} z_1,\ z_2,\ z_3\end{bmatrix},
$$
where
$$s_i=s_{i-1} + z_i$$
and
$$
s_1=z_1.
$$</p>
<p>The following figure shows how the values flow in an MXU.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="-40 0 680 270" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_mxu_1_p.0"><path d="m0 0l523.1181 0l0 279.92126l-523.1181 0l0 -279.92126z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_mxu_1_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l523.1181 0l0 279.92126l-523.1181 0z" fill-rule="evenodd"/><path fill="var(--gray-text-color)" d="m13.8011675 32.102238l0 0c0 -10.634172 8.6207 -19.254873 19.254875 -19.254873l91.45026 0c5.1067123 0 10.004265 2.0286322 13.615257 5.6396217c3.6109772 3.6109886 5.639618 8.508541 5.639618 13.615252l0 162.16078c0 10.634171 -8.620697 19.254868 -19.254875 19.254868l-91.45026 0c-10.634174 0 -19.254875 -8.620697 -19.254875 -19.254868z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m94.48893 183.67427l0 0c0 -10.728561 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093063 2.046646 13.736099 5.689682c3.6430435 3.643036 5.6896744 8.584061 5.6896744 13.736099l0 0c0 10.728561 -8.697212 19.425781 -19.425774 19.425781l0 0c-10.728561 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m94.48893 183.67427l0 0c0 -10.728561 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093063 2.046646 13.736099 5.689682c3.6430435 3.643036 5.6896744 8.584061 5.6896744 13.736099l0 0c0 10.728561 -8.697212 19.425781 -19.425774 19.425781l0 0c-10.728561 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m94.47188 164.23143l38.851562 0l0 38.851562l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m109.85075 190.53346q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m116.88826 197.37721l0 -1.015625l2.453125 0l0 -7.125l-2.453125 1.828125l0 -1.234375l2.046875 -1.53125l1.53125 0l0 8.0625l2.015625 0l0 1.015625l-5.59375 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m113.89669 62.111073c0 7.909313 -17.558243 11.866108 -35.116486 15.81863c-17.558247 3.9525223 -35.116493 7.9007797 -35.116493 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m113.89669 62.111073c0 7.909313 -17.558243 11.866108 -35.116486 15.81863c-17.558247 3.9525223 -35.116493 7.9007797 -35.116493 15.801559" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m63.10467 113.174095c7.8417473 0 11.760815 0.017059326 15.683491 0.034111023c3.922676 0.017051697 7.848961 0.034111023 15.697929 0.034111023" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m63.104675 113.1741c7.841736 0 11.760811 0.017044067 15.683487 0.034111023l3.6979446 0.013587952" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m82.48046 116.52527l9.081825 -3.2879486l-9.070534 -3.3189697z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m94.48893 113.191154l0 0c0 -10.728561 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093063 2.0466385 13.736099 5.6896744c3.6430435 3.6430435 5.6896744 8.584068 5.6896744 13.736107l0 0c0 10.728561 -8.697212 19.425774 -19.425774 19.425774l0 0c-10.728561 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m94.48893 113.191154l0 0c0 -10.728561 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093063 2.0466385 13.736099 5.6896744c3.6430435 3.6430435 5.6896744 8.584068 5.6896744 13.736107l0 0c0 10.728561 -8.697212 19.425774 -19.425774 19.425774l0 0c-10.728561 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m94.47188 93.748314l38.851562 0l0 38.851555l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m106.24138 119.8941l0 -1.671875l5.75 -7.0625l-5.59375 0l0 -1.53125l7.484375 0l0 1.6875l-5.859375 7.046875l6.0625 0l0 1.53125l-7.84375 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m116.88826 126.8941l0 -1.015625l2.453125 0l0 -7.125l-2.453125 1.828125l0 -1.234375l2.046875 -1.53125l1.53125 0l0 8.0625l2.015625 0l0 1.015625l-5.59375 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m113.89766 132.59987c0 7.909317 -17.54972 11.866119 -35.099434 15.818634c-17.54972 3.95253 -35.099434 7.9007874 -35.099434 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m113.89766 132.59987c0 7.909317 -17.54972 11.866119 -35.099434 15.818634c-17.54972 3.95253 -35.099434 7.9007874 -35.099434 15.801559" fill-rule="evenodd"/><path fill="var(--background-color)" d="m94.48797 42.702347l0 0c0 -10.7285595 8.697212 -19.425777 19.425774 -19.425777l0 0c5.1520386 0 10.093063 2.0466385 13.736099 5.689678c3.6430511 3.6430397 5.689682 8.5840645 5.689682 13.736099l0 0c0 10.728561 -8.69722 19.425781 -19.425781 19.425781l0 0c-10.728561 0 -19.425774 -8.69722 -19.425774 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m94.48797 42.702347l0 0c0 -10.7285595 8.697212 -19.425777 19.425774 -19.425777l0 0c5.1520386 0 10.093063 2.0466385 13.736099 5.689678c3.6430511 3.6430397 5.689682 8.5840645 5.689682 13.736099l0 0c0 10.728561 -8.69722 19.425781 -19.425781 19.425781l0 0c-10.728561 0 -19.425774 -8.69722 -19.425774 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m94.47091 23.259514l38.851555 0l0 38.85156l-38.851555 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m105.506035 49.405293l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m116.88729 56.405293l0 -1.015625l2.453125 0l0 -7.125l-2.453125 1.828125l0 -1.234375l2.046875 -1.53125l1.53125 0l0 8.0625l2.015625 0l0 1.015625l-5.59375 0z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m24.268747 42.702347l0 0c0 -10.7285595 8.697218 -19.425777 19.42578 -19.425777l0 0c5.1520348 0 10.09306 2.0466385 13.736099 5.689678c3.6430397 3.6430397 5.689678 8.5840645 5.689678 13.736099l0 0c0 10.728561 -8.697216 19.425781 -19.425777 19.425781l0 0c-10.728561 0 -19.42578 -8.69722 -19.42578 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m24.268747 42.702347l0 0c0 -10.7285595 8.697218 -19.425777 19.42578 -19.425777l0 0c5.1520348 0 10.09306 2.0466385 13.736099 5.689678c3.6430397 3.6430397 5.689678 8.5840645 5.689678 13.736099l0 0c0 10.728561 -8.697216 19.425781 -19.425777 19.425781l0 0c-10.728561 0 -19.42578 -8.69722 -19.42578 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m24.251692 23.259514l38.851555 0l0 38.85156l-38.851555 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m38.775063 52.764668l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m47.765694 56.405293l0 -1.015625l2.453125 0l0 -7.125l-2.453125 1.828125l0 -1.234375l2.046875 -1.53125l1.53125 0l0 8.0625l2.015625 0l0 1.015625l-5.59375 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m43.67747 62.111073c0 7.909313 0.017055511 11.866108 0.034111023 15.81863c0.017055511 3.9525223 0.034111023 7.9007797 0.034111023 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m43.67747 62.111073c0 7.909313 0.017055511 11.866108 0.034111023 15.81863c0.017055511 3.9525223 0.034111023 7.9007797 0.034111023 15.801559" fill-rule="evenodd"/><path fill="var(--text-color)" d="m24.270168 113.191154l0 0c0 -10.728561 8.697216 -19.425781 19.425777 -19.425781l0 0c5.1520386 0 10.093063 2.0466385 13.736103 5.6896744c3.6430397 3.6430435 5.689678 8.584068 5.689678 13.736107l0 0c0 10.728561 -8.69722 19.425774 -19.425781 19.425774l0 0c-10.728561 0 -19.425777 -8.697212 -19.425777 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m24.253113 93.748314l38.85156 0l0 38.851555l-38.85156 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m41.457012 118.1441l-1.34375 -0.9375l1.015625 -1.453125q0.234375 -0.359375 0.5625 -0.703125q0.34375 -0.34375 0.6875 -0.640625q0.34375 -0.3125 0.609375 -0.546875l-0.0625 -0.125q-0.34375 -0.03125 -0.796875 -0.078125q-0.453125 -0.046875 -0.90625 -0.15625q-0.453125 -0.109375 -0.84375 -0.234375l-1.640625 -0.59375l0.5625 -1.546875l1.640625 0.59375q0.390625 0.125 0.796875 0.359375q0.421875 0.234375 0.8125 0.484375q0.40625 0.25 0.6875 0.484375l0.09375 -0.078125q-0.09375 -0.359375 -0.21875 -0.8125q-0.109375 -0.453125 -0.1875 -0.9375q-0.0625 -0.5 -0.0625 -0.9375l0 -1.765625l1.640625 0l0 1.765625q0 0.4375 -0.078125 0.9375q-0.078125 0.484375 -0.1875 0.9375q-0.109375 0.453125 -0.21875 0.8125l0.09375 0.078125q0.296875 -0.234375 0.671875 -0.484375q0.390625 -0.25 0.796875 -0.484375q0.421875 -0.234375 0.828125 -0.359375l1.65625 -0.59375l0.5625 1.546875l-1.671875 0.59375q-0.375 0.125 -0.828125 0.234375q-0.453125 0.109375 -0.90625 0.15625q-0.4375 0.046875 -0.796875 0.078125l-0.078125 0.15625q0.28125 0.21875 0.625 0.53125q0.34375 0.296875 0.671875 0.640625q0.328125 0.34375 0.546875 0.6875l1.03125 1.453125l-1.34375 0.9375l-1.03125 -1.453125q-0.21875 -0.34375 -0.4375 -0.765625q-0.203125 -0.4375 -0.390625 -0.859375q-0.171875 -0.4375 -0.28125 -0.765625l-0.140625 0q-0.109375 0.328125 -0.28125 0.765625q-0.171875 0.421875 -0.390625 0.859375q-0.21875 0.421875 -0.453125 0.765625l-1.015625 1.453125z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m24.29575 183.67996l0 0c0 -10.728577 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520348 0 10.09306 2.0466309 13.736099 5.6896667c3.6430397 3.6430511 5.689678 8.584076 5.689678 13.7361145l0 0c0 10.728561 -8.697216 19.425766 -19.425777 19.425766l0 0c-10.728561 0 -19.425781 -8.697205 -19.425781 -19.425766z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m24.278696 164.23712l38.851555 0l0 38.851562l-38.851555 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m42.888847 188.61728l0 -3.640625l-3.5625 0l0 -1.5l3.5625 0l0 -3.640625l1.640625 0l0 3.640625l3.5625 0l0 1.5l-3.5625 0l0 3.640625l-1.640625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m63.130253 183.6629c7.835415 0 11.752422 0.017059326 15.670834 0.034103394c3.9184113 0.017059326 7.838234 0.034118652 15.676476 0.034118652" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m63.130253 183.66289c7.835415 0 11.752422 0.017059326 15.670834 0.03413391c0.9796066 0.004257202 1.9592972 0.008529663 3.0002823 0.012527466l0.67620087 0.0021820068" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m82.47224 187.0152l9.08152 -3.288803l-9.070847 -3.3181152z" fill-rule="evenodd"/><path fill="var(--gray-text-color)" d="m199.15785 32.102238l0 0c0 -10.634172 8.620712 -19.254873 19.254883 -19.254873l91.45024 0c5.10672 0 10.004272 2.0286322 13.615265 5.6396217c3.6109924 3.6109886 5.639618 8.508541 5.639618 13.615252l0 162.16078c0 10.634171 -8.620697 19.254868 -19.254883 19.254868l-91.45024 0c-10.634171 0 -19.254883 -8.620697 -19.254883 -19.254868z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m279.8456 183.67427l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.046646 13.7361145 5.689682c3.643036 3.643036 5.6896667 8.584061 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m279.8456 183.67427l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.046646 13.7361145 5.689682c3.643036 3.643036 5.6896667 8.584061 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m279.82855 164.23143l38.851562 0l0 38.851562l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m295.20743 190.53346q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m302.26056 197.37721l0 -1.140625l2.78125 -2.953125q0.703125 -0.75 1.03125 -1.34375q0.328125 -0.59375 0.328125 -1.140625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.4375 -1.140625 -0.4375q-0.796875 0 -1.25 0.453125q-0.453125 0.4375 -0.453125 1.21875l-1.125 0q0.03125 -0.8125 0.375 -1.421875q0.359375 -0.609375 0.984375 -0.921875q0.640625 -0.328125 1.46875 -0.328125q0.828125 0 1.421875 0.328125q0.609375 0.3125 0.921875 0.890625q0.328125 0.578125 0.328125 1.390625q0 0.765625 -0.40625 1.53125q-0.40625 0.765625 -1.34375 1.734375l-2.234375 2.3125l4.140625 0l0 1.015625l-5.40625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m299.2534 62.111073c0 7.909313 -17.558258 11.866108 -35.116486 15.81863c-17.558258 3.9525223 -35.1165 7.9007797 -35.1165 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m299.2534 62.111073c0 7.909313 -17.558258 11.866108 -35.116486 15.81863c-17.558258 3.9525223 -35.1165 7.9007797 -35.1165 15.801559" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m248.46136 113.174095c7.841736 0 11.760803 0.017059326 15.683502 0.034111023c3.9226685 0.017051697 7.848938 0.034111023 15.6979065 0.034111023" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m248.46136 113.1741c7.841736 0 11.760803 0.017044067 15.683472 0.034111023l3.6979675 0.013587952" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m267.83716 116.52527l9.081818 -3.2879486l-9.070526 -3.3189697z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m279.8456 113.191154l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.6896744c3.643036 3.6430435 5.6896667 8.584068 5.6896667 13.736107l0 0c0 10.728561 -8.697205 19.425774 -19.425781 19.425774l0 0c-10.728546 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m279.8456 113.191154l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.6896744c3.643036 3.6430435 5.6896667 8.584068 5.6896667 13.736107l0 0c0 10.728561 -8.697205 19.425774 -19.425781 19.425774l0 0c-10.728546 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m279.82855 93.748314l38.851562 0l0 38.851555l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m291.59805 119.8941l0 -1.671875l5.75 -7.0625l-5.59375 0l0 -1.53125l7.484375 0l0 1.6875l-5.859375 7.046875l6.0625 0l0 1.53125l-7.84375 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m302.26056 126.8941l0 -1.140625l2.78125 -2.953125q0.703125 -0.75 1.03125 -1.34375q0.328125 -0.59375 0.328125 -1.140625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.4375 -1.140625 -0.4375q-0.796875 0 -1.25 0.453125q-0.453125 0.4375 -0.453125 1.21875l-1.125 0q0.03125 -0.8125 0.375 -1.421875q0.359375 -0.609375 0.984375 -0.921875q0.640625 -0.328125 1.46875 -0.328125q0.828125 0 1.421875 0.328125q0.609375 0.3125 0.921875 0.890625q0.328125 0.578125 0.328125 1.390625q0 0.765625 -0.40625 1.53125q-0.40625 0.765625 -1.34375 1.734375l-2.234375 2.3125l4.140625 0l0 1.015625l-5.40625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m299.25433 132.59987c0 7.909317 -17.549713 11.866119 -35.099426 15.818634c-17.549713 3.95253 -35.099426 7.9007874 -35.099426 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m299.25433 132.59987c0 7.909317 -17.549713 11.866119 -35.099426 15.818634c-17.549713 3.95253 -35.099426 7.9007874 -35.099426 15.801559" fill-rule="evenodd"/><path fill="var(--background-color)" d="m279.84467 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093048 2.0466385 13.736084 5.689678c3.643036 3.6430397 5.6896973 8.5840645 5.6896973 13.736099l0 0c0 10.728561 -8.697235 19.425781 -19.425781 19.425781l0 0c-10.728577 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m279.84467 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093048 2.0466385 13.736084 5.689678c3.643036 3.6430397 5.6896973 8.5840645 5.6896973 13.736099l0 0c0 10.728561 -8.697235 19.425781 -19.425781 19.425781l0 0c-10.728577 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m279.8276 23.259514l38.851562 0l0 38.85156l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m290.86273 49.405293l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m302.2596 56.405293l0 -1.140625l2.78125 -2.953125q0.703125 -0.75 1.03125 -1.34375q0.328125 -0.59375 0.328125 -1.140625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.4375 -1.140625 -0.4375q-0.796875 0 -1.25 0.453125q-0.453125 0.4375 -0.453125 1.21875l-1.125 0q0.03125 -0.8125 0.375 -1.421875q0.359375 -0.609375 0.984375 -0.921875q0.640625 -0.328125 1.46875 -0.328125q0.828125 0 1.421875 0.328125q0.609375 0.3125 0.921875 0.890625q0.328125 0.578125 0.328125 1.390625q0 0.765625 -0.40625 1.53125q-0.40625 0.765625 -1.34375 1.734375l-2.234375 2.3125l4.140625 0l0 1.015625l-5.40625 0z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m209.62544 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425766 -19.425777l0 0c5.1520386 0 10.093063 2.0466385 13.7361145 5.689678c3.643036 3.6430397 5.6896667 8.5840645 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728561 0 -19.425766 -8.69722 -19.425766 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m209.62544 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425766 -19.425777l0 0c5.1520386 0 10.093063 2.0466385 13.7361145 5.689678c3.643036 3.6430397 5.6896667 8.5840645 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728561 0 -19.425766 -8.69722 -19.425766 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m209.60838 23.259514l38.851562 0l0 38.85156l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m223.03413 52.764668l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m232.04039 56.405293l0 -1.140625l2.78125 -2.953125q0.703125 -0.75 1.03125 -1.34375q0.328125 -0.59375 0.328125 -1.140625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.4375 -1.140625 -0.4375q-0.796875 0 -1.25 0.453125q-0.453125 0.4375 -0.453125 1.21875l-1.125 0q0.03125 -0.8125 0.375 -1.421875q0.359375 -0.609375 0.984375 -0.921875q0.640625 -0.328125 1.46875 -0.328125q0.828125 0 1.421875 0.328125q0.609375 0.3125 0.921875 0.890625q0.328125 0.578125 0.328125 1.390625q0 0.765625 -0.40625 1.53125q-0.40625 0.765625 -1.34375 1.734375l-2.234375 2.3125l4.140625 0l0 1.015625l-5.40625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m229.03416 62.111073c0 7.909313 0.017044067 11.866108 0.034103394 15.81863c0.017059326 3.9525223 0.034118652 7.9007797 0.034118652 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m229.03416 62.111073c0 7.909313 0.017044067 11.866108 0.034103394 15.81863c0.017059326 3.9525223 0.034118652 7.9007797 0.034118652 15.801559" fill-rule="evenodd"/><path fill="var(--text-color)" d="m209.62686 113.191154l0 0c0 -10.728561 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093048 2.0466385 13.736099 5.6896744c3.643036 3.6430435 5.689682 8.584068 5.689682 13.736107l0 0c0 10.728561 -8.69722 19.425774 -19.425781 19.425774l0 0c-10.728561 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m209.6098 93.748314l38.851562 0l0 38.851555l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m226.8137 118.1441l-1.34375 -0.9375l1.015625 -1.453125q0.234375 -0.359375 0.5625 -0.703125q0.34375 -0.34375 0.6875 -0.640625q0.34375 -0.3125 0.609375 -0.546875l-0.0625 -0.125q-0.34375 -0.03125 -0.796875 -0.078125q-0.453125 -0.046875 -0.90625 -0.15625q-0.453125 -0.109375 -0.84375 -0.234375l-1.640625 -0.59375l0.5625 -1.546875l1.640625 0.59375q0.390625 0.125 0.796875 0.359375q0.421875 0.234375 0.8125 0.484375q0.40625 0.25 0.6875 0.484375l0.09375 -0.078125q-0.09375 -0.359375 -0.21875 -0.8125q-0.109375 -0.453125 -0.1875 -0.9375q-0.0625 -0.5 -0.0625 -0.9375l0 -1.765625l1.640625 0l0 1.765625q0 0.4375 -0.078125 0.9375q-0.078125 0.484375 -0.1875 0.9375q-0.109375 0.453125 -0.21875 0.8125l0.09375 0.078125q0.296875 -0.234375 0.671875 -0.484375q0.390625 -0.25 0.796875 -0.484375q0.421875 -0.234375 0.828125 -0.359375l1.65625 -0.59375l0.5625 1.546875l-1.671875 0.59375q-0.375 0.125 -0.828125 0.234375q-0.453125 0.109375 -0.90625 0.15625q-0.4375 0.046875 -0.796875 0.078125l-0.078125 0.15625q0.28125 0.21875 0.625 0.53125q0.34375 0.296875 0.671875 0.640625q0.328125 0.34375 0.546875 0.6875l1.03125 1.453125l-1.34375 0.9375l-1.03125 -1.453125q-0.21875 -0.34375 -0.4375 -0.765625q-0.203125 -0.4375 -0.390625 -0.859375q-0.171875 -0.4375 -0.28125 -0.765625l-0.140625 0q-0.109375 0.328125 -0.28125 0.765625q-0.171875 0.421875 -0.390625 0.859375q-0.21875 0.421875 -0.453125 0.765625l-1.015625 1.453125z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m209.65244 183.67996l0 0c0 -10.728577 8.69722 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093063 2.0466309 13.736099 5.6896667c3.6430511 3.6430511 5.689682 8.584076 5.689682 13.7361145l0 0c0 10.728561 -8.69722 19.425766 -19.425781 19.425766l0 0c-10.728561 0 -19.425781 -8.697205 -19.425781 -19.425766z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m209.63539 164.23712l38.851547 0l0 38.851562l-38.851547 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m228.24553 188.61728l0 -3.640625l-3.5625 0l0 -1.5l3.5625 0l0 -3.640625l1.640625 0l0 3.640625l3.5625 0l0 1.5l-3.5625 0l0 3.640625l-1.640625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m248.48694 183.6629c7.8354187 0 11.752411 0.017059326 15.670837 0.034103394c3.9184265 0.017059326 7.8382263 0.034118652 15.676483 0.034118652" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m248.48694 183.66289c7.8354187 0 11.752411 0.017059326 15.670837 0.03413391c0.97961426 0.004257202 1.9592896 0.008529663 3.0002747 0.012527466l0.676239 0.0021820068" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m267.82895 187.0152l9.081512 -3.288803l-9.070862 -3.3181152z" fill-rule="evenodd"/><path fill="var(--gray-text-color)" d="m384.51456 32.102238l0 0c0 -10.634172 8.620697 -19.254873 19.254852 -19.254873l91.45026 0c5.10672 0 10.004272 2.0286322 13.615265 5.6396217c3.6109924 3.6109886 5.639618 8.508541 5.639618 13.615252l0 162.16078c0 10.634171 -8.620697 19.254868 -19.254883 19.254868l-91.45026 0c-10.634155 0 -19.254852 -8.620697 -19.254852 -19.254868z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m465.2023 183.67427l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.046646 13.7361145 5.689682c3.643036 3.643036 5.6896667 8.584061 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m465.2023 183.67427l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.046646 13.7361145 5.689682c3.643036 3.643036 5.6896667 8.584061 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m465.18524 164.23143l38.851562 0l0 38.851562l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m480.56412 190.53346q-1.046875 0 -1.828125 -0.328125q-0.765625 -0.328125 -1.234375 -0.921875q-0.453125 -0.609375 -0.546875 -1.421875l1.6875 0q0.109375 0.53125 0.59375 0.859375q0.5 0.3125 1.328125 0.3125l0.78125 0q0.984375 0 1.46875 -0.390625q0.484375 -0.40625 0.484375 -1.078125q0 -0.65625 -0.4375 -1.03125q-0.4375 -0.390625 -1.296875 -0.515625l-1.375 -0.234375q-1.515625 -0.25 -2.25 -0.9375q-0.734375 -0.6875 -0.734375 -1.984375q0 -1.390625 0.875 -2.140625q0.875 -0.75 2.625 -0.75l0.71875 0q1.46875 0 2.359375 0.6875q0.90625 0.6875 1.09375 1.859375l-1.6875 0q-0.109375 -0.484375 -0.5625 -0.765625q-0.4375 -0.296875 -1.203125 -0.296875l-0.71875 0q-0.9375 0 -1.390625 0.359375q-0.4375 0.359375 -0.4375 1.0625q0 0.640625 0.375 0.9375q0.375 0.296875 1.1875 0.421875l1.375 0.234375q1.65625 0.25 2.40625 0.96875q0.765625 0.703125 0.765625 2.046875q0 1.421875 -0.90625 2.234375q-0.90625 0.8125 -2.734375 0.8125l-0.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m490.07037 197.50221q-0.8125 0 -1.4375 -0.328125q-0.609375 -0.328125 -0.953125 -0.90625q-0.328125 -0.59375 -0.328125 -1.375l1.109375 0q0 0.75 0.421875 1.1875q0.4375 0.421875 1.203125 0.421875q0.765625 0 1.1875 -0.4375q0.421875 -0.453125 0.421875 -1.171875l0 -0.65625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.421875 -1.1875 -0.421875l-0.9375 0l0 -1.125l2.25 -2.1875l-3.671875 0l0 -1.015625l4.8125 0l0 1.125l-2.4375 2.34375l0 -0.140625q1.25 0 1.984375 0.71875q0.734375 0.703125 0.734375 1.890625l0 0.65625q0 0.78125 -0.34375 1.375q-0.34375 0.578125 -0.96875 0.90625q-0.609375 0.328125 -1.4375 0.328125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m484.61008 62.111073c0 7.909313 -17.558258 11.866108 -35.116486 15.81863c-17.558258 3.9525223 -35.116486 7.9007797 -35.116486 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m484.61008 62.111073c0 7.909313 -17.558258 11.866108 -35.116486 15.81863c-17.558258 3.9525223 -35.116486 7.9007797 -35.116486 15.801559" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m433.81805 113.174095c7.841736 0 11.760803 0.017059326 15.683502 0.034111023c3.9226685 0.017051697 7.848938 0.034111023 15.6979065 0.034111023" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m433.81802 113.1741c7.8417664 0 11.760834 0.017044067 15.683533 0.034111023l3.6979065 0.013587952" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m453.19385 116.52527l9.081818 -3.2879486l-9.070526 -3.3189697z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m465.2023 113.191154l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.6896744c3.643036 3.6430435 5.6896667 8.584068 5.6896667 13.736107l0 0c0 10.728561 -8.697205 19.425774 -19.425781 19.425774l0 0c-10.728546 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m465.2023 113.191154l0 0c0 -10.728561 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.6896744c3.643036 3.6430435 5.6896667 8.584068 5.6896667 13.736107l0 0c0 10.728561 -8.697205 19.425774 -19.425781 19.425774l0 0c-10.728546 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m465.18524 93.748314l38.851562 0l0 38.851555l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m476.95474 119.8941l0 -1.671875l5.75 -7.0625l-5.59375 0l0 -1.53125l7.484375 0l0 1.6875l-5.859375 7.046875l6.0625 0l0 1.53125l-7.84375 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m490.07037 127.0191q-0.8125 0 -1.4375 -0.328125q-0.609375 -0.328125 -0.953125 -0.90625q-0.328125 -0.59375 -0.328125 -1.375l1.109375 0q0 0.75 0.421875 1.1875q0.4375 0.421875 1.203125 0.421875q0.765625 0 1.1875 -0.4375q0.421875 -0.453125 0.421875 -1.171875l0 -0.65625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.421875 -1.1875 -0.421875l-0.9375 0l0 -1.125l2.25 -2.1875l-3.671875 0l0 -1.015625l4.8125 0l0 1.125l-2.4375 2.34375l0 -0.140625q1.25 0 1.984375 0.71875q0.734375 0.703125 0.734375 1.890625l0 0.65625q0 0.78125 -0.34375 1.375q-0.34375 0.578125 -0.96875 0.90625q-0.609375 0.328125 -1.4375 0.328125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m484.61102 132.59987c0 7.909317 -17.549713 11.866119 -35.099426 15.818634c-17.549713 3.95253 -35.099426 7.9007874 -35.099426 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m484.61102 132.59987c0 7.909317 -17.549713 11.866119 -35.099426 15.818634c-17.549713 3.95253 -35.099426 7.9007874 -35.099426 15.801559" fill-rule="evenodd"/><path fill="var(--background-color)" d="m465.20135 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093048 2.0466385 13.736084 5.689678c3.643036 3.6430397 5.6896973 8.5840645 5.6896973 13.736099l0 0c0 10.728561 -8.697235 19.425781 -19.425781 19.425781l0 0c-10.728577 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m465.20135 42.702347l0 0c0 -10.7285595 8.697205 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093048 2.0466385 13.736084 5.689678c3.643036 3.6430397 5.6896973 8.5840645 5.6896973 13.736099l0 0c0 10.728561 -8.697235 19.425781 -19.425781 19.425781l0 0c-10.728577 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m465.1843 23.259514l38.851562 0l0 38.85156l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m476.21942 49.405293l3.640625 -5.28125l-3.421875 -4.984375l1.984375 0l2.03125 3.171875q0.125 0.203125 0.25 0.453125q0.125 0.234375 0.1875 0.390625q0.0625 -0.15625 0.171875 -0.390625q0.125 -0.25 0.265625 -0.453125l2.046875 -3.171875l1.96875 0l-3.421875 5.0l3.625 5.265625l-1.984375 0l-2.203125 -3.359375q-0.125 -0.203125 -0.25 -0.46875q-0.125 -0.28125 -0.21875 -0.4375q-0.0625 0.15625 -0.203125 0.4375q-0.140625 0.265625 -0.296875 0.46875l-2.21875 3.359375l-1.953125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m490.06943 56.530293q-0.8125 0 -1.4375 -0.328125q-0.609375 -0.328125 -0.953125 -0.90625q-0.328125 -0.59375 -0.328125 -1.375l1.109375 0q0 0.75 0.421875 1.1875q0.4375 0.421875 1.203125 0.421875q0.765625 0 1.1875 -0.4375q0.421875 -0.453125 0.421875 -1.171875l0 -0.65625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.421875 -1.1875 -0.421875l-0.9375 0l0 -1.125l2.25 -2.1875l-3.671875 0l0 -1.015625l4.8125 0l0 1.125l-2.4375 2.34375l0 -0.140625q1.25 0 1.984375 0.71875q0.734375 0.703125 0.734375 1.890625l0 0.65625q0 0.78125 -0.34375 1.375q-0.34375 0.578125 -0.96875 0.90625q-0.609375 0.328125 -1.4375 0.328125z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m394.98212 42.702347l0 0c0 -10.7285595 8.697235 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.689678c3.643036 3.6430397 5.6896667 8.5840645 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m394.98212 42.702347l0 0c0 -10.7285595 8.697235 -19.425777 19.425781 -19.425777l0 0c5.1520386 0 10.093079 2.0466385 13.7361145 5.689678c3.643036 3.6430397 5.6896667 8.5840645 5.6896667 13.736099l0 0c0 10.728561 -8.697205 19.425781 -19.425781 19.425781l0 0c-10.728546 0 -19.425781 -8.69722 -19.425781 -19.425781z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m394.96506 23.259514l38.851562 0l0 38.85156l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m408.3908 52.764668l1.5 -4.0l-3.796875 -9.625l1.859375 0l2.390625 6.34375q0.125 0.328125 0.234375 0.765625q0.125 0.4375 0.1875 0.734375q0.078125 -0.296875 0.1875 -0.734375q0.109375 -0.4375 0.234375 -0.765625l2.234375 -6.34375l1.8125 0l-5.0625 13.625l-1.78125 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m419.8502 56.530293q-0.8125 0 -1.4375 -0.328125q-0.609375 -0.328125 -0.953125 -0.90625q-0.328125 -0.59375 -0.328125 -1.375l1.109375 0q0 0.75 0.421875 1.1875q0.4375 0.421875 1.203125 0.421875q0.765625 0 1.1875 -0.4375q0.421875 -0.453125 0.421875 -1.171875l0 -0.65625q0 -0.765625 -0.421875 -1.1875q-0.421875 -0.421875 -1.1875 -0.421875l-0.9375 0l0 -1.125l2.25 -2.1875l-3.671875 0l0 -1.015625l4.8125 0l0 1.125l-2.4375 2.34375l0 -0.140625q1.25 0 1.984375 0.71875q0.734375 0.703125 0.734375 1.890625l0 0.65625q0 0.78125 -0.34375 1.375q-0.34375 0.578125 -0.96875 0.90625q-0.609375 0.328125 -1.4375 0.328125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m414.39084 62.111073c0 7.909313 0.017059326 11.866108 0.034118652 15.81863c0.017059326 3.9525223 0.034118652 7.9007797 0.034118652 15.801559" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m414.39084 62.111073c0 7.909313 0.017059326 11.866108 0.034118652 15.81863c0.017059326 3.9525223 0.034118652 7.9007797 0.034118652 15.801559" fill-rule="evenodd"/><path fill="var(--text-color)" d="m394.98355 113.191154l0 0c0 -10.728561 8.697205 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093048 2.0466385 13.736084 5.6896744c3.6430664 3.6430435 5.6896973 8.584068 5.6896973 13.736107l0 0c0 10.728561 -8.697235 19.425774 -19.425781 19.425774l0 0c-10.728577 0 -19.425781 -8.697212 -19.425781 -19.425774z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m394.9665 93.748314l38.851562 0l0 38.851555l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m412.17038 118.1441l-1.34375 -0.9375l1.015625 -1.453125q0.234375 -0.359375 0.5625 -0.703125q0.34375 -0.34375 0.6875 -0.640625q0.34375 -0.3125 0.609375 -0.546875l-0.0625 -0.125q-0.34375 -0.03125 -0.796875 -0.078125q-0.453125 -0.046875 -0.90625 -0.15625q-0.453125 -0.109375 -0.84375 -0.234375l-1.640625 -0.59375l0.5625 -1.546875l1.640625 0.59375q0.390625 0.125 0.796875 0.359375q0.421875 0.234375 0.8125 0.484375q0.40625 0.25 0.6875 0.484375l0.09375 -0.078125q-0.09375 -0.359375 -0.21875 -0.8125q-0.109375 -0.453125 -0.1875 -0.9375q-0.0625 -0.5 -0.0625 -0.9375l0 -1.765625l1.640625 0l0 1.765625q0 0.4375 -0.078125 0.9375q-0.078125 0.484375 -0.1875 0.9375q-0.109375 0.453125 -0.21875 0.8125l0.09375 0.078125q0.296875 -0.234375 0.671875 -0.484375q0.390625 -0.25 0.796875 -0.484375q0.421875 -0.234375 0.828125 -0.359375l1.65625 -0.59375l0.5625 1.546875l-1.671875 0.59375q-0.375 0.125 -0.828125 0.234375q-0.453125 0.109375 -0.90625 0.15625q-0.4375 0.046875 -0.796875 0.078125l-0.078125 0.15625q0.28125 0.21875 0.625 0.53125q0.34375 0.296875 0.671875 0.640625q0.328125 0.34375 0.546875 0.6875l1.03125 1.453125l-1.34375 0.9375l-1.03125 -1.453125q-0.21875 -0.34375 -0.4375 -0.765625q-0.203125 -0.4375 -0.390625 -0.859375q-0.171875 -0.4375 -0.28125 -0.765625l-0.140625 0q-0.109375 0.328125 -0.28125 0.765625q-0.171875 0.421875 -0.390625 0.859375q-0.21875 0.421875 -0.453125 0.765625l-1.015625 1.453125z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m395.00912 183.67996l0 0c0 -10.728577 8.697235 -19.425781 19.425781 -19.425781l0 0c5.1520386 0 10.093079 2.0466309 13.7361145 5.6896667c3.643036 3.6430511 5.6896667 8.584076 5.6896667 13.7361145l0 0c0 10.728561 -8.697205 19.425766 -19.425781 19.425766l0 0c-10.728546 0 -19.425781 -8.697205 -19.425781 -19.425766z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m394.99207 164.23712l38.851562 0l0 38.851562l-38.851562 0z" fill-rule="evenodd"/><path fill="var(--background-color)" d="m413.60223 188.61728l0 -3.640625l-3.5625 0l0 -1.5l3.5625 0l0 -3.640625l1.640625 0l0 3.640625l3.5625 0l0 1.5l-3.5625 0l0 3.640625l-1.640625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m433.84363 183.6629c7.8354187 0 11.752411 0.017059326 15.670837 0.034103394c3.9184265 0.017059326 7.8382263 0.034118652 15.676483 0.034118652" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m433.84363 183.66289c7.8354187 0 11.752411 0.017059326 15.670837 0.03413391c0.97961426 0.004257202 1.9592896 0.008529663 3.0002441 0.012527466l0.676239 0.0021820068" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m453.18564 187.0152l9.081512 -3.288803l-9.070862 -3.3181152z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m133.32344 183.65721c19.077972 0 28.616577 0.01574707 38.155945 0.03149414c9.539368 0.01574707 19.079514 0.03149414 38.15901 0.03149414" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m133.32344 183.65721c19.077972 0 28.616577 0.01574707 38.155945 0.03149414c9.539368 0.01574707 19.079514 0.03149414 38.15901 0.03149414" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m318.6801 183.65721c19.078003 0 28.616608 0.01574707 38.155975 0.03149414c9.539368 0.01574707 19.079498 0.03149414 38.158997 0.03149414" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m318.6801 183.65721c19.078003 0 28.616608 0.01574707 38.155975 0.03149414c9.539368 0.01574707 19.079498 0.03149414 38.158997 0.03149414" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m183.35564 235.71129l77.98427 0l0 37.16536l-77.98427 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m195.57439 258.7913l0 -6.046875l-2.34375 0l0 -1.203125l2.34375 0l0 -1.390625q0 -0.953125 0.59375 -1.5q0.609375 -0.5625 1.671875 -0.5625l2.359375 0l0 1.203125l-2.359375 0q-0.453125 0 -0.703125 0.21875q-0.234375 0.203125 -0.234375 0.640625l0 1.390625l3.296875 0l0 1.203125l-3.296875 0l0 6.046875l-1.328125 0zm11.075027 0q-0.703125 0 -1.234375 -0.28125q-0.53125 -0.28125 -0.828125 -0.78125q-0.28125 -0.515625 -0.28125 -1.203125l0 -7.234375l-2.71875 0l0 -1.203125l4.03125 0l0 8.4375q0 0.5 0.28125 0.78125q0.28125 0.28125 0.75 0.28125l2.5625 0l0 1.203125l-2.5625 0zm7.6844025 0.125q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm8.090652 1.203125q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm10.059402 0.890625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0z" fill-rule="nonzero"/><path fill="var(--background-color)" d="m161.11942 254.27916l0 0c0 -6.1403656 4.9777527 -11.118118 11.118118 -11.118118l0 0c2.9487 0 5.776642 1.1713715 7.8616943 3.256424c2.0850525 2.0850525 3.2564087 4.912979 3.2564087 7.8616943l0 0c0 6.1403656 -4.9777374 11.118118 -11.118103 11.118118l0 0c-6.1403656 0 -11.118118 -4.9777527 -11.118118 -11.118118z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m161.11942 254.27916l0 0c0 -6.1403656 4.9777527 -11.118118 11.118118 -11.118118l0 0c2.9487 0 5.776642 1.1713715 7.8616943 3.256424c2.0850525 2.0850525 3.2564087 4.912979 3.2564087 7.8616943l0 0c0 6.1403656 -4.9777374 11.118118 -11.118103 11.118118l0 0c-6.1403656 0 -11.118118 -4.9777527 -11.118118 -11.118118z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m266.9357 254.29347l0 0c0 -6.149063 4.9777527 -11.133865 11.118103 -11.133865l0 0c2.9487 0 5.776642 1.1730347 7.8616943 3.261032c2.0850525 2.0880127 3.2564087 4.9199524 3.2564087 7.8728333l0 0c0 6.149063 -4.977722 11.133865 -11.118103 11.133865l0 0c-6.1403503 0 -11.118103 -4.9848022 -11.118103 -11.133865z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m289.1732 235.71129l105.82678 0l0 37.16536l-105.82678 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m302.5638 258.9163q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm5.746887 3.703125l0 -10.703125l1.328125 0l0 1.546875l0.265625 0l-0.265625 0.296875q0 -0.921875 0.609375 -1.453125q0.609375 -0.53125 1.59375 -0.53125q1.21875 0 1.9375 0.796875q0.734375 0.796875 0.734375 2.1875l0 2.359375q0 0.921875 -0.328125 1.59375q-0.328125 0.671875 -0.921875 1.046875q-0.59375 0.359375 -1.421875 0.359375q-0.96875 0 -1.59375 -0.53125q-0.609375 -0.546875 -0.609375 -1.453125l0.265625 0.3125l-0.296875 0l0.03125 1.828125l0 2.34375l-1.328125 0zm3.09375 -3.640625q0.84375 0 1.3125 -0.5q0.484375 -0.5 0.484375 -1.4375l0 -2.1875q0 -0.9375 -0.484375 -1.4375q-0.46875 -0.5 -1.3125 -0.5q-0.796875 0 -1.28125 0.515625q-0.484375 0.515625 -0.484375 1.421875l0 2.1875q0 0.921875 0.484375 1.4375q0.484375 0.5 1.28125 0.5zm8.746918 1.140625q-0.953125 0 -1.671875 -0.375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.625l0 -2.1875q0 -0.9375 0.375 -1.625q0.390625 -0.703125 1.09375 -1.078125q0.71875 -0.375 1.671875 -0.375q0.953125 0 1.65625 0.375q0.71875 0.375 1.109375 1.078125q0.390625 0.6875 0.390625 1.625l0 1.421875l-5.015625 0l0 0.765625q0 0.953125 0.484375 1.46875q0.484375 0.5 1.375 0.5q0.765625 0 1.234375 -0.265625q0.46875 -0.28125 0.578125 -0.8125l1.3125 0q-0.125 1.0 -0.984375 1.59375q-0.859375 0.59375 -2.140625 0.59375zm1.875 -4.734375l0 -0.53125q0 -0.953125 -0.484375 -1.46875q-0.46875 -0.515625 -1.390625 -0.515625q-0.890625 0 -1.375 0.515625q-0.484375 0.515625 -0.484375 1.46875l0 0.421875l3.828125 0l-0.09375 0.109375zm4.153137 4.59375l0 -8.0625l1.296875 0l0 1.546875l0.296875 0l-0.421875 0.953125q0 -1.3125 0.5625 -1.96875q0.5625 -0.671875 1.6875 -0.671875q1.265625 0 2.015625 0.796875q0.765625 0.796875 0.765625 2.15625l0 0.484375l-1.328125 0l0 -0.359375q0 -0.953125 -0.46875 -1.453125q-0.453125 -0.515625 -1.296875 -0.515625q-0.828125 0 -1.3125 0.515625q-0.46875 0.515625 -0.46875 1.453125l0 5.125l-1.328125 0zm10.856293 0.140625q-1.265625 0 -2.0 -0.640625q-0.734375 -0.65625 -0.734375 -1.765625q0 -1.140625 0.734375 -1.78125q0.734375 -0.640625 1.984375 -0.640625l2.484375 0l0 -0.8125q0 -0.75 -0.453125 -1.140625q-0.4375 -0.40625 -1.265625 -0.40625q-0.71875 0 -1.203125 0.296875q-0.484375 0.296875 -0.546875 0.8125l-1.328125 0q0.140625 -1.03125 0.984375 -1.640625q0.84375 -0.625 2.125 -0.625q1.390625 0 2.1875 0.71875q0.8125 0.71875 0.8125 1.9375l0 5.546875l-1.28125 0l0 -1.53125l-0.21875 0l0.21875 -0.234375q0 0.890625 -0.6875 1.40625q-0.671875 0.5 -1.8125 0.5zm0.328125 -1.03125q0.9375 0 1.53125 -0.46875q0.609375 -0.46875 0.609375 -1.203125l0 -1.140625l-2.46875 0q-0.65625 0 -1.046875 0.375q-0.375 0.359375 -0.375 0.984375q0 0.671875 0.453125 1.0625q0.46875 0.390625 1.296875 0.390625zm10.059387 0.890625q-1.046875 0 -1.625 -0.546875q-0.578125 -0.5625 -0.578125 -1.578125l0 -4.734375l-2.265625 0l0 -1.203125l2.265625 0l0 -2.265625l1.3125 0l0 2.265625l3.234375 0l0 1.203125l-3.234375 0l0 4.734375q0 0.921875 0.890625 0.921875l2.1875 0l0 1.203125l-2.1875 0zm4.7625427 0l0 -1.203125l2.859375 0l0 -5.65625l-2.5 0l0 -1.203125l3.8125 0l0 6.859375l2.71875 0l0 1.203125l-6.890625 0zm3.359375 -9.515625q-0.484375 0 -0.765625 -0.25q-0.265625 -0.25 -0.265625 -0.671875q0 -0.4375 0.265625 -0.6875q0.28125 -0.265625 0.765625 -0.265625q0.484375 0 0.765625 0.265625q0.28125 0.25 0.28125 0.6875q0 0.421875 -0.28125 0.671875q-0.28125 0.25 -0.765625 0.25zm8.575012 9.640625q-0.96875 0 -1.671875 -0.359375q-0.703125 -0.375 -1.09375 -1.078125q-0.375 -0.703125 -0.375 -1.640625l0 -2.140625q0 -0.96875 0.375 -1.65625q0.390625 -0.703125 1.09375 -1.0625q0.703125 -0.359375 1.671875 -0.359375q0.96875 0 1.671875 0.359375q0.703125 0.359375 1.09375 1.0625q0.390625 0.6875 0.390625 1.640625l0 2.15625q0 0.9375 -0.390625 1.640625q-0.390625 0.703125 -1.09375 1.078125q-0.703125 0.359375 -1.671875 0.359375zm0 -1.1875q0.875 0 1.359375 -0.46875q0.484375 -0.484375 0.484375 -1.421875l0 -2.140625q0 -0.9375 -0.484375 -1.421875q-0.484375 -0.484375 -1.359375 -0.484375q-0.84375 0 -1.34375 0.484375q-0.484375 0.484375 -0.484375 1.421875l0 2.140625q0 0.9375 0.484375 1.421875q0.5 0.46875 1.34375 0.46875zm5.7469177 1.0625l0 -8.0625l1.328125 0l0 1.546875l0.28125 0l-0.28125 0.296875q0 -0.953125 0.5625 -1.46875q0.578125 -0.515625 1.59375 -0.515625q1.203125 0 1.921875 0.75q0.71875 0.734375 0.71875 2.03125l0 5.421875l-1.3125 0l0 -5.28125q0 -0.859375 -0.46875 -1.3125q-0.453125 -0.46875 -1.25 -0.46875q-0.8125 0 -1.296875 0.5q-0.46875 0.5 -0.46875 1.4375l0 5.125l-1.328125 0z" fill-rule="nonzero"/></g></svg></figure>

<p>All the multiplications to compute $\mathbf{z}$ are independent, which can run in parallel.
All the add operations to compute $\mathbf{s}$ are sequential. However, no
operation will be blocked by this accumulation because the data of input vectors
are streamed into the MXU. As soon as the multiplication of two floats are done, a new pair of $x$ and $y$ from the next pair of vectors are pushed in.
Similarly, when we finish one add operaion to produce $s_2=s_1+z_2$, the $s_1$ and $z_2$ for the next vector pair is already there.
It is like a factory pipeline, though the steps are sequential, they do not wait
in idle, but stream the products through the pipeline.</p>
<p>This example only shows how to compute the vector inner-product of length 3.
However, this small structure is tileable. If we stack more of such structure
together, we can compute much longer vectors and matrices.</p>
<p>With MXU instead of GPU, we save time by removing the need for instruction
decoding since it just multiplies and adds whatever it receives. There is no
register for writing and reading since we already know where the results should
go, and there is no need to store it for arbitrary operations that come next.</p>
<p>Besides the MXU, the TPU has also been designed for better scalability. It has dedicated ports for high-bandwidth inter-chip interconnection (ICI). It is designed to sit on the racks in Google's data centers and to be used in clusters. Since it is for proprietary usage only, they don't need to worry about selling single chips or the complexity of installing the chips on the racks.</p>
<h2>Are TPUs still faster today?</h2>
<p>It doesn't make sense others didn't come up with the same simple idea of building dedicated cores for tensor operations (matrix multiplication). Even if they did not, it doesn't make sense that they don't copy.</p>
<p>From the timeline, it seems Nvidia came up with the same idea at about the same time. A similar product from Nvidia, the Tensor Cores, was first announced to the public in 2017<sup id="cite-2-a"><a href="#ref-2" class="citation-link">2</a></sup>, one year after Google's TPU announcement.</p>
<p>It is unclear whether TPUs are still faster than GPUs today. I cannot find public benchmarks of the latest generations of TPUs and GPUs, and it is unclear to me which generation and metrics should be used for benchmarking.</p>
<p>However, we can use one universal application-oriented metric: dollars per epoch. I found an interesting benchmark from Google Cloud<sup id="cite-3-a"><a href="#ref-3" class="citation-link">3</a></sup> that aligns different hardware to the same axis: money. TPUs appear cheaper on Google Cloud if you have the same model, data, and number of epochs.</p>
<p>Large models, like Midjourney, Claude, and Gemini, are all very sensitive to the training cost because they consume too much computing power. As a result, many of them use TPUs on Google Cloud.</p>
<h2>Why are TPUs cheaper?</h2>
<p>One important reason is the software stack. You are using not only the hardware but also the software stack associated with it. Google has better vertical integration for its software stack and AI hardware than GPUs.</p>
<p>Google has dedicated engineering teams to build a whole software stack for it with strong vertical integration, from the model implementation (Vertex Model Garden) to the deep learning frameworks (Keras, JAX, and TensorFlow) to a compiler well-optimized for the TPUs (XLA).</p>
<p>The software stack for GPUs is very different. PyTorch is the most popular deep learning framework used with Nvidia GPUs, and it was mainly developed by Meta. The most widely used model pools with PyTorch are transformers and diffusers developed by HuggingFace. It is much harder to do perfect vertical integration for the software stack across all these companies.</p>
<p>One caveat is that fewer models are implemented with JAX and TensorFlow. Sometimes, you may need to implement the model yourself or use it from PyTorch on TPUs. Depending on the implementation, you may experience some friction when using PyTorch on TPUs. So, there might be extra engineering costs besides the hardware cost itself.</p>
<h2>Why not start selling TPUs?</h2>
<p>We understand the project was started for proprietary usage and acquired a pretty good user base on Google Cloud because of its lower price. Why did not Google just start to sell it to customers directly, just like Nvidia's GPUs?</p>
<p>The short answer is to stay focused. Google is in fierce competition with OpenAI for generative AI. At the same time, it is in the middle of multiple waves of tech layoffs to lower its cost. A wise strategy now would be to focus its limited resources on the most important projects.</p>
<p>If Google ever wants to start selling its TPUs, it will be competing with two strong opponents, Nvidia and OpenAI, at the same time, which may not be a wise move at the moment.</p>
<h2>The huge overhead of selling hardware</h2>
<p>Selling hardware directly to customers creates huge overheads for the company. Conversely, renting TPUs on their cloud services is much more manageable.</p>
<p>When TPUs are only served on the cloud, they can have a centralized way to install all the TPUs and related software. There is no need to deal with various installation environments or the difficulty of deploying a TPU cluster.</p>
<p>They know exactly how many TPUs to make. The demands are all internal, so there is no uncertainty. Thus, managing the supply chain is much easier.</p>
<p>Sales also become much easier since it is just selling the cloud service. There is no need to build a new team experienced in selling hardware.</p>
<h2>The advantages of the TPU approach</h2>
<p>Without all the overhead of selling hardware directly to the customers, Google got a few advantages in return.</p>
<p>First, they can have a more aggressive TPU architecture design. The TPUs have a unique way of connecting the chips. Unlike multiple GPUs that connect to the same board, TPUs are organized in cubes. They arranged 64 TPUs in a 4 by 4 by 4 cube to interconnect them with each other for faster inter-chip communication. There are 8960 chips in a single v5p Pod. They can be easily used together. This is the advantage of fully control your hardware installation environment.</p>
<p>Second, they can iterate faster to push out new generations. Since they only need to support a small set of use cases for proprietary usages, it drastically reduces their research and development cycle for every generation of the chips. I wonder if Nvidia came up with the TensorCore idea earlier than Google, but because of the overhead of selling hardware to external customers, they could only announce it one year later than Google.</p>
<p>From the perspective of serving its most important purpose, competing in GenAI, these advantages put Google in a very good position. Most importantly, with this in-house hardware solution, Google saved huge money by not buying GPUs from Nvidia at a monopoly price.</p>
<h2>The downside of the TPU approach</h2>
<p>So far, we have discussed many advantages of Google's AI hardware approach, but is there any downside? Indeed, there is a big one. Google became a tech island.</p>
<p>Every pioneer in tech will become an island isolated from the rest of the world, at least for a while. This is because they started early when the corresponding infrastructure was not ready. They need to build everything from scratch. Due to the migration cost, they will stick with their solution even if everyone else uses something else.</p>
<p>This is exactly what Google is experiencing right now. The rest of the world is innovating with models from HuggingFace and PyTorch. Everyone is quickly tweaking each other's models to develop better ones. However, Google cannot join this process easily since its infra is largely built around TensorFlow and JAX. When putting a model from external into production, it must be re-implemented with Google's framework.</p>
<p>This “tech island” problem slows Google down in taking good solutions from the external world and further isolates it from others. Google will either start bringing more external solutions like HuggingFace, PyTorch, and GPUs or always ensure its in-house solutions are the best in the world.</p>
<h2>What does the future of AI hardware look like?</h2>
<p>Finally, let's peek into the future of AI hardware. What would the future AI
hardware look like? I have two observations: mode collapse as the hardware
becomes more specialized, scale-out rather than scale-up.</p>
<h3>Mode collapse</h3>
<p>Hardware will be further coupled with the applications. For example, support more precision formats for better language model serving. Like with bfloat16 and TF32, they may better support int8 and int4. Nvidia announced their second generation of the <a href="https://docs.nvidia.com/deeplearning/transformer-engine/index.html">Transformer Engine</a>, which works with Blackwell GPU. This made optimizing their hardware for transformer models easier without changing the user code. A lot of codesign is happening.</p>
<p>On the other hand, software cannot easily jump out of the transformer realm. If they do, they will be slow due to a lack of hardware support. On the contrary, they implement their models with the hardware in mind. For example, the FlashAttention algorithm<sup id="cite-4-a"><a href="#ref-4" class="citation-link">4</a></sup> is designed to leverage the memory hierarchy of GPUs for better performance.</p>
<p>We see a big mode collapse coming soon. The hardware and software are so well optimized for each other for the current models. Neither of them can easily leave the current design or algorithm. If there is a new model completely different from the transformers, it needs to be 10x better to get widely adopted. It must incentivize people to make new hardware as fast and cheap as transformers.</p>
<h3>Scaling out rather than scaling up</h3>
<p>Today, AI hardware cares more about scaling out than scaling up. They prefer to optimize the overall performance of a large cluster over improving the performance of a single chip. This is mainly for two reasons. </p>
<p>First, it is hard to improve single-chip performance anymore. Both compute and high bandwidth memory sizes are facing challenges for further improvement. It is also hard to build a chip with a smaller feature size while balancing its cost and heat dissipation.</p>
<p>Second, the single-chip performance is no longer the bottleneck. In the era of large language models, no single chip can handle the entire training job. It requires hundreds or even thousands of GPUs to work together to train a model. The communication bandwidth between chips, machines, rooms, buildings, or even data centers becomes the bottleneck. NVidia is inventing new technologies like <a href="https://www.nvidia.com/en-us/data-center/nvlink/">NVlink</a> and <a href="https://www.nvidia.com/en-us/networking/infiniband-switching/">InfiniBand</a>, to replace the traditional PCIe. Google is also optimizing its TPU's inter-chip interconnection (ICI) for faster large-scale training. These technologies significantly increase the communication speed. We expect to see many more innovations in this direction.</p>
<h2>Summary</h2>
<p>In conclusion, the TPU project started for proprietary usage when the GPU's computing power was insufficient. Google wants to focus on GenAI instead of competing in the AI hardware market to avoid slowing the iteration speed and sacrificing its innovative design. Faster computing at a lower cost helped Google significantly in doing AI research and developing AI applications. However, it also made Google a tech island.</p>
<p>In the future, AI hardware will be even more optimized for specific applications, like the transformer models. Neither the hardware nor the models could easily jump out of this mode collapse. Also, the hardware will work better on a larger scale with more bandwidth between them.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="tpu-announcement" class="csl-entry">Jouppi, N. (2016). <i>Google supercharges machine learning tasks with custom chip</i>. Google Cloud Blog. <a href="https://cloud.google.com/blog/products/ai-machine-learning/google-supercharges-machine-learning-tasks-with-custom-chip" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-2"><div class="csl-bib-body">
  <div data-csl-entry-id="volta-announcement" class="csl-entry">Nvidia. (2017). <i>Inside Volta: The world’s most advanced data center GPU</i>. Nvidia Developer Blog. <a href="https://developer.nvidia.com/blog/inside-volta/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-2-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-3"><div class="csl-bib-body">
  <div data-csl-entry-id="vertex-tpu-benchmark" class="csl-entry">Google Cloud Platform. (2023). <i>JAX ViT benchmarking report on TPU vs GPU</i>. Vertex AI Model Garden Samples. <a href="https://github.com/GoogleCloudPlatform/vertex-ai-samples/blob/03efaadb875e3ba0ccbf5047c308f20dcd4f93d2/community-content/vertex_model_garden/benchmarking_reports/jax_vit_benchmarking_report.md" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-3-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-4"><div class="csl-bib-body">
  <div data-csl-entry-id="flashattention" class="csl-entry">Dao, T., Fu, D. Y., Ermon, S., Rudra, A., &#38; Ré, C. (2022). FlashAttention: Fast and memory-efficient exact attention with IO-awareness. <i>NeurIPS</i>. <a href="https://arxiv.org/abs/2205.14135" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-4-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>Design an Easy-to-Use Deep Learning Framework</title>
    <link href="https://haifengjin.com/design-an-easy-to-use-deep-learning-framework/"/>
    <id>https://haifengjin.com/design-an-easy-to-use-deep-learning-framework/</id>
    <published>2024-04-10T00:00:00+00:00</published>
    <updated>2024-04-10T00:00:00+00:00</updated>
    <summary>The design principles I learned as an open-source contributor</summary>
    <content type="html"><![CDATA[<p>Deep learning frameworks are extremely transitory. If you compare the deep learning frameworks people use today with what it was eight years ago, you will find the landscape is completely different. There were Theano, Caffe2, and MXNet, which all went obsolete. Today's most popular frameworks, like TensorFlow and PyTorch, were just released to the public.</p>
<p>Through all these years, Keras has survived as a high-level user-facing library supporting different backends, including TensorFlow, PyTorch, and JAX. Studying Keras closely, I learned how much the team cares about user experience for the software and how they ensured a good user experience by following a few simple yet powerful principles in their design process.</p>
<p>In this article, I will share the 3 most important software design principles I learned from studying Keras over the past few years, which may be generalizable to all types of software and help you make an impact in the open-source community with yours.</p>
<h2>Why user experience is important for open-source software</h2>
<p>Before we dive into the main content, let's quickly discuss why user experience is so important. We can learn this through the PyTorch vs. TensorFlow case.</p>
<p>They were developed by two tech giants, Meta and Google, and have quite different cultural strengths. Meta is good at product, while Google is good at engineering. As a result, Google's frameworks like TensorFlow and JAX are the fastest to run and technically superior to PyTorch, as they support sparse tensors and distributed training well. However, PyTorch still took away half of the market share from TensorFlow because it prioritizes user experience over other aspects of the software.</p>
<p>Better user experience wins for the research scientists who build the models and propagate them to the engineers, who take models from them since they don't always want to convert the models they receive from the research scientists to another framework. They will build new software around PyTorch to smooth their workflow, which will establish a software ecosystem around PyTorch.</p>
<p>TensorFlow also made a few blunders that caused its users to lose. TensorFlow's general user experience is good. However, its installation guide for GPU support was broken for years before it was fixed in 2022. TensorFlow 2 broke the backward compatibility, which cost its users millions of dollars to migrate.</p>
<p>So, the lesson we learned here is that despite technical superiority, user experience decides which software the open-source users would choose.</p>
<h2>All deep learning frameworks invest heavily in user experience</h2>
<p>All the deep learning frameworks, TensorFlow, PyTorch, and JAX, invest heavily in user experience. Good evidence is that they all have a relatively high Python percentage in their codebases.</p>
<p>All the core logic of deep learning frameworks, including tensor operations, automatic differentiation, compilation, and distribution are implemented in C++. Why would they want to expose a set of Python APIs to the users? It is just because the users love Python and they want to polish their user experience.</p>
<h2>Investing in user experience is of high ROI</h2>
<p>Imagine how much engineering effort it requires to make your deep learning framework a little bit faster than others. A lot.</p>
<p>However, for a better user experience, as long as you follow a certain design process and some principles, you can achieve it. For attracting more users, your user experience is as important as the computing efficiency of your framework. So, investing in user experience is of high return on investment (ROI).</p>
<h2>The three principles</h2>
<p>I will share the three important software design principles I learned by contributing to Keras, each with good and bad code examples from different frameworks.</p>
<h2>Principle 1: Design end-to-end workflows</h2>
<p>When we think of designing the APIs of a piece of software, you may look like this.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Model</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">input</span><span class="p">):</span>
<span class="w">        </span><span class="sd">"""The forward call of the model.</span>

<span class="sd">        Args:</span>
<span class="sd">          input: A tensor. The input to the model.</span>
<span class="sd">        """</span>
        <span class="k">pass</span>
</code></pre></div>

<p>Define the class and add the documentation. Now, we know all the class names, method names, and arguments. However, this would not help us understand much about the user experience.</p>
<p>What we should do is something like this.</p>
<div class="highlight"><pre><span></span><code><span class="nb">input</span> <span class="o">=</span> <span class="n">keras</span><span class="o">.</span><span class="n">Input</span><span class="p">(</span><span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,))</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">layers</span><span class="o">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span><span class="s1">'relu'</span><span class="p">)(</span><span class="nb">input</span><span class="p">)</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">layers</span><span class="o">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span><span class="s1">'softmax'</span><span class="p">)(</span><span class="n">x</span><span class="p">)</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">keras</span><span class="o">.</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">(</span><span class="n">inputs</span><span class="o">=</span><span class="nb">input</span><span class="p">,</span> <span class="n">outputs</span><span class="o">=</span><span class="n">output</span><span class="p">)</span>
<span class="n">model</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span>
    <span class="n">optimizer</span><span class="o">=</span><span class="s1">'adam'</span><span class="p">,</span> <span class="n">loss</span><span class="o">=</span><span class="s1">'categorical_crossentropy'</span>
<span class="p">)</span>
</code></pre></div>

<p>We want to write out the entire user workflow of using the software. Ideally, it should be a tutorial on how to use the software. It provides much more information about the user experience. It may help us spot many more UX problems during the design phase compared with just writing out the class and methods.</p>
<p>Let's look at another example. This is how I discovered a user experience problem by following this principle when implementing a hyperparameter tuning library.</p>
<p>When using KerasTuner, users can use this RandomSearch class to select the best model. We have the metrics, and objectives in the arguments. By default, objective equals validation loss. So, it helps us find the model with the smallest validation loss.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">RandomSearch</span><span class="p">:</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">...</span><span class="p">,</span> <span class="n">metrics</span><span class="p">,</span> <span class="n">objective</span><span class="o">=</span><span class="s2">"val_loss"</span><span class="p">,</span> <span class="o">...</span><span class="p">):</span>
<span class="w">        </span><span class="sd">"""The initializer.</span>

<span class="sd">        Args:</span>
<span class="sd">            metrics: A list of Keras metrics.</span>
<span class="sd">            objective: String or a custom metric function. The</span>
<span class="sd">                name of the metirc we want to minimize.</span>
<span class="sd">        """</span>
        <span class="k">pass</span>
</code></pre></div>

<p>Again, it doesn't provide much information about the user experience. So, everything looks OK for now.</p>
<p>However, if we write an end-to-end workflow like the following. It exposes many more problems. The user is trying to define a custom metric function named <code>custom_metric</code>. The objective is not so straightforward to use anymore. What should we pass to the objective argument now?</p>
<div class="highlight"><pre><span></span><code><span class="n">tuner</span> <span class="o">=</span> <span class="n">RandomSearch</span><span class="p">(</span>
    <span class="o">...</span><span class="p">,</span>
    <span class="n">metrics</span><span class="o">=</span><span class="p">[</span><span class="n">custom_metric</span><span class="p">],</span>
    <span class="n">objective</span><span class="o">=</span><span class="s2">"val_???"</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>

<p>It should be just <code>"val_custom_metric”</code>. Just use the prefix of <code>"val_"</code> and the name of the metric function. It is not intuitive enough. We want to make it better instead of forcing the user to learn this. We easily spotted a user experience problem by writing this workflow.</p>
<p>If you wrote the design more comprehensively by including the implementation of the <code>custom_metric</code> function, you will find you even need to learn how to write a Keras custom metric. You have to follow the function signature to make it work, as shown in the following code snippet.</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">custom_metric</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">):</span>
    <span class="n">squared_diff</span> <span class="o">=</span> <span class="n">ops</span><span class="o">.</span><span class="n">square</span><span class="p">(</span><span class="n">y_true</span> <span class="o">-</span> <span class="n">y_pred</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">ops</span><span class="o">.</span><span class="n">mean</span><span class="p">(</span><span class="n">squared_diff</span><span class="p">,</span> <span class="n">axis</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div>

<p>After discovering this problem. We specially designed a better workflow for custom metrics. You only need to override <code>HyperModel.fit()</code> to compute your custom metric and return it. No strings to name the objective. No function signature to follow. Just a return value. The user experience is much better right now.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">MyHyperModel</span><span class="p">(</span><span class="n">HyperModel</span><span class="p">):</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">fit</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">trial</span><span class="p">,</span> <span class="n">model</span><span class="p">,</span> <span class="n">validation_data</span><span class="p">):</span>
        <span class="n">x_val</span><span class="p">,</span> <span class="n">y_true</span> <span class="o">=</span> <span class="n">validation_data</span>
        <span class="n">y_pred</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">x_val</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">custom_metric</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">)</span>

<span class="n">tuner</span> <span class="o">=</span> <span class="n">RandomSearch</span><span class="p">(</span><span class="n">MyHyperModel</span><span class="p">(),</span> <span class="n">max_trials</span><span class="o">=</span><span class="mi">20</span><span class="p">)</span>
</code></pre></div>

<p>One more thing to remember is we should always start from the user experience. The designed workflows backpropagate to the implementation.</p>
<h2>Principle 2: Minimize cognitive load</h2>
<p>Do not force the user to learn anything unless it is really necessary. Let's see some good examples.</p>
<p>The Keras modeling API is a good example shown in the following code snippet. The model builders already have these concepts in mind, for example, a model is a stack of layers. It needs a loss function. We can fit it with data or make it predict on data.</p>
<div class="highlight"><pre><span></span><code><span class="n">model</span> <span class="o">=</span> <span class="n">keras</span><span class="o">.</span><span class="n">Sequential</span><span class="p">([</span>
    <span class="n">layers</span><span class="o">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span><span class="s2">"relu"</span><span class="p">),</span>
    <span class="n">layers</span><span class="o">.</span><span class="n">Dense</span><span class="p">(</span><span class="n">num_classes</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span><span class="s2">"softmax"</span><span class="p">),</span>
<span class="p">])</span>
<span class="n">model</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">loss</span><span class="o">=</span><span class="s1">'categorical_crossentropy'</span><span class="p">)</span>
<span class="n">model</span><span class="o">.</span><span class="n">fit</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
<span class="n">model</span><span class="o">.</span><span class="n">predict</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</code></pre></div>

<p>So basically, no new concepts were learned to use Keras.</p>
<p>Another good example is the PyTorch modeling. The code is executed just like Python code. All tensors are just real tensors with real values. You can depend on the value of a tensor to decide your path with plain Python code.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">MyModel</span><span class="p">(</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">x</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_a</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_b</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</code></pre></div>

<p>You can also do this with Keras with TensorFlow or JAX backend but needs to be written differently. All the if conditions need to be written with this ops.cond function as shown in the following code snippet.</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">MyModel</span><span class="p">(</span><span class="n">keras</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="k">def</span><span class="w"> </span><span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">inputs</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">ops</span><span class="o">.</span><span class="n">cond</span><span class="p">(</span>
            <span class="n">ops</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">inputs</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span>
            <span class="k">lambda</span> <span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_a</span><span class="p">(</span><span class="n">inputs</span><span class="p">),</span>
            <span class="k">lambda</span> <span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_b</span><span class="p">(</span><span class="n">inputs</span><span class="p">),</span>
        <span class="p">)</span>
</code></pre></div>

<p>This is teaching the user to learn a new op instead of using the if-else clause they are familiar with, which is bad. In compensation, it brings significant improvement in training speed.</p>
<p>Here is the catch of the flexibility of PyTorch. If you ever needed to optimize the memory and speed of your model, you would have to do it by yourself using the following APIs and new concepts to do so, including the inplace arguments for the ops, the parallel op APIs, and explicit device placement. It introduces a rather high learning curve for the users.</p>
<div class="highlight"><pre><span></span><code><span class="n">torch</span><span class="o">.</span><span class="n">relu</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">inplace</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">_foreach_add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
<span class="n">torch</span><span class="o">.</span><span class="n">_foreach_add_</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="o">.</span><span class="n">cuda</span><span class="p">()</span>
</code></pre></div>

<p>Some other good examples are keras.ops, tensorflow.numpy, jax.numpy. They are just a reimplementation of the numpy API. When introducing some cognitive load, just reuse what people already know. Every framework has to provide some low-level ops in these frameworks. Instead of letting people learn a new set of APIs, which may have a hundred functions, they just use the most popular existing API for it. The numpy APIs are well-documented and have tons of Stack Overflow questions and answers related to it.</p>
<p>The worst thing you can do with user experience is to trick the users. Trick the user to believe your API is something they are familiar with but it is not. I will give two examples. One is on PyTorch. The other one is on TensorFlow.</p>
<p>What should we pass as the pad argument in <code>F.pad()</code> function if you want to pad the input tensor of the shape <code>(100, 3, 32, 32)</code> to <code>(100, 3, 1+32+1, 2+32+2)</code> or <code>(100, 3, 34, 36)</code>?</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">torch.nn.functional</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">F</span>
<span class="c1"># pad the 32x32 images to (1+32+1)x(2+32+2)</span>
<span class="c1"># (100, 3, 32, 32) to (100, 3, 34, 36)</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">pad</span><span class="p">(</span>
    <span class="n">torch</span><span class="o">.</span><span class="n">empty</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="mi">32</span><span class="p">),</span>
    <span class="n">pad</span><span class="o">=</span><span class="err">???</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>

<p>My first intuition is that it should be <code>((0, 0), (0, 0), (1, 1), (2, 2))</code>, where each sub-tuple corresponds to one of the 4 dimensions, and the two numbers are the padding size before and after the existing values. My guess is originated from the numpy API.</p>
<p>However, the correct answer is <code>(2, 2, 1, 1)</code>. There is no sub-tuple, but one plain tuple. Moreover, the dimensions are reversed. The last dimension goes the first.</p>
<p>The following is a bad example from TensorFlow. Can you guess what is the output of the following code snippet?</p>
<div class="highlight"><pre><span></span><code><span class="n">value</span> <span class="o">=</span> <span class="kc">True</span>

<span class="nd">@tf</span><span class="o">.</span><span class="n">function</span>
<span class="k">def</span><span class="w"> </span><span class="nf">get_value</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">value</span>

<span class="n">value</span> <span class="o">=</span> <span class="kc">False</span>
<span class="nb">print</span><span class="p">(</span><span class="n">get_value</span><span class="p">())</span>
</code></pre></div>

<p>Without the <code>tf.function</code> decorator, the output should be <code>False</code>, which is pretty simple. However, with the decorator, the output is <code>True</code>. This is because TensorFlow compiles the function and any Python variable is compiled into a new constant. Changing the old variable's value would not affect the created constant.</p>
<p>It tricks the user into believing it is the Python code they are familiar with, but actually, it is not.</p>
<h2>Principle 3: Interaction over documentation</h2>
<p>No one likes to read long documentation if they can figure it out just by running some example code and tweaking it by themselves. So, we try to make the user workflow of the software follow the same logic.</p>
<p>Here is a good example shown in the following code snippet. In PyTorch, all methods with the underscore are inplace ops, while the ones without are not. From an interactive perspective, these are good, because they are easy to follow, and the users do not need to check the docs whenever they want the inplace version of a method. However, of course, they introduced some cognitive load. The users need to know what does inplace means and when to use them.</p>
<div class="highlight"><pre><span></span><code><span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
<span class="n">x</span><span class="o">.</span><span class="n">add_</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="o">.</span><span class="n">mul</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
<span class="n">x</span><span class="o">.</span><span class="n">mul_</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
</code></pre></div>

<p>Another good example is the Keras layers. They strictly follow the same naming convention as shown in the following code snippet. With a clear naming convention, the users can easily remember the layer names without checking the documentation.</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">keras</span><span class="w"> </span><span class="kn">import</span> <span class="n">layers</span>

<span class="n">layers</span><span class="o">.</span><span class="n">MaxPooling2D</span><span class="p">()</span>
<span class="n">layers</span><span class="o">.</span><span class="n">GlobalMaxPooling1D</span><span class="p">()</span>
<span class="n">layers</span><span class="o">.</span><span class="n">GlobalAveragePooling3D</span><span class="p">()</span>
</code></pre></div>

<p>Another important part of the interaction between the user and the software is the error message. You cannot expect the user to write everything correctly the very first time. We should always do the necessary checks in the code and try to print helpful error messages.</p>
<p>Let's see the following two examples shown in the code snippet. The first one has not much information. It just says tensor shape mismatch. The
second one contains much more useful information for the user to find the bug. It not only tells you the error is because of tensor shape mismatch, but it also shows what is the expected shape and what is the wrong shape it received. If you did not mean to pass that shape, you have a better idea
of the bug now.</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Bad example:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Tensor shape mismatch."</span><span class="p">)</span>

<span class="c1"># Good example:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
    <span class="s2">"Tensor shape mismatch. "</span>
    <span class="s2">"Expected: (batch, num_features). "</span>
    <span class="sa">f</span><span class="s2">"Received: </span><span class="si">{</span><span class="n">x</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span>
</code></pre></div>

<p>The best error message would be directly pointing the user to the fix. The following code snippet shows a general Python error message. It guessed what was wrong with the code and directly pointed the user to the fix.</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">math</span>

<span class="n">math</span><span class="o">.</span><span class="n">sqr</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
</code></pre></div>

<div class="highlight"><pre><span></span><code>AttributeError: module 'math' has no attribute 'sqr'.
Did you mean: 'sqrt'?
</code></pre></div>

<h2>Final words</h2>
<p>So far we have introduced the three most valuable software design principles I have learned when contributing to the deep learning frameworks. First, write end-to-end workflows to discover more user experience problems. Second, reduce cognitive load and do not teach the user anything unless necessary. Third, follow the same logic in your API design and throw meaningful error messages so that the users can learn your software by interacting with it instead of constantly checking the documentation.</p>
<p>However, there are many more principles to follow if you want to make your software even better. You can refer to the <a href="https://github.com/keras-team/governance/blob/24401c1addf521e522fd363f6eb40e7c4c4881d5/keras_api_design_guidelines.md">Keras API design guidelines</a> as a complete API design guide.</p>]]></content>
  </entry>
  <entry>
    <title>The AutoML Dilemma</title>
    <link href="https://haifengjin.com/the-automl-dilemma/"/>
    <id>https://haifengjin.com/the-automl-dilemma/</id>
    <published>2023-09-16T00:00:00+00:00</published>
    <updated>2023-09-16T00:00:00+00:00</updated>
    <summary>An infrastructure engineer's perspective</summary>
    <content type="html"><![CDATA[<p>AutoML has been a hot topic for the past few years. The hype has been built up so high, even with the ambition to replace human machine learning experts. However, not seeing much adoption in a long while, the expectation for AutoML is dropping quickly, which strictly follows Gartner's curve.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="0.0 0.0 551.8451443569554 341.1653543307087" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_b_1_p.0"><path d="m0 0l551.84515 0l0 341.16534l-551.84515 0l0 -341.16534z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_b_1_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l551.84515 0l0 341.16534l-551.84515 0z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m53.59055 297.8635c11.232723 -45.069107 41.602802 -260.70776 67.39633 -270.41467c25.793518 -9.706911 56.440506 194.007 87.36482 212.17323c30.924332 18.16623 46.17804 -83.20648 98.18111 -103.17586c52.00305 -19.969376 178.19772 -13.86702 213.83725 -16.640419" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m53.59055 297.8635c11.232723 -45.069107 41.602802 -260.70776 67.39633 -270.41467c25.793518 -9.706911 56.440506 194.007 87.36482 212.17323c30.924332 18.16623 46.17804 -83.20648 98.18111 -103.17586c52.00305 -19.969376 178.19772 -13.86702 213.83725 -16.640419" fill-rule="evenodd"/><path fill="var(--background-color)" d="m168.71129 174.06299l0 0c0 -4.4617615 3.6099243 -8.078735 8.062988 -8.078735l0 0c2.138443 0 4.1893005 0.8511505 5.7014008 2.366211c1.5121002 1.5150604 2.3615875 3.5699158 2.3615875 5.7125244l0 0c0 4.4617615 -3.6099243 8.078751 -8.062988 8.078751l0 0c-4.453064 0 -8.062988 -3.6169891 -8.062988 -8.078751z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m168.71129 174.06299l0 0c0 -4.4617615 3.6099243 -8.078735 8.062988 -8.078735l0 0c2.138443 0 4.1893005 0.8511505 5.7014008 2.366211c1.5121002 1.5150604 2.3615875 3.5699158 2.3615875 5.7125244l0 0c0 4.4617615 -3.6099243 8.078751 -8.062988 8.078751l0 0c-4.453064 0 -8.062988 -3.6169891 -8.062988 -8.078751z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m250.85564 101.18898l-55.02362 59.653534" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m250.85564 101.18898l-38.751465 42.01219" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="4.0" stroke-linecap="butt" d="m207.2477 138.72163l-7.4509583 17.82257l17.163895 -8.863495z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m47.766403 305.35434l0 -305.35434" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m47.766403 305.35434l0 -281.35434" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="4.0" stroke-linecap="butt" d="m54.373333 24.0l-6.60693 -18.152391l-6.60693 18.152391z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m47.766403 305.35434l491.74805 0" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m47.766403 305.35434l467.74805 0" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="4.0" stroke-linecap="butt" d="m515.51447 311.96127l18.152344 -6.6069336l-18.152344 -6.6069336z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m0 237.51312l0 -182.77165l42.015747 0l0 182.77165z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m26.919998 211.64922l-13.625 0l0 -8.171875l2.046875 0l0 5.875l3.53125 0l0 -5.21875l2.015625 0l0 5.21875l4.0 0l0 -5.875l2.03125 0l0 8.171875zm0 -10.225006l-5.296875 -3.578125l-4.96875 3.328125l0 -2.625l2.515625 -1.578125q0.234375 -0.15625 0.515625 -0.265625q0.28125 -0.125 0.4375 -0.203125q-0.15625 -0.078125 -0.4375 -0.203125q-0.28125 -0.140625 -0.515625 -0.265625l-2.515625 -1.609375l0 -2.625l4.96875 3.34375l5.296875 -3.5625l0 2.640625l-2.8125 1.78125q-0.25 0.140625 -0.53125 0.28125q-0.28125 0.125 -0.4375 0.21875q0.15625 0.078125 0.4375 0.203125q0.28125 0.125 0.53125 0.28125l2.8125 1.8125l0 2.625zm3.359375 -11.959381l-13.625 0l0 -2.28125l1.96875 0l0 -0.515625l0.53125 0.515625q-1.25 0 -1.96875 -0.734375q-0.71875 -0.75 -0.71875 -2.015625q0 -1.546875 1.0625 -2.484375q1.0625 -0.9375 2.859375 -0.9375l2.78125 0q1.1875 0 2.078125 0.421875q0.890625 0.421875 1.375 1.1875q0.484375 0.765625 0.484375 1.8125q0 1.265625 -0.71875 2.015625q-0.71875 0.734375 -1.96875 0.734375l0.546875 -0.515625l0 0.546875l2.4375 -0.078125l2.875 0l0 2.328125zm-5.1875 -4.21875q0 -0.890625 -0.515625 -1.390625q-0.53125 -0.515625 -1.5 -0.515625l-2.578125 0q-0.96875 0 -1.484375 0.515625q-0.53125 0.5 -0.53125 1.390625q0 0.875 0.53125 1.390625q0.53125 0.5 1.484375 0.5l2.578125 0q0.953125 0 1.484375 -0.5q0.53125 -0.515625 0.53125 -1.390625zm2.015625 -11.146881q0 1.296875 -0.5 2.265625q-0.515625 0.96875 -1.40625 1.5q-0.90625 0.53125 -2.125 0.53125l-2.578125 0q-1.203125 0 -2.109375 -0.53125q-0.90625 -0.53125 -1.40625 -1.5q-0.515625 -0.96875 -0.515625 -2.265625q0 -1.296875 0.515625 -2.25q0.5 -0.96875 1.40625 -1.5q0.90625 -0.53125 2.109375 -0.53125l1.84375 0l0 6.328125l0.734375 0q1.078125 0 1.640625 -0.515625q0.546875 -0.53125 0.546875 -1.5625q0 -0.78125 -0.265625 -1.265625q-0.28125 -0.484375 -0.796875 -0.609375l0 -2.296875q1.3125 0.265625 2.109375 1.421875q0.796875 1.140625 0.796875 2.78125zm-6.0625 -2.015625l-0.5625 0q-1.0625 0 -1.625 0.5q-0.578125 0.5 -0.578125 1.515625q0 1.0 0.578125 1.53125q0.578125 0.515625 1.640625 0.515625l0.390625 0l-0.03125 -4.234375l0.1875 0.171875zm6.0625 -9.225006q0 1.3125 -0.484375 2.296875q-0.5 0.96875 -1.40625 1.515625q-0.90625 0.53125 -2.140625 0.53125l-2.578125 0q-1.25 0 -2.140625 -0.53125q-0.890625 -0.546875 -1.390625 -1.515625q-0.5 -0.984375 -0.5 -2.296875q0 -1.9375 1.0 -3.09375q1.0 -1.15625 2.71875 -1.21875l0 2.34375q-0.796875 0.046875 -1.234375 0.5625q-0.4375 0.515625 -0.4375 1.40625q0 0.9375 0.515625 1.484375q0.5 0.53125 1.453125 0.53125l2.59375 0q0.953125 0 1.484375 -0.53125q0.515625 -0.546875 0.515625 -1.484375q0 -0.890625 -0.4375 -1.40625q-0.4375 -0.515625 -1.25 -0.5625l0 -2.34375q1.71875 0.0625 2.71875 1.21875q1.0 1.15625 1.0 3.09375zm-0.1875 -12.256256q0 1.46875 -0.8125 2.3125q-0.828125 0.828125 -2.265625 0.828125l-5.078125 0l0 2.78125l-2.109375 0l0 -2.78125l-2.890625 0l0 -2.359375l2.890625 0l0 -3.9375l2.109375 0l0 3.9375l5.078125 0q0.96875 0 0.96875 -0.953125l0 -2.796875l2.109375 0l0 2.96875zm0.1875 -8.928131q0 1.578125 -0.84375 2.5q-0.84375 0.90625 -2.25 0.90625q-1.515625 0 -2.328125 -1.0q-0.828125 -1.015625 -0.828125 -2.859375l0 -2.5625l-0.875 0q-0.75 0 -1.171875 0.484375q-0.421875 0.484375 -0.421875 1.328125q0 0.765625 0.34375 1.28125q0.328125 0.5 0.90625 0.59375l0 2.265625q-1.453125 -0.15625 -2.3125 -1.296875q-0.859375 -1.140625 -0.859375 -2.921875q0 -1.875 0.953125 -2.96875q0.9375 -1.09375 2.546875 -1.09375l6.953125 0l0 2.265625l-1.796875 0l0 0.375l-0.5 -0.375q1.140625 0 1.8125 0.84375q0.671875 0.828125 0.671875 2.234375zm-1.75 -0.765625q0 -1.0 -0.5 -1.625q-0.515625 -0.625 -1.3125 -0.625l-1.25 0l0 2.53125q0 0.703125 0.40625 1.140625q0.40625 0.421875 1.078125 0.421875q0.734375 0 1.15625 -0.484375q0.421875 -0.5 0.421875 -1.359375zm1.5625 -12.693756q0 1.46875 -0.8125 2.3125q-0.828125 0.828125 -2.265625 0.828125l-5.078125 0l0 2.78125l-2.109375 0l0 -2.78125l-2.890625 0l0 -2.359375l2.890625 0l0 -3.9375076l2.109375 0l0 3.9375076l5.078125 0q0.96875 0 0.96875 -0.953125l0 -2.7968826l2.109375 0l0 2.9687576zm0 -5.8968887l-2.125 0l0 -3.578125l-6.03125 0l0 3.109375l-2.109375 0l0 -5.34375l8.140625 0l0 -3.171875l2.125 0l0 8.984375zm-11.984375 -4.5q0 0.703125 -0.359375 1.125q-0.359375 0.40625 -0.984375 0.40625q-0.609375 0 -0.96875 -0.40625q-0.359375 -0.421875 -0.359375 -1.125q0 -0.71875 0.359375 -1.125q0.359375 -0.40625 0.96875 -0.40625q0.625 0 0.984375 0.40625q0.359375 0.40625 0.359375 1.125zm12.15625 -10.865639q0 1.296875 -0.5 2.265625q-0.5 0.9531326 -1.390625 1.4843826q-0.90625 0.53125 -2.140625 0.53125l-2.546875 0q-1.21875 0 -2.125 -0.53125q-0.90625 -0.53125 -1.390625 -1.4843826q-0.5 -0.96875 -0.5 -2.265625q0 -1.328125 0.5 -2.28125q0.484375 -0.953125 1.390625 -1.484375q0.90625 -0.53125 2.109375 -0.53125l2.5625 0q1.234375 0 2.140625 0.53125q0.890625 0.53125 1.390625 1.484375q0.5 0.953125 0.5 2.28125zm-2.03125 0q0 -0.9375 -0.515625 -1.453125q-0.515625 -0.515625 -1.484375 -0.515625l-2.546875 0q-0.984375 0 -1.484375 0.515625q-0.5 0.515625 -0.5 1.453125q0 0.90625 0.5 1.4375q0.5 0.515625 1.484375 0.515625l2.546875 0q0.96875 0 1.484375 -0.515625q0.515625 -0.53125 0.515625 -1.4375zm1.859375 -7.037506l-10.265625 0l0 -2.28125l1.96875 0l0 -0.625l0.53125 0.625q-1.265625 0 -1.96875 -0.734375q-0.71875 -0.734375 -0.71875 -2.015625q0 -1.515625 1.015625 -2.421875q1.0 -0.90625 2.703125 -0.90625l6.734375 0l0 2.328125l-6.5 0q-0.921875 0 -1.421875 0.484375q-0.515625 0.484375 -0.515625 1.375q0 0.859375 0.53125 1.359375q0.515625 0.484375 1.484375 0.484375l6.421875 0l0 2.328125zm0.171875 -14.959381q0 1.171875 -0.375 2.046875q-0.375 0.875 -1.03125 1.375q-0.671875 0.5 -1.5625 0.53125l0 -2.328125q0.484375 -0.046875 0.78125 -0.484375q0.296875 -0.4375 0.296875 -1.140625l0 -0.828125q0 -0.859375 -0.328125 -1.28125q-0.34375 -0.4375 -0.921875 -0.4375q-0.53125 0 -0.84375 0.390625q-0.3125 0.375 -0.40625 1.1875l-0.1875 1.265625q-0.234375 1.75 -0.921875 2.578125q-0.703125 0.8125 -2.078125 0.8125q-1.4375 0 -2.21875 -0.96875q-0.796875 -0.96875 -0.796875 -2.84375l0 -0.71875q0 -1.71875 0.765625 -2.734375q0.75 -1.03125 2.046875 -1.109375l0 2.328125q-0.40625 0.0625 -0.671875 0.46875q-0.265625 0.390625 -0.265625 1.046875l0 0.71875q0 0.796875 0.296875 1.15625q0.28125 0.359375 0.84375 0.359375q0.515625 0 0.78125 -0.3125q0.25 -0.3125 0.359375 -1.03125l0.1875 -1.3125q0.25 -1.828125 0.96875 -2.6875q0.71875 -0.875 2.125 -0.875q1.484375 0 2.328125 1.015625q0.828125 1.015625 0.828125 2.984375l0 0.828125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m203.49869 305.35303l182.77167 0l0 42.015747l-182.77167 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m276.91888 332.273l0 -11.453125l-3.5625 0l0 -2.171875l9.46875 0l0 2.171875l-3.5625 0l0 11.453125l-2.34375 0zm8.193756 0l0 -2.125l3.578125 0l0 -6.03125l-3.109375 0l0 -2.109375l5.34375 0l0 8.140625l3.171875 0l0 2.125l-8.984375 0zm4.5 -11.984375q-0.703125 0 -1.125 -0.359375q-0.40625 -0.359375 -0.40625 -0.984375q0 -0.609375 0.40625 -0.96875q0.421875 -0.359375 1.125 -0.359375q0.71875 0 1.125 0.359375q0.40625 0.359375 0.40625 0.96875q0 0.625 -0.40625 0.984375q-0.40625 0.359375 -1.125 0.359375zm6.240631 11.984375l0 -10.265625l1.90625 0l0 1.390625l0.390625 0l-0.3125 0.328125q0 -0.859375 0.484375 -1.375q0.484375 -0.53125 1.25 -0.53125q0.8125 0 1.265625 0.671875q0.46875 0.671875 0.46875 1.8125l-0.421875 -0.90625l0.640625 0l-0.234375 0.328125q0 -0.859375 0.484375 -1.375q0.484375 -0.53125 1.28125 -0.53125q0.953125 0 1.515625 0.71875q0.5625 0.703125 0.5625 1.921875l0 7.8125l-2.0625 0l0 -7.71875q0 -0.515625 -0.21875 -0.796875q-0.21875 -0.296875 -0.65625 -0.296875q-0.40625 0 -0.640625 0.28125q-0.234375 0.28125 -0.234375 0.8125l0 7.71875l-1.6875 0l0 -7.71875q0 -0.53125 -0.21875 -0.8125q-0.21875 -0.28125 -0.65625 -0.28125q-0.421875 0 -0.640625 0.28125q-0.21875 0.28125 -0.21875 0.8125l0 7.71875l-2.046875 0zm15.834381 0.1875q-1.296875 0 -2.265625 -0.5q-0.96875 -0.515625 -1.5 -1.40625q-0.53125 -0.90625 -0.53125 -2.125l0 -2.578125q0 -1.203125 0.53125 -2.109375q0.53125 -0.90625 1.5 -1.40625q0.96875 -0.515625 2.265625 -0.515625q1.296875 0 2.25 0.515625q0.96875 0.5 1.5 1.40625q0.53125 0.90625 0.53125 2.109375l0 1.84375l-6.328125 0l0 0.734375q0 1.078125 0.515625 1.640625q0.53125 0.546875 1.5625 0.546875q0.78125 0 1.265625 -0.265625q0.484375 -0.28125 0.609375 -0.796875l2.296875 0q-0.265625 1.3125 -1.421875 2.109375q-1.140625 0.796875 -2.78125 0.796875zm2.015625 -6.0625l0 -0.5625q0 -1.0625 -0.5 -1.625q-0.5 -0.578125 -1.515625 -0.578125q-1.0 0 -1.53125 0.578125q-0.515625 0.578125 -0.515625 1.640625l0 0.390625l4.234375 -0.03125l-0.171875 0.1875z" fill-rule="nonzero"/></g></svg></figure>

<p>At this point, we need to understand the current status of AutoML and figure out the way for tomorrow. In this article, I will help you review what AutoML is and what are the missing pieces that prevented AutoML from mass adoption.</p>
<h2>What is AutoML?</h2>
<p>Imagine someone with limited machine learning expertise facing a real-world image classification problem. They can clearly define the problem and have the training data available. AutoML can help to build a trained machine learning model in this case.</p>
<p>From an input and output perspective, AutoML does the following.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="0.0 0.0 688.6719160104986 351.17847769028873" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_c_2_p.0"><path d="m0 0l688.67194 0l0 351.17847l-688.67194 0l0 -351.17847z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_c_2_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l688.67194 0l0 351.17847l-688.67194 0z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m250.25867 178.8959l0 0c0 -14.16806 11.485474 -25.653534 25.653534 -25.653534l134.4567 0c6.8037415 0 13.328827 2.702774 18.139801 7.513748c4.810974 4.810974 7.5137634 11.336044 7.5137634 18.139786l0 0l0 0c0 14.16806 -11.485504 25.65355 -25.653564 25.65355l-134.4567 0c-14.16806 0 -25.653534 -11.485489 -25.653534 -25.65355z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m250.25867 178.8959l0 0c0 -14.16806 11.485474 -25.653534 25.653534 -25.653534l134.4567 0c6.8037415 0 13.328827 2.702774 18.139801 7.513748c4.810974 4.810974 7.5137634 11.336044 7.5137634 18.139786l0 0l0 0c0 14.16806 -11.485504 25.65355 -25.653564 25.65355l-134.4567 0c-14.16806 0 -25.653534 -11.485489 -25.653534 -25.65355z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m281.30292 184.7559l2.9375 -11.6875l2.578125 0l2.96875 11.6875l-2.0625 0l-0.625 -2.828125l-3.109375 0l-0.640625 2.828125l-2.046875 0zm3.046875 -4.46875l2.375 0l-0.703125 -3.125q-0.171875 -0.796875 -0.3125 -1.421875q-0.125 -0.625 -0.171875 -0.921875q-0.046875 0.296875 -0.171875 0.921875q-0.109375 0.625 -0.3125 1.40625l-0.703125 3.140625zm10.787537 4.625q-1.671875 0 -2.640625 -0.9375q-0.953125 -0.9375 -0.953125 -2.546875l0 -5.46875l2.0 0l0 5.453125q0 0.84375 0.40625 1.3125q0.421875 0.453125 1.1875 0.453125q0.75 0 1.171875 -0.453125q0.4375 -0.46875 0.4375 -1.3125l0 -5.453125l2.0 0l0 5.46875q0 1.609375 -0.984375 2.546875q-0.96875 0.9375 -2.625 0.9375zm10.568756 -0.15625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0zm8.631287 0.140625q-1.109375 0 -1.9375 -0.421875q-0.828125 -0.421875 -1.28125 -1.1875q-0.453125 -0.78125 -0.453125 -1.84375l0 -2.171875q0 -1.0625 0.453125 -1.828125q0.453125 -0.78125 1.28125 -1.203125q0.828125 -0.421875 1.9375 -0.421875q1.140625 0 1.953125 0.421875q0.8125 0.421875 1.265625 1.203125q0.46875 0.765625 0.46875 1.8125l0 2.1875q0 1.0625 -0.46875 1.84375q-0.453125 0.765625 -1.265625 1.1875q-0.8125 0.421875 -1.953125 0.421875zm0 -1.734375q0.796875 0 1.234375 -0.4375q0.453125 -0.453125 0.453125 -1.28125l0 -2.171875q0 -0.859375 -0.453125 -1.28125q-0.4375 -0.4375 -1.234375 -0.4375q-0.78125 0 -1.234375 0.4375q-0.4375 0.421875 -0.4375 1.28125l0 2.171875q0 0.828125 0.4375 1.28125q0.453125 0.4375 1.234375 0.4375zm5.693756 1.59375l0 -11.6875l2.375 0l1.140625 3.71875q0.171875 0.5625 0.28125 1.03125q0.109375 0.453125 0.140625 0.671875q0.03125 -0.21875 0.140625 -0.671875q0.125 -0.46875 0.28125 -1.03125l1.09375 -3.71875l2.359375 0l0 11.6875l-1.875 0l0 -3.625q0 -0.8125 0.046875 -1.75q0.046875 -0.9375 0.109375 -1.875q0.078125 -0.953125 0.15625 -1.796875q0.09375 -0.859375 0.15625 -1.5l-1.5625 5.515625l-1.828125 0l-1.625 -5.515625q0.09375 0.609375 0.171875 1.4375q0.078125 0.828125 0.140625 1.765625q0.078125 0.921875 0.125 1.890625q0.046875 0.953125 0.046875 1.828125l0 3.625l-1.875 0zm10.350037 0l0 -11.6875l2.0 0l0 9.828125l4.96875 0l0 1.859375l-6.96875 0zm22.387543 0.15625q-1.203125 0 -2.078125 -0.390625q-0.875 -0.40625 -1.359375 -1.140625q-0.46875 -0.75 -0.484375 -1.765625l2.0 0q0 0.71875 0.515625 1.140625q0.53125 0.40625 1.421875 0.40625q0.875 0 1.359375 -0.40625q0.484375 -0.40625 0.484375 -1.109375q0 -0.59375 -0.34375 -1.03125q-0.34375 -0.4375 -0.984375 -0.59375l-1.34375 -0.375q-1.375 -0.34375 -2.125 -1.25q-0.734375 -0.921875 -0.734375 -2.21875q0 -0.984375 0.4375 -1.71875q0.453125 -0.75 1.28125 -1.140625q0.828125 -0.40625 1.9375 -0.40625q1.703125 0 2.6875 0.875q1.0 0.875 1.015625 2.34375l-2.0 0q0 -0.6875 -0.453125 -1.078125q-0.453125 -0.390625 -1.265625 -0.390625q-0.78125 0 -1.21875 0.375q-0.421875 0.359375 -0.421875 1.03125q0 0.609375 0.328125 1.03125q0.328125 0.421875 0.953125 0.59375l1.390625 0.390625q1.390625 0.359375 2.125 1.265625q0.734375 0.890625 0.734375 2.21875q0 1.0 -0.484375 1.765625q-0.46875 0.75 -1.34375 1.171875q-0.859375 0.40625 -2.03125 0.40625zm7.428131 2.71875l1.234375 -3.359375l-3.28125 -8.3125l2.1875 0l1.640625 4.5q0.125 0.359375 0.25 0.890625q0.125 0.515625 0.1875 0.84375q0.078125 -0.328125 0.203125 -0.84375q0.140625 -0.53125 0.265625 -0.890625l1.53125 -4.5l2.109375 0l-4.234375 11.671875l-2.09375 0zm11.412537 -2.734375q-1.015625 0 -1.765625 -0.3125q-0.75 -0.328125 -1.171875 -0.890625q-0.421875 -0.5625 -0.453125 -1.34375l2.0 0q0.03125 0.421875 0.40625 0.6875q0.375 0.25 0.984375 0.25l0.703125 0q0.734375 0 1.09375 -0.28125q0.375 -0.296875 0.375 -0.796875q0 -0.46875 -0.328125 -0.71875q-0.328125 -0.265625 -1.015625 -0.34375l-1.09375 -0.171875q-1.5 -0.203125 -2.203125 -0.796875q-0.703125 -0.59375 -0.703125 -1.78125q0 -1.21875 0.828125 -1.890625q0.828125 -0.6875 2.453125 -0.6875l0.609375 0q1.46875 0 2.34375 0.65625q0.890625 0.65625 0.953125 1.75l-2.0 0q-0.046875 -0.34375 -0.390625 -0.5625q-0.34375 -0.234375 -0.90625 -0.234375l-0.609375 0q-0.6875 0 -1.0 0.25q-0.3125 0.25 -0.3125 0.71875q0 0.453125 0.265625 0.671875q0.28125 0.21875 0.890625 0.3125l1.125 0.15625q1.578125 0.234375 2.3125 0.84375q0.75 0.609375 0.75 1.8125q0 1.28125 -0.875 2.0q-0.875 0.703125 -2.5625 0.703125l-0.703125 0zm10.896881 -0.140625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0zm8.646912 0.15625q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm3.881256 5.046875l0 -8.796875l1.640625 0l0 1.1875l0.328125 0l-0.265625 0.28125q0 -0.734375 0.40625 -1.171875q0.421875 -0.453125 1.078125 -0.453125q0.703125 0 1.09375 0.578125q0.390625 0.5625 0.390625 1.546875l-0.34375 -0.78125l0.546875 0l-0.21875 0.28125q0 -0.734375 0.40625 -1.171875q0.421875 -0.453125 1.125 -0.453125q0.8125 0 1.28125 0.609375q0.484375 0.59375 0.484375 1.640625l0 6.703125l-1.75 0l0 -6.625q0 -0.4375 -0.203125 -0.671875q-0.1875 -0.25 -0.5625 -0.25q-0.34375 0 -0.546875 0.234375q-0.203125 0.234375 -0.203125 0.6875l0 6.625l-1.4375 0l0 -6.625q0 -0.453125 -0.1875 -0.6875q-0.1875 -0.234375 -0.5625 -0.234375q-0.375 0 -0.5625 0.234375q-0.171875 0.234375 -0.171875 0.6875l0 6.625l-1.765625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m486.67746 178.8959l0 0c0 -14.16806 11.485474 -25.653534 25.653534 -25.653534l134.45667 0c6.803772 0 13.328857 2.702774 18.139832 7.513748c4.810974 4.810974 7.513733 11.336044 7.513733 18.139786l0 0l0 0c0 14.16806 -11.485474 25.65355 -25.653564 25.65355l-134.45667 0c-14.16806 0 -25.653534 -11.485489 -25.653534 -25.65355z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m486.67746 178.8959l0 0c0 -14.16806 11.485474 -25.653534 25.653534 -25.653534l134.45667 0c6.803772 0 13.328857 2.702774 18.139832 7.513748c4.810974 4.810974 7.513733 11.336044 7.513733 18.139786l0 0l0 0c0 14.16806 -11.485474 25.65355 -25.653564 25.65355l-134.45667 0c-14.16806 0 -25.653534 -11.485489 -25.653534 -25.65355z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m520.95605 184.7559l0 -9.828125l-3.0625 0l0 -1.859375l8.125 0l0 1.859375l-3.046875 0l0 9.828125l-2.015625 0zm7.3031616 0l0 -8.796875l1.890625 0l0 1.671875l0.5 0l-0.59375 1.03125q0 -1.421875 0.640625 -2.140625q0.640625 -0.71875 1.890625 -0.71875q1.421875 0 2.234375 0.875q0.8125 0.875 0.8125 2.4375l0 0.625l-2.09375 0l0 -0.484375q0 -0.859375 -0.4375 -1.3125q-0.4375 -0.46875 -1.203125 -0.46875q-0.78125 0 -1.21875 0.46875q-0.421875 0.453125 -0.421875 1.3125l0 5.5l-2.0 0zm11.928162 0.15625q-1.359375 0 -2.15625 -0.71875q-0.78125 -0.71875 -0.78125 -1.9375q0 -1.296875 0.859375 -2.0q0.875 -0.703125 2.453125 -0.703125l2.203125 0l0 -0.75q0 -0.640625 -0.421875 -1.0q-0.421875 -0.359375 -1.140625 -0.359375q-0.65625 0 -1.09375 0.296875q-0.421875 0.28125 -0.5 0.78125l-1.953125 0q0.140625 -1.25 1.109375 -1.984375q0.984375 -0.734375 2.5 -0.734375q1.625 0 2.5625 0.8125q0.9375 0.796875 0.9375 2.171875l0 5.96875l-1.9375 0l0 -1.53125l-0.328125 0l0.328125 -0.4375q0 0.984375 -0.71875 1.5625q-0.71875 0.5625 -1.921875 0.5625zm0.65625 -1.5q0.84375 0 1.375 -0.421875q0.546875 -0.4375 0.546875 -1.125l0 -1.078125l-2.171875 0q-0.609375 0 -0.984375 0.359375q-0.359375 0.34375 -0.359375 0.921875q0 0.625 0.421875 0.984375q0.4375 0.359375 1.171875 0.359375zm6.3344116 1.34375l0 -1.828125l3.0625 0l0 -5.15625l-2.671875 0l0 -1.8125l4.59375 0l0 6.96875l2.71875 0l0 1.828125l-7.703125 0zm3.859375 -10.265625q-0.609375 0 -0.96875 -0.3125q-0.34375 -0.3125 -0.34375 -0.84375q0 -0.53125 0.34375 -0.84375q0.359375 -0.3125 0.96875 -0.3125q0.609375 0 0.953125 0.3125q0.359375 0.3125 0.359375 0.84375q0 0.53125 -0.359375 0.84375q-0.34375 0.3125 -0.953125 0.3125zm5.7562256 10.265625l0 -8.796875l1.953125 0l0 1.671875l0.546875 0l-0.546875 0.46875q0 -1.09375 0.625 -1.6875q0.640625 -0.609375 1.734375 -0.609375q1.296875 0 2.078125 0.859375q0.78125 0.859375 0.78125 2.3125l0 5.78125l-2.0 0l0 -5.5625q0 -0.8125 -0.421875 -1.234375q-0.421875 -0.4375 -1.171875 -0.4375q-0.734375 0 -1.15625 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 5.5l-2.0 0zm13.178162 0.15625q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm7.1000366 5.203125q-1.3125 0 -2.125 -0.90625q-0.796875 -0.90625 -0.796875 -2.453125l0 -2.375q0 -1.5625 0.796875 -2.46875q0.796875 -0.90625 2.125 -0.90625q1.09375 0 1.734375 0.609375q0.640625 0.609375 0.640625 1.6875l-0.453125 -0.46875l0.46875 0l-0.0625 -2.09375l0 -2.46875l2.0 0l0 11.6875l-1.953125 0l0 -1.6875l-0.453125 0l0.453125 -0.453125q0 1.0625 -0.640625 1.6875q-0.640625 0.609375 -1.734375 0.609375zm0.703125 -1.71875q0.765625 0 1.1875 -0.453125q0.4375 -0.46875 0.4375 -1.28125l0 -2.203125q0 -0.8125 -0.4375 -1.265625q-0.421875 -0.46875 -1.1875 -0.46875q-0.765625 0 -1.203125 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 2.203125q0 0.828125 0.421875 1.28125q0.4375 0.453125 1.203125 0.453125zm15.340637 1.5625l0 -11.6875l2.375 0l1.140625 3.71875q0.171875 0.5625 0.28125 1.03125q0.109375 0.453125 0.140625 0.671875q0.03125 -0.21875 0.140625 -0.671875q0.125 -0.46875 0.28125 -1.03125l1.09375 -3.71875l2.359375 0l0 11.6875l-1.875 0l0 -3.625q0 -0.8125 0.046875 -1.75q0.046875 -0.9375 0.109375 -1.875q0.078125 -0.953125 0.15625 -1.796875q0.09375 -0.859375 0.15625 -1.5l-1.5625 5.515625l-1.828125 0l-1.625 -5.515625q0.09375 0.609375 0.171875 1.4375q0.078125 0.828125 0.140625 1.765625q0.078125 0.921875 0.125 1.890625q0.046875 0.953125 0.046875 1.828125l0 3.625l-1.875 0zm13.506287 0.140625q-1.109375 0 -1.9375 -0.421875q-0.828125 -0.421875 -1.28125 -1.1875q-0.453125 -0.78125 -0.453125 -1.84375l0 -2.171875q0 -1.0625 0.453125 -1.828125q0.453125 -0.78125 1.28125 -1.203125q0.828125 -0.421875 1.9375 -0.421875q1.140625 0 1.953125 0.421875q0.8125 0.421875 1.265625 1.203125q0.46875 0.765625 0.46875 1.8125l0 2.1875q0 1.0625 -0.46875 1.84375q-0.453125 0.765625 -1.265625 1.1875q-0.8125 0.421875 -1.953125 0.421875zm0 -1.734375q0.796875 0 1.234375 -0.4375q0.453125 -0.453125 0.453125 -1.28125l0 -2.171875q0 -0.859375 -0.453125 -1.28125q-0.4375 -0.4375 -1.234375 -0.4375q-0.78125 0 -1.234375 0.4375q-0.4375 0.421875 -0.4375 1.28125l0 2.171875q0 0.828125 0.4375 1.28125q0.453125 0.4375 1.234375 0.4375zm8.850037 1.75q-1.3125 0 -2.125 -0.90625q-0.796875 -0.90625 -0.796875 -2.453125l0 -2.375q0 -1.5625 0.796875 -2.46875q0.796875 -0.90625 2.125 -0.90625q1.09375 0 1.734375 0.609375q0.640625 0.609375 0.640625 1.6875l-0.453125 -0.46875l0.46875 0l-0.0625 -2.09375l0 -2.46875l2.0 0l0 11.6875l-1.953125 0l0 -1.6875l-0.453125 0l0.453125 -0.453125q0 1.0625 -0.640625 1.6875q-0.640625 0.609375 -1.734375 0.609375zm0.703125 -1.71875q0.765625 0 1.1875 -0.453125q0.4375 -0.46875 0.4375 -1.28125l0 -2.203125q0 -0.8125 -0.4375 -1.265625q-0.421875 -0.46875 -1.1875 -0.46875q-0.765625 0 -1.203125 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 2.203125q0 0.828125 0.421875 1.28125q0.4375 0.453125 1.203125 0.453125zm9.662537 1.71875q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm9.193726 5.046875q-0.859375 0 -1.515625 -0.34375q-0.640625 -0.359375 -1.015625 -1.0q-0.359375 -0.65625 -0.359375 -1.5l0 -7.03125l-2.8125 0l0 -1.8125l4.8125 0l0 8.84375q0 0.46875 0.28125 0.75q0.28125 0.28125 0.75 0.28125l2.65625 0l0 1.8125l-2.796875 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m436.03177 178.8959l50.64569 0" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m436.03177 178.89592l38.64569 0" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="2.0" stroke-linecap="butt" d="m474.67746 182.19939l9.076202 -3.3034668l-9.076202 -3.3034668z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m5.8398952 30.086615l0 0c0 -7.427578 6.021241 -13.44882 13.448818 -13.44882l158.86615 0c3.5668488 0 6.98761 1.4169254 9.50975 3.9390678c2.5221405 2.5221443 3.9390564 5.9429035 3.9390564 9.509752l0 0l0 0c0 7.427576 -6.02124 13.44882 -13.448807 13.44882l-158.86615 0c-7.427577 0 -13.448818 -6.021244 -13.448818 -13.44882z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m76.15923 35.946613l0 -1.765625l2.359375 0l0 -8.15625l-2.359375 0l0 -1.765625l6.71875 0l0 1.765625l-2.34375 0l0 8.15625l2.34375 0l0 1.765625l-6.71875 0zm9.396896 0l0 -8.796875l1.953125 0l0 1.671875l0.546875 0l-0.546875 0.46875q0 -1.09375 0.625 -1.6875q0.640625 -0.609375 1.734375 -0.609375q1.296875 0 2.078125 0.859375q0.78125 0.859375 0.78125 2.3125l0 5.78125l-2.0 0l0 -5.5625q0 -0.8125 -0.421875 -1.234375q-0.421875 -0.4375 -1.171875 -0.4375q-0.734375 0 -1.15625 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 5.5l-2.0 0zm9.600021 2.875l0 -11.671875l1.953125 0l0 1.671875l0.4375 0l-0.4375 0.46875q0 -1.078125 0.640625 -1.6875q0.640625 -0.609375 1.71875 -0.609375q1.328125 0 2.125 0.90625q0.8125 0.90625 0.8125 2.453125l0 2.375q0 1.03125 -0.359375 1.796875q-0.359375 0.75 -1.015625 1.171875q-0.65625 0.40625 -1.5625 0.40625q-1.078125 0 -1.71875 -0.609375q-0.640625 -0.625 -0.640625 -1.6875l0.4375 0.453125l-0.453125 0l0.0625 2.109375l0 2.453125l-2.0 0zm3.609375 -4.4375q0.765625 0 1.203125 -0.453125q0.4375 -0.453125 0.4375 -1.28125l0 -2.203125q0 -0.84375 -0.4375 -1.28125q-0.4375 -0.453125 -1.203125 -0.453125q-0.75 0 -1.1875 0.46875q-0.421875 0.453125 -0.421875 1.265625l0 2.203125q0 0.8125 0.421875 1.28125q0.4375 0.453125 1.1875 0.453125zm9.553146 1.71875q-1.671875 0 -2.640625 -0.9375q-0.953125 -0.9375 -0.953125 -2.546875l0 -5.46875l2.0 0l0 5.453125q0 0.84375 0.40625 1.3125q0.421875 0.453125 1.1875 0.453125q0.75 0 1.171875 -0.453125q0.4375 -0.46875 0.4375 -1.3125l0 -5.453125l2.0 0l0 5.46875q0 1.609375 -0.984375 2.546875q-0.96875 0.9375 -2.625 0.9375zm10.568771 -0.15625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m5.8398952 103.7102l0 0c0 -14.16806 11.485483 -25.65355 25.653543 -25.65355l134.4567 0c6.8037415 0 13.328812 2.7027817 18.139786 7.513748c4.810974 4.810974 7.513748 11.336052 7.513748 18.139801l0 0l0 0c0 14.16806 -11.485474 25.653542 -25.653534 25.653542l-134.4567 0c-14.16806 0 -25.653543 -11.485481 -25.653543 -25.653542z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m5.8398952 103.7102l0 0c0 -14.16806 11.485483 -25.65355 25.653543 -25.65355l134.4567 0c6.8037415 0 13.328812 2.7027817 18.139786 7.513748c4.810974 4.810974 7.513748 11.336052 7.513748 18.139801l0 0l0 0c0 14.16806 -11.485474 25.653542 -25.653534 25.653542l-134.4567 0c-14.16806 0 -25.653543 -11.485481 -25.653543 -25.653542z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m66.35609 100.07019l0 -11.6875l3.890625 0q1.15625 0 2.03125 0.453125q0.875 0.453125 1.34375 1.265625q0.484375 0.796875 0.484375 1.890625q0 1.0625 -0.484375 1.890625q-0.484375 0.8125 -1.359375 1.265625q-0.859375 0.4375 -2.015625 0.4375l-1.890625 0l0 4.484375l-2.0 0zm2.0 -6.28125l1.890625 0q0.828125 0 1.328125 -0.484375q0.5 -0.5 0.5 -1.3125q0 -0.828125 -0.5 -1.3125q-0.5 -0.484375 -1.328125 -0.484375l-1.890625 0l0 3.59375zm7.8656464 6.28125l0 -8.796875l1.890625 0l0 1.671875l0.5 0l-0.59375 1.03125q0 -1.421875 0.640625 -2.140625q0.640625 -0.71875 1.890625 -0.71875q1.421875 0 2.234375 0.875q0.8125 0.875 0.8125 2.4375l0 0.625l-2.09375 0l0 -0.484375q0 -0.859375 -0.4375 -1.3125q-0.4375 -0.46875 -1.203125 -0.46875q-0.78125 0 -1.21875 0.46875q-0.421875 0.453125 -0.421875 1.3125l0 5.5l-2.0 0zm12.896896 0.140625q-1.109375 0 -1.9375 -0.421875q-0.828125 -0.421875 -1.28125 -1.1875q-0.453125 -0.78125 -0.453125 -1.84375l0 -2.171875q0 -1.0625 0.453125 -1.828125q0.453125 -0.78125 1.28125 -1.203125q0.828125 -0.421875 1.9375 -0.421875q1.140625 0 1.953125 0.421875q0.8125 0.421875 1.265625 1.203125q0.46875 0.765625 0.46875 1.8125l0 2.1875q0 1.0625 -0.46875 1.84375q-0.453125 0.765625 -1.265625 1.1875q-0.8125 0.421875 -1.953125 0.421875zm0 -1.734375q0.796875 0 1.234375 -0.4375q0.453125 -0.453125 0.453125 -1.28125l0 -2.171875q0 -0.859375 -0.453125 -1.28125q-0.4375 -0.4375 -1.234375 -0.4375q-0.78125 0 -1.234375 0.4375q-0.4375 0.421875 -0.4375 1.28125l0 2.171875q0 0.828125 0.4375 1.28125q0.453125 0.4375 1.234375 0.4375zm10.350021 1.75q-1.078125 0 -1.71875 -0.609375q-0.640625 -0.625 -0.640625 -1.6875l0.4375 0.453125l-0.4375 0l0 1.6875l-1.953125 0l0 -11.6875l2.0 0l0 2.46875l-0.0625 2.09375l0.453125 0l-0.4375 0.46875q0 -1.078125 0.640625 -1.6875q0.640625 -0.609375 1.71875 -0.609375q1.328125 0 2.125 0.90625q0.8125 0.90625 0.8125 2.46875l0 2.375q0 1.546875 -0.8125 2.453125q-0.796875 0.90625 -2.125 0.90625zm-0.703125 -1.71875q0.765625 0 1.203125 -0.453125q0.4375 -0.453125 0.4375 -1.28125l0 -2.203125q0 -0.84375 -0.4375 -1.28125q-0.4375 -0.453125 -1.203125 -0.453125q-0.75 0 -1.1875 0.46875q-0.421875 0.453125 -0.421875 1.265625l0 2.203125q0 0.8125 0.421875 1.28125q0.4375 0.453125 1.1875 0.453125zm10.896896 1.5625q-0.859375 0 -1.515625 -0.34375q-0.640625 -0.359375 -1.015625 -1.0q-0.359375 -0.65625 -0.359375 -1.5l0 -7.03125l-2.8125 0l0 -1.8125l4.8125 0l0 8.84375q0 0.46875 0.28125 0.75q0.28125 0.28125 0.75 0.28125l2.65625 0l0 1.8125l-2.796875 0zm8.271896 0.15625q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm3.8812714 5.046875l0 -8.796875l1.640625 0l0 1.1875l0.328125 0l-0.265625 0.28125q0 -0.734375 0.40625 -1.171875q0.421875 -0.453125 1.078125 -0.453125q0.703125 0 1.09375 0.578125q0.39061737 0.5625 0.39061737 1.546875l-0.34374237 -0.78125l0.5468674 0l-0.21875 0.28125q0 -0.734375 0.40625 -1.171875q0.421875 -0.453125 1.125 -0.453125q0.8125 0 1.28125 0.609375q0.484375 0.59375 0.484375 1.640625l0 6.703125l-1.75 0l0 -6.625q0 -0.4375 -0.203125 -0.671875q-0.1875 -0.25 -0.5625 -0.25q-0.34375 0 -0.546875 0.234375q-0.203125 0.234375 -0.203125 0.6875l0 6.625l-1.4374924 0l0 -6.625q0 -0.453125 -0.1875 -0.6875q-0.1875 -0.234375 -0.5625 -0.234375q-0.375 0 -0.5625 0.234375q-0.171875 0.234375 -0.171875 0.6875l0 6.625l-1.765625 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m51.924805 119.0702l0 -11.6875l3.46875 0q1.1875 0 2.046875 0.453125q0.875 0.453125 1.359375 1.265625q0.484375 0.8125 0.484375 1.9375l0 4.375q0 1.09375 -0.484375 1.921875q-0.484375 0.828125 -1.359375 1.28125q-0.859375 0.453125 -2.046875 0.453125l-3.46875 0zm2.0 -1.84375l1.46875 0q0.859375 0 1.375 -0.484375q0.515625 -0.484375 0.515625 -1.328125l0 -4.375q0 -0.828125 -0.515625 -1.3125q-0.515625 -0.5 -1.375 -0.5l-1.46875 0l0 8.0zm11.209396 2.0q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm6.3343964 5.046875l0 -6.015625l-2.421875 0l0 -1.8125l2.421875 0l0 -1.328125q0 -1.140625 0.765625 -1.828125q0.765625 -0.703125 2.03125 -0.703125l2.578125 0l0 1.765625l-2.53125 0q-0.375 0 -0.609375 0.21875q-0.234375 0.21875 -0.234375 0.578125l0 1.296875l3.375 0l0 1.8125l-3.375 0l0 6.015625l-2.0 0zm7.5375214 0l0 -1.828125l3.0625 0l0 -5.15625l-2.671875 0l0 -1.8125l4.59375 0l0 6.96875l2.71875 0l0 1.828125l-7.703125 0zm3.859375 -10.265625q-0.609375 0 -0.96875 -0.3125q-0.34375 -0.3125 -0.34375 -0.84375q0 -0.53125 0.34375 -0.84375q0.359375 -0.3125 0.96875 -0.3125q0.609375 0 0.953125 0.3125q0.359375 0.3125 0.359375 0.84375q0 0.53125 -0.359375 0.84375q-0.34375 0.3125 -0.953125 0.3125zm5.7562714 10.265625l0 -8.796875l1.953125 0l0 1.671875l0.546875 0l-0.546875 0.46875q0 -1.09375 0.625 -1.6875q0.640625 -0.609375 1.734375 -0.609375q1.296875 0 2.078125 0.859375q0.78125 0.859375 0.78125 2.3125l0 5.78125l-2.0 0l0 -5.5625q0 -0.8125 -0.421875 -1.234375q-0.421875 -0.4375 -1.171875 -0.4375q-0.734375 0 -1.15625 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 5.5l-2.0 0zm9.584396 0l0 -1.828125l3.0625 0l0 -5.15625l-2.671875 0l0 -1.8125l4.59375 0l0 6.96875l2.71875 0l0 1.828125l-7.703125 0zm3.859375 -10.265625q-0.609375 0 -0.96875 -0.3125q-0.34375 -0.3125 -0.34375 -0.84375q0 -0.53125 0.34375 -0.84375q0.359375 -0.3125 0.96875 -0.3125q0.609375 0 0.953125 0.3125q0.359375 0.3125 0.359375 0.84375q0 0.53125 -0.359375 0.84375q-0.34375 0.3125 -0.953125 0.3125zm10.287521 10.265625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0zm5.0531464 0l0 -1.828125l3.0625 0l0 -5.15625l-2.671875 0l0 -1.8125l4.59375 0l0 6.96875l2.71875 0l0 1.828125l-7.703125 0zm3.859375 -10.265625q-0.609375 0 -0.96875 -0.3125q-0.34375 -0.3125 -0.34375 -0.84375q0 -0.53125 0.34375 -0.84375q0.359375 -0.3125 0.96875 -0.3125q0.609375 0 0.953125 0.3125q0.359375 0.3125 0.359375 0.84375q0 0.53125 -0.359375 0.84375q-0.34375 0.3125 -0.953125 0.3125zm9.318771 10.40625q-1.109375 0 -1.9375 -0.421875q-0.828125 -0.421875 -1.28125 -1.1875q-0.453125 -0.78125 -0.453125 -1.84375l0 -2.171875q0 -1.0625 0.453125 -1.828125q0.453125 -0.78125 1.28125 -1.203125q0.828125 -0.421875 1.9375 -0.421875q1.140625 0 1.953125 0.421875q0.8125 0.421875 1.265625 1.203125q0.46875 0.765625 0.46875 1.8125l0 2.1875q0 1.0625 -0.46875 1.84375q-0.453125 0.765625 -1.265625 1.1875q-0.8125 0.421875 -1.953125 0.421875zm0 -1.734375q0.796875 0 1.234375 -0.4375q0.453125 -0.453125 0.453125 -1.28125l0 -2.171875q0 -0.859375 -0.453125 -1.28125q-0.4375 -0.4375 -1.234375 -0.4375q-0.78125 0 -1.234375 0.4375q-0.4375 0.421875 -0.4375 1.28125l0 2.171875q0 0.828125 0.4375 1.28125q0.453125 0.4375 1.234375 0.4375zm6.0375214 1.59375l0 -8.796875l1.953125 0l0 1.671875l0.546875 0l-0.546875 0.46875q0 -1.09375 0.625 -1.6875q0.640625 -0.609375 1.734375 -0.609375q1.296875 0 2.078125 0.859375q0.78125 0.859375 0.78125 2.3125l0 5.78125l-2.0 0l0 -5.5625q0 -0.8125 -0.421875 -1.234375q-0.421875 -0.4375 -1.171875 -0.4375q-0.734375 0 -1.15625 0.453125q-0.421875 0.4375 -0.421875 1.28125l0 5.5l-2.0 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m5.8398952 254.07753l0 0c0 -14.16806 11.485483 -25.653534 25.653543 -25.653534l134.4567 0c6.8037415 0 13.328812 2.702774 18.139786 7.513748c4.810974 4.810959 7.513748 11.336044 7.513748 18.139786l0 0l0 0c0 14.168076 -11.485474 25.65355 -25.653534 25.65355l-134.4567 0c-14.16806 0 -25.653543 -11.485474 -25.653543 -25.65355z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m5.8398952 254.07753l0 0c0 -14.16806 11.485483 -25.653534 25.653543 -25.653534l134.4567 0c6.8037415 0 13.328812 2.702774 18.139786 7.513748c4.810974 4.810959 7.513748 11.336044 7.513748 18.139786l0 0l0 0c0 14.168076 -11.485474 25.65355 -25.653534 25.65355l-134.4567 0c-14.16806 0 -25.653543 -11.485474 -25.653543 -25.65355z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m66.32484 259.93753l0 -11.6875l3.46875 0q1.1875 0 2.046875 0.453125q0.875 0.453125 1.359375 1.265625q0.484375 0.8125 0.484375 1.9375l0 4.375q0 1.09375 -0.484375 1.921875q-0.484375 0.828125 -1.359375 1.28125q-0.859375 0.453125 -2.046875 0.453125l-3.46875 0zm2.0 -1.84375l1.46875 0q0.859375 0 1.375 -0.484375q0.515625 -0.484375 0.515625 -1.328125l0 -4.375q0 -0.828125 -0.515625 -1.3125q-0.515625 -0.5 -1.375 -0.5l-1.46875 0l0 8.0zm10.225021 2.0q-1.359375 0 -2.15625 -0.71875q-0.78125 -0.71875 -0.78125 -1.9375q0 -1.296875 0.859375 -2.0q0.875 -0.703125 2.453125 -0.703125l2.203125 0l0 -0.75q0 -0.640625 -0.421875 -1.0q-0.421875 -0.359375 -1.140625 -0.359375q-0.65625 0 -1.09375 0.296875q-0.421875 0.28125 -0.5 0.78125l-1.953125 0q0.140625 -1.25 1.109375 -1.984375q0.984375 -0.734375 2.5 -0.734375q1.625 0 2.5625 0.8125q0.9375 0.796875 0.9375 2.171875l0 5.96875l-1.9375 0l0 -1.53125l-0.328125 0l0.328125 -0.4375q0 0.984375 -0.71875 1.5625q-0.71875 0.5625 -1.921875 0.5625zm0.65625 -1.5q0.84375 0 1.375 -0.421875q0.546875 -0.4375 0.546875 -1.125l0 -1.078125l-2.171875 0q-0.609375 0 -0.984375 0.359375q-0.359375 0.34375 -0.359375 0.921875q0 0.625 0.421875 0.984375q0.4375 0.359375 1.171875 0.359375zm10.881271 1.34375q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0zm7.6625214 0.15625q-1.359375 0 -2.15625 -0.71875q-0.78125 -0.71875 -0.78125 -1.9375q0 -1.296875 0.859375 -2.0q0.875 -0.703125 2.453125 -0.703125l2.203125 0l0 -0.75q0 -0.640625 -0.421875 -1.0q-0.421875 -0.359375 -1.140625 -0.359375q-0.65625 0 -1.09375 0.296875q-0.421875 0.28125 -0.5 0.78125l-1.953125 0q0.140625 -1.25 1.109375 -1.984375q0.984375 -0.734375 2.5 -0.734375q1.625 0 2.5625 0.8125q0.9375 0.796875 0.9375 2.171875l0 5.96875l-1.9375 0l0 -1.53125l-0.328125 0l0.328125 -0.4375q0 0.984375 -0.71875 1.5625q-0.71875 0.5625 -1.921875 0.5625zm0.65625 -1.5q0.84375 0 1.375 -0.421875q0.546875 -0.4375 0.546875 -1.125l0 -1.078125l-2.171875 0q-0.609375 0 -0.984375 0.359375q-0.359375 0.34375 -0.359375 0.921875q0 0.625 0.421875 0.984375q0.4375 0.359375 1.171875 0.359375zm9.584396 1.484375q-1.015625 0 -1.765625 -0.3125q-0.75 -0.328125 -1.171875 -0.890625q-0.421875 -0.5625 -0.453125 -1.34375l2.0 0q0.03125 0.421875 0.40625 0.6875q0.375 0.25 0.984375 0.25l0.703125 0q0.734375 0 1.09375 -0.28125q0.375 -0.296875 0.375 -0.796875q0 -0.46875 -0.328125 -0.71875q-0.328125 -0.265625 -1.015625 -0.34375l-1.09375 -0.171875q-1.5 -0.203125 -2.203125 -0.796875q-0.703125 -0.59375 -0.703125 -1.78125q0 -1.21875 0.828125 -1.890625q0.828125 -0.6875 2.453125 -0.6875l0.609375 0q1.46875 0 2.34375 0.65625q0.890625 0.65625 0.953125 1.75l-2.0 0q-0.046875 -0.34375 -0.390625 -0.5625q-0.34375 -0.234375 -0.90625 -0.234375l-0.609375 0q-0.6875 0 -1.0 0.25q-0.3125 0.25 -0.3125 0.71875q0 0.453125 0.265625 0.671875q0.28125 0.21875 0.890625 0.3125l1.125 0.15625q1.578125 0.234375 2.3125 0.84375q0.75 0.609375 0.75 1.8125q0 1.28125 -0.875 2.0q-0.875 0.703125 -2.5625 0.703125l-0.703125 0zm9.943771 0.015625q-1.109375 0 -1.953125 -0.421875q-0.828125 -0.4375 -1.28125 -1.203125q-0.453125 -0.78125 -0.453125 -1.828125l0 -2.203125q0 -1.046875 0.453125 -1.8125q0.453125 -0.78125 1.28125 -1.203125q0.84375 -0.4375 1.953125 -0.4375q1.109375 0 1.921875 0.4375q0.828125 0.421875 1.28125 1.203125q0.46875 0.765625 0.46875 1.8125l0 1.578125l-5.421875 0l0 0.625q0 0.921875 0.4375 1.40625q0.453125 0.46875 1.328125 0.46875q0.671875 0 1.09375 -0.234375q0.421875 -0.234375 0.53125 -0.6875l1.96875 0q-0.234375 1.140625 -1.21875 1.828125q-0.984375 0.671875 -2.390625 0.671875zm1.734375 -5.203125l0 -0.46875q0 -0.921875 -0.4375 -1.40625q-0.421875 -0.484375 -1.296875 -0.484375q-0.859375 0 -1.3125 0.5q-0.4375 0.484375 -0.4375 1.40625l0 0.328125l3.625 -0.03125l-0.140625 0.15625zm8.818764 5.046875q-1.2656174 0 -1.9843674 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.3749924 0l0 1.8125l-3.3749924 0l0 4.34375q0 0.828125 0.8124924 0.828125l2.390625 0l0 1.8125l-2.53125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m486.67746 30.086615l0 0c0 -7.427578 6.02124 -13.44882 13.448822 -13.44882l158.86615 0c3.5668335 0 6.98761 1.4169254 9.509705 3.9390678c2.5221558 2.5221443 3.939087 5.9429035 3.939087 9.509752l0 0l0 0c0 7.427576 -6.02124 13.44882 -13.4487915 13.44882l-158.86615 0c-7.427582 0 -13.448822 -6.021244 -13.448822 -13.44882z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m555.55615 36.102863q-1.109375 0 -1.9375 -0.421875q-0.828125 -0.421875 -1.265625 -1.1875q-0.4375 -0.78125 -0.4375 -1.84375l0 -5.09375q0 -1.046875 0.4375 -1.8125q0.4375 -0.78125 1.265625 -1.203125q0.828125 -0.4375 1.9375 -0.4375q1.140625 0 1.953125 0.4375q0.8125 0.421875 1.25 1.203125q0.453125 0.765625 0.453125 1.796875l0 5.109375q0 1.0625 -0.453125 1.84375q-0.4375 0.765625 -1.25 1.1875q-0.8125 0.421875 -1.953125 0.421875zm0 -1.75q0.8125 0 1.234375 -0.4375q0.421875 -0.453125 0.421875 -1.265625l0 -5.09375q0 -0.828125 -0.421875 -1.25q-0.421875 -0.4375 -1.234375 -0.4375q-0.8125 0 -1.234375 0.4375q-0.40625 0.421875 -0.40625 1.25l0 5.09375q0 0.8125 0.421875 1.265625q0.421875 0.4375 1.21875 0.4375zm9.600037 1.75q-1.671875 0 -2.640625 -0.9375q-0.953125 -0.9375 -0.953125 -2.546875l0 -5.46875l2.0 0l0 5.453125q0 0.84375 0.40625 1.3125q0.421875 0.453125 1.1875 0.453125q0.75 0 1.171875 -0.453125q0.4375 -0.46875 0.4375 -1.3125l0 -5.453125l2.0 0l0 5.46875q0 1.609375 -0.984375 2.546875q-0.96875 0.9375 -2.625 0.9375zm10.568726 -0.15625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0zm5.0687866 2.875l0 -11.671875l1.953125 0l0 1.671875l0.4375 0l-0.4375 0.46875q0 -1.078125 0.640625 -1.6875q0.640625 -0.609375 1.71875 -0.609375q1.328125 0 2.125 0.90625q0.8125 0.90625 0.8125 2.453125l0 2.375q0 1.03125 -0.359375 1.796875q-0.359375 0.75 -1.015625 1.171875q-0.65625 0.40625 -1.5625 0.40625q-1.078125 0 -1.71875 -0.609375q-0.640625 -0.625 -0.640625 -1.6875l0.4375 0.453125l-0.453125 0l0.0625 2.109375l0 2.453125l-2.0 0zm3.609375 -4.4375q0.765625 0 1.203125 -0.453125q0.4375 -0.453125 0.4375 -1.28125l0 -2.203125q0 -0.84375 -0.4375 -1.28125q-0.4375 -0.453125 -1.203125 -0.453125q-0.75 0 -1.1875 0.46875q-0.421875 0.453125 -0.421875 1.265625l0 2.203125q0 0.8125 0.421875 1.28125q0.4375 0.453125 1.1875 0.453125zm9.553162 1.71875q-1.671875 0 -2.640625 -0.9375q-0.953125 -0.9375 -0.953125 -2.546875l0 -5.46875l2.0 0l0 5.453125q0 0.84375 0.40625 1.3125q0.421875 0.453125 1.1875 0.453125q0.75 0 1.171875 -0.453125q0.4375 -0.46875 0.4375 -1.3125l0 -5.453125l2.0 0l0 5.46875q0 1.609375 -0.984375 2.546875q-0.96875 0.9375 -2.625 0.9375zm10.568787 -0.15625q-1.265625 0 -1.984375 -0.703125q-0.703125 -0.703125 -0.703125 -1.9375l0 -4.34375l-2.390625 0l0 -1.8125l2.390625 0l0 -2.484375l2.015625 0l0 2.484375l3.375 0l0 1.8125l-3.375 0l0 4.34375q0 0.828125 0.8125 0.828125l2.390625 0l0 1.8125l-2.53125 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m191.60367 103.7102c14.663773 0 21.996826 18.795273 29.32753 37.590553c7.330719 18.795273 14.659073 37.590546 29.318146 37.590546" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m191.60367 103.7102c14.663773 0 21.996826 18.795273 29.32753 37.590553c7.330719 18.795273 14.659073 37.590546 29.318146 37.590546" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m191.60367 254.07753c14.663773 0 21.996826 -18.795273 29.32753 -37.590546c7.330719 -18.795273 14.659073 -37.59056 29.318146 -37.59056" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" d="m191.60367 254.07753c14.663773 0 21.996826 -18.795273 29.32753 -37.590546c7.330719 -18.795273 14.659073 -37.59056 29.318146 -37.59056" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m215.97638 5.0052495l0 330.4252" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="8.0,6.0" d="m215.97638 5.0052495l0 330.4252" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m456.7979 5.0052495l0 330.4252" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="2.0" stroke-linejoin="round" stroke-linecap="butt" stroke-dasharray="8.0,6.0" d="m456.7979 5.0052495l0 330.4252" fill-rule="evenodd"/></g></svg></figure>

<p>It takes in the problem definition and training data and outputs a trained machine learning model ready for deployment. For example, if given an image classification task, it takes in the training image dataset as input and outputs a trained image classification model.</p>
<p>The steps AutoML tries to automate may include data preprocessing, featurization, model selection, hyperparameter tuning, neural architecture search, model training, inferencing on testing data, and data post-processing.</p>
<p>In summary, automated machine learning (AutoML) tries to <strong>bridge the gap</strong> between the various fancy <strong>machine learning models</strong> and training techniques available today and the <strong>real-world problems</strong> they could solve by providing <strong>end-to-end solutions</strong> in an automated way.</p>
<h2>How does AutoML work?</h2>
<p>For a given task and dataset, the AutoML system would efficiently try out a series of relevant methods or models and pick the best one for you.</p>
<p>You can think of it as a for loop containing the following steps:</p>
<ul>
<li>Generate a model configuration.</li>
<li>Create and train the model with the configuration.</li>
<li>Evaluate the model on validation data.</li>
<li>Learn from the evaluation results to improve the configuration.</li>
</ul>
<p>A smart agent in the AutoML system generates the configurations and improves over time by learning the evaluation results.</p>
<p>Many algorithms could be used as smart agents, for example, Bayesian optimization or reinforcement learning. However, at the core of the smart agent, what it does is <strong>function approximation</strong> and <strong>function maximization</strong>. Let's see them one by one.</p>
<ul>
<li><strong>Function approximation</strong>. The smart agent is trying to learn the relation between the model configurations and the model performance. In math language, it is trying to learn a function <strong>y=f(x)</strong>, where <strong>x</strong> is the model configuration, and <strong>y</strong> is the model's performance.</li>
<li><strong>Function maximization</strong>. The end goal of the smart agent is to find a model configuration with the best model performance. In other words, we want to find the <strong>x</strong> that maximizes the value of <strong>f(x)</strong>, i.e., <strong>argmax f(x)</strong>.</li>
</ul>
<h2>The impact of AutoML</h2>
<p>As you can imagine, the impact of AutoML is huge if widely adopted. It can dramatically increase the productivity of machine learning practitioners. They no longer need to spend a lot of time fine-tuning the details of the model configurations. They may only need to carefully define the task and manually constrain the search space to get the result faster.</p>
<h3>What can AutoML do today?</h3>
<p>The applications of AutoML today are quite limited, mainly focusing on the following two aspects.</p>
<ul>
<li><strong>Quick tryouts</strong>. Some machine learning engineers may want to quickly try machine learning on their tasks and datasets. They can use AutoML as a starting point. They can further develop the ML solution by hand if they achieve comparatively good results.</li>
<li><strong>ML education</strong>. The students who just started learning ML may use AutoML to understand what ML can do. They do not need to touch all the details of the ML solution but get a quick overview of the process.</li>
</ul>
<h3>What can AutoML do in the future?</h3>
<p>The expectation of what AutoML can do in the future is much higher than it can today. We summarize it into three main goals as follows.</p>
<ul>
<li><strong>For ML experts:</strong> Boost the productivity of data scientists and machine learning engineers.</li>
<li><strong>For domain experts:</strong> Domain experts, like medical doctors or mechanical engineers, can easily apply AutoML to their problems.</li>
<li><strong>For production engineers:</strong> The found solution can be easily deployed for production.</li>
</ul>
<h2>The problems of AutoML</h2>
<p>We learned where we are now and where we are going with AutoML. The question is how we are getting there. We summarize the problems we face today into three categories. When these problems are solved, AutoML will reach mass adoption.</p>
<h3>Problem 1: Lack of business incentives</h3>
<p><strong>Modeling is trivial</strong> compared with developing a usable machine learning solution, which may include but is not limited to data collection, cleaning, verification, model deployment, and monitoring. For any company that can afford to hire people to do all these steps, the cost overhead of hiring machine learning experts to do the modeling is trivial. When they can build a team of experts without much cost overhead, they do not bother experimenting with new techniques like AutoML.</p>
<p>So, people would only start to use AutoML when the costs of all other steps are reduced to the bottom. That is when the cost of hiring people for modeling becomes significant. Now, let's see our roadmap towards this.</p>
<p><strong>Many steps can be automated.</strong> We should be optimistic that as the cloud services evolve, many steps in developing a machine learning solution could be automated, like data verification, monitoring, and serving. However, there is one crucial step that can never be automated, which is data labeling. Unless machines can teach themselves, humans will always need to prepare the data for machines to learn.</p>
<p><strong>Data labeling may become the main cost</strong> of developing an ML solution at the end of the day. If we can reduce the cost of data labeling, they would have the business incentive to use AutoML to remove the modeling cost, which would be the only cost of developing an ML solution.</p>
<p><strong>The long-term solution</strong>: Unfortunately, the ultimate solution to reduce the cost of data labeling does not exist today. We will rely on future research breakthroughs on “learning with small data”. One possible path is to invest in transfer learning.</p>
<p>However, people are not interested in working on transfer learning because it is hard to publish on this topic. For more details, see the talk “Why Most Machine Learning Research is Useless”<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup>.</p>
<p><strong>The short-term solution</strong>: In the short-term, we can just fine-tune the pretrained large models with small data, which is a simple way of transfer learning and learning with small data.</p>
<p><strong>In summary</strong>, with most of the steps in developing an ML solution automated by cloud services, and AutoML can use pretrained models to learn from smaller datasets to reduce the data labeling cost, there will be business incentives to apply AutoML to reduce their cost in ML modeling.</p>
<h3>Problem 2: Lack of maintainability</h3>
<p><strong>All deep learning models are not reliable.</strong> The behavior of the model is unpredictable sometimes. It is hard to understand why the model gives specific outputs.</p>
<p><strong>Engineers maintain the models</strong>. Today, we need an engineer to diagnose and fix the model when problems occur. The company communicates with the engineers for anything they want to change for the deep learning model.</p>
<p><strong>The AutoML system is much harder to interact with than an engineer</strong>. Today, you can only use it as a one-shot method to create the deep learning model by giving the AutoML system a series of objectives clearly defined in math in advance. If you encounter any problem using the model in practice, it will not help you fix it.</p>
<p><strong>The long-term solution</strong>: We need more research in HCI (Human-Computer Interaction). We need a more intuitive way to define the objectives so that the models created by AutoML are more reliable. We also need better ways to interact with the AutoML system to update the model to meet new requirements or fix any problems without spending too much resources searching all the different models again.</p>
<p><strong>The short-term solution</strong>: Support more objective types, like FLOPS and the number of parameters to limit the model size and inferencing time, and weighted confusion matrix to deal with imbalanced data. When a problem occurs in the model, people can add a relevant objective to the AutoML system to let it generate a new model.</p>
<h3>Problem 3: Lack of infrastructure support</h3>
<p>When developing an AutoML system, we found some features we need from the deep learning frameworks that just do not exist today. Without these features, the power of the AutoML system is limited. They are summarized as follows.</p>
<p><strong>First, state-of-the-art models with flexible unified APIs.</strong> To build an effective AutoML system, we need a large pool of state-of-the-art models to assemble the final solution. The model pool needs to be updated regularly and well-maintained. Moreover, the APIs to call the models need to be highly flexible and unified so we can call them programmatically from the AutoML system. They are used as building blocks to construct an end-to-end ML solution.</p>
<p>To solve this problem, domain-specific libraries like <a href="https://keras.io/keras_cv/">KerasCV</a> and <a href="https://keras.io/keras_nlp/">KerasNLP</a> were built for computer vision and natural language processing tasks upon Keras. They wrap the state-of-the-art models into simple, clean, yet flexible APIs, which meet the requirements of an AutoML system.</p>
<p><strong>Second, automatic hardware placement of the models</strong>. The AutoML system may need to build and train large models distributed across multiple GPUs on multiple machines. An AutoML system should be runnable on any given amount of computing resources, which requires it to dynamically decide how to distribute the model (model parallelism) or the training data (data parallelism) for the given hardware.</p>
<p>Surprisingly and unfortunately, none of the deep learning frameworks today can automatically distribute a model on multiple GPUs. You will have to explicitly specify the GPU allocation for each tensor. When the hardware environment changes, for example, the number of GPUs is reduced, your model code may no longer work.</p>
<p>I do not see a clear solution for this problem yet. We must allow some time for the deep learning frameworks to evolve. Some day, the model definition code will be independent from the code for tensor hardware placement.</p>
<p><strong>Third, the ease of deployment of the models.</strong> Any model produced by the AutoML system may need to be deployed down the stream to the cloud services, end devices, etc. Suppose you still need to hire an engineer to reimplement the model for specific hardware before deployment, which is most likely the case today. Why don't you just use the same engineer to implement the model in the first place instead of using an AutoML system?</p>
<p>People are working on this deployment problem today. For example, <a href="https://www.modular.com/">Modular</a> created a unified format for all models and integrated all the major hardware providers and deep learning frameworks into this representation. When a model is implemented with a deep learning framework, it can be exported to this format and become deployable to the hardware supporting it.</p>
<h2>Conclusions</h2>
<p>With all the problems we discussed, I am still confident in AutoML in the long run. I believe they will be solved eventually because automation and efficiency are the future of deep learning development. Though AutoML has not been massively adopted today, it will be as long as the ML revolution continues.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="why-ml-research-useless" class="csl-entry">Hutter, F. (2020). <i>Why most machine learning research is useless</i>. YouTube. <a href="https://www.youtube.com/watch?v=Bi7f1JSSlh8" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>Don't Drive Your Contributors Away</title>
    <link href="https://haifengjin.com/don-t-drive-your-contributors-away/"/>
    <id>https://haifengjin.com/don-t-drive-your-contributors-away/</id>
    <published>2023-01-18T00:00:00+00:00</published>
    <updated>2023-01-18T00:00:00+00:00</updated>
    <summary>The essentials of a contributor-friendly open-source project</summary>
    <content type="html"><![CDATA[<p>You have an amazing open-source project with a good number of users. Many of them are trying to contribute code to help you improve your project. However, they soon give up because your project's setup is not welcoming. Don't let this happen to your project!</p>
<p>In this article, I will introduce you to the essential setups that lead to a perfect open-source contributing experience.</p>
<h3>Why Do I Want People to Contribute?</h3>
<p>Open-source contributions lead to product excellence. There are tons of small improvements you can make to your software, and you cannot do everything on your own. Open-source contributors may help you fix bugs and create new features. They have the incentive to do this since they will have a better experience using your software.</p>
<p>Moreover, a strong community gains trust in the product. Even a small contribution, your responsiveness gives them confidence in your product because they see it is evolving and the issue can potentially be resolved in the future.</p>
<p>Based on this trust, they may build new things upon your software. An entirely new ecosystem may be built up this way, leading to your software's real prosperity.</p>
<h3>The Million-Dollar Metric</h3>
<p>I estimate the engineering resources we are willing to spend to optimize this metric would be worth one million dollars. This metric is like Polaris pointing us in the right direction in improving the contributing experience, which is: the average life span of the pull requests.</p>
<p>The metric is pretty intuitive. The faster your pull request gets merged, the better the experience is. The focus of the developers is always on the intellectual parts during the pull request. Let's compare a good and a bad contributing experience, and see if the metric works or not.</p>
<p><strong>What does a bad contributing experience look like?</strong> The author made some changes that don't follow the code style or break a bunch of unit tests. The owner spent several rounds of code review just to get these basic things right.</p>
<p><strong>What does a good contributing experience look like?</strong> Everything just works. It is very easy for the contributor to set up the development environment to run the unit tests and code style checks. When creating the pull request, the continuous integration (CI) did not take too long to finish. Therefore, when the contributor requests for review. Everyone can focus on the important things instead of the minutiae.</p>
<p>The proposed metric can differentiate the two scenarios. The bad experience's pull request took much longer than the good experience's pull request because of the extra rounds of code reviews.</p>
<p>Another important observation we can draw from above is that as long as you do the setups and have clear guidance, the contributing experience should be good.</p>
<p><strong>How do you do the right setups for your project?</strong> Following is a checklist of all the things you need to optimize for this metric. Just by doing all the things on the checklist, your project's contributing experience would be good enough.</p>
<h3>Development Environment Setup</h3>
<p>Most of the contributors are one-time contributors. It is not worth it to spend a lot of time in setting up a development environment. To optimize the contributing experience for these users, you have to make the process of setting up your development environment simple enough.</p>
<p>Our trick is to support <a href="https://github.com/features/codespaces">GitHub Codespaces</a>, which provides a web-based <a href="https://code.visualstudio.com">Visual Studio Code</a> IDE. The best thing is you can specify a Dockerfile with all the required dependency software installed. With one click on the repo's webpage, your contributors are ready to code. Here is <a href="https://github.com/keras-team/keras/tree/master/.devcontainer">our setup</a> for your reference.</p>
<p>For those who are willing to spend time customizing their development environment, we also provide very clear instructions on how to set up the environment locally.</p>
<h3>Running the Tests</h3>
<p>There are two ways that a contributor would like to run the tests. Run a specific test or run all the tests.</p>
<p>When contributing code the contributor would usually need to write some unit tests. They would want to easily run a specific test many times during their development to debug their code.</p>
<p>Before creating the pull request, some contributors would also like to run all the tests locally to make sure everything works in their pull request. The running settings should be as similar to the CI run as possible since the goal is just to pre-testing the CI.</p>
<p>Therefore, it is really important to support these two cases of running tests well. Clear and detailed instructions should be provided for running a specific test and for running all the tests.</p>
<h3>Continuous integration (CI) Time</h3>
<p>Ideally, the CI should finish in minutes or at most a couple of hours. The sooner the contributors get the CI results the sooner they can start working on it again if failed. Imagine if your CI takes days to finish, the contributors may already have forgotten what the pull request was about when they received the results.</p>
<p>The first way to shorten your CI is to make your tests run faster. For example, you can try to run the tests in parallel, or just rewrite some of your slowest test cases to run faster.</p>
<p>Another way to shorten your CI is to run less number of tests by splitting the repo into multiple ones, which is exactly what we are doing with TensorFlow. The CI usually takes longer for larger projects, because compiling a large codebase takes longer and there are more tests to run. Therefore, you may consider splitting your repo into multiple ones if the code can be decoupled.</p>
<p>This is where we spent our resources to optimize the metric we mentioned above. In 2021, we have successfully finished the split of Keras and TensorFlow on GitHub. It is non-trivial work as you can imagine. It involves a lot of code decoupling and refactoring. After that, CI takes less than 20 minutes to run, which is much shorter compared with before.</p>
<h3>Code Style Check</h3>
<p>The code style check is important for a project to ensure its code quality. If your code follows certain style guidelines, it is important to make it clear to the contributors.</p>
<p>The worst case is like this. The contributors do not know how to run the automated code style check. The only way they can do it is through the code review. The code reviewer catches the code style issues and reported back to the contributor. It takes several rounds of code reviews just to get the code style right, which is extremely inefficient.</p>
<p>The code style checks should be automated whenever possible. In the CI, we use tools like <a href="https://flake8.pycqa.org/en/latest/">Flake8</a> to catch style violations. We also use <a href="https://black.readthedocs.io/en/stable/index.html">Black</a>, a code auto-formatter, to format the code. The contributor can just use the auto-formatter instead of fixing all the style issues by themselves.</p>
<p>For the parts of the code that is hard to auto-format, like the Python docstrings, a clear style guide should be provided. The contributors can easily follow the guide to avoid leaving the issues to the code reviews.</p>
<p>Besides the above, we took it one step further for Keras. We integrated the code style checks into the GitHub Codespaces, which marks all the style violations in the editor and autoformats the code on save.</p>
<h3>The Contributing Guide</h3>
<p>Finally, everything a contributor should know should be summarized into a contributing guide, including almost everything we mentioned above and more.</p>
<p>It should first be clear about what types of contributions are welcome. Refer to a list of contribution-welcome issues if you have one. You may also mention the special cases. For example, any change to the APIs would need to go through the <a href="https://www.tensorflow.org/community/contribute/rfc_process">Request for Comments (RFC)</a> process first.</p>
<p>Then, it should describe the general process of contributing code, for example, creating a fork of the repo, creating a pull request, waiting for the CI to pass, and so on.</p>
<p>It should include clear instructions for each of the aspects we discussed above, including how to set up the environment, how to run the tests, and how to check your code style.</p>
<p>Here is <a href="https://github.com/keras-team/keras/blob/master/CONTRIBUTING.md">our contributing guide</a> as an example.</p>
<h3>Conclusion</h3>
<p>In this article, we first introduced the importance of having open-source contributions for your software. You need open-source contributions to reach prosperity.</p>
<p>Then, we introduced the million-dollar metric for improving your open-source software contributing experience, which is the average life span of the pull requests. The faster it gets merged the better the contributing experience is. You only need to do the project setups right to optimize for the metric.</p>
<p>Finally, a list of actionable items is provided to optimize the metric.</p>
<p>Hope this article could help improve your open-source software and attract more contributors!</p>]]></content>
  </entry>
  <entry>
    <title>Towards Auto-Generated Models</title>
    <link href="https://haifengjin.com/towards-auto-generated-models/"/>
    <id>https://haifengjin.com/towards-auto-generated-models/</id>
    <published>2022-11-21T00:00:00+00:00</published>
    <updated>2022-11-21T00:00:00+00:00</updated>
    <summary>A brief history of neural architecture search and beyond</summary>
    <content type="html"><![CDATA[<p>An interesting paper was quietly released on arXiv in 2016 by a group of research scientists from Google Brain, which used machine learning (ML) to create new ML models. No one expected that it would raise every possible controversy about ML research and put Google on the spot in the next three years. Then, it took another three years for a Turing award winner at Google to fully analyze the issue and make a formal response.</p>
<p>I will walk you through all the interesting ideas and dramas that happened in this field with this article.</p>
<h2>Breakthrough: The initial paper</h2>
<p>Reinforcement learning can teach a smart agent to perform many tasks, like playing chess, or arcade games. Almost anything, as long as there is a clear feedback loop to evaluate the agent's performance, can be learned by the agent. What about designing ML models? We can easily evaluate a machine learning model. That was the idea of the paper.</p>
<p>The title of the paper is “Neural Architecture Search with Reinforcement Learning”<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup> published in 2016. Instead of manually designing a new type of neural network architecture, like AlexNet, ResNet, or DenseNet, it uses reinforcement learning to automatically generate new neural architectures. As shown in the following figure, an agent, which is also a neural network, is trained to produce neural architectures. Each neural architecture is trained and evaluated. The evaluation result is the reward function to the agent. By producing new architectures and learning the performance of each one of them, the agent gradually learns how to produce good neural architectures.</p>
<figure class="svg-figure"><svg version="1.1" viewBox="0.0 0.0 666.1338582677165 329.4199475065617" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="svg_a_1_p.0"><path d="m0 0l666.13385 0l0 329.41995l-666.13385 0l0 -329.41995z" clip-rule="nonzero"/></clipPath><g clip-path="url(#svg_a_1_p.0)"><path fill="none" fill-opacity="0.0" d="m0 0l666.13385 0l0 329.41995l-666.13385 0z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m5.881761 116.46165l0 0c0 -12.912994 10.46805 -23.38105 23.381044 -23.38105l187.31665 0c6.20105 0 12.148102 2.463356 16.532898 6.848152c4.384796 4.384796 6.8481445 10.331856 6.8481445 16.532898l0 93.52137c0 12.912994 -10.468048 23.381042 -23.381042 23.381042l-187.31665 0c-12.912994 0 -23.381044 -10.468048 -23.381044 -23.381042z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m5.881761 116.46165l0 0c0 -12.912994 10.46805 -23.38105 23.381044 -23.38105l187.31665 0c6.20105 0 12.148102 2.463356 16.532898 6.848152c4.384796 4.384796 6.8481445 10.331856 6.8481445 16.532898l0 93.52137c0 12.912994 -10.468048 23.381042 -23.381042 23.381042l-187.31665 0c-12.912994 0 -23.381044 -10.468048 -23.381044 -23.381042z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m86.19451 172.54234l4.8125 -18.484375l3.0625 0l4.796875 18.484375l-2.3125 0l-1.21875 -4.90625l-5.59375 0l-1.21875 4.90625l-2.328125 0zm4.0 -6.84375l4.65625 0l-1.40625 -5.6875q-0.40625 -1.625 -0.640625 -2.71875q-0.21875 -1.09375 -0.28125 -1.421875q-0.046875 0.328125 -0.28125 1.421875q-0.21875 1.09375 -0.625 2.6875l-1.421875 5.71875zm14.009399 11.40625l0 -2.078125l4.4375 0q1.0625 0 1.546875 -0.484375q0.5 -0.484375 0.5 -1.5l0 -1.765625l0.046875 -2.53125l-0.453125 0l0.4375 -0.375q0 1.515625 -1.015625 2.40625q-1.015625 0.875 -2.71875 0.875q-2.140625 0 -3.390625 -1.40625q-1.234375 -1.40625 -1.234375 -3.78125l0 -2.9375q0 -2.375 1.234375 -3.765625q1.25 -1.40625 3.390625 -1.40625q1.703125 0 2.71875 0.890625q1.015625 0.890625 1.015625 2.40625l-0.4375 -0.375l0.4375 0l0 -2.671875l2.25 0l0 14.4375q0 1.875 -1.15625 2.96875q-1.15625 1.09375 -3.203125 1.09375l-4.40625 0zm3.46875 -7.421875q1.421875 0 2.234375 -0.890625q0.8125 -0.890625 0.8125 -2.453125l0 -2.65625q0 -1.578125 -0.8125 -2.453125q-0.8125 -0.890625 -2.234375 -0.890625q-1.4375 0 -2.234375 0.859375q-0.796875 0.859375 -0.796875 2.484375l0 2.65625q0 1.625 0.796875 2.484375q0.796875 0.859375 2.234375 0.859375zm15.243774 3.109375q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.6718674 1.1875 0.6718674 2.8125l0 2.453125l-8.671867 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.2812424 0q-0.234375 1.75 -1.7187424 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm6.712517 7.921875l0 -13.9375l2.28125 0l0 2.671875l0.484375 0l-0.484375 0.53125q0 -1.65625 0.984375 -2.546875q0.984375 -0.90625 2.734375 -0.90625q2.109375 0 3.34375 1.296875q1.25 1.296875 1.25 3.515625l0 9.375l-2.28125 0l0 -9.125q0 -1.484375 -0.796875 -2.28125q-0.796875 -0.796875 -2.171875 -0.796875q-1.421875 0 -2.25 0.859375q-0.8125 0.859375 -0.8125 2.484375l0 8.859375l-2.28125 0zm21.993774 0q-1.8125 0 -2.8125 -0.953125q-1.0 -0.96875 -1.0 -2.71875l0 -8.1875l-3.921875 0l0 -2.078125l3.921875 0l0 -3.921875l2.28125 0l0 3.921875l5.578125 0l0 2.078125l-5.578125 0l0 8.1875q0 1.59375 1.53125 1.59375l3.796875 0l0 2.078125l-3.796875 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m423.81616 116.46165l0 0c0 -12.912994 10.468048 -23.38105 23.381042 -23.38105l187.31665 0c6.20105 0 12.148132 2.463356 16.532898 6.848152c4.3848267 4.384796 6.8481445 10.331856 6.8481445 16.532898l0 93.52137c0 12.912994 -10.468018 23.381042 -23.381042 23.381042l-187.31665 0c-12.912994 0 -23.381042 -10.468048 -23.381042 -23.381042z" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m423.81616 116.46165l0 0c0 -12.912994 10.468048 -23.38105 23.381042 -23.38105l187.31665 0c6.20105 0 12.148132 2.463356 16.532898 6.848152c4.3848267 4.384796 6.8481445 10.331856 6.8481445 16.532898l0 93.52137c0 12.912994 -10.468018 23.381042 -23.381042 23.381042l-187.31665 0c-12.912994 0 -23.381042 -10.468048 -23.381042 -23.381042z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m497.54608 157.54233l0 -18.484375l3.03125 0l5.65625 15.828125q-0.046875 -0.640625 -0.109375 -1.5625q-0.0625 -0.921875 -0.109375 -1.9375q-0.03125 -1.03125 -0.03125 -1.953125l0 -10.375l2.203125 0l0 18.484375l-3.046875 0l-5.625 -15.828125q0.0625 0.609375 0.109375 1.53125q0.046875 0.921875 0.078125 1.953125q0.046875 1.015625 0.046875 1.953125l0 10.390625l-2.203125 0zm20.5094 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm11.962524 8.171875q-2.40625 0 -3.859375 -1.421875q-1.4375 -1.4375 -1.4375 -3.890625l0 -8.875l2.28125 0l0 8.875q0 1.5625 0.8125 2.4375q0.8125 0.875 2.203125 0.875q1.40625 0 2.234375 -0.875q0.828125 -0.875 0.828125 -2.4375l0 -8.875l2.28125 0l0 8.875q0 2.453125 -1.46875 3.890625q-1.46875 1.421875 -3.875 1.421875zm10.431274 -0.25l0 -13.9375l2.234375 0l0 2.671875l0.53125 0l-0.734375 1.640625q0 -2.25 0.96875 -3.40625q0.96875 -1.15625 2.90625 -1.15625q2.203125 0 3.5 1.390625q1.3125 1.375 1.3125 3.734375l0 0.828125l-2.28125 0l0 -0.625q0 -1.65625 -0.796875 -2.53125q-0.796875 -0.875 -2.265625 -0.875q-1.453125 0 -2.28125 0.890625q-0.8125 0.890625 -0.8125 2.515625l0 8.859375l-2.28125 0zm18.7594 0.25q-2.203125 0 -3.46875 -1.125q-1.265625 -1.125 -1.265625 -3.046875q0 -1.953125 1.265625 -3.0625q1.265625 -1.125 3.421875 -1.125l4.3125 0l0 -1.390625q0 -1.296875 -0.78125 -1.984375q-0.765625 -0.703125 -2.1875 -0.703125q-1.234375 0 -2.078125 0.53125q-0.828125 0.515625 -0.953125 1.390625l-2.28125 0q0.21875 -1.796875 1.6875 -2.859375q1.46875 -1.0625 3.671875 -1.0625q2.40625 0 3.796875 1.25q1.40625 1.234375 1.40625 3.359375l0 9.578125l-2.234375 0l0 -2.65625l-0.375 0l0.375 -0.390625q0 1.53125 -1.1875 2.421875q-1.171875 0.875 -3.125 0.875zm0.5625 -1.796875q1.625 0 2.65625 -0.8125q1.046875 -0.8125 1.046875 -2.078125l0 -1.96875l-4.265625 0q-1.125 0 -1.796875 0.640625q-0.65625 0.625 -0.65625 1.71875q0 1.15625 0.796875 1.828125q0.796875 0.671875 2.21875 0.671875zm17.7594 1.546875q-1.21875 0 -2.140625 -0.484375q-0.90625 -0.484375 -1.40625 -1.359375q-0.5 -0.890625 -0.5 -2.078125l0 -12.484375l-4.6875 0l0 -2.078125l6.953125 0l0 14.5625q0 0.859375 0.484375 1.359375q0.484375 0.484375 1.296875 0.484375l4.4375 0l0 2.078125l-4.4375 0z" fill-rule="nonzero"/><path fill="var(--text-color)" d="m443.3413 187.54234l4.8125 -18.484375l3.0625 0l4.796875 18.484375l-2.3125 0l-1.21875 -4.90625l-5.59375 0l-1.21875 4.90625l-2.328125 0zm4.0 -6.84375l4.65625 0l-1.40625 -5.6875q-0.40625 -1.625 -0.640625 -2.71875q-0.21875 -1.09375 -0.28125 -1.421875q-0.046875 0.328125 -0.28125 1.421875q-0.21875 1.09375 -0.625 2.6875l-1.421875 5.71875zm12.743774 6.84375l0 -13.9375l2.234375 0l0 2.671875l0.53125 0l-0.734375 1.640625q0 -2.25 0.96875 -3.40625q0.96875 -1.15625 2.90625 -1.15625q2.203125 0 3.5 1.390625q1.3125 1.375 1.3125 3.734375l0 0.828125l-2.28125 0l0 -0.625q0 -1.65625 -0.796875 -2.53125q-0.796875 -0.875 -2.265625 -0.875q-1.453125 0 -2.28125 0.890625q-0.8125 0.890625 -0.8125 2.515625l0 8.859375l-2.28125 0zm20.087524 0.25q-1.671875 0 -2.9375 -0.625q-1.25 -0.640625 -1.9375 -1.84375q-0.671875 -1.203125 -0.671875 -2.84375l0 -3.796875q0 -1.671875 0.671875 -2.859375q0.6875 -1.203125 1.9375 -1.828125q1.265625 -0.640625 2.9375 -0.640625q2.40625 0 3.875 1.296875q1.46875 1.296875 1.546875 3.515625l-2.28125 0q-0.078125 -1.34375 -0.90625 -2.0625q-0.8125 -0.71875 -2.234375 -0.71875q-1.5 0 -2.390625 0.859375q-0.875 0.84375 -0.875 2.40625l0 3.828125q0 1.5625 0.875 2.4375q0.890625 0.859375 2.390625 0.859375q1.421875 0 2.234375 -0.734375q0.828125 -0.734375 0.90625 -2.0625l2.28125 0q-0.078125 2.234375 -1.546875 3.53125q-1.46875 1.28125 -3.875 1.28125zm9.821899 -0.25l0 -18.484375l2.28125 0l0 4.546875l0 2.671875l0.484375 0l-0.484375 0.53125q0 -1.65625 0.984375 -2.546875q0.984375 -0.90625 2.734375 -0.90625q2.109375 0 3.34375 1.296875q1.25 1.296875 1.25 3.515625l0 9.375l-2.28125 0l0 -9.125q0 -1.484375 -0.796875 -2.3125q-0.796875 -0.828125 -2.171875 -0.828125q-1.421875 0 -2.25 0.890625q-0.8125 0.890625 -0.8125 2.515625l0 8.859375l-2.28125 0zm15.025024 0l0 -2.078125l4.9375 0l0 -9.78125l-4.3125 0l0 -2.078125l6.59375 0l0 11.859375l4.6875 0l0 2.078125l-11.90625 0zm5.8125 -16.4375q-0.828125 0 -1.3125 -0.421875q-0.484375 -0.4375 -0.484375 -1.171875q0 -0.765625 0.484375 -1.203125q0.484375 -0.453125 1.3125 -0.453125q0.84375 0 1.3125 0.453125q0.484375 0.4375 0.484375 1.203125q0 0.734375 -0.484375 1.171875q-0.46875 0.421875 -1.3125 0.421875zm16.35315 16.4375q-1.8125 0 -2.8125 -0.953125q-1.0 -0.96875 -1.0 -2.71875l0 -8.1875l-3.921875 0l0 -2.078125l3.921875 0l0 -3.921875l2.28125 0l0 3.921875l5.578125 0l0 2.078125l-5.578125 0l0 8.1875q0 1.59375 1.53125 1.59375l3.796875 0l0 2.078125l-3.796875 0zm13.665649 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm12.087524 8.171875q-1.671875 0 -2.9375 -0.625q-1.25 -0.640625 -1.9375 -1.84375q-0.671875 -1.203125 -0.671875 -2.84375l0 -3.796875q0 -1.671875 0.671875 -2.859375q0.6875 -1.203125 1.9375 -1.828125q1.265625 -0.640625 2.9375 -0.640625q2.40625 0 3.875 1.296875q1.46875 1.296875 1.546875 3.515625l-2.28125 0q-0.078125 -1.34375 -0.90625 -2.0625q-0.8125 -0.71875 -2.234375 -0.71875q-1.5 0 -2.390625 0.859375q-0.875 0.84375 -0.875 2.40625l0 3.828125q0 1.5625 0.875 2.4375q0.890625 0.859375 2.390625 0.859375q1.421875 0 2.234375 -0.734375q0.828125 -0.734375 0.90625 -2.0625l2.28125 0q-0.078125 2.234375 -1.546875 3.53125q-1.46875 1.28125 -3.875 1.28125zm16.618774 -0.25q-1.8125 0 -2.8125 -0.953125q-1.0 -0.96875 -1.0 -2.71875l0 -8.1875l-3.921875 0l0 -2.078125l3.921875 0l0 -3.921875l2.28125 0l0 3.921875l5.578125 0l0 2.078125l-5.578125 0l0 8.1875q0 1.59375 1.53125 1.59375l3.796875 0l0 2.078125l-3.796875 0zm13.650024 0.25q-2.40625 0 -3.859375 -1.421875q-1.4375 -1.4375 -1.4375 -3.890625l0 -8.875l2.28125 0l0 8.875q0 1.5625 0.8125 2.4375q0.8125 0.875 2.203125 0.875q1.40625 0 2.234375 -0.875q0.828125 -0.875 0.828125 -2.4375l0 -8.875l2.28125 0l0 8.875q0 2.453125 -1.46875 3.890625q-1.46875 1.421875 -3.875 1.421875zm10.431274 -0.25l0 -13.9375l2.234375 0l0 2.671875l0.53125 0l-0.734375 1.640625q0 -2.25 0.96875 -3.40625q0.96875 -1.15625 2.90625 -1.15625q2.203125 0 3.5 1.390625q1.3125 1.375 1.3125 3.734375l0 0.828125l-2.28125 0l0 -0.625q0 -1.65625 -0.796875 -2.53125q-0.796875 -0.875 -2.265625 -0.875q-1.453125 0 -2.28125 0.890625q-0.8125 0.890625 -0.8125 2.515625l0 8.859375l-2.28125 0zm19.97815 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm11.556274 8.125q-1.421875 0 -2.484375 -0.4375q-1.046875 -0.453125 -1.671875 -1.265625q-0.609375 -0.8125 -0.734375 -1.921875l2.28125 0q0.15625 0.734375 0.8125 1.171875q0.671875 0.421875 1.796875 0.421875l1.0625 0q1.34375 0 2.0 -0.53125q0.65625 -0.546875 0.65625 -1.46875q0 -0.890625 -0.59375 -1.40625q-0.59375 -0.515625 -1.765625 -0.6875l-1.875 -0.3125q-2.046875 -0.34375 -3.046875 -1.265625q-1.0 -0.9375 -1.0 -2.703125q0 -1.875 1.1875 -2.90625q1.1875 -1.03125 3.578125 -1.03125l0.953125 0q2.0 0 3.21875 0.9375q1.21875 0.9375 1.46875 2.546875l-2.28125 0q-0.140625 -0.671875 -0.75 -1.0625q-0.609375 -0.390625 -1.65625 -0.390625l-0.953125 0q-1.296875 0 -1.890625 0.484375q-0.59375 0.484375 -0.59375 1.4375q0 0.859375 0.5 1.265625q0.515625 0.40625 1.59375 0.59375l1.875 0.296875q2.265625 0.359375 3.28125 1.328125q1.03125 0.953125 1.03125 2.78125q0 1.921875 -1.234375 3.03125q-1.21875 1.09375 -3.703125 1.09375l-1.0625 0z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m122.921135 93.0806c0 -45.364826 104.48031 -90.73753 208.96063 -90.72966c104.480316 0.007874012 208.96063 45.396324 208.96063 90.792656" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m122.92113 93.0806c0 -45.364826 104.48033 -90.73753 208.96065 -90.72966c52.240143 0.003937006 104.480316 11.35302 143.66043 28.375654c19.590057 8.51132 35.9151 18.44103 47.34262 29.08018c2.8568726 2.6597824 5.407715 5.3639107 7.626831 8.101299c0.5548096 0.68434906 1.0889282 1.370781 1.6018677 2.059105c0.12823486 0.17208862 0.255188 0.34428406 0.3807373 0.5166092l0.090270996 0.12522125" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="4.0" stroke-linecap="butt" d="m526.381 72.88232l12.449402 14.770714l-0.0423584 -19.317322z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m540.8555 233.36406c0 44.423874 -104.480316 88.83989 -208.96063 88.84776c-104.4803 0.007873535 -208.96062 -44.392365 -208.96062 -88.78476" fill-rule="evenodd"/><path stroke="var(--text-color)" stroke-width="4.0" stroke-linejoin="round" stroke-linecap="butt" d="m540.8555 233.36407c0 44.42389 -104.480316 88.839905 -208.96063 88.84778c-52.240143 0.0039367676 -104.4803 -11.094177 -143.66042 -27.74231c-19.590057 -8.324066 -35.915115 -18.035645 -47.34265 -28.440704c-2.8568726 -2.6012878 -5.407669 -5.24588 -7.6268463 -7.923004c-0.5548096 -0.669281 -1.0888672 -1.3406067 -1.6018219 -2.0137634l-0.1802063 -0.23913574" fill-rule="evenodd"/><path fill="var(--text-color)" stroke="var(--text-color)" stroke-width="4.0" stroke-linecap="butt" d="m137.65654 253.49959l-12.639374 -14.608444l0.29219055 19.315155z" fill-rule="evenodd"/><path fill="none" fill-opacity="0.0" d="m247.11267 45.103073l177.00787 0l0 50.078743l-177.00787 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m282.53214 78.67307q-1.671875 0 -2.90625 -0.625q-1.21875 -0.640625 -1.890625 -1.84375q-0.671875 -1.203125 -0.671875 -2.84375l0 -8.359375q0 -1.671875 0.671875 -2.859375q0.671875 -1.1875 1.890625 -1.828125q1.234375 -0.640625 2.90625 -0.640625q1.671875 0 2.875 0.65625q1.21875 0.640625 1.875 1.84375q0.671875 1.1875 0.671875 2.828125l-2.28125 0q0 -1.578125 -0.828125 -2.421875q-0.828125 -0.84375 -2.3125 -0.84375q-1.5 0 -2.34375 0.84375q-0.84375 0.828125 -0.84375 2.390625l0 8.390625q0 1.5625 0.84375 2.4375q0.84375 0.859375 2.34375 0.859375q1.484375 0 2.3125 -0.859375q0.828125 -0.875 0.828125 -2.4375l0 -2.28125l-3.75 0l0 -2.078125l6.03125 0l0 4.359375q0 1.625 -0.671875 2.828125q-0.65625 1.203125 -1.875 1.84375q-1.203125 0.640625 -2.875 0.640625zm15.087524 0q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm6.7125244 7.921875l0 -13.9375l2.28125 0l0 2.671875l0.484375 0l-0.484375 0.53125q0 -1.65625 0.984375 -2.546875q0.984375 -0.90625 2.734375 -0.90625q2.109375 0 3.34375 1.296875q1.25 1.296875 1.25 3.515625l0 9.375l-2.28125 0l0 -9.125q0 -1.484375 -0.796875 -2.28125q-0.796875 -0.796875 -2.171875 -0.796875q-1.421875 0 -2.25 0.859375q-0.8125 0.859375 -0.8125 2.484375l0 8.859375l-2.28125 0zm20.462524 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm7.1968994 7.921875l0 -13.9375l2.234375 0l0 2.671875l0.53125 0l-0.734375 1.640625q0 -2.25 0.96875 -3.40625q0.96875 -1.15625 2.90625 -1.15625q2.203125 0 3.5 1.390625q1.3125 1.375 1.3125 3.734375l0 0.828125l-2.28125 0l0 -0.625q0 -1.65625 -0.796875 -2.53125q-0.796875 -0.875 -2.265625 -0.875q-1.453125 0 -2.28125 0.890625q-0.8125 0.890625 -0.8125 2.515625l0 8.859375l-2.28125 0zm18.7594 0.25q-2.203125 0 -3.46875 -1.125q-1.265625 -1.125 -1.265625 -3.046875q0 -1.953125 1.265625 -3.0625q1.265625 -1.125 3.421875 -1.125l4.3125 0l0 -1.390625q0 -1.296875 -0.78125 -1.984375q-0.765625 -0.703125 -2.1875 -0.703125q-1.234375 0 -2.078125 0.53125q-0.828125 0.515625 -0.953125 1.390625l-2.28125 0q0.21875 -1.796875 1.6875 -2.859375q1.46875 -1.0625 3.671875 -1.0625q2.40625 0 3.796875 1.25q1.40625 1.234375 1.40625 3.359375l0 9.578125l-2.234375 0l0 -2.65625l-0.375 0l0.375 -0.390625q0 1.53125 -1.1875 2.421875q-1.171875 0.875 -3.125 0.875zm0.5625 -1.796875q1.625 0 2.65625 -0.8125q1.046875 -0.8125 1.046875 -2.078125l0 -1.96875l-4.265625 0q-1.125 0 -1.796875 0.640625q-0.65625 0.625 -0.65625 1.71875q0 1.15625 0.796875 1.828125q0.796875 0.671875 2.21875 0.671875zm17.3844 1.546875q-1.8125 0 -2.8125 -0.953125q-1.0 -0.96875 -1.0 -2.71875l0 -8.1875l-3.921875 0l0 -2.078125l3.921875 0l0 -3.921875l2.28125 0l0 3.921875l5.578125 0l0 2.078125l-5.578125 0l0 8.1875q0 1.59375 1.53125 1.59375l3.796875 0l0 2.078125l-3.796875 0zm13.665649 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125z" fill-rule="nonzero"/><path fill="none" fill-opacity="0.0" d="m90.91872 233.34933l489.41736 0l0 50.078735l-489.41736 0z" fill-rule="evenodd"/><path fill="var(--text-color)" d="m277.24603 266.66934l0 -18.515625l10.890625 0l0 2.078125l-8.65625 0l0 6.15625l8.0 0l0 2.078125l-7.953125 0l0 8.203125l-2.28125 0zm20.3844 0.25q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm11.978149 8.171875q-1.640625 0 -2.875 -0.65625q-1.21875 -0.65625 -1.890625 -1.859375q-0.671875 -1.203125 -0.671875 -2.796875l0 -3.796875q0 -1.625 0.671875 -2.8125q0.671875 -1.1875 1.890625 -1.84375q1.234375 -0.671875 2.875 -0.671875q1.65625 0 2.875 0.671875q1.234375 0.65625 1.90625 1.84375q0.671875 1.1875 0.671875 2.8125l0 2.453125l-8.671875 0l0 1.34375q0 1.640625 0.828125 2.515625q0.84375 0.875 2.390625 0.875q1.328125 0 2.125 -0.46875q0.8125 -0.46875 1.0 -1.40625l2.28125 0q-0.234375 1.75 -1.71875 2.78125q-1.484375 1.015625 -3.6875 1.015625zm3.21875 -8.171875l0 -0.9375q0 -1.65625 -0.828125 -2.53125q-0.8125 -0.890625 -2.390625 -0.890625q-1.546875 0 -2.390625 0.890625q-0.828125 0.875 -0.828125 2.53125l0 0.734375l6.625 0l-0.1875 0.203125zm11.165649 8.171875q-2.0625 0 -3.34375 -1.390625q-1.28125 -1.390625 -1.28125 -3.765625l0 -4.078125q0 -2.40625 1.265625 -3.796875q1.265625 -1.40625 3.359375 -1.40625q1.734375 0 2.765625 0.9375q1.046875 0.9375 1.046875 2.515625l-0.46875 -0.53125l0.515625 0l-0.046875 -3.171875l0 -4.046875l2.265625 0l0 18.484375l-2.265625 0l0 -2.65625l-0.46875 0l0.46875 -0.53125q0 1.59375 -1.046875 2.515625q-1.03125 0.921875 -2.765625 0.921875zm0.765625 -1.96875q1.421875 0 2.234375 -0.890625q0.8125 -0.890625 0.8125 -2.453125l0 -3.796875q0 -1.578125 -0.8125 -2.453125q-0.8125 -0.890625 -2.234375 -0.890625q-1.4375 0 -2.28125 0.859375q-0.828125 0.859375 -0.828125 2.484375l0 3.796875q0 1.625 0.828125 2.484375q0.84375 0.859375 2.28125 0.859375zm16.056274 1.96875q-1.71875 0 -2.765625 -0.921875q-1.03125 -0.921875 -1.03125 -2.515625l0.453125 0.53125l-0.453125 0l0 2.65625l-2.28125 0l0 -18.484375l2.28125 0l0 4.046875l-0.046875 3.171875l0.5 0l-0.453125 0.53125q0 -1.578125 1.046875 -2.515625q1.0625 -0.9375 2.75 -0.9375q2.109375 0 3.3750305 1.40625q1.265625 1.390625 1.265625 3.796875l0 4.078125q0 2.375 -1.265625 3.765625q-1.2656555 1.390625 -3.3750305 1.390625zm-0.75 -1.96875q1.4375 0 2.265625 -0.859375q0.84375 -0.859375 0.84375 -2.484375l0 -3.796875q0 -1.625 -0.84375 -2.484375q-0.828125 -0.859375 -2.265625 -0.859375q-1.40625 0 -2.234375 0.890625q-0.8125 0.875 -0.8125 2.453125l0 3.796875q0 1.5625 0.8125 2.453125q0.828125 0.890625 2.234375 0.890625zm13.91568 1.96875q-2.203125 0 -3.46875 -1.125q-1.265625 -1.125 -1.265625 -3.046875q0 -1.953125 1.265625 -3.0625q1.265625 -1.125 3.421875 -1.125l4.3125 0l0 -1.390625q0 -1.296875 -0.78125 -1.984375q-0.765625 -0.703125 -2.1875 -0.703125q-1.234375 0 -2.078125 0.53125q-0.828125 0.515625 -0.953125 1.390625l-2.28125 0q0.21875 -1.796875 1.6875 -2.859375q1.46875 -1.0625 3.671875 -1.0625q2.40625 0 3.796875 1.25q1.40625 1.234375 1.40625 3.359375l0 9.578125l-2.234375 0l0 -2.65625l-0.375 0l0.375 -0.390625q0 1.53125 -1.1875 2.421875q-1.171875 0.875 -3.125 0.875zm0.5625 -1.796875q1.625 0 2.65625 -0.8125q1.046875 -0.8125 1.046875 -2.078125l0 -1.96875l-4.265625 0q-1.125 0 -1.796875 0.640625q-0.65625 0.625 -0.65625 1.71875q0 1.15625 0.796875 1.828125q0.796875 0.671875 2.21875 0.671875zm15.962524 1.796875q-1.671875 0 -2.9375 -0.625q-1.25 -0.640625 -1.9375 -1.84375q-0.671875 -1.203125 -0.671875 -2.84375l0 -3.796875q0 -1.671875 0.671875 -2.859375q0.6875 -1.203125 1.9375 -1.828125q1.265625 -0.640625 2.9375 -0.640625q2.40625 0 3.875 1.296875q1.46875 1.296875 1.546875 3.515625l-2.28125 0q-0.078125 -1.34375 -0.90625 -2.0625q-0.8125 -0.71875 -2.234375 -0.71875q-1.5 0 -2.390625 0.859375q-0.875 0.84375 -0.875 2.40625l0 3.828125q0 1.5625 0.875 2.4375q0.890625 0.859375 2.390625 0.859375q1.421875 0 2.234375 -0.734375q0.828125 -0.734375 0.90625 -2.0625l2.28125 0q-0.078125 2.234375 -1.546875 3.53125q-1.46875 1.28125 -3.875 1.28125zm9.900024 -0.25l0 -18.484375l2.28125 0l0 10.296875l2.6875 0l4.046875 -5.75l2.609375 0l-4.6875 6.640625l4.765625 7.296875l-2.65625 0l-4.0625 -6.203125l-2.703125 0l0 6.203125l-2.28125 0z" fill-rule="nonzero"/></g></svg></figure>

<p>In the paper, a new type of neural network, NasNet, was auto-generated by reinforcement learning and outperformed most of the rest of the neural architectures at the time on image classification tasks.</p>
<p>The paper also opened up a whole new research area in ML, named Neural Architecture Search (NAS), which is to use automated methods to produce new types of neural architectures.</p>
<p>The paper was game-changing. Seemingly, it was just another attempt at creating better neural architectures. However, it attempted to automate the work of an ML researcher. Many people fear that their jobs will be taken by ML. Well, the ML researchers don't. On the contrary, they have been working hard to replace themselves with ML. If this idea can work, they will start to produce ML models that are beyond the reach of ML researchers.</p>
<p>Such a great attempt came at a price. It took 800 GPUs for Google Brain to run the experiments due to training and evaluating a large number of neural architectures. Such high cost was intimidating for most of the players in the field. No paper was published by any other group following this topic immediately.</p>
<p>Google Brain continued this line of research by expanding this automated design idea to other parts of the ML process, such as searching activation functions<sup id="cite-2-a"><a href="#ref-2" class="citation-link">2</a></sup> and searching data augmentations<sup id="cite-3-a"><a href="#ref-3" class="citation-link">3</a></sup>.</p>
<h2>Breakthrough: Cost reduction</h2>
<p>To lower the computing resources consumed by NAS, they published a new paper about one year after the initial paper. The title of the new paper is “Efficient Neural Architecture Search via Parameter Sharing” (ENAS)<sup id="cite-4-a"><a href="#ref-4" class="citation-link">4</a></sup>. It reduced the cost of NAS to nearly the cost of training a single neural network. How did they do it?</p>
<p>The neural architectures generated by the agent may not be so different from one to another. In the previous work, they had to train every neural architecture end-to-end even if they were very similar to each other. Can we reuse the weights in the previously trained neural architectures to speed up the training of the neural architectures generated later? That is the idea of this paper.</p>
<p>They still use an agent to generate new neural architectures. The efficiency is improved by changing how they train each neural architecture. Every neural architecture is warm-started using weights from previously trained neural architectures. With this improvement, the total cost of training all the neural architectures generated by the agent almost equals the cost of training a big neural architecture.</p>
<h2>Breakthrough: Neural architectures are differentiable</h2>
<p>Soon after the paper above, some other research groups also started to join the game. They were also aiming at making NAS more efficient but from a different aspect. Instead of improving the training and evaluation, they improved the agent.</p>
<p>From discrete mathematics, we know that graphs are discrete structures. Neural architectures are computing graphs, so they are discrete. The agent may not easily perceive the correlations between different neural architectures and their performance due to their discreteness. What if we can make the neural architectures differentiable? Then, we no longer need an agent to generate the neural architectures. We can directly use gradient descent to optimize a neural architecture just like how we train a neural network.</p>
<p>It sounds like a crazy idea, but the authors of “DARTS: Differentiable Architecture Search”<sup id="cite-5-a"><a href="#ref-5" class="citation-link">5</a></sup> and “Neural Architecture Optimization”<sup id="cite-6-a"><a href="#ref-6" class="citation-link">6</a></sup> found ways to do it and implemented them. We will not go into the details here, but you are welcome to read the papers yourself.</p>
<h2>Other attempts</h2>
<p>As more researchers contributed to this topic, it got harder to make breakthroughs on the original NAS problem. They started to make improvements based on these differentiable solutions or to add constraints to the problem. Among these papers, some high-quality ones are worth reading, including SNAS<sup id="cite-7-a"><a href="#ref-7" class="citation-link">7</a></sup>, ProxylessNAS<sup id="cite-8-a"><a href="#ref-8" class="citation-link">8</a></sup>, and MnasNet<sup id="cite-9-a"><a href="#ref-9" class="citation-link">9</a></sup>.</p>
<h2>Controversy: NAS doesn't make sense</h2>
<p>As we all thought this topic had reached its plateau and nothing big would happen in this field, a huge controversy arose to destroy the entire research area.</p>
<p>Imagine this situation. If I only allow the agent to generate neural architectures very similar to the ResNet, the generated neural architecture is guaranteed to perform similarly to ResNet. In this case, can we say the agent is smart enough to find good neural architectures? No matter how dumb the agent is, as long as the researcher is smart enough to constrain the search space to a small grasp of good neural architectures, the results will be good. Did the authors of the NAS papers employ this trick? A paper was published just to test it out.</p>
<p>The title of the paper is “Random Search and Reproducibility for Neural Architecture Search”<sup id="cite-10-a"><a href="#ref-10" class="citation-link">10</a></sup>. They replaced the reinforcement learning or whatever search algorithm used in the NAS papers with a random search algorithm.</p>
<p>The results are shocking. Random search is as good as any other search algorithm, or sometimes even better. This conclusion invalidated the entire research field of NAS.</p>
<p>If this conclusion in the paper is true, NAS is just a more fancy way to manually design neural architectures. It can never become self-sufficient in ML model designing since it can never find good neural architectures without the carefully designed search space by an experienced ML researcher.</p>
<h2>Breakthrough: Discover models from scratch</h2>
<p>As we all thought the NAS research was over, it had a remarkable comeback with another game-changing paper, “AutoML-Zero: Evolving Machine Learning Algorithms From Scratch”<sup id="cite-11-a"><a href="#ref-11" class="citation-link">11</a></sup>, which shed new light on whether ML can do the work of generating new ML models.</p>
<p>Unlike previous NAS papers, the neural architectures are no longer limited to a carefully human-designed search space. It does not even search neural architectures. It builds ML models from scratch.</p>
<p>It uses evolutionary algorithms to combine basic mathematical operations, like addition, multiplication, or uniform sampling. Use these combinations as ML models, and evaluate their performance on the datasets to select the good models. With this setting, almost any ML model is included in the search space.</p>
<p>The results are amazing. Many widely used ML models were generated by the algorithm. For example, it rediscovered linear regression, stochastic gradient descent, ReLU, and 2-layer neural networks. The search process was like replaying the ML research history.</p>
<p>Although the paper did not come up with better ML models, it proved the concept of auto-generated ML models. It is possible, at least on a basic level, for ML to generate usable ML models.</p>
<h2>Controversy: NAS vs the environment</h2>
<p>Soon after the previous controversy, an even bigger crisis arose about NAS, which started with a paper named “Energy and Policy Considerations for Deep Learning in NLP”<sup id="cite-12-a"><a href="#ref-12" class="citation-link">12</a></sup>. It was highlighted by MIT Technology Review<sup id="cite-13-a"><a href="#ref-13" class="citation-link">13</a></sup> and drew a lot of attention.</p>
<p>The work investigated the energy consumption and carbon emission of conducting deep learning research, especially training large deep learning models for natural language processing (NLP). The biggest model mentioned in the paper was a Transformer model based on NAS, which contains 0.2 billion parameters. Training such a model would emit the same amount of CO2 as taking 300 passengers for a roundtrip from San Francisco to New York.</p>
<p>Deep learning VS. the environment. It is a fundamental question to answer as deep learning advanced at a crazy speed in recent years. Suddenly, a lot of criticism arose. Google was criticized for harming the environment by training large deep learning models. Will Google successfully address this PR crisis? Most importantly, will Google take the responsibility to solve the problem of eco-friendliness for deep learning?</p>
<p>As a response, Google committed its top talents to address the issue. David Patterson, a Turing award winner, led a group of genius research scientists to fully investigate the problem for almost three years. Finally, they came up with a response in confidence in 2022.</p>
<p>The title of the paper is “The Carbon Footprint of Machine Learning Training Will Plateau, Then Shrink”<sup id="cite-14-a"><a href="#ref-14" class="citation-link">14</a></sup>. They proposed 4 best practices to reduce the carbon footprint and energy consumption of training deep learning models. By adopting these best practices, Google dramatically reduced its carbon emission for its deep learning-related activities. The amount of CO2 emission in 2021 (of training the Transformer) is only 1/700 of that in 2017.</p>
<h2>Neural architecture search is not AutoML</h2>
<p>Before we go to the conclusion, I would like to explain the confusion between NAS and AutoML since these two terms often appear together. AutoML even appeared in the name of “AutoML-Zero”. However, most of the NAS research is not AutoML.</p>
<p>People refer to NAS as AutoML because NAS is an automated way to create new ML models. NAS produces new ML models that generalize well to other datasets and tasks. However, from a practitioner's perspective, they are not so different from ResNet except for better performance. They will still have to do data preprocessing, post-processing, hyperparameter tuning, and so on if they would like to use the model in practice.</p>
<p>AutoML is not about producing new ML models but about the ease of adoption of ML solutions. Given a dataset and a task, AutoML tries to automatically assemble a suitable end-to-end ML solution, including data preprocessing, post-processing, hyperparameter tuning, and everything. From a practitioner's perspective, it dramatically reduces the knowledge requirements and engineering workload to adopt ML to solve their problems.</p>
<h2>Conclusions</h2>
<p>So far we have reviewed the history of auto-generated ML models. Now we need to answer the question: Can we rely on auto-generated ML models?</p>
<p>From the discussions above, as this field progresses, the solutions did get better and better. Especially, when AutoML-Zero came out, I felt it is on the right track. However, it has already exhausted today's computing power just to find a simple 2-layer neural network. I cannot imagine how much computing resources it would need to produce models better than the state-of-the-art models today. Also, the financial benefits are not as clear as other ML research like the GPT-3 or the Stable Diffusion. Big companies are not likely to devote huge amounts of resources to it. Therefore, my answer is: It is not achievable any time soon.</p>
<h2>The greater implication</h2>
<p>Besides the conclusions above, these efforts have some greater implications for ML research in general that cannot be ignored. The papers we discussed above are so brute-force. They are so different from typical ML research.</p>
<p>What is the exact difference here? We used to build ML models with beautiful math equations and rigorously proved theorems. However, the NAS algorithms do not understand math at all. They treat ML models as black boxes and evaluate them solely by experiments.</p>
<p>The methodology of ML research starts to look like experimental sciences. Just like experimental physics, we discover new findings based on experiments. So, should we conduct ML research like math or experimental sciences? Is the math behind the ML models necessary? NAS raised these questions for all of us to answer, which will have a profound influence on future ML research.</p>
<p>In my opinion, creating ML models without strong math backup may be inevitable in the future. Math was a perfect world built by humans, where every conclusion was discovered by rigorous derivation. However, as the math world evolves, it gradually became as complex as the real world. In the end, we will have to rely on experimental methods to discover new findings in math just like what we do with the real world today.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="zoph2016nas" class="csl-entry">Zoph, B., &#38; Le, Q. V. (2016). Neural architecture search with reinforcement learning. <i>ICLR</i>. <a href="https://arxiv.org/abs/1611.01578" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-2"><div class="csl-bib-body">
  <div data-csl-entry-id="swish" class="csl-entry">Ramachandran, P., Zoph, B., &#38; Le, Q. V. (2017). Searching for activation functions. <i>arXiv Preprint arXiv:1710.05941</i>. <a href="https://arxiv.org/abs/1710.05941" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-2-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-3"><div class="csl-bib-body">
  <div data-csl-entry-id="autoaugment" class="csl-entry">Cubuk, E. D., Zoph, B., Mane, D., Vasudevan, V., &#38; Le, Q. V. (2018). AutoAugment: Learning augmentation policies from data. <i>CVPR</i>. <a href="https://arxiv.org/abs/1805.09501" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-3-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-4"><div class="csl-bib-body">
  <div data-csl-entry-id="enas" class="csl-entry">Pham, H., Guan, M. Y., Zoph, B., Le, Q. V., &#38; Dean, J. (2018). Efficient neural architecture search via parameter sharing. <i>ICML</i>. <a href="https://arxiv.org/abs/1802.03268" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-4-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-5"><div class="csl-bib-body">
  <div data-csl-entry-id="darts" class="csl-entry">Liu, H., Simonyan, K., &#38; Yang, Y. (2018). DARTS: Differentiable architecture search. <i>ICLR</i>. <a href="https://arxiv.org/abs/1806.09055" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-5-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-6"><div class="csl-bib-body">
  <div data-csl-entry-id="nao" class="csl-entry">Luo, R., Tian, F., Qin, T., Chen, E., &#38; Liu, T. Y. (2018). Neural architecture optimization. <i>NeurIPS</i>. <a href="https://arxiv.org/abs/1808.07233" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-6-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-7"><div class="csl-bib-body">
  <div data-csl-entry-id="snas" class="csl-entry">Xie, S., Zheng, H., Liu, C., &#38; Lin, L. (2018). SNAS: Stochastic neural architecture search. <i>ICLR</i>. <a href="https://arxiv.org/abs/1812.09926" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-7-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-8"><div class="csl-bib-body">
  <div data-csl-entry-id="proxylessnas" class="csl-entry">Cai, H., Zhu, L., &#38; Han, S. (2018). ProxylessNAS: Direct neural architecture search on target task and hardware. <i>ICLR</i>. <a href="https://arxiv.org/abs/1812.00332" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-8-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-9"><div class="csl-bib-body">
  <div data-csl-entry-id="mnasnet" class="csl-entry">Tan, M., Chen, B., Pang, R., Vasudevan, V., Sandler, M., Howard, A., &#38; Le, Q. V. (2018). MnasNet: Platform-aware neural architecture search for mobile. <i>CVPR</i>. <a href="https://arxiv.org/abs/1807.11626" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-9-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-10"><div class="csl-bib-body">
  <div data-csl-entry-id="random-search-nas" class="csl-entry">Li, L., &#38; Talwalkar, A. (2019). Random search and reproducibility for neural architecture search. <i>UAI</i>. <a href="https://arxiv.org/abs/1902.07638" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-10-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-11"><div class="csl-bib-body">
  <div data-csl-entry-id="automl-zero" class="csl-entry">Real, E., Liang, C., So, D. R., &#38; Le, Q. V. (2020). AutoML-Zero: Evolving machine learning algorithms from scratch. <i>ICML</i>. <a href="https://arxiv.org/abs/2003.03384" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-11-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-12"><div class="csl-bib-body">
  <div data-csl-entry-id="energy-nlp" class="csl-entry">Strubell, E., Ganesh, A., &#38; McCallum, A. (2019). Energy and policy considerations for deep learning in NLP. <i>ACL</i>. <a href="https://arxiv.org/abs/1906.02243" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-12-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-13"><div class="csl-bib-body">
  <div data-csl-entry-id="mit-tech-review-carbon" class="csl-entry">Hao, K. (2019). <i>Training a single AI model can emit as much carbon as five cars in their lifetimes</i>. MIT Technology Review. <a href="https://www.technologyreview.com/2019/06/06/239031/training-a-single-ai-model-can-emit-as-much-carbon-as-five-cars-in-their-lifetimes/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-13-a" class="reference-back-link">↩</a></div>
</div></li><li id="ref-14"><div class="csl-bib-body">
  <div data-csl-entry-id="carbon-footprint" class="csl-entry">Patterson, D., Gonzalez, J., Hölzle, U., Le, Q., Liang, C., Mambretti, L. M., &#38; others. (2022). <i>The Carbon Footprint of Machine Learning Training Will Plateau, Then Shrink</i>. TechRxiv. <a href="https://www.techrxiv.org/articles/preprint/The_Carbon_Footprint_of_Machine_Learning_Training_Will_Plateau_Then_Shrink/19139645" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-14-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
  <entry>
    <title>TensorFlow Is Open-Source, But Why?</title>
    <link href="https://haifengjin.com/tensorflow-is-open-source-but-why/"/>
    <id>https://haifengjin.com/tensorflow-is-open-source-but-why/</id>
    <published>2022-11-01T00:00:00+00:00</published>
    <updated>2022-11-01T00:00:00+00:00</updated>
    <summary>A peek into Google's open-source strategy</summary>
    <content type="html"><![CDATA[<p>The reason behind the open-source of TensorFlow may be quite different from what you think. At least, it is quite different from many popular open-source projects today.</p>
<p>This article summarizes my analysis of the reasons behind the open source of TensorFlow.</p>
<p>DISCLAIMER: The article represents the writer's personal opinion and in no way reflects the opinions and/or ideas of Google or its partners. The entirety of the information presented within this article is sourced exclusively from publicly available materials.</p>
<h2>A popular business model of open-source software</h2>
<p>The most popular business model for running open-source software is the “free code, paid service” model. They use the open-source code to attract more users, but there is always a paid service based on the code down the road somewhere to earn the money back from the users. Let us see two examples.</p>
<p>The first example is a big open-source software, Android. Android is open-source so all smartphone manufacturers want to use this operating system instead of developing their own. Therefore, whatever brand of smartphone you buy, you are going to use the Android operating system. A large number of users are connected to their ecosystem. If any of these users ever purchase an app, android will get its share.</p>
<p>The second example is reveal.js from a small startup company. It is a JavaScript library for making slides. You need to write JavaScript code to use it. It is open-source and free to use. They also host a premium service on slides.com, which is a web graphical user interface based on the open-source library. Again, the open-source library is to make technical impacts and attract users, and the premium service is to make profits from some of the attracted users.</p>
<h2>A similar conspiracy theory around TensorFlow</h2>
<p>Similarly, we can expect that TensorFlow is trying to board more people on the platform and get profit from them somewhere down the road. The most widely spread theory is that Google open-sourced TensorFlow to connect users to their cloud services.</p>
<p>It makes perfect sense. TensorFlow is there to make a technical impact and attract users, and the Google Cloud Platform (GCP) would be the best premium service for using TensorFlow. Especially, the TPUs, which pushed TensorFlow's performance to the next level, are exclusive to the GCP.</p>
<h2>If this is true …</h2>
<p>If this theory is true, the slide from using open-source TensorFlow to premium TensorFlow services should be buttery smooth. This is because they want to push the conversion rate from free users to paid users as high as possible. We should at least see the following things happening.</p>
<p><strong>First, TPUs should be promoted in the TensorFlow documentation.</strong> Promoting TPUs can direct users to GCP, where the TPUs live. However, the TPU instructions are hidden <a href="https://www.tensorflow.org/guide/tpu">quite deep on the website</a>. If you land on <a href="https://www.tensorflow.org/">the index page</a> of the website, it is hard to find anything related to TPU unless you use the search bar.</p>
<p><strong>Second, TensorFlow services on GCP should be super easy to use.</strong> However, on the <a href="https://cloud.google.com/products/ai/">AI &amp; ML page</a> of the GCP website, there is only one dedicated product for TensorFlow, which is the <a href="https://cloud.google.com/tensorflow-enterprise">TensorFlow Enterprise</a>. None of the rest of the products even mention TensorFlow as a promotion.</p>
<p><strong>Third, Google Colab users should be able to pay more for premium services.</strong> Google Colab is the most popular TensorFlow service hosted by Google. Many data scientists and machine learning practitioners use it as their “TensorFlow IDE”. Google Cloud can easily make a profit by providing premium GPU and TPU services in Google Colab. However, Google Colab has extremely strict <a href="https://research.google.com/colaboratory/faq.html#resource-limits">computing resource limits</a> and there is no way to pay more to get more. You are only allowed to use <a href="https://stackoverflow.com/a/58032694/7910419">a single GPU or TPU</a>. Even if you are on the <a href="https://colab.research.google.com/signup/pricing">Colab Pro+</a>, which is the top tier, your environment may still be preempted after running for 24 hours.</p>
<h2>Why Google did not follow this path</h2>
<p>There are always such conspiracy theories about giant companies. There is a big brother, who oversees everything and makes all the decisions. However, in reality, it is largely not true. Google is one of the world's biggest companies. We cannot think of it as a small company, where all teams work towards a single clear goal pointed out by the upper management.</p>
<p>Each division at Google makes its own OKRs. As a big tech company, Google has always been living in the fear of being broken up into pieces by antitrust lawsuits<sup id="cite-1-a"><a href="#ref-1" class="citation-link">1</a></sup>. In preparation for the apocalypse, Google let every division make its OKRs, a side effect of which is that the divisions are harder to have deep strategic cooperation as they have to prioritize their own OKRs.</p>
<p>TensorFlow and GCP belong to different divisions at Google. TensorFlow was a tool first developed for internal research and machine learning applications at Google, which is more like an internal infrastructure software. However, cloud services are aiming at driving external revenue. Therefore, they have their own top priority goals to care of. Thus, may not have deep collaborations with each other.</p>
<h2>Why was it initially open-sourced?</h2>
<p>We first need to understand how big companies make small decisions. Most small decisions are made bottom-up instead of top-down. The management operates in the background and the great ideas come from engineers, who do the actual work. I believe the decision to open-source TensorFlow is not big enough to have all the upper management coordinate the teams across multiple divisions at Google to plan a long-term monetization strategy for it.</p>
<p>It could be a simple decision just because engineers and researchers prefer open source. The engineers, who developed TensorFlow, prefer open source for better exposure to their work. The researchers, who used TensorFlow in their research, also prefer open source to share their experimental code for credibility.</p>
<p>The management at Google will approve the open source request since it aligns with Google's interest. First, it would not help their competitors beat Google. Second, it would strengthen Google's world-leading role in technology. Third, open source drives product excellence as a world of developers contributing ideas and code to it. Fourth, Building an open-source community also reaches and locks more users. Fifth, they may make a profit from these users in the future, who knows?</p>
<h2>Why keep devoting resources to open source?</h2>
<p>The previous answer may be boring. However, the real interesting question here is: Why does Google keep devoting more resources to building the open-source community for TensorFlow?</p>
<p>Up to now, you should see that TensorFlow does not have a clear business model to make a profit. However, Google is pretty happy to spend a lot of resources just to make TensorFlow a more successful open-source project. They host online and offline events about TensorFlow. Keep renovating TensorFlow official documentation. Spend engineering power on modularizing TensorFlow, like splitting Keras and XLA out of the TensorFlow repo just for easier community contributions. Why is Google throwing so much money away just to power something that is not critical?</p>
<p>The answer is simple. <strong>It is critical actually.</strong> To see why it is critical, we need to approach the problem from another perspective. Let's try to answer another similar question: If they do not care about the open-source community, what would happen?</p>
<h2>If Google does not commit resources…</h2>
<p>If Google only keeps releasing the code updates but does nothing to build its open-source community, such as maintaining the documentation, publishing tutorials, and calling for code contributions, here is what is going to happen. First, TensorFlow would lose a significant amount of users. Second, TensorFlow will become less useful to developers outside of Google. Google would only care about internal needs instead of the rest of the world. Third, TensorFlow will become harder to use even internally without support from the open-source community.</p>
<p>If it goes down this path, no one outside of Google will use TensorFlow. People in the tech world are constantly jumping around between companies. This trend will eventually penetrate Google. Even Google engineers do not want to use TensorFlow. Then, another request would be submitted to the upper management to replace TensorFlow with the most successful third-party deep learning framework at the time. The management will approve it since TensorFlow is not easy to use anyway.</p>
<p>Finally, Google lost control of an important piece of infrastructure software that powers everything related to machine learning at Google, which will damage the quality of many Google services. Moreover, Google needs to spend much more money on migrating its services to the new framework. It costs hundreds of times more than building an open-source community for TensorFlow.</p>
<h2>Conclusions</h2>
<p>Google did not have a business plan when TensorFlow was first open-sourced. It was rather a bottom-up decision from the engineers for self-interest instead of a top-down one. Google keeps investing more resources into TensorFlow to build a successful open-source community for the open-source's own sake instead of aiming for a profit. It is just because the cost of losing the battle of open-source deep learning frameworks is too high.</p>
<p>Therefore, in the foreseeable future, if you are building your deep learning infrastructure, TensorFlow is a trustworthy platform to build upon.</p>
<h2>References</h2>
<ol class="reference-list"><li id="ref-1"><div class="csl-bib-body">
  <div data-csl-entry-id="antitrust-cnet" class="csl-entry">CNET. (2020). <i>Google’s three antitrust battles: Here’s what you need to know</i>. CNET. <a href="https://www.cnet.com/tech/tech-industry/googles-three-antitrust-battles-heres-what-you-need-to-know-faq/" target="_blank" rel="noopener noreferrer" class="reference-url">&#x2197;</a> <a href="#cite-1-a" class="reference-back-link">↩</a></div>
</div></li></ol>]]></content>
  </entry>
</feed>