[MiniMax-H3] TeaCache support and Cache-DiT validation (#5840)

Signed-off-by: dpeng123 <diyapeng123@gmail.com>
Signed-off-by: lishunyang12 <lishunyang12@163.com>
Co-authored-by: lishunyang12 <lishunyang12@163.com>
This commit is contained in:
Diya Pengandlishunyang12 authored and GitHub committed 2026-08-09 15:07:21 +00:00
1 parent fc8946fcf5
commit 45629a8e26
10 files changed
+669 -15

No files matched your search

+1 -1
View File
@@ -159,7 +159,7 @@ The following tables show which models support each feature:
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **MiniMax-H3** | ❌ | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
**Frame Interpolation Support**
+69
View File
@@ -750,8 +750,73 @@ audio spectral cosine 0.9589 (minimum 0.80), and audio RMS ratio 0.9342. The
resident per-GPU peak was 68.52 GiB for BF16 and 53.51 GiB for FP8, a 22%
reduction.
## TeaCache acceleration
TeaCache reuses DiT block residuals across denoising steps when consecutive
timestep embeddings are similar. MiniMax-H3 TeaCache is currently calibrated
only for the FL2VA partition. In combined serving, FL2VA requests use TeaCache
while Ref2VA requests run uncached; Ref2VA-only serving rejects TeaCache.
TeaCache and Cache-DiT are mutually exclusive; pick one cache backend per server.
The model-specific default and examples use `rel_l1_thresh=0.17`, which
provided the best conservative speed/quality balance in the validated 107-frame
T2VA workload. Lower values
may produce few or no cache hits, while higher values can improve performance
at the cost of output quality. Validate the threshold on representative
prompts and generation settings before changing it.
### Offline (Python API)
Export the directory containing the `FL2VA` and `Ref2VA` subdirectories before
running the Python example, for example `export MODEL_ROOT=/models/MiniMax-H3`.
```python
import os
from vllm_omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
omni = Omni(
model=os.path.join(os.environ["MODEL_ROOT"], "FL2VA"),
cache_backend="tea_cache",
cache_config={"rel_l1_thresh": 0.17},
trust_remote_code=True,
enable_cpu_offload=True,
)
outputs = omni.generate(
"A quiet cinematic night scene with matching ambient sound.",
OmniDiffusionSamplingParams(
height=256,
width=448,
num_frames=29,
fps=24,
num_inference_steps=50,
seed=42,
extra_args={
"task": "t2va",
"duration": 4.0,
"aspect_ratio": "16:9",
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
},
),
)
```
### Online serving
```bash
vllm serve "${MODEL_ROOT}/FL2VA" \
--omni \
--trust-remote-code \
--cache-backend tea_cache \
--cache-config '{"rel_l1_thresh":0.17}'
```
## Known limitations
- TeaCache is calibrated for FL2VA only; Ref2VA requests run uncached.
- Combined serving requires sibling `FL2VA` and `Ref2VA` directories, loads
both task-specific DiTs, and loads shared components once from `FL2VA`.
- H3 currently executes one generation request per diffusion batch.
@@ -770,6 +835,10 @@ reduction.
- A U2 x Ring2 hybrid currently fails with an attention-mask length mismatch; use
pure Ulysses.
- Online FP8 is not compatible with layerwise offload.
- TeaCache and Cache-DiT cannot be enabled on the same server.
- Image+audio Ref2VA accepts exactly one image and one audio reference.
- Video Ref2VA accepts one or more video files, but not an additional standalone
audio reference.
- Pure Ulysses still replicates the full DiT on every rank, so smaller-memory GPUs
cannot use `--usp N --tp 1` as a resident capacity path. Use DiT tensor parallelism
or model-level CPU offload; text-encoder TP alone is not sufficient.
+60
View File
@@ -471,6 +471,66 @@ class TestTeaCacheBackend:
assert backend.enabled is True
mock_apply_hook.assert_called_once()
@patch("vllm_omni.diffusion.cache.teacache.backend.apply_teacache_hook")
def test_enable_uses_generic_default_threshold(self, mock_apply_hook):
pipeline = Mock()
pipeline.__class__.__name__ = "QwenImagePipeline"
pipeline.transformer = Mock()
pipeline.transformer.__class__.__name__ = "QwenImageTransformer2DModel"
TeaCacheBackend(DiffusionCacheConfig()).enable(pipeline)
assert mock_apply_hook.call_args.args[1].rel_l1_thresh == 0.2
@pytest.mark.parametrize("partition", ["fl2va", "combined"])
@pytest.mark.parametrize(
("configured_threshold", "expected_threshold"),
[(None, 0.17), (0.2, 0.2)],
)
@patch("vllm_omni.diffusion.cache.teacache.backend.apply_teacache_hook")
def test_minimax_h3_only_enables_fl2va_teacache(
self,
mock_apply_hook,
partition,
configured_threshold,
expected_threshold,
):
pipeline = Mock()
pipeline.__class__.__name__ = "MiniMaxH3Pipeline"
pipeline.partition = partition
pipeline.transformer = Mock()
pipeline.transformer.__class__.__name__ = "MiniMaxH3DiTModel"
pipeline.transformers_ref = Mock()
config = (
DiffusionCacheConfig()
if configured_threshold is None
else DiffusionCacheConfig(rel_l1_thresh=configured_threshold)
)
backend = TeaCacheBackend(config)
backend.enable(pipeline)
mock_apply_hook.assert_called_once()
transformer, teacache_config = mock_apply_hook.call_args.args
assert transformer is pipeline.transformer
assert transformer is not pipeline.transformers_ref
assert teacache_config.rel_l1_thresh == expected_threshold
@patch("vllm_omni.diffusion.cache.teacache.backend.apply_teacache_hook")
def test_minimax_h3_rejects_ref2va_teacache(self, mock_apply_hook):
pipeline = Mock()
pipeline.__class__.__name__ = "MiniMaxH3Pipeline"
pipeline.partition = "ref2va"
pipeline.transformer = Mock()
pipeline.transformer.__class__.__name__ = "MiniMaxH3DiTModel"
backend = TeaCacheBackend(DiffusionCacheConfig())
with pytest.raises(ValueError, match="only supports the MiniMax-H3 FL2VA partition"):
backend.enable(pipeline)
assert backend.enabled is False
mock_apply_hook.assert_not_called()
@patch("vllm_omni.diffusion.cache.teacache.backend.apply_teacache_hook")
def test_enable_with_coefficients(self, mock_apply_hook):
"""Test enabling TeaCache with custom coefficients."""
+281
View File
@@ -15,19 +15,23 @@ Currently implemented:
- TestFlux2KleinExtractor: Flux2Klein model extractor
- TestFlux2Extractor: Flux2 model extractor
- TestFluxExtractor: Flux model extractor
- TestMiniMaxH3Extractor: MiniMaxH3DiTModel extractor
"""
from abc import ABC, abstractmethod
from types import SimpleNamespace
from unittest.mock import MagicMock, Mock, patch
import pytest
import torch
import torch.nn as nn
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.cache.teacache.extractors import (
extract_flux2_context,
extract_flux2_klein_context,
extract_flux_context,
extract_minimax_h3_context,
)
from vllm_omni.diffusion.models.flux.flux_transformer import FluxTransformer2DModel
from vllm_omni.diffusion.models.flux2_klein.flux2_klein_transformer import (
@@ -423,3 +427,280 @@ class TestFluxExtractor(BaseExtractorTest):
img_ids=torch.randint(0, 64, (1, 16, 3)),
txt_ids=torch.randint(0, 64, (1, 8, 3)),
)
class _MiniMaxH3FakeLinear(nn.Module):
def __init__(
self,
in_features,
out_features,
*,
bias=False,
params_dtype=None,
quant_config=None,
prefix="",
total_num_heads=None,
total_num_kv_heads=None,
**kwargs,
):
del kwargs
super().__init__()
self.prefix = prefix
self.quant_config = quant_config
self.weight = nn.Parameter(torch.randn(out_features, in_features, dtype=params_dtype or torch.float32))
if bias:
self.bias = nn.Parameter(torch.zeros(out_features, dtype=params_dtype or torch.float32))
else:
self.register_parameter("bias", None)
self.num_heads = total_num_heads
self.num_kv_heads = total_num_kv_heads
def forward(self, x):
return torch.nn.functional.linear(x, self.weight, self.bias), None
class _MiniMaxH3FakeMergedLinear(_MiniMaxH3FakeLinear):
def __init__(self, in_features, out_features, **kwargs):
super().__init__(in_features, sum(out_features), **kwargs)
class _MiniMaxH3FakeQKVLinear(_MiniMaxH3FakeLinear):
def __init__(
self,
*,
hidden_size,
head_size,
total_num_heads,
total_num_kv_heads,
**kwargs,
):
qkv_features = (total_num_heads + 2 * total_num_kv_heads) * head_size
super().__init__(
hidden_size,
qkv_features,
total_num_heads=total_num_heads,
total_num_kv_heads=total_num_kv_heads,
**kwargs,
)
class _MiniMaxH3FakeAttention(nn.Module):
class FakeBackend:
supports_prefix_kv_slicing = False
def __init__(self, **kwargs):
del kwargs
super().__init__()
self.attn_backend = self.FakeBackend
self.use_ring = False
def forward(self, q, k, v, metadata=None):
del k, v, metadata
return q
def _minimax_h3_small_od_config():
arch = {
"num_layers": 1,
"token_refiner_num_layers": 1,
"hidden_size": 8,
"num_attention_heads": 1,
"attention_head_dim": 8,
"ffn_hidden_size": 16,
"latents_dim": 2,
"audio_latents_dim": 2,
"patch_size": (1, 2, 2),
"text_dim": 6,
"timestep_input_dim": 4,
"time_embed_hidden_size": 8,
"time_embed_dim": 4,
"adaln_out_features": 18 * 8,
"final_adaln_out_features": 2 * 8,
"rope_inv_freq_len": 1,
}
return SimpleNamespace(
tf_model_config=arch,
parallel_config=SimpleNamespace(ulysses_degree=1),
)
def _minimax_h3_sample_kwargs(*, seq_len: int = 8, device: torch.device | str = "cpu") -> dict:
video_row_dim = 2 * 1 * 2 * 2
audio_row_dim = 2
text_len = 2
img_rows = 2
img_pos = torch.tensor([2, 3], dtype=torch.long, device=device)
audio_pos = torch.tensor([4, 5], dtype=torch.long, device=device)
text_pos = torch.tensor([0, 1], dtype=torch.long, device=device)
infer_out_pos = img_pos.clone()
token_tags = torch.full((seq_len,), -1, dtype=torch.long, device=device)
token_tags[text_pos] = 1
token_tags[img_pos] = 0
token_tags[audio_pos] = 2
inverse_indices = torch.zeros(seq_len, dtype=torch.long, device=device)
return {
"x": torch.randn(1, seq_len, video_row_dim, device=device),
"audio_x": torch.randn(1, seq_len, audio_row_dim, device=device),
"img_position_ids": torch.zeros(1, seq_len, 3, dtype=torch.float64, device=device),
"unique_timesteps": torch.tensor([0.5], dtype=torch.float32, device=device),
"inverse_indices": inverse_indices,
"update_mask": torch.ones(img_rows, dtype=torch.bool, device=device),
"token_tags": token_tags,
"prompt_embeds": torch.randn(text_len, 6, device=device),
"img_pos_info": {"position_ids": img_pos},
"audio_pos_info": {"position_ids": audio_pos},
"text_pos_info": {"position_ids": text_pos},
"img_pos_for_infer_output_info": {"position_ids": infer_out_pos},
"packed_seq_params": {
"cu_seqlens_q": torch.tensor([0, seq_len], dtype=torch.int32, device=device),
"max_seqlen_q": seq_len,
},
"refiner_packed_seq_params": {
"cu_seqlens_q": torch.tensor([0, text_len], dtype=torch.int32, device=device),
"max_seqlen_q": text_len,
},
}
@pytest.mark.cpu
class TestMiniMaxH3Extractor(BaseExtractorTest):
"""Test extract_minimax_h3_context function."""
def get_extractor(self):
return extract_minimax_h3_context
@pytest.fixture
def minimax_h3_module(self, monkeypatch):
"""Create a minimal MiniMaxH3DiTModel with fake parallel linears for CPU tests."""
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
monkeypatch.setattr(h3, "ColumnParallelLinear", _MiniMaxH3FakeLinear)
monkeypatch.setattr(h3, "MergedColumnParallelLinear", _MiniMaxH3FakeMergedLinear)
monkeypatch.setattr(h3, "QKVParallelLinear", _MiniMaxH3FakeQKVLinear)
monkeypatch.setattr(h3, "RowParallelLinear", _MiniMaxH3FakeLinear)
monkeypatch.setattr(h3, "Attention", _MiniMaxH3FakeAttention)
monkeypatch.setattr(h3, "get_tensor_model_parallel_world_size", lambda: 1)
model = h3.MiniMaxH3DiTModel(_minimax_h3_small_od_config(), quant_config=None)
for submodule in model.modules():
if isinstance(submodule, h3.MiniMaxH3Attention):
submodule.rope._forward_method = submodule.rope.forward_native
model.eval()
return model
def get_module(self, minimax_h3_module):
return minimax_h3_module
@pytest.fixture
def sample_inputs(self):
"""Create sample packed forward kwargs for MiniMaxH3DiTModel."""
return _minimax_h3_sample_kwargs(seq_len=8)
def get_sample_inputs(self, sample_inputs):
return sample_inputs
def test_modulated_input_shape(self, minimax_h3_module, sample_inputs):
"""Test that modulated_input matches packed sequence shape [T, H]."""
context = extract_minimax_h3_context(minimax_h3_module, **sample_inputs)
seq_len = sample_inputs["x"].shape[1]
assert context.modulated_input.shape == (seq_len, minimax_h3_module.hidden_size)
def test_run_transformer_blocks_callable(self, minimax_h3_module, sample_inputs):
"""Test that run_transformer_blocks is callable."""
context = extract_minimax_h3_context(minimax_h3_module, **sample_inputs)
assert callable(context.run_transformer_blocks)
def test_postprocess_callable(self, minimax_h3_module, sample_inputs):
"""Test that postprocess is callable."""
context = extract_minimax_h3_context(minimax_h3_module, **sample_inputs)
assert callable(context.postprocess)
def test_postprocess_output_shape(self, minimax_h3_module, sample_inputs):
"""Test that postprocess returns video/audio logits for infer rows."""
context = extract_minimax_h3_context(minimax_h3_module, **sample_inputs)
(hidden,) = context.run_transformer_blocks()
video_logits, audio_logits = context.postprocess(hidden)
num_video_rows = sample_inputs["img_pos_info"]["position_ids"].shape[0]
num_audio_rows = sample_inputs["audio_pos_info"]["position_ids"].shape[0]
assert video_logits.shape[0] == num_video_rows
assert audio_logits.shape[0] == num_audio_rows
def test_extractor_matches_native_forward(self, minimax_h3_module, sample_inputs):
"""The uncached extractor path must preserve the model's forward result."""
expected_video, expected_audio = minimax_h3_module(**sample_inputs)
context = extract_minimax_h3_context(minimax_h3_module, **sample_inputs)
(hidden,) = context.run_transformer_blocks()
actual_video, actual_audio = context.postprocess(hidden)
torch.testing.assert_close(actual_video, expected_video)
torch.testing.assert_close(actual_audio, expected_audio)
def test_run_transformer_blocks_forwards_packed_layout(
self,
minimax_h3_module,
sample_inputs,
monkeypatch,
):
"""The extractor block call must mirror the native packed contract."""
from vllm_omni.diffusion.attention.backends.abstract import VideoTokenLayout
video_layout = VideoTokenLayout(prefix_len=2, latent_grid=(1, 1, 2))
inputs = {**sample_inputs, "video_token_layout": video_layout}
block = minimax_h3_module.blocks[0]
original_forward = block.forward
received: list[tuple[int, VideoTokenLayout | None]] = []
def capture_forward(*args, **kwargs):
received.append((kwargs["packed_total"], kwargs["video_layout"]))
return original_forward(*args, **kwargs)
monkeypatch.setattr(block, "forward", capture_forward)
context = extract_minimax_h3_context(minimax_h3_module, **inputs)
context.run_transformer_blocks()
assert received == [(sample_inputs["x"].shape[1], video_layout)]
def test_teacache_reuses_h3_block_residual(self, minimax_h3_module, sample_inputs, monkeypatch):
"""A guaranteed cache hit must skip the H3 transformer block."""
from vllm_omni.diffusion.cache.teacache.config import TeaCacheConfig
from vllm_omni.diffusion.cache.teacache.hook import TeaCacheHook
block = minimax_h3_module.blocks[0]
original_forward = block.forward
block_calls = 0
def counted_forward(*args, **kwargs):
nonlocal block_calls
block_calls += 1
return original_forward(*args, **kwargs)
monkeypatch.setattr(block, "forward", counted_forward)
hook = TeaCacheHook(
TeaCacheConfig(
transformer_type="MiniMaxH3DiTModel",
rel_l1_thresh=0.3,
)
)
hook.initialize_hook(minimax_h3_module)
hook.new_forward(minimax_h3_module, **sample_inputs)
video_logits, audio_logits = hook.new_forward(minimax_h3_module, **sample_inputs)
assert block_calls == 1
assert video_logits.shape[0] == sample_inputs["img_pos_info"]["position_ids"].shape[0]
assert audio_logits.shape[0] == sample_inputs["audio_pos_info"]["position_ids"].shape[0]
def test_invalid_module_raises_error(self):
"""Test that invalid module without blocks raises ValueError."""
invalid_module = Mock()
invalid_module.blocks = []
with pytest.raises(ValueError, match="Module must have blocks"):
extract_minimax_h3_context(invalid_module, **_minimax_h3_sample_kwargs())
@@ -19,7 +19,7 @@ MODEL_ENV = "VLLM_TEST_MINIMAX_H3_FL2VA_MODEL"
def _assert_joint_output(outputs):
assert len(outputs) == 1
frames = np.asarray(outputs[0].images[0])
assert frames.shape == (39, 256, 448, 3)
assert frames.shape == (107, 256, 448, 3)
multimodal = outputs[0].multimodal_output
assert multimodal is not None
assert np.asarray(multimodal["audio"]).shape[1] == 2
@@ -122,6 +122,58 @@ def test_minimax_h3_t2va_fp8_single_gpu_smoke():
output_type="np",
extra_args={
"task": "t2va",
"duration": 4.0,
"aspect_ratio": "16:9",
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
},
),
use_tqdm=False,
)
finally:
engine.close()
_assert_joint_output(outputs)
@pytest.mark.skipif(
not os.environ.get(MODEL_ENV),
reason=f"set {MODEL_ENV} to an authorized FL2VA checkpoint path",
)
def test_minimax_h3_t2va_teacache_single_gpu_smoke():
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
if torch.accelerator.device_count() < 1:
pytest.skip("MiniMax H3 TeaCache smoke requires a CUDA device")
engine = Omni(
model=os.environ[MODEL_ENV],
cache_backend="tea_cache",
cache_config={"rel_l1_thresh": 0.17},
parallel_config=DiffusionParallelConfig(ulysses_degree=1),
trust_remote_code=True,
enable_cpu_offload=True,
enforce_eager=True,
)
try:
outputs = engine.generate(
"A quiet cinematic night scene with matching ambient sound.",
OmniDiffusionSamplingParams(
height=256,
width=448,
num_frames=29,
fps=24,
num_inference_steps=2,
seed=42,
output_type="np",
extra_args={
"task": "t2va",
"duration": 4.0,
"aspect_ratio": "16:9",
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
},
+29 -1
View File
@@ -73,6 +73,34 @@ def enable_sensenova_u1_teacache(pipeline: Any, config: DiffusionCacheConfig) ->
)
def enable_minimax_h3_teacache(pipeline: Any, config: DiffusionCacheConfig) -> None:
"""Enable TeaCache for the calibrated MiniMax-H3 FL2VA partition."""
partition = getattr(pipeline, "partition", None)
if partition == "ref2va":
raise ValueError(
"TeaCache only supports the MiniMax-H3 FL2VA partition; Ref2VA TeaCache has not been calibrated"
)
if partition not in {"fl2va", "combined"}:
raise ValueError(f"Unsupported MiniMax-H3 partition for TeaCache: {partition!r}")
teacache_config = TeaCacheConfig(
transformer_type="MiniMaxH3DiTModel",
rel_l1_thresh=config.rel_l1_thresh,
coefficients=config.coefficients,
)
apply_teacache_hook(pipeline.transformer, teacache_config)
if partition == "combined":
logger.warning(
"TeaCache is enabled only for MiniMax-H3 FL2VA; "
"Ref2VA requests on this combined pipeline will run without TeaCache"
)
logger.info(
f"TeaCache applied with rel_l1_thresh={teacache_config.rel_l1_thresh}, "
"transformer_class=MiniMaxH3DiTModel, partition=FL2VA"
)
def enable_flux2_klein_teacache(pipeline: Any, config: DiffusionCacheConfig) -> None:
"""
Enable TeaCache for Flux2 Klein model.
@@ -96,6 +124,7 @@ CUSTOM_TEACACHE_ENABLERS = {
"BagelPipeline": enable_bagel_teacache,
"Flux2KleinPipeline": enable_flux2_klein_teacache,
"HunyuanImage3Pipeline": enable_hunyuan_image3_teacache,
"MiniMaxH3Pipeline": enable_minimax_h3_teacache,
"SenseNovaU1Pipeline": enable_sensenova_u1_teacache,
}
@@ -145,7 +174,6 @@ class TeaCacheBackend(CacheBackend):
# Create TeaCacheConfig from DiffusionCacheConfig with transformer_type
# Access parameters via attribute access: config.rel_l1_thresh
# rel_l1_thresh already has a default value of 0.2 in DiffusionCacheConfig
try:
teacache_config = TeaCacheConfig(
transformer_type=transformer_type,
+22 -7
View File
@@ -83,6 +83,19 @@ _MODEL_COEFFICIENTS = {
],
# LongCat Image transformer coefficients
"LongCatImageTransformer2DModel": [652.5980, -424.1615, 84.5526, -4.5923, 0.1694],
# MiniMax-H3 FL2VA coefficients.
"MiniMaxH3DiTModel": [
2.283704065852778e03,
-7.775977277886368e02,
9.408414741359490e01,
-4.232669906169421e00,
2.173782527946167e-01,
],
}
_DEFAULT_REL_L1_THRESH = 0.2
_MODEL_DEFAULT_REL_L1_THRESH = {
"MiniMaxH3DiTModel": 0.17,
}
@@ -97,10 +110,8 @@ class TeaCacheConfig:
Args:
rel_l1_thresh: Threshold for accumulated relative L1 distance. When below threshold,
cached residual is reused. Values in [0.1, 0.3] work best:
- 0.2: ~1.5x speedup with minimal quality loss
- 0.4: ~1.8x speedup with slight quality loss
- 0.6: ~2.0x speedup with noticeable quality loss
cached residual is reused. If None, uses the model-specific default or 0.2
when no model-specific default is registered.
coefficients: Polynomial coefficients for rescaling L1 distance. If None, uses
model-specific defaults based on transformer_type.
transformer_type: Transformer class name (e.g., "QwenImageTransformer2DModel").
@@ -108,14 +119,18 @@ class TeaCacheConfig:
Defaults to "QwenImageTransformer2DModel".
"""
rel_l1_thresh: float = 0.2
rel_l1_thresh: float | None = None
coefficients: list[float] | None = None
transformer_type: str = "QwenImageTransformer2DModel"
def __post_init__(self) -> None:
"""Validate and set default coefficients."""
if self.rel_l1_thresh <= 0:
raise ValueError(f"rel_l1_thresh must be positive, got {self.rel_l1_thresh}")
threshold = self.rel_l1_thresh
if threshold is None:
threshold = _MODEL_DEFAULT_REL_L1_THRESH.get(self.transformer_type, _DEFAULT_REL_L1_THRESH)
if threshold <= 0:
raise ValueError(f"rel_l1_thresh must be positive, got {threshold}")
self.rel_l1_thresh = threshold
if self.coefficients is None:
# Use model-specific coefficients, explicitly check if the type exists or not
+147
View File
@@ -1249,6 +1249,152 @@ def extract_sensenova_u1_context(
)
def extract_minimax_h3_context(
module: nn.Module,
**kwargs: Any,
) -> CacheContext:
"""Extract cache context for MiniMaxH3DiTModel.
Mirrors the packed multimodal forward in ``MiniMaxH3DiTModel.forward``:
preprocessing via ``_embed``, cacheable ``blocks`` loop, and
``final_layer`` postprocessing with row selection and update masks.
"""
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
_BF16_DTYPE,
_FORWARD_SUPPORTED_KWARGS,
MINIMAX_H3_ADALN_MODALITY_NUM,
_modulate_scale_shift,
_required_kwarg,
)
if not hasattr(module, "blocks") or len(module.blocks) == 0:
raise ValueError("Module must have blocks")
unexpected = sorted(set(kwargs) - _FORWARD_SUPPORTED_KWARGS)
if unexpected:
raise TypeError(
"MiniMaxH3DiTModel.forward received unexpected kwargs: "
f"{unexpected}; supported kwargs: "
f"{sorted(_FORWARD_SUPPORTED_KWARGS)}"
)
x = _required_kwarg(kwargs, "x")
audio_x = _required_kwarg(kwargs, "audio_x")
img_position_ids = _required_kwarg(kwargs, "img_position_ids")
unique_timesteps = _required_kwarg(kwargs, "unique_timesteps")
inverse_indices = _required_kwarg(kwargs, "inverse_indices").view(-1).to(torch.long)
update_mask = _required_kwarg(kwargs, "update_mask")
token_tags = _required_kwarg(kwargs, "token_tags").view(-1).to(torch.long)
skip_mask_out_condition = bool(kwargs.get("skip_mask_out_condition", False))
update_audio_mask = kwargs.get("update_audio_mask")
text_selected = _required_kwarg(kwargs, "prompt_embeds")
img_pos = module._pos_ids(_required_kwarg(kwargs, "img_pos_info"), "img_pos_info")
audio_pos = module._pos_ids(_required_kwarg(kwargs, "audio_pos_info"), "audio_pos_info")
text_pos = module._pos_ids(
_required_kwarg(kwargs, "text_pos_info"),
"text_pos_info",
)
infer_out_pos = module._pos_ids(
_required_kwarg(kwargs, "img_pos_for_infer_output_info"),
"img_pos_for_infer_output_info",
)
psp = _required_kwarg(kwargs, "packed_seq_params")
cu_seqlens = module._psp_field(psp, "packed_seq_params", "cu_seqlens_q").to(torch.int32)
max_seqlen = int(module._psp_field(psp, "packed_seq_params", "max_seqlen_q"))
refiner_psp = _required_kwarg(kwargs, "refiner_packed_seq_params")
refiner_cu = module._psp_field(refiner_psp, "refiner_packed_seq_params", "cu_seqlens_q").to(torch.int32)
refiner_max = int(module._psp_field(refiner_psp, "refiner_packed_seq_params", "max_seqlen_q"))
video_layout = kwargs.get("video_token_layout")
if x.dim() != 3 or x.shape[0] != 1:
raise ValueError(f"x must be [1, S, C], got {list(x.shape)}")
seq_len = int(x.shape[1])
if token_tags.shape[0] != seq_len:
raise ValueError(f"token_tags must cover the full packed sequence ({seq_len}), got {token_tags.shape[0]}.")
if inverse_indices.shape[0] != seq_len:
raise ValueError(f"inverse_indices must be [{seq_len}], got {list(inverse_indices.shape)}")
device = x.device
rope_freqs = module.rope(img_position_ids).to(device)
decoder_input, t_emb = module._embed(
x=x,
audio_x=audio_x,
text_embeddings_selected=text_selected,
unique_timesteps=unique_timesteps.view(-1).to(device),
img_pos=img_pos.to(device),
audio_pos=audio_pos.to(device),
text_pos=text_pos.to(device),
refiner_cu_seqlens=refiner_cu.to(device),
refiner_max_seqlen=refiner_max,
seq_len=seq_len,
device=device,
)
combined_indices = (inverse_indices * MINIMAX_H3_ADALN_MODALITY_NUM + token_tags.clamp(min=0)).to(device)
inverse_indices = inverse_indices.to(device)
cu_seqlens = cu_seqlens.to(device)
shift_msa, scale_msa, *_ = module.blocks[0].adaln_proj(t_emb)
modulated_hidden = module.blocks[0].norm1(decoder_input)
modulated_input = _modulate_scale_shift(
modulated_hidden,
shift_msa,
scale_msa,
combined_indices,
dtype=_BF16_DTYPE,
)
def run_transformer_blocks() -> tuple[torch.Tensor, ...]:
hidden, block_rope, block_combined = module.sp_prepare(
decoder_input,
rope_freqs,
combined_indices,
)
for block in module.blocks:
hidden = block(
hidden,
t_emb=t_emb,
combined_indices=block_combined,
rope_freqs=block_rope,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
packed_total=seq_len,
video_layout=video_layout,
)
hidden = module.sp_gather(hidden)
return (hidden,)
def postprocess(hidden: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
video_logits, audio_logits = module.final_layer(
hidden,
t_emb=t_emb,
inverse_indices=inverse_indices,
)
video_logits = video_logits.index_select(0, infer_out_pos.to(device))
audio_logits = audio_logits.index_select(0, audio_pos.to(device))
if not skip_mask_out_condition:
mask = update_mask.view(-1).to(device)
if mask.shape[0] != video_logits.shape[0]:
raise ValueError(f"update_mask length mismatch: {mask.shape[0]} != {video_logits.shape[0]}")
video_logits = video_logits * mask.unsqueeze(-1)
if update_audio_mask is not None:
audio_logits = audio_logits * update_audio_mask.view(-1).unsqueeze(-1)
return video_logits, audio_logits
return CacheContext(
modulated_input=modulated_input,
hidden_states=decoder_input,
encoder_hidden_states=None,
temb=t_emb,
run_transformer_blocks=run_transformer_blocks,
postprocess=postprocess,
)
# Registry for model-specific extractors
# Key: Transformer class name
# Value: extractor function with signature (module, *args, **kwargs) -> CacheContext
@@ -1265,6 +1411,7 @@ EXTRACTOR_REGISTRY: dict[str, Callable] = {
"LongCatImageTransformer2DModel": extract_longcat_context,
"FluxTransformer2DModel": extract_flux_context,
"SenseNovaU1ForCausalLM": extract_sensenova_u1_context,
"MiniMaxH3DiTModel": extract_minimax_h3_context,
# Future models:
# "CogVideoXTransformer3DModel": extract_cogvideox_context,
}
+3 -1
View File
@@ -231,7 +231,9 @@ class TeaCacheHook(ModelHook):
state.accumulated_rel_l1_distance += abs(rescaled_distance)
# Decision: below threshold = cache, above = compute
if state.accumulated_rel_l1_distance < self.config.rel_l1_thresh:
rel_l1_thresh = self.config.rel_l1_thresh
assert rel_l1_thresh is not None
if state.accumulated_rel_l1_distance < rel_l1_thresh:
return False # Use cache
else:
state.accumulated_rel_l1_distance = 0.0 # Reset accumulator
+4 -4
View File
@@ -412,14 +412,14 @@ class DiffusionCacheConfig:
>>> # Access via attribute
>>> print(config.rel_l1_thresh) # 0.3 (from dict)
>>> print(config.Fn_compute_blocks) # 8 (default)
>>> # Empty dict uses all defaults
>>> # Empty dict defers model-specific defaults to the TeaCache backend
>>> default_config = DiffusionCacheConfig.from_dict({})
>>> print(config.rel_l1_thresh) # 0.2 (default)
>>> print(default_config.rel_l1_thresh) # None
"""
# TeaCache parameters [tea_cache only]
# Default: 0.2 provides ~1.5x speedup with minimal quality loss (optimal balance)
rel_l1_thresh: float = 0.2
# None defers to the model-specific TeaCache default (0.2 fallback).
rel_l1_thresh: float | None = None
coefficients: list[float] | None = None # Uses model-specific defaults if None
# MagCache parameters [mag_cache only]