Skip to content
8 changes: 4 additions & 4 deletions docs/design/feature/teacache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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):

Copy link
Copy Markdown

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 :275 def load_pipeline(model_path: str, device: str, dtype: torch.dtype) -> Any: returning :279 pipeline = 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:198 self.pipeline = adapter.load_pipeline(model_path, device, dtype) and :217 self.pipeline.forward(DiffusionRequestBatch(requests=[req])). Base class, unchanged by this diff: coefficient_estimator.py:78-80 loader = DiffusersPipelineLoader(LoadConfig(), od_config=od_config) / return loader.load_model(load_device=device); and :131-134 class ZImageAdapter(DefaultAdapter): with only model_class_name = "ZImagePipeline" (no load_pipeline override).

Suggestion: class YourModelAdapter(DefaultAdapter):
"""Adapter for coefficient estimation on your model."""

model_class_name = "YourModelPipeline"

@staticmethod
def get_transformer(pipeline: Any) -> tuple[Any, str]:
    """Extract transformer from pipeline."""
    return pipeline.transformer, "YourTransformer2DModel"

Register your adapter

_MODEL_ADAPTERS["YourModel"] = YourModelAdapter

"""Adapter for coefficient estimation on your model."""

@staticmethod
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 2. **Compare performance** - Measure speedup vs baseline (expect up to 1.5x-2.0x on 50-step runs; less on few-step distilled models) — this is the line this diff reworded (hunk @@ -398,7 +398,7 @@, old text 'expect 1.5x-2.0x'); the next line is unchanged by the diff, read in the PR-time tree: docs/design/feature/teacache.md:402 3. **Verify output quality** - Visually compare cached vs uncached outputs (should be nearly identical). Corroboration added by this same diff (not pre-existing): vllm_omni/diffusion/cache/teacache/config.py:60-63 With the default rel_l1_thresh=0.2 Z-Image-Turbo skips / about half of its steps at 50 steps (~1.5x) and few at 25 steps (~1.1x), / because the distilled model changes >20% per step early on; raising the / threshold to 0.25-0.30 buys ~10% more speed for a visible quality drop. The SSIM sweep figures cited in the comment (0.902 mean / 0.762 min at 0.20; 1.00x at 0.15) are from the PR discussion, not the repo: a tree-wide grep for 0.902|0.762|SSIM returns no hit in either teacache doc or config.py, so those exact numbers rest on the discussion thread, which a reviewer must check on GitHub.

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).
Expand Down
14 changes: 7 additions & 7 deletions docs/user_guide/diffusion/cache_acceleration/teacache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 - Scenarios where a model-dependent 1.2-2x speedup is valuable (this diff rewrites it from '- Scenarios where 1.5-2x speedup is valuable'); same file, same diff, teacache.md:193 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; corroborated in the same diff by vllm_omni/diffusion/cache/teacache/config.py:61 about half of its steps at 50 steps (~1.5x) and few at 25 steps (~1.1x),. Both doc lines are newly rewritten by this PR, so the 1.2x floor vs ~1.1x mismatch is introduced here, not pre-existing.

Suggestion: - Scenarios where a model-dependent 1.1-2x speedup is valuable

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 - Scenarios where a model-dependent 1.2-2x speedup is valuable vs same file :193 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`.

Suggestion: - Scenarios where a model-dependent 1.1-2x speedup is valuable

- Useful for single-card acceleration

**Not for:**
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 # served from cache despite being a large step. Validated only on / # Z-Image-Turbo; other checkpoints that reach TeaCache through this class / # name inherit these values uncalibrated. (scope caveat lives only in a code comment). Unchanged by this diff, present in the PR-time tree — vllm_omni/diffusion/models/z_image/pipeline_z_image.py:228 self.transformer = ZImageTransformer2DModel(quant_config=od_config.quantization_config) (unconditional, both checkpoints); vllm_omni/diffusion/registry.py:63-67 "ZImagePipeline": ("z_image", "pipeline_z_image", "ZImagePipeline",) (single pipeline, no Turbo variant); vllm_omni/diffusion/cache/teacache/backend.py:173 transformer_type = transformer.__class__.__name__ (auto-detect keys _MODEL_COEFFICIENTS lookup); docs/user_guide/diffusion_features.md:171 | **Z-Image** | ✅ | ✅ | ✅ | ... (supported-models row with no calibration-scope note). Anchor: docs/user_guide/diffusion/cache_acceleration/teacache.md:193 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.

Suggestion: 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. Coefficients are calibrated on Z-Image-Turbo only; the base Z-Image checkpoint inherits them uncalibrated.

2. ✅ **(Optional) Customize** - Adjust thresholds and polynomial coefficients for specific speed/quality trade-offs
44 changes: 44 additions & 0 deletions tests/diffusion/cache/test_teacache_config.py
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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 pytestmark = [pytest.mark.core_model, pytest.mark.cpu] (line 16), but the Test Plan cites the L2 job 'Diffusion · Cache Test' as covering evidence, and that job runs timeout 15m pytest -sv tests/diffusion/cache -m 'core_model and cuda' --run-level "core_model" (.buildkite/cuda/test-ready.yml:63) — a selector that collects none of this module's items, since they are cpu-marked and no hook auto-adds cuda (the only collection hook adds xdist groups only; cuda marks come solely from the @hardware_test decorator, which this module doesn't use). The tests do run per-PR via the L1 CPU lane pytest -sv tests/diffusion -m 'core_model and cpu' (.buildkite/cuda/test-ready.yml:31, mirrored on AMD at .buildkite/amd/test-amd-merge.yml:46), and the author's direct run passed 3, so coverage exists — only the plan's attribution is wrong. Correct the Test Plan bullet to name that CPU lane (or keep the direct pytest invocation) so the plan matches what CI actually executes for this file.

Evidence: tests/diffusion/cache/test_teacache_config.py:16 pytestmark = [pytest.mark.core_model, pytest.mark.cpu] — the module requests CPU, not CUDA. Decisive CI rule, unchanged by this diff, present in the PR-time tree: .buildkite/cuda/test-ready.yml:61-63 — label "Diffusion · Cache Test", command timeout 15m pytest -sv tests/diffusion/cache -m 'core_model and cuda' --run-level "core_model", which cannot match cpu-only items. No auto-marking undermines this: tests/helpers/fixtures/pytest_collection.py:16-30 (only collection hook in tests/) does item.add_marker(pytest.mark.xdist_group(name="serial")) for unmarked tests and nothing else; tests/helpers/mark.py:160 marks.extend([pytest.mark.cuda, _cards_mark(num_cards)]) attaches cuda only inside the opt-in hardware_marks/hardware_test decorators, unused by the new module. Covering lane, also unchanged by this diff: .buildkite/cuda/test-ready.yml:31 pytest -sv tests/diffusion -m 'core_model and cpu' (label 'Simple · Diffusion Test' at :28) selects the module, and .buildkite/amd/test-amd-merge.yml:46 runs the AMD equivalent over tests/diffusion with the cpu selector.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 core_model, cpu (line 16), but the Test Plan cites the 'Diffusion · Cache Test' ready-lane job as CI validation — that lane runs pytest -sv tests/diffusion/cache -m 'core_model and cuda' (.buildkite/cuda/test-ready.yml:63) and tools/run_ready_jobs.sh replays test-ready.yml's pytest commands verbatim (tools/run_ready_jobs.sh:5,44), so it deselects every test in this cpu-marked module and cannot evidence it. The selector that actually runs this file is the merge-gate lane pytest -sv tests/diffusion -m 'core_model and cpu' (.buildkite/cuda/test-merge.yml:23; AMD equivalent at .buildkite/amd/test-amd-merge.yml:46, which adds card-marker exclusions and ignores but still collects this module). Cite that lane (or a direct run) in the Test Plan so the cited evidence names a command that can collect this module, and note the ready lane deselects it.

Evidence: tests/diffusion/cache/test_teacache_config.py:16 (added by this diff): pytestmark = [pytest.mark.core_model, pytest.mark.cpu] — no test in the module carries a cuda marker. Decisive CI selectors, unchanged by this diff but present in the PR-time tree: .buildkite/cuda/test-ready.yml:61-63 — - label: "Diffusion · Cache Test" / timeout 15m pytest -sv tests/diffusion/cache -m 'core_model and cuda' --run-level "core_model" (the and cuda term deselects this cpu-only module, so a green run of the cited lane proves nothing about it); .buildkite/cuda/test-merge.yml:20,23 — - label: "Simple · Diffusion Test" / "pytest -sv tests/diffusion -m 'core_model and cpu'" (matches both markers and the directory: the lane that actually runs the file); .buildkite/amd/test-amd-merge.yml:46 — "pytest -sv tests/diffusion -m 'core_model and cpu and not (cards_2 or cards_3 or cards_4 or cards_5 or cards_6 or cards_7 or cards_8)' --ignore=tests/diffusion/cache/test_teacache_extractors.py --ignore=tests/diffusion/models/pi05/test_pi05_units.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB" (does not ignore the new file); tools/run_ready_jobs.sh:5,44 — # Extract steps from .buildkite/cuda/test-ready.yml that contain pytest, synthesize / BUILDKITE_REL=".buildkite/cuda/test-ready.yml" (replay consumes that same YAML).



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")
19 changes: 11 additions & 8 deletions vllm_omni/diffusion/cache/teacache/coefficient_estimator.py
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
Expand All @@ -25,13 +25,9 @@ class DataCollectionHook(ModelHook):
def __init__(self, transformer_type: str):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 self.extractor_fn = get_extractor(transformer_type) — inside init (lines 25-29), changed by this diff from None-until-initialize_hook. vllm_omni/diffusion/cache/teacache/coefficient_estimator.py:20-49 — class DataCollectionHook defines only init/new_forward/start_collection/stop_collection; no initialize_hook override. Unchanged by this diff, present in the PR-time tree: vllm_omni/diffusion/hooks/base.py:68 def initialize_hook(self, module: nn.Module) -> nn.Module: (the base fallback now used). vllm_omni/diffusion/cache/teacache/coefficient_estimator.py:200 self.hook = DataCollectionHook(self.transformer_type) — sole instantiation in the repo; its surrounding init line 198 self.pipeline = adapter.load_pipeline(model_path, device, dtype) shows the estimator path cannot serve as a CPU unit test. Unchanged by this diff, present in the PR-time tree: tests/diffusion/cache/test_teacache_extractors.py:777-783 hook = TeaCacheHook( ... hook.initialize_hook(minimax_h3_module) (likewise lines 944 and 1002), and vllm_omni/diffusion/cache/teacache/hook.py:91 def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module: (TeaCacheHook's override remains). Repo-wide grep confirms DataCollectionHook and TeaCacheCoefficientEstimator appear in no file under tests/ — the no-coverage claim holds; note the draft's 'only hook-lifecycle test in the repo' is overbroad since tests/diffusion/distributed/test_sp_plan_hooks.py:734 and tests/diffusion/offloader/test_layerwise_backend.py:97 also call initialize_hook on unrelated hooks.

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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 23 additions & 8 deletions vllm_omni/diffusion/cache/teacache/config.py
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

from dataclasses import dataclass

Expand Down Expand Up @@ -46,13 +46,27 @@
7.61309272e-01,
],
# Z-Image transformer coefficients
# Copied from Qwen-Image, need to be tuned specifically for Z-Image in future
# Calibrated on Z-Image-Turbo with ZImageAdapter + the TeaCacheCoefficientEstimator
# hook path (per-step rel-L1 computed on the GPU instead of storing full CPU
# trajectories): 70 Parti prompts x {25, 50} steps, 1024x1024, guidance_scale=4.0,
# 5,110 step pairs, 4th-order fit (R^2 0.69). Measured input rel_l1 range
# 0.024-0.336 (mean 0.075); the quartic peaks near x~0.28 and crosses zero
# near x~0.40, so it is only valid inside that range: the hook applies it
# unclamped, and a step with input distance ~0.40 would rescale to ~0 and be
# served from cache despite being a large step. Validated only on
# Z-Image-Turbo; other checkpoints that reach TeaCache through this class
# name inherit these values uncalibrated. The previous Qwen-Image
# placeholder predicted ~1/3 of the measured output change and over-skipped
# steps (see #8270). With the default rel_l1_thresh=0.2 Z-Image-Turbo skips
# about half of its steps at 50 steps (~1.5x) and few at 25 steps (~1.1x),
# because the distilled model changes >20% per step early on; raising the
# threshold to 0.25-0.30 buys ~10% more speed for a visible quality drop.
"ZImageTransformer2DModel": [
-4.50000000e02,
2.80000000e02,
-4.50000000e01,
3.20000000e00,
-2.00000000e-02,
-7.54613422e01,
-9.23596156e01,
5.75318402e01,
-3.76790311e00,
2.27176809e-01,
],
# Estimated TeaCache polynomial coefficients for StableAudioDiTModel.
"StableAudioDiTModel": [
Expand All @@ -73,7 +87,8 @@
6.78098549e-01,
],
# Flux2 transformer coefficients
# Copied from Qwen-Image, need to be tuned specifically for Flux2 in future
# Copied from Qwen-Image, still uncalibrated; tracked in #8411 together with
# the Flux2Klein borrow and the Ming subclasses of ZImageTransformer2DModel.
"Flux2Transformer2DModel": [
-4.50000000e02,
2.80000000e02,
Expand Down
Loading