Repository navigation
Implement TeaCache - #12652
Implement TeaCache #12652LawJarp-A wants to merge 47 commits into
Conversation
|
Work done
Waiting for feedback and review :) |
|
Hi @sayakpaul @dhruvrnaik any updates? |
|
@LawJarp-A sorry about the delay on our end. @DN6 will review it soon. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Hi @LawJarp-A I think we would need TeaCache to be implemented in a model agnostic way in order to merge the PR. The First Block Cache implementation is a good reference for this. |
Yep @DN6 , I agree, I wanted to first implement it just for a single model and get feedback on that before I work on Model agnostic full implementation. I'm sort of working on it, didn't push it yet. I'll take a look at First block cache for reference as well. |
|
@DN6 updated it in a more model agnostic way. |
…th auto-detection
|
Added multi model support, testing it thoroughly though. |
|
Hi @DN6 @sayakpaul
In the meantime any feedback would be appreciated |
|
Thanks @LawJarp-A!
You can refer to #12569 for testing
Yes, I think that is informative for users. |
sayakpaul
left a comment
There was a problem hiding this comment.
Some initial feedback. Most important question is it seems like we need to craft different logic based on different model? Can we not keep it model agnostic?
|
I am trying to think if ways we can avoid having the forward model for each model now. Initially that seemed like th ebe
t was fine when I wrote for flux, but lumina needed multi stage preprocessing. |
|
@LawJarp-A sounds good. Let us know whenever you're ready. |
|
@LawJarp-A let us know. |
Yessir. Will ping here |
|
Hi @sayakpaul, here’s a quick update addressing the remaining review comments. Fixes in the latest commit:
Responses to open comments:
|
But we can handle through pure tensors no? Problem with .item() is that it becomes data-dependent and that wouldn't be known during runtime. Sorry for the delay. |
would torch.cond address this? it keeps the predicae as a 0-dim bool tensor and traces both branches, so no graph break, but it needs state mutation moved out of the branches (gating the residual update with torch.where), which is a non-trivial refactor across the four per-model forwards. Happy to do it if that's the direction. The lighter alternative is matching what first_block_cache.py:133 and mag_cache.py:300 do today, wrap the decision callsite with @torch.compiler.disable so the rest of the forward still compiles. |
# Conflicts: # src/diffusers/hooks/__init__.py # src/diffusers/models/cache_utils.py
|
@sayakpaul @DN6 any more comments to address? |
TeaCache was bundled inline in the model forward and threaded through every guidance branch of the pipeline denoising loop. Per reviewer guidance, drop it: diffusers already ships FirstBlockCache via CacheMixin and a TeaCache backend is in progress (huggingface#12652), so caching should live in the model framework, not be hand-rolled here. Remove the model-side enable_teacache / teacache_params / rescale-coefficient state and the cache short-circuit in the single-stream stage, the per-condition TeaCacheParams bookkeeping in the pipeline loop, and delete the now-unused utils/teacache_util.py. TeaCache defaulted to off, so the default inference path is unchanged. Addresses reviewer comments r3470572315 / r3470616028 / r3470619173 / r3470928656. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@sayakpaul / @DN6 gentle ping on this one. I also had some suggestions from claude, which i want to review and implement (full disclosure) |
The skip/compute decision is data-dependent and has to become a Python bool for control flow. Mark the decision helpers with torch.compiler.disable so the graph break is contained there and the surrounding forward still compiles, matching first_block_cache.py, taylorseer_cache.py and mag_cache.py, which make the same decision the same way.
|
@sayakpaul rebased onto main, and answered the the decision helpers now carry branch no longer conflicts, 27 teacache tests pass, ruff clean. one still open: the @AtulJoshi1206 rebase is done, thanks for the nudge. |
|
Hi @LawJarp-A. Sorry for the delay on our end. I think one thing about the current design that in hindsight isn't sustainable is the custom forwards defined for each model. Ensuring there is no drift between cached forwards and the ones on main will be difficult, so I think it would be best to use the Head/Block hook approach, similar to MagCache and FirstBlockCache diffusers/src/diffusers/hooks/mag_cache.py Line 171 in ae2e4c7 It would also be good to provide a way for users to calibrate the coefficients for a new model like Magcache. |
|
Hi @LawJarp-A, a small thing I ran into while testing this with The Guarding the calls with The commit also adds a test that compiles the Flux forward through those three state transitions and asserts there are no logger graph breaks, using The code will probably change with the Head/Block hook approach, so I'm not opening a separate PR. It might be worth keeping in mind for any logging in the new hooks. Happy to adapt the test once the new version is up. |
GiuseppeBellamacina
left a comment
There was a problem hiding this comment.
This adds a TeaCache hook that skips transformer blocks and reuses the cached residual when the modulated input changes little between steps. It covers Flux, Mochi, Lumina2 and CogVideoX, and also changes StateManager to fall back to a default context.
I read through the hook and the per-model forward paths. I have three points, two about when caching actually applies and one about LoRA, and they're in the inline comments below.
| # handle counting internally via the per-cache-key state | ||
| has_both_lengths = state.cond_seq_len != state.uncond_seq_len | ||
| is_cond_pass = cache_key == state.cond_seq_len | ||
| if has_both_lengths and is_cond_pass: |
There was a problem hiding this comment.
For Lumina2, state.cnt only increments when has_both_lengths and is_cond_pass. Without CFG, or when cond and uncond have the same padded length, cnt stays at 0. The first-step check would then always be true, and the cache would never be used. The comment at lines 852-855 says caching decisions still work in that case. Can you confirm that, or count steps independently of CFG, for example once per forward when only one length is seen?
| if self.config.num_inference_steps is not None: | ||
| new_num_steps = self.config.num_inference_steps | ||
| elif self.config.num_inference_steps_callback is not None: | ||
| new_num_steps = self.config.num_inference_steps_callback() | ||
| elif hasattr(module, "num_steps"): | ||
| new_num_steps = module.num_steps |
There was a problem hiding this comment.
num_steps only comes from config.num_inference_steps, the callback, or a module.num_steps attribute. I couldn't find any transformer that defines that attribute, so with the default TeaCacheConfig(), num_steps stays 0. Then the reset at line 409 never fires and the last step is never forced to compute. A second pipeline call would reuse the previous run's residual and counters. The docstring on num_inference_steps (line 252) says "Auto-detected if not provided". Could this resolve num_steps from the pipeline or scheduler, or warn when it's unset?
There was a problem hiding this comment.
The Flux replacement forward passes joint_attention_kwargs straight to the blocks and never extracts or applies the LoRA scale, as the original forward does through its LoRA-scale handling. With a LoRA loaded and TeaCache enabled, I think the scale would be ignored. Could you apply the same scaling here, or tell me if I'm missing where it happens?

What does this PR do?
What is TeaCache?
TeaCache (Timestep Embedding Aware Cache) is a training-free caching technique that speeds up diffusion model inference by 1.5x-2.6x by reusing transformer block computations when consecutive timestep embeddings are similar.
Architecture & Design
TeaCache uses a
ModelHookto intercept transformer forward passes without modifying model code. The algorithm:c[0]*x^4 + c[1]*x^3 + c[2]*x^2 + c[3]*x + c[4]Key Design Features:
HookRegistryandCacheMixinfor lifecycle managementStateManagerwith context-aware state for CFG conditional/unconditional branchesSupported Models
All models support automatic coefficient detection based on model class name and config path. Custom coefficients can also be provided via
TeaCacheConfig.Benchmark Results (FLUX.1-dev)
Benchmark Results (Lumina2)
Benchmark Results (CogVideoX-2b)
Benchmark Results (Mochi)
Test Hardware: NVIDIA h100
Framework: Diffusers with TeaCache hooks
All tests: Same seed (42) for reproducibility
Usage
Configuration Options
The
TeaCacheConfigsupports the following parameters:rel_l1_thresh(float, default=0.2): Threshold for accumulated relative L1 distance. Recommended values: 0.25 for ~1.5x speedup, 0.4 for ~1.8x, 0.6 for ~2.0x. Mochi models require lower thresholds (0.06-0.09).coefficients(List[float], optional): Polynomial coefficients for rescaling L1 distance. Auto-detected based on model type if not provided.num_inference_steps(int, optional): Total inference steps. Ensures first/last timesteps are always computed. Auto-detected if not provided.num_inference_steps_callback(Callable[[], int], optional): Callback returning total inference steps. Alternative tonum_inference_steps.current_timestep_callback(Callable[[], int], optional): Callback returning current timestep. Used for debugging/statistics.Files Changed
src/diffusers/hooks/teacache.py- Core implementation with model-specific forward functionssrc/diffusers/models/cache_utils.py- CacheMixin integrationsrc/diffusers/hooks/__init__.py- Export TeaCacheConfig and apply_teacachetests/hooks/test_teacache.py- Comprehensive unit testsFixes # (issue)
#12589
#12635
Before submitting
documentation guidelines, and
here are tips on formatting docstrings.
Who can review?
@sayakpaul @yiyixuxu @DN6