<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: xbill</title>
    <description>The latest articles on DEV Community by xbill (@xbill).</description>
    <link>https://dev.to/xbill</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3490099%2Fc6a975d0-cd94-485d-82b1-14ed5b344fcf.jpg</url>
      <title>DEV Community: xbill</title>
      <link>https://dev.to/xbill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xbill"/>
    <language>en</language>
    <item>
      <title>Profile Sprawl: Solving a Problem You Didn't Know You Had</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:35:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/profile-sprawl-solving-a-problem-you-didnt-know-you-had-3gi9</link>
      <guid>https://dev.to/aws-builders/profile-sprawl-solving-a-problem-you-didnt-know-you-had-3gi9</guid>
      <description>&lt;p&gt;This article provides a step by step guide to keeping a public web presence consistent across many sites. An agent skill for Claude Code is built that treats a bio as source code: one file holds the facts, one table records where it has landed, and scripts read the live sites to confirm it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/profile-sync" rel="noopener noreferrer"&gt;https://github.com/xbill9/profile-sync&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every profile you own is a copy of your bio, and copies drift. The fix is the one software already uses: a single source, a status table and a check that reads production.&lt;/strong&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Profile Sprawl?
&lt;/h4&gt;

&lt;p&gt;A working developer collects profiles. LinkedIn, GitHub, Medium, dev.to, Substack, a certification wallet, a community portal, a program dashboard. Each one was accurate on the day you filled it in.&lt;/p&gt;

&lt;p&gt;Profile sprawl is what happens next. A new role or credential goes on the site you happened to have open, and the rest keep the old text. No site reports an error, because each one is internally fine. The inconsistency exists only across sites, and nobody reads them side by side except a recruiter, a conference organizer or a reviewer.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Bad Could It Be?
&lt;/h4&gt;

&lt;p&gt;One audit of ten profiles belonging to one person, read on 2026-10-31, the day after a new certification was issued:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Certifications&lt;/th&gt;
&lt;th&gt;Years of experience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn headline&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn About&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;over 30+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Developers profile&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credly&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advocu&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same LinkedIn profile disagreed with itself. The headline said 10 and the About said 9, and each line looked fine alone.&lt;/p&gt;

&lt;p&gt;The other four sites had a different problem. AWS Builder Center listed 0 credentials, dev.to held a tagline, GitHub held an unexplained string, and Substack was empty apart from the platform's default text.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A list of your public profiles and a browser where you are logged in to them&lt;/li&gt;
&lt;li&gt;Python 3, with no extra packages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; with the Chrome extension, or the patience to run the same steps by hand&lt;/li&gt;
&lt;li&gt;A git repository, which can be public as long as it holds public text only&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Write One Source File
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;bio.md&lt;/code&gt; holds everything a site could need, in plain Markdown with no site-specific syntax. Facts are lists. Text comes in lengths, written from those facts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-31&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Bio&lt;/span&gt;

&lt;span class="gu"&gt;## Headline&lt;/span&gt;
&lt;span class="gu"&gt;## Tagline&lt;/span&gt;
&lt;span class="gu"&gt;## Short&lt;/span&gt;
&lt;span class="gu"&gt;## Summary&lt;/span&gt;
&lt;span class="gu"&gt;## Long&lt;/span&gt;
&lt;span class="gu"&gt;## Roles&lt;/span&gt;
&lt;span class="gu"&gt;## Community&lt;/span&gt;
&lt;span class="gu"&gt;## Certifications&lt;/span&gt;
&lt;span class="gu"&gt;## Education&lt;/span&gt;
&lt;span class="gu"&gt;## Links&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lengths cover every form measured here: Short at 133 characters, Summary at 561 and Long at 735. A site gets its own text only when a limit forces it, and even then the shorter text becomes a section of the file. Per-site copies are how sprawl starts.&lt;/p&gt;

&lt;p&gt;The rule that keeps this working: a change starts in &lt;code&gt;bio.md&lt;/code&gt;, never on a site. If a site was edited directly, that change is copied back first.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Track Where It Has Landed
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;tracker.md&lt;/code&gt; is one table with one row per site. Status takes one of four values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;Synced version matches &lt;code&gt;bio.md&lt;/code&gt; and the live profile was checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behind&lt;/td&gt;
&lt;td&gt;The site has an older version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;td&gt;Nobody has compared this site to &lt;code&gt;bio.md&lt;/code&gt; yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Something outside your control stops the update, explained in Notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Text used&lt;/strong&gt; column records which sections a site takes. A change to Long reaches only the sites that use Long, and every other row moves to the new version with no edit.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Make the Computer Do the Counting
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;preflight.py&lt;/code&gt; compares the two files and exits 1 on any disagreement. It counts the &lt;code&gt;Certifications&lt;/code&gt; list and compares that count with every claim in the text, so a number in a headline can never drift from the list it summarizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/preflight.py
bio.md 2026-10-31, 10 sites: {'Behind': 0, 'Blocked': 1, 'Current': 9, 'Unverified': 0}
preflight OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also fails on a text longer than a site's form allows, a &lt;code&gt;Current&lt;/code&gt; row with no check date, and a status outside the four values. The repository is public, so it scans both files for email addresses and phone numbers too.&lt;/p&gt;

&lt;p&gt;Arithmetic belongs in code. A model or a person can miscount a list. A script compares 10 with 10.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Read the Live Sites
&lt;/h4&gt;

&lt;p&gt;A tracker row is a claim. &lt;code&gt;check-live.py&lt;/code&gt; fetches what a visitor gets from each site with a public source and compares it with &lt;code&gt;bio.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/check-live.py
  SKIP  LinkedIn: browser check
  OK    GitHub: live text matches Short
  SKIP  Medium: browser check
  OK    dev.to: live text matches Short
  OK    Substack: live text matches Short
  SKIP  AWS Builder Center: browser check
  SKIP  Google Developers profile: browser check
  SKIP  Google Cloud Skills Boost: browser check
  SKIP  Credly: browser check
  SKIP  Advocu: browser check
check-live OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A site marked &lt;code&gt;Current&lt;/code&gt; that differs fails the run. A check that can only pass proves nothing, so the same script was run against a stale value to confirm it exits 1.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Measure Every Form
&lt;/h4&gt;

&lt;p&gt;Each site has its own field, limit and save button. These are read from the forms themselves, and they live in one reference file so the next run starts with them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Short bio&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to&lt;/td&gt;
&lt;td&gt;Bio&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Builder Center&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credly&lt;/td&gt;
&lt;td&gt;Personal Summary&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Developers profile&lt;/td&gt;
&lt;td&gt;Bio&lt;/td&gt;
&lt;td&gt;750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;2,600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A property of one site is evidence about that site only. The Long text at 735 characters fits 2 of these fields and fails the other 4, so the limits go in &lt;code&gt;biolib.py&lt;/code&gt; and &lt;code&gt;preflight.py&lt;/code&gt; enforces them before any browser opens.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Reading and Writing Are Different Routes
&lt;/h4&gt;

&lt;p&gt;dev.to has a good API, and a key reads the profile with no trouble. Profile fields are read-only there: &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;PATCH&lt;/code&gt; on &lt;code&gt;/api/users/me&lt;/code&gt; return 404, so the bio is saved through the settings page. Test both directions before planning a rollout around an API.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Compare Before Saving
&lt;/h4&gt;

&lt;p&gt;Browser automation can drop a character while reporting success, and a page can show a success message for the wrong text. So each save follows the same order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scroll to the field and confirm it has focus.&lt;/li&gt;
&lt;li&gt;Select the old text and type the new text.&lt;/li&gt;
&lt;li&gt;Compare the field with the exact payload from &lt;code&gt;bio.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read the position of the save button after scrolling.&lt;/li&gt;
&lt;li&gt;Save, reload and compare the live text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;payload.py&lt;/code&gt; supplies step 3. It prints the text for a site, or a small JavaScript function that compares a field with it. The text travels base64-encoded, which survives quotes, emoji and line breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/payload.py Credly &amp;gt; /dev/null
Summary: 561 characters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🔎 Tip: Decline the Announcement
&lt;/h4&gt;

&lt;p&gt;Adding a certification on LinkedIn opens a prompt to post about it to your network. Updating a profile and announcing to it are separate decisions. The skill clicks Skip unless an announcement was requested.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Package It as a Skill
&lt;/h4&gt;

&lt;p&gt;The steps above become an agent skill, in the shape of &lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/profile-sync/
  SKILL.md               the workflow
  references/sites.md    route, field, limit and save button per site
  scripts/biolib.py      reads bio.md and tracker.md
  scripts/preflight.py   the two files agree
  scripts/check-live.py  the live sites agree
  scripts/payload.py     the exact text for a field
tests/test_preflight.py  one case per failure, with a passing case beside it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests are the guard on the guards. Each failure gets a case that must fail and a case that must pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m unittest discover -s tests
Ran 18 tests in 0.004s

OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The passing cases carry weight too. One holds a date, so a phone number pattern that matches &lt;code&gt;2026-10-31&lt;/code&gt; fails the suite before it can fail every row in the tracker.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Roll Out One Change
&lt;/h4&gt;

&lt;p&gt;The certification that started this run shows the whole loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Edit bio.md, set version to today&lt;/span&gt;
&lt;span class="c"&gt;# 2. Confirm the files agree&lt;/span&gt;
python3 .claude/skills/profile-sync/scripts/preflight.py
&lt;span class="c"&gt;# 3. In Claude Code, run /profile-sync to roll it out&lt;/span&gt;
&lt;span class="c"&gt;# 4. Confirm the live sites agree&lt;/span&gt;
python3 .claude/skills/profile-sync/scripts/check-live.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill works down the tracker, saves each field in Chrome in the logged-in session, reloads each page, records the check date, then commits and pushes. Credentials stay in the browser, so the repository holds public text only.&lt;/p&gt;

&lt;p&gt;Before starting, confirm the fact at its source. Here that source was the Credly badge, issued 2026-10-30, which settled whether 10 or 9 was correct.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;By hand&lt;/th&gt;
&lt;th&gt;A notes file&lt;/th&gt;
&lt;th&gt;profile-sync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Whichever site is newest&lt;/td&gt;
&lt;td&gt;A notes file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bio.md&lt;/code&gt; in git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knows which sites are behind&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;If you update it&lt;/td&gt;
&lt;td&gt;🥇 Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catches a wrong count&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;🥇 Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads the live site&lt;/td&gt;
&lt;td&gt;When you look&lt;/td&gt;
&lt;td&gt;When you look&lt;/td&gt;
&lt;td&gt;🥇 3 sites by script, the rest by browser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;Start with Step 1 and Step 2, even with no automation. The first side-by-side read will show you what your profiles have been saying.&lt;/p&gt;

&lt;p&gt;Add the skill once you hold more profiles than you can check in one sitting. That number is small, and a new credential is a good day to start.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to keep one person's public profiles consistent across many sites. The key to the solution was treating a bio as source code, with one source file, a status table and a check that reads the live sites. The profile-sync results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 9 of the 10 sites are &lt;code&gt;Current&lt;/code&gt;, each confirmed on its live page&lt;/li&gt;
&lt;li&gt;🟢 The certification count is computed from a list, so the headline and the About can no longer disagree&lt;/li&gt;
&lt;li&gt;🟢 18 tests keep each failure beside a passing case&lt;/li&gt;
&lt;li&gt;❌ 5 sites said 9 certifications the day after a tenth was issued, and 3 said 20+ years&lt;/li&gt;
&lt;li&gt;⚠️ Google Skills stays &lt;code&gt;Blocked&lt;/code&gt;: it has no bio field and its public profile page returns an error&lt;/li&gt;
&lt;li&gt;⚠️ &lt;code&gt;check-live.py&lt;/code&gt; reads 3 sites and only the Short text, so the other sites still need a browser check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one person's ten profiles on 2026-10-31, in one Chrome session. Form limits were read from each site's own form on that date and can change. Two edits on the same day share one &lt;code&gt;version&lt;/code&gt;, so version equality alone does not show that a site has the latest text, and the live comparison covers that gap.&lt;/p&gt;

&lt;p&gt;The strategy for keeping a public profile consistent was validated with an incremental step by step approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync" rel="noopener noreferrer"&gt;profile-sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync/blob/main/.claude/skills/profile-sync/SKILL.md" rel="noopener noreferrer"&gt;The skill's own instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync/tree/main/articles/profile-sprawl" rel="noopener noreferrer"&gt;Evidence for this article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Profile Sprawl: Solving a Problem You Didn't Know You Had</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:35:26 +0000</pubDate>
      <link>https://dev.to/gde/profile-sprawl-solving-a-problem-you-didnt-know-you-had-19k5</link>
      <guid>https://dev.to/gde/profile-sprawl-solving-a-problem-you-didnt-know-you-had-19k5</guid>
      <description>&lt;p&gt;This article provides a step by step guide to keeping a public web presence consistent across many sites. An agent skill for Claude Code is built that treats a bio as source code: one file holds the facts, one table records where it has landed, and scripts read the live sites to confirm it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/profile-sync" rel="noopener noreferrer"&gt;https://github.com/xbill9/profile-sync&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every profile you own is a copy of your bio, and copies drift. The fix is the one software already uses: a single source, a status table and a check that reads production.&lt;/strong&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Profile Sprawl?
&lt;/h4&gt;

&lt;p&gt;A working developer collects profiles. LinkedIn, GitHub, Medium, dev.to, Substack, a certification wallet, a community portal, a program dashboard. Each one was accurate on the day you filled it in.&lt;/p&gt;

&lt;p&gt;Profile sprawl is what happens next. A new role or credential goes on the site you happened to have open, and the rest keep the old text. No site reports an error, because each one is internally fine. The inconsistency exists only across sites, and nobody reads them side by side except a recruiter, a conference organizer or a reviewer.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Bad Could It Be?
&lt;/h4&gt;

&lt;p&gt;One audit of ten profiles belonging to one person, read on 2026-10-31, the day after a new certification was issued:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Certifications&lt;/th&gt;
&lt;th&gt;Years of experience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn headline&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn About&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;over 30+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Developers profile&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credly&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advocu&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same LinkedIn profile disagreed with itself. The headline said 10 and the About said 9, and each line looked fine alone.&lt;/p&gt;

&lt;p&gt;The other four sites had a different problem. AWS Builder Center listed 0 credentials, dev.to held a tagline, GitHub held an unexplained string, and Substack was empty apart from the platform's default text.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A list of your public profiles and a browser where you are logged in to them&lt;/li&gt;
&lt;li&gt;Python 3, with no extra packages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; with the Chrome extension, or the patience to run the same steps by hand&lt;/li&gt;
&lt;li&gt;A git repository, which can be public as long as it holds public text only&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Write One Source File
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;bio.md&lt;/code&gt; holds everything a site could need, in plain Markdown with no site-specific syntax. Facts are lists. Text comes in lengths, written from those facts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-31&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Bio&lt;/span&gt;

&lt;span class="gu"&gt;## Headline&lt;/span&gt;
&lt;span class="gu"&gt;## Tagline&lt;/span&gt;
&lt;span class="gu"&gt;## Short&lt;/span&gt;
&lt;span class="gu"&gt;## Summary&lt;/span&gt;
&lt;span class="gu"&gt;## Long&lt;/span&gt;
&lt;span class="gu"&gt;## Roles&lt;/span&gt;
&lt;span class="gu"&gt;## Community&lt;/span&gt;
&lt;span class="gu"&gt;## Certifications&lt;/span&gt;
&lt;span class="gu"&gt;## Education&lt;/span&gt;
&lt;span class="gu"&gt;## Links&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lengths cover every form measured here: Short at 133 characters, Summary at 561 and Long at 735. A site gets its own text only when a limit forces it, and even then the shorter text becomes a section of the file. Per-site copies are how sprawl starts.&lt;/p&gt;

&lt;p&gt;The rule that keeps this working: a change starts in &lt;code&gt;bio.md&lt;/code&gt;, never on a site. If a site was edited directly, that change is copied back first.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Track Where It Has Landed
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;tracker.md&lt;/code&gt; is one table with one row per site. Status takes one of four values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;Synced version matches &lt;code&gt;bio.md&lt;/code&gt; and the live profile was checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behind&lt;/td&gt;
&lt;td&gt;The site has an older version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;td&gt;Nobody has compared this site to &lt;code&gt;bio.md&lt;/code&gt; yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Something outside your control stops the update, explained in Notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Text used&lt;/strong&gt; column records which sections a site takes. A change to Long reaches only the sites that use Long, and every other row moves to the new version with no edit.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Make the Computer Do the Counting
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;preflight.py&lt;/code&gt; compares the two files and exits 1 on any disagreement. It counts the &lt;code&gt;Certifications&lt;/code&gt; list and compares that count with every claim in the text, so a number in a headline can never drift from the list it summarizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/preflight.py
bio.md 2026-10-31, 10 sites: {'Behind': 0, 'Blocked': 1, 'Current': 9, 'Unverified': 0}
preflight OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also fails on a text longer than a site's form allows, a &lt;code&gt;Current&lt;/code&gt; row with no check date, and a status outside the four values. The repository is public, so it scans both files for email addresses and phone numbers too.&lt;/p&gt;

&lt;p&gt;Arithmetic belongs in code. A model or a person can miscount a list. A script compares 10 with 10.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Read the Live Sites
&lt;/h4&gt;

&lt;p&gt;A tracker row is a claim. &lt;code&gt;check-live.py&lt;/code&gt; fetches what a visitor gets from each site with a public source and compares it with &lt;code&gt;bio.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/check-live.py
  SKIP  LinkedIn: browser check
  OK    GitHub: live text matches Short
  SKIP  Medium: browser check
  OK    dev.to: live text matches Short
  OK    Substack: live text matches Short
  SKIP  AWS Builder Center: browser check
  SKIP  Google Developers profile: browser check
  SKIP  Google Cloud Skills Boost: browser check
  SKIP  Credly: browser check
  SKIP  Advocu: browser check
check-live OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A site marked &lt;code&gt;Current&lt;/code&gt; that differs fails the run. A check that can only pass proves nothing, so the same script was run against a stale value to confirm it exits 1.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Measure Every Form
&lt;/h4&gt;

&lt;p&gt;Each site has its own field, limit and save button. These are read from the forms themselves, and they live in one reference file so the next run starts with them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Short bio&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to&lt;/td&gt;
&lt;td&gt;Bio&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Builder Center&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credly&lt;/td&gt;
&lt;td&gt;Personal Summary&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Developers profile&lt;/td&gt;
&lt;td&gt;Bio&lt;/td&gt;
&lt;td&gt;750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;About&lt;/td&gt;
&lt;td&gt;2,600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A property of one site is evidence about that site only. The Long text at 735 characters fits 2 of these fields and fails the other 4, so the limits go in &lt;code&gt;biolib.py&lt;/code&gt; and &lt;code&gt;preflight.py&lt;/code&gt; enforces them before any browser opens.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Reading and Writing Are Different Routes
&lt;/h4&gt;

&lt;p&gt;dev.to has a good API, and a key reads the profile with no trouble. Profile fields are read-only there: &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;PATCH&lt;/code&gt; on &lt;code&gt;/api/users/me&lt;/code&gt; return 404, so the bio is saved through the settings page. Test both directions before planning a rollout around an API.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Compare Before Saving
&lt;/h4&gt;

&lt;p&gt;Browser automation can drop a character while reporting success, and a page can show a success message for the wrong text. So each save follows the same order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scroll to the field and confirm it has focus.&lt;/li&gt;
&lt;li&gt;Select the old text and type the new text.&lt;/li&gt;
&lt;li&gt;Compare the field with the exact payload from &lt;code&gt;bio.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read the position of the save button after scrolling.&lt;/li&gt;
&lt;li&gt;Save, reload and compare the live text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;payload.py&lt;/code&gt; supplies step 3. It prints the text for a site, or a small JavaScript function that compares a field with it. The text travels base64-encoded, which survives quotes, emoji and line breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 .claude/skills/profile-sync/scripts/payload.py Credly &amp;gt; /dev/null
Summary: 561 characters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🔎 Tip: Decline the Announcement
&lt;/h4&gt;

&lt;p&gt;Adding a certification on LinkedIn opens a prompt to post about it to your network. Updating a profile and announcing to it are separate decisions. The skill clicks Skip unless an announcement was requested.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Package It as a Skill
&lt;/h4&gt;

&lt;p&gt;The steps above become an agent skill, in the shape of &lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/profile-sync/
  SKILL.md               the workflow
  references/sites.md    route, field, limit and save button per site
  scripts/biolib.py      reads bio.md and tracker.md
  scripts/preflight.py   the two files agree
  scripts/check-live.py  the live sites agree
  scripts/payload.py     the exact text for a field
tests/test_preflight.py  one case per failure, with a passing case beside it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests are the guard on the guards. Each failure gets a case that must fail and a case that must pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m unittest discover -s tests
Ran 18 tests in 0.004s

OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The passing cases carry weight too. One holds a date, so a phone number pattern that matches &lt;code&gt;2026-10-31&lt;/code&gt; fails the suite before it can fail every row in the tracker.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Roll Out One Change
&lt;/h4&gt;

&lt;p&gt;The certification that started this run shows the whole loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Edit bio.md, set version to today&lt;/span&gt;
&lt;span class="c"&gt;# 2. Confirm the files agree&lt;/span&gt;
python3 .claude/skills/profile-sync/scripts/preflight.py
&lt;span class="c"&gt;# 3. In Claude Code, run /profile-sync to roll it out&lt;/span&gt;
&lt;span class="c"&gt;# 4. Confirm the live sites agree&lt;/span&gt;
python3 .claude/skills/profile-sync/scripts/check-live.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill works down the tracker, saves each field in Chrome in the logged-in session, reloads each page, records the check date, then commits and pushes. Credentials stay in the browser, so the repository holds public text only.&lt;/p&gt;

&lt;p&gt;Before starting, confirm the fact at its source. Here that source was the Credly badge, issued 2026-10-30, which settled whether 10 or 9 was correct.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;By hand&lt;/th&gt;
&lt;th&gt;A notes file&lt;/th&gt;
&lt;th&gt;profile-sync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;Whichever site is newest&lt;/td&gt;
&lt;td&gt;A notes file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bio.md&lt;/code&gt; in git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knows which sites are behind&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;If you update it&lt;/td&gt;
&lt;td&gt;🥇 Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catches a wrong count&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;🥇 Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reads the live site&lt;/td&gt;
&lt;td&gt;When you look&lt;/td&gt;
&lt;td&gt;When you look&lt;/td&gt;
&lt;td&gt;🥇 3 sites by script, the rest by browser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;Start with Step 1 and Step 2, even with no automation. The first side-by-side read will show you what your profiles have been saying.&lt;/p&gt;

&lt;p&gt;Add the skill once you hold more profiles than you can check in one sitting. That number is small, and a new credential is a good day to start.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to keep one person's public profiles consistent across many sites. The key to the solution was treating a bio as source code, with one source file, a status table and a check that reads the live sites. The profile-sync results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 9 of the 10 sites are &lt;code&gt;Current&lt;/code&gt;, each confirmed on its live page&lt;/li&gt;
&lt;li&gt;🟢 The certification count is computed from a list, so the headline and the About can no longer disagree&lt;/li&gt;
&lt;li&gt;🟢 18 tests keep each failure beside a passing case&lt;/li&gt;
&lt;li&gt;❌ 5 sites said 9 certifications the day after a tenth was issued, and 3 said 20+ years&lt;/li&gt;
&lt;li&gt;⚠️ Google Skills stays &lt;code&gt;Blocked&lt;/code&gt;: it has no bio field and its public profile page returns an error&lt;/li&gt;
&lt;li&gt;⚠️ &lt;code&gt;check-live.py&lt;/code&gt; reads 3 sites and only the Short text, so the other sites still need a browser check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one person's ten profiles on 2026-10-31, in one Chrome session. Form limits were read from each site's own form on that date and can change. Two edits on the same day share one &lt;code&gt;version&lt;/code&gt;, so version equality alone does not show that a site has the latest text, and the live comparison covers that gap.&lt;/p&gt;

&lt;p&gt;The strategy for keeping a public profile consistent was validated with an incremental step by step approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync" rel="noopener noreferrer"&gt;profile-sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync/blob/main/.claude/skills/profile-sync/SKILL.md" rel="noopener noreferrer"&gt;The skill's own instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/profile-sync/tree/main/articles/profile-sprawl" rel="noopener noreferrer"&gt;Evidence for this article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Agent Skills Drift: A Feedback Loop for Keeping One True</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sat, 31 Oct 2026 23:02:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/agent-skills-drift-a-feedback-loop-for-keeping-one-true-511</link>
      <guid>https://dev.to/aws-builders/agent-skills-drift-a-feedback-loop-for-keeping-one-true-511</guid>
      <description>&lt;p&gt;This article provides a step by step guide to finding drift in an agent skill and closing it with a repeatable feedback loop. The working example is publishing-kit, an agent skill for Claude Code, Codex and Antigravity that publishes one markdown article to five destinations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;https://github.com/xbill9/publishing-kit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A skill is a set of claims about a world that keeps moving. Every claim has a shelf life, so maintenance is a loop you run on a schedule.&lt;/strong&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Skill Drift?
&lt;/h4&gt;

&lt;p&gt;A skill is instructions plus scripts, and both describe something outside the skill: an editor, an API, a file format, or another script.&lt;/p&gt;

&lt;p&gt;Drift is the distance between what the skill says and what is true today. It grows without any error message, because the skill keeps loading and its checks keep passing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Why Does a Green Check Prove So Little?
&lt;/h4&gt;

&lt;p&gt;The kit runs a pre-flight against its own article. On the day of this audit that run exited 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 skills/publishing/scripts/preflight.py articles/publishing-kit-skill/devto-publishing-kit.md --live
all checks passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same day, a code review of the scripts returned 20 findings and running every generator by hand added 2 more. One of those unpublished live articles.&lt;/p&gt;

&lt;p&gt;The pre-flight walks a happy path. An article that never contains a &lt;code&gt;~~~&lt;/code&gt; block can never show that 8 files mishandle one.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 1 — Measure Install Drift
&lt;/h4&gt;

&lt;p&gt;Hosts install a skill as a snapshot, so the copy a session loads can lag the repository by weeks. Compare the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep '"version"' .claude-plugin/plugin.json
  "version": "0.32.0",
$ grep -A4 'publishing@publishing-kit' ~/.claude/plugins/installed_plugins.json | grep version
        "version": "0.28.0",
$ git rev-list --count a49983f..dc4f31f
89
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fix in those 89 commits existed in the repository and in no session. The whole Substack destination was among them.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Measure Version Drift
&lt;/h4&gt;

&lt;p&gt;A version string identifies a release. It says nothing about whether the content changed, and an updater that compares strings inherits that blind spot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git rev-list --count 631cf11..dc4f31f
29
$ git diff --stat 631cf11 dc4f31f -- skills .claude-plugin | tail -1
 16 files changed, 549 insertions(+), 26 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those 29 commits changed behavior under an unchanged version. Bump the version in the same session as the change, so the update has something to see.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Find Warnings That Should Be Guards
&lt;/h4&gt;

&lt;p&gt;A warning in a document depends on the next reader. The kit's notes recorded that &lt;code&gt;--update&lt;/code&gt; pushes the source's &lt;code&gt;published: false&lt;/code&gt; and takes a live article down. The script still did it and printed &lt;code&gt;updated&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m unittest tests.test_regressions.DevtoUpdate
.....
----------------------------------------------------------------------
Ran 5 tests in 0.002s

OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script now reads the listing first, keeps a live article live, and exits non-zero if the article leaves the published listing.&lt;/p&gt;

&lt;p&gt;Search the skill for words like "remember", "always" and "make sure". Each one is a candidate for a guard inside the script.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Test the Checks Themselves
&lt;/h4&gt;

&lt;p&gt;A check drifts when its logic stops matching what its name promises. The fact checker joined every number in an evidence file into one string of digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;     &lt;span class="c1"&gt;# "123.4", so a claim of 123.4 ms traced
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing whole numbers fixed that and failed the kit's own article on the next run. Its &lt;code&gt;320px&lt;/code&gt; had been traced to a file with no 320 in it.&lt;/p&gt;

&lt;p&gt;Give every check an input it must reject, and run that input. A check that has never failed tells you nothing about what it can catch.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Collapse Duplicated Logic
&lt;/h4&gt;

&lt;p&gt;Copies drift apart. At audit time 9 files carried their own logic for a code fence. One handled &lt;code&gt;~~~&lt;/code&gt;, and the other 8 treated it as prose.&lt;/p&gt;

&lt;p&gt;One script therefore saw a code block where the next saw paragraphs, and a fix in one file reached none of the others.&lt;/p&gt;

&lt;p&gt;One function now classifies fences, and the repository's rules forbid a second.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Date Every Claim About a Destination
&lt;/h4&gt;

&lt;p&gt;Destinations change without notice, so an undated claim about one has no expiry. The kit labels each behavioral claim with the day it was taken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEASURED 2026-09-01: PUT {"article": {"published": true}} returns 200 and changes nothing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill carries 143 of those labels. A date tells a reader how far to trust the claim and tells a reviewer where to look first.&lt;/p&gt;

&lt;p&gt;When a claim reverses, keep it visible and write a &lt;strong&gt;Correction&lt;/strong&gt; beside it with the new measurement. A quiet edit removes the record that the old belief was ever held, and the next session can repeat it.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Feedback Loop
&lt;/h4&gt;

&lt;p&gt;Six kinds of drift, one loop. Each pass turns one surprise into something the next session cannot repeat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─► 1 OBSERVE   a real run, a failed publish, a review, a user correction
  │       │
  │   2 MEASURE   reproduce it and archive the output, dated
  │       │
  │   3 ENCODE    guard in the script, test with the failing input,
  │       │       Correction in the reference, one source of truth
  │       │
  │   4 VERIFY    the test fails on the old code and passes on the new
  │       │
  │   5 RELEASE   bump the version, update the install, compare served bytes
  │       │
  └───────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 sets the order of preference. Code beats a test, a test beats a document, and a document beats memory, because each one depends less on someone paying attention.&lt;/p&gt;

&lt;p&gt;Step 4 keeps the loop true. The regression tests here failed 39 of 44 against the scripts as they stood, and all 44 pass now. A test that passes on broken code proves nothing.&lt;/p&gt;

&lt;p&gt;Step 5 is the step most often skipped, and skipping it produced the 89 commit gap from Step 1.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Drift&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Detect with&lt;/th&gt;
&lt;th&gt;Close with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;Sessions run old code&lt;/td&gt;
&lt;td&gt;Installed version against the repo&lt;/td&gt;
&lt;td&gt;Update, then compare content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;Updater sees no change&lt;/td&gt;
&lt;td&gt;Commits since the last bump&lt;/td&gt;
&lt;td&gt;Bump with the change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;td&gt;A warning nobody enforces&lt;/td&gt;
&lt;td&gt;Search for "remember" and "always"&lt;/td&gt;
&lt;td&gt;A guard in the script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;Green over a real defect&lt;/td&gt;
&lt;td&gt;An input the check must reject&lt;/td&gt;
&lt;td&gt;A regression test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplication&lt;/td&gt;
&lt;td&gt;Copies disagree&lt;/td&gt;
&lt;td&gt;Search for the repeated test&lt;/td&gt;
&lt;td&gt;One shared function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination&lt;/td&gt;
&lt;td&gt;A claim goes stale&lt;/td&gt;
&lt;td&gt;Oldest dated claims first&lt;/td&gt;
&lt;td&gt;Re-measure, write a Correction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  How Often Should the Loop Run?
&lt;/h4&gt;

&lt;p&gt;Run the loop on three triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;Every failed or awkward run.&lt;/strong&gt; A retry is the cheapest signal available, so write it down while it is fresh.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Every release.&lt;/strong&gt; Run the tests, run the live pre-flight, bump the version and update the install.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;On a calendar.&lt;/strong&gt; A review reads the code cold and catches what a passing run cannot. It also finds claims that no recent run touched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give the calendar review to a reader with no stake in the code. Hand it the code and withhold your conclusions, so it can disagree.&lt;/p&gt;




&lt;h4&gt;
  
  
  So, Which One First?
&lt;/h4&gt;

&lt;p&gt;Start with install drift. It takes one command, and until it is closed every other fix stays out of reach of your sessions.&lt;/p&gt;

&lt;p&gt;Then add one rejecting input to the check you trust most. If that check accepts bad input, you have learned what your green runs are worth.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to find and close drift in an agent skill. The key to the solution was treating every claim as perishable and every surprise as input to a loop. The publishing-kit results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 22 defects surfaced on a day when the pre-flight passed&lt;/li&gt;
&lt;li&gt;🟢 44 regression tests now keep the failing inputs, each beside a passing case&lt;/li&gt;
&lt;li&gt;❌ The installed skill was 89 commits behind the repository&lt;/li&gt;
&lt;li&gt;⚠️ The &lt;code&gt;--update&lt;/code&gt; guard is tested against a fake API, and no live article has gone through it yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one skill, one repository and one audit on 2026-10-31. Counts come from that repository's git history, and the defects come from one code review and one manual run, so they are a lower bound on what remains.&lt;/p&gt;

&lt;p&gt;With this approach a skill stays true for as long as someone keeps running the loop. The full source and every measurement are in the repository.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit/blob/main/skills/publishing/SKILL.md" rel="noopener noreferrer"&gt;The skill's own instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit/tree/main/articles/skill-drift" rel="noopener noreferrer"&gt;Evidence for this article&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Agent Skills Drift: A Feedback Loop for Keeping One True</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sat, 31 Oct 2026 23:02:02 +0000</pubDate>
      <link>https://dev.to/gde/agent-skills-drift-a-feedback-loop-for-keeping-one-true-2il7</link>
      <guid>https://dev.to/gde/agent-skills-drift-a-feedback-loop-for-keeping-one-true-2il7</guid>
      <description>&lt;p&gt;This article provides a step by step guide to finding drift in an agent skill and closing it with a repeatable feedback loop. The working example is publishing-kit, an agent skill for Claude Code, Codex and Antigravity that publishes one markdown article to five destinations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;https://github.com/xbill9/publishing-kit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A skill is a set of claims about a world that keeps moving. Every claim has a shelf life, so maintenance is a loop you run on a schedule.&lt;/strong&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Skill Drift?
&lt;/h4&gt;

&lt;p&gt;A skill is instructions plus scripts, and both describe something outside the skill: an editor, an API, a file format, or another script.&lt;/p&gt;

&lt;p&gt;Drift is the distance between what the skill says and what is true today. It grows without any error message, because the skill keeps loading and its checks keep passing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Why Does a Green Check Prove So Little?
&lt;/h4&gt;

&lt;p&gt;The kit runs a pre-flight against its own article. On the day of this audit that run exited 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 skills/publishing/scripts/preflight.py articles/publishing-kit-skill/devto-publishing-kit.md --live
all checks passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same day, a code review of the scripts returned 20 findings and running every generator by hand added 2 more. One of those unpublished live articles.&lt;/p&gt;

&lt;p&gt;The pre-flight walks a happy path. An article that never contains a &lt;code&gt;~~~&lt;/code&gt; block can never show that 8 files mishandle one.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 1 — Measure Install Drift
&lt;/h4&gt;

&lt;p&gt;Hosts install a skill as a snapshot, so the copy a session loads can lag the repository by weeks. Compare the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep '"version"' .claude-plugin/plugin.json
  "version": "0.32.0",
$ grep -A4 'publishing@publishing-kit' ~/.claude/plugins/installed_plugins.json | grep version
        "version": "0.28.0",
$ git rev-list --count a49983f..dc4f31f
89
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fix in those 89 commits existed in the repository and in no session. The whole Substack destination was among them.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Measure Version Drift
&lt;/h4&gt;

&lt;p&gt;A version string identifies a release. It says nothing about whether the content changed, and an updater that compares strings inherits that blind spot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git rev-list --count 631cf11..dc4f31f
29
$ git diff --stat 631cf11 dc4f31f -- skills .claude-plugin | tail -1
 16 files changed, 549 insertions(+), 26 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those 29 commits changed behavior under an unchanged version. Bump the version in the same session as the change, so the update has something to see.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Find Warnings That Should Be Guards
&lt;/h4&gt;

&lt;p&gt;A warning in a document depends on the next reader. The kit's notes recorded that &lt;code&gt;--update&lt;/code&gt; pushes the source's &lt;code&gt;published: false&lt;/code&gt; and takes a live article down. The script still did it and printed &lt;code&gt;updated&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 -m unittest tests.test_regressions.DevtoUpdate
.....
----------------------------------------------------------------------
Ran 5 tests in 0.002s

OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script now reads the listing first, keeps a live article live, and exits non-zero if the article leaves the published listing.&lt;/p&gt;

&lt;p&gt;Search the skill for words like "remember", "always" and "make sure". Each one is a candidate for a guard inside the script.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Test the Checks Themselves
&lt;/h4&gt;

&lt;p&gt;A check drifts when its logic stops matching what its name promises. The fact checker joined every number in an evidence file into one string of digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;     &lt;span class="c1"&gt;# "123.4", so a claim of 123.4 ms traced
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing whole numbers fixed that and failed the kit's own article on the next run. Its &lt;code&gt;320px&lt;/code&gt; had been traced to a file with no 320 in it.&lt;/p&gt;

&lt;p&gt;Give every check an input it must reject, and run that input. A check that has never failed tells you nothing about what it can catch.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Collapse Duplicated Logic
&lt;/h4&gt;

&lt;p&gt;Copies drift apart. At audit time 9 files carried their own logic for a code fence. One handled &lt;code&gt;~~~&lt;/code&gt;, and the other 8 treated it as prose.&lt;/p&gt;

&lt;p&gt;One script therefore saw a code block where the next saw paragraphs, and a fix in one file reached none of the others.&lt;/p&gt;

&lt;p&gt;One function now classifies fences, and the repository's rules forbid a second.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Date Every Claim About a Destination
&lt;/h4&gt;

&lt;p&gt;Destinations change without notice, so an undated claim about one has no expiry. The kit labels each behavioral claim with the day it was taken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MEASURED 2026-09-01: PUT {"article": {"published": true}} returns 200 and changes nothing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill carries 143 of those labels. A date tells a reader how far to trust the claim and tells a reviewer where to look first.&lt;/p&gt;

&lt;p&gt;When a claim reverses, keep it visible and write a &lt;strong&gt;Correction&lt;/strong&gt; beside it with the new measurement. A quiet edit removes the record that the old belief was ever held, and the next session can repeat it.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Feedback Loop
&lt;/h4&gt;

&lt;p&gt;Six kinds of drift, one loop. Each pass turns one surprise into something the next session cannot repeat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─► 1 OBSERVE   a real run, a failed publish, a review, a user correction
  │       │
  │   2 MEASURE   reproduce it and archive the output, dated
  │       │
  │   3 ENCODE    guard in the script, test with the failing input,
  │       │       Correction in the reference, one source of truth
  │       │
  │   4 VERIFY    the test fails on the old code and passes on the new
  │       │
  │   5 RELEASE   bump the version, update the install, compare served bytes
  │       │
  └───────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 sets the order of preference. Code beats a test, a test beats a document, and a document beats memory, because each one depends less on someone paying attention.&lt;/p&gt;

&lt;p&gt;Step 4 keeps the loop true. The regression tests here failed 39 of 44 against the scripts as they stood, and all 44 pass now. A test that passes on broken code proves nothing.&lt;/p&gt;

&lt;p&gt;Step 5 is the step most often skipped, and skipping it produced the 89 commit gap from Step 1.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Drift&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Detect with&lt;/th&gt;
&lt;th&gt;Close with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;Sessions run old code&lt;/td&gt;
&lt;td&gt;Installed version against the repo&lt;/td&gt;
&lt;td&gt;Update, then compare content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;Updater sees no change&lt;/td&gt;
&lt;td&gt;Commits since the last bump&lt;/td&gt;
&lt;td&gt;Bump with the change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;td&gt;A warning nobody enforces&lt;/td&gt;
&lt;td&gt;Search for "remember" and "always"&lt;/td&gt;
&lt;td&gt;A guard in the script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check&lt;/td&gt;
&lt;td&gt;Green over a real defect&lt;/td&gt;
&lt;td&gt;An input the check must reject&lt;/td&gt;
&lt;td&gt;A regression test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplication&lt;/td&gt;
&lt;td&gt;Copies disagree&lt;/td&gt;
&lt;td&gt;Search for the repeated test&lt;/td&gt;
&lt;td&gt;One shared function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination&lt;/td&gt;
&lt;td&gt;A claim goes stale&lt;/td&gt;
&lt;td&gt;Oldest dated claims first&lt;/td&gt;
&lt;td&gt;Re-measure, write a Correction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  How Often Should the Loop Run?
&lt;/h4&gt;

&lt;p&gt;Run the loop on three triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;Every failed or awkward run.&lt;/strong&gt; A retry is the cheapest signal available, so write it down while it is fresh.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Every release.&lt;/strong&gt; Run the tests, run the live pre-flight, bump the version and update the install.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;On a calendar.&lt;/strong&gt; A review reads the code cold and catches what a passing run cannot. It also finds claims that no recent run touched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give the calendar review to a reader with no stake in the code. Hand it the code and withhold your conclusions, so it can disagree.&lt;/p&gt;




&lt;h4&gt;
  
  
  So, Which One First?
&lt;/h4&gt;

&lt;p&gt;Start with install drift. It takes one command, and until it is closed every other fix stays out of reach of your sessions.&lt;/p&gt;

&lt;p&gt;Then add one rejecting input to the check you trust most. If that check accepts bad input, you have learned what your green runs are worth.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to find and close drift in an agent skill. The key to the solution was treating every claim as perishable and every surprise as input to a loop. The publishing-kit results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 22 defects surfaced on a day when the pre-flight passed&lt;/li&gt;
&lt;li&gt;🟢 44 regression tests now keep the failing inputs, each beside a passing case&lt;/li&gt;
&lt;li&gt;❌ The installed skill was 89 commits behind the repository&lt;/li&gt;
&lt;li&gt;⚠️ The &lt;code&gt;--update&lt;/code&gt; guard is tested against a fake API, and no live article has gone through it yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one skill, one repository and one audit on 2026-10-31. Counts come from that repository's git history, and the defects come from one code review and one manual run, so they are a lower bound on what remains.&lt;/p&gt;

&lt;p&gt;With this approach a skill stays true for as long as someone keeps running the loop. The full source and every measurement are in the repository.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit/blob/main/skills/publishing/SKILL.md" rel="noopener noreferrer"&gt;The skill's own instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/publishing-kit/tree/main/articles/skill-drift" rel="noopener noreferrer"&gt;Evidence for this article&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Desert Hours: Mapping When Transit Disappears With Amazon Location Service</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sat, 31 Oct 2026 19:05:23 +0000</pubDate>
      <link>https://dev.to/aws-builders/desert-hours-mapping-when-transit-disappears-with-amazon-location-service-26m6</link>
      <guid>https://dev.to/aws-builders/desert-hours-mapping-when-transit-disappears-with-amazon-location-service-26m6</guid>
      <description>&lt;p&gt;This article provides a step by step guide to building an hourly transit reach map with Amazon Location Service. Walking areas come from Pedestrian isolines, journey times come from Transit routing, and the finished map runs as static files on Amazon S3 and Amazon CloudFront.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/xbill9/desert-hours" rel="noopener noreferrer"&gt;https://github.com/xbill9/desert-hours&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live map: &lt;a href="https://d35zkoavyfqlat.cloudfront.net/" rel="noopener noreferrer"&gt;https://d35zkoavyfqlat.cloudfront.net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo video: &lt;a href="https://youtu.be/jfC_d6nq2EM" rel="noopener noreferrer"&gt;https://youtu.be/jfC_d6nq2EM&lt;/a&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is a Transit Desert?
&lt;/h4&gt;

&lt;p&gt;A transit desert is a place where demand for transit outruns the service on offer. Published maps usually score that gap once per area. Service changes by the hour, so this project scores it for every hour of a weekday.&lt;/p&gt;

&lt;p&gt;The study area is Mercer County, New Jersey. It holds Trenton, Princeton and a ring of suburbs, and 16,114 of its 144,114 households have no car.&lt;/p&gt;




&lt;h4&gt;
  
  
  What You Can Explore
&lt;/h4&gt;

&lt;p&gt;Drag the clock from 5 AM to 4 AM and the blue walkable service area shrinks across the county. The running total shows how many car-free households are out of reach at that hour.&lt;/p&gt;

&lt;p&gt;Click a census tract for its day: car-free households, the share of housing that can reach service, departures by hour, and transit times to three destinations at 8 AM, 5 PM, 11 PM and 2 AM beside the driving time.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with access to Amazon Location Service&lt;/li&gt;
&lt;li&gt;The AWS CLI, version 2&lt;/li&gt;
&lt;li&gt;Python 3 with &lt;code&gt;pyshp&lt;/code&gt; and &lt;code&gt;shapely&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A GTFS feed for your area, a car ownership table and Census block boundaries&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Gather the Data
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NJ TRANSIT GTFS&lt;/td&gt;
&lt;td&gt;Scheduled bus and rail departures&lt;/td&gt;
&lt;td&gt;Departures per stop, per hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Census ACS table B08201&lt;/td&gt;
&lt;td&gt;Households by vehicles available&lt;/td&gt;
&lt;td&gt;Car-free households per tract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020 Census blocks&lt;/td&gt;
&lt;td&gt;Housing units per block&lt;/td&gt;
&lt;td&gt;Where households sit in a tract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mercer County route layer&lt;/td&gt;
&lt;td&gt;Route 130 Connection stops&lt;/td&gt;
&lt;td&gt;A county service outside the feed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every published figure is a county, municipal or tract total, so the map holds no personal data.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Count Departures by Hour
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;pipeline/build_data.py&lt;/code&gt; picks one Wednesday in the feed and counts departures per stop in each hour. It keeps stops inside the county and within 1.5 km of it, because people cross county lines to catch a bus. That leaves 1,012 stops.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Ask Amazon Location for a 10-Minute Walk
&lt;/h4&gt;

&lt;p&gt;Each stop gets a Pedestrian isoline along the street network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws geo-routes calculate-isolines &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--origin&lt;/span&gt; &lt;span class="nt"&gt;-74&lt;/span&gt;.7548 40.2186 &lt;span class="nt"&gt;--travel-mode&lt;/span&gt; Pedestrian &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--thresholds&lt;/span&gt; &lt;span class="s1"&gt;'{"Time":[600]}'&lt;/span&gt; &lt;span class="nt"&gt;--isoline-geometry-format&lt;/span&gt; Simple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape depends on the streets around the stop. Both of these reach about 560 m at their farthest point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;10-minute walk area&lt;/th&gt;
&lt;th&gt;Farthest point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trenton station&lt;/td&gt;
&lt;td&gt;0.33 km²&lt;/td&gt;
&lt;td&gt;562 m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hamilton suburb&lt;/td&gt;
&lt;td&gt;0.09 km²&lt;/td&gt;
&lt;td&gt;560 m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The median across all 1,012 stops is 0.26 km², against 0.50 km² for a 400 m circle.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Place Households Where Housing Is
&lt;/h4&gt;

&lt;p&gt;A tract includes parks, rail yards and farmland. Spreading its households evenly across that area counts people who live nowhere near those spaces.&lt;/p&gt;

&lt;p&gt;Census blocks carry a housing unit count, so each tract's car-free households are shared across its blocks by housing units. A block counts as served in an hour when its interior point falls inside that hour's service area.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Build the Hourly Service Area
&lt;/h4&gt;

&lt;p&gt;For each hour, the pipeline joins the walking areas of the stops with at least one departure in that hour. Testing every block against that shape gives the hourly total.&lt;/p&gt;

&lt;p&gt;Code does the counting and the page quotes it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hour&lt;/th&gt;
&lt;th&gt;Car-free households out of reach&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 AM&lt;/td&gt;
&lt;td&gt;6,388&lt;/td&gt;
&lt;td&gt;39.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 PM&lt;/td&gt;
&lt;td&gt;8,129&lt;/td&gt;
&lt;td&gt;50.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 AM&lt;/td&gt;
&lt;td&gt;14,095&lt;/td&gt;
&lt;td&gt;87.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  Step 5 — Ask for Real Journeys
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;CalculateIsolines&lt;/code&gt; covers walking and driving modes. &lt;code&gt;CalculateRoutes&lt;/code&gt; adds a &lt;code&gt;Transit&lt;/code&gt; mode with timetable departures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws geo-routes calculate-routes &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--origin&lt;/span&gt; &lt;span class="nt"&gt;-74&lt;/span&gt;.7548 40.2186 &lt;span class="nt"&gt;--destination&lt;/span&gt; &lt;span class="nt"&gt;-74&lt;/span&gt;.6672 40.3495 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--travel-mode&lt;/span&gt; Transit &lt;span class="nt"&gt;--departure-time&lt;/span&gt; 2026-11-04T02:00:00-05:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request for 2 AM from Trenton to Princeton returns a timed journey:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Leg&lt;/th&gt;
&lt;th&gt;Departs&lt;/th&gt;
&lt;th&gt;Arrives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Walk to Trenton Transit Center&lt;/td&gt;
&lt;td&gt;3:56 AM&lt;/td&gt;
&lt;td&gt;4:11 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Train to Princeton Junction&lt;/td&gt;
&lt;td&gt;4:11 AM&lt;/td&gt;
&lt;td&gt;4:24 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Train to Princeton&lt;/td&gt;
&lt;td&gt;4:40 AM&lt;/td&gt;
&lt;td&gt;4:45 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Walk to the destination&lt;/td&gt;
&lt;td&gt;4:45 AM&lt;/td&gt;
&lt;td&gt;5:09 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trip takes 3 hours 9 minutes from the moment you wanted to leave, and most of that is waiting for the first train. The pipeline stores that full wait.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Ship It as Static Files
&lt;/h4&gt;

&lt;p&gt;The results are GeoJSON and HTML in a private S3 bucket behind CloudFront. Precomputing everything leaves the page with one Amazon Location key, limited to map actions and the site's domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; desert-hours &lt;span class="nt"&gt;--template-file&lt;/span&gt; deploy/stack.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The findings sit in the page's HTML, so they read without JavaScript. MapLibre draws the tracts and service areas over an Amazon Location base map.&lt;/p&gt;




&lt;h4&gt;
  
  
  One Tract, One Day
&lt;/h4&gt;

&lt;p&gt;Trenton Tract 1 is a strip along the Delaware River with about 445 car-free households of 1,332, with a margin of ±227.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Housing within a 10-minute walk of service, 8 AM&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Housing within a 10-minute walk of service, 11 PM&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drive to Capital Health Regional Medical Center&lt;/td&gt;
&lt;td&gt;11 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transit to the hospital, leaving at 8 AM&lt;/td&gt;
&lt;td&gt;52 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transit to the hospital, leaving at 2 AM&lt;/td&gt;
&lt;td&gt;3 hours 29 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Walking area&lt;/th&gt;
&lt;th&gt;Time of day&lt;/th&gt;
&lt;th&gt;Journey times&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;400 m circles&lt;/td&gt;
&lt;td&gt;Same everywhere&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static desert score&lt;/td&gt;
&lt;td&gt;Varies by method&lt;/td&gt;
&lt;td&gt;One score&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desert Hours&lt;/td&gt;
&lt;td&gt;Street network&lt;/td&gt;
&lt;td&gt;Every hour&lt;/td&gt;
&lt;td&gt;Timetable by Amazon Location&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  What Did It Cost?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;List price&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Isolines&lt;/td&gt;
&lt;td&gt;1,012&lt;/td&gt;
&lt;td&gt;$1.50 per thousand&lt;/td&gt;
&lt;td&gt;$1.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Journeys&lt;/td&gt;
&lt;td&gt;1,260&lt;/td&gt;
&lt;td&gt;$0.50 per thousand&lt;/td&gt;
&lt;td&gt;$0.63&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Totals are arithmetic from AWS Price List rates. Results are cached, so a rebuild makes no new calls.&lt;/p&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;Use isolines when reach depends on the street network, which it does in any suburb. Use Transit routing for the stories a map shape cannot tell, such as how long a 2 AM trip takes. Keep both in a batch pipeline, and the live page never spends money on routing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to map how transit reach changes through a weekday. The key to the solution was Amazon Location Pedestrian isolines joined by hour and tested against Census blocks. The Mercer County results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 6,388 car-free households are out of walking reach at 8 AM, 39.6% of the county's total&lt;/li&gt;
&lt;li&gt;❌ 14,095 are out of reach at 3 AM, 87.5% of the total&lt;/li&gt;
&lt;li&gt;🟢 Street-based walking areas separate a city grid from a suburb at the same distance&lt;/li&gt;
&lt;li&gt;⚠️ Trenton Tract 1 falls from 73% of housing in reach at 8 AM to 0% at 11 PM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: scheduled service for one weekday, which may be incomplete, inaccurate or out of date. TigerTransit, the Princeton municipal shuttle, the Z-Line and on-demand services are not included, so reach is understated where they run. Route 130 Connection stops use a September 2023 timetable and count in every hour with a departure. Car-free households are assumed to be spread like housing within a tract, and tract estimates carry wide margins. Journey times come from one point per tract. Schedule data comes from NJ TRANSIT's published GTFS through the Mobility Database archive, and this project is independent of NJ TRANSIT.&lt;/p&gt;

&lt;p&gt;The strategy for using Amazon Location Service for an hourly transit map was validated with an incremental step by step approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://d35zkoavyfqlat.cloudfront.net/" rel="noopener noreferrer"&gt;Live map&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/desert-hours" rel="noopener noreferrer"&gt;Code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/jfC_d6nq2EM" rel="noopener noreferrer"&gt;Demo video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/location/latest/APIReference/API_CalculateIsolines.html" rel="noopener noreferrer"&gt;CalculateIsolines API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/location/latest/APIReference/API_CalculateRoutes.html" rel="noopener noreferrer"&gt;CalculateRoutes API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://transitdeserts.info/about.html" rel="noopener noreferrer"&gt;Transit Deserts method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>maps</category>
      <category>python</category>
      <category>opendata</category>
    </item>
    <item>
      <title>Four GPUs, One Quantization Ladder: Gemma 4 E2B From a 4 GB Laptop to an NVIDIA L4</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:05:27 +0000</pubDate>
      <link>https://dev.to/gde/four-gpus-one-quantization-ladder-gemma-4-e2b-from-a-4-gb-laptop-to-an-nvidia-l4-4olp</link>
      <guid>https://dev.to/gde/four-gpus-one-quantization-ladder-gemma-4-e2b-from-a-4-gb-laptop-to-an-nvidia-l4-4olp</guid>
      <description>&lt;p&gt;This article provides a step by step guide to running Gemma 4 E2B's quantization ladder, BF16 down to Q2_K, on four NVIDIA GPUs with the same nine GGUF files and the same llama.cpp build, and comparing the four cards question by question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma-graveyard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;first article&lt;/a&gt; asked every rung of the ladder the same 250 GSM8K questions on a Tesla T4. The &lt;a href="https://dev.to/gde/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-4b90"&gt;second&lt;/a&gt; repeated it on an NVIDIA L4 in AWS. This one adds two consumer cards: a GTX 1650 Ti laptop GPU with 4 GB and an RTX 3050 desktop card with 8 GB. All four run llama.cpp &lt;code&gt;b11699&lt;/code&gt; on files with the same sha256, and each card gives identical text when BF16 is run twice.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Results in Brief
&lt;/h4&gt;

&lt;p&gt;The ladder keeps its shape on all four GPUs. Against each card's own BF16, F16 to Q5_K_M lose 1 to 8 answers, Q4_K_M and Q4_0 lose 9 to 19, Q3_K_M loses 42 to 49, and Q2_K gets 3 to 6 right out of 250.&lt;/p&gt;

&lt;p&gt;The counts agree; the questions move. Any two GPUs grade 2 to 4 questions differently at BF16 and 12 to 22 at Q4_0. No question is lost at F16 or Q8_0 on all four cards, while 216 are lost at Q2_K on all four. 218 questions die at some rung on every GPU, and 147 of them die first at the same rung on every GPU.&lt;/p&gt;

&lt;p&gt;The 4 GB laptop runs every quantized rung entirely on its GPU. BF16 and F16 do not fit, so llama.cpp splits them between GPU and CPU, and they decode at 9.2 and 9.6 tokens per second against 45.9 for Q8_0 on the same card.&lt;/p&gt;

&lt;p&gt;The cover lays the four GPUs' grades over each other. One row per rung, BF16 at the top and Q2_K at the bottom, one cell per question: marigold where all four GPUs answer right, grey where all four answer wrong, violet where they disagree. &lt;code&gt;docs/make_cover_gpu4.py&lt;/code&gt; draws it from the committed rows.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Changes and What Stays
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tesla T4&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;th&gt;GTX 1650 Ti&lt;/th&gt;
&lt;th&gt;RTX 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where&lt;/td&gt;
&lt;td&gt;GCE &lt;code&gt;n1-standard-2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;AWS &lt;code&gt;g6.xlarge&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;laptop&lt;/td&gt;
&lt;td&gt;desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory&lt;/td&gt;
&lt;td&gt;15,360 MiB&lt;/td&gt;
&lt;td&gt;23,034 MiB&lt;/td&gt;
&lt;td&gt;4,096 MiB&lt;/td&gt;
&lt;td&gt;8,192 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute capability&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host CPU&lt;/td&gt;
&lt;td&gt;2 vCPU&lt;/td&gt;
&lt;td&gt;AMD EPYC 7R13&lt;/td&gt;
&lt;td&gt;Intel i7-10750H&lt;/td&gt;
&lt;td&gt;Intel i7-5820K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver&lt;/td&gt;
&lt;td&gt;615.71.10&lt;/td&gt;
&lt;td&gt;595.112.07&lt;/td&gt;
&lt;td&gt;615.71.10&lt;/td&gt;
&lt;td&gt;615.78.08&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same on all four: llama.cpp &lt;code&gt;b11699&lt;/code&gt; with &lt;code&gt;GGML_CUDA=ON&lt;/code&gt;, built for each card's own compute capability; the nine GGUFs and Google's QAT Q4_0 file, byte for byte; the 250 questions in &lt;code&gt;data/heldout.jsonl&lt;/code&gt;; temperature 0, one request at a time, thinking off, 1,024 max tokens, &lt;code&gt;llama-server -c 4096 -np 1 --seed 0&lt;/code&gt;. Every rung runs with &lt;code&gt;-ngl 99&lt;/code&gt; except BF16 and F16 on the GTX 1650 (Step 4).&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An NVIDIA GPU with at least 4 GB and a CUDA toolkit&lt;/li&gt;
&lt;li&gt;About 58 GB of free disk for the checkpoint, the nine GGUFs and the QAT file&lt;/li&gt;
&lt;li&gt;A clone: &lt;code&gt;git clone https://github.com/xbill9/gemma-graveyard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The T4 run's committed results in &lt;code&gt;runs/2026-10-27-ladder/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Build llama.cpp at the Same Tag
&lt;/h4&gt;

&lt;p&gt;On the RTX 3050:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git -C ~/llama.cpp worktree add --detach /android/graveyard/llama.cpp b11699
cd /android/graveyard/llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=86 -DCMAKE_BUILD_TYPE=Release
cmake --build build -j12 --target llama-server llama-quantize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one build flag that changes per card is &lt;code&gt;CMAKE_CUDA_ARCHITECTURES&lt;/code&gt;: 75 for the T4 and the GTX 1650, 89 for the L4, 86 for the RTX 3050.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nvidia-smi --query-gpu=name,memory.total,compute_cap,driver_version --format=csv
name, memory.total [MiB], compute_cap, driver_version
NVIDIA GeForce RTX 3050 OEM, 8192 MiB, 8.6, 615.78.08
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔎 &lt;strong&gt;Tip: keep the pinned build in its own worktree.&lt;/strong&gt; The desktop already had a newer llama.cpp checkout (&lt;code&gt;b11983&lt;/code&gt;). &lt;code&gt;git worktree add --detach &amp;lt;dir&amp;gt; b11699&lt;/code&gt; gives a second working copy at the tag from the same clone, so the measured build and the everyday build sit side by side.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Rebuild the Ladder and Check It Against the T4
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ladder.sh&lt;/code&gt; converts the checkpoint to BF16 and F16 and quantizes seven rungs from BF16. Each run script then compares every file's size and sha256 with the T4's &lt;code&gt;ladder.tsv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;diff &amp;lt;(cut -f1,3,4 runs/2026-10-27-ladder/ladder.tsv) &amp;lt;(cut -f1,3,4 runs/2026-10-29-3050/ladder.tsv) &amp;amp;&amp;amp; echo "ladder matches T4 sha256"
ladder matches T4 sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GTX 1650 and the RTX 3050 both print the same line, and Google's QAT Q4_0 file matches too (sha256 &lt;code&gt;fa401b55…&lt;/code&gt;). Across the four machines, the GGUF bytes are identical; the card, its driver, its CUDA version and the kernels built for it are what differ.&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: the converter writes the checkpoint directory into the GGUF.&lt;/strong&gt; &lt;code&gt;convert_hf_to_gguf.py&lt;/code&gt; copies the model card and takes the model's name from the directory it reads. A checkpoint taken straight from the Hugging Face cache snapshot has neither the &lt;code&gt;README.md&lt;/code&gt; nor the name &lt;code&gt;gemma-4-E2B-it&lt;/code&gt;, and gives different bytes. Download with &lt;code&gt;hf download google/gemma-4-E2B-it --local-dir &amp;lt;dir&amp;gt;/gemma-4-E2B-it&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Prove Each Card Repeats Itself
&lt;/h4&gt;

&lt;p&gt;BF16 on 50 questions, twice, on every GPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py repeat runs/2026-10-29-3050/determinism-1 runs/2026-10-29-3050/determinism-2 BF16
{"rung": "BF16", "n": 50, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 46, "right_b": 46}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The T4, the L4 and the GTX 1650 give the same result: 0 mismatched characters and 46 of 50 right both times. Each card on its own is deterministic, so a difference between two cards comes from the GPU and its CUDA stack.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Fit the 16-bit Rungs on 4 GB
&lt;/h4&gt;

&lt;p&gt;BF16 and F16 need a 4,384.87 MiB GPU buffer for their 36 layers (from the T4's load log), and the GTX 1650 has 4,096 MiB. With &lt;code&gt;-ngl 99&lt;/code&gt; the load fails. &lt;code&gt;graveyard.py run --ngl auto&lt;/code&gt; leaves &lt;code&gt;-ngl&lt;/code&gt; off, and llama.cpp's fit step places what it can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;common_params_fit_impl:   - CUDA0 (NVIDIA GeForce GTX 1650 Ti with Max-Q Design): 14 layers,   2546 MiB used,   1125 MiB free
load_tensors: offloaded 14/36 layers to GPU
load_tensors:        CUDA0 model buffer size =  2407.92 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every other rung fits whole. Q8_0 on the same card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_tensors: offloaded 36/36 layers to GPU
load_tensors:        CUDA0 model buffer size =  2342.30 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split changes speed and leaves the grades in line with the other GPUs: the GTX 1650's BF16 scores 229, the same as the T4 and the L4, and grades 2 to 4 questions differently from each of them. The 8 GB RTX 3050 holds BF16 whole at 4,651 MiB.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run the Ladder on Each Card and Score It
&lt;/h4&gt;

&lt;p&gt;Each run script runs the nine rungs and QAT Q4_0, then scores every rung against that card's own BF16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-29-3050 BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M Q2_K --json graveyard-ladder.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right answers, out of 250:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;T4&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GTX 1650&lt;/th&gt;
&lt;th&gt;RTX 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;232&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;232&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;td&gt;183&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lost, against the same card's BF16:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;T4&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GTX 1650&lt;/th&gt;
&lt;th&gt;RTX 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Lost&lt;/strong&gt;: right at BF16, wrong at this rung, on the same GPU.&lt;/p&gt;

&lt;p&gt;The failures fall into the same kinds on every card. Q3_K_M runs out of room: 33 to 36 of its losses on each GPU are answers cut off at 1,024 tokens. Q2_K loops: 208 to 228 of its 250 answers repeat a line five or more times, and 246 to 249 hit the token cap.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Line the Four GPUs Up, Question by Question
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docs/gpu4_derived.py&lt;/code&gt; rebuilds the T4's grid from its committed results and reads the other three from their rows, then compares all six pairs of GPUs at every rung:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 docs/gpu4_derived.py &amp;gt; runs/2026-10-29-3050/four-gpus.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Graded differently, any two GPUs&lt;/th&gt;
&lt;th&gt;Lost on all four&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;2 to 4&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;3 to 5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;3 to 9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;4 to 10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;7 to 10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;9 to 15&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;12 to 22&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;26 to 34&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;6 to 9&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Graded differently&lt;/strong&gt;: right on one GPU and wrong on the other, at that rung, for the closest and the furthest of the six pairs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The light rungs lose different questions on each card.&lt;/strong&gt; F16 and Q8_0 lose 1 to 7 answers per GPU, and none of them is lost on all four.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deep losses are shared.&lt;/strong&gt; Q3_K_M loses 20 questions on all four GPUs and Q2_K 216.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most of the graveyard is common ground.&lt;/strong&gt; Each GPU has 226 questions that die at some rung, the RTX 3050 230. 218 die on all four, and 147 die first at the same rung on all four. One question dies only on the RTX 3050 (&lt;code&gt;test-358&lt;/code&gt;) and one only on the GTX 1650 (&lt;code&gt;test-147&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BF16 itself moves a few answers.&lt;/strong&gt; 227 questions are right at BF16 on all four cards and 18 wrong on all four. Against the L4, the RTX 3050's BF16 changes the text of 31 answers out of 250, the extracted number of 4 and the grade of 3.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 7 — Google's QAT Q4_0 on Four Cards
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-29-3050 BF16 QAT_Q4_0 Q4_0 --json graveyard-qat.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;QAT Q4_0 right&lt;/th&gt;
&lt;th&gt;Q4_0 right&lt;/th&gt;
&lt;th&gt;QAT Q4_0 lost&lt;/th&gt;
&lt;th&gt;Q4_0 lost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;🥇 222&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;🥇 220&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GTX 1650&lt;/td&gt;
&lt;td&gt;🥇 222&lt;/td&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3050&lt;/td&gt;
&lt;td&gt;🥇 221&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QAT Q4_0 scores 219 to 222 on the four cards and plain Q4_0 216 to 222. On the T4 and the L4 the plain file is ahead by 1; on the GTX 1650 QAT is ahead by 6 and on the RTX 3050 by 3. The two files lose mostly different questions: on the RTX 3050, 5 questions are lost under both, 10 only under QAT and 11 only under the plain Q4_0. Across the four GPUs, 6 of QAT's losses and 5 of Q4_0's are lost on every card.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Speed on Each Card
&lt;/h4&gt;

&lt;p&gt;Median decode speed in tokens per second. Each column is one machine; read down a column.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;T4&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GTX 1650&lt;/th&gt;
&lt;th&gt;RTX 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;40.0&lt;/td&gt;
&lt;td&gt;50.1&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;td&gt;40.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;39.6&lt;/td&gt;
&lt;td&gt;50.0&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;40.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;64.3&lt;/td&gt;
&lt;td&gt;83.0&lt;/td&gt;
&lt;td&gt;45.9&lt;/td&gt;
&lt;td&gt;67.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;70.8&lt;/td&gt;
&lt;td&gt;98.1&lt;/td&gt;
&lt;td&gt;57.2&lt;/td&gt;
&lt;td&gt;75.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;107.7&lt;/td&gt;
&lt;td&gt;61.9&lt;/td&gt;
&lt;td&gt;82.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;84.1&lt;/td&gt;
&lt;td&gt;118.7&lt;/td&gt;
&lt;td&gt;66.5&lt;/td&gt;
&lt;td&gt;91.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;🥇 93.6&lt;/td&gt;
&lt;td&gt;125.6&lt;/td&gt;
&lt;td&gt;🥇 70.8&lt;/td&gt;
&lt;td&gt;🥇 99.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;74.2&lt;/td&gt;
&lt;td&gt;111.1&lt;/td&gt;
&lt;td&gt;55.9&lt;/td&gt;
&lt;td&gt;64.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;td&gt;🥇 139.5&lt;/td&gt;
&lt;td&gt;62.2&lt;/td&gt;
&lt;td&gt;95.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q4_0 is the fastest rung on three of the four cards&lt;/strong&gt;; on the L4 it is Q2_K, which is wrong on 244 of 250 questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q5_K_M decodes 1.21 to 1.35 times as fast as Q8_0 on each card&lt;/strong&gt; (arithmetic, from the table). Q8_0 is the baseline here because it is the largest rung that fits whole on all four GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3_K_M is slower than Q4_0 on every card&lt;/strong&gt;, and on the RTX 3050 it is slower than Q8_0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BF16 and F16 run at the same speed on the T4, the L4 and the RTX 3050.&lt;/strong&gt; On the GTX 1650, with 22 of 36 layers on the CPU, they run at a fifth of Q8_0's speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full ladder with QAT took 5 hours 16 minutes on the RTX 3050 and 12 hours 25 minutes on the GTX 1650, from the first log line to the last (&lt;code&gt;run-3050.log&lt;/code&gt;, &lt;code&gt;run-1650.log&lt;/code&gt;).&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;T4&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GTX 1650&lt;/th&gt;
&lt;th&gt;RTX 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 right of 250&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;🥇 232&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M lost&lt;/td&gt;
&lt;td&gt;🥇 3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0 right, tok/s&lt;/td&gt;
&lt;td&gt;222, 93.6&lt;/td&gt;
&lt;td&gt;220, 125.6&lt;/td&gt;
&lt;td&gt;216, 70.8&lt;/td&gt;
&lt;td&gt;218, 99.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every rung whole on the GPU&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;❌ BF16, F16 split&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same text twice at BF16&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;On any of the four cards, take Q5_K_M: 39.0% of BF16's bytes, 226 to 231 right, 3 to 8 answers lost against the card's own BF16, and 1.21 to 1.35 times Q8_0's decode speed. For the fastest file that stays within 19 answers, take Q4_0, the fastest rung on three of the cards. On a 4 GB card, every quantized rung fits whole and the 16-bit rungs run at under 10 tokens per second. To compare two quantizations question by question, run both on one GPU: a different card moves 2 to 4 grades at BF16 and up to 34 at Q3_K_M.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to run Gemma 4 E2B's quantization ladder on four GPUs, from a 4 GB laptop card to a data-centre L4, and see which grades hold. The key was holding everything else fixed: byte-identical GGUFs, the same llama.cpp tag and the same sampling, with each card checked to give identical text twice. The four-GPU results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 All nine GGUFs and the QAT file match the T4's sha256 on every machine, and BF16 gives identical text twice on 50 questions on every card.&lt;/li&gt;
&lt;li&gt;🟢 The ladder keeps its shape on all four GPUs: F16 to Q5_K_M lose 1 to 8, Q4_K_M and Q4_0 lose 9 to 19, Q3_K_M loses 42 to 49.&lt;/li&gt;
&lt;li&gt;🟢 Q5_K_M is the best trade on every card: 226 to 231 right at 1.21 to 1.35 times Q8_0's speed.&lt;/li&gt;
&lt;li&gt;🟢 A 4 GB GTX 1650 runs every quantized rung whole on the GPU; BF16 and F16 run split, 14 of 36 layers on the GPU, at 9.2 and 9.6 tokens per second.&lt;/li&gt;
&lt;li&gt;⚠️ Any two GPUs grade 2 to 4 questions differently at BF16 and 12 to 22 at Q4_0; no question is lost at F16 or Q8_0 on all four.&lt;/li&gt;
&lt;li&gt;⚠️ 218 questions die on all four GPUs, 147 of them first at the same rung.&lt;/li&gt;
&lt;li&gt;⚠️ QAT Q4_0 and plain Q4_0 trade places by card: plain ahead by 1 on the T4 and L4, QAT ahead by 6 on the GTX 1650 and by 3 on the RTX 3050.&lt;/li&gt;
&lt;li&gt;❌ Q3_K_M loses 42 to 49, mostly answers cut off at 1,024 tokens; Q2_K gets 3 to 6 right, with 208 to 228 looping answers per card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one run of each rung per GPU on four NVIDIA cards (a Tesla T4 on GCE, an L4 on AWS, a GTX 1650 Ti laptop GPU and an RTX 3050 desktop card), 250 GSM8K test questions, greedy, thinking off, 1,024 max tokens, llama.cpp &lt;code&gt;b11699&lt;/code&gt; built for each card. Determinism checked at BF16 on each card. The T4 grid is rebuilt from its committed per-question results. The hosts also differ in CPU, RAM, driver and CUDA version, and on the GTX 1650 BF16 and F16 run split between GPU and CPU. Speed is compared within each card only.&lt;/p&gt;

&lt;p&gt;The strategy for measuring Gemma 4's accuracy one quantization rung at a time on four GPUs was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;Accuracy vs Speed: Gemma 4 E2B Quantization Tradeoffs, BF16 to Q2_K&lt;/a&gt;, the T4 article&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gde/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-4b90"&gt;Performance vs Quality: Gemma 4 E2B Quantization on an NVIDIA L4 and a Tesla T4&lt;/a&gt;, the L4 article&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;gemma-graveyard on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard/tree/main/runs/2026-10-29-1650" rel="noopener noreferrer"&gt;GTX 1650 run: runs/2026-10-29-1650&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard/tree/main/runs/2026-10-29-3050" rel="noopener noreferrer"&gt;RTX 3050 run: runs/2026-10-29-3050&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>quantization</category>
      <category>nvidia</category>
    </item>
    <item>
      <title>Performance vs Quality: Gemma 4 E2B Quantization on an NVIDIA L4 and a Tesla T4</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:39:09 +0000</pubDate>
      <link>https://dev.to/gde/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-4b90</link>
      <guid>https://dev.to/gde/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-4b90</guid>
      <description>&lt;p&gt;This article provides a step by step guide to measuring Gemma 4 E2B's speed and accuracy at every quantization rung a second time, on an NVIDIA L4 in AWS, with the same nine GGUF files and the same llama.cpp build as the Tesla T4 run, and comparing the two GPUs question by question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma-graveyard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;first article&lt;/a&gt; quantized Gemma 4 E2B from BF16 to Q2_K on one Tesla T4, asked every rung the same 250 GSM8K questions, and recorded each answer's grade at every rung. This one moves the whole ladder to a &lt;code&gt;g6.xlarge&lt;/code&gt;. The nine files have the same sha256 on both machines, both run llama.cpp &lt;code&gt;b11699&lt;/code&gt;, and each machine gives identical text when a rung is run twice. The GPU changes, and with it the driver, the CUDA version and the kernels that do the arithmetic.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Results in Brief
&lt;/h4&gt;

&lt;p&gt;The L4 decodes every rung faster than the T4, from 1.25 times at BF16 to 1.89 times at Q2_K, and Q5_K_M stays the best trade between speed and accuracy: 228 right against BF16's 229, at 2.15 times BF16's decode speed on the L4.&lt;/p&gt;

&lt;p&gt;The grades move with the GPU. With the same file and the same build, the L4 and the T4 grade 2 questions differently at BF16, 7 at Q5_K_M, 16 at Q4_0 and 33 at Q3_K_M. At Q5_K_M that is nearly the whole quantization effect on the T4, where Q5_K_M against BF16 was 8 questions: 3 lost and 5 gained. Of the 226 questions lost at some rung on each GPU, 180 are first lost at the same rung on both.&lt;/p&gt;

&lt;p&gt;The cover lays the two GPUs' grades over each other. One row per rung, BF16 at the top and Q2_K at the bottom, one cell per question: marigold where both GPUs answer right, grey where both answer wrong, violet where the two GPUs disagree. &lt;code&gt;docs/make_cover_gpu.py&lt;/code&gt; draws it from the committed rows.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Changes and What Stays
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tesla T4 (GCE)&lt;/th&gt;
&lt;th&gt;NVIDIA L4 (AWS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n1-standard-2&lt;/code&gt;, &lt;code&gt;us-west2-b&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g6.xlarge&lt;/code&gt;, &lt;code&gt;us-west-2c&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory&lt;/td&gt;
&lt;td&gt;15,360 MiB&lt;/td&gt;
&lt;td&gt;23,034 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute capability&lt;/td&gt;
&lt;td&gt;7.5 (Turing)&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;2 vCPU, 7,436 MiB RAM&lt;/td&gt;
&lt;td&gt;4 vCPU AMD EPYC 7R13, 15,363 MiB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver, CUDA&lt;/td&gt;
&lt;td&gt;615.71.10, 13.4&lt;/td&gt;
&lt;td&gt;595.112.07, 13.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same on both: llama.cpp &lt;code&gt;b11699&lt;/code&gt; with &lt;code&gt;GGML_CUDA=ON&lt;/code&gt;, built for each card's own compute capability; the nine GGUFs and Google's QAT Q4_0 file, byte for byte; the 250 questions in &lt;code&gt;data/heldout.jsonl&lt;/code&gt;; temperature 0, one request at a time, no prompt cache, thinking off, 1,024 max tokens, &lt;code&gt;llama-server -ngl 99 -c 4096 -np 1 --seed 0&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with quota for G instances (a &lt;code&gt;g6.xlarge&lt;/code&gt; uses 4 vCPU of it)&lt;/li&gt;
&lt;li&gt;The AWS CLI signed in, a key pair and a security group that allows SSH&lt;/li&gt;
&lt;li&gt;A clone: &lt;code&gt;git clone https://github.com/xbill9/gemma-graveyard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The T4 run's committed results in &lt;code&gt;runs/2026-10-27-ladder/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Launch the L4
&lt;/h4&gt;

&lt;p&gt;The AWS Deep Learning Base AMI ships with the NVIDIA driver and the CUDA toolkits, so the box needs no driver install. Its ID comes from a public SSM parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ssm get-parameter --name /aws/service/deeplearning/ami/x86_64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id \
  --query Parameter.Value --output text
ami-0fcc8fa7ee8d2dc4c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ec2 run-instances --image-id ami-0fcc8fa7ee8d2dc4c --instance-type g6.xlarge --key-name graveyard-l4 \
  --security-group-ids sg-0d721a15b91250482 --metadata-options HttpTokens=required \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=200,VolumeType=gp3,DeleteOnTermination=true}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;nvidia-smi&lt;/code&gt; on the new box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| NVIDIA-SMI 595.112.07             Driver Version: 595.112.07     CUDA Version: 13.2     |
|   0  NVIDIA L4                      On  |   00000000:31:00.0 Off |                    0 |
| N/A   55C    P0             28W /   72W |       0MiB /  23034MiB |      0%      Default |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;g6.xlarge&lt;/code&gt; in us-west-2 is $0.8048 an hour on demand, from the AWS Price List API.&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: work on the instance's own NVMe drive.&lt;/strong&gt; A &lt;code&gt;g6.xlarge&lt;/code&gt; carries a local NVMe disk, which the Deep Learning AMI mounts at &lt;code&gt;/opt/dlami/nvme&lt;/code&gt;. The checkpoint, the nine GGUFs, the llama.cpp build and a 16 GB swapfile all go there. It is wiped when the instance stops, so copy the results off first.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Build llama.cpp at the Same Tag
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/ggml-org/llama.cpp /opt/dlami/nvme/llama.cpp
cd /opt/dlami/nvme/llama.cpp &amp;amp;&amp;amp; git checkout b11699
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=89 -DCMAKE_BUILD_TYPE=Release
cmake --build build -j4 --target llama-server llama-quantize llama-cli
git describe --tags
b11699
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one build flag that differs from the T4 is &lt;code&gt;CMAKE_CUDA_ARCHITECTURES&lt;/code&gt;: 75 for the T4, 89 for the L4. llama.cpp's log on the L4 confirms it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ggml_cuda_init: found 1 CUDA devices (Total VRAM: 22565 MiB):
  Device 0: NVIDIA L4, compute capability 8.9, VMM: yes, VRAM: 22565 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔎 &lt;strong&gt;Tip: llama.cpp's pinned &lt;code&gt;transformers&lt;/code&gt; cannot read Gemma 4's tokenizer.&lt;/strong&gt; &lt;code&gt;pip install -r requirements/requirements-convert_hf_to_gguf.txt&lt;/code&gt; installs &lt;code&gt;transformers&lt;/code&gt; 4.57.6, and &lt;code&gt;convert_hf_to_gguf.py&lt;/code&gt; then stops while loading the tokenizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: 'list' object has no attribute 'keys'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pip install -U transformers&lt;/code&gt; (5.21.0) converts the checkpoint, and Step 3 shows the files match the T4's byte for byte.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Rebuild the Ladder and Check It Against the T4
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ladder.sh&lt;/code&gt; converts the checkpoint to BF16 and F16 and quantizes seven rungs from BF16, as on the T4. &lt;code&gt;runs/2026-10-28-l4/run-l4.sh&lt;/code&gt; then compares each file's size and sha256 with the T4's &lt;code&gt;ladder.tsv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./ladder.sh /opt/dlami/nvme/models/gemma-4-E2B-it /opt/dlami/nvme/graveyard-gguf /opt/dlami/nvme/llama.cpp
diff &amp;lt;(cut -f1,3,4 runs/2026-10-27-ladder/ladder.tsv) &amp;lt;(cut -f1,3,4 runs/2026-10-28-l4/ladder.tsv) &amp;amp;&amp;amp; echo "ladder matches T4 sha256"
ladder matches T4 sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All nine files are identical, and so is Google's QAT Q4_0 file (sha256 &lt;code&gt;fa401b55…&lt;/code&gt;). From here on, every answer that differs between the two machines comes from the GPU and its CUDA stack: the card, its driver, its CUDA version and the kernels built for it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Prove the L4 Repeats Itself
&lt;/h4&gt;

&lt;p&gt;The same check as on the T4: BF16 on 50 questions, twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py repeat runs/2026-10-28-l4/determinism-1 runs/2026-10-28-l4/determinism-2 BF16
{"rung": "BF16", "n": 50, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 46, "right_b": 46}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every character matches. Each GPU on its own is deterministic, so a difference between the two machines belongs to the GPU and its CUDA stack.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run the Ladder on the L4 and Score It
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;run-l4.sh&lt;/code&gt; runs the nine rungs and QAT Q4_0, then scores each against BF16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-28-l4 BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M Q2_K --json graveyard-ladder.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Right, T4&lt;/th&gt;
&lt;th&gt;Right, L4&lt;/th&gt;
&lt;th&gt;Lost, L4&lt;/th&gt;
&lt;th&gt;Gained, L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;🥇 228&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;🥇 228&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Lost&lt;/strong&gt;: BF16 right, this rung wrong. &lt;strong&gt;Gained&lt;/strong&gt;: BF16 wrong, this rung right. Both are counted against the same GPU's own BF16.&lt;/p&gt;

&lt;p&gt;The shape of the ladder holds. Q8_0 to Q5_K_M stay within 2 of BF16; Q4_K_M and Q4_0 lose 8 and 9; Q3_K_M loses 43; Q2_K collapses. The failures fall into the same kinds as on the T4: Q3_K_M runs out of room (36 of its 45 losses cut off at 1,024 tokens), and Q2_K loops (218 of 250 answers repeat a line five or more times, 71 open a thinking block with thinking off, 63 leave &lt;code&gt;content&lt;/code&gt; empty).&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: llama-server can refuse an answer it cannot parse.&lt;/strong&gt; One Q2_K question, &lt;code&gt;test-194&lt;/code&gt;, came back as HTTP 500 on the L4: &lt;code&gt;The model produced output that does not match the expected peg-gemma4 format&lt;/code&gt;. &lt;code&gt;graveyard.py&lt;/code&gt; records it as an error and grades it wrong, so Q2_K's 6 right are out of 250 with 1 error.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Line the Two GPUs Up, Question by Question
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docs/gpu_derived.py&lt;/code&gt; rebuilds the T4's right/wrong grid for all 250 questions at all nine rungs from its committed results, and the rebuilt grid reproduces all 2,250 cells of the T4 article's cover and the T4's own right, lost and gained counts at every rung.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 docs/gpu_derived.py &amp;gt; docs/evidence-l4/derived-gpu.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Graded differently, T4 vs L4&lt;/th&gt;
&lt;th&gt;Lost, T4&lt;/th&gt;
&lt;th&gt;Lost, L4&lt;/th&gt;
&lt;th&gt;Lost on both&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Graded differently&lt;/strong&gt;: questions right on one GPU and wrong on the other, at that rung.&lt;/p&gt;

&lt;p&gt;Three things show in the grid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Above Q4_K_M, the GPU moves nearly as many grades as the quantization.&lt;/strong&gt; At Q5_K_M the two GPUs disagree on 7 questions; on the T4, Q5_K_M and BF16 disagree on 8 (3 lost, 5 gained). Q5_K_M's 231 on the T4 and 228 on the L4 sit either side of BF16's 229 on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most losses are shared, often at a different rung.&lt;/strong&gt; 222 questions are lost at some rung on both GPUs, 4 only on the T4 and 4 only on the L4. 180 of them are first lost at the same rung on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deep losses are the stable ones.&lt;/strong&gt; Q2_K's losses are 221 of 226 shared and Q3_K_M's 28 of 42. Between F16 and Q5_K_M, 7 of 12 T4 losses are lost again on the L4 at the same rung.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The twins and triplets question shows it. The T4 article's one F16 loss, &lt;code&gt;test-687&lt;/code&gt; (answer 13), was right at BF16 on the T4 and answered 17 at F16. On the L4, BF16 itself answers 17, so the question counts as gained there: wrong at BF16, right again at Q8_0, Q6_K, Q5_K_M and Q4_0. &lt;code&gt;test-93&lt;/code&gt; (answer 36) runs the other way: gained on the T4, right at BF16 on the L4 and lost from Q8_0 on.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Google's QAT Q4_0 on the L4
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-28-l4 BF16 QAT_Q4_0 Q4_0 --json graveyard-qat.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Lost&lt;/th&gt;
&lt;th&gt;Cut off at 1,024&lt;/th&gt;
&lt;th&gt;Median tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QAT Q4_0&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;125.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;125.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QAT Q4_0 gains 5 questions against BF16 and Q4_0 gains 7. The QAT file is 3,349,516,256 bytes and loads in 1,730 MiB; the plain Q4_0 is 3,360,161,216 bytes and loads in 1,740 MiB.&lt;/p&gt;

&lt;p&gt;The same picture as the T4, one right answer apart again: 219 against the plain Q4_0's 220, where the T4 had 221 against 222. On the L4 the two decode at the same speed, 125.7 and 125.6 tok/s; on the T4 the plain Q4_0 was faster, 93.6 against 87.8. 10 of QAT Q4_0's 15 losses on the T4 are lost on the L4 too.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Speed and Memory on Each Card
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Median tok/s, T4&lt;/th&gt;
&lt;th&gt;Median tok/s, L4&lt;/th&gt;
&lt;th&gt;L4 ÷ T4&lt;/th&gt;
&lt;th&gt;× BF16, T4 / L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;40.0&lt;/td&gt;
&lt;td&gt;50.1&lt;/td&gt;
&lt;td&gt;1.25&lt;/td&gt;
&lt;td&gt;1.00 / 1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;39.6&lt;/td&gt;
&lt;td&gt;50.0&lt;/td&gt;
&lt;td&gt;1.26&lt;/td&gt;
&lt;td&gt;0.99 / 1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;64.3&lt;/td&gt;
&lt;td&gt;83.0&lt;/td&gt;
&lt;td&gt;1.29&lt;/td&gt;
&lt;td&gt;1.61 / 1.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;70.8&lt;/td&gt;
&lt;td&gt;98.1&lt;/td&gt;
&lt;td&gt;1.39&lt;/td&gt;
&lt;td&gt;1.77 / 1.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;107.7&lt;/td&gt;
&lt;td&gt;1.37&lt;/td&gt;
&lt;td&gt;1.97 / 2.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;84.1&lt;/td&gt;
&lt;td&gt;118.7&lt;/td&gt;
&lt;td&gt;1.41&lt;/td&gt;
&lt;td&gt;2.10 / 2.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;93.6&lt;/td&gt;
&lt;td&gt;125.6&lt;/td&gt;
&lt;td&gt;1.34&lt;/td&gt;
&lt;td&gt;2.34 / 2.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;74.2&lt;/td&gt;
&lt;td&gt;111.1&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.85 / 2.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;td&gt;139.5&lt;/td&gt;
&lt;td&gt;1.89&lt;/td&gt;
&lt;td&gt;1.85 / 2.78&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BF16 and F16 run at the same speed on both cards&lt;/strong&gt;, within 0.4 tok/s of each other on each GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The smaller the file, the bigger the L4's lead&lt;/strong&gt;, up to 1.89 times at Q2_K. Q2_K is the fastest rung on the L4 and among the slowest on the T4; it is also wrong on 244 of 250 questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q3_K_M is slower than Q4_K_M and Q4_0 on both cards.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The L4 uses 101 MiB more GPU memory at every rung&lt;/strong&gt; (&lt;code&gt;nvidia-smi&lt;/code&gt; after load: 4,772 against 4,671 MiB at BF16, 1,740 against 1,639 at Q4_0), the same fixed amount whatever the file size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The L4 run took 4 hours 3 minutes from the start of the ladder build to the last scoring pass (&lt;code&gt;run-l4.log&lt;/code&gt;), about $3.26 at the on-demand price (arithmetic).&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tesla T4&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 right of 250&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M right, × BF16 speed&lt;/td&gt;
&lt;td&gt;🥇 231, 1.97&lt;/td&gt;
&lt;td&gt;🥇 228, 2.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0 right, median tok/s&lt;/td&gt;
&lt;td&gt;222, 93.6&lt;/td&gt;
&lt;td&gt;220, 125.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M right&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same text twice on one GPU&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory after load, Q4_0&lt;/td&gt;
&lt;td&gt;1,639 MiB&lt;/td&gt;
&lt;td&gt;1,740 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;🟢 Q5_K_M, slower card&lt;/td&gt;
&lt;td&gt;🟢 Q5_K_M, 1.37 times faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;On either card, take Q5_K_M: 39.0% of BF16's bytes, within 1 of BF16's score on the L4 and 2 above it on the T4, and 1.97 times BF16's decode speed on the T4 or 2.15 times on the L4. For the fastest file that stays within 9 answers, take Q4_0. The L4 is the faster card at every rung, by 1.25 to 1.89 times. To compare two quantizations question by question, run both on one GPU: a different card moves a few grades at every rung, and up to 33 at Q3_K_M.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to rerun Gemma 4 E2B's quantization ladder on a second GPU, measure speed against accuracy at every rung, and see which grades stay put. The key was holding everything else fixed: byte-identical GGUFs, the same llama.cpp tag and the same sampling, with each GPU checked to give identical text twice. The L4 results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 All nine GGUFs and the QAT file match the T4's sha256; BF16 on the L4 gives identical text twice on 50 questions, the one rung checked for determinism.&lt;/li&gt;
&lt;li&gt;🟢 BF16 scores 229 of 250 on both GPUs, and the ladder keeps its shape: Q8_0 to Q5_K_M within 2, Q4_K_M and Q4_0 within 9.&lt;/li&gt;
&lt;li&gt;🟢 Q5_K_M is the best trade on both: 228 right at 2.15 times BF16's speed on the L4.&lt;/li&gt;
&lt;li&gt;🟢 The L4 decodes 1.25 to 1.89 times faster than the T4, and BF16 runs at F16's speed on both cards.&lt;/li&gt;
&lt;li&gt;⚠️ The two GPUs grade 2 questions differently at BF16, 7 at Q5_K_M and 16 at Q4_0; above Q4_K_M that is nearly the size of the quantization effect.&lt;/li&gt;
&lt;li&gt;⚠️ 180 of 226 lost questions are first lost at the same rung on both GPUs; 222 questions are lost on both at some rung.&lt;/li&gt;
&lt;li&gt;⚠️ QAT Q4_0 scores 219 against the plain Q4_0's 220 on the L4, one apart again, at the same speed.&lt;/li&gt;
&lt;li&gt;❌ Q3_K_M loses 43, 36 of its 45 losses cut off at 1,024 tokens; Q2_K gets 6 right, with 218 looping answers and 1 answer the server refused to parse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one Tesla T4 on GCE (&lt;code&gt;n1-standard-2&lt;/code&gt;, us-west2-b) and one NVIDIA L4 on AWS (&lt;code&gt;g6.xlarge&lt;/code&gt;, us-west-2c, $0.8048 per hour on demand), one run of each rung per GPU, 250 GSM8K test questions, greedy, thinking off, 1,024 max tokens, llama.cpp &lt;code&gt;b11699&lt;/code&gt; built for each card. Determinism checked on the L4 at BF16. The T4 grid is rebuilt from its committed per-question results and cover. The two hosts also differ in CPU, RAM, driver and CUDA version.&lt;/p&gt;

&lt;p&gt;The strategy for measuring Gemma 4's speed and accuracy one quantization rung at a time on two GPUs was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;Accuracy vs Speed: Gemma 4 E2B Quantization Tradeoffs, BF16 to Q2_K&lt;/a&gt;, the T4 article&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;gemma-graveyard on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard/tree/main/runs/2026-10-28-l4" rel="noopener noreferrer"&gt;L4 run: runs/2026-10-28-l4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;Amazon EC2 G6 instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>aws</category>
      <category>quantization</category>
    </item>
    <item>
      <title>Performance vs Quality: Gemma 4 E2B Quantization on an NVIDIA L4 and a Tesla T4</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:38:56 +0000</pubDate>
      <link>https://dev.to/aws-builders/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-404g</link>
      <guid>https://dev.to/aws-builders/performance-vs-quality-gemma-4-e2b-quantization-on-an-nvidia-l4-and-a-tesla-t4-404g</guid>
      <description>&lt;p&gt;This article provides a step by step guide to measuring Gemma 4 E2B's speed and accuracy at every quantization rung a second time, on an NVIDIA L4 in AWS, with the same nine GGUF files and the same llama.cpp build as the Tesla T4 run, and comparing the two GPUs question by question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma-graveyard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;first article&lt;/a&gt; quantized Gemma 4 E2B from BF16 to Q2_K on one Tesla T4, asked every rung the same 250 GSM8K questions, and recorded each answer's grade at every rung. This one moves the whole ladder to a &lt;code&gt;g6.xlarge&lt;/code&gt;. The nine files have the same sha256 on both machines, both run llama.cpp &lt;code&gt;b11699&lt;/code&gt;, and each machine gives identical text when a rung is run twice. The GPU changes, and with it the driver, the CUDA version and the kernels that do the arithmetic.&lt;/p&gt;




&lt;h4&gt;
  
  
  The Results in Brief
&lt;/h4&gt;

&lt;p&gt;The L4 decodes every rung faster than the T4, from 1.25 times at BF16 to 1.89 times at Q2_K, and Q5_K_M stays the best trade between speed and accuracy: 228 right against BF16's 229, at 2.15 times BF16's decode speed on the L4.&lt;/p&gt;

&lt;p&gt;The grades move with the GPU. With the same file and the same build, the L4 and the T4 grade 2 questions differently at BF16, 7 at Q5_K_M, 16 at Q4_0 and 33 at Q3_K_M. At Q5_K_M that is nearly the whole quantization effect on the T4, where Q5_K_M against BF16 was 8 questions: 3 lost and 5 gained. Of the 226 questions lost at some rung on each GPU, 180 are first lost at the same rung on both.&lt;/p&gt;

&lt;p&gt;The cover lays the two GPUs' grades over each other. One row per rung, BF16 at the top and Q2_K at the bottom, one cell per question: marigold where both GPUs answer right, grey where both answer wrong, violet where the two GPUs disagree. &lt;code&gt;docs/make_cover_gpu.py&lt;/code&gt; draws it from the committed rows.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Changes and What Stays
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tesla T4 (GCE)&lt;/th&gt;
&lt;th&gt;NVIDIA L4 (AWS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n1-standard-2&lt;/code&gt;, &lt;code&gt;us-west2-b&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g6.xlarge&lt;/code&gt;, &lt;code&gt;us-west-2c&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory&lt;/td&gt;
&lt;td&gt;15,360 MiB&lt;/td&gt;
&lt;td&gt;23,034 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute capability&lt;/td&gt;
&lt;td&gt;7.5 (Turing)&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;2 vCPU, 7,436 MiB RAM&lt;/td&gt;
&lt;td&gt;4 vCPU AMD EPYC 7R13, 15,363 MiB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver, CUDA&lt;/td&gt;
&lt;td&gt;615.71.10, 13.4&lt;/td&gt;
&lt;td&gt;595.112.07, 13.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same on both: llama.cpp &lt;code&gt;b11699&lt;/code&gt; with &lt;code&gt;GGML_CUDA=ON&lt;/code&gt;, built for each card's own compute capability; the nine GGUFs and Google's QAT Q4_0 file, byte for byte; the 250 questions in &lt;code&gt;data/heldout.jsonl&lt;/code&gt;; temperature 0, one request at a time, no prompt cache, thinking off, 1,024 max tokens, &lt;code&gt;llama-server -ngl 99 -c 4096 -np 1 --seed 0&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with quota for G instances (a &lt;code&gt;g6.xlarge&lt;/code&gt; uses 4 vCPU of it)&lt;/li&gt;
&lt;li&gt;The AWS CLI signed in, a key pair and a security group that allows SSH&lt;/li&gt;
&lt;li&gt;A clone: &lt;code&gt;git clone https://github.com/xbill9/gemma-graveyard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The T4 run's committed results in &lt;code&gt;runs/2026-10-27-ladder/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Launch the L4
&lt;/h4&gt;

&lt;p&gt;The AWS Deep Learning Base AMI ships with the NVIDIA driver and the CUDA toolkits, so the box needs no driver install. Its ID comes from a public SSM parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ssm get-parameter --name /aws/service/deeplearning/ami/x86_64/base-oss-nvidia-driver-gpu-ubuntu-24.04/latest/ami-id \
  --query Parameter.Value --output text
ami-0fcc8fa7ee8d2dc4c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ec2 run-instances --image-id ami-0fcc8fa7ee8d2dc4c --instance-type g6.xlarge --key-name graveyard-l4 \
  --security-group-ids sg-0d721a15b91250482 --metadata-options HttpTokens=required \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=200,VolumeType=gp3,DeleteOnTermination=true}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;nvidia-smi&lt;/code&gt; on the new box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| NVIDIA-SMI 595.112.07             Driver Version: 595.112.07     CUDA Version: 13.2     |
|   0  NVIDIA L4                      On  |   00000000:31:00.0 Off |                    0 |
| N/A   55C    P0             28W /   72W |       0MiB /  23034MiB |      0%      Default |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;g6.xlarge&lt;/code&gt; in us-west-2 is $0.8048 an hour on demand, from the AWS Price List API.&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: work on the instance's own NVMe drive.&lt;/strong&gt; A &lt;code&gt;g6.xlarge&lt;/code&gt; carries a local NVMe disk, which the Deep Learning AMI mounts at &lt;code&gt;/opt/dlami/nvme&lt;/code&gt;. The checkpoint, the nine GGUFs, the llama.cpp build and a 16 GB swapfile all go there. It is wiped when the instance stops, so copy the results off first.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Build llama.cpp at the Same Tag
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/ggml-org/llama.cpp /opt/dlami/nvme/llama.cpp
cd /opt/dlami/nvme/llama.cpp &amp;amp;&amp;amp; git checkout b11699
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=89 -DCMAKE_BUILD_TYPE=Release
cmake --build build -j4 --target llama-server llama-quantize llama-cli
git describe --tags
b11699
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one build flag that differs from the T4 is &lt;code&gt;CMAKE_CUDA_ARCHITECTURES&lt;/code&gt;: 75 for the T4, 89 for the L4. llama.cpp's log on the L4 confirms it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ggml_cuda_init: found 1 CUDA devices (Total VRAM: 22565 MiB):
  Device 0: NVIDIA L4, compute capability 8.9, VMM: yes, VRAM: 22565 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔎 &lt;strong&gt;Tip: llama.cpp's pinned &lt;code&gt;transformers&lt;/code&gt; cannot read Gemma 4's tokenizer.&lt;/strong&gt; &lt;code&gt;pip install -r requirements/requirements-convert_hf_to_gguf.txt&lt;/code&gt; installs &lt;code&gt;transformers&lt;/code&gt; 4.57.6, and &lt;code&gt;convert_hf_to_gguf.py&lt;/code&gt; then stops while loading the tokenizer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: 'list' object has no attribute 'keys'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pip install -U transformers&lt;/code&gt; (5.21.0) converts the checkpoint, and Step 3 shows the files match the T4's byte for byte.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Rebuild the Ladder and Check It Against the T4
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ladder.sh&lt;/code&gt; converts the checkpoint to BF16 and F16 and quantizes seven rungs from BF16, as on the T4. &lt;code&gt;runs/2026-10-28-l4/run-l4.sh&lt;/code&gt; then compares each file's size and sha256 with the T4's &lt;code&gt;ladder.tsv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./ladder.sh /opt/dlami/nvme/models/gemma-4-E2B-it /opt/dlami/nvme/graveyard-gguf /opt/dlami/nvme/llama.cpp
diff &amp;lt;(cut -f1,3,4 runs/2026-10-27-ladder/ladder.tsv) &amp;lt;(cut -f1,3,4 runs/2026-10-28-l4/ladder.tsv) &amp;amp;&amp;amp; echo "ladder matches T4 sha256"
ladder matches T4 sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All nine files are identical, and so is Google's QAT Q4_0 file (sha256 &lt;code&gt;fa401b55…&lt;/code&gt;). From here on, every answer that differs between the two machines comes from the GPU and its CUDA stack: the card, its driver, its CUDA version and the kernels built for it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Prove the L4 Repeats Itself
&lt;/h4&gt;

&lt;p&gt;The same check as on the T4: BF16 on 50 questions, twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py repeat runs/2026-10-28-l4/determinism-1 runs/2026-10-28-l4/determinism-2 BF16
{"rung": "BF16", "n": 50, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 46, "right_b": 46}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every character matches. Each GPU on its own is deterministic, so a difference between the two machines belongs to the GPU and its CUDA stack.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run the Ladder on the L4 and Score It
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;run-l4.sh&lt;/code&gt; runs the nine rungs and QAT Q4_0, then scores each against BF16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-28-l4 BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M Q2_K --json graveyard-ladder.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Right, T4&lt;/th&gt;
&lt;th&gt;Right, L4&lt;/th&gt;
&lt;th&gt;Lost, L4&lt;/th&gt;
&lt;th&gt;Gained, L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;🥇 228&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;🥇 228&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Lost&lt;/strong&gt;: BF16 right, this rung wrong. &lt;strong&gt;Gained&lt;/strong&gt;: BF16 wrong, this rung right. Both are counted against the same GPU's own BF16.&lt;/p&gt;

&lt;p&gt;The shape of the ladder holds. Q8_0 to Q5_K_M stay within 2 of BF16; Q4_K_M and Q4_0 lose 8 and 9; Q3_K_M loses 43; Q2_K collapses. The failures fall into the same kinds as on the T4: Q3_K_M runs out of room (36 of its 45 losses cut off at 1,024 tokens), and Q2_K loops (218 of 250 answers repeat a line five or more times, 71 open a thinking block with thinking off, 63 leave &lt;code&gt;content&lt;/code&gt; empty).&lt;/p&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: llama-server can refuse an answer it cannot parse.&lt;/strong&gt; One Q2_K question, &lt;code&gt;test-194&lt;/code&gt;, came back as HTTP 500 on the L4: &lt;code&gt;The model produced output that does not match the expected peg-gemma4 format&lt;/code&gt;. &lt;code&gt;graveyard.py&lt;/code&gt; records it as an error and grades it wrong, so Q2_K's 6 right are out of 250 with 1 error.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Line the Two GPUs Up, Question by Question
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docs/gpu_derived.py&lt;/code&gt; rebuilds the T4's right/wrong grid for all 250 questions at all nine rungs from its committed results, and the rebuilt grid reproduces all 2,250 cells of the T4 article's cover and the T4's own right, lost and gained counts at every rung.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 docs/gpu_derived.py &amp;gt; docs/evidence-l4/derived-gpu.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Graded differently, T4 vs L4&lt;/th&gt;
&lt;th&gt;Lost, T4&lt;/th&gt;
&lt;th&gt;Lost, L4&lt;/th&gt;
&lt;th&gt;Lost on both&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Graded differently&lt;/strong&gt;: questions right on one GPU and wrong on the other, at that rung.&lt;/p&gt;

&lt;p&gt;Three things show in the grid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Above Q4_K_M, the GPU moves nearly as many grades as the quantization.&lt;/strong&gt; At Q5_K_M the two GPUs disagree on 7 questions; on the T4, Q5_K_M and BF16 disagree on 8 (3 lost, 5 gained). Q5_K_M's 231 on the T4 and 228 on the L4 sit either side of BF16's 229 on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most losses are shared, often at a different rung.&lt;/strong&gt; 222 questions are lost at some rung on both GPUs, 4 only on the T4 and 4 only on the L4. 180 of them are first lost at the same rung on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deep losses are the stable ones.&lt;/strong&gt; Q2_K's losses are 221 of 226 shared and Q3_K_M's 28 of 42. Between F16 and Q5_K_M, 7 of 12 T4 losses are lost again on the L4 at the same rung.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The twins and triplets question shows it. The T4 article's one F16 loss, &lt;code&gt;test-687&lt;/code&gt; (answer 13), was right at BF16 on the T4 and answered 17 at F16. On the L4, BF16 itself answers 17, so the question counts as gained there: wrong at BF16, right again at Q8_0, Q6_K, Q5_K_M and Q4_0. &lt;code&gt;test-93&lt;/code&gt; (answer 36) runs the other way: gained on the T4, right at BF16 on the L4 and lost from Q8_0 on.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Google's QAT Q4_0 on the L4
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-28-l4 BF16 QAT_Q4_0 Q4_0 --json graveyard-qat.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Lost&lt;/th&gt;
&lt;th&gt;Cut off at 1,024&lt;/th&gt;
&lt;th&gt;Median tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QAT Q4_0&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;125.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;125.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QAT Q4_0 gains 5 questions against BF16 and Q4_0 gains 7. The QAT file is 3,349,516,256 bytes and loads in 1,730 MiB; the plain Q4_0 is 3,360,161,216 bytes and loads in 1,740 MiB.&lt;/p&gt;

&lt;p&gt;The same picture as the T4, one right answer apart again: 219 against the plain Q4_0's 220, where the T4 had 221 against 222. On the L4 the two decode at the same speed, 125.7 and 125.6 tok/s; on the T4 the plain Q4_0 was faster, 93.6 against 87.8. 10 of QAT Q4_0's 15 losses on the T4 are lost on the L4 too.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Speed and Memory on Each Card
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Median tok/s, T4&lt;/th&gt;
&lt;th&gt;Median tok/s, L4&lt;/th&gt;
&lt;th&gt;L4 ÷ T4&lt;/th&gt;
&lt;th&gt;× BF16, T4 / L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;40.0&lt;/td&gt;
&lt;td&gt;50.1&lt;/td&gt;
&lt;td&gt;1.25&lt;/td&gt;
&lt;td&gt;1.00 / 1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;39.6&lt;/td&gt;
&lt;td&gt;50.0&lt;/td&gt;
&lt;td&gt;1.26&lt;/td&gt;
&lt;td&gt;0.99 / 1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;64.3&lt;/td&gt;
&lt;td&gt;83.0&lt;/td&gt;
&lt;td&gt;1.29&lt;/td&gt;
&lt;td&gt;1.61 / 1.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;70.8&lt;/td&gt;
&lt;td&gt;98.1&lt;/td&gt;
&lt;td&gt;1.39&lt;/td&gt;
&lt;td&gt;1.77 / 1.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;107.7&lt;/td&gt;
&lt;td&gt;1.37&lt;/td&gt;
&lt;td&gt;1.97 / 2.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;84.1&lt;/td&gt;
&lt;td&gt;118.7&lt;/td&gt;
&lt;td&gt;1.41&lt;/td&gt;
&lt;td&gt;2.10 / 2.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;93.6&lt;/td&gt;
&lt;td&gt;125.6&lt;/td&gt;
&lt;td&gt;1.34&lt;/td&gt;
&lt;td&gt;2.34 / 2.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;74.2&lt;/td&gt;
&lt;td&gt;111.1&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;1.85 / 2.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;td&gt;139.5&lt;/td&gt;
&lt;td&gt;1.89&lt;/td&gt;
&lt;td&gt;1.85 / 2.78&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BF16 and F16 run at the same speed on both cards&lt;/strong&gt;, within 0.4 tok/s of each other on each GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The smaller the file, the bigger the L4's lead&lt;/strong&gt;, up to 1.89 times at Q2_K. Q2_K is the fastest rung on the L4 and among the slowest on the T4; it is also wrong on 244 of 250 questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q3_K_M is slower than Q4_K_M and Q4_0 on both cards.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The L4 uses 101 MiB more GPU memory at every rung&lt;/strong&gt; (&lt;code&gt;nvidia-smi&lt;/code&gt; after load: 4,772 against 4,671 MiB at BF16, 1,740 against 1,639 at Q4_0), the same fixed amount whatever the file size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The L4 run took 4 hours 3 minutes from the start of the ladder build to the last scoring pass (&lt;code&gt;run-l4.log&lt;/code&gt;), about $3.26 at the on-demand price (arithmetic).&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tesla T4&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16 right of 250&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M right, × BF16 speed&lt;/td&gt;
&lt;td&gt;🥇 231, 1.97&lt;/td&gt;
&lt;td&gt;🥇 228, 2.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0 right, median tok/s&lt;/td&gt;
&lt;td&gt;222, 93.6&lt;/td&gt;
&lt;td&gt;220, 125.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M right&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same text twice on one GPU&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;td&gt;🟢 yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory after load, Q4_0&lt;/td&gt;
&lt;td&gt;1,639 MiB&lt;/td&gt;
&lt;td&gt;1,740 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;🟢 Q5_K_M, slower card&lt;/td&gt;
&lt;td&gt;🟢 Q5_K_M, 1.37 times faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;On either card, take Q5_K_M: 39.0% of BF16's bytes, within 1 of BF16's score on the L4 and 2 above it on the T4, and 1.97 times BF16's decode speed on the T4 or 2.15 times on the L4. For the fastest file that stays within 9 answers, take Q4_0. The L4 is the faster card at every rung, by 1.25 to 1.89 times. To compare two quantizations question by question, run both on one GPU: a different card moves a few grades at every rung, and up to 33 at Q3_K_M.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to rerun Gemma 4 E2B's quantization ladder on a second GPU, measure speed against accuracy at every rung, and see which grades stay put. The key was holding everything else fixed: byte-identical GGUFs, the same llama.cpp tag and the same sampling, with each GPU checked to give identical text twice. The L4 results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 All nine GGUFs and the QAT file match the T4's sha256; BF16 on the L4 gives identical text twice on 50 questions, the one rung checked for determinism.&lt;/li&gt;
&lt;li&gt;🟢 BF16 scores 229 of 250 on both GPUs, and the ladder keeps its shape: Q8_0 to Q5_K_M within 2, Q4_K_M and Q4_0 within 9.&lt;/li&gt;
&lt;li&gt;🟢 Q5_K_M is the best trade on both: 228 right at 2.15 times BF16's speed on the L4.&lt;/li&gt;
&lt;li&gt;🟢 The L4 decodes 1.25 to 1.89 times faster than the T4, and BF16 runs at F16's speed on both cards.&lt;/li&gt;
&lt;li&gt;⚠️ The two GPUs grade 2 questions differently at BF16, 7 at Q5_K_M and 16 at Q4_0; above Q4_K_M that is nearly the size of the quantization effect.&lt;/li&gt;
&lt;li&gt;⚠️ 180 of 226 lost questions are first lost at the same rung on both GPUs; 222 questions are lost on both at some rung.&lt;/li&gt;
&lt;li&gt;⚠️ QAT Q4_0 scores 219 against the plain Q4_0's 220 on the L4, one apart again, at the same speed.&lt;/li&gt;
&lt;li&gt;❌ Q3_K_M loses 43, 36 of its 45 losses cut off at 1,024 tokens; Q2_K gets 6 right, with 218 looping answers and 1 answer the server refused to parse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one Tesla T4 on GCE (&lt;code&gt;n1-standard-2&lt;/code&gt;, us-west2-b) and one NVIDIA L4 on AWS (&lt;code&gt;g6.xlarge&lt;/code&gt;, us-west-2c, $0.8048 per hour on demand), one run of each rung per GPU, 250 GSM8K test questions, greedy, thinking off, 1,024 max tokens, llama.cpp &lt;code&gt;b11699&lt;/code&gt; built for each card. Determinism checked on the L4 at BF16. The T4 grid is rebuilt from its committed per-question results and cover. The two hosts also differ in CPU, RAM, driver and CUDA version.&lt;/p&gt;

&lt;p&gt;The strategy for measuring Gemma 4's speed and accuracy one quantization rung at a time on two GPUs was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea"&gt;Accuracy vs Speed: Gemma 4 E2B Quantization Tradeoffs, BF16 to Q2_K&lt;/a&gt;, the T4 article&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;gemma-graveyard on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard/tree/main/runs/2026-10-28-l4" rel="noopener noreferrer"&gt;L4 run: runs/2026-10-28-l4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;Amazon EC2 G6 instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>aws</category>
      <category>quantization</category>
    </item>
    <item>
      <title>Accuracy vs Speed: Gemma 4 E2B Quantization Tradeoffs, BF16 to Q2_K</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Wed, 28 Oct 2026 16:02:20 +0000</pubDate>
      <link>https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea</link>
      <guid>https://dev.to/gde/accuracy-vs-speed-gemma-4-e2b-quantization-tradeoffs-bf16-to-q2k-aea</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This article provides a step by step guide to quantizing Gemma 4 E2B down a ladder of nine GGUF files with llama.cpp, asking every rung the same 250 maths questions on one Tesla T4, and burying each answer at the rung where it first goes wrong. Every count is computed by code and every row is committed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma-graveyard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A score per quantization level says how many answers a smaller file loses; a graveyard says which ones. Against the 229 of 250 questions BF16 gets right, Q8_0, Q6_K and Q5_K_M stay within 2, Q4_K_M and Q4_0 lose 5 and 7, Q3_K_M loses 38 and Q2_K gets 3 right. Above Q3_K_M most of the dead come back: 16 of the 23 questions that die between F16 and Q4_0 are right again at a lower rung. With thinking on, Q3_K_M climbs from 191 to 208.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Quantization is the most common thing done to an open-weight model before it ships to a small device. The usual report is one accuracy number per file size. Two files with the same score can still get different questions wrong, and a net loss hides its parts: Q4_0 here is 7 answers down on BF16, made of 15 questions lost and 8 gained.&lt;/p&gt;

&lt;p&gt;The gemma-graveyard asks every rung of a quantization ladder the same 250 questions at temperature 0, so each answer is the model's single most likely one. BF16 is the reference, and every question a rung answers differently from BF16 lands in one of four records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead&lt;/strong&gt;: BF16 answers it right and the rung answers it wrong. A rung's dead count is every such question at that rung.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tombstone&lt;/strong&gt;: one per question, placed at the first rung, going down from BF16, where the question dies. A question dead at Q8_0 and again at Q4_0 has one tombstone, at Q8_0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rose again&lt;/strong&gt;: a lower rung answers a tombstoned question right once more. test-687 dies at F16 and is right again at Q8_0, Q6_K, Q5_K_M and Q4_K_M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ghost&lt;/strong&gt;: BF16 answers it wrong and the rung answers it right. test-1166 asks for an avocado tree's fruit over ten years, answer 2,350. BF16 says 1,000; Q8_0, Q6_K and Q4_0 get it right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rung's score is BF16's 229, minus its dead, plus its ghosts: Q4_0 is 229 − 15 + 8 = 222. Quantization moves answers in both directions, and a score that holds can hide questions lost and found, so the article reports both.&lt;/p&gt;

&lt;p&gt;For Día de los Muertos: the dead are remembered one at a time, by name. Each tombstone here carries the question's id, the right answer, the wrong answer the rung gave, and every lower rung where it rose again.&lt;/p&gt;

&lt;p&gt;What the project delivers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ladder.sh&lt;/code&gt;: BF16, F16 and seven quantized GGUFs of Gemma 4 E2B, with bytes and sha256&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graveyard.py run&lt;/code&gt;: one rung, its own llama-server, 250 questions, answers graded in code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graveyard.py bury&lt;/code&gt;: deaths, tombstones and ghosts per rung, computed in code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graveyard.py autopsy&lt;/code&gt;: how each rung's answers fail (cut off, looping, writing into the thinking block)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graveyard.py repeat&lt;/code&gt;: two runs of one rung compared question by question&lt;/li&gt;
&lt;li&gt;one run of the whole ladder plus Google's QAT Q4_0 on a Tesla T4, committed with its report&lt;/li&gt;
&lt;li&gt;a second pass with thinking on, over seven rungs from BF16 to Q3_K_M&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h4&gt;
  
  
  The Results in Brief
&lt;/h4&gt;

&lt;p&gt;Q5_K_M is the best trade between speed and accuracy on the T4. It keeps BF16's score, 231 right against 229, at 39.0% of the bytes and 1.97 times BF16's decode speed. The 2 extra right answers come from the 8 questions where the two disagree, 5 ghosts against 3 dead; two equally accurate models split 8 questions at least that unevenly about three times in four, so the gap is within the noise. Q4_0 is the fastest rung at 2.34 times BF16, and costs 9 right answers against Q5_K_M for 1.19 times its speed. Q4_K_M sits between the two.&lt;/p&gt;

&lt;p&gt;Every other rung is beaten on both counts by one of those three: Q8_0 and Q6_K are slower than Q5_K_M with fewer or equal right answers, and Q3_K_M is slower than Q4_0 with 31 fewer. Below Q4 the accuracy collapses: Q3_K_M loses 38 and Q2_K gets 3 right. With thinking on, the order holds: Q5_K_M 230, Q4_K_M 226 and Q4_0 219, against BF16's 227.&lt;/p&gt;

&lt;p&gt;The cover is the graveyard. One row per rung, BF16 at the top and Q2_K at the bottom, one cell per question: marigold where the rung answers right, a grey headstone where it answers wrong. &lt;code&gt;docs/make_cover.py&lt;/code&gt; draws it from the committed rows.&lt;/p&gt;

&lt;p&gt;The ladder, from &lt;code&gt;graveyard.py bury&lt;/code&gt;, on 250 GSM8K test questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;% of BF16 bytes&lt;/th&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Dead&lt;/th&gt;
&lt;th&gt;Ghosts&lt;/th&gt;
&lt;th&gt;Median tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;100.0&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;40.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;100.0&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;39.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;53.3&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;64.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;41.3&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;70.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;39.0&lt;/td&gt;
&lt;td&gt;🥇 231&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;36.8&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;84.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;36.1&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;🥇 93.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;34.4&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;74.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;32.1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Dead&lt;/strong&gt;: BF16 right, this rung wrong. &lt;strong&gt;Ghosts&lt;/strong&gt;: BF16 wrong, this rung right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma-graveyard&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ladder.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Converts the checkpoint to BF16 and F16 GGUF, quantizes seven rungs from BF16, writes &lt;code&gt;ladder.tsv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f16_range.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counts the weights that overflow or underflow when BF16 is cast to F16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graveyard.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run&lt;/code&gt;, &lt;code&gt;bury&lt;/code&gt;, &lt;code&gt;autopsy&lt;/code&gt; and &lt;code&gt;repeat&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs/think_derived.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Thinking on against thinking off, question by question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lazy_eval.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompt, answer extraction and grading, reused from Week 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every command for the T4, in order, and how to resume after the VM stops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runs/2026-10-27-ladder/run-ladder.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs the nine rungs in order, skips finished ones, then buries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runs/2026-10-27-ladder/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REPORT.md&lt;/code&gt;, the bury and autopsy tables, per-question tombstones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A GCE &lt;code&gt;n1-standard-2&lt;/code&gt; in &lt;code&gt;us-west2-b&lt;/code&gt; with one Tesla T4, 15,360 MiB, driver 615.71.10, 2 vCPU and 7,436 MiB of RAM&lt;/li&gt;
&lt;li&gt;llama.cpp built with CUDA for compute capability 7.5, at &lt;code&gt;b11699&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; from Hugging Face, the BF16 checkpoint, text model only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/code&gt;, Google's quantization-aware Q4_0 file&lt;/li&gt;
&lt;li&gt;A clone: &lt;code&gt;git clone https://github.com/xbill9/gemma-graveyard&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Build the Ladder
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ladder.sh&lt;/code&gt; converts the checkpoint twice, to BF16 and to F16, then quantizes seven rungs from the BF16 file with plain &lt;code&gt;llama-quantize&lt;/code&gt;, with no importance matrix. That is the default a user gets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TMPDIR=/opt1/tmp ./ladder.sh /opt1/models/gemma-4-E2B-it /opt1/graveyard-gguf /opt1/llama.cpp
rung    file    bytes   sha256
BF16    /opt1/graveyard-gguf/e2b-BF16.gguf  9311305152  9fe40af9a72302151a5abc28a5057c5fe335eb80145e795ba745085fc73a2ad6
F16 /opt1/graveyard-gguf/e2b-F16.gguf   9311305152  5ab927e4b24a248de319e70467fc74d4ab87347ee9d6fb3ee60b293b12198cf8
Q8_0    /opt1/graveyard-gguf/e2b-Q8_0.gguf  4967497152  6b0301104c89a43c3bec5bae4b14d744f6000bc21e00e46e887fbbfe024de38a
Q6_K    /opt1/graveyard-gguf/e2b-Q6_K.gguf  3845346752  e800e0389f48646994d8ff6d3409dbf0c13b3fc217d0647b481385da33bd3d99
Q5_K_M  /opt1/graveyard-gguf/e2b-Q5_K_M.gguf    3630288320  f6c3f4395a9d3839535d8c14ae48ab784096945cfc4485005853eede915cb4a9
Q4_K_M  /opt1/graveyard-gguf/e2b-Q4_K_M.gguf    3427880384  dd9dc465bb535989205f158d77f28f6f8348e47d8ac8870920c152b5996cbc16
Q4_0    /opt1/graveyard-gguf/e2b-Q4_0.gguf  3360161216  2d84758474be87d0a74edfa98efbeb602484e26f2e5fa8695fd9641ad323c7b2
Q3_K_M  /opt1/graveyard-gguf/e2b-Q3_K_M.gguf    3201351104  b10c1f38c93635ccee1c7f382e03f5d2f4e9516087d7d62a73ca7de0f52693ab
Q2_K    /opt1/graveyard-gguf/e2b-Q2_K.gguf  2989088192  65850f1a31efed2013ed148adc5dc937e973a8e79fd55cdba31edd53369fddb2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reference is BF16 because that is the checkpoint's own dtype, so the BF16 GGUF holds the trained weights unchanged. F16 is a rung.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Check What F16 Costs Before Running It
&lt;/h4&gt;

&lt;p&gt;F16 has more mantissa than BF16 and less exponent range, so the cast can overflow or round small weights to zero. &lt;code&gt;f16_range.py&lt;/code&gt; counts both on the checkpoint, one line per tensor and a total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 f16_range.py /opt1/models/gemma-4-E2B-it/model.safetensors &amp;gt; runs/2026-10-27-f16-range/out.txt
grep -E 'position_embedding_table|TOTAL' runs/2026-10-27-f16-range/out.txt
{"tensor": "model.vision_tower.patch_embedder.position_embedding_table", "n": 15728640, "overflow": 0, "underflow": 5507733, "subnormal": 834716, "absmax": 0.431640625}
{"tensor": "TOTAL", "tensors": 2011, "n": 5123178979, "overflow": 0, "underflow": 5523265, "subnormal": 7013885, "absmax": 900.0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No weight overflows. Of the 5,523,265 that underflow, 5,507,733 are in the vision tower's position table, which the text GGUF does not carry.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Ask One Rung 250 Questions
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;graveyard.py run&lt;/code&gt; starts its own &lt;code&gt;llama-server&lt;/code&gt; on one GGUF and asks each question once: temperature 0, one request at a time, no prompt cache, thinking off, 1,024 max tokens. Only the reply's &lt;code&gt;content&lt;/code&gt; is graded, in code: the number after &lt;code&gt;Answer:&lt;/code&gt;, compared with the reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py run BF16 /opt1/graveyard-gguf/e2b-BF16.gguf data/heldout.jsonl runs/2026-10-27-ladder --llama /opt1/llama.cpp
{"rung": "BF16", "n": 250, "right": 229, "errors": 0, "truncated": 4, "median_decode_tps": 40.0, "mean_tokens": 391.2}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole model fits on the T4 at BF16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_tensors: offloaded 36/36 layers to GPU
load_tensors:   CPU_Mapped model buffer size =   768.00 MiB
load_tensors:        CUDA0 model buffer size =  4384.87 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔎 &lt;strong&gt;Tip: Gemma 4 E2B keeps its embedding tables in CPU memory.&lt;/strong&gt; The per-layer embedding table holds 2,348,810,240 of the checkpoint's weights, and llama.cpp leaves it memory-mapped on the CPU with &lt;code&gt;-ngl 99&lt;/code&gt;. So each rung's GPU memory is about half its file size: 4,671 MiB in use at BF16 for a 9.3 GB file, 1,639 MiB at Q4_0, 1,287 MiB at Q2_K.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Prove the Same Rung Gives the Same Answers
&lt;/h4&gt;

&lt;p&gt;A tombstone means something only if the same model on the same question always gives the same answer. BF16 on 50 questions, twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for i in 1 2; do
  ./graveyard.py run BF16 /opt1/graveyard-gguf/e2b-BF16.gguf data/heldout.jsonl runs/2026-10-27-determinism-$i \
    --llama /opt1/llama.cpp --limit 50
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two runs compared in code, question by question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 mismatched extracted, 0 mismatched text, 46/50 both times
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every answer and every character of text matched. The same holds below BF16. &lt;code&gt;graveyard.py repeat&lt;/code&gt; compares a second full run of four rungs with the ladder's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py repeat runs/2026-10-27-ladder runs/2026-10-28-repeat Q4_0 Q3_K_M Q8_0 F16 --common
{"rung": "Q4_0", "n": 250, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 222, "right_b": 222}
{"rung": "Q3_K_M", "n": 250, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 191, "right_b": 191}
{"rung": "Q8_0", "n": 250, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 227, "right_b": 227}
{"rung": "F16", "n": 250, "mismatched_extracted": 0, "mismatched_text": 0, "mismatched_right": 0, "right_a": 229, "right_b": 229}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A changed answer at a lower rung comes from the weights.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run the Ladder and Bury It
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;runs/2026-10-27-ladder/run-ladder.sh&lt;/code&gt; runs the nine rungs in order and skips any rung whose results are complete, so it picks up again after the VM's evening stop. Then &lt;code&gt;bury&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-27-ladder BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M Q2_K --json graveyard-ladder.json
{"first_deaths_by_rung": {"Q2_K": 177, "Q3_K_M": 26, "Q4_0": 9, "Q8_0": 4, "Q5_K_M": 2, "Q4_K_M": 6, "Q6_K": 1, "F16": 1}, "tombstones": 226, "ghosts": 11}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;226 tombstones and 11 ghosts: 226 different questions die at some rung, and 11 questions BF16 gets wrong come back right at one rung or more. The first-death counts add up to the tombstones, one per question; the per-rung dead counts in the ladder table add up to more, because one question can be dead at several rungs. Per-question detail is in &lt;code&gt;graveyard-ladder.json&lt;/code&gt;. The one question that dies at F16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{'id': 'test-687', 'died_at': 'F16', 'answer': '13', 'said': 17.0, 'right_again_at': ['Q8_0', 'Q6_K', 'Q5_K_M', 'Q4_K_M']}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"One twin is 7 years older than one triplet. If their combined ages are 44, how old is one of the twins?" BF16 answers 13, from two twins and three triplets. F16 adds up two twins and one triplet, and answers 17. Four lower rungs answer 13 again.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Autopsy: How Each Rung Dies
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;graveyard.py autopsy&lt;/code&gt; counts, per rung, answers cut off at 1,024 tokens, answers that repeat one line five or more times, and answers that put text in the thinking block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py autopsy runs/2026-10-27-ladder BF16 F16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M Q2_K
{"rung": "Q3_K_M", "cut_off": 49, "looped": 0, "thought": 0, "empty_content": 0, "empty_right_in_reasoning": 0, "dead": 42, "dead_cut_off": 34, "dead_looped": 0, "dead_empty_content": 0, "first_deaths": 26, "first_deaths_right_again_lower": 0}
{"rung": "Q2_K", "cut_off": 246, "looped": 228, "thought": 75, "empty_content": 63, "empty_right_in_reasoning": 2, "dead": 226, "dead_cut_off": 222, "dead_looped": 207, "dead_empty_content": 60, "first_deaths": 177, "first_deaths_right_again_lower": 0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every rung:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Cut off&lt;/th&gt;
&lt;th&gt;Looped&lt;/th&gt;
&lt;th&gt;Thinking unprompted&lt;/th&gt;
&lt;th&gt;Dead&lt;/th&gt;
&lt;th&gt;Dead, cut off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F16&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2_K&lt;/td&gt;
&lt;td&gt;246&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three kinds of death:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;F16 to Q4_0, a different road through the same problem.&lt;/strong&gt; 23 first deaths; 16 of them are right again at some lower rung. Down to Q5_K_M most of the dead answers end normally with a wrong number: 4 of 12 were cut off. At Q4_K_M and Q4_0, 11 of 24 were. From F16 to Q5_K_M, ghosts (1 to 5 per rung) match deaths (1 to 4), so the score holds. From Q4_K_M down, deaths outnumber ghosts, 9 to 4 and 15 to 8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3_K_M, out of room.&lt;/strong&gt; 34 of its 42 deaths were cut off at 1,024 tokens, still working, with no repeated lines. BF16 used a median 466.5 tokens on the 44 questions Q3_K_M got wrong while cut off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q2_K, collapse.&lt;/strong&gt; 246 of 250 answers run to the limit and 228 repeat a line five or more times. With thinking off, 75 answers open a thinking block anyway, and in 63 of them the whole answer lands there, leaving &lt;code&gt;content&lt;/code&gt; empty. No other rung writes into the thinking block at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔎 &lt;strong&gt;Tip: give two buries in one directory two names.&lt;/strong&gt; &lt;code&gt;bury&lt;/code&gt; writes its per-question tombstones to &lt;code&gt;graveyard.json&lt;/code&gt;; &lt;code&gt;--json&lt;/code&gt; picks the file, so the QAT comparison in Step 7 keeps the ladder's file intact.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Google's QAT Q4_0 Beside the Plain Q4_0
&lt;/h4&gt;

&lt;p&gt;Google publishes a Q4_0 GGUF of Gemma 4 E2B trained for 4-bit (quantization-aware training). Its weights differ from the base checkpoint, so it sits beside the ladder, against the same BF16 reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-27-ladder BF16 QAT_Q4_0 Q4_0 --json graveyard-qat.json
{"rung": "QAT_Q4_0", "n": 250, "right": 221, "errors": 0, "truncated": 6, "median_decode_tps": 87.8, "mean_tokens": 391.9, "dead": 15, "ghosts": 7, "answer_changed": 27, "bytes": 3349516256, "gpu_mib_loaded": 1629, "gpu_mib_before": 0}
{"rung": "Q4_0", "n": 250, "right": 222, "errors": 0, "truncated": 10, "median_decode_tps": 93.6, "mean_tokens": 430.4, "dead": 15, "ghosts": 8, "answer_changed": 27, "bytes": 3360161216, "gpu_mib_loaded": 1639, "gpu_mib_before": 0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same number of deaths, same number of changed answers, one right answer apart. The graves differ: 6 of the 15 dead are the same question, 9 die only under QAT and 9 only under the plain Q4_0. QAT runs out of room less often: 2 of its deaths were cut off at 1,024 tokens, against 8 for the plain Q4_0.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Bury the Ladder Again With Thinking On
&lt;/h4&gt;

&lt;p&gt;Gemma 4 can think before it answers. &lt;code&gt;--think&lt;/code&gt; turns it on; the thinking goes to &lt;code&gt;reasoning_content&lt;/code&gt; and only &lt;code&gt;content&lt;/code&gt; is graded. Thinking needs room, so this pass allows 4,096 tokens in an 8,192-token context. It covers seven rungs, BF16 to Q3_K_M; F16, Q2_K and QAT Q4_0 were not run with thinking on. Each rung is buried against BF16 with thinking on, which gets 227 right. A second BF16 run of 50 questions matched the first exactly, 47 right both times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./graveyard.py bury runs/2026-10-28-think BF16 Q8_0 Q6_K Q5_K_M Q4_K_M Q4_0 Q3_K_M --json graveyard.json
{"rung": "BF16", "n": 250, "right": 227, "errors": 0, "truncated": 0, "median_decode_tps": 34.5, "mean_tokens": 842.2, "dead": 0, "ghosts": 0, "answer_changed": 0, "bytes": 9311305152, "gpu_mib_loaded": 4699, "gpu_mib_before": 0}
{"rung": "Q3_K_M", "n": 250, "right": 208, "errors": 0, "truncated": 11, "median_decode_tps": 68.0, "mean_tokens": 1320.1, "dead": 26, "ghosts": 7, "answer_changed": 42, "bytes": 3201351104, "gpu_mib_loaded": 1517, "gpu_mib_before": 0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docs/think_derived.py&lt;/code&gt; lines the two passes up question by question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 docs/think_derived.py | tail -3
 "q3_dead_cut_off_with_thinking_off": 34,
 "of_them_right_with_thinking_on": 21
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Right, thinking off&lt;/th&gt;
&lt;th&gt;Right, thinking on&lt;/th&gt;
&lt;th&gt;Dead, off&lt;/th&gt;
&lt;th&gt;Dead, on&lt;/th&gt;
&lt;th&gt;Dead in both&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q8_0&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;🥇 230&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q6_K&lt;/td&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;🥇 230&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5_K_M&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;td&gt;🥇 230&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_K_M&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4_0&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3_K_M&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Q3_K_M recovers most of its loss.&lt;/strong&gt; It gains 17 right answers. Of the 34 questions it lost by running out of room at 1,024 tokens, 21 are right with thinking on and 4,096 tokens. Both the thinking and the larger limit changed at once, so this run cannot say which one did it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Above Q3_K_M the score holds and the graves move.&lt;/strong&gt; Each rung stays within 3 answers of its thinking-off score, but the questions it loses are almost all new: 0 of the deaths at Q8_0, Q6_K and Q5_K_M are shared between the two passes, 1 of 9 at Q4_K_M and 3 of 15 at Q4_0. BF16 itself gets 218 questions right in both modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost is time.&lt;/strong&gt; Answers use 2.08 to 2.18 times as many tokens, decoding runs at 0.79 to 0.93 of the thinking-off speed, and each rung takes 2.22 to 2.70 times as long on the T4.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Q5_K_M&lt;/th&gt;
&lt;th&gt;Q4_K_M&lt;/th&gt;
&lt;th&gt;Q4_0&lt;/th&gt;
&lt;th&gt;QAT Q4_0&lt;/th&gt;
&lt;th&gt;Q3_K_M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right of 250 (BF16: 229)&lt;/td&gt;
&lt;td&gt;🥇 231&lt;/td&gt;
&lt;td&gt;🥈 224&lt;/td&gt;
&lt;td&gt;🥉 222&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;🥇 3&lt;/td&gt;
&lt;td&gt;🥈 9&lt;/td&gt;
&lt;td&gt;🥉 15&lt;/td&gt;
&lt;td&gt;🥉 15&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU MiB&lt;/td&gt;
&lt;td&gt;1,897&lt;/td&gt;
&lt;td&gt;1,705&lt;/td&gt;
&lt;td&gt;🥉 1,639&lt;/td&gt;
&lt;td&gt;🥈 1,629&lt;/td&gt;
&lt;td&gt;🥇 1,489&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median tok/s on the T4&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;🥉 84.1&lt;/td&gt;
&lt;td&gt;🥇 93.6&lt;/td&gt;
&lt;td&gt;🥈 87.8&lt;/td&gt;
&lt;td&gt;74.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answers cut off&lt;/td&gt;
&lt;td&gt;🥇 3&lt;/td&gt;
&lt;td&gt;🥈 6&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;🥈 6&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;🟢 same score as BF16&lt;/td&gt;
&lt;td&gt;🟢 small loss&lt;/td&gt;
&lt;td&gt;⚠️ 15 dead&lt;/td&gt;
&lt;td&gt;⚠️ 15 dead, fewer cut off&lt;/td&gt;
&lt;td&gt;❌ 38 fewer right&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;To ship Gemma 4 E2B on a small GPU with BF16's score, take Q5_K_M: 39.0% of the bytes, 231 right against 229, and 1.97 times BF16's decode speed on the T4 (arithmetic from the medians). For the fastest file that stays within 7 answers, take Q4_0 or Q4_K_M. Below Q4, the losses stop being a few flipped answers: Q3_K_M saves 150 MiB of GPU memory over Q4_0 and costs 31 right answers, and Q2_K stops answering. With thinking on and 4,096 tokens, Q3_K_M gets 208 right, 11 short of Q4_0, at more than twice the time per question.&lt;/p&gt;




&lt;h4&gt;
  
  
  Prior Work
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt, answer extraction and grading&lt;/strong&gt; (&lt;code&gt;lazy_eval.py&lt;/code&gt;) and &lt;strong&gt;the 250-question GSM8K split&lt;/strong&gt; (&lt;code&gt;data/&lt;/code&gt;, seed 0): from my Week 3 entry, &lt;a href="https://github.com/xbill9/lazy-gemma" rel="noopener noreferrer"&gt;lazy-gemma&lt;/a&gt;, whose grader came from my earlier Gemma 4 TPU work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source used&lt;/strong&gt;: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; (MIT) for conversion, quantization and serving; Gemma 4 E2B and Google's QAT Q4_0 GGUF (Gemma terms); &lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A graveyard needs the same model at nine sizes, run the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The weights are on the disk, so the ladder is built here.&lt;/strong&gt; &lt;code&gt;ladder.sh&lt;/code&gt; produces every rung from one BF16 file with the open converter and quantizer, and &lt;code&gt;ladder.tsv&lt;/code&gt; records each file's sha256. A hosted API serves whichever quantization its provider chose, and it can change without notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same answer twice.&lt;/strong&gt; Greedy decoding on a fixed llama.cpp build gave identical text on every repeated run, BF16 to Q3_K_M, so every tombstone counts. That check needs control of the server, the seed and the build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure is visible.&lt;/strong&gt; Q2_K's answers in the thinking block show up only because llama-server returns &lt;code&gt;reasoning_content&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; separately and every row is kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-token bill.&lt;/strong&gt; Ten models times 250 questions ran on one rented T4.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;Every row in the graveyard is a grade-school maths problem, and each tombstone keeps its own epitaph:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;test-687&lt;/strong&gt;, twins and triplets, answer 13. Died at F16, said 17. Rose again at Q8_0, Q6_K, Q5_K_M and Q4_K_M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test-2&lt;/strong&gt;, a house flipped for profit, answer 70,000. Died at Q4_0, said 120,000. Never rose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test-564&lt;/strong&gt;, Judy's two weeks of reading, answer 240. Died at Q8_0, said 3. Rose again at every rung from Q6_K to Q4_0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most graves above Q3_K_M have someone come back to them: 16 of 23.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemma&lt;/strong&gt;: Gemma 4 E2B, quantized locally with llama.cpp at nine levels and compared with Google's quantization-aware Q4_0 release, served on one Tesla T4.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The goal of this project was to find which answers Gemma 4 E2B loses at each quantization level, one question at a time. The key was a deterministic run: greedy decoding on one llama.cpp build, checked to give identical text twice at five rungs, so every changed answer comes from the weights. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Q8_0, Q6_K and Q5_K_M stay within 2 right answers of BF16's 229 of 250; Q5_K_M scores 231 at 39.0% of the bytes.&lt;/li&gt;
&lt;li&gt;🟢 Every rung fits on the T4, BF16 included: all 36 layers on the GPU, 4,671 MiB in use, with the embedding tables in CPU memory.&lt;/li&gt;
&lt;li&gt;🟢 Above Q3_K_M, 16 of the 23 questions that die are right again at a lower rung.&lt;/li&gt;
&lt;li&gt;⚠️ Q4_K_M and Q4_0 lose 5 and 7, with 9 and 15 deaths.&lt;/li&gt;
&lt;li&gt;⚠️ Google's QAT Q4_0 scores 221 against the plain Q4_0's 222; the two share 6 of their 15 deaths.&lt;/li&gt;
&lt;li&gt;🟢 Q4_0, Q3_K_M, Q8_0 and F16 run a second time give the same text on all 250 questions.&lt;/li&gt;
&lt;li&gt;⚠️ With thinking on, rungs above Q3_K_M keep their score but lose different questions; at Q4_0, 3 of 15 deaths are shared.&lt;/li&gt;
&lt;li&gt;⚠️ Thinking on costs 2.1 times the tokens and 2.2 to 2.7 times the time per rung.&lt;/li&gt;
&lt;li&gt;❌ Q3_K_M loses 38; 34 of its 42 deaths are cut off at 1,024 tokens. With thinking on and 4,096 tokens it loses 19.&lt;/li&gt;
&lt;li&gt;❌ Q2_K gets 3 right: 246 answers hit the token limit, 228 loop, and 75 write into a thinking block with thinking off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one Tesla T4 on GCE, llama.cpp &lt;code&gt;b11699&lt;/code&gt;, 250 GSM8K test questions per rung, greedy. Thinking off with a 1,024-token limit on all ten models; thinking on with a 4,096-token limit on seven rungs, BF16 to Q3_K_M. Determinism checked at BF16, F16, Q8_0, Q4_0 and Q3_K_M. No importance matrix, none of the other 1,069 GSM8K test questions, and no other machine. Speeds compare rungs on this T4 only.&lt;/p&gt;

&lt;p&gt;The strategy for burying Gemma 4's answers one quantization rung at a time was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard" rel="noopener noreferrer"&gt;gemma-graveyard on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma-graveyard/blob/main/runs/2026-10-27-ladder/REPORT.md" rel="noopener noreferrer"&gt;Run report: runs/2026-10-27-ladder/REPORT.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/lazy-gemma" rel="noopener noreferrer"&gt;lazy-gemma, the Week 3 entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf" rel="noopener noreferrer"&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/grade-school-math" rel="noopener noreferrer"&gt;GSM8K&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>llamacpp</category>
    </item>
    <item>
      <title>Size Your Model to Where You Run It: Gemma 4 E2B and E4B on an AWS L4 and Two Small GPUs</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:30:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/size-your-model-to-where-you-run-it-gemma-4-e2b-and-e4b-on-an-aws-l4-and-two-small-gpus-3c9p</link>
      <guid>https://dev.to/aws-builders/size-your-model-to-where-you-run-it-gemma-4-e2b-and-e4b-on-an-aws-l4-and-two-small-gpus-3c9p</guid>
      <description>&lt;p&gt;This article provides a step by step guide to serving Gemma 4 E2B and E4B, Google's quantization-aware trained (QAT) small models, on an NVIDIA L4 in an AWS EC2 G6 instance with llama.cpp, and compares the result with the same models on two small local GPUs: an 8 GB RTX 3050 desktop card and a 4 GB GTX 1650 Ti laptop card. Every card runs the same llama.cpp build, the same model files, the same drafter and the same tests. The measures are generation speed for one user and for many, what the drafter adds on each card and why, answer quality, and cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For one user, the 24 GB L4 is 1.2-1.3x an 8 GB desktop card, with the same answers.&lt;/strong&gt; The 4-bit small models use 13-29% of the L4's memory, and the rest of the card sits idle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma 4, 4-bit QAT, one user&lt;/th&gt;
&lt;th&gt;GTX 1650 Ti, 4 GB laptop&lt;/th&gt;
&lt;th&gt;RTX 3050 OEM, 8 GB desktop&lt;/th&gt;
&lt;th&gt;NVIDIA L4, 24 GB, AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B, no drafter&lt;/td&gt;
&lt;td&gt;76.5 tok/s&lt;/td&gt;
&lt;td&gt;108.2 tok/s&lt;/td&gt;
&lt;td&gt;133.4 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, best drafter setting&lt;/td&gt;
&lt;td&gt;90.3 tok/s&lt;/td&gt;
&lt;td&gt;151.2 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217.6 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, no drafter&lt;/td&gt;
&lt;td&gt;38.0 tok/s&lt;/td&gt;
&lt;td&gt;58.3 tok/s&lt;/td&gt;
&lt;td&gt;74.0 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, best drafter setting&lt;/td&gt;
&lt;td&gt;56.5 tok/s&lt;/td&gt;
&lt;td&gt;100.0 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;129.7 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3050 as a share of the L4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;69-81%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The desktop card delivers 79-81% of the L4 without the drafter and 69-77% with it. The L4 earns its memory with many users at once (1.6x the 3050 at 64) and with models too large for 8 GB. The rule this article arrives at: size the model to where it runs.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Being Measured?
&lt;/h4&gt;

&lt;p&gt;Gemma 4 E2B and E4B are the two small models of the family, served here as exact 4-bit (Q4_0) GGUF files whose every block sits on Google's QAT grid: &lt;code&gt;xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/code&gt; and &lt;code&gt;xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf&lt;/code&gt;. Google also ships a QAT drafter for each, a small model that proposes the next few tokens for the main model to check in one pass; it is stored the same way, as &lt;code&gt;xbill9/gemma-4-E{2,4}B-it-qat-assistant-q4_0-exact-gguf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When one user is generating, every token reads every weight of the model once. The speed limit is then how fast the card reads its memory, and the size of the memory plays no part as long as the model fits. That is why a card with three times the memory can be only a little faster.&lt;/p&gt;

&lt;p&gt;The three cards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GTX 1650 Ti Max-Q&lt;/th&gt;
&lt;th&gt;RTX 3050 OEM&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;laptop&lt;/td&gt;
&lt;td&gt;i7-5820K desktop&lt;/td&gt;
&lt;td&gt;EC2 &lt;code&gt;g6.2xlarge&lt;/code&gt;, 8 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;AWS &lt;code&gt;us-east-1&lt;/code&gt;, spot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three run llama.cpp &lt;code&gt;77c4540ce&lt;/code&gt; with flash attention, a 16-bit context cache and the per-layer embedding table on the card. The L4 build uses CUDA 13.1 for sm_89.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 1 — Serve the Rigs
&lt;/h4&gt;

&lt;p&gt;Each model has a rig directory with a &lt;code&gt;tpu.env&lt;/code&gt; holding every setting and a &lt;code&gt;Makefile&lt;/code&gt; to run on the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/gemma4-dev
&lt;span class="nb"&gt;cd &lt;/span&gt;gemma4-dev/gpu-llamacpp-g6-2b-q4_0      &lt;span class="c"&gt;# or gpu-llamacpp-g6-4b-q4_0&lt;/span&gt;
make toolkit build download draft serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The G6 instance comes from &lt;code&gt;gpu-pytorch-g6-2b-q4w4a16&lt;/code&gt;'s MCP tool &lt;code&gt;create_g6_instance&lt;/code&gt;, on the PyTorch Deep Learning AMI. The AMI has an NVIDIA driver (595.112.07, CUDA 13.2) and no CUDA compiler, so &lt;code&gt;make toolkit&lt;/code&gt; installs CUDA 13.1 from NVIDIA's apt repository, one minor version below what the driver supports. &lt;code&gt;make build&lt;/code&gt; compiles llama.cpp for sm_89 in 633 s.&lt;/p&gt;

&lt;p&gt;The serving line, E2B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; gemma-4-E2B-it-q4_0-exact.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-c&lt;/span&gt; 131072 &lt;span class="nt"&gt;-ctk&lt;/span&gt; f16 &lt;span class="nt"&gt;-ctv&lt;/span&gt; f16 &lt;span class="nt"&gt;-fa&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; 4 &lt;span class="nt"&gt;-tb&lt;/span&gt; 8 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1 &lt;span class="nt"&gt;--reasoning&lt;/span&gt; off &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="nt"&gt;-ot&lt;/span&gt; &lt;span class="s1"&gt;'per_layer_token_embd\.weight=CUDA0'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-md&lt;/span&gt; gemma-4-E2B-it-qat-assistant-q4_0-exact.gguf &lt;span class="nt"&gt;--spec-type&lt;/span&gt; draft-mtp &lt;span class="nt"&gt;-ngld&lt;/span&gt; all &lt;span class="nt"&gt;--spec-draft-n-max&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the full 131,072-token context with the drafter, E2B peaks at 3,952 MiB and E4B at 6,774 MiB of the L4's 23,034.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Measure One User
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;speed.py&lt;/code&gt; sends six prompts (code, chat, story, explanation, facts, summary), 256 tokens each, three greedy repeats per prompt with the prompt cache off, and reports the geometric mean over prompts. Each arm gets its own server, started on a cooled card. On the L4:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drafted tokens&lt;/th&gt;
&lt;th&gt;E2B tok/s&lt;/th&gt;
&lt;th&gt;x none&lt;/th&gt;
&lt;th&gt;E4B tok/s&lt;/th&gt;
&lt;th&gt;x none&lt;/th&gt;
&lt;th&gt;tokens kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;133.4&lt;/td&gt;
&lt;td&gt;1.000x&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;td&gt;1.000x&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;185.5&lt;/td&gt;
&lt;td&gt;1.391x&lt;/td&gt;
&lt;td&gt;106.8&lt;/td&gt;
&lt;td&gt;1.443x&lt;/td&gt;
&lt;td&gt;0.60-0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;211.0&lt;/td&gt;
&lt;td&gt;1.582x&lt;/td&gt;
&lt;td&gt;121.8&lt;/td&gt;
&lt;td&gt;1.646x&lt;/td&gt;
&lt;td&gt;0.48-0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.631x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;129.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.754x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;196.6&lt;/td&gt;
&lt;td&gt;1.474x&lt;/td&gt;
&lt;td&gt;129.7&lt;/td&gt;
&lt;td&gt;1.754x&lt;/td&gt;
&lt;td&gt;0.30-0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A no-drafter arm run last lands 0.7% (E2B) and 1.2% (E4B) under the first. Code gains most from the drafter (2.39x E2B, 2.55x E4B at 3 tokens) and stories least (1.31x, 1.38x).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With the drafter, E4B runs at 0.972x E2B without it&lt;/strong&gt;: 129.7 against 133.4 tok/s.&lt;/p&gt;

&lt;p&gt;The same prompts on the two local cards, best setting per card:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;1650 Ti&lt;/th&gt;
&lt;th&gt;3050&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B best&lt;/td&gt;
&lt;td&gt;1 token, 1.18x&lt;/td&gt;
&lt;td&gt;2 tokens, 1.40x&lt;/td&gt;
&lt;td&gt;3 tokens, 1.63x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B best&lt;/td&gt;
&lt;td&gt;1 token, 1.49x&lt;/td&gt;
&lt;td&gt;2 tokens, 1.72x&lt;/td&gt;
&lt;td&gt;3 tokens, 1.75x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔎 Tip: The Card Sets the Draft Length
&lt;/h4&gt;

&lt;p&gt;The drafter's guesses are kept at the same rate on every card. What changes is what each guess costs. Splitting each reply into steps (one pass of the main model over the last token plus the drafted ones) shows it, here for E2B:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drafted tokens&lt;/th&gt;
&lt;th&gt;tokens per step, all three cards&lt;/th&gt;
&lt;th&gt;step time vs a plain step: 1650 Ti&lt;/th&gt;
&lt;th&gt;3050&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.60&lt;/td&gt;
&lt;td&gt;1.35x&lt;/td&gt;
&lt;td&gt;1.16x&lt;/td&gt;
&lt;td&gt;1.15x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.92-1.98&lt;/td&gt;
&lt;td&gt;1.77x&lt;/td&gt;
&lt;td&gt;1.39x&lt;/td&gt;
&lt;td&gt;1.26x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2.12-2.20&lt;/td&gt;
&lt;td&gt;2.12x&lt;/td&gt;
&lt;td&gt;1.63x&lt;/td&gt;
&lt;td&gt;1.36x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speed-up is tokens per step divided by step time. On the L4 &lt;code&gt;llama-bench&lt;/code&gt; times the main model over 1 to 6 tokens in one pass: 4 tokens take at most 1.04x the time of 1, at 0, 8,192 and 32,768 tokens of context. So checking 3 guesses is close to free there, and the added time is the drafter's own, 0.75-1.18 ms per guess. That is 33% of an E2B step at 3 guesses and 19% of E4B's longer step, which is why E4B gains more from its drafter. A slower card pays more per guess and peaks at fewer.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Hold a Long Conversation
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;longctx.py&lt;/code&gt; puts the first D tokens of a long document ahead of a story, a code and a summary request, at &lt;code&gt;-c 131072&lt;/code&gt;. Mean speed-up over the requests at 2 drafted tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tokens ahead&lt;/th&gt;
&lt;th&gt;E2B, L4&lt;/th&gt;
&lt;th&gt;E2B, 3050&lt;/th&gt;
&lt;th&gt;E4B, L4&lt;/th&gt;
&lt;th&gt;E4B, 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.70x&lt;/td&gt;
&lt;td&gt;1.51x&lt;/td&gt;
&lt;td&gt;1.75x&lt;/td&gt;
&lt;td&gt;1.86x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;1.53x&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1.69x&lt;/td&gt;
&lt;td&gt;1.64x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120,000&lt;/td&gt;
&lt;td&gt;1.49x&lt;/td&gt;
&lt;td&gt;1.21x&lt;/td&gt;
&lt;td&gt;1.57x&lt;/td&gt;
&lt;td&gt;1.55x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation without the drafter slows with context (E2B story 134.1 tok/s empty, 66.7 at 120,000 ahead on the L4), and the drafter keeps paying at every depth on both cards.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Serve Many Users at Once
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;mixed.py&lt;/code&gt; sends c requests at once, each one of the six chat prompts, 256 tokens, five repeats, one shared context cache (&lt;code&gt;-kvu&lt;/code&gt;) at &lt;code&gt;-c 32768 --parallel 64&lt;/code&gt;. Total output tok/s:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;requests at once&lt;/th&gt;
&lt;th&gt;E2B, 3050&lt;/th&gt;
&lt;th&gt;E2B, L4&lt;/th&gt;
&lt;th&gt;E4B, 3050&lt;/th&gt;
&lt;th&gt;E4B, L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;106.1&lt;/td&gt;
&lt;td&gt;131.4&lt;/td&gt;
&lt;td&gt;57.3&lt;/td&gt;
&lt;td&gt;73.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;297.1&lt;/td&gt;
&lt;td&gt;505.2&lt;/td&gt;
&lt;td&gt;199.3&lt;/td&gt;
&lt;td&gt;309.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;569.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;918.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;472.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;778.1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;This is where the L4's size shows: 1.61-1.65x the 3050 at 64 requests.&lt;/strong&gt; The drafter (2 tokens) helps on the L4 up to 16 requests (E2B 1.06-1.19x, E4B 1.15-1.34x) and costs output at 64 (0.87x for both).&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Check the Answers
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;gen_eval.py&lt;/code&gt; runs GSM8K (1,319 grade-school math questions, up to 2,048 tokens per reply) and BFCL simple (400 single function calls) at &lt;code&gt;-c 32768&lt;/code&gt;, 8 requests at once, with and without the drafter at 2 tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GSM8K, none / drafter&lt;/th&gt;
&lt;th&gt;BFCL simple, none / drafter&lt;/th&gt;
&lt;th&gt;GSM8K time, none / drafter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;0.8992 / 0.8984&lt;/td&gt;
&lt;td&gt;0.930 / 0.925&lt;/td&gt;
&lt;td&gt;1,014 s / 713 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;0.9310 / 0.9287&lt;/td&gt;
&lt;td&gt;0.910 / 0.915&lt;/td&gt;
&lt;td&gt;1,416 s / 893 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Paired question by question, every drafter-against-none difference has a 95% range that includes zero. The 3050 scored E2B 0.8984 / 0.9022 and E4B 0.9340 / 0.9325 on GSM8K under the same settings: the same model files give the same answers on every card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E4B answers more GSM8K questions right than E2B&lt;/strong&gt;, +0.0318 without the drafter (range +0.0167 to +0.0470) and +0.0303 with it, and BFCL is level between the two. With its drafter E4B finishes GSM8K in 893 s, sooner than E2B without one (1,014 s).&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Price It
&lt;/h4&gt;

&lt;p&gt;The L4 instance, &lt;code&gt;g6.2xlarge&lt;/code&gt; in &lt;code&gt;us-east-1&lt;/code&gt;, listed at $0.9776/h on demand and $0.8393/h spot on the day of the runs. Per million output tokens, one machine kept busy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;L4, spot&lt;/th&gt;
&lt;th&gt;tok/s&lt;/th&gt;
&lt;th&gt;$ per million tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B, one user, no drafter&lt;/td&gt;
&lt;td&gt;133.4&lt;/td&gt;
&lt;td&gt;$1.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, one user, drafter&lt;/td&gt;
&lt;td&gt;217.6&lt;/td&gt;
&lt;td&gt;$1.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, one user, drafter&lt;/td&gt;
&lt;td&gt;129.7&lt;/td&gt;
&lt;td&gt;$1.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, 64 users&lt;/td&gt;
&lt;td&gt;918.9&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, 64 users&lt;/td&gt;
&lt;td&gt;778.1&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One user pays for the whole card while using 13-29% of its memory. Filled with 64 requests, the same card costs a sixth to a seventh as much per token. A local card's cost is the card and its electricity, neither measured here.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Where the Bigger Card Earns Its Keep
&lt;/h4&gt;

&lt;p&gt;The 8 GB 3050 runs every larger Gemma 4 too, at a price paid in speed once the model outgrows the card. From the same series on the same card:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;on the RTX 3050, 8 GB&lt;/th&gt;
&lt;th&gt;where the model lives&lt;/th&gt;
&lt;th&gt;generation, with drafter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12B, 6.7 GB&lt;/td&gt;
&lt;td&gt;all on the card&lt;/td&gt;
&lt;td&gt;55.1 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B A4B, 14.4 GB&lt;/td&gt;
&lt;td&gt;experts of 21 of 30 layers in system RAM&lt;/td&gt;
&lt;td&gt;33.7 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B, 17.7 GB&lt;/td&gt;
&lt;td&gt;40 of 60 layers in system RAM&lt;/td&gt;
&lt;td&gt;4.14 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three fit a 24 GB card whole; this article measures them on the 3050 only.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma 4 E2B / E4B, 4-bit QAT&lt;/th&gt;
&lt;th&gt;🥇 RTX 3050, 8 GB, local&lt;/th&gt;
&lt;th&gt;🥈 NVIDIA L4, 24 GB, AWS&lt;/th&gt;
&lt;th&gt;🥉 GTX 1650 Ti, 4 GB, local&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One user, E2B / E4B, best&lt;/td&gt;
&lt;td&gt;151.2 / 100.0 tok/s&lt;/td&gt;
&lt;td&gt;217.6 / 129.7 tok/s&lt;/td&gt;
&lt;td&gt;90.3 / 56.5 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 users, E2B / E4B&lt;/td&gt;
&lt;td&gt;569.5 / 472.4 tok/s&lt;/td&gt;
&lt;td&gt;918.9 / 778.1 tok/s&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best draft length&lt;/td&gt;
&lt;td&gt;2 tokens&lt;/td&gt;
&lt;td&gt;3 tokens&lt;/td&gt;
&lt;td&gt;1 token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSM8K, E4B with drafter&lt;/td&gt;
&lt;td&gt;0.9325&lt;/td&gt;
&lt;td&gt;0.9287&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;owned&lt;/td&gt;
&lt;td&gt;$0.84/h spot&lt;/td&gt;
&lt;td&gt;owned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;For one user on E2B or E4B, a local 8 GB card. It delivers 69-81% of the L4's speed with the same answers, and the model uses a fraction of either card.&lt;/p&gt;

&lt;p&gt;For many users at once, the L4. At 64 requests it serves 1.6x the 3050's output, and at about $0.25-0.30 per million tokens on spot.&lt;/p&gt;

&lt;p&gt;For quality at small-model speed, E4B with its drafter. On the L4 it generates at 0.97x E2B without a drafter and scores 3 points higher on GSM8K.&lt;/p&gt;

&lt;p&gt;For the 26B and 31B, a card that holds them. On 8 GB they spill into system RAM and slow to 34 and 4 tok/s.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to find what a 24 GB cloud GPU buys over small local GPUs when serving Gemma 4's small models. The key to the answer was running the same llama.cpp build, model files, drafter and tests on all three cards. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 One user: the L4 is 1.23-1.27x the 3050 without the drafter and 1.30-1.44x with it, at 13-29% of its memory&lt;/li&gt;
&lt;li&gt;🟢 The same model files give the same answers on every card; the drafter costs no GSM8K or BFCL answers&lt;/li&gt;
&lt;li&gt;🟢 The drafter's guesses are kept at the same rate on every card; the card's cost per guess sets the best draft length, 1 / 2 / 3 tokens on the 1650 Ti / 3050 / L4&lt;/li&gt;
&lt;li&gt;🟢 E4B with its drafter generates at 0.97x E2B without one, 3 GSM8K points higher&lt;/li&gt;
&lt;li&gt;🟢 64 users: the L4 serves 1.6x the 3050, $0.25-0.30 per million tokens on spot&lt;/li&gt;
&lt;li&gt;⚠️ The drafter helps on the L4 up to 16 users at once and costs output at 64&lt;/li&gt;
&lt;li&gt;❌ One user on the L4 pays for a whole card and uses under a third of its memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: Gemma 4 E2B-it and E4B-it QAT only, exact Q4_0 GGUF files and exact Q4_0 QAT drafters, text only, llama.cpp &lt;code&gt;77c4540ce&lt;/code&gt; on all three cards, 16-bit context cache, thinking off. One L4 instance on one day (spot &lt;code&gt;g6.2xlarge&lt;/code&gt;, &lt;code&gt;us-east-1d&lt;/code&gt;); the 3050 (i7-5820K desktop) and 1650 Ti (laptop) runs are from 2026-10-14 and 2026-10-15 on different hosts with the same prompts and scripts, so cross-card ratios are indicative. The many-users test uses short chat prompts and a shared context cache. Larger cloud GPUs and the 12B, 26B and 31B on the L4 were not run.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for local accelerator comparison was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Part 2, Do Drafters Work on Small Models?: &lt;a href="https://dev.to/gde/do-drafters-work-on-small-models-gemma-4-e2b-and-e4b-on-two-small-gpus-1b4g"&gt;https://dev.to/gde/do-drafters-work-on-small-models-gemma-4-e2b-and-e4b-on-two-small-gpus-1b4g&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6, A Dense 31B on an 8 GB GPU: &lt;a href="https://dev.to/gde/a-dense-31b-on-an-8-gb-gpu-gemma-4-31b-on-an-rtx-3050-26p9"&gt;https://dev.to/gde/a-dense-31b-on-an-8-gb-gpu-gemma-4-31b-on-an-rtx-3050-26p9&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-4b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-4b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Drafter cost and many users on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-cost-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-cost-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B GGUF: &lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B GGUF: &lt;a href="https://huggingface.co/xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B drafter: &lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-assistant-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E2B-it-qat-assistant-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B drafter: &lt;a href="https://huggingface.co/xbill9/gemma-4-E4B-it-qat-assistant-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E4B-it-qat-assistant-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;llama.cpp: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;https://github.com/ggml-org/llama.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon EC2 G6 instances: &lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;https://aws.amazon.com/ec2/instance-types/g6/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>aws</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Size Your Model to Where You Run It: Gemma 4 E2B and E4B on an AWS L4 and Two Small GPUs</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:30:52 +0000</pubDate>
      <link>https://dev.to/gde/size-your-model-to-where-you-run-it-gemma-4-e2b-and-e4b-on-an-aws-l4-and-two-small-gpus-1d47</link>
      <guid>https://dev.to/gde/size-your-model-to-where-you-run-it-gemma-4-e2b-and-e4b-on-an-aws-l4-and-two-small-gpus-1d47</guid>
      <description>&lt;p&gt;This article provides a step by step guide to serving Gemma 4 E2B and E4B, Google's quantization-aware trained (QAT) small models, on an NVIDIA L4 in an AWS EC2 G6 instance with llama.cpp, and compares the result with the same models on two small local GPUs: an 8 GB RTX 3050 desktop card and a 4 GB GTX 1650 Ti laptop card. Every card runs the same llama.cpp build, the same model files, the same drafter and the same tests. The measures are generation speed for one user and for many, what the drafter adds on each card and why, answer quality, and cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For one user, the 24 GB L4 is 1.2-1.3x an 8 GB desktop card, with the same answers.&lt;/strong&gt; The 4-bit small models use 13-29% of the L4's memory, and the rest of the card sits idle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma 4, 4-bit QAT, one user&lt;/th&gt;
&lt;th&gt;GTX 1650 Ti, 4 GB laptop&lt;/th&gt;
&lt;th&gt;RTX 3050 OEM, 8 GB desktop&lt;/th&gt;
&lt;th&gt;NVIDIA L4, 24 GB, AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B, no drafter&lt;/td&gt;
&lt;td&gt;76.5 tok/s&lt;/td&gt;
&lt;td&gt;108.2 tok/s&lt;/td&gt;
&lt;td&gt;133.4 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, best drafter setting&lt;/td&gt;
&lt;td&gt;90.3 tok/s&lt;/td&gt;
&lt;td&gt;151.2 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217.6 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, no drafter&lt;/td&gt;
&lt;td&gt;38.0 tok/s&lt;/td&gt;
&lt;td&gt;58.3 tok/s&lt;/td&gt;
&lt;td&gt;74.0 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, best drafter setting&lt;/td&gt;
&lt;td&gt;56.5 tok/s&lt;/td&gt;
&lt;td&gt;100.0 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;129.7 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3050 as a share of the L4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;69-81%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The desktop card delivers 79-81% of the L4 without the drafter and 69-77% with it. The L4 earns its memory with many users at once (1.6x the 3050 at 64) and with models too large for 8 GB. The rule this article arrives at: size the model to where it runs.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Being Measured?
&lt;/h4&gt;

&lt;p&gt;Gemma 4 E2B and E4B are the two small models of the family, served here as exact 4-bit (Q4_0) GGUF files whose every block sits on Google's QAT grid: &lt;code&gt;xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/code&gt; and &lt;code&gt;xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf&lt;/code&gt;. Google also ships a QAT drafter for each, a small model that proposes the next few tokens for the main model to check in one pass; it is stored the same way, as &lt;code&gt;xbill9/gemma-4-E{2,4}B-it-qat-assistant-q4_0-exact-gguf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When one user is generating, every token reads every weight of the model once. The speed limit is then how fast the card reads its memory, and the size of the memory plays no part as long as the model fits. That is why a card with three times the memory can be only a little faster.&lt;/p&gt;

&lt;p&gt;The three cards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GTX 1650 Ti Max-Q&lt;/th&gt;
&lt;th&gt;RTX 3050 OEM&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;laptop&lt;/td&gt;
&lt;td&gt;i7-5820K desktop&lt;/td&gt;
&lt;td&gt;EC2 &lt;code&gt;g6.2xlarge&lt;/code&gt;, 8 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;AWS &lt;code&gt;us-east-1&lt;/code&gt;, spot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three run llama.cpp &lt;code&gt;77c4540ce&lt;/code&gt; with flash attention, a 16-bit context cache and the per-layer embedding table on the card. The L4 build uses CUDA 13.1 for sm_89.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 1 — Serve the Rigs
&lt;/h4&gt;

&lt;p&gt;Each model has a rig directory with a &lt;code&gt;tpu.env&lt;/code&gt; holding every setting and a &lt;code&gt;Makefile&lt;/code&gt; to run on the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/gemma4-dev
&lt;span class="nb"&gt;cd &lt;/span&gt;gemma4-dev/gpu-llamacpp-g6-2b-q4_0      &lt;span class="c"&gt;# or gpu-llamacpp-g6-4b-q4_0&lt;/span&gt;
make toolkit build download draft serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The G6 instance comes from &lt;code&gt;gpu-pytorch-g6-2b-q4w4a16&lt;/code&gt;'s MCP tool &lt;code&gt;create_g6_instance&lt;/code&gt;, on the PyTorch Deep Learning AMI. The AMI has an NVIDIA driver (595.112.07, CUDA 13.2) and no CUDA compiler, so &lt;code&gt;make toolkit&lt;/code&gt; installs CUDA 13.1 from NVIDIA's apt repository, one minor version below what the driver supports. &lt;code&gt;make build&lt;/code&gt; compiles llama.cpp for sm_89 in 633 s.&lt;/p&gt;

&lt;p&gt;The serving line, E2B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; gemma-4-E2B-it-q4_0-exact.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-c&lt;/span&gt; 131072 &lt;span class="nt"&gt;-ctk&lt;/span&gt; f16 &lt;span class="nt"&gt;-ctv&lt;/span&gt; f16 &lt;span class="nt"&gt;-fa&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; 4 &lt;span class="nt"&gt;-tb&lt;/span&gt; 8 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1 &lt;span class="nt"&gt;--reasoning&lt;/span&gt; off &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="nt"&gt;-ot&lt;/span&gt; &lt;span class="s1"&gt;'per_layer_token_embd\.weight=CUDA0'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-md&lt;/span&gt; gemma-4-E2B-it-qat-assistant-q4_0-exact.gguf &lt;span class="nt"&gt;--spec-type&lt;/span&gt; draft-mtp &lt;span class="nt"&gt;-ngld&lt;/span&gt; all &lt;span class="nt"&gt;--spec-draft-n-max&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the full 131,072-token context with the drafter, E2B peaks at 3,952 MiB and E4B at 6,774 MiB of the L4's 23,034.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Measure One User
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;speed.py&lt;/code&gt; sends six prompts (code, chat, story, explanation, facts, summary), 256 tokens each, three greedy repeats per prompt with the prompt cache off, and reports the geometric mean over prompts. Each arm gets its own server, started on a cooled card. On the L4:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drafted tokens&lt;/th&gt;
&lt;th&gt;E2B tok/s&lt;/th&gt;
&lt;th&gt;x none&lt;/th&gt;
&lt;th&gt;E4B tok/s&lt;/th&gt;
&lt;th&gt;x none&lt;/th&gt;
&lt;th&gt;tokens kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;133.4&lt;/td&gt;
&lt;td&gt;1.000x&lt;/td&gt;
&lt;td&gt;74.0&lt;/td&gt;
&lt;td&gt;1.000x&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;185.5&lt;/td&gt;
&lt;td&gt;1.391x&lt;/td&gt;
&lt;td&gt;106.8&lt;/td&gt;
&lt;td&gt;1.443x&lt;/td&gt;
&lt;td&gt;0.60-0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;211.0&lt;/td&gt;
&lt;td&gt;1.582x&lt;/td&gt;
&lt;td&gt;121.8&lt;/td&gt;
&lt;td&gt;1.646x&lt;/td&gt;
&lt;td&gt;0.48-0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;217.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.631x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;129.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.754x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;196.6&lt;/td&gt;
&lt;td&gt;1.474x&lt;/td&gt;
&lt;td&gt;129.7&lt;/td&gt;
&lt;td&gt;1.754x&lt;/td&gt;
&lt;td&gt;0.30-0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A no-drafter arm run last lands 0.7% (E2B) and 1.2% (E4B) under the first. Code gains most from the drafter (2.39x E2B, 2.55x E4B at 3 tokens) and stories least (1.31x, 1.38x).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With the drafter, E4B runs at 0.972x E2B without it&lt;/strong&gt;: 129.7 against 133.4 tok/s.&lt;/p&gt;

&lt;p&gt;The same prompts on the two local cards, best setting per card:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;1650 Ti&lt;/th&gt;
&lt;th&gt;3050&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B best&lt;/td&gt;
&lt;td&gt;1 token, 1.18x&lt;/td&gt;
&lt;td&gt;2 tokens, 1.40x&lt;/td&gt;
&lt;td&gt;3 tokens, 1.63x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B best&lt;/td&gt;
&lt;td&gt;1 token, 1.49x&lt;/td&gt;
&lt;td&gt;2 tokens, 1.72x&lt;/td&gt;
&lt;td&gt;3 tokens, 1.75x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔎 Tip: The Card Sets the Draft Length
&lt;/h4&gt;

&lt;p&gt;The drafter's guesses are kept at the same rate on every card. What changes is what each guess costs. Splitting each reply into steps (one pass of the main model over the last token plus the drafted ones) shows it, here for E2B:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drafted tokens&lt;/th&gt;
&lt;th&gt;tokens per step, all three cards&lt;/th&gt;
&lt;th&gt;step time vs a plain step: 1650 Ti&lt;/th&gt;
&lt;th&gt;3050&lt;/th&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.60&lt;/td&gt;
&lt;td&gt;1.35x&lt;/td&gt;
&lt;td&gt;1.16x&lt;/td&gt;
&lt;td&gt;1.15x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.92-1.98&lt;/td&gt;
&lt;td&gt;1.77x&lt;/td&gt;
&lt;td&gt;1.39x&lt;/td&gt;
&lt;td&gt;1.26x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2.12-2.20&lt;/td&gt;
&lt;td&gt;2.12x&lt;/td&gt;
&lt;td&gt;1.63x&lt;/td&gt;
&lt;td&gt;1.36x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speed-up is tokens per step divided by step time. On the L4 &lt;code&gt;llama-bench&lt;/code&gt; times the main model over 1 to 6 tokens in one pass: 4 tokens take at most 1.04x the time of 1, at 0, 8,192 and 32,768 tokens of context. So checking 3 guesses is close to free there, and the added time is the drafter's own, 0.75-1.18 ms per guess. That is 33% of an E2B step at 3 guesses and 19% of E4B's longer step, which is why E4B gains more from its drafter. A slower card pays more per guess and peaks at fewer.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Hold a Long Conversation
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;longctx.py&lt;/code&gt; puts the first D tokens of a long document ahead of a story, a code and a summary request, at &lt;code&gt;-c 131072&lt;/code&gt;. Mean speed-up over the requests at 2 drafted tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tokens ahead&lt;/th&gt;
&lt;th&gt;E2B, L4&lt;/th&gt;
&lt;th&gt;E2B, 3050&lt;/th&gt;
&lt;th&gt;E4B, L4&lt;/th&gt;
&lt;th&gt;E4B, 3050&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.70x&lt;/td&gt;
&lt;td&gt;1.51x&lt;/td&gt;
&lt;td&gt;1.75x&lt;/td&gt;
&lt;td&gt;1.86x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;1.53x&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1.69x&lt;/td&gt;
&lt;td&gt;1.64x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120,000&lt;/td&gt;
&lt;td&gt;1.49x&lt;/td&gt;
&lt;td&gt;1.21x&lt;/td&gt;
&lt;td&gt;1.57x&lt;/td&gt;
&lt;td&gt;1.55x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation without the drafter slows with context (E2B story 134.1 tok/s empty, 66.7 at 120,000 ahead on the L4), and the drafter keeps paying at every depth on both cards.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Serve Many Users at Once
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;mixed.py&lt;/code&gt; sends c requests at once, each one of the six chat prompts, 256 tokens, five repeats, one shared context cache (&lt;code&gt;-kvu&lt;/code&gt;) at &lt;code&gt;-c 32768 --parallel 64&lt;/code&gt;. Total output tok/s:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;requests at once&lt;/th&gt;
&lt;th&gt;E2B, 3050&lt;/th&gt;
&lt;th&gt;E2B, L4&lt;/th&gt;
&lt;th&gt;E4B, 3050&lt;/th&gt;
&lt;th&gt;E4B, L4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;106.1&lt;/td&gt;
&lt;td&gt;131.4&lt;/td&gt;
&lt;td&gt;57.3&lt;/td&gt;
&lt;td&gt;73.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;297.1&lt;/td&gt;
&lt;td&gt;505.2&lt;/td&gt;
&lt;td&gt;199.3&lt;/td&gt;
&lt;td&gt;309.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;569.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;918.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;472.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;778.1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;This is where the L4's size shows: 1.61-1.65x the 3050 at 64 requests.&lt;/strong&gt; The drafter (2 tokens) helps on the L4 up to 16 requests (E2B 1.06-1.19x, E4B 1.15-1.34x) and costs output at 64 (0.87x for both).&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Check the Answers
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;gen_eval.py&lt;/code&gt; runs GSM8K (1,319 grade-school math questions, up to 2,048 tokens per reply) and BFCL simple (400 single function calls) at &lt;code&gt;-c 32768&lt;/code&gt;, 8 requests at once, with and without the drafter at 2 tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;L4&lt;/th&gt;
&lt;th&gt;GSM8K, none / drafter&lt;/th&gt;
&lt;th&gt;BFCL simple, none / drafter&lt;/th&gt;
&lt;th&gt;GSM8K time, none / drafter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;0.8992 / 0.8984&lt;/td&gt;
&lt;td&gt;0.930 / 0.925&lt;/td&gt;
&lt;td&gt;1,014 s / 713 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;0.9310 / 0.9287&lt;/td&gt;
&lt;td&gt;0.910 / 0.915&lt;/td&gt;
&lt;td&gt;1,416 s / 893 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Paired question by question, every drafter-against-none difference has a 95% range that includes zero. The 3050 scored E2B 0.8984 / 0.9022 and E4B 0.9340 / 0.9325 on GSM8K under the same settings: the same model files give the same answers on every card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E4B answers more GSM8K questions right than E2B&lt;/strong&gt;, +0.0318 without the drafter (range +0.0167 to +0.0470) and +0.0303 with it, and BFCL is level between the two. With its drafter E4B finishes GSM8K in 893 s, sooner than E2B without one (1,014 s).&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Price It
&lt;/h4&gt;

&lt;p&gt;The L4 instance, &lt;code&gt;g6.2xlarge&lt;/code&gt; in &lt;code&gt;us-east-1&lt;/code&gt;, listed at $0.9776/h on demand and $0.8393/h spot on the day of the runs. Per million output tokens, one machine kept busy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;L4, spot&lt;/th&gt;
&lt;th&gt;tok/s&lt;/th&gt;
&lt;th&gt;$ per million tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B, one user, no drafter&lt;/td&gt;
&lt;td&gt;133.4&lt;/td&gt;
&lt;td&gt;$1.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, one user, drafter&lt;/td&gt;
&lt;td&gt;217.6&lt;/td&gt;
&lt;td&gt;$1.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, one user, drafter&lt;/td&gt;
&lt;td&gt;129.7&lt;/td&gt;
&lt;td&gt;$1.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B, 64 users&lt;/td&gt;
&lt;td&gt;918.9&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B, 64 users&lt;/td&gt;
&lt;td&gt;778.1&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One user pays for the whole card while using 13-29% of its memory. Filled with 64 requests, the same card costs a sixth to a seventh as much per token. A local card's cost is the card and its electricity, neither measured here.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Where the Bigger Card Earns Its Keep
&lt;/h4&gt;

&lt;p&gt;The 8 GB 3050 runs every larger Gemma 4 too, at a price paid in speed once the model outgrows the card. From the same series on the same card:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;on the RTX 3050, 8 GB&lt;/th&gt;
&lt;th&gt;where the model lives&lt;/th&gt;
&lt;th&gt;generation, with drafter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12B, 6.7 GB&lt;/td&gt;
&lt;td&gt;all on the card&lt;/td&gt;
&lt;td&gt;55.1 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B A4B, 14.4 GB&lt;/td&gt;
&lt;td&gt;experts of 21 of 30 layers in system RAM&lt;/td&gt;
&lt;td&gt;33.7 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B, 17.7 GB&lt;/td&gt;
&lt;td&gt;40 of 60 layers in system RAM&lt;/td&gt;
&lt;td&gt;4.14 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three fit a 24 GB card whole; this article measures them on the 3050 only.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma 4 E2B / E4B, 4-bit QAT&lt;/th&gt;
&lt;th&gt;🥇 RTX 3050, 8 GB, local&lt;/th&gt;
&lt;th&gt;🥈 NVIDIA L4, 24 GB, AWS&lt;/th&gt;
&lt;th&gt;🥉 GTX 1650 Ti, 4 GB, local&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One user, E2B / E4B, best&lt;/td&gt;
&lt;td&gt;151.2 / 100.0 tok/s&lt;/td&gt;
&lt;td&gt;217.6 / 129.7 tok/s&lt;/td&gt;
&lt;td&gt;90.3 / 56.5 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 users, E2B / E4B&lt;/td&gt;
&lt;td&gt;569.5 / 472.4 tok/s&lt;/td&gt;
&lt;td&gt;918.9 / 778.1 tok/s&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best draft length&lt;/td&gt;
&lt;td&gt;2 tokens&lt;/td&gt;
&lt;td&gt;3 tokens&lt;/td&gt;
&lt;td&gt;1 token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSM8K, E4B with drafter&lt;/td&gt;
&lt;td&gt;0.9325&lt;/td&gt;
&lt;td&gt;0.9287&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;owned&lt;/td&gt;
&lt;td&gt;$0.84/h spot&lt;/td&gt;
&lt;td&gt;owned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;For one user on E2B or E4B, a local 8 GB card. It delivers 69-81% of the L4's speed with the same answers, and the model uses a fraction of either card.&lt;/p&gt;

&lt;p&gt;For many users at once, the L4. At 64 requests it serves 1.6x the 3050's output, and at about $0.25-0.30 per million tokens on spot.&lt;/p&gt;

&lt;p&gt;For quality at small-model speed, E4B with its drafter. On the L4 it generates at 0.97x E2B without a drafter and scores 3 points higher on GSM8K.&lt;/p&gt;

&lt;p&gt;For the 26B and 31B, a card that holds them. On 8 GB they spill into system RAM and slow to 34 and 4 tok/s.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to find what a 24 GB cloud GPU buys over small local GPUs when serving Gemma 4's small models. The key to the answer was running the same llama.cpp build, model files, drafter and tests on all three cards. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 One user: the L4 is 1.23-1.27x the 3050 without the drafter and 1.30-1.44x with it, at 13-29% of its memory&lt;/li&gt;
&lt;li&gt;🟢 The same model files give the same answers on every card; the drafter costs no GSM8K or BFCL answers&lt;/li&gt;
&lt;li&gt;🟢 The drafter's guesses are kept at the same rate on every card; the card's cost per guess sets the best draft length, 1 / 2 / 3 tokens on the 1650 Ti / 3050 / L4&lt;/li&gt;
&lt;li&gt;🟢 E4B with its drafter generates at 0.97x E2B without one, 3 GSM8K points higher&lt;/li&gt;
&lt;li&gt;🟢 64 users: the L4 serves 1.6x the 3050, $0.25-0.30 per million tokens on spot&lt;/li&gt;
&lt;li&gt;⚠️ The drafter helps on the L4 up to 16 users at once and costs output at 64&lt;/li&gt;
&lt;li&gt;❌ One user on the L4 pays for a whole card and uses under a third of its memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: Gemma 4 E2B-it and E4B-it QAT only, exact Q4_0 GGUF files and exact Q4_0 QAT drafters, text only, llama.cpp &lt;code&gt;77c4540ce&lt;/code&gt; on all three cards, 16-bit context cache, thinking off. One L4 instance on one day (spot &lt;code&gt;g6.2xlarge&lt;/code&gt;, &lt;code&gt;us-east-1d&lt;/code&gt;); the 3050 (i7-5820K desktop) and 1650 Ti (laptop) runs are from 2026-10-14 and 2026-10-15 on different hosts with the same prompts and scripts, so cross-card ratios are indicative. The many-users test uses short chat prompts and a shared context cache. Larger cloud GPUs and the 12B, 26B and 31B on the L4 were not run.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for local accelerator comparison was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Part 2, Do Drafters Work on Small Models?: &lt;a href="https://dev.to/gde/do-drafters-work-on-small-models-gemma-4-e2b-and-e4b-on-two-small-gpus-1b4g"&gt;https://dev.to/gde/do-drafters-work-on-small-models-gemma-4-e2b-and-e4b-on-two-small-gpus-1b4g&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 6, A Dense 31B on an 8 GB GPU: &lt;a href="https://dev.to/gde/a-dense-31b-on-an-8-gb-gpu-gemma-4-31b-on-an-rtx-3050-26p9"&gt;https://dev.to/gde/a-dense-31b-on-an-8-gb-gpu-gemma-4-31b-on-an-rtx-3050-26p9&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-4b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-4b-q4_0/benchmarks/runs/2026-10-26-drafter-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Drafter cost and many users on the L4: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-cost-g6/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/gpu-llamacpp-g6-2b-q4_0/benchmarks/runs/2026-10-26-drafter-cost-g6/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B GGUF: &lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E2B-it-qat-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B GGUF: &lt;a href="https://huggingface.co/xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E4B-it-qat-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E2B drafter: &lt;a href="https://huggingface.co/xbill9/gemma-4-E2B-it-qat-assistant-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E2B-it-qat-assistant-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;E4B drafter: &lt;a href="https://huggingface.co/xbill9/gemma-4-E4B-it-qat-assistant-q4_0-exact-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/xbill9/gemma-4-E4B-it-qat-assistant-q4_0-exact-gguf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;llama.cpp: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;https://github.com/ggml-org/llama.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon EC2 G6 instances: &lt;a href="https://aws.amazon.com/ec2/instance-types/g6/" rel="noopener noreferrer"&gt;https://aws.amazon.com/ec2/instance-types/g6/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>aws</category>
      <category>cuda</category>
    </item>
    <item>
      <title>Inference on Google Cloud: Precision, Memory and Capacity on TPU, from v5e to Ironwood</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 25 Oct 2026 23:02:38 +0000</pubDate>
      <link>https://dev.to/gde/inference-on-google-cloud-precision-memory-and-capacity-on-tpu-from-v5e-to-ironwood-5dlf</link>
      <guid>https://dev.to/gde/inference-on-google-cloud-precision-memory-and-capacity-on-tpu-from-v5e-to-ironwood-5dlf</guid>
      <description>&lt;p&gt;This article is the written companion to the talk "Inference on Google Cloud". It covers serving Gemma 4 on Cloud TPU v5e and v6e through vLLM, measured on the gemma4-dev Cloud TPU test setups between July and October 2026, and compares them with the NVIDIA L4 on Google Cloud, Blackwell list prices, an AMD MI300X and AWS Inferentia. Four ideas explain most of the results: the number format the chip computes natively, the memory bandwidth that feeds it, the slice shape, and the rules that decide whether a TPU is there to rent at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hands-on steps run from a second repository, which starts one TPU and serves Gemma 4 on it with a single command:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gcp-inference" rel="noopener noreferrer"&gt;https://github.com/xbill9/gcp-inference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The slides: &lt;a href="https://docs.google.com/presentation/d/1hT8UXOEBueZfoFdIH0ZyhYhQ3WZJ_0xbCsvNDDs8H4A/edit" rel="noopener noreferrer"&gt;https://docs.google.com/presentation/d/1hT8UXOEBueZfoFdIH0ZyhYhQ3WZJ_0xbCsvNDDs8H4A/edit&lt;/a&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is the Problem?
&lt;/h4&gt;

&lt;p&gt;This article compares four TPU generations, the L4 and Blackwell GPUs beside them, and every way Google Cloud prices a TPU chip-hour. The same Gemma 4 E2B serves at 144 tokens a second on a v5e in bf16 and at 243 in int8 with 4-bit embedding tables, on the same chip. A v6e-1 serves 3.3 times the L4's tokens on the same 26B checkpoint. And a zone that reports quota for 1,536 chips can still return a stockout.&lt;/p&gt;




&lt;h4&gt;
  
  
  Which Generations?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chip&lt;/th&gt;
&lt;th&gt;HBM&lt;/th&gt;
&lt;th&gt;Peak bf16&lt;/th&gt;
&lt;th&gt;Native low precision&lt;/th&gt;
&lt;th&gt;Max pod&lt;/th&gt;
&lt;th&gt;On-demand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v5e&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;197 TFLOPs&lt;/td&gt;
&lt;td&gt;int8, 393 TOPs&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;$1.20/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v6e Trillium&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;918 TFLOPs&lt;/td&gt;
&lt;td&gt;int8, 1,836 TOPs&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;$2.70/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5p&lt;/td&gt;
&lt;td&gt;95 GiB&lt;/td&gt;
&lt;td&gt;459 TFLOPs&lt;/td&gt;
&lt;td&gt;int8; fp8 at bf16 rate&lt;/td&gt;
&lt;td&gt;8,960&lt;/td&gt;
&lt;td&gt;$4.20/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU7x Ironwood&lt;/td&gt;
&lt;td&gt;192 GiB&lt;/td&gt;
&lt;td&gt;2,307 TFLOPs&lt;/td&gt;
&lt;td&gt;fp8, 4,614 TFLOPs&lt;/td&gt;
&lt;td&gt;9,216&lt;/td&gt;
&lt;td&gt;$12.00/hr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google's first TPU, in its datacenters since 2015, was an 8-bit inference chip with a 92 TOPS peak, and Google announced Ironwood on 2025-04-09 as "the first Google TPU for the age of inference". The gemma4-dev test setups measured v5e and v6e. v5p and TPU7x rows come from Google's documentation only.&lt;/p&gt;

&lt;p&gt;vLLM reaches the TPU through tpu-inference, a JAX backend. XLA compiles one program per input shape, which makes the compile the largest part of a cold start.&lt;/p&gt;




&lt;h4&gt;
  
  
  At This Point You Should Have…
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;gcloud&lt;/code&gt; CLI with its &lt;code&gt;alpha&lt;/code&gt; and &lt;code&gt;beta&lt;/code&gt; components, signed in with &lt;code&gt;gcloud auth login&lt;/code&gt; and &lt;code&gt;gcloud auth application-default login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A project with Cloud TPU quota for one v5e chip in us-west4-a&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python3&lt;/code&gt; with &lt;code&gt;pip install 'google-adk[extensions]' google-genai mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A clone of &lt;code&gt;xbill9/gcp-inference&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — One TPU, One Command
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;demo.sh&lt;/code&gt; asks for a TPU v5e-1 as a flex-start request in us-west4-a, waits for the node, opens an SSH tunnel to vLLM on the TPU's port 8000, waits for the model to load and compile, then runs a Google ADK agent against it. Flex-start stops the node by itself after 4 hours. On a TPU that is already up, &lt;code&gt;./demo.sh again&lt;/code&gt; skips the first two steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./demo.sh again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== [0m01s] 3/5  Connecting to it over SSH (the demo reaches vLLM on the TPU's port 8000 through a tunnel)
   [0m05s] SSH works
   [0m11s] tunnel open: localhost:8005 -&amp;gt; TPU port 8000
== [0m11s] 4/5  Starting Gemma on vLLM (first start downloads and compiles: several minutes)
   [0m30s] vLLM is answering
  20:21:25Z  PASS  tpu        chat 0.198s; tool run filters=['id &amp;gt;= 10'] quoted=8 3.71s
READY
== [0m37s] 5/5  The demo: Gemma on the TPU counts ids with the count_ids tool from the MCP server
Question: How many of the ids are 10 or more? Answer with the number.
Ids: [0, 2, 3, 20, 21, 22, 23, 10, 11, 12, 13]
True answer: 8  tools: mcp
  tpu        run 1/3  ok   quoted=8  filters=['id &amp;gt;= 10']  3.87s
  tpu        run 2/3  ok   quoted=8  filters=['id &amp;gt;= 10']  0.51s
  tpu        run 3/3  ok   quoted=8  filters=['id &amp;gt;= 10']  0.49s
backend     runs  correct  right-filter  errors  median s  filters sent
tpu            3    3/3             3/3       0      0.51  id &amp;gt;= 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent is an ADK &lt;code&gt;LlmAgent&lt;/code&gt; whose model is &lt;code&gt;LiteLlm&lt;/code&gt; pointed at vLLM's OpenAI-compatible API, and an ADK &lt;code&gt;McpToolset&lt;/code&gt; starts &lt;code&gt;mcp_server.py&lt;/code&gt; over stdio and offers it one tool. The model never counts. It calls &lt;code&gt;count_ids&lt;/code&gt; on that MCP server, which returns the exact count, and each run is graded on the filter it sent as well as the number it quoted. Gemma 4 E2B on the v5e sent &lt;code&gt;id &amp;gt;= 10&lt;/code&gt; in all three runs.&lt;/p&gt;

&lt;p&gt;The node serves &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; in bf16 with &lt;code&gt;vllm/vllm-tpu:v0.30.0&lt;/code&gt;, one chip, no sharding.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — What the 16 GB Chip Holds
&lt;/h4&gt;

&lt;p&gt;vLLM logs the chip's memory as it starts. In order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Init worker | ... | hbm=[(0.0, 15.75)]GiB
Init model | hbm=[(8.97, 15.75)]GiB
Memory statistics | total_hbm_limit_gb=15.75GiB | total_hbm_limit_cap_gb=14.49GiB | total_hbm_used_gb=8.97GiB | total_hbm_avail_gb=5.52GiB
TPU KV cache size: 321,280 tokens, Maximum concurrency for 8,192 tokens per request: 39.22x
Init kv-cache | num_total_layers=15 | num_blocks=[1255, ...] | regular_attn_shape=(num_blocks, (256, 1, 2, 256)) | regular_attn_dtype=bfloat16 | hbm=[(14.51, 15.75)]Gb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HBM on one v5e chip&lt;/th&gt;
&lt;th&gt;GiB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weights, bf16&lt;/td&gt;
&lt;td&gt;8.97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM's ceiling (92% of 15.75)&lt;/td&gt;
&lt;td&gt;14.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Left for the KV cache&lt;/td&gt;
&lt;td&gt;5.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In use after the KV cache is allocated&lt;/td&gt;
&lt;td&gt;14.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Never allocated&lt;/td&gt;
&lt;td&gt;1.24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of the 15 cached layers holds 1,255 blocks of 256 tokens, one KV head of dimension 256, in bf16. That is 1,024 bytes per token per layer, 15,360 bytes per token across all 15, and 321,280 token slots in all. The model's config lists 35 layers with &lt;code&gt;num_kv_shared_layers: 20&lt;/code&gt;: 20 layers reuse another layer's keys and values, so the cache holds 15.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — The Full 131,072-Token Context
&lt;/h4&gt;

&lt;p&gt;Gemma 4 E2B's config allows 131,072 positions. vLLM sizes its KV cache from the memory left after the weights, so raising &lt;code&gt;--max-model-len&lt;/code&gt; from 8,192 to 131,072 changes the concurrency and leaves the cache the same size. To change a flag on a running TPU, replace the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute tpus tpu-vm ssh gcp-inference-tpu-node &lt;span class="nt"&gt;--project&lt;/span&gt; &amp;lt;tpu-project&amp;gt; &lt;span class="nt"&gt;--zone&lt;/span&gt; us-west4-a &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'sudo docker rm -f vllm &amp;amp;&amp;amp; sudo docker run -d --name vllm --restart unless-stopped --privileged --net=host \
   -v /dev/shm:/dev/shm --shm-size 10gb -e HF_HOME=/dev/shm vllm/vllm-tpu:v0.30.0 \
   vllm serve google/gemma-4-E2B-it --served-model-name gemma --max-model-len 131072 --port 8000 \
   --max-num-batched-tokens 4096 --enable-auto-tool-choice --tool-call-parser gemma4'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory statistics | total_hbm_limit_gb=15.75GiB | total_hbm_limit_cap_gb=14.49GiB | total_hbm_used_gb=8.97GiB | total_hbm_avail_gb=5.52GiB
TPU KV cache size: 321,280 tokens, Maximum concurrency for 131,072 tokens per request: 2.45x
init engine (profile, create kv cache, warmup model) took 503.54 s (compilation: 413.41 s)
Application startup complete.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;--max-model-len&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;8,192&lt;/th&gt;
&lt;th&gt;131,072&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;KV cache, tokens&lt;/td&gt;
&lt;td&gt;321,280&lt;/td&gt;
&lt;td&gt;321,280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-length requests at once&lt;/td&gt;
&lt;td&gt;39.22&lt;/td&gt;
&lt;td&gt;2.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine start&lt;/td&gt;
&lt;td&gt;502.84 s&lt;/td&gt;
&lt;td&gt;503.54 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;of which XLA compile&lt;/td&gt;
&lt;td&gt;408.59 s&lt;/td&gt;
&lt;td&gt;413.41 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One full-length request needs 1.875 GiB of KV cache. &lt;code&gt;scripts/longctx.py&lt;/code&gt; in &lt;code&gt;tpu-survey&lt;/code&gt; buries a passphrase at a chosen depth in up to 124,949 tokens of filler, starts each prompt with a unique line so vLLM's prefix cache cannot reuse an earlier run, and asks for the passphrase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 scripts/longctx.py $TPU_URL 8000 0.5
{"prompt_tokens": 7949, "completion_tokens": 10, "depth": 0.5, "seconds": 0.61, "prompt_tokens_per_second": 13110.4, "answer": "violet-harbor-7341", "found": true}
$ python3 scripts/longctx.py $TPU_URL 120000 0.1
{"prompt_tokens": 124949, "completion_tokens": 10, "depth": 0.1, "seconds": 9.53, "prompt_tokens_per_second": 13116.9, "answer": "violet-harbor-7341", "found": true}
$ python3 scripts/longctx.py $TPU_URL 120000 0.5
{"prompt_tokens": 124949, "completion_tokens": 10, "depth": 0.5, "seconds": 9.54, "prompt_tokens_per_second": 13102.3, "answer": "violet-harbor-7341", "found": true}
$ python3 scripts/longctx.py $TPU_URL 120000 0.9
{"prompt_tokens": 124949, "completion_tokens": 10, "depth": 0.9, "seconds": 9.41, "prompt_tokens_per_second": 13274.6, "answer": "violet-harbor-7341", "found": true}
$ python3 scripts/longctx.py $TPU_URL 140000 0.5
HTTP 400: {"error":{"message":"This model's maximum context length is 131072 tokens. However, you requested 16 output tokens and your prompt contains at least 131057 input tokens, for a total of at least 131073 tokens. ..."}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The passphrase comes back at 10%, 50% and 90% depth of a 124,949-token prompt, in 9.4 to 9.5 seconds each, about 13,100 prompt tokens a second as timed from the client. vLLM's own log reads 12,495 tokens a second, a 0.0% prefix cache hit rate, and 38 to 39% of the KV cache in use while the request runs. A prompt past the limit is refused before it reaches the chip.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Raise the Context on the Chip You Have
&lt;/h4&gt;

&lt;p&gt;Flex-start capacity is granted per request. Replacing the container keeps the node, and its 4-hour clock, and costs one engine start, 503.54 s on this v5e. Deleting the node to relaunch it means a new flex-start request, and a new wait for capacity.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Precision: What the Chip Computes
&lt;/h4&gt;

&lt;p&gt;A smaller format saves memory and bandwidth on every chip. It saves compute only where the matrix unit multiplies that format natively.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;v5e&lt;/th&gt;
&lt;th&gt;v6e&lt;/th&gt;
&lt;th&gt;v5p&lt;/th&gt;
&lt;th&gt;TPU7x&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int8&lt;/td&gt;
&lt;td&gt;2x bf16&lt;/td&gt;
&lt;td&gt;2x bf16&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fp8&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;bf16 rate&lt;/td&gt;
&lt;td&gt;2x bf16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int4&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Storage" means the weights widen to bf16 before every matrix multiply. On v5e that ordering shows up directly in E2B throughput at one stream:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;E2B format on v5e-1&lt;/th&gt;
&lt;th&gt;tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit QAT + 4-bit embeddings&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fp8&lt;/td&gt;
&lt;td&gt;182&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fp8 + 4-bit embeddings&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int8 W8A8&lt;/td&gt;
&lt;td&gt;220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int8 + 4-bit embeddings&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Accuracy holds: 0.683 for bf16 against 0.677 for int8 with 4-bit embeddings, on a 3,880-record suite. At 12B, fp8 serves 518 tok/s at 16 streams against 624 for int8 at the same file size.&lt;/p&gt;

&lt;p&gt;On v6e the 4-bit path pays off through a fused kernel. Gemma 4 26B-A4B in 4-bit QAT W4A16 serves 1,283 tok/s at 16 streams against 668 for fp8, in 17.43 GiB of weights against 27.99 GiB, and holds 53,888 KV-cache tokens against 3,456. Accuracy is 75.3% against 76.0%. W4A16 needs tpu-inference PRs #3299, #3653 and #3660 on top of the stock image.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Memory and Bandwidth
&lt;/h4&gt;

&lt;p&gt;Each generated token reads every weight once, so bandwidth sets the ceiling for one request.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chip&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;E2B bf16 tok/s, 1 stream&lt;/th&gt;
&lt;th&gt;tok/s per TB/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TPU v5e&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;859 GB/s&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU v6e&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;1,638 GB/s&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU v5p&lt;/td&gt;
&lt;td&gt;95 GiB&lt;/td&gt;
&lt;td&gt;2,765 GB/s&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU7x&lt;/td&gt;
&lt;td&gt;192 GiB&lt;/td&gt;
&lt;td&gt;7,380 GB/s&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA L4&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;300 GB/s&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;154&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three measured chips land between 122 and 154 tokens a second per TB/s. Google quotes v5e bandwidth as 800 GiBps, which is 859 GB/s. The L4 figure was measured on an AWS g6.2xlarge, the same chip as Google Cloud's g2.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Topologies and Slices
&lt;/h4&gt;

&lt;p&gt;Weight size picks the slice. Every Gemma 4 size serves on one host:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;v5e-1&lt;/th&gt;
&lt;th&gt;v6e-1&lt;/th&gt;
&lt;th&gt;v6e-4, TP=4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;bf16 or int8&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;int8 or 4-bit; bf16 does not fit&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12B&lt;/td&gt;
&lt;td&gt;int8 or 4-bit&lt;/td&gt;
&lt;td&gt;bf16, 22.18 GiB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B-A4B&lt;/td&gt;
&lt;td&gt;4-bit, 201 tok/s&lt;/td&gt;
&lt;td&gt;4-bit, 1,283 tok/s&lt;/td&gt;
&lt;td&gt;bf16, 1,982 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;does not fit&lt;/td&gt;
&lt;td&gt;4-bit, 500 tok/s&lt;/td&gt;
&lt;td&gt;bf16, 2,031 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small models scale out. On a v5e-4, four one-chip E2B engines serve 1.39x the output of one TP=4 engine at 16 streams, 2.52x at 64 and 3.36x at 256. E2B has one KV head, so tensor parallelism keeps four copies of the KV cache. 12B gains from TP=4: 2.25x the v5e-1 output at one stream, and 122,624 KV tokens against 9,728.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Quota Lets You Ask, Availability Decides
&lt;/h4&gt;

&lt;p&gt;Quota is permission to request a TPU. Availability decides whether the request gets hardware, through three checks: hardware in the zone, the provisioning model offered there, and capacity at that moment. 37 zones report v6e quota; 18 offer v6e-1 hardware. Three of five zones holding 1,536 chips of quota returned stockout. us-central1-b has hardware, quota and a flex-start price, and rejects flex-start. Check capacity with a spot request: it fails fast with "reason: stockout", where a flex-start request with no quota queues as PENDING exactly like a stockout.&lt;/p&gt;

&lt;p&gt;Ask for fewer chips. In europe-west4-a a v6e-8 flex-start request expired after 26 minutes, and a v6e-4 was granted at once less than an hour later.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Against Other Accelerators
&lt;/h4&gt;

&lt;p&gt;On the same 26B 4-bit checkpoint, with the same flags and client:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;NVIDIA L4&lt;/th&gt;
&lt;th&gt;TPU v6e-1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output tok/s, 16 x 256 tokens&lt;/td&gt;
&lt;td&gt;393.7&lt;/td&gt;
&lt;td&gt;1,283.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache tokens&lt;/td&gt;
&lt;td&gt;17,990&lt;/td&gt;
&lt;td&gt;53,888&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3,880-record suite, wall time&lt;/td&gt;
&lt;td&gt;139 s&lt;/td&gt;
&lt;td&gt;58 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit kernel&lt;/td&gt;
&lt;td&gt;Marlin&lt;/td&gt;
&lt;td&gt;gmm_v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software&lt;/td&gt;
&lt;td&gt;stock vLLM 0.30.0&lt;/td&gt;
&lt;td&gt;three tpu-inference patches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List price per hour&lt;/td&gt;
&lt;td&gt;$0.71 (g2-standard-4)&lt;/td&gt;
&lt;td&gt;$2.70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloud Run serves the L4 as a managed service: one L4 per instance, weights mounted from Cloud Storage at /mnt/models, a startup check that waits 180 s before its first check, and a default that scales between zero and one instance, so an idle service means a cold GPU start.&lt;/p&gt;

&lt;p&gt;Blackwell on Google Cloud lists above v6e per accelerator-hour: an RTX PRO 6000 on g4-standard-48 at $4.50 on-demand and $2.25 flex-start, and a B200 at $8.06 per GPU flex-start ($64.44 for the 8-GPU a4-highgpu-8g, divided by 8). Both are list prices only, with no measurements in this article.&lt;/p&gt;

&lt;p&gt;Per dollar, on E2B bf16 with &lt;code&gt;vllm bench serve&lt;/code&gt; at 1,024 prompt and 128 output tokens, v5e at its $0.58 spot price leads to 16 streams and the MI300X at $1.99 leads at 64. The v6e-1 is priced at the europe-west4 on-demand rate of $2.97:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tok/s per $/hr&lt;/th&gt;
&lt;th&gt;MI300X ($1.99)&lt;/th&gt;
&lt;th&gt;v5e-1 spot ($0.58)&lt;/th&gt;
&lt;th&gt;v6e-1 europe-west4 ($2.97)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 stream&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 streams&lt;/td&gt;
&lt;td&gt;487&lt;/td&gt;
&lt;td&gt;711&lt;/td&gt;
&lt;td&gt;233&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 streams&lt;/td&gt;
&lt;td&gt;1,333&lt;/td&gt;
&lt;td&gt;1,572&lt;/td&gt;
&lt;td&gt;587&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 streams&lt;/td&gt;
&lt;td&gt;2,713&lt;/td&gt;
&lt;td&gt;1,972&lt;/td&gt;
&lt;td&gt;719&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each software stack fails in its own way:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;TPU, vLLM + JAX&lt;/th&gt;
&lt;th&gt;CUDA, L4&lt;/th&gt;
&lt;th&gt;ROCm, MI300X&lt;/th&gt;
&lt;th&gt;Neuron, inf2 / trn1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start to healthy, E2B&lt;/td&gt;
&lt;td&gt;510 s v6e, 986 s v5e&lt;/td&gt;
&lt;td&gt;334 s&lt;/td&gt;
&lt;td&gt;about 160 s&lt;/td&gt;
&lt;td&gt;171 s on trn1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 support&lt;/td&gt;
&lt;td&gt;4-bit needed patches&lt;/td&gt;
&lt;td&gt;26B 4-bit loads stock&lt;/td&gt;
&lt;td&gt;fp8 only as fnuz&lt;/td&gt;
&lt;td&gt;vLLM-Neuron cannot load it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hazard&lt;/td&gt;
&lt;td&gt;OOM after a long compile&lt;/td&gt;
&lt;td&gt;sm_89 missing; runs sm_86 code&lt;/td&gt;
&lt;td&gt;AITER flag 0.97 to 0.99x&lt;/td&gt;
&lt;td&gt;large gather returns zeros, HTTP 200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Cache the Compile
&lt;/h4&gt;

&lt;p&gt;XLA compile is 81 to 91% of vLLM's engine start for E2B: 329 of 404 seconds on v6e-1 and 738 of 814 on v5e-1, inside 510 and 986 seconds from container start to healthy. On today's v5e-1 with &lt;code&gt;vllm/vllm-tpu:v0.30.0&lt;/code&gt; the engine start was 503.54 seconds, 413.41 of them compile. A mounted compile cache cut a v5e restart from 857 to 497 seconds, 42%. 31B weights load in 5.6 minutes from in-region Cloud Storage against 60 to 90 from Hugging Face.&lt;/p&gt;

&lt;p&gt;Serving flags that work on GPUs fail late on TPU, after the compile. &lt;code&gt;--gpu-memory-utilization 0.95&lt;/code&gt; failed on v5e after a 691-second compile, twice; 0.92 serves. &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt; logs fp8 on v6e and allocates bf16. The &lt;code&gt;vllm/vllm-tpu:nightly&lt;/code&gt; tag broke two 4-bit builds on 2026-10-02, so pin an image digest.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — What Each Chip Costs
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;$ per chip-hour&lt;/th&gt;
&lt;th&gt;On-demand&lt;/th&gt;
&lt;th&gt;Flex-start&lt;/th&gt;
&lt;th&gt;Calendar&lt;/th&gt;
&lt;th&gt;1-yr CUD&lt;/th&gt;
&lt;th&gt;3-yr CUD&lt;/th&gt;
&lt;th&gt;Spot read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v5e&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;td&gt;$0.84&lt;/td&gt;
&lt;td&gt;$0.84&lt;/td&gt;
&lt;td&gt;$0.54&lt;/td&gt;
&lt;td&gt;$0.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v6e Trillium&lt;/td&gt;
&lt;td&gt;$2.70&lt;/td&gt;
&lt;td&gt;$1.35&lt;/td&gt;
&lt;td&gt;$1.89&lt;/td&gt;
&lt;td&gt;$1.89&lt;/td&gt;
&lt;td&gt;$1.22&lt;/td&gt;
&lt;td&gt;$1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5p&lt;/td&gt;
&lt;td&gt;$4.20&lt;/td&gt;
&lt;td&gt;$2.10&lt;/td&gt;
&lt;td&gt;$2.94&lt;/td&gt;
&lt;td&gt;$2.94&lt;/td&gt;
&lt;td&gt;$1.89&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU7x Ironwood&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;td&gt;$6.00&lt;/td&gt;
&lt;td&gt;$8.40&lt;/td&gt;
&lt;td&gt;$8.40&lt;/td&gt;
&lt;td&gt;$5.40&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flex-start halves on-demand on every generation and can undercut spot: v6e in us-east5 lists $1.35 flex-start against a $1.40 spot price. Flex-start runs 10 minutes to 7 days and bills until deleted.&lt;/p&gt;

&lt;p&gt;Utilization sets the price per token. A v6e-1 at the europe-west4 on-demand rate of $2.97 an hour serves E2B at $4.14 per million output tokens with one stream, $0.70 with eight and $0.55 with 32, at 1,024-token prompts. A v5e at the $0.60 flex-start rate goes from $1.31 per million at one stream to $0.09 at 64, at 128-token prompts.&lt;/p&gt;




&lt;h4&gt;
  
  
  Compare and Contrast
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Precision served&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;Measured result&lt;/th&gt;
&lt;th&gt;List $/hr&lt;/th&gt;
&lt;th&gt;Software&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥇 TPU v6e-1&lt;/td&gt;
&lt;td&gt;4-bit QAT W4A16&lt;/td&gt;
&lt;td&gt;1,638 GB/s&lt;/td&gt;
&lt;td&gt;26B at 1,283 tok/s&lt;/td&gt;
&lt;td&gt;$2.70&lt;/td&gt;
&lt;td&gt;tpu-inference, patched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥈 TPU v5e-1&lt;/td&gt;
&lt;td&gt;int8 W8A8 + 4-bit embeddings&lt;/td&gt;
&lt;td&gt;859 GB/s&lt;/td&gt;
&lt;td&gt;E2B at 243 tok/s, one stream&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;tpu-inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥉 NVIDIA L4&lt;/td&gt;
&lt;td&gt;4-bit QAT&lt;/td&gt;
&lt;td&gt;300 GB/s&lt;/td&gt;
&lt;td&gt;26B at 394 tok/s&lt;/td&gt;
&lt;td&gt;$0.71&lt;/td&gt;
&lt;td&gt;stock vLLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU7x Ironwood&lt;/td&gt;
&lt;td&gt;fp8 native&lt;/td&gt;
&lt;td&gt;7,380 GB/s&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;td&gt;allowlisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blackwell B200&lt;/td&gt;
&lt;td&gt;fp4 native&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;$8.06 flex, per GPU&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  So, Which One?
&lt;/h4&gt;

&lt;p&gt;For E2B or E4B at the lowest cost, run v5e-1 in int8 with one engine per chip, on spot or flex-start. For 12B to 31B on one chip, run v6e-1 in 4-bit QAT. For bf16 26B or 31B with long context, run v6e-4 with TP=4. For stock software and the fastest boot, run the L4, on Compute Engine or on Cloud Run. TPU7x brings fp8 math and 192 GiB per chip, behind an allowlist. Whichever you pick: check capacity with a spot request, ask for the smallest slice that fits, pin the image digest and mount the compile cache.&lt;/p&gt;




&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to serve Gemma 4 on a Cloud TPU in one command, then explain the results across TPU generations through native precision, memory bandwidth, slice shape and capacity. The key to the solution was measuring each claim on real hardware, with every ratio and price computed in code. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;One v5e chip holds Gemma 4 E2B's full 131,072-token context&lt;/strong&gt;: a 321,280-token KV cache beside 8.97 GiB of bf16 weights, and a passphrase found at 10%, 50% and 90% depth of a 124,949-token prompt in 9.4 to 9.5 s.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;The agent sends the right filter&lt;/strong&gt;: 3 of 3 runs on the v5e called &lt;code&gt;count_ids&lt;/code&gt; with &lt;code&gt;id &amp;gt;= 10&lt;/code&gt; and quoted 8.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;int8 is the fast format on v5e&lt;/strong&gt;: with 4-bit embedding tables it serves E2B at 1.7x bf16, with accuracy within a point.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;v6e-1 serves 3.3x the L4's tokens&lt;/strong&gt; on the same 26B 4-bit checkpoint, with 3x the KV cache.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Flex-start halves on-demand&lt;/strong&gt; on every generation, and can undercut spot.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Cold start takes 8.5 to 16.4 minutes&lt;/strong&gt;, most of it XLA compile; cache it.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;One-stream decode follows bandwidth&lt;/strong&gt;: 122 to 154 tok/s per TB/s on v5e, v6e and the L4.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Quota is permission to request; availability decides the grant&lt;/strong&gt;: 37 zones report v6e quota, 18 have v6e-1 hardware, and zones holding full quota still return stockout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: the hands-on steps ran on one flex-start v5e-1 in us-west4-a on 2026-10-25, &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; in bf16 on &lt;code&gt;vllm/vllm-tpu:v0.30.0&lt;/code&gt;, three demo runs and one long-context request per depth. The rest covers Gemma 4 E2B to 31B on Cloud TPU v5e and v6e, measured on the gemma4-dev test setups between July and October 2026 with vLLM and tpu-inference; L4 on Google Cloud g2 and AWS g6; MI300X on DigitalOcean; Inferentia2 and Trainium on AWS. The cross-vendor tokens-per-dollar table mixes vLLM builds, hosts and repeat counts, so it shows direction. v5p, TPU7x and Blackwell figures are Google's published specifications and list prices, read on 2026-10-12 and re-read on 2026-10-25 with the same values; spot prices come from the Cloud Billing Catalog on recorded dates.&lt;/p&gt;

&lt;p&gt;The strategy for serving Gemma 4 on Google Cloud from v5e to Ironwood was validated with an incremental step by step approach.&lt;/p&gt;




&lt;h4&gt;
  
  
  References
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;gemma4-dev | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/gcp-inference" rel="noopener noreferrer"&gt;gcp-inference | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xbill9/tpu-survey" rel="noopener noreferrer"&gt;tpu-survey, long-context test and evidence | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/google/gemma-4-E2B-it" rel="noopener noreferrer"&gt;Gemma 4 E2B-it | Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.google.com/presentation/d/1hT8UXOEBueZfoFdIH0ZyhYhQ3WZJ_0xbCsvNDDs8H4A/edit" rel="noopener noreferrer"&gt;Inference on Google Cloud slides | Google Slides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.google.com/presentation/d/1Ax8KGF8T0EYffshY7WfUM61QcHmLk0q72EE7G_hIwgM/edit" rel="noopener noreferrer"&gt;Inference on AWS slides | Google Slides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vllm-project/tpu-inference" rel="noopener noreferrer"&gt;tpu-inference | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/tpu/docs" rel="noopener noreferrer"&gt;Cloud TPU documentation | Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/tpu/docs/tpu7x" rel="noopener noreferrer"&gt;TPU7x (Ironwood) | Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/tpu/pricing" rel="noopener noreferrer"&gt;Cloud TPU pricing | Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/products/compute/pricing/accelerator-optimized" rel="noopener noreferrer"&gt;Accelerator-optimized machine pricing | Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/1704.04760" rel="noopener noreferrer"&gt;In-Datacenter Performance Analysis of a Tensor Processing Unit | arXiv&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>googlecloud</category>
      <category>tpu</category>
      <category>machinelearning</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
