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

439 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
End-to-end tests for the unified quantization framework (PR #1764).
Validates FP8 quantization works correctly for all supported model types:
- Single-stage diffusion models (FLUX.1-dev, Qwen-Image, Z-Image-Turbo)
- Multi-stage models (BAGEL: LLM + Diffusion)
Tests verify:
1. FP8 quantization produces valid images
2. Memory usage is lower than BF16 baseline
3. Multi-stage models only quantize the diffusion stage (not the LLM stage)
Usage:
# Run all FP8 quantization tests
pytest tests/e2e/offline_inference/test_quantization_fp8.py -v
# Run single-stage tests only (faster, needs ~25GB VRAM)
pytest tests/e2e/offline_inference/test_quantization_fp8.py -v -k "single_stage"
# Run BAGEL multi-stage test (needs ~55GB VRAM, H100 recommended)
pytest tests/e2e/offline_inference/test_quantization_fp8.py -v -k "bagel"
# Run FLUX test only
pytest tests/e2e/offline_inference/test_quantization_fp8.py -v -k "flux"
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from typing import Any
import pytest
import torch
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
# ─── helpers ──────────────────────────────────────────────────────────────────
def _generate_single_stage_image(
model: str,
quantization: str | None = None,
height: int = 256,
width: int = 256,
num_inference_steps: int = 2,
seed: int = 42,
**extra_omni_kwargs: Any,
) -> tuple[list, float]:
"""Generate an image with a single-stage diffusion model.
Returns (images, peak_memory_gib).
"""
omni_kwargs: dict[str, Any] = dict(extra_omni_kwargs)
if quantization:
omni_kwargs["quantization"] = quantization
with OmniRunner(model, **omni_kwargs) as runner:
torch.accelerator.reset_peak_memory_stats()
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(seed)
outputs = runner.omni.generate(
"a photo of a cat sitting on a laptop keyboard",
OmniDiffusionSamplingParams(
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=0.0,
generator=generator,
),
)
peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
first_output = outputs[0]
assert first_output.final_output_type == "image"
if hasattr(first_output, "images") and first_output.images:
images = first_output.images
else:
assert hasattr(first_output, "request_output") and first_output.request_output
request_output = first_output.request_output
if isinstance(request_output, list):
req_out = request_output[0]
else:
req_out = request_output
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
images = req_out.images
assert len(images) >= 1
assert images[0].width == width
assert images[0].height == height
peak_mem_mb = getattr(first_output, "peak_memory_mb", None)
if peak_mem_mb is not None:
peak_mem = float(peak_mem_mb) / 1024.0
else:
peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
return images, peak_mem
def _generate_single_stage_video(
model: str,
quantization: str | None = None,
height: int = 256,
width: int = 256,
num_frames: int = 25,
num_inference_steps: int = 8,
guidance_scale: float = 4.0,
seed: int = 42,
prompt: str = "A serene lakeside sunrise with mist over the water",
**extra_omni_kwargs: Any,
) -> tuple[int, float]:
"""Generate a t2v output with a single-stage diffusion model.
Returns (num_frames_produced, peak_memory_gib)
"""
omni_kwargs: dict[str, Any] = dict(extra_omni_kwargs)
if quantization:
omni_kwargs["quantization"] = quantization
with OmniRunner(model, **omni_kwargs) as runner:
torch.accelerator.reset_peak_memory_stats()
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(seed)
outputs = runner.omni.generate(
{"prompt": prompt, "negative_prompt": ""},
OmniDiffusionSamplingParams(
height=height,
width=width,
num_frames=num_frames,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
generator=generator,
),
)
first = outputs[0]
# Unwrap pipeline-style outputs (multi-stage / OmniRequestOutput.request_output).
frames: Any = None
if hasattr(first, "request_output") and isinstance(first.request_output, list):
inner = first.request_output[0]
if isinstance(inner, OmniRequestOutput) and inner.images:
frames = inner.images[0]
if frames is None and hasattr(first, "images") and first.images:
frames = first.images[0]
assert frames is not None, "No video frames returned from generate()"
# LTX-2 (audio+video) may surface (video, audio) tuples or {"video": ...} dicts.
if isinstance(frames, dict):
frames = frames.get("video") or frames.get("frames")
elif isinstance(frames, tuple) and len(frames) == 2:
frames = frames[0]
assert frames is not None, "Could not extract video frames from output"
if isinstance(frames, torch.Tensor):
video = frames.detach().cpu()
if video.dim() == 5:
video = video[0]
if video.dim() == 4 and video.shape[0] in (3, 4):
video = video.permute(1, 2, 3, 0)
num_frames_produced = int(video.shape[0])
else:
import numpy as np
arr = np.asarray(frames)
if arr.ndim == 5:
arr = arr[0]
num_frames_produced = int(arr.shape[0])
peak_mem_mb = getattr(first, "peak_memory_mb", None)
if peak_mem_mb:
peak_mem = float(peak_mem_mb) / 1024.0
else:
peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
return num_frames_produced, peak_mem
def _generate_bagel_image(
quantization_config: str | None = None,
num_inference_steps: int = 15,
) -> tuple[Any, float]:
"""Generate an image with BAGEL (multi-stage: LLM + Diffusion).
Returns (generated_image, peak_memory_gib).
"""
config_path = get_deploy_config_path("ci/bagel.yaml")
omni_kwargs: dict[str, Any] = {
"model": "ByteDance-Seed/BAGEL-7B-MoT",
"stage_configs_path": config_path,
"stage_init_timeout": 300,
}
if quantization_config:
omni_kwargs["quantization_config"] = quantization_config
model_name = omni_kwargs.pop("model")
with OmniRunner(model_name, **omni_kwargs) as runner:
omni = runner.omni
torch.accelerator.reset_peak_memory_stats()
params_list = omni.default_sampling_params_list
if len(params_list) > 1:
params_list[1].num_inference_steps = num_inference_steps # type: ignore
params_list[1].extra_args = { # type: ignore
"cfg_text_scale": 4.0,
"cfg_img_scale": 1.5,
}
prompt = "<|im_start|>A futuristic city skyline at twilight, cyberpunk style<|im_end|>"
omni_outputs = list(
omni.generate(
prompts=[{"prompt": prompt, "modalities": ["image"]}],
sampling_params_list=params_list,
)
)
peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
# Extract image
generated_image = None
for req_output in omni_outputs:
if images := getattr(req_output, "images", None):
generated_image = images[0]
break
if hasattr(req_output, "request_output") and req_output.request_output:
stage_outputs = req_output.request_output
if not isinstance(stage_outputs, list):
stage_outputs = [stage_outputs]
for stage_out in stage_outputs:
if hasattr(stage_out, "images") and stage_out.images:
generated_image = stage_out.images[0]
break
if generated_image:
break
assert generated_image is not None, "No images generated from BAGEL"
assert generated_image.size == (1024, 1024), f"Expected 1024x1024, got {generated_image.size}"
# Check LLM stage output — should have finish_reason=stop (not length)
for req_output in omni_outputs:
if hasattr(req_output, "request_output") and req_output.request_output:
stage_outputs = req_output.request_output
if not isinstance(stage_outputs, list):
stage_outputs = [stage_outputs]
for stage_out in stage_outputs:
if hasattr(stage_out, "outputs"):
for comp_out in stage_out.outputs:
if hasattr(comp_out, "finish_reason"):
assert comp_out.finish_reason == "stop", (
f"LLM stage finish_reason={comp_out.finish_reason}, "
f"text={comp_out.text!r}. "
"FP8 may have leaked to the LLM stage."
)
return generated_image, peak_mem
# ─── Single-stage diffusion model tests ──────────────────────────────────────
@hardware_test(res={"cuda": "L4"})
def test_single_stage_zimage_fp8():
"""Z-Image-Turbo with FP8 generates valid images."""
images, _ = _generate_single_stage_image(
model="Tongyi-MAI/Z-Image-Turbo",
quantization="fp8",
)
assert len(images) >= 1
images[0].save("test_zimage_fp8.png")
@hardware_test(res={"cuda": "L4"})
def test_single_stage_zimage_fp8_uses_less_memory():
"""FP8 should use less peak memory than BF16 for Z-Image-Turbo."""
_, mem_bf16 = _generate_single_stage_image(
model="Tongyi-MAI/Z-Image-Turbo",
quantization=None,
)
torch.accelerator.empty_cache()
_, mem_fp8 = _generate_single_stage_image(
model="Tongyi-MAI/Z-Image-Turbo",
quantization="fp8",
)
print(f"Z-Image BF16 peak memory: {mem_bf16:.2f} GiB")
print(f"Z-Image FP8 peak memory: {mem_fp8:.2f} GiB")
assert mem_fp8 < mem_bf16, f"FP8 ({mem_fp8:.2f} GiB) should use less memory than BF16 ({mem_bf16:.2f} GiB)"
@hardware_test(res={"cuda": "L4"})
def test_single_stage_qwen_image_fp8():
"""Qwen-Image (random weights) with FP8 generates valid images."""
model = "riverclouds/qwen_image_random"
if current_omni_platform.is_npu() or current_omni_platform.is_rocm():
pytest.skip("qwen_image_random not available on this platform")
images, _ = _generate_single_stage_image(
model=model,
quantization="fp8",
)
assert len(images) >= 1
images[0].save("test_qwen_image_fp8.png")
@hardware_test(res={"cuda": "H100"})
@pytest.mark.skip(reason="This model is not authorized on Hugging Face Hub yet")
def test_single_stage_flux_fp8():
"""FLUX.1-dev with FP8 generates valid images."""
images, _ = _generate_single_stage_image(
model="black-forest-labs/FLUX.1-dev",
quantization="fp8",
height=512,
width=512,
num_inference_steps=4,
)
assert len(images) >= 1
images[0].save("test_flux_fp8.png")
@hardware_test(res={"cuda": "H100"})
@pytest.mark.skip(reason="This model is not authorized on Hugging Face Hub yet")
def test_single_stage_flux_fp8_uses_less_memory():
"""FP8 should use less peak memory than BF16 for FLUX.1-dev."""
_, mem_bf16 = _generate_single_stage_image(
model="black-forest-labs/FLUX.1-dev",
quantization=None,
height=512,
width=512,
num_inference_steps=4,
)
torch.accelerator.empty_cache()
_, mem_fp8 = _generate_single_stage_image(
model="black-forest-labs/FLUX.1-dev",
quantization="fp8",
height=512,
width=512,
num_inference_steps=4,
)
print(f"FLUX BF16 peak memory: {mem_bf16:.2f} GiB")
print(f"FLUX FP8 peak memory: {mem_fp8:.2f} GiB")
assert mem_fp8 < mem_bf16, f"FP8 ({mem_fp8:.2f} GiB) should use less memory than BF16 ({mem_bf16:.2f} GiB)"
@hardware_test(res={"cuda": "H100"})
def test_single_stage_ltx2_fp8_uses_less_memory():
"""FP8 should use less peak memory than BF16 for LTX-2."""
_, mem_bf16 = _generate_single_stage_video(
model="Lightricks/LTX-2",
quantization=None,
)
torch.accelerator.empty_cache()
_, mem_fp8 = _generate_single_stage_video(
model="Lightricks/LTX-2",
quantization="fp8",
)
print(f"LTX-2 BF16 peak memory: {mem_bf16:.2f} GiB")
print(f"LTX-2 FP8 peak memory: {mem_fp8:.2f} GiB")
assert mem_fp8 < mem_bf16, f"FP8 ({mem_fp8:.2f} GiB) should use less memory than BF16 ({mem_bf16:.2f} GiB)"
# ─── Multi-stage model tests (BAGEL) ─────────────────────────────────────────
@hardware_test(res={"cuda": "H100"})
def test_bagel_fp8_generates_image():
"""BAGEL with FP8 quantization_config generates a valid image.
FP8 should only apply to the diffusion stage (Stage-1), not the
LLM stage (Stage-0). We verify this by checking:
1. Image is generated successfully
2. LLM stage finish_reason is 'stop' (not 'length' from garbled output)
"""
image, _ = _generate_bagel_image(quantization_config="fp8")
image.save("test_bagel_fp8.png")
@hardware_test(res={"cuda": "H100"})
def test_bagel_bf16_generates_image():
"""BAGEL without quantization generates a valid image (baseline)."""
image, _ = _generate_bagel_image(quantization_config=None)
image.save("test_bagel_bf16.png")
# ─── Quantization config routing tests ────────────────────────────────────────
@hardware_test(res={"cuda": "L4"})
def test_quantization_key_maps_to_quantization_config():
"""The old 'quantization' kwarg should map to 'quantization_config'
in OmniDiffusionConfig.from_kwargs for backwards compatibility."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
config = OmniDiffusionConfig.from_kwargs(model="test", quantization="fp8")
assert config.quantization_config is not None
assert config.quantization_config.get_name() == "fp8"
@hardware_test(res={"cuda": "L4"})
def test_quantization_config_key_takes_priority():
"""When both 'quantization' and 'quantization_config' are set,
'quantization_config' takes priority."""
from vllm_omni.diffusion.data import OmniDiffusionConfig
config = OmniDiffusionConfig.from_kwargs(
model="test",
quantization="fp8",
quantization_config={"method": "fp8", "activation_scheme": "static"},
)
assert config.quantization_config is not None
assert config.quantization_config.activation_scheme == "static"
@hardware_test(res={"cuda": "L4"})
def test_single_stage_quantization_config_key():
"""Single-stage model using quantization_config (dict) key generates images."""
images, _ = _generate_single_stage_image(
model="Tongyi-MAI/Z-Image-Turbo",
quantization=None,
quantization_config="fp8",
)
assert len(images) >= 1