492 lines
17 KiB
Markdown
492 lines
17 KiB
Markdown
# TeaCache
|
|
|
|
This section describes how to add TeaCache to a diffusion transformer model. We use the Qwen-Image transformer as the reference implementation.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Step-by-Step Implementation](#step-by-step-implementation)
|
|
- [Customization](#customization)
|
|
- [Testing](#testing)
|
|
- [Troubleshooting](#troubleshooting)
|
|
- [Reference Implementations](#reference-implementations)
|
|
- [Summary](#summary)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
vLLM-omni provides a **hook-based** TeaCache system that requires **zero changes to model code**. The hook completely intercepts the transformer's forward pass and implements adaptive caching transparently. This design allows easy integration with any transformer model by simply writing an extractor function.
|
|
|
|
### Architecture
|
|
|
|
The TeaCache system consists of three main components:
|
|
|
|
| Component | Purpose | Location |
|
|
|-----------|---------|----------|
|
|
| [`CacheContext`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/#vllm_omni.diffusion.cache.CacheContext) | Dataclass containing model-specific information for caching | `vllm_omni/diffusion/cache/teacache/context.py` |
|
|
| [`EXTRACTOR_REGISTRY`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/teacache/extractors/#vllm_omni.diffusion.cache.teacache.extractors.EXTRACTOR_REGISTRY) | Maps transformer class names to extractor functions | `vllm_omni/diffusion/cache/teacache/extractors.py` |
|
|
| [`TeaCacheConfig`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/#vllm_omni.diffusion.cache.TeaCacheConfig) | Configuration including thresholds and polynomial coefficients | `vllm_omni/diffusion/cache/teacache/config.py` |
|
|
|
|
The hook handles all caching logic automatically, including:
|
|
|
|
- CFG-aware state management (separate states for positive/negative branches)
|
|
- CFG-parallel compatibility
|
|
- L1 distance computation with polynomial rescaling
|
|
- Residual caching and reuse
|
|
|
|
|
|
---
|
|
|
|
## Step-by-Step Implementation
|
|
|
|
To add TeaCache support for a new model, you need to:
|
|
|
|
1. Write an **extractor function** that returns a `CacheContext` object
|
|
2. Register the extractor in the `EXTRACTOR_REGISTRY`
|
|
3. Add model-specific polynomial coefficients to `TeaCacheConfig`
|
|
|
|
### Step 1: Model-Specific Preprocessing
|
|
|
|
Extract and process model inputs. This typically involves:
|
|
- Embedding image/latent inputs
|
|
- Processing text encoder outputs (if dual-stream)
|
|
- Creating timestep embeddings
|
|
- Computing positional embeddings
|
|
|
|
**Example (Qwen-Image):**
|
|
|
|
```python
|
|
def extract_qwen_context(
|
|
module: nn.Module,
|
|
hidden_states: torch.Tensor,
|
|
encoder_hidden_states: torch.Tensor,
|
|
encoder_hidden_states_mask: torch.Tensor,
|
|
timestep: torch.Tensor,
|
|
img_shapes: torch.Tensor,
|
|
txt_seq_lens: torch.Tensor,
|
|
guidance: torch.Tensor | None = None,
|
|
**kwargs: Any,
|
|
) -> CacheContext:
|
|
# Validate model structure
|
|
if not hasattr(module, "transformer_blocks") or len(module.transformer_blocks) == 0:
|
|
raise ValueError("Module must have transformer_blocks")
|
|
|
|
# Preprocessing: embed inputs
|
|
hidden_states = module.img_in(hidden_states)
|
|
timestep = timestep.to(device=hidden_states.device, dtype=hidden_states.dtype)
|
|
encoder_hidden_states = module.txt_norm(encoder_hidden_states)
|
|
encoder_hidden_states = module.txt_in(encoder_hidden_states)
|
|
|
|
# Create timestep embedding
|
|
if guidance is not None:
|
|
guidance = guidance.to(hidden_states.dtype) * 1000
|
|
temb = (
|
|
module.time_text_embed(timestep, hidden_states)
|
|
if guidance is None
|
|
else module.time_text_embed(timestep, guidance, hidden_states)
|
|
)
|
|
|
|
# Compute position embeddings
|
|
image_rotary_emb = module.pos_embed(img_shapes, txt_seq_lens, device=hidden_states.device)
|
|
```
|
|
|
|
### Step 2: Extract Modulated Input
|
|
|
|
The modulated input is used for cache decisions. Extract it from the **first transformer block** after normalization and modulation.
|
|
|
|
**Example (Qwen-Image):**
|
|
|
|
```python
|
|
# Extract modulated input from first transformer block
|
|
block = module.transformer_blocks[0]
|
|
img_mod_params = block.img_mod(temb)
|
|
img_mod1, _ = img_mod_params.chunk(2, dim=-1)
|
|
img_modulated, _ = block.img_norm1(hidden_states, img_mod1)
|
|
```
|
|
|
|
**Key Points:**
|
|
|
|
- Use the **first block** to extract modulated input early
|
|
- Apply the same normalization and modulation as the actual forward pass
|
|
- The tensor should represent the processed features that will change across timesteps
|
|
|
|
### Step 3: Define Transformer Execution
|
|
|
|
Create a callable that executes all transformer blocks. This encapsulates the main computation loop.
|
|
|
|
**Example (Qwen-Image dual-stream):**
|
|
|
|
```python
|
|
def run_transformer_blocks():
|
|
"""Execute all Qwen transformer blocks."""
|
|
h = hidden_states
|
|
e = encoder_hidden_states
|
|
|
|
for block in module.transformer_blocks:
|
|
e, h = block(
|
|
hidden_states=h,
|
|
encoder_hidden_states=e,
|
|
encoder_hidden_states_mask=encoder_hidden_states_mask,
|
|
temb=temb,
|
|
image_rotary_emb=image_rotary_emb,
|
|
)
|
|
return (h, e) # Return both image and text hidden states
|
|
```
|
|
|
|
**Example (Single-stream model like Flux):**
|
|
|
|
```python
|
|
def run_transformer_blocks():
|
|
"""Execute all Flux transformer blocks."""
|
|
h = hidden_states
|
|
|
|
for block in module.transformer_blocks:
|
|
h = block(h, temb=temb)
|
|
return (h,) # Return only image hidden states
|
|
```
|
|
|
|
**Key Points:**
|
|
|
|
- Return format:
|
|
- For single-stream models: return `(hidden_states,)`
|
|
- For dual-stream models: return `(hidden_states, encoder_hidden_states)`
|
|
|
|
### Step 4: Define Postprocessing
|
|
|
|
Create a callable that applies final transformations to produce the model output.
|
|
|
|
**Example (Qwen-Image):**
|
|
|
|
```python
|
|
return_dict = kwargs.get("return_dict", True)
|
|
|
|
def postprocess(h):
|
|
"""Apply Qwen-specific output postprocessing."""
|
|
h = module.norm_out(h, temb)
|
|
output = module.proj_out(h)
|
|
if not return_dict:
|
|
return (output,)
|
|
return Transformer2DModelOutput(sample=output)
|
|
```
|
|
|
|
### Step 5: Return CacheContext
|
|
|
|
Package all information into a `CacheContext` object.
|
|
|
|
```python
|
|
return CacheContext(
|
|
modulated_input=img_modulated,
|
|
hidden_states=hidden_states,
|
|
encoder_hidden_states=encoder_hidden_states, # or None for single-stream
|
|
temb=temb,
|
|
run_transformer_blocks=run_transformer_blocks,
|
|
postprocess=postprocess,
|
|
)
|
|
```
|
|
|
|
**CacheContext Fields:**
|
|
|
|
| Field | Type | Purpose |
|
|
|-------|------|---------|
|
|
| `modulated_input` | `torch.Tensor` | Tensor used for cache decision (similarity comparison) |
|
|
| `hidden_states` | `torch.Tensor` | Current hidden states (will be modified by caching) |
|
|
| `encoder_hidden_states` | `torch.Tensor | None` | Encoder states for dual-stream models, `None` for single-stream |
|
|
| `temb` | `torch.Tensor` | Timestep embedding tensor |
|
|
| `run_transformer_blocks` | `Callable[[], tuple]` | Executes transformer blocks, returns `(hidden_states, [encoder_hidden_states])` |
|
|
| `postprocess` | `Callable[[torch.Tensor], Any]` | Applies final transformations to produce model output |
|
|
| `extra_states` | `dict | None` | Optional dict for additional model-specific state |
|
|
|
|
### Step 6: Register the Extractor
|
|
|
|
Add your extractor to the `EXTRACTOR_REGISTRY` in `vllm_omni/diffusion/cache/teacache/extractors.py`:
|
|
|
|
```python
|
|
EXTRACTOR_REGISTRY: dict[str, Callable] = {
|
|
"QwenImageTransformer2DModel": extract_qwen_context,
|
|
"Bagel": extract_bagel_context,
|
|
"ZImageTransformer2DModel": extract_zimage_context,
|
|
"YourModelTransformer2DModel": extract_your_model_context, # Add here
|
|
}
|
|
```
|
|
|
|
**Key:** Use the transformer class name (`module.__class__.__name__`)
|
|
|
|
### Step 7: Add Model Coefficients
|
|
|
|
Add polynomial rescaling coefficients to `vllm_omni/diffusion/cache/teacache/config.py`:
|
|
|
|
```python
|
|
_MODEL_COEFFICIENTS = {
|
|
"QwenImageTransformer2DModel": [
|
|
-4.50000000e02,
|
|
2.80000000e02,
|
|
-4.50000000e01,
|
|
3.20000000e00,
|
|
-2.00000000e-02,
|
|
],
|
|
"YourModelTransformer2DModel": [ # Add your model's coefficients
|
|
# 5 polynomial coefficients (can reuse similar model's coefficients initially)
|
|
],
|
|
}
|
|
```
|
|
|
|
|
|
**Initial approach:** Start with coefficients from a similar model architecture, then tune empirically following [Customization](#customization) section.
|
|
|
|
---
|
|
|
|
## Customization
|
|
|
|
### Coefficient Estimation
|
|
|
|
While you can start with coefficients from a similar model architecture, estimating custom coefficients for your specific model typically improves TeaCache performance.
|
|
|
|
**Why Estimate Coefficients?**
|
|
|
|
The polynomial coefficients rescale L1 distances between consecutive modulated inputs to better predict when cached residuals can be reused. Model-specific coefficients account for:
|
|
|
|
- Architecture differences (layer count, hidden size, attention patterns)
|
|
- Training data characteristics
|
|
- Noise prediction behavior across timesteps
|
|
|
|
| Approach | Performance | Effort |
|
|
|----------|-------------|--------|
|
|
| Using defaults from similar model | Within 5-10% of optimal | 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:
|
|
"""Adapter for coefficient estimation on your model."""
|
|
|
|
@staticmethod
|
|
def load_pipeline(model_path: str, device: str, dtype: torch.dtype) -> Any:
|
|
"""Load your diffusion pipeline."""
|
|
from your_model_package import YourModelPipeline
|
|
|
|
pipeline = YourModelPipeline.from_pretrained(
|
|
model_path,
|
|
torch_dtype=dtype,
|
|
)
|
|
pipeline = pipeline.to(device)
|
|
return pipeline
|
|
|
|
@staticmethod
|
|
def get_transformer(pipeline: Any) -> tuple[Any, str]:
|
|
"""Extract transformer from pipeline."""
|
|
return pipeline.transformer, "YourTransformer2DModel"
|
|
|
|
@staticmethod
|
|
def install_hook(transformer: Any, hook: DataCollectionHook) -> None:
|
|
"""Install data collection hook on transformer."""
|
|
from vllm_omni.diffusion.hooks import HookRegistry
|
|
|
|
registry = HookRegistry.get_or_create(transformer)
|
|
registry.register_hook(hook._HOOK_NAME, hook)
|
|
|
|
|
|
# Register your adapter
|
|
_MODEL_ADAPTERS["YourModel"] = YourModelAdapter
|
|
```
|
|
|
|
#### Collect Data and Estimate
|
|
|
|
```python
|
|
from vllm_omni.diffusion.cache.teacache.coefficient_estimator import (
|
|
TeaCacheCoefficientEstimator,
|
|
)
|
|
from datasets import load_dataset
|
|
from tqdm import tqdm
|
|
|
|
# Initialize estimator
|
|
estimator = TeaCacheCoefficientEstimator(
|
|
model_path="/path/to/your/model",
|
|
model_type="YourModel",
|
|
)
|
|
|
|
# Load diverse prompts (paper recommends ~70 prompts)
|
|
dataset = load_dataset("nateraw/parti-prompts", split="train")
|
|
prompts = dataset["Prompt"][:70]
|
|
|
|
# Collect data
|
|
for prompt in tqdm(prompts, desc="Collecting data"):
|
|
estimator.collect_from_prompt(prompt=prompt, num_inference_steps=50)
|
|
|
|
# Estimate coefficients
|
|
coeffs = estimator.estimate(poly_order=4)
|
|
print(f"Estimated coefficients: {coeffs}")
|
|
```
|
|
|
|
Note: some models may require the vLLM context and config to be initialized to initialize vLLM modules. To this end, you may need a workaround like the following to be able to run coefficient estimation.
|
|
```python
|
|
from vllm_omni.diffusion.forward_context import set_forward_context
|
|
from vllm_omni.diffusion.distributed.parallel_state import (
|
|
init_distributed_environment,
|
|
initialize_model_parallel,
|
|
)
|
|
from vllm.config import VllmConfig
|
|
...
|
|
|
|
if __name__ == "__main__":
|
|
os.environ["MASTER_ADDR"] = "localhost"
|
|
os.environ["MASTER_PORT"] = "8192"
|
|
os.environ["LOCAL_RANK"] = "0"
|
|
os.environ["RANK"] = "0"
|
|
os.environ["WORLD_SIZE"] = "1"
|
|
|
|
vllm_config = VllmConfig()
|
|
init_distributed_environment()
|
|
initialize_model_parallel()
|
|
|
|
# NOTE: you may have to pass an initialized OmniDiffusionConfig as a kwarg
|
|
# here to make current sp checks happy; if this is the case, just create one
|
|
# .from_kwargs() with the model name to get around this check for now,
|
|
# since your estimator subclass should handle the actual model configuration.
|
|
#
|
|
# This will be cleaned up in the future
|
|
with set_forward_context(vllm_config):
|
|
<create the estimator + run estimation here>
|
|
```
|
|
|
|
|
|
**Data Statistics Guide:**
|
|
|
|
| Metric | Good Range | Warning Signs |
|
|
|--------|------------|---------------|
|
|
| **Count** | 2000-5000+ | < 1000: too few prompts |
|
|
| **Input Diffs (x)** | 0.01-0.10 | Very small (<0.001): model may not modulate properly |
|
|
| **Output Diffs (y)** | Should correlate with x | No correlation: check extractor |
|
|
| **Coefficient magnitude** | -1e6 to 1e6 | > 1e8: numerical instability |
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
After adding TeaCache support, test with:
|
|
|
|
```python
|
|
from vllm_omni import Omni
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
|
|
|
omni = Omni(
|
|
model="your-model-name",
|
|
cache_backend="tea_cache",
|
|
cache_config={
|
|
"rel_l1_thresh": 0.2,
|
|
"coefficients": [1.33e6, -1.69e5, 7.95e3, -1.64e2, 1.26], # Your coefficients
|
|
}
|
|
)
|
|
|
|
images = omni.generate(
|
|
"a beautiful landscape",
|
|
OmniDiffusionSamplingParams(num_inference_steps=50),
|
|
)
|
|
```
|
|
|
|
**Verify:**
|
|
|
|
1. **Check logs** - Look for TeaCache initialization messages
|
|
2. **Compare performance** - Measure speedup vs baseline (expect 1.5x-2.0x)
|
|
3. **Verify output quality** - Visually compare cached vs uncached outputs (should be nearly identical)
|
|
|
|
See more detailed examples in [user guide for teacache](../../user_guide/diffusion/cache_acceleration/teacache.md).
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: "Unknown model type"
|
|
|
|
**Symptoms:** Error message indicating the model type is not recognized when enabling TeaCache.
|
|
|
|
**Causes & Solutions:**
|
|
|
|
- **Extractor not registered:**
|
|
|
|
**Problem:** The transformer class name doesn't exist in `EXTRACTOR_REGISTRY`.
|
|
|
|
**Solution:** Check the class name and add to registry:
|
|
```python
|
|
# Check transformer class name
|
|
print(pipeline.transformer.__class__.__name__)
|
|
|
|
# Add to EXTRACTOR_REGISTRY
|
|
EXTRACTOR_REGISTRY["YourTransformer2DModel"] = extract_your_context
|
|
```
|
|
|
|
- **Transformer class name mismatch:**
|
|
|
|
**Solution:** Ensure the registry key matches exactly with `module.__class__.__name__`.
|
|
|
|
### Issue: "Cannot find coefficients"
|
|
|
|
**Symptoms:** Error when initializing TeaCache about missing model coefficients.
|
|
|
|
**Causes & Solutions:**
|
|
|
|
- **Missing coefficients in config:**
|
|
|
|
**Solution:** Add coefficients to `_MODEL_COEFFICIENTS` in `config.py`, or pass custom coefficients:
|
|
```python
|
|
omni = Omni(
|
|
model="your-model",
|
|
cache_backend="tea_cache",
|
|
cache_config={"coefficients": [1.0, -0.5, 0.1, -0.01, 0.001]}
|
|
)
|
|
```
|
|
|
|
### Issue: Quality Degradation
|
|
|
|
**Symptoms:** Output images look noticeably different or have artifacts compared to baseline.
|
|
|
|
**Causes & Solutions:**
|
|
|
|
- **Threshold too high:**
|
|
|
|
**Problem:** `rel_l1_thresh` is too aggressive, causing cache reuse when outputs differ significantly.
|
|
|
|
**Solution:** Lower the threshold:
|
|
```python
|
|
cache_config={"rel_l1_thresh": 0.1} # Try 0.1-0.2
|
|
```
|
|
|
|
- **Coefficients not tuned:**
|
|
|
|
**Solution:** Estimate model-specific coefficients using the coefficient estimation process described above.
|
|
|
|
---
|
|
|
|
## Reference Implementations
|
|
|
|
Complete examples in the codebase:
|
|
|
|
| Model | Path | Pattern | Notes |
|
|
|-------|------|---------|-------|
|
|
| **Qwen-Image** | `vllm_omni/diffusion/cache/teacache/extractors.py` | Dual-stream | `extract_qwen_context` |
|
|
| **Bagel** | `vllm_omni/diffusion/cache/teacache/extractors.py` | Omni model | `extract_bagel_context` |
|
|
| **TeaCache Core** | `vllm_omni/diffusion/cache/teacache/` | Base implementation | Hook and config |
|
|
| **Coefficient Estimator** | `vllm_omni/diffusion/cache/teacache/coefficient_estimator.py` | Estimation tool | Adapter pattern |
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Adding TeaCache support:
|
|
|
|
1. ✅ **Write extractor** - Create function returning `CacheContext` with model-specific preprocessing
|
|
2. ✅ **Register extractor** - Add to `EXTRACTOR_REGISTRY` with transformer class name
|
|
3. ✅ **Add coefficients** - Add polynomial coefficients to `_MODEL_COEFFICIENTS`
|
|
4. ✅ **Test** - Verify with `cache_backend="tea_cache"`
|