<?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: Google Developer Experts</title>
    <description>The latest articles on DEV Community by Google Developer Experts (gde).</description>
    <link>https://dev.to/gde</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%2Forganization%2Fprofile_image%2F11939%2Fe3080d5b-ecde-42a8-b089-bafecc31fa97.png</url>
      <title>DEV Community: Google Developer Experts</title>
      <link>https://dev.to/gde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gde"/>
    <language>en</language>
    <item>
      <title>Building File4Base: The Modern, Open-Source Alternative to old file bases (Powered by Antigravity)</title>
      <dc:creator>Mario Ezquerro</dc:creator>
      <pubDate>Thu, 24 Sep 2026 22:07:55 +0000</pubDate>
      <link>https://dev.to/gde/building-file4base-the-modern-open-source-alternative-to-file4base-powered-by-antigravity-47g7</link>
      <guid>https://dev.to/gde/building-file4base-the-modern-open-source-alternative-to-file4base-powered-by-antigravity-47g7</guid>
      <description>&lt;p&gt;For decades, platforms like Claris FileMaker, Microsoft Access, and 4D enabled businesses to build tailored database applications rapidly without needing dedicated enterprise engineering teams.&lt;/p&gt;

&lt;p&gt;However, the legacy RAD (Rapid Application Development) ecosystem comes with well-known bottlenecks: proprietary runtimes, opaque file formats, steep licensing costs, and vendor lock-in.&lt;/p&gt;

&lt;p&gt;Today, I’m excited to share &lt;strong&gt;&lt;a href="https://github.com/file4base/file4base-app" rel="noopener noreferrer"&gt;File4Base&lt;/a&gt;&lt;/strong&gt;: an open-source, modern alternative designed to bring back the speed and visual intuition of FileMaker, powered by clean architecture, open databases, and native cross-platform performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Vision: What is File4Base?
&lt;/h2&gt;

&lt;p&gt;File4Base aims to bridge the gap between relational databases and business users by providing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Visual Canvas Layout Engine&lt;/strong&gt;: Design forms, lists, reports, and search queries dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-Class Relational Schemas&lt;/strong&gt;: Real PostgreSQL tables and columns under the hood—no black-box database engines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Scripting Workspace&lt;/strong&gt;: Step-based automation (similar to FileMaker’s Script Workspace), serialized in structured JSON and executed deterministically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hostable &amp;amp; Zero Lock-in&lt;/strong&gt;: Fully dockerized backend with standard SQL data access.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🏗️ The Modern Stack
&lt;/h2&gt;

&lt;p&gt;Instead of building a monolith, File4Base separates the presentation layer from the database engine using modern industry standards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client (Desktop &amp;amp; Tablet)&lt;/strong&gt;: &lt;strong&gt;Flutter&lt;/strong&gt; (macOS, Windows, Linux). Native rendering, fast UI redraws, and cross-platform consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend Core&lt;/strong&gt;: &lt;strong&gt;Go (Golang)&lt;/strong&gt;. A lightweight, memory-efficient service managing metadata catalogs, generic dynamic CRUD operations, and permission enforcement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: &lt;strong&gt;PostgreSQL 16&lt;/strong&gt;. Rock-solid reliability with JSONB metadata support, native migrations, and real-time triggers via &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: Single command via &lt;code&gt;docker compose up -d&lt;/code&gt; for the server infrastructure.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────┐
│        Flutter Desktop Client (Native)       │
│  [Layout Designer]  [Browse]  [Scripts]      │
└──────────────────────┬───────────────────────┘
                       │ REST / WebSockets
┌──────────────────────▼───────────────────────┐
│              Go Backend Engine               │
│   (Schema Manager, CEL Evaluator, Auth)      │
└──────────────────────┬───────────────────────┘
                       │ pgx / SQL
┌──────────────────────▼───────────────────────┐
│                PostgreSQL 16                 │
│      (Dynamic Schemas + System Catalogs)     │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🤖 Built with Google Antigravity: Spec-Driven Development
&lt;/h2&gt;

&lt;p&gt;A unique aspect of how File4Base is coming to life is the engineering workflow. We are orchestrating development through &lt;strong&gt;Google Antigravity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of writing repetitive boilerplate manually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We write rigorous architectural blueprints, JSON schemas, and roadmap documents (&lt;code&gt;.antigravity/rules.md&lt;/code&gt;, &lt;code&gt;docs/specs/*&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Antigravity takes these specifications and iteratively develops tested modules in Go and Flutter.&lt;/li&gt;
&lt;li&gt;The codebase stays decoupled, clean, and strictly aligned with documented architectural standards.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 What's in Progress?
&lt;/h2&gt;

&lt;p&gt;We've already mapped out the foundational layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[x] Monorepo architecture &amp;amp; Dockerized development environment.&lt;/li&gt;
&lt;li&gt;[x] Complete FileMaker Menu &amp;amp; Feature functional reference guide.&lt;/li&gt;
&lt;li&gt;[x] Script Workspace UI specification and mockups (3-panel workflow).&lt;/li&gt;
&lt;li&gt;[ ] Backend metadata catalog (&lt;code&gt;sys_tables&lt;/code&gt;, &lt;code&gt;sys_columns&lt;/code&gt;, &lt;code&gt;sys_layouts&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;[ ] Dynamic JSON layout renderer in Flutter.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤝 Join the Project!
&lt;/h2&gt;

&lt;p&gt;File4Base is 100% open-source, and we are building it in public from day one.&lt;/p&gt;

&lt;p&gt;If you have experience with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flutter / Dart&lt;/strong&gt; (Desktop layouts, state management, drag-and-drop engines)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go / Golang&lt;/strong&gt; (Dynamic SQL builders, AST parsers, real-time WebSockets)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FileMaker / Low-Code architecture&lt;/strong&gt; (Feature parity, ergonomic UX workflows)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We would love your feedback, stars, and contributions!&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/file4base/file4base-app" rel="noopener noreferrer"&gt;https://github.com/file4base/file4base-app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let us know in the comments: what is the single most critical feature you miss from classic desktop database builders?&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>flutter</category>
      <category>go</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Running a Jev-Style Decision Model on One TPU v6e: What Fits, What It Costs, and What Changes From a GPU</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:32:26 +0000</pubDate>
      <link>https://dev.to/gde/running-a-jev-style-decision-model-on-one-tpu-v6e-what-fits-what-it-costs-and-what-changes-from-1j0g</link>
      <guid>https://dev.to/gde/running-a-jev-style-decision-model-on-one-tpu-v6e-what-fits-what-it-costs-and-what-changes-from-1j0g</guid>
      <description>&lt;p&gt;This article provides a step by step guide to running a Jev-style decision model on one Google Cloud TPU v6e chip with Gemma 4 and vLLM, and compares it with the same read on one NVIDIA L4 GPU. The measurement was pre-registered, and every per-item output is committed.&lt;/p&gt;

&lt;p&gt;One v6e chip serves Gemma 4 E2B, E4B and 12B at bf16 and a 26B-A4B fp8 build; no 31B checkpoint loads. The same checkpoints give the same answers on the TPU and the L4, and the 26B decides in 27 to 33 ms against 61 ms on the L4. On demand, the TPU costs more per decision than the L4 or Jev; 12B is the size to pick.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  Why Measure This?
&lt;/h4&gt;

&lt;p&gt;A Jev-style decision model answers a typed question with a probability for each allowed option: end the prompt where the answer starts, read the scores of the allowed label tokens, and apply a softmax over those. TypeSafe's Jev does this as a hosted service, and any open model can be read the same way.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6"&gt;companion article&lt;/a&gt; measured this read on one NVIDIA L4 and set it beside Jev's published results. This one asks what changes on a TPU: which Gemma 4 sizes fit one v6e chip, whether the read works the same way, and what the chip buys in speed and cost.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;A Google Cloud project with v6e quota in a region that offers &lt;code&gt;ct6e-standard-1t&lt;/code&gt;, and the &lt;code&gt;gcloud&lt;/code&gt; CLI logged in&lt;/li&gt;
&lt;li&gt;A Hugging Face token stored as the Secret Manager secret &lt;code&gt;hf-token&lt;/code&gt;, readable by the Compute Engine default service account&lt;/li&gt;
&lt;li&gt;A Cloud Storage bucket for results, set as &lt;code&gt;BUCKET&lt;/code&gt; in &lt;code&gt;tpu/run.sh&lt;/code&gt; and &lt;code&gt;tpu/startup.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The repository cloned: &lt;code&gt;git clone https://github.com/xbill9/gemma4-dev&lt;/code&gt; and &lt;code&gt;cd gemma4-dev/jev-tpu&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Pre-Register the Measurement
&lt;/h4&gt;

&lt;p&gt;The models, image, serving flags, data, metrics and the order of the quantized attempts are written down and committed before any model call, in &lt;code&gt;PREREGISTRATION.md&lt;/code&gt;; each departure from it is recorded there with a date before the affected results are scored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; e0cce69 4937fb8
&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;e0cce69 jev-tpu: sibling of jev for one TPU v6e chip — copied read, data, suite and scoring code (proxy byte-identical); pre-registration for E2B/E4B/12B bf16 and exploratory quantized 26B-A4B/31B probes
4937fb8 jev-tpu: VM boot, serve and run drivers; pre-registration amended before any model call — suite built locally at 0e67403 (13/13 checksums) and re-checked on the VM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 2 — Pick Checkpoints That Fit One Chip
&lt;/h4&gt;

&lt;p&gt;One v6e chip has 31.24 GiB of memory, of which vLLM uses up to 28.74 GiB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory statistics | total_hbm_limit_gb=31.24GiB | total_hbm_limit_cap_gb=28.74GiB | total_hbm_used_gb=24.56GiB | total_hbm_avail_gb=4.19GiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E2B, E4B and 12B fit at bf16. The 26B-A4B and 31B do not, so the pre-registration lists quantized builds to try in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Size on disk&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;&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;&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;&lt;code&gt;google/gemma-4-E4B-it&lt;/code&gt;&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;&lt;code&gt;google/gemma-4-12B-it&lt;/code&gt;&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;26B-A4B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RedHatAI/gemma-4-26B-A4B-it-FP8-dynamic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fp8&lt;/td&gt;
&lt;td&gt;26.67 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google/gemma-4-31B-it-qat-w4a16-ct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;w4a16&lt;/td&gt;
&lt;td&gt;21.67 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cyankiwi/gemma-4-31B-it-AWQ-4bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4-bit&lt;/td&gt;
&lt;td&gt;19.47 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 31B fp8 builds are 30.98 GiB, over the chip's usable memory, and were not tried.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Launch One v6e Chip
&lt;/h4&gt;

&lt;p&gt;The VM runs the whole measurement from its startup script, fetching the code and the suite from Cloud Storage, and deletes itself when the run ends.&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 instances create jev-tpu-v6e1 &lt;span class="nt"&gt;--zone&lt;/span&gt; europe-west4-a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine-type&lt;/span&gt; ct6e-standard-1t &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-family&lt;/span&gt; ubuntu-accel-2204-amd64-tpu-v5e-v5p-v6e &lt;span class="nt"&gt;--image-project&lt;/span&gt; ubuntu-os-accelerator-images &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--boot-disk-size&lt;/span&gt; 200GB &lt;span class="nt"&gt;--scopes&lt;/span&gt; cloud-platform &lt;span class="nt"&gt;--maintenance-policy&lt;/span&gt; TERMINATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provisioning-model&lt;/span&gt; STANDARD &lt;span class="nt"&gt;--max-run-duration&lt;/span&gt; 6h &lt;span class="nt"&gt;--instance-termination-action&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; jev-code&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;code-tarball&amp;gt;,jev-run&lt;span class="o"&gt;=&lt;/span&gt;2026-09-24-v6e1 &lt;span class="nt"&gt;--metadata-from-file&lt;/span&gt; startup-script&lt;span class="o"&gt;=&lt;/span&gt;tpu/startup.sh
&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;NAME          ZONE            MACHINE_TYPE      PREEMPTIBLE  INTERNAL_IP    EXTERNAL_IP    STATUS
jev-tpu-v6e1  europe-west4-a  ct6e-standard-1t               10.164.15.207  34.32.155.166  RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--scopes cloud-platform&lt;/code&gt; lets the VM read the Hugging Face token from Secret Manager at boot, so the token never enters instance metadata.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Serve Each Model
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;tpu/serve.sh&lt;/code&gt; starts one model at a time with the same flags for every arm and waits for &lt;code&gt;/v1/models&lt;/code&gt;. Abridged, with the token and volume options left out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; vllm &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;--shm-size&lt;/span&gt; 10gb vllm/vllm-tpu:nightly &lt;span class="se"&gt;\&lt;/span&gt;
  vllm serve google/gemma-4-12B-it &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 1 &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 2048 &lt;span class="nt"&gt;--max-num-seqs&lt;/span&gt; 16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-logprobs&lt;/span&gt; 32 &lt;span class="nt"&gt;--generation-config&lt;/span&gt; vllm &lt;span class="nt"&gt;--limit-mm-per-prompt&lt;/span&gt; &lt;span class="s1"&gt;'{"image":0,"audio":0}'&lt;/span&gt; &lt;span class="nt"&gt;--enable-prefix-caching&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;[jev-run 2026-09-24T14:42:48Z] READY google/gemma-4-E2B-it after 346s
[jev-run 2026-09-24T14:51:34Z] READY google/gemma-4-E4B-it after 421s
[jev-run 2026-09-24T15:02:01Z] READY google/gemma-4-12B-it after 512s
[jev-run 2026-09-24T15:14:59Z] READY RedHatAI/gemma-4-26B-A4B-it-FP8-dynamic after 616s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image was &lt;code&gt;vllm/vllm-tpu@sha256:19a1a0526476f902eb83e1057f3d8938f35b457dd7716a30e9ab4f7bee90d507&lt;/code&gt;, vLLM &lt;code&gt;0.29.1rc1.dev468+g0b7f11a1e&lt;/code&gt;. On v6e it stores the KV cache in fp8 by default for every model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO 09-24 14:53:37 [tpu_platform.py:232] Automatically using fp8_e5m2 for FP8 KV cache on TPU v6e.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "bf16" below describes the weights. The L4 run kept the default 16-bit KV cache.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Read the Labels Out of the Top 32
&lt;/h4&gt;

&lt;p&gt;On a GPU, the read asks vLLM for the log-probabilities of the label tokens by id. vLLM's TPU backend returns only the top-k log-probabilities, and a request for specific token ids fails. Four request shapes are sent before each model's reads, and the answers are kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== probe 3: {"prompt":[2,106,1645,108],"max_tokens":1,"temperature":0,"logprobs":32,"return_tokens_as_token_ids":true}
{"id":"cmpl-9ee6dc5f0461280b","object":"text_completion", ...
== probe 4: {"prompt":[2,106,1645,108],"max_tokens":1,"temperature":0,"logprobs":5,"logprob_token_ids":[236776,236799,236780],"return_tokens_as_token_ids":true}
{"error":{"message":"list index out of range","type":"InternalServerError","param":null,"code":500}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the read asks for the top 32 log-probabilities (&lt;code&gt;JEV_TOPK=32&lt;/code&gt;) and takes the label tokens from them. A label inside the 32 gets its exact log-probability; a label outside gets the proxy's fallback value, the lowest returned log-probability minus 5, and every four-task record keeps how many labels came back.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Run and Score
&lt;/h4&gt;

&lt;p&gt;Each model gets a five-example smoke read, the four tasks (300 examples each from sst2, AG News, DAIR Emotion and tweet_eval irony), the three choice tasks with their options reversed, a latency pass at one request at a time, and Bespoke Labs' 3,880-record public suite, rebuilt with Nimble's converters and checked against all 13 published checksums on the VM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[jev-run 2026-09-24T14:36:51Z] suite: 13 of 13 subsets match
[jev-run 2026-09-24T15:02:42Z] 12b four tasks: 23s for 1200 decisions at concurrency 8
[jev-run 2026-09-24T15:04:25Z] 12b suite: 60s for 3880 records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sweep_score.py &lt;span class="nt"&gt;--prefix&lt;/span&gt; 2026-09-24-v6e1 &lt;span class="nt"&gt;--arms&lt;/span&gt; e2b e4b 12b 26b-fp8
python3 nimble_suite/suite_sweep.py &lt;span class="nt"&gt;--prefix&lt;/span&gt; 2026-09-24-v6e1 &lt;span class="nt"&gt;--arms&lt;/span&gt; e2b e4b 12b 26b-fp8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both write their tables into &lt;code&gt;results/&lt;/code&gt;, and every figure below comes from those files and &lt;code&gt;article_figures.py&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Fits and Loads on One v6e Chip?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Serves&lt;/th&gt;
&lt;th&gt;Time to serve&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&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;346 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;421 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12B&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;512 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B-A4B&lt;/td&gt;
&lt;td&gt;fp8, 26.67 GiB&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;616 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;w4a16, 21.67 GiB&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fails at 120 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31B&lt;/td&gt;
&lt;td&gt;4-bit, 19.47 GiB&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fails at 120 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both 31B builds stop with the same error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NotImplementedError: compressed-tensors scheme for layer 'model.language_model.layers.0.self_attn.q_proj' is not yet supported in the JAX path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 runs only on the JAX path of vLLM's TPU backend, which loads fp8 but no 4-bit format for a dense model. The one 31B format it loads is 30.98 GiB, over one chip's usable memory.&lt;/p&gt;




&lt;h4&gt;
  
  
  Does the TPU Change the Answers?
&lt;/h4&gt;

&lt;p&gt;No. E2B and E4B were also read on the L4, with the same checkpoints and prompts. The predicted label matched on 1,186 and 1,193 of 1,200 examples, and accuracy per task differed by at most 1.0 and 0.4 points. The 26B-A4B fp8 build here and the 4-bit AWQ build on the L4 are different quantizations of the same model and differ by at most 1.3 points per task and 0.7 points on the suite.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Majority&lt;/th&gt;
&lt;th&gt;E2B&lt;/th&gt;
&lt;th&gt;E4B&lt;/th&gt;
&lt;th&gt;12B&lt;/th&gt;
&lt;th&gt;26B-A4B fp8&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;94.0%&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;94.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;td&gt;86.3%&lt;/td&gt;
&lt;td&gt;86.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;35.0%&lt;/td&gt;
&lt;td&gt;53.3%&lt;/td&gt;
&lt;td&gt;54.3%&lt;/td&gt;
&lt;td&gt;59.3%&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;84.0%&lt;/td&gt;
&lt;td&gt;84.7%&lt;/td&gt;
&lt;td&gt;91.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per-task calibration, option-order changes and 90th-percentile times are in &lt;code&gt;results/2026-09-24-v6e1-SWEEP.md&lt;/code&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Do the Sizes Compare With Jev?
&lt;/h4&gt;

&lt;p&gt;The Jev comparison is the &lt;a href="https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6"&gt;L4 article's&lt;/a&gt; subject, with its caveats in full. On the same 3,880 public records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;th&gt;Jev 1.13.0, published&lt;/th&gt;
&lt;th&gt;26B-A4B fp8&lt;/th&gt;
&lt;th&gt;12B&lt;/th&gt;
&lt;th&gt;E4B&lt;/th&gt;
&lt;th&gt;E2B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All, 3,880&lt;/td&gt;
&lt;td&gt;77.3%&lt;/td&gt;
&lt;td&gt;76.0%&lt;/td&gt;
&lt;td&gt;76.2%&lt;/td&gt;
&lt;td&gt;72.8%&lt;/td&gt;
&lt;td&gt;68.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes/no, 1,399&lt;/td&gt;
&lt;td&gt;84.6%&lt;/td&gt;
&lt;td&gt;84.2%&lt;/td&gt;
&lt;td&gt;85.1%&lt;/td&gt;
&lt;td&gt;81.5%&lt;/td&gt;
&lt;td&gt;76.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple choice, 1,848&lt;/td&gt;
&lt;td&gt;82.8%&lt;/td&gt;
&lt;td&gt;79.7%&lt;/td&gt;
&lt;td&gt;78.1%&lt;/td&gt;
&lt;td&gt;74.5%&lt;/td&gt;
&lt;td&gt;70.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five-level rating, 633&lt;/td&gt;
&lt;td&gt;45.2%&lt;/td&gt;
&lt;td&gt;47.4%&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;48.5%&lt;/td&gt;
&lt;td&gt;44.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median ECE after 50 labels (Jev 0.071 as shipped)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.070&lt;/td&gt;
&lt;td&gt;0.070&lt;/td&gt;
&lt;td&gt;0.077&lt;/td&gt;
&lt;td&gt;0.092&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev leads 12B by 1.1 points overall (95% range −0.8 to +3.0) and 26B-A4B by 1.3 (−0.6 to +3.2), and on multiple choice by 4.7 and 3.2. The L4's 4-bit 26B scored 75.3%, 2.1 points behind with a range from 0.2 to 4.0, so read the 26B as one to two points behind Jev on either platform. Jev's per-record answers are unpublished, so these ranges compare two independent proportions, and records that share a passage widen them. Gemma was read in the PR proxy's prompt format with no tuning; a different prompt could move the multiple-choice gap in either direction.&lt;/p&gt;




&lt;h4&gt;
  
  
  Did the Top-32 Read Lose Anything?
&lt;/h4&gt;

&lt;p&gt;Not on accuracy. At least one label came back on every four-task read, so the predicted label is exact; only the probabilities of labels outside the 32 are approximate.&lt;/p&gt;

&lt;p&gt;On the four tasks, moving every missing label from the fallback value to the top-32 bound, the most probability it could have had, changes raw ECE by less than 0.001 and ECE after 50 labels by at most 0.005. That check was added after the run. The suite records do not keep how many labels came back, so the suite calibration figures have no such check; by a lower-bound count, at least 10.2% of 12B's suite records and 17.8% of 26B-A4B's used the fallback value.&lt;/p&gt;

&lt;p&gt;The share of probability on the allowed labels varies too. On irony, 12B's most likely token was a label on 5 of 300 reads, with a median of 3.1% of its probability on the two labels; 26B-A4B's was a label on 161 of 300 AG News reads, with a median of 55.2%. The read rescales the labels to sum to one, so the answer reads as confident either way, and &lt;code&gt;label_mass&lt;/code&gt; in every record shows it.&lt;/p&gt;




&lt;h4&gt;
  
  
  TPU v6e Against the L4
&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;One TPU v6e chip&lt;/th&gt;
&lt;th&gt;One NVIDIA L4 (&lt;code&gt;g6.xlarge&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory for the model&lt;/td&gt;
&lt;td&gt;🥇 28.74 GiB&lt;/td&gt;
&lt;td&gt;23034 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest Gemma 4 read here&lt;/td&gt;
&lt;td&gt;12B at bf16, 26B-A4B at fp8&lt;/td&gt;
&lt;td&gt;26B-A4B at 4-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy, same checkpoint&lt;/td&gt;
&lt;td&gt;same answers on 1,186 to 1,193 of 1,200&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B time per decision, one at a time&lt;/td&gt;
&lt;td&gt;🥇 27 to 33 ms&lt;/td&gt;
&lt;td&gt;61 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price per hour, on demand&lt;/td&gt;
&lt;td&gt;$2.97&lt;/td&gt;
&lt;td&gt;🥇 $0.8048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26B cost per million decisions, on demand&lt;/td&gt;
&lt;td&gt;$10.37&lt;/td&gt;
&lt;td&gt;🥇 at most $5.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label read&lt;/td&gt;
&lt;td&gt;top 32 log-probabilities&lt;/td&gt;
&lt;td&gt;🥇 exact label token ids&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache&lt;/td&gt;
&lt;td&gt;fp8 by default&lt;/td&gt;
&lt;td&gt;🥇 16-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this table 🥇 marks the better value. The 26B decides 1.8 to 2.2 times faster on the TPU. The chip costs 3.7 times as much per hour on demand, or 1.7 times at the flex-start price of $1.35, a mode that had no capacity during this run. The L4's cost comes from its first run's throughput with the client on a home connection, so it is an upper bound; the TPU's comes from the request times at eight in flight.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Serve the Same Checkpoint on Both Platforms First
&lt;/h4&gt;

&lt;p&gt;Before comparing hardware, serve one checkpoint on both with the same prompts and compare predicted labels record by record. It checks the whole read, from chat template to label tokens; here the TPU's top-32 read and the L4's exact read agreed on 1,186 and 1,193 of 1,200 answers.&lt;/p&gt;




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

&lt;p&gt;For a Jev-style decision service on a TPU, Gemma 4 12B at bf16 plus one temperature fitted on about 50 labels. It matches the 26B on the suite, 76.2% against 76.0%, though the 26B scores 6.3 points higher on irony; it reaches a median ECE of 0.070 after fitting, decides in 26 to 27 ms, and needs no third-party quantized checkpoint. Check &lt;code&gt;label_mass&lt;/code&gt; on your own task first: on irony, 12B put a median of 3.1% of its probability on the labels.&lt;/p&gt;

&lt;p&gt;Choose the TPU when time per decision matters or the rest of the stack is already on Google Cloud: it halves the 26B's time per decision against the L4. Choose the L4, or Jev itself, when cost per decision matters: on demand, 12B on one v6e chip costs $8.19 per million decisions, against Jev's $5.54 and at most $5.43 for the 26B on an L4.&lt;/p&gt;




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

&lt;p&gt;Measured from the request times at eight in flight, one v6e chip answers about 274 decisions a second with E2B, 206 with E4B, 101 with 12B and 80 with 26B-A4B. At the europe-west4 on-demand price of $2.97 an hour that is $3.01, $4.00, $8.19 and $10.37 per million decisions; at the flex-start price of $1.35, $1.37, $1.82, $3.72 and $4.71. TypeSafe prices Jev at $5.54 per million decisions at the L4 run's median prompt of 132 tokens. The chip is charged by the hour whether busy or idle, and eight requests in flight is below the 16 the server allows, so these figures hold only while the chip is kept at least this busy.&lt;/p&gt;

&lt;p&gt;The measured run took 48 minutes of on-demand time, $2.39, and all instance time for the measurement came to at most $3.25.&lt;/p&gt;




&lt;h4&gt;
  
  
  Teardown
&lt;/h4&gt;

&lt;p&gt;The VM deletes itself when &lt;code&gt;tpu/run.sh&lt;/code&gt; finishes, and &lt;code&gt;--max-run-duration 6h&lt;/code&gt; with &lt;code&gt;--instance-termination-action DELETE&lt;/code&gt; is the backstop. Confirm it is gone:&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 instances describe jev-tpu-v6e1 &lt;span class="nt"&gt;--zone&lt;/span&gt; europe-west4-a
&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;ERROR: (gcloud.compute.instances.describe) Could not fetch resource:
 - The resource 'projects/aisprint-491218/zones/europe-west4-a/instances/jev-tpu-v6e1' was not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The goal of this article was to run a Jev-style decision model on one TPU v6e chip with Gemma 4 and find what changes from a GPU. The key to the solution was the same prompts, labels and scoring as the L4 run, a pre-registered design, and a label read taken from the top 32 log-probabilities. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 One v6e chip serves Gemma 4 E2B, E4B and 12B at bf16 and a 26B-A4B fp8 build&lt;/li&gt;
&lt;li&gt;❌ No 31B checkpoint loads: the w4a16 and 4-bit builds fail on the JAX path, and the fp8 build is 30.98 GiB&lt;/li&gt;
&lt;li&gt;🟢 The same checkpoints give the same answers on the TPU and the L4: 1,186 and 1,193 of 1,200&lt;/li&gt;
&lt;li&gt;🟢 The 26B decides in 27 to 33 ms on the TPU against 61 ms on the L4&lt;/li&gt;
&lt;li&gt;⚠️ On demand, 26B costs $10.37 per million decisions on the TPU against at most $5.43 on the L4 and $5.54 for Jev&lt;/li&gt;
&lt;li&gt;⚠️ vLLM on TPU returns only the top-k log-probabilities; reading labels from the top 32 changes no answer, and changes four-task calibration by at most 0.005&lt;/li&gt;
&lt;li&gt;🟢 12B matches the 26B: 76.2% and 76.0% on the 3,880-record suite, against Jev's 77.3%&lt;/li&gt;
&lt;li&gt;🟢 After 50 labels, 12B and 26B-A4B reach a median ECE of 0.070, against Jev's 0.071 as shipped&lt;/li&gt;
&lt;li&gt;🟢 The whole measurement cost at most $3.25 of instance time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one TPU v6e chip (&lt;code&gt;ct6e-standard-1t&lt;/code&gt;, on demand) in europe-west4-a, vLLM &lt;code&gt;0.29.1rc1.dev468+g0b7f11a1e&lt;/code&gt; at the image digest above, E2B, E4B and 12B with bf16 weights and 26B-A4B through RedHatAI's fp8 build, every model with vLLM's default fp8 KV cache on v6e, &lt;code&gt;--max-model-len 2048&lt;/code&gt;, one run per model, 300 examples per task plus the 3,880-record public suite, client on the VM. The top-32 read and on-demand capacity are dated deviations from the pre-registration, and the top-32 bound check, added after the run, covers the four tasks only. L4 figures come from the companion run on a &lt;code&gt;g6.xlarge&lt;/code&gt; and &lt;code&gt;g6.4xlarge&lt;/code&gt; in us-east-1, and the two platforms were timed with different clients. Every source dataset was published before Gemma 4 and may be in its training data. No Jev call was made: the Jev figures are Bespoke Labs' published results on the same records, from one run of Jev 1.13.0 by a company that sells a competing model, counting an invalid Jev response as wrong, with Jev's probabilities rounded to two decimals by its API. Parts of the analysis and writing were done with AI assistance (Claude); every figure comes from the committed output files.&lt;/p&gt;

&lt;p&gt;The strategy for running a Jev-style decision model on TPU with Gemma 4 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;Code, pre-registration and per-item results: &lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/jev-tpu" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/tree/main/jev-tpu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion L4 measurement: &lt;a href="https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6"&gt;https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion review of the independent evidence on Jev: &lt;a href="https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln"&gt;https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM TPU documentation: &lt;a href="https://docs.vllm.ai/projects/tpu/en/latest/" rel="noopener noreferrer"&gt;https://docs.vllm.ai/projects/tpu/en/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;tpu-inference: &lt;a href="https://github.com/vllm-project/tpu-inference" rel="noopener noreferrer"&gt;https://github.com/vllm-project/tpu-inference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;26B-A4B fp8 checkpoint: &lt;a href="https://huggingface.co/RedHatAI/gemma-4-26B-A4B-it-FP8-dynamic" rel="noopener noreferrer"&gt;https://huggingface.co/RedHatAI/gemma-4-26B-A4B-it-FP8-dynamic&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gemma 4 12B: &lt;a href="https://huggingface.co/google/gemma-4-12B-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/gemma-4-12B-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bespoke Labs, public-suite results for Jev 1.13.0: &lt;a href="https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloud TPU v6e: &lt;a href="https://cloud.google.com/tpu/docs/v6e" rel="noopener noreferrer"&gt;https://cloud.google.com/tpu/docs/v6e&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guo et al., On Calibration of Modern Neural Networks: &lt;a href="https://arxiv.org/abs/1706.04599" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.04599&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>googlecloud</category>
      <category>machinelearning</category>
      <category>llm</category>
    </item>
    <item>
      <title>Jev After Eight Days of Independent Tests: Level With Mid-Price LLMs, Behind the Frontier</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 24 Sep 2026 02:00:52 +0000</pubDate>
      <link>https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln</link>
      <guid>https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln</guid>
      <description>&lt;p&gt;This article provides a review of the independent evidence on TypeSafe's Jev, the open models built to replace it, and the prior art behind both, as of September 23, 2026. Every figure below is traced to a primary source, and re-scored from committed per-item outputs wherever the author published them. This is a snapshot eight days after launch, and the arXiv preprints it cites are days old and unrefereed.&lt;/p&gt;

&lt;p&gt;On accuracy, Jev sits level with mid-price LLMs and 6.5 to 11.5 points behind the frontier in the cleanest comparison. Out of the box its probabilities are the best calibrated of the models measured on familiar English tasks and are wrong in both directions off them, and one temperature fitted on 50 to a few hundred labels fixes most of the error.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  What Is Jev?
&lt;/h4&gt;

&lt;p&gt;Jev is a hosted model from TypeSafe AI, launched on September 15, 2026. It answers typed questions about a piece of text: &lt;code&gt;choice&lt;/code&gt; picks one option from a list, &lt;code&gt;score&lt;/code&gt; places the text on an ordered scale, and &lt;code&gt;noul&lt;/code&gt; returns the probability that a statement is true. It writes no text.&lt;/p&gt;

&lt;p&gt;The API is &lt;code&gt;POST /v1/systemone&lt;/code&gt; at &lt;code&gt;api.typesafe.ai&lt;/code&gt;. The price is $0.042 per million input tokens, and output is free. Access is through a waitlist, OpenRouter and Vercel's AI Gateway.&lt;/p&gt;

&lt;p&gt;TypeSafe calls this category a System One model, after Kahneman's fast and intuitive system. The company raised a $40M seed round led by DCVC, and its co-founders are Diogo Almeida (CEO), Sasha Sheng (COO) and Erik Gafni (CTO).&lt;/p&gt;

&lt;p&gt;The read itself is an old technique: end the prompt where the answer would start, take the model's scores for only the allowed label tokens, and apply a softmax over them. Any open model can be read this way; vLLM exposes it through &lt;code&gt;logprob_token_ids&lt;/code&gt;, SGLang through &lt;code&gt;/v1/score&lt;/code&gt;, and Featherless AI's SimpleJev wraps it as a Jev-compatible server.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Counts as Evidence Here?
&lt;/h4&gt;

&lt;p&gt;The review covers 14 arXiv preprints read in full, 104 GitHub evaluation repositories, 33 dev.to and Medium posts, TypeSafe's own pages and documentation, and press and forum coverage. Each figure carries one of three levels of support.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-scored:&lt;/strong&gt; recomputed from committed per-item outputs, and it matched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checked:&lt;/strong&gt; the source's arithmetic recomputed from its published tables, with no per-item data available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reported:&lt;/strong&gt; the author's statement, which cannot be checked from outside&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Studies are graded A (human or public labels, at least 200 items, stated method, committed outputs), B (real but small, synthetic, LLM-labelled or unreproducible) or C (a demo or a restatement).&lt;/p&gt;

&lt;p&gt;By that rubric, the posts split sharply. Of 33 dev.to and Medium posts read in full, 5 are careful measurements on human or public labels, 11 are small, synthetic or scored by agreement between models, and 17 restate vendor claims. Of the 104 GitHub repositories, 33 grade A, 57 grade B, 5 grade C, and 9 are tools. Of the 14 arXiv preprints, 10 call TypeSafe's hosted Jev.&lt;/p&gt;

&lt;p&gt;Who ran a study matters too. The studies with committed per-item outputs and no product in the race are Ibrahim and Zaki, Janardhan, sanand0, SamuelSacco and OmarMujahid, the last the most favourable to Jev; Rafe and Das also has none, and withholds its per-item outputs. Every's review was written by a launch partner. LangChain, which ships the &lt;code&gt;langchain-typesafe&lt;/code&gt; integration, reported 100% on 500 judgments that are 5 test cases repeated 100 times (not re-checked). And the "15.9% faster pipeline" headline came from a TypeSafe employee's three-case demo. Bespoke Labs sells Nimble, and the replica authors test their own models. Matt Mastracci wrote the DiffusionGemma PR he compares with Jev, and Google's Gemma account shared his thread. heiko-hotz, whose SNIPS comparison includes Gemma 4 31B, works at Google. synthorai, which found Jev behind flash LLMs, runs a gateway that sells access to the models it compared.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Does Jev Land on Accuracy?
&lt;/h4&gt;

&lt;p&gt;Level with mid-price LLMs and behind the frontier, at a fraction of the frontier's price. That trade is the product's design, and the numbers below show its size.&lt;/p&gt;

&lt;p&gt;The largest pre-registered study, Ibrahim and Zaki's replication of a social-science annotation suite with 7,977 human-labelled items (arXiv 2609.24574), has Jev behind the best of 19 LLMs on 14 of 15 tasks, by a median 11.6 macro-F1 points. That comparator is the best model per task, picked after the results; within Jev's price band, Gemma 4 31B and Qwen3 235B score 3 and 2 F1 points above it. Its one per-item file checked, the 266-item dialect task, re-scores exactly to the paper's table.&lt;/p&gt;

&lt;p&gt;Manjunath Janardhan's 200-item, six-model comparison on BANKING77, BoolQ, Yelp and ChaosNLI re-scores exactly from its committed logs.&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;Accuracy&lt;/th&gt;
&lt;th&gt;Calibration error (ECE)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥇 Claude Fable 5.1&lt;/td&gt;
&lt;td&gt;84.0%&lt;/td&gt;
&lt;td&gt;0.064&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥈 GPT-6 Astra&lt;/td&gt;
&lt;td&gt;79.0%&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥉 DeepSeek V4.1 Flash&lt;/td&gt;
&lt;td&gt;76.0%&lt;/td&gt;
&lt;td&gt;0.138&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M3&lt;/td&gt;
&lt;td&gt;75.5%&lt;/td&gt;
&lt;td&gt;0.112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K3&lt;/td&gt;
&lt;td&gt;74.5%&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jev&lt;/td&gt;
&lt;td&gt;72.5%&lt;/td&gt;
&lt;td&gt;0.161&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two frontier leads (+11.5 and +6.5 points) exclude zero in their 95% intervals. Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash sit within noise of Jev.&lt;/p&gt;

&lt;p&gt;Banking77 is the one task many authors ran independently. Eight runs, one of them JevBench's own first-party run, put Jev at 0.753 to 0.840, with a median of 0.809. In every study that ran LLMs beside it, the best of them was ahead.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does Jev Do Well?
&lt;/h4&gt;

&lt;p&gt;Binary and few-class decisions, fast, with answers that always fit the schema.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spam:&lt;/strong&gt; 98.33% on 18,514 emails, level with TF-IDF logistic regression at 98.39%, and it held up on newer mail where the trained baseline fell to 72.5%; the question wording was tuned on labelled errors from the same sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reranking:&lt;/strong&gt; level with a dedicated reranker on eight retrieval sets, 0.692 against 0.691&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring code for backdoors:&lt;/strong&gt; AUROC of 0.976 and 0.970, catching about 90% of backdoors at 2% false positives; a best-of-5 attack cut detection to 60%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema compliance:&lt;/strong&gt; zero invalid answers across 23,703 decision-model calls, Jev's included, in the largest study&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability:&lt;/strong&gt; 1.33% of answers changed between identical passes in one study, 2.2% in the phishing benchmark, and 0 of 96 repeats in another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; German cost 0.5 points on MASSIVE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed and price:&lt;/strong&gt; about 0.1 s of server time, at $0.042 per million input tokens&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Are Its Probabilities Calibrated?
&lt;/h4&gt;

&lt;p&gt;They rank answers usefully; out of the box they beat most alternatives on familiar English tasks and miss badly off them.&lt;/p&gt;

&lt;p&gt;TypeSafe publishes no calibration error, reliability plot, Brier score or log loss for Jev on any dataset. Its only confidence-to-accuracy figure is one cookbook of 60 SEC filings: 90% right at confidence 0.9 or above, 40% below.&lt;/p&gt;

&lt;p&gt;The independent picture depends on the comparison.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Bespoke Labs' 13-subset public suite, Jev's median ECE is 0.071, lower than Nimble-9B's on 11 of 13 subsets, and lower than every Gemma 4 read in the companion article (0.114 to 0.180) before any fitting.&lt;/li&gt;
&lt;li&gt;Against LLMs that write their confidence as a number, Jev's own probabilities usually win. Its median ECE of 0.157 on the social-science tasks beats 16 of 19 LLMs.&lt;/li&gt;
&lt;li&gt;Once every model gets one fitted temperature, 15 of those LLMs beat it.&lt;/li&gt;
&lt;li&gt;Against LLMs that return full probability distributions, Jev's calibration error is the highest in Janardhan's run (0.161) and in sanand0's nine-model Banking77 pilot (0.138).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction of the error changes with the data. On public multi-class sets Jev is overconfident: on GoEmotions, labels it scored between 0.80 and 0.95 matched the human label 15% of the time. On crash narratives and synthetic items it is under-confident. Within one judge study, refit temperatures ran from 0.65 to 4.45, and its authors conclude that no single temperature fits.&lt;/p&gt;

&lt;p&gt;The API also rounds every probability to 0.01. In one committed sample, 70.4% of &lt;code&gt;choice&lt;/code&gt; probabilities came back as exactly 0, and &lt;code&gt;noul&lt;/code&gt; is clamped between 0.01 and 0.98.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Fit a Temperature on 50 to a Few Hundred of Your Own Labels
&lt;/h4&gt;

&lt;p&gt;A small refit fixes most of the calibration error, for Jev and for any open model read the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A two-parameter refit on civil_comments took Jev's ECE from 0.16–0.21 to under 0.025 on a held-out half&lt;/li&gt;
&lt;li&gt;Transferring a fitted slope and refitting only the intercept on 50 labels cut mean ECE by 74%&lt;/li&gt;
&lt;li&gt;Out-of-fold Platt scaling cut Jev's ECE on crash narratives by 3.35x&lt;/li&gt;
&lt;li&gt;In the companion article, one temperature fitted on 50 labels brought plain Gemma 4 26B's median ECE from 0.180 to 0.080 on the public suite, against Jev's 0.071 as shipped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The open replicas report the same order of correction: Nimble fitted 2.179, Luce 2.5 to 3.3 on unseen tasks. Luce's rule is to ship with 100 to 300 real labelled items and fit the temperature on them.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Much Faster and Cheaper Is It?
&lt;/h4&gt;

&lt;p&gt;Speed runs from 0.5x, slower than a local Gemma, to 12.1x faster; cost runs from 0.6x, dearer, to 478x cheaper, depending on the comparison model, the network path and where the clock runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Faster by&lt;/th&gt;
&lt;th&gt;Cheaper by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Luna, reasoning off, client clock&lt;/td&gt;
&lt;td&gt;OmarMujahid&lt;/td&gt;
&lt;td&gt;1.04x&lt;/td&gt;
&lt;td&gt;4.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, Banking77&lt;/td&gt;
&lt;td&gt;sanand0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1.18x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5, phishing verdict&lt;/td&gt;
&lt;td&gt;jev-phishing-bench&lt;/td&gt;
&lt;td&gt;2.9x&lt;/td&gt;
&lt;td&gt;12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-size open model, 47 account reviews&lt;/td&gt;
&lt;td&gt;devopsdaily&lt;/td&gt;
&lt;td&gt;12.1x&lt;/td&gt;
&lt;td&gt;7.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1, via OpenRouter&lt;/td&gt;
&lt;td&gt;Janardhan&lt;/td&gt;
&lt;td&gt;9.9x&lt;/td&gt;
&lt;td&gt;478x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19 LLMs, social-science tasks (median)&lt;/td&gt;
&lt;td&gt;arXiv 2609.24574&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;18.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, 6G edge orchestration&lt;/td&gt;
&lt;td&gt;arXiv 2609.23136&lt;/td&gt;
&lt;td&gt;1.3x&lt;/td&gt;
&lt;td&gt;0.6x (Jev dearer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek, edge orchestration, fees per correct completion&lt;/td&gt;
&lt;td&gt;arXiv 2609.22753, same lab&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3.2–3.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Gemma 4 26B-A4B Q4 on a mini-PC&lt;/td&gt;
&lt;td&gt;ikkun1222&lt;/td&gt;
&lt;td&gt;0.5–0.65x (Jev slower)&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 server itself is fast: about 105 ms in one run, and about 76 ms once the network round trip is subtracted in another. Most of the cost gap comes from output being free. In devopsdaily's workload 83% of the old bill was output, and per input token the two prices differed by 1.31x. In the companion article, plain Gemma 4 26B on one EC2 L4 at full load cost at most $5.43 per million decisions, against Jev's $5.54 at the same 132-token prompts.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Does It Break?
&lt;/h4&gt;

&lt;p&gt;On wording, language, and questions whose answer is absent from the input.&lt;/p&gt;

&lt;p&gt;Option names move answers. With the question, input, rubric and option set held fixed, swapping which rubric sits behind "no" and "yes" changed 32.5% of Jev's answers, against about 2% with neutral names, and AUROC fell from 0.81 to 0.58 (arXiv 2609.26758, n = 1,200; reported). The paper is graded C as evidence about Jev: it names no dataset or Jev version, its gold labels likely come from a teacher LLM, its code is unreleased, and no generative LLM was given the same swap.&lt;/p&gt;

&lt;p&gt;Bare labels mislead it. A router given option names with no descriptions sent all 40 hard tasks to the cheap model, at a median confidence of 0.96. One-line option descriptions fixed 37 of 40.&lt;/p&gt;

&lt;p&gt;Language shift costs accuracy. Russian XNLI dropped from 88.3% to 77.3% with ECE tripling, and Spanish cost 3 to 6 points.&lt;/p&gt;

&lt;p&gt;Jev is most confidently wrong where the answer does not follow from the input. With no "unknown" option, every answer on KoBBQ bias questions is wrong by design, and Jev gave them 0.79 confidence. On a fair die its &lt;code&gt;choice&lt;/code&gt; probabilities averaged 83% against a true 17%, while &lt;code&gt;noul&lt;/code&gt; gave 19%. On heart-risk data its probabilities ran about 3x too high, and a chat LLM on the same 5,000 people ranked risk slightly better (AUC 0.79 against 0.77).&lt;/p&gt;

&lt;p&gt;Questions batched in one request cannot see each other's answers. One reviewer's request returned "suspend" for an account the same response classified as a developer sending tests.&lt;/p&gt;

&lt;p&gt;TypeSafe's own list of known weaknesses names literal reading, arithmetic and counting, dates, long irrelevant input, adversarial content and phrasing: "refund" scored 0.72 and "not a refund" 0.47 on the same ticket.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does TypeSafe Claim, Checked?
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;193.6x faster and 444.6x cheaper.&lt;/strong&gt; The source is TypeSafe's four in-house workflow evals, which the launch post calls "on the higher end of real world gains". TypeSafe discloses that the workflows "were made by individuals on our model capabilities team, so some bias could exist", and that its LLM comparators ran through its own probability adapter, which it says is "slower and more expensive". The comparator is never named. Checked against the published summary points, 444.6x fits only Jev against Opus 5 in the workflow setup. Averaged over all eight workflow setups, the figures are 97.8x faster and 149.2x cheaper. Timings came from laptops on the US West Coast, with no case counts, run counts or variance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval accuracy.&lt;/strong&gt; Reference labels are "an average of the responses of GPT-6 Astra and Claude Fable 5.1, both at high thinking", so accuracy there means agreement with two LLMs. Jev scores 67.8%, equal to Sonnet 5, with GPT-5.6 Sol leading at 74.1%. On invoice processing Jev scores 61.8% against Sol's 79.1%. TypeSafe states that it chose to publish no public-benchmark results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"0% hallucination".&lt;/strong&gt; TypeSafe's launch post: "Our number is not empirical. Schema matching is guaranteed." Every answer is a valid option; the wrong-but-valid rate is the accuracy figures above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RLCD.&lt;/strong&gt; Reinforcement learning for calibrated decisions has no paper, patent or method description. The CEO said on Hacker News that the architecture is "close to the chest for now, but we have talked about writing a paper." On Latent Space he agreed that all of Jev's training data is synthetic. Model size and base are undisclosed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing.&lt;/strong&gt; The launch post says "We can't prove it isn't subsidized", and the home-page FAQ says "We can serve Jev profitably at our current prices." How long the launch price holds is an open question for anyone building on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frontier comparison.&lt;/strong&gt; On Hacker News a commenter described Jev as "basically a zero-shot classifier" that classifies "as accurately (they claim) as a frontier-level LLM", and the CEO replied "exactly right!" The independent accuracy results above are the test of the second half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The founder.&lt;/strong&gt; TypeSafe's pages say Diogo Almeida "co-invented RLHF". He is not an author of Christiano et al. (2017), which introduced it; he is a primary author of InstructGPT (arXiv 2203.02155), which applied it to instruction following, and on Latent Space he separates the two.&lt;/p&gt;

&lt;p&gt;How the claims travelled:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim as circulated&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;194x faster and 445x cheaper against GPT-6 Astra (Tom's Hardware)&lt;/td&gt;
&lt;td&gt;Astra is a reference labeller with no scored setup; 444.6x fits Opus 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage of arXiv 2609.26758: Jev flips 70.4 of 100 answers&lt;/td&gt;
&lt;td&gt;The paper gives 70.4 for an open ModernBERT head; Jev's is 32.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Its own employee measured 15.9% on a real pipeline" (Cherry Creek News)&lt;/td&gt;
&lt;td&gt;3 cases of a demo support ticket; costs modelled from an estimated price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Several self-published evaluations also state more than their own data supports, in both directions. The full report lists the corrections for Luce, Laya and NanoJev and uses the corrected figures from every study.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Predates Jev?
&lt;/h4&gt;

&lt;p&gt;Every piece of the mechanism.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Prior art&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Score options by label likelihood&lt;/td&gt;
&lt;td&gt;GPT-3 §2.4 (2020); MMLU §4.1 (2021); lm-evaluation-harness &lt;code&gt;multiple_choice&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map each label to one token&lt;/td&gt;
&lt;td&gt;Verbalizers, Schick &amp;amp; Schütze (arXiv 2001.07676)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label probabilities carry prior bias&lt;/td&gt;
&lt;td&gt;Calibrate Before Use, Zhao et al. (2021)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One fitted temperature fixes overconfidence&lt;/td&gt;
&lt;td&gt;Guo et al. (2017); Kadavath et al. (2022), a temperature of 2.5 for an RLHF policy, beside Jev's 2.66, Nimble's 2.179 and Luce's 2.5 to 3.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-training worsens calibration&lt;/td&gt;
&lt;td&gt;GPT-4 report, Fig. 8: MMLU ECE 0.007 to 0.074 after PPO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-entropy rewards reporting true probabilities&lt;/td&gt;
&lt;td&gt;Gneiting &amp;amp; Raftery (2007): the log score is strictly proper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheap model first, defer when unsure&lt;/td&gt;
&lt;td&gt;Selective classification (2017); FrugalGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The visible contribution is the packaging: a typed API, one hosted endpoint, output priced at zero, and many questions evaluated against one input in a single request. Whether RLCD adds more can be judged once it is published; Jev's native probabilities beating most LLMs' self-reported confidence suggests the training does something.&lt;/p&gt;




&lt;h4&gt;
  
  
  What About the Open Alternatives?
&lt;/h4&gt;

&lt;p&gt;Each trained replica is built on Qwen or an encoder, and each publishes its own evaluation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Own headline&lt;/th&gt;
&lt;th&gt;Independent evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nimble-9B (Bespoke Labs)&lt;/td&gt;
&lt;td&gt;Qwen3.5-9B + LoRA&lt;/td&gt;
&lt;td&gt;75.9% vs Jev 77.3% on 13 public sets&lt;/td&gt;
&lt;td&gt;none; run by the seller, with raw probabilities (fitted temperature 2.179); its own model trails Jev&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kev (Jared Palmer)&lt;/td&gt;
&lt;td&gt;Qwen3.5 + LoRA + pointer head&lt;/td&gt;
&lt;td&gt;Kev-9B 0.852 test vs Jev 0.857 dev&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Luce (scienthoon)&lt;/td&gt;
&lt;td&gt;Qwen3-4B + LoRA&lt;/td&gt;
&lt;td&gt;86.9% vs Jev 84.7% on 500 kubernetes issues&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laya (Convai)&lt;/td&gt;
&lt;td&gt;ModernBERT-large 421M&lt;/td&gt;
&lt;td&gt;0.766 vs 0.727 after training on the benchmark&lt;/td&gt;
&lt;td&gt;ikkun1222: 29.2 / 60.0 / 40.0 on three Japanese tasks; the card's own zero-shot 0.362 sits below its 0.461 majority baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleJev (Featherless)&lt;/td&gt;
&lt;td&gt;any HF model; demo serves Gemma 4 26B&lt;/td&gt;
&lt;td&gt;no accuracy published&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiffusionGemma via vLLM PR #57250&lt;/td&gt;
&lt;td&gt;one denoise step of &lt;code&gt;diffusiongemma-26B-A4B-it&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;198/201 vs Jev 191/201, "roughly tied"&lt;/td&gt;
&lt;td&gt;ywchiu: 32.2% vs Jev 61.4% on synthetic routing; companion article: 75.9% vs Jev 77.3% on the public suite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SimpleJev's README states that its probabilities "are not calibrated probabilities of correctness". The DiffusionGemma headline comes from 201 items built by the PR's author; publishing the items would let others reproduce it.&lt;/p&gt;

&lt;p&gt;Labels beat zero-shot wherever they were tried. A 310M Japanese encoder trained on 200 rows beat Jev on news topics by 12 points, 88.8% against 76.8%, and tied it on two sentiment tasks, and fine-tuned models led by 2 to 15 points on five public splits. On phishing, Luce trained on 1,000 labels reached 97.4% against Jev's 62.6% on the same benchmark, though on different items. On that benchmark, a two-line regex scored 91.6%, five narrow Jev questions combined by logistic regression reached 95.0% on a held-out half, and Haiku 4.5 asked the same five questions reached 93.2%, a difference too small to be significant (p = 0.063).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;thejevai.com&lt;/code&gt;, registered on September 20 and promoted on Hugging Face as the endpoint for API keys, has no stated connection to TypeSafe. TypeSafe's API is &lt;code&gt;api.typesafe.ai&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;Jev&lt;/th&gt;
&lt;th&gt;Trained open replica&lt;/th&gt;
&lt;th&gt;Plain open model, label scores read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Labels needed&lt;/td&gt;
&lt;td&gt;none to run; about 50 to fit a temperature&lt;/td&gt;
&lt;td&gt;hundreds to thousands, plus a training run&lt;/td&gt;
&lt;td&gt;none to run; about 50 to fit a temperature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy on human labels&lt;/td&gt;
&lt;td&gt;level with Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash; 6.5 to 11.5 behind the frontier&lt;/td&gt;
&lt;td&gt;trails or matches Jev generically; beats it when trained on the task's own labels&lt;/td&gt;
&lt;td&gt;Gemma 4 26B, 4-bit: 75.3% vs Jev 77.3% on the 3,880-record suite; level on yes/no, 4.5 behind on multiple choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calibration&lt;/td&gt;
&lt;td&gt;🥇 median ECE 0.071 as shipped on the public suite; none published by the vendor&lt;/td&gt;
&lt;td&gt;own evaluations&lt;/td&gt;
&lt;td&gt;median ECE 0.180 as shipped, 0.080 after 50 labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;TypeSafe, OpenRouter, Vercel&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0.042 per million input tokens; $5.54 per million decisions at 132 tokens&lt;/td&gt;
&lt;td&gt;your hardware&lt;/td&gt;
&lt;td&gt;at most $5.43 per million decisions on an L4 at full load&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 binary and few-class decisions in English, at high volume, with no infrastructure to run and no labels yet, Jev is fast and cheap, its accuracy sits with mid-price LLMs, and its probabilities are the best calibrated out of the box on familiar English tasks. Plan on labelling 50 to a few hundred decisions to set thresholds and fit its probabilities.&lt;/p&gt;

&lt;p&gt;For multi-class work with many options, decisions where the rule lives outside the input, or non-English text, where Jev loses accuracy, a model trained on your own labels won wherever one was tried; for non-English that evidence is one Japanese study.&lt;/p&gt;

&lt;p&gt;For data that cannot leave your network, any open model read by label scores gives the same typed interface. In the companion article plain Gemma 4 26B trails Jev by 2.1 points overall and 4.5 on multiple choice, and needs about 50 labels to come within 0.01 of Jev's calibration.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Still Unmeasured?
&lt;/h4&gt;

&lt;p&gt;The published Gemma data points so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemma 4 31B beats Jev's median F1 in the pre-registered social-science study, 0.611 against 0.581&lt;/li&gt;
&lt;li&gt;Gemma 4 31B scores 98.86% on SNIPS against Jev's 97.14% after its output's code fences were stripped; 80.57% before&lt;/li&gt;
&lt;li&gt;Gemma 4 26B-A4B trails Jev by 11.6 points on Japanese news topics and ties it on two sentiment tasks&lt;/li&gt;
&lt;li&gt;Gemma 4 E4B, stating its confidence as text, scores 81.0% against Jev's 95.5% on synthetic emails&lt;/li&gt;
&lt;li&gt;Plain Gemma 4 26B read by its label probabilities, in the companion article: on four public tasks (sst2, AG News, DAIR Emotion, tweet_eval irony) and on Bespoke Labs' 3,880-record, 13-subset suite, 2.1 points behind Jev overall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still unpublished: Jev's behaviour across model versions, calibration across difficulty on real data, and whether a generative LLM shows the same option-name sensitivity.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to establish what independent evidence shows about TypeSafe's Jev and its open alternatives. The key to the solution was tracing every figure to its primary source and re-scoring from published per-item outputs wherever they existed. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Jev returns a valid option on every call, and its server time is about 0.1 s&lt;/li&gt;
&lt;li&gt;⚠️ Accuracy sits with mid-price LLMs: 72.5% against 74.5–76.0% for Kimi K3, MiniMax M3 and DeepSeek V4.1 Flash in one six-model study, and a median 11.6 F1 behind the best LLM per task in the largest pre-registered one&lt;/li&gt;
&lt;li&gt;🟢 Strong on binary and few-class work: 98.33% on 18,514 emails, and level with a dedicated reranker&lt;/li&gt;
&lt;li&gt;🟢 Best calibrated out of the box on familiar English tasks: median ECE 0.071 on the 13-subset public suite&lt;/li&gt;
&lt;li&gt;🟢 50 to a few hundred of your own labels and one fitted temperature fix most of its calibration error, and most of an open model's&lt;/li&gt;
&lt;li&gt;⚠️ Measured speed gains run from 0.5x to 12.1x and cost gains from 0.6x to 478x; TypeSafe's 193.6x and 444.6x have no named comparator&lt;/li&gt;
&lt;li&gt;⚠️ No calibration metric is published by TypeSafe, and the direction of its calibration error changes by domain&lt;/li&gt;
&lt;li&gt;⚠️ Swapping option names changed about a third of its answers in one unreplicated study&lt;/li&gt;
&lt;li&gt;⚠️ Plain Gemma 4 26B read by label probabilities (companion article) trails Jev by 2.1 points on the 3,880-record suite, is level on yes/no, 4.5 behind on multiple choice, and needs 50 labels to come within 0.01 of Jev's calibration&lt;/li&gt;
&lt;li&gt;❌ RLCD has no published method, and the model's size, base and training data are undisclosed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: sources read on September 23, 2026, eight days after launch: 14 arXiv preprints, 104 GitHub repositories, 33 dev.to and Medium posts, and TypeSafe's own pages; the companion article's public-suite run finished on September 24 UTC. Every accuracy, calibration and timing figure here is a third party's measurement, marked in the full report as re-scored, checked or reported, except the Gemma figures in Compare and Contrast and the Summary, which are the author's own measurement in the companion article; that article's Jev figures come from Bespoke Labs, which sells a competing model. Every run that states a version used &lt;code&gt;jev-1.13.0&lt;/code&gt;. Sources were gathered and audited with AI assistance (Claude), and every figure was checked against its source, cited in the full report, except LangChain's, marked not re-checked there. The author is a Google Developer Expert and an AWS Community Builder; neither Google nor AWS funded, reviewed or saw this work. The author has no relationship with TypeSafe, Bespoke Labs or any replica reviewed.&lt;/p&gt;

&lt;p&gt;The strategy for using primary sources to evaluate Jev 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;Full evidence review with every source: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/jev/reports/Jev%20independent%20evidence%20review.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/jev/reports/Jev%20independent%20evidence%20review.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion measurement article: &lt;a href="https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6"&gt;https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe launch post: &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;https://typesafe.ai/blog/introducing-system-one-models-and-jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe models and pricing: &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe evals: &lt;a href="https://evals.typesafe.ai/" rel="noopener noreferrer"&gt;https://evals.typesafe.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jev 1.13 known weaknesses: &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/model-jaggedness/jev-1.13&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ibrahim &amp;amp; Zaki, arXiv 2609.24574: &lt;a href="https://arxiv.org/abs/2609.24574" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.24574&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rafe &amp;amp; Das, arXiv 2609.24052: &lt;a href="https://arxiv.org/abs/2609.24052" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.24052&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Li et al., arXiv 2609.26550: &lt;a href="https://arxiv.org/abs/2609.26550" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.26550&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Option naming, arXiv 2609.26758: &lt;a href="https://arxiv.org/abs/2609.26758" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.26758&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Janardhan, jev-frontier-bench: &lt;a href="https://github.com/manjunathshiva/jev-frontier-bench" rel="noopener noreferrer"&gt;https://github.com/manjunathshiva/jev-frontier-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nimble public benchmarks: &lt;a href="https://github.com/bespokelabsai/nimble/blob/main/docs/PUBLIC_BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/bespokelabsai/nimble/blob/main/docs/PUBLIC_BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;jev-phishing-bench: &lt;a href="https://github.com/anisselbd/jev-phishing-bench" rel="noopener noreferrer"&gt;https://github.com/anisselbd/jev-phishing-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Luce: &lt;a href="https://github.com/scienthoon/luce" rel="noopener noreferrer"&gt;https://github.com/scienthoon/luce&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OmarMujahid, jev-decision-bench: &lt;a href="https://github.com/OmarMujahid/jev-decision-bench" rel="noopener noreferrer"&gt;https://github.com/OmarMujahid/jev-decision-bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;bitnovus, jev-spam-eval: &lt;a href="https://github.com/bitnovus/jev-spam-eval" rel="noopener noreferrer"&gt;https://github.com/bitnovus/jev-spam-eval&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SamuelSacco, jev-exploration: &lt;a href="https://github.com/SamuelSacco/jev-exploration" rel="noopener noreferrer"&gt;https://github.com/SamuelSacco/jev-exploration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;sanand0, BANKING77 pilot: &lt;a href="https://sanand0.github.io/llmevals/jev/" rel="noopener noreferrer"&gt;https://sanand0.github.io/llmevals/jev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;heiko-hotz, jev-evaluation: &lt;a href="https://github.com/heiko-hotz/jev-evaluation" rel="noopener noreferrer"&gt;https://github.com/heiko-hotz/jev-evaluation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ywchiu, jev_benchmark: &lt;a href="https://github.com/ywchiu/jev_benchmark" rel="noopener noreferrer"&gt;https://github.com/ywchiu/jev_benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ikkun1222, Jev against a 310M encoder: &lt;a href="https://dev.to/ikkun1222/jev-vs-a-310m-encoder-i-trained-myself-750-rows-three-tasks-two-different-winners-242e"&gt;https://dev.to/ikkun1222/jev-vs-a-310m-encoder-i-trained-myself-750-rows-three-tasks-two-different-winners-242e&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SimpleJev: &lt;a href="https://github.com/featherless-ai/simple-jev" rel="noopener noreferrer"&gt;https://github.com/featherless-ai/simple-jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM PR #57250: &lt;a href="https://github.com/vllm-project/vllm/pull/57250" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/57250&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matt Mastracci, Jev against DiffusionGemma: &lt;a href="https://x.com/mmastrac/status/2100626193943052784" rel="noopener noreferrer"&gt;https://x.com/mmastrac/status/2100626193943052784&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guo et al., On Calibration of Modern Neural Networks: &lt;a href="https://arxiv.org/abs/1706.04599" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.04599&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kadavath et al., Language Models (Mostly) Know What They Know: &lt;a href="https://arxiv.org/abs/2207.05221" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2207.05221&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Brown et al., GPT-3: &lt;a href="https://arxiv.org/abs/2005.14165" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2005.14165&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hendrycks et al., MMLU: &lt;a href="https://arxiv.org/abs/2009.03300" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2009.03300&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schick &amp;amp; Schütze, verbalizers: &lt;a href="https://arxiv.org/abs/2001.07676" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2001.07676&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zhao et al., Calibrate Before Use: &lt;a href="https://arxiv.org/abs/2102.09690" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2102.09690&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gneiting &amp;amp; Raftery, Strictly Proper Scoring Rules, Prediction, and Estimation, Journal of the American Statistical Association 102 (2007)&lt;/li&gt;
&lt;li&gt;Geifman &amp;amp; El-Yaniv, Selective Classification: &lt;a href="https://arxiv.org/abs/1705.08500" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1705.08500&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chen et al., FrugalGPT: &lt;a href="https://arxiv.org/abs/2305.05176" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2305.05176&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GPT-4 technical report: &lt;a href="https://arxiv.org/abs/2303.08774" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2303.08774&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;InstructGPT: &lt;a href="https://arxiv.org/abs/2203.02155" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2203.02155&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Christiano et al. 2017: &lt;a href="https://arxiv.org/abs/1706.03741" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.03741&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Plain Gemma 4 26B vs Jev on One EC2 L4: 2.1 Points Behind Overall, Level on Yes/No, 4.5 Behind on Multiple Choice</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 24 Sep 2026 02:00:37 +0000</pubDate>
      <link>https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6</link>
      <guid>https://dev.to/gde/plain-gemma-4-26b-vs-jev-on-one-ec2-l4-21-points-behind-overall-level-on-yesno-45-behind-on-15k6</guid>
      <description>&lt;p&gt;This article provides a step by step guide to measuring Gemma 4 26B as a Jev-style decision model on an AWS EC2 L4 GPU, and compares a plain read of its label probabilities against DiffusionGemma's one-step read and against Jev's published results. The measurement was pre-registered, and every per-item output is committed.&lt;/p&gt;

&lt;p&gt;On Bespoke Labs' 3,880-record public suite, plain Gemma 4 26B trails Jev 1.13.0 by 2.1 points overall, shows no measurable difference on yes/no questions, and trails by 4.5 points on multiple choice. Jev is better calibrated out of the box, and one temperature fitted on 50 labels brings Gemma's median calibration error within 0.01 of Jev's. Against DiffusionGemma, the plain read is level on accuracy, worse calibrated before fitting and similar after, and 1.9 to 5.1 times faster per decision.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  Why Measure This?
&lt;/h4&gt;

&lt;p&gt;A Jev-style decision model answers a typed question with a probability for each allowed option, in one forward pass, with no generated text. TypeSafe's Jev does this as a hosted service. Any open model can do it: end the prompt where the answer starts, read the scores of the allowed label tokens, and apply a softmax over those.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln"&gt;companion review of the independent evidence on Jev&lt;/a&gt; found two open questions for Gemma. Plain Gemma read this way had no published accuracy or calibration result. DiffusionGemma, read through vLLM PR #57250, had been described by Google's Gemma account on September 18 as "yielding well-calibrated decision distributions", with no published measurement behind it.&lt;/p&gt;

&lt;p&gt;This run answers both on the same GPU, the same prompts, the same label tokens and the same scoring code, using community 4-bit (AWQ) builds of both models. A third run puts both, and Gemma 4 E4B, on the public suite where Bespoke Labs has published results for Jev, so the Gemma numbers sit beside Jev's on identical records.&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 one G-family instance, and the AWS CLI logged in&lt;/li&gt;
&lt;li&gt;A security group, subnet and instance profile with &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; for remote commands&lt;/li&gt;
&lt;li&gt;Python 3 with &lt;code&gt;transformers&lt;/code&gt; and &lt;code&gt;pybase64&lt;/code&gt; for the client side&lt;/li&gt;
&lt;li&gt;The repository cloned: &lt;code&gt;git clone https://github.com/xbill9/gemma4-dev&lt;/code&gt; and &lt;code&gt;cd gemma4-dev/jev&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Pre-Register the Measurement
&lt;/h4&gt;

&lt;p&gt;The models, image, serving flags, data, metrics and comparisons are written down and committed before any model call. The file is &lt;code&gt;PREREGISTRATION.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; 3c67f62 d94471d a5cda09 894323e adf0c16
&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;3c67f62 jev: pre-registration for the L4 AWQ run; option-order variant, label-count calibration curve, EC2 user-data
d94471d jev: run 2026-09-23-l4-awq — Gemma 4 26B label logits vs DiffusionGemma one-step reads, both AWQ 4-bit on one EC2 L4; per-item outputs, summary, host evidence
a5cda09 jev: pre-registration addendum (latency pass on-instance, off-label tokens, E2B/E4B bf16 arms); 20-split calibration analysis; archived run log and throughput arithmetic
894323e jev: plain arm builds its prompt from the served model's own chat template (26B unchanged, checked); E4B/E2B deviation recorded
adf0c16 jev: Bespoke Labs public-suite run — rebuild with checksum checks, runner via the proxy's Jev parser, scorer matched to Nimble's definitions, instance driver, generic launcher; pre-registration addendum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latency and small-model runs were added in one pre-registration addendum after the first run, and the public-suite run in another; each was committed before its own first call, and each deviation from the plan is recorded there. The pre-registration commits to publishing every result, including any where either arm does worse.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Pick a Matched Pair of Checkpoints
&lt;/h4&gt;

&lt;p&gt;An NVIDIA L4 has 24 GB of memory, so both 26B models run at 4 bits. Both checkpoints come from the same uploader and apply the same quantization settings to both models, so what differs is the model and how it is read.&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;Plain arm&lt;/th&gt;
&lt;th&gt;Diffusion arm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;compressed-tensors, 4-bit, group 32, symmetric&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;17.19 GB&lt;/td&gt;
&lt;td&gt;17.22 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both arms share one set of serving flags.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Launch One L4 on EC2
&lt;/h4&gt;

&lt;p&gt;The security group opens port 8000 to one address only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-security-group &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--group-name&lt;/span&gt; jev-eval-sg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"jev eval: vLLM 8000 from one IP"&lt;/span&gt; &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; &amp;lt;vpc-id&amp;gt; &lt;span class="nt"&gt;--query&lt;/span&gt; GroupId &lt;span class="nt"&gt;--output&lt;/span&gt; text
aws ec2 authorize-security-group-ingress &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--group-id&lt;/span&gt; &amp;lt;sg-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="nt"&gt;--cidr&lt;/span&gt; &amp;lt;your-ip&amp;gt;/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance boots the regional GPU Deep Learning AMI and runs &lt;code&gt;aws/user-data.sh&lt;/code&gt;, which pulls a vLLM nightly that contains PR #57250 and serves one model at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 run-instances &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--image-id&lt;/span&gt; &amp;lt;gpu-dlami&amp;gt; &lt;span class="nt"&gt;--instance-type&lt;/span&gt; g6.xlarge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &amp;lt;subnet-id&amp;gt; &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &amp;lt;sg-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-instance-profile&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;profile&amp;gt; &lt;span class="nt"&gt;--user-data&lt;/span&gt; file://aws/user-data.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--block-device-mappings&lt;/span&gt; &lt;span class="s1"&gt;'DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3,DeleteOnTermination=true}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=jev-eval},{Key=ManagedBy,Value=jev}]'&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;i-0c5b14e913b4c1019 g6.xlarge   us-east-1a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;g6.xlarge&lt;/code&gt; has the same L4 as the larger sizes with 16 GB of host memory, so &lt;code&gt;user-data.sh&lt;/code&gt; adds a 16 GB swapfile on hosts under 30 GB; both 17 GB checkpoints loaded with it in place.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Serve the Plain Arm
&lt;/h4&gt;

&lt;p&gt;One flag set serves both arms: Triton attention, eager mode, prefix caching, 2048-token context, 16 sequences and 32 logprobs. The diffusion arm adds a 64-token canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/opt/jev/serve.sh ar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client waits on &lt;code&gt;/v1/models&lt;/code&gt; until the model answers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;READY after ~480s
['cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers the image pull, the 17 GB download and the load. The host record from the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NVIDIA L4, 595.91.07, 23034 MiB
image: vllm/vllm-openai:nightly-e9757321527ca1ecd514c07c1418dd2c53da3d19 digest: sha256:f75fec992c293dd41042745f8aa71bfdf12b188b6a01e0baf075e4bca5d297e5
vllm 0.29.1rc1.dev573+ge97573215 torch 2.13.0+cu130
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 5 — Check the Read Before the Full Run
&lt;/h4&gt;

&lt;p&gt;Both arms use the prompt, answer template, label tokens and probability code from the PR's own &lt;code&gt;structured_server.py&lt;/code&gt;. &lt;code&gt;run_eval.py&lt;/code&gt; confirms both chat templates render the identical prefix before it sends anything. A five-example run checks the path end to end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://&amp;lt;host&amp;gt;:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit &lt;span class="nt"&gt;--run&lt;/span&gt; smoke &lt;span class="nt"&gt;--limit&lt;/span&gt; 5 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 2
&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;autoregressive sst2: 5/5 (5s)
autoregressive ag_news: 5/5 (1s)
autoregressive emotion: 5/5 (0s)
autoregressive irony: 5/5 (0s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record stores the label probabilities, &lt;code&gt;labels_returned&lt;/code&gt;, which confirms every label came back with a real probability, and &lt;code&gt;label_mass&lt;/code&gt;, the share of the model's probability that landed on the allowed labels.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Run Both Arms
&lt;/h4&gt;

&lt;p&gt;The data is 300 labelled examples each from sst2, AG News, DAIR Emotion and tweet_eval irony, stratified with a fixed seed. sst2 and irony carry annotators' labels, AG News its source's news categories, and DAIR Emotion labels taken from the hashtags on each tweet. Each arm runs once as written, and the three choice tasks run once more with their options listed in reverse order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 8
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 8 &lt;span class="nt"&gt;--variant&lt;/span&gt; reversed
/opt/jev/serve.sh diffusion
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 4
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="nv"&gt;$U&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 4 &lt;span class="nt"&gt;--variant&lt;/span&gt; reversed
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; results/2026-09-23-l4-awq/diffusion-&lt;span class="k"&gt;*&lt;/span&gt;.jsonl
&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;    300 results/2026-09-23-l4-awq/diffusion-ag_news.jsonl
    300 results/2026-09-23-l4-awq/diffusion-ag_news--reversed.jsonl
    300 results/2026-09-23-l4-awq/diffusion-emotion.jsonl
    300 results/2026-09-23-l4-awq/diffusion-emotion--reversed.jsonl
    300 results/2026-09-23-l4-awq/diffusion-irony.jsonl
    300 results/2026-09-23-l4-awq/diffusion-sst2.jsonl
    300 results/2026-09-23-l4-awq/diffusion-sst2--reversed.jsonl
   2100 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diffusion arm reads each example four times with different noise in the answer slots, as the PR's proxy does. The DiffusionGemma server came up in about 300 seconds with the image already pulled.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Score It
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 score.py &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-awq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;score.py&lt;/code&gt; writes &lt;code&gt;SUMMARY.md&lt;/code&gt; and &lt;code&gt;summary.json&lt;/code&gt; beside the records. Every figure below comes from those two files. Readouts: &lt;code&gt;plain&lt;/code&gt; is one read of plain Gemma. &lt;code&gt;diffusion&lt;/code&gt; is the proxy's automatic readout, the mean of four DiffusionGemma reads on the 96% to 100% of examples where its rule re-reads and one read on the rest; the label-count tables use the four-read mean throughout.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Accurate Is Each One?
&lt;/h4&gt;

&lt;p&gt;Level on three tasks, plain ahead on AG News.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Always-majority&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;th&gt;Difference (95% range)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;51.0%&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;93.3%&lt;/td&gt;
&lt;td&gt;−1.7 (−4.0 to +0.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;🥇 86.7%&lt;/td&gt;
&lt;td&gt;83.0%&lt;/td&gt;
&lt;td&gt;−3.7 (−6.7 to −0.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;35.0%&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;+2.0 (−1.0 to +5.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;td&gt;87.3%&lt;/td&gt;
&lt;td&gt;−2.3 (−5.7 to +1.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughout, level means the 95% range of the difference includes zero, and 🥇 in an accuracy table marks a lead whose range excludes it; in Compare and Contrast 🥇 marks the better value. Only the AG News difference excludes zero.&lt;/p&gt;




&lt;h4&gt;
  
  
  Are the Raw Probabilities Calibrated?
&lt;/h4&gt;

&lt;p&gt;DiffusionGemma's are closer. Plain Gemma's are strongly overconfident.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma ECE&lt;/th&gt;
&lt;th&gt;DiffusionGemma ECE&lt;/th&gt;
&lt;th&gt;Difference (95% range)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;0.051&lt;/td&gt;
&lt;td&gt;0.045&lt;/td&gt;
&lt;td&gt;−0.005 (−0.021 to +0.020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.129&lt;/td&gt;
&lt;td&gt;0.119&lt;/td&gt;
&lt;td&gt;−0.010 (−0.030 to +0.020)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;0.386&lt;/td&gt;
&lt;td&gt;0.261&lt;/td&gt;
&lt;td&gt;−0.125 (−0.154 to −0.089)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;0.098&lt;/td&gt;
&lt;td&gt;0.043&lt;/td&gt;
&lt;td&gt;−0.055 (−0.077 to −0.014)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ECE is expected calibration error over 15 bins: the gap between how confident a model says it is and how often it is right. DiffusionGemma is better calibrated on emotion and irony, and level on the other two.&lt;/p&gt;

&lt;p&gt;The fitted temperatures show the size of the gap. On the pre-registered single split, one temperature that best fits the labels was 3.48 to 7.24 for plain Gemma once 25 or more labels were used, and 0.73 to 2.36 for DiffusionGemma. A temperature above 1 means the model's probabilities are too extreme.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Do 50 Labels Buy?
&lt;/h4&gt;

&lt;p&gt;Most of the correction for plain Gemma; a smaller gain for DiffusionGemma, and none on irony.&lt;/p&gt;

&lt;p&gt;Each task's examples split into a fitting half and a held-out half. One temperature is fitted on the first N labels of the fitting half and scored on the held-out half. On the pre-registered single split, 50 labels took plain Gemma from 0.040, 0.139, 0.440 and 0.088 to 0.029, 0.064, 0.102 and 0.042 on sst2, AG News, DAIR Emotion and irony, and DiffusionGemma from 0.047, 0.132, 0.311 and 0.055 to 0.025, 0.061, 0.115 and 0.054.&lt;/p&gt;

&lt;p&gt;One split of 150 held-out examples moves ECE by several hundredths, so the same fit was repeated over 20 random splits, an analysis added after the run. Means, with the lowest and highest split in brackets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain, 0 labels&lt;/th&gt;
&lt;th&gt;Plain, 50 labels&lt;/th&gt;
&lt;th&gt;Diffusion, 0 labels&lt;/th&gt;
&lt;th&gt;Diffusion, 50 labels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;0.048&lt;/td&gt;
&lt;td&gt;0.039 (0.021–0.061)&lt;/td&gt;
&lt;td&gt;0.050&lt;/td&gt;
&lt;td&gt;0.042 (0.025–0.081)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.130&lt;/td&gt;
&lt;td&gt;0.066 (0.037–0.132)&lt;/td&gt;
&lt;td&gt;0.121&lt;/td&gt;
&lt;td&gt;0.085 (0.060–0.138)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;0.392&lt;/td&gt;
&lt;td&gt;0.089 (0.047–0.161)&lt;/td&gt;
&lt;td&gt;0.266&lt;/td&gt;
&lt;td&gt;0.111 (0.062–0.155)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;0.099&lt;/td&gt;
&lt;td&gt;0.060 (0.035–0.130)&lt;/td&gt;
&lt;td&gt;0.054&lt;/td&gt;
&lt;td&gt;0.066 (0.031–0.116)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With no labels, DiffusionGemma is better calibrated on emotion and irony. With 50, plain Gemma's mean is at or below DiffusionGemma's on all four tasks, and the split-to-split ranges overlap throughout.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does DiffusionGemma's Read Do?
&lt;/h4&gt;

&lt;p&gt;About half of its probability lands outside the allowed labels. The median share on the allowed labels per task:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;0.370&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.392&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.593&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the answer slot the server returns the allowed labels plus the single most likely token. In the latency run that token was &lt;code&gt;&amp;lt;eos&amp;gt;&lt;/code&gt;, the end-of-sequence token, in 40% of reads on sst2, 53% on AG News and 64% on DAIR Emotion, and &lt;code&gt;the&lt;/code&gt; in 24% of reads on irony. The proxy rescales the label probabilities to sum to one, so the answer reads as confident either way; &lt;code&gt;label_mass&lt;/code&gt; is the field that shows it, and it is worth logging in any deployment.&lt;/p&gt;

&lt;p&gt;The proxy re-reads when the answer slot's entropy is above 0.1. It computes that entropy from the returned tokens' full-vocabulary probabilities without rescaling them, so a label holding half the probability on its own already scores 0.35. It re-read 100% of sst2 and AG News examples, 99.0% of DAIR Emotion and 96.3% of irony.&lt;/p&gt;

&lt;p&gt;The re-reads barely change the answer: one read and the mean of four are within 0.6 points of accuracy on every task (93.3% and 93.3% on sst2, 83.3% and 83.0% on AG News, 60.7% and 60.3% on DAIR Emotion, 86.7% and 87.3% on irony). How much the four reads disagree separates right from wrong well on sst2 and irony, AUROC 0.891 and 0.873, and weakly on AG News and DAIR Emotion, 0.685 and 0.635.&lt;/p&gt;




&lt;h4&gt;
  
  
  Does Option Order Change the Answer?
&lt;/h4&gt;

&lt;p&gt;For both models, by similar amounts. Listing the choice options in reverse order changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Plain Gemma&lt;/th&gt;
&lt;th&gt;DiffusionGemma&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;6 of 300 (2.0%)&lt;/td&gt;
&lt;td&gt;7 of 300 (2.3%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;14 of 300 (4.7%)&lt;/td&gt;
&lt;td&gt;21 of 300 (7.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;27 of 300 (9.0%)&lt;/td&gt;
&lt;td&gt;26 of 300 (8.7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  How Fast Is Each One?
&lt;/h4&gt;

&lt;p&gt;A second run timed both arms with the client on the instance itself, against &lt;code&gt;localhost&lt;/code&gt;, one request at a time, over the first 100 examples of each task. Same image, flags and checkpoints, on a &lt;code&gt;g6.4xlarge&lt;/code&gt;: the same L4 with a larger host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-latency &lt;span class="nt"&gt;--limit&lt;/span&gt; 100 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1
python3 run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; diffusion &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$DIFF&lt;/span&gt; &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-23-l4-latency &lt;span class="nt"&gt;--limit&lt;/span&gt; 100 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--reads&lt;/span&gt; 4 &lt;span class="nt"&gt;--keep-top&lt;/span&gt; 5
&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;| Task | Plain, median | Plain, 90th pct | Diffusion one read, median | Diffusion one read, 90th pct | Diffusion 4 reads (first + 3 parallel), median |
|---|---|---|---|---|---|
| sst2 | 61 | 62 | 118 | 119 | 306 |
| ag_news | 61 | 62 | 119 | 120 | 307 |
| emotion | 61 | 62 | 119 | 120 | 308 |
| irony | 61 | 61 | 121 | 122 | 312 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Times are in milliseconds. A plain Gemma 26B decision takes 61 ms on one L4 for these prompts, which run 105 to 299 tokens with a median of 132; longer inputs such as full tickets or logs take longer. One DiffusionGemma read takes 1.9 to 2.0 times as long, and the automatic rule's four reads 5.0 to 5.1 times, which is the path it takes on nearly every call. At 61 ms one request at a time, one L4 answers about 16 plain decisions a second before any batching.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Fit One Temperature Before You Trust a Threshold
&lt;/h4&gt;

&lt;p&gt;Both models give usable probabilities after one fitted temperature. &lt;code&gt;score.py&lt;/code&gt; does it with a grid search on log loss:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fit_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T_GRID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;temper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;gold_idx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Label 50 real decisions from the task, fit on them, and set any act-or-escalate threshold on the rescaled probabilities. Plain Gemma needs the larger correction, and both reach similar calibration once it is applied.&lt;/p&gt;




&lt;h4&gt;
  
  
  What About the Smaller Gemma 4 Models?
&lt;/h4&gt;

&lt;p&gt;The label read works with any Gemma 4. DiffusionGemma ships only at 26B-A4B, so the smaller models get the plain arm alone, run in bf16 on the same L4 with the same flags. Their chat templates end at the model turn without the empty thought block the 26B template adds, so their prompt follows their own template. These two arms were added in a pre-registration addendum after the first run, and are exploratory. The 26B column is 4-bit and the others bf16, so the gaps mix model size with precision. All three read letter labels (A to F) with the meanings listed in the prompt, the format of the PR's proxy; smaller models may do better with the label words as the answers themselves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;26B, 4-bit&lt;/th&gt;
&lt;th&gt;E4B, bf16&lt;/th&gt;
&lt;th&gt;E2B, bf16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sst2&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;94.3%&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;30.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAIR Emotion&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;td&gt;54.0%&lt;/td&gt;
&lt;td&gt;53.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tweet_eval irony&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;E4B is 0.7 to 5.4 points behind the 26B, needs a smaller correction (fitted temperature 1.89 to 3.77 at 50 labels, against 4.65 to 7.05, both means over 20 splits), and reaches similar calibration once corrected: 0.042, 0.071, 0.089 and 0.081 against the 26B's 0.039, 0.066, 0.089 and 0.060. It changed more answers when the options were reversed on AG News, 35 of 300 against 14.&lt;/p&gt;

&lt;p&gt;E2B answered "world" on 277 of 300 AG News examples, and on 263 when the options were listed in reverse order, so the same option wins whatever its position, and its topic accuracy sits near the 25% majority rate. Its fitted temperatures reach 7.99, the top of the pre-registered search range, so its corrected calibration figures understate what a wider search would reach.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Does It Compare With Jev?
&lt;/h4&gt;

&lt;p&gt;Bespoke Labs, which makes the open Nimble-9B decision model, ran Jev 1.13.0 through its API on a 13-subset public suite and published the results per subset, with the converters and record ids. The suite holds 3,880 human-labelled records: yes/no questions from BoolQ, PAWS, SQuAD 2.0, Civil Comments and Aegis 2.0; multiple choice from MultiNLI, PubMedQA, VitaminC and MASSIVE intents in English and German; and five-level ratings from HelpSteer2 and SummEval.&lt;/p&gt;

&lt;p&gt;A third run, on a &lt;code&gt;g6.xlarge&lt;/code&gt; with the same image and flags, rebuilt the suite from the public sources and read it with all three Gemma arms. All 13 rebuilt subsets matched the published checksums, so the Gemma figures and Bespoke Labs' Jev figures come from the same records.&lt;/p&gt;

&lt;p&gt;Each record is a Jev request, which the PR's proxy converts with its own Jev parser, so the Gemma arms see the same prompt format as the first run: yes/no questions answered with the word yes or no, multiple-choice options lettered A, B, C with the answer read as a letter, and rating levels numbered from 1. Gemma was read in that format as published, with no prompt tuning, while Jev takes the request in its own format; a different prompt could move the multiple-choice gap in either direction. Scoring uses Bespoke Labs' definitions, and &lt;code&gt;tests/test_suite_scoring.py&lt;/code&gt; checks it against Bespoke Labs' own code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash nimble_suite/build.sh &amp;lt;workdir&amp;gt;
python3 nimble_suite/run_suite.py &lt;span class="nt"&gt;--arm&lt;/span&gt; autoregressive &lt;span class="nt"&gt;--upstream&lt;/span&gt; http://localhost:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$PLAIN&lt;/span&gt; &lt;span class="nt"&gt;--records&lt;/span&gt; &amp;lt;workdir&amp;gt;/public &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite
python3 nimble_suite/suite_stats.py &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite &lt;span class="nt"&gt;--run&lt;/span&gt; 2026-09-24-l4-suite-e4b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accuracy pooled over records, with the 95% range in brackets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Questions&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;th&gt;Jev 1.13.0, published&lt;/th&gt;
&lt;th&gt;Nimble-9B, published&lt;/th&gt;
&lt;th&gt;Plain Gemma 26B&lt;/th&gt;
&lt;th&gt;DiffusionGemma 26B&lt;/th&gt;
&lt;th&gt;Gemma 4 E4B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;3,880&lt;/td&gt;
&lt;td&gt;77.3% (76.0–78.6)&lt;/td&gt;
&lt;td&gt;75.9%&lt;/td&gt;
&lt;td&gt;75.3% (73.9–76.6)&lt;/td&gt;
&lt;td&gt;75.9% (74.5–77.2)&lt;/td&gt;
&lt;td&gt;73.3% (71.9–74.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes/no&lt;/td&gt;
&lt;td&gt;1,399&lt;/td&gt;
&lt;td&gt;84.6% (82.6–86.4)&lt;/td&gt;
&lt;td&gt;80.1%&lt;/td&gt;
&lt;td&gt;84.8% (82.8–86.6)&lt;/td&gt;
&lt;td&gt;84.5% (82.5–86.3)&lt;/td&gt;
&lt;td&gt;81.9% (79.8–83.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple choice&lt;/td&gt;
&lt;td&gt;1,848&lt;/td&gt;
&lt;td&gt;82.8% (81.1–84.5)&lt;/td&gt;
&lt;td&gt;81.1%&lt;/td&gt;
&lt;td&gt;78.3% (76.4–80.1)&lt;/td&gt;
&lt;td&gt;77.3% (75.4–79.2)&lt;/td&gt;
&lt;td&gt;74.8% (72.8–76.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five-level rating&lt;/td&gt;
&lt;td&gt;633&lt;/td&gt;
&lt;td&gt;45.2% (41.3–49.1)&lt;/td&gt;
&lt;td&gt;51.2%&lt;/td&gt;
&lt;td&gt;45.5% (41.7–49.4)&lt;/td&gt;
&lt;td&gt;52.8% (48.9–56.6)&lt;/td&gt;
&lt;td&gt;49.6% (45.7–53.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev leads plain Gemma by 2.1 points over all records (95% range 0.2 to 4.0) and by 4.5 on multiple choice (2.0 to 7.1). Jev's per-record answers are unpublished, so these ranges compare two independent proportions: pairing would narrow them, and records that share a passage or article widen them, most of all for ratings. The overall gap's range starts at 0.2 points, so it is the least secure of the three; the multiple-choice gap is the firm one. PubMedQA and VitaminC each account for 33 of the 84 records behind it, and PubMedQA has the largest single-subset gap, 77.2% against 64.0%.&lt;/p&gt;

&lt;p&gt;On yes/no questions pooled, plain Gemma shows no measurable difference from Jev, 84.8% against 84.6%, a difference anywhere from 2.8 points ahead to 2.5 behind. Within them Jev is ahead on BoolQ by 5.7 points and plain Gemma on Civil Comments and SQuAD 2.0 by 6.0 and 6.4; PAWS, 5.2 points to Jev, is within its range.&lt;/p&gt;

&lt;p&gt;On ratings DiffusionGemma leads both Jev, by 7.6 points (2.1 to 13.1), and plain Gemma, on exact-level accuracy; Bespoke Labs advises reading that beside the error of the probability-weighted level, which this article does not report. Plain Gemma and DiffusionGemma are level over all 3,880 records: 187 right only for plain, 211 right only for DiffusionGemma, exact McNemar p = 0.25. On ratings DiffusionGemma was right alone on 97 records against 51, p = 0.0002, and the whole gap comes from SummEval, whose 384 records come from 24 news articles. Records from one article move together, so that p-value overstates the evidence.&lt;/p&gt;

&lt;p&gt;Bespoke Labs' open Nimble-9B scores 75.9% over all records, level with the two 26B reads. Gemma 4 E4B trails the 26B by 2.0 points over the whole suite, with single subsets ranging from 9.6 points ahead to 8.0 behind, and trails Jev by 4.1 (2.2 to 6.0).&lt;/p&gt;

&lt;p&gt;Median calibration error over the 13 subsets, with Bespoke Labs' 10 bins:&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;ECE as shipped&lt;/th&gt;
&lt;th&gt;ECE after 50 labels&lt;/th&gt;
&lt;th&gt;Brier as shipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jev 1.13.0, published&lt;/td&gt;
&lt;td&gt;0.071&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.267&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nimble-9B, published&lt;/td&gt;
&lt;td&gt;0.109&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.314&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain Gemma 26B&lt;/td&gt;
&lt;td&gt;0.180&lt;/td&gt;
&lt;td&gt;0.080&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiffusionGemma 26B&lt;/td&gt;
&lt;td&gt;0.114&lt;/td&gt;
&lt;td&gt;0.074&lt;/td&gt;
&lt;td&gt;0.290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;0.173&lt;/td&gt;
&lt;td&gt;0.077&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As shipped, Jev has a lower calibration error than plain Gemma on all 13 subsets, than DiffusionGemma on 11 and than E4B on 12. Brier score, which rewards accuracy and calibration together, favours Jev too: plain Gemma and E4B beat it on 2 of 13 subsets, DiffusionGemma on 4. DiffusionGemma's raw calibration error is lower than plain Gemma's on all 13, and its median share on the allowed labels was 66.5% per read on this suite.&lt;/p&gt;

&lt;p&gt;After one temperature per subset, fitted on 50 labels from that subset, the Gemma medians sit 0.003 to 0.009 above Jev's as-shipped median. Per subset, fitted plain Gemma is still above Jev on 8 of 13, by up to 0.049, and the Gemma arms are at or below Jev on 4 to 6. The fitted figures are scored on the held-out half of each subset, 72 to 300 records, and ECE reads higher on fewer records, which works against the Gemma columns. Jev's figures are as shipped, and a temperature fitted on Jev's own output could lower them as well.&lt;/p&gt;

&lt;p&gt;Matt Mastracci, who wrote vLLM PR #57250, compared DiffusionGemma with Jev on 201 hand-built items on September 17, and Google's Gemma account shared the thread the next day. DiffusionGemma answered 198 correctly and Jev 191; he called the two "roughly tied" and DiffusionGemma "the winner, I think." Seven of Jev's ten errors fell in one set, 89 words drawn from five sentences, and the items, code and per-item outputs are unpublished. On the public records DiffusionGemma is 1.4 points behind Jev, anywhere from 0.4 ahead to 3.3 behind, so both measurements find the two close overall; by question type they part, level on yes/no, 5.5 points behind on multiple choice (3.0 to 8.1) and 7.6 ahead on ratings. Plain Gemma 26B, read the same way without the diffusion step, lands within 0.6 points of DiffusionGemma over the suite (p = 0.25), so this suite shows no accuracy gain from the diffusion step.&lt;/p&gt;

&lt;p&gt;His timings show the same cost for the re-reads: 1.9 to 4.6 times one read on his eight sets, against 2.6 times here. One read beat Jev's API on seven of his eight sets and the automatic re-reads lost on all eight; his single reads were timed warm and the re-reads cold, which exaggerates that second gap.&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;Jev 1.13.0, published&lt;/th&gt;
&lt;th&gt;Plain Gemma 4 26B, label read&lt;/th&gt;
&lt;th&gt;DiffusionGemma 26B, one-step read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy, public suite&lt;/td&gt;
&lt;td&gt;🥇 77.3%&lt;/td&gt;
&lt;td&gt;75.3%, 2.1 behind Jev; level with DiffusionGemma&lt;/td&gt;
&lt;td&gt;75.9%, 1.4 behind Jev, a range that includes a tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes/no, public suite&lt;/td&gt;
&lt;td&gt;84.6%&lt;/td&gt;
&lt;td&gt;84.8%&lt;/td&gt;
&lt;td&gt;84.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple choice, public suite&lt;/td&gt;
&lt;td&gt;🥇 82.8%&lt;/td&gt;
&lt;td&gt;78.3%&lt;/td&gt;
&lt;td&gt;77.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Five-level rating, public suite&lt;/td&gt;
&lt;td&gt;45.2%&lt;/td&gt;
&lt;td&gt;45.5%&lt;/td&gt;
&lt;td&gt;🥇 52.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median ECE, public suite, as shipped&lt;/td&gt;
&lt;td&gt;🥇 0.071&lt;/td&gt;
&lt;td&gt;0.180&lt;/td&gt;
&lt;td&gt;0.114, lower than plain on all 13 subsets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median ECE, public suite, after 50 labels&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0.080&lt;/td&gt;
&lt;td&gt;0.074&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median Brier, public suite, as shipped&lt;/td&gt;
&lt;td&gt;🥇 0.267&lt;/td&gt;
&lt;td&gt;0.359&lt;/td&gt;
&lt;td&gt;0.290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 level on three, ahead on AG News&lt;/td&gt;
&lt;td&gt;level on three, 3.7 points behind on AG News&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw calibration, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;overconfident, fitted temperature 3.5 to 7.2&lt;/td&gt;
&lt;td&gt;🥇 closer, fitted temperature 0.7 to 2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probability on the allowed labels&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 about 100%&lt;/td&gt;
&lt;td&gt;37% to 59%, 66.5% on the public suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per decision&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;🥇 61 ms on the instance&lt;/td&gt;
&lt;td&gt;118 to 121 ms for one read, 306 to 312 ms with the automatic rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option-order changes, four tasks&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2.0% to 9.0%&lt;/td&gt;
&lt;td&gt;2.3% to 8.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price per million decisions&lt;/td&gt;
&lt;td&gt;$5.54 at 132 input tokens&lt;/td&gt;
&lt;td&gt;at most $5.43 on a &lt;code&gt;g6.xlarge&lt;/code&gt; at full load&lt;/td&gt;
&lt;td&gt;at most $31.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serving&lt;/td&gt;
&lt;td&gt;TypeSafe's hosted API&lt;/td&gt;
&lt;td&gt;any vLLM&lt;/td&gt;
&lt;td&gt;vLLM with PR #57250&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;Between the two Gemma 4 26B reads, for a Jev-style decision service on one L4: plain Gemma read by its label probabilities, plus one temperature fitted on about 50 labels. It is at least as accurate on the four tasks and level with DiffusionGemma over the public suite, faster, and places all of its probability on the answers you allowed.&lt;/p&gt;

&lt;p&gt;DiffusionGemma's lower raw calibration error has a range excluding zero on two of four tasks and holds on all 13 public-suite subsets, and it matters when no labels exist at all. With 50 labels the difference is no longer detectable on the four tasks, and on the suite the two split 8 to 5, a count within chance (sign test p = 0.58). On five-level ratings DiffusionGemma scored higher than both Jev and plain Gemma, on few source articles; a rating task is the one place to try both.&lt;/p&gt;

&lt;p&gt;Against Jev, on the same public records: plain Gemma 4 26B trails Jev by 2.1 points overall, shows no measurable difference on yes/no questions pooled, and trails it by 4.5 points on multiple choice. Its median calibration error comes within 0.01 of Jev's as-shipped median once one temperature per subset is fitted on 50 labels from that subset; per subset it stays higher on 8 of 13. Jev is the better calibrated with no labels at all.&lt;/p&gt;

&lt;p&gt;Where a smaller model has to do, Gemma 4 E4B in bf16 gives up 0.7 to 5.4 points against the 26B on the four tasks and 2.0 points over the public suite. E2B falls apart on four-way topic classification with this prompt.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Does a Decision Cost?
&lt;/h4&gt;

&lt;p&gt;The run logs give a throughput. Plain Gemma answered 2,100 decisions in 51 seconds, 41.2 a second, at client concurrency 8. DiffusionGemma, reading each example four times, answered 2,100 in 298 seconds, 7.0 a second, at concurrency 4. The client was on a home connection, so these are lower bounds on what the L4 serves.&lt;/p&gt;

&lt;p&gt;At the &lt;code&gt;g6.xlarge&lt;/code&gt; on-demand price of $0.8048 an hour, that is at most $5.43 per million decisions for plain Gemma and $31.72 for DiffusionGemma with four reads, which ran at half the concurrency, so its figure is the looser bound. TypeSafe prices Jev at $0.042 per million input tokens, which is $5.54 per million decisions at this run's median of 132 input tokens and $12.56 at the longest, 299: about the same as the L4 at full load. The L4 is charged by the hour whether busy or idle, so its per-decision cost holds only at full load. The public suite's longer prompts would raise both the L4's cost and Jev's.&lt;/p&gt;

&lt;p&gt;The first run, from launch to termination in under 1.2 hours, cost at most $0.97 of instance time. The second, on a &lt;code&gt;g6.4xlarge&lt;/code&gt; at $1.3232 an hour for 1.19 hours, cost $1.57. The public-suite run, on a &lt;code&gt;g6.xlarge&lt;/code&gt; for 0.82 hours, cost $0.66, including rebuilding the suite and reading 11,640 records across three models. All three, $3.20 together, exclude the prorated 100 GB volume. All ran on demand; G-family spot capacity in us-east-1 was unavailable at launch time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Teardown
&lt;/h4&gt;

&lt;p&gt;Every command on the instance went through AWS Systems Manager, so no SSH port was ever open, and port 8000 was open to one address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 terminate-instances &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-0c5b14e913b4c1019
&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;shutting-down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then delete the security group, and confirm that nothing tagged &lt;code&gt;ManagedBy=jev&lt;/code&gt; remains.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to measure Gemma 4 26B as a Jev-style decision model, read two ways, for accuracy and calibration on labelled data, and to set it beside Jev's published results on a public suite. The key to the solution was a matched pair of 4-bit checkpoints on one EC2 L4, identical prompts and label tokens, and a measurement pre-registered before any call. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ On Bespoke Labs' 3,880-record public suite, plain Gemma 4 26B trails Jev 1.13.0 by 2.1 points overall (range 0.2 to 4.0) and 4.5 on multiple choice (2.0 to 7.1)&lt;/li&gt;
&lt;li&gt;🟢 No measurable difference from Jev on yes/no questions pooled, 84.8% against 84.6%&lt;/li&gt;
&lt;li&gt;⚠️ Jev is the best calibrated with no labels: median ECE 0.071 against 0.114 to 0.180 for the Gemma arms&lt;/li&gt;
&lt;li&gt;🟢 One temperature fitted on 50 labels brings the Gemma medians to 0.074 to 0.080; per subset plain Gemma stays above Jev on 8 of 13&lt;/li&gt;
&lt;li&gt;⚠️ DiffusionGemma 1.4 points behind Jev over the suite (0.4 ahead to 3.3 behind): 5.5 behind on multiple choice, 7.6 ahead on ratings; Mastracci's hand-built items had it 198 against 191, both close overall&lt;/li&gt;
&lt;li&gt;🟢 Plain Gemma and DiffusionGemma level on accuracy: plain ahead only on AG News of the four tasks, 75.3% against 75.9% over the suite, p = 0.25&lt;/li&gt;
&lt;li&gt;🟢 DiffusionGemma better calibrated as shipped, on emotion and irony and on all 13 suite subsets; after 50 labels the difference is no longer detectable&lt;/li&gt;
&lt;li&gt;⚠️ DiffusionGemma places only 37% to 59% of its probability on the allowed labels, and the proxy's rescaling hides it&lt;/li&gt;
&lt;li&gt;⚠️ The automatic re-read rule fires on 96% to 100% of examples, costs 2.6 times one read, and moves accuracy by at most 0.6 points&lt;/li&gt;
&lt;li&gt;🟢 61 ms per plain decision on the instance, and at most $5.43 per million at full load, level with Jev's $5.54 at this run's prompt length&lt;/li&gt;
&lt;li&gt;🟢 Gemma 4 E4B in bf16 0.7 to 5.4 points behind the 26B on the four tasks and 2.0 over the suite&lt;/li&gt;
&lt;li&gt;❌ Gemma 4 E2B answered "world" on 277 of 300 AG News examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one NVIDIA L4 in us-east-1 on three instances, a &lt;code&gt;g6.xlarge&lt;/code&gt; for the accuracy run, a &lt;code&gt;g6.4xlarge&lt;/code&gt; for the latency and small-model run and a &lt;code&gt;g6.xlarge&lt;/code&gt; for the public suite, vLLM &lt;code&gt;0.29.1rc1.dev573+ge97573215&lt;/code&gt;, the 26B models 4-bit from the same uploader with the same quantization settings and E4B and E2B in bf16, 300 examples per task plus the 3,880-record public suite, one run per arm. Google's reference checkpoints are bf16, and 4-bit quantization may affect a diffusion model differently from an autoregressive one, so results at bf16 may differ for either arm. Latency comes from 100 examples per task with the client on the instance; throughput comes from the first run's client on a home connection. The latency, E4B and E2B arms were added in a pre-registration addendum after the first run, and E4B and E2B are exploratory; the 20-split calibration and the public suite's ranges, paired test, medians and per-subset counts were added after their runs, and the per-subset and pooled suite figures are the pre-registered ones. The suite's E4B arm ran under its own run name, &lt;code&gt;2026-09-24-l4-suite-e4b&lt;/code&gt;, with the same instance, image, flags and records. All four test sets and all 13 suite datasets were published before Gemma 4 and may be in its training data, and the high irony scores may indicate it there; whether they are in Jev's is unknown. No Jev call was made: the Jev and Nimble-9B figures are Bespoke Labs' published results on the same records, from one run of Jev 1.13.0 by a company that publishes a competing model, counting an invalid Jev response as wrong, with Jev's probabilities rounded to two decimals by its API. Code, pre-registration and every per-item output are in the repository. Parts of the analysis and writing were done with AI assistance (Claude); every figure comes from the committed output files.&lt;/p&gt;

&lt;p&gt;The strategy for using label probabilities to run Gemma 4 as a decision model 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;Code, pre-registration and per-item results: &lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/jev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/tree/main/jev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion review of the independent evidence on Jev: &lt;a href="https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln"&gt;https://dev.to/gde/jev-after-eight-days-of-independent-tests-level-with-mid-price-llms-behind-the-frontier-1kln&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;vLLM PR #57250, DiffusionGemma structured reads: &lt;a href="https://github.com/vllm-project/vllm/pull/57250" rel="noopener noreferrer"&gt;https://github.com/vllm-project/vllm/pull/57250&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Plain checkpoint: &lt;a href="https://huggingface.co/cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit" rel="noopener noreferrer"&gt;https://huggingface.co/cyankiwi/gemma-4-26B-A4B-it-AWQ-4bit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Diffusion checkpoint: &lt;a href="https://huggingface.co/cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4" rel="noopener noreferrer"&gt;https://huggingface.co/cyankiwi/diffusiongemma-26B-A4B-it-AWQ-INT4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DiffusionGemma model card: &lt;a href="https://huggingface.co/google/diffusiongemma-26B-A4B-it" rel="noopener noreferrer"&gt;https://huggingface.co/google/diffusiongemma-26B-A4B-it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google's Gemma account on DiffusionGemma's calibration: &lt;a href="https://x.com/googlegemma/status/2101069861598482817" rel="noopener noreferrer"&gt;https://x.com/googlegemma/status/2101069861598482817&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Matt Mastracci, Jev against DiffusionGemma on hand-built items: &lt;a href="https://x.com/mmastrac/status/2100626193943052784" rel="noopener noreferrer"&gt;https://x.com/mmastrac/status/2100626193943052784&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guo et al., On Calibration of Modern Neural Networks: &lt;a href="https://arxiv.org/abs/1706.04599" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.04599&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;li&gt;TypeSafe Jev: &lt;a href="https://docs.typesafe.ai/concepts/system-one" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/concepts/system-one&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bespoke Labs, Nimble public-suite results for Jev 1.13.0 and Nimble-9B: &lt;a href="https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/bespokelabsai/nimble/blob/0e67403/docs/PUBLIC_BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gemma</category>
      <category>aws</category>
      <category>machinelearning</category>
      <category>llm</category>
    </item>
    <item>
      <title>A 4 GB Laptop GPU vs a 6-Core CPU on Gemma 4, Re-Measured in ABBA Order: 4.1x</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Wed, 23 Sep 2026 16:48:49 +0000</pubDate>
      <link>https://dev.to/gde/a-4-gb-laptop-gpu-vs-a-6-core-cpu-on-gemma-4-re-measured-in-abba-order-41x-5g56</link>
      <guid>https://dev.to/gde/a-4-gb-laptop-gpu-vs-a-6-core-cpu-on-gemma-4-re-measured-in-abba-order-41x-5g56</guid>
      <description>&lt;p&gt;This article provides a step by step guide to measuring a laptop GPU against the CPU in the same chassis for serving Gemma 4 E2B through llama.cpp, with both builds on a fresh Debian sid toolchain and the passes run in ABBA order so heat cannot pick the winner. A suite of Python MCP tools is built to simplify management of the llama.cpp hosted deployment.&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 GTX 1650 Ti decodes &lt;strong&gt;4.14x&lt;/strong&gt; faster than the i7-10750H it shares a chassis with, prefills &lt;strong&gt;3.42x&lt;/strong&gt; faster and finishes requests &lt;strong&gt;3.62x&lt;/strong&gt; faster end to end. Running the two devices in one order and then the other moves that decode ratio from 4.09x to 4.17x, so a single-order benchmark on this laptop is off by about 2%.&lt;/p&gt;




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

&lt;p&gt;The same 3.35 GB quantization-aware GGUF, &lt;code&gt;google/gemma-4-E2B-it-qat-q4_0-gguf&lt;/code&gt;, served by &lt;code&gt;llama-server&lt;/code&gt; twice on one laptop: once on the CPU, once on the GPU. Both builds come from one llama.cpp commit and the two command lines differ by one flag.&lt;/p&gt;

&lt;p&gt;An earlier run of this comparison is published as &lt;a href="https://dev.to/gde/a-4-gb-laptop-gpu-beats-a-12-core-cpu-by-43x-on-gemma-4-4150"&gt;A 4 GB Laptop GPU Beats a 12-Core CPU by 4.3x on Gemma 4&lt;/a&gt;. This one re-measures it after the laptop moved to Debian sid, with three changes: a newer llama.cpp commit built on gcc 16.2 and CUDA 13.4, thread flags set for this CPU's real core count, and a run order that cancels thermal drift.&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 a CUDA driver — here a GTX 1650 Ti with Max-Q Design, driver 615.71.09&lt;/li&gt;
&lt;li&gt;The CUDA toolkit — &lt;code&gt;nvcc&lt;/code&gt; 13.4 here&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cmake&lt;/code&gt; and a host compiler CUDA accepts — gcc 16.2.0 here&lt;/li&gt;
&lt;li&gt;Python 3.10 or newer, the system &lt;code&gt;python3&lt;/code&gt;, no virtualenv&lt;/li&gt;
&lt;li&gt;The repository cloned, with &lt;code&gt;local-llamacpp-1650ti-2b-q4_0/&lt;/code&gt; and &lt;code&gt;local-llamacpp-cpu-2b-q4_0/&lt;/code&gt; side by side&lt;/li&gt;
&lt;li&gt;Claude Code, or any MCP client that speaks stdio&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  The Laptop
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine&lt;/td&gt;
&lt;td&gt;Lenovo Yoga 9 15IMH5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Intel Core i7-10750H, 6 cores / 12 threads, AVX2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;GeForce GTX 1650 Ti with Max-Q Design, 4096 MiB, compute capability 7.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Debian GNU/Linux forky/sid, kernel 7.2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;gcc 16.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA&lt;/td&gt;
&lt;td&gt;13.4 (V13.4.92)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;f95b0d9&lt;/code&gt;, build 318&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CPU and the Max-Q card sit under one cooling system, which is why the run order below matters.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 1 — Check the Toolchain Still Supports the Card
&lt;/h4&gt;

&lt;p&gt;A distribution upgrade can move the compiler past what CUDA accepts, and a CUDA upgrade can drop old GPUs. Both were close here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nvidia-smi &lt;span class="nt"&gt;--query-gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name,compute_cap,memory.total,driver_version &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;csv
&lt;span class="go"&gt;name, compute_cap, memory.total [MiB], driver_version
NVIDIA GeForce GTX 1650 Ti with Max-Q Design, 7.5, 4096 MiB, 615.71.09

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nvcc &lt;span class="nt"&gt;--list-gpu-arch&lt;/span&gt;
&lt;span class="go"&gt;compute_75
compute_80
compute_86
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card is compute capability 7.5, and &lt;code&gt;compute_75&lt;/code&gt; is the first entry CUDA 13.4 lists. This laptop's GPU is the oldest architecture the current toolkit still builds for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcc &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;gcc (Debian 16.2.0-3) 16.2.0

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'later than 16'&lt;/span&gt; /usr/local/cuda/include/crt/host_config.h
&lt;span class="gp"&gt;137:#&lt;/span&gt;error &lt;span class="nt"&gt;--&lt;/span&gt; unsupported GNU version! gcc versions later than 16 are not supported! ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CUDA 13.4 accepts gcc up to 16, and sid ships 16.2.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Build Both Devices From One Commit
&lt;/h4&gt;

&lt;p&gt;Each rig's Makefile carries its own &lt;code&gt;build&lt;/code&gt; target. The GPU build targets only this card's architecture, sm_75; the CPU build turns every GPU backend off.&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;# local-llamacpp-1650ti-2b-q4_0&lt;/span&gt;
cmake &lt;span class="nt"&gt;-S&lt;/span&gt; ~/llama.cpp &lt;span class="nt"&gt;-B&lt;/span&gt; ~/llama.cpp/build &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="nt"&gt;-DCMAKE_CUDA_ARCHITECTURES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;75 &lt;span class="nt"&gt;-DGGML_NATIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; ~/llama.cpp/build &lt;span class="nt"&gt;--config&lt;/span&gt; Release &lt;span class="nt"&gt;-j6&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; llama-server llama-bench

&lt;span class="c"&gt;# local-llamacpp-cpu-2b-q4_0&lt;/span&gt;
cmake &lt;span class="nt"&gt;-S&lt;/span&gt; ~/llama.cpp &lt;span class="nt"&gt;-B&lt;/span&gt; ~/llama.cpp/build-cpu &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DGGML_VULKAN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DGGML_NATIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; ~/llama.cpp/build-cpu &lt;span class="nt"&gt;--config&lt;/span&gt; Release &lt;span class="nt"&gt;-j6&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; llama-server llama-bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both binaries report the same commit and compiler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp/build/bin/llama-server &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;version: 0.4.1-dev (build 318, commit f95b0d9)
built with GNU 16.2.0 for Linux x86_64

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp/build-cpu/bin/llama-server &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;version: 0.4.1-dev (build 318, commit f95b0d9)
built with GNU 16.2.0 for Linux x86_64
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And only one of them can see the card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/llama.cpp/build/bin/llama-server &lt;span class="nt"&gt;--list-devices&lt;/span&gt;
&lt;span class="go"&gt;Available devices:
  CUDA0: NVIDIA GeForce GTX 1650 Ti with Max-Q Design (3732 MiB, 3671 MiB free)

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CUDA_VISIBLE_DEVICES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; ~/llama.cpp/build-cpu/bin/llama-server &lt;span class="nt"&gt;--list-devices&lt;/span&gt;
&lt;span class="go"&gt;Available devices:
  (none)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  🔎 Tip: Always Give the CUDA Build a Job Count
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;cmake --build -j&lt;/code&gt; with no number starts every CUDA source file at once. llama.cpp has about 188 of them, each &lt;code&gt;nvcc&lt;/code&gt; forks &lt;code&gt;cicc&lt;/code&gt; and &lt;code&gt;cudafe++&lt;/code&gt;, and a 15 GiB laptop runs out of memory. At &lt;code&gt;-j6&lt;/code&gt; the full CUDA build peaked at 4.0 GiB and never touched swap, so the limit costs nothing. Both Makefiles pass &lt;code&gt;-j$(BUILD_JOBS)&lt;/code&gt; with a default of 6.&lt;/p&gt;

&lt;p&gt;After a compiler or CUDA upgrade, delete the build directory before configuring. A stale &lt;code&gt;CMakeCache.txt&lt;/code&gt; keeps the old compiler, and &lt;code&gt;/usr/local/cuda&lt;/code&gt; is a symlink, so the cached path looks current.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Serve One Device at a Time on the Same Port
&lt;/h4&gt;

&lt;p&gt;Both devices serve on &lt;code&gt;127.0.0.1:8080&lt;/code&gt;, so the endpoint, the prompts and the benchmark script stay fixed while the device changes underneath. The flags match except for &lt;code&gt;-ngl&lt;/code&gt;, the number of layers offloaded to the GPU:&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_q4_0-it.gguf &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ngl&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;0|99&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 8192 &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="nt"&gt;-t&lt;/span&gt; 6 &lt;span class="nt"&gt;-tb&lt;/span&gt; 12 &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1 &lt;span class="nt"&gt;--metrics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-t 6 -tb 12&lt;/code&gt; matches the CPU's six physical cores and twelve threads, and both devices get it. GPU decode barely responds to thread count, so matching them costs the GPU nothing and removes one difference between the two runs.&lt;/p&gt;

&lt;p&gt;For the GPU side there is a small wrapper, &lt;code&gt;llamacpp-1650ti&lt;/code&gt;, that reads these values out of the rig's &lt;code&gt;tpu.env&lt;/code&gt;, starts the server in the background and checks which device answered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llamacpp-1650ti start
&lt;span class="go"&gt;llamacpp-1650ti: starting /home/xbill/models/gemma-4-E2B-it-qat-q4_0/gemma-4-E2B_q4_0-it.gguf on 127.0.0.1:8080 (-ngl 99)
llamacpp-1650ti: pid 88166, log /home/xbill/gemma4-dev/local-llamacpp-1650ti-2b-q4_0/run/llama-server.log
llamacpp-1650ti: waiting up to 180s for http://127.0.0.1:8080/health
     0s  VRAM 3 MiB, 0 %
llamacpp-1650ti: healthy after 2s -- http://127.0.0.1:8080
llamacpp-1650ti: device=gpu · pid=88166 · -ngl 99 · mapped: ggml-cuda, libcublas, libcuda, libcudart · /home/xbill/llama.cpp/build/bin/llama-server

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llamacpp-1650ti stop
&lt;span class="gp"&gt;llamacpp-1650ti: sent SIGTERM to pid 88166;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;VRAM is released on &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper refuses to start if anything already holds the port, and refuses to stop the CPU build if that is what holds it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Read the Device Off the Running Process
&lt;/h4&gt;

&lt;p&gt;Both devices answer the same URL with the same JSON, so the HTTP response cannot say which one produced it. The rigs' &lt;code&gt;attest.py&lt;/code&gt; reads it from &lt;code&gt;/proc&lt;/code&gt; instead: &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/exe&lt;/code&gt; for the binary, &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/maps&lt;/code&gt; for the GPU libraries it has loaded, &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; for the real &lt;code&gt;-ngl&lt;/code&gt;. The benchmark script refuses to start if the answer is the wrong device, and writes it at the top of every log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arm: device=cpu · pid=72478 · -ngl 0 · /home/xbill/llama.cpp/build-cpu/bin/llama-server
arm: device=gpu · pid=74308 · -ngl 99 · mapped: ggml-cuda, libcublas, libcuda, libcudart · /home/xbill/llama.cpp/build/bin/llama-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maps&lt;/code&gt; is used because llama.cpp loads its CUDA backend at runtime, where &lt;code&gt;ldd&lt;/code&gt; does not see it. Each report also records the SHA-256 of the executable that ran: &lt;code&gt;9c88c7821fcc11a6…&lt;/code&gt; for the CPU build and &lt;code&gt;9f2a8b0b6ed5365d…&lt;/code&gt; for the GPU build, the same on both passes of each.&lt;/p&gt;

&lt;p&gt;The MCP servers expose the same check as a tool, so an agent that starts a server can confirm what it started before it measures anything.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Run in ABBA Order With a Temperature Gate
&lt;/h4&gt;

&lt;p&gt;Each device gets two passes, in the order CPU, GPU, GPU, CPU. Each device then has one early pass and one late pass, so a slow drift in the laptop's temperature over the session affects both equally. Before every pass the script waits at least 120 seconds and until the CPU package is at or below 50 °C and the GPU at or below 45 °C.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;13:20:12 pkg=38C gpu=36C throttle=35105 START cpu pass1
13:27:57 pkg=85C gpu=52C throttle=44709 END cpu pass1 rc=0
13:30:00 pkg=45C gpu=40C throttle=44730 START gpu pass1
13:32:12 pkg=79C gpu=56C throttle=45150 END gpu pass1 rc=0
13:34:15 pkg=42C gpu=39C throttle=45150 START gpu pass2
13:36:26 pkg=78C gpu=55C throttle=48034 END gpu pass2 rc=0
13:38:29 pkg=41C gpu=39C throttle=48060 START cpu pass2
13:46:31 pkg=82C gpu=53C throttle=67346 END cpu pass2 rc=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;throttle&lt;/code&gt; is the CPU package's cumulative thermal-throttle counter. CPU pass 1 added 9,604 events, the two GPU passes 420 and 2,879, and CPU pass 2 added 19,279. A CPU pass takes about eight minutes and a GPU pass about two.&lt;/p&gt;

&lt;p&gt;Each pass is the same sweep: four prompt lengths by two output lengths, three repeats per cell, one request at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sweep.py &lt;span class="nt"&gt;--base&lt;/span&gt; http://127.0.0.1:8080/v1 &lt;span class="nt"&gt;--out&lt;/span&gt; &amp;lt;run&amp;gt;/passN &lt;span class="nt"&gt;--rig&lt;/span&gt; &amp;lt;rig&amp;gt; &lt;span class="nt"&gt;--expect-device&lt;/span&gt; &amp;lt;cpu|gpu&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  The Result
&lt;/h4&gt;

&lt;p&gt;Each figure is the mean of the two passes' per-cell medians. Decode is the token rate measured off the response stream; TTFT is time to first token.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;in tok&lt;/th&gt;
&lt;th&gt;out tok&lt;/th&gt;
&lt;th&gt;CPU decode&lt;/th&gt;
&lt;th&gt;GPU decode&lt;/th&gt;
&lt;th&gt;GPU lead&lt;/th&gt;
&lt;th&gt;CPU TTFT ms&lt;/th&gt;
&lt;th&gt;GPU TTFT ms&lt;/th&gt;
&lt;th&gt;GPU lead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;18.31&lt;/td&gt;
&lt;td&gt;71.89&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.93x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1084&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.84x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;17.96&lt;/td&gt;
&lt;td&gt;71.57&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.98x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1171&lt;/td&gt;
&lt;td&gt;385&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.04x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;516&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;17.06&lt;/td&gt;
&lt;td&gt;70.61&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.14x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5528&lt;/td&gt;
&lt;td&gt;1608&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.44x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;516&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;17.19&lt;/td&gt;
&lt;td&gt;70.35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.09x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5527&lt;/td&gt;
&lt;td&gt;1610&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.43x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;998&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;16.10&lt;/td&gt;
&lt;td&gt;69.25&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.30x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11071&lt;/td&gt;
&lt;td&gt;3242&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.41x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;998&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;16.77&lt;/td&gt;
&lt;td&gt;69.38&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.14x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11057&lt;/td&gt;
&lt;td&gt;3246&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.41x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1959&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;16.14&lt;/td&gt;
&lt;td&gt;68.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.25x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23118&lt;/td&gt;
&lt;td&gt;6497&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.56x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1959&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;16.37&lt;/td&gt;
&lt;td&gt;68.22&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.17x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22718&lt;/td&gt;
&lt;td&gt;6496&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.50x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Medians over the eight cells: decode &lt;strong&gt;4.14x&lt;/strong&gt;, prefill &lt;strong&gt;3.42x&lt;/strong&gt;, end to end &lt;strong&gt;3.62x&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Decode stays nearly flat on both devices across a 21x range of prompt length (94 to 1959 tokens), from 18.31 down to 16.14 tok/s on the CPU and from 71.89 down to 68.22 on the GPU, while TTFT grows in proportion to the prompt on both. Decode reads the model once per token and is limited by memory bandwidth; prefill does arithmetic over the whole prompt. The GPU wins both, by different margins, and the prefill margin grows with the prompt from 2.84x to 3.56x.&lt;/p&gt;




&lt;h4&gt;
  
  
  Is the Difference Bigger Than the Noise?
&lt;/h4&gt;

&lt;p&gt;By an order of magnitude. The decode ratio stays between 3.93x and 4.30x in every cell. The noise is the drift between a device's two passes:&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;decode drift, pass 1 to pass 2&lt;/th&gt;
&lt;th&gt;worst cell&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;median &lt;strong&gt;+0.6%&lt;/strong&gt;, range 0.0% to +1.3%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;median &lt;strong&gt;-1.1%&lt;/strong&gt;, range -9.3% to +0.1%&lt;/td&gt;
&lt;td&gt;998 in / 32 out: 16.89 → 15.32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Within a pass, the three repeats of a cell spread by at most 1.14% on the GPU. On the CPU they spread by at most 2.69% in pass 1 and &lt;strong&gt;14.05%&lt;/strong&gt; in pass 2, which had twice the throttle events. CPU time to first token also rose in pass 2, by up to 7.4% at the longest prompt, while the GPU's moved by at most 0.9%.&lt;/p&gt;

&lt;p&gt;All of the measurable noise in this comparison comes from the CPU, and it tracks heat.&lt;/p&gt;




&lt;h4&gt;
  
  
  What Is Run Order Worth?
&lt;/h4&gt;

&lt;p&gt;Each ordering can be read on its own. CPU pass 1 against GPU pass 1 is the CPU-first design; GPU pass 2 against CPU pass 2 is GPU-first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order&lt;/th&gt;
&lt;th&gt;decode ratio&lt;/th&gt;
&lt;th&gt;prefill ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU first, then GPU&lt;/td&gt;
&lt;td&gt;4.09x&lt;/td&gt;
&lt;td&gt;3.38x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU first, then CPU&lt;/td&gt;
&lt;td&gt;4.17x&lt;/td&gt;
&lt;td&gt;3.47x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ABBA, both&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.14x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.42x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Running the CPU first understates the GPU's lead, because the CPU pass runs while the laptop is still cool and the GPU pass inherits its heat. On this laptop the effect is about 2% of the ratio. ABBA order removes it for the cost of one extra pass per device.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Count Tokens, Not Stream Chunks
&lt;/h4&gt;

&lt;p&gt;llama.cpp can put more than one token in a streamed chunk, so a decode rate computed from chunk timing undercounts. Recomputed as completion tokens over the measured decode time, the rates read 16.77-20.27 tok/s on the CPU and 69.87-79.58 on the GPU, and the median ratio stays at &lt;strong&gt;4.14x&lt;/strong&gt;. Both devices run the same server code, so the undercount cancels in the ratio. It does not cancel when comparing either absolute figure against a different server.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Confirm the Prompt Cache Stayed Cold
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;llama-server&lt;/code&gt; reuses the key/value cache for a prompt prefix it has seen, which makes time to first token look faster than prefill really is. Each prompt starts with a unique prefix, and &lt;code&gt;--metrics&lt;/code&gt; exposes a counter that 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;cpu pass1: llamacpp:prompt_tokens_total 28553 llamacpp:prompt_tokens_cached_total 0
cpu pass2: llamacpp:prompt_tokens_total 28553 llamacpp:prompt_tokens_cached_total 0
gpu pass1: llamacpp:prompt_tokens_total 28553 llamacpp:prompt_tokens_cached_total 0
gpu pass2: llamacpp:prompt_tokens_total 28553 llamacpp:prompt_tokens_cached_total 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the 28,553 prompt tokens in any pass came from the cache.&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;🥇 GTX 1650 Ti&lt;/th&gt;
&lt;th&gt;🥈 i7-10750H&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decode, single stream&lt;/td&gt;
&lt;td&gt;68.22-71.89 tok/s&lt;/td&gt;
&lt;td&gt;16.10-18.31 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first token, 1959-token prompt&lt;/td&gt;
&lt;td&gt;6.5 s&lt;/td&gt;
&lt;td&gt;22.7-23.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass-to-pass drift&lt;/td&gt;
&lt;td&gt;≤1.3%&lt;/td&gt;
&lt;td&gt;up to 9.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throttle events per pass&lt;/td&gt;
&lt;td&gt;420 and 2,879&lt;/td&gt;
&lt;td&gt;9,604 and 19,279&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass length&lt;/td&gt;
&lt;td&gt;about 2 minutes&lt;/td&gt;
&lt;td&gt;about 8 minutes&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;The GPU, for any interactive use of this model on this laptop. It is about four times faster on every prompt length measured, it is steadier, and it heats the shared package far less than the CPU does for the same work.&lt;/p&gt;

&lt;p&gt;The CPU build is still worth keeping: it serves the same model at 16-18 tok/s when the card is busy or absent. It is also the device to benchmark with the most care, because its numbers move with the laptop's temperature.&lt;/p&gt;




&lt;h4&gt;
  
  
  How Does This Compare With the Earlier Run?
&lt;/h4&gt;

&lt;p&gt;The earlier run measured 4.27x decode and 3.63x prefill. Three things changed at once — the llama.cpp commit (&lt;code&gt;c6824a9&lt;/code&gt; to &lt;code&gt;f95b0d9&lt;/code&gt;), the thread flags (&lt;code&gt;-t 4 -tb 8&lt;/code&gt; to &lt;code&gt;-t 6 -tb 12&lt;/code&gt;) and the run order — so the difference between the two results cannot be assigned to any one of them. The more-threads explanation fits the direction, since CPU time to first token fell and the prefill ratio narrowed with it, but this run does not isolate it.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to re-measure a 4 GB laptop GPU against the 6-core CPU in the same chassis for serving Gemma 4 E2B, after a toolchain upgrade and with thermal drift controlled. The key to the solution was ABBA ordering with a temperature gate, identical flags on both devices apart from &lt;code&gt;-ngl&lt;/code&gt;, and reading the serving device from &lt;code&gt;/proc&lt;/code&gt; on every pass. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 GPU decode is &lt;strong&gt;4.14x&lt;/strong&gt; the CPU, 3.93x to 4.30x across every cell&lt;/li&gt;
&lt;li&gt;🟢 GPU prefill is &lt;strong&gt;3.42x&lt;/strong&gt;, growing with prompt length; end to end is &lt;strong&gt;3.62x&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🟢 The GTX 1650 Ti still builds and runs on CUDA 13.4 with gcc 16.2, as the oldest architecture that toolkit supports&lt;/li&gt;
&lt;li&gt;⚠️ Run order is worth about 2% of the ratio here: 4.09x CPU-first against 4.17x GPU-first&lt;/li&gt;
&lt;li&gt;⚠️ The CPU carries all of the measurable noise, up to 9.3% between passes and 14% within one, and it follows the throttle count&lt;/li&gt;
&lt;li&gt;❌ The earlier 4.27x and this 4.14x cannot be compared directly, because commit, threads and order all changed together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one laptop, one GGUF, llama.cpp &lt;code&gt;f95b0d9&lt;/code&gt; built twice with gcc 16.2 and CUDA 13.4, eight paired cells, two passes per device in ABBA order, three repeats per cell per pass, one request at a time, prompt cache verified cold, page cache warm throughout.&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;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;GPU rig: &lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/local-llamacpp-1650ti-2b-q4_0" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/tree/main/local-llamacpp-1650ti-2b-q4_0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CPU rig: &lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/local-llamacpp-cpu-2b-q4_0" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/tree/main/local-llamacpp-cpu-2b-q4_0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run report: &lt;a href="https://github.com/xbill9/gemma4-dev/blob/main/local-llamacpp-1650ti-2b-q4_0/benchmarks/runs/2026-09-22-paired-sweep-1650ti/REPORT.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev/blob/main/local-llamacpp-1650ti-2b-q4_0/benchmarks/runs/2026-09-22-paired-sweep-1650ti/REPORT.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Earlier run: &lt;a href="https://dev.to/gde/a-4-gb-laptop-gpu-beats-a-12-core-cpu-by-43x-on-gemma-4-4150"&gt;A 4 GB Laptop GPU Beats a 12-Core CPU by 4.3x on Gemma 4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model: &lt;a href="https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf" rel="noopener noreferrer"&gt;https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-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;/ul&gt;

</description>
      <category>gemma</category>
      <category>llamacpp</category>
      <category>cuda</category>
      <category>benchmarking</category>
    </item>
    <item>
      <title>Production RAG on the Lakehouse with BigQuery Vector Search and Apache Iceberg</title>
      <dc:creator>Vo Tu Duc</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:24:08 +0000</pubDate>
      <link>https://dev.to/gde/production-rag-on-the-lakehouse-with-bigquery-vector-search-and-apache-iceberg-5g3</link>
      <guid>https://dev.to/gde/production-rag-on-the-lakehouse-with-bigquery-vector-search-and-apache-iceberg-5g3</guid>
      <description>&lt;p&gt;As teams rush to build with Generative AI, they're creating a dangerous chasm between their data and AI stacks. This common architectural flaw introduces massive technical debt and business risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The Great Divide Between Data and AI Stacks
&lt;/h2&gt;

&lt;p&gt;The explosion of Generative AI and &lt;a href="https://votuduc.com/building-a-rag-context-manager-with-apps-script-and-gemini-pro-p-20260505625174" rel="noopener noreferrer"&gt;Building a RAG Context Manager with Apps Script and Gemini Pro&lt;/a&gt; (RAG) has unlocked incredible potential, but it has also exposed a fundamental architectural flaw in how many organizations are building these systems. As teams rush to production, they often inadvertently create a deep chasm between their core data infrastructure and their new AI stack. This divide isn't just an inconvenience; it's a source of significant technical debt, operational complexity, and business risk.&lt;br&gt;
At the heart of the issue is the separation of concerns gone awry. The data lives in one universe—the data lakehouse, governed by decades of best practices in security, governance, and reliability. The AI, particularly the vector search component, lives in another—a specialized, often external, database. Bridging this gap requires brittle pipelines, data duplication, and fragmented security models, ultimately undermining the very reliability and trustworthiness we seek to build into our AI applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtgf2qgo30qgj5sbucdz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtgf2qgo30qgj5sbucdz.png" alt="The Traditional, Fragmented RAG Architecture" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Traditional Vector Databases Create Data Silos
&lt;/h3&gt;

&lt;p&gt;The conventional approach to building a RAG system follows a familiar, yet problematic, pattern. You begin with your curated, high-quality data residing in a centralized platform like a data lakehouse. This is your source of truth. To make this data accessible to a Large Language Model (LLM), you must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Extract:&lt;/strong&gt; Pull the data out of your source-of-truth system (e.g., an Apache Iceberg table in your lakehouse).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Transform:&lt;/strong&gt; Chunk the text into manageable pieces and generate vector embeddings for each chunk using a model.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Load:&lt;/strong&gt; Push both the vector embeddings and the associated text/metadata into a separate, standalone vector database (e.g., Pinecone, Milvus, Weaviate).
This ETL-like process effectively creates a &lt;em&gt;read replica&lt;/em&gt; of your original data, but one that is optimized for vector similarity search. While this works for a proof-of-concept, it's a recipe for disaster in production. You've just created a new data silo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This new silo is completely disconnected from the original data's lifecycle. It has its own infrastructure to manage, its own APIs to learn, and its own failure modes to handle. More importantly, it requires a complex and often fragile synchronization process to keep it from becoming stale. Every time data is updated, deleted, or added in the source system, a corresponding change must be perfectly orchestrated and propagated to the vector database. This adds immense operational overhead and introduces a new, critical point of failure in your AI stack.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Challenge of Metadata Drift and Security Fragmentation
&lt;/h3&gt;

&lt;p&gt;The consequences of this data silo extend far beyond mere operational complexity. Two critical challenges emerge that directly impact the quality and security of your AI application: metadata drift and security fragmentation.&lt;br&gt;
&lt;strong&gt;Metadata Drift&lt;/strong&gt; is the silent killer of RAG system accuracy. It occurs when the data in your source-of-truth lakehouse changes, but those changes aren't immediately and atomically reflected in the vector database.&lt;br&gt;
Consider a product catalog table in your lakehouse. A product's price is updated, or its status changes to "recalled." If your synchronization pipeline fails or runs on a delay, your RAG-powered chatbot could retrieve the old, stale context from the vector database and confidently provide a customer with an incorrect price or, worse, recommend a recalled product. This isn't just a technical glitch; it's a direct erosion of user trust and a potential business liability. The vector index has "drifted" from the ground truth, and your RAG system is now hallucinating based on outdated facts.&lt;br&gt;
&lt;strong&gt;Security Fragmentation&lt;/strong&gt; presents an equally severe governance and compliance risk. Your enterprise data lakehouse is built upon a robust, unified security model. You have fine-grained controls—IAM roles, row-level access policies, and column-level security—that dictate precisely who can see what data.&lt;br&gt;
When you copy that data into a separate vector database, you are forced to reimplement that entire security model from scratch in a new environment. This is not only a duplication of effort but also a massive security risk. It's incredibly difficult to keep two disparate security models perfectly in sync. An employee who leaves the company might have their access revoked in the lakehouse, but their access to the sensitive data copied in the vector store might persist. This fragmentation creates security gaps, doubles the administrative burden, and makes compliance audits a nightmare.&lt;/p&gt;
&lt;h3&gt;
  
  
  Introducing the Lakehouse as the Single Source of Truth for Enterprise AI
&lt;/h3&gt;

&lt;p&gt;What if we could eliminate the divide? What if, instead of moving the data to a separate AI system, we brought the AI capabilities directly to the data? This is the foundational principle of building production-grade AI on the Lakehouse.&lt;br&gt;
The modern data lakehouse, combining the scalability of a data lake with the performance and transactional integrity of a data warehouse, is already the established single source of truth for enterprise analytics. It houses your most valuable, curated, and governed data assets. By integrating vector search as a native feature within this platform—as BigQuery has done—we can fundamentally change the architectural paradigm.&lt;br&gt;
In this model, vector embeddings are not shipped to an external system; they become just another data type, a new column (&lt;code&gt;ARRAY&amp;amp;lt;FLOAT64&amp;amp;gt;&lt;/code&gt;) in your existing Apache Iceberg or BigQuery native tables. The vector index is built directly on top of this column, co-located with the source data it represents.&lt;br&gt;
This elegant simplification solves our critical challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No More Silos:&lt;/strong&gt; Data never leaves the lakehouse. There is no duplication, no ETL to a separate vector store, and no synchronization pipelines to maintain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Zero Data Drift:&lt;/strong&gt; When you update a row in your Iceberg table, the change is atomic. The text, the metadata, and the vector embedding are all updated together in a single transaction. The vector index is always perfectly consistent with the source of truth because it &lt;em&gt;is&lt;/em&gt; the source of truth.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unified Security and Governance:&lt;/strong&gt; The same robust security model that protects your entire data estate automatically applies to your vector embeddings and search queries. The row-level permissions, column-level security, and IAM policies you've already defined are inherited seamlessly. There is one security model to manage, not two.
By treating vector search as a first-class workload within the data lakehouse, we move from a brittle, fragmented architecture to a unified, robust, and secure platform for enterprise AI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwfrw4d1s27o0bcl24z6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwfrw4d1s27o0bcl24z6.png" alt=" The Unified Production RAG Blueprint" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Architectural Blueprint: A Unified RAG Pipeline on Google Cloud
&lt;/h2&gt;

&lt;p&gt;To build a robust, production-grade RAG system on the lakehouse, we need more than just a collection of services; we need a cohesive architecture where each component plays a specific, complementary role. Our blueprint unifies data management, machine learning, and analytics within a single, governable ecosystem on Google Cloud. This approach moves beyond siloed vector databases, bringing AI capabilities directly to your data's center of gravity—the data lakehouse. The result is a streamlined, scalable, and cost-effective pipeline that transforms raw information into intelligent, contextual responses.&lt;/p&gt;
&lt;h3&gt;
  
  
  Core Components: BigQuery, &lt;a href="https://votuduc.com/building-self-correcting-agentic-workflows-with-vertex-ai-p-20260505760079" rel="noopener noreferrer"&gt;Building Self-Correcting Agentic Workflows  with Vertex AI&lt;/a&gt;, Apache Iceberg, and Cloud Storage.
&lt;/h3&gt;

&lt;p&gt;The power of this architecture lies in the synergy between four key Google Cloud and open-source technologies. Let's break down the role of each player.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Google Cloud Storage (GCS):&lt;/strong&gt; This is the foundational layer of our lakehouse. GCS acts as the scalable, durable, and cost-effective landing zone for all our raw, unstructured source data—PDFs, Word documents, Markdown files, transcripts, and more. It is the "lake" where our data assets reside in their native format before being processed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Apache Iceberg:&lt;/strong&gt; This is the star of our data management strategy. Iceberg is not just a file format; it's an open table format that brings the reliability and structure of a traditional database directly to the vast data lake on GCS. In our RAG pipeline, Iceberg is critical for several reasons:&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Transactional Integrity:&lt;/strong&gt; It provides ACID-like transactional guarantees for our embeddings and metadata, preventing data corruption during concurrent writes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Schema Evolution:&lt;/strong&gt; It allows us to evolve our data schema (e.g., adding new metadata fields) without rewriting the entire dataset.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Performance:&lt;/strong&gt; Features like partition evolution and file pruning optimize query performance, which is crucial when dealing with billions of vectors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Openness:&lt;/strong&gt; As an open standard, it prevents vendor lock-in and ensures our core data assets are portable and accessible by various engines like Spark, Flink, and, most importantly for us, BigQuery.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Vertex AI:&lt;/strong&gt; This is our intelligence engine, providing the state-of-the-art models needed to understand and represent our text data. Specifically, we leverage the &lt;strong&gt;Vertex AI Embedding APIs&lt;/strong&gt; (e.g., &lt;code&gt;text-embedding-004&lt;/code&gt;). These managed, scalable endpoints take our processed text chunks as input and convert them into high-dimensional numerical vectors (embeddings). This process is the heart of the "retrieval" mechanism, as it encodes the semantic meaning of our text into a format that machines can compare for similarity.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;BigQuery:&lt;/strong&gt; BigQuery is the central nervous system that unifies the entire architecture. It has evolved far beyond a traditional data warehouse and serves two primary functions in our pipeline:&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Lakehouse Query Engine:&lt;/strong&gt; Through its &lt;strong&gt;BigLake&lt;/strong&gt; capabilities, BigQuery can directly read from and query the Apache Iceberg tables stored on GCS. This allows us to use familiar SQL to manage, inspect, and analyze our text chunks and their corresponding embeddings without moving the data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Vector Search Engine:&lt;/strong&gt; This is the game-changer. BigQuery has native &lt;strong&gt;Vector Search&lt;/strong&gt; functionality. It can build and manage a highly efficient Approximate Nearest Neighbor (ANN) index directly on the embedding column within our Iceberg table. This eliminates the need for a separate, dedicated vector database, consolidating our entire RAG backend into a single, powerful platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Data Flow: From Unstructured Data to Indexed Embeddings in Iceberg
&lt;/h3&gt;

&lt;p&gt;The process of converting raw documents into a searchable vector index follows a clear, automated data pipeline. This is the "indexing" half of the RAG workflow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Ingestion:&lt;/strong&gt; The pipeline begins when new unstructured documents (e.g., &lt;code&gt;annual-report-2023.pdf&lt;/code&gt;) are uploaded to a designated GCS bucket.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Parsing and Chunking:&lt;/strong&gt; An event-driven process, such as a Cloud Function or a more robust Dataflow job, is triggered by the new file. This process is responsible for:&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Parsing:&lt;/strong&gt; Extracting the raw text content from the source file.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Chunking:&lt;/strong&gt; Strategically splitting the extracted text into smaller, semantically coherent chunks. The chunking strategy (e.g., fixed size, recursive character splitting, etc.) is a critical factor in the quality of the retrieval results.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Embedding Generation:&lt;/strong&gt; For each text chunk, the processing job makes an API call to a Vertex AI Embedding model endpoint. The API responds with a high-dimensional vector (e.g., a 768-dimension array of floating-point numbers) that captures the semantic essence of that chunk.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Writing to Iceberg:&lt;/strong&gt; The original text chunk, its newly generated vector embedding, and any relevant metadata (e.g., source document name, page number, chunk ID) are packaged together. This structured record is then appended to our primary Apache Iceberg table residing on GCS. Thanks to Iceberg's transactional nature, this write operation is atomic and safe.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Vector Indexing:&lt;/strong&gt; After the data is written to the Iceberg table, we use a simple BigQuery DDL statement to create or update a &lt;code&gt;VECTOR_INDEX&lt;/code&gt;. BigQuery automatically handles the complex process of building the ANN index in the background. This index is what enables lightning-fast similarity searches across potentially billions of vectors during the retrieval step.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  The Role of BigQuery Vector Search as the Unifying Engine
&lt;/h3&gt;

&lt;p&gt;The most transformative aspect of this architecture is how BigQuery Vector Search acts as the unifying force, collapsing what were once disparate systems into a single, cohesive plane.&lt;br&gt;
Traditionally, a RAG pipeline required managing at least three separate systems: an object store for raw files (GCS), a dedicated vector database for ANN search (e.g., Pinecone, Weaviate), and a data warehouse for structured metadata and analytics (BigQuery). This separation introduces complexity in data movement (ETL), security, governance, and operational overhead.&lt;br&gt;
Our blueprint eliminates this fragmentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Data Stays Put:&lt;/strong&gt; By querying Iceberg tables on GCS via BigLake and building the vector index in place, BigQuery brings the compute to the data. There is no need to duplicate and move terabytes of embedding data into a separate database, significantly simplifying the data pipeline and reducing costs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unified Governance and Security:&lt;/strong&gt; Your embeddings and metadata are governed by the same robust security model you already use for BigQuery. You can manage access control at the project, dataset, table, and even column level using familiar IAM policies. This is a massive win for enterprise security and compliance.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Powerful Hybrid Search with SQL:&lt;/strong&gt; This is the killer feature. Because the vector embeddings live in the same table as structured metadata, you can perform sophisticated filtered vector searches in a single, elegant SQL query. Consider a query like: "Find the top 5 document chunks most similar to {user_query}, but only from documents published after '2023-01-01' and tagged with 'finance'." In a siloed system, this is a complex multi-step process: query the vector DB, get IDs, then query the data warehouse with those IDs to filter. With BigQuery, it's a single, optimized query, unlocking powerful, context-aware retrieval that is simply not feasible otherwise.
By positioning BigQuery as the central engine for both data management and vector search, we create a truly unified, scalable, and operationally efficient foundation for production-grade RAG on the lakehouse.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 1 Preparing and Embedding Your Enterprise Data
&lt;/h2&gt;

&lt;p&gt;The foundation of any high-performing RAG system isn't the LLM—it's the data. The quality, structure, and semantic representation of your knowledge base directly dictate the relevance and accuracy of the generated responses. In a Lakehouse architecture, this first step is about establishing a robust, scalable, and open foundation for your data and then transforming it into a format that machine learning models can understand: high-dimensional vectors.&lt;br&gt;
We'll tackle this by first defining our data's home using Apache Iceberg tables in BigQuery, and then processing our raw documents into vectorized "chunks" using Vertex AI's powerful embedding models.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting Up Apache Iceberg Tables in BigQuery
&lt;/h3&gt;

&lt;p&gt;Before we can ingest anything, we need a destination. Why Apache Iceberg? In the context of a Lakehouse, Iceberg provides critical features that traditional data warehousing tables lack. It's an open table format that decouples the table structure from the physical storage (in our case, Google Cloud Storage), offering schema evolution, time travel, and efficient file-level operations. This makes it perfect for managing large, evolving datasets of document chunks and their corresponding embeddings.&lt;br&gt;
We'll create a BigQuery "BigLake" table backed by Iceberg. This table will serve as our "vector store" source of truth, holding the original text chunks, their vector embeddings, and any relevant metadata.&lt;/p&gt;

&lt;p&gt;Here’s the DDL to create our core table, &lt;code&gt;doc_embeddings_iceberg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;your_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_embeddings_iceberg&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"Unique identifier for the text chunk"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;doc_source&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"Identifier for the original source document, e.g., GCS path"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;chunk_text&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt; &lt;span class="k"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"The actual text content of the chunk"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FLOAT64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"The 768-dimension vector embedding from Vertex AI"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;OPTIONS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ICEBERG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;table_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;uris&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gs://your-gcs-bucket/iceberg-warehouse/doc_embeddings'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="n"&gt;connector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'biglake-connector-v1'&lt;/span&gt; &lt;span class="c1"&gt;-- Ensure your BigLake connection is set up&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the key components of this schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;chunk_id&lt;/code&gt;&lt;/strong&gt;: A unique primary key for each piece of text. This is crucial for referencing and updating specific chunks. A UUID or a hash of the content works well.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;doc_source&lt;/code&gt;&lt;/strong&gt;: Links the chunk back to its parent document. This is vital for providing citations and context in the final RAG output.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;chunk_text&lt;/code&gt;&lt;/strong&gt;: The raw text that was vectorized. We store this so we can retrieve the actual content to feed into the LLM's context window.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;embedding&lt;/code&gt;&lt;/strong&gt;: The star of the show. This &lt;code&gt;ARRAY&amp;lt;FLOAT64&amp;gt;&lt;/code&gt; column will hold the numerical vector generated by our embedding model.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;OPTIONS&lt;/code&gt;&lt;/strong&gt;: We explicitly define the format as &lt;code&gt;ICEBERG&lt;/code&gt; and specify the GCS path where the underlying Parquet and metadata files will be stored. This is the core of the Lakehouse pattern—SQL on open files in your data lake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Using Vertex AI Embedding Models for High-Dimensional Vectors
&lt;/h3&gt;

&lt;p&gt;With our table ready, we need a way to convert text into meaningful vectors. An embedding is a dense vector representation of a piece of data (in our case, text) where semantically similar items are located closer together in the vector space.&lt;br&gt;
Google's Vertex AI offers state-of-the-art embedding models that are managed, scalable, and optimized for various tasks. For our RAG use case, we'll use the &lt;code&gt;text-embedding-004&lt;/code&gt; model, which generates a 768-dimensional vector. Its &lt;code&gt;task_type&lt;/code&gt; parameter is specifically designed to optimize embeddings for retrieval, making it ideal for creating a searchable knowledge base.&lt;br&gt;
Here’s a &lt;a href="https://votuduc.com/JSON-to-Video-Automated-Rendering-Engine-p618510" rel="noopener noreferrer"&gt;JSON-to-Video Automated Rendering Engine&lt;/a&gt; snippet demonstrating how to generate embeddings for a batch of text chunks using the Vertex AI SDK:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;vertexai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vertexai.language_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TextEmbeddingModel&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;text_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generates embeddings for a list of text chunks.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;vertexai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# We use the latest text embedding model, optimized for retrieval
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextEmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-004&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# The 'task_type' is critical for optimizing vectors for RAG
# 'RETRIEVAL_DOCUMENT' is used for the text being indexed.
# 'RETRIEVAL_QUERY' would be used for the user's input query.
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;text_chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRIEVAL_DOCUMENT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Extract the numerical vector from the response object
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# --- Example Usage ---
&lt;/span&gt;&lt;span class="n"&gt;my_project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gcp-project-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;my_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;my_chunks&lt;/span&gt; &lt;span class="o"&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;Apache Iceberg is an open table format for huge analytic datasets.&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;BigQuery vector search enables efficient similarity search on embeddings.&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;A Lakehouse architecture combines the benefits of data lakes and data warehouses.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;vector_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# The output 'vector_embeddings' is a list of lists,
# where each inner list is a 768-dimension vector.
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generated &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector_embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; embeddings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dimension of first embedding: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector_embeddings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function is the core of our text-to-vector transformation. It takes a list of strings and returns a corresponding list of 768-dimension floating-point vectors, ready to be inserted into our Iceberg table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch Ingestion and Structuring Data for Vectorization
&lt;/h3&gt;

&lt;p&gt;Now we connect the pieces. The final step is to create a scalable batch pipeline that reads raw documents, processes them into chunks, generates embeddings, and loads the results into our BigQuery Iceberg table.&lt;br&gt;
&lt;strong&gt;1. Data Sourcing and Chunking&lt;/strong&gt;&lt;br&gt;
Your enterprise data likely lives in various formats (PDFs, DOCX, HTML) and locations (GCS, Confluence, etc.). The first task is to extract the raw text. Once you have the text, you must break it down into smaller, semantically meaningful chunks. This is perhaps the most important tuning parameter in a RAG system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Why Chunk?&lt;/strong&gt; LLMs have a limited context window, and embedding models work best on focused, concise pieces of text. Sending an entire 100-page document to an embedding model is ineffective.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Chunking Strategy:&lt;/strong&gt; A simple fixed-size chunk (e.g., 500 characters) is a start, but it can awkwardly split sentences or ideas. A better approach is to use a recursive character text splitter, which tries to split on natural boundaries like paragraphs (&lt;code&gt;\n\n&lt;/code&gt;), then sentences (&lt;code&gt;.&lt;/code&gt;), then spaces (&lt;code&gt;&lt;/code&gt;). Adding a small overlap between chunks (e.g., 50 characters) helps preserve context across boundaries.
&lt;strong&gt;2. The Batch Processing Pipeline&lt;/strong&gt;
A production-grade ingestion pipeline can be orchestrated with tools like Cloud Run, Cloud Functions, or Apache Beam on Dataflow. The logic remains the same:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is a conceptual pipeline structure, not a complete, runnable script.
# You would use libraries like 'google-cloud-bigquery' and 'pypdf'
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bigquery&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_and_ingest_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents_to_process&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Conceptual pipeline to chunk, embed, and ingest documents.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;all_rows_to_insert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;documents_to_process&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="c1"&gt;# Step 1: Extract text from the source document (e.g., a PDF in GCS)
&lt;/span&gt;&lt;span class="n"&gt;raw_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_text_from_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Your custom text extraction logic
# Step 2: Chunk the text using a chosen strategy
&lt;/span&gt;&lt;span class="n"&gt;text_chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk_text_recursively&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Step 3: Generate embeddings for the chunks in batches
# (API has a limit on items per call)
&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gcp-project-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;text_chunks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text_chunks&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Step 4: Structure the data for insertion
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&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;chunk_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk_embeddings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&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;CURRENT_TIMESTAMP()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Let BigQuery handle this
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;all_rows_to_insert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Step 5: Batch load the data into the BigQuery Iceberg table
# The BigQuery Python client can handle streaming inserts or batch loads from GCS
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_rows_to_insert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;table_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_dataset.doc_embeddings_iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# For large volumes, loading from a file (JSON, Parquet) in GCS is more robust
# For simplicity, this example uses streaming inserts
&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_rows_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;all_rows_to_insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Successfully inserted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_rows_to_insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Encountered errors while inserting rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# --- Example Invocation ---
# In a real pipeline, this list would come from scanning a GCS bucket
&lt;/span&gt;&lt;span class="n"&gt;source_docs&lt;/span&gt; &lt;span class="o"&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;gs://my-knowledge-base/doc1.pdf&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;gs://my-knowledge-base/doc2.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;process_and_ingest_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By executing this pipeline, you systematically convert your unstructured enterprise documents into a structured, vectorized dataset within your Lakehouse. This Iceberg table is now the single source of truth for your knowledge base, ready to be indexed for lightning-fast similarity search in the next step.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Indexing and Searching Directly on Iceberg Tables
&lt;/h2&gt;

&lt;p&gt;With our embeddings now residing in an Apache Iceberg table managed by BigQuery, we can unlock the power of high-performance retrieval without any data movement. This is where the tight integration between the Lakehouse storage format and BigQuery's analytical engine truly shines. We will create a vector index directly on the Iceberg table, enabling low-latency similarity searches that are essential for a responsive RAG application.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating a Vector Index on Your BigQuery Iceberg Table
&lt;/h3&gt;

&lt;p&gt;A vector index is a specialized data structure that reorganizes your high-dimensional embedding data to enable Approximate Nearest Neighbor (ANN) search. Instead of exhaustively comparing a query vector to every single vector in your table (a brute-force approach), the index allows the system to quickly narrow down the search to a small, promising subset of candidates. This is the key to achieving millisecond-level latency on datasets with millions or even billions of vectors.&lt;br&gt;
In BigQuery, creating a vector index is a straightforward DDL operation. Let's assume our Iceberg table is named &lt;code&gt;rag_documents&lt;/code&gt; and has the following simplified schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;doc_id&lt;/code&gt; (STRING, PRIMARY KEY)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chunk_text&lt;/code&gt; (STRING)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embedding&lt;/code&gt; (ARRAY&amp;lt;FLOAT64&amp;gt;)
You would create an index on the &lt;code&gt;embedding&lt;/code&gt; column using the &lt;code&gt;CREATE VECTOR INDEX&lt;/code&gt; statement.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;my_doc_index&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;index_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'IVF'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;distance_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'COSINE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;ivf_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{"num_lists": 500}'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's break down the &lt;code&gt;OPTIONS&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;index_type = 'IVF'&lt;/code&gt;&lt;/strong&gt;: This specifies the Inverted File Index, a highly efficient and widely used ANN indexing algorithm. IVF works by clustering the vectors into partitions (or lists). During a search, it only inspects the partitions closest to the query vector, dramatically reducing the search space.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;distance_type = 'COSINE'&lt;/code&gt;&lt;/strong&gt;: This defines the metric used to measure similarity. For embeddings generated by modern transformer models (like those from Vertex AI or OpenAI), &lt;code&gt;COSINE&lt;/code&gt; similarity is almost always the correct choice. It measures the angle between two vectors, making it robust to differences in vector magnitude. Other options include &lt;code&gt;EUCLIDEAN&lt;/code&gt; (L2 distance) and &lt;code&gt;DOT_PRODUCT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;ivf_options = '{"num_lists": 500}'&lt;/code&gt;&lt;/strong&gt;: This JSON string configures the IVF index. The most critical parameter is &lt;code&gt;num_lists&lt;/code&gt;, which sets the number of partitions to create.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Choosing &lt;code&gt;num_lists&lt;/code&gt;&lt;/strong&gt;: The optimal value depends on your dataset size. A good starting point is the square root of the number of rows in your table. For a table with 1 million vectors, a value between 100 and 1,000 is reasonable. A higher &lt;code&gt;num_lists&lt;/code&gt; can lead to faster queries but may require tuning the query-time &lt;code&gt;probe_count&lt;/code&gt; (which we'll cover next) to maintain high recall.
Index creation is an asynchronous background job. You can monitor its progress by querying the information schema:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;coverage_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;last_refresh_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VECTOR_INDEXES&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'rag_documents'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A &lt;code&gt;coverage_percentage&lt;/code&gt; of 100 indicates that the index is fully built and ready for use. BigQuery automatically keeps the index updated as new data is inserted into your Iceberg table.&lt;/p&gt;
&lt;h3&gt;
  
  
  Executing Low-Latency Similarity Searches with the VECTOR_SEARCH Function
&lt;/h3&gt;

&lt;p&gt;Once the index is active, you can perform searches using the &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; function. This function is the core of the retrieval step in your RAG pipeline. It takes a query vector and efficiently finds the &lt;code&gt;top_k&lt;/code&gt; most similar vectors from your indexed table.&lt;br&gt;
The basic syntax is:&lt;br&gt;
&lt;code&gt;VECTOR_SEARCH(TABLE table_name, column_to_search, query_vector, top_k =&amp;gt; k, options =&amp;gt; '...')&lt;/code&gt;&lt;br&gt;
Here is a practical example. Imagine your application has generated an embedding for the user's question, "What are the latest query optimization techniques?". You would use that embedding to find the most relevant document chunks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Assume @query_embedding is a query parameter passed from your application&lt;/span&gt;
&lt;span class="c1"&gt;-- For this example, we'll use a placeholder array.&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FLOAT64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...];&lt;/span&gt; &lt;span class="c1"&gt;-- Your 768 or 1536-dimension query vector&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="n"&gt;VECTOR_SEARCH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- The table with the index&lt;/span&gt;
&lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                             &lt;span class="c1"&gt;-- The indexed column&lt;/span&gt;
&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;-- The vector to search for&lt;/span&gt;
&lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                             &lt;span class="c1"&gt;-- Number of results to return&lt;/span&gt;
&lt;span class="k"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"probe_count": 20}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;search_results&lt;/span&gt;
&lt;span class="c1"&gt;-- Join back to the base table to retrieve the actual text content&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;
&lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt;
&lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
&lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- COSINE distance is 0 for identical, 2 for opposite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Points:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The &lt;code&gt;options&lt;/code&gt; Parameter&lt;/strong&gt;: The &lt;code&gt;probe_count&lt;/code&gt; option is the most important performance tuning knob at query time. It tells the IVF index how many partitions (or lists) to inspect during the search. A higher &lt;code&gt;probe_count&lt;/code&gt; increases the chance of finding the true nearest neighbors (higher recall) at the cost of slightly higher latency. A good starting value is &lt;code&gt;sqrt(num_lists)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The &lt;code&gt;JOIN&lt;/code&gt; Pattern&lt;/strong&gt;: &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; returns the primary key columns of your table (&lt;code&gt;doc_id&lt;/code&gt; in this case) and the &lt;code&gt;distance&lt;/code&gt; for each match. You must &lt;code&gt;JOIN&lt;/code&gt; these results back to your base table to retrieve other columns like &lt;code&gt;chunk_text&lt;/code&gt;, which you'll need to pass to the Large Language Model.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Query Optimization for Production Workloads
&lt;/h3&gt;

&lt;p&gt;For a production RAG system, performance and accuracy are paramount. Simply running a basic vector search is often not enough. You need to consider filtering and tuning to ensure your application is both fast and relevant.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pre-filtering vs. Post-filtering
&lt;/h4&gt;

&lt;p&gt;A common requirement in RAG is to search only within a subset of documents. For example, you might want to find information relevant only to a specific user, product, or date range. There are two ways to apply these filters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Post-filtering (Inefficient)&lt;/strong&gt;: You run the vector search on the entire table and then apply a &lt;code&gt;WHERE&lt;/code&gt; clause to the final result set. This is highly inefficient because the vector search wastes resources finding top matches that are immediately discarded by the filter.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Pre-filtering (Efficient)&lt;/strong&gt;: You apply the filter &lt;em&gt;before&lt;/em&gt; the vector search. BigQuery's engine is smart enough to push these predicates down, meaning the vector search only operates on the subset of data that matches your filter. This dramatically reduces the search space, lowers latency, and reduces cost.
To implement pre-filtering, apply the &lt;code&gt;WHERE&lt;/code&gt; clause to the base table within the &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; function itself.
Let's add a &lt;code&gt;source_year&lt;/code&gt; column to our table and compare the two approaches.
&lt;strong&gt;Inefficient Post-filtering:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- AVOID THIS PATTERN&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="n"&gt;VECTOR_SEARCH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;
&lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_year&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Filter is applied AFTER the expensive search&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Efficient Pre-filtering:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- USE THIS PATTERN&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="n"&gt;VECTOR_SEARCH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="c1"&gt;-- The filter is applied to a subquery on the base table&lt;/span&gt;
&lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;source_year&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;
&lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By filtering the table &lt;em&gt;before&lt;/em&gt; it's passed to &lt;code&gt;VECTOR_SEARCH&lt;/code&gt;, you ensure the ANN search is performed only on the relevant slice of your data, leading to significant performance gains in production workloads. This is a critical optimization for building scalable, multi-tenant RAG applications on the Lakehouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 Grounding the Conversational Agent
&lt;/h2&gt;

&lt;p&gt;With a robust retrieval mechanism in place, the next critical step is to use the retrieved information to generate a coherent, accurate, and contextually relevant answer. This is the "Generation" part of Retrieval-Augmented Generation (RAG). It involves skillfully weaving the search results from BigQuery into a prompt that instructs a Large Language Model (LLM) on how to synthesize a final response. This process transforms raw, retrieved data into a conversational and helpful answer, ensuring the model's output is grounded in the facts contained within our Iceberg table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating Vector Search Results into a Language Model Prompt
&lt;/h3&gt;

&lt;p&gt;The core of grounding lies in &lt;a href="https://votuduc.com/prompt-engineering-for-reliable-autonomous-workspace-agents-p-20260504436320" rel="noopener noreferrer"&gt;Prompt Engineering for Reliable Autonomous Workspace Agents&lt;/a&gt; for &lt;a href="https://votuduc.com/prompt-engineering-for-reliable-autonomous-workspace-agents-p-20260319404106" rel="noopener noreferrer"&gt;Reliable Autonomous Workspace Agents&lt;/a&gt;. We are not simply asking the LLM a question; we are providing it with a specific set of instructions and the exact context it must use to formulate its answer. A well-structured RAG prompt is the key to minimizing hallucinations and ensuring factual consistency.&lt;/p&gt;

&lt;p&gt;A typical RAG prompt consists of three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;System Instructions:&lt;/strong&gt; This is the preamble that defines the LLM's persona, its task, and its constraints. It's where you enforce the rule that the model &lt;em&gt;must&lt;/em&gt; base its answer only on the provided context. This is your primary defense against the model reverting to its parametric knowledge and making things up.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Retrieved Context:&lt;/strong&gt; This is the payload from our BigQuery &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; query. We take the text from the top-k retrieved document chunks and concatenate them into a single block of text. It's good practice to clearly delineate each document chunk, for instance, by numbering them or separating them with a distinct marker.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The User's Question:&lt;/strong&gt; The final part of the prompt is the original query from the user.
By combining these elements, we create a single, comprehensive prompt that gives the LLM everything it needs to generate a grounded response.
Here is a template illustrating this structure. Notice how we use placeholders like &lt;code&gt;&amp;amp;#123;context&amp;amp;#125;&lt;/code&gt; and &lt;code&gt;&amp;amp;#123;question&amp;amp;#125;&lt;/code&gt; which our application logic will replace with the actual data at runtime.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an expert Q&amp;amp;A system that is a world-class expert on internal company documentation.
Your instructions are:
1. Answer the user's QUESTION based ONLY on the provided CONTEXT.
2. Do not use any prior knowledge or information outside of the CONTEXT.
3. If the CONTEXT does not contain the answer, you MUST state that you cannot answer the question with the information provided.
4. Synthesize the information from the CONTEXT into a clear and concise answer. Do not simply copy and paste sections.
5. If the CONTEXT includes source URIs, cite the relevant sources in your answer.
---
CONTEXT:
&amp;amp;#123;context&amp;amp;#125;
---
QUESTION:
&amp;amp;#123;question&amp;amp;#125;
Final Answer:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Building the RAG Logic to Synthesize Answers from Retrieved Context
&lt;/h3&gt;

&lt;p&gt;The orchestration logic is the glue that connects our BigQuery vector index to the LLM. This logic, typically implemented in an application backend (e.g., a Python service running on Cloud Run or a Cloud Function), executes a precise sequence of operations for every incoming user query.&lt;br&gt;
The end-to-end flow is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Receive Query:&lt;/strong&gt; The application receives a question from the end-user.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Embed Query:&lt;/strong&gt; The raw question string is passed to the same text embedding model (e.g., &lt;code&gt;textembedding-gecko@003&lt;/code&gt;) that was used to embed the documents in our Iceberg table. This generates a query vector.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Execute Vector Search:&lt;/strong&gt; The application constructs and executes a &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; query against BigQuery, passing the query vector as a parameter. It retrieves the &lt;code&gt;base_document&lt;/code&gt; (the original text chunk) and any other relevant metadata for the top-k most similar documents.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Format Context:&lt;/strong&gt; The retrieved &lt;code&gt;base_document&lt;/code&gt; texts are collected and formatted into a single string. For example, they can be joined together with a separator like &lt;code&gt;\n---\n&lt;/code&gt;. This string will replace the &lt;code&gt;&amp;amp;#123;context&amp;amp;#125;&lt;/code&gt; placeholder in our prompt template.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Construct Final Prompt:&lt;/strong&gt; The formatted context string and the original user question are injected into the predefined prompt template.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Invoke LLM:&lt;/strong&gt; The complete, final prompt is sent to a generative model API, such as Vertex AI's Gemini 1.0 Pro (&lt;code&gt;gemini-1.0-pro&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Return Response:&lt;/strong&gt; The LLM processes the prompt and generates a response based on the provided context. This response is then returned to the user, completing the RAG cycle.
This sequence ensures that every answer is freshly generated based on the most relevant documents available in the Lakehouse at that moment.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Example Implementation of a Question-Answering Pipeline
&lt;/h3&gt;

&lt;p&gt;Let's translate the logic above into a practical Python implementation. This example uses the &lt;code&gt;google-cloud-bigquery&lt;/code&gt; and &lt;code&gt;vertexai&lt;/code&gt; client libraries to orchestrate the entire pipeline. This function encapsulates the full RAG process: embedding the query, searching BigQuery, and generating the final answer with Gemini.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;vertexai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vertexai.language_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TextEmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextGenerationModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bigquery&lt;/span&gt;
&lt;span class="c1"&gt;# --- Configuration ---
&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-gcp-project-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;LOCATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BQ_DATASET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rag_dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BQ_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iceberg_docs_embedded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMBEDDING_MODEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textembedding-gecko@003&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;GENERATION_MODEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-1.0-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Or your preferred Gemini model
# --- Initialize clients ---
&lt;/span&gt;&lt;span class="n"&gt;vertexai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LOCATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bq_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;embedding_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextEmbeddingModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBEDDING_MODEL_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# It's best practice to initialize the model once
&lt;/span&gt;&lt;span class="n"&gt;generation_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextGenerationModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GENERATION_MODEL_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PROMPT_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are an expert Q&amp;amp;A system that is a world-class expert on internal company documentation.
Your instructions are:
1. Answer the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s QUESTION based ONLY on the provided CONTEXT.
2. Do not use any prior knowledge or information outside of the CONTEXT.
3. If the CONTEXT does not contain the answer, you MUST state that you cannot answer the question with the information provided.
4. Synthesize the information from the CONTEXT into a clear and concise answer.
---
CONTEXT:
{context}
---
QUESTION:
{question}
Final Answer:
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_rag_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Orchestrates the RAG pipeline:
1. Embeds the user question.
2. Searches BigQuery for relevant documents.
3. Generates a response using an LLM.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="c1"&gt;# 1. Embed the user's question
&lt;/span&gt;&lt;span class="n"&gt;question_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedding_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_embeddings&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Execute VECTOR_SEARCH in BigQuery
&lt;/span&gt;&lt;span class="n"&gt;sql_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
SELECT
base_document,
distance
FROM
VECTOR_SEARCH(
TABLE `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BQ_DATASET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BQ_TABLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`,
&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,
(SELECT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question_embedding&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; AS embedding),
top_k =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,
distance_type =&amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;COSINE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;query_job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Format the retrieved context
&lt;/span&gt;&lt;span class="n"&gt;context_chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_document&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;context_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I could not find any relevant information to answer your question.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;context_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&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="n"&gt;context_chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 4. Construct the final prompt
&lt;/span&gt;&lt;span class="n"&gt;final_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 5. Invoke the LLM to generate the final answer
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generation_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;max_output_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="c1"&gt;# --- Example Usage ---
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;user_question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What are the key performance metrics for the Q3 marketing campaign?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_rag_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--- Question ---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;--- Answer ---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production Considerations: Security, Performance, and Governance
&lt;/h2&gt;

&lt;p&gt;Moving a Retrieval-Augmented Generation (RAG) system from a proof-of-concept to a production environment introduces a host of non-functional requirements that are critical for success. It's no longer just about getting the right answer; it's about delivering that answer securely, performantly, and in a way that aligns with your organization's governance and cost management principles. Building your RAG system on a lakehouse architecture with BigQuery and Iceberg provides a powerful foundation to address these challenges head-on, allowing you to leverage existing enterprise-grade features rather than building new solutions from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveraging Existing Lakehouse Security for AI Workloads
&lt;/h3&gt;

&lt;p&gt;One of the most significant advantages of this architecture is the ability to extend your existing data security and governance framework to your AI workloads. Your vector embeddings and source documents are not siloed in a separate, specialized database; they are first-class citizens within your BigQuery lakehouse, inheriting its robust security posture.&lt;br&gt;
&lt;strong&gt;Unified Access Control with IAM:&lt;/strong&gt;&lt;br&gt;
Access to both the source Apache Iceberg tables and the BigQuery vector indexes is managed through Google Cloud's Identity and Access Management (IAM). This means you can use the same roles and permissions you've already defined for your analytical workloads. There's no need to manage a separate set of credentials or access policies for your RAG application's data layer. A service account for your RAG application can be granted a fine-grained role, like &lt;code&gt;roles/bigquery.dataViewer&lt;/code&gt;, on only the specific datasets it needs to access.&lt;br&gt;
&lt;strong&gt;Fine-Grained Data Segmentation:&lt;/strong&gt;&lt;br&gt;
For sensitive data, you can enforce granular control using BigQuery's built-in security features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Column-Level Security (CLS):&lt;/strong&gt; Restrict access to specific columns containing sensitive information. For instance, you could prevent the embedding model pipeline from accessing columns with Personally Identifiable Information (PII) in your source Iceberg table, even if other parts of the table are needed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Row-Level Security (RLS):&lt;/strong&gt; This is a game-changer for multi-tenant or department-specific RAG applications. You can create policies that filter which rows (i.e., which documents or text chunks) are visible to a user or service account based on their identity. An HR-specific RAG bot, for example, could be restricted to only query documents where &lt;code&gt;department = 'HR'&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example of a Row-Level Access Policy&lt;/span&gt;
&lt;span class="c1"&gt;-- This policy ensures that users can only query vectors&lt;/span&gt;
&lt;span class="c1"&gt;-- related to their own department.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;department_filter&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;my_project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my_dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_embeddings&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"group:sales-team@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Sales'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Auditing and Lineage:&lt;/strong&gt;&lt;br&gt;
Every query, including vector searches, is logged in Cloud Audit Logs. This provides an immutable record of what data was accessed, by whom, and when. This is invaluable for compliance, security audits, and debugging. You can trace a specific generated response back to the exact &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; query that was run, providing full data lineage for your AI application's knowledge retrieval step.&lt;br&gt;
&lt;strong&gt;Network Security with VPC Service Controls:&lt;/strong&gt;&lt;br&gt;
For organizations with stringent data exfiltration requirements, you can place your BigQuery datasets and the underlying Cloud Storage buckets for your Iceberg tables within a VPC Service Controls perimeter. This creates a virtual network boundary, ensuring that your sensitive data and embeddings can only be accessed by authorized services and networks, effectively preventing data from leaving your trusted environment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Benchmarking Indexing and Query Performance
&lt;/h3&gt;

&lt;p&gt;Performance in a RAG system is a multi-faceted concern, primarily revolving around the trade-off between search quality (recall) and speed (latency). A systematic benchmarking approach is essential to find the right balance for your application's Service Level Objectives (SLOs).&lt;br&gt;
&lt;strong&gt;Indexing Performance:&lt;/strong&gt;&lt;br&gt;
The creation of a vector index in BigQuery is an asynchronous, back-end process. The time it takes is influenced by the number of vectors, their dimensionality, and the index configuration. While you don't need to manage the underlying compute, you should monitor the build process.&lt;br&gt;
You can track the progress of index creation using the &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;coverage_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;last_refresh_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="nv"&gt;`my_project.my_dataset.INFORMATION_SCHEMA.VECTOR_INDEXES`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'document_embeddings'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index is queryable before it reaches 100% coverage, but performance and recall will improve as it approaches full coverage. For production systems, your data ingestion pipeline should have a step to verify that the &lt;code&gt;coverage_percentage&lt;/code&gt; is 100 before routing live traffic to a newly refreshed index.&lt;br&gt;
&lt;strong&gt;Query Performance: The Latency vs. Recall Trade-off:&lt;/strong&gt;&lt;br&gt;
Approximate Nearest Neighbor (ANN) search, which powers &lt;code&gt;VECTOR_SEARCH&lt;/code&gt;, is designed to be fast by trading perfect accuracy for speed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; The time it takes for the &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; function to return results. This is a critical metric for user-facing applications.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Recall:&lt;/strong&gt; The percentage of the true nearest neighbors that are returned by the query. 100% recall would be equivalent to a brute-force (exact) search, which is computationally expensive.
In BigQuery, the primary tuning knob for this trade-off is the &lt;code&gt;num_lists_to_search&lt;/code&gt; option within &lt;code&gt;ivf_options&lt;/code&gt;. A higher value instructs the query engine to scan more of the index's "inverted file" lists, increasing the probability of finding the true nearest neighbors (higher recall) at the cost of increased processing and higher latency.
&lt;strong&gt;A Practical Benchmarking Strategy:&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Establish Ground Truth:&lt;/strong&gt; On a representative sample of your data (e.g., 10k-100k vectors), run a brute-force distance calculation to find the true top-K nearest neighbors for a set of test queries. This is your "ground truth."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Run Experiments:&lt;/strong&gt; Execute the &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; function against the full, indexed dataset using the same test queries. Vary the &lt;code&gt;num_lists_to_search&lt;/code&gt; parameter for each run (e.g., 10, 20, 50, 100).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Measure and Plot:&lt;/strong&gt; For each parameter setting, calculate the average query latency and the recall (i.e., &lt;code&gt;(number of true neighbors found) / K&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Find the Sweet Spot:&lt;/strong&gt; Plot your results on a latency vs. recall curve. This visualization will help you and your product stakeholders make an informed decision, choosing the lowest &lt;code&gt;num_lists_to_search&lt;/code&gt; value that meets your application's minimum recall requirement, thereby optimizing for the lowest possible latency and cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost Management Strategies for Embedding and Search Operations
&lt;/h3&gt;

&lt;p&gt;Generative AI workloads can become expensive if not managed carefully. A proactive approach to cost optimization is crucial for building a sustainable, production-grade RAG system.&lt;br&gt;
&lt;strong&gt;1. Embedding Costs:&lt;/strong&gt;&lt;br&gt;
The initial and ongoing cost of generating embeddings via an external model API is often the largest component.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Incremental Embedding:&lt;/strong&gt; This is the most effective cost-control strategy. Instead of re-embedding your entire corpus on every update, leverage Apache Iceberg's time-travel capabilities. By querying a snapshot of the table from the last time the pipeline ran, you can easily identify only the new or modified rows that require embedding. This transforms a potentially massive, expensive batch job into a small, efficient, and low-cost incremental update.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Model Selection:&lt;/strong&gt; Carefully choose your embedding model. Higher-dimensional, state-of-the-art models are more expensive per token. Evaluate whether a smaller, more cost-effective model provides sufficient performance for your specific use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Indexing and Storage Costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Storage:&lt;/strong&gt; BigQuery charges for the storage of the vector index itself. While typically much smaller than the raw data, this should be monitored via your Google Cloud billing reports.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Compute:&lt;/strong&gt; The &lt;code&gt;CREATE VECTOR INDEX&lt;/code&gt; job consumes BigQuery compute resources. Since this is often an infrequent operation (e.g., daily or weekly), its cost is usually predictable and manageable. Plan these jobs during off-peak hours if you are using a shared slot pool.
&lt;strong&gt;3. Query Costs:&lt;/strong&gt;
Vector search queries contribute to your BigQuery analysis costs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Query Optimization:&lt;/strong&gt; As determined during benchmarking, use the lowest possible &lt;code&gt;num_lists_to_search&lt;/code&gt; that meets your recall SLOs. This directly reduces the amount of data processed per query.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Caching:&lt;/strong&gt; For frequently asked questions or popular search terms, implement a caching layer (e.g., Redis, Memorystore) in your application to store the retrieved context. This avoids re-running the same &lt;code&gt;VECTOR_SEARCH&lt;/code&gt; query repeatedly, saving significant cost and reducing latency.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Edition and Capacity Planning:&lt;/strong&gt; Align your BigQuery edition with your workload. For applications with predictable, high query volumes, purchasing reserved slots with the Enterprise or Enterprise Plus editions can be more cost-effective than the on-demand model. BigQuery's autoscaling is ideal for handling spiky, unpredictable traffic without overprovisioning.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Monitoring and Alerting:&lt;/strong&gt; Use the &lt;code&gt;INFORMATION_SCHEMA.JOBS&lt;/code&gt; view to monitor the bytes processed by your vector search queries. Set up Cloud Billing budgets and alerts to get notified if costs exceed your forecasts, allowing you to take corrective action before you get a surprise at the end of the month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: Unifying AI and Data on the Lakehouse
&lt;/h2&gt;

&lt;p&gt;We've journeyed through a paradigm shift in building AI applications—moving from complex, fragmented architectures to a streamlined, powerful model centered on the data lakehouse. By integrating vector search capabilities directly into BigQuery and leveraging the open standard of Apache Iceberg, we've demonstrated that production-grade RAG is not just feasible but fundamentally more efficient and secure when AI is brought &lt;em&gt;to&lt;/em&gt; the data. This approach dissolves the traditional boundaries between analytical and AI workloads, paving the way for a new generation of data-driven, intelligent applications built directly on your source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recap of Key Benefits: Reduced Complexity and Enhanced Security
&lt;/h3&gt;

&lt;p&gt;The advantages of this unified architecture are immediate and substantial, primarily revolving around simplification and fortification.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reduced Architectural Complexity:&lt;/strong&gt; The most significant benefit is the radical simplification of the tech stack. By eliminating the need for a separate, specialized vector database, you collapse multiple systems into one. This means:&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No More Data Duplication:&lt;/strong&gt; Your embeddings and source data coexist, managed under a single storage layer with Apache Iceberg. This eradicates complex and brittle ETL pipelines designed solely to sync data between your analytical store and a vector database.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Streamlined MLOps:&lt;/strong&gt; The entire lifecycle—from data ingestion and transformation in BigLake, to embedding generation with remote models, to indexing and querying with &lt;code&gt;VECTOR_SEARCH&lt;/code&gt;—occurs within a cohesive GCP environment. This simplifies orchestration, monitoring, and maintenance.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unified Interface:&lt;/strong&gt; Developers and analysts can leverage familiar SQL to perform sophisticated similarity searches, lowering the barrier to entry and accelerating development cycles.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Enhanced Data Security and Governance:&lt;/strong&gt; Centralizing your vector data within the lakehouse means it inherits the robust security posture of your core data platform.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Single Governance Pane:&lt;/strong&gt; Data security is no longer a federated problem. All your existing BigQuery security controls—IAM permissions, column-level security, row-level access policies, and data masking—apply directly to your vector embeddings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Minimized Data Egress:&lt;/strong&gt; Sensitive data doesn't need to be exfiltrated to another system for processing. It remains within the secure perimeter of your Google Cloud project, drastically reducing the attack surface and simplifying compliance audits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Future of In-Database Vector Processing
&lt;/h3&gt;

&lt;p&gt;What we've built here is not an endpoint but a glimpse into the future of data platforms. The trend is clear: databases are evolving from passive repositories into active, intelligent engines capable of handling diverse workloads, including AI. We can anticipate several exciting developments on this front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Deeper Model Integration:&lt;/strong&gt; Expect tighter integration of embedding models directly within the database engine. Imagine SQL functions that can generate embeddings on the fly from raw text or image data without calling an external endpoint, further simplifying the pipeline.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Advanced, Automated Indexing:&lt;/strong&gt; While the IVFFlat index is powerful, the future will likely bring more advanced, native indexing strategies like HNSW (Hierarchical Navigable Small World) to platforms like BigQuery. This will offer even greater performance and flexibility, with the database optimizer intelligently choosing the best strategy for your query.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;End-to-End In-Database RAG:&lt;/strong&gt; The ultimate evolution is a fully managed, in-database RAG function. A single SQL query or stored procedure could potentially take a user's question, perform the vector search, retrieve context, pass it to a generative model like Gemini, and return a fully synthesized answer—all as a single, atomic database operation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Power of Open Formats:&lt;/strong&gt; Apache Iceberg's role here is critical. It ensures that as these powerful in-database AI capabilities emerge across different engines (like Spark, Flink, or Trino), your underlying data remains open, accessible, and free from vendor lock-in. Your vector-enabled data lakehouse becomes a truly interoperable asset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Next Steps and Further Resources
&lt;/h3&gt;

&lt;p&gt;You now have the architectural blueprint for building a scalable, secure, and efficient RAG system on the lakehouse. The next step is to put it into practice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Experiment and Adapt:&lt;/strong&gt; Take the principles and code from this article and apply them to your own datasets. Start with a small-scale proof-of-concept to understand the nuances of index tuning and query performance for your specific use case.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Explore Advanced Features:&lt;/strong&gt; Dive deeper into BigQuery's capabilities, such as using remote models for embedding generation or integrating the vector search results into complex analytical queries to uncover new insights.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Contribute to the Community:&lt;/strong&gt; Engage with the open-source communities around Apache Iceberg and related projects. Sharing your findings and contributing to the ecosystem benefits everyone building on the modern data stack.
To continue your journey, here are some valuable resources:&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Google Cloud Documentation:&lt;/strong&gt; &lt;a href="https://cloud.google.com/bigquery/docs/vector-search-overview" rel="noopener noreferrer"&gt;BigQuery Vector Search Overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Apache Iceberg Project:&lt;/strong&gt; &lt;a href="https://iceberg.apache.org/docs/latest/" rel="noopener noreferrer"&gt;Official Iceberg Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Google Cloud Blog:&lt;/strong&gt; &lt;a href="https://cloud.google.com/blog/products/data-analytics" rel="noopener noreferrer"&gt;Building AI Applications on BigQuery&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Vertex AI Model Garden:&lt;/strong&gt; &lt;a href="https://cloud.google.com/vertex-ai/docs/generative-ai/model-garden" rel="noopener noreferrer"&gt;Explore Text Embedding Models&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>lakehouse</category>
      <category>bigquery</category>
      <category>apacheiceberg</category>
      <category>rag</category>
    </item>
    <item>
      <title>Gemma 4 on a Tesla T4, Part 2: The Minimum GCE VM and a Script to Drive It</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:24:01 +0000</pubDate>
      <link>https://dev.to/gde/gemma-4-on-a-tesla-t4-part-2-the-minimum-gce-vm-and-a-script-to-drive-it-3gk1</link>
      <guid>https://dev.to/gde/gemma-4-on-a-tesla-t4-part-2-the-minimum-gce-vm-and-a-script-to-drive-it-3gk1</guid>
      <description>&lt;p&gt;&lt;em&gt;This article provides a step by step deployment guide for **Gemma 4 E2B&lt;/em&gt;* to a &lt;strong&gt;Tesla T4&lt;/strong&gt; hosted GPU enabled system. A suite of Python MCP tools is built to simplify management of the vLLM hosted deployment. Part 1 measured which checkpoint the card runs fastest; this part builds the machine underneath it, installs the stack after first boot, and walks through &lt;code&gt;vllm-t4&lt;/code&gt;, the shell script that owns the host state the MCP tools do not.*&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-t4-2b" rel="noopener noreferrer"&gt;github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-t4-2b&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;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;Compute Engine &lt;code&gt;n1-standard-2&lt;/code&gt;, &lt;code&gt;us-west2-b&lt;/code&gt; — 2 vCPU, 7.8 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;1x Tesla T4, Turing, compute capability 7.5, 15360 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;debian-13-trixie-v20260609&lt;/code&gt;, no driver preinstalled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disks&lt;/td&gt;
&lt;td&gt;32 GB pd-ssd boot, 250 GB pd-balanced at &lt;code&gt;/opt1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software&lt;/td&gt;
&lt;td&gt;vLLM 0.29.0, torch 2.13.0+cu130, driver 615.71.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;362 s&lt;/strong&gt; from &lt;code&gt;vllm-t4 start&lt;/code&gt; to a healthy endpoint, 13371 MiB of 15360 claimed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where Do I Start?
&lt;/h2&gt;

&lt;p&gt;Part 1 began with the GPU already working. This part begins with a Google Cloud project and nothing in it.&lt;/p&gt;

&lt;p&gt;The work splits in two. Everything before first boot is a single &lt;code&gt;gcloud&lt;/code&gt; command whose choices are GPU choices: which zone sells a T4, what a GPU does to the maintenance policy, and how much disk to attach given where the checkpoints land. Everything after first boot is software on a Debian image that ships no NVIDIA driver at all.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A Google Cloud project with billing enabled and the Compute Engine API turned on&lt;/li&gt;
&lt;li&gt;GPU quota in the region you intend to use — T4 GPU quota in that region starts at zero on a new project&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gcloud&lt;/code&gt; authenticated locally&lt;/li&gt;
&lt;li&gt;A Hugging Face account, for the Gemma checkpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Machine Is
&lt;/h2&gt;

&lt;p&gt;Read from the instance metadata server and the guest OS on the running VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;machine-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n1-standard-2&lt;/span&gt;
&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;us-west2-b&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;projects/debian-cloud/global/images/debian-13-trixie-v20260609&lt;/span&gt;
&lt;span class="na"&gt;scheduling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automaticRestart"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRUE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;onHostMaintenance"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TERMINATE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preemptible"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FALSE"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;disks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deviceName"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debian13"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERSISTENT-SSD"&lt;/span&gt;&lt;span class="pi"&gt;},&lt;/span&gt;
               &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deviceName"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;persistent-disk-1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERSISTENT-BALANCED"&lt;/span&gt;&lt;span class="pi"&gt;}]&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;Tesla T4, 7.5, 15360 MiB, 615.71.09
nproc: 2
MemTotal: 7436 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Compute Engine the T4 attaches to the N1 machine family, so the host shape is an N1 choice. This one is the smallest N1 that has been used for this work. Two vCPU costs startup time, and 7.8 GB of RAM is the number that decides the swapfile section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the VM
&lt;/h2&gt;

&lt;p&gt;The command below is reconstructed from the running VM's own metadata — machine type, zone, image, scheduling policy and both disk types are read back from the instance, and the accelerator from &lt;code&gt;nvidia-smi&lt;/code&gt;. Re-running the create call was out of scope for this article.&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 instances create gemma4-t4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west2-b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n1-standard-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--accelerator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nvidia-tesla-t4,count&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--maintenance-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TERMINATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart-on-failure&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian-cloud &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian-13 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--boot-disk-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;32GB &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--boot-disk-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pd-ssd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-disk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma4-t4-data,size&lt;span class="o"&gt;=&lt;/span&gt;250GB,type&lt;span class="o"&gt;=&lt;/span&gt;pd-balanced,auto-delete&lt;span class="o"&gt;=&lt;/span&gt;no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three flags in there are about the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Policy Is Forced
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--maintenance-policy=TERMINATE&lt;/code&gt; is required on any instance with an attached GPU. Compute Engine live-migrates ordinary VMs during host maintenance and cannot migrate one with a GPU, so the API rejects the default &lt;code&gt;MIGRATE&lt;/code&gt; policy. The metadata confirms what the instance ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"automaticRestart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"TRUE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"onHostMaintenance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"TERMINATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"preemptible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"FALSE"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pairing it with &lt;code&gt;--restart-on-failure&lt;/code&gt; means maintenance stops the VM and brings it back. The server process does not come back with it, and neither does the swapfile — there is a reboot checklist at the end of this article for that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Disks, Because the Checkpoints Do Not Fit on One
&lt;/h2&gt;

&lt;p&gt;The boot disk is 32 GB and the model cache goes on a second 250 GB disk mounted at &lt;code&gt;/opt1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;NAME     SIZE TYPE MOUNTPOINT
sda       32G disk
├─sda1  31.9G part /
├─sda14    3M part
└─sda15  124M part /boot/efi
sdb      250G disk /opt1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bf16 E2B checkpoint is 10.2 GB and the QAT build is 8.3 GB, so both together are most of a 32 GB root disk before pip has unpacked a CUDA torch wheel. Splitting them keeps the root disk for the OS and puts every multi-gigabyte write on the larger, cheaper volume. &lt;code&gt;~/.cache&lt;/code&gt; is a symlink to &lt;code&gt;/opt1/cache&lt;/code&gt;, so Hugging Face downloads land there without any environment variable.&lt;/p&gt;

&lt;p&gt;The data disk is mounted from &lt;code&gt;/etc/fstab&lt;/code&gt; and survives a reboot:&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="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8bd96fe8-e301-41d9-8ccc-8123ce89c4a8 /opt1 ext4 discard,defaults,nofail 0 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Measure Disk Per Path, Never Once
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mounted on    1B-blocks        Avail
/           33570021376   8450138112
/tmp         3898789888   3897544704
/opt1      263086084096 200857059328
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three filesystems, and the two multi-gigabyte writes an install makes land on different ones. &lt;code&gt;/tmp&lt;/code&gt; is its own 3.9 GB filesystem, which is where pip unpacks wheels, and a CUDA torch plus its NVIDIA runtime dependencies do not fit in it. A single &lt;code&gt;df /&lt;/code&gt; reports the wrong answer for all three writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  After Boot: the Driver
&lt;/h2&gt;

&lt;p&gt;The Debian 13 image carries no NVIDIA driver. &lt;code&gt;nvidia-smi&lt;/code&gt; does not exist on a fresh boot. The driver comes from NVIDIA's own CUDA repository for Debian 13:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/etc/apt/sources.list.d/cuda-debian13-x86_64.list:
deb &lt;span class="o"&gt;[&lt;/span&gt;signed-by&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/keyrings/cuda-archive-keyring.gpg] https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64/ /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; linux-headers-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
wget https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64/cuda-keyring_1.1-1_all.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; cuda-keyring_1.1-1_all.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nvidia-driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What that pulls in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linux-headers-7.1.8+deb13-cloud-amd64   7.1.8-1~bpo13+1
nvidia-driver                           615.71.09-2
nvidia-driver-cuda                      615.71.09-2
nvidia-kernel-open-dkms                 615.71.09-2
nvidia-kernel-support                   615.71.09-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel module is the open variant and DKMS compiles it against the running kernel, so the matching &lt;code&gt;linux-headers&lt;/code&gt; package has to be installed first. The running kernel here is &lt;code&gt;7.1.8+deb13-cloud-amd64&lt;/code&gt; and its headers come from backports, so the headers install gets its own line ahead of the driver.&lt;/p&gt;

&lt;p&gt;Reboot, then check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nvidia-smi &lt;span class="nt"&gt;--query-gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name,compute_cap,memory.total,driver_version &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;csv
&lt;span class="go"&gt;name, compute_cap, memory.total [MiB], driver_version
Tesla T4, 7.5, 15360 MiB, 615.71.09
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compute capability 7.5 is Turing, and it is the number that governs everything in Part 1: no bfloat16 datapath, no fp8, and a 64 KiB shared-memory ceiling per block that the Triton attention kernel has to be clamped to fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Swapfile Is Part of the Deploy
&lt;/h2&gt;

&lt;p&gt;7.8 GB of RAM, and the VM ships with no swap. vLLM is killed by the kernel while staging E2B weights without some, which reads as a crash with no traceback.&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="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 16G /opt1/swapfile
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /opt1/swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /opt1/swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /opt1/swapfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The swapfile lives on the data disk, so the file itself persists. &lt;code&gt;/etc/fstab&lt;/code&gt; has no swap line, so a reboot leaves the file in place and disabled. While serving the QAT build, 3592 MiB of the 16 GB is in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python, and Where Packages Land
&lt;/h2&gt;

&lt;p&gt;There are two interpreters on this host and the default is the one with no room:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3             -&amp;gt; pyenv 3.12.13, site-packages on /        8.45 GB free
/usr/bin/python3.13 -&amp;gt; user site at /opt1/pyuser            200.86 GB free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server runs under &lt;code&gt;python3&lt;/code&gt;. vLLM runs under &lt;code&gt;/usr/bin/python3.13&lt;/code&gt; with &lt;code&gt;PYTHONUSERBASE=/opt1/pyuser&lt;/code&gt;, which puts its packages on the large disk. A relocated user site is the same interpreter's own site directory on a different filesystem, so site-packages stays system-wide and no virtualenv is involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing vLLM
&lt;/h2&gt;

&lt;p&gt;Both redirections below are load-bearing on this host — one for where packages land, one for where pip unpacks them:&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="nv"&gt;PYTHONUSERBASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt1/pyuser &lt;span class="nv"&gt;PIP_CACHE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt1/pipcache &lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt1/tmp &lt;span class="se"&gt;\&lt;/span&gt;
  /usr/bin/python3.13 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--break-system-packages&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upgrade-strategy&lt;/span&gt; eager &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://download.pytorch.org/whl/cu130 vllm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vLLM pins torch to an exact version, so this moves torch with it. Installing torch with &lt;code&gt;--no-deps&lt;/code&gt; skips cuDNN and the NVIDIA runtime wheels, and &lt;code&gt;import torch&lt;/code&gt; then fails on &lt;code&gt;libcudnn.so.9&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;torch        2.13.0+cu130
transformers 5.17.0
triton       3.7.1
vllm         0.29.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The published cu130 wheels carry &lt;code&gt;sm_75&lt;/code&gt;, so Turing needs no source build. &lt;code&gt;make arch&lt;/code&gt; asks the interpreter for the list instead of reading the directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turing Clamp
&lt;/h2&gt;

&lt;p&gt;Gemma 4 mixes two attention widths — 256 in its sliding-window layers, 512 in its global layers — and vLLM forces its Triton backend for that mix. At width 512 the kernel asks for more shared memory than a Turing block allows. &lt;code&gt;make patch&lt;/code&gt; clamps the tile sizes in the installed vLLM:&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="gs"&gt;**Patched**&lt;/span&gt;, in the site-packages this host's &lt;span class="sb"&gt;`python3`&lt;/span&gt; imports.

&lt;span class="gs"&gt;__FILE__&lt;/span&gt;/opt1/pyuser/lib/python3.13/site-packages/vllm/v1/attention/ops/triton_unified_attention.py
CLAMP PRESENT
OCCURRENCES 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reinstalling or upgrading vLLM reverts it, because the patch edits a file in site-packages and pip replaces that file. Re-run &lt;code&gt;make verify-patch&lt;/code&gt; after any vLLM change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter &lt;code&gt;vllm-t4&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;At this point the host is ready and the remaining job is running the server. The rig's &lt;code&gt;tpu.env&lt;/code&gt; holds every serving value, and the MCP tools cover capacity, architecture and patching. Three things are neither rig config nor MCP concerns: the swapfile, launching a process that outlives the shell, and waiting for the endpoint to answer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;~/bin/vllm-t4&lt;/code&gt; owns those three. Every serving value it passes to vLLM is read out of the rig's &lt;code&gt;tpu.env&lt;/code&gt; at call time and none is spelled in the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rigval&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"s/^&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;=//p"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rig&lt;/span&gt;&lt;span class="s2"&gt;/tpu.env"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tpu.env&lt;/code&gt; is a dotenv file whose comments are prose, so sourcing it would both fail as shell and drag &lt;code&gt;TMPDIR&lt;/code&gt; and &lt;code&gt;PYTHONUSERBASE&lt;/code&gt; into the calling shell. Reading one key at a time keeps the file authoritative without importing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Script Launches with &lt;code&gt;nohup&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;make serve&lt;/code&gt; runs &lt;code&gt;python3 -c "asyncio.run(server.start_vllm_server())"&lt;/code&gt;. asyncio terminates the child subprocess when that short-lived interpreter exits, so the engine is gone about a second after launch, having written nothing to the log. The Part 1 sweep was started under the long-lived MCP server process, where the parent stays alive.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vllm-t4 start&lt;/code&gt; launches with &lt;code&gt;nohup&lt;/code&gt; and &lt;code&gt;disown&lt;/code&gt; from bash, and writes the same &lt;code&gt;run/vllm.pid&lt;/code&gt; and &lt;code&gt;run/vllm.log&lt;/code&gt; that the rig's own status and stop tools read, so the script and the MCP tools agree about what is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable swap, confirm the clamp, launch detached, wait for &lt;code&gt;/health&lt;/code&gt;, print status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 start nowait&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The same, returning as soon as the process is up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;make stop&lt;/code&gt; — SIGTERM to the pid, VRAM released on exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;make status&lt;/code&gt; — serving or not, plus claimed VRAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;make query&lt;/code&gt; — one chat completion against the endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tail -f&lt;/code&gt; on &lt;code&gt;run/vllm.log&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm-t4 swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create and enable the swapfile, without starting anything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four environment variables override the defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&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;&lt;code&gt;VLLM_T4_RIG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$HOME/gemma4-dev/gpu-vllm-t4-2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Point at a different rig directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VLLM_T4_SWAPFILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/opt1/swapfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Put swap on another volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VLLM_T4_SWAPSIZE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;16G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Size it differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VLLM_T4_WAIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1800&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds to wait for &lt;code&gt;/health&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;start&lt;/code&gt; Runs Three Guards Before It Launches
&lt;/h2&gt;

&lt;p&gt;Each guard costs less than the failure it prevents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swap&lt;/strong&gt;, because the engine is killed during weight loading without it and the kernel log is the only place that says so. &lt;code&gt;ensure_swap&lt;/code&gt; creates the file if it is absent, enables it if it exists, and reports when it is already on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clamp&lt;/strong&gt;, because an unpatched engine spends minutes loading and then dies with an out-of-resources error that gets attributed to configuration. The check matches the verifier's positive string and refuses on anything else:&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="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s1"&gt;'✅ **Patched**'&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prog&lt;/span&gt;&lt;span class="s2"&gt;: Turing clamp confirmed"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
     die &lt;span class="s2"&gt;"the Turing clamp is not confirmed -- run 'make -C &lt;/span&gt;&lt;span class="nv"&gt;$rig&lt;/span&gt;&lt;span class="s2"&gt; patch'"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A verifier that cannot import vLLM at all answers neither way, so a whitelist of known-bad strings would admit every unknown-bad one. Matching the single good answer fails closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The config&lt;/strong&gt;, because an empty value from &lt;code&gt;tpu.env&lt;/code&gt; becomes an empty CLI argument. Every key is checked for a value and &lt;code&gt;PYTHON_BIN&lt;/code&gt; for executability before anything launches.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Full Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-t4 start
&lt;span class="go"&gt;vllm-t4: swap already on: /opt1/swapfile (16777212 KB)
vllm-t4: Turing clamp confirmed
vllm-t4: starting google/gemma-4-E2B-it-qat-w4a16-ct on 127.0.0.1:8000
vllm-t4: pid 17215, log /home/xbill_glitnir_com/gemma4-dev/gpu-vllm-t4-2b/run/vllm.log
vllm-t4: waiting up to 1800s for http://127.0.0.1:8000/health
     0s  VRAM 0 MiB, 0 %
    75s  VRAM 0 MiB, 0 %
    90s  VRAM 1093 MiB, 10 %
   105s  VRAM 9005 MiB, 10 %
   166s  VRAM 9301 MiB, 0 %
   181s  VRAM 8403 MiB, 0 %
   241s  VRAM 13371 MiB, 0 %
   347s  VRAM 13371 MiB, 0 %
vllm-t4: healthy after 362s -- http://127.0.0.1:8000
✅ Serving at http://127.0.0.1:8000 (pid 17215).

VRAM 13371 MiB, 15360 MiB, 0 %
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VRAM is printed beside the clock because for the first minutes a compiling engine and a dead one look identical from outside, and claimed device memory is what tells them apart. The shape of that column is the startup: nothing for 75 seconds while Python imports and the weights are read off disk, 9005 MiB once the weights are resident, a dip to 8403 while the engine profiles, then 13371 when the KV cache is allocated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Engine Allocated
&lt;/h2&gt;

&lt;p&gt;The same start, in &lt;code&gt;run/vllm.log&lt;/code&gt;:&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="n"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&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;host&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;127.0.0.1&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;model&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;google/gemma-4-E2B-it-qat-w4a16-ct&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;dtype&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;float16&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;max_model_len&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16384&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gpu_memory_utilization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_num_seqs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Casting&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Using&lt;/span&gt; &lt;span class="n"&gt;MarlinLinearKernel&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;CompressedTensorsWNA16&lt;/span&gt;
&lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="n"&gt;loading&lt;/span&gt; &lt;span class="n"&gt;took&lt;/span&gt; &lt;span class="mf"&gt;8.02&lt;/span&gt; &lt;span class="n"&gt;GiB&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mf"&gt;88.981559&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;
&lt;span class="n"&gt;Available&lt;/span&gt; &lt;span class="n"&gt;KV&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.66&lt;/span&gt; &lt;span class="n"&gt;GiB&lt;/span&gt;
&lt;span class="n"&gt;GPU&lt;/span&gt; &lt;span class="n"&gt;KV&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;519&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;681&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Maximum&lt;/span&gt; &lt;span class="n"&gt;concurrency&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;31.72&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="nf"&gt;engine &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warmup&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;took&lt;/span&gt; &lt;span class="mf"&gt;61.77&lt;/span&gt; &lt;span class="nf"&gt;s &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.59&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Casting torch.bfloat16 to torch.float16&lt;/code&gt; is the checkpoint's stored dtype meeting &lt;code&gt;--dtype float16&lt;/code&gt; from &lt;code&gt;tpu.env&lt;/code&gt;. Turing has no bfloat16 datapath, so PyTorch would upconvert regardless; setting the flag makes the conversion a decision with a record.&lt;/p&gt;

&lt;p&gt;Compilation took 2.59 s here against 112.77 s on the first start of this stack, because &lt;code&gt;torch.compile&lt;/code&gt; caches its artifacts under &lt;code&gt;~/.cache&lt;/code&gt; and that cache is warm. A first start on a fresh VM pays the full compile, and on 2 vCPU it is the largest single item in the wall clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;query&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-t4 status
&lt;span class="go"&gt;✅ Serving at http://127.0.0.1:8000 (pid 17215).

VRAM 13371 MiB, 15360 MiB, 0 %
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vllm-t&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google/gemma-4-E2B-it-qat-w4a16-ct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A TPU, or Tensor Processing Unit, is a specialized type of integrated circuit designed to significantly accelerate the mathematical operations central to training and running machine learning models, particularly those involving matrix multiplications."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"system_fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vllm-0.29.0-9a66a08c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That target posts to &lt;code&gt;/v1/chat/completions&lt;/code&gt;. Raw &lt;code&gt;/v1/completions&lt;/code&gt; returns an empty string on an instruction-tuned checkpoint, so an empty result there means the wrong endpoint was called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying It Directly
&lt;/h2&gt;

&lt;p&gt;The server is the standard OpenAI-compatible one, on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://127.0.0.1:8000/v1/models
&lt;span class="go"&gt;{"object":"list","data":[{"id":"google/gemma-4-E2B-it-qat-w4a16-ct","object":"model",
 "owned_by":"vllm","root":"google/gemma-4-E2B-it-qat-w4a16-ct","max_model_len":16384}]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://127.0.0.1:8000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"google/gemma-4-E2B-it-qat-w4a16-ct",
       "messages":[{"role":"user","content":"Name the four inner planets, comma separated."}],
       "temperature":0,"max_tokens":64}'&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;Mercury, Venus, Earth, Mars
usage: {'prompt_tokens': 18, 'total_tokens': 26, 'completion_tokens': 8}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streaming works the same way, with &lt;code&gt;"stream": true&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;chunks: 16
data: {"object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":""}}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"1"}}]}
...
data: [DONE]

assembled: 1, 2, 3, 4, 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prometheus metrics are on &lt;code&gt;/metrics&lt;/code&gt;, which is the route to per-request counters without a benchmark tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;vllm:num_requests_running&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"google/gemma-4-E2B-it-qat-w4a16-ct"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;vllm:prompt_tokens_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;53.0&lt;/span&gt;
&lt;span class="n"&gt;vllm:generation_tokens_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;49.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model id in every request body has to match what &lt;code&gt;/v1/models&lt;/code&gt; reports, which is &lt;code&gt;MODEL_NAME&lt;/code&gt; from &lt;code&gt;tpu.env&lt;/code&gt;. Serving the bf16 build for an A/B means overriding &lt;code&gt;MODEL_NAME&lt;/code&gt; and &lt;code&gt;MODEL_SAFETENSORS_BYTES&lt;/code&gt; in the environment, and the id in the request body changes with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each Subcommand Refuses
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-t4 start                                  &lt;span class="c"&gt;# one is already running&lt;/span&gt;
&lt;span class="go"&gt;vllm-t4: already running (pid 14977) -- 'vllm-t4 status', or 'vllm-t4 stop' first   [exit 1]

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-t4 bogus
&lt;span class="go"&gt;usage: vllm-t4 [start|stop|status|query|log|swap] [nowait]                          [exit 2]

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vllm-t4 swap                                   &lt;span class="c"&gt;# already enabled&lt;/span&gt;
&lt;span class="go"&gt;vllm-t4: swap already on: /opt1/swapfile (16777212 KB)                              [exit 0]

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;VLLM_T4_RIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/nonexistent vllm-t4 status
&lt;span class="go"&gt;vllm-t4: rig not found: /nonexistent (set VLLM_T4_RIG)                              [exit 1]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the engine dies during the wait, the script prints the last 20 log lines and points at the kernel log, because a memory kill leaves no traceback in vLLM's own output:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;running_pid&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prog&lt;/span&gt;&lt;span class="s2"&gt;: the engine died during startup; last 20 log lines:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rig&lt;/span&gt;&lt;span class="s2"&gt;/run/vllm.log"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prog&lt;/span&gt;&lt;span class="s2"&gt;: if there is no traceback, check 'dmesg -T | grep -i oom'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stopping
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vllm-t4 stop
✅ Sent SIGTERM to vLLM (pid 14977). VRAM is released on exit.

The T4 stays attached and the VM stays billed — stopping the server is not releasing capacity here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VRAM returns to &lt;code&gt;0 MiB, 15360 MiB&lt;/code&gt; within seconds. Swap is left enabled, since it costs nothing idle and the next start needs it.&lt;/p&gt;

&lt;p&gt;The VM and its attached T4 bill by the hour whether vLLM runs or not. &lt;code&gt;gcloud compute instances stop gemma4-t4 --zone us-west2-b&lt;/code&gt; stops the instance charge and keeps both disks, which keep billing at the much lower storage rate. Deleting the instance with &lt;code&gt;auto-delete=no&lt;/code&gt; on the data disk leaves the 250 GB of checkpoints for the next VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  After a Reboot
&lt;/h2&gt;

&lt;p&gt;Three things come back on their own and one does not.&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;Survives a reboot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/opt1&lt;/code&gt; data disk&lt;/td&gt;
&lt;td&gt;🟢 in &lt;code&gt;/etc/fstab&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA driver&lt;/td&gt;
&lt;td&gt;🟢 DKMS module, loads at boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turing clamp&lt;/td&gt;
&lt;td&gt;🟢 a file in site-packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/opt1/swapfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ the file persists, the &lt;code&gt;swapon&lt;/code&gt; does not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;vllm-t4 start&lt;/code&gt; re-enables swap every time, so the checklist after a reboot is one command. Adding a swap line to &lt;code&gt;/etc/fstab&lt;/code&gt; makes it survive on its own.&lt;/p&gt;

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

&lt;p&gt;The goal of this article was to build the smallest Compute Engine VM that serves Gemma 4 E2B on one Tesla T4, deploy the stack after first boot, and document the script that runs it. The key to the solution was keeping the rig's &lt;code&gt;tpu.env&lt;/code&gt; authoritative for every serving value and giving the script only the three things that are host state: the swapfile, a detached launch, and waiting for the endpoint. The measured results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;362 s&lt;/strong&gt; from &lt;code&gt;vllm-t4 start&lt;/code&gt; to a healthy endpoint on a warm compile cache, &lt;strong&gt;13371 MiB&lt;/strong&gt; of 15360 claimed&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;8.02 GiB&lt;/strong&gt; of weights and &lt;strong&gt;4.66 GiB&lt;/strong&gt; of KV cache, giving &lt;strong&gt;519,681&lt;/strong&gt; tokens and 31.72x concurrency at 16,384 tokens per request&lt;/li&gt;
&lt;li&gt;🟢 One &lt;code&gt;gcloud&lt;/code&gt; command, one apt repository for the driver, one pip install, one patch&lt;/li&gt;
&lt;li&gt;⚠️ &lt;code&gt;--maintenance-policy=TERMINATE&lt;/code&gt; is required for an attached GPU, and stops the VM during host maintenance&lt;/li&gt;
&lt;li&gt;⚠️ 7.8 GB of host RAM needs a &lt;strong&gt;16 GB&lt;/strong&gt; swapfile, and the &lt;code&gt;swapon&lt;/code&gt; does not survive a reboot&lt;/li&gt;
&lt;li&gt;❌ A single &lt;code&gt;df&lt;/code&gt; reports the wrong free space for all three writes the install makes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One Tesla T4 on one &lt;code&gt;n1-standard-2&lt;/code&gt; VM in &lt;code&gt;us-west2-b&lt;/code&gt;, Debian 13, vLLM 0.29.0 on torch 2.13.0+cu130, driver 615.71.09. The start timing is a single run on a warm &lt;code&gt;torch.compile&lt;/code&gt; cache; a first start on a fresh VM pays the full compilation, which was 112.77 s on this host. The &lt;code&gt;gcloud compute instances create&lt;/code&gt; command is reconstructed from the running instance's metadata and was not re-executed. Throughput figures for the two checkpoints are in Part 1.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for Tesla T4 deployment and benchmarking was validated with an incremental step by step approach.&lt;/p&gt;

</description>
      <category>gemma</category>
      <category>vllm</category>
      <category>gcp</category>
      <category>cuda</category>
    </item>
    <item>
      <title>What Nobody Is Using in Your Google Cloud Projects, and What It Costs</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:14:36 +0000</pubDate>
      <link>https://dev.to/gde/what-nobody-is-using-in-your-google-cloud-projects-and-what-it-costs-1k0</link>
      <guid>https://dev.to/gde/what-nobody-is-using-in-your-google-cloud-projects-and-what-it-costs-1k0</guid>
      <description>&lt;p&gt;This article provides a step by step guide to building a Google Cloud waste scanner from source, running it across every project your credentials can see, pricing each finding from the Cloud Billing Catalog API, and drafting the cleanup. A suite of Python checks is built to cover Compute Engine, Cloud SQL, Storage, DNS, KMS, Secret Manager, Artifact Registry, Filestore, Cloud Logging, Cloud Monitoring and GKE.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  What Gets Left Behind
&lt;/h4&gt;

&lt;p&gt;A Persistent Disk survives the instance it was attached to. A static IP outlives the migration that freed it. A GKE cluster keeps charging its management fee after the last node pool scaled to zero. Each one bills every hour and reports nothing.&lt;/p&gt;

&lt;p&gt;The billing console shows the total. The Recommender surfaces candidates. A figure that drives a decision names one resource, in one project, and its monthly cost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;zombiescan&lt;/code&gt; produces that figure for 20 classes of resource across two packs.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;A Google Cloud account and Application Default Credentials, from &lt;code&gt;gcloud auth application-default login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv&lt;/code&gt; on the path, and Python 3.11 or newer&lt;/li&gt;
&lt;li&gt;Read access to the projects you mean to scan — the predefined &lt;code&gt;roles/viewer&lt;/code&gt; covers every call, and &lt;code&gt;zombiescan-scanner-role.yaml&lt;/code&gt;, under &lt;code&gt;policy/&lt;/code&gt; in the repository, is a 16-permission custom role that covers exactly as much&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Build It From Source
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/zombiescan-gcp
&lt;span class="nb"&gt;cd &lt;/span&gt;zombiescan-gcp
uv &lt;span class="nb"&gt;sync&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;Resolved 39 packages in 0.58ms
Checked 38 packages in 0.19ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uv sync&lt;/code&gt; reads the committed &lt;code&gt;uv.lock&lt;/code&gt;, so the resolved set is the one the tests ran against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run zombiescan &lt;span class="nt"&gt;--version&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;zombiescan, version 0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Step 2 — Install It as a Tool
&lt;/h4&gt;

&lt;p&gt;Working from the clone keeps &lt;code&gt;uv run&lt;/code&gt; in front of every command. Installing it puts &lt;code&gt;zombiescan&lt;/code&gt; on the path instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/xbill9/zombiescan-gcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both routes run the same engine. The rest of this article uses the installed form.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Authenticate
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;google.auth.default()&lt;/code&gt; picks that up. No key file is downloaded, and no service account is created.&lt;/p&gt;

&lt;p&gt;Every worker thread in a scan shares one credentials object, and the first token is fetched before the threads start. Refreshing credentials signs through OpenSSL, and one credentials object refreshed from several threads at once ends the process instead of raising, so the wrapper serialises the refreshes behind a lock.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — List the Checks
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan checks
&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;  disabled-kms-key  Disabled KMS key versions still billing  (core)
  empty-vpc-network  VPC networks with nothing running in them  (core)
  gke-idle-cluster  GKE clusters running no nodes  (gke)
  idle-cloud-nat  Cloud NAT gateways serving no instances  (core)
  idle-filestore  Filestore instances nothing can mount  (core)
  idle-forwarding-rule  Load balancers with no backends  (core)
  orphaned-snapshot  Snapshots of deleted disks  (core)
  stale-artifact-repository  Artifact Registry repositories with no recent pushes  (core)
  stale-secret  Secrets nothing has updated in 90 days  (core)
  stopped-instance  Stopped instances still paying for disks  (core)
  stopped-sql-instance  Stopped Cloud SQL instances still paying for storage  (core)
  unattached-disk  Unattached Persistent Disks  (core)
  unbounded-log-bucket  Log buckets retaining logs indefinitely  (core)
  unmanaged-gcs-bucket  Versioned buckets with no lifecycle rule  (core)
  unused-dns-zone  Cloud DNS zones publishing nothing  (core)
  unused-firewall-rule  Firewall rules matching nothing  (core)
  unused-image  Custom images nothing boots from  (core)
  unused-static-ip  Unused static IP addresses  (core)
  unused-subnet  Subnets reserving ranges nothing uses  (core)
  unused-uptime-check  Uptime checks monitoring deleted resources  (core)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nineteen ship in the &lt;code&gt;core&lt;/code&gt; pack and one in &lt;code&gt;gke&lt;/code&gt;. GKE sits in its own pack because it carries its own API, its own rate section and its own price fetcher, which is what a third-party pack has to supply.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Which APIs the Checks Need
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan apis
&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;  artifactregistry.googleapis.com  1 check(s)
  cloudkms.googleapis.com  1 check(s)
  compute.googleapis.com  12 check(s)
  container.googleapis.com  1 check(s)
  dns.googleapis.com  1 check(s)
  file.googleapis.com  1 check(s)
  logging.googleapis.com  1 check(s)
  monitoring.googleapis.com  1 check(s)
  secretmanager.googleapis.com  1 check(s)
  sqladmin.googleapis.com  1 check(s)
  storage.googleapis.com  1 check(s)

An API that is not enabled on a project is skipped, not reported as an error: a
project that never used a service has no waste in it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each check declares the APIs it calls. That declaration generates this list and the read-only custom role, and the test suite fails when a check names an API the role does not cover.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Scan One Project
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan scan &lt;span class="nt"&gt;--project&lt;/span&gt; glitnir-dev
&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;Scanning as application default credentials
1 project(s), 20 check(s) — read-only

zombiescan — 2 findings across 1 project

Check                      Found  Monthly
stale-artifact-repository      1    $0.01
empty-vpc-network              1    $0.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every call is a list or a get. The scan has no code path that deletes, modifies or releases anything.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Scan Every Project
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan scan &lt;span class="nt"&gt;--all-projects&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;Scanning as application default credentials
75 project(s), 20 check(s) — read-only

zombiescan — 713 findings across 75 projects

Check                      Found  Monthly
unattached-disk               36  $153.53
unused-static-ip              12   $89.06
unused-image                  64   $75.25
stopped-instance              12   $35.76
orphaned-snapshot             39   $28.66
stale-artifact-repository     13    $4.75
stale-secret                   9    $0.54
unused-dns-zone                1    $0.20
empty-vpc-network             13    $0.00
unmanaged-gcs-bucket         118    $0.00
unused-firewall-rule          43    $0.00
unused-subnet                349    $0.00
unused-uptime-check            4    $0.00
&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;Estimated waste: $387.74/month ($4,652.87/year)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--all-projects&lt;/code&gt; asks Resource Manager's &lt;code&gt;projects.search&lt;/code&gt; which projects the caller can see, then fans out over them. 75 projects times 20 checks is 1,500 project/check pairs, and 411 of them were skipped because the API was not enabled on that project. The sweep took 79 seconds.&lt;/p&gt;

&lt;p&gt;Thirteen of the 20 checks fired. Five of those thirteen classes carry no charge and account for 527 of the 713 findings: empty VPC networks, versioned buckets with no lifecycle rule, firewall rules matching nothing, unused subnets and uptime checks monitoring deleted resources. They are reported because they accumulate without limit and block deletions.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Read the Per-Resource Table
&lt;/h4&gt;

&lt;p&gt;The detail table sorts the costliest findings first and guarantees a row to every check that fired.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Project        ┃ Location                ┃ Resource               ┃  Monthly ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ comglitn       │ global                  │ w5-opt                 │   $53.19 │
│ glitnir-ba1    │ europe-west1-b          │ disk-20260326-160533   │   $50.00 │
│ glitnir-ba1    │ europe-west1-c          │ disk-20260326-152217   │   $50.00 │
│ glitnir-mb1    │ us-east1-c              │ mb1                    │   $21.76 │
│ comglitn       │ us-central1-c           │ disk-opt-w1            │   $20.00 │
│ xbill-b1       │ us                      │ snapshot-b1-east       │   $16.57 │
│ comglitn       │ us-central1-c           │ w2                     │   $10.88 │
│ glitnir-sx1    │ us-east4                │ sx2                    │    $8.03 │
│ swoonbox1      │ us-east4                │ swoonbox-landing-4     │    $8.03 │
│ comglitn       │ us-central1             │ w1                     │    $7.30 │
└────────────────┴─────────────────────────┴────────────────────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Project is the first column because a resource id is unique inside a project and nowhere else. Location holds a zone, a region or &lt;code&gt;global&lt;/code&gt;, read off each resource.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;~&lt;/code&gt; marks an estimate or an upper bound, and the per-finding &lt;code&gt;note&lt;/code&gt; in JSON output names the reason.&lt;/p&gt;

&lt;p&gt;🔎 Tip: &lt;code&gt;--min-cost 5&lt;/code&gt; hides findings under five dollars a month, &lt;code&gt;--limit 0&lt;/code&gt; prints every row instead of the top 25, and &lt;code&gt;--location us-central1&lt;/code&gt; keeps a region's zones along with the region itself.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 9 — Where a Check Runs
&lt;/h4&gt;

&lt;p&gt;A check runs once per project. Two properties of Google's APIs make that a complete sweep.&lt;/p&gt;

&lt;p&gt;Compute Engine's &lt;code&gt;aggregatedList&lt;/code&gt; returns disks, addresses, instances, routers, subnetworks and forwarding rules across every zone and region in one call. Most other APIs accept &lt;code&gt;locations/-&lt;/code&gt;, a wildcard meaning every location.&lt;/p&gt;

&lt;p&gt;Cloud KMS and Artifact Registry reject &lt;code&gt;locations/-&lt;/code&gt;. Those two enumerate the API's locations and walk them in parallel, and each worker gets its own client, because the HTTP connection underneath a discovery client cannot be shared between threads.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 10 — Where the Prices Come From
&lt;/h4&gt;

&lt;p&gt;The bundled price table is generated from the Cloud Billing Catalog API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; zombiescan.pricing.refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scan reads &lt;code&gt;table.json&lt;/code&gt; from the installed package, so it runs offline and adds no latency. Rates are looked up by key and region: &lt;code&gt;ctx.pricing.rate("disk.gb_month", region=...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verified rates in us-central1, from the bundled table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;us-central1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pd-standard&lt;/td&gt;
&lt;td&gt;$0.04/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pd-balanced&lt;/td&gt;
&lt;td&gt;$0.10/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pd-ssd&lt;/td&gt;
&lt;td&gt;$0.17/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshots and custom images&lt;/td&gt;
&lt;td&gt;$0.05/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle static IP&lt;/td&gt;
&lt;td&gt;$0.010/hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forwarding rule minimum&lt;/td&gt;
&lt;td&gt;$0.025/hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GKE cluster&lt;/td&gt;
&lt;td&gt;$0.10/hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret version&lt;/td&gt;
&lt;td&gt;$0.06/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMS software key version&lt;/td&gt;
&lt;td&gt;$0.06/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMS HSM key version&lt;/td&gt;
&lt;td&gt;$1.00/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact Registry storage&lt;/td&gt;
&lt;td&gt;$0.10/GB/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 730 hours to the month, arithmetic over those hourly rates gives $7.30 a month for an idle static IP, $18.25 for a forwarding rule, and $73.00 for a GKE cluster with no nodes on it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 11 — Read the First Tier That Charges
&lt;/h4&gt;

&lt;p&gt;Google fronts many SKUs with a free allowance priced at zero: the first 30 GB of standard Persistent Disk, the first 0.5 GB of Artifact Registry, the first six secret versions. A fetcher that reads tier 0 records the rate as free, which prices every finding in that section at nothing and reports a clean project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unit_price()&lt;/code&gt; takes the first tier that charges. Reading a named tier deliberately — the way the Cloud DNS fetcher walks the zone tiers at $0.20 for the first 25 and $0.10 beyond them — is a separate call.&lt;/p&gt;

&lt;p&gt;A SKU published against the region &lt;code&gt;global&lt;/code&gt; has no per-region entry at all. Cloud NAT addresses, Artifact Registry storage and log retention are all published that way, so they live in a global section with a &lt;code&gt;scope="global"&lt;/code&gt; rate spec.&lt;/p&gt;

&lt;p&gt;The refresher refuses to write a table that loses or empties a section the previous one had, because an empty section means a matcher stopped matching.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 12 — JSON Output
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan scan &lt;span class="nt"&gt;--project&lt;/span&gt; glitnir-dev &lt;span class="nt"&gt;--json&lt;/span&gt; findings.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-21T19:55:55Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application default credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"projects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"glitnir-dev"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pairs_attempted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pairs_unavailable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pricing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-21T16:35:39Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"basis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Google Cloud Billing Catalog API, on-demand USD list prices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"excludes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"committed use discounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sustained use discounts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credits"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"monthly_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"annual_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"finding_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each finding carries its own reason, cost, remediation command and a &lt;code&gt;details&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stale-artifact-repository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gcr.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glitnir-dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monthly_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approximate_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remediation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gcloud artifacts repositories delete gcr.io --location=us --project=glitnir-dev --quiet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stored_gb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"idle_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;282&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usd_per_gb_month"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upper bound: Artifact Registry bills each unique layer once, and images sharing a base layer are counted once per image here"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing block dates the table and lists what list prices exclude, so a report read six months later says which rates produced it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 13 — The HTML Report
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan scan &lt;span class="nt"&gt;--project&lt;/span&gt; glitnir-dev &lt;span class="nt"&gt;--html&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One self-contained file with the styles inline, which prints to PDF without fetching anything.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 14 — The Cleanup Plan
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan scan &lt;span class="nt"&gt;--project&lt;/span&gt; glitnir-dev &lt;span class="nt"&gt;--script&lt;/span&gt; cleanup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# zombiescan cleanup plan — generated 2026-09-21T19:55:55Z&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# READ EVERY LINE BEFORE RUNNING THIS.&lt;/span&gt;
&lt;span class="c"&gt;# zombiescan generated this file and did not run it. Deleting Google&lt;/span&gt;
&lt;span class="c"&gt;# Cloud resources is not reversible. Where a backup is possible the&lt;/span&gt;
&lt;span class="c"&gt;# command takes one first, but a backup is not a substitute for&lt;/span&gt;
&lt;span class="c"&gt;# knowing what you are deleting.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2 resource(s), about $0.01/month.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# glitnir-dev us gcr.io — DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days&lt;/span&gt;
&lt;span class="c"&gt;# saves about $0.01/month&lt;/span&gt;
gcloud artifacts repositories delete gcr.io &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;glitnir-dev &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="c"&gt;# glitnir-dev global default — VPC network runs no instances; it still holds none. This is the auto-created default network&lt;/span&gt;
&lt;span class="c"&gt;# saves about $0.00/month&lt;/span&gt;
gcloud compute networks delete default &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;glitnir-dev &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every generated command carries &lt;code&gt;--project&lt;/code&gt; and &lt;code&gt;--quiet&lt;/code&gt;, and interpolated resource ids are shell-quoted. The test suite checks all three at the source, because a remediation command is printed for an operator to run.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 15 — Cleaning Up
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;clean&lt;/code&gt; is a separate command, and a dry run is what it does with no flags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zombiescan clean &lt;span class="nt"&gt;--project&lt;/span&gt; glitnir-dev
&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;Scanning as application default credentials — 1 project(s)

dry run — 1 of 2 finding(s) can be cleaned, 1 cannot
  skip empty-vpc-network default: a network cannot be deleted until everything
inside it is gone -- subnets, routes, firewall rules, Cloud Routers, peerings
and any Private Service Connect attachment -- and the order depends on what else
references them. Clean the findings inside the network first; this one goes away
with them

stale-artifact-repository  gcr.io  glitnir-dev · us · $0.01/mo
  DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days
    → IRREVERSIBLE delete Artifact Registry repository gcr.io and its images
      artifactregistry.projects.locations.repositories.delete({'name':
'projects/glitnir-dev/locations/us/repositories/gcr.io'})

Would free about $0.01/month across 1 resource(s); 1 include irreversible steps.
Nothing was changed. Re-run with --apply to perform these.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--apply&lt;/code&gt; gates one thing: whether a planned step is sent to Google Cloud. It never changes which steps get planned, so the preview above is what runs.&lt;/p&gt;

&lt;p&gt;Planning is read-only. A cleaner may read — fetching an instance's disks to clear &lt;code&gt;autoDelete&lt;/code&gt;, reading a router's NAT list — and it yields the mutations as objects for the runner to send.&lt;/p&gt;

&lt;p&gt;Where the API allows a backup, the backup step is ordered before the destruction, and a failed step stops the rest of that finding. A failed snapshot can never be followed by the delete that assumed it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IRREVERSIBLE&lt;/code&gt; marks a step with no recovery window. A released static IP is gone, so it carries the mark; a destroyed KMS key version is held for 24 hours, so it does not.&lt;/p&gt;

&lt;p&gt;A check with no cleaner reports the finding as unsupported with a reason, which is the &lt;code&gt;skip&lt;/code&gt; line above. The test suite fails a check that has neither a cleaner nor a reason.&lt;/p&gt;

&lt;p&gt;🔎 Tip: &lt;code&gt;clean --from findings.json&lt;/code&gt; acts on a report already reviewed, and refuses a report produced by different credentials.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 16 — The Read-Only Role
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam roles create zombiescanScanner &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;policy/zombiescan-scanner-role.yaml &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixteen permissions, every one a list or a get. The file is generated from the &lt;code&gt;apis=&lt;/code&gt; each check declares, and the suite keeps it in step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clean&lt;/code&gt; needs more than this, deliberately: the identity that reports waste should be unable to delete what it reports.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 17 — The Claude Code Plugin
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add xbill9/zombiescan-gcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin ships two slash commands, a skill and an MCP server over the same engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  scan_project  Scan Google Cloud projects for unused resources and price them
  estimate_savings  Total, count and break down the findings in a report
  explain_finding  Explain why a check treats a resource as waste
  list_checks  List the installed checks
  plan_cleanup  Show exactly what `zombiescan clean` would do
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool is read-only. &lt;code&gt;plan_cleanup&lt;/code&gt; builds the step objects and stops there; the function that sends them to Google Cloud is unreachable from the server, and a test asserts the module names no other &lt;code&gt;clean.*&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;The tools return computed figures — totals, counts, breakdowns, cheapest and costliest — and echo the filter they applied. A tool that returned rows for the model to add up would move the arithmetic to the place least able to do it.&lt;/p&gt;

&lt;p&gt;The protocol is JSON-RPC over stdio in the standard library, with no MCP SDK dependency for about a hundred lines of framing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Three Findings That Differ From the AWS Instinct
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Google Cloud behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle Cloud NAT&lt;/td&gt;
&lt;td&gt;Gateway uptime bills per VM using it, so a gateway serving nothing costs the addresses it holds — $3.65 a month each at $0.005/hour. An AWS NAT gateway bills a flat hourly charge whatever uses it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GKE cluster&lt;/td&gt;
&lt;td&gt;$0.10/hour whatever runs on it. Scaling every node pool to zero removes the node cost and leaves the $73.00 a month management fee.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved static IP&lt;/td&gt;
&lt;td&gt;Google bills an idle static IP at a higher hourly rate than one attached to a running instance, so the waste costs more than the work.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  What the Checks Leave Alone
&lt;/h4&gt;

&lt;p&gt;A false positive here costs an outage, so each check states the condition it declines to act on.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Treatment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load balancer with backends that are unhealthy&lt;/td&gt;
&lt;td&gt;An outage; left alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot whose source disk still exists&lt;/td&gt;
&lt;td&gt;Load-bearing; left alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk attached to a stopped instance&lt;/td&gt;
&lt;td&gt;Reported under &lt;code&gt;stopped-instance&lt;/code&gt;, counted once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google-managed KMS key, disabled&lt;/td&gt;
&lt;td&gt;Free; never reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key or secret already scheduled for destruction&lt;/td&gt;
&lt;td&gt;Already on a timer; left alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default VPC network with nothing in it&lt;/td&gt;
&lt;td&gt;Reported, and flagged as the auto-created default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An API not enabled on a project&lt;/td&gt;
&lt;td&gt;Skipped; a service never used holds no waste&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;empty-vpc-network&lt;/code&gt; reports without a cleanup step. A network deletes only after its subnets, routes, firewall rules, Cloud Routers and peerings are gone, in an order that depends on what references them, so the finding says so where a command would otherwise appear.&lt;/p&gt;

&lt;p&gt;Four of the 75 projects returned &lt;code&gt;permission denied: no access&lt;/code&gt; on every check, producing 42 skipped project/check pairs. Those appear in the report as a list, and the monthly total covers the projects that were readable.&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;zombiescan&lt;/th&gt;
&lt;th&gt;Recommender / Active Assist&lt;/th&gt;
&lt;th&gt;Billing reports&lt;/th&gt;
&lt;th&gt;Cross-project SaaS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-resource dollar figure&lt;/td&gt;
&lt;td&gt;🥇 yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;aggregate only&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credentials leave the machine&lt;/td&gt;
&lt;td&gt;🥇 never&lt;/td&gt;
&lt;td&gt;n/a, Google-side&lt;/td&gt;
&lt;td&gt;n/a, Google-side&lt;/td&gt;
&lt;td&gt;❌ service account granted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deletes on request&lt;/td&gt;
&lt;td&gt;🥈 opt-in, dry run first&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;Price source&lt;/td&gt;
&lt;td&gt;Billing Catalog API, dated&lt;/td&gt;
&lt;td&gt;Google-side&lt;/td&gt;
&lt;td&gt;your bill&lt;/td&gt;
&lt;td&gt;vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs offline after install&lt;/td&gt;
&lt;td&gt;🥇 yes&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;td&gt;❌ no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breadth&lt;/td&gt;
&lt;td&gt;20 checks&lt;/td&gt;
&lt;td&gt;broader&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;broader&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;Billing reports answer what the projects spent. Recommender covers more ground and reaches areas outside these 20 checks, including the metric-driven judgements that need a lookback window.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;zombiescan&lt;/code&gt; fits the case where the answer has to be per-resource, priced, and produced without granting anything access to the projects. A laptop, existing credentials, and about 80 seconds for 75 projects.&lt;/p&gt;




&lt;h4&gt;
  
  
  Cost
&lt;/h4&gt;

&lt;p&gt;A scan costs nothing. List and get calls carry no charge, and the price table ships with the package, so a scan makes no Billing Catalog call.&lt;/p&gt;

&lt;p&gt;Regenerating the table calls the Cloud Billing Catalog API, which is also free.&lt;/p&gt;




&lt;h4&gt;
  
  
  Tests
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-q&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;324 passed, 4 deselected in 0.37s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite runs offline against recorded API responses, with no credentials. The four deselected tests reach a real project and run under &lt;code&gt;ZOMBIESCAN_LIVE=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recorded responses are keyed the way the discovery client returns them, and every aggregated one includes a scope holding only a &lt;code&gt;warning&lt;/code&gt;, which is how Compute Engine reports an empty zone. A check that walks past that shape without skipping it fails here instead of in production.&lt;/p&gt;




&lt;h4&gt;
  
  
  Teardown
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool uninstall zombiescan-gcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is left in any project. The tool creates no service account, no role, no bucket and no stored state. Revoke the local credentials with &lt;code&gt;gcloud auth application-default revoke&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;The goal of this article was to audit every Google Cloud project a set of credentials can reach and attach a monthly cost to each unused resource. The key to the solution was fanning out per project instead of per region, and fetching every rate from the Cloud Billing Catalog API while keeping the scan read-only. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 20 checks in two packs across Compute Engine, Cloud SQL, Storage, DNS, KMS, Secret Manager, Artifact Registry, Filestore, Cloud Logging, Cloud Monitoring and GKE&lt;/li&gt;
&lt;li&gt;🟢 75 projects swept in 79 seconds, 713 findings, $387.74 a month and $4,652.87 a year&lt;/li&gt;
&lt;li&gt;🟢 One call per project: &lt;code&gt;aggregatedList&lt;/code&gt; covers every zone and region, and &lt;code&gt;locations/-&lt;/code&gt; covers every location for the APIs that accept it&lt;/li&gt;
&lt;li&gt;🟢 Prices dated in every report, with the first charging tier read instead of a free tier 0&lt;/li&gt;
&lt;li&gt;🟢 JSON against a published schema, a self-contained HTML report, and a shell cleanup plan that is printed and never run&lt;/li&gt;
&lt;li&gt;🟢 &lt;code&gt;clean&lt;/code&gt; previews by default, orders a backup before the destruction it protects, and marks steps with no recovery window&lt;/li&gt;
&lt;li&gt;⚠️ 411 of 1,500 project/check pairs were skipped because the API was not enabled, and 42 more for lack of access to 4 projects, so $387.74 covers the projects that were readable&lt;/li&gt;
&lt;li&gt;⚠️ Figures are on-demand list prices, excluding committed use discounts, sustained use discounts, private pricing and credits&lt;/li&gt;
&lt;li&gt;⚠️ Artifact Registry is an upper bound: it bills a shared layer once, and a finding counts it once per image&lt;/li&gt;
&lt;li&gt;❌ &lt;code&gt;empty-vpc-network&lt;/code&gt; reports without a cleanup step, because a network needs its dependencies removed in an order that depends on what references them&lt;/li&gt;
&lt;li&gt;❌ Idle judgements that need a lookback window — Cloud SQL by connection count, KMS keys by audit log — need Cloud Monitoring and are outside these 20 checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: one set of Application Default Credentials, 75 projects reachable through &lt;code&gt;projects.search&lt;/code&gt;, 20 checks, a single sweep per figure quoted, run from one laptop. The read-only guarantee holds for the packs in this repository, which are reviewed; a pack installed from PyPI runs with the same credentials and carries no such guarantee.&lt;/p&gt;

&lt;p&gt;The strategy for using per-resource pricing for Google Cloud waste detection 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;Repository, MIT: &lt;a href="https://github.com/xbill9/zombiescan-gcp" rel="noopener noreferrer"&gt;https://github.com/xbill9/zombiescan-gcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Findings JSON Schema: &lt;a href="https://github.com/xbill9/zombiescan-gcp/blob/main/docs/findings.schema.json" rel="noopener noreferrer"&gt;https://github.com/xbill9/zombiescan-gcp/blob/main/docs/findings.schema.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pack author guide: &lt;a href="https://github.com/xbill9/zombiescan-gcp/blob/main/docs/PACKS.md" rel="noopener noreferrer"&gt;https://github.com/xbill9/zombiescan-gcp/blob/main/docs/PACKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Billing Catalog API, &lt;code&gt;services.skus.list&lt;/code&gt;: &lt;a href="https://cloud.google.com/billing/docs/reference/rest/v1/services.skus/list" rel="noopener noreferrer"&gt;https://cloud.google.com/billing/docs/reference/rest/v1/services.skus/list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Compute Engine &lt;code&gt;aggregatedList&lt;/code&gt;: &lt;a href="https://cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList" rel="noopener noreferrer"&gt;https://cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VPC pricing, external IP addresses: &lt;a href="https://cloud.google.com/vpc/network-pricing" rel="noopener noreferrer"&gt;https://cloud.google.com/vpc/network-pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GKE cluster management fee: &lt;a href="https://cloud.google.com/kubernetes-engine/pricing" rel="noopener noreferrer"&gt;https://cloud.google.com/kubernetes-engine/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Application Default Credentials: &lt;a href="https://cloud.google.com/docs/authentication/application-default-credentials" rel="noopener noreferrer"&gt;https://cloud.google.com/docs/authentication/application-default-credentials&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>googlecloud</category>
      <category>python</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Share State Across Dart Isolates Without Losing Your Mind: Enter shared_map</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:43:19 +0000</pubDate>
      <link>https://dev.to/gde/share-state-across-dart-isolates-without-losing-your-mind-enter-sharedmap-221b</link>
      <guid>https://dev.to/gde/share-state-across-dart-isolates-without-losing-your-mind-enter-sharedmap-221b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 3 of the &lt;strong&gt;Dart and Flutter&lt;/strong&gt; series—practical guides, architectural deep dives, and hard-earned engineering lessons from the field. Each article is completely standalone.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Dart’s concurrency model is built on &lt;strong&gt;Isolates&lt;/strong&gt;. Unlike threads in Java, C++, or Go, Dart isolates share no memory. Each isolate has its own private heap and its own single-threaded event loop.&lt;/p&gt;

&lt;p&gt;This "share-nothing" model is a brilliant design decision. It completely eliminates data races, deadlocks, mutex contention, and tricky thread-synchronization bugs. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Until, of course, you actually &lt;em&gt;need&lt;/em&gt; to share data across isolates.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine this common production scenario:&lt;/p&gt;

&lt;p&gt;You’re building a Flutter app that crunches heavy data in the background—perhaps resizing multiple images, decoding massive JSON payloads, computing cryptographic hashes, or running complex ML calculations. To keep your UI silky smooth at 120 FPS, you offload the work to background isolates using &lt;code&gt;Isolate.run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now suppose all these concurrent background workers need access to a &lt;strong&gt;shared, in-memory cache&lt;/strong&gt; (like parsed metadata, authentication tokens, or shared computation results) to avoid duplicate work.&lt;/p&gt;

&lt;p&gt;How do you do that in Dart?&lt;/p&gt;

&lt;p&gt;Traditionally, you only had two bad choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serialize and copy the whole data structure&lt;/strong&gt; back and forth across isolate boundaries every time. For large maps or high-frequency operations, this burns CPU and produces massive GC pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-roll a message-passing server&lt;/strong&gt; using &lt;code&gt;ReceivePort&lt;/code&gt; and &lt;code&gt;SendPort&lt;/code&gt;. You have to invent custom request/response DTOs, generate unique request correlation IDs, wire up response completers, and write 150 lines of brittle plumbing just to perform a simple key-value lookup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a third, vastly superior option that almost nobody talks about: &lt;strong&gt;&lt;a href="https://pub.dev/packages/shared_map" rel="noopener noreferrer"&gt;&lt;code&gt;package:shared_map&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is &lt;code&gt;shared_map&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Created by veteran Dart engineer Graciliano M. Passos, &lt;code&gt;shared_map&lt;/code&gt; provides a versatile, synchronized &lt;code&gt;Map&lt;/code&gt; data structure designed specifically to be shared across Dart isolates and asynchronous workflows.&lt;/p&gt;

&lt;p&gt;Here is what makes it an architectural gem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Dependencies&lt;/strong&gt;: It is pure, clean Dart with zero third-party dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;160 / 160 Pub Points&lt;/strong&gt;: Flawless quality score on pub.dev and fully Dart 3 compatible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal Platform Support&lt;/strong&gt;: Runs anywhere Dart runs—iOS, Android, macOS, Windows, Linux, Web, and backend CLI/servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Familiar Map Semantics&lt;/strong&gt;: You interact with it using standard async key-value methods like &lt;code&gt;get()&lt;/code&gt;, &lt;code&gt;put()&lt;/code&gt;, &lt;code&gt;putIfAbsent()&lt;/code&gt;, and &lt;code&gt;update()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of you manually orchestrating ports, &lt;code&gt;shared_map&lt;/code&gt; manages the cross-isolate communication protocol transparently under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works: The Reference Pattern
&lt;/h2&gt;

&lt;p&gt;The core mental model of &lt;code&gt;shared_map&lt;/code&gt; is dead simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Main Instance&lt;/strong&gt;: You create a &lt;code&gt;SharedMap&lt;/code&gt; on your primary isolate (like your Flutter UI thread or main server loop). This instance acts as the authoritative source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Reference&lt;/strong&gt;: You call &lt;code&gt;.sharedReference()&lt;/code&gt; to generate a lightweight, serializable token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auxiliary Instance&lt;/strong&gt;: You pass that lightweight token across an isolate boundary (e.g. into &lt;code&gt;Isolate.run&lt;/code&gt;). Inside the isolate, you reconstruct a proxy instance using &lt;code&gt;SharedMap.fromSharedReference(ref)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any reads, writes, or mutations performed by the worker isolate are automatically dispatched back to the main instance and synchronized across all isolates!&lt;/p&gt;




&lt;h2&gt;
  
  
  See It In Action
&lt;/h2&gt;

&lt;p&gt;Let’s write a complete, self-contained example. We'll simulate multiple concurrent worker isolates crunching data, reading from a shared cache, and populating cache entries on the fly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:isolate'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:shared_map/shared_map.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Create a SharedStore and a SharedMap on the main isolate&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'app_cache'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Seed an initial value&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;userCache&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Randal (Admin)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Extract the lightweight, serializable reference&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cacheReference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sharedReference&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'--- Spawning Background Worker 1 ---'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Pass the reference into a background isolate&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;worker1Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Isolate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Reconstitute the synchronized map proxy&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSharedReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheReference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Read the value previously stored by the main isolate:&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_101'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[Worker 1] Read from shared cache: &lt;/span&gt;&lt;span class="si"&gt;$user&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Put a new value into the shared cache from this background worker:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_102'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Wilhelm (Engineer)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'Worker 1 finished'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker1Result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'--- Spawning Background Worker 2 ---'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Spawn a second isolate to prove cross-isolate synchronization&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;worker2Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Isolate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSharedReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheReference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Worker 2 can immediately read what Worker 1 just wrote!&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user102&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_102'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[Worker 2] Read value written by Worker 1: &lt;/span&gt;&lt;span class="si"&gt;$user102&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Use putIfAbsent atomically&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user103&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;putIfAbsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_103'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'Guest User'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'[Worker 2] Added: &lt;/span&gt;&lt;span class="si"&gt;$user103&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker2Result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Verify the main isolate reflects all updates&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'--- Back on Main Isolate ---'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Total entries in cache: &lt;/span&gt;&lt;span class="si"&gt;${await userCache.length()}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_102 on main: &lt;/span&gt;&lt;span class="si"&gt;${await userCache.get('user_102')}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'user_103 on main: &lt;/span&gt;&lt;span class="si"&gt;${await userCache.get('user_103')}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Console Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- Spawning Background Worker 1 ---
[Worker 1] Read from shared cache: Randal (Admin)
Worker 1 finished
--- Spawning Background Worker 2 ---
[Worker 2] Read value written by Worker 1: Wilhelm (Engineer)
[Worker 2] Added: Guest User
--- Back on Main Isolate ---
Total entries in cache: 3
user_102 on main: Wilhelm (Engineer)
user_103 on main: Guest User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what just happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two independent background isolates communicated and shared data back to the main thread.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not a single &lt;code&gt;SendPort&lt;/code&gt;, &lt;code&gt;ReceivePort&lt;/code&gt;, &lt;code&gt;Completer&lt;/code&gt;, or serialization boilerplate line was written.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Superpower: Local Read Caching with &lt;code&gt;SharedMapCached&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If your worker isolates perform thousands of rapid reads, you might not want every single &lt;code&gt;get()&lt;/code&gt; call to perform a cross-isolate message dispatch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shared_map&lt;/code&gt; includes a built-in subclass called &lt;strong&gt;&lt;code&gt;SharedMapCached&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cachedWorkerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedMapCached&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSharedReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;cacheReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Cache items locally in this isolate for high-throughput reads&lt;/span&gt;
  &lt;span class="nl"&gt;timeout:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you query an existing key with &lt;code&gt;SharedMapCached&lt;/code&gt;, it caches the value locally in the worker's isolate heap. If subsequent reads occur within the timeout window, they resolve instantly without cross-isolate latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Grouping Maps with &lt;code&gt;SharedStore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In complex applications, you rarely have just one cache. You might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An image cache (&lt;code&gt;SharedMap&amp;lt;String, Uint8List&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A user profile cache (&lt;code&gt;SharedMap&amp;lt;String, UserProfile&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A rate limiter map (&lt;code&gt;SharedMap&amp;lt;String, int&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of passing dozens of individual references around, you pass a single &lt;strong&gt;&lt;code&gt;SharedStoreReference&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Main thread:&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'global_store'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'tokens'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'rate_limits'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;storeRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sharedReference&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Inside any background isolate:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Isolate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;workerStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SharedStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSharedReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storeRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Dynamically resolve any map registered under this store:&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'tokens'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;rateLimits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workerStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSharedMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'rate_limits'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Senior Engineering Wisdom: When to Use (and Not Use) &lt;code&gt;shared_map&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;As with any tool, understanding the architectural sweet spot is key.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ When to Reach for &lt;code&gt;shared_map&lt;/code&gt;:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CPU-Intensive Worker Coordination&lt;/strong&gt;: When running pools of background isolates (&lt;code&gt;Isolate.run&lt;/code&gt; or persistent worker isolates) that need shared lookups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Deduping &amp;amp; Caching&lt;/strong&gt;: Preventing concurrent workers from calculating or downloading the exact same asset twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Isolate Metrics &amp;amp; Counters&lt;/strong&gt;: Collecting stats, telemetry, or rate-limit tokens across multiple threads.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ⚠️ When NOT to Use &lt;code&gt;shared_map&lt;/code&gt;:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persistent On-Disk Storage&lt;/strong&gt;: &lt;code&gt;shared_map&lt;/code&gt; is an in-memory data structure. If your data must survive app restarts, use SQLite, Drift, or a persistent key-value store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-Isolate Applications&lt;/strong&gt;: If all your code runs on the root UI isolate, a standard Dart &lt;code&gt;Map&amp;lt;K, V&amp;gt;&lt;/code&gt; or reactive Signal is all you need—there's no reason to pay the asynchronous abstraction cost.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;Dart's isolate architecture keeps our code safe from concurrency bugs, but you shouldn't have to write hundreds of lines of port plumbing just to share an in-memory cache across worker tasks.&lt;/p&gt;

&lt;p&gt;By bringing in &lt;strong&gt;&lt;a href="https://pub.dev/packages/shared_map" rel="noopener noreferrer"&gt;&lt;code&gt;shared_map&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You keep isolates isolated and UI threads responsive.&lt;/li&gt;
&lt;li&gt;You eliminate manual &lt;code&gt;SendPort&lt;/code&gt; and &lt;code&gt;ReceivePort&lt;/code&gt; spaghetti code.&lt;/li&gt;
&lt;li&gt;You get atomic, synchronized key-value storage with zero external dependencies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add &lt;code&gt;shared_map: ^1.1.9&lt;/code&gt; to your &lt;code&gt;pubspec.yaml&lt;/code&gt; and stop reinventing isolate messaging from scratch.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's your take?
&lt;/h3&gt;

&lt;p&gt;How do you currently coordinate data between background isolates in your Flutter apps? Have you been writing custom ports, or relying on &lt;code&gt;Isolate.run&lt;/code&gt; return values? Let me know in the comments below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Randal L. Schwartz is a Google Developer Expert (GDE) for Dart &amp;amp; Flutter and veteran software architect.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📺 Watch deep dives and walkthroughs on YouTube: &lt;strong&gt;&lt;a href="https://www.youtube.com/@RandalOnDartAndFlutter" rel="noopener noreferrer"&gt;@RandalOnDartAndFlutter&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💻 Connect on GitHub: &lt;strong&gt;&lt;a href="https://github.com/RandalSchwartz" rel="noopener noreferrer"&gt;@RandalSchwartz&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>architecture</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>One Iceberg MCP Server, Seven Catalogs: What It Takes to Reach Each One</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:30:58 +0000</pubDate>
      <link>https://dev.to/gde/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-2605</link>
      <guid>https://dev.to/gde/one-iceberg-mcp-server-seven-catalogs-what-it-takes-to-reach-each-one-2605</guid>
      <description>&lt;p&gt;This article provides a step by step guide to one MCP server for Apache Iceberg tables, pointed at seven Iceberg REST catalogs in turn. The server offers four read-only tools, and one environment variable decides which catalog it reads.&lt;/p&gt;

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




&lt;h4&gt;
  
  
  What is this project trying to Do?
&lt;/h4&gt;

&lt;p&gt;An MCP server gives any MCP client, such as Claude Code or another coding agent, a fixed set of tools. This one has four, all reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iceberg_list_tables&lt;/code&gt;&lt;/strong&gt; — every &lt;code&gt;namespace.table&lt;/code&gt; in the catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iceberg_describe_table&lt;/code&gt;&lt;/strong&gt; — columns, partitioning, snapshots and the metadata location&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iceberg_count_rows&lt;/code&gt;&lt;/strong&gt; — the exact row count from the snapshot summary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;iceberg_scan_table&lt;/code&gt;&lt;/strong&gt; — a few rows, plus an exact count, min and max&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every Iceberg REST catalog speaks the same protocol, so in principle the same server should work against all of them. This project checks that against seven catalogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt; 1.7.0, in Docker on the same machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google BigLake&lt;/strong&gt;, &lt;strong&gt;Microsoft OneLake&lt;/strong&gt;, &lt;strong&gt;AWS Glue&lt;/strong&gt;, &lt;strong&gt;AWS S3 Tables&lt;/strong&gt; and &lt;strong&gt;Snowflake Horizon&lt;/strong&gt;, over the internet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databricks Unity&lt;/strong&gt;, not run: its trial account has ended&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server is called directly over MCP, with no AI model involved, so each result depends only on the catalog and the server's settings. Measured on 2026-09-18 and 2026-09-19 (UTC), one run per catalog, reads only.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where do I start?
&lt;/h4&gt;

&lt;p&gt;The strategy is an incremental step by step approach.&lt;/p&gt;

&lt;p&gt;First, the local Polaris catalog, which needs only Docker. Then the MCP server, called directly to check all four tools. Then one catalog at a time: its login, its storage package, and a run of the same four calls.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Docker, for the local Polaris catalog&lt;/li&gt;
&lt;li&gt;Python 3.10+ with &lt;code&gt;pyiceberg&lt;/code&gt; 0.12.0 — this run used Python 3.14.7&lt;/li&gt;
&lt;li&gt;For each managed catalog you want to reach, an account with a table in it and a working command-line login: &lt;code&gt;gcloud&lt;/code&gt;, &lt;code&gt;az&lt;/code&gt;, &lt;code&gt;aws&lt;/code&gt; or a Snowflake key pair&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Step 1 — Start Polaris
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/xbill9/lakehouse-iceberg-2026
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;lakehouse-iceberg-2026/iceberg-conformance
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./polaris-up.sh
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;POLARIS_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="nv"&gt;POLARIS_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;s3cr3t
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 seed_table.py &lt;span class="nt"&gt;--catalog&lt;/span&gt; apache-polaris
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates &lt;code&gt;probe_ns.probe_table&lt;/code&gt;: 11 rows, partitioned by day, with four snapshots.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 2 — Look at the Server
&lt;/h4&gt;

&lt;p&gt;The server is &lt;code&gt;iceberg-mcp-hosts/servers/iceberg_mcp.py&lt;/code&gt;. It speaks MCP over stdio as newline-delimited JSON-RPC and answers &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt; and &lt;code&gt;ping&lt;/code&gt;, with no MCP SDK.&lt;/p&gt;

&lt;p&gt;It picks its catalog from two environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ICEBERG_CATALOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws-glue &lt;span class="nv"&gt;ICEBERG_CATALOGS_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalogs.yaml python3 servers/iceberg_mcp.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;catalogs.yaml&lt;/code&gt; holds one entry per catalog: the URL, the warehouse, and how to log in. The server code is the same for every catalog.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 3 — Call It Without a Model
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;sweep_catalogs.py&lt;/code&gt; starts the server once per catalog and makes the same MCP calls each time: &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, then the four tools on the first table listed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../iceberg-mcp-hosts
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 sweep_catalogs.py &lt;span class="nt"&gt;--only&lt;/span&gt; apache-polaris
&lt;span class="go"&gt;apache-polaris       probe_ns.probe_table                 list_tables=ok(1.0s)  describe_table=ok(0.4s)  count_rows=ok(0.0s)  scan_table=ok(0.1s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan's reply shows what a client receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id | ts | payload | region
0 | 2026-09-01 00:00:00+00:00 | row-0-0 | None
2 | 2026-09-01 02:00:00+00:00 | row-0-2 | None
3 | 2026-09-01 03:00:00+00:00 | row-0-3 | None

3 of 11 row(s) shown, read from snapshot-id 1196292829914853564
COUNT: exactly 11 row(s) are in the table in snapshot-id 1196292829914853564. Exact, over the whole table.
MIN and MAX of id over those 11 row(s): 0 and 23. Exact.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed catalog call comes back as text starting &lt;code&gt;CATALOG ERROR&lt;/code&gt;, so an agent can still say what it could not read. The sweep counts that text as a failure.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 4 — Add One Entry per Catalog
&lt;/h4&gt;

&lt;p&gt;Each managed catalog needs its own login in &lt;code&gt;catalogs.yaml&lt;/code&gt;. The server builds a PyIceberg &lt;code&gt;RestCatalog&lt;/code&gt; from it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;catalog&lt;/th&gt;
&lt;th&gt;login&lt;/th&gt;
&lt;th&gt;how files are read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Polaris&lt;/td&gt;
&lt;td&gt;OAuth2 client ID and secret&lt;/td&gt;
&lt;td&gt;PyArrow, local &lt;code&gt;file:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigLake&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gcloud&lt;/code&gt; token, &lt;code&gt;x-goog-user-project&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;PyArrow, &lt;code&gt;gs://&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OneLake&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;az&lt;/code&gt; token&lt;/td&gt;
&lt;td&gt;fsspec with &lt;code&gt;adlfs&lt;/code&gt;, &lt;code&gt;abfss://&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue&lt;/td&gt;
&lt;td&gt;SigV4, service &lt;code&gt;glue&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;PyArrow, &lt;code&gt;s3://&lt;/code&gt;, local AWS login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Tables&lt;/td&gt;
&lt;td&gt;SigV4, service &lt;code&gt;s3tables&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fsspec with &lt;code&gt;s3fs&lt;/code&gt;, catalog-issued credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizon&lt;/td&gt;
&lt;td&gt;Snowflake key-pair JWT&lt;/td&gt;
&lt;td&gt;PyArrow, &lt;code&gt;s3://&lt;/code&gt;, catalog-issued credential&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;S3 Tables issues a storage credential when asked with the &lt;code&gt;X-Iceberg-Access-Delegation: vended-credentials&lt;/code&gt; header. Horizon returns one without being asked.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 5 — Install the Storage Packages
&lt;/h4&gt;

&lt;p&gt;Listing, describing and counting read only catalog metadata, so they work with &lt;code&gt;pyiceberg&lt;/code&gt; alone. Scanning reads data files, and three catalogs need packages that &lt;code&gt;pyiceberg&lt;/code&gt; does not install by default. Without them the server answers three of the four tools and the scan fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CATALOG ERROR while scanning dbo.probe_table: ModuleNotFoundError: No module named 'adlfs'.

CATALOG ERROR while scanning probe_ns.probe_table: ModuleNotFoundError: No module named 's3fs'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS logins made with the newer &lt;code&gt;aws login&lt;/code&gt; command need one more package before any call works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CATALOG ERROR while listing tables: MissingDependencyException: Missing Dependency: Using the login credential provider requires an additional dependency. You will need to pip install "botocore[crt]" before proceeding.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;adlfs s3fs &lt;span class="s2"&gt;"botocore[crt]"&lt;/span&gt;
&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;package&lt;/th&gt;
&lt;th&gt;needed for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adlfs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OneLake scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s3fs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;S3 Tables scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;botocore[crt]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Glue and S3 Tables with an &lt;code&gt;aws login&lt;/code&gt; session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Use the &lt;code&gt;az&lt;/code&gt; Credential Directly for OneLake
&lt;/h4&gt;

&lt;p&gt;OneLake's data files need an Azure storage credential as well as the catalog token. &lt;code&gt;DefaultAzureCredential&lt;/code&gt; is the usual choice, and it tries the Azure VM metadata service before the &lt;code&gt;az&lt;/code&gt; login. Off Azure, that attempt waits out its retries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     917ms No environment configuration found.
     921ms ManagedIdentityCredential will use IMDS
  553989ms DefaultAzureCredential acquired a token from AzureCliCredential
AzureCliCredential      0.6s
DefaultAzureCredential  553.1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through the MCP server, a three-row OneLake scan took 858.5 seconds with &lt;code&gt;DefaultAzureCredential&lt;/code&gt; and 3.4 seconds with &lt;code&gt;AzureCliCredential&lt;/code&gt;. Every other tool call was unaffected, because only the scan reads files.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 6 — Run All Six
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 sweep_catalogs.py &lt;span class="nt"&gt;--only&lt;/span&gt; apache-polaris,google-lakehouse,microsoft-onelake,aws-glue,aws-s3tables,snowflake-horizon
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 sweep_catalogs.py &lt;span class="nt"&gt;--report-only&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;catalog             measured (UTC)        table                   rows  partitioned     seconds: list describe count scan
apache-polaris      2026-09-18T23:58:37Z  probe_ns.probe_table      11  ts_day          1.0 0.5 0.0 0.1
aws-glue            2026-09-18T23:58:51Z  probe_ns.probe_table      11  ts_day          2.1 0.7 0.2 1.5
aws-s3tables        2026-09-18T23:58:57Z  probe_ns.probe_table      11  ts_day          2.2 0.3 0.2 2.0
google-lakehouse    2026-09-19T00:11:14Z  probe_ns.probe_table      11  ts_day          10.1 1.6 0.5 3.6
microsoft-onelake   2026-09-18T23:58:45Z  dbo.probe_table            6  (unpartitioned) 2.5 0.2 0.2 3.2
snowflake-horizon   2026-09-18T23:59:02Z  PROBE_NS.PROBE_TABLE      12  ts_day          7.3 2.2 1.4 1.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All four tools work on all six catalogs.&lt;/strong&gt; Each one reported the same four tools from &lt;code&gt;tools/list&lt;/code&gt;, and no call returned an error.&lt;/p&gt;

&lt;p&gt;The first call on each catalog also builds the client and fetches a login token, which is why &lt;code&gt;list&lt;/code&gt; is the slowest column. Each figure is a single call, so the times show scale only.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 7 — Read What Came Back
&lt;/h4&gt;

&lt;p&gt;The tables were created separately for an &lt;a href="https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj"&gt;earlier article&lt;/a&gt;, and the server reports what each catalog holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OneLake&lt;/strong&gt; names its namespace &lt;code&gt;dbo&lt;/code&gt;, stores &lt;code&gt;id&lt;/code&gt; as an optional &lt;code&gt;int&lt;/code&gt;, and has 6 rows in an unpartitioned table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizon&lt;/strong&gt; returns upper-case names, &lt;code&gt;PROBE_NS.PROBE_TABLE&lt;/code&gt;, and has 12 rows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The other four&lt;/strong&gt; hold the same 11-row table, partitioned by day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A client only has to use the names &lt;code&gt;iceberg_list_tables&lt;/code&gt; returns. The server passes them back unchanged, so &lt;code&gt;dbo&lt;/code&gt; and upper-case names need no special handling.&lt;/p&gt;




&lt;h4&gt;
  
  
  🔎 Tip: Horizon Hands Out Storage Credentials Unasked
&lt;/h4&gt;

&lt;p&gt;Loading a Horizon table leaves S3 credentials on the client's file reader, although the catalog entry asks for none:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;snowflake-horizon
  client.region
  py-io-impl
  s3.access-key-id
  s3.secret-access-key
  s3.session-token
  s3.session-token-expires-at-ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Horizon's catalog entry supplies no storage login, so its scan reads files with that credential. It also means a Horizon table load carries a live storage credential, so logs of that response need redacting.&lt;/p&gt;




&lt;h4&gt;
  
  
  Step 8 — Use It From an MCP Client
&lt;/h4&gt;

&lt;p&gt;Any MCP client that starts stdio servers can run it. For Claude Code, a project &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"iceberg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iceberg-mcp-hosts/servers/iceberg_mcp.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ICEBERG_CATALOG"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws-glue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ICEBERG_CATALOGS_FILE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iceberg-conformance/catalogs.yaml"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To reach a different catalog, change &lt;code&gt;ICEBERG_CATALOG&lt;/code&gt;. The measurements above call the server directly and do not use this file.&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;catalog&lt;/th&gt;
&lt;th&gt;four tools&lt;/th&gt;
&lt;th&gt;extra package&lt;/th&gt;
&lt;th&gt;login on the machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Polaris&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;OAuth2 client secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google BigLake&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcloud&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft OneLake&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adlfs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;az&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;botocore[crt]&lt;/code&gt; with &lt;code&gt;aws login&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS S3 Tables&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3fs&lt;/code&gt;, &lt;code&gt;botocore[crt]&lt;/code&gt; with &lt;code&gt;aws login&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Horizon&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;key pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks Unity&lt;/td&gt;
&lt;td&gt;not run&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;The goal of this article was to point one Iceberg MCP server at seven catalogs and record what each one takes. The key to the solution was keeping the server code fixed, changing only the catalog entry, and calling the tools directly so that no model sits between the catalog and the result. The results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 All four tools work on all six catalogs run: Polaris, BigLake, OneLake, Glue, S3 Tables and Horizon&lt;/li&gt;
&lt;li&gt;🟢 The only change between catalogs is one environment variable and one entry in &lt;code&gt;catalogs.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;⚠️ Three packages outside &lt;code&gt;pyiceberg&lt;/code&gt;'s defaults are needed: &lt;code&gt;adlfs&lt;/code&gt; for OneLake, &lt;code&gt;s3fs&lt;/code&gt; for S3 Tables, and &lt;code&gt;botocore[crt]&lt;/code&gt; for an &lt;code&gt;aws login&lt;/code&gt; session; without them the metadata tools work and the scan fails&lt;/li&gt;
&lt;li&gt;⚠️ &lt;code&gt;DefaultAzureCredential&lt;/code&gt; took 553.1 seconds off Azure; &lt;code&gt;AzureCliCredential&lt;/code&gt; took 0.6&lt;/li&gt;
&lt;li&gt;⚠️ Horizon returns S3 credentials with a table load, unasked&lt;/li&gt;
&lt;li&gt;❌ Databricks Unity was not run, because its trial account has ended&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: &lt;code&gt;iceberg_mcp.py&lt;/code&gt; 1.0.0 on &lt;code&gt;pyiceberg&lt;/code&gt; 0.12.0, &lt;code&gt;pyarrow&lt;/code&gt; 25.0.1, &lt;code&gt;s3fs&lt;/code&gt; 2026.9.0, &lt;code&gt;adlfs&lt;/code&gt; 2026.8.0, &lt;code&gt;botocore&lt;/code&gt; 1.43.75 and &lt;code&gt;azure-identity&lt;/code&gt; 1.25.3, on Python 3.14.7. Apache Polaris 1.7.0 in Docker with local file storage; the managed catalogs over the internet from one machine, AWS in &lt;code&gt;us-east-1&lt;/code&gt;. One run per catalog on 2026-09-18 and 2026-09-19 (UTC); Unity not run. Reads only. The tools are checked for answering without error; the tables differ between catalogs, so values are reported and not compared. Managed catalogs do not report a version.&lt;/p&gt;

&lt;p&gt;The strategy for pointing one Iceberg MCP server at seven catalogs 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/lakehouse-iceberg-2026" rel="noopener noreferrer"&gt;lakehouse-iceberg-2026 | GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification" rel="noopener noreferrer"&gt;Model Context Protocol specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/iceberg-python" rel="noopener noreferrer"&gt;apache/iceberg-python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://polaris.apache.org/" rel="noopener noreferrer"&gt;Apache Polaris&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/seven-iceberg-rest-catalogs-what-they-declare-and-what-they-serve-40oj"&gt;Seven Iceberg REST Catalogs: What They Declare, and What They Serve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/four-iceberg-tools-three-agent-frameworks-what-ports-and-what-doesnt-a5g"&gt;Four Iceberg Tools, Three Agent Frameworks: What Ports, and What Doesn't&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>iceberg</category>
      <category>python</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Dart Enhanced Enums Are Secretly Factories: Unlocking Constructor Tearoffs</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:03:10 +0000</pubDate>
      <link>https://dev.to/gde/dart-enhanced-enums-are-secretly-factories-unlocking-constructor-tearoffs-54n9</link>
      <guid>https://dev.to/gde/dart-enhanced-enums-are-secretly-factories-unlocking-constructor-tearoffs-54n9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 2 of the &lt;strong&gt;Dart and Flutter&lt;/strong&gt; series—practical guides, architectural deep dives, and hard-earned engineering lessons from the field. Each article is completely standalone.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;How many times have you written (or reviewed) a piece of code that looks like this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;NotificationWidget&lt;/span&gt; &lt;span class="nf"&gt;buildNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NotificationData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;NotificationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EmailNotificationWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;NotificationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SmsNotificationWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;NotificationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PushNotificationWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or worse, a dedicated &lt;code&gt;NotificationWidgetFactory&lt;/code&gt; class containing a 40-line &lt;code&gt;switch&lt;/code&gt; statement or a mutable &lt;code&gt;Map&amp;lt;NotificationType, Function&amp;gt;&lt;/code&gt; registry.&lt;/p&gt;

&lt;p&gt;It feels routine. It’s what we were taught in classic OOP textbooks. But it introduces subtle friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The enum &lt;code&gt;NotificationType&lt;/code&gt; knows nothing about the widgets it represents.&lt;/li&gt;
&lt;li&gt;The factory switch ladder must be updated every time a new case is added.&lt;/li&gt;
&lt;li&gt;The creation logic is split across multiple files and layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What if your enum wasn't just a list of identifiers, but &lt;strong&gt;was itself the polymorphic factory&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;By marrying two features of modern Dart—&lt;strong&gt;Enhanced Enums&lt;/strong&gt; and &lt;strong&gt;Constructor Tearoffs&lt;/strong&gt;—you can delete the switch ladders and turn your enum values into self-instantiating factories in under 15 lines of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Ingredients: A Brief History
&lt;/h2&gt;

&lt;p&gt;To understand how clean this pattern is, we have to appreciate two language features that quietly revolutionized Dart over the last couple of years:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Constructor Tearoffs (Dart 2.15+)
&lt;/h3&gt;

&lt;p&gt;Before Dart 2.15, if you wanted to pass a constructor as a first-class function, you had to wrap it in an awkward lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The old, clunky way:&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;builders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmailNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dart 2.15 introduced &lt;strong&gt;Constructor Tearoffs&lt;/strong&gt;. Constructors became first-class closures. You can reference default constructors using &lt;code&gt;.new&lt;/code&gt;, or named constructors directly by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The modern Dart way:&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;builders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EmailNotification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;parsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Enhanced Enums (Dart 2.17+)
&lt;/h3&gt;

&lt;p&gt;Before Dart 2.17, Dart enums were glorified integers. They had an &lt;code&gt;index&lt;/code&gt; and a &lt;code&gt;name&lt;/code&gt;, and virtually nothing else.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Enhanced Enums&lt;/strong&gt;, enums gained full class powers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They can declare &lt;code&gt;final&lt;/code&gt; fields.&lt;/li&gt;
&lt;li&gt;They can have &lt;code&gt;const&lt;/code&gt; constructors.&lt;/li&gt;
&lt;li&gt;They can implement interfaces and mixins.&lt;/li&gt;
&lt;li&gt;They can define methods, getters, and operator overloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you put constructor tearoffs &lt;em&gt;inside&lt;/em&gt; enhanced enums, something magical happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fusion: Enums as Self-Instantiating Factories
&lt;/h2&gt;

&lt;p&gt;Let’s model a common domain scenario: a document rendering engine. We have different document types that share a common interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfDocument&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Rendering PDF: &lt;/span&gt;&lt;span class="si"&gt;$title&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MarkdownDocument&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;MarkdownDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Rendering Markdown: &lt;/span&gt;&lt;span class="si"&gt;$title&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HtmlDocument&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;HtmlDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Rendering HTML: &lt;/span&gt;&lt;span class="si"&gt;$title&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of writing an external &lt;code&gt;DocumentFactory&lt;/code&gt; or a &lt;code&gt;switch&lt;/code&gt; statement, we declare an Enhanced Enum where &lt;strong&gt;each enum member holds a tearoff reference to its class constructor&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DocumentType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PdfDocument&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MarkdownDocument&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HtmlDocument&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// A field holding a function that creates a Document given a String title&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DocumentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely at &lt;code&gt;DocumentType&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Document Function(String title) create&lt;/code&gt;: A strongly typed function signature stored as a &lt;code&gt;final&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pdf(PdfDocument.new)&lt;/code&gt;: We pass the constructor tearoff directly to the enum value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;const DocumentType(this.create)&lt;/code&gt;: The constructor is &lt;code&gt;const&lt;/code&gt;, so the entire enum remains compile-time constant!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How You Use It
&lt;/h3&gt;

&lt;p&gt;Instantiating a polymorphic object is now as simple as calling the field on the enum instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;selectedType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DocumentType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Polymorphic instantiation with ZERO switch statements:&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectedType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Architecture_Notes.md'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Output: Rendering Markdown: Architecture_Notes.md&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No switch statement. No map lookups. No reflection. If you add a new enum value (e.g. &lt;code&gt;epub&lt;/code&gt;), the compiler &lt;em&gt;forces&lt;/em&gt; you to supply a matching constructor tearoff right there. You cannot accidentally forget to handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Example: Polymorphic API Payload Parsers
&lt;/h2&gt;

&lt;p&gt;This pattern shines when deserializing polymorphic JSON payloads (like webhooks, analytics events, or push notifications).&lt;/p&gt;

&lt;p&gt;Imagine an incoming stream of server events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1711000000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have distinct payload models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EventPayload&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginPayload&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;EventPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;LoginPayload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'userId'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PurchasePayload&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;EventPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;PurchasePayload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toDouble&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a bulky JSON parsing switch, our enum maps incoming strings directly to the named constructor tearoff (&lt;code&gt;.fromJson&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoginPayload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PurchasePayload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;EventPayload&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="n"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstOrNull&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, your dispatcher parses any incoming event in two clean lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;EventPayload&lt;/span&gt; &lt;span class="nf"&gt;parseEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;typeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typeName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; 
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;UnsupportedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Unknown event: &lt;/span&gt;&lt;span class="si"&gt;$typeName&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Real-World Example: Flutter Widget Builders
&lt;/h2&gt;

&lt;p&gt;In Flutter applications, you frequently have a selection control (tabs, filters, or segmented buttons) that drives which widget to render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DashboardView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AnalyticsView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ActivityView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SettingsView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DashboardView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your widget tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;DashboardView&lt;/span&gt; &lt;span class="n"&gt;currentView&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DashboardScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentView&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;currentView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you add a 4th tab tomorrow, you don't hunt through widget tree switch statements—you simply declare it on the enum.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Senior Perspective: When NOT to Use This Pattern
&lt;/h2&gt;

&lt;p&gt;Every pattern has architectural boundaries. While enum constructor tearoffs are powerful, here are two caveats to keep in mind:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Beware of Layer Inversion (Separation of Concerns)
&lt;/h3&gt;

&lt;p&gt;If your enum lives in your pure Dart &lt;strong&gt;Domain Layer&lt;/strong&gt; (core business logic), do &lt;strong&gt;not&lt;/strong&gt; attach Flutter widget constructor tearoffs to it. Doing so couples your domain models to &lt;code&gt;package:flutter&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good&lt;/strong&gt;: An enum in the presentation layer mapping UI modes to widget constructors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good&lt;/strong&gt;: An enum in the data layer mapping API event types to DTO constructors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad&lt;/strong&gt;: A domain entity enum importing &lt;code&gt;flutter/material.dart&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. When to Prefer Dart 3 Sealed Classes &amp;amp; Pattern Matching
&lt;/h3&gt;

&lt;p&gt;Dart 3 introduced &lt;code&gt;sealed&lt;/code&gt; class hierarchies and exhaustive &lt;code&gt;switch&lt;/code&gt; expressions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Alternative: Dart 3 pattern matching&lt;/span&gt;
&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;buildView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DashboardView&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;DashboardView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AnalyticsView&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;DashboardView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ActivityView&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;DashboardView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SettingsView&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Which should you choose?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Enum Constructor Tearoffs&lt;/strong&gt; when the creation parameters are identical, the association between the enum and the class is 1:1, and you want self-contained encapsulation with zero boilerplate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Sealed Classes &amp;amp; Pattern Matching&lt;/strong&gt; when each subclass takes radically different parameters, when cases have unique construction logic, or when you want to avoid coupling the enum to the concrete implementations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Enhanced Enums and Constructor Tearoffs are two of modern Dart's finest language ergonomics. When combined, they eliminate entire classes of boilerplate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Self-documenting&lt;/strong&gt;: The enum member explicitly declares the constructor that builds it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time safe&lt;/strong&gt;: Missing a constructor is impossible; the compiler will not let you compile an enum member without satisfying the signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero switch statements&lt;/strong&gt;: Replaces sprawling factory classes with a clean, single-line invocation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next time you catch yourself writing a 30-line &lt;code&gt;switch&lt;/code&gt; statement just to instantiate a class from an enum value, pause. Let the enum do the work.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's your take?
&lt;/h3&gt;

&lt;p&gt;Have you started using constructor tearoffs in your enums, or do you prefer Dart 3 &lt;code&gt;switch&lt;/code&gt; expressions? Let me know in the comments below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Randal L. Schwartz is a Google Developer Expert (GDE) for Dart &amp;amp; Flutter and veteran software architect.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📺 Watch deep dives and walkthroughs on YouTube: &lt;strong&gt;&lt;a href="https://www.youtube.com/@RandalOnDartAndFlutter" rel="noopener noreferrer"&gt;@RandalOnDartAndFlutter&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💻 Connect on GitHub: &lt;strong&gt;&lt;a href="https://github.com/RandalSchwartz" rel="noopener noreferrer"&gt;@RandalSchwartz&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Paying the build_runner Tax: Why I Refuse to Use Mockito in Modern Dart</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Sun, 20 Sep 2026 19:48:39 +0000</pubDate>
      <link>https://dev.to/gde/stop-paying-the-buildrunner-tax-why-i-refuse-to-use-mockito-in-modern-dart-4cif</link>
      <guid>https://dev.to/gde/stop-paying-the-buildrunner-tax-why-i-refuse-to-use-mockito-in-modern-dart-4cif</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 1 of the &lt;strong&gt;Dart and Flutter&lt;/strong&gt; series—practical guides, architectural deep dives, and hard-earned engineering lessons from the field. Each article is completely standalone.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Picture this familiar scene:&lt;/p&gt;

&lt;p&gt;You’re deep in the zone. You’re practicing disciplined Test-Driven Development (TDD). You write a failing test, write the minimal code to satisfy it, and refactor. Red, green, refactor. Your fingers are flying.&lt;/p&gt;

&lt;p&gt;Then, you add a single new method to your &lt;code&gt;UserRepository&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;updatePreferences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserPreferences&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You switch back to your test file to stub it, and everything grinds to a dead stop. Your IDE lights up with red squiggly lines because the generated mock doesn't know about &lt;code&gt;updatePreferences&lt;/code&gt; yet. &lt;/p&gt;

&lt;p&gt;You sigh, drop into the terminal, and type the dreaded incantation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart run build_runner build &lt;span class="nt"&gt;--delete-conflicting-outputs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you wait. 15 seconds. 30 seconds. On a large enterprise Flutter codebase, maybe two full minutes. The fan spins up. Your train of thought derails. By the time the builder finishes generating boilerplate Dart files, you've lost your flow state.&lt;/p&gt;

&lt;p&gt;Why on earth are we running heavyweight compile-time code generation just to create a mock object for a unit test?&lt;/p&gt;

&lt;p&gt;It’s time to stop using &lt;code&gt;mockito&lt;/code&gt; in modern Dart. There is a much better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Did We End Up Here?
&lt;/h2&gt;

&lt;p&gt;To be fair to the &lt;code&gt;mockito&lt;/code&gt; maintainers, &lt;code&gt;mockito&lt;/code&gt; didn’t set out to be slow. &lt;/p&gt;

&lt;p&gt;In the pre-null-safety era of Dart, &lt;code&gt;mockito&lt;/code&gt; was the undisputed king. You created a mock class with &lt;code&gt;class MockRepo extends Mock implements Repo {}&lt;/code&gt;, and Dart’s runtime dynamic invocation (&lt;code&gt;noSuchMethod&lt;/code&gt;) handled everything under the hood with zero code generation. It was snappy and painless.&lt;/p&gt;

&lt;p&gt;Then came &lt;strong&gt;Sound Null Safety&lt;/strong&gt; in Dart 2.12.&lt;/p&gt;

&lt;p&gt;Null safety is one of the best things that ever happened to Dart, but it broke &lt;code&gt;mockito&lt;/code&gt;'s runtime magic. In a soundly typed null-safe language, if an interface method declares that it returns a non-nullable &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;noSuchMethod&lt;/code&gt; cannot simply return &lt;code&gt;null&lt;/code&gt; while waiting for your &lt;code&gt;when(...)&lt;/code&gt; stub to register. Returning &lt;code&gt;null&lt;/code&gt; violates the type system and throws an immediate runtime error before your stubbing logic even runs.&lt;/p&gt;

&lt;p&gt;Because Dart has deliberately avoided heavy runtime reflection (&lt;code&gt;dart:mirrors&lt;/code&gt; is disabled in Flutter for performance and tree-shaking reasons), &lt;code&gt;mockito&lt;/code&gt; chose the only route available to it at the time: &lt;strong&gt;compile-time code generation via &lt;code&gt;build_runner&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It solved the type-safety problem, but it introduced the dreaded &lt;strong&gt;&lt;code&gt;build_runner&lt;/code&gt; tax&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Sins of Modern Mockito
&lt;/h2&gt;

&lt;p&gt;Relying on &lt;code&gt;mockito&lt;/code&gt; today saddles your codebase with three distinct engineering bottlenecks:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Iteration Tax (Destroying the TDD Flow)
&lt;/h3&gt;

&lt;p&gt;TDD relies on tight feedback loops measured in milliseconds. When every interface change or mock signature update requires running code generation, the feedback loop stretches into tens of seconds. Developers stop running tests frequently. They batch their changes, commit without verifying, and run tests as an afterthought. Velocity plummets.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Git Diff Pollution &amp;amp; Merge Conflict Hell
&lt;/h3&gt;

&lt;p&gt;For every test file with mocks, &lt;code&gt;mockito&lt;/code&gt; generates a &lt;code&gt;.mocks.dart&lt;/code&gt; sibling file. These files are thousands of lines of machine-generated boilerplate. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They pollute pull request reviews.&lt;/li&gt;
&lt;li&gt;They clutter repository search results.&lt;/li&gt;
&lt;li&gt;When two developers touch overlapping service interfaces on separate branches, merging their generated &lt;code&gt;.mocks.dart&lt;/code&gt; files triggers brutal, unreadable merge conflicts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Brittle Annotation Coupling
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;mockito&lt;/code&gt;, your tests are coupled to global library annotations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/user_service_test.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'user_service_test.mocks.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@GenerateMocks&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to mock a third dependency? You have to edit the annotation list, rerun &lt;code&gt;build_runner&lt;/code&gt;, wait for the file system to update, and import the generated symbols. It's clunky and mechanical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter Mocktail: Zero-Config Runtime Mocking
&lt;/h2&gt;

&lt;p&gt;Enter &lt;strong&gt;&lt;a href="https://pub.dev/packages/mocktail" rel="noopener noreferrer"&gt;mocktail&lt;/a&gt;&lt;/strong&gt;, created by Felix Angelov (of Bloc and Very Good Ventures fame).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mocktail&lt;/code&gt; delivers the exact same intuitive API you know from &lt;code&gt;mockito&lt;/code&gt;, but with &lt;strong&gt;zero code generation&lt;/strong&gt;. No &lt;code&gt;build_runner&lt;/code&gt;. No &lt;code&gt;.mocks.dart&lt;/code&gt; files. No annotations.&lt;/p&gt;

&lt;p&gt;How? By leveraging two native features of Dart:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dart’s implicit interfaces&lt;/strong&gt;: In Dart, every class implicitly defines an interface. You can implement any class without inheriting its implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closures for stubbing&lt;/strong&gt;: Instead of evaluating the mock invocation directly (which triggers the null-safety problem), &lt;code&gt;mocktail&lt;/code&gt; wraps the invocation inside a closure: &lt;code&gt;() =&amp;gt; mock.getUser()&lt;/code&gt;. This defers execution until &lt;code&gt;mocktail&lt;/code&gt; can safely intercept and return the registered stub or a fallback value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how you declare a mock in &lt;code&gt;mocktail&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:mocktail/mocktail.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// No annotations. No build_runner. Just pure, clean Dart.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MockUserRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MockHttpClient&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Mock&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. You define the class right in your test file (or in a shared &lt;code&gt;test_helpers.dart&lt;/code&gt;), hit save, and run your tests instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-Side: Mockito vs. Mocktail
&lt;/h2&gt;

&lt;p&gt;The migration from &lt;code&gt;mockito&lt;/code&gt; to &lt;code&gt;mocktail&lt;/code&gt; requires practically zero mental re-learning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;mockito&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;mocktail&lt;/code&gt; 🍹&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires &lt;code&gt;build_runner&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None (Pure runtime)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Declaration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@GenerateMocks([UserRepo])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;class MockUserRepo extends Mock implements UserRepo {}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Synchronous Stub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;when(repo.name).thenReturn('Randal')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;when(() =&amp;gt; repo.name).thenReturn('Randal')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Asynchronous Stub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;when(repo.fetch()).thenAnswer((_) async =&amp;gt; user)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;when(() =&amp;gt; repo.fetch()).thenAnswer((_) async =&amp;gt; user)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Argument Matchers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anyNamed('id')&lt;/code&gt;, &lt;code&gt;anyString&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;any(named: 'id')&lt;/code&gt;, &lt;code&gt;any()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;verify(repo.login()).called(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;verify(() =&amp;gt; repo.login()).called(1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the only primary syntax difference: &lt;code&gt;mocktail&lt;/code&gt; passes the invocation as an anonymous function &lt;code&gt;() =&amp;gt; mock.method()&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;That simple closure is the secret sauce that bypasses the need for compile-time code generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One "Gotcha": Custom Type Fallbacks
&lt;/h2&gt;

&lt;p&gt;To be an objective engineer, you must evaluate the trade-offs. &lt;code&gt;mocktail&lt;/code&gt; has exactly one gotcha, and once you understand it, it takes 5 seconds to address.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;When you use flexible argument matchers like &lt;code&gt;any()&lt;/code&gt; on a method that accepts a &lt;strong&gt;non-nullable custom type&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mockRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dart’s sound type system insists that &lt;em&gt;something&lt;/em&gt; matching the type &lt;code&gt;User&lt;/code&gt; must be passed into &lt;code&gt;saveUser&lt;/code&gt; during internal matcher registration. Under the hood, &lt;code&gt;mocktail&lt;/code&gt; cannot synthesize an instance of your custom &lt;code&gt;User&lt;/code&gt; class out of thin air. If it passed &lt;code&gt;null&lt;/code&gt;, Dart would throw a type error.&lt;/p&gt;

&lt;p&gt;If you don't tell &lt;code&gt;mocktail&lt;/code&gt; how to satisfy that type, it will throw an informative exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bad state: A test tried to use any() or captureAny() on a User which was not registered.
Register a fallback value using `registerFallbackValue`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;You define a dummy &lt;code&gt;Fake&lt;/code&gt; and register it in &lt;code&gt;setUpAll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_test/flutter_test.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:mocktail/mocktail.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Create a Fake implementation that satisfies the type checker&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeUser&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Fake&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;setUpAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Register it once before any tests run&lt;/span&gt;
    &lt;span class="n"&gt;registerFallbackValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FakeUser&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'saves user profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MockUserRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="s"&gt;'123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'Randal'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;called&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all there is to it. A two-line &lt;code&gt;Fake&lt;/code&gt; registration in &lt;code&gt;setUpAll&lt;/code&gt; completely eliminates the need to run &lt;code&gt;build_runner&lt;/code&gt; on your entire project.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Migrate Without Boiling the Ocean
&lt;/h2&gt;

&lt;p&gt;If you’re staring at an existing codebase with dozens of &lt;code&gt;build_runner&lt;/code&gt;-powered &lt;code&gt;.mocks.dart&lt;/code&gt; files, you don't need to rewrite everything in a single frantic weekend PR. &lt;/p&gt;

&lt;p&gt;Take an incremental approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;mocktail&lt;/code&gt; to &lt;code&gt;dev_dependencies&lt;/code&gt;&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;dev_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;mocktail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Migrate one test file at a time&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Whenever you open an existing test file to add or modify a test, migrate &lt;em&gt;just that file&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Delete the &lt;code&gt;.mocks.dart&lt;/code&gt; import.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;@GenerateMocks&lt;/code&gt; with simple &lt;code&gt;class MockX extends Mock implements X {}&lt;/code&gt; declarations.&lt;/li&gt;
&lt;li&gt;Wrap your &lt;code&gt;when&lt;/code&gt; and &lt;code&gt;verify&lt;/code&gt; invocations in &lt;code&gt;() =&amp;gt; ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delete the obsolete &lt;code&gt;.mocks.dart&lt;/code&gt; file from disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit the diff&lt;/strong&gt;: Notice how clean and readable your git diff is without hundreds of lines of autogenerated noise.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;The official Flutter documentation and cookbooks still mention &lt;code&gt;mockito&lt;/code&gt; largely due to historical inertia. But we no longer live in 2020. &lt;/p&gt;

&lt;p&gt;In modern Dart, testing should be fast, expressive, and friction-free. Waiting on &lt;code&gt;build_runner&lt;/code&gt; just to verify that a service method was called with the right argument is a waste of your machine's CPU cycles and your mental bandwidth.&lt;/p&gt;

&lt;p&gt;Switch to &lt;code&gt;mocktail&lt;/code&gt;. Your TDD cycle will thank you.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's your take?
&lt;/h3&gt;

&lt;p&gt;Are you still running &lt;code&gt;build_runner&lt;/code&gt; for your mocks, or have you already made the switch to &lt;code&gt;mocktail&lt;/code&gt; (or hand-rolled fakes)? Let me know in the comments below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Randal L. Schwartz is a Google Developer Expert (GDE) for Dart &amp;amp; Flutter and veteran software architect.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📺 Watch deep dives and walkthroughs on YouTube: &lt;strong&gt;&lt;a href="https://www.youtube.com/@RandalOnDartAndFlutter" rel="noopener noreferrer"&gt;@RandalOnDartAndFlutter&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💻 Connect on GitHub: &lt;strong&gt;&lt;a href="https://github.com/RandalSchwartz" rel="noopener noreferrer"&gt;@RandalSchwartz&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
