Why the TeaCache Gate Did Not Engage on Short Distilled FLUX Schedules

Denis Ineshin · 2026-07-24

A negative result from mlx-teacache, and the measurement practice it forced

A wall-clock improvement can be real while its explanation is wrong. In May 2026 the mlx-teacache project shipped, and then corrected, exactly that combination: its TeaCache wrapper ran measurably faster than vanilla mflux on FLUX.2 Klein's distilled 8-step schedule, and the caching mechanism the library exists to provide never fired once. Every eligible denoising step computed in full. Zero steps were skipped. The speed difference therefore could not be attributed to caching; source inspection pointed to an unrelated execution-path difference that the benchmark of the day could not isolate.

This note documents the negative result: why the TeaCache polynomial gate, with its shipped input signal, calibrated fits, and default threshold, did not engage on the tested 8-step distilled Klein trajectories, and why zero skips was consistent with the gate's decision rule rather than evidence of a software fault. It then lays out what can and cannot be concluded about the wall-clock difference, and it closes with the methodological lesson, which generalizes past diffusion caching: a feature's benefit must be attributed by measuring whether the feature engaged, not by assuming the headline mechanism did the work.

The note is documentary. Numbers are source-reported from the project's public calibration files, benchmark reports, and release documentation, cross-checked against the released README, changelog, and per-variant documentation; nothing was rerun for this write-up. The v0.3-era distilled measurements used an M1 Max with 32 GB unified memory, mflux 0.17.5, 4-bit quantization, 512×512 output, a fixed prompt and seed, a warmup, and three timed repetitions per condition in one process. The later v0.6.x controls cited below used the same chip, memory size, mflux version, quantization, resolution, prompt, and seed, but ran each condition and repetition cold in a fresh subprocess and reported the median of three runs.

1. The gate and its premise

TeaCache (arXiv:2411.19108) starts from an empirical property of diffusion sampling: across much of the denoising trajectory, consecutive transformer outputs are nearly the same. The expensive transformer body produces a residual that is added to its input, and that residual stays roughly stable for stretches of adjacent steps. TeaCache trains a small polynomial that predicts how much the body output will change from how much a cheap input signal changed. In the FLUX implementations that signal is the relative-L1 distance of the modulated block-0 input between the current and previous step. The gate accumulates the predicted output change since the last fully computed step. While the accumulated prediction stays under a threshold (rel_l1_thresh, package default 0.20), the wrapper reuses the cached residual and skips the transformer body; when it crosses the threshold, the step computes in full and the accumulator resets.

Where the premise holds, the mechanism does what the paper says. On FLUX.1-dev at 25 steps the gate skips 6 of 25 steps at the default threshold, for a measured 1.46× wall-clock speedup with visually equivalent output (subprocess-isolated harness, three repetitions; a three-way decomposition attributes 1.47× to step-skipping and 1.00× to everything else). Engagement is threshold-banded even there: single-run sweeps at thresholds 0.10 and 0.15 recorded zero skips on the same 25-step schedule. The gate only fires inside a band where the predicted per-step change is small relative to the threshold.

One gate, two schedules: where the skip branch is actually reachedDenoising stepCheap input signal: relative-L1 of the modulatedblock-0 input against the previous stepCalibrated degree-4 polynomialpredicts the body-output change from the signalAccumulator adds the prediction(running total since the last full compute)Accumulated prediction underrel_l1_thresh (default 0.20)?Skip the transformer bodyReuse the cached residual; prelude + tail onlyCompute the step in fullRefresh the cache; reset the accumulator to 0FLUX.1-dev, 25-step schedulePredicted changes stay small for stretches,so the accumulator crawls toward 0.20Measured: 6 of 25 steps skippedFLUX.2 Klein 4B / 9B, 8-step distilled schedulesThe shipped fit predicts above 0.20 across theobserved signal range (0.1316-0.4498 for 9B)Measured: 0 of 8 steps skipped, every repetitionyesnoreached repeatedlythe only branch ever reachedThe decision flow is conceptual structure; the two skip counts are measured, repetition-stable results. The Klein branch summarizes the calibration finding, not a separate measurement.
Figure 1. The gate's per-step decision, and where each tested schedule ends up. The decision flow is conceptual structure; the two skip counts are measured, repetition-stable results. The Klein branch summarizes the section 3 calibration finding (the shipped origin-constrained fit predicts above the 0.20 default threshold over the observed input-signal range), not a separate measurement. The editable PlantUML source is published with the paper.

2. The observation: faster wall clock, zero skips

The project's v0.2.0 release described a "~1.2× speedup" on FLUX.2 Klein 4B as a TeaCache caching outcome. That figure came from a single ad-hoc timing run. No benchmark script was committed, and no skip telemetry was captured alongside the wall clock.

While preparing v0.3.0 (FLUX.2 Klein 9B support), the project committed a reproducible benchmark: scripts/bench_speedup.py at v0.3.0 pins seed, prompt, dimensions, and step count, and records per-repetition skip counts next to the timings. Its first run across the supported variants produced this:

Variant Steps Vanilla median Wrapper median Skipped steps
FLUX.1-dev 25 103.67 s 71.79 s 6 / 25
FLUX.2 Klein 4B (distilled) 8 28.1 s 22.3 s 0 / 8
FLUX.2 Klein 9B (distilled) 8 119.0 s 61.8 s 0 / 8

The load-bearing column is the last one. The skip counts were stable across every repetition: FLUX.1-dev genuinely cached, and both distilled Klein variants computed every eligible step while still finishing ahead of vanilla mflux.

The wall-clock ratios in that table deserve less trust than the skip counts, for reasons the project only fully understood later. These were same-process measurements (all vanilla repetitions, then all wrapper repetitions, in one Python interpreter), and the Klein 9B median in particular combined a thermally throttled vanilla repetition (227 s) with a recovered wrapper repetition (46 s), inflating the apparent ratio to 1.93×. The original raw distilled benchmark report is not public, and those rows have not been repeated under the later cold subprocess harness. This note therefore treats the distilled-row ratios only as evidence that the wrapper was faster in those runs, not as precise or reusable magnitudes. The zero-skip result, by contrast, is recorded in the release documentation as stable across repetitions.

3. Zero skips is the gate working correctly

The v0.3.0 calibration recipe captured, for each variant, 10 prompts × 8 steps at a fixed seed: 70 consecutive-step pairs of (input-signal relative-L1, body-output relative-L1).

For Klein 9B, the empirical body-output change between adjacent steps spans 0.250 to 0.880 (the raw arrays are committed in scripts/_calibration_flux2_klein_9b.json). Read that against the 0.20 default threshold: in the recorded sample, every adjacent pair in the 8-step trajectories changed by at least 25% relative-L1. That empirical floor does not by itself determine what a fitted polynomial predicts between observations. The gate acts on the fitted prediction, not directly on the empirical body-output distance.

The project fit the polynomial twice during the diagnosis:

The shipped fit's above-threshold predictions, together with the recorded zero-skip telemetry, explain the 9B gate outcome at the default threshold. Klein 4B has the same documented outcome: its public summary records an empirical output-change range of 0.261–1.008, and the release documentation records zero skips at the default threshold. Its committed calibration report is summary-only, so this note does not claim a point-by-point reconstruction of the 4B fit.

FLUX.1-schnell is a related but weaker observation. Its public variant page records zero skips at the 4-step default, where the skip-first and skip-last windows leave only two gate-eligible steps. It has no calibration artifact comparable to the Klein 9B raw sample, so it is not part of the quantitative Klein result.

Distillation is a plausible explanation for these observations. A distilled model compresses the denoising trajectory into a handful of consequential strides, which is consistent with larger changes between adjacent steps. This study does not isolate distillation from model family, scheduler, gate signal, polynomial form, or step count, so it does not establish that distillation itself removes all cacheable similarity.

Raising the threshold is not a characterized remedy. Engagement begins only when the threshold exceeds some accumulated fitted predictions; the empirical 0.25 floor is not that decision boundary. No threshold sweep established where skips begin or whether their image quality is acceptable on these schedules. The project therefore kept distilled schedules out of scope for step-skipping.

4. What the evidence says about the speed

mflux 0.17.5 wraps the FLUX.2 predict function in mx.compile on every Apple Silicon chip except base and Pro M1/M2, using its chip-family predicate. To keep the gate's per-step decision live, mlx-teacache replaces that compiled function with an eager Python closure: a compiled function would bake the first step's gate decision into the trace. On chips where mflux compiles, the side effect is that the wrapper also sidesteps the compiled execution path entirely. On the test machine (M1 Max, 4-bit quantization, 8-step schedule) the wrapper's eager path ran faster than vanilla mflux's compiled path in the same-process measurements. Zero skips rules out caching as the cause. Source inspection makes the eager-versus-compiled execution-path difference the leading explanation, but the 8-step benchmark did not isolate that effect from thermal state, condition order, allocator state, or other wrapper differences.

Two later corrections sharpened the attribution.

First, the project rebuilt its benchmark as subprocess-per-repetition: every (variant, condition, repetition) triple runs in a fresh Python interpreter, so each timing starts from a cold state. Under that harness, the eager-versus-compiled difference on 50-step non-distilled Klein recipes contributes only 1.01–1.02×. This shows that the effect is small on those recipes. The rebuilt harness did not remeasure the 8-step distilled rows. Fixed per-step overhead taking a larger share of a short run is a plausible explanation for their larger apparent difference, but the original measurements remain same-process and confounded, so that explanation is a hypothesis rather than a result.

Second, the harness change independently corrected a headline elsewhere in the project: a 2.68× claim on a non-distilled 9B variant collapsed to 1.36× once both conditions ran cold. Under the same-process ordering, the compiled vanilla condition had paid MLX's full cold-start compilation cost, while the wrapper condition, running second, inherited warm allocator state from it. Two separate numbers, one measurement pathology: same-process A/B timing on MLX misattributes.

The corrected pieces establish a narrower picture. In the clean three-way measurements, step-skipping is the dominant mechanism where it engages (1.47× of FLUX.1-dev's 1.46×; 1.22–1.34× of the non-distilled Klein results), while execution-path avoidance contributes only 1.00–1.02× on those 25–50-step recipes. For the earlier 8-step rows, the firm conclusion is only that caching contributed nothing because the skip count was zero; the magnitude and exclusivity of the execution-path effect were not isolated.

5. Why every gate passed anyway

The v0.3.0 finding was not caught by the test suite, and could not have been, because every existing check answered a different question.

The image-quality gates (SSIM against vanilla output) pass trivially when the cache never engages: with zero skips, the wrapper's output differs from vanilla only by kernel-dispatch noise. A quality assertion cannot distinguish "the feature preserved quality" from "the feature did nothing." Nothing in the suite asserted skipped_count > 0.

The library's no-benefit warning checked schedule shape (whether the step count minus the protected windows left any skippable steps), not gate behavior. Klein 9B at 8 steps has five skippable steps in principle, so no warning fired; the polynomial simply never chose to use them.

And until v0.3.0 there was no committed benchmark at all, so the v0.2.0 claim was never re-derivable. A single unreproducible timing fossilized into documentation.

The general form of the failure: a feature can be structurally exercised (the wrapper installs, runs, restores, and produces correct images) while never functionally engaging. Tests that only inspect outputs cannot tell those apart. The fix shipped in v0.3.0 as a reporting change and a policy: skip counts published next to every wall-clock number, an explicit mechanism attribution per benchmark row, and a benchmark script committed for subsequent measurements.

6. Scope, corroboration, and what the project did

The negative result is deliberately narrow. It covers the tested gate signal (modulated block-0 input relative-L1), the shipped degree-4 polynomial fits, the default 0.20 threshold, and the distilled Klein 4B and 9B 8-step recipes on the stated hardware. The separately documented FLUX.1-schnell result has less supporting evidence and is not included in that quantitative scope. This note does not claim that all caching techniques fail on distilled diffusion models. Block-level approaches such as FirstBlockCache (diffusers cache documentation) skip part of every step rather than whole steps, a different premise this project has not tested.

External sources give context, not direct corroboration of the gate result. The upstream ali-vilab/TeaCache repository has no FLUX.2 path, and its issue #83 records an unresolved floating-point exception from a user running FLUX.1-schnell at four steps; it does not establish non-engagement or the same failure mechanism. The TeaCache paper evaluates video models rather than distilled FLUX schedules. NVIDIA's published FLUX.2-dev configuration uses 50 steps and teacache_thresh=0.05, skipping an average of 16 of 50 steps across 20 prompts; it is not a Klein result. Runware exposes TeaCache controls for Klein 4B and Klein 9B, but those pages publish no skip-rate or quality calibration for the controls. None of these linked sources demonstrates step-level caching engaging on a 4–8-step distilled FLUX schedule.

The project's resolution shipped across two releases. v0.3.0 corrected the v0.2.0 mechanism framing in the README and changelog, published skip telemetry alongside every timing, and declared distilled schedules out of scope for step-skipping. Subsequent releases redirected the library to schedules where the premise holds: the non-distilled Klein variants skip 9–13 of 48 active steps at 50-step recipes (1.23× and 1.36× under the cold-isolated harness), and later non-FLUX variants engage similarly. For the tested Klein recipes, the implementation followed its decision rule; the shipped gate configuration and trajectory did not produce a skip.

Claim Status Source
0 skips across all repetitions, Klein 4B and 9B at 8 steps source-reported measurement preserved in release documentation; the original raw report is not public README; changelog
Klein 9B adjacent-step output change 0.250–0.880 committed calibration data (raw arrays) scripts/_calibration_flux2_klein_9b.json
Klein 4B empirical output-change range 0.261–1.008 committed summary; raw per-pair arrays are not public scripts/_calibration_flux2_klein_4b.json
distilled-row wall-clock ratios (1.26×, 1.93×) thermally confounded, same-process era; superseded for attribution changelog; README benchmark footnotes
execution-path contribution 1.01–1.02× at 50 steps subprocess-isolated measurement v0.6.0 benchmark artifacts
Short distilled schedules intrinsically defeat step-level caching hypothesis consistent with these observations; not established by this study bounded evidence above

7. Lessons

Measure whether the feature engaged, not only the wall clock. If a library claims a mechanism, its benchmark must count mechanism events (here, skipped steps) next to every timing. A speedup number without an engagement count cannot attribute anything; this project shipped a wrong attribution for exactly one release cycle, the one where telemetry was missing.

Quality gates pass when the feature is dormant. An output-similarity assertion is satisfied by a no-op. Feature test suites need either an explicit engagement assertion or a documented statement of why zero engagement is the expected outcome for that configuration.

Same-process A/B timing can misattribute on MLX. Thermal drift across sequential conditions inflated one ratio in this project's history; cold-start asymmetry, where one condition pays compilation and allocator warmup that the next condition inherits, inflated another. Cold subprocess isolation per condition and repetition is what made the honest numbers reproducible.

Ship the correction in the product's own voice. The corrected README states, per variant, what mechanism produces the measured win and where the feature does not help. That includes telling users to run distilled Klein through vanilla mflux. A negative result documented where users read is worth more than a quiet fix.

References and source notes


Prepared 2026-07-19. Last updated 2026-07-24. Denis Ineshin.

Buy me a coffee