[Perf][DreamZero]dreamzero supports CUDA graph, torch.compile, and DiT Caching. (#4213)

Signed-off-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
amy-why-3459 authored and GitHub committed 2026-06-19 14:24:37 +08:00
1 parent 63ba11b8b1
commit f05e03aea8
22 files changed
+1204 -177

No files matched your search

@@ -0,0 +1,129 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""DreamZero AR client frame schedule aligned with upstream ``test_client_AR.py``."""
from __future__ import annotations
import logging
from typing import Any
import numpy as np
# Frame schedule constants (matching upstream debug_inference.py / test_client_AR.py).
ACTION_HORIZON = 24
DEFAULT_ACTION_DIM = 8
RELATIVE_OFFSETS = [-23, -16, -8, 0]
DEFAULT_NUM_AR_CHUNKS = 15
CAMERA_FILES = {
"observation/exterior_image_0_left": "exterior_image_1_left.mp4",
"observation/exterior_image_1_left": "exterior_image_2_left.mp4",
"observation/wrist_image_left": "wrist_image_left.mp4",
}
def build_frame_schedule(
total_frames: int,
num_chunks: int,
*,
logger: logging.Logger | None = None,
) -> list[list[int]]:
"""Build the frame index schedule for multi-frame chunks.
Args:
total_frames: Number of frames available in each camera video.
num_chunks: Number of 4-frame chunks to schedule after the initial frame.
Returns:
A list of frame-index lists. Each inner list has four indices. The
returned list may be shorter than ``num_chunks`` when the videos run
out of frames (upstream stops early instead of erroring).
"""
chunks: list[list[int]] = []
current_frame = 23
for _ in range(num_chunks):
indices = [max(current_frame + offset, 0) for offset in RELATIVE_OFFSETS]
if indices[-1] >= total_frames:
if logger is not None:
logger.info(
"Frame %s >= %s, stopping at %s chunks",
indices[-1],
total_frames,
len(chunks),
)
break
chunks.append(indices)
current_frame += ACTION_HORIZON
return chunks
def make_obs_from_video(
camera_frames: dict[str, np.ndarray],
frame_indices: list[int],
*,
prompt: str,
session_id: str,
) -> dict[str, Any]:
"""Build an observation dict from real video frames.
For one frame each image key is ``(H, W, 3)``. For four frames each key is
``(4, H, W, 3)``.
"""
obs: dict[str, Any] = {}
for camera_key, all_frames in camera_frames.items():
selected = all_frames[frame_indices]
obs[camera_key] = selected[0] if len(frame_indices) == 1 else selected
obs["observation/joint_position"] = np.zeros(7, dtype=np.float32)
obs["observation/cartesian_position"] = np.zeros(6, dtype=np.float32)
obs["observation/gripper_position"] = np.zeros(1, dtype=np.float32)
obs["prompt"] = prompt
obs["session_id"] = session_id
return obs
def build_ar_observations(
camera_frames: dict[str, np.ndarray],
*,
prompt: str,
session_id: str,
num_chunks: int = DEFAULT_NUM_AR_CHUNKS,
repeat_chunk_observations: bool = False,
logger: logging.Logger | None = None,
) -> list[dict[str, Any]]:
"""Build the AR observation sequence used by upstream ``test_client_AR.py``.
Step 0 sends frame ``[0]``. Each subsequent step sends one 4-frame chunk.
``num_chunks`` counts only the 4-frame chunks after the initial frame, not
the total number of inferences.
"""
if num_chunks < 0:
raise ValueError("num_chunks must be non-negative")
total_frames = min(frames.shape[0] for frames in camera_frames.values())
observations = [
make_obs_from_video(
camera_frames,
[0],
prompt=prompt,
session_id=session_id,
)
]
chunk_schedule = build_frame_schedule(total_frames, num_chunks, logger=logger)
if repeat_chunk_observations and chunk_schedule and len(chunk_schedule) < num_chunks:
while len(chunk_schedule) < num_chunks:
chunk_schedule.append(chunk_schedule[-1])
for frame_indices in chunk_schedule:
observations.append(
make_obs_from_video(
camera_frames,
frame_indices,
prompt=prompt,
session_id=session_id,
)
)
return observations
@@ -11,6 +11,11 @@ from pathlib import Path
import cv2
import numpy as np
import torch
from client_schedule import (
CAMERA_FILES,
DEFAULT_NUM_AR_CHUNKS,
build_ar_observations,
)
from PIL import Image
from vllm_omni import Omni
@@ -20,13 +25,6 @@ from vllm_omni.outputs import OmniRequestOutput
WORKER_EXTENSION = "vllm_omni.diffusion.models.dreamzero.video_export_worker.DreamZeroVideoExportWorkerExtension"
ASSET_REPO_ID = "YangshenDeng/vllm-omni-dreamzero-assets"
DEFAULT_SESSION_PREFIX = "dreamzero-export"
RELATIVE_OFFSETS = [-23, -16, -8, 0]
ACTION_HORIZON = 24
CAMERA_FILES = {
"observation/exterior_image_0_left": "exterior_image_1_left.mp4",
"observation/exterior_image_1_left": "exterior_image_2_left.mp4",
"observation/wrist_image_left": "wrist_image_left.mp4",
}
def _parse_args() -> argparse.Namespace:
@@ -49,6 +47,17 @@ def _parse_args() -> argparse.Namespace:
default="Move the pan forward and use the brush in the middle of the plates to brush the inside of the pan",
)
parser.add_argument("--session-id", default=None)
parser.add_argument(
"--num-chunks",
type=int,
default=DEFAULT_NUM_AR_CHUNKS,
help="Number of 4-frame chunks to send after the initial frame (default: 15).",
)
parser.add_argument(
"--repeat-chunk-observations",
action="store_true",
help="Repeat the last valid chunk observation when bundled assets run out of frames.",
)
parser.add_argument("--save-input-video", action="store_true")
parser.add_argument("--save-gif", action="store_true")
parser.add_argument("--save-actions", action="store_true")
@@ -84,57 +93,25 @@ def _load_camera_frames(video_dir: Path) -> dict[str, np.ndarray]:
return camera_frames
def _build_frame_schedule(total_frames: int, num_chunks: int) -> list[list[int]]:
chunks: list[list[int]] = []
current_frame = 23
for _ in range(num_chunks):
indices = [max(current_frame + offset, 0) for offset in RELATIVE_OFFSETS]
if indices[-1] >= total_frames:
break
chunks.append(indices)
current_frame += ACTION_HORIZON
return chunks
def _make_obs_from_video(
camera_frames: dict[str, np.ndarray],
frame_indices: list[int],
*,
def _build_observations(
video_dir: Path,
prompt: str,
session_id: str,
) -> dict:
obs: dict = {}
for camera_key, all_frames in camera_frames.items():
selected = all_frames[frame_indices]
obs[camera_key] = selected[0] if len(frame_indices) == 1 else selected
obs["observation/joint_position"] = np.zeros(7, dtype=np.float32)
obs["observation/cartesian_position"] = np.zeros(6, dtype=np.float32)
obs["observation/gripper_position"] = np.zeros(1, dtype=np.float32)
obs["prompt"] = prompt
obs["session_id"] = session_id
return obs
def _build_observations(video_dir: Path, prompt: str, session_id: str) -> tuple[dict[str, np.ndarray], list[dict]]:
*,
num_chunks: int,
repeat_chunk_observations: bool,
) -> tuple[dict[str, np.ndarray], list[dict]]:
camera_frames = _load_camera_frames(video_dir)
total_frames = min(frames.shape[0] for frames in camera_frames.values())
chunks = _build_frame_schedule(total_frames, 1)
observations = [
_make_obs_from_video(camera_frames, [0], prompt=prompt, session_id=session_id),
]
if chunks:
observations.append(
_make_obs_from_video(
camera_frames,
chunks[0],
prompt=prompt,
session_id=session_id,
)
)
observations = build_ar_observations(
camera_frames,
prompt=prompt,
session_id=session_id,
num_chunks=num_chunks,
repeat_chunk_observations=repeat_chunk_observations,
)
if len(observations) < 2:
raise RuntimeError("Need at least two DreamZero example observations to export a prediction video.")
return camera_frames, observations[:2]
raise RuntimeError("Need at least two DreamZero observations to export a prediction video.")
return camera_frames, observations
def _extract_latents(output: OmniRequestOutput) -> torch.Tensor:
@@ -229,6 +206,26 @@ def _run_generation(
return omni, outputs
def _decode_accumulated_with_worker(omni: Omni, session_id: str) -> np.ndarray:
"""Decode server-side accumulated latents when the pipeline supports it."""
stage_client = omni.engine.stage_clients[0]
engine = getattr(stage_client, "_engine", None)
if engine is None:
raise RuntimeError("DreamZero export requires inline diffusion stage access.")
decoded = engine.executor.collective_rpc(
"decode_accumulated_video_latents_to_uint8",
args=(session_id,),
unique_reply_rank=0,
exec_all_ranks=True,
)
if isinstance(decoded, torch.Tensor):
decoded = decoded.numpy()
if not isinstance(decoded, np.ndarray):
raise TypeError(f"Unexpected decoded output type: {type(decoded)!r}")
return decoded
def _decode_with_worker(omni: Omni, full_latents: torch.Tensor) -> np.ndarray:
stage_client = omni.engine.stage_clients[0]
engine = getattr(stage_client, "_engine", None)
@@ -248,6 +245,15 @@ def _decode_with_worker(omni: Omni, full_latents: torch.Tensor) -> np.ndarray:
return decoded
def _decode_video(omni: Omni, session_id: str, outputs: list[OmniRequestOutput]) -> np.ndarray:
"""Decode full AR rollout by concatenating per-step latents from all outputs."""
latent_steps = [_extract_latents(output) for output in outputs]
latent_t = [step.shape[2] for step in latent_steps]
print(f"Decoding {len(latent_steps)} latent steps, latent_t={latent_t}, total_t={sum(latent_t)}")
full_latents = torch.cat(latent_steps, dim=2)
return _decode_with_worker(omni, full_latents)
def main() -> None:
args = _parse_args()
session_id = args.session_id or f"{DEFAULT_SESSION_PREFIX}-{uuid.uuid4()}"
@@ -256,6 +262,12 @@ def main() -> None:
video_dir=args.video_dir,
prompt=args.prompt,
session_id=session_id,
num_chunks=args.num_chunks,
repeat_chunk_observations=args.repeat_chunk_observations,
)
print(
f"Running {len(observations)} inferences "
f"(1 initial + {len(observations) - 1} chunks; requested {args.num_chunks} post-initial chunks)"
)
args.output_dir.mkdir(parents=True, exist_ok=True)
@@ -273,17 +285,19 @@ def main() -> None:
deploy_config_path=args.deploy_config,
observations=observations,
)
latent_steps = [_extract_latents(output) for output in outputs]
full_latents = torch.cat(latent_steps, dim=2)
frames = _decode_with_worker(omni, full_latents)
frames = _decode_video(omni, session_id, outputs)
finally:
if omni is not None:
omni.close()
expected_frames = 8 * len(observations) + 1
mp4_path = args.output_dir / f"{args.output_stem}.mp4"
_write_mp4(mp4_path, frames, fps=args.fps)
print(f"SAVED_MP4={mp4_path}")
print(
f"SAVED_MP4={mp4_path} frames={len(frames)} "
f"expected≈{expected_frames} duration≈{len(frames) / max(args.fps, 1):.1f}s"
)
if args.save_gif:
gif_path = args.output_dir / f"{args.output_stem}.gif"
@@ -294,8 +308,10 @@ def main() -> None:
npz_path = args.output_dir / f"{args.output_stem}_actions.npz"
np.savez(
npz_path,
step0=np.asarray(outputs[0].multimodal_output.get("actions")),
step1=np.asarray(outputs[1].multimodal_output.get("actions")),
**{
f"step{index}": np.asarray(output.multimodal_output.get("actions"))
for index, output in enumerate(outputs)
},
)
print(f"SAVED_ACTIONS={npz_path}")
+116
View File
@@ -0,0 +1,116 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for StepCacheBackend and StepCacheState."""
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.cache.selector import get_cache_backend
from vllm_omni.diffusion.cache.stepcache import (
STEP_CACHE_CONFIG_ATTR,
STEP_CACHE_STATE_ATTR,
StepCacheBackend,
StepCacheConfig,
StepCacheState,
is_stepcache_active,
)
from vllm_omni.diffusion.data import DiffusionCacheConfig
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_prev_predictions(sim: float, dim: int = 8) -> list[tuple[torch.Tensor]]:
v_prev = torch.randn(1, dim)
if sim >= 0.99:
v_last = v_prev.clone()
else:
v_last = torch.randn(1, dim)
return [(v_prev,), (v_last,)]
class TestStepCacheConfig:
def test_from_diffusion_cache_config_respects_step_cache_dit_enabled(self):
enabled = StepCacheConfig.from_diffusion_cache_config(DiffusionCacheConfig(step_cache_dit_enabled=True))
disabled = StepCacheConfig.from_diffusion_cache_config(DiffusionCacheConfig(step_cache_dit_enabled=False))
assert enabled.enabled is True
assert disabled.enabled is False
class TestStepCacheState:
def test_disabled_always_runs(self):
state = StepCacheState(StepCacheConfig(enabled=False))
assert state.should_run_step(_make_prev_predictions(0.99)) is True
def test_insufficient_history_always_runs(self):
state = StepCacheState(StepCacheConfig(min_history_steps=2))
assert state.should_run_step([(torch.randn(1, 4),)]) is True
def test_high_similarity_skips_with_countdown(self):
state = StepCacheState(
StepCacheConfig(
sim_thresholds=(0.95,),
skip_countdowns=(2,),
)
)
preds = _make_prev_predictions(0.99)
assert state.should_run_step(preds) is False
assert state.skip_countdown == 2
assert state.should_run_step(preds) is False
assert state.should_run_step(preds) is True
def test_low_similarity_runs(self):
state = StepCacheState(StepCacheConfig())
preds = _make_prev_predictions(0.5)
assert state.should_run_step(preds) is True
def test_trim_history(self):
state = StepCacheState(StepCacheConfig(max_history=2))
history: list[tuple[torch.Tensor]] = [(torch.randn(1, 2),) for _ in range(4)]
state.trim_history(history)
assert len(history) == 2
class TestStepCacheBackend:
def test_enable_dreamzero_pipeline(self):
mock_pipeline = Mock()
mock_pipeline.__class__.__name__ = "DreamZeroPipeline"
backend = StepCacheBackend(DiffusionCacheConfig())
backend.enable(mock_pipeline)
assert backend.enabled is True
cfg = getattr(mock_pipeline, STEP_CACHE_CONFIG_ATTR)
assert isinstance(cfg, StepCacheConfig)
assert is_stepcache_active(mock_pipeline)
state = getattr(mock_pipeline, STEP_CACHE_STATE_ATTR)
assert isinstance(state, StepCacheState)
def test_refresh_resets_state(self):
mock_pipeline = Mock()
mock_pipeline.__class__.__name__ = "DreamZeroPipeline"
backend = StepCacheBackend(DiffusionCacheConfig())
backend.enable(mock_pipeline)
state = getattr(mock_pipeline, STEP_CACHE_STATE_ATTR)
state.skip_countdown = 3
backend.refresh(mock_pipeline, num_inference_steps=16, verbose=False)
assert state.skip_countdown == 0
def test_enable_unsupported_pipeline_raises(self):
mock_pipeline = Mock()
mock_pipeline.__class__.__name__ = "QwenImagePipeline"
backend = StepCacheBackend(DiffusionCacheConfig())
with pytest.raises(ValueError, match="step_cache backend does not support"):
backend.enable(mock_pipeline)
def test_selector_returns_backend(self):
backend = get_cache_backend("step_cache", {"velocity_sim_thresholds": [0.9]})
assert isinstance(backend, StepCacheBackend)
assert backend.config.velocity_sim_thresholds == [0.9]
def test_legacy_backend_aliases(self):
assert isinstance(get_cache_backend("stepcache", {}), StepCacheBackend)
assert isinstance(get_cache_backend("step_cache_dit", {}), StepCacheBackend)
@@ -28,8 +28,6 @@ test_params = [
server_args=[
"--deploy-config",
"vllm_omni/deploy/dreamzero_tp1_cfg2.yaml",
"--enforce-eager",
"--disable-log-stats",
],
env_dict={
"ATTENTION_BACKEND": "torch",
@@ -88,6 +88,47 @@ def test_unpack_accepts_msgpack_numpy_marker_dicts():
np.testing.assert_allclose(decoded[b"actions"], np.asarray([[1.0, 0.0]], dtype=np.float32))
def test_unpack_accepts_openpi_client_ndarray_markers():
image = np.arange(6, dtype=np.uint8).reshape(2, 3)
payload = {
"observation/exterior_image_0_left": {
b"__ndarray__": True,
b"dtype": image.dtype.str,
b"shape": image.shape,
b"data": image.tobytes(),
}
}
decoded = openpi_connection._unpack_numpy(payload)
np.testing.assert_array_equal(decoded["observation/exterior_image_0_left"], image)
assert decoded["observation/exterior_image_0_left"].dtype == np.uint8
def test_unpack_openpi_client_packed_observation():
openpi_msgpack = pytest.importorskip("openpi_client.msgpack_numpy")
image = np.zeros((180, 320, 3), dtype=np.uint8)
obs = {
"observation/exterior_image_0_left": image,
"observation/joint_position": np.zeros(7, dtype=np.float32),
}
decoded = openpi_connection._unpack(openpi_msgpack.packb(obs))
np.testing.assert_array_equal(decoded["observation/exterior_image_0_left"], image)
np.testing.assert_allclose(decoded["observation/joint_position"], obs["observation/joint_position"])
def test_pack_uses_openpi_client_ndarray_markers():
openpi_msgpack = pytest.importorskip("openpi_client.msgpack_numpy")
actions = np.asarray([[1.0, 2.0]], dtype=np.float32)
packed = openpi_connection._pack(actions)
decoded = openpi_msgpack.unpackb(packed)
np.testing.assert_allclose(decoded, actions)
def test_unpack_leaves_user_dict_without_numpy_kind_marker_unchanged():
payload = {
"metadata": {
+6 -1
View File
@@ -12,7 +12,7 @@ stages:
- stage_id: 0
devices: "0"
max_num_seqs: 1
enforce_eager: true
enforce_eager: false
model_class_name: DreamZeroPipeline
model_config:
default_robot_embodiment: roboarena
@@ -23,3 +23,8 @@ stages:
needs_stereo_camera: false
needs_session_id: true
action_space: joint_position
cache_backend: step_cache
cache_config:
step_cache_dit_enabled: true
velocity_sim_thresholds: [0.95, 0.93]
velocity_skip_countdowns: [4, 2]
+6 -1
View File
@@ -9,7 +9,7 @@ stages:
- stage_id: 0
devices: "0,1"
max_num_seqs: 1
enforce_eager: true
enforce_eager: false
model_class_name: DreamZeroPipeline
parallel_config:
tensor_parallel_size: 1
@@ -23,3 +23,8 @@ stages:
needs_stereo_camera: false
needs_session_id: true
action_space: joint_position
cache_backend: step_cache
cache_config:
step_cache_dit_enabled: true
velocity_sim_thresholds: [0.95, 0.93]
velocity_skip_countdowns: [4, 2]
+2
View File
@@ -13,6 +13,8 @@ Main cache backend implementations:
the cache-dit library. Inherits from CacheBackend. Used via cache_backend="cache_dit".
2. TeaCacheBackend: Hook-based backend for TeaCache acceleration. Inherits from
CacheBackend. Used via cache_backend="tea_cache".
3. StepCacheBackend: Velocity cosine step-skipping for DreamZero. Inherits from
CacheBackend. Used via cache_backend="step_cache".
All backends implement the same interface:
- enable(pipeline): Enable cache on the pipeline
+9 -4
View File
@@ -3,6 +3,7 @@ from typing import Any
from vllm_omni.diffusion.cache.base import CacheBackend
from vllm_omni.diffusion.cache.cache_dit_backend import CacheDiTBackend
from vllm_omni.diffusion.cache.magcache.backend import MagCacheBackend
from vllm_omni.diffusion.cache.stepcache import StepCacheBackend
from vllm_omni.diffusion.cache.teacache.backend import TeaCacheBackend
from vllm_omni.diffusion.data import DiffusionCacheConfig
@@ -14,14 +15,15 @@ def get_cache_backend(cache_backend: str | None, cache_config: Any) -> CacheBack
- cache_dit: Uses CacheDiTBackend with enable()/refresh() interface
- tea_cache: Uses TeaCacheBackend with enable()/refresh() interface
- mag_cache: Uses MagCacheBackend with enable()/refresh() interface
- step_cache: Uses StepCacheBackend for DreamZero velocity step skip
Args:
cache_backend: Cache backend name ("cache_dit", "tea_cache", "mag_cache", or None).
cache_backend: Cache backend name ("cache_dit", "tea_cache",
"mag_cache", "step_cache", or None).
cache_config: Cache configuration (dict or DiffusionCacheConfig instance).
Returns:
Cache backend instance (CacheDiTBackend, TeaCacheBackend, or MagCacheBackend)
if cache_backend is set, None otherwise.
Cache backend instance if cache_backend is set, None otherwise.
Raises:
ValueError: If cache_backend is unsupported.
@@ -38,7 +40,10 @@ def get_cache_backend(cache_backend: str | None, cache_config: Any) -> CacheBack
return TeaCacheBackend(cache_config)
elif cache_backend == "mag_cache":
return MagCacheBackend(cache_config)
elif cache_backend in ("step_cache", "stepcache", "step_cache_dit"):
return StepCacheBackend(cache_config)
else:
raise ValueError(
f"Unsupported cache backend: {cache_backend}. Supported: 'cache_dit', 'tea_cache', 'mag_cache'"
f"Unsupported cache backend: {cache_backend}. "
"Supported: 'cache_dit', 'tea_cache', 'mag_cache', 'step_cache'"
)
+66
View File
@@ -0,0 +1,66 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
StepCache: velocity-based step skipping for DreamZero-style DiT denoising.
Unlike block-level Cache-DiT, this backend skips entire DiT forwards during the
pipeline denoise loop when successive velocity predictions are highly aligned.
Usage:
from vllm_omni import Omni
omni = Omni(
model="...",
cache_backend="step_cache",
)
# Alternative: environment variable
# export DIFFUSION_CACHE_BACKEND=step_cache
"""
from vllm_omni.diffusion.cache.stepcache.backend import (
CUSTOM_STEP_CACHE_DIT_ENABLERS,
CUSTOM_STEPCACHE_ENABLERS,
STEP_CACHE_CONFIG_ATTR,
STEP_CACHE_DIT_CONFIG_ATTR,
STEP_CACHE_DIT_STATE_ATTR,
STEP_CACHE_STATE_ATTR,
StepCacheBackend,
StepCacheDiTBackend,
StepCacheDiTConfig,
StepCacheDiTState,
enable_dreamzero_step_cache_dit,
enable_dreamzero_stepcache,
get_step_cache_dit_config,
get_step_cache_dit_state,
get_stepcache_config,
get_stepcache_state,
is_step_cache_dit_active,
is_stepcache_active,
)
from vllm_omni.diffusion.cache.stepcache.config import StepCacheConfig
from vllm_omni.diffusion.cache.stepcache.state import StepCacheState
__all__ = [
"CUSTOM_STEP_CACHE_DIT_ENABLERS",
"CUSTOM_STEPCACHE_ENABLERS",
"STEP_CACHE_CONFIG_ATTR",
"STEP_CACHE_DIT_CONFIG_ATTR",
"STEP_CACHE_DIT_STATE_ATTR",
"STEP_CACHE_STATE_ATTR",
"StepCacheBackend",
"StepCacheConfig",
"StepCacheDiTBackend",
"StepCacheDiTConfig",
"StepCacheDiTState",
"StepCacheState",
"enable_dreamzero_step_cache_dit",
"enable_dreamzero_stepcache",
"get_step_cache_dit_config",
"get_step_cache_dit_state",
"get_stepcache_config",
"get_stepcache_state",
"is_step_cache_dit_active",
"is_stepcache_active",
]
+129
View File
@@ -0,0 +1,129 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
StepCache backend implementation.
This module provides the stepcache backend that implements the CacheBackend
interface for DreamZero-style velocity-based step skipping in the denoise loop.
"""
from __future__ import annotations
from typing import Any
from vllm.logger import init_logger
from vllm_omni.diffusion.cache.base import CacheBackend
from vllm_omni.diffusion.cache.stepcache.config import (
STEP_CACHE_CONFIG_ATTR,
STEP_CACHE_STATE_ATTR,
StepCacheConfig,
)
from vllm_omni.diffusion.cache.stepcache.state import StepCacheState
from vllm_omni.diffusion.data import DiffusionCacheConfig
logger = init_logger(__name__)
# Backward-compatible aliases for older imports and pipeline attribute names.
STEP_CACHE_DIT_CONFIG_ATTR = STEP_CACHE_CONFIG_ATTR
STEP_CACHE_DIT_STATE_ATTR = STEP_CACHE_STATE_ATTR
StepCacheDiTConfig = StepCacheConfig
StepCacheDiTState = StepCacheState
def is_stepcache_active(pipeline: Any) -> bool:
"""Return True when stepcache is enabled on *pipeline*."""
cfg = getattr(pipeline, STEP_CACHE_CONFIG_ATTR, None)
return isinstance(cfg, StepCacheConfig) and cfg.enabled
def get_stepcache_config(pipeline: Any) -> StepCacheConfig | None:
cfg = getattr(pipeline, STEP_CACHE_CONFIG_ATTR, None)
return cfg if isinstance(cfg, StepCacheConfig) else None
def get_stepcache_state(pipeline: Any) -> StepCacheState | None:
state = getattr(pipeline, STEP_CACHE_STATE_ATTR, None)
return state if isinstance(state, StepCacheState) else None
def is_step_cache_dit_active(pipeline: Any) -> bool:
return is_stepcache_active(pipeline)
def get_step_cache_dit_config(pipeline: Any) -> StepCacheConfig | None:
return get_stepcache_config(pipeline)
def get_step_cache_dit_state(pipeline: Any) -> StepCacheState | None:
return get_stepcache_state(pipeline)
def enable_dreamzero_stepcache(pipeline: Any, config: DiffusionCacheConfig) -> None:
"""Enable stepcache for DreamZeroPipeline."""
cache_config = StepCacheConfig.from_diffusion_cache_config(config)
setattr(pipeline, STEP_CACHE_CONFIG_ATTR, cache_config)
setattr(pipeline, STEP_CACHE_STATE_ATTR, StepCacheState(cache_config))
logger.info(
"stepcache enabled on DreamZeroPipeline (enabled=%s, thresholds=%s, countdowns=%s)",
cache_config.enabled,
cache_config.sim_thresholds,
cache_config.skip_countdowns,
)
enable_dreamzero_step_cache_dit = enable_dreamzero_stepcache
CUSTOM_STEPCACHE_ENABLERS = {
"DreamZeroPipeline": enable_dreamzero_stepcache,
}
CUSTOM_STEP_CACHE_DIT_ENABLERS = CUSTOM_STEPCACHE_ENABLERS
class StepCacheBackend(CacheBackend):
"""
Velocity cosine step-skipping cache backend for DreamZero.
Attaches :class:`StepCacheConfig` and :class:`StepCacheState` to supported
pipelines. The denoise loop calls :meth:`StepCacheState.should_run_step` to
decide whether to run ``predict_noise``.
Example:
>>> from vllm_omni.diffusion.data import DiffusionCacheConfig
>>> backend = StepCacheBackend(DiffusionCacheConfig())
>>> backend.enable(pipeline)
>>> backend.refresh(pipeline, num_inference_steps=16)
"""
def enable(self, pipeline: Any) -> None:
"""Enable stepcache on a supported pipeline."""
pipeline_type = pipeline.__class__.__name__
if pipeline_type in CUSTOM_STEPCACHE_ENABLERS:
logger.info("Using custom stepcache enabler for model: %s", pipeline_type)
CUSTOM_STEPCACHE_ENABLERS[pipeline_type](pipeline, self.config)
else:
raise ValueError(
f"step_cache backend does not support {pipeline_type}. Supported: {sorted(CUSTOM_STEPCACHE_ENABLERS)}"
)
self.enabled = True
def refresh(self, pipeline: Any, num_inference_steps: int, verbose: bool = True) -> None:
"""Refresh stepcache state for a new generation."""
state = get_stepcache_state(pipeline)
if state is not None:
state.reset()
if verbose:
logger.debug(
"stepcache state refreshed (num_inference_steps=%d)",
num_inference_steps,
)
elif verbose and is_stepcache_active(pipeline):
logger.warning("stepcache config active but state not found on pipeline")
StepCacheDiTBackend = StepCacheBackend
+51
View File
@@ -0,0 +1,51 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Configuration for step-level DiT velocity caching."""
from __future__ import annotations
from dataclasses import dataclass
from vllm_omni.diffusion.data import DiffusionCacheConfig
# Pipeline attributes set by StepCacheBackend.enable().
STEP_CACHE_CONFIG_ATTR = "_stepcache_config"
STEP_CACHE_STATE_ATTR = "_stepcache_state"
@dataclass(frozen=True)
class StepCacheConfig:
"""Runtime config for velocity-based step skipping.
Skips entire DiT forwards when successive video velocity predictions are
highly aligned (cosine similarity above configured thresholds).
Reference: DreamZero paper DiT Caching / dreamzero.git ``should_run_model``.
"""
enabled: bool = True
min_history_steps: int = 2
max_history: int = 2
sim_thresholds: tuple[float, ...] = (0.95, 0.93)
skip_countdowns: tuple[int, ...] = (4, 2)
@classmethod
def from_diffusion_cache_config(cls, config: DiffusionCacheConfig) -> StepCacheConfig:
enabled = config.step_cache_dit_enabled
thresholds = tuple(config.velocity_sim_thresholds)
countdowns = tuple(config.velocity_skip_countdowns)
if len(thresholds) != len(countdowns):
raise ValueError(
"velocity_sim_thresholds and velocity_skip_countdowns must have the same length; "
f"got {len(thresholds)} and {len(countdowns)}."
)
return cls(
enabled=enabled,
min_history_steps=config.step_cache_dit_min_history,
max_history=config.step_cache_dit_max_history,
sim_thresholds=thresholds,
skip_countdowns=countdowns,
)
+51
View File
@@ -0,0 +1,51 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Mutable state for step-level DiT velocity caching."""
from __future__ import annotations
import torch
from vllm_omni.diffusion.cache.stepcache.config import StepCacheConfig
class StepCacheState:
"""Per-generation mutable state (skip countdown)."""
def __init__(self, config: StepCacheConfig) -> None:
self.config = config
self.skip_countdown = 0
def reset(self) -> None:
self.skip_countdown = 0
def should_run_step(self, prev_predictions: list[tuple[torch.Tensor, ...]]) -> bool:
"""Return True when the DiT forward should execute at this scheduler step."""
if not self.config.enabled:
return True
if len(prev_predictions) < self.config.min_history_steps:
return True
if self.skip_countdown > 1:
self.skip_countdown -= 1
return False
if self.skip_countdown == 1:
self.skip_countdown = 0
return True
v_last = prev_predictions[-1][0].flatten(1).float()
v_prev = prev_predictions[-2][0].flatten(1).float()
sim = torch.nn.functional.cosine_similarity(v_last, v_prev, dim=1).mean()
for threshold, countdown in zip(self.config.sim_thresholds, self.config.skip_countdowns):
if sim > threshold:
self.skip_countdown = countdown
return False
return True
def trim_history(self, prev_predictions: list[tuple[torch.Tensor, ...]]) -> None:
while len(prev_predictions) > self.config.max_history:
prev_predictions.pop(0)
+9
View File
@@ -302,6 +302,8 @@ class DiffusionCacheConfig:
scm_steps_mask_policy, scm_steps_policy
- MagCache: mag_threshold, mag_max_skip_steps, mag_retention_ratio,
mag_ratios, mag_calibrate
- step_cache: step_cache_dit_enabled, velocity_sim_thresholds,
velocity_skip_countdowns, step_cache_dit_min_history
Example:
>>> # From dict (user-facing API) - partial config uses defaults for missing keys
@@ -375,6 +377,13 @@ class DiffusionCacheConfig:
# "repeat" refreshes every force_refresh_step_hint steps.
force_refresh_step_policy: str = "once"
# step_cache parameters [step_cache only] — DreamZero velocity schedule
step_cache_dit_enabled: bool = True
velocity_sim_thresholds: list[float] = field(default_factory=lambda: [0.95, 0.93])
velocity_skip_countdowns: list[int] = field(default_factory=lambda: [4, 2])
step_cache_dit_min_history: int = 2
step_cache_dit_max_history: int = 2
# Additional parameters that may be passed but not explicitly defined
_extra_params: dict[str, Any] = field(default_factory=dict, repr=False)
@@ -23,6 +23,10 @@ from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer, UMT5Config, UMT5EncoderModel
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm_omni.diffusion.cache.stepcache import (
get_stepcache_state,
is_stepcache_active,
)
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import (
DistributedAutoencoderKLWan,
@@ -55,10 +59,6 @@ logger = logging.getLogger(__name__)
MAX_DREAMZERO_SESSIONS = 64
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class VideoActionScheduler:
"""Wraps video + action schedulers into single .step() interface."""
@@ -205,6 +205,9 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
self._max_session_states = MAX_DREAMZERO_SESSIONS
self.state = self._get_or_create_state("default")
# DiT step cache is configured by StepCacheBackend
# (cache_backend="step_cache") via pipeline._stepcache_config.
# Keep runtime inference settings separate from the training-time config.
self.num_inference_steps: int = model_config.get(
"num_inference_steps",
@@ -306,6 +309,27 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
def predict_noise(self, **kwargs) -> tuple[torch.Tensor, torch.Tensor]:
"""Call CausalWanModel, return (video_pred, action_pred)."""
video_pred, action_pred = self._predict_noise_eager(kwargs)
if is_stepcache_active(self):
video_pred = video_pred.clone()
if action_pred is not None:
action_pred = action_pred.clone()
if action_pred is None:
batch_size = kwargs["hidden_states"].shape[0]
action_pred = torch.empty(
batch_size,
0,
self.transformer.action_dim,
device=video_pred.device,
dtype=video_pred.dtype,
)
return (video_pred, action_pred)
def _predict_noise_eager(self, kwargs: dict) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Eager DiT forward; also handles KV-cache write-back on prefill."""
self._cudagraph_mark_step_begin()
video_pred, action_pred, updated_kv_caches = self.transformer(
x=kwargs["hidden_states"],
timestep=kwargs["timestep_video"],
@@ -327,19 +351,118 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
for i, kv in enumerate(updated_kv_caches):
state.update_kv_cache(i, kv, is_negative=is_neg)
video_pred = video_pred.clone()
if action_pred is not None:
action_pred = action_pred.clone()
else:
batch_size = kwargs["hidden_states"].shape[0]
action_pred = torch.empty(
batch_size,
0,
self.transformer.action_dim,
device=video_pred.device,
dtype=video_pred.dtype,
) # CFG-parallel-safe dummy action pred
return (video_pred, action_pred)
return video_pred, action_pred
# -----------------------------------------------------------------------
# torch.compile setup (paper D.2 extended: encoders + VAE + DiT blocks)
# -----------------------------------------------------------------------
def setup_compile(self) -> None:
"""Compile DreamZero encoders, VAE, and per-block DiT for inference.
Paper D.2 uses ``mode=reduce-overhead``, ``fullgraph=True``, ``dynamic=False``
on text/image/VAE and DiT. VAE decode uses a tensor feat_cache patch and
compiles ``decoder.forward`` (not ``_decode``, which has a Python frame loop).
DiT blocks use per-block ``fullgraph=True``.
"""
if not torch.cuda.is_available():
logger.info("DreamZero setup_compile skipped: CUDA not available.")
return
from vllm_omni.diffusion.models.dreamzero.wan_vae_feat_cache_patch import (
apply_wan_vae_feat_cache_tensor_patch,
)
apply_wan_vae_feat_cache_tensor_patch()
compile_ro = {"mode": "reduce-overhead", "fullgraph": True, "dynamic": False}
# DiT blocks: default avoids CUDAGraph overwrite on modulation tensors; encoders use reduce-overhead.
dit_compile = {"mode": "default", "fullgraph": True, "dynamic": False}
logger.info(
"DreamZero: torch.compile text/image/VAE encode + per-block DiT (encoders reduce-overhead, DiT default)."
)
try:
self.text_encoder.forward = torch.compile(self.text_encoder.forward, **compile_ro)
except Exception as exc:
logger.warning("DreamZero: text_encoder compile failed (%s); skipping.", exc)
try:
self.image_encoder.model.visual.forward = torch.compile(
self.image_encoder.model.visual.forward,
**compile_ro,
)
except Exception as exc:
logger.warning("DreamZero: image_encoder compile failed (%s); skipping.", exc)
try:
self.vae.encode = torch.compile(self.vae.encode, **compile_ro)
except Exception as exc:
logger.warning("DreamZero: vae.encode compile failed (%s); skipping.", exc)
compiled_blocks = 0
for block in self.transformer.blocks:
try:
block.forward = torch.compile(block.forward, **dit_compile)
compiled_blocks += 1
except Exception as exc:
logger.warning(
"DreamZero: transformer block %d compile failed (%s); leaving remaining eager.",
compiled_blocks,
exc,
)
break
if compiled_blocks:
logger.info(
"DreamZero: compiled %d/%d transformer blocks.",
compiled_blocks,
len(self.transformer.blocks),
)
self.warmup_compile()
def warmup_compile(self) -> None:
"""Warm up compiled text/image/VAE paths before timed inference."""
if not torch.cuda.is_available():
return
device = next(self.text_encoder.parameters()).device
with torch.inference_mode():
try:
text_tokens = torch.zeros(1, 16, dtype=torch.long, device=device)
attention_mask = torch.ones_like(text_tokens)
self._encode_text(text_tokens, attention_mask)
except Exception as exc:
logger.warning("DreamZero compile warmup (text_encoder) skipped: %s", exc)
try:
image = torch.zeros(1, 1, 3, 180, 320, dtype=torch.bfloat16, device=device)
self._encode_image(image, self.num_frames, 180, 320)
except Exception as exc:
logger.warning("DreamZero compile warmup (image_encoder) skipped: %s", exc)
try:
latent_h, latent_w = 180 // 8, 320 // 8
dummy_latent = torch.zeros(
1,
16,
self.num_frame_per_block,
latent_h * 2,
latent_w * 2,
dtype=self.vae.dtype,
device=device,
)
mean = self.vae_latents_mean.to(device=device, dtype=self.vae.dtype)
inv_std = self.vae_latents_inv_std.to(device=device, dtype=self.vae.dtype)
dummy_latent_denorm = dummy_latent / inv_std + mean
self._cudagraph_mark_step_begin()
self.vae.decode(dummy_latent_denorm, return_dict=False)
except Exception as exc:
logger.warning("DreamZero compile warmup (vae decode) skipped: %s", exc)
torch.accelerator.synchronize(device)
logger.info("DreamZero compile warmup finished (text / image / vae decode).")
def combine_cfg_noise(
self,
@@ -388,12 +511,20 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
videos = videos.reshape(b, t, c, h, w).permute(0, 2, 1, 3, 4)
return videos.to(dtype=torch.bfloat16)
@staticmethod
def _cudagraph_mark_step_begin() -> None:
try:
torch.compiler.cudagraph_mark_step_begin()
except Exception:
pass
# -----------------------------------------------------------------------
# Text encoding
# -----------------------------------------------------------------------
def _encode_text(self, text_tokens: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""Encode text prompt via UMT5."""
self._cudagraph_mark_step_begin()
seq_lens = attention_mask.gt(0).sum(dim=1).long()
prompt_emb = self.text_encoder(
text_tokens,
@@ -422,6 +553,7 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
batch_size = image.shape[0]
with torch.amp.autocast(dtype=torch.bfloat16, device_type=device.type):
self._cudagraph_mark_step_begin()
clip_context = self.image_encoder.encode_image(image)
msk = torch.ones(batch_size, num_frames, height // 8, width // 8, device=device)
@@ -469,8 +601,23 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
inv_std = self.vae_latents_inv_std.to(device=vae_device, dtype=vae_dtype)
latents = latents / inv_std + mean
with torch.no_grad():
self._cudagraph_mark_step_begin()
return self.vae.decode(latents, return_dict=False)[0]
def decode_accumulated_video_latents(self, session_id: str | None = None) -> torch.Tensor:
"""Decode all AR-chunk latents accumulated for ``session_id``."""
state = self._get_or_create_state(session_id)
latents = state.get_concatenated_video_latents()
if latents is None:
session_key = str(session_id or "default")
raise RuntimeError(f"No accumulated video latents for session {session_key!r}.")
return self.decode_video_latents(latents)
def clear_accumulated_video_latents(self, session_id: str | None = None) -> None:
"""Clear accumulated video latents for ``session_id`` without resetting KV state."""
state = self._get_or_create_state(session_id)
state.clear_video_latents()
# -----------------------------------------------------------------------
# KV cache prefill
# -----------------------------------------------------------------------
@@ -635,6 +782,12 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
noisy_input = video_latents
noisy_input_action = action_latents
# ---- step_cache (StepCacheBackend / dreamzero.git) ----
_cached_flow_pred: torch.Tensor | None = None
_cached_flow_pred_action: torch.Tensor | None = None
_prev_predictions: list[tuple[torch.Tensor]] = []
_step_cache = get_stepcache_state(self) if is_stepcache_active(self) else None
for index in range(len(timesteps_video)):
video_timestep = timesteps_video[index]
action_timestep = timesteps_action[index]
@@ -663,46 +816,58 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
else:
y = state.ys[:, :, -self.num_frame_per_block :]
positive_kwargs = dict(
hidden_states=noisy_input.transpose(1, 2),
timestep_video=timestep,
encoder_hidden_states=prompt_embeds,
kv_cache=state.get_kv_caches(False),
crossattn_cache=state.get_crossattn_caches(False),
y=y,
clip_feature=state.clip_feas,
action=noisy_input_action,
timestep_action=timestep_action,
is_negative=False,
**common_kwargs,
)
# Negative (uncond) kwargs
if do_true_cfg and negative_prompt_embeds is not None:
negative_kwargs = dict(
run_dit = _step_cache is None or _step_cache.should_run_step(_prev_predictions)
if run_dit:
positive_kwargs = dict(
hidden_states=noisy_input.transpose(1, 2),
timestep_video=timestep,
encoder_hidden_states=negative_prompt_embeds,
kv_cache=state.get_kv_caches(True),
crossattn_cache=state.get_crossattn_caches(True),
encoder_hidden_states=prompt_embeds,
kv_cache=state.get_kv_caches(False),
crossattn_cache=state.get_crossattn_caches(False),
y=y,
clip_feature=state.clip_feas,
action=noisy_input_action,
timestep_action=timestep_action,
is_negative=True,
is_negative=False,
**common_kwargs,
)
else:
negative_kwargs = None
noise_pred = self.predict_noise_maybe_with_cfg(
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
do_true_cfg=do_true_cfg,
true_cfg_scale=self.cfg_scale,
cfg_normalize=False,
)
flow_pred, flow_pred_action = noise_pred
if do_true_cfg and negative_prompt_embeds is not None:
negative_kwargs = dict(
hidden_states=noisy_input.transpose(1, 2),
timestep_video=timestep,
encoder_hidden_states=negative_prompt_embeds,
kv_cache=state.get_kv_caches(True),
crossattn_cache=state.get_crossattn_caches(True),
y=y,
clip_feature=state.clip_feas,
action=noisy_input_action,
timestep_action=timestep_action,
is_negative=True,
**common_kwargs,
)
else:
negative_kwargs = None
noise_pred = self.predict_noise_maybe_with_cfg(
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
do_true_cfg=do_true_cfg,
true_cfg_scale=self.cfg_scale,
cfg_normalize=False,
)
flow_pred, flow_pred_action = noise_pred
_cached_flow_pred = flow_pred
_cached_flow_pred_action = flow_pred_action
_prev_predictions.append((flow_pred,))
if _step_cache is not None:
_step_cache.trim_history(_prev_predictions)
else:
# Reuse previous prediction (DiT step-skipping cache).
assert _cached_flow_pred is not None, "DiT cache: no cached prediction available for step skip."
flow_pred = _cached_flow_pred
flow_pred_action = _cached_flow_pred_action
latents = (noisy_input, noisy_input_action)
t = (video_timestep, action_timestep)
@@ -811,9 +976,12 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
# on the next inference request after websocket reset/session switch.
if extra_args.get("reset", False):
state.reset()
# Auto-reset based on model state (before accumulation)
if state.should_reset(text_tokens, 0, self.transformer.local_attn_size):
state.reset()
else:
reset_reason = state.reset_reason(text_tokens, 0, self.transformer.local_attn_size)
if reset_reason == "session":
state.reset()
elif reset_reason == "inference":
state.reset_inference_state()
state.language = text_tokens
# Frame accumulation: stitched single frame → multi-frame video
@@ -959,6 +1127,8 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
video_out = torch.cat([image, video_out], dim=1)
state.current_start_frame += self.num_frame_per_block
state.append_video_latents(video_out)
# q99 denorm: [-1,1] → real values
action_out = self._denormalize_action(action_out.float(), embodiment_name)
@@ -23,7 +23,8 @@ class DreamZeroState:
Lifecycle:
- Created once in DreamZeroPipeline.__init__()
- Mutated every forward() call (frame append, KV cache grow)
- reset() on new session / language change / local_attn_size exceeded
- reset() on new session / language change
- reset_inference_state() on local_attn_size exceeded (KV only)
"""
def __init__(self) -> None:
@@ -65,50 +66,106 @@ class DreamZeroState:
self.call_count += 1
return np.stack(frames, axis=0) # (T, H, W, C)
def append_video_latents(self, video_out: torch.Tensor) -> None:
"""Append one AR chunk of normalized video latents for later decode.
Args:
video_out: ``(B, T, C, H, W)`` tensor returned by the denoise loop
(before the ``transpose(1, 2)`` stored in ``DiffusionOutput``).
"""
if video_out.dim() != 5:
raise ValueError(f"Expected 5D video_out, got shape {tuple(video_out.shape)}")
# Upstream ``torch.cat(self.video_across_time, dim=2)`` uses (B, C, T, H, W).
chunk = video_out.transpose(1, 2).detach().cpu()
self.video_latents_across_time.append(chunk)
logger.info(
"append_video_latents: chunk_shape=%s total_chunks=%d total_latent_t=%d",
tuple(chunk.shape),
len(self.video_latents_across_time),
int(sum(c.shape[2] for c in self.video_latents_across_time)),
)
def get_concatenated_video_latents(self) -> torch.Tensor | None:
"""Return all accumulated chunks concatenated along the time dimension."""
if not self.video_latents_across_time:
return None
if len(self.video_latents_across_time) == 1:
return self.video_latents_across_time[0]
return torch.cat(self.video_latents_across_time, dim=2)
def clear_video_latents(self) -> None:
"""Drop accumulated video latents without resetting KV/frame state."""
self.video_latents_across_time = []
# ------------------------------------------------------------------
# Reset / should_reset
# ------------------------------------------------------------------
def reset(self) -> None:
"""Clear all state."""
self.stitched_buffer: deque[np.ndarray] = deque(maxlen=FRAMES_PER_CHUNK)
self.call_count: int = 0
def reset(self, *, clear_video_latents: bool = True) -> None:
"""Clear session state.
Args:
clear_video_latents: When ``False``, keep ``video_latents_across_time``
so offline/online export can decode the full AR rollout.
"""
saved_video_latents = [] if clear_video_latents else list(self.video_latents_across_time)
self.stitched_buffer = deque(maxlen=FRAMES_PER_CHUNK)
self.call_count = 0
# Normalized VAE latents per AR chunk, each (B, C, T, H, W) on CPU.
# Concatenated along T before decode (matches upstream video_across_time).
self.video_latents_across_time = saved_video_latents
# KV cache once robot-policy diffusion supports that integration.
self.kv_cache: list[torch.Tensor] | None = None
self.kv_cache_neg: list[torch.Tensor] | None = None
self.crossattn_cache: list[dict[str, bool | torch.Tensor | None]] | None = None
self.crossattn_cache_neg: list[dict[str, bool | torch.Tensor | None]] | None = None
self.current_start_frame: int = 0
self.kv_cache = None
self.kv_cache_neg = None
self.crossattn_cache = None
self.crossattn_cache_neg = None
self.current_start_frame = 0
self.clip_feas: torch.Tensor | None = None
self.ys: torch.Tensor | None = None
self.language: torch.Tensor | None = None
self.clip_feas = None
self.ys = None
if clear_video_latents:
self.language = None
def should_reset(self, text_tokens: torch.Tensor | None, num_video_frames: int, local_attn_size: int) -> bool:
"""Determine if state should be reset before this forward()."""
def reset_inference_state(self) -> None:
"""Reset KV/frame state after local attention rolls without dropping video latents."""
self.reset(clear_video_latents=False)
def reset_reason(
self,
text_tokens: torch.Tensor | None,
num_video_frames: int,
local_attn_size: int,
) -> str | None:
"""Return why state should reset before the next forward(), if any."""
if self.language is None:
logger.info("language is None, resetting")
return True
return "session"
if text_tokens is not None and not torch.equal(self.language, text_tokens):
logger.info("language changed, resetting")
return True
return "session"
# NOTE: after accumulate_frames, num_video_frames is the accumulated T
# (1 for first call, 4 for subsequent). Only reset on true single-frame
# which happens when the stitched_buffer was cleared externally.
if num_video_frames == 1 and self.call_count > 1:
logger.info("single frame input after first call, resetting")
return True
return "session"
if local_attn_size != -1 and self.current_start_frame >= local_attn_size:
logger.info(
"current_start_frame %d >= local_attn_size %d, resetting", self.current_start_frame, local_attn_size
"current_start_frame %d >= local_attn_size %d, resetting inference state",
self.current_start_frame,
local_attn_size,
)
return True
return "inference"
return False
return None
def should_reset(self, text_tokens: torch.Tensor | None, num_video_frames: int, local_attn_size: int) -> bool:
"""Determine if state should be reset before this forward()."""
return self.reset_reason(text_tokens, num_video_frames, local_attn_size) is not None
# ------------------------------------------------------------------
# KV cache management
@@ -9,13 +9,30 @@ import torch
class DreamZeroVideoExportWorkerExtension:
"""DreamZero worker RPCs used by offline example video export."""
@staticmethod
def _latents_to_uint8_frames(decoded: torch.Tensor) -> torch.Tensor:
decoded = decoded.squeeze(0).permute(1, 2, 3, 0).contiguous()
decoded = decoded.clamp(-1, 1) * 0.5 + 0.5
return (decoded * 255.0).round().to(torch.uint8).cpu()
def decode_video_latents_to_uint8(self, video_latents: torch.Tensor) -> torch.Tensor:
if self.model_runner is None or self.model_runner.pipeline is None:
raise RuntimeError("DreamZero pipeline is not initialized on this worker.")
with torch.inference_mode():
decoded = self.model_runner.pipeline.decode_video_latents(video_latents)
decoded = decoded.squeeze(0).permute(1, 2, 3, 0).contiguous()
decoded = decoded.clamp(-1, 1) * 0.5 + 0.5
decoded = (decoded * 255.0).round().to(torch.uint8).cpu()
return decoded
return self._latents_to_uint8_frames(decoded)
def decode_accumulated_video_latents_to_uint8(self, session_id: str = "default") -> torch.Tensor:
"""Decode all AR-chunk latents accumulated on the server for one session."""
if self.model_runner is None or self.model_runner.pipeline is None:
raise RuntimeError("DreamZero pipeline is not initialized on this worker.")
with torch.inference_mode():
decoded = self.model_runner.pipeline.decode_accumulated_video_latents(session_id)
return self._latents_to_uint8_frames(decoded)
def clear_accumulated_video_latents(self, session_id: str = "default") -> None:
if self.model_runner is None or self.model_runner.pipeline is None:
raise RuntimeError("DreamZero pipeline is not initialized on this worker.")
self.model_runner.pipeline.clear_accumulated_video_latents(session_id)
@@ -0,0 +1,92 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Make diffusers Wan VAE feat_cache compile-friendly.
diffusers ``WanResample`` stores the string sentinel ``"Rep"`` in feat_cache slots.
``tensor == "Rep"`` returns a non-Tensor and breaks ``torch.compile(fullgraph=True)``.
We replace it with a scalar int8 tensor marker (value 0).
"""
from __future__ import annotations
from typing import Any
import torch
from vllm.logger import init_logger
logger = init_logger(__name__)
_PATCHED = False
def _is_feat_rep(entry: Any) -> bool:
if not isinstance(entry, torch.Tensor):
return False
return entry.numel() == 1 and entry.dtype == torch.int8
def _make_feat_rep(x: torch.Tensor) -> torch.Tensor:
return torch.zeros(1, device=x.device, dtype=torch.int8)
def _wan_resample_forward_tensorized(self, x, feat_cache=None, feat_idx=[0]):
"""``WanResample.forward`` with tensor feat_cache sentinel (no ``"Rep"`` string)."""
from diffusers.models.autoencoders.autoencoder_kl_wan import CACHE_T
b, c, t, h, w = x.size()
if self.mode == "upsample3d":
if feat_cache is not None:
idx = feat_idx[0]
if feat_cache[idx] is None:
feat_cache[idx] = _make_feat_rep(x)
feat_idx[0] += 1
else:
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None and not _is_feat_rep(feat_cache[idx]):
cache_x = torch.cat(
[feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x],
dim=2,
)
if cache_x.shape[2] < 2 and feat_cache[idx] is not None and _is_feat_rep(feat_cache[idx]):
cache_x = torch.cat([torch.zeros_like(cache_x).to(cache_x.device), cache_x], dim=2)
if _is_feat_rep(feat_cache[idx]):
x = self.time_conv(x)
else:
x = self.time_conv(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
x = x.reshape(b, c, t * 2, h, w)
t = x.shape[2]
x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
x = self.resample(x)
x = x.view(b, t, x.size(1), x.size(2), x.size(3)).permute(0, 2, 1, 3, 4)
if self.mode == "downsample3d":
if feat_cache is not None:
idx = feat_idx[0]
if feat_cache[idx] is None:
feat_cache[idx] = x.clone()
feat_idx[0] += 1
else:
cache_x = x[:, :, -1:, :, :].clone()
x = self.time_conv(torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
feat_cache[idx] = cache_x
feat_idx[0] += 1
return x
def apply_wan_vae_feat_cache_tensor_patch() -> None:
"""Monkeypatch diffusers WanResample once per process."""
global _PATCHED
if _PATCHED:
return
from diffusers.models.autoencoders.autoencoder_kl_wan import WanResample
WanResample.forward = _wan_resample_forward_tensorized
_PATCHED = True
logger.info("DreamZero: applied Wan VAE feat_cache tensor sentinel patch.")
@@ -168,8 +168,17 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
# Apply torch.compile if not in eager mode
if not self.od_config.enforce_eager:
if current_omni_platform.supports_torch_inductor():
self._compile_transformer("transformer")
self._compile_transformer("transformer_2")
if hasattr(self.pipeline, "setup_compile"):
try:
self.pipeline.setup_compile()
except Exception as exc:
logger.warning(
"Model runner: setup_compile() failed (%s); running without compile.",
exc,
)
else:
self._compile_transformer("transformer")
self._compile_transformer("transformer_2")
else:
logger.warning(
"Model runner: Platform %s does not support torch inductor, skipping torch.compile.",
@@ -306,8 +315,15 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
# num_inference_steps at all (i.e., just resets state and clears
# stale residuals).
num_inference_steps = req.sampling_params.num_inference_steps
if self.od_config.cache_backend == "tea_cache" and num_inference_steps is None:
num_inference_steps = 0
if num_inference_steps is None and self.od_config.cache_backend in (
"tea_cache",
"step_cache",
):
# TeaCache refresh ignores the value; step_cache refresh is a
# no-op (per-chunk state resets in the denoise loop). DreamZero often
# leaves sampling_params.num_inference_steps unset and uses the
# pipeline default instead.
num_inference_steps = getattr(self.pipeline, "num_inference_steps", 0) or 0
if num_inference_steps is not None:
self.cache_backend.refresh(self.pipeline, num_inference_steps)
+9 -1
View File
@@ -520,7 +520,7 @@ class AsyncOmniEngine:
return
mm_data = prompt.get("multi_modal_data")
if not isinstance(mm_data, Mapping) or not mm_data:
if not isinstance(mm_data, dict) or not mm_data:
return
from vllm.multimodal.hasher import MultiModalHasher
@@ -842,6 +842,14 @@ class AsyncOmniEngine:
"mag_max_skip_steps": 5,
"mag_retention_ratio": 0.1,
}
if cache_backend in ("step_cache"):
return {
"step_cache_dit_enabled": True,
"velocity_sim_thresholds": [0.95, 0.93],
"velocity_skip_countdowns": [4, 2],
"step_cache_dit_min_history": 2,
"step_cache_dit_max_history": 2,
}
return None
@staticmethod
+1 -1
View File
@@ -512,7 +512,7 @@ class OmniServeCommand(CLISubcommand):
"--cache-backend",
type=str,
default="none",
help="Cache backend for diffusion models, options: 'tea_cache', 'cache_dit', 'mag_cache'",
help="Cache backend for diffusion models, options: 'tea_cache', 'cache_dit', 'mag_cache', 'step_cache'",
)
omni_config_group.add_argument(
"--cache-config",
+71 -27
View File
@@ -8,7 +8,13 @@ Protocol (compatible with OpenPI policy clients):
Infer -> client sends msgpack(obs), server sends msgpack(ndarray)
Reset -> client sends msgpack({endpoint:reset}), server sends msgpack(status)
NumPy values use the msgpack-numpy marker mapping:
NumPy values use msgpack-numpy marker mappings. Outbound payloads follow the
openpi-client format so official OpenPI clients can decode action responses:
ndarray -> {__ndarray__: true, dtype, shape, data}
scalar -> {__npgeneric__: true, dtype, data}
Inbound payloads accept both the openpi-client markers above and the legacy
vLLM-native markers:
ndarray -> {nd: true, type, kind, shape, data}
scalar -> {nd: false, type, kind, data}
"""
@@ -41,18 +47,16 @@ def _pack_numpy(obj: Any) -> Any:
if not obj.flags.c_contiguous:
obj = np.ascontiguousarray(obj)
return {
b"nd": True,
b"__ndarray__": True,
b"data": obj.tobytes(),
b"type": obj.dtype.str,
b"kind": obj.dtype.kind,
b"dtype": obj.dtype.str,
b"shape": obj.shape,
}
if isinstance(obj, np.generic):
return {
b"nd": False,
b"data": obj.tobytes(),
b"type": obj.dtype.str,
b"kind": obj.dtype.kind,
b"__npgeneric__": True,
b"data": obj.item(),
b"dtype": obj.dtype.str,
}
raise TypeError(f"Unsupported type: {type(obj)!r}")
@@ -67,27 +71,67 @@ def _decode_marker_text(value: Any) -> str:
return str(value)
def _is_truthy_marker(value: Any) -> bool:
return value is True or value == 1
def _decode_vllm_numpy_marker(obj: dict[Any, Any]) -> Any:
nd = _mapping_get(obj, "nd", _MISSING)
dtype = _mapping_get(obj, "type", _MISSING)
kind = _mapping_get(obj, "kind", _MISSING)
data = _mapping_get(obj, "data", _MISSING)
if nd is _MISSING or dtype is _MISSING or kind is _MISSING or data is _MISSING:
return _MISSING
dtype_obj = np.dtype(_decode_marker_text(dtype))
kind_text = _decode_marker_text(kind)
if dtype_obj.kind != kind_text:
raise ValueError(f"NumPy dtype marker kind mismatch: {dtype_obj.kind!r} != {kind_text!r}")
if dtype_obj.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {dtype_obj}")
array = np.frombuffer(data, dtype=dtype_obj).copy()
if nd:
shape = _mapping_get(obj, "shape", _MISSING)
if shape is _MISSING:
raise ValueError("NumPy ndarray marker is missing shape")
return array.reshape(tuple(shape))
return array[0]
def _decode_openpi_numpy_marker(obj: dict[Any, Any]) -> Any:
if _is_truthy_marker(_mapping_get(obj, "__ndarray__", False)):
data = _mapping_get(obj, "data", _MISSING)
dtype = _mapping_get(obj, "dtype", _MISSING)
shape = _mapping_get(obj, "shape", _MISSING)
if data is _MISSING or dtype is _MISSING or shape is _MISSING:
raise ValueError("OpenPI ndarray marker is missing required fields")
dtype_obj = np.dtype(_decode_marker_text(dtype))
if dtype_obj.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {dtype_obj}")
array = np.frombuffer(data, dtype=dtype_obj).copy()
return array.reshape(tuple(shape))
if _is_truthy_marker(_mapping_get(obj, "__npgeneric__", False)):
data = _mapping_get(obj, "data", _MISSING)
dtype = _mapping_get(obj, "dtype", _MISSING)
if data is _MISSING or dtype is _MISSING:
raise ValueError("OpenPI scalar marker is missing required fields")
dtype_obj = np.dtype(_decode_marker_text(dtype))
if dtype_obj.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {dtype_obj}")
return dtype_obj.type(data)
return _MISSING
def _unpack_numpy(obj: Any) -> Any:
if isinstance(obj, dict):
nd = _mapping_get(obj, "nd", _MISSING)
dtype = _mapping_get(obj, "type", _MISSING)
kind = _mapping_get(obj, "kind", _MISSING)
data = _mapping_get(obj, "data", _MISSING)
if nd is not _MISSING and dtype is not _MISSING and kind is not _MISSING and data is not _MISSING:
dtype_obj = np.dtype(_decode_marker_text(dtype))
kind_text = _decode_marker_text(kind)
if dtype_obj.kind != kind_text:
raise ValueError(f"NumPy dtype marker kind mismatch: {dtype_obj.kind!r} != {kind_text!r}")
if dtype_obj.kind in ("V", "O", "c"):
raise ValueError(f"Unsupported dtype: {dtype_obj}")
array = np.frombuffer(data, dtype=dtype_obj).copy()
if nd:
shape = _mapping_get(obj, "shape", _MISSING)
if shape is _MISSING:
raise ValueError("NumPy ndarray marker is missing shape")
return array.reshape(tuple(shape))
return array[0]
for decoder in (_decode_vllm_numpy_marker, _decode_openpi_numpy_marker):
decoded = decoder(obj)
if decoded is not _MISSING:
return decoded
return {key: _unpack_numpy(value) for key, value in obj.items()}
if isinstance(obj, list):
return [_unpack_numpy(value) for value in obj]