Merge remote-tracking branch 'origin/main' into qwen-image-2.1
# Conflicts: # vllm_omni/diffusion/executor/multiproc_executor.py # vllm_omni/diffusion/offloader/sequential_backend.py
This commit is contained in:
commit
9174554e40
1332 files changed
+160051
-60504
No files matched your search
@@ -0,0 +1,163 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Executable contracts for the two behaviors the generic cutover changed.
|
||||
|
||||
Placement now follows resolved block identity, and the backends run on the
|
||||
resolved plan alone. Declaration parsing, selection errors, rollback,
|
||||
residency and enable/disable cycles are covered by ``test_plan_resolver.py``,
|
||||
``test_layerwise_backend.py`` and ``test_sequential_backend.py``.
|
||||
"""
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from tests.diffusion.offloader.helpers import patch_offload_runtime
|
||||
from vllm_omni.diffusion.offloader import layerwise_backend, sequential_backend
|
||||
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
|
||||
from vllm_omni.diffusion.offloader.layerwise_backend import LayerWiseOffloadBackend
|
||||
from vllm_omni.diffusion.offloader.offload_plan import OffloadPlan
|
||||
from vllm_omni.diffusion.offloader.plan_resolver import resolve_offload_plan
|
||||
from vllm_omni.diffusion.offloader.sequential_backend import ModelLevelOffloadBackend
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.core_model]
|
||||
|
||||
STRATEGIES = [OffloadStrategy.MODEL_LEVEL, OffloadStrategy.LAYER_WISE]
|
||||
CPU = torch.device("cpu")
|
||||
|
||||
|
||||
class _Block(nn.Linear):
|
||||
def __init__(self):
|
||||
super().__init__(4, 4)
|
||||
self.register_buffer("scale", torch.tensor(0.5))
|
||||
|
||||
def forward(self, x):
|
||||
return super().forward(x).tanh() * self.scale
|
||||
|
||||
|
||||
class _Stack(nn.Module):
|
||||
"""Two block containers plus non-block state that must stay resident."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.blocks = nn.ModuleList([_Block() for _ in range(3)])
|
||||
self.tail = nn.ModuleList([_Block() for _ in range(2)])
|
||||
self.bias = nn.Parameter(torch.ones(4))
|
||||
self.register_buffer("scale", torch.tensor(0.25))
|
||||
self.proj = nn.Linear(4, 4)
|
||||
|
||||
def forward(self, x):
|
||||
for block in (*self.blocks, *self.tail):
|
||||
x = block(x)
|
||||
return self.proj(x) * self.scale + self.bias
|
||||
|
||||
|
||||
class _Pipeline(nn.Module):
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder", "image_encoder"]
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
_resident_modules: ClassVar[list[str]] = ["resident"]
|
||||
_offload_plan = OffloadPlan(
|
||||
block_attrs={"transformer": ("blocks", "tail")},
|
||||
encoder_block_attrs={"text_encoder": ("blocks", "tail")},
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = _Stack()
|
||||
self.text_encoder = _Stack()
|
||||
self.image_encoder = nn.Linear(4, 4)
|
||||
self.vae = nn.Linear(4, 4)
|
||||
self.resident = nn.Linear(4, 4)
|
||||
|
||||
def forward(self, x):
|
||||
return self.resident(self.vae(self.transformer(self.text_encoder(self.image_encoder(x)))))
|
||||
|
||||
|
||||
@pytest.fixture(params=[pytest.param("cpu", marks=pytest.mark.cpu), pytest.param("cuda", marks=pytest.mark.cuda)])
|
||||
def execution_device(request, monkeypatch):
|
||||
if request.param == "cuda":
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required for real transfer/residency coverage")
|
||||
return torch.device("cuda:0")
|
||||
patch_offload_runtime(monkeypatch, current_omni_platform, synchronize=True)
|
||||
monkeypatch.setattr(current_omni_platform, "get_free_memory", lambda: 0)
|
||||
return CPU
|
||||
|
||||
|
||||
def _config(strategy, components):
|
||||
return OffloadConfig(strategy=strategy, components=components, pin_cpu_memory=False)
|
||||
|
||||
|
||||
def _backend(config, device):
|
||||
kind = ModelLevelOffloadBackend if config.strategy is OffloadStrategy.MODEL_LEVEL else LayerWiseOffloadBackend
|
||||
return kind(config, device)
|
||||
|
||||
|
||||
def _assert_no_offload_hooks(pipeline):
|
||||
for module in pipeline.modules():
|
||||
registry = getattr(module, "_hook_registry", None)
|
||||
if registry is not None:
|
||||
assert registry.get_hook("sequential_offload") is None
|
||||
assert registry.get_hook("layerwise_offload") is None
|
||||
assert not getattr(module, "_omni_layerwise_enabled", False)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_alias_of_a_streamed_block_is_not_placed(execution_device):
|
||||
"""Placement moves DiT state by resolved block identity, not by attribute.
|
||||
|
||||
An attribute aliasing a streamed block must stay with its ring: copying it
|
||||
to the device makes the ring record the device as that block's home and
|
||||
strands those weights there after teardown.
|
||||
"""
|
||||
pipeline = _Pipeline()
|
||||
pipeline.transformer.proj.weight = pipeline.transformer.blocks[1].weight
|
||||
original = pipeline.transformer.proj.weight.clone()
|
||||
backend = _backend(_config(OffloadStrategy.LAYER_WISE, frozenset({"dit"})), execution_device)
|
||||
|
||||
backend.enable(pipeline)
|
||||
try:
|
||||
assert pipeline.transformer.proj.weight is pipeline.transformer.blocks[1].weight
|
||||
assert pipeline.transformer.proj.weight.device == CPU
|
||||
# Non-block state around the alias is still placed.
|
||||
assert pipeline.transformer.proj.bias.device == execution_device
|
||||
assert pipeline.transformer.bias.device == execution_device
|
||||
finally:
|
||||
backend.disable()
|
||||
|
||||
assert pipeline.transformer.proj.weight.device == CPU
|
||||
torch.testing.assert_close(pipeline.transformer.proj.weight, original)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("strategy", STRATEGIES)
|
||||
@torch.inference_mode()
|
||||
def test_backends_execute_the_resolved_plan_alone(execution_device, strategy, monkeypatch):
|
||||
pipeline = _Pipeline().to(execution_device)
|
||||
x = torch.randn(2, 4, device=execution_device)
|
||||
expected = pipeline(x)
|
||||
config = _config(strategy, frozenset({"dit", "text_encoder"}))
|
||||
resolved = resolve_offload_plan(pipeline, config)
|
||||
|
||||
def forbidden_read(*args, **kwargs):
|
||||
pytest.fail("Backend reinterpreted topology after plan resolution")
|
||||
|
||||
# Once resolved, neither declarations nor selector helpers are an input to
|
||||
# backend execution. Transport options stay readable.
|
||||
monkeypatch.setattr(config, "offloads", forbidden_read)
|
||||
monkeypatch.setattr(config, "should_offload_encoder", forbidden_read)
|
||||
monkeypatch.setattr(pipeline, "_offload_plan", None)
|
||||
backend_module = sequential_backend if strategy is OffloadStrategy.MODEL_LEVEL else layerwise_backend
|
||||
monkeypatch.setattr(backend_module, "resolve_offload_plan", lambda *_: resolved)
|
||||
|
||||
backend = _backend(config, execution_device)
|
||||
try:
|
||||
backend.enable(pipeline)
|
||||
torch.testing.assert_close(pipeline(x), expected)
|
||||
finally:
|
||||
backend.disable()
|
||||
_assert_no_offload_hooks(pipeline)
|
||||
@@ -9,14 +9,14 @@ import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.mark import hardware_marks, hardware_test
|
||||
from tests.helpers.monitor import DeviceMemoryMonitor
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
AUDIO_MODEL: dict[str, dict[str, int | None]] = {
|
||||
"stabilityai/stable-audio-open-1.0": {"cuda": 100, "rocm": None},
|
||||
"stabilityai/stable-audio-open-1.0": {"cuda": 150, "rocm": None},
|
||||
}
|
||||
|
||||
IMAGE_MODELS: dict[str, dict[str, int | None]] = {
|
||||
@@ -27,11 +27,19 @@ MODELS: dict[str, dict[str, int | None]] = {**AUDIO_MODEL, **IMAGE_MODELS}
|
||||
|
||||
MODEL_MARKS = {
|
||||
"riverclouds/qwen_image_random": pytest.mark.core_model,
|
||||
"stabilityai/stable-audio-open-1.0": pytest.mark.full_model,
|
||||
"stabilityai/stable-audio-open-1.0": [
|
||||
pytest.mark.full_model,
|
||||
*hardware_marks(res={"cuda": ["L4", "B200"], "rocm": "MI325"}),
|
||||
],
|
||||
}
|
||||
|
||||
_GATED_MODELS = {"stabilityai/stable-audio-open-1.0"}
|
||||
|
||||
# Models whose thresholds are calibrated against max_memory_allocated. All
|
||||
# other models keep asserting on the polled device-wide figure until their
|
||||
# thresholds have been re-measured on that metric on both CUDA and ROCm.
|
||||
_ALLOCATED_THRESHOLD_MODELS = {"stabilityai/stable-audio-open-1.0"}
|
||||
|
||||
|
||||
AUDIO_MODEL_PARAMS: dict[str, dict[str, Any]] = {
|
||||
"runner_params": {},
|
||||
@@ -47,7 +55,7 @@ IMAGE_MODEL_PARAMS: dict[str, dict[str, Any]] = {
|
||||
}
|
||||
|
||||
|
||||
def inference(model_name: str, offload: bool = True) -> tuple[float, Any]:
|
||||
def inference(model_name: str, offload: bool = True) -> tuple[float, float, Any]:
|
||||
gc.collect()
|
||||
current_omni_platform.empty_cache()
|
||||
device_index = current_omni_platform.current_device()
|
||||
@@ -78,12 +86,17 @@ def inference(model_name: str, offload: bool = True) -> tuple[float, Any]:
|
||||
),
|
||||
)
|
||||
peak = monitor.peak_used_mb
|
||||
# Exact in-process peak of live tensors. Unlike the polled device-wide
|
||||
# figure above, this excludes caching-allocator slack and CUDA context,
|
||||
# which differ between the offload and no-offload paths (the offload hook
|
||||
# calls empty_cache() on every swap) and drift between runs.
|
||||
peak_allocated = current_omni_platform.max_memory_allocated(device=device_index) / (1024**2)
|
||||
monitor.stop()
|
||||
|
||||
gc.collect()
|
||||
current_omni_platform.empty_cache()
|
||||
|
||||
return peak, output
|
||||
return peak, peak_allocated, output
|
||||
|
||||
|
||||
def check_audio_determinism(audio1, audio2, atol=1e-2):
|
||||
@@ -109,9 +122,9 @@ def check_audio_determinism(audio1, audio2, atol=1e-2):
|
||||
)
|
||||
def test_cpu_offload_diffusion_model(model_name: str):
|
||||
try:
|
||||
offload_peak_memory, output_offload = inference(model_name, offload=True)
|
||||
offload_peak_memory, offload_peak_allocated, output_offload = inference(model_name, offload=True)
|
||||
cleanup_dist_env_and_memory()
|
||||
no_offload_peak_memory, output_no_offload = inference(model_name, offload=False)
|
||||
no_offload_peak_memory, no_offload_peak_allocated, output_no_offload = inference(model_name, offload=False)
|
||||
except ValueError as exc:
|
||||
# omni_snapshot_download wraps GatedRepoError in a ValueError; skip instead of failing.
|
||||
if "Access to model" in str(exc) and "is restricted" in str(exc):
|
||||
@@ -122,16 +135,14 @@ def test_cpu_offload_diffusion_model(model_name: str):
|
||||
pytest.fail(f"Inference failed: {exc}")
|
||||
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")
|
||||
print(f"Offload peak memory: {offload_peak_memory} MB (allocated: {offload_peak_allocated:.1f} MB)")
|
||||
print(f"No offload peak memory: {no_offload_peak_memory} MB (allocated: {no_offload_peak_allocated:.1f} MB)")
|
||||
|
||||
if model_name == "stabilityai/stable-audio-open-1.0":
|
||||
audio_offload = output_offload[0].multimodal_output.get("audio")
|
||||
audio_no_offload = output_no_offload[0].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
|
||||
platform = "rocm" if is_rocm else "cuda"
|
||||
threshold = MODELS[model_name][platform]
|
||||
@@ -139,7 +150,22 @@ def test_cpu_offload_diffusion_model(model_name: str):
|
||||
pytest.skip(f"Threshold not defined for {platform} on {model_name}")
|
||||
assert threshold is not None
|
||||
|
||||
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"
|
||||
)
|
||||
if model_name in _ALLOCATED_THRESHOLD_MODELS:
|
||||
# Threshold is a lower bound on the peak *allocated* saving, i.e. the
|
||||
# weights the offloader keeps off-GPU at the moment of peak usage
|
||||
# (stable-audio: the fp16 T5 encoder, ~209 MB). max_memory_allocated
|
||||
# is used instead of the polled device-wide figure because the latter
|
||||
# includes caching-allocator slack that differs between the two paths
|
||||
# and drifts by tens of MB between runs.
|
||||
assert offload_peak_allocated + threshold < no_offload_peak_allocated, (
|
||||
f"Offload peak allocated memory {offload_peak_allocated:.1f} MB should be less than "
|
||||
f"no offload peak allocated memory {no_offload_peak_allocated:.1f} MB by {threshold} MB"
|
||||
)
|
||||
else:
|
||||
# Threshold is calibrated against the polled device-wide figure and
|
||||
# accounts for runtime memory overhead and fragmentation differences
|
||||
# between CUDA and ROCm.
|
||||
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"
|
||||
)
|
||||
@@ -1,7 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import concurrent.futures
|
||||
import gc
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -9,7 +12,7 @@ import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.mark import hardware_marks
|
||||
from tests.helpers.monitor import DeviceMemoryMonitor
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
@@ -31,8 +34,14 @@ IMAGE_VIDEO_MODELS: dict[str, dict[str, int | None]] = {
|
||||
MODELS: dict[str, dict[str, int | None]] = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS}
|
||||
|
||||
MODEL_MARKS = {
|
||||
"riverclouds/qwen_image_random": pytest.mark.core_model,
|
||||
"stabilityai/stable-audio-open-1.0": pytest.mark.full_model,
|
||||
"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"}),
|
||||
],
|
||||
}
|
||||
|
||||
AUDIO_MODEL_PARAMS: dict[str, dict[str, Any]] = {
|
||||
@@ -45,32 +54,98 @@ IMAGE_VIDEO_MODELS_PARAMS: dict[str, dict[str, Any]] = {
|
||||
"sampler_params": {"height": 480, "width": 640, "num_frames": 5},
|
||||
}
|
||||
|
||||
_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),
|
||||
}
|
||||
|
||||
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()}")
|
||||
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()}")
|
||||
raise AssertionError(f"Audio outputs differ beyond tolerance atol={atol}")
|
||||
return True
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def run_inference(
|
||||
model_name: str,
|
||||
layerwise_offload: bool = False,
|
||||
num_inference_steps: int = 3,
|
||||
) -> tuple[float, Any]:
|
||||
) -> dict[str, Any]:
|
||||
current_omni_platform.empty_cache()
|
||||
device_index = current_omni_platform.current_device()
|
||||
with current_omni_platform.device(device_index):
|
||||
free_bytes, total_bytes = current_omni_platform.mem_get_info()
|
||||
initial_used_mb = (total_bytes - free_bytes) / (1024**2)
|
||||
initial_used_mb = _device_used_mb(device_index)
|
||||
|
||||
if model_name in AUDIO_MODEL:
|
||||
params = AUDIO_MODEL_PARAMS
|
||||
@@ -80,10 +155,17 @@ def run_inference(
|
||||
with OmniRunner(
|
||||
model_name,
|
||||
enable_layerwise_offload=layerwise_offload,
|
||||
worker_extension_cls=f"{OffloadStateProbe.__module__}.{OffloadStateProbe.__qualname__}",
|
||||
# TODO: we might want to add overlapped feature e2e tests
|
||||
# cache_backend="cache_dit",
|
||||
**params["runner_params"],
|
||||
) as runner:
|
||||
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")
|
||||
|
||||
# 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
|
||||
@@ -108,20 +190,135 @@ def run_inference(
|
||||
finally:
|
||||
monitor.stop()
|
||||
|
||||
audio = _extract_audio(output)
|
||||
del output
|
||||
|
||||
# DeviceMemoryMonitor reports absolute device usage. Subtract this run's
|
||||
# starting usage so process-wide compiler/workspace caches retained from a
|
||||
# previous run do not make the second measurement order-dependent.
|
||||
peak = max(0.0, monitor.peak_used_mb - initial_used_mb)
|
||||
# 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)
|
||||
|
||||
del runner
|
||||
gc.collect()
|
||||
cleanup_dist_env_and_memory()
|
||||
current_omni_platform.empty_cache()
|
||||
post_cleanup_used_mb = _device_used_mb(device_index)
|
||||
|
||||
return peak, output
|
||||
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
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
|
||||
@pytest.mark.parametrize("model_name", list(MODELS.keys()))
|
||||
@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)
|
||||
|
||||
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[pytest.param(name, marks=MODEL_MARKS[name]) for name in MODELS],
|
||||
)
|
||||
def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
"""Test that layerwise offloading reduces GPU memory usage.
|
||||
|
||||
@@ -130,14 +327,15 @@ def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
offloader keeps only a single transformer block on GPU at a time, with
|
||||
prefetching for compute-memory overlap.
|
||||
"""
|
||||
measurements: dict[bool, dict[str, Any]] = {}
|
||||
try:
|
||||
# Run without layerwise offloading (baseline)
|
||||
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, output_offload = run_inference(model_name, layerwise_offload=True)
|
||||
cleanup_dist_env_and_memory()
|
||||
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)
|
||||
except ValueError as exc:
|
||||
# omni_snapshot_download wraps GatedRepoError in a ValueError; skip instead of failing.
|
||||
if "Access to model" in str(exc) and "is restricted" in str(exc):
|
||||
@@ -145,20 +343,18 @@ def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
f"Skipping: gated HF repo {model_name!r} inaccessible "
|
||||
f"({exc}). See docs/contributing/ci/hf_credentials.md."
|
||||
)
|
||||
pytest.fail(f"Inference failed: {exc}")
|
||||
except Exception:
|
||||
pytest.fail("Inference failed")
|
||||
raise
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
if model_name == "stabilityai/stable-audio-open-1.0":
|
||||
audio_offload = output_offload[0].multimodal_output.get("audio")
|
||||
audio_no_offload = output_no_offload[0].multimodal_output.get("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)
|
||||
audio_no_offload = no_offload["audio"]
|
||||
audio_offload = layerwise_offload["audio"]
|
||||
_assert_audio_outputs(model_name, audio_no_offload, audio_offload)
|
||||
|
||||
is_rocm = torch.version.hip is not None
|
||||
platform = "rocm" if is_rocm else "cuda"
|
||||
@@ -170,6 +366,8 @@ def test_layerwise_offload_diffusion_model(model_name: str):
|
||||
|
||||
# Verify that layerwise offloading significantly reduces memory usage
|
||||
# Passes only if the actual savings meets the expected savings
|
||||
no_offload_peak_memory = no_offload["incremental_peak_mb"]
|
||||
layerwise_offload_peak_memory = layerwise_offload["incremental_peak_mb"]
|
||||
actual_saved_memory = no_offload_peak_memory - layerwise_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 "
|
||||
|
||||
@@ -32,6 +32,7 @@ from vllm_omni.diffusion.model_loader.host_weight_plan import (
|
||||
TensorBinding,
|
||||
build_checkpoint_mmap_plan,
|
||||
)
|
||||
from vllm_omni.diffusion.offloader import plan_resolver
|
||||
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
|
||||
from vllm_omni.diffusion.offloader.block_discovery import (
|
||||
get_blocks_attr_names,
|
||||
@@ -52,7 +53,7 @@ from vllm_omni.diffusion.offloader.offload_plan import (
|
||||
OffloadPlan,
|
||||
get_offload_plan,
|
||||
)
|
||||
from vllm_omni.diffusion.offloader.plan_resolver import resolve_offload_plan
|
||||
from vllm_omni.diffusion.offloader.plan_resolver import ResolvedComponent, resolve_offload_plan
|
||||
from vllm_omni.diffusion.offloader.startup import OffloadStartupState, attach_offload_startup_state
|
||||
from vllm_omni.host_weight_runtime import MappedHostRegion
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
@@ -1235,6 +1236,78 @@ class _MmapPostLoadPipeline(nn.Module):
|
||||
|
||||
|
||||
class TestMmapWeightLoading:
|
||||
@pytest.mark.parametrize("initial_device", ["cpu", "meta"])
|
||||
@pytest.mark.parametrize("use_allgather", [False, True])
|
||||
def test_mmap_keeps_undeclared_nested_blocks_streamed(
|
||||
self, tmp_path, patched_offload_runtime, monkeypatch, initial_device, use_allgather
|
||||
):
|
||||
class Refiner(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)])
|
||||
|
||||
def forward(self, x):
|
||||
for block in self.layers:
|
||||
x = block(x)
|
||||
return x
|
||||
|
||||
class Transformer(nn.Module):
|
||||
_layerwise_offload_blocks_attrs = ["blocks"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.blocks = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)])
|
||||
self.refiner = Refiner()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.refiner(x)
|
||||
for block in self.blocks:
|
||||
x = block(x)
|
||||
return x
|
||||
|
||||
pipeline = nn.Module()
|
||||
pipeline.transformer = Transformer()
|
||||
weights = {name: param.detach().clone() for name, param in pipeline.named_parameters()}
|
||||
x = torch.randn(1, 2)
|
||||
with torch.no_grad():
|
||||
expected = pipeline.transformer(x)
|
||||
checkpoint_file = tmp_path / "model.safetensors"
|
||||
save_file(weights, str(checkpoint_file))
|
||||
# Exercise the real mmap path with a small model instead of allocating
|
||||
# the 1 GiB needed to trigger the production compatibility threshold.
|
||||
monkeypatch.setattr(plan_resolver, "_NESTED_OFFLOAD_THRESHOLD_MB", 0)
|
||||
if initial_device == "meta":
|
||||
pipeline.transformer.to_empty(device="meta")
|
||||
plan = HostWeightPlan(
|
||||
backing_kind="checkpoint_mmap",
|
||||
bindings={name: TensorBinding(checkpoint_key=name, file_path=str(checkpoint_file)) for name in weights},
|
||||
)
|
||||
backend = DistributedLayerwiseOffloadBackend(
|
||||
OffloadConfig(
|
||||
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
|
||||
pin_cpu_memory=False,
|
||||
dlo_use_allgather=use_allgather,
|
||||
),
|
||||
torch.device("cpu"),
|
||||
host_weight_plan=plan,
|
||||
)
|
||||
|
||||
for _ in range(2):
|
||||
try:
|
||||
backend.enable(pipeline)
|
||||
assert len(backend._all_hook_groups) == 2
|
||||
with torch.no_grad():
|
||||
torch.testing.assert_close(pipeline.transformer(x), expected)
|
||||
finally:
|
||||
backend.disable()
|
||||
for name, parameter in pipeline.named_parameters():
|
||||
assert not parameter.is_meta
|
||||
torch.testing.assert_close(parameter, weights[name])
|
||||
for module in pipeline.modules():
|
||||
registry = getattr(module, "_hook_registry", None)
|
||||
if registry is not None:
|
||||
assert registry.get_hook("distributed_layerwise_offload") is None
|
||||
|
||||
def test_runs_model_post_load_hook(self, tmp_path, patched_offload_runtime):
|
||||
pipeline = _MmapPostLoadPipeline()
|
||||
weights = {name: torch.ones(param.shape, dtype=torch.bfloat16) for name, param in pipeline.named_parameters()}
|
||||
@@ -1250,10 +1323,7 @@ class TestMmapWeightLoading:
|
||||
),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
modules = SimpleNamespace(
|
||||
dits=[pipeline.transformer],
|
||||
dit_names=["transformer"],
|
||||
)
|
||||
dits = (ResolvedComponent(path="transformer", module=pipeline.transformer, selected=True),)
|
||||
plan = HostWeightPlan(
|
||||
backing_kind="checkpoint_mmap",
|
||||
bindings={
|
||||
@@ -1265,7 +1335,7 @@ class TestMmapWeightLoading:
|
||||
},
|
||||
)
|
||||
|
||||
backend._load_weights_via_mmap(pipeline, modules, plan)
|
||||
backend._load_weights_via_mmap(pipeline, dits, plan)
|
||||
|
||||
assert pipeline.transformer.post_load_calls == 1
|
||||
assert pipeline.transformer.time_embedder.weight.dtype == torch.float32
|
||||
@@ -1717,6 +1787,128 @@ class TestOffloadPlan:
|
||||
for name, parameter in pipeline.named_parameters():
|
||||
torch.testing.assert_close(parameter, expected_parameters[name])
|
||||
|
||||
@pytest.mark.parametrize("shared_owner", ["parent", "encoder", "sibling"])
|
||||
def test_nested_duplicate_ownership_is_rejected_before_any_hook(self, patched_offload_runtime, shared_owner):
|
||||
class Pipeline(nn.Module):
|
||||
_offload_plan = OffloadPlan(
|
||||
offload_submodules={"refiner": "blocks", "other_refiner": "blocks"},
|
||||
encoder_block_attrs={"text_encoder": ("encoder.block",)},
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = _SingleBlockModel(num_blocks=2)
|
||||
self.transformer.refiner = _SingleBlockModel(num_blocks=2)
|
||||
self.transformer.other_refiner = _SingleBlockModel(num_blocks=2)
|
||||
self.text_encoder = _PlainEncoder()
|
||||
owners = {
|
||||
"parent": self.transformer.blocks,
|
||||
"encoder": self.text_encoder.encoder.block,
|
||||
"sibling": self.transformer.other_refiner.blocks,
|
||||
}
|
||||
self.transformer.refiner.blocks = owners[shared_owner]
|
||||
|
||||
pipeline = Pipeline()
|
||||
backend = DistributedLayerwiseOffloadBackend(
|
||||
OffloadConfig(strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE, pin_cpu_memory=False),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
storage = {name: parameter.data_ptr() for name, parameter in pipeline.named_parameters()}
|
||||
try:
|
||||
with pytest.raises(ValueError, match="claimed by both"):
|
||||
backend.enable(pipeline)
|
||||
assert not backend.enabled
|
||||
assert not backend._all_hook_groups
|
||||
assert {name: parameter.data_ptr() for name, parameter in pipeline.named_parameters()} == storage
|
||||
assert not getattr(pipeline.text_encoder, "_omni_layerwise_enabled", False)
|
||||
for module in pipeline.modules():
|
||||
assert getattr(module, "_hook_registry", None) is None
|
||||
finally:
|
||||
backend.disable()
|
||||
|
||||
def test_unstageable_submodule_is_rejected_before_any_hook(self, patched_offload_runtime):
|
||||
"""Plan-dependent rejection happens before earlier components are hooked."""
|
||||
|
||||
class Pipeline(nn.Module):
|
||||
_offload_plan = OffloadPlan(
|
||||
offload_submodules={"refiner": "missing"},
|
||||
encoder_component_types={"text_encoder": "text_encoder"},
|
||||
encoder_block_attrs={"text_encoder": ("encoder.block",)},
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = _SingleBlockModel(num_blocks=2)
|
||||
# Declared for submodule offload but without a block list or the
|
||||
# load_to_device/offload_to_cpu lifecycle.
|
||||
self.transformer.refiner = nn.Linear(2, 2)
|
||||
self.text_encoder = _PlainEncoder()
|
||||
|
||||
pipeline = Pipeline()
|
||||
backend = DistributedLayerwiseOffloadBackend(
|
||||
OffloadConfig(strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE, pin_cpu_memory=False),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
|
||||
# Storage identity proves the encoder was never staged and rolled back.
|
||||
encoder_storage = [block.weight.data_ptr() for block in pipeline.text_encoder.encoder.block]
|
||||
|
||||
with pytest.raises(ValueError, match="must implement load_to_device"):
|
||||
backend.enable(pipeline)
|
||||
|
||||
assert not backend.enabled
|
||||
assert [block.weight.data_ptr() for block in pipeline.text_encoder.encoder.block] == encoder_storage
|
||||
assert not getattr(pipeline.text_encoder, "_omni_layerwise_enabled", False)
|
||||
for block in pipeline.transformer.blocks:
|
||||
assert getattr(block, "_hook_registry", None) is None
|
||||
|
||||
def test_undeclared_submodule_block_scan_warns(self, patched_offload_runtime, monkeypatch):
|
||||
"""The size-triggered attribute scan still works and is deprecated."""
|
||||
|
||||
class Refiner(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)])
|
||||
|
||||
class Transformer(nn.Module):
|
||||
_layerwise_offload_blocks_attrs = ["blocks"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.blocks = nn.ModuleList([nn.Linear(2, 2), nn.Linear(2, 2)])
|
||||
self.refiner = Refiner()
|
||||
|
||||
class Pipeline(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = Transformer()
|
||||
|
||||
warnings: list[str] = []
|
||||
|
||||
class _Recorder:
|
||||
def warning(self, message, *args):
|
||||
warnings.append(message % args if args else message)
|
||||
|
||||
def __getattr__(self, _name):
|
||||
return lambda *args, **kwargs: None
|
||||
|
||||
monkeypatch.setattr(plan_resolver, "logger", _Recorder())
|
||||
monkeypatch.setattr(plan_resolver, "_NESTED_OFFLOAD_THRESHOLD_MB", 0)
|
||||
plan_resolver._warn_nested_block_scan.cache_clear()
|
||||
plan_resolver._warn_legacy_discovery.cache_clear()
|
||||
pipeline = Pipeline()
|
||||
backend = DistributedLayerwiseOffloadBackend(
|
||||
OffloadConfig(strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE, pin_cpu_memory=False),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
|
||||
backend.enable(pipeline)
|
||||
|
||||
assert len(backend._all_hook_groups) == 2
|
||||
assert sum("OffloadPlan.offload_submodules" in message for message in warnings) == 1
|
||||
|
||||
backend.disable()
|
||||
|
||||
|
||||
class TestMmapValidation:
|
||||
"""Tests for loader preflight and backend plan realization."""
|
||||
@@ -2101,7 +2293,7 @@ class TestMmapValidation:
|
||||
),
|
||||
torch.device("cpu"),
|
||||
)
|
||||
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
|
||||
dits = (ResolvedComponent(path="transformer", module=pipeline.transformer, selected=True),)
|
||||
result = build_checkpoint_mmap_plan(
|
||||
pipeline,
|
||||
dit_modules=(("transformer", pipeline.transformer),),
|
||||
@@ -2113,7 +2305,7 @@ class TestMmapValidation:
|
||||
)
|
||||
|
||||
assert result.plan is not None
|
||||
backend._load_weights_via_mmap(pipeline, modules, result.plan)
|
||||
backend._load_weights_via_mmap(pipeline, dits, result.plan)
|
||||
assert pipeline.transformer.validate_called, "validate_loaded_weights should be called"
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ from tests.diffusion.offloader.helpers import (
|
||||
patch_offload_runtime,
|
||||
)
|
||||
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
|
||||
from vllm_omni.diffusion.offloader.block_discovery import (
|
||||
get_blocks_attr_names,
|
||||
get_blocks_from_dit,
|
||||
set_blocks_attr_names,
|
||||
)
|
||||
from vllm_omni.diffusion.offloader.layerwise_backend import (
|
||||
LayerWiseOffloadBackend,
|
||||
LayerwiseOffloadHook,
|
||||
@@ -209,21 +214,21 @@ class _NoAttrsModel(nn.Module):
|
||||
class TestGetBlocksFromDit:
|
||||
def test_get_blocks_from_dit_single_block_attr(self):
|
||||
model = _SingleBlockModel(num_blocks=3)
|
||||
attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
attr_names, blocks = get_blocks_from_dit(model)
|
||||
assert attr_names == ["blocks"]
|
||||
assert len(blocks) == 3
|
||||
assert all(isinstance(b, _DummyBlock) for b in blocks)
|
||||
|
||||
def test_get_blocks_from_dit_multi_block_attrs(self):
|
||||
model = _MultiBlockModel(num_transformer=2, num_single=3)
|
||||
attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
attr_names, blocks = get_blocks_from_dit(model)
|
||||
assert set(attr_names) == {"transformer_blocks", "single_transformer_blocks"}
|
||||
assert len(blocks) == 5
|
||||
assert all(isinstance(b, _DummyBlock) for b in blocks)
|
||||
|
||||
def test_get_blocks_from_dit_empty_blocks(self):
|
||||
model = _EmptyBlocksModel()
|
||||
attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
attr_names, blocks = get_blocks_from_dit(model)
|
||||
assert attr_names == []
|
||||
assert blocks == []
|
||||
|
||||
@@ -233,17 +238,17 @@ class TestGetBlocksFromDit:
|
||||
AttributeError,
|
||||
match="Attribute 'nonexistent_blocks' declared in _layerwise_offload_blocks_attrs does not exist",
|
||||
):
|
||||
LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
get_blocks_from_dit(model)
|
||||
|
||||
def test_get_blocks_from_dit_no_attrs_defined(self):
|
||||
model = _NoAttrsModel(num_blocks=3)
|
||||
attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
attr_names, blocks = get_blocks_from_dit(model)
|
||||
assert attr_names == []
|
||||
assert blocks == []
|
||||
|
||||
def test_get_blocks_from_dit_deprecated_single_attr(self):
|
||||
model = _DeprecatedSingleAttrModel(num_blocks=2)
|
||||
attr_names, blocks = LayerWiseOffloadBackend.get_blocks_from_dit(model)
|
||||
attr_names, blocks = get_blocks_from_dit(model)
|
||||
assert attr_names == ["blocks"]
|
||||
assert len(blocks) == 2
|
||||
|
||||
@@ -251,17 +256,17 @@ class TestGetBlocksFromDit:
|
||||
class TestGetBlocksAttrNames:
|
||||
def test_get_blocks_attr_names_new_format(self):
|
||||
model = _MultiBlockModel()
|
||||
attrs = LayerWiseOffloadBackend.get_blocks_attr_names(model)
|
||||
attrs = get_blocks_attr_names(model)
|
||||
assert attrs == ["transformer_blocks", "single_transformer_blocks"]
|
||||
|
||||
def test_get_blocks_attr_names_no_attrs(self):
|
||||
model = _NoAttrsModel()
|
||||
attrs = LayerWiseOffloadBackend.get_blocks_attr_names(model)
|
||||
attrs = get_blocks_attr_names(model)
|
||||
assert attrs == []
|
||||
|
||||
def test_set_blocks_attr_names(self):
|
||||
model = _NoAttrsModel()
|
||||
LayerWiseOffloadBackend.set_blocks_attr_names(model, ["new_blocks"])
|
||||
set_blocks_attr_names(model, ["new_blocks"])
|
||||
assert hasattr(model.__class__, "_layerwise_offload_blocks_attrs")
|
||||
assert model.__class__._layerwise_offload_blocks_attrs == ["new_blocks"]
|
||||
|
||||
|
||||
Reference in new issue
Block a user