[diffusion] optimization: accelerate CUDA video output finalization (#32784)

This commit is contained in:
Mick
2026-07-29 22:04:20 +08:00
committed by GitHub
parent 0ebbe43dbb
commit 22151edca1
2 changed files with 514 additions and 13 deletions
@@ -8,11 +8,15 @@ This module provides a consolidated interface for generating videos using
diffusion models.
"""
import atexit
import json
import mmap
import os
import shutil
import subprocess
import tempfile
import threading
from contextlib import contextmanager
from copy import copy
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional, Sequence, Union
@@ -43,6 +47,114 @@ from sglang.srt.observability.trace import TraceReqContext
logger = init_logger(__name__)
_MAX_CACHED_CUDA_VIDEO_BUFFER_BYTES = 1024 * 1024 * 1024
_cuda_video_buffer_cache_lock = threading.Lock()
_cached_cuda_video_buffer: "_CudaMemfdVideoBuffer | None" = None
class _CudaMemfdVideoBuffer:
"""CUDA-registered memfd used as direct ffmpeg raw-video input."""
def __init__(self, shape: tuple[int, ...]):
self.shape = shape
self.nbytes = int(np.prod(shape, dtype=np.int64))
self.fd = -1
self.mapping: mmap.mmap | None = None
self.array: np.ndarray | None = None
self.tensor: torch.Tensor | None = None
self._registered = False
try:
self.fd = os.memfd_create(
"sglang-video-frames",
flags=getattr(os, "MFD_CLOEXEC", 0),
)
os.ftruncate(self.fd, self.nbytes)
self.mapping = mmap.mmap(
self.fd,
self.nbytes,
flags=mmap.MAP_SHARED,
prot=mmap.PROT_READ | mmap.PROT_WRITE,
)
self.array = np.ndarray(shape, dtype=np.uint8, buffer=self.mapping)
error = torch.cuda.cudart().cudaHostRegister(
self.array.ctypes.data,
self.nbytes,
0,
)
if error != 0:
raise RuntimeError(f"cudaHostRegister failed: {error}")
self._registered = True
self.tensor = torch.from_numpy(self.array)
if not self.tensor.is_pinned():
raise RuntimeError("CUDA-registered memfd is not pinned")
except Exception:
self.close()
raise
def close(self) -> None:
if self._registered and self.array is not None:
try:
torch.cuda.cudart().cudaHostUnregister(self.array.ctypes.data)
except Exception:
pass
self._registered = False
self.tensor = None
self.array = None
if self.mapping is not None:
self.mapping.close()
self.mapping = None
if self.fd >= 0:
os.close(self.fd)
self.fd = -1
@contextmanager
def _acquire_cuda_video_buffer(shape: tuple[int, ...]):
global _cached_cuda_video_buffer
buffer = None
stale_buffer = None
with _cuda_video_buffer_cache_lock:
if _cached_cuda_video_buffer is not None:
if _cached_cuda_video_buffer.shape == shape:
buffer = _cached_cuda_video_buffer
else:
stale_buffer = _cached_cuda_video_buffer
_cached_cuda_video_buffer = None
if stale_buffer is not None:
stale_buffer.close()
if buffer is None:
buffer = _CudaMemfdVideoBuffer(shape)
try:
yield buffer
finally:
with _cuda_video_buffer_cache_lock:
if (
buffer.nbytes <= _MAX_CACHED_CUDA_VIDEO_BUFFER_BYTES
and _cached_cuda_video_buffer is None
):
_cached_cuda_video_buffer = buffer
buffer = None
if buffer is not None:
buffer.close()
def _close_cached_cuda_video_buffer() -> None:
global _cached_cuda_video_buffer
with _cuda_video_buffer_cache_lock:
buffer = _cached_cuda_video_buffer
_cached_cuda_video_buffer = None
if buffer is not None:
buffer.close()
atexit.register(_close_cached_cuda_video_buffer)
@dataclass
class SetLoraReq:
@@ -335,6 +447,157 @@ def _resolve_ffmpeg_exe() -> str:
return ffmpeg_exe
def _x264_auto_thread_count(height: int) -> int:
"""Match x264's auto frame-thread count for progressive video."""
try:
cpu_count = len(os.sched_getaffinity(0))
except (AttributeError, OSError):
cpu_count = os.cpu_count() or 1
cpu_limit = max(1, cpu_count * 3 // 2)
macroblock_rows = max(1, (height + 15) // 16)
row_limit = max(1, macroblock_rows // 2)
return min(cpu_limit, row_limit, 128)
def _try_save_cuda_video_direct(
*,
save_file_path: str,
sample: Any,
fps: int,
audio_sample_rate: Optional[int],
output_compression: Optional[int],
) -> bool:
"""Save a CUDA RGB video through a registered memfd instead of a Python pipe."""
if not hasattr(os, "memfd_create") or not os.path.isdir("/proc/self/fd"):
return False
sample_without_audio, audio = _split_sample_audio(sample)
if not (
isinstance(sample_without_audio, torch.Tensor)
and sample_without_audio.device.type == "cuda"
and sample_without_audio.dim() in (3, 4)
):
return False
if os.path.splitext(save_file_path)[1].lower() != ".mp4":
return False
video = sample_without_audio
if video.dim() == 3:
video = video.unsqueeze(1)
if video.shape[0] != 3:
return False
frames = (video * 255).clamp(0, 255).to(torch.uint8)
frames = frames.permute(1, 2, 3, 0).contiguous()
num_frames, height, width, _ = frames.shape
quality = output_compression / 10 if output_compression is not None else 5
if not 1 <= quality <= 10:
return False
crf = int((1 - quality / 10.0) * 51)
audio_np = _normalize_audio_to_numpy(audio)
tmp_wav_path = None
try:
if audio_np is not None:
if scipy_wavfile is None:
return False
selected_sr = _pick_audio_sample_rate(
audio_np=audio_np,
audio_sample_rate=audio_sample_rate,
fps=fps,
num_frames=num_frames,
)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_wav_path = f.name
scipy_wavfile.write(tmp_wav_path, selected_sr, audio_np)
ffmpeg_exe = _resolve_ffmpeg_exe()
shape = tuple(frames.shape)
with _acquire_cuda_video_buffer(shape) as buffer:
assert buffer.tensor is not None
buffer.tensor.copy_(frames, non_blocking=True)
torch.cuda.current_stream(frames.device).synchronize()
del frames
os.lseek(buffer.fd, 0, os.SEEK_SET)
command = [
ffmpeg_exe,
"-y",
"-f",
"rawvideo",
"-vcodec",
"rawvideo",
"-s",
f"{width}x{height}",
"-pix_fmt",
"rgb24",
"-r",
f"{fps:.02f}",
"-i",
f"/proc/self/fd/{buffer.fd}",
]
if tmp_wav_path is None:
command += ["-an"]
else:
command += ["-i", tmp_wav_path]
command += [
"-vcodec",
"libx264",
"-pix_fmt",
"yuv420p",
"-crf",
str(crf),
]
macro_block_size = 16
if width % macro_block_size or height % macro_block_size:
output_width = (
width
if width % macro_block_size == 0
else width + macro_block_size - width % macro_block_size
)
output_height = (
height
if height % macro_block_size == 0
else height + macro_block_size - height % macro_block_size
)
command += ["-vf", f"scale={output_width}:{output_height}"]
command += ["-threads", str(_x264_auto_thread_count(height))]
if tmp_wav_path is not None:
command += [
"-acodec",
"aac",
"-map",
"0:v:0",
"-map",
"1:a:0",
]
command += ["-v", "warning", save_file_path]
subprocess.run(
command,
check=True,
pass_fds=(buffer.fd,),
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
return True
except Exception as e:
logger.warning(
"Direct CUDA video save failed; falling back to imageio: %s",
str(e),
)
return False
finally:
if tmp_wav_path:
try:
os.remove(tmp_wav_path)
except OSError:
pass
def _mux_audio_np_into_mp4(
*,
save_file_path: str,
@@ -423,6 +686,62 @@ def _maybe_mux_audio_into_mp4(
)
def _try_save_video_with_audio(
*,
save_file_path: str,
frames: list,
fps: int,
audio: Any,
audio_sample_rate: Optional[int],
output_format: str,
quality: float,
) -> bool:
"""Encode video and audio in one ffmpeg pass when audio is available."""
audio_np = _normalize_audio_to_numpy(audio)
if audio_np is None:
return False
selected_sr = _pick_audio_sample_rate(
audio_np=audio_np,
audio_sample_rate=audio_sample_rate,
fps=fps,
num_frames=len(frames),
)
tmp_wav_path = None
try:
if scipy_wavfile is None:
raise RuntimeError(
"scipy is required to mux audio into mp4 (pip install scipy)"
)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_wav_path = f.name
scipy_wavfile.write(tmp_wav_path, selected_sr, audio_np)
imageio.mimsave(
save_file_path,
frames,
fps=fps,
format=output_format,
codec="libx264",
quality=quality,
audio_path=tmp_wav_path,
audio_codec="aac",
)
return True
except Exception as e:
logger.warning(
"Failed to encode video and audio in one pass; "
"falling back to the compatible two-pass path: %s",
str(e),
)
return False
finally:
if tmp_wav_path:
try:
os.remove(tmp_wav_path)
except OSError:
pass
def prepare_request(
server_args: ServerArgs,
sampling_params: SamplingParams,
@@ -502,7 +821,23 @@ def _sample_to_uint8_frames(sample: Any) -> list[Any]:
if sample.dim() == 3:
sample = sample.unsqueeze(1)
sample = (sample * 255).clamp(0, 255).to(torch.uint8)
videos = sample.permute(1, 2, 3, 0).contiguous().cpu().numpy()
videos = sample.permute(1, 2, 3, 0).contiguous()
if videos.device.type == "cuda":
try:
host_videos = torch.empty(
videos.shape,
dtype=videos.dtype,
device="cpu",
pin_memory=True,
)
except RuntimeError:
videos = videos.cpu().numpy()
else:
host_videos.copy_(videos, non_blocking=True)
torch.cuda.current_stream(videos.device).synchronize()
videos = host_videos.numpy()
else:
videos = videos.cpu().numpy()
return list(videos)
if not isinstance(sample, np.ndarray):
@@ -592,22 +927,33 @@ def save_materialized_output(
os.makedirs(os.path.dirname(save_file_path), exist_ok=True)
if data_type == DataType.VIDEO:
quality = output_compression / 10 if output_compression is not None else 5
imageio.mimsave(
save_file_path,
materialized.frames,
fps=materialized.fps,
format=data_type.get_default_extension(),
codec="libx264",
quality=quality,
)
_maybe_mux_audio_into_mp4(
output_format = data_type.get_default_extension()
saved_with_audio = _try_save_video_with_audio(
save_file_path=save_file_path,
audio=materialized.audio,
frames=materialized.frames,
fps=materialized.fps,
audio=materialized.audio,
audio_sample_rate=audio_sample_rate,
output_format=output_format,
quality=quality,
)
if not saved_with_audio:
imageio.mimsave(
save_file_path,
materialized.frames,
fps=materialized.fps,
format=output_format,
codec="libx264",
quality=quality,
)
_maybe_mux_audio_into_mp4(
save_file_path=save_file_path,
audio=materialized.audio,
frames=materialized.frames,
fps=materialized.fps,
audio_sample_rate=audio_sample_rate,
)
else:
quality = output_compression if output_compression is not None else 75
if len(materialized.frames) > 1:
@@ -681,6 +1027,28 @@ def save_outputs(
if data_type == DataType.VIDEO:
sample = attach_audio_to_video_sample(sample, audio, idx)
if (
save_output
and save_file_path
and frames_out is None
and not enable_frame_interpolation
and not enable_upscaling
):
os.makedirs(os.path.dirname(save_file_path) or ".", exist_ok=True)
if _try_save_cuda_video_direct(
save_file_path=save_file_path,
sample=sample,
fps=fps,
audio_sample_rate=audio_sample_rate,
output_compression=output_compression,
):
if samples_out is not None:
samples_out.append(sample)
if audios_out is not None:
audios_out.append(select_output_audio(audio, idx))
output_paths.append(save_file_path)
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
continue
frames = post_process_sample(
sample,
@@ -1,10 +1,15 @@
import numpy as np
import pytest
import torch
from PIL import Image
import sglang.multimodal_gen.runtime.entrypoints.utils as output_utils
from sglang.multimodal_gen.configs.sample.sampling_params import DataType
from sglang.multimodal_gen.runtime.entrypoints.utils import post_process_sample
from sglang.multimodal_gen.runtime.entrypoints.utils import (
MaterializedOutput,
post_process_sample,
save_materialized_output,
)
def _rgb_frame() -> np.ndarray:
@@ -66,3 +71,131 @@ def test_png_output_saving_uses_fast_pillow_path(
)
assert save_calls == [("PNG", expected_compress_level)]
def test_video_with_audio_uses_single_pass_encoder(tmp_path, monkeypatch):
output_path = tmp_path / "sample.mp4"
calls = []
class FakeWavFile:
@staticmethod
def write(*_args, **_kwargs):
pass
def mimsave_spy(path, frames, **kwargs):
calls.append((path, frames, kwargs))
assert kwargs["audio_path"].endswith(".wav")
assert kwargs["audio_codec"] == "aac"
def fail_legacy_mux(**_kwargs):
raise AssertionError("the two-pass mux path should not run")
monkeypatch.setattr(output_utils.imageio, "mimsave", mimsave_spy)
monkeypatch.setattr(output_utils, "scipy_wavfile", FakeWavFile)
monkeypatch.setattr(output_utils, "_maybe_mux_audio_into_mp4", fail_legacy_mux)
materialized = MaterializedOutput(
sample=None,
frames=[_rgb_frame()],
audio=np.zeros((320, 2), dtype=np.float32),
fps=24,
)
save_materialized_output(
materialized,
DataType.VIDEO,
str(output_path),
audio_sample_rate=32000,
)
assert len(calls) == 1
def test_video_audio_single_pass_failure_falls_back(tmp_path, monkeypatch):
output_path = tmp_path / "sample.mp4"
calls = []
mux_calls = []
class FakeWavFile:
@staticmethod
def write(*_args, **_kwargs):
pass
def mimsave_spy(path, frames, **kwargs):
calls.append((path, frames, kwargs))
if "audio_path" in kwargs:
raise RuntimeError("unsupported audio input")
monkeypatch.setattr(output_utils.imageio, "mimsave", mimsave_spy)
monkeypatch.setattr(output_utils, "scipy_wavfile", FakeWavFile)
monkeypatch.setattr(
output_utils,
"_maybe_mux_audio_into_mp4",
lambda **kwargs: mux_calls.append(kwargs),
)
materialized = MaterializedOutput(
sample=None,
frames=[_rgb_frame()],
audio=np.zeros((320, 2), dtype=np.float32),
fps=24,
)
save_materialized_output(
materialized,
DataType.VIDEO,
str(output_path),
audio_sample_rate=32000,
)
assert len(calls) == 2
assert "audio_path" in calls[0][2]
assert "audio_path" not in calls[1][2]
assert len(mux_calls) == 1
@pytest.mark.parametrize(
("height", "available_cpus", "expected_threads"),
[
(768, 256, 24),
(720, 256, 22),
(2160, 16, 24),
(4320, 256, 128),
(16, 1, 1),
],
)
def test_x264_auto_thread_count(monkeypatch, height, available_cpus, expected_threads):
monkeypatch.setattr(
output_utils.os,
"sched_getaffinity",
lambda _pid: set(range(available_cpus)),
)
assert output_utils._x264_auto_thread_count(height) == expected_threads
def test_video_direct_save_short_circuits_materialization(tmp_path, monkeypatch):
output_path = tmp_path / "sample.mp4"
direct_calls = []
monkeypatch.setattr(
output_utils,
"_try_save_cuda_video_direct",
lambda **kwargs: direct_calls.append(kwargs) or True,
)
monkeypatch.setattr(
output_utils,
"post_process_sample",
lambda *_args, **_kwargs: pytest.fail(
"successful direct save should skip frame materialization"
),
)
paths = output_utils.save_outputs(
[torch.zeros((3, 1, 2, 3))],
DataType.VIDEO,
fps=24,
save_output=True,
build_output_path=lambda _idx: str(output_path),
)
assert paths == [str(output_path)]
assert len(direct_calls) == 1