- fastvideo-kernel/wan22_vae_decode: the Wan2.2 (TI2V-5B) VAE decoder on sm_100a / sm_103a at 704x1248, built on first use against fastvideo-kernel's CUTLASS submodule. It frees intermediates early and carries 4 latent frames per pass: a 5 s video peaks at 20.3 GiB, down from 94.6 GiB in the kernel's first version, with the same output. - WanVAEConfig.decode_kernel routes AutoencoderKLWan.decode through it where it applies (bf16 with fp32 accumulation, about 7x faster than the fp32 decode on a B300, PSNR 61-63 dB, SSIM 0.998 against it); decode_kernel_parallel splits the decode over the sequence-parallel group (2, 4 or 8 GPUs) and broadcasts the video, with the ranks agreeing on whether every one of them could build the kernel. - PAN2's DiT runs fused Triton kernels for Q/K RMSNorm + RoPE and LayerNorm + modulation (pan2_fusions, set_fused_kernels). - PAN2PipelineConfig turns all three on (vae_decode_kernel, vae_decode_kernel_parallel, dit_fused_kernels). - VideoGenerator copies a decoded RGB video's frames to the host once instead of once per frame. - The PAN2 presets sample at guidance 2.5, the reference launcher's default since 2026-10-23.
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""``_quantize_video_frames_to_uint8``, ``_video_frames`` and their CUDA-OOM fallback.
|
|
|
|
Found on a real GPU run of the K6 T2IVA-distilled example (30B params, ``dit_cpu_offload=False``): the worker process
|
|
keeps the transformer resident on the device after denoising and decode, so the client process's on-device quantize
|
|
(``fastvideo/entrypoints/video_generator.py``) can be the allocation that finally OOMs, even though the data it needs
|
|
(``src``, IPC-shared from the worker) is already there and only needs a device->host copy. These tests force that path
|
|
on CPU (no GPU here) by making the on-device arithmetic raise ``torch.cuda.OutOfMemoryError`` once, and check the
|
|
fallback produces the identical result instead of propagating the error.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from fastvideo.entrypoints.video_generator import _quantize_video_frames_to_uint8
|
|
|
|
|
|
def _reference(src: torch.Tensor) -> torch.Tensor:
|
|
"""Plain (non-fused) equivalent of the function under test, for comparison."""
|
|
return ((src.float() * 255).clamp(0, 255).to(torch.uint8)).permute(2, 0, 1, 3, 4)
|
|
|
|
|
|
@pytest.fixture()
|
|
def sample_video() -> torch.Tensor:
|
|
# [b, c, t, h, w]; includes values outside [0, 1] to exercise clamp_().
|
|
torch.manual_seed(0)
|
|
return torch.rand(2, 3, 4, 5, 6) * 1.3 - 0.15
|
|
|
|
|
|
def test_happy_path_matches_the_plain_computation(sample_video):
|
|
out = _quantize_video_frames_to_uint8(sample_video.clone())
|
|
assert out.shape == (sample_video.shape[2], sample_video.shape[0], sample_video.shape[1], sample_video.shape[3],
|
|
sample_video.shape[4])
|
|
assert out.dtype == torch.uint8
|
|
assert out.device.type == "cpu"
|
|
torch.testing.assert_close(out, _reference(sample_video), rtol=0, atol=0)
|
|
|
|
|
|
def test_out_of_range_values_are_clamped_not_wrapped(sample_video):
|
|
src = sample_video.clone()
|
|
src[0, 0, 0, 0, 0] = 5.0 # would wrap past 255 without clamp_()
|
|
src[0, 0, 0, 0, 1] = -5.0
|
|
out = _quantize_video_frames_to_uint8(src)
|
|
assert out[0, 0, 0, 0, 0].item() == 255
|
|
assert out[0, 0, 0, 0, 1].item() == 0
|
|
|
|
|
|
def test_cuda_oom_on_the_fast_path_falls_back_to_a_cpu_quantize(sample_video, monkeypatch, caplog):
|
|
calls = {"n": 0}
|
|
real_clamp_ = torch.Tensor.clamp_
|
|
|
|
def flaky_clamp_(self, *args, **kwargs):
|
|
calls["n"] += 1
|
|
if calls["n"] == 1:
|
|
raise torch.cuda.OutOfMemoryError("simulated: device holds the resident DiT, nothing left to allocate")
|
|
return real_clamp_(self, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(torch.Tensor, "clamp_", flaky_clamp_)
|
|
with caplog.at_level(logging.WARNING):
|
|
out = _quantize_video_frames_to_uint8(sample_video.clone())
|
|
|
|
assert calls["n"] == 2 # the failed fast-path attempt, then the fallback
|
|
assert out.dtype == torch.uint8 and out.device.type == "cpu"
|
|
torch.testing.assert_close(out, _reference(sample_video), rtol=0, atol=0)
|
|
assert any("out of memory" in message.lower() for message in caplog.messages)
|
|
|
|
|
|
def test_a_non_oom_runtime_error_is_not_swallowed(sample_video, monkeypatch):
|
|
def always_fails(self, *args, **kwargs):
|
|
raise RuntimeError("some unrelated failure")
|
|
|
|
monkeypatch.setattr(torch.Tensor, "clamp_", always_fails)
|
|
with pytest.raises(RuntimeError, match="unrelated failure"):
|
|
_quantize_video_frames_to_uint8(sample_video.clone())
|
|
|
|
|
|
def _frames_by_grid(src: torch.Tensor) -> list:
|
|
"""The frames as the generator built them before `_video_frames`: make_grid and a channels-last copy per frame."""
|
|
import torchvision
|
|
|
|
return [
|
|
torchvision.utils.make_grid(x, nrow=6).permute(1, 2, 0).squeeze(-1).contiguous().numpy()
|
|
for x in _quantize_video_frames_to_uint8(src)
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("shape", [(1, 3, 4, 5, 6), (1, 1, 4, 5, 6), (2, 3, 4, 5, 6)])
|
|
def test_video_frames_match_the_per_frame_grid(shape):
|
|
import numpy as np
|
|
|
|
from fastvideo.entrypoints.video_generator import _video_frames
|
|
|
|
torch.manual_seed(1)
|
|
src = torch.rand(*shape) * 1.3 - 0.15
|
|
frames, expected = _video_frames(src.clone()), _frames_by_grid(src.clone())
|
|
assert len(frames) == len(expected) == shape[2]
|
|
for frame, reference in zip(frames, expected):
|
|
assert frame.dtype == np.uint8 and frame.shape == reference.shape
|
|
np.testing.assert_array_equal(frame, reference)
|
|
|
|
|
|
def test_video_frames_fall_back_to_the_cpu_on_cuda_oom(monkeypatch, caplog):
|
|
import numpy as np
|
|
|
|
from fastvideo.entrypoints.video_generator import _video_frames
|
|
|
|
torch.manual_seed(2)
|
|
src = torch.rand(1, 3, 4, 5, 6)
|
|
expected = _frames_by_grid(src.clone())
|
|
calls = {"n": 0}
|
|
real_clamp_ = torch.Tensor.clamp_
|
|
|
|
def flaky_clamp_(self, *args, **kwargs):
|
|
calls["n"] += 1
|
|
if calls["n"] == 1:
|
|
raise torch.cuda.OutOfMemoryError("simulated")
|
|
return real_clamp_(self, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(torch.Tensor, "clamp_", flaky_clamp_)
|
|
with caplog.at_level(logging.WARNING):
|
|
frames = _video_frames(src.clone())
|
|
assert calls["n"] == 2
|
|
for frame, reference in zip(frames, expected):
|
|
np.testing.assert_array_equal(frame, reference)
|
|
assert any("out of memory" in message.lower() for message in caplog.messages)
|