<?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: Kumar K Jha</title>
    <description>The latest articles on DEV Community by Kumar K Jha (@kkjcodes).</description>
    <link>https://dev.to/kkjcodes</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%2F4004304%2F0a105ec7-3a78-4563-a1c8-506722b96754.png</url>
      <title>DEV Community: Kumar K Jha</title>
      <link>https://dev.to/kkjcodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kkjcodes"/>
    <language>en</language>
    <item>
      <title>A 1.125x FLUX speedup was real. The harder question is where diffusion gives back time.</title>
      <dc:creator>Kumar K Jha</dc:creator>
      <pubDate>Sun, 19 Jul 2026 20:08:58 +0000</pubDate>
      <link>https://dev.to/kkjcodes/a-1125x-flux-speedup-was-real-the-harder-question-is-where-diffusion-gives-back-time-ppi</link>
      <guid>https://dev.to/kkjcodes/a-1125x-flux-speedup-was-real-the-harder-question-is-where-diffusion-gives-back-time-ppi</guid>
      <description>&lt;p&gt;In the previous post, I benchmarked training-free block-residual caching for 4-bit FLUX.1-dev on an Apple M5 Max.&lt;/p&gt;

&lt;p&gt;The short version: caching helped, but it did not give me the clean win I wanted.&lt;/p&gt;

&lt;p&gt;The longer version was more interesting. A fixed-interval cache produced real speedups, then failed a same-seed PSNR gate so badly that I almost wrote the wrong conclusion. The images were not necessarily bad. They had drifted from the uncached denoising trajectory. That sent me down the rabbit hole of metric floors, windowed SSIM, LPIPS, CLIP scoring, prompt stratification, and interleaved timing.&lt;/p&gt;

&lt;p&gt;That work is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kkjcodes/m5-flux-block-cache-benchmark" rel="noopener noreferrer"&gt;github.com/kkjcodes/m5-flux-block-cache-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the handoff from that experiment. Not a victory lap. Not a promise that I have the next speedup solved. More like: here is where block caching stopped being the most interesting question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the cache experiment landed
&lt;/h2&gt;

&lt;p&gt;The best policy I found was a warmup split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;joint blocks 12-18  -&amp;gt; start_step=8
single blocks 28-37 -&amp;gt; start_step=6
interval            -&amp;gt; 2
value mode          -&amp;gt; residual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code, &lt;code&gt;start_step&lt;/code&gt; is the warmup boundary. That boundary step is still a refresh point; reuse is allowed after it on interval misses.&lt;/p&gt;

&lt;p&gt;That policy passed an interleaved timing gate at &lt;strong&gt;1.125×&lt;/strong&gt; and scored:&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median PSNR&lt;/td&gt;
&lt;td&gt;29.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median windowed SSIM&lt;/td&gt;
&lt;td&gt;0.94575&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median LPIPS Alex&lt;/td&gt;
&lt;td&gt;0.05519&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median CLIP delta&lt;/td&gt;
&lt;td&gt;0.00181&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a real result. It is also a bounded result.&lt;/p&gt;

&lt;p&gt;The cache is not pixel-identical. It still fails a strict same-seed trajectory gate. And the improvement is content-dependent: portraits, product renders, and typography are close to the uncached references, while dense interiors remain the hard case.&lt;/p&gt;

&lt;p&gt;Here is the prompt-stratified picture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt class&lt;/th&gt;
&lt;th&gt;PSNR&lt;/th&gt;
&lt;th&gt;Windowed SSIM&lt;/th&gt;
&lt;th&gt;LPIPS Alex&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dense_interior&lt;/td&gt;
&lt;td&gt;22.62&lt;/td&gt;
&lt;td&gt;0.83994&lt;/td&gt;
&lt;td&gt;0.12326&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;portrait&lt;/td&gt;
&lt;td&gt;29.90&lt;/td&gt;
&lt;td&gt;0.95402&lt;/td&gt;
&lt;td&gt;0.04808&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product_render&lt;/td&gt;
&lt;td&gt;29.10&lt;/td&gt;
&lt;td&gt;0.96124&lt;/td&gt;
&lt;td&gt;0.03791&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;typography&lt;/td&gt;
&lt;td&gt;31.80&lt;/td&gt;
&lt;td&gt;0.94660&lt;/td&gt;
&lt;td&gt;0.05070&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table is the actual frontier. Not "cache works." Not "cache fails." More like: &lt;strong&gt;warmup-delayed residual reuse is near-fidelity on some content classes, but dense interiors expose the weakness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is one of the easier cases. Same prompt, same seed, uncached reference on the left and warmup-split cached variant on the right:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Uncached reference&lt;/th&gt;
&lt;th&gt;Warmup-split cached variant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2F6dbwkakyl8mk7eirdrpy.png" alt="Warmup split portrait reference" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&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%2Fqkeg2mozc41v5tugyi2t.png" alt="Warmup split portrait variant" width="800" height="800"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And here is the kind of case that kept the result honest. Dense interiors were still hard, even after the residual gate. The two cached versions are near-indistinguishable from each other, which is the point: the gate fired, but it did not change this failure mode enough.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Uncached reference&lt;/th&gt;
&lt;th&gt;Warmup split&lt;/th&gt;
&lt;th&gt;Residual-gated m2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fan652o7liqdaag0byueg.png" alt="Dense interior reference" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&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%2Fauvrs1zdbt31gdfpk1ze.png" alt="Dense interior warmup split variant" width="800" height="800"&gt;&lt;/td&gt;
&lt;td&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%2F34be8y4evwl3d329rs06.png" alt="Dense interior residual-gated m2 variant" width="800" height="800"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The adaptive gate I thought would help
&lt;/h2&gt;

&lt;p&gt;The obvious next move was residual-volatility-gated reuse.&lt;/p&gt;

&lt;p&gt;The idea was simple enough: keep the warmup split schedule, but when a block's residual changes too much relative to its recent history, veto reuse and recompute the block.&lt;/p&gt;

&lt;p&gt;I pre-registered the target in plain English: lift dense-interior PSNR from &lt;strong&gt;22.62&lt;/strong&gt; toward roughly &lt;strong&gt;29&lt;/strong&gt;, without dropping below the split policy's timing class.&lt;/p&gt;

&lt;p&gt;That did not happen.&lt;/p&gt;

&lt;p&gt;On the matched interleaved timing comparison, the split re-measured at &lt;strong&gt;1.121×&lt;/strong&gt; in the same run as the gated policy. The gated threshold measured &lt;strong&gt;1.093×&lt;/strong&gt;. So the gate gave back speed.&lt;/p&gt;

&lt;p&gt;Quality moved only a little:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Dense PSNR&lt;/th&gt;
&lt;th&gt;Dense LPIPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;split warmup&lt;/td&gt;
&lt;td&gt;22.62&lt;/td&gt;
&lt;td&gt;0.1233&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gated m2&lt;/td&gt;
&lt;td&gt;22.82&lt;/td&gt;
&lt;td&gt;0.1146&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gated m3&lt;/td&gt;
&lt;td&gt;22.78&lt;/td&gt;
&lt;td&gt;0.1166&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is not close. It needed roughly 6 dB of dense-interior improvement. It found about 0.2 dB.&lt;/p&gt;

&lt;p&gt;Then I checked whether the gate was even firing. It was. But it did not fire more on dense interiors than on easier prompt classes. Product renders and portraits often fired at similar or higher rates.&lt;/p&gt;

&lt;p&gt;So the conclusion is narrow but useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This residual-volatility signal, at these thresholds and block spans, did not target the dense-interior failure mode.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not the same as "adaptive reuse can never work." It is just enough evidence to stop spending time on this particular gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changed about the next question
&lt;/h2&gt;

&lt;p&gt;At the start, I thought the path was probably:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find cacheable blocks -&amp;gt; tune schedule -&amp;gt; add adaptive gate -&amp;gt; get speed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the experiments, that feels too small.&lt;/p&gt;

&lt;p&gt;A veto layer can only block reuse decisions that a fixed schedule already admitted. If the failure is created by the schedule itself, then a gate on top can only claw back part of the damage. That may be exactly what happened here.&lt;/p&gt;

&lt;p&gt;Dense interiors may not need "reuse unless volatility spikes." They may need a different schedule, a different approximation surface, or a different way of spending compute across the denoising trajectory.&lt;/p&gt;

&lt;p&gt;Which leads to the question I actually want to ask next:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we make diffusion faster without treating transformer blocks as the only reusable unit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I do not have the answer yet. I am not claiming the next project will beat the warmup split. I am saying the cache benchmark changed what I think is worth investigating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why block caching may be the wrong level
&lt;/h2&gt;

&lt;p&gt;Block-residual caching is attractive because it is mechanical. You can hook transformer blocks, store deltas, and measure wall time. It does not require training. It does not require changing the model. It gives you a clean benchmark surface.&lt;/p&gt;

&lt;p&gt;But diffusion is not just a stack of interchangeable block calls. The denoising trajectory has phases. Early steps decide composition. Later steps refine detail. Different content classes stress different parts of the trajectory. A fixed block schedule ignores most of that structure.&lt;/p&gt;

&lt;p&gt;The warmup result made that obvious. Skipping the early steps mattered more than almost anything else.&lt;/p&gt;

&lt;p&gt;The residual-gate result made the next problem obvious. A block-level volatility score did not know enough about the failure mode to protect dense interiors.&lt;/p&gt;

&lt;p&gt;So the next acceleration surface I want to study is narrower than "try more caching tricks": &lt;strong&gt;trajectory-aware compute scheduling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The question is whether the denoising process can decide, step by step, when a full expensive transformer evaluation is worth paying for. Early steps carry composition. Later steps refine detail. Dense interiors may be sensitive in places where portraits and product renders are not. A fixed block-reuse schedule flattens all of that structure into a yes/no decision per block.&lt;/p&gt;

&lt;p&gt;That feels like the wrong abstraction. The next question is whether cheap signals from the latent trajectory can guide the amount of compute spent across denoising phases without moving the image onto a different composition path.&lt;/p&gt;

&lt;p&gt;That is a direction, not a commitment. The last project taught me to be careful with promises before the metric harness exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am taking forward
&lt;/h2&gt;

&lt;p&gt;The useful artifact from this work is not just the 1.125× speedup. It is the measurement discipline.&lt;/p&gt;

&lt;p&gt;For the next round, I want the same rules from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interleaved timing for candidate-vs-baseline comparisons;&lt;/li&gt;
&lt;li&gt;prompt-stratified quality summaries;&lt;/li&gt;
&lt;li&gt;cross-seed floors for reference metrics;&lt;/li&gt;
&lt;li&gt;perceptual metrics, not only PSNR;&lt;/li&gt;
&lt;li&gt;visual samples in the report;&lt;/li&gt;
&lt;li&gt;explicit distinction between trajectory preservation and image quality;&lt;/li&gt;
&lt;li&gt;negative results written down instead of quietly discarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. The residual gate was a miss, but it was not wasted. It told me that a simple per-block residual-volatility signal is probably not the lever. It also told me that dense interiors are the content class to keep in the loop for every future speed claim.&lt;/p&gt;

&lt;p&gt;A speedup that only works on simple prompts is a demo. A speedup that survives dense interiors is a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The current state
&lt;/h2&gt;

&lt;p&gt;The block-residual caching milestone is closed.&lt;/p&gt;

&lt;p&gt;What I am comfortable saying publicly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed-interval block-residual caching exposes real acceleration headroom on this setup.&lt;/li&gt;
&lt;li&gt;Same-seed PSNR is a trajectory metric, not an image-quality verdict.&lt;/li&gt;
&lt;li&gt;Warmup-delayed reuse is the best tested cache policy so far.&lt;/li&gt;
&lt;li&gt;The warmup split reaches a timing-pass near-fidelity point at &lt;strong&gt;1.125×&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Dense interiors remain the hard case.&lt;/li&gt;
&lt;li&gt;Residual-volatility gating did not solve that hard case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I am not claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;that the cache is generally quality-preserving;&lt;/li&gt;
&lt;li&gt;that 1.125× transfers to other hardware, quantization settings, or model variants;&lt;/li&gt;
&lt;li&gt;that adaptive reuse is dead;&lt;/li&gt;
&lt;li&gt;that I already know the next acceleration method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next thing is a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If block caching gives a bounded win, can trajectory-aware scheduling give diffusion a better way to spend compute?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is where I want to go next.&lt;/p&gt;




&lt;p&gt;The benchmark repo is here: &lt;strong&gt;&lt;a href="https://github.com/kkjcodes/m5-flux-block-cache-benchmark" rel="noopener noreferrer"&gt;github.com/kkjcodes/m5-flux-block-cache-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The README now includes the curated reports, visual samples, and the residual-gated negative result. If you are doing similar work, my strongest recommendation is still the boring one: measure your floor, interleave your timing, and do not let a single prompt write your headline.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>apple</category>
      <category>python</category>
      <category>performance</category>
    </item>
    <item>
      <title>I made FLUX 1.16 faster on an M5 Max. Then I found out my quality gate was measuring the wrong thing.</title>
      <dc:creator>Kumar K Jha</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:13:42 +0000</pubDate>
      <link>https://dev.to/kkjcodes/i-made-flux-116x-faster-on-an-m5-max-then-i-found-out-my-quality-gate-was-measuring-the-wrong-3ci4</link>
      <guid>https://dev.to/kkjcodes/i-made-flux-116x-faster-on-an-m5-max-then-i-found-out-my-quality-gate-was-measuring-the-wrong-3ci4</guid>
      <description>&lt;p&gt;I spent a few weeks benchmarking training-free block-residual caching for FLUX.1-dev on an Apple M5 Max. The timing result was real: a stable ~1.16× speedup, tight variance, reproducible across prompts.&lt;/p&gt;

&lt;p&gt;Then the quality sweep came back and every policy failed my gate. Median PSNR of 14.27 dB against a gate of 30 dB. Decisive failure.&lt;/p&gt;

&lt;p&gt;I almost published that as "block-residual caching doesn't preserve quality on Apple Silicon." That would have been wrong — not because the numbers were wrong, but because &lt;strong&gt;PSNR wasn't measuring what I thought it was measuring&lt;/strong&gt;. This post is about the speedup, the trap, and the one cheap calibration that caught it.&lt;/p&gt;

&lt;p&gt;Everything here is reproducible — harness, raw per-pair data, and the two image pairs the argument turns on:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkjcodes/m5-flux-block-cache-benchmark" rel="noopener noreferrer"&gt;github.com/kkjcodes/m5-flux-block-cache-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;FLUX.1-dev has 19 joint transformer blocks (image and text streams attending together) and 38 single blocks (a fused stream). Phase attribution on my machine said:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DiT forward: &lt;strong&gt;95.2%&lt;/strong&gt; of uncached wall time&lt;/li&gt;
&lt;li&gt;Single-stream blocks: &lt;strong&gt;63.8%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Joint blocks: &lt;strong&gt;31.2%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So essentially all the time is in the transformer, and the single blocks are where the money is. That's the headroom.&lt;/p&gt;

&lt;p&gt;The idea behind block-residual caching is simple. A transformer block computes &lt;code&gt;out = f(in)&lt;/code&gt;. Instead of caching &lt;code&gt;out&lt;/code&gt; (which is wrong the moment the input changes), you cache the &lt;strong&gt;residual&lt;/strong&gt; &lt;code&gt;out - in&lt;/code&gt;, then on a reuse step you apply the stale residual to the fresh input:&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;_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value_mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&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;value&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# joint blocks return (encoder_hidden_states, hidden_states)
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoder_hidden_states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;value&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden_states&lt;/span&gt;&lt;span class="sh"&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;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden_states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_restore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value_mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&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;cached&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint&lt;/span&gt;&lt;span class="sh"&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;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoder_hidden_states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cached&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;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden_states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cached&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden_states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reuse is on a fixed interval — every other step:&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;should_reuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestep&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&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;step_index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;step_index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config throughout: &lt;strong&gt;4-bit quantized&lt;/strong&gt; FLUX.1-dev, 28 steps, linear scheduler, 1024×1024, Apple M5 Max. The 4-bit part matters — quantization shifts the compute/memory balance that determines how much caching can win, so none of these numbers should be assumed to transfer to a full-precision build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timing result
&lt;/h2&gt;

&lt;p&gt;Twelve measured runs per policy, one warmup, acceptance gated on &lt;code&gt;delta &amp;gt; 3 × sqrt(baseline_stdev² + candidate_stdev²)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Blocks&lt;/th&gt;
&lt;th&gt;Clean median&lt;/th&gt;
&lt;th&gt;Stdev&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline_uncached&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;119.33s&lt;/td&gt;
&lt;td&gt;2.89s&lt;/td&gt;
&lt;td&gt;1.000×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_12_18_i2_residual&lt;/td&gt;
&lt;td&gt;joint:12-18&lt;/td&gt;
&lt;td&gt;134.82s&lt;/td&gt;
&lt;td&gt;18.15s&lt;/td&gt;
&lt;td&gt;0.885× ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_10_18_i2_residual&lt;/td&gt;
&lt;td&gt;joint:10-18&lt;/td&gt;
&lt;td&gt;98.82s&lt;/td&gt;
&lt;td&gt;2.86s&lt;/td&gt;
&lt;td&gt;1.208×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_8_18_i2_residual&lt;/td&gt;
&lt;td&gt;joint:8-18&lt;/td&gt;
&lt;td&gt;100.50s&lt;/td&gt;
&lt;td&gt;2.60s&lt;/td&gt;
&lt;td&gt;1.187×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single_28_37_i2_residual&lt;/td&gt;
&lt;td&gt;single:28-37&lt;/td&gt;
&lt;td&gt;101.03s&lt;/td&gt;
&lt;td&gt;2.20s&lt;/td&gt;
&lt;td&gt;1.181×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_12_18_single_28_37&lt;/td&gt;
&lt;td&gt;joint:12-18,single:28-37&lt;/td&gt;
&lt;td&gt;91.87s&lt;/td&gt;
&lt;td&gt;0.99s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.299×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real speedup, cleanly separated from noise. But that table has a problem I only caught later, so hold onto it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quality sweep
&lt;/h2&gt;

&lt;p&gt;4 prompts × 8 seeds × 3 surviving policies = 96 comparisons. Each variant is compared against an uncached reference generated with the &lt;strong&gt;same seed&lt;/strong&gt; — same latents, same prompt, same everything but the cache. Gate: median PSNR &amp;gt;= 30 dB, median global-SSIM &amp;gt;= 0.98.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Median PSNR&lt;/th&gt;
&lt;th&gt;Median Global-SSIM&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;joint_10_18_i2_residual&lt;/td&gt;
&lt;td&gt;16.75&lt;/td&gt;
&lt;td&gt;0.857&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_12_18_single_28_37&lt;/td&gt;
&lt;td&gt;14.27&lt;/td&gt;
&lt;td&gt;0.693&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single_28_37_i2_residual&lt;/td&gt;
&lt;td&gt;14.23&lt;/td&gt;
&lt;td&gt;0.727&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total wipeout. 0 of 96 pairs cleared 30 dB. Zero cleared 25.&lt;/p&gt;

&lt;p&gt;So: caching breaks the images, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;I pulled the single worst pair in the sweep — 11.34 dB, a typography prompt — expecting mush.&lt;/p&gt;

&lt;p&gt;It was a clean, well-composed "OPEN LATE" bookstore sign. Sharp letterforms, correct prompt adherence, nice rain-slicked reflections. It was a &lt;em&gt;good image&lt;/em&gt;. It just wasn't the &lt;em&gt;same&lt;/em&gt; image as the reference.&lt;/p&gt;

&lt;p&gt;That reframes everything. PSNR against a same-seed reference doesn't measure quality. It measures &lt;strong&gt;trajectory divergence&lt;/strong&gt; — how far the denoising path drifted from where it would have gone. A cached run that lands on a different-but-equally-good image scores catastrophically, and a genuinely degraded run scores catastrophically, and PSNR cannot tell you which one you're looking at.&lt;/p&gt;

&lt;p&gt;Worse, the gate itself was unreachable. The single best pair in my whole sweep was 22.25 dB. I looked at it: same face, same pose, same lighting, same composition — differing in fingernail detail and pot texture. Visually near-identical output, and it still misses a 30 dB gate by 8 dB. &lt;strong&gt;A gate that rejects near-perfect output isn't a fidelity gate, it's a pixel-identity gate.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The calibration that caught it
&lt;/h2&gt;

&lt;p&gt;Here's the cheap trick, and it's the most portable thing in this post.&lt;/p&gt;

&lt;p&gt;If you don't know what your metric's numbers mean, &lt;strong&gt;measure the floor&lt;/strong&gt;: score two images that are both good but share no trajectory at all. For me that's two &lt;em&gt;reference&lt;/em&gt; images, same prompt, different seeds — no cache anywhere near them.&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="c1"&gt;# both images uncached, both good, different seeds
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seeds&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="n"&gt;psnr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;global_ssim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result over 112 unrelated pairs: &lt;strong&gt;9.73 dB, global-SSIM 0.23.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now the whole scale snaps into focus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 9.73 dB  two unrelated good images  &amp;lt;- the floor
14–17 dB  my cached variants
22.25 dB  visually near-identical (verified by eye)
30.00 dB  my gate                    &amp;lt;- nothing reaches this, ever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story in global-SSIM units: floor 0.23, my variants 0.69–0.86, gate 0.98.&lt;/p&gt;

&lt;p&gt;My variants at 14–17 dB sit meaningfully above "unrelated," so the cache does retain real trajectory structure. But the worst cases at 11.3 dB are creeping toward "might as well have used a different seed." That's a genuinely interesting, defensible finding — and I could only state it &lt;em&gt;because&lt;/em&gt; I had the floor. Without it, "14.27 dB" is a number with no semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're gating generative output on a reference metric, go measure your floor first.&lt;/strong&gt; It takes ten minutes and it tells you whether your gate has any discriminating power at all. Mine didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My SSIM wasn't SSIM.&lt;/strong&gt; I'd written a global SSIM over the whole flattened image. Real SSIM (Wang et al., which is where the 0.98 convention comes from) is an 11×11 windowed mean computed per channel. My numbers were inflated by ~0.15 across the board — 0.857 where &lt;code&gt;skimage&lt;/code&gt; says 0.698. It didn't flip any verdict, but I'd have published a column labeled "SSIM" that wasn't. If you hand-roll a metric, diff it against the reference implementation before you put it in a table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My headline speedup didn't replicate.&lt;/strong&gt; That 1.299× came from one prompt ("A simple red cube on a white table") at one seed. The quality sweep incidentally timed 32 prompt/seed cells per policy, so I ran a paired comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Paired median (n=32)&lt;/th&gt;
&lt;th&gt;IQR&lt;/th&gt;
&lt;th&gt;Headline (n=1 prompt)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;joint_10_18&lt;/td&gt;
&lt;td&gt;1.085×&lt;/td&gt;
&lt;td&gt;1.067–1.113&lt;/td&gt;
&lt;td&gt;1.208×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single_28_37&lt;/td&gt;
&lt;td&gt;1.095×&lt;/td&gt;
&lt;td&gt;1.075–1.110&lt;/td&gt;
&lt;td&gt;1.181×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joint_12_18_single_28_37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.159×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.141–1.185&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.299×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ordering preserved, variance tight, and speedup is essentially prompt-independent (1.084–1.108 across four very different prompts) — so "stable" is actually &lt;em&gt;better&lt;/em&gt; supported than the single-prompt run showed. But every magnitude lands ~10 points lower. The n=32 sample is the weaker timing protocol (no warmup, N=1 per cell, fixed ordering), so it's not a refutation — but it's my own data disagreeing with my own headline, and the honest move is to quote the range and name the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big caveat: I never tested a warmup window
&lt;/h2&gt;

&lt;p&gt;Look at that reuse schedule again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;step 0: compute → step 1: REUSE → step 2: compute → step 3: REUSE ...
14 of 28 steps reused. First reuse at step index 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caching kicks in at the &lt;strong&gt;second denoising step&lt;/strong&gt; — while composition is still being decided. And composition divergence is exactly what my PSNR was detecting.&lt;/p&gt;

&lt;p&gt;Every established training-free cache method (DeepCache, TeaCache, FBCache) skips an early warmup window for precisely this reason. My policy dataclass has no start-step field, so I couldn't express "skip the first N steps" even if I'd wanted to. I tested the configuration most likely to diverge and then generalized from it.&lt;/p&gt;

&lt;p&gt;That's the next experiment, and I'd bet it's where the actual frontier is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm actually claiming
&lt;/h2&gt;

&lt;p&gt;Training-free block-residual caching exposes real acceleration headroom for 4-bit FLUX.1-dev on Apple M5 Max: broad single-stream and joint+single coverage produce stable, prompt-independent speedups in the &lt;strong&gt;1.09×–1.30× range&lt;/strong&gt; depending on policy and timing protocol.&lt;/p&gt;

&lt;p&gt;But fixed-interval residual reuse — enabled from the first denoising step, with no warmup exclusion, at the block spans tested — &lt;strong&gt;does not preserve same-seed reference trajectories&lt;/strong&gt;, and divergence grows with coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image quality remains unmeasured.&lt;/strong&gt; I have no perceptual metric and no prompt-alignment metric in this run. The images I inspected by hand looked good. I'm not claiming they are, because I didn't measure it.&lt;/p&gt;

&lt;p&gt;That's a timing and trajectory-divergence characterization. It is not a quality result, and I'm not going to dress it up as one.&lt;/p&gt;

&lt;p&gt;Next up: LPIPS, real windowed SSIM, and CLIP prompt-alignment over the 32 references and 96 variants already sitting on disk — each with its own cross-seed floor, because now I know better than to report a number without one. Then a warmup-window knob.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code and the data
&lt;/h2&gt;

&lt;p&gt;Everything is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/kkjcodes/m5-flux-block-cache-benchmark" rel="noopener noreferrer"&gt;github.com/kkjcodes/m5-flux-block-cache-benchmark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repo ships the harness (a custom mflux denoising loop with block-level hooks, so the timing has no hidden synchronization in it), plus the audited evidence bundle — all 96 per-pair rows in &lt;code&gt;quality_results.jsonl&lt;/code&gt;, the timing sweep JSON, the environment record, and the two reference/variant pairs this post argues from. The 153MB of remaining images stayed out, but the two that carry the argument are there, so you can look at the 11.34 dB "failure" and the 22.25 dB "near-identical" pair and judge my read for yourself.&lt;/p&gt;

&lt;p&gt;If you disagree with my interpretation, the raw numbers are right there to disagree with.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're doing similar work on Apple Silicon: measure your floor, diff your hand-rolled metrics against reference implementations, and never let a headline number rest on one prompt. All three of my mistakes were free to catch and would have been expensive to publish.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>apple</category>
      <category>python</category>
      <category>performance</category>
    </item>
    <item>
      <title>What it actually costs to generate one AI cartoon video, line by line</title>
      <dc:creator>Kumar K Jha</dc:creator>
      <pubDate>Fri, 26 Jun 2026 17:13:57 +0000</pubDate>
      <link>https://dev.to/kkjcodes/what-it-actually-costs-to-generate-one-ai-cartoon-video-line-by-line-3omh</link>
      <guid>https://dev.to/kkjcodes/what-it-actually-costs-to-generate-one-ai-cartoon-video-line-by-line-3omh</guid>
      <description>&lt;p&gt;I sat down to look at my fal.ai billing dashboard last week for the first time in a few months. Not because something was wrong — just because I wanted to write a "here's what this costs" post and I'd been winging the numbers for a while.&lt;/p&gt;

&lt;p&gt;A couple of hours later I had a spreadsheet, three tabs of receipts, and a slightly different view of my own pipeline than I started with. So I'm writing it up.&lt;/p&gt;

&lt;p&gt;This is about the cost structure of a small AI-video product I built solo. The point isn't to argue it's a great cost structure — it isn't, particularly — but to break down where the money actually goes. If you're building one of these and trying to figure out where to optimize, the numbers might be useful. And if you're not, the surprise at the end is at least kind of fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built, briefly and honestly
&lt;/h2&gt;

&lt;p&gt;A user uploads a photo. The pipeline glues together a few third-party models — image generator, image-to-video, TTS, a few LLM calls — and out the other end pops a personalized animated MP4 starring that person (and optionally up to four people in the same video).&lt;/p&gt;

&lt;p&gt;It's &lt;a href="https://www.atveanimation.com" rel="noopener noreferrer"&gt;atveanimation.com&lt;/a&gt; if you want to poke at it. Free tier is real. Free tier is also why this cost post exists — running a free tier means knowing where the money goes.&lt;/p&gt;

&lt;p&gt;I did not invent any of the techniques here. Character consistency, keyframe anchoring, LoRA conditioning — those are all standard patterns at this point. Frontier models (Kling 3.0 Motion Control, Seedance 2.0, Hedra, Wan 2.7 multi-ref) do the consistency part natively and arguably better. What I'm posting is just the unit economics of a particular stack: WAN + Flux Kontext + flux-lora + Kokoro.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured this
&lt;/h2&gt;

&lt;p&gt;Prices come from fal.ai, Replicate, and Anthropic posted API rates, cross-referenced with averaged usage across the last ~100 generations on my account. For models priced by output (Kontext Pro) or character count (Kokoro), I used the per-call mean rather than the marginal-token rate.&lt;/p&gt;

&lt;p&gt;A couple of definitions worth being precise about, because I tripped over them in an earlier draft:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generated seconds&lt;/strong&gt;: what you actually pay for. WAN i2v emits ~6-second clips at my frame settings, so 4 scenes = 24 generated
seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finished seconds&lt;/strong&gt;: what the user watches. My pipeline trims each scene to &lt;code&gt;audio_length + 0.5s&lt;/code&gt; after merging, so a 6-second clip with a 3-second voice line becomes a 3.5-second finished scene. A 4-scene video usually finishes around 16 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both numbers are real. The per-second figure depend and I'll keep them straight throughout.&lt;/p&gt;

&lt;p&gt;Amortization: I'm dividing per-character setup costs across &lt;strong&gt;5 videos per character&lt;/strong&gt;, which is what the repeat-use pattern looks like in my own data. At one video per character the per-second cost roughly doubles. I'll show the range.&lt;/p&gt;

&lt;p&gt;What this excludes: failed-and-rerolled generations (handled separately below), Azure blob storage and egress (rounding error), Container Apps baseline (~$30–50/month, fixed, amortized across all traffic), developer time. What it includes: the vision call that auto-picks each character's voice, the Claude Haiku call that writes the scene brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  The teardown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Per scene
&lt;/h3&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;Model&lt;/th&gt;
&lt;th&gt;Per call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyframe (solo character)&lt;/td&gt;
&lt;td&gt;fal-ai/flux-lora&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyframe (multi-character)&lt;/td&gt;
&lt;td&gt;fal-ai/flux-pro/kont&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyframe (anchor scene)&lt;/td&gt;
&lt;td&gt;FLUX Kontext Pro&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated clip (100 frames, ~6s)&lt;/td&gt;
&lt;td&gt;fal-ai/wan-i2v&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice line&lt;/td&gt;
&lt;td&gt;Kokoro TTS&lt;/td&gt;
&lt;td&gt;$0.005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt sanitization&lt;/td&gt;
&lt;td&gt;Claude Haiku&lt;/td&gt;
&lt;td&gt;$0.0008&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per multi-character scene: about &lt;strong&gt;$0.555&lt;/strong&gt;. Four scenes: &lt;strong&gt;$2.22&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth noting: fal's posted price for wan-i2v at 720r clip, not per second. The 1.25× multiplier kicks in for clips over 81 frames. My pipeline requests 16 fps) per scene to give the audio room, which lands me in the multiplier band at $0.50 per clip. Dropping to 80 frames would save $0.10 per clip and 5 seconds is plenty for most voice lines — that's a real optimization I should run, and I'll get to it below.&lt;/p&gt;

&lt;p&gt;WAN dominates the per-scene line. No surprise — I expected the video model to be the expensive part. What I didn't expect was where&lt;br&gt;
the &lt;em&gt;rest&lt;/em&gt; of the bill came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per character (one-time, amortized)
&lt;/h3&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;Model&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Visual description&lt;/td&gt;
&lt;td&gt;Claude Sonnet (vision)&lt;/td&gt;
&lt;td&gt;$0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style transfer (4 cartoon options)&lt;/td&gt;
&lt;td&gt;FLUX Kontext Pro × 4&lt;/td&gt;
&lt;td&gt;$0.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training augmentations (35 images)&lt;/td&gt;
&lt;td&gt;FLUX Kontext&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoRA fine-tune (1500 steps)&lt;/td&gt;
&lt;td&gt;fal-ai/flux-lora-fast-training&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total per character&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1.975&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the surprise that made me actually write this post: &lt;strong&gt;the augmentation step costs 3.5× more than the LoRA training itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've never built one of these, you might assume LoRA training is the expensive part — it's the line item with "training" in the name. It's not. The expensive line item is generating the 35 cartoon variations you need to &lt;em&gt;feed&lt;/em&gt; the training, because a LoRA fine-tuned on a single source photo overfits horribly and the resulting character looks generic and same-y across scenes.&lt;/p&gt;

&lt;p&gt;So you need pose variation. Expression variation. Lighting variation. Each one costs ~$0.04 to generate via Kontext Pro. Stack 35 of them and you've spent $1.40 before you've trained a single weight.&lt;/p&gt;

&lt;p&gt;I generate 20 variations from the cartoon style image (poses, expressions) plus 15 variations from the original selfie (anchored on the real face, to counterbalance the cartoon-side darkening of skin tone that I observed when I had a more skewed mix). That 20+15 split is what makes the LoRA actually produce a recognizable person.&lt;/p&gt;

&lt;p&gt;It's the hidden cost nobody flags when they talk about "LoRA fine-tuning is cheap now."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation
&lt;/h2&gt;

&lt;p&gt;For a typical 4-scene, 2-character video, amortized over 5 videos per character:&lt;/p&gt;

&lt;p&gt;Per-character setup (amortized):   2 × $1.975 / 5  =  $0.79&lt;br&gt;
Per-scene (4 × $0.555):                            =  $2.22&lt;br&gt;
Per-project brief:                                &amp;lt;  $0.01&lt;br&gt;
────────────────────────────────&lt;br&gt;
Total per video                                    ≈  $3.02&lt;br&gt;
÷ 24 generated seconds (4 × 6.25s clips)           ≈  $0.126 / sec&lt;br&gt;
÷ 16 finished seconds (after audio-aware trim)     ≈  $0.189 / sec&lt;/p&gt;

&lt;p&gt;So: about &lt;strong&gt;$0.13 per generated second&lt;/strong&gt; (what fal/Replicate/Anthropic invoice me for), or &lt;strong&gt;$0.19 per finished second&lt;/strong&gt; (what a&lt;br&gt;
viewer actually experiences). Both are real; the finished-second number is the more honest headline because it's what the user gets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Share of the bill
&lt;/h3&gt;

&lt;p&gt;This is the part I think is actually useful:&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;$ / video&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAN i2v (4 clips)&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Augmentation (amortized, 2 characters)&lt;/td&gt;
&lt;td&gt;$0.56&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoRA training (amortized)&lt;/td&gt;
&lt;td&gt;$0.16&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Kontext keyframes (4 scenes)&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style transfer + vision describe (amortized)&lt;/td&gt;
&lt;td&gt;$0.07&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kokoro voice lines (4 scenes)&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything else (LLM, moderation)&lt;/td&gt;
&lt;td&gt;&amp;lt;$0.01&lt;/td&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The video model is the biggest line. It's not the only line, and it doesn't dominate the way I expected. Augmentation alone is almost a fifth of the bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amortization range
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Videos / character&lt;/th&gt;
&lt;th&gt;Per generated sec (24s)&lt;/th&gt;
&lt;th&gt;Per finished sec (16s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (single video, new character)&lt;/td&gt;
&lt;td&gt;~$0.26&lt;/td&gt;
&lt;td&gt;~$0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;~$0.15&lt;/td&gt;
&lt;td&gt;~$0.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 (my measured average)&lt;/td&gt;
&lt;td&gt;~$0.13&lt;/td&gt;
&lt;td&gt;~$0.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;~$0.11&lt;/td&gt;
&lt;td&gt;~$0.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;~$0.10&lt;/td&gt;
&lt;td&gt;~$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The generated-second column is what your accountant cares about (it matches the invoice). The finished-second column is what the user experiences. They diverge because each WAN clip generates ~6 seconds but the concat step trims to audio-length + 0.5s — most voice lines come back at 3-4 seconds, so a lot of generated frames get cut.&lt;/p&gt;

&lt;p&gt;Shape worth noticing: going from 5 to 10 videos only saves $0.02–0.03/sec. Going from 1 to 2 saves about $0.08/sec. The biggest unit-economic win is getting a user to make their &lt;em&gt;second&lt;/em&gt; video on an existing character, not their tenth. Most of the product features I've been building (preset scenes, group videos, "make a sequel") are essentially shaped by that math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective vs. sticker cost
&lt;/h2&gt;

&lt;p&gt;In my logs, scene image generation fails on the first try roughly 1 in 8–10 attempts. Two main causes: transient 5xx from fal, and WAN's content filter rejecting a scene description with action-y or fight-y language even after my Claude Haiku rewriter swaps the trigger words. With the rewriter, the practical reroll rate is about &lt;strong&gt;10–12%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So effective cost is ~1.10–1.12× sticker. Not nothing, but not the 1.4× you'd get with a stricter video model. If I were on Sora 2 Pro or early Veo this multiplier would be much bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this stacks up against just the video model
&lt;/h2&gt;

&lt;p&gt;For raw per-second video-model pricing as of mid-20&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;Posted price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seedance 1.5 Pro&lt;/td&gt;
&lt;td&gt;~$0.025/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 3.0&lt;/td&gt;
&lt;td&gt;~$0.029/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runway Gen-4 Turbo&lt;/td&gt;
&lt;td&gt;~$0.05/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sora 2 base&lt;/td&gt;
&lt;td&gt;~$0.10/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Veo 3.1 Fast&lt;/td&gt;
&lt;td&gt;~$0.10–$0.15/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Veo 3.1 Standard&lt;/td&gt;
&lt;td&gt;~$0.40/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sora 2 Pro&lt;/td&gt;
&lt;td&gt;~$0.30–$0.50/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAN 2.1 i2v (720p, ≤81 frames)&lt;/td&gt;
&lt;td&gt;$0.40 / clip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAN 2.1 i2v (720p, 82–100 frames)&lt;/td&gt;
&lt;td&gt;$0.50 / clip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on WAN pricing: fal bills it per clip, not per second. At 720p the base is $0.40 per clip, but clips over 81 frames incur a 1.25× multiplier — $0.50 per clip. My pipeline requests 100 frames per scene (~6.25 seconds raw) so I'm in the multiplier band. A clip generates 5-6 seconds of footage but I pay the same regardless of how short the finished cut is. Roundup posts that quote $0.04–$0.08/sec for WAN are usually referring to the 480p variant ($0.20/clip, halving the price) or dividing $0.40/clip by the maximum frame count rather than what the model actually emits at default settings.&lt;/p&gt;

&lt;p&gt;So in raw video-model terms my pipeline is mid-range — about Sora 2 base, cheaper than Veo Standard, more expensive than Kling. The all-in cost works out to roughly &lt;strong&gt;1.5–2× the video model alone&lt;/strong&gt;. That overhead is structural to a multi-model personalization stack: keyframe conditioning, per-character training, voice, vision, brief.&lt;/p&gt;

&lt;p&gt;Migrating to Seedance would save ~$0.04/sec on the video line and leave the other overhead untouched. The optimization question isn't "which cheaper video model?" — it's "how much can I cut from the wrapper around it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually optimize
&lt;/h2&gt;

&lt;p&gt;Honest, in rough order of impact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cut augmentation calls.&lt;/strong&gt; Biggest non-video line item and the most room. Replacing the 35-image Kontext-Pro augmentation set with a Flux LoRA training pass directly on the source + selected style image would save ~$1.40 per character. The trade-off is real (less expression range in the LoRA) — that's the next A/B I want to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop WAN num_frames from 100 to 80.&lt;/strong&gt; Per fal's posted pricing, 720p clips over 81 frames pay a 1.25× multiplier — so I'm paying $0.50 when I could be paying $0.40. The audio-aware trim downstream means my finished scenes rarely exceed 5 seconds anyway. Net savings: $0.40 per video, ~$0.025/finished second. This is the easiest unit-economic win in the whole stack and I have no excuse for not having shipped it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise videos-per-character.&lt;/strong&gt; Every additional video on an existing character drops per-second cost by ~$0.02/sec. Product features that bring users back to existing characters have a direct unit-economic lever. Cheaper than optimizing models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't touch TTS.&lt;/strong&gt; Kokoro is $0.005/scene. Anything cheaper would be rounding error and Kokoro sounds better than the alternatives at this price.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I would &lt;em&gt;not&lt;/em&gt; spend time on: switching the video model. Cheaper options exist but the headroom isn't in the model — it's in the&lt;br&gt;
 conditioning and training around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it if you want
&lt;/h2&gt;

&lt;p&gt;If you want to see what $0.19/finished second looks like as an actual video — and figure out whether my math is right — the product is at &lt;a href="https://www.atveanimation.com" rel="noopener noreferrer"&gt;atveanimation.com&lt;/a&gt;. Upload a photo, pick a style, hit generate. Free tier gives you 10 scenes a day, which is enough for two short videos.&lt;/p&gt;

&lt;p&gt;If you find a way to crash the augmentation step or rack up a $20 bill on a single account, please tell me. I'd genuinely like to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this teardown is novel — anyone with a billing dashboard and a calculator can produce one. But I hadn't seen one written publicly for a WAN + Kontext + flux-lora + Kokoro stack, and the augmentation surprise (3.5× the cost of the LoRA training it feeds) was non-obvious enough to me, after a year of building this, that it probably warranted writing down.&lt;/p&gt;

&lt;p&gt;If you spot something off in the numbers, the comments are open. I'll fix the post rather than defend it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Posted from my own desk on a Friday afternoon. Numbers reconcile to the nearest cent; if they don't reconcile to yours I'd love to know why.&lt;/em&gt;&lt;/p&gt;

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