Files
vllm-omni/tests/diffusion/test_media_output_contract.py
2026-09-10 09:50:54 +00:00

547 lines
21 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import numpy as np
import pytest
import torch
import vllm_omni.diffusion.diffusion_engine as diffusion_engine_module
import vllm_omni.diffusion.ipc as diffusion_ipc
import vllm_omni.diffusion.postprocess.media as media_postprocess
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig, VideoOutputTransportConfig
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine
from vllm_omni.diffusion.ipc import pack_diffusion_output_shm, unpack_diffusion_output_shm
from vllm_omni.diffusion.media import (
DiffusionMediaOutput,
FloatVideoConsumer,
VideoMediaOutput,
VideoTensorEncoding,
VideoTensorLayout,
VideoTensorSpec,
VideoTransportConstraints,
VideoValueRange,
)
from vllm_omni.diffusion.postprocess.device_reduction import prepare_diffusion_media_for_transport
from vllm_omni.diffusion.postprocess.media import finalize_diffusion_media
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def _video(
tensor: torch.Tensor,
*,
encoding: VideoTensorEncoding,
value_range: VideoValueRange,
consumers: frozenset[FloatVideoConsumer] = frozenset(),
) -> VideoMediaOutput:
layout = VideoTensorLayout.BTHWC if encoding is VideoTensorEncoding.UINT8_FRAMES else VideoTensorLayout.BCTHW
return VideoMediaOutput(
tensor=tensor,
spec=VideoTensorSpec(
layout=layout,
encoding=encoding,
value_range=value_range,
),
constraints=VideoTransportConstraints(pending_float_consumers=consumers),
)
def _prepared_media(video: VideoMediaOutput) -> DiffusionMediaOutput:
return DiffusionMediaOutput(video=video, prepared_for_transport=True)
def _od_config(*, enabled: bool = False) -> OmniDiffusionConfig:
config = OmniDiffusionConfig(
model=None,
video_output_transport=VideoOutputTransportConfig(enable_device_postprocess=enabled),
)
config.model_class_name = "WanPipeline"
return config
def test_finalizer_rejects_media_that_skipped_runner_preparation() -> None:
media = DiffusionMediaOutput(
video=_video(
torch.randn(1, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
)
with pytest.raises(ValueError, match="before transport preparation"):
finalize_diffusion_media(media, sampling_params=OmniDiffusionSamplingParams(output_type="np"))
def test_uint8_media_finalization_preserves_frames() -> None:
frames = torch.randint(0, 256, (1, 4, 8, 8, 3), dtype=torch.uint8)
output = finalize_diffusion_media(
_prepared_media(
_video(
frames,
encoding=VideoTensorEncoding.UINT8_FRAMES,
value_range=VideoValueRange.ZERO_TO_255,
)
),
sampling_params=OmniDiffusionSamplingParams(output_type="np"),
)
np.testing.assert_array_equal(output["payload"]["video"], frames.numpy())
assert output["metadata"] == {}
def test_float_media_finalization_uses_the_declared_range() -> None:
video = torch.tensor([[[[[-1.0, 0.0, 1.0]]]]]).expand(1, 3, 1, 1, 3).contiguous()
output = finalize_diffusion_media(
_prepared_media(
_video(
video,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
),
sampling_params=OmniDiffusionSamplingParams(output_type="np"),
)
expected = (video.permute(0, 2, 3, 4, 1).numpy() * 0.5 + 0.5).clip(0.0, 1.0)
np.testing.assert_array_equal(output["payload"]["video"], expected)
def test_zero_to_one_float_media_is_not_denormalized() -> None:
video = torch.tensor([[[[[0.0, 0.5, 1.0]]]]]).expand(1, 3, 1, 1, 3).contiguous()
media = _prepared_media(
_video(
video,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.ZERO_TO_ONE,
)
)
output = finalize_diffusion_media(media, sampling_params=OmniDiffusionSamplingParams(output_type="np"))
expected = video.permute(0, 2, 3, 4, 1).numpy()
np.testing.assert_array_equal(output["payload"]["video"], expected)
def test_presentation_is_resolved_outside_the_tensor_descriptor() -> None:
video = torch.zeros(1, 3, 2, 4, 5)
media = _prepared_media(
_video(
video,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
)
output = finalize_diffusion_media(media, sampling_params=OmniDiffusionSamplingParams(output_type="pil"))
assert "presentation" not in media.video.__dataclass_fields__
assert len(output["payload"]["video"]) == 1
assert len(output["payload"]["video"][0]) == 2
def test_frame_interpolation_consumes_its_constraint(monkeypatch: pytest.MonkeyPatch) -> None:
video = torch.randn(1, 3, 2, 4, 5)
interpolated = torch.randn(1, 3, 3, 4, 5)
monkeypatch.setattr(
media_postprocess,
"interpolate_video_tensor",
lambda *args, **kwargs: (interpolated, 2.0),
)
output = finalize_diffusion_media(
_prepared_media(
_video(
video,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
consumers=frozenset({FloatVideoConsumer.FRAME_INTERPOLATION}),
)
),
sampling_params=OmniDiffusionSamplingParams(output_type="np", enable_frame_interpolation=True),
)
assert output["payload"]["video"].shape == (1, 3, 4, 5, 3)
assert output["metadata"] == {"video": {"video_fps_multiplier": 2.0}}
def test_uint8_media_rejects_a_pending_float_consumer() -> None:
media = _prepared_media(
_video(
torch.zeros(1, 2, 4, 5, 3, dtype=torch.uint8),
encoding=VideoTensorEncoding.UINT8_FRAMES,
value_range=VideoValueRange.ZERO_TO_255,
consumers=frozenset({FloatVideoConsumer.FRAME_INTERPOLATION}),
)
)
with pytest.raises(ValueError, match="cannot have pending float consumers"):
media.validate()
def test_unconsumed_float_constraint_fails_before_public_formatting() -> None:
media = _prepared_media(
_video(
torch.randn(1, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
consumers=frozenset({FloatVideoConsumer.VIDEO_GUARDRAILS}),
)
)
with pytest.raises(ValueError, match="pending float consumers"):
finalize_diffusion_media(media, sampling_params=OmniDiffusionSamplingParams(output_type="np"))
def test_engine_routes_typed_media_exclusively(monkeypatch: pytest.MonkeyPatch) -> None:
media = _prepared_media(
_video(
torch.zeros(1, 1, 2, 2, 3, dtype=torch.uint8),
encoding=VideoTensorEncoding.UINT8_FRAMES,
value_range=VideoValueRange.ZERO_TO_255,
)
)
request = OmniDiffusionRequest(
prompt="test",
sampling_params=OmniDiffusionSamplingParams(output_type="np"),
request_id="request-0",
)
engine = object.__new__(DiffusionEngine)
engine.od_config = _od_config()
engine.post_process_func = lambda _: pytest.fail("legacy postprocessor was called")
engine._post_process_accepts_sampling_params = False
finalized = {"payload": {"video": np.zeros((1, 1, 2, 2, 3), dtype=np.uint8)}, "metadata": {}}
def _format_outputs(
*,
request: object,
od_config: object,
diffusion_output: object,
output_data: object,
postprocess_output: object,
) -> list[object]:
del request, od_config, diffusion_output, output_data
return [postprocess_output]
monkeypatch.setattr(diffusion_engine_module, "finalize_diffusion_media", lambda *args, **kwargs: finalized)
monkeypatch.setattr(diffusion_engine_module, "normalize_diffusion_postprocess_output", lambda value: value)
monkeypatch.setattr(diffusion_engine_module, "format_diffusion_outputs", _format_outputs)
result = engine.postprocess_output(request, DiffusionOutput(media=media))
assert result == [finalized]
def test_typed_media_crosses_runner_ipc_and_engine_without_legacy_postprocess(
monkeypatch: pytest.MonkeyPatch,
) -> None:
sampling_params = OmniDiffusionSamplingParams(output_type="np", num_outputs_per_prompt=1)
raw_media = DiffusionMediaOutput(
video=_video(
torch.linspace(-1.0, 1.0, 1 * 3 * 2 * 4 * 5).reshape(1, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
)
prepared = prepare_diffusion_media_for_transport(
raw_media,
od_config=_od_config(enabled=True),
sampling_params=sampling_params,
)
diffusion_output = DiffusionOutput(media=prepared)
monkeypatch.setattr(diffusion_ipc, "_SHM_TENSOR_THRESHOLD", 1)
pack_diffusion_output_shm(diffusion_output)
unpack_diffusion_output_shm(diffusion_output)
engine = object.__new__(DiffusionEngine)
engine.od_config = _od_config()
engine.post_process_func = lambda _: pytest.fail("legacy postprocessor was called")
engine._post_process_accepts_sampling_params = False
request = OmniDiffusionRequest(prompt="test", sampling_params=sampling_params, request_id="request-0")
outputs = engine.postprocess_output(request, diffusion_output)
assert len(outputs) == 1
assert outputs[0].images[0].dtype == np.uint8
assert outputs[0].images[0].shape == (1, 2, 4, 5, 3)
def test_diffusion_output_rejects_typed_and_legacy_output_together() -> None:
media = _prepared_media(
_video(
torch.zeros(1, 1, 2, 2, 3, dtype=torch.uint8),
encoding=VideoTensorEncoding.UINT8_FRAMES,
value_range=VideoValueRange.ZERO_TO_255,
)
)
with pytest.raises(ValueError, match="both media and legacy output"):
DiffusionOutput(output=torch.zeros(1), media=media)
def test_diffusion_output_rejects_postprocessor_on_typed_media() -> None:
media = _prepared_media(
_video(
torch.zeros(1, 1, 2, 2, 3, dtype=torch.uint8),
encoding=VideoTensorEncoding.UINT8_FRAMES,
value_range=VideoValueRange.ZERO_TO_255,
)
)
with pytest.raises(ValueError, match="model-specific post_process_func"):
DiffusionOutput(media=media, post_process_func=lambda value: value)
def test_diffusion_output_preserves_the_legacy_positional_constructor() -> None:
# ``media`` was originally declared right after ``output``, which broke
# ``DiffusionOutput(output, trajectory_timesteps)`` by binding the second
# argument to media. It must stay last so out-of-tree positional callers keep
# the pre-existing field order.
output_tensor = torch.randn(1)
timesteps = torch.tensor([1, 2, 3])
output = DiffusionOutput(output_tensor, timesteps)
assert output.output is output_tensor
assert output.trajectory_timesteps is timesteps
assert output.media is None
def test_frame_interpolation_uses_the_declared_range_negative_one_to_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# RIFE infers [0,1] vs [-1,1] from the tensor min/max, so an all-nonnegative
# [-1,1] clip is misread as [0,1] and interpolated in the wrong space. The
# interpolator must receive the unit-range form per the declared spec.
video = torch.full((1, 3, 2, 3, 3), 0.25)
media = DiffusionMediaOutput(
video=VideoMediaOutput(
tensor=video,
spec=VideoTensorSpec(
layout=VideoTensorLayout.BCTHW,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
),
constraints=VideoTransportConstraints(
pending_float_consumers=frozenset({FloatVideoConsumer.FRAME_INTERPOLATION})
),
),
prepared_for_transport=True,
)
seen: list[torch.Tensor] = []
def _square_interpolator(tensor: torch.Tensor, **_kwargs: object) -> tuple[torch.Tensor, float]:
seen.append(tensor)
return tensor * tensor, 2.0
monkeypatch.setattr(media_postprocess, "interpolate_video_tensor", _square_interpolator)
output = finalize_diffusion_media(
media,
sampling_params=OmniDiffusionSamplingParams(output_type="np", enable_frame_interpolation=True),
)
# The interpolator is fed the unit-range form 0.25→0.625, not raw 0.25.
assert len(seen) == 1
assert seen[0].shape == video.shape
assert float(seen[0][0, 0, 0, 0, 0]) == pytest.approx(0.625, abs=1e-6)
# Squaring gives 0.390625; restored to [-1,1] (-0.21875) by the fix, then the
# existing denormalize maps it back to [0,1] as 0.390625 in the public frames.
frames = output["payload"]["video"]
assert np.allclose(np.asarray(frames), 0.390625, atol=1e-6)
def test_frame_interpolation_uses_the_declared_range_zero_to_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# With a declared [0,1] range, the all-0.25 clip is already in unit space, so
# squaring gives 0.390625 and there is no restore step.
video = torch.full((1, 3, 2, 3, 3), 0.25)
media = DiffusionMediaOutput(
video=VideoMediaOutput(
tensor=video,
spec=VideoTensorSpec(
layout=VideoTensorLayout.BCTHW,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.ZERO_TO_ONE,
),
constraints=VideoTransportConstraints(
pending_float_consumers=frozenset({FloatVideoConsumer.FRAME_INTERPOLATION})
),
),
prepared_for_transport=True,
)
def _square_interpolator(tensor: torch.Tensor, **_kwargs: object) -> tuple[torch.Tensor, float]:
return tensor * tensor, 2.0
monkeypatch.setattr(media_postprocess, "interpolate_video_tensor", _square_interpolator)
output = finalize_diffusion_media(
media,
sampling_params=OmniDiffusionSamplingParams(output_type="np", enable_frame_interpolation=True),
)
# 0.25 is already [0,1]; squaring gives 0.0625, and no denormalize is applied.
frames = output["payload"]["video"]
assert np.allclose(np.asarray(frames), 0.0625, atol=1e-6)
def test_frame_interpolation_clamps_zero_to_one_overshoot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# A declared [0,1] clip whose sample is almost entirely inside [0,1] — but
# with one bf16 value overshooting 1 — is still in unit space, so it must be
# clamped to [0,1] before RIFE and interpolated as [0,1]. Without the clamp
# the interpolator classifies it as [-1,1] (amax > 1) and returns the wrong
# space, which is then written back with no *2-1 restore.
video = torch.full((1, 3, 2, 3, 3), 0.25, dtype=torch.bfloat16)
video[0, 0, 0, 0, 0] = 1.25 # single overshoot > 1 -> amax > 1
media = DiffusionMediaOutput(
video=VideoMediaOutput(
tensor=video,
spec=VideoTensorSpec(
layout=VideoTensorLayout.BCTHW,
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.ZERO_TO_ONE,
),
constraints=VideoTransportConstraints(
pending_float_consumers=frozenset({FloatVideoConsumer.FRAME_INTERPOLATION})
),
),
prepared_for_transport=True,
)
seen: list[torch.Tensor] = []
def _square_interpolator(tensor: torch.Tensor, **_kwargs: object) -> tuple[torch.Tensor, float]:
seen.append(tensor)
return tensor.float().square(), 2.0
monkeypatch.setattr(media_postprocess, "interpolate_video_tensor", _square_interpolator)
output = finalize_diffusion_media(
media,
sampling_params=OmniDiffusionSamplingParams(output_type="np", enable_frame_interpolation=True),
)
# The interpolator is fed the clamped bf16 unit-range tensor (max now 1.0),
# not the raw 1.25 overshoot that would be misread as [-1,1].
assert len(seen) == 1
assert seen[0].dtype is torch.bfloat16
assert float(seen[0].min()) == pytest.approx(0.25, abs=1e-6)
assert float(seen[0].max()) == pytest.approx(1.0, abs=1e-6)
# Squaring the clamped values gives 0.0625 at the 0.25 samples and 1.0 at the
# overshoot; the overall shape is unchanged (no *2-1 for ZERO_TO_ONE).
frames = output["payload"]["video"]
assert np.allclose(np.asarray(frames).reshape(-1)[1:], 0.0625, atol=1e-6)
assert np.isclose(float(np.asarray(frames).reshape(-1)[0]), 1.0, atol=1e-6)
def test_packing_failure_unlinks_created_segments_and_leaves_output_untouched(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Packing a late field can fail after an SHM segment is already created;
# the segment must be unlinked and ``output`` left unmutated so a partial
# object is never enqueued.
prepared = prepare_diffusion_media_for_transport(
DiffusionMediaOutput(
video=_video(
torch.linspace(-1.0, 1.0, 2 * 3 * 2 * 4 * 5).reshape(2, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
),
od_config=_od_config(enabled=False),
sampling_params=OmniDiffusionSamplingParams(output_type="np"),
)
output = DiffusionOutput(media=prepared, trajectory_latents=torch.randn(30))
original_media = prepared
original_latents = output.trajectory_latents
real_array_to_shm = diffusion_ipc._array_to_shm
created_handles: list[dict[str, object]] = []
def _fail_on_second(array: np.ndarray) -> dict[str, object]:
if created_handles:
raise RuntimeError("injected packing failure")
handle = real_array_to_shm(array)
created_handles.append(handle)
return handle
monkeypatch.setattr(diffusion_ipc, "_SHM_TENSOR_THRESHOLD", 1)
monkeypatch.setattr(diffusion_ipc, "_array_to_shm", _fail_on_second)
with pytest.raises(RuntimeError, match="injected packing failure"):
pack_diffusion_output_shm(output)
assert created_handles, "no SHM segment was created before the injected failure"
# Output was not mutated despite the media segment being created first.
assert output.media is original_media
assert isinstance(output.media, DiffusionMediaOutput)
assert output.trajectory_latents is original_latents
from multiprocessing import shared_memory
with pytest.raises(FileNotFoundError):
shared_memory.SharedMemory(name=str(created_handles[0]["name"]))
def test_second_batch_entry_failure_leaves_the_whole_batch_untouched(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from multiprocessing import shared_memory
from vllm_omni.diffusion.worker.utils import BatchRunnerOutput, RunnerOutput
first_media = _prepared_media(
_video(
torch.zeros(1, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
)
second_media = _prepared_media(
_video(
torch.ones(1, 3, 2, 4, 5),
encoding=VideoTensorEncoding.NORMALIZED_FLOAT,
value_range=VideoValueRange.NEGATIVE_ONE_TO_ONE,
)
)
first = DiffusionOutput(media=first_media)
second = DiffusionOutput(media=second_media)
batch = BatchRunnerOutput(
runner_outputs=[
RunnerOutput(request_id="first", result=first),
RunnerOutput(request_id="second", result=second),
]
)
real_array_to_shm = diffusion_ipc._array_to_shm
created_handles: list[dict[str, object]] = []
def _fail_on_second(array: np.ndarray) -> dict[str, object]:
if created_handles:
raise RuntimeError("second batch entry failed")
handle = real_array_to_shm(array)
created_handles.append(handle)
return handle
monkeypatch.setattr(diffusion_ipc, "_SHM_TENSOR_THRESHOLD", 1)
monkeypatch.setattr(diffusion_ipc, "_array_to_shm", _fail_on_second)
with pytest.raises(RuntimeError, match="second batch entry failed"):
pack_diffusion_output_shm(batch)
# No per-entry assignment is committed until every entry has packed.
assert first.media is first_media
assert second.media is second_media
with pytest.raises(FileNotFoundError):
shared_memory.SharedMemory(name=str(created_handles[0]["name"]))