# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum import torch from torch import nn from vllm.logger import init_logger from vllm_omni.diffusion.data import OmniDiffusionConfig logger = init_logger(__name__) class OffloadStrategy(Enum): NONE = "none" MODEL_LEVEL = "model_level" # Sequential offloading between DiT and encoders LAYER_WISE = "layer_wise" # Block-level DISTRIBUTED_LAYER_WISE = "distributed_layer_wise" # Block-level with DP sharding + H2D/AllGather overlap @dataclass class OffloadConfig: strategy: OffloadStrategy pin_cpu_memory: bool = True use_hsdp: bool = False dp_size: int = 1 # derived from parallel_config, not user-configurable dlo_use_allgather: bool = True # if True: shard + AllGather; if False: full weights H2D only model_path: str | None = None # checkpoint path for mmap weight loading @classmethod def from_od_config(cls, od_config: OmniDiffusionConfig) -> "OffloadConfig": """Extract and validate offload settings from OmniDiffusionConfig. Enforces mutual exclusion among the three offload strategies. Distributed layer-wise takes the highest priority, then layer-wise, then model-level. The ``dp_size`` is automatically derived from ``parallel_config`` — it is NOT a user-configurable parameter. The distributed layerwise offload works with whatever DP/SP parallelism is already set up. Args: od_config: OmniDiffusionConfig with offload settings Returns: OffloadConfig with validated settings """ enable_cpu_offload = getattr(od_config, "enable_cpu_offload", False) enable_layerwise_offload = getattr(od_config, "enable_layerwise_offload", False) enable_distributed_layerwise_offload = getattr(od_config, "enable_distributed_layerwise_offload", False) pin_cpu_memory = getattr(od_config, "pin_cpu_memory", True) parallel_config = getattr(od_config, "parallel_config", None) use_hsdp = getattr(parallel_config, "use_hsdp", False) if parallel_config else False # Derive dp_size from parallel_config — not user-configurable. # The offload adapts to whatever DP/SP is already configured. dp_size = 1 if parallel_config is not None: dp_size = getattr(parallel_config, "data_parallel_size", 1) # HSDP's fully_shard_degree also contributes to effective DP hsdp_shard_size = getattr(parallel_config, "hsdp_shard_size", -1) if use_hsdp else -1 hsdp_replicate_size = getattr(parallel_config, "hsdp_replicate_size", 1) if use_hsdp else 1 if use_hsdp and hsdp_shard_size > 0: dp_size = hsdp_shard_size * hsdp_replicate_size # When there is no DP but SP > 1, shard weights across SP ranks. # AllGather reconstructs full weights per layer; each rank then # computes on its SP portion of the sequence. This gives N× # compute parallelism with 1/N H2D transfer, reusing the exact # same AllGather code path — only the process group changes. if dp_size <= 1: sp_size = getattr(parallel_config, "sequence_parallel_size", 1) if sp_size and sp_size > 1: dp_size = sp_size # Determine strategy (mutual exclusion, distributed layer-wise takes priority) if enable_distributed_layerwise_offload: strategy = OffloadStrategy.DISTRIBUTED_LAYER_WISE if enable_layerwise_offload or enable_cpu_offload: logger.info("Distributed layer-wise offloading takes priority, disabling other offloading strategies.") elif enable_layerwise_offload: strategy = OffloadStrategy.LAYER_WISE if enable_cpu_offload: logger.info( "Both model-level and layer-wise offloading enabled. " "Layer-wise takes priority, disabling model-level offloading." ) elif enable_cpu_offload: strategy = OffloadStrategy.MODEL_LEVEL else: strategy = OffloadStrategy.NONE # When dlo_use_allgather=False, each rank loads full weights (no sharding, # no AllGather). This avoids AllGather synchronization requirements # (concurrent requests, dummy run skip) at the cost of N× CPU memory. dlo_use_allgather = getattr(od_config, "dlo_use_allgather", True) # If dlo_use_allgather=False, force dp_size=1 (each rank independent) if enable_distributed_layerwise_offload and not dlo_use_allgather: dp_size = 1 logger.info( "Distributed layerwise offload: dlo_use_allgather=False, " "each rank loads full weights (no shard, no AllGather)" ) # HSDP already shards parameters into DTensors. Running distributed # layerwise offload on top would shard each to_local() again, producing # incorrect reconstruction after AllGather. Reject this combination. if enable_distributed_layerwise_offload and use_hsdp and dlo_use_allgather: raise ValueError( "Distributed layerwise offload with AllGather is incompatible with " "HSDP: HSDP parameters are already sharded DTensors, and the offloader " "would double-shard them. Use --dlo-no-use-allgather (full weights per " "rank) or disable HSDP." ) return cls( strategy=strategy, pin_cpu_memory=pin_cpu_memory, use_hsdp=use_hsdp, dp_size=dp_size, dlo_use_allgather=dlo_use_allgather, model_path=getattr(od_config, "model", None), ) class OffloadBackend(ABC): """Base class for CPU offload backends""" def __init__(self, config: OffloadConfig, device: torch.device): self.config = config self.device = device self.enabled = False @abstractmethod def enable(self, pipeline: nn.Module) -> None: """Enable offloading on the pipeline. Discovers modules, moves them to appropriate devices, and registers forward hooks for swapping/prefetching. Args: pipeline: Diffusion pipeline model (e.g., Wan22Pipeline) """ raise NotImplementedError @abstractmethod def disable(self) -> None: """Disable offloading and cleanup resources. Removes all registered hooks. Does NOT move modules back to original devices (caller responsible for that). """ raise NotImplementedError def is_enabled(self) -> bool: return self.enabled