2026-08-25 09:10:17 +08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
import concurrent.futures
|
2026-08-28 14:34:11 +08:00
|
|
|
import gc
|
2026-09-10 02:58:24 -07:00
|
|
|
import multiprocessing as mp
|
|
|
|
|
import os
|
2026-08-25 09:10:17 +08:00
|
|
|
from typing import Any
|
|
|
|
|
|
2026-05-08 14:12:23 +08:00
|
|
|
import numpy as np
|
2026-01-30 17:35:09 +08:00
|
|
|
import pytest
|
|
|
|
|
import torch
|
|
|
|
|
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
|
|
|
|
|
2026-09-14 14:27:10 +08:00
|
|
|
from tests.helpers.mark import hardware_marks
|
2026-08-25 09:10:17 +08:00
|
|
|
from tests.helpers.monitor import DeviceMemoryMonitor
|
2026-04-20 14:23:42 +08:00
|
|
|
from tests.helpers.runtime import OmniRunner
|
2026-01-30 17:35:09 +08:00
|
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
2026-02-27 00:52:15 -06:00
|
|
|
from vllm_omni.platforms import current_omni_platform
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-08-25 09:10:17 +08:00
|
|
|
AUDIO_MODEL: dict[str, dict[str, int | None]] = {
|
2026-08-28 14:34:11 +08:00
|
|
|
# The inference peak includes backend workspaces as well as model weights.
|
|
|
|
|
# On ROCm, large MIOpen workspaces and allocator fragmentation can mask the
|
|
|
|
|
# resident-weight reduction, so use a conservative floor that still catches
|
|
|
|
|
# a disabled/no-op layerwise offloader.
|
|
|
|
|
"stabilityai/stable-audio-open-1.0": {"cuda": 1500, "rocm": 512},
|
2026-05-05 22:49:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-25 09:10:17 +08:00
|
|
|
IMAGE_VIDEO_MODELS: dict[str, dict[str, int | None]] = {
|
2026-05-05 22:49:57 +08:00
|
|
|
"riverclouds/qwen_image_random": {"cuda": 4500, "rocm": None},
|
|
|
|
|
# "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {"cuda": 45000, "rocm": None},
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 09:10:17 +08:00
|
|
|
MODELS: dict[str, dict[str, int | None]] = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS}
|
2026-05-05 22:49:57 +08:00
|
|
|
|
2026-08-12 22:58:23 +08:00
|
|
|
MODEL_MARKS = {
|
2026-09-14 14:27:10 +08:00
|
|
|
"riverclouds/qwen_image_random": [
|
|
|
|
|
pytest.mark.core_model,
|
|
|
|
|
*hardware_marks(res={"cuda": "L4", "rocm": "MI325"}),
|
|
|
|
|
],
|
|
|
|
|
"stabilityai/stable-audio-open-1.0": [
|
|
|
|
|
pytest.mark.full_model,
|
|
|
|
|
*hardware_marks(res={"cuda": ["L4", "B200"], "rocm": "MI325"}),
|
|
|
|
|
],
|
2026-08-12 22:58:23 +08:00
|
|
|
}
|
2026-07-29 23:53:03 +08:00
|
|
|
|
2026-08-25 09:10:17 +08:00
|
|
|
AUDIO_MODEL_PARAMS: dict[str, dict[str, Any]] = {
|
2026-05-05 22:49:57 +08:00
|
|
|
"runner_params": {},
|
|
|
|
|
"sampler_params": {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 09:10:17 +08:00
|
|
|
IMAGE_VIDEO_MODELS_PARAMS: dict[str, dict[str, Any]] = {
|
2026-05-05 22:49:57 +08:00
|
|
|
"runner_params": {"boundary_ratio": 0.875, "flow_shift": 5.0},
|
|
|
|
|
"sampler_params": {"height": 480, "width": 640, "num_frames": 5},
|
2026-02-12 17:29:55 +08:00
|
|
|
}
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
_OFFLOAD_STATE_PROBE = "vllm_omni_layerwise_offload_test"
|
|
|
|
|
# A second CI build can reverse the same exact-head measurement without a
|
|
|
|
|
# source edit; the default keeps the existing baseline-first workload.
|
|
|
|
|
_MEASUREMENT_ORDER_ENV = "VLLM_OMNI_OFFLOAD_TEST_ORDER"
|
|
|
|
|
_MEASUREMENT_ORDERS: dict[str, tuple[bool, bool]] = {
|
|
|
|
|
"baseline-first": (False, True),
|
|
|
|
|
"offload-first": (True, False),
|
|
|
|
|
}
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-05-08 14:12:23 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
class OffloadStateProbe:
|
|
|
|
|
"""Test-only worker extension exposing the configured offloader state."""
|
|
|
|
|
|
|
|
|
|
model_runner: Any
|
|
|
|
|
|
|
|
|
|
def get_offload_state_for_test(self) -> dict[str, Any]:
|
|
|
|
|
backend = getattr(self.model_runner, "offload_backend", None)
|
|
|
|
|
block_groups = getattr(backend, "_blocks", ())
|
|
|
|
|
group_sizes = [len(group) for group in block_groups] if isinstance(block_groups, (list, tuple)) else []
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"probe": _OFFLOAD_STATE_PROBE,
|
|
|
|
|
"backend_type": (
|
|
|
|
|
None if backend is None else f"{backend.__class__.__module__}.{backend.__class__.__qualname__}"
|
|
|
|
|
),
|
|
|
|
|
"requested": bool(getattr(self.model_runner.od_config, "enable_layerwise_offload", False)),
|
|
|
|
|
"enabled": bool(backend is not None and backend.is_enabled()),
|
|
|
|
|
"block_group_count": len(group_sizes),
|
|
|
|
|
"block_group_sizes": group_sizes,
|
|
|
|
|
"block_count": sum(group_sizes),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_audio_determinism(audio1: np.ndarray, audio2: np.ndarray, atol: float = 1e-2) -> bool:
|
|
|
|
|
if not np.allclose(audio1, audio2, atol=atol):
|
|
|
|
|
diff = np.abs(audio1 - audio2)
|
|
|
|
|
print(f"Max difference: {diff.max()}")
|
|
|
|
|
print(f"Mean difference: {diff.mean()}")
|
2026-05-08 14:12:23 +08:00
|
|
|
raise AssertionError(f"Audio outputs differ beyond tolerance atol={atol}")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
def _device_used_mb(device_index: int) -> float:
|
|
|
|
|
current_omni_platform.synchronize()
|
|
|
|
|
with current_omni_platform.device(device_index):
|
|
|
|
|
free_bytes, total_bytes = current_omni_platform.mem_get_info()
|
|
|
|
|
return (total_bytes - free_bytes) / (1024**2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_audio(output: Any) -> np.ndarray | None:
|
|
|
|
|
if not output:
|
|
|
|
|
return None
|
|
|
|
|
multimodal_output = getattr(output[0], "multimodal_output", None)
|
|
|
|
|
if not isinstance(multimodal_output, dict):
|
|
|
|
|
return None
|
|
|
|
|
audio = multimodal_output.get("audio")
|
|
|
|
|
if audio is None:
|
|
|
|
|
return None
|
|
|
|
|
if isinstance(audio, torch.Tensor):
|
|
|
|
|
return audio.detach().cpu().numpy()
|
|
|
|
|
return np.asarray(audio)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _collect_offload_states(value: Any) -> list[dict[str, Any]]:
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
return [value] if value.get("probe") == _OFFLOAD_STATE_PROBE else []
|
|
|
|
|
if isinstance(value, (list, tuple)):
|
|
|
|
|
states: list[dict[str, Any]] = []
|
|
|
|
|
for item in value:
|
|
|
|
|
states.extend(_collect_offload_states(item))
|
|
|
|
|
return states
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _measurement_order() -> tuple[bool, bool]:
|
|
|
|
|
"""Return the requested order without changing the default CI workload."""
|
|
|
|
|
configured = os.environ.get(_MEASUREMENT_ORDER_ENV, "baseline-first")
|
|
|
|
|
try:
|
|
|
|
|
return _MEASUREMENT_ORDERS[configured]
|
|
|
|
|
except KeyError as exc:
|
|
|
|
|
allowed = ", ".join(_MEASUREMENT_ORDERS)
|
|
|
|
|
raise ValueError(f"{_MEASUREMENT_ORDER_ENV} must be one of: {allowed}; got {configured!r}") from exc
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 17:35:09 +08:00
|
|
|
def run_inference(
|
|
|
|
|
model_name: str,
|
|
|
|
|
layerwise_offload: bool = False,
|
|
|
|
|
num_inference_steps: int = 3,
|
2026-09-10 02:58:24 -07:00
|
|
|
) -> dict[str, Any]:
|
2026-02-27 00:52:15 -06:00
|
|
|
current_omni_platform.empty_cache()
|
2026-03-10 23:19:36 -05:00
|
|
|
device_index = current_omni_platform.current_device()
|
2026-09-10 02:58:24 -07:00
|
|
|
initial_used_mb = _device_used_mb(device_index)
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-05-05 22:49:57 +08:00
|
|
|
if model_name in AUDIO_MODEL:
|
|
|
|
|
params = AUDIO_MODEL_PARAMS
|
|
|
|
|
else:
|
|
|
|
|
params = IMAGE_VIDEO_MODELS_PARAMS
|
|
|
|
|
|
2026-04-13 14:47:55 +08:00
|
|
|
with OmniRunner(
|
|
|
|
|
model_name,
|
2026-01-30 17:35:09 +08:00
|
|
|
enable_layerwise_offload=layerwise_offload,
|
2026-09-10 02:58:24 -07:00
|
|
|
worker_extension_cls=f"{OffloadStateProbe.__module__}.{OffloadStateProbe.__qualname__}",
|
2026-03-09 22:25:41 +08:00
|
|
|
# TODO: we might want to add overlapped feature e2e tests
|
|
|
|
|
# cache_backend="cache_dit",
|
2026-05-05 22:49:57 +08:00
|
|
|
**params["runner_params"],
|
2026-04-13 14:47:55 +08:00
|
|
|
) as runner:
|
2026-09-10 02:58:24 -07:00
|
|
|
offload_states = _collect_offload_states(
|
|
|
|
|
runner.omni.engine.collective_rpc(method="get_offload_state_for_test", timeout=60)
|
|
|
|
|
)
|
|
|
|
|
if not offload_states:
|
|
|
|
|
raise AssertionError("The offload-state worker probe returned no diffusion worker results")
|
|
|
|
|
|
2026-08-28 14:34:11 +08:00
|
|
|
# Measure steady-state inference memory, not model construction. Enabling
|
|
|
|
|
# layerwise offload first loads the model and then replaces each block's
|
|
|
|
|
# device storage with CPU-backed weights. Monitoring that transition
|
|
|
|
|
# captures both the original model and temporary staging allocations,
|
|
|
|
|
# which is not representative of layerwise-offloaded inference.
|
|
|
|
|
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
|
2026-04-13 14:47:55 +08:00
|
|
|
current_omni_platform.reset_peak_memory_stats()
|
2026-08-28 14:34:11 +08:00
|
|
|
monitor.start()
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-08-28 14:34:11 +08:00
|
|
|
try:
|
|
|
|
|
# Refer to tests/e2e/offline_inference/test_wan22.py
|
|
|
|
|
# Use minimal settings for testing
|
|
|
|
|
output = runner.omni.generate(
|
|
|
|
|
"A cat sitting on a table",
|
|
|
|
|
OmniDiffusionSamplingParams(
|
|
|
|
|
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
|
|
|
|
|
guidance_scale=1.0,
|
|
|
|
|
num_inference_steps=num_inference_steps,
|
|
|
|
|
**params["sampler_params"],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
monitor.stop()
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
audio = _extract_audio(output)
|
|
|
|
|
del output
|
|
|
|
|
|
2026-08-28 14:34:11 +08:00
|
|
|
# DeviceMemoryMonitor reports absolute device usage. Subtract this run's
|
2026-09-10 02:58:24 -07:00
|
|
|
# starting usage. Each mode runs in its own spawned process below, so model
|
|
|
|
|
# objects and process-local allocator/compiler/backend workspace state cannot
|
|
|
|
|
# carry into the other measurement. Shared filesystem caches can persist;
|
|
|
|
|
# _MEASUREMENT_ORDER_ENV enables a reversed-order validation run for that.
|
|
|
|
|
peak_used_mb = monitor.peak_used_mb
|
|
|
|
|
incremental_peak_mb = max(0.0, peak_used_mb - initial_used_mb)
|
2026-08-28 14:34:11 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
del runner
|
2026-08-28 14:34:11 +08:00
|
|
|
gc.collect()
|
2026-09-10 02:58:24 -07:00
|
|
|
cleanup_dist_env_and_memory()
|
2026-08-28 14:34:11 +08:00
|
|
|
current_omni_platform.empty_cache()
|
2026-09-10 02:58:24 -07:00
|
|
|
post_cleanup_used_mb = _device_used_mb(device_index)
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
return {
|
|
|
|
|
"initial_used_mb": initial_used_mb,
|
|
|
|
|
"peak_used_mb": peak_used_mb,
|
|
|
|
|
"incremental_peak_mb": incremental_peak_mb,
|
|
|
|
|
"post_cleanup_used_mb": post_cleanup_used_mb,
|
|
|
|
|
"audio": audio,
|
|
|
|
|
"offload_states": offload_states,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_inference_isolated(
|
|
|
|
|
model_name: str,
|
|
|
|
|
layerwise_offload: bool = False,
|
|
|
|
|
num_inference_steps: int = 3,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Run one measurement in a fresh interpreter and accelerator context."""
|
|
|
|
|
with concurrent.futures.ProcessPoolExecutor(max_workers=1, mp_context=mp.get_context("spawn")) as executor:
|
|
|
|
|
return executor.submit(run_inference, model_name, layerwise_offload, num_inference_steps).result()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert_offload_state(measurement: dict[str, Any], *, expected_enabled: bool) -> None:
|
|
|
|
|
states = measurement["offload_states"]
|
|
|
|
|
assert states, "Expected at least one offload-state result"
|
|
|
|
|
for state in states:
|
|
|
|
|
assert state["requested"] is expected_enabled, f"Unexpected offload request state: {state}"
|
|
|
|
|
assert state["enabled"] is expected_enabled, f"Unexpected offload state: {state}"
|
|
|
|
|
if expected_enabled:
|
|
|
|
|
assert state["backend_type"], f"Enabled offloader has no backend type: {state}"
|
|
|
|
|
assert state["block_group_count"] > 0, f"Enabled offloader has no block groups: {state}"
|
|
|
|
|
assert state["block_count"] > 0, f"Enabled offloader has no managed blocks: {state}"
|
|
|
|
|
else:
|
|
|
|
|
assert state["block_group_count"] == 0, f"Disabled offloader retained block groups: {state}"
|
|
|
|
|
assert state["block_count"] == 0, f"Disabled offloader retained managed blocks: {state}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert_audio_outputs(
|
|
|
|
|
model_name: str,
|
|
|
|
|
audio_no_offload: np.ndarray | None,
|
|
|
|
|
audio_offload: np.ndarray | None,
|
|
|
|
|
) -> None:
|
|
|
|
|
if model_name not in AUDIO_MODEL:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
assert audio_no_offload is not None, "Baseline Stable Audio inference returned no audio"
|
|
|
|
|
assert audio_offload is not None, "Layerwise-offloaded Stable Audio inference returned no audio"
|
|
|
|
|
# Match the sibling cpu-offload test's tolerance: layerwise offload moves
|
|
|
|
|
# blocks across the PCIe bus on a side stream, which can perturb cuBLAS
|
|
|
|
|
# algorithm selection and produce ~ULP-level drift larger than 1e-3.
|
|
|
|
|
check_audio_determinism(audio_offload, audio_no_offload, atol=1e-2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _print_measurement(label: str, measurement: dict[str, Any]) -> None:
|
|
|
|
|
print(
|
|
|
|
|
f"{label}: initial={measurement['initial_used_mb']:.1f} MB, "
|
|
|
|
|
f"peak={measurement['peak_used_mb']:.1f} MB, "
|
|
|
|
|
f"incremental_peak={measurement['incremental_peak_mb']:.1f} MB, "
|
|
|
|
|
f"post_cleanup={measurement['post_cleanup_used_mb']:.1f} MB"
|
|
|
|
|
)
|
|
|
|
|
print(f"{label} offload state: {measurement['offload_states']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("configured", "expected"),
|
|
|
|
|
[
|
|
|
|
|
(None, (False, True)),
|
|
|
|
|
("baseline-first", (False, True)),
|
|
|
|
|
("offload-first", (True, False)),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_measurement_order(monkeypatch, configured: str | None, expected: tuple[bool, bool]) -> None:
|
|
|
|
|
if configured is None:
|
|
|
|
|
monkeypatch.delenv(_MEASUREMENT_ORDER_ENV, raising=False)
|
|
|
|
|
else:
|
|
|
|
|
monkeypatch.setenv(_MEASUREMENT_ORDER_ENV, configured)
|
|
|
|
|
|
|
|
|
|
assert _measurement_order() == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
def test_measurement_order_rejects_unknown_value(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setenv(_MEASUREMENT_ORDER_ENV, "unknown")
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError, match=_MEASUREMENT_ORDER_ENV):
|
|
|
|
|
_measurement_order()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.diffusion
|
|
|
|
|
@pytest.mark.core_model
|
|
|
|
|
@pytest.mark.cpu
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("audio_no_offload", "audio_offload", "message"),
|
|
|
|
|
[
|
|
|
|
|
(None, None, "Baseline Stable Audio inference returned no audio"),
|
|
|
|
|
(None, np.zeros(1), "Baseline Stable Audio inference returned no audio"),
|
|
|
|
|
(np.zeros(1), None, "Layerwise-offloaded Stable Audio inference returned no audio"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_stable_audio_requires_both_outputs(audio_no_offload, audio_offload, message) -> None:
|
|
|
|
|
with pytest.raises(AssertionError, match=message):
|
|
|
|
|
_assert_audio_outputs("stabilityai/stable-audio-open-1.0", audio_no_offload, audio_offload)
|
2026-01-30 17:35:09 +08:00
|
|
|
|
|
|
|
|
|
2026-08-12 22:58:23 +08:00
|
|
|
@pytest.mark.diffusion
|
2026-09-10 02:58:24 -07:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"model_name",
|
|
|
|
|
[pytest.param(name, marks=MODEL_MARKS[name]) for name in MODELS],
|
|
|
|
|
)
|
2026-01-30 17:35:09 +08:00
|
|
|
def test_layerwise_offload_diffusion_model(model_name: str):
|
|
|
|
|
"""Test that layerwise offloading reduces GPU memory usage.
|
|
|
|
|
|
|
|
|
|
This test verifies that layerwise offloading significantly reduces peak
|
|
|
|
|
GPU memory usage compared to loading the entire model on GPU. The layerwise
|
|
|
|
|
offloader keeps only a single transformer block on GPU at a time, with
|
|
|
|
|
prefetching for compute-memory overlap.
|
|
|
|
|
"""
|
2026-09-10 02:58:24 -07:00
|
|
|
measurements: dict[bool, dict[str, Any]] = {}
|
2026-01-30 17:35:09 +08:00
|
|
|
try:
|
2026-09-10 02:58:24 -07:00
|
|
|
measurement_order = _measurement_order()
|
|
|
|
|
print(
|
|
|
|
|
"Measurement order: "
|
|
|
|
|
+ " -> ".join("layerwise-offload" if enabled else "baseline" for enabled in measurement_order)
|
|
|
|
|
)
|
|
|
|
|
for enabled in measurement_order:
|
|
|
|
|
measurements[enabled] = run_inference_isolated(model_name, layerwise_offload=enabled)
|
2026-05-16 18:18:43 +08:00
|
|
|
except ValueError as exc:
|
2026-07-29 23:53:03 +08:00
|
|
|
# omni_snapshot_download wraps GatedRepoError in a ValueError; skip instead of failing.
|
2026-05-16 18:18:43 +08:00
|
|
|
if "Access to model" in str(exc) and "is restricted" in str(exc):
|
|
|
|
|
pytest.skip(
|
|
|
|
|
f"Skipping: gated HF repo {model_name!r} inaccessible "
|
|
|
|
|
f"({exc}). See docs/contributing/ci/hf_credentials.md."
|
|
|
|
|
)
|
2026-09-10 02:58:24 -07:00
|
|
|
raise
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
no_offload = measurements[False]
|
|
|
|
|
layerwise_offload = measurements[True]
|
|
|
|
|
_assert_offload_state(no_offload, expected_enabled=False)
|
|
|
|
|
_assert_offload_state(layerwise_offload, expected_enabled=True)
|
|
|
|
|
_print_measurement("No offload", no_offload)
|
|
|
|
|
_print_measurement("Layerwise offload", layerwise_offload)
|
2026-01-30 17:35:09 +08:00
|
|
|
|
2026-09-10 02:58:24 -07:00
|
|
|
audio_no_offload = no_offload["audio"]
|
|
|
|
|
audio_offload = layerwise_offload["audio"]
|
|
|
|
|
_assert_audio_outputs(model_name, audio_no_offload, audio_offload)
|
2026-05-05 22:49:57 +08:00
|
|
|
|
|
|
|
|
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}")
|
2026-08-25 09:10:17 +08:00
|
|
|
assert expected_saved_memory is not None
|
2026-05-05 22:49:57 +08:00
|
|
|
|
2026-01-30 17:35:09 +08:00
|
|
|
# Verify that layerwise offloading significantly reduces memory usage
|
2026-08-28 14:34:11 +08:00
|
|
|
# Passes only if the actual savings meets the expected savings
|
2026-09-10 02:58:24 -07:00
|
|
|
no_offload_peak_memory = no_offload["incremental_peak_mb"]
|
|
|
|
|
layerwise_offload_peak_memory = layerwise_offload["incremental_peak_mb"]
|
2026-08-28 14:34:11 +08:00
|
|
|
actual_saved_memory = no_offload_peak_memory - layerwise_offload_peak_memory
|
|
|
|
|
assert layerwise_offload_peak_memory + expected_saved_memory <= no_offload_peak_memory, (
|
2026-01-30 17:35:09 +08:00
|
|
|
f"Layerwise offload peak memory {layerwise_offload_peak_memory} MB "
|
2026-08-28 14:34:11 +08:00
|
|
|
f"should be at least {expected_saved_memory} MB less than no offload peak memory "
|
|
|
|
|
f"{no_offload_peak_memory} MB (actual savings: {actual_saved_memory} MB)"
|
2026-01-30 17:35:09 +08:00
|
|
|
)
|