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

126 lines
5.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""GPU correctness/size tests for device-side video reduction.
The reduction must be byte-identical to the current production output (SHM
widens bf16->f32, then diffusers postprocess, then the API server's ``*255``
rounding), otherwise enabling it would silently change pixels.
"""
from __future__ import annotations
import numpy as np
import pytest
import torch
from tests.helpers.mark import hardware_marks
from vllm_omni.diffusion.postprocess.device_reduction import reduce_video_to_uint8_frames
pytestmark = [
pytest.mark.core_model,
pytest.mark.diffusion,
*hardware_marks(res={"cuda": "L4"}, num_cards=1),
]
_GPU = torch.accelerator.is_available() if hasattr(torch, "accelerator") else torch.cuda.is_available()
requires_gpu = pytest.mark.skipif(not _GPU, reason="device reduction is a GPU path")
def _device() -> str:
return "cuda:0"
def _reference_uint8(video: torch.Tensor, *, compute_dtype: torch.dtype | None = torch.float32) -> np.ndarray:
"""The diffusers postprocess plus the API server's rounding, for the same input.
``compute_dtype`` selects the precision the denormalize runs at.
``torch.float32`` is the precision the device reduction uses; ``None`` keeps
the input's own dtype, which is what the engine's postprocess actually does.
"""
from diffusers.video_processor import VideoProcessor
source = video if compute_dtype is None else video.to(compute_dtype)
processed = VideoProcessor(vae_scale_factor=8).postprocess_video(source, output_type="np")
scaled = np.clip(processed.astype(np.float32), 0.0, 1.0) * 255.0
np.rint(scaled, out=scaled)
return scaled.astype(np.uint8)
@requires_gpu
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
def test_device_reduction_matches_a_float32_reference(dtype: torch.dtype) -> None:
"""The reduction must be the float32 computation, exactly, for every input dtype."""
torch.manual_seed(0)
# Values slightly outside [-1, 1] so the clamp is actually exercised.
video = (torch.rand(1, 3, 8, 64, 96, device=_device()) * 2.4 - 1.2).to(dtype)
reference = _reference_uint8(video)
produced = reduce_video_to_uint8_frames(video).cpu().numpy()
assert produced.shape == reference.shape == (1, 8, 64, 96, 3)
assert produced.dtype == np.uint8
np.testing.assert_array_equal(produced, reference)
@requires_gpu
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
def test_device_reduction_deviates_from_a_narrow_postprocess_by_at_most_one(dtype: torch.dtype) -> None:
"""Pin the cost of computing in float32: at most one 255th.
A postprocess that denormalizes in the VAE's own dtype (WAN, HunyuanVideo,
LTX-2, Cosmos3) does not match this reduction bit for bit; ~20% of pixels
differ by one on a real 480x832x81 WAN generation. Keep that a bound rather
than a surprise, so a regression widening it fails here.
"""
torch.manual_seed(0)
video = (torch.rand(1, 3, 8, 64, 96, device=_device()) * 2.4 - 1.2).to(dtype)
narrow = _reference_uint8(video, compute_dtype=None)
produced = reduce_video_to_uint8_frames(video).cpu().numpy()
deviation = np.abs(produced.astype(np.int16) - narrow.astype(np.int16))
assert deviation.max() <= 1
# The narrow path really is different, so the bound above is not vacuous.
assert deviation.any()
@requires_gpu
def test_device_reduction_shrinks_hop1_payload_4x() -> None:
"""The whole point: the D2H payload drops from float32 to uint8 (4x)."""
video = torch.rand(1, 3, 8, 704, 1280, device=_device(), dtype=torch.bfloat16) * 2 - 1
# Production transports bf16 widened to float32 over SHM.
widened_f32_bytes = video.numel() * 4
reduced = reduce_video_to_uint8_frames(video)
reduced_bytes = reduced.numel() * reduced.element_size()
assert reduced.dtype == torch.uint8
assert widened_f32_bytes / reduced_bytes == pytest.approx(4.0, rel=1e-6)
@requires_gpu
def test_device_reduction_stays_on_device() -> None:
"""Reduction must happen before D2H, so the result is still on the GPU."""
video = torch.rand(1, 3, 4, 32, 48, device=_device(), dtype=torch.bfloat16) * 2 - 1
reduced = reduce_video_to_uint8_frames(video)
assert reduced.device.type == "cuda"
assert reduced.is_contiguous()
@requires_gpu
def test_device_reduction_without_denormalize_matches_already_unit_range() -> None:
"""Pipelines with do_normalize=False emit [0,1]; skip the denorm step."""
video = torch.rand(1, 3, 4, 16, 16, device=_device(), dtype=torch.float32) # already [0,1]
produced = reduce_video_to_uint8_frames(video, do_denormalize=False).cpu().numpy()
expected = video.permute(0, 2, 3, 4, 1).cpu().numpy()
expected = np.rint(np.clip(expected, 0, 1) * 255.0).astype(np.uint8)
np.testing.assert_array_equal(produced, expected)
def test_device_reduction_rejects_non_5d() -> None:
"""A wrong-rank tensor is a programming error, not a silent reshape."""
with pytest.raises(ValueError, match=r"\[B, C, F, H, W\]"):
reduce_video_to_uint8_frames(torch.rand(3, 8, 64, 64))