Repository navigation
[Bugfix] Calibrate Z-Image TeaCache coefficients and add ZImageAdapter #8408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
974481c
f25abe6
50f3b2f
4f253f6
a4800c2
beef467
7fade11
be82914
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ This section describes how to add TeaCache to a diffusion transformer model. We | |
|
|
||
| ### What is TeaCache? | ||
|
|
||
| TeaCache speeds up diffusion inference by caching transformer block computations when consecutive timesteps are similar. It provides **1.5x-2.0x speedup** with minimal quality loss. | ||
| TeaCache speeds up diffusion inference by caching transformer block computations when consecutive timesteps are similar. It provides up to **1.5x-2.0x speedup** with minimal quality loss on models with calibrated coefficients; few-step distilled models gain less. | ||
|
|
||
| The core insight is that the modulated input (after normalization and timestep conditioning) changes gradually across timesteps. By measuring the L1 distance between consecutive modulated inputs and comparing it to a threshold, TeaCache decides whether to execute the full transformer blocks or reuse the cached residual from the previous step. | ||
|
|
||
|
|
@@ -260,15 +260,15 @@ The polynomial coefficients rescale L1 distances between consecutive modulated i | |
|
|
||
| | Approach | Performance | Effort | | ||
| |----------|-------------|--------| | ||
| | Using defaults from similar model | Within 5-10% of optimal | Low | | ||
| | Using defaults from similar model | Unreliable: Z-Image with Qwen-Image's coefficients under-predicted output change ~3x and over-skipped steps (#8270); treat as a placeholder and calibrate | Low | | ||
| | Estimating custom coefficients | Best performance | Medium | | ||
|
|
||
| #### Implement Data Collection Adapter | ||
|
|
||
| Add an adapter in `vllm_omni/diffusion/cache/teacache/coefficient_estimator.py`: | ||
|
|
||
| ```python | ||
| class YourModelAdapter: | ||
| class YourModelAdapter(DefaultAdapter): | ||
| """Adapter for coefficient estimation on your model.""" | ||
|
|
||
| @staticmethod | ||
|
|
@@ -398,7 +398,7 @@ images = omni.generate( | |
| **Verify:** | ||
|
|
||
| 1. **Check logs** - Look for TeaCache initialization messages | ||
| 2. **Compare performance** - Measure speedup vs baseline (expect 1.5x-2.0x) | ||
| 2. **Compare performance** - Measure speedup vs baseline (expect up to 1.5x-2.0x on 50-step runs; less on few-step distilled models) | ||
| 3. **Verify output quality** - Visually compare cached vs uncached outputs (should be nearly identical) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] This diff qualifies the checklist's speedup bullet (line 401 now reads 'expect… This diff qualifies the checklist's speedup bullet (line 401 now reads 'expect up to 1.5x-2.0x on 50-step runs; less on few-step distilled models') but leaves line 402's verification step asserting the cached output 'should be nearly identical' unconditionally. The PR's own sweep (discussion table) contradicts that at the shipped default operating point (rel_l1_thresh 0.2, Z-Image-Turbo, 50 steps): mean SSIM 0.902, min 0.762 vs uncached — visibly different; 'nearly identical' holds only at 0.15, where nothing is skipped (1.00x). The diff's own calibration note concedes the same direction: ~half the steps are already skipped at 0.2/50 steps and 'raising the threshold to 0.25-0.30 buys ~10% more speed for a visible quality drop' (config.py). Qualify the step, e.g. '(nearly identical at conservative thresholds; expect visible drift at the default threshold on few-step distilled models)'. Evidence: docs/design/feature/teacache.md:401 Suggestion: 3. Verify output quality - Visually compare cached vs uncached outputs (nearly identical at conservative thresholds; expect visible drift at the default threshold on few-step distilled models) |
||
|
|
||
| See more detailed examples in [user guide for teacache](../../user_guide/diffusion/cache_acceleration/teacache.md). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
|
|
||
| ## Overview | ||
|
|
||
| TeaCache accelerates diffusion model inference by caching transformer computations when consecutive timesteps are similar, providing **1.5x-2.0x speedup** with minimal quality loss. It dynamically decides whether to reuse cached outputs based on input similarity, making it ideal for production deployments where inference speed matters without sacrificing generation quality. | ||
| TeaCache accelerates diffusion model inference by caching transformer computations when consecutive timesteps are similar, providing up to **1.5x-2.0x speedup** with minimal quality loss on models with calibrated coefficients and enough steps to skip (few-step distilled models gain less; see the Summary). It dynamically decides whether to reuse cached outputs based on input similarity, making it ideal for production deployments where inference speed matters without sacrificing generation quality. | ||
|
|
||
| See supported models list in [Supported Models](../../diffusion_features.md#supported-models). | ||
|
|
||
|
|
@@ -119,7 +119,7 @@ In `OmniDiffusionConfig` | |
|
|
||
| | Parameter | Type | Default | Description | | ||
| |-----------|------|---------|-------------| | ||
| | `rel_l1_thresh` | float | `0.2` | Similarity threshold for cache reuse. Lower values prioritize quality (less caching), higher values prioritize speed (more caching). Suggested range: 0.1-0.8 | | ||
| | `rel_l1_thresh` | float | `0.2` | Similarity threshold for cache reuse. Lower values prioritize quality (less caching), higher values prioritize speed (more caching). Suggested range: 0.1-0.3; higher values mostly cost quality on calibrated models | | ||
| | `coefficients` | list[float] \| None | `None` | Polynomial coefficients for rescaling L1 distance. Must contain exactly 5 elements if provided. If `None`, uses model-specific defaults based on transformer type. | | ||
|
|
||
| Users can find the default model coefficients in [`vllm_omni/diffusion/cache/teacache/config.py`](https://github.com/vllm-project/vllm-omni/blob/main/vllm_omni/diffusion/cache/teacache/config.py), for example: | ||
|
|
@@ -149,7 +149,7 @@ _MODEL_COEFFICIENTS = { | |
| **Good for:** | ||
|
|
||
| - Production deployments requiring faster inference, tolerant of minimal quality loss | ||
| - Scenarios where 1.5-2x speedup is valuable | ||
| - Scenarios where a model-dependent 1.2-2x speedup is valuable | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] This diff rewrites both this 'Good for' bullet and the Summary, and the two hun… This diff rewrites both this 'Good for' bullet and the Summary, and the two hunks now state different ranges: the Summary says to expect ~1.1x on few-step distilled models (Z-Image-Turbo at 25 steps; matching config.py's calibration note), which is below this bullet's 1.2x floor. Lower the floor to 1.1x so both rewritten hunks state one consistent range. Evidence: docs/user_guide/diffusion/cache_acceleration/teacache.md:152 Suggestion: - Scenarios where a model-dependent 1.1-2x speedup is valuable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] This diff rewrites the good-for bullet to 'a model-dependent 1.2-2x speedup' (l… This diff rewrites the good-for bullet to 'a model-dependent 1.2-2x speedup' (line 152), but the Summary the same diff adds (line 193) tells Z-Image-Turbo users at 25 steps to 'expect ~1.1x there' — the bullet's lower bound excludes a case this PR itself documents later in the same file. Lower it to 1.1 so the two sections agree. Evidence: Both decisive lines are added by this diff and read verbatim in the PR-time tree: docs/user_guide/diffusion/cache_acceleration/teacache.md:152 Suggestion: - Scenarios where a model-dependent 1.1-2x speedup is valuable |
||
| - Useful for single-card acceleration | ||
|
|
||
| **Not for:** | ||
|
|
@@ -178,17 +178,17 @@ cache_config={"rel_l1_thresh": 0.1} | |
| **Symptoms**: Actual speedup is less than expected (< 1.3x) | ||
|
|
||
| **Solutions**: | ||
| 1. Increase the threshold to enable more aggressive caching: | ||
| 1. Increase the threshold to enable more aggressive caching (non-distilled models only; on few-step distilled models such as Z-Image-Turbo a higher threshold mostly costs quality, see the Summary): | ||
| ```python | ||
| cache_config={"rel_l1_thresh": 0.8} | ||
| cache_config={"rel_l1_thresh": 0.3} | ||
| ``` | ||
| 2. Ensure you're using sufficient inference steps (35+ recommended) | ||
| 2. Ensure you're using sufficient inference steps (35+ recommended for non-distilled models; few-step distilled models cannot add steps) | ||
| 3. Check that your model architecture is supported (see Supported Models section) | ||
|
|
||
| --- | ||
|
|
||
|
|
||
| ## Summary | ||
|
|
||
| 1. ✅ **Enable TeaCache** - Set `cache_backend="tea_cache"` to get 1.5x-2.0x speedup with optimized defaults | ||
| 1. ✅ **Enable TeaCache** - Set `cache_backend="tea_cache"` to get up to 1.5x-2.0x speedup with optimized defaults on 50-step runs; few-step distilled models (e.g. Z-Image-Turbo at 25 steps) change too much per step to skip many, so expect ~1.1x there | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] This diff replaces the ZImageTransformer2DModel placeholder with coefficients c… This diff replaces the ZImageTransformer2DModel placeholder with coefficients calibrated only on Z-Image-Turbo and states that scope only in a code comment (config.py:56-58: 'Validated only on Z-Image-Turbo; other checkpoints that reach TeaCache through this class name inherit these values uncalibrated'). Both Z-Image checkpoints reach this single entry: pipeline_z_image.py:228 unconditionally constructs ZImageTransformer2DModel (registry.py:63-67 has one ZImagePipeline, no base/Turbo split), and teacache/backend.py:173 auto-detects transformer_type from the class name — so the base Tongyi-MAI/Z-Image checkpoint silently inherits Turbo-fitted values. Neither the user guide Summary nor the supported-models row (docs/user_guide/diffusion_features.md:171) signals this. State the calibration scope where users read — append to the Summary bullet at docs/user_guide/diffusion/cache_acceleration/teacache.md:193: 'Coefficients are calibrated on Z-Image-Turbo only; the base Z-Image checkpoint inherits them uncalibrated.' Evidence: Decisive lines: in-diff — vllm_omni/diffusion/cache/teacache/config.py:56-58 Suggestion: 1. ✅ Enable TeaCache - Set |
||
| 2. ✅ **(Optional) Customize** - Adjust thresholds and polynomial coefficients for specific speed/quality trade-offs | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project | ||
|
|
||
| """CPU tests for TeaCache model-specific defaults (coefficients and estimator adapters).""" | ||
|
|
||
| import pytest | ||
|
|
||
| from vllm_omni.diffusion.cache.teacache.coefficient_estimator import ( | ||
| _MODEL_ADAPTERS, | ||
| DataCollectionHook, | ||
| ZImageAdapter, | ||
| ) | ||
| from vllm_omni.diffusion.cache.teacache.config import _MODEL_COEFFICIENTS, TeaCacheConfig | ||
| from vllm_omni.diffusion.cache.teacache.extractors import extract_zimage_context | ||
|
|
||
| pytestmark = [pytest.mark.core_model, pytest.mark.cpu] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] The new tests/diffusion/cache/test_teacache_config.py carries `pytestmark = [py… The new tests/diffusion/cache/test_teacache_config.py carries Evidence: tests/diffusion/cache/test_teacache_config.py:16 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] This diff adds tests/diffusion/cache/test_teacache_config.py marked `core_model… This diff adds tests/diffusion/cache/test_teacache_config.py marked Evidence: tests/diffusion/cache/test_teacache_config.py:16 (added by this diff): |
||
|
|
||
|
|
||
| def test_zimage_has_calibrated_coefficients(): | ||
| """Z-Image must not fall back to the Qwen-Image placeholder polynomial (#8270).""" | ||
| zimage = _MODEL_COEFFICIENTS["ZImageTransformer2DModel"] | ||
| assert zimage != _MODEL_COEFFICIENTS["QwenImageTransformer2DModel"] | ||
| # Pin the calibrated fit so a recalibration has to update this test deliberately. | ||
| assert zimage == pytest.approx([-7.54613422e01, -9.23596156e01, 5.75318402e01, -3.76790311e00, 2.27176809e-01]) | ||
|
|
||
| config = TeaCacheConfig(transformer_type="ZImageTransformer2DModel") | ||
| assert config.coefficients == zimage | ||
| assert config.rel_l1_thresh == 0.2 | ||
|
|
||
|
|
||
| def test_zimage_estimator_adapter_registered(): | ||
| assert _MODEL_ADAPTERS["ZImage"] is ZImageAdapter | ||
| assert ZImageAdapter.model_class_name == "ZImagePipeline" | ||
| assert ZImageAdapter.uses_tf_config is True | ||
|
|
||
|
|
||
| def test_data_collection_hook_resolves_extractor_at_init(): | ||
| """The estimator hook binds its extractor in __init__, so an unknown type fails before any forward.""" | ||
| hook = DataCollectionHook("ZImageTransformer2DModel") | ||
| assert hook.extractor_fn is extract_zimage_context | ||
| assert hook.current_trajectory == [] | ||
|
|
||
| with pytest.raises(ValueError, match="Unknown model type"): | ||
| DataCollectionHook("NotARegisteredTransformer") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project | ||
|
|
||
| import os | ||
| from typing import Any | ||
|
|
@@ -25,13 +25,9 @@ class DataCollectionHook(ModelHook): | |
| def __init__(self, transformer_type: str): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] The only runtime-machinery change in this diff ships with no test selecting it:… The only runtime-machinery change in this diff ships with no test selecting it: DataCollectionHook now resolves its extractor eagerly in init (coefficient_estimator.py:28) and its initialize_hook override is removed, so it falls back to base ModelHook.initialize_hook (hooks/base.py:68). No test in the repo constructs DataCollectionHook — the sole instantiation is TeaCacheCoefficientEstimator.init (coefficient_estimator.py:200) — and the only cache-hook tests that call initialize_hook exercise TeaCacheHook, whose override remains (test_teacache_extractors.py:777-783; hook.py:91); the SP and offloader hook tests cover unrelated hooks. The PR's new tests cover only TeaCacheConfig.post_init and the _MODEL_ADAPTERS registry. Add a core_model/cpu test that constructs DataCollectionHook(transformer_type=...) directly — going through TeaCacheCoefficientEstimator would load a real pipeline (coefficient_estimator.py:198) — and asserts extractor_fn is resolved at init with no None window before new_forward, so the path this PR changed is exercised by CI. Evidence: vllm_omni/diffusion/cache/teacache/coefficient_estimator.py:28 |
||
| super().__init__() | ||
| self.transformer_type = transformer_type | ||
| self.extractor_fn = None | ||
| self.extractor_fn = get_extractor(transformer_type) | ||
| self.current_trajectory: list[tuple[np.ndarray, np.ndarray]] = [] | ||
|
|
||
| def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module: | ||
| self.extractor_fn = get_extractor(self.transformer_type) | ||
| return module | ||
|
|
||
| def new_forward(self, module: torch.nn.Module, *args: Any, **kwargs: Any) -> Any: | ||
| ctx = self.extractor_fn(module, *args, **kwargs) | ||
| # NOTE: We upcast to float32 to also handle bfloat16. | ||
|
|
@@ -56,7 +52,7 @@ def stop_collection(self) -> list[tuple[np.ndarray, np.ndarray]]: | |
| class DefaultAdapter: | ||
| """Default adapter for standard diffusers pipelines.""" | ||
|
|
||
| model_class_name = None | ||
| model_class_name: str | None = None | ||
| uses_tf_config = True | ||
|
|
||
| @classmethod | ||
|
|
@@ -132,11 +128,18 @@ class StableAudioAdapter(DefaultAdapter): | |
| model_class_name = "StableAudioPipeline" | ||
|
|
||
|
|
||
| _MODEL_ADAPTERS: dict[str, type] = { | ||
| class ZImageAdapter(DefaultAdapter): | ||
| """Adapter for Z-Image coefficient estimation.""" | ||
|
|
||
| model_class_name = "ZImagePipeline" | ||
|
|
||
|
|
||
| _MODEL_ADAPTERS: dict[str, type[DefaultAdapter]] = { | ||
| "Bagel": BagelAdapter, | ||
| "StableAudio": StableAudioAdapter, | ||
| "Flux2": Flux2Adapter, | ||
| "LongCat": LongCatAdapter, | ||
| "ZImage": ZImageAdapter, | ||
| } | ||
|
|
||
| _EPSILON = 1e-6 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] teacache.md:271 now subclasses DefaultAdapter but keeps the example's raw-diffu…
teacache.md:271 now subclasses DefaultAdapter but keeps the example's raw-diffusers load_pipeline override (returning YourModelPipeline.from_pretrained(...) at :279) — exactly what the base class exists to replace: DefaultAdapter.load_pipeline loads through vLLM's DiffusersPipelineLoader (coefficient_estimator.py:78-80). TeaCacheCoefficientEstimator drives whatever load_pipeline returns via self.pipeline.forward(DiffusionRequestBatch(requests=[req])) (coefficient_estimator.py:198, 217), which a raw diffusers pipeline does not implement, so a reader following the example verbatim crashes at the very next 'Collect Data and Estimate' step. All five in-tree adapters rely on the inherited loader — including ZImageAdapter, the pattern this PR just added (coefficient_estimator.py:131-134, sets only model_class_name). Rewrite the example in that minimal pattern (set model_class_name = "YourModelPipeline", keep get_transformer) and drop the load_pipeline and install_hook overrides (the latter duplicates DefaultAdapter verbatim, coefficient_estimator.py:87-89).
Evidence: docs/design/feature/teacache.md:271
class YourModelAdapter(DefaultAdapter):(line changed by this diff) with unchanged-by-this-diff override at :275def load_pipeline(model_path: str, device: str, dtype: torch.dtype) -> Any:returning :279pipeline = YourModelPipeline.from_pretrained(— present in the PR-time tree. Decisive consumer, unchanged by this diff, present in PR-time tree: vllm_omni/diffusion/cache/teacache/coefficient_estimator.py:198self.pipeline = adapter.load_pipeline(model_path, device, dtype)and :217self.pipeline.forward(DiffusionRequestBatch(requests=[req])). Base class, unchanged by this diff: coefficient_estimator.py:78-80loader = DiffusersPipelineLoader(LoadConfig(), od_config=od_config)/return loader.load_model(load_device=device); and :131-134class ZImageAdapter(DefaultAdapter):with onlymodel_class_name = "ZImagePipeline"(no load_pipeline override).Suggestion: class YourModelAdapter(DefaultAdapter):
"""Adapter for coefficient estimation on your model."""
Register your adapter
_MODEL_ADAPTERS["YourModel"] = YourModelAdapter