[Feat]add cpu-offload/layerwise-offload for stable-audio-open & fix output inconsistency with same seed (#2909)
Signed-off-by: sphinxkkkbc <binchengkang8@gmail.com> Co-authored-by: sphinxkkkbc <binchengkang8@gmail.com>
This commit is contained in:
1 parent
f19891e4f1
commit
a0918ce583
7 files changed
+229
-40
No files matched your search
@@ -156,7 +156,7 @@ The following tables show which models support each feature:
|
||||
|
||||
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization | 🔄Step Execution |
|
||||
|-------|:----------:|:-----------:|:---------------------:|:--------------:|:-----------------:|:------:|:------------------------:|:--------------------:|:--------------:|:----------------:|
|
||||
| **Stable-Audio-Open** | ✅ | ❌ | ❓ | ❓ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **Stable-Audio-Open** | ✅ | ❌ | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
|
||||
|
||||
## Feature Compatibility
|
||||
|
||||
@@ -57,6 +57,8 @@ Key arguments:
|
||||
- `--hsdp-shard-size`: number of GPUs used for HSDP sharding.
|
||||
- `--hsdp-replicate-size`: number of HSDP replica groups.
|
||||
- `--output`: path to save the generated WAV file.
|
||||
- `--enable-cpu-offload`: enabling model-wise offloading to save gpu memory
|
||||
- `--enable-layerwise-offload`: enabling layerwise offloading to save gpu memory
|
||||
|
||||
## Example materials
|
||||
|
||||
|
||||
@@ -114,6 +114,12 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Enable diffusion pipeline profiler to display stage durations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-cpu-offload", action="store_true", help="Enable ModelWise cpu offloading to save gpu memory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-layerwise-offload", action="store_true", help="Enable Layerwise cpu offloading to save gpu memory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-hsdp",
|
||||
action="store_true",
|
||||
@@ -214,6 +220,8 @@ def main():
|
||||
print(f" Inference steps: {args.num_inference_steps}")
|
||||
print(f" Guidance scale: {args.guidance_scale}")
|
||||
print(f" Cache backend: {args.cache_backend if args.cache_backend else 'None (no acceleration)'}")
|
||||
print(f" ModelWise Offload: {'Enabled' if args.enable_cpu_offload else 'None'}")
|
||||
print(f" LayerWise Offload: {'Enabled' if args.enable_layerwise_offload else 'None'}")
|
||||
if args.use_hsdp:
|
||||
print(f" HSDP: enabled (shard_size={args.hsdp_shard_size}, replicate_size={args.hsdp_replicate_size})")
|
||||
else:
|
||||
@@ -234,6 +242,8 @@ def main():
|
||||
cache_backend=args.cache_backend,
|
||||
cache_config=cache_config,
|
||||
enable_diffusion_pipeline_profiler=args.enable_diffusion_pipeline_profiler,
|
||||
enable_cpu_offload=args.enable_cpu_offload,
|
||||
enable_layerwise_offload=args.enable_layerwise_offload,
|
||||
)
|
||||
|
||||
# Calculate audio end time
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import gc
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
@@ -10,7 +11,28 @@ from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
models = ["riverclouds/qwen_image_random"]
|
||||
AUDIO_MODEL = {
|
||||
"stabilityai/stable-audio-open-1.0": {"cuda": 100, "rocm": None},
|
||||
}
|
||||
|
||||
IMAGE_VIDEO_MODELS = {
|
||||
"riverclouds/qwen_image_random": {"cuda": 2500, "rocm": 2100},
|
||||
}
|
||||
|
||||
MODELS = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS}
|
||||
|
||||
AUDIO_MODEL_PARAMS = {
|
||||
"runner_params": {},
|
||||
"sampler_params": {},
|
||||
}
|
||||
|
||||
IMAGE_VIDEO_MODELS_PARAMS = {
|
||||
"runner_params": {},
|
||||
"sampler_params": {
|
||||
"height": 256,
|
||||
"width": 256,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def inference(model_name: str, offload: bool = True):
|
||||
@@ -19,25 +41,28 @@ def inference(model_name: str, offload: bool = True):
|
||||
device_index = current_omni_platform.current_device()
|
||||
current_omni_platform.reset_peak_memory_stats()
|
||||
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
|
||||
monitor.start()
|
||||
|
||||
if model_name in AUDIO_MODEL:
|
||||
params = AUDIO_MODEL_PARAMS
|
||||
else:
|
||||
params = IMAGE_VIDEO_MODELS_PARAMS
|
||||
|
||||
with OmniRunner(
|
||||
model_name,
|
||||
# TODO: we might want to add overlapped feature e2e tests
|
||||
# cache_backend="cache_dit",
|
||||
enable_cpu_offload=offload,
|
||||
**params["runner_params"],
|
||||
) as runner:
|
||||
current_omni_platform.reset_peak_memory_stats()
|
||||
height = 256
|
||||
width = 256
|
||||
|
||||
runner.omni.generate(
|
||||
monitor.start()
|
||||
output = runner.omni.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=9,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
|
||||
**params["sampler_params"],
|
||||
),
|
||||
)
|
||||
peak = monitor.peak_used_mb
|
||||
@@ -46,26 +71,51 @@ def inference(model_name: str, offload: bool = True):
|
||||
gc.collect()
|
||||
current_omni_platform.empty_cache()
|
||||
|
||||
return peak
|
||||
return peak, output
|
||||
|
||||
|
||||
def check_audio_determinism(audio1, audio2, atol=1e-2):
|
||||
device = current_omni_platform.device_type
|
||||
if isinstance(audio1, np.ndarray):
|
||||
audio1 = torch.from_numpy(audio1).to(device)
|
||||
if isinstance(audio2, np.ndarray):
|
||||
audio2 = torch.from_numpy(audio2).to(device)
|
||||
|
||||
if not torch.allclose(audio1, audio2, atol=atol):
|
||||
diff = torch.abs(audio1 - audio2)
|
||||
print(f"Max difference: {diff.max().item()}")
|
||||
print(f"Mean difference: {diff.mean().item()}")
|
||||
raise AssertionError(f"Audio outputs differ beyond tolerance atol={atol}")
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("model_name", list(MODELS.keys()))
|
||||
def test_cpu_offload_diffusion_model(model_name: str):
|
||||
try:
|
||||
offload_peak_memory = inference(model_name, offload=True)
|
||||
offload_peak_memory, output_offload = inference(model_name, offload=True)
|
||||
cleanup_dist_env_and_memory()
|
||||
no_offload_peak_memory = inference(model_name, offload=False)
|
||||
no_offload_peak_memory, output_no_offload = inference(model_name, offload=False)
|
||||
except Exception:
|
||||
pytest.fail("Inference failed")
|
||||
print(f"Offload peak memory: {offload_peak_memory} MB")
|
||||
print(f"No offload peak memory: {no_offload_peak_memory} MB")
|
||||
|
||||
if model_name == "stabilityai/stable-audio-open-1.0":
|
||||
audio_offload = output_offload[0].request_output.multimodal_output.get("audio")
|
||||
audio_no_offload = output_no_offload[0].request_output.multimodal_output.get("audio")
|
||||
check_audio_determinism(audio_offload, audio_no_offload, atol=1e-2)
|
||||
|
||||
# Set platform-specific VRAM saving thresholds to account
|
||||
# for varying runtime memory overhead and fragmentation between CUDA and ROCm.
|
||||
is_rocm = torch.version.hip is not None
|
||||
threshold = 2500 if not is_rocm else 2100
|
||||
platform = "rocm" if is_rocm else "cuda"
|
||||
threshold = MODELS[model_name][platform]
|
||||
if threshold is None:
|
||||
pytest.skip(f"Threshold not defined for {platform} on {model_name}")
|
||||
|
||||
assert offload_peak_memory + threshold < no_offload_peak_memory, (
|
||||
f"Offload peak memory {offload_peak_memory} MB should be less than "
|
||||
f"no offload peak memory {no_offload_peak_memory} MB by {threshold} MB"
|
||||
|
||||
@@ -2,15 +2,31 @@ import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
|
||||
from tests.e2e.offline_inference.test_diffusion_cpu_offload import check_audio_determinism
|
||||
from tests.helpers.env import DeviceMemoryMonitor
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
# Models to test and expected saved memory in MB, correspondingly
|
||||
MODELS_SAVED_MEMORY_MB = {
|
||||
"riverclouds/qwen_image_random": 4500,
|
||||
# "Wan-AI/Wan2.2-T2V-A14B-Diffusers": 45000,
|
||||
AUDIO_MODEL = {
|
||||
"stabilityai/stable-audio-open-1.0": {"cuda": 1500, "rocm": 1500},
|
||||
}
|
||||
|
||||
IMAGE_VIDEO_MODELS = {
|
||||
"riverclouds/qwen_image_random": {"cuda": 4500, "rocm": None},
|
||||
# "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {"cuda": 45000, "rocm": None},
|
||||
}
|
||||
|
||||
MODELS = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS}
|
||||
|
||||
AUDIO_MODEL_PARAMS = {
|
||||
"runner_params": {},
|
||||
"sampler_params": {},
|
||||
}
|
||||
|
||||
IMAGE_VIDEO_MODELS_PARAMS = {
|
||||
"runner_params": {"boundary_ratio": 0.875, "flow_shift": 5.0},
|
||||
"sampler_params": {"height": 480, "width": 640, "num_frames": 5},
|
||||
}
|
||||
|
||||
|
||||
@@ -24,41 +40,39 @@ def run_inference(
|
||||
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
|
||||
monitor.start()
|
||||
|
||||
if model_name in AUDIO_MODEL:
|
||||
params = AUDIO_MODEL_PARAMS
|
||||
else:
|
||||
params = IMAGE_VIDEO_MODELS_PARAMS
|
||||
|
||||
with OmniRunner(
|
||||
model_name,
|
||||
enable_layerwise_offload=layerwise_offload,
|
||||
# TODO: we might want to add overlapped feature e2e tests
|
||||
# cache_backend="cache_dit",
|
||||
boundary_ratio=0.875,
|
||||
flow_shift=5.0,
|
||||
**params["runner_params"],
|
||||
) as runner:
|
||||
current_omni_platform.reset_peak_memory_stats()
|
||||
|
||||
# Refer to tests/e2e/offline_inference/test_t2v_model.py
|
||||
# Use minimal settings for testing
|
||||
height = 480
|
||||
width = 640
|
||||
num_frames = 5
|
||||
|
||||
runner.omni.generate(
|
||||
output = runner.omni.generate(
|
||||
"A cat sitting on a table",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
|
||||
guidance_scale=1.0,
|
||||
num_inference_steps=num_inference_steps,
|
||||
num_frames=num_frames,
|
||||
**params["sampler_params"],
|
||||
),
|
||||
)
|
||||
|
||||
peak = monitor.peak_used_mb
|
||||
monitor.stop()
|
||||
|
||||
return peak
|
||||
return peak, output
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", MODELS_SAVED_MEMORY_MB.keys())
|
||||
@pytest.mark.parametrize("model_name", list(MODELS.keys()))
|
||||
def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
"""Test that layerwise offloading reduces GPU memory usage.
|
||||
|
||||
@@ -69,11 +83,11 @@ def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
"""
|
||||
try:
|
||||
# Run without layerwise offloading (baseline)
|
||||
no_offload_peak_memory = run_inference(model_name, layerwise_offload=False)
|
||||
no_offload_peak_memory, output_no_offload = run_inference(model_name, layerwise_offload=False)
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
# Run with layerwise offloading (1 layer on device)
|
||||
layerwise_offload_peak_memory = run_inference(model_name, layerwise_offload=True)
|
||||
layerwise_offload_peak_memory, output_offload = run_inference(model_name, layerwise_offload=True)
|
||||
cleanup_dist_env_and_memory()
|
||||
except Exception:
|
||||
pytest.fail("Inference failed")
|
||||
@@ -81,9 +95,21 @@ def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
print(f"Layerwise offload peak memory (1 GPU layer): {layerwise_offload_peak_memory} MB")
|
||||
print(f"No offload peak memory: {no_offload_peak_memory} MB")
|
||||
|
||||
if model_name == "stabilityai/stable-audio-open-1.0":
|
||||
audio_offload = output_offload[0].request_output.multimodal_output.get("audio")
|
||||
audio_no_offload = output_no_offload[0].request_output.multimodal_output.get("audio")
|
||||
check_audio_determinism(audio_offload, audio_no_offload, atol=1e-3)
|
||||
|
||||
is_rocm = torch.version.hip is not None
|
||||
platform = "rocm" if is_rocm else "cuda"
|
||||
expected_saved_memory = MODELS[model_name][platform]
|
||||
|
||||
if expected_saved_memory is None:
|
||||
pytest.skip(f"Threshold not defined for {platform} on {model_name}")
|
||||
|
||||
# Verify that layerwise offloading significantly reduces memory usage
|
||||
# Passes only if the actual savings exceeds the expected savings
|
||||
assert layerwise_offload_peak_memory + MODELS_SAVED_MEMORY_MB[model_name] < no_offload_peak_memory, (
|
||||
assert layerwise_offload_peak_memory + expected_saved_memory < no_offload_peak_memory, (
|
||||
f"Layerwise offload peak memory {layerwise_offload_peak_memory} MB "
|
||||
f"should be significantly less than no offload peak memory {no_offload_peak_memory} MB"
|
||||
)
|
||||
@@ -28,7 +28,10 @@ from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
from vllm_omni.diffusion.models.interface import SupportAudioOutput
|
||||
from vllm_omni.diffusion.models.stable_audio.stable_audio_transformer import StableAudioDiTModel
|
||||
from vllm_omni.diffusion.models.stable_audio.stable_audio_transformer import (
|
||||
StableAudioDiTModel,
|
||||
StableAudioSchedulerWrapper,
|
||||
)
|
||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs
|
||||
@@ -134,10 +137,12 @@ class StableAudioPipeline(nn.Module, SupportAudioOutput, DiffusionPipelineProfil
|
||||
self.transformer = StableAudioDiTModel(od_config=od_config, **transformer_kwargs)
|
||||
|
||||
# Load scheduler
|
||||
self.scheduler = CosineDPMSolverMultistepScheduler.from_pretrained(
|
||||
model,
|
||||
subfolder="scheduler",
|
||||
local_files_only=local_files_only,
|
||||
self.scheduler = StableAudioSchedulerWrapper(
|
||||
CosineDPMSolverMultistepScheduler.from_pretrained(
|
||||
model,
|
||||
subfolder="scheduler",
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
)
|
||||
|
||||
# Compute rotary embedding dimension
|
||||
@@ -560,7 +565,7 @@ class StableAudioPipeline(nn.Module, SupportAudioOutput, DiffusionPipelineProfil
|
||||
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
|
||||
|
||||
# Scheduler step
|
||||
latents = self.scheduler.step(noise_pred, t, latents).prev_sample
|
||||
latents = self.scheduler.step(noise_pred, t, latents, generator).prev_sample
|
||||
|
||||
self._current_timestep = None
|
||||
|
||||
|
||||
@@ -22,6 +22,19 @@ from vllm_omni.diffusion.layers.fourier import GaussianFourierProjection
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _preprocess_stable_audio_weight(
|
||||
param: torch.Tensor,
|
||||
loaded_weight: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if param.shape == loaded_weight.shape:
|
||||
return loaded_weight
|
||||
|
||||
if loaded_weight.ndim + 1 == param.ndim and param.shape[-1] == 1 and loaded_weight.shape == param.shape[:-1]:
|
||||
return loaded_weight.unsqueeze(-1)
|
||||
|
||||
return loaded_weight
|
||||
|
||||
|
||||
def apply_rotary_emb_stable_audio(
|
||||
hidden_states: torch.Tensor,
|
||||
freqs_cis: tuple[torch.Tensor, torch.Tensor],
|
||||
@@ -56,6 +69,84 @@ def apply_rotary_emb_stable_audio(
|
||||
return torch.cat([x_rot, x_pass], dim=-1)
|
||||
|
||||
|
||||
class StableAudioSchedulerWrapper:
|
||||
def __init__(self, scheduler):
|
||||
self.scheduler = scheduler
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.scheduler, name)
|
||||
|
||||
def _get_step_index(self, timestep):
|
||||
step_index = getattr(self.scheduler, "step_index", None)
|
||||
if step_index is not None:
|
||||
return step_index
|
||||
|
||||
timesteps = getattr(self.scheduler, "timesteps", None)
|
||||
if timesteps is None:
|
||||
return None
|
||||
|
||||
timestep = torch.as_tensor(
|
||||
timestep,
|
||||
device=timesteps.device,
|
||||
dtype=timesteps.dtype,
|
||||
)
|
||||
indices = (timesteps == timestep).nonzero()
|
||||
if len(indices) == 0:
|
||||
return None
|
||||
|
||||
return indices[0].item()
|
||||
|
||||
def _is_final_zero_sigma_step(self, timestep):
|
||||
step_index = self._get_step_index(timestep)
|
||||
if step_index is None:
|
||||
return False
|
||||
|
||||
sigmas = self.scheduler.sigmas
|
||||
if step_index + 1 >= len(sigmas):
|
||||
return False
|
||||
|
||||
sigma = sigmas[step_index]
|
||||
next_sigma = sigmas[step_index + 1]
|
||||
sigma_min = torch.as_tensor(
|
||||
self.scheduler.config.sigma_min,
|
||||
device=sigma.device,
|
||||
dtype=sigma.dtype,
|
||||
)
|
||||
|
||||
return torch.isclose(sigma, sigma_min) and torch.isclose(
|
||||
next_sigma,
|
||||
torch.zeros_like(next_sigma),
|
||||
)
|
||||
|
||||
def step(self, model_output, timestep, sample, generator=None, return_dict=True):
|
||||
use_zero_noise = self._is_final_zero_sigma_step(timestep)
|
||||
old_noise_sampler = None
|
||||
|
||||
if use_zero_noise:
|
||||
old_noise_sampler = getattr(self.scheduler, "noise_sampler", None)
|
||||
self.scheduler.noise_sampler = _StableAudioZeroNoiseSampler(sample)
|
||||
|
||||
try:
|
||||
return self.scheduler.step(
|
||||
model_output,
|
||||
timestep,
|
||||
sample,
|
||||
generator=generator,
|
||||
return_dict=return_dict,
|
||||
)
|
||||
finally:
|
||||
if use_zero_noise:
|
||||
self.scheduler.noise_sampler = old_noise_sampler
|
||||
|
||||
|
||||
class _StableAudioZeroNoiseSampler:
|
||||
def __init__(self, sample: torch.Tensor):
|
||||
self.sample = sample
|
||||
|
||||
def __call__(self, sigma, next_sigma):
|
||||
return torch.zeros_like(self.sample)
|
||||
|
||||
|
||||
class StableAudioGaussianFourierProjection(GaussianFourierProjection):
|
||||
"""Gaussian Fourier embeddings for noise levels.
|
||||
|
||||
@@ -374,6 +465,7 @@ class StableAudioDiTModel(nn.Module):
|
||||
"""
|
||||
|
||||
_repeated_blocks = ["StableAudioDiTBlock"]
|
||||
_layerwise_offload_blocks_attrs = ["transformer_blocks"]
|
||||
_hsdp_shard_conditions = [is_transformer_block_module]
|
||||
|
||||
def __init__(
|
||||
@@ -594,8 +686,12 @@ class StableAudioDiTModel(nn.Module):
|
||||
|
||||
if mapped_name in params_dict:
|
||||
param = params_dict[mapped_name]
|
||||
loaded_weight = _preprocess_stable_audio_weight(param, loaded_weight)
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
try:
|
||||
weight_loader(param, loaded_weight)
|
||||
except AssertionError as err:
|
||||
raise AssertionError(f"Failed to load Stable Audio weight {name!r} as {mapped_name!r}") from err
|
||||
loaded_params.add(mapped_name)
|
||||
else:
|
||||
logger.debug(f"Skipping weight {name} - not found in model")
|
||||
|
||||
Reference in new issue
Block a user