Signed-off-by: leffff <levnovitskiy@gmail.com> Signed-off-by: Artem Borisov <divotionchannel@gmail.com> Co-authored-by: leffff <levnovitskiy@gmail.com> Co-authored-by: Artem Borisov <divotionchannel@gmail.com>
326 lines
13 KiB
Python
326 lines
13 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
import os
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _init_distributed(monkeypatch):
|
|
"""The native transformer uses vLLM parallel linear layers, which require a
|
|
tensor-parallel group; initialize a single-process group for CPU tests
|
|
(mirrors tests/diffusion/models/sana_video/test_transformer_sana_video.py)."""
|
|
from vllm.distributed.parallel_state import (
|
|
cleanup_dist_env_and_memory,
|
|
init_distributed_environment,
|
|
initialize_model_parallel,
|
|
)
|
|
from vllm.model_executor.layers.utils import default_unquantized_gemm
|
|
|
|
monkeypatch.setattr(
|
|
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
|
lambda *_args, **_kwargs: default_unquantized_gemm,
|
|
)
|
|
os.environ.setdefault("MASTER_ADDR", "localhost")
|
|
os.environ.setdefault("MASTER_PORT", "29502")
|
|
init_distributed_environment(world_size=1, rank=0, local_rank=0, distributed_init_method="env://")
|
|
initialize_model_parallel()
|
|
yield
|
|
cleanup_dist_env_and_memory()
|
|
|
|
|
|
# model_dim must be an exact multiple of head_dim = sum(axes_dims); this is
|
|
# an architectural constraint of the native model (DiffusionTransformer3D),
|
|
# not specific to this port.
|
|
_TINY_T2V_CONFIG = {
|
|
"in_visual_dim": 4,
|
|
"out_visual_dim": 4,
|
|
"in_text_dim": 8,
|
|
"in_text_dim2": 6,
|
|
"time_dim": 16,
|
|
"patch_size": (1, 2, 2),
|
|
"model_dim": 24,
|
|
"ff_dim": 32,
|
|
"num_text_blocks": 1,
|
|
"num_visual_blocks": 2,
|
|
"axes_dims": (4, 4, 4),
|
|
"visual_cond": False,
|
|
"is_multimodal": False,
|
|
"attention_engine": "sdpa",
|
|
}
|
|
|
|
# time_dim_a is intentionally omitted (defaults to time_dim): the fused
|
|
# block's cross-modal modulation (va_mod/av_mod) consumes the *other*
|
|
# modality's time embedding by default (fix_modulation=False), so time_dim
|
|
# and time_dim_a must match unless fix_modulation=True.
|
|
_TINY_T2VA_CONFIG = {
|
|
**_TINY_T2V_CONFIG,
|
|
"is_multimodal": True,
|
|
"in_audio_dim": 6,
|
|
"model_dim_a": 12,
|
|
"ff_dim_a": 16,
|
|
"axes_dims_a": (2, 2, 2),
|
|
}
|
|
|
|
|
|
def _sdpa_config():
|
|
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec
|
|
|
|
return SimpleNamespace(
|
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="TORCH_SDPA")),
|
|
parallel_config=SimpleNamespace(ring_degree=1, allgather_degree=1),
|
|
)
|
|
|
|
|
|
def _build_transformer(config):
|
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
|
from vllm_omni.diffusion.models.kandinsky6 import Kandinsky6Transformer3DModel
|
|
|
|
with set_current_diffusion_config(_sdpa_config()):
|
|
model = Kandinsky6Transformer3DModel(**config)
|
|
model.eval()
|
|
return model
|
|
|
|
|
|
def test_t2v_only_forward_shape():
|
|
model = _build_transformer(_TINY_T2V_CONFIG)
|
|
time_frames, height, width = 2, 4, 4
|
|
x_video = torch.randn(time_frames, height, width, 4)
|
|
text_embed = torch.randn(5, 8)
|
|
pooled = torch.randn(1, 6)
|
|
time = torch.tensor([500.0])
|
|
visual_rope = model.visual_rope_embeddings(
|
|
shape=(time_frames, height // 2, width // 2),
|
|
pos=[torch.arange(time_frames), torch.arange(height // 2), torch.arange(width // 2)],
|
|
scale_factor=(1.0, 1.0, 1.0),
|
|
)
|
|
text_rope = model.text_rope_embeddings(torch.arange(5))
|
|
|
|
with torch.no_grad():
|
|
out = model(
|
|
x_video=x_video,
|
|
x_audio=None,
|
|
text_embed=text_embed,
|
|
pooled_text_embed=pooled,
|
|
time=time,
|
|
visual_rope=visual_rope,
|
|
audio_rope=None,
|
|
text_rope=text_rope,
|
|
)
|
|
|
|
assert out.shape == (time_frames, height, width, 4)
|
|
|
|
|
|
def test_visual_cond_channel_concat_path():
|
|
"""visual_cond=True doubles+1 the visual input channel count (cond +
|
|
mask channels appended by _build_video_input, stripped again by
|
|
OutLayer) — the I2V conditioning path."""
|
|
config = {**_TINY_T2V_CONFIG, "visual_cond": True}
|
|
model = _build_transformer(config)
|
|
time_frames, height, width = 2, 4, 4
|
|
x_video = torch.randn(time_frames, height, width, 2 * 4 + 1)
|
|
text_embed = torch.randn(5, 8)
|
|
pooled = torch.randn(1, 6)
|
|
time = torch.tensor([500.0])
|
|
visual_rope = model.visual_rope_embeddings(
|
|
shape=(time_frames, height // 2, width // 2),
|
|
pos=[torch.arange(time_frames), torch.arange(height // 2), torch.arange(width // 2)],
|
|
scale_factor=(1.0, 1.0, 1.0),
|
|
)
|
|
text_rope = model.text_rope_embeddings(torch.arange(5))
|
|
|
|
with torch.no_grad():
|
|
out = model(
|
|
x_video=x_video,
|
|
x_audio=None,
|
|
text_embed=text_embed,
|
|
pooled_text_embed=pooled,
|
|
time=time,
|
|
visual_rope=visual_rope,
|
|
audio_rope=None,
|
|
text_rope=text_rope,
|
|
)
|
|
|
|
assert out.shape == (time_frames, height, width, 4)
|
|
|
|
|
|
def test_t2va_joint_forward_shapes():
|
|
"""Exercises the fused cross-modal decoder blocks (Kandinsky6FusedTransformerDecoderBlock)."""
|
|
model = _build_transformer(_TINY_T2VA_CONFIG)
|
|
time_frames, height, width = 2, 4, 4
|
|
audio_len = 7
|
|
x_video = torch.randn(time_frames, height, width, 4)
|
|
x_audio = torch.randn(audio_len, 6)
|
|
text_embed = torch.randn(5, 8)
|
|
pooled = torch.randn(1, 6)
|
|
time = torch.tensor([500.0])
|
|
visual_rope = model.visual_rope_embeddings(
|
|
shape=(time_frames, height // 2, width // 2),
|
|
pos=[torch.arange(time_frames), torch.arange(height // 2), torch.arange(width // 2)],
|
|
scale_factor=(1.0, 1.0, 1.0),
|
|
)
|
|
audio_rope = model.audio_rope_embeddings(torch.arange(audio_len))
|
|
video_text_rope = model.video_text_rope_embeddings(torch.arange(5))
|
|
audio_text_rope = model.audio_text_rope_embeddings(torch.arange(5))
|
|
|
|
with torch.no_grad():
|
|
video_out, audio_out = model(
|
|
x_video=x_video,
|
|
x_audio=x_audio,
|
|
text_embed=[text_embed, text_embed],
|
|
pooled_text_embed=[pooled, pooled],
|
|
time=[time, time],
|
|
visual_rope=visual_rope,
|
|
audio_rope=audio_rope,
|
|
text_rope=[video_text_rope, audio_text_rope],
|
|
)
|
|
|
|
assert video_out.shape == (time_frames, height, width, 4)
|
|
assert audio_out.shape == (audio_len, 6)
|
|
|
|
|
|
@pytest.mark.parametrize("modality", ["video", "audio"])
|
|
def test_single_modality_through_multimodal_model(modality):
|
|
"""A multimodal (is_multimodal=True) model must still support a pure
|
|
video-only or audio-only forward call (denoise_loop's partial-sampling
|
|
path uses this)."""
|
|
model = _build_transformer(_TINY_T2VA_CONFIG)
|
|
time_frames, height, width = 2, 4, 4
|
|
audio_len = 7
|
|
text_embed = torch.randn(5, 8)
|
|
pooled = torch.randn(1, 6)
|
|
time = torch.tensor([500.0])
|
|
|
|
if modality == "video":
|
|
x_video = torch.randn(time_frames, height, width, 4)
|
|
visual_rope = model.visual_rope_embeddings(
|
|
shape=(time_frames, height // 2, width // 2),
|
|
pos=[torch.arange(time_frames), torch.arange(height // 2), torch.arange(width // 2)],
|
|
scale_factor=(1.0, 1.0, 1.0),
|
|
)
|
|
text_rope = model.video_text_rope_embeddings(torch.arange(5))
|
|
with torch.no_grad():
|
|
out = model(
|
|
x_video=x_video,
|
|
x_audio=None,
|
|
text_embed=text_embed,
|
|
pooled_text_embed=pooled,
|
|
time=time,
|
|
visual_rope=visual_rope,
|
|
audio_rope=None,
|
|
text_rope=text_rope,
|
|
)
|
|
assert out.shape == (time_frames, height, width, 4)
|
|
else:
|
|
x_audio = torch.randn(audio_len, 6)
|
|
audio_rope = model.audio_rope_embeddings(torch.arange(audio_len))
|
|
text_rope = model.audio_text_rope_embeddings(torch.arange(5))
|
|
with torch.no_grad():
|
|
out = model(
|
|
x_video=None,
|
|
x_audio=x_audio,
|
|
text_embed=text_embed,
|
|
pooled_text_embed=pooled,
|
|
time=time,
|
|
visual_rope=None,
|
|
audio_rope=audio_rope,
|
|
text_rope=text_rope,
|
|
)
|
|
assert out.shape == (audio_len, 6)
|
|
|
|
|
|
@pytest.mark.parametrize("batched", [False, True])
|
|
def test_self_attention_attends_over_tokens_for_batched_and_unbatched_inputs(batched):
|
|
"""Self-attention must contract over the *token* axis whether its input is
|
|
unbatched text ``(S, C)`` or batched=1 visual/audio ``(1, N, C)``.
|
|
|
|
Regression: an unconditional ``unsqueeze(0)`` on the already-batched
|
|
visual path handed SDPA a 5-D ``(1, 1, N, H, D)`` tensor, which it
|
|
accepts silently but then treats the head axis as the sequence — every
|
|
generated video decoded to pure noise."""
|
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
|
from vllm_omni.diffusion.models.kandinsky6.kandinsky6_transformer import Kandinsky6Attention, apply_rotary
|
|
|
|
torch.manual_seed(0)
|
|
channels, head_dim, tokens = 24, 12, 7
|
|
with set_current_diffusion_config(_sdpa_config()):
|
|
attn = Kandinsky6Attention(channels, head_dim, engine="sdpa").eval()
|
|
with torch.no_grad():
|
|
# vLLM parallel linears allocate with torch.empty (no init).
|
|
for param in attn.parameters():
|
|
param.normal_(std=0.2)
|
|
x = torch.randn(tokens, channels)
|
|
rope = torch.randn(tokens, 1, head_dim // 2, 2, 2)
|
|
hidden = x.unsqueeze(0) if batched else x
|
|
|
|
with torch.no_grad():
|
|
out = attn(hidden, rotary_emb=rope)
|
|
|
|
heads = channels // head_dim
|
|
q = attn.query_norm(attn.to_query(x).reshape(tokens, heads, head_dim))
|
|
k = attn.key_norm(attn.to_key(x).reshape(tokens, heads, head_dim))
|
|
v = attn.to_value(x).reshape(tokens, heads, head_dim)
|
|
q = apply_rotary(q, rope).type_as(q)
|
|
k = apply_rotary(k, rope).type_as(k)
|
|
ref = torch.nn.functional.scaled_dot_product_attention(
|
|
q.transpose(0, 1), k.transpose(0, 1), v.transpose(0, 1)
|
|
).transpose(0, 1)
|
|
expected = attn.out_layer(ref.reshape(tokens, channels))
|
|
|
|
assert out.shape == (hidden.shape[:-1] + (channels,))
|
|
torch.testing.assert_close(out.reshape(tokens, channels), expected, rtol=1e-4, atol=1e-5)
|
|
|
|
|
|
def test_modulation_projections_are_zero_initialized():
|
|
"""Modulation layers are zero-initialized at construction (standard
|
|
AdaLN-zero init) — confirms the zero-init in Kandinsky6Modulation.__init__
|
|
reaches the tensor-parallel out_layer correctly (it's a
|
|
ColumnParallelLinear, not plain nn.Linear)."""
|
|
model = _build_transformer(_TINY_T2V_CONFIG)
|
|
for block in model.text_transformer_blocks:
|
|
assert torch.all(block.text_modulation.out_layer.weight == 0)
|
|
for block in model.visual_transformer_blocks:
|
|
assert torch.all(block.visual_modulation.out_layer.weight == 0)
|
|
|
|
|
|
def test_dit_constructs_from_a_diffusers_config_dict():
|
|
from vllm_omni.diffusion.config import set_current_diffusion_config
|
|
from vllm_omni.diffusion.models.kandinsky6 import Kandinsky6Transformer3DModel
|
|
|
|
with set_current_diffusion_config(_sdpa_config()):
|
|
model = Kandinsky6Transformer3DModel.from_diffusers_config(
|
|
{**_TINY_T2VA_CONFIG, "patch_size": [1, 2, 2], "axes_dims": [4, 4, 4]}
|
|
)
|
|
assert tuple(model.patch_size) == (1, 2, 2)
|
|
assert model._repeated_blocks == [
|
|
"Kandinsky6FusedTransformerDecoderBlock",
|
|
"Kandinsky6TransformerEncoderBlock",
|
|
"Kandinsky6TransformerDecoderBlock",
|
|
]
|
|
visual = model.visual_transformer_blocks[0].video_dec_block.self_attention.attn
|
|
audio = model.visual_transformer_blocks[0].audio_dec_block.self_attention.attn
|
|
text = model.video_text_transformer_blocks[0].self_attention.attn
|
|
cross = model.visual_transformer_blocks[0].va_cross_attention.attn
|
|
assert visual.role == "kandinsky6.visual_self"
|
|
assert visual.skip_sequence_parallel is False
|
|
assert audio.skip_sequence_parallel is True
|
|
assert text.role == "kandinsky6.text_self"
|
|
assert text.skip_sequence_parallel is True
|
|
assert cross.role == "kandinsky6.video_audio_cross"
|
|
assert cross.skip_sequence_parallel is True
|
|
|
|
|
|
def test_cfg_combine_matches_apply_cfg_without_normalization():
|
|
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
|
from vllm_omni.diffusion.models.kandinsky6.pipeline_kandinsky6 import apply_cfg
|
|
|
|
positive = torch.tensor([1.0, 3.0])
|
|
negative = torch.tensor([0.25, -1.0])
|
|
combined = CFGParallelMixin.combine_cfg_noise(object(), positive, negative, 5.0, False)
|
|
torch.testing.assert_close(combined, apply_cfg(positive, negative, 5.0))
|