[Perf][AuK] Compile the DiT step, use cuDNN attention and warm up at startup (#8300)

Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
This commit is contained in:
Yueqian Lin authored and GitHub committed 2026-09-30 11:21:21 +08:00
1 parent 9097fa6547
commit 146ccc133a
8 files changed
+722 -297

No files matched your search

+20 -10
View File
@@ -12,7 +12,7 @@
both with `Qwen/Qwen2.5-Omni-3B` as the frozen encoder
- Task: zero-shot TTS, instruct TTS, content and acoustic editing,
paralinguistic editing, speech enhancement, speaker and music separation
- Mode: offline inference (online chat serving is not yet qualified)
- Mode: offline inference and online `/v1/audio/speech` (online chat serving is not yet qualified)
- Hardware: 1x H100 80GB
- Maintainer: Community
@@ -130,10 +130,16 @@ Measured on one H100 against the upstream implementation with the same seeds
and mean VAE latents (four cookbook cases, base checkpoint): identical
transcripts, speaker similarity to the upstream output 0.993 to 0.999, log-mel
L1 0.11 to 0.47 (the upstream VAE-resampling noise floor is 0.39). Flash:
0.989 to 0.999 and 0.24 to 0.51. Wall per request on the second call: base
1.0 to 2.0 s for 3.5 to 10.9 s of audio, Flash 0.17 to 0.30 s; engine start
about 47 s with a warm page cache, plus the codec decode compile on the
diffusion stage (about a minute on a cold Inductor cache, ~30 s warm).
0.989 to 0.999 and 0.24 to 0.51.
Latency with the DiT and codec CUDA graphs, measured end to end on one
H200-class GPU with both stages on it, concurrency 1, median of 10 warm
requests: base (32 steps, CFG 2) zero-shot 0.25 / 0.31 / 0.40 s for 3 / 6 /
12 s of audio, instruct TTS 0.17 / 0.23 / 0.32 s; Flash zero-shot 0.08 /
0.10 / 0.14 s. The first request after start is 1.2 to 1.6 s, because the
common shapes are compiled at startup. Engine start adds the codec decode
compile and the DiT warmup to model loading (a few minutes on a cold
Inductor cache, about 30 s less warm).
## Notes
@@ -144,11 +150,15 @@ diffusion stage (about a minute on a cold Inductor cache, ~30 s warm).
- Key flags: `enforce_eager` on the encoder stage (it walks the decoder
layers itself for the layer fusion); the diffusion stage runs with
`enforce_eager: false` so the codec decode is compiled into bucketed CUDA
graphs at startup (128/256/512 latent frames, i.e. up to 10.24 s; longer
clips are decoded in overlapping 512-frame tiles of the same graph;
graphs at startup (the deploy config sets 160/320/640 latent frames, i.e.
up to 12.8 s; longer clips are decoded in overlapping 640-frame tiles of
the same graph;
override with `model_config.auk_vae_compile_shapes` and
`auk_vae_tile_frames`). The DiT itself stays eager unless
`diffusion_compile_granularity: full`. `enable_prefix_caching` must stay off
`auk_vae_tile_frames`). Each DiT denoise step replays a per-shape CUDA
graph of the regionally compiled double- and single-stream blocks; the
per-request conditioning (text projection, reference embedding, padding
biases, rotary tables) is prepared once per request outside the graph.
`enable_prefix_caching` must stay off
for the encoder: a cache hit skips prompt positions that the fused
condition needs. `enable_chunked_prefill` is off by default: forcing it
(128-token chunks, so two to three chunks per prompt) reproduces the
@@ -169,7 +179,7 @@ diffusion stage (about a minute on a cold Inductor cache, ~30 s warm).
| --- | --- | --- |
| Offline `Omni.generate` | supported (base and Flash) | `docs/getting_started/quickstart.md` |
| Online `/v1/chat/completions` with audio | not yet qualified | `docs/serving/` |
| `/v1/audio/speech` | not supported (needs a TTS adapter) | `docs/contributing/model/adding_tts_model.md` |
| `/v1/audio/speech` | supported (base and Flash, `instructions` or `task_type`) | `docs/user_guide/examples/online_serving/text_to_speech.md` |
| Streaming / async chunk | not supported | `docs/design/feature/async_chunk.md` |
| Batching across requests | one request per DiT forward | `docs/user_guide/diffusion/` |
| Tensor / sequence parallelism | not supported | `docs/configuration/composable_parallel.md` |
@@ -29,13 +29,14 @@ def _make_dit(device: str) -> AuKTransformer:
)
def _sample_inputs(device: str, offset: float = 0.0) -> dict[str, torch.Tensor]:
def _sample_inputs(device: str, offset: float = 0.0, ref_frames: int = 4) -> dict[str, torch.Tensor]:
"""``ref_frames=0`` is the text-only (instruct TTS) request shape."""
generator = torch.Generator(device=device).manual_seed(42)
return {
"text": torch.randn(1, 7, 8, generator=generator, device=device) + offset,
"c_mask": torch.ones(1, 7, dtype=torch.bool, device=device),
"ref": torch.randn(1, 4, 4, generator=generator, device=device) - offset,
"ref_mask": torch.ones(1, 4, dtype=torch.bool, device=device),
"ref": (torch.randn(1, 4, 4, generator=generator, device=device) - offset)[:, :ref_frames],
"ref_mask": torch.ones(1, ref_frames, dtype=torch.bool, device=device),
}
@@ -44,8 +45,8 @@ def _sample_inputs(device: str, offset: float = 0.0) -> dict[str, torch.Tensor]:
def test_single_request_graph_wrapper_cpu_falls_back_to_eager(cfg_strength: float, mocker) -> None:
dit = _make_dit("cpu")
wrapper = AuKCUDAGraphWrapper(dit)
run_spy = mocker.spy(wrapper, "_run")
run_cfg_spy = mocker.spy(wrapper, "_run_cfg")
prepare_spy = mocker.spy(wrapper, "_prepare")
step_spy = mocker.spy(wrapper, "_step")
capture_spy = mocker.spy(wrapper, "_capture")
inputs = _sample_inputs("cpu")
common = dict(
@@ -58,13 +59,10 @@ def test_single_request_graph_wrapper_cpu_falls_back_to_eager(cfg_strength: floa
graph = sample_latents(dit, **common, generator=torch.Generator().manual_seed(7), sampler=wrapper)
torch.testing.assert_close(graph, eager)
if cfg_strength >= 1e-5:
run_spy.assert_not_called()
assert run_cfg_spy.call_count == 2
else:
assert run_spy.call_count == 2
run_cfg_spy.assert_not_called()
# The conditioning is prepared once per request and reused by every step.
assert prepare_spy.call_count == 1
assert prepare_spy.call_args.args[-1] is (cfg_strength >= 1e-5)
assert step_spy.call_count == 2
capture_spy.assert_not_called()
assert not wrapper._cache
@@ -79,14 +77,14 @@ def test_graph_inputs_use_bounded_length_buckets() -> None:
padded = wrapper._bucket_inputs(x, text, c_mask, ref, ref_mask)
assert padded[0].shape == (1, 128, 4)
assert padded[1].shape == (1, 128)
assert padded[2].shape == (1, 128, 8)
assert padded[3].shape == (1, 128)
assert padded[0].shape == (1, 96, 4)
assert padded[1].shape == (1, 96)
assert padded[2].shape == (1, 96, 8)
assert padded[3].shape == (1, 96)
assert padded[4].shape == (1, 100, 4)
assert padded[5].shape == (1, 100)
assert [mask.sum().item() for mask in (padded[1], padded[3], padded[5])] == [65, 65, 51]
assert wrapper._key(padded[0], padded[2], padded[4], False) == (128, 128, 100, False)
assert wrapper._key(padded[0], padded[2], padded[4], False) == (96, 96, 100, False)
assert wrapper.max_graphs == 32
@@ -105,43 +103,24 @@ def test_full_graph_cache_retires_as_one_generation() -> None:
@torch.inference_mode()
@pytest.mark.parametrize("ref_frames", [0, 4])
@pytest.mark.parametrize("cfg_strength", [0.0, 2.0])
def test_bucket_padding_preserves_real_frame_outputs(cfg_strength: float) -> None:
def test_bucket_padding_preserves_real_frame_outputs(cfg_strength: float, ref_frames: int) -> None:
dit = _make_dit("cpu")
wrapper = AuKCUDAGraphWrapper(dit)
inputs = _sample_inputs("cpu")
inputs = _sample_inputs("cpu", ref_frames=ref_frames)
x = torch.randn(1, 9, 4)
timestep = torch.tensor(0.4)
cfg = torch.tensor(cfg_strength)
if cfg_strength >= 1e-5:
eager = wrapper._run_cfg(
x,
None,
inputs["text"],
inputs["c_mask"],
inputs["ref"],
inputs["ref_mask"],
timestep,
cfg_strength=cfg,
)
else:
eager = wrapper._run(
x,
None,
inputs["text"],
inputs["c_mask"],
inputs["ref"],
inputs["ref_mask"],
timestep,
)
dit.clear_cache()
uses_cfg = cfg_strength >= 1e-5
text, c_mask, ref, ref_mask = inputs["text"], inputs["c_mask"], inputs["ref"], inputs["ref_mask"]
bucketed = wrapper._bucket_inputs(x, inputs["text"], inputs["c_mask"], inputs["ref"], inputs["ref_mask"])
if cfg_strength >= 1e-5:
padded = wrapper._run_cfg(*bucketed, timestep, cfg_strength=cfg)
else:
padded = wrapper._run(*bucketed, timestep)
dit.clear_cache()
ctx = wrapper._prepare(x, None, text, c_mask, ref, ref_mask, uses_cfg)
eager = wrapper._step(x, timestep, ctx, cfg)
bucketed = wrapper._bucket_inputs(x, text, c_mask, ref, ref_mask)
padded_ctx = wrapper._prepare(*bucketed, uses_cfg)
padded = wrapper._step(bucketed[0], timestep, padded_ctx, cfg)
torch.testing.assert_close(padded[:, : x.shape[1]], eager)
@@ -149,13 +128,14 @@ def test_bucket_padding_preserves_real_frame_outputs(cfg_strength: float) -> Non
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA graph replay requires CUDA")
@torch.inference_mode()
@pytest.mark.parametrize("ref_frames", [0, 4])
@pytest.mark.parametrize("cfg_strength", [0.0, 2.0])
def test_single_request_graph_replay_matches_eager_and_updates_inputs(cfg_strength: float) -> None:
def test_single_request_graph_replay_matches_eager_and_updates_inputs(cfg_strength: float, ref_frames: int) -> None:
dit = _make_dit("cuda")
wrapper = AuKCUDAGraphWrapper(dit)
for offset in (0.0, 0.25):
inputs = _sample_inputs("cuda", offset)
inputs = _sample_inputs("cuda", offset, ref_frames=ref_frames)
common = dict(
**inputs,
gen_frames=9,
@@ -176,12 +156,14 @@ def test_single_request_graph_replay_matches_eager_and_updates_inputs(cfg_streng
key = wrapper._key(bucketed[0], bucketed[2], bucketed[4], cfg_strength >= 1e-5)
assert key in wrapper._cache
# The static context was refreshed for this request's conditioning.
entry = wrapper._cache[key]
torch.testing.assert_close(entry.static_x_mask, bucketed[1])
torch.testing.assert_close(entry.static_text, bucketed[2])
torch.testing.assert_close(entry.static_c_mask, bucketed[3])
torch.testing.assert_close(entry.static_ref, bucketed[4])
torch.testing.assert_close(entry.static_ref_mask, bucketed[5])
fresh = wrapper._prepare(*bucketed, cfg_strength >= 1e-5)
for static, want in zip(entry.static_ctx.tensors(), fresh.tensors(), strict=True):
if want is None:
assert static is None
else:
torch.testing.assert_close(static, want)
torch.testing.assert_close(entry.static_timestep, torch.tensor(0.4, device="cuda"))
assert len(wrapper._cache) == 1
@@ -194,7 +176,7 @@ def test_graph_capture_failure_is_propagated(mocker) -> None:
dit = _make_dit("cuda")
wrapper = AuKCUDAGraphWrapper(dit)
capture = mocker.patch.object(wrapper, "_capture", side_effect=RuntimeError("capture failed"))
eager = mocker.spy(wrapper, "_run")
eager = mocker.spy(wrapper, "_step")
inputs = _sample_inputs("cuda")
common = dict(
**inputs,
@@ -0,0 +1,148 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The AuK DiT's per-request prepare() / per-step step() split.
The Euler loop prepares the conditioning once and then only steps; these
tests pin that the split, the additive key-padding bias and the shared CFG
target embedding reproduce the one-shot forward exactly.
"""
import pytest
import torch
import torch.nn.functional as F
from vllm_omni.diffusion.models.auk.auk_transformer import (
AuKTransformer,
_key_padding_bias,
_sdpa,
build_time_grid,
sample_latents,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_dit(attn_mask_enabled: bool = True) -> AuKTransformer:
torch.manual_seed(3)
return AuKTransformer(
dim=32,
heads=2,
dim_head=16,
ff_mult=2,
latent_dim=4,
text_hidden_dim=8,
num_layers=2,
num_single_layers=2,
attn_mask_enabled=attn_mask_enabled,
).eval()
def _inputs(ref_frames: int, pad: bool) -> dict[str, torch.Tensor | None]:
generator = torch.Generator().manual_seed(5)
text = torch.randn(1, 7, 8, generator=generator)
c_mask = torch.ones(1, 7, dtype=torch.bool)
mask = None
ref = torch.randn(1, ref_frames, 4, generator=generator)
ref_mask = torch.ones(1, ref_frames, dtype=torch.bool)
if pad:
# Padded tails in every stream, as the CUDA graph buckets produce.
c_mask[:, 5:] = False
text[:, 5:] = 0.0
mask = torch.ones(1, 9, dtype=torch.bool)
mask[:, 7:] = False
if ref_frames:
ref_mask[:, ref_frames - 1 :] = False
return {"text": text, "c_mask": c_mask, "mask": mask, "ref": ref, "ref_mask": ref_mask}
@torch.inference_mode()
def test_key_padding_bias_matches_boolean_mask() -> None:
generator = torch.Generator().manual_seed(0)
q, k, v = (torch.randn(2, 3, 6, 8, generator=generator) for _ in range(3))
keep = torch.tensor([[True] * 6, [True, True, True, True, False, False]])
expanded = keep[:, None, None, :].expand(2, 3, 6, 6)
want = F.scaled_dot_product_attention(q, k, v, attn_mask=expanded)
torch.testing.assert_close(_sdpa(q, k, v, _key_padding_bias(keep, q.dtype)), want)
assert _key_padding_bias(None, q.dtype) is None
@torch.inference_mode()
@pytest.mark.parametrize("attn_mask_enabled", [True, False])
@pytest.mark.parametrize("cfg_infer", [False, True])
@pytest.mark.parametrize("ref_frames", [0, 4])
@pytest.mark.parametrize("pad", [False, True])
def test_prepare_then_step_matches_forward(attn_mask_enabled: bool, cfg_infer: bool, ref_frames: int, pad: bool):
dit = _make_dit(attn_mask_enabled)
inputs = _inputs(ref_frames, pad)
x = torch.randn(1, 9, 4, generator=torch.Generator().manual_seed(9))
time = torch.tensor(0.3)
want = dit(x, time=time, cfg_infer=cfg_infer, **inputs)
ctx = dit.prepare(inputs.pop("text"), target_len=x.shape[1], cfg_infer=cfg_infer, **inputs)
got = dit.step(x, time, ctx)
torch.testing.assert_close(got, want, rtol=0, atol=0)
assert got.shape == (2 if cfg_infer else 1, 9, 4)
assert ctx.branches == (2 if cfg_infer else 1)
if not attn_mask_enabled:
assert ctx.joint_bias is None and ctx.single_bias is None
@torch.inference_mode()
def test_cfg_branches_match_separate_cond_and_uncond_forwards() -> None:
"""The shared target embedding keeps each CFG branch equal to its own forward."""
dit = _make_dit()
inputs = _inputs(4, pad=True)
x = torch.randn(1, 9, 4, generator=torch.Generator().manual_seed(9))
time = torch.tensor(0.6)
both = dit(x, time=time, cfg_infer=True, **inputs)
cond = dit(x, time=time, **inputs)
uncond = dit(x, time=time, drop_audio_cond=True, drop_text=True, **inputs)
torch.testing.assert_close(both[:1], cond)
torch.testing.assert_close(both[1:], uncond)
@torch.inference_mode()
@pytest.mark.parametrize("cfg_strength", [0.0, 2.0])
def test_hoisted_euler_loop_matches_per_step_forward(cfg_strength: float) -> None:
"""sample_latents prepares once; stepping the full forward every time agrees."""
dit = _make_dit()
inputs = _inputs(4, pad=False)
inputs.pop("mask")
grid = [0.0, 0.3, 0.7, 1.0]
got = sample_latents(
dit, **inputs, gen_frames=9, t_grid=grid, cfg_strength=cfg_strength, generator=torch.Generator().manual_seed(1)
)
x = torch.randn(9, 4, generator=torch.Generator().manual_seed(1)).unsqueeze(0)
timesteps = build_time_grid(nfe=len(grid) - 1, sway_sampling_coef=None, t_grid=grid, device="cpu")
for i in range(len(grid) - 1):
if cfg_strength >= 1e-5:
cond, uncond = dit(x, time=timesteps[i], cfg_infer=True, **inputs).chunk(2, dim=0)
v = cond + (cond - uncond) * cfg_strength
else:
v = dit(x, time=timesteps[i], **inputs)
x = x + (timesteps[i + 1] - timesteps[i]) * v
torch.testing.assert_close(got, x, rtol=0, atol=0)
assert dit.text_cond is None and dit.text_uncond is None
@torch.inference_mode()
def test_context_copy_rejects_structural_mismatch() -> None:
dit = _make_dit()
inputs = _inputs(4, pad=True)
text = inputs.pop("text")
cfg = dit.prepare(text, target_len=9, cfg_infer=True, **inputs)
plain = dit.prepare(text, target_len=9, **inputs)
with pytest.raises(ValueError, match="branches"):
cfg.copy_(plain)
clone = cfg.clone()
other = dit.prepare(text * 2.0, target_len=9, cfg_infer=True, **inputs)
clone.copy_(other)
torch.testing.assert_close(clone.c, other.c)
assert clone.c.data_ptr() != other.c.data_ptr()
@@ -424,24 +424,56 @@ class TestRequestParsing:
def test_setup_compile_warms_the_decode_buckets_on_the_pipeline_device(self, build_pipeline, mocker):
pipeline, _ = build_pipeline()
warmup = mocker.patch.object(pipeline.vae_decode, "warmup")
compile_dit = mocker.patch.object(pipeline.dit, "compile")
regional = mocker.patch.object(pipeline_auk, "regionally_compile")
mocker.patch.object(pipeline, "_warmup_dit")
pipeline.setup_compile()
warmup.assert_called_once_with(pipeline.device)
# Regional is the default and a no-op for the DiT, which has no repeated blocks.
compile_dit.assert_not_called()
# Regional is the default: the DiT's repeated blocks are compiled.
regional.assert_called_once_with(pipeline.dit, dynamic=pipeline.od_config.diffusion_compile_dynamic)
def test_setup_compile_honours_full_dit_granularity(self, build_pipeline, mocker):
pipeline, _ = build_pipeline()
pipeline.od_config.diffusion_compile_granularity = "full"
pipeline.od_config.diffusion_compile_dynamic = False
mocker.patch.object(pipeline.vae_decode, "warmup")
compile_dit = mocker.patch.object(pipeline.dit, "compile")
mocker.patch.object(pipeline, "_warmup_dit")
step = mocker.Mock()
pipeline.dit.step = step
compile_fn = mocker.patch.object(pipeline_auk.torch, "compile", side_effect=lambda fn, **_: fn)
pipeline.setup_compile()
compile_dit.assert_called_once_with(dynamic=False)
# The samplers call step() directly, so that is what gets compiled.
compile_fn.assert_called_once_with(step, dynamic=False)
@pytest.mark.parametrize("variant, cfg", [("base", 2.0), ("flash", 0.0)])
def test_setup_compile_warms_the_dit_graph_buckets(self, build_pipeline, mocker, variant, cfg):
pipeline, _ = build_pipeline(variant)
mocker.patch.object(pipeline.vae_decode, "warmup")
mocker.patch.object(pipeline_auk, "regionally_compile")
wrapper = mocker.Mock(enabled=True)
pipeline.cudagraph_wrapper = wrapper
pipeline.setup_compile()
shapes = [(call.kwargs["x"].shape[1], call.kwargs["ref"].shape[1]) for call in wrapper.call_args_list]
assert shapes == [(150, 0), (300, 0), (600, 0), (150, 150), (300, 150), (600, 150)]
for call in wrapper.call_args_list:
assert call.kwargs["cfg_strength"] == cfg
assert call.kwargs["new_request"] is True
assert call.kwargs["text"].shape == (1, 96, TEXT_HIDDEN_DIM)
@pytest.mark.parametrize("model_config, enabled", [({"auk_dit_warmup_frames": []}, True), ({}, False)])
def test_dit_warmup_can_be_skipped(self, build_pipeline, mocker, model_config, enabled):
pipeline, _ = build_pipeline(model_config=model_config)
wrapper = mocker.Mock(enabled=enabled)
pipeline.cudagraph_wrapper = wrapper
pipeline._warmup_dit()
wrapper.assert_not_called()
def test_latent_output_type_skips_the_decoder(self, build_pipeline):
pipeline, _ = build_pipeline()
+9 -3
View File
@@ -43,16 +43,22 @@ stages:
# Not eager: the codec decode is compiled into bucketed CUDA graphs at
# startup (about a minute more on a cold Inductor cache, ~30 s warm, and
# about 1 GB of graph pools); clips longer than the largest bucket are
# decoded in overlapping tiles of it. Each DiT denoise step replays a lazily
# captured per-shape CUDA graph. Buckets, tile size and the
# decoded in overlapping tiles of it. Each DiT denoise step replays a
# per-shape CUDA graph of the regionally compiled blocks; the common 3/6/12 s
# shapes are compiled and captured at startup (auk_dit_warmup_frames), other
# shapes on first use. Buckets, tile size and the
# plain-graph cache are tunable through model_config:
# model_config: {auk_vae_compile_shapes: [128, 256, 512], auk_vae_tile_frames: 512, auk_vae_max_graphs: 32}
# model_config: {auk_vae_compile_shapes: [...], auk_vae_tile_frames: <frames>, auk_vae_max_graphs: 32}
- stage_id: 1
devices: "0"
max_num_seqs: 1
gpu_memory_utilization: 0.35
enforce_eager: false
model_config:
# Codec decode buckets in latent frames (3.2 / 6.4 / 12.8 s). Tighter
# than the 128/256/512 defaults for the common 3-12 s requests: a
# 3 s clip decodes in the 160 bucket instead of padding to 256.
auk_vae_compile_shapes: [160, 320, 640]
# Maximum number of cached DiT graphs. When the cache reaches this limit,
# capturing a new shape retires all existing graphs before capture.
max_dit_graphs: 32
+310 -116
View File
@@ -29,25 +29,50 @@ Deliberate differences from the reference, all inference-only:
import math
from collections.abc import Callable, Iterable, Mapping
from dataclasses import dataclass
import torch
import torch.nn.functional as F
from torch import nn
from torch.nn.attention import SDPBackend, sdpa_kernel
__all__ = ["AuKTransformer", "build_time_grid", "dit_state_dict", "sample_latents"]
__all__ = ["AuKStepContext", "AuKTransformer", "build_time_grid", "dit_state_dict", "sample_latents"]
def _sdpa(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, mask: torch.Tensor | None) -> torch.Tensor:
def _sdpa(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, bias: torch.Tensor | None) -> torch.Tensor:
"""The single attention call site, so the backend can be swapped in one place.
``q``/``k``/``v`` are ``[B, H, N, D]``. ``mask`` is a boolean key-padding
mask ``[B, K]`` in which ``True`` marks a valid key; it is broadcast to
``[B, H, Q, K]`` only when given.
``q``/``k``/``v`` are ``[B, H, N, D]``. ``bias`` is an additive key-padding
bias ``[B, 1, 1, K]`` (see :func:`_key_padding_bias`) or ``None``. Passing
it unexpanded lets SDPA broadcast it inside a fused kernel (cuDNN on
Hopper) instead of materializing a ``[B, H, Q, K]`` mask per layer.
"""
attn_mask = None
if mask is not None:
attn_mask = mask[:, None, None, :].expand(q.shape[0], q.shape[1], q.shape[-2], k.shape[-2])
return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
if bias is None or not q.is_cuda:
return F.scaled_dot_product_attention(q, k, v, attn_mask=bias, dropout_p=0.0, is_causal=False)
# With a bias, SDPA's default choice here is the memory-efficient kernel,
# about 3x slower than cuDNN's at these lengths on Hopper.
with sdpa_kernel(_MASKED_SDPA_PRIORITY, set_priority=True):
return F.scaled_dot_product_attention(q, k, v, attn_mask=bias, dropout_p=0.0, is_causal=False)
_MASKED_SDPA_PRIORITY = [
SDPBackend.CUDNN_ATTENTION,
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
def _key_padding_bias(mask: torch.Tensor | None, dtype: torch.dtype) -> torch.Tensor | None:
"""Additive attention bias ``[B, 1, 1, K]`` from a boolean key mask ``[B, K]``.
Valid keys add 0 and padded keys add ``-inf``, which is exactly what SDPA
does with a boolean mask internally.
"""
if mask is None:
return None
bias = torch.zeros(mask.shape, dtype=dtype, device=mask.device).masked_fill(~mask, float("-inf"))
return bias[:, None, None, :]
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
@@ -57,14 +82,26 @@ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
return torch.stack((-x2, x1), dim=-1).flatten(-2)
def _apply_rope(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
"""Apply rotary frequencies ``[1, 1, N, D]`` to ``x`` ``[B, H, N, D]`` in fp32."""
def _apply_rope_cos_sin(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
"""Rotate ``x`` ``[B, H, N, D]`` in fp32 with precomputed ``cos``/``sin`` of the frequencies."""
out_dtype = x.dtype
x32 = x.float()
out = x32 * freqs.cos() + _rotate_half(x32) * freqs.sin()
out = x32 * cos + _rotate_half(x32) * sin
return out.to(out_dtype)
def _apply_rope(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
"""Apply rotary frequencies ``[1, 1, N, D]`` to ``x`` ``[B, H, N, D]`` in fp32."""
return _apply_rope_cos_sin(x, freqs.cos(), freqs.sin())
RopeCache = tuple[torch.Tensor, torch.Tensor]
def _rope_cos_sin(freqs: torch.Tensor) -> RopeCache:
return freqs.cos(), freqs.sin()
class Rotary(nn.Module):
"""Rotary position frequencies for one stream, evaluated in fp32.
@@ -230,32 +267,50 @@ class Attention(nn.Module):
packed: torch.Tensor,
q_norm: nn.Module,
k_norm: nn.Module,
rope: torch.Tensor | None,
rope: RopeCache | None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Split a packed QKV projection into heads, RMSNorm Q/K, then rotate."""
"""Split a packed QKV projection into heads, RMSNorm Q/K, then rotate.
``rope`` is the ``(cos, sin)`` pair of the rotary frequencies.
"""
q, k, v = (t.view(t.shape[0], -1, self.heads, self.dim_head).transpose(1, 2) for t in packed.chunk(3, dim=-1))
q = q_norm(q)
k = k_norm(k)
if rope is not None:
q = _apply_rope(q, rope)
k = _apply_rope(k, rope)
q = _apply_rope_cos_sin(q, *rope)
k = _apply_rope_cos_sin(k, *rope)
return q, k, v
def _project(self, x: torch.Tensor, rope: torch.Tensor | None) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
def _project(self, x: torch.Tensor, rope: RopeCache | None) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return self._qkv(self.to_qkv(x), self.q_norm, self.k_norm, rope)
def forward(
self, x: torch.Tensor, mask: torch.Tensor | None = None, rope: torch.Tensor | None = None
self,
x: torch.Tensor,
mask: torch.Tensor | None = None,
rope: RopeCache | None = None,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""``bias`` is the precomputed key-padding bias for ``mask``; built here when absent."""
q, k, v = self._project(x, rope)
attn_mask = mask if self.attn_mask_enabled else None
out = self._merge_heads(_sdpa(q, k, v, attn_mask).to(q.dtype))
if bias is None and self.attn_mask_enabled:
bias = _key_padding_bias(mask, q.dtype)
out = self._merge_heads(_sdpa(q, k, v, bias).to(q.dtype))
out = self.to_out[0](out)
if mask is not None:
out = out.masked_fill(~mask.unsqueeze(-1), 0.0)
return out
def joint_key_mask(mask: torch.Tensor | None, c_mask: torch.Tensor | None, text_len: int) -> torch.Tensor | None:
"""Key mask of joint attention over ``[audio | text]``; ``None`` when the audio has no mask."""
if mask is None:
return None
if c_mask is not None:
return torch.cat([mask, c_mask], dim=1)
return F.pad(mask, (0, text_len), value=True)
class JointAttention(Attention):
"""Attention over the audio and text streams jointly, with separate projections."""
@@ -267,7 +322,7 @@ class JointAttention(Attention):
self.c_k_norm = nn.RMSNorm(dim_head, elementwise_affine=True)
self.to_out_c = nn.Linear(inner_dim, context_dim)
def _project_context(self, c: torch.Tensor, c_rope: torch.Tensor | None) -> tuple[torch.Tensor, ...]:
def _project_context(self, c: torch.Tensor, c_rope: RopeCache | None) -> tuple[torch.Tensor, ...]:
return self._qkv(self.to_qkv_c(c), self.c_q_norm, self.c_k_norm, c_rope)
def forward( # type: ignore[override] # joint attention takes the context stream too
@@ -275,10 +330,12 @@ class JointAttention(Attention):
x: torch.Tensor,
c: torch.Tensor,
mask: torch.Tensor | None = None,
rope: torch.Tensor | None = None,
c_rope: torch.Tensor | None = None,
rope: RopeCache | None = None,
c_rope: RopeCache | None = None,
c_mask: torch.Tensor | None = None,
bias: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""``bias`` is the precomputed :func:`joint_key_mask` bias; built here when absent."""
audio_len = x.shape[1]
q, k, v = self._project(x, rope)
c_q, c_k, c_v = self._project_context(c, c_rope)
@@ -287,14 +344,10 @@ class JointAttention(Attention):
k = torch.cat([k, c_k], dim=2)
v = torch.cat([v, c_v], dim=2)
joint_mask = None
if self.attn_mask_enabled and mask is not None:
if c_mask is not None:
joint_mask = torch.cat([mask, c_mask], dim=1)
else:
joint_mask = F.pad(mask, (0, c.shape[1]), value=True)
if bias is None and self.attn_mask_enabled:
bias = _key_padding_bias(joint_key_mask(mask, c_mask, c.shape[1]), q.dtype)
out = self._merge_heads(_sdpa(q, k, v, joint_mask).to(q.dtype))
out = self._merge_heads(_sdpa(q, k, v, bias).to(q.dtype))
x_out = self.to_out[0](out[:, :audio_len])
c_out = self.to_out_c(out[:, audio_len:])
@@ -326,14 +379,15 @@ class DoubleBlock(nn.Module):
c: torch.Tensor,
t: torch.Tensor,
mask: torch.Tensor | None,
rope: torch.Tensor,
c_rope: torch.Tensor,
rope: RopeCache,
c_rope: RopeCache,
c_mask: torch.Tensor | None,
bias: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
norm_c, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.attn_norm_c(c, t)
norm_x, x_gate_msa, x_shift_mlp, x_scale_mlp, x_gate_mlp = self.attn_norm_x(x, t)
x_attn, c_attn = self.attn(x=norm_x, c=norm_c, mask=mask, rope=rope, c_rope=c_rope, c_mask=c_mask)
x_attn, c_attn = self.attn(x=norm_x, c=norm_c, mask=mask, rope=rope, c_rope=c_rope, c_mask=c_mask, bias=bias)
c = c + c_gate_msa[:, None] * c_attn
norm_c = self.ff_norm_c(c) * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None]
@@ -355,9 +409,16 @@ class SingleBlock(nn.Module):
self.ff_norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
self.ff = FeedForward(dim=dim, mult=ff_mult)
def forward(self, x: torch.Tensor, t: torch.Tensor, mask: torch.Tensor | None, rope: torch.Tensor) -> torch.Tensor:
def forward(
self,
x: torch.Tensor,
t: torch.Tensor,
mask: torch.Tensor | None,
rope: RopeCache,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
norm, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.attn_norm(x, t)
x = x + gate_msa[:, None] * self.attn(x=norm, mask=mask, rope=rope)
x = x + gate_msa[:, None] * self.attn(x=norm, mask=mask, rope=rope, bias=bias)
norm = self.ff_norm(x) * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
return x + gate_mlp[:, None] * self.ff(norm)
@@ -365,6 +426,9 @@ class SingleBlock(nn.Module):
class AuKTransformer(nn.Module):
"""Flow-matching velocity predictor for AuK audio latents.
The forward pass is :meth:`prepare` (per request) followed by
:meth:`step` (per timestep); an Euler loop calls the two separately.
Args:
dim: Model width.
heads: Attention heads.
@@ -379,6 +443,9 @@ class AuKTransformer(nn.Module):
which is what the reference does with the flag off.
"""
# Regional torch.compile targets (see vllm_omni.diffusion.compile).
_repeated_blocks = ["DoubleBlock", "SingleBlock"]
def __init__(
self,
*,
@@ -395,6 +462,7 @@ class AuKTransformer(nn.Module):
super().__init__()
self.dim = dim
self.latent_dim = latent_dim
self.attn_mask_enabled = attn_mask_enabled
self.text_cond: torch.Tensor | None = None
self.text_uncond: torch.Tensor | None = None
@@ -433,40 +501,134 @@ class AuKTransformer(nn.Module):
self.text_cond = None
self.text_uncond = None
def _embed_audio(
self,
x: torch.Tensor,
ref: torch.Tensor | None,
drop_audio_cond: bool,
mask: torch.Tensor | None,
ref_mask: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None, int]:
"""Embed the target latents, prepending the reference prompt when present.
def _embed_prompt(self, ref: torch.Tensor, ref_mask: torch.Tensor | None, drop: bool) -> torch.Tensor:
"""Embed the reference prompt, zeroed for the audio-dropped CFG branch."""
if drop:
ref = torch.zeros_like(ref)
return self.audio_embed(ref, mask=ref_mask)
Returns the ``[ref | target]`` sequence, its padding mask and the
reference length, which the caller uses to slice the target back out.
def prepare(
self,
text: torch.Tensor,
*,
target_len: int,
mask: torch.Tensor | None = None,
c_mask: torch.Tensor | None = None,
ref: torch.Tensor | None = None,
ref_mask: torch.Tensor | None = None,
drop_audio_cond: bool = False,
drop_text: bool = False,
cfg_infer: bool = False,
cache: bool = False,
) -> "AuKStepContext":
"""Everything a denoise step needs that does not depend on ``x`` or the time.
An Euler loop over fixed conditioning calls this once and then
:meth:`step` per timestep, so the text projection, the reference
prompt embedding, the padding masks and biases and the rotary tables
are not recomputed on every step. Arguments are those of
:meth:`forward`; ``target_len`` is the target frame count ``n``.
"""
batch = text.shape[0]
if c_mask is None:
# Padding rows are all-zero in the text encoder's output.
c_mask = text.abs().sum(-1) > 0
if ref is not None and ref.shape[1] == 0:
ref = None
target = self.audio_embed(x, mask=mask)
if ref is None:
return target, mask, 0
if cfg_infer:
if cache and self.text_cond is not None:
c_cond, c_uncond = self.text_cond, self.text_uncond
else:
c_cond = self.project_text(text)
# The uncond branch drops the text, which is exactly a zeroed projection.
c_uncond = torch.zeros_like(c_cond)
if cache:
self.text_cond, self.text_uncond = c_cond, c_uncond
c = torch.cat((c_cond, c_uncond), dim=0)
prompt = None
if ref is not None:
prompt = torch.cat(
(self._embed_prompt(ref, ref_mask, False), self._embed_prompt(ref, ref_mask, True)), dim=0
)
else:
c = self.project_text(text)
if drop_text:
c = torch.zeros_like(c)
prompt = None if ref is None else self._embed_prompt(ref, ref_mask, drop_audio_cond)
if drop_audio_cond:
ref = torch.zeros_like(ref)
prompt = self.audio_embed(ref, mask=ref_mask)
audio = torch.cat([prompt, target], dim=1)
prompt_len = prompt.shape[1]
audio_mask = None
if mask is not None or ref_mask is not None:
audio_mask = mask
if ref is not None and (mask is not None or ref_mask is not None):
if mask is None:
mask = x.new_ones(x.shape[:2], dtype=torch.bool)
mask = torch.ones((batch, target_len), dtype=torch.bool, device=ref.device)
if ref_mask is None:
ref_mask = ref.new_ones(ref.shape[:2], dtype=torch.bool)
ref_mask = torch.ones(ref.shape[:2], dtype=torch.bool, device=ref.device)
audio_mask = torch.cat([ref_mask, mask], dim=1)
return audio, audio_mask, prompt_len
branches = 2 if cfg_infer else 1
if branches == 2:
c_mask = torch.cat((c_mask, c_mask), dim=0)
if audio_mask is not None:
audio_mask = torch.cat((audio_mask, audio_mask), dim=0)
prompt_len = 0 if prompt is None else prompt.shape[1]
text_len = c.shape[1]
audio_len = prompt_len + target_len
single_mask = None if audio_mask is None else torch.cat([c_mask, audio_mask], dim=1)
joint_bias = single_bias = None
if self.attn_mask_enabled:
joint_bias = _key_padding_bias(joint_key_mask(audio_mask, c_mask, text_len), c.dtype)
single_bias = _key_padding_bias(single_mask, c.dtype)
return AuKStepContext(
c=c,
prompt=prompt,
target_mask=mask,
c_mask=c_mask,
audio_mask=audio_mask,
single_mask=single_mask,
joint_bias=joint_bias,
single_bias=single_bias,
rope_audio=_rope_cos_sin(self.rotary_embed(audio_len, audio_mask)),
rope_text=_rope_cos_sin(self.rotary_embed(text_len, c_mask)),
rope_single=_rope_cos_sin(self.rotary_embed(text_len + audio_len, single_mask)),
branches=branches,
)
def step(self, x: torch.Tensor, time: torch.Tensor, ctx: "AuKStepContext") -> torch.Tensor:
"""Predict the velocity of ``x`` at ``time`` under a :meth:`prepare` context.
The target embedding does not depend on the CFG branch, so it is
computed once and shared by both branches.
"""
if time.ndim == 0:
time = time.repeat(x.shape[0])
t = self.time_embed(time)
target = self.audio_embed(x, mask=ctx.target_mask)
if ctx.branches == 2:
t = torch.cat((t, t), dim=0)
target = torch.cat((target, target), dim=0)
audio = target if ctx.prompt is None else torch.cat([ctx.prompt, target], dim=1)
c = ctx.c
for block in self.transformer_blocks:
c, audio = block(
audio,
c,
t,
mask=ctx.audio_mask,
rope=ctx.rope_audio,
c_rope=ctx.rope_text,
c_mask=ctx.c_mask,
bias=ctx.joint_bias,
)
h = torch.cat([c, audio], dim=1)
for block in self.single_transformer_blocks:
h = block(h, t, mask=ctx.single_mask, rope=ctx.rope_single, bias=ctx.single_bias)
h = h[:, h.shape[1] - x.shape[1] :]
return self.proj_out(self.norm_out(h, t))
def forward(
self,
@@ -508,57 +670,89 @@ class AuKTransformer(nn.Module):
Velocity ``[B, n, latent_dim]``, or ``[2B, n, latent_dim]`` under
``cfg_infer``.
"""
batch = x.shape[0]
if time.ndim == 0:
time = time.repeat(batch)
t = self.time_embed(time)
ctx = self.prepare(
text,
target_len=x.shape[1],
mask=mask,
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
drop_audio_cond=drop_audio_cond,
drop_text=drop_text,
cfg_infer=cfg_infer,
cache=cache and cfg_infer,
)
return self.step(x, time, ctx)
if c_mask is None:
# Padding rows are all-zero in the text encoder's output.
c_mask = text.abs().sum(-1) > 0
if cfg_infer:
if cache and self.text_cond is not None:
c_cond, c_uncond = self.text_cond, self.text_uncond
else:
c_cond = self.project_text(text)
# The uncond branch drops the text, which is exactly a zeroed projection.
c_uncond = torch.zeros_like(c_cond)
if cache:
self.text_cond, self.text_uncond = c_cond, c_uncond
x_cond, mask_cond, prompt_len = self._embed_audio(x, ref, False, mask, ref_mask)
x_uncond, mask_uncond, _ = self._embed_audio(x, ref, True, mask, ref_mask)
@dataclass
class AuKStepContext:
"""Per-request state of the denoise step, built by :meth:`AuKTransformer.prepare`.
x = torch.cat((x_cond, x_uncond), dim=0)
c = torch.cat((c_cond, c_uncond), dim=0)
t = torch.cat((t, t), dim=0)
c_mask = torch.cat((c_mask, c_mask), dim=0)
audio_mask = None
if mask_cond is not None and mask_uncond is not None:
audio_mask = torch.cat((mask_cond, mask_uncond), dim=0)
else:
c = self.project_text(text)
if drop_text:
c = torch.zeros_like(c)
x, audio_mask, prompt_len = self._embed_audio(x, ref, drop_audio_cond, mask, ref_mask)
Batch rows are ``branches`` copies of the request (cond first under CFG).
``target_mask`` covers the target frames only and has the request's batch
size; the other masks cover their full sequences and every branch.
"""
seq_len = x.shape[1]
text_len = c.shape[1]
rope_audio = self.rotary_embed(seq_len, audio_mask)
rope_text = self.rotary_embed(text_len, c_mask)
c: torch.Tensor
prompt: torch.Tensor | None
target_mask: torch.Tensor | None
c_mask: torch.Tensor | None
audio_mask: torch.Tensor | None
single_mask: torch.Tensor | None
joint_bias: torch.Tensor | None
single_bias: torch.Tensor | None
rope_audio: RopeCache
rope_text: RopeCache
rope_single: RopeCache
branches: int
for block in self.transformer_blocks:
c, x = block(x, c, t, mask=audio_mask, rope=rope_audio, c_rope=rope_text, c_mask=c_mask)
def tensors(self) -> list[torch.Tensor | None]:
"""Every tensor field in a fixed order, rope pairs flattened."""
return [
self.c,
self.prompt,
self.target_mask,
self.c_mask,
self.audio_mask,
self.single_mask,
self.joint_bias,
self.single_bias,
*self.rope_audio,
*self.rope_text,
*self.rope_single,
]
x = torch.cat([c, x], dim=1)
single_mask = None if audio_mask is None else torch.cat([c_mask, audio_mask], dim=1)
rope = self.rotary_embed(text_len + seq_len, single_mask)
def copy_(self, other: "AuKStepContext") -> None:
"""Overwrite these tensors in place with ``other``'s, for CUDA graph static inputs."""
if other.branches != self.branches:
raise ValueError(f"AuK step context mismatch: {other.branches} vs {self.branches} branches")
for dst, src in zip(self.tensors(), other.tensors(), strict=True):
if (dst is None) != (src is None):
raise ValueError("AuK step context mismatch: a mask is present in only one context")
if dst is not None:
dst.copy_(src)
for block in self.single_transformer_blocks:
x = block(x, t, mask=single_mask, rope=rope)
def clone(self) -> "AuKStepContext":
"""A copy whose tensors own fresh storage."""
x = x[:, text_len + prompt_len :]
return self.proj_out(self.norm_out(x, t))
def _clone(value: torch.Tensor | None) -> torch.Tensor | None:
return None if value is None else value.clone()
return AuKStepContext(
c=self.c.clone(),
prompt=_clone(self.prompt),
target_mask=_clone(self.target_mask),
c_mask=_clone(self.c_mask),
audio_mask=_clone(self.audio_mask),
single_mask=_clone(self.single_mask),
joint_bias=_clone(self.joint_bias),
single_bias=_clone(self.single_bias),
rope_audio=(self.rope_audio[0].clone(), self.rope_audio[1].clone()),
rope_text=(self.rope_text[0].clone(), self.rope_text[1].clone()),
rope_single=(self.rope_single[0].clone(), self.rope_single[1].clone()),
branches=self.branches,
)
def dit_state_dict(
@@ -686,6 +880,7 @@ def sample_latents(
ref_mask=ref_mask,
timestep=t[i],
cfg_strength=cfg_strength,
new_request=i == 0,
)
x = x + (t[i + 1] - t[i]) * velocity
finally:
@@ -718,22 +913,21 @@ def _sample_latents(
x = initial_latents
guided = cfg_strength >= 1e-5
try:
# The conditioning is fixed across the loop: prepare it once.
ctx = dit.prepare(
text,
target_len=x.shape[1],
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
cfg_infer=guided,
cache=guided,
)
for i in range(timesteps.shape[0] - 1):
v = dit.step(x, timesteps[i], ctx)
if guided:
pred = dit(
x,
text,
timesteps[i],
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
cfg_infer=True,
cache=True,
)
v_cond, v_uncond = pred.chunk(2, dim=0)
v_cond, v_uncond = v.chunk(2, dim=0)
v = v_cond + (v_cond - v_uncond) * cfg_strength
else:
v = dit(x, text, timesteps[i], c_mask=c_mask, ref=ref, ref_mask=ref_mask)
x = x + (timesteps[i + 1] - timesteps[i]) * v
finally:
# The cached text projections belong to this request only; a failed
@@ -12,7 +12,7 @@ import torch.nn.functional as F
from vllm.logger import init_logger
from vllm.utils.math_utils import round_up
from vllm_omni.diffusion.models.auk.auk_transformer import AuKTransformer
from vllm_omni.diffusion.models.auk.auk_transformer import AuKStepContext, AuKTransformer
logger = init_logger(__name__)
@@ -21,11 +21,7 @@ logger = init_logger(__name__)
class _GraphEntry:
graph: torch.cuda.CUDAGraph
static_x: torch.Tensor
static_x_mask: torch.Tensor
static_text: torch.Tensor
static_c_mask: torch.Tensor
static_ref: torch.Tensor
static_ref_mask: torch.Tensor
static_ctx: AuKStepContext
static_timestep: torch.Tensor
static_cfg: torch.Tensor | None
static_out: torch.Tensor
@@ -38,10 +34,15 @@ class AuKCUDAGraphWrapper:
whether the CFG branch is enabled. Timestep and the CFG strength are
mutable scalar buffers, so all Euler steps and CFG values within one path
reuse one graph.
Only :meth:`AuKTransformer.step` is captured. The per-request context
(text projection, reference embedding, padding biases, rotary tables) is
built by :meth:`AuKTransformer.prepare` on the first step of a request and
copied into the graph's static context, so the replayed steps skip it.
"""
_TARGET_ALIGNMENT = 64
_TEXT_ALIGNMENT = 64
_TARGET_ALIGNMENT = 32
_TEXT_ALIGNMENT = 32
_REF_ALIGNMENT = 50
def __init__(self, dit: AuKTransformer, *, enabled: bool = True, max_graphs: int = 32) -> None:
@@ -50,6 +51,10 @@ class AuKCUDAGraphWrapper:
self.max_graphs = max(1, int(max_graphs))
self._cache: OrderedDict[tuple, _GraphEntry] = OrderedDict()
self._pool_handle: int | None = None
# Key whose static context holds the current request, and the context
# the eager fallback reuses across the steps of one request.
self._active_key: tuple | None = None
self._eager_ctx: AuKStepContext | None = None
logger.info("Initialized AuK DiT lazy CUDA graph cache: enabled=%s slots=%d", self.enabled, self.max_graphs)
@classmethod
@@ -87,6 +92,41 @@ class AuKCUDAGraphWrapper:
"""Retire all graphs together so none outlive shared workspaces."""
if len(self._cache) >= self.max_graphs:
self._cache.clear()
self._active_key = None
def _prepare(
self,
x: torch.Tensor,
x_mask: torch.Tensor | None,
text: torch.Tensor,
c_mask: torch.Tensor,
ref: torch.Tensor,
ref_mask: torch.Tensor,
uses_cfg: bool,
) -> AuKStepContext:
return self.dit.prepare(
text,
target_len=x.shape[1],
mask=x_mask,
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
cfg_infer=uses_cfg,
)
def _step(
self,
x: torch.Tensor,
timestep: torch.Tensor,
ctx: AuKStepContext,
cfg_strength: torch.Tensor | float,
) -> torch.Tensor:
"""One guided (or unguided) velocity; the CFG combine is part of the graph."""
velocity = self.dit.step(x, timestep, ctx)
if ctx.branches == 1:
return velocity
conditional, unconditional = velocity.chunk(2, dim=0)
return conditional + (conditional - unconditional) * cfg_strength
@torch.no_grad()
def __call__(
@@ -99,25 +139,37 @@ class AuKCUDAGraphWrapper:
ref_mask: torch.Tensor,
timestep: torch.Tensor,
cfg_strength: float,
new_request: bool = True,
) -> torch.Tensor:
"""Velocity of ``x`` at ``timestep``.
``new_request`` marks the first step of a request, whose conditioning
(``text``, ``c_mask``, ``ref``, ``ref_mask``) is then prepared anew; the
later steps of the request pass ``False`` and reuse it. The default
prepares on every call, which is always correct.
"""
uses_cfg = cfg_strength >= 1e-5
if not self.enabled or x.device.type != "cuda" or torch.cuda.is_current_stream_capturing():
if uses_cfg:
return self._run_cfg(x, None, text, c_mask, ref, ref_mask, timestep, cfg_strength=cfg_strength)
return self._run(x, None, text, c_mask, ref, ref_mask, timestep)
if new_request or self._eager_ctx is None:
self._eager_ctx = self._prepare(x, None, text, c_mask, ref, ref_mask, uses_cfg)
return self._step(x, timestep, self._eager_ctx, cfg_strength)
target_frames = x.shape[1]
x, x_mask, text, c_mask, ref, ref_mask = self._bucket_inputs(x, text, c_mask, ref, ref_mask)
inputs = (x, x_mask, text, c_mask, ref, ref_mask, timestep)
key = self._key(x, text, ref, uses_cfg)
entry = self._cache.get(key)
ctx = None
if entry is None or new_request or key != self._active_key:
ctx = self._prepare(x, x_mask, text, c_mask, ref, ref_mask, uses_cfg)
if entry is None:
# Same Retirement as #6587, details discussed in #7469
self._retire_graph_generation_if_full()
try:
entry = self._capture(*inputs, cfg_strength=cfg_strength, uses_cfg=uses_cfg)
assert ctx is not None
entry = self._capture(x, ctx, timestep, cfg_strength=cfg_strength)
except Exception:
self._cache.clear()
self._active_key = None
self.enabled = False
logger.exception(
"Disabling AuK DiT CUDA graphs after capture failure for key=%s; "
@@ -128,112 +180,55 @@ class AuKCUDAGraphWrapper:
self._cache[key] = entry
else:
self._cache.move_to_end(key)
if ctx is not None:
entry.static_ctx.copy_(ctx)
self._active_key = key
entry.static_x.copy_(x)
entry.static_x_mask.copy_(x_mask)
entry.static_text.copy_(text)
entry.static_c_mask.copy_(c_mask)
entry.static_ref.copy_(ref)
entry.static_ref_mask.copy_(ref_mask)
entry.static_timestep.copy_(timestep)
if entry.static_cfg is not None:
entry.static_cfg.fill_(cfg_strength)
entry.graph.replay()
return entry.static_out[:, :target_frames].clone()
def _run(
self,
x: torch.Tensor,
x_mask: torch.Tensor | None,
text: torch.Tensor,
c_mask: torch.Tensor,
ref: torch.Tensor,
ref_mask: torch.Tensor,
timestep: torch.Tensor,
) -> torch.Tensor:
return self.dit(x, text, timestep, mask=x_mask, c_mask=c_mask, ref=ref, ref_mask=ref_mask)
def _run_cfg(
self,
x: torch.Tensor,
x_mask: torch.Tensor | None,
text: torch.Tensor,
c_mask: torch.Tensor,
ref: torch.Tensor,
ref_mask: torch.Tensor,
timestep: torch.Tensor,
*,
cfg_strength: torch.Tensor | float,
) -> torch.Tensor:
pred = self.dit(
x,
text,
timestep,
mask=x_mask,
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
cfg_infer=True,
cache=True,
)
conditional, unconditional = pred.chunk(2, dim=0)
return conditional + (conditional - unconditional) * cfg_strength
def _capture(
self,
*inputs: torch.Tensor,
x: torch.Tensor,
ctx: AuKStepContext,
timestep: torch.Tensor,
*,
cfg_strength: float,
uses_cfg: bool,
) -> _GraphEntry:
static_inputs = tuple(value.clone() for value in inputs)
static_x = x.clone()
static_ctx = ctx.clone()
static_timestep = timestep.clone()
static_cfg = None
if uses_cfg:
static_cfg = torch.empty((), device=static_inputs[0].device, dtype=torch.float32)
if ctx.branches == 2:
static_cfg = torch.empty((), device=static_x.device, dtype=torch.float32)
static_cfg.fill_(cfg_strength)
try:
for _ in range(3):
if uses_cfg:
assert static_cfg is not None
self._run_cfg(*static_inputs, cfg_strength=static_cfg)
else:
self._run(*static_inputs)
# CFG warm-up populates AuKTransformer's Python-side projected-text
# cache. Clear it before capture so the graph includes
# project_text(static_text), rather than closing over the first
# request's projection and ignoring later static_text updates.
self.dit.clear_cache()
if static_cfg is not None:
static_cfg.fill_(cfg_strength)
if self._pool_handle is None:
self._pool_handle = torch.cuda.graph_pool_handle()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, pool=self._pool_handle):
# Capture the actual CFG path. The scalar value remains a
# mutable buffer, so cfg=2 and cfg=3 share this graph.
if uses_cfg:
assert static_cfg is not None
static_out = self._run_cfg(*static_inputs, cfg_strength=static_cfg)
else:
static_out = self._run(*static_inputs)
finally:
self.dit.clear_cache()
scale = static_cfg if static_cfg is not None else cfg_strength
# Warm-up runs any lazy torch.compile outside the capture.
for _ in range(3):
self._step(static_x, static_timestep, static_ctx, scale)
if self._pool_handle is None:
self._pool_handle = torch.cuda.graph_pool_handle()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph, pool=self._pool_handle):
# The CFG scalar stays a mutable buffer, so cfg=2 and cfg=3 share this graph.
static_out = self._step(static_x, static_timestep, static_ctx, scale)
logger.info(
"Captured AuK DiT single-step CUDA graph: target_frames=%d text_tokens=%d ref_frames=%d cfg=%s",
inputs[0].shape[1],
inputs[2].shape[1],
inputs[4].shape[1],
x.shape[1],
ctx.c.shape[1],
0 if ctx.prompt is None else ctx.prompt.shape[1],
cfg_strength,
)
return _GraphEntry(
graph=graph,
static_x=static_inputs[0],
static_x_mask=static_inputs[1],
static_text=static_inputs[2],
static_c_mask=static_inputs[3],
static_ref=static_inputs[4],
static_ref_mask=static_inputs[5],
static_timestep=static_inputs[6],
static_x=static_x,
static_ctx=static_ctx,
static_timestep=static_timestep,
static_cfg=static_cfg,
static_out=static_out,
)
+68 -10
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import json
import os
import time
from collections.abc import Iterable
from typing import Any, ClassVar
@@ -23,6 +24,7 @@ from safetensors import safe_open
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.compile import regionally_compile
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.models.auk.auk_transformer import AuKTransformer, dit_state_dict, sample_latents
@@ -50,6 +52,13 @@ _FUSION_KEYS = frozenset({"layer_weights", "layer_scale"})
# The distilled variant only reproduces its training recipe at these settings.
_FLASH_NFE = 4
_FLASH_CFG = 0.0
# Startup DiT warm-up shapes: target frames of 3 / 6 / 12 s requests, with no
# reference (instruct TTS) and a 3 s one (zero-shot). The first call compiles
# the blocks; each shape then captures its graph bucket. Other shapes still
# capture lazily, at about a second each.
_WARMUP_TARGET_FRAMES = (150, 300, 600)
_WARMUP_REF_FRAMES = (0, 150)
_WARMUP_TEXT_TOKENS = 96
# Decorrelates the VAE posterior draw from the initial latent for the same request seed.
_VAE_SEED_OFFSET = 0x5EED_0A0C
@@ -182,6 +191,7 @@ class AuKPipeline(nn.Module, SupportAudioInput, SupportAudioOutput, SupportsComp
# The transformer owns every key the checkpoint tool writes into `dit`,
# `attn_mask_enabled` included, so the section passes straight through.
self.text_hidden_dim = int(config["dit"]["text_hidden_dim"])
self.dit = AuKTransformer(latent_dim=self.latent_dim, **config["dit"])
self.dit = self.dit.to(dtype=self.dtype)
self.dit.load_state_dict(_read_dit_weights(model_dir, self.dtype), strict=True)
@@ -218,22 +228,70 @@ class AuKPipeline(nn.Module, SupportAudioInput, SupportAudioOutput, SupportsComp
Defining this hook replaces the runner's generic transformer compile,
so the DiT's ``diffusion_compile_granularity`` is honoured here: full
compiles the whole transformer lazily, exactly as the runner would;
regional is a no-op because the DiT declares no repeated blocks. The
startup cost worth paying is the VAE decode, whose buckets are
compiled and captured before the first request.
compiles the whole denoise step;
regional compiles the double- and single-stream blocks. Compilation is
lazy and happens in the DiT CUDA graph's warm-up, before capture, so
each graph replays the compiled kernels. The startup cost worth paying
is the VAE decode, whose buckets are compiled and captured before the
first request.
"""
granularity = self.od_config.diffusion_compile_granularity
dynamic = self.od_config.diffusion_compile_dynamic
if granularity == "full":
self.dit.compile(dynamic=self.od_config.diffusion_compile_dynamic)
logger.info(
"AuK DiT configured for lazy full torch.compile with dynamic=%s",
self.od_config.diffusion_compile_dynamic,
)
# The samplers drive prepare() and step() directly rather than
# __call__, so the per-step body is what gets compiled.
self.dit.step = torch.compile(self.dit.step, dynamic=dynamic)
else:
logger.info("AuK DiT declares no repeated blocks; regional torch.compile is a no-op, running eager")
regionally_compile(self.dit, dynamic=dynamic)
logger.info("AuK DiT configured for lazy %s torch.compile with dynamic=%s", granularity, dynamic)
self._warmup_dit()
self.vae_decode.warmup(self.device)
def _warmup_dit(self) -> None:
"""Compile the DiT step and capture the common graph buckets before serving.
The engine's synthetic warm-up request cannot reach this pipeline
(``dummy_run_num_frames = 0``), so without this the first real request
pays the whole torch.compile. Runs the variant's default guidance path
on zero conditioning; ``model_config.auk_dit_warmup_frames`` overrides
the target lengths and ``[]`` disables the warm-up.
"""
if not self.cudagraph_wrapper.enabled:
return
model_config = getattr(self.od_config, "model_config", None) or {}
frames = model_config.get("auk_dit_warmup_frames", _WARMUP_TARGET_FRAMES)
if not frames:
logger.info("AuK DiT startup warm-up disabled")
return
cfg = _FLASH_CFG if self.is_flash else self.default_cfg
text = torch.zeros(1, _WARMUP_TEXT_TOKENS, self.text_hidden_dim, device=self.device, dtype=self.dtype)
c_mask = torch.ones(text.shape[:2], dtype=torch.bool, device=self.device)
timestep = torch.zeros((), device=self.device, dtype=torch.float32)
start = time.perf_counter()
with torch.inference_mode(), self._dit_autocast():
for ref_frames in _WARMUP_REF_FRAMES:
ref = torch.zeros(1, ref_frames, self.latent_dim, device=self.device, dtype=torch.float32)
ref_mask = torch.ones(ref.shape[:2], dtype=torch.bool, device=self.device)
for target_frames in frames:
x = torch.zeros(1, int(target_frames), self.latent_dim, device=self.device, dtype=torch.float32)
self.cudagraph_wrapper(
x=x,
text=text,
c_mask=c_mask,
ref=ref,
ref_mask=ref_mask,
timestep=timestep,
cfg_strength=cfg,
new_request=True,
)
if self.device.type != "cpu":
torch.accelerator.synchronize(self.device)
logger.info(
"AuK DiT warm-up: compiled and captured %d shapes in %.1f s",
len(frames) * len(_WARMUP_REF_FRAMES),
time.perf_counter() - start,
)
# The assembled checkpoint is not a diffusers layout: __init__ reads
# auk.safetensors and vae.safetensors directly, so the loader has no
# component sources to stream and load_weights only reports what is loaded.