Files
trandangtrungduc-Comfyui-Tu…/utils/layerwise_gpu_offload_wrapper.py

189 lines
6.5 KiB
Python

"""
Layerwise GPU offload wrapper.
Motivation
----------
The previous `CPUOffloadWrapper` forces the entire forward pass to run on CPU.
That is extremely slow and also differs from how ComfyUI typically handles large
models (swap blocks/layers to GPU just-in-time).
This wrapper keeps the full model on CPU, but temporarily moves selected
submodules (embeddings, blocks, head) to GPU for computation and then back to CPU.
This allows "layer-by-layer" GPU execution on limited VRAM.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import torch
import torch.nn as nn
@dataclass
class _OffloadManager:
device: torch.device
empty_cache_every: int = 8
_calls: int = 0
def after_module(self):
if self.device.type != "cuda":
return
self._calls += 1
if self.empty_cache_every > 0 and (self._calls % self.empty_cache_every == 0):
torch.cuda.empty_cache()
class _OffloadModule(nn.Module):
"""
Wrap a submodule so it is moved to GPU only for its forward pass.
"""
def __init__(self, module: nn.Module, manager: _OffloadManager):
super().__init__()
self.module = module
self.manager = manager
def forward(self, *args, **kwargs):
dev = self.manager.device
if dev.type == "cuda":
self.module.to(dev)
try:
# Match input dtypes to module weights (common with quantized / bf16 checkpoints).
# PyTorch Linear requires mat1/mat2 dtypes to match.
param = next(self.module.parameters(), None)
target_dtype = param.dtype if param is not None else None
def convert(obj):
if isinstance(obj, torch.Tensor):
if (
target_dtype is not None
and obj.is_floating_point()
and obj.dtype != target_dtype
):
return obj.to(dtype=target_dtype)
return obj
# Containers: be careful with NamedTuple-like objects (e.g. VideoSize)
# which are `tuple` subclasses that must be constructed with positional args.
if isinstance(obj, list):
return [convert(x) for x in obj]
if isinstance(obj, tuple):
# Plain tuples
if type(obj) is tuple:
return tuple(convert(x) for x in obj)
# NamedTuple / tuple subclass (e.g. VideoSize)
if hasattr(obj, "_fields"):
return type(obj)(*(convert(x) for x in obj))
# Unknown tuple subclass: safest is to return as-is to avoid constructor issues.
return obj
if isinstance(obj, dict):
return {k: convert(v) for k, v in obj.items()}
return obj
args = convert(args)
kwargs = convert(kwargs)
out = self.module(*args, **kwargs)
finally:
self.module.to("cpu")
self.manager.after_module()
return out
return self.module(*args, **kwargs)
def to(self, device):
# Keep weights on CPU; only update target execution device.
self.manager.device = torch.device(device)
return self
def cpu(self):
self.manager.device = torch.device("cpu")
return self
def cuda(self, device: Optional[int] = None):
idx = 0 if device is None else int(device)
self.manager.device = torch.device(f"cuda:{idx}")
return self
class LayerwiseGPUOffloadWrapper(nn.Module):
"""
Wrap a WAN diffusion model to run block-by-block on GPU.
The underlying model stays on CPU. Selected modules are replaced with
`_OffloadModule` which transfers weights to GPU just-in-time.
"""
def __init__(self, model: nn.Module, device: torch.device, empty_cache_every: int = 8):
super().__init__()
self.model = model
self.manager = _OffloadManager(device=torch.device(device), empty_cache_every=empty_cache_every)
self._apply_wrapping()
def _wrap_attr(self, attr: str):
if hasattr(self.model, attr):
mod = getattr(self.model, attr)
if isinstance(mod, nn.Module) and not isinstance(mod, _OffloadModule):
setattr(self.model, attr, _OffloadModule(mod, self.manager))
def _apply_wrapping(self):
# Keep model on CPU.
self.model.to("cpu")
self.model.eval()
# Modules used early in forward (need to run on GPU if inputs are on GPU)
for attr in [
"patch_embedding",
"time_embedding",
"time_projection",
"text_embedding",
"rope_position_embedding",
"img_emb", # i2v / flf2v
"head",
]:
self._wrap_attr(attr)
# Transformer blocks
if hasattr(self.model, "blocks"):
blocks = getattr(self.model, "blocks")
# ModuleList or list
if isinstance(blocks, nn.ModuleList):
for i in range(len(blocks)):
blocks[i] = _OffloadModule(blocks[i], self.manager)
elif isinstance(blocks, (list, tuple)):
new_blocks = []
for b in blocks:
new_blocks.append(_OffloadModule(b, self.manager) if isinstance(b, nn.Module) else b)
try:
setattr(self.model, "blocks", nn.ModuleList(new_blocks))
except Exception:
setattr(self.model, "blocks", new_blocks)
def forward(self, *args, **kwargs):
# Delegate to underlying model; wrapped submodules handle transfers.
return self.model(*args, **kwargs)
def __call__(self, *args, **kwargs):
return self.forward(*args, **kwargs)
def to(self, device):
# Keep model on CPU; update execution device for offloaded submodules.
self.manager.device = torch.device(device)
return self
def cpu(self):
self.manager.device = torch.device("cpu")
return self
def cuda(self, device=None):
idx = 0 if device is None else int(device)
self.manager.device = torch.device(f"cuda:{idx}")
return self
def eval(self):
self.model.eval()
return self