<?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: Jarek</title>
    <description>The latest articles on DEV Community by Jarek (@shadbb).</description>
    <link>https://dev.to/shadbb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4016320%2Faf9d5c6b-808b-4190-9ea6-f3e836e36bce.png</url>
      <title>DEV Community: Jarek</title>
      <link>https://dev.to/shadbb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shadbb"/>
    <language>en</language>
    <item>
      <title>Deterministic where you can, model where you must, human for taste</title>
      <dc:creator>Jarek</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:45:00 +0000</pubDate>
      <link>https://dev.to/shadbb/deterministic-where-you-can-model-where-you-must-human-for-taste-4l7c</link>
      <guid>https://dev.to/shadbb/deterministic-where-you-can-model-where-you-must-human-for-taste-4l7c</guid>
      <description>&lt;p&gt;Last time I wrote about &lt;a href="https://czub.info/en/2026/why-i-still-score-my-own-benchmark-even-though-two-llm-judges-do-it-for-me/" rel="noopener noreferrer"&gt;the judge that scores my benchmark&lt;/a&gt;, I left one thing out. That post was about adding a model to the scoring. It was not about the fact that the model is the &lt;strong&gt;last&lt;/strong&gt; thing I reach for in this machine.&lt;/p&gt;

&lt;p&gt;Evaluating an AI coding model is not one number. It is several different questions: did it meet the spec, does the thing actually run, does it look good, is the code clean, and did the agent behave sensibly. Each of those questions has a different natural judge. So in Refio I split scoring into three layers: a &lt;strong&gt;deterministic judge&lt;/strong&gt; (measures what a machine can measure), &lt;strong&gt;strong LLM judges&lt;/strong&gt; (read the code and score what a machine cannot weigh), and a &lt;strong&gt;human&lt;/strong&gt; (visual taste and the final call). The whole thing runs on the same agent engine the production plugin uses, just headless. The result: model comparisons you can actually trust, because they are multi-dimensional, repeatable, and reflect how the tool really behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Works on my machine" does not scale to 480 runs
&lt;/h2&gt;

&lt;p&gt;Anyone who has plugged a new model into their agent knows the moment. You fire off a prompt, the model spits out an app, you open it in a browser and say "huh, not bad" or "nope, garbage". You try again and get something different. You plug in the next model, and an hour later all you have is a vague "the first one was probably better". That is not evaluation. That is guessing in good faith.&lt;/p&gt;

&lt;p&gt;Leaderboards do not save you, because they measure something else: how a model handles closed, self-contained puzzles. They do not tell you whether &lt;strong&gt;your&lt;/strong&gt; agent, with your tools and your context budget, will get something out of it that opens and does not flood the console with errors. I have that problem in a sharp form while building the plugin, because Refio only makes sense if I can honestly say: this local model, free, on your GPU, is good enough for this kind of task, and that one is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one number lies
&lt;/h2&gt;

&lt;p&gt;Take a real task from my catalogue: a simple 3D editor in the browser, single HTML file - scene view, adding and moving objects, a properties panel, zero console errors. How do you score that with one digit? You cannot, because several independent questions live inside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;spec compliance&lt;/strong&gt; - can you actually add an object, is there a properties panel, does the file name match,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;works out of the box&lt;/strong&gt; - does it open without manual fixing, does it throw errors,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;look&lt;/strong&gt; - this is taste, not measurement,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;code quality and structure&lt;/strong&gt; - clean, or a tangle with dead functions,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;logic correctness&lt;/strong&gt; - does it hold up when you &lt;strong&gt;read&lt;/strong&gt; the code. A screen can lie beautifully,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;agent logic&lt;/strong&gt; - did the agent check files, generate, verify, summarise. Or did it wander.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model can crush one dimension and faceplant in another - a great look on top of broken logic is a daily occurrence here. On top of that comes &lt;strong&gt;non-determinism&lt;/strong&gt;: the same prompt twice gives you two different files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle this stands on
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not judge everything with one tool.&lt;/strong&gt; Do not call a model where a plain function will do, and do not pretend a regex can weigh taste.&lt;/p&gt;

&lt;p&gt;Look at the extremes. "Does this page render?" has a binary, machine-checkable answer; asking a model "do you think it renders?" injects noise where you could have had certainty. Just render it. In the other direction, "does this look good?" is an aesthetic judgement by definition - a regex will not weigh it, and even a strong model does it worse than a human with taste.&lt;/p&gt;

&lt;p&gt;Hence three layers.&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%2F0jfjqqi3gupd2dfp61wm.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%2F0jfjqqi3gupd2dfp61wm.png" alt="benchmark.refio" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: the judge with no opinions
&lt;/h2&gt;

&lt;p&gt;The first layer has nothing to do with AI, and an important caveat up front: &lt;strong&gt;it is a hint, not a result&lt;/strong&gt;. Those scores sit next to an entry while I am looking at it, and that is where their role ends - they never make it into the final results. It measures three things you can compute objectively from an e2e run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance through needles.&lt;/strong&gt; For every task I define a set of needles - content markers that must appear in the delivered file. All of them hit is 1, some is 0.5, none is 0. A needle is either plain text or a regex, so the rule is hard and repeatable. This is where people usually ask "but there are no unit tests here, how do you measure anything?" - exactly like this. When the artifact is an HTML file rather than a function with a signature, you cannot write classic tests, but you can say what &lt;strong&gt;must&lt;/strong&gt; be in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works out of the box through an actual render.&lt;/strong&gt; The file goes into a headless browser. Renders clean - 1, renders with console errors - 0.5, does not render at all - 0. Zero interpretation. This layer grew directly out of what I wrote about in &lt;a href="https://czub.info/en/2026/code-that-kills-the-browser-observations-from-a-small-llm-benchmark/" rel="noopener noreferrer"&gt;the code that kills the browser&lt;/a&gt;: once you have seen an infinite draw loop lock up a tab, you stop trusting a screenshot as proof that something "works".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent logic through status and tool order.&lt;/strong&gt; The run has to finish with a SUCCESS status, and if a task requires a specific tool order, I check whether it is a &lt;strong&gt;subsequence&lt;/strong&gt; of the actual calls - they do not have to be adjacent, but the order has to hold. SUCCESS with the right order is 1, with the wrong one 0.5, no SUCCESS is 0.&lt;/p&gt;

&lt;p&gt;All of this is free, instant, and one hundred percent repeatable. Before I look at anything, I already know whether the page came up and whether the agent took the right path. And importantly: it &lt;strong&gt;deliberately does not touch&lt;/strong&gt; look or code quality, because those cannot be measured mechanically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: models, but only now
&lt;/h2&gt;

&lt;p&gt;The second layer takes what a machine cannot weigh but a careful read of the code can: code structure and logic correctness. This is where judgement needs intelligence, so this is where I finally call a model (through the Claude Code and Codex agents). I described the construction &lt;a href="https://czub.info/en/2026/why-i-still-score-my-own-benchmark-even-though-two-llm-judges-do-it-for-me/" rel="noopener noreferrer"&gt;in the previous post&lt;/a&gt; - an evidence folder instead of "rate this 1 to 10", screenshots at three points in time, clicking through controls, blind scoring. Two things follow directly from the layering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I aggregate with the median, not the mean.&lt;/strong&gt; The median shrugs off one judge who went off the rails; the mean politely folds that excursion into the result. The known sins of model judges (verbosity bias, position sensitivity, inconsistency across runs) do not disappear, but they stop deciding once you have two of them and take the middle. Which is why the judge here is an advisor, not a final authority - it tells me where to look, and the ranking is still signed by me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM judges do not score agent logic.&lt;/strong&gt; Not because they cannot - because how the agent worked live cannot be reconstructed from a dead file. That is a question about process, not product. A good evaluation system knows what its judges &lt;strong&gt;should not&lt;/strong&gt; touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: the human, trimmed to the minimum
&lt;/h2&gt;

&lt;p&gt;The last mile is mine, and I do exactly one thing in it that cannot be automated: I add &lt;strong&gt;look&lt;/strong&gt; (the only criterion on a 0-2 scale, because that is where "better than I expected" belongs) and make the final call.&lt;/p&gt;

&lt;p&gt;An automated step assembles a ready entry in a review queue - artifact, screenshots, duration, tokens, cost, deterministic hints, judge verdicts and a tentative pass/fail. I walk in, look, add my part, and either promote the entry into the results or reject it. &lt;strong&gt;Nothing that could be computed gets retyped by hand.&lt;/strong&gt; On top of that there is a divergence marker: it shows the largest gap between my score and the judges' aggregate. It works both ways - either the judge got it wrong, or I missed something it caught while reading the code.&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%2Fk4t3ptsi3g0qj18861z5.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%2Fk4t3ptsi3g0qj18861z5.png" alt="benchmark.refio" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeatability, and where the tasks come from
&lt;/h2&gt;

&lt;p&gt;Since models are non-deterministic, one good result can be luck, so I measure &lt;strong&gt;stability&lt;/strong&gt; separately. For me that is six runs per model per task - fewer than three and you cannot tell luck from form, more than six and it stops paying off across a dozen-plus models. For any group (task, model, environment) with at least two runs I compute the variance of scores and the similarity of the generated code. Incidentally, that second measure turned into its own post - it turns out &lt;a href="https://czub.info/en/2026/every-llm-writes-the-same-todo-app-and-i-mean-that-literally/" rel="noopener noreferrer"&gt;every LLM writes the same Todo App&lt;/a&gt;, quite literally.&lt;/p&gt;

&lt;p&gt;The tasks themselves have a single source of truth: one description plus one prompt, from which a generator emits &lt;strong&gt;both&lt;/strong&gt; the automated e2e scenario and the review task in the admin panel. I learned this the hard way - keep a task description in two places and a month later you have two different tasks under one name. Difficulty is spread on purpose: from Snake and Todo, through the 3D editor, up to single-file monsters like a modular Web Audio synth, a logic circuit simulator or a procedural voxel world. That is where weaker models simply break, which is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same engine, just without the windows
&lt;/h2&gt;

&lt;p&gt;The most important part last. The benchmark does &lt;strong&gt;not&lt;/strong&gt; call the model through a clean API under laboratory conditions - it drives it through exactly the same agent engine, the same tools, the same context pipeline and the same guardrails the production IntelliJ plugin uses. Just headless.&lt;/p&gt;

&lt;p&gt;So I measure the model the way you will actually experience it in Refio: with its tool-use discipline and its behaviour inside the agent loop. That makes the benchmark useful twice over - it scores models, but it is also my own proving ground. When a guardrail kills a sensible run or a model falls into a loop, I see it here before anyone else does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Honesty.&lt;/strong&gt; A model cannot hide broken logic behind a pretty surface, because those are two criteria judged by two different judges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability.&lt;/strong&gt; The hints I start every entry with are deterministic, and stability is measured explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realism.&lt;/strong&gt; I measure the model in the same engine you use it in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap decisions.&lt;/strong&gt; I drop in a new local model and know by the end of the afternoon whether it is worth shipping.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole philosophy fits in one sentence: &lt;strong&gt;deterministic where you can, model where you must, and a human for taste.&lt;/strong&gt; Boringly simple, and it turns "the first one was probably better" into numbers I can trust.&lt;/p&gt;

&lt;p&gt;Results live here: &lt;a href="https://benchmark.refio.dev" rel="noopener noreferrer"&gt;benchmark.refio.dev&lt;/a&gt;. If you are building your own evaluation harness, I am curious where you drew the line between "I will compute this myself" and "let a model score it". My hunch is that most people jump straight to layer two and pay for it in noise.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>benchmarking</category>
    </item>
    <item>
      <title>Quantization for the impatient - why int4 doesn't speed up image generation on GPU</title>
      <dc:creator>Jarek</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/shadbb/quantization-for-the-impatient-why-int4-doesnt-speed-up-image-generation-on-gpu-51cp</link>
      <guid>https://dev.to/shadbb/quantization-for-the-impatient-why-int4-doesnt-speed-up-image-generation-on-gpu-51cp</guid>
      <description>&lt;p&gt;Recently I sat down with an old codebase and put together a quantization pipeline for diffusion models out of curiosity - SDXL, FLUX, SD3, some video. Just to see quantization in practice, since almost every "tutorial" online repeats the same thing: fewer bits = faster. Makes sense, right? Less to multiply, less work for the GPU, image pops out quicker.&lt;/p&gt;

&lt;p&gt;Except... no. And this isn't a minor nuance, it's a fundamental misunderstanding.&lt;/p&gt;

&lt;p&gt;I ran measurements on a laptop with RTX 4090 mobile (16 GB VRAM), Windows 11, CUDA 12.9, Python 3.14, diffusers 0.38, torch 2.5+. Real numbers, on a real card. Without that it's just &lt;strong&gt;"it runs faster on my machine"&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why quantize at all
&lt;/h2&gt;

&lt;p&gt;Let's start from basics, because this is the first place things get muddled. Model quantization has two goals - and almost never both at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fit the model in VRAM&lt;/strong&gt; - weights go from fp16 (2 bytes/parameter) to int8 (1 byte) or int4 (half a byte) or fp4 (on the new RTX 5000). A 12B parameter model drops from 24 GB to 6 GB. This is the main reason anyone does it at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sometimes speed up inference&lt;/strong&gt; - and here's where the minefield starts. Because "sometimes" means "only if your GPU has native tensor cores for that format". And most formats used by quantization libraries... don't have them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else in this article follows from point two. If you take only one thing from this post, make it this: &lt;strong&gt;quantize if you have to because of VRAM. Don't quantize for speed, because it'll almost always be slower.&lt;/strong&gt;&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%2Fb5yom2zfw2g0ozjdbd98.jpg" 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%2Fb5yom2zfw2g0ozjdbd98.jpg" alt="Full FP16" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Full FP16&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fewer bits ≠ faster
&lt;/h2&gt;

&lt;p&gt;The mechanics are simple once you see them. A GPU has physical, silicon-level MatMul instructions only for specific number formats. Everything else is software.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Native tensor cores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ampere (RTX 30xx, A100)&lt;/td&gt;
&lt;td&gt;fp16, bf16, tf32, int8, int4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ada Lovelace (RTX 40xx, incl. 4090 mobile)&lt;/td&gt;
&lt;td&gt;+ fp8 (E4M3/E5M2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blackwell (RTX 50xx, B200)&lt;/td&gt;
&lt;td&gt;+ fp4, mxfp8, mxfp4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything else - &lt;code&gt;nf4&lt;/code&gt;, &lt;code&gt;int2&lt;/code&gt;, &lt;code&gt;int1&lt;/code&gt;, &lt;code&gt;int3&lt;/code&gt;, &lt;code&gt;fp4&lt;/code&gt; on consumer Ada - &lt;strong&gt;has no native MatMul on any GPU available to mortals&lt;/strong&gt;. Which means when a quantization library "promises" you NF4, what actually happens is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Weights sit in VRAM packed (e.g. 8 NF4 weights squeezed into one int32). VRAM saved, works here.&lt;/li&gt;
&lt;li&gt;Right before the multiplication... we unpack them back to fp16. Custom CUDA kernel bitwise unpack.&lt;/li&gt;
&lt;li&gt;MatMul runs normally in fp16, on the same tensor cores as the fp16 baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So we save VRAM (weights are packed), but we add 10-30% overhead for dequantize on every step. The lower the bits, the more bitwise operations on unpack. More operations = slower.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;fp16 → int4&lt;/strong&gt; the savings are 4×, dequantize is relatively cheap - sometimes it roughly breaks even. For &lt;strong&gt;fp16 → int2 or int1&lt;/strong&gt; the savings are 8×-16×, but dequantize starts costing more than the multiplication itself. Result: on 1-bit weights, inference can be 3× slower than fp16.&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%2Flyi5znliy1ylb9ywqpm5.jpg" 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%2Flyi5znliy1ylb9ywqpm5.jpg" alt="quanto int8" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;quanto int8&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark table - so nobody has to take my word for it
&lt;/h2&gt;

&lt;p&gt;SDXL 1024×1024, 25 steps, RTX 4090 mobile (Ada, 16 GB VRAM), same base SDXL 1.0 weights:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Time / image&lt;/th&gt;
&lt;th&gt;vs fp16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fp16 / bf16 (baseline)&lt;/td&gt;
&lt;td&gt;9 s&lt;/td&gt;
&lt;td&gt;1.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bnb-int4&lt;/td&gt;
&lt;td&gt;9 s&lt;/td&gt;
&lt;td&gt;1.0× (int4 has tensor cores!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bnb-nf4&lt;/td&gt;
&lt;td&gt;13 s&lt;/td&gt;
&lt;td&gt;1.4×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;torchao-int8&lt;/td&gt;
&lt;td&gt;17 s&lt;/td&gt;
&lt;td&gt;1.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;torchao-fp8&lt;/td&gt;
&lt;td&gt;17 s&lt;/td&gt;
&lt;td&gt;1.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hqq-8bit&lt;/td&gt;
&lt;td&gt;13 s&lt;/td&gt;
&lt;td&gt;1.4×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hqq-4bit&lt;/td&gt;
&lt;td&gt;18 s&lt;/td&gt;
&lt;td&gt;2.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hqq-3bit&lt;/td&gt;
&lt;td&gt;52 s&lt;/td&gt;
&lt;td&gt;5.8× (!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hqq-2bit&lt;/td&gt;
&lt;td&gt;24 s&lt;/td&gt;
&lt;td&gt;2.7×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hqq-1bit&lt;/td&gt;
&lt;td&gt;26 s&lt;/td&gt;
&lt;td&gt;2.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bnb-int8&lt;/td&gt;
&lt;td&gt;32 s&lt;/td&gt;
&lt;td&gt;3.6×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the first two rows. &lt;code&gt;bnb-int4&lt;/code&gt; is exactly as fast as fp16. Because Ampere/Ada have int4 tensor cores - weights go through natively, no on-the-fly dequantize.&lt;/p&gt;

&lt;p&gt;Now look at &lt;code&gt;bnb-int8&lt;/code&gt; - 3.6× slower. Why? Because &lt;code&gt;bnb-int8&lt;/code&gt; isn't pure INT8 weight-only - it's mixed precision with outlier decomposition (Tim Dettmers' LLM.int8()). Some weights go in INT8, some in fp16, there are two separate multiplications, plus a bf16→fp16 cast between operations. It was designed for LLM stability, not speed. Works great for what it was built for, but for diffusion it's not a good "default" choice.&lt;/p&gt;

&lt;p&gt;HQQ-3bit - 52 seconds instead of 9. &lt;strong&gt;Six times slower&lt;/strong&gt; than baseline. So to "save VRAM" you trade away 5× more time on every image. If you have 1000 images to generate, that difference gets very real very fast.&lt;/p&gt;

&lt;p&gt;The conclusion from the table is brutal: &lt;strong&gt;don't look at the bit count, look at whether your GPU has native MatMul for that format&lt;/strong&gt;. That's the one and only heuristic that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five backends - what to use when
&lt;/h2&gt;

&lt;p&gt;The diffusers/transformers ecosystem has five main quantization libraries. Each with its own character. Quick overview, because each deserves its own post, but you need a map:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. bitsandbytes (&lt;code&gt;bnb&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Widest support, simplest path. Supports int8 and 4-bit (NF4, INT4, FP4-E2M1). OOTB in diffusers via &lt;code&gt;BitsAndBytesConfig&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bnb-int8&lt;/code&gt; - mixed precision with outlier decomposition. Stable, but &lt;strong&gt;slow&lt;/strong&gt; (see table).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bnb-nf4&lt;/code&gt; - format designed for LLMs (normal weight distribution). Stable, but no tensor cores → dequant overhead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bnb-int4&lt;/code&gt; - classic INT4 weight-only. On Ampere/Ada hits int4 tensor cores, &lt;strong&gt;free speed&lt;/strong&gt;.&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%2Fj4b3fdqoobo6h9wgjbtp.jpg" 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%2Fj4b3fdqoobo6h9wgjbtp.jpg" alt="pic_torchao_int8 -- torchao int8" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;torchao int8&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. torchao
&lt;/h3&gt;

&lt;p&gt;The official PyTorch path. Requires torchao≥0.7 + diffusers≥0.38. The package that will catch up to the rest of the ecosystem in 2026/2027 - but still a bit painful to use because the API keeps changing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;torchao-int8&lt;/code&gt; - &lt;code&gt;Int8WeightOnlyConfig&lt;/code&gt;. Stable everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torchao-int4&lt;/code&gt; - &lt;code&gt;Int4WeightOnlyConfig&lt;/code&gt;. Stable on the denoiser, &lt;strong&gt;unstable on the CLIP-G text encoder&lt;/strong&gt; (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torchao-fp8&lt;/code&gt; - &lt;code&gt;Float8WeightOnlyConfig&lt;/code&gt;. Unstable on text encoder. OK on UNet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torchao-fp4&lt;/code&gt; - &lt;code&gt;Float8DynamicActivationInt4WeightConfig&lt;/code&gt;. Requires Blackwell. Won't run on consumer Ada.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important gotcha: in diffusers 0.38, &lt;code&gt;TorchAoConfig(quant_type=...)&lt;/code&gt; requires an &lt;strong&gt;instance of &lt;code&gt;AOBaseConfig&lt;/code&gt;&lt;/strong&gt;, not a string. Older diffusers accepted &lt;code&gt;"int8wo"&lt;/code&gt;. If something suddenly breaks after &lt;code&gt;pip upgrade&lt;/code&gt; - this is probably why.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. quanto (optimum-quanto)
&lt;/h3&gt;

&lt;p&gt;HuggingFace's backend. Narrow precision support (&lt;code&gt;int2&lt;/code&gt;, &lt;code&gt;int4&lt;/code&gt;, &lt;code&gt;int8&lt;/code&gt;, &lt;code&gt;float8&lt;/code&gt;), but &lt;strong&gt;int2 is a unique feature&lt;/strong&gt; - you won't find it anywhere else. Just don't expect it to speed anything up :)&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%2Fk3izdowkvbc7oqcygc8r.jpg" 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%2Fk3izdowkvbc7oqcygc8r.jpg" alt="hqq 8bit" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;hqq 8bit&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. HQQ (Half-Quadratic Quantization)
&lt;/h3&gt;

&lt;p&gt;Calibration-free, arbitrary bit count. 1, 2, 3, 4, 8 bit. Replaces &lt;code&gt;nn.Linear&lt;/code&gt; with &lt;code&gt;HQQLinear&lt;/code&gt; at runtime.&lt;/p&gt;

&lt;p&gt;Upsides: no calibration dataset (unlike GPTQ/AWQ), free choice of bits, runs on any GPU. Downsides: Python &lt;code&gt;forward()&lt;/code&gt; path with no fused kernel (slow for low bits - see HQQ-3bit in the table), 1/2/3 bit produce visible artifacts on SDXL, 30+ second load times.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. GGUF (community)
&lt;/h3&gt;

&lt;p&gt;Format from llama.cpp, adapted by the community for FLUX/SD3 (e.g. &lt;code&gt;city96/FLUX.1-dev-gguf&lt;/code&gt; repo).&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%2Fv34l8677f5vjo46svttr.jpg" 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%2Fv34l8677f5vjo46svttr.jpg" alt="bnb int4" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;bnb int4&lt;/strong&gt;&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%2Fo76gxlowtj3ryrgzfow7.jpg" 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%2Fo76gxlowtj3ryrgzfow7.jpg" alt="hqq 4bit" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;hqq 4bit&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stability table: not everything quantizes equally well
&lt;/h2&gt;

&lt;p&gt;Here's where it gets actually interesting - because some components handle quantization fine, while others... fall apart completely. Empirical observations on SDXL:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;bnb-int8&lt;/th&gt;
&lt;th&gt;bnb-int4/nf4&lt;/th&gt;
&lt;th&gt;torchao-int8&lt;/th&gt;
&lt;th&gt;torchao-fp8&lt;/th&gt;
&lt;th&gt;torchao-int4&lt;/th&gt;
&lt;th&gt;quanto-int8&lt;/th&gt;
&lt;th&gt;hqq-4/8bit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UNet&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text_encoder (CLIP-L)&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;questionable&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text_encoder_2 (CLIP-G)&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NaN!&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NaN!&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VAE&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;td&gt;avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two observations worth noting.&lt;/p&gt;

&lt;p&gt;First - &lt;strong&gt;CLIP-G hates fp8 and int4 weight-only&lt;/strong&gt;. Why? CLIP-G has 1.4B parameters with significant dynamics outside the &lt;code&gt;[-448, 448]&lt;/code&gt; range (the fp8 E4M3 range). Quantize the weights → lose the outliers → embeddings come out garbage → UNet computes noise → VAE outputs NaN → black image.&lt;/p&gt;

&lt;p&gt;A black image is the classic symptom of NaN in latents. If you see it, first thought should be: "what did I break in the text encoder". That's where it is 90% of the time.&lt;/p&gt;

&lt;p&gt;Second - &lt;strong&gt;VAE stays in fp16, always&lt;/strong&gt;. It's a small component (~80 MB), but the operations (convolutions, upsampling) are very sensitive to noise. There's no point touching it - the VRAM savings are negligible and the quality degradation can be visible.&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%2Fmfhiqq9197v0arozbave.jpg" 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%2Fmfhiqq9197v0arozbave.jpg" alt="hqq 2bit" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;hqq 2bit&lt;/strong&gt;&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%2F8ytg859ga65i8h7zy5ff.jpg" 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%2F8ytg859ga65i8h7zy5ff.jpg" alt="quantro int2" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;quantro int2&lt;/strong&gt;&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%2Fzone0fjkx4a1oxsn8q34.jpg" 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%2Fzone0fjkx4a1oxsn8q34.jpg" alt="hqq 1bit" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;hqq 1bit&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiler output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sdf-xl-1.0:torchao-fp4   TextToImageGenerator:create_model  not finished  N/A           N/A            N/A           N/A                               1
sdf-xl-1.0:quanto-int8   TextToImageGenerator:create_model  9.67669 sec   9.67669 sec   9.67669 sec    9.67669 sec   4458246144                        1
sdf-xl-1.0:quanto-int8   TextToImageGenerator:generate      10.05025 sec  10.05025 sec  10.05025 sec   10.05025 sec  -12623872                         1
sdf-xl-1.0:quanto-int8   TextToImageGenerator:save_results  0.02003 sec   0.02003 sec   0.02003 sec    0.02003 sec   0                                 1
sdf-xl-1.0:torchao-int8  TextToImageGenerator:create_model  11.80200 sec  11.80200 sec  11.80200 sec   11.80200 sec  3877761024                        1
sdf-xl-1.0:torchao-int8  TextToImageGenerator:generate      13.60181 sec  13.60181 sec  13.60181 sec   13.60181 sec  10059776                          1
sdf-xl-1.0:torchao-int8  TextToImageGenerator:save_results  0.01174 sec   0.01174 sec   0.01174 sec    0.01174 sec   0                                 1
sdf-xl-1.0:hqq-8bit      TextToImageGenerator:create_model  29.19757 sec  29.19757 sec  29.19757 sec   29.19757 sec  4755091456                        1
sdf-xl-1.0:hqq-8bit      TextToImageGenerator:generate      16.60006 sec  16.60006 sec  16.60006 sec   16.60006 sec  4853760                           1
sdf-xl-1.0:hqq-8bit      TextToImageGenerator:save_results  0.01172 sec   0.01172 sec   0.01172 sec    0.01172 sec   0                                 1
sdf-xl-1.0:torchao-int4  TextToImageGenerator:create_model  not finished  N/A           N/A            N/A           N/A                               2
sdf-xl-1.0:hqq-4bit      TextToImageGenerator:create_model  35.28185 sec  35.28185 sec  35.28185 sec   35.28185 sec  1573851136                        1
sdf-xl-1.0:hqq-4bit      TextToImageGenerator:generate      28.32846 sec  28.32846 sec  28.32846 sec   28.32846 sec  22056960                          1
sdf-xl-1.0:hqq-4bit      TextToImageGenerator:save_results  0.01318 sec   0.01318 sec   0.01318 sec    0.01318 sec   0                                 1
sdf-xl-1.0:bnb-int4      TextToImageGenerator:create_model  17.52835 sec  17.52835 sec  17.52835 sec   17.52835 sec  1704247296                        1
sdf-xl-1.0:bnb-int4      TextToImageGenerator:generate      13.44075 sec  13.44075 sec  13.44075 sec   13.44075 sec  -14581760                         1
sdf-xl-1.0:bnb-int4      TextToImageGenerator:save_results  0.01033 sec   0.01033 sec   0.01033 sec    0.01033 sec   0                                 1
sdf-xl-1.0:quanto-int2   TextToImageGenerator:create_model  14.87508 sec  14.87508 sec  14.87508 sec   14.87508 sec  870027264                         1
sdf-xl-1.0:quanto-int2   TextToImageGenerator:generate      24.74862 sec  24.74862 sec  24.74862 sec   24.74862 sec  13602816                          1
sdf-xl-1.0:quanto-int2   TextToImageGenerator:save_results  0.01268 sec   0.01268 sec   0.01268 sec    0.01268 sec   0                                 1
sdf-xl-1.0:hqq-2bit      TextToImageGenerator:create_model  29.01123 sec  29.01123 sec  29.01123 sec   29.01123 sec  1251397632                        1
sdf-xl-1.0:hqq-2bit      TextToImageGenerator:generate      15.55375 sec  15.55375 sec  15.55375 sec   15.55375 sec  21401600                          1
sdf-xl-1.0:hqq-2bit      TextToImageGenerator:save_results  0.01248 sec   0.01248 sec   0.01248 sec    0.01248 sec   0                                 1
sdf-xl-1.0:hqq-1bit      TextToImageGenerator:create_model  31.83030 sec  31.83030 sec  31.83030 sec   31.83030 sec  558329856                         1
sdf-xl-1.0:hqq-1bit      TextToImageGenerator:generate      34.51453 sec  34.51453 sec  34.51453 sec   34.51453 sec  -3112960                          1
sdf-xl-1.0:hqq-1bit      TextToImageGenerator:save_results  0.01281 sec   0.01281 sec   0.01281 sec    0.01281 sec   0                                 1

Overall time for each index:

Index                    Total Time     Min          Max            Average         Call Count
-----------------------  -------------  -----------  -------------  ------------  ------------
sdf-xl-1.0:quanto-int8   19.74698 sec   0.02003 sec  10.05025 sec   6.58233 sec              3
sdf-xl-1.0:torchao-int8  25.41556 sec   0.01174 sec  13.60181 sec   8.47185 sec              3
sdf-xl-1.0:hqq-8bit      45.80934 sec   0.01172 sec  29.19757 sec   15.26978 sec             3
sdf-xl-1.0:hqq-4bit      63.62349 sec   0.01318 sec  35.28185 sec   21.20783 sec             3
sdf-xl-1.0:bnb-int4      30.97943 sec   0.01033 sec  17.52835 sec   10.32648 sec             3
sdf-xl-1.0:quanto-int2   39.63639 sec   0.01268 sec  24.74862 sec   13.21213 sec             3
sdf-xl-1.0:hqq-2bit      44.57746 sec   0.01248 sec  29.01123 sec   14.85915 sec             3
sdf-xl-1.0:hqq-1bit      66.35763 sec   0.01281 sec  34.51453 sec   22.11921 sec             3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>learning</category>
      <category>performance</category>
    </item>
    <item>
      <title>Every LLM writes the same Todo App. And I mean that literally</title>
      <dc:creator>Jarek</dc:creator>
      <pubDate>Sun, 05 Jul 2026 17:53:35 +0000</pubDate>
      <link>https://dev.to/shadbb/every-llm-writes-the-same-todo-app-and-i-mean-that-literally-721</link>
      <guid>https://dev.to/shadbb/every-llm-writes-the-same-todo-app-and-i-mean-that-literally-721</guid>
      <description>&lt;p&gt;I'm building a &lt;a href="https://benchmark.refio.dev/" rel="noopener noreferrer"&gt;benchmark of local&lt;/a&gt; models.&lt;/p&gt;

&lt;p&gt;Simple task: build a Todo App in a single HTML file - add, delete, mark as done, filters, localStorage. Trivial. Each model gets 6 attempts. 96 files total from 16 models - from Claude Sonnet 4.6 and Haiku 4.5, through the whole Qwen family (9B to 122B), Gemma 4, GPT-OSS 20B and 120B, GPT-4.1-mini, GPT-5.4-mini, Codex-mini, all the way to Z.AI GLM-5-turbo.&lt;/p&gt;

&lt;p&gt;I open file after file to judge whether it works, how the code looks, and run it in the browser - just like a caveman, instead of automating it, because I actually want to see what the AI produced.&lt;/p&gt;

&lt;p&gt;Somewhere around the Nth one I get this weird &lt;em&gt;déjà vu&lt;/em&gt;. After the next one it clicks - it's as if one person wrote all of them. The same function names (&lt;code&gt;addTodo&lt;/code&gt;, &lt;code&gt;saveTodos&lt;/code&gt;, &lt;code&gt;deleteTodo&lt;/code&gt;), the same CSS classes (&lt;code&gt;.filter-btn&lt;/code&gt;, &lt;code&gt;.container&lt;/code&gt;, &lt;code&gt;.todo-text&lt;/code&gt;), the same localStorage key: &lt;code&gt;'todos'&lt;/code&gt;. Models from three different companies, trained on three different datasets - structurally indistinguishable.&lt;/p&gt;

&lt;p&gt;I decided to check it - but this time using an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprint instead of diff
&lt;/h2&gt;

&lt;p&gt;I'm not comparing the code literally - whitespace, quotes, function order are noise. I want to compare structure. For each file I pull out a set of "tokens":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id="..."&lt;/code&gt; attributes from the HTML&lt;/li&gt;
&lt;li&gt;CSS class names&lt;/li&gt;
&lt;li&gt;JS function names&lt;/li&gt;
&lt;li&gt;CSS variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file becomes a bag of ~30 labels. For every pair I compute the &lt;strong&gt;Jaccard coefficient&lt;/strong&gt; - &lt;code&gt;|A ∩ B| / |A ∪ B|&lt;/code&gt;. 1.0 = identical fingerprint, 0.0 = not a single shared identifier.&lt;/p&gt;

&lt;p&gt;Then single-link clustering with a 0.45 threshold: two files land in the same cluster if there's a chain of pairs with similarity ≥ 0.45. A naive algorithm, but on 96 files it runs in a fraction of a second. The script is ~150 lines of Node.js with no dependencies beyond Playwright for generating the PNGs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result: one giant monocluster
&lt;/h2&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%2Fg84yaeq2omzmn46z4pa2.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%2Fg84yaeq2omzmn46z4pa2.png" alt="todo-similarity-heatmap" width="800" height="954"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 96×96 heatmap. The top-left corner is one huge dark block taking up &lt;strong&gt;62.5% of the whole matrix&lt;/strong&gt; - 60 files, four companies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic Haiku 4.5 - all 6 attempts&lt;/li&gt;
&lt;li&gt;Qwen 3.5 (9B, 27B, 35B, 122B) and Qwen 3.6 (27B, 35B)&lt;/li&gt;
&lt;li&gt;Gemma 4 (26B and 31B)&lt;/li&gt;
&lt;li&gt;Z.AI GLM-5-turbo - all 6 attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four companies, five model families, spanning 9-122 billion parameters. &lt;strong&gt;Structurally the same code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most surprising numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jaccard 1.000  qwen3.5_122b_03  ↔  qwen3.5_122b_04
Jaccard 1.000  qwen3.6_35b_01   ↔  qwen3.6_35b_03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same model, two different runs - &lt;strong&gt;an identical set of identifiers&lt;/strong&gt;. Zero variance. The model has a "favorite piece of code" that it returns almost deterministically.&lt;/p&gt;

&lt;p&gt;The most common signatures across the whole corpus: the &lt;code&gt;.active&lt;/code&gt; class in 92/96 files, &lt;code&gt;addTodo&lt;/code&gt; in 80/96, &lt;code&gt;deleteTodo&lt;/code&gt; and &lt;code&gt;.filters&lt;/code&gt; in 75/96 each, &lt;code&gt;saveTodos&lt;/code&gt; in 74/96. These aren't coincidences.&lt;/p&gt;

&lt;p&gt;It's the imprint of one specific canon - &lt;strong&gt;TodoMVC plus its hundreds of clones on GitHub&lt;/strong&gt;, which all these models very likely swallowed during training.&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%2Ffyf5cmogkuanlv8p53zj.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%2Ffyf5cmogkuanlv8p53zj.png" alt="todo-similarity-network" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  There are other schools, though
&lt;/h2&gt;

&lt;p&gt;Beyond the mainstream you can see two distinct bubbles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "OpenAI minimalist" cluster&lt;/strong&gt; - GPT-OSS 120B and 20B, GPT-4.1-mini, part of Codex-mini. Characteristics: ~200 LOC instead of ~330, fewer CSS classes, simpler structure. Same problem, terser and without the decoration. Interestingly, GPT-OSS 120B and 20B sit together despite a 6× difference in parameters - clearly the same "style school" inside the company.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPT-5.4-mini&lt;/strong&gt; stands out differently: it consistently uses CSS variables with a color palette - &lt;code&gt;--primary-color&lt;/code&gt;, &lt;code&gt;--bg-color&lt;/code&gt;, &lt;code&gt;--danger-color&lt;/code&gt;. The other models hard-code their colors. This one ran into newer frontend patterns during training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sonnet 4.6&lt;/strong&gt; - an interesting case. 6 attempts split into three small subgroups and one singleton. Intra-model Jaccard = 0.441 - the lowest in the whole "class A". Haiku 4.5 sits at 0.815. So the same Haiku gives you the same thing every time, while Sonnet gives you something different every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex-mini&lt;/strong&gt; - the lowest score in the set, mean Jaccard = 0.277. Three attempts in the OpenAI cluster, three singletons. A model that doesn't even have a "favorite solution" for a trivial task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Will Snake be any different?
&lt;/h2&gt;

&lt;p&gt;I said above that todo is a recall task. Snake - with modes (human-vs-human, human-vs-cpu, cpu-vs-cpu), a score, a start menu and a game-over screen - has enough decision dimensions to force the models to &lt;em&gt;design&lt;/em&gt; rather than &lt;em&gt;remember&lt;/em&gt;. I ran the same script on another 96 files.&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%2Fhc1jpaec816u2n0ijnz0.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%2Fhc1jpaec816u2n0ijnz0.png" alt="snake-similarity-heatmap" width="800" height="927"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The heatmap looks completely different. No big dark blocks. The network view: a few small groups in one corner, surrounded by a ring of &lt;strong&gt;73 singletons&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Todo App&lt;/th&gt;
&lt;th&gt;Snake&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Largest cluster&lt;/td&gt;
&lt;td&gt;60 files (62.5%)&lt;/td&gt;
&lt;td&gt;6 files (6.3%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singletons&lt;/td&gt;
&lt;td&gt;4 (4%)&lt;/td&gt;
&lt;td&gt;73 (76%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identical pairs (Jaccard 1.0)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-company clusters&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All seven Snake clusters are &lt;strong&gt;intra-family&lt;/strong&gt; - not one of them links models from different companies. The exact opposite of todo.&lt;/p&gt;

&lt;p&gt;In todo the dominant signatures were things like &lt;code&gt;addTodo&lt;/code&gt;, &lt;code&gt;.active&lt;/code&gt;, &lt;code&gt;.filter-btn&lt;/code&gt; - names of &lt;em&gt;specific design decisions&lt;/em&gt; that everyone could have made differently, but everyone made the same. In Snake the dominant ones are &lt;code&gt;startGame&lt;/code&gt;, &lt;code&gt;id="gameCanvas"&lt;/code&gt;, &lt;code&gt;endGame&lt;/code&gt;, &lt;code&gt;draw&lt;/code&gt;, &lt;code&gt;spawnFood&lt;/code&gt; - that's the &lt;strong&gt;domain vocabulary&lt;/strong&gt;, the minimum you can't get around. Everything else - game modes, menu structure, how state is held - is different in every file.&lt;/p&gt;

&lt;p&gt;Sonnet 4.6 in Snake: 6 attempts, 6 singletons, max Jaccard = 0.324. The highest intra-model diversity in the whole set.&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%2F88rj4fpzj3em5uie15rk.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%2F88rj4fpzj3em5uie15rk.png" alt="snake-similarity-network" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What follows from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Todo App isn't a benchmark of programming skill.&lt;/strong&gt; It's a recall test. The code exists in the training data in hundreds of copies, and the model reproduces it. Swap the task for something with real complexity and the "model consensus" shatters into 78% individual approaches.&lt;/p&gt;

&lt;p&gt;Four companies in one bucket isn't a coincidence - Anthropic, Alibaba, Google and Z.AI very likely trained on overlapping GitHub scrapes (TodoMVC + clones) and/or on synthetic data from a shared stronger teacher. It's not a conspiracy, it's the consequence of "good code" on the web being a finite set.&lt;/p&gt;

&lt;p&gt;Low variance between attempts isn't a feature - it's a defect. Codex-mini with a mean Jaccard of 0.277 isn't the "worse" model. It's the model that gives you different approaches on the first answer. In real work, where the first answer is rarely the final one, that has value. On leaderboards with one attempt per task it counts as "instability".&lt;/p&gt;

&lt;p&gt;And practically: if you're building CRUDs and wondering whether to grab Haiku, Qwen 9B or Z.AI GLM - &lt;strong&gt;it probably makes no difference&lt;/strong&gt; to code quality. What matters is price, latency and context size. Not "intelligence".&lt;/p&gt;

&lt;p&gt;I started this benchmark to check how the agent I'm building works with different models, and which is the "best local model for coding".&lt;/p&gt;

&lt;p&gt;I ended up with the conclusion that on basic tasks all local models are the same model in different wrappers. The real difference starts where TodoMVC ends in the training data - at real projects with specific context and state the model has never seen before.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
