项目文件夹

文件
wehub-resource-sync eec33d25b2
Build Wheel / build (3.11) (push) Failing after 1s
Build Wheel / build (3.12) (push) Failing after 0s
pre-commit / pre-commit (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:29:08 +08:00

274 行
9.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""End-to-end ModelOpt FP8 quality accuracy for video-gen DiTs.
Modeled on ``test_hunyuan_image3.py``'s ``test_quantized_dit_matches_bf16_accuracy``:
for each calibrated FP8 checkpoint the test generates one video with BF16 and one
with the FP8 DiT under the same seed, scores prompt-faithfulness with CLIP on the
middle frame, then asserts both ``CLIP >= absolute_floor`` and
``CLIP_drop <= drop_threshold``. The test is gated by an opt-in env var (model
paths are local) so it never runs in generic CI.
To run::
VIDEOGEN_RUN_QUANT_ACCURACY=1 \\
WAN22_A14B_BF16_MODEL=Wan-AI/Wan2.2-T2V-A14B-Diffusers \\
WAN22_A14B_FP8_MODEL=/path/to/wan22-a14b-modelopt-fp8 \\
HV15_BF16_MODEL=hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v \\
HV15_FP8_MODEL=/path/to/hv15-modelopt-fp8 \\
pytest -s -v tests/e2e/accuracy/test_videogen_modelopt_quant.py
"""
from __future__ import annotations
import gc
import json
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
import pytest
import torch
from PIL import Image
from tests.e2e.accuracy.helpers import CLIPScorer, model_output_dir
pytestmark = [pytest.mark.local_model, pytest.mark.diffusion]
# ---------------------------------------------------------------------------
# Tunables (override via env to suit local hardware / calibration budget)
# ---------------------------------------------------------------------------
SEED = 42
HEIGHT = int(os.environ.get("VIDEOGEN_QUANT_HEIGHT", "480"))
WIDTH = int(os.environ.get("VIDEOGEN_QUANT_WIDTH", "832"))
NUM_FRAMES = int(os.environ.get("VIDEOGEN_QUANT_NUM_FRAMES", "17"))
NUM_INFERENCE_STEPS = int(os.environ.get("VIDEOGEN_QUANT_STEPS", "20"))
WAN_PROMPT = "A red fox running through a snowy forest at sunrise, cinematic."
HV_PROMPT = "A red fox running through a snowy forest at sunrise, cinematic."
# CLIP gates: absolute floor + max drop from the BF16 reference.
CLIP_ABSOLUTE_FLOOR = float(os.environ.get("VIDEOGEN_QUANT_CLIP_FLOOR", "20.0"))
CLIP_DROP_FP8 = float(os.environ.get("VIDEOGEN_QUANT_CLIP_DROP_FP8", "7.0"))
RUN_ENV = "VIDEOGEN_RUN_QUANT_ACCURACY"
_TRUE = {"1", "true", "yes", "on"}
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class _Spec:
"""A model under test (BF16 + its FP8 variant)."""
family: str
bf16_env: str
fp8_env: str
prompt: str
guidance_scale: float
guidance_scale_2: float | None = None
boundary_ratio: float | None = None
WAN_SPEC = _Spec(
family="wan22_a14b_t2v",
bf16_env="WAN22_A14B_BF16_MODEL",
fp8_env="WAN22_A14B_FP8_MODEL",
prompt=WAN_PROMPT,
guidance_scale=3.5,
guidance_scale_2=4.0,
boundary_ratio=0.875,
)
HV_SPEC = _Spec(
family="hunyuanvideo15_t2v",
bf16_env="HV15_BF16_MODEL",
fp8_env="HV15_FP8_MODEL",
prompt=HV_PROMPT,
guidance_scale=6.0,
)
@dataclass(frozen=True)
class _QuantCase:
name: str
spec: _Spec
clip_drop: float
def _cases() -> list[pytest.ParameterSet]:
cases = [
_QuantCase("wan_fp8", WAN_SPEC, CLIP_DROP_FP8),
_QuantCase("hv_fp8", HV_SPEC, CLIP_DROP_FP8),
]
params: list[pytest.ParameterSet] = []
opted_in = os.environ.get(RUN_ENV, "").lower() in _TRUE
for case in cases:
marks: list[Any] = []
if not opted_in:
marks.append(pytest.mark.skip(reason=f"Set {RUN_ENV}=1 to run video-gen quant accuracy."))
if not os.environ.get(case.spec.bf16_env):
marks.append(pytest.mark.skip(reason=f"Set {case.spec.bf16_env} to run {case.name}."))
if not os.environ.get(case.spec.fp8_env):
marks.append(pytest.mark.skip(reason=f"Set {case.spec.fp8_env} to run {case.name}."))
params.append(pytest.param(case, id=case.name, marks=marks))
return params
# ---------------------------------------------------------------------------
def _middle_frame(frames: list[Image.Image] | list[np.ndarray] | np.ndarray) -> Image.Image:
if isinstance(frames, np.ndarray):
idx = frames.shape[0] // 2
arr = frames[idx]
if arr.dtype != np.uint8:
arr = (arr * 255).clip(0, 255).astype(np.uint8)
return Image.fromarray(arr).convert("RGB")
if not frames:
raise AssertionError("Empty video output")
mid = frames[len(frames) // 2]
if isinstance(mid, np.ndarray):
if mid.dtype != np.uint8:
mid = (mid * 255).clip(0, 255).astype(np.uint8)
return Image.fromarray(mid).convert("RGB")
return mid.convert("RGB")
def _extract_frames(result) -> list[Image.Image]:
"""Pull a frame list out of an Omni text-to-video result."""
from vllm_omni.entrypoints.omni import OmniRequestOutput
if isinstance(result, list):
result = result[0] if result else None
if isinstance(result, OmniRequestOutput) and result.is_pipeline_output and result.request_output is not None:
result = result.request_output
images = getattr(result, "images", None)
if not images:
raise AssertionError("Pipeline output had no images")
if isinstance(images, list) and images and isinstance(images[0], tuple):
# (frames, fps)
images = images[0][0]
return list(images)
def _generate_video(
*,
model: str,
spec: _Spec,
quantization: str | None,
output_path: Path,
) -> tuple[Image.Image, float]:
"""Generate one video and return its middle frame + elapsed wall time."""
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform
kwargs: dict[str, Any] = dict(
model=model,
parallel_config=DiffusionParallelConfig(),
enforce_eager=True,
vae_use_tiling=True,
)
if quantization:
kwargs["quantization"] = quantization
if spec.boundary_ratio is not None:
kwargs["boundary_ratio"] = spec.boundary_ratio
omni = Omni(**kwargs)
try:
gen = torch.Generator(device=current_omni_platform.device_type).manual_seed(SEED)
sampling = dict(
height=HEIGHT,
width=WIDTH,
generator=gen,
guidance_scale=spec.guidance_scale,
num_inference_steps=NUM_INFERENCE_STEPS,
num_frames=NUM_FRAMES,
seed=SEED,
)
if spec.guidance_scale_2 is not None:
sampling["guidance_scale_2"] = spec.guidance_scale_2
import time as _time
t0 = _time.perf_counter()
out = omni.generate({"prompt": spec.prompt}, OmniDiffusionSamplingParams(**sampling))
elapsed = _time.perf_counter() - t0
frames = _extract_frames(out)
mid = _middle_frame(frames)
mid.save(output_path)
return mid, elapsed
finally:
del omni
gc.collect()
if torch.cuda.is_available():
torch.accelerator.empty_cache()
@pytest.mark.parametrize("case", _cases())
@pytest.mark.skipif(
not torch.cuda.is_available() or torch.accelerator.device_count() < 1,
reason="Needs at least 1 GPU.",
)
def test_quantized_videogen_dit_matches_bf16_clip(case: _QuantCase, accuracy_artifact_root: Path) -> None:
"""FP8 DiT should preserve prompt-aligned video quality vs BF16."""
out_dir = model_output_dir(accuracy_artifact_root, case.spec.family + "-quant")
with tempfile.TemporaryDirectory():
bf16_model = os.environ[case.spec.bf16_env]
quant_model = os.environ[case.spec.fp8_env]
bf16_frame, bf16_time = _generate_video(
model=bf16_model,
spec=case.spec,
quantization=None,
output_path=out_dir / "bf16.png",
)
quant_frame, quant_time = _generate_video(
model=quant_model,
spec=case.spec,
quantization="fp8",
output_path=out_dir / f"{case.name}.png",
)
clip = CLIPScorer()
bf16_clip = clip.score(bf16_frame, case.spec.prompt)
quant_clip = clip.score(quant_frame, case.spec.prompt)
clip_drop = bf16_clip - quant_clip
speedup = (bf16_time / quant_time) if quant_time else float("nan")
metrics = {
"case": case.name,
"family": case.spec.family,
"quantization": "fp8",
"bf16_model": bf16_model,
"quant_model": quant_model,
"prompt": case.spec.prompt,
"height": HEIGHT,
"width": WIDTH,
"num_frames": NUM_FRAMES,
"num_inference_steps": NUM_INFERENCE_STEPS,
"bf16_elapsed_s": bf16_time,
"quant_elapsed_s": quant_time,
"speedup_vs_bf16": speedup,
"bf16_clip_score": bf16_clip,
"quant_clip_score": quant_clip,
"clip_score_drop": clip_drop,
}
metrics_path = out_dir / f"{case.name}_metrics.json"
metrics_path.write_text(json.dumps(metrics, indent=2, sort_keys=True), encoding="utf-8")
print(f"\n[{case.name}] BF16 {bf16_time:.2f}s | FP8 {quant_time:.2f}s (x{speedup:.2f})")
print(f" CLIP BF16={bf16_clip:.2f} FP8={quant_clip:.2f} drop={clip_drop:.2f}")
print(f" metrics={metrics_path}")
assert quant_clip >= CLIP_ABSOLUTE_FLOOR, (
f"{case.name} CLIP below floor: got {quant_clip:.2f}, expected >= {CLIP_ABSOLUTE_FLOOR:.2f}"
)
assert clip_drop <= case.clip_drop, (
f"{case.name} CLIP drop too large: got {clip_drop:.2f}, expected <= {case.clip_drop:.2f}"
)