[diffusion] CI: set an explicit x264 preset for video output (#38657)
Co-authored-by: Mick Qian <mickqian@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Mick Qian
Claude Opus 5
parent
6d6d42d1c0
commit
492f346d88
@@ -320,6 +320,11 @@ def _resolve_ffmpeg_exe() -> str:
|
|||||||
return ffmpeg_exe
|
return ffmpeg_exe
|
||||||
|
|
||||||
|
|
||||||
|
# ffmpeg's implicit libx264 default is `medium`. On diffusion output `fast` is
|
||||||
|
# both quicker and measurably closer to the frames the model produced.
|
||||||
|
X264_PRESET = "fast"
|
||||||
|
|
||||||
|
|
||||||
def _x264_auto_thread_count(height: int) -> int:
|
def _x264_auto_thread_count(height: int) -> int:
|
||||||
"""Match x264's auto frame-thread count for progressive video."""
|
"""Match x264's auto frame-thread count for progressive video."""
|
||||||
try:
|
try:
|
||||||
@@ -427,6 +432,8 @@ def _try_save_cuda_video_direct(
|
|||||||
command += [
|
command += [
|
||||||
"-vcodec",
|
"-vcodec",
|
||||||
"libx264",
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
X264_PRESET,
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
"-crf",
|
"-crf",
|
||||||
@@ -735,6 +742,7 @@ def _try_save_video_with_audio(
|
|||||||
quality=quality,
|
quality=quality,
|
||||||
audio_path=tmp_wav_path,
|
audio_path=tmp_wav_path,
|
||||||
audio_codec="aac",
|
audio_codec="aac",
|
||||||
|
output_params=["-preset", X264_PRESET],
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -968,6 +976,7 @@ def save_materialized_output(
|
|||||||
format=output_format,
|
format=output_format,
|
||||||
codec="libx264",
|
codec="libx264",
|
||||||
quality=quality,
|
quality=quality,
|
||||||
|
output_params=["-preset", X264_PRESET],
|
||||||
)
|
)
|
||||||
|
|
||||||
_maybe_mux_audio_into_mp4(
|
_maybe_mux_audio_into_mp4(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ logger = init_logger(__name__)
|
|||||||
# NPU/ascend) is read from sgl-project/ci-data-diffusion, where the GT-gen workflows
|
# NPU/ascend) is read from sgl-project/ci-data-diffusion, where the GT-gen workflows
|
||||||
# publish.
|
# publish.
|
||||||
SGL_TEST_FILES_CI_DATA_REPO = "sgl-project/ci-data-diffusion"
|
SGL_TEST_FILES_CI_DATA_REPO = "sgl-project/ci-data-diffusion"
|
||||||
SGL_TEST_FILES_CI_DATA_REVISION = "4ce5eeb9606e378478b2d0964d83e960af4e88cf"
|
SGL_TEST_FILES_CI_DATA_REVISION = "0b9d7313c6bd31795fe6531a61ac45c89d9ed78e"
|
||||||
|
|
||||||
# The NPU pin is kept as a separate branch so ascend GT can be bumped independently
|
# The NPU pin is kept as a separate branch so ascend GT can be bumped independently
|
||||||
# when it's regenerated on its own cadence.
|
# when it's regenerated on its own cadence.
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""The configured x264 preset has to reach the encoder, not just the command.
|
||||||
|
|
||||||
|
libx264 records the options it resolved into the mp4 it writes, so a real encode
|
||||||
|
can be checked against a reference encode made with the preset spelled out. That
|
||||||
|
catches the failure this guards against -- the preset never being passed, and
|
||||||
|
ffmpeg silently applying its own default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.sample.sampling_params import DataType
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.utils import X264_PRESET, save_outputs
|
||||||
|
|
||||||
|
FPS = 8
|
||||||
|
FRAMES = 8
|
||||||
|
SIZE = 64
|
||||||
|
# Options libx264 derives from the preset alone, so a reference encode pins them
|
||||||
|
# without hard-coding values that move with the x264 build.
|
||||||
|
PRESET_DERIVED_KEYS = ("subme", "ref", "rc_lookahead", "me", "trellis")
|
||||||
|
|
||||||
|
|
||||||
|
def _x264_options(path) -> dict[str, str]:
|
||||||
|
"""The `options:` line libx264 embeds in its own output."""
|
||||||
|
blob = path.read_bytes()
|
||||||
|
start = blob.find(b"x264 - core")
|
||||||
|
assert start >= 0, "libx264 did not stamp its settings into the file"
|
||||||
|
text = blob[start : blob.find(b"\x00", start)].decode("utf-8", "replace")
|
||||||
|
_, _, options = text.partition("options: ")
|
||||||
|
return dict(kv.split("=", 1) for kv in options.split() if "=" in kv)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_encode(tmp_path, frames, preset):
|
||||||
|
out = tmp_path / f"ref_{preset}.mp4"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"ffmpeg",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"-pix_fmt",
|
||||||
|
"rgb24",
|
||||||
|
"-s",
|
||||||
|
f"{SIZE}x{SIZE}",
|
||||||
|
"-r",
|
||||||
|
str(FPS),
|
||||||
|
"-i",
|
||||||
|
"pipe:0",
|
||||||
|
"-vcodec",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
preset,
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
str(out),
|
||||||
|
],
|
||||||
|
input=frames.tobytes(),
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="needs ffmpeg")
|
||||||
|
def test_saved_video_carries_the_configured_preset(tmp_path):
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
frames = rng.integers(0, 256, (FRAMES, SIZE, SIZE, 3), dtype=np.uint8)
|
||||||
|
sample = torch.from_numpy(frames).permute(3, 0, 1, 2).float() / 255.0
|
||||||
|
|
||||||
|
saved = tmp_path / "clip.mp4"
|
||||||
|
paths = save_outputs([sample], DataType.VIDEO, FPS, True, lambda _idx: str(saved))
|
||||||
|
|
||||||
|
assert paths == [str(saved)] and saved.exists()
|
||||||
|
got = _x264_options(saved)
|
||||||
|
expected = _x264_options(_reference_encode(tmp_path, frames, X264_PRESET))
|
||||||
|
for key in PRESET_DERIVED_KEYS:
|
||||||
|
assert got.get(key) == expected.get(key), f"{key} does not match {X264_PRESET}"
|
||||||
|
|
||||||
|
if X264_PRESET != "medium":
|
||||||
|
# ffmpeg's implicit default, i.e. what a dropped -preset would give.
|
||||||
|
default = _x264_options(_reference_encode(tmp_path, frames, "medium"))
|
||||||
|
assert any(got.get(k) != default.get(k) for k in PRESET_DERIVED_KEYS), (
|
||||||
|
"encode is indistinguishable from ffmpeg's default preset"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="needs ffmpeg")
|
||||||
|
def test_saved_video_decodes_to_every_frame(tmp_path):
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
frames = rng.integers(0, 256, (FRAMES, SIZE, SIZE, 3), dtype=np.uint8)
|
||||||
|
sample = torch.from_numpy(frames).permute(3, 0, 1, 2).float() / 255.0
|
||||||
|
|
||||||
|
saved = tmp_path / "clip.mp4"
|
||||||
|
save_outputs([sample], DataType.VIDEO, FPS, True, lambda _idx: str(saved))
|
||||||
|
|
||||||
|
probe = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-count_frames",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=nb_read_frames,codec_name",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
str(saved),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
).stdout.strip()
|
||||||
|
codec, count = probe.split(",")
|
||||||
|
assert codec == "h264"
|
||||||
|
assert int(count) == FRAMES
|
||||||
Reference in New Issue
Block a user