chore: import upstream snapshot with attribution
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""
|
||||
Init file for the model loader.
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
|
||||
from invokeai.backend.model_manager.load.load_base import LoadedModel, LoadedModelWithoutConfig, ModelLoaderBase
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry, ModelLoaderRegistryBase
|
||||
|
||||
# This registers the subclasses that implement loaders of specific model types
|
||||
loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*.py") if x.stem != "__init__"]
|
||||
for module in loaders:
|
||||
import_module(f"{__package__}.model_loaders.{module}")
|
||||
|
||||
__all__ = [
|
||||
"LoadedModel",
|
||||
"LoadedModelWithoutConfig",
|
||||
"ModelCache",
|
||||
"ModelLoaderBase",
|
||||
"ModelLoader",
|
||||
"ModelLoaderRegistryBase",
|
||||
"ModelLoaderRegistry",
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""
|
||||
Base class for model loading in InvokeAI.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord
|
||||
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
|
||||
CachedModelWithPartialLoad,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType
|
||||
|
||||
|
||||
class LoadedModelWithoutConfig:
|
||||
"""Context manager object that mediates transfer from RAM<->VRAM.
|
||||
|
||||
This is a context manager object that has two distinct APIs:
|
||||
|
||||
1. Older API (deprecated):
|
||||
Use the LoadedModel object directly as a context manager. It will move the model into VRAM (on CUDA devices), and
|
||||
return the model in a form suitable for passing to torch.
|
||||
Example:
|
||||
```
|
||||
loaded_model_= loader.get_model_by_key('f13dd932', SubModelType('vae'))
|
||||
with loaded_model as vae:
|
||||
image = vae.decode(latents)[0]
|
||||
```
|
||||
|
||||
2. Newer API (recommended):
|
||||
Call the LoadedModel's `model_on_device()` method in a context. It returns a tuple consisting of a copy of the
|
||||
model's state dict in CPU RAM followed by a copy of the model in VRAM. The state dict is provided to allow LoRAs and
|
||||
other model patchers to return the model to its unpatched state without expensive copy and restore operations.
|
||||
|
||||
Example:
|
||||
```
|
||||
loaded_model_= loader.get_model_by_key('f13dd932', SubModelType('vae'))
|
||||
with loaded_model.model_on_device() as (state_dict, vae):
|
||||
image = vae.decode(latents)[0]
|
||||
```
|
||||
|
||||
The state_dict should be treated as a read-only object and never modified. Also be aware that some loadable models
|
||||
do not have a state_dict, in which case this value will be None.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_record: CacheRecord, cache: ModelCache):
|
||||
self._cache_record = cache_record
|
||||
self._cache = cache
|
||||
|
||||
def __enter__(self) -> AnyModel:
|
||||
self._cache.lock(self._cache_record, None)
|
||||
try:
|
||||
self.repair_required_tensors_on_device()
|
||||
return self.model
|
||||
except Exception:
|
||||
self._cache.unlock(self._cache_record)
|
||||
raise
|
||||
|
||||
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
||||
self._cache.unlock(self._cache_record)
|
||||
|
||||
@contextmanager
|
||||
def model_on_device(
|
||||
self, working_mem_bytes: Optional[int] = None
|
||||
) -> Generator[Tuple[Optional[Dict[str, torch.Tensor]], AnyModel], None, None]:
|
||||
"""Return a tuple consisting of the model's state dict (if it exists) and the locked model on execution device.
|
||||
|
||||
:param working_mem_bytes: The amount of working memory to keep available on the compute device when loading the
|
||||
model.
|
||||
"""
|
||||
self._cache.lock(self._cache_record, working_mem_bytes)
|
||||
try:
|
||||
self.repair_required_tensors_on_device()
|
||||
yield (self._cache_record.cached_model.get_cpu_state_dict(), self._cache_record.cached_model.model)
|
||||
finally:
|
||||
self._cache.unlock(self._cache_record)
|
||||
|
||||
@property
|
||||
def model(self) -> AnyModel:
|
||||
"""Return the model without locking it."""
|
||||
return self._cache_record.cached_model.model
|
||||
|
||||
def repair_required_tensors_on_device(self) -> int:
|
||||
"""Repair required tensors that should be resident on the cached model's execution device."""
|
||||
cached_model = self._cache_record.cached_model
|
||||
if not isinstance(cached_model, CachedModelWithPartialLoad):
|
||||
return 0
|
||||
return cached_model.repair_required_tensors_on_compute_device()
|
||||
|
||||
|
||||
class LoadedModel(LoadedModelWithoutConfig):
|
||||
"""Context manager object that mediates transfer from RAM<->VRAM."""
|
||||
|
||||
def __init__(self, config: Optional[AnyModelConfig], cache_record: CacheRecord, cache: ModelCache):
|
||||
super().__init__(cache_record=cache_record, cache=cache)
|
||||
self.config = config
|
||||
|
||||
|
||||
class ModelLoaderBase(ABC):
|
||||
"""Abstract base class for loading models into RAM/VRAM."""
|
||||
|
||||
@abstractmethod
|
||||
def __init__(
|
||||
self,
|
||||
app_config: InvokeAIAppConfig,
|
||||
logger: Logger,
|
||||
ram_cache: ModelCache,
|
||||
):
|
||||
"""Initialize the loader."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel:
|
||||
"""
|
||||
Return a model given its confguration.
|
||||
|
||||
Given a model identified in the model configuration backend,
|
||||
return a ModelInfo object that can be used to retrieve the model.
|
||||
|
||||
:param model_config: Model configuration, as returned by ModelConfigRecordStore
|
||||
:param submodel_type: an ModelType enum indicating the portion of
|
||||
the model to retrieve (e.g. ModelType.Vae)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_size_fs(
|
||||
self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None
|
||||
) -> int:
|
||||
"""Return size in bytes of the model, calculated before loading."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def ram_cache(self) -> ModelCache:
|
||||
"""Return the ram cache associated with this loader."""
|
||||
pass
|
||||
@@ -0,0 +1,312 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Default implementation of model loading in InvokeAI."""
|
||||
|
||||
import re
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase
|
||||
from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord
|
||||
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache, get_model_cache_key
|
||||
from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs
|
||||
from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
|
||||
# Layer classes that benefit from FP8 storage. Mirrors diffusers'
|
||||
# `_GO_LC_SUPPORTED_PYTORCH_LAYERS` so the plain-nn.Module fallback path makes the same
|
||||
# precision/quality trade-offs as the ModelMixin path. Notably excludes norm and embedding
|
||||
# wrapper modules — those are handled by their direct param types (Embedding is included
|
||||
# but pos_embed/patch_embed are filtered by `_FP8_DEFAULT_SKIP_PATTERNS`).
|
||||
_FP8_SUPPORTED_PYTORCH_LAYERS: tuple[type[torch.nn.Module], ...] = (
|
||||
torch.nn.Linear,
|
||||
torch.nn.Conv1d,
|
||||
torch.nn.Conv2d,
|
||||
torch.nn.Conv3d,
|
||||
torch.nn.ConvTranspose1d,
|
||||
torch.nn.ConvTranspose2d,
|
||||
torch.nn.ConvTranspose3d,
|
||||
torch.nn.Embedding,
|
||||
)
|
||||
|
||||
# Module-path regexes (matched against `named_modules()` dotted paths) for precision-sensitive
|
||||
# layers that should never be cast to FP8. Mirrors diffusers' `DEFAULT_SKIP_MODULES_PATTERN`
|
||||
# — without these, FLUX RMSNorm.scale and similar tiny learned scalars get crushed to FP8 and
|
||||
# inference quality degrades. Includes anything named `norm`, position/patch embeddings, and
|
||||
# the in/out projection of transformer blocks.
|
||||
_FP8_DEFAULT_SKIP_PATTERNS: tuple[str, ...] = (
|
||||
"pos_embed",
|
||||
"patch_embed",
|
||||
"norm",
|
||||
r"^proj_in$",
|
||||
r"^proj_out$",
|
||||
)
|
||||
|
||||
|
||||
# TO DO: The loader is not thread safe!
|
||||
class ModelLoader(ModelLoaderBase):
|
||||
"""Default implementation of ModelLoaderBase."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_config: InvokeAIAppConfig,
|
||||
logger: Logger,
|
||||
ram_cache: ModelCache,
|
||||
):
|
||||
"""Initialize the loader."""
|
||||
self._app_config = app_config
|
||||
self._logger = logger
|
||||
self._ram_cache = ram_cache
|
||||
self._torch_dtype = TorchDevice.choose_torch_dtype()
|
||||
self._torch_device = TorchDevice.choose_torch_device()
|
||||
|
||||
def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel:
|
||||
"""
|
||||
Return a model given its configuration.
|
||||
|
||||
Given a model's configuration as returned by the ModelRecordConfigStore service,
|
||||
return a LoadedModel object that can be used for inference.
|
||||
|
||||
:param model config: Configuration record for this model
|
||||
:param submodel_type: an ModelType enum indicating the portion of
|
||||
the model to retrieve (e.g. ModelType.Vae)
|
||||
"""
|
||||
model_path = self._get_model_path(model_config)
|
||||
|
||||
if not model_path.exists():
|
||||
raise FileNotFoundError(f"Files for model '{model_config.name}' not found at {model_path}")
|
||||
|
||||
with skip_torch_weight_init():
|
||||
cache_record = self._load_and_cache(model_config, submodel_type)
|
||||
return LoadedModel(config=model_config, cache_record=cache_record, cache=self._ram_cache)
|
||||
|
||||
@property
|
||||
def ram_cache(self) -> ModelCache:
|
||||
"""Return the ram cache associated with this loader."""
|
||||
return self._ram_cache
|
||||
|
||||
def _get_model_path(self, config: AnyModelConfig) -> Path:
|
||||
model_base = self._app_config.models_path
|
||||
return (model_base / config.path).resolve()
|
||||
|
||||
def _get_execution_device(
|
||||
self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None
|
||||
) -> Optional[torch.device]:
|
||||
"""Determine the execution device for a model based on its configuration.
|
||||
|
||||
CPU-only execution is only applied to text encoder submodels to save VRAM while keeping
|
||||
the denoiser on GPU for performance. Conditioning tensors are moved to GPU after encoding.
|
||||
|
||||
Returns:
|
||||
torch.device("cpu") if the model should run on CPU only, None otherwise (use cache default).
|
||||
"""
|
||||
# Check if this is a text encoder submodel of a main model with cpu_only setting
|
||||
if hasattr(config, "default_settings") and config.default_settings is not None:
|
||||
if hasattr(config.default_settings, "cpu_only") and config.default_settings.cpu_only is True:
|
||||
# Only apply CPU execution to text encoder submodels
|
||||
if submodel_type in [SubModelType.TextEncoder, SubModelType.TextEncoder2, SubModelType.TextEncoder3]:
|
||||
return torch.device("cpu")
|
||||
|
||||
# Check if this is a standalone text encoder config with cpu_only field (T5Encoder, Qwen3Encoder, etc.)
|
||||
if hasattr(config, "cpu_only") and config.cpu_only is True:
|
||||
return torch.device("cpu")
|
||||
|
||||
return None
|
||||
|
||||
def _load_and_cache(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> CacheRecord:
|
||||
stats_name = ":".join([config.base, config.type, config.name, (submodel_type or "")])
|
||||
try:
|
||||
return self._ram_cache.get(key=get_model_cache_key(config.key, submodel_type), stats_name=stats_name)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
config.path = str(self._get_model_path(config))
|
||||
self._ram_cache.make_room(self.get_size_fs(config, Path(config.path), submodel_type))
|
||||
loaded_model = self._load_model(config, submodel_type)
|
||||
|
||||
# Determine execution device from model config, considering submodel type
|
||||
execution_device = self._get_execution_device(config, submodel_type)
|
||||
|
||||
self._ram_cache.put(
|
||||
get_model_cache_key(config.key, submodel_type),
|
||||
model=loaded_model,
|
||||
execution_device=execution_device,
|
||||
)
|
||||
|
||||
return self._ram_cache.get(key=get_model_cache_key(config.key, submodel_type), stats_name=stats_name)
|
||||
|
||||
def get_size_fs(
|
||||
self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None
|
||||
) -> int:
|
||||
"""Get the size of the model on disk."""
|
||||
return calc_model_size_by_fs(
|
||||
model_path=model_path,
|
||||
subfolder=submodel_type.value if submodel_type else None,
|
||||
variant=config.repo_variant if isinstance(config, Diffusers_Config_Base) else None,
|
||||
)
|
||||
|
||||
def _should_use_fp8(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> bool:
|
||||
"""Check if FP8 layerwise casting should be applied to a model."""
|
||||
# FP8 storage only works on CUDA
|
||||
if self._torch_device.type != "cuda":
|
||||
return False
|
||||
|
||||
# Z-Image has dtype mismatch issues with diffusers' layerwise casting
|
||||
# (skipped modules produce bf16, hooked modules expect fp16).
|
||||
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
|
||||
|
||||
if hasattr(config, "base") and config.base == BaseModelType.ZImage:
|
||||
return False
|
||||
|
||||
# VAEs are excluded — fp8 storage causes noticeable quality degradation in decode.
|
||||
if hasattr(config, "type") and config.type == ModelType.VAE:
|
||||
return False
|
||||
|
||||
# LoRAs (including ControlLoRA) are excluded — they are not run as a standalone forward pass,
|
||||
# they are patched into a base model, so the layerwise-casting hooks would never fire. The
|
||||
# toggle is also hidden in the UI for ControlLoRA; this guard handles legacy persisted values.
|
||||
if hasattr(config, "type") and config.type in (ModelType.LoRA, ModelType.ControlLoRa):
|
||||
return False
|
||||
|
||||
# Don't apply FP8 to text encoders, tokenizers, schedulers, VAEs, etc.
|
||||
_excluded_submodel_types = {
|
||||
SubModelType.TextEncoder,
|
||||
SubModelType.TextEncoder2,
|
||||
SubModelType.TextEncoder3,
|
||||
SubModelType.Tokenizer,
|
||||
SubModelType.Tokenizer2,
|
||||
SubModelType.Tokenizer3,
|
||||
SubModelType.Scheduler,
|
||||
SubModelType.SafetyChecker,
|
||||
SubModelType.VAE,
|
||||
SubModelType.VAEDecoder,
|
||||
SubModelType.VAEEncoder,
|
||||
}
|
||||
if submodel_type in _excluded_submodel_types:
|
||||
return False
|
||||
|
||||
# Check default_settings.fp8_storage (Main models, ControlNet)
|
||||
if hasattr(config, "default_settings") and config.default_settings is not None:
|
||||
if hasattr(config.default_settings, "fp8_storage") and config.default_settings.fp8_storage is True:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _apply_fp8_layerwise_casting(
|
||||
self, model: AnyModel, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None
|
||||
) -> AnyModel:
|
||||
"""Apply FP8 layerwise casting to a model if enabled in its config."""
|
||||
if not self._should_use_fp8(config, submodel_type):
|
||||
return model
|
||||
|
||||
storage_dtype = torch.float8_e4m3fn
|
||||
compute_dtype = self._torch_dtype
|
||||
|
||||
# Detect the model's current dtype to use as compute dtype, since models
|
||||
# (e.g. Flux) may require a specific dtype (bf16) that differs from the global torch dtype (fp16).
|
||||
if isinstance(model, torch.nn.Module):
|
||||
first_param = next(model.parameters(), None)
|
||||
if first_param is not None:
|
||||
compute_dtype = first_param.dtype
|
||||
|
||||
# We use our own hook-based path for every nn.Module — including diffusers ModelMixin —
|
||||
# rather than `model.enable_layerwise_casting()`. Diffusers' LayerwiseCastingHook installs
|
||||
# an instance-level `forward` attribute that captures the original `Linear.forward` in a
|
||||
# closure. `ModelCache.put()` later runs `apply_custom_layers_to_model`, which constructs a
|
||||
# new `CustomLinear` sharing the original Linear's `__dict__` — so the diffusers wrapper
|
||||
# carries over and routes calls back to the captured original forward, silently bypassing
|
||||
# `CustomLinear.forward` and its `cast_to_device` autocast. With partial loading (e.g. FLUX.2
|
||||
# Klein 9B) some weights stay on CPU, the diffusers pre_forward only casts dtype, and
|
||||
# `F.linear` then sees input on cuda and weight on cpu. Our `register_forward_pre_hook` /
|
||||
# `register_forward_hook` path fires around `nn.Module._call_impl` without replacing
|
||||
# `forward`, so `CustomLinear.forward` is still reached.
|
||||
if isinstance(model, torch.nn.Module):
|
||||
self._apply_fp8_to_nn_module(model, storage_dtype=storage_dtype, compute_dtype=compute_dtype)
|
||||
else:
|
||||
return model
|
||||
|
||||
param_bytes = sum(p.nelement() * p.element_size() for p in model.parameters())
|
||||
self._logger.info(
|
||||
f"FP8 layerwise casting enabled for {config.name} "
|
||||
f"(storage=float8_e4m3fn, compute={compute_dtype}, "
|
||||
f"param_size={param_bytes / (1024**2):.0f}MB)"
|
||||
)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def _apply_fp8_to_nn_module(model: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype) -> None:
|
||||
"""Apply FP8 layerwise casting to a plain nn.Module.
|
||||
|
||||
Mirrors diffusers' `apply_layerwise_casting` semantics: only the layer classes in
|
||||
`_FP8_SUPPORTED_PYTORCH_LAYERS` are cast, and modules whose dotted path matches any of
|
||||
`_FP8_DEFAULT_SKIP_PATTERNS` (norm, pos_embed, patch_embed, proj_in/out) are skipped.
|
||||
Without the skip list, precision-sensitive tiny learned scalars (e.g. FLUX RMSNorm.scale)
|
||||
get crushed to FP8 and quality degrades noticeably.
|
||||
"""
|
||||
for module_name, module in model.named_modules():
|
||||
if not isinstance(module, _FP8_SUPPORTED_PYTORCH_LAYERS):
|
||||
continue
|
||||
if any(re.search(pattern, module_name) for pattern in _FP8_DEFAULT_SKIP_PATTERNS):
|
||||
continue
|
||||
params = list(module.parameters(recurse=False))
|
||||
if not params:
|
||||
continue
|
||||
|
||||
for param in params:
|
||||
param.data = param.data.to(storage_dtype)
|
||||
|
||||
ModelLoader._wrap_forward_with_fp8_cast(module, storage_dtype, compute_dtype)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_forward_with_fp8_cast(
|
||||
module: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype
|
||||
) -> None:
|
||||
"""Register pre/post forward hooks that cast params to compute dtype on entry and back
|
||||
to storage dtype on exit.
|
||||
|
||||
We use hooks (rather than overriding `module.forward`) for two reasons:
|
||||
|
||||
1. **Correct dispatch after `apply_custom_layers_to_model`.** `ModelCache.put()` calls
|
||||
`apply_custom_layers_to_model`, which creates a NEW `CustomLinear` instance and
|
||||
shares the original `Linear.__dict__` (see `wrap_custom_layer`). Anything stored in
|
||||
that dict — including an instance-level `forward` attribute — gets carried over to
|
||||
the new object. An overridden `forward` would close over the OLD instance, so calls
|
||||
to the new `CustomLinear` would silently route to `Linear.forward(old_instance, ...)`
|
||||
and bypass the LoRA-patch-aware branch in `CustomLinear.forward`. Hooks, by contrast,
|
||||
live in `_forward_hooks` / `_forward_pre_hooks` and are dispatched by
|
||||
`nn.Module.__call__` with the *actual* called instance — so they run on the new
|
||||
`CustomLinear` and the class's `forward` is still resolved normally.
|
||||
|
||||
2. **Exception safety.** `register_forward_hook(..., always_call=True)` fires the
|
||||
post-hook even when `forward` raises. The plain pre-hook/post-hook pair without
|
||||
`always_call` would leave params in compute dtype on exception, defeating FP8
|
||||
storage savings and making cache size accounting stale.
|
||||
"""
|
||||
|
||||
def pre_hook(mod: torch.nn.Module, _args: object) -> None:
|
||||
for p in mod.parameters(recurse=False):
|
||||
p.data = p.data.to(compute_dtype)
|
||||
|
||||
def post_hook(mod: torch.nn.Module, _args: object, _output: object) -> None:
|
||||
for p in mod.parameters(recurse=False):
|
||||
p.data = p.data.to(storage_dtype)
|
||||
|
||||
module.register_forward_pre_hook(pre_hook)
|
||||
module.register_forward_hook(post_hook, always_call=True)
|
||||
|
||||
# This needs to be implemented in the subclass
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,100 @@
|
||||
import gc
|
||||
from typing import Optional
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
from typing_extensions import Self
|
||||
|
||||
from invokeai.backend.model_manager.util.libc_util import LibcUtil, Struct_mallinfo2
|
||||
|
||||
GB = 2**30 # 1 GB
|
||||
|
||||
|
||||
class MemorySnapshot:
|
||||
"""A snapshot of RAM and VRAM usage. All values are in bytes."""
|
||||
|
||||
def __init__(self, process_ram: int, vram: Optional[int], malloc_info: Optional[Struct_mallinfo2]):
|
||||
"""Initialize a MemorySnapshot.
|
||||
|
||||
Most of the time, `MemorySnapshot` will be constructed with `MemorySnapshot.capture()`.
|
||||
|
||||
Args:
|
||||
process_ram (int): CPU RAM used by the current process.
|
||||
vram (Optional[int]): VRAM used by torch.
|
||||
malloc_info (Optional[Struct_mallinfo2]): Malloc info obtained from LibcUtil.
|
||||
"""
|
||||
self.process_ram = process_ram
|
||||
self.vram = vram
|
||||
self.malloc_info = malloc_info
|
||||
|
||||
@classmethod
|
||||
def capture(cls, run_garbage_collector: bool = True) -> Self:
|
||||
"""Capture and return a MemorySnapshot.
|
||||
|
||||
Note: This function has significant overhead, particularly if `run_garbage_collector == True`.
|
||||
|
||||
Args:
|
||||
run_garbage_collector (bool, optional): If true, gc.collect() will be run before checking the process RAM
|
||||
usage. Defaults to True.
|
||||
|
||||
Returns:
|
||||
MemorySnapshot
|
||||
"""
|
||||
if run_garbage_collector:
|
||||
gc.collect()
|
||||
|
||||
# According to the psutil docs (https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info), rss is
|
||||
# supported on all platforms.
|
||||
process_ram = psutil.Process().memory_info().rss
|
||||
|
||||
if torch.cuda.is_available():
|
||||
vram = torch.cuda.memory_allocated()
|
||||
else:
|
||||
# TODO: We could add support for mps.current_allocated_memory() as well. Leaving out for now until we have
|
||||
# time to test it properly.
|
||||
vram = None
|
||||
|
||||
try:
|
||||
malloc_info = LibcUtil().mallinfo2()
|
||||
except (OSError, AttributeError):
|
||||
# OSError: This is expected in environments that do not have the 'libc.so.6' shared library.
|
||||
# AttributeError: This is expected in environments that have `libc.so.6` but do not have the `mallinfo2` (e.g. glibc < 2.33)
|
||||
# TODO: Does `mallinfo` work?
|
||||
malloc_info = None
|
||||
|
||||
return cls(process_ram, vram, malloc_info)
|
||||
|
||||
|
||||
def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: Optional[MemorySnapshot]) -> str:
|
||||
"""Get a pretty string describing the difference between two `MemorySnapshot`s."""
|
||||
|
||||
def get_msg_line(prefix: str, val1: int, val2: int) -> str:
|
||||
diff = val2 - val1
|
||||
return f"{prefix: <30} ({(diff / GB):+5.3f}): {(val1 / GB):5.3f}GB -> {(val2 / GB):5.3f}GB\n"
|
||||
|
||||
msg = ""
|
||||
|
||||
if snapshot_1 is None or snapshot_2 is None:
|
||||
return msg
|
||||
|
||||
msg += get_msg_line("Process RAM", snapshot_1.process_ram, snapshot_2.process_ram)
|
||||
|
||||
if snapshot_1.malloc_info is not None and snapshot_2.malloc_info is not None:
|
||||
msg += get_msg_line("libc mmap allocated", snapshot_1.malloc_info.hblkhd, snapshot_2.malloc_info.hblkhd)
|
||||
|
||||
msg += get_msg_line("libc arena used", snapshot_1.malloc_info.uordblks, snapshot_2.malloc_info.uordblks)
|
||||
|
||||
msg += get_msg_line("libc arena free", snapshot_1.malloc_info.fordblks, snapshot_2.malloc_info.fordblks)
|
||||
|
||||
libc_total_allocated_1 = snapshot_1.malloc_info.arena + snapshot_1.malloc_info.hblkhd
|
||||
libc_total_allocated_2 = snapshot_2.malloc_info.arena + snapshot_2.malloc_info.hblkhd
|
||||
msg += get_msg_line("libc total allocated", libc_total_allocated_1, libc_total_allocated_2)
|
||||
|
||||
libc_total_used_1 = snapshot_1.malloc_info.uordblks + snapshot_1.malloc_info.hblkhd
|
||||
libc_total_used_2 = snapshot_2.malloc_info.uordblks + snapshot_2.malloc_info.hblkhd
|
||||
msg += get_msg_line("libc total used", libc_total_used_1, libc_total_used_2)
|
||||
|
||||
if snapshot_1.vram is not None and snapshot_2.vram is not None:
|
||||
msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram)
|
||||
|
||||
return msg
|
||||
@@ -0,0 +1,38 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import (
|
||||
CachedModelOnlyFullLoad,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
|
||||
CachedModelWithPartialLoad,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheRecord:
|
||||
"""A class that represents a model in the model cache."""
|
||||
|
||||
# Cache key.
|
||||
key: str
|
||||
# Model in memory.
|
||||
cached_model: CachedModelWithPartialLoad | CachedModelOnlyFullLoad
|
||||
_locks: int = 0
|
||||
# Set by ModelCache.drop_model() when the entry was locked at invalidation time.
|
||||
# ModelCache.unlock() evicts the entry as soon as the last lock releases so a setting
|
||||
# change (e.g. fp8_storage toggled during an in-flight generation) takes effect on the
|
||||
# next load instead of silently being ignored.
|
||||
is_stale: bool = False
|
||||
|
||||
def lock(self) -> None:
|
||||
"""Lock this record."""
|
||||
self._locks += 1
|
||||
|
||||
def unlock(self) -> None:
|
||||
"""Unlock this record."""
|
||||
self._locks -= 1
|
||||
assert self._locks >= 0
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
"""Return true if record is locked."""
|
||||
return self._locks > 0
|
||||
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheStats(object):
|
||||
"""Collect statistics on cache performance."""
|
||||
|
||||
hits: int = 0 # cache hits
|
||||
misses: int = 0 # cache misses
|
||||
high_watermark: int = 0 # amount of cache used
|
||||
in_cache: int = 0 # number of models in cache
|
||||
cleared: int = 0 # number of models cleared to make space
|
||||
cache_size: int = 0 # total size of cache
|
||||
loaded_model_sizes: Dict[str, int] = field(default_factory=dict)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
class CachedModelOnlyFullLoad:
|
||||
"""A wrapper around a PyTorch model to handle full loads and unloads between the CPU and the compute device.
|
||||
Note: "VRAM" is used throughout this class to refer to the memory on the compute device. It could be CUDA memory,
|
||||
MPS memory, etc.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, model: torch.nn.Module | Any, compute_device: torch.device, total_bytes: int, keep_ram_copy: bool = False
|
||||
):
|
||||
"""Initialize a CachedModelOnlyFullLoad.
|
||||
Args:
|
||||
model (torch.nn.Module | Any): The model to wrap. Should be on the CPU.
|
||||
compute_device (torch.device): The compute device to move the model to.
|
||||
total_bytes (int): The total size (in bytes) of all the weights in the model.
|
||||
keep_ram_copy (bool): Whether to keep a read-only copy of the model's state dict in RAM. Keeping a RAM copy
|
||||
increases RAM usage, but speeds up model offload from VRAM and LoRA patching (assuming there is
|
||||
sufficient RAM).
|
||||
"""
|
||||
# model is often a torch.nn.Module, but could be any model type. Throughout this class, we handle both cases.
|
||||
self._model = model
|
||||
self._compute_device = compute_device
|
||||
self._offload_device = torch.device("cpu")
|
||||
|
||||
# A CPU read-only copy of the model's state dict.
|
||||
self._cpu_state_dict: dict[str, torch.Tensor] | None = None
|
||||
if isinstance(model, torch.nn.Module) and keep_ram_copy:
|
||||
self._cpu_state_dict = model.state_dict()
|
||||
|
||||
self._total_bytes = total_bytes
|
||||
self._is_in_vram = False
|
||||
|
||||
@property
|
||||
def model(self) -> torch.nn.Module:
|
||||
return self._model
|
||||
|
||||
def get_cpu_state_dict(self) -> dict[str, torch.Tensor] | None:
|
||||
"""Get a read-only copy of the model's state dict in RAM."""
|
||||
# TODO(ryand): Document this better.
|
||||
return self._cpu_state_dict
|
||||
|
||||
def total_bytes(self) -> int:
|
||||
"""Get the total size (in bytes) of all the weights in the model."""
|
||||
return self._total_bytes
|
||||
|
||||
def cur_vram_bytes(self) -> int:
|
||||
"""Get the size (in bytes) of the weights that are currently in VRAM."""
|
||||
if self._is_in_vram:
|
||||
return self._total_bytes
|
||||
else:
|
||||
return 0
|
||||
|
||||
def is_in_vram(self) -> bool:
|
||||
"""Return true if the model is currently in VRAM."""
|
||||
return self._is_in_vram
|
||||
|
||||
@property
|
||||
def compute_device(self) -> torch.device:
|
||||
"""Return the compute device for this model."""
|
||||
return self._compute_device
|
||||
|
||||
def full_load_to_vram(self) -> int:
|
||||
"""Load all weights into VRAM (if supported by the model).
|
||||
Returns:
|
||||
The number of bytes loaded into VRAM.
|
||||
"""
|
||||
if self._is_in_vram:
|
||||
# Already in VRAM.
|
||||
return 0
|
||||
|
||||
if not hasattr(self._model, "to"):
|
||||
# Model doesn't support moving to a device.
|
||||
return 0
|
||||
|
||||
if self._cpu_state_dict is not None:
|
||||
new_state_dict: dict[str, torch.Tensor] = {}
|
||||
for k, v in self._cpu_state_dict.items():
|
||||
new_state_dict[k] = v.to(self._compute_device, copy=True)
|
||||
self._model.load_state_dict(new_state_dict, assign=True)
|
||||
|
||||
check_for_gguf = hasattr(self._model, "state_dict") and self._model.state_dict().get("img_in.weight")
|
||||
if isinstance(check_for_gguf, GGMLTensor):
|
||||
old_value = torch.__future__.get_overwrite_module_params_on_conversion()
|
||||
torch.__future__.set_overwrite_module_params_on_conversion(True)
|
||||
self._model.to(self._compute_device)
|
||||
torch.__future__.set_overwrite_module_params_on_conversion(old_value)
|
||||
else:
|
||||
self._model.to(self._compute_device)
|
||||
|
||||
self._is_in_vram = True
|
||||
return self._total_bytes
|
||||
|
||||
def full_unload_from_vram(self) -> int:
|
||||
"""Unload all weights from VRAM.
|
||||
Returns:
|
||||
The number of bytes unloaded from VRAM.
|
||||
"""
|
||||
if not self._is_in_vram:
|
||||
# Already in RAM.
|
||||
return 0
|
||||
|
||||
if self._cpu_state_dict is not None:
|
||||
self._model.load_state_dict(self._cpu_state_dict, assign=True)
|
||||
|
||||
check_for_gguf = hasattr(self._model, "state_dict") and self._model.state_dict().get("img_in.weight")
|
||||
if isinstance(check_for_gguf, GGMLTensor):
|
||||
old_value = torch.__future__.get_overwrite_module_params_on_conversion()
|
||||
torch.__future__.set_overwrite_module_params_on_conversion(True)
|
||||
self._model.to(self._offload_device)
|
||||
torch.__future__.set_overwrite_module_params_on_conversion(old_value)
|
||||
else:
|
||||
self._model.to(self._offload_device)
|
||||
|
||||
self._is_in_vram = False
|
||||
return self._total_bytes
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.util.calc_tensor_size import calc_tensor_size
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
|
||||
class CachedModelWithPartialLoad:
|
||||
"""A wrapper around a PyTorch model to handle partial loads and unloads between the CPU and the compute device.
|
||||
|
||||
Note: "VRAM" is used throughout this class to refer to the memory on the compute device. It could be CUDA memory,
|
||||
MPS memory, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, model: torch.nn.Module, compute_device: torch.device, keep_ram_copy: bool = False):
|
||||
self._model = model
|
||||
self._compute_device = compute_device
|
||||
|
||||
model_state_dict = model.state_dict()
|
||||
# A CPU read-only copy of the model's state dict. Used for faster model unloads from VRAM, and to speed up LoRA
|
||||
# patching. Set to `None` if keep_ram_copy is False.
|
||||
self._cpu_state_dict: dict[str, torch.Tensor] | None = model_state_dict if keep_ram_copy else None
|
||||
|
||||
# A dictionary of the size of each tensor in the state dict.
|
||||
# HACK(ryand): We use this dictionary any time we are doing byte tracking calculations. We do this for
|
||||
# consistency in case the application code has modified the model's size (e.g. by casting to a different
|
||||
# precision). Of course, this means that we are making model cache load/unload decisions based on model size
|
||||
# data that may not be fully accurate.
|
||||
self._state_dict_bytes = {k: calc_tensor_size(v) for k, v in model_state_dict.items()}
|
||||
|
||||
self._total_bytes = sum(self._state_dict_bytes.values())
|
||||
self._cur_vram_bytes: int | None = None
|
||||
|
||||
self._modules_that_support_autocast = self._find_modules_that_support_autocast()
|
||||
self._keys_in_modules_that_do_not_support_autocast = self._find_keys_in_modules_that_do_not_support_autocast(
|
||||
model_state_dict
|
||||
)
|
||||
self._state_dict_keys_by_module_prefix = self._group_state_dict_keys_by_module_prefix(model_state_dict)
|
||||
|
||||
def _find_modules_that_support_autocast(self) -> dict[str, torch.nn.Module]:
|
||||
"""Find all modules that support autocasting."""
|
||||
return {n: m for n, m in self._model.named_modules() if isinstance(m, CustomModuleMixin)} # type: ignore
|
||||
|
||||
def _find_keys_in_modules_that_do_not_support_autocast(self, state_dict: dict[str, torch.Tensor]) -> set[str]:
|
||||
keys_in_modules_that_do_not_support_autocast: set[str] = set()
|
||||
for key in state_dict.keys():
|
||||
for module_name in self._modules_that_support_autocast.keys():
|
||||
if key.startswith(module_name):
|
||||
break
|
||||
else:
|
||||
keys_in_modules_that_do_not_support_autocast.add(key)
|
||||
return keys_in_modules_that_do_not_support_autocast
|
||||
|
||||
def _group_state_dict_keys_by_module_prefix(self, state_dict: dict[str, torch.Tensor]) -> dict[str, list[str]]:
|
||||
"""A helper function that groups state dict keys by module prefix.
|
||||
|
||||
Example:
|
||||
```
|
||||
state_dict = {
|
||||
"weight": ...,
|
||||
"module.submodule.weight": ...,
|
||||
"module.submodule.bias": ...,
|
||||
"module.other_submodule.weight": ...,
|
||||
"module.other_submodule.bias": ...,
|
||||
}
|
||||
|
||||
output = group_state_dict_keys_by_module_prefix(state_dict)
|
||||
|
||||
# The output will be:
|
||||
output = {
|
||||
"": [
|
||||
"weight",
|
||||
],
|
||||
"module.submodule": [
|
||||
"module.submodule.weight",
|
||||
"module.submodule.bias",
|
||||
],
|
||||
"module.other_submodule": [
|
||||
"module.other_submodule.weight",
|
||||
"module.other_submodule.bias",
|
||||
],
|
||||
}
|
||||
```
|
||||
"""
|
||||
state_dict_keys_by_module_prefix: dict[str, list[str]] = {}
|
||||
for key in state_dict.keys():
|
||||
split = key.rsplit(".", 1)
|
||||
# `split` will have length 1 if the root module has parameters.
|
||||
module_name = split[0] if len(split) > 1 else ""
|
||||
if module_name not in state_dict_keys_by_module_prefix:
|
||||
state_dict_keys_by_module_prefix[module_name] = []
|
||||
state_dict_keys_by_module_prefix[module_name].append(key)
|
||||
return state_dict_keys_by_module_prefix
|
||||
|
||||
def _move_non_persistent_buffers_to_device(self, device: torch.device):
|
||||
"""Move the non-persistent buffers to the target device. These buffers are not included in the state dict,
|
||||
so we need to move them manually.
|
||||
"""
|
||||
# HACK(ryand): Typically, non-persistent buffers are moved when calling module.to(device). We don't move entire
|
||||
# modules, because we manage the devices of individual tensors using the state dict. Since non-persistent
|
||||
# buffers are not included in the state dict, we need to handle them manually. The only way to do this is by
|
||||
# using private torch.nn.Module attributes.
|
||||
for module in self._model.modules():
|
||||
for name, buffer in module.named_buffers():
|
||||
if name in module._non_persistent_buffers_set:
|
||||
module._buffers[name] = buffer.to(device, copy=True)
|
||||
|
||||
def _set_autocast_enabled_in_all_modules(self, enabled: bool):
|
||||
"""Set autocast_enabled flag in all modules that support device autocasting."""
|
||||
for module in self._modules_that_support_autocast.values():
|
||||
module.set_device_autocasting_enabled(enabled)
|
||||
|
||||
@property
|
||||
def model(self) -> torch.nn.Module:
|
||||
return self._model
|
||||
|
||||
def get_cpu_state_dict(self) -> dict[str, torch.Tensor] | None:
|
||||
"""Get a read-only copy of the model's state dict in RAM."""
|
||||
# TODO(ryand): Document this better.
|
||||
return self._cpu_state_dict
|
||||
|
||||
def total_bytes(self) -> int:
|
||||
"""Get the total size (in bytes) of all the weights in the model."""
|
||||
return self._total_bytes
|
||||
|
||||
def cur_vram_bytes(self) -> int:
|
||||
"""Get the size (in bytes) of the weights that are currently in VRAM."""
|
||||
if self._cur_vram_bytes is None:
|
||||
cur_state_dict = self._model.state_dict()
|
||||
self._cur_vram_bytes = sum(
|
||||
self._state_dict_bytes[k]
|
||||
for k, v in cur_state_dict.items()
|
||||
if v.device.type == self._compute_device.type
|
||||
)
|
||||
return self._cur_vram_bytes
|
||||
|
||||
@property
|
||||
def compute_device(self) -> torch.device:
|
||||
"""Return the compute device for this model."""
|
||||
return self._compute_device
|
||||
|
||||
def full_load_to_vram(self) -> int:
|
||||
"""Load all weights into VRAM."""
|
||||
return self.partial_load_to_vram(self.total_bytes())
|
||||
|
||||
def full_unload_from_vram(self) -> int:
|
||||
"""Unload all weights from VRAM."""
|
||||
return self.partial_unload_from_vram(self.total_bytes())
|
||||
|
||||
@torch.no_grad()
|
||||
def repair_required_tensors_on_compute_device(self) -> int:
|
||||
"""Repair required non-autocast tensors that were left off the compute device.
|
||||
|
||||
This can happen if an interrupted run leaves the model in a partially inconsistent state. Any repaired device
|
||||
movement invalidates the cached VRAM accounting.
|
||||
"""
|
||||
cur_state_dict = self._model.state_dict()
|
||||
keys_to_repair = {
|
||||
key
|
||||
for key in self._keys_in_modules_that_do_not_support_autocast
|
||||
if cur_state_dict[key].device.type != self._compute_device.type
|
||||
}
|
||||
if len(keys_to_repair) == 0:
|
||||
return 0
|
||||
|
||||
self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_repair, self._compute_device)
|
||||
self._move_non_persistent_buffers_to_device(self._compute_device)
|
||||
self._cur_vram_bytes = None
|
||||
return len(keys_to_repair)
|
||||
|
||||
def _load_state_dict_with_device_conversion(
|
||||
self, state_dict: dict[str, torch.Tensor], keys_to_convert: set[str], target_device: torch.device
|
||||
):
|
||||
if self._cpu_state_dict is not None:
|
||||
# Run the fast version.
|
||||
self._load_state_dict_with_fast_device_conversion(
|
||||
state_dict=state_dict,
|
||||
keys_to_convert=keys_to_convert,
|
||||
target_device=target_device,
|
||||
cpu_state_dict=self._cpu_state_dict,
|
||||
)
|
||||
else:
|
||||
# Run the low-virtual-memory version.
|
||||
self._load_state_dict_with_jit_device_conversion(
|
||||
state_dict=state_dict,
|
||||
keys_to_convert=keys_to_convert,
|
||||
target_device=target_device,
|
||||
)
|
||||
|
||||
def _load_state_dict_with_jit_device_conversion(
|
||||
self,
|
||||
state_dict: dict[str, torch.Tensor],
|
||||
keys_to_convert: set[str],
|
||||
target_device: torch.device,
|
||||
):
|
||||
"""A custom state dict loading implementation with good peak memory properties.
|
||||
|
||||
This implementation has the important property that it copies parameters to the target device one module at a time
|
||||
rather than applying all of the device conversions and then calling load_state_dict(). This is done to minimize the
|
||||
peak virtual memory usage. Specifically, we want to avoid a case where we hold references to all of the CPU weights
|
||||
and CUDA weights simultaneously, because Windows will reserve virtual memory for both.
|
||||
"""
|
||||
for module_name, module in self._model.named_modules():
|
||||
module_keys = self._state_dict_keys_by_module_prefix.get(module_name, [])
|
||||
# Calculate the length of the module name prefix.
|
||||
prefix_len = len(module_name)
|
||||
if prefix_len > 0:
|
||||
prefix_len += 1
|
||||
|
||||
module_state_dict = {}
|
||||
for key in module_keys:
|
||||
if key in keys_to_convert:
|
||||
# It is important that we overwrite `state_dict[key]` to avoid keeping two copies of the same
|
||||
# parameter.
|
||||
state_dict[key] = state_dict[key].to(target_device)
|
||||
# Note that we keep parameters that have not been moved to a new device in case the module implements
|
||||
# weird custom state dict loading logic that requires all parameters to be present.
|
||||
module_state_dict[key[prefix_len:]] = state_dict[key]
|
||||
|
||||
if len(module_state_dict) > 0:
|
||||
# We set strict=False, because if `module` has both parameters and child modules, then we are loading a
|
||||
# state dict that only contains the parameters of `module` (not its children).
|
||||
# We assume that it is rare for non-leaf modules to have parameters. Calling load_state_dict() on non-leaf
|
||||
# modules will recurse through all of the children, so is a bit wasteful.
|
||||
incompatible_keys = module.load_state_dict(module_state_dict, strict=False, assign=True)
|
||||
# Missing keys are ok, unexpected keys are not.
|
||||
assert len(incompatible_keys.unexpected_keys) == 0
|
||||
|
||||
def _load_state_dict_with_fast_device_conversion(
|
||||
self,
|
||||
state_dict: dict[str, torch.Tensor],
|
||||
keys_to_convert: set[str],
|
||||
target_device: torch.device,
|
||||
cpu_state_dict: dict[str, torch.Tensor],
|
||||
):
|
||||
"""Convert parameters to the target device and load them into the model. Leverages the `cpu_state_dict` to speed
|
||||
up transfers of weights to the CPU.
|
||||
"""
|
||||
for key in keys_to_convert:
|
||||
if target_device.type == "cpu":
|
||||
state_dict[key] = cpu_state_dict[key]
|
||||
else:
|
||||
state_dict[key] = state_dict[key].to(target_device)
|
||||
|
||||
self._model.load_state_dict(state_dict, assign=True)
|
||||
|
||||
@torch.no_grad()
|
||||
def partial_load_to_vram(self, vram_bytes_to_load: int) -> int:
|
||||
"""Load more weights into VRAM without exceeding vram_bytes_to_load.
|
||||
|
||||
Returns:
|
||||
The number of bytes loaded into VRAM.
|
||||
"""
|
||||
# TODO(ryand): Handle the case where an exception is thrown while loading or unloading weights. At the very
|
||||
# least, we should reset self._cur_vram_bytes to None.
|
||||
|
||||
vram_bytes_loaded = 0
|
||||
|
||||
cur_state_dict = self._model.state_dict()
|
||||
|
||||
# Identify the keys that will be loaded into VRAM.
|
||||
keys_to_load: set[str] = set()
|
||||
|
||||
# First, process the keys that *must* be loaded into VRAM.
|
||||
for key in self._keys_in_modules_that_do_not_support_autocast:
|
||||
param = cur_state_dict[key]
|
||||
if param.device.type == self._compute_device.type:
|
||||
continue
|
||||
|
||||
keys_to_load.add(key)
|
||||
param_size = self._state_dict_bytes[key]
|
||||
vram_bytes_loaded += param_size
|
||||
|
||||
if vram_bytes_loaded > vram_bytes_to_load:
|
||||
logger = InvokeAILogger.get_logger()
|
||||
logger.warning(
|
||||
f"Loading {vram_bytes_loaded / 2**20} MB into VRAM, but only {vram_bytes_to_load / 2**20} MB were "
|
||||
"requested. This is the minimum set of weights in VRAM required to run the model."
|
||||
)
|
||||
|
||||
# Next, process the keys that can optionally be loaded into VRAM.
|
||||
fully_loaded = True
|
||||
for key, param in cur_state_dict.items():
|
||||
# Skip the keys that have already been processed above.
|
||||
if key in keys_to_load:
|
||||
continue
|
||||
|
||||
if param.device.type == self._compute_device.type:
|
||||
continue
|
||||
|
||||
param_size = self._state_dict_bytes[key]
|
||||
if vram_bytes_loaded + param_size > vram_bytes_to_load:
|
||||
# TODO(ryand): Should we just break here? If we couldn't fit this parameter into VRAM, is it really
|
||||
# worth continuing to search for a smaller parameter that would fit?
|
||||
fully_loaded = False
|
||||
continue
|
||||
|
||||
keys_to_load.add(key)
|
||||
vram_bytes_loaded += param_size
|
||||
|
||||
if len(keys_to_load) > 0:
|
||||
# We load the entire state dict, not just the parameters that changed, in case there are modules that
|
||||
# override _load_from_state_dict() and do some funky stuff that requires the entire state dict.
|
||||
# Alternatively, in the future, grouping parameters by module could probably solve this problem.
|
||||
self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_load, self._compute_device)
|
||||
|
||||
if self._cur_vram_bytes is not None:
|
||||
self._cur_vram_bytes += vram_bytes_loaded
|
||||
|
||||
if fully_loaded:
|
||||
self._set_autocast_enabled_in_all_modules(False)
|
||||
else:
|
||||
self._set_autocast_enabled_in_all_modules(True)
|
||||
|
||||
# Move all non-persistent buffers to the compute device. These are a weird edge case and do not participate in
|
||||
# the vram_bytes_loaded tracking.
|
||||
self._move_non_persistent_buffers_to_device(self._compute_device)
|
||||
|
||||
return vram_bytes_loaded
|
||||
|
||||
@torch.no_grad()
|
||||
def partial_unload_from_vram(self, vram_bytes_to_free: int, keep_required_weights_in_vram: bool = False) -> int:
|
||||
"""Unload weights from VRAM until vram_bytes_to_free bytes are freed. Or the entire model is unloaded.
|
||||
|
||||
:param keep_required_weights_in_vram: If True, any weights that must be kept in VRAM to run the model will be
|
||||
kept in VRAM.
|
||||
|
||||
Returns:
|
||||
The number of bytes unloaded from VRAM.
|
||||
"""
|
||||
vram_bytes_freed = 0
|
||||
required_weights_in_vram = 0
|
||||
|
||||
offload_device = "cpu"
|
||||
cur_state_dict = self._model.state_dict()
|
||||
|
||||
# Identify the keys that will be offloaded to CPU.
|
||||
keys_to_offload: set[str] = set()
|
||||
|
||||
for key, param in cur_state_dict.items():
|
||||
if vram_bytes_freed >= vram_bytes_to_free:
|
||||
break
|
||||
|
||||
if param.device.type == offload_device:
|
||||
continue
|
||||
|
||||
if keep_required_weights_in_vram and key in self._keys_in_modules_that_do_not_support_autocast:
|
||||
required_weights_in_vram += self._state_dict_bytes[key]
|
||||
continue
|
||||
|
||||
keys_to_offload.add(key)
|
||||
vram_bytes_freed += self._state_dict_bytes[key]
|
||||
|
||||
if len(keys_to_offload) > 0:
|
||||
self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_offload, torch.device("cpu"))
|
||||
|
||||
if self._cur_vram_bytes is not None:
|
||||
self._cur_vram_bytes -= vram_bytes_freed
|
||||
|
||||
# We may have gone from a fully-loaded model to a partially-loaded model, so we need to reapply the custom
|
||||
# layers.
|
||||
self._set_autocast_enabled_in_all_modules(True)
|
||||
return vram_bytes_freed
|
||||
@@ -0,0 +1,33 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
|
||||
@contextmanager
|
||||
def log_operation_vram_usage(operation_name: str):
|
||||
"""A helper function for tuning working memory requirements for memory-intensive ops.
|
||||
|
||||
Sample usage:
|
||||
|
||||
```python
|
||||
with log_operation_vram_usage("some_operation"):
|
||||
some_operation()
|
||||
```
|
||||
"""
|
||||
torch.cuda.synchronize()
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
max_allocated_before = torch.cuda.max_memory_allocated()
|
||||
max_reserved_before = torch.cuda.max_memory_reserved()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
torch.cuda.synchronize()
|
||||
max_allocated_after = torch.cuda.max_memory_allocated()
|
||||
max_reserved_after = torch.cuda.max_memory_reserved()
|
||||
logger = InvokeAILogger.get_logger()
|
||||
logger.info(
|
||||
f">>>{operation_name} Peak VRAM allocated: {(max_allocated_after - max_allocated_before) / 2**20} MB, "
|
||||
f"Peak VRAM reserved: {(max_reserved_after - max_reserved_before) / 2**20} MB"
|
||||
)
|
||||
@@ -0,0 +1,931 @@
|
||||
import gc
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from logging import Logger
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot
|
||||
from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord
|
||||
from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
|
||||
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import (
|
||||
CachedModelOnlyFullLoad,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import (
|
||||
CachedModelWithPartialLoad,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import (
|
||||
apply_custom_layers_to_model,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
from invokeai.backend.util.prefix_logger_adapter import PrefixedLoggerAdapter
|
||||
|
||||
# Size of a GB in bytes.
|
||||
GB = 2**30
|
||||
|
||||
# Size of a MB in bytes.
|
||||
MB = 2**20
|
||||
|
||||
|
||||
# TODO(ryand): Where should this go? The ModelCache shouldn't be concerned with submodels.
|
||||
def get_model_cache_key(model_key: str, submodel_type: Optional[SubModelType] = None) -> str:
|
||||
"""Get the cache key for a model based on the optional submodel type."""
|
||||
if submodel_type:
|
||||
return f"{model_key}:{submodel_type.value}"
|
||||
else:
|
||||
return model_key
|
||||
|
||||
|
||||
def synchronized(method: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""A decorator that applies the class's self._lock to the method."""
|
||||
|
||||
@wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
with self._lock: # Automatically acquire and release the lock
|
||||
return method(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def record_activity(method: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""A decorator that records activity after a method completes successfully.
|
||||
|
||||
Note: This decorator should be applied to methods that already hold self._lock.
|
||||
"""
|
||||
|
||||
@wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
result = method(self, *args, **kwargs)
|
||||
self._record_activity()
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheEntrySnapshot:
|
||||
cache_key: str
|
||||
total_bytes: int
|
||||
current_vram_bytes: int
|
||||
|
||||
|
||||
class CacheMissCallback(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model_key: str,
|
||||
cache_snapshot: dict[str, CacheEntrySnapshot],
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class CacheHitCallback(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model_key: str,
|
||||
cache_snapshot: dict[str, CacheEntrySnapshot],
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class CacheModelsClearedCallback(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
models_cleared: int,
|
||||
bytes_requested: int,
|
||||
bytes_freed: int,
|
||||
cache_snapshot: dict[str, CacheEntrySnapshot],
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class ModelCache:
|
||||
"""A cache for managing models in memory.
|
||||
|
||||
The cache is based on two levels of model storage:
|
||||
- execution_device: The device where most models are executed (typically "cuda", "mps", or "cpu").
|
||||
- storage_device: The device where models are offloaded when not in active use (typically "cpu").
|
||||
|
||||
The model cache is based on the following assumptions:
|
||||
- storage_device_mem_size > execution_device_mem_size
|
||||
- disk_to_storage_device_transfer_time >> storage_device_to_execution_device_transfer_time
|
||||
|
||||
A copy of all models in the cache is always kept on the storage_device. A subset of the models also have a copy on
|
||||
the execution_device.
|
||||
|
||||
Models are moved between the storage_device and the execution_device as necessary. Cache size limits are enforced
|
||||
on both the storage_device and the execution_device. The execution_device cache uses a smallest-first offload
|
||||
policy. The storage_device cache uses a least-recently-used (LRU) offload policy.
|
||||
|
||||
Note: Neither of these offload policies has really been compared against alternatives. It's likely that different
|
||||
policies would be better, although the optimal policies are likely heavily dependent on usage patterns and HW
|
||||
configuration.
|
||||
|
||||
The cache returns context manager generators designed to load the model into the execution device (often GPU) within
|
||||
the context, and unload outside the context.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
cache = ModelCache(max_cache_size=7.5, max_vram_cache_size=6.0)
|
||||
with cache.get_model('runwayml/stable-diffusion-1-5') as SD1:
|
||||
do_something_on_gpu(SD1)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
execution_device_working_mem_gb: float,
|
||||
enable_partial_loading: bool,
|
||||
keep_ram_copy_of_weights: bool,
|
||||
max_ram_cache_size_gb: float | None = None,
|
||||
max_vram_cache_size_gb: float | None = None,
|
||||
execution_device: torch.device | str = "cuda",
|
||||
storage_device: torch.device | str = "cpu",
|
||||
log_memory_usage: bool = False,
|
||||
logger: Optional[Logger] = None,
|
||||
keep_alive_minutes: float = 0,
|
||||
):
|
||||
"""Initialize the model RAM cache.
|
||||
|
||||
:param execution_device_working_mem_gb: The amount of working memory to keep on the GPU (in GB) i.e. non-model
|
||||
VRAM.
|
||||
:param enable_partial_loading: Whether to enable partial loading of models.
|
||||
:param max_ram_cache_size_gb: The maximum amount of CPU RAM to use for model caching in GB. This parameter is
|
||||
kept to maintain compatibility with previous versions of the model cache, but should be deprecated in the
|
||||
future. If set, this parameter overrides the default cache size logic.
|
||||
:param max_vram_cache_size_gb: The amount of VRAM to use for model caching in GB. This parameter is kept to
|
||||
maintain compatibility with previous versions of the model cache, but should be deprecated in the future.
|
||||
If set, this parameter overrides the default cache size logic.
|
||||
:param execution_device: Torch device to load active model into [torch.device('cuda')]
|
||||
:param storage_device: Torch device to save inactive model in [torch.device('cpu')]
|
||||
:param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache
|
||||
operation, and the result will be logged (at debug level). There is a time cost to capturing the memory
|
||||
snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's
|
||||
behaviour.
|
||||
:param logger: InvokeAILogger to use (otherwise creates one)
|
||||
:param keep_alive_minutes: How long to keep models in cache after last use (in minutes). 0 means keep indefinitely.
|
||||
"""
|
||||
self._enable_partial_loading = enable_partial_loading
|
||||
self._keep_ram_copy_of_weights = keep_ram_copy_of_weights
|
||||
self._execution_device_working_mem_gb = execution_device_working_mem_gb
|
||||
self._execution_device: torch.device = torch.device(execution_device)
|
||||
self._storage_device: torch.device = torch.device(storage_device)
|
||||
|
||||
self._max_ram_cache_size_gb = max_ram_cache_size_gb
|
||||
self._max_vram_cache_size_gb = max_vram_cache_size_gb
|
||||
|
||||
self._logger = PrefixedLoggerAdapter(
|
||||
logger or InvokeAILogger.get_logger(self.__class__.__name__), "MODEL CACHE"
|
||||
)
|
||||
self._log_memory_usage = log_memory_usage
|
||||
self._stats: Optional[CacheStats] = None
|
||||
|
||||
self._cached_models: Dict[str, CacheRecord] = {}
|
||||
self._cache_stack: List[str] = []
|
||||
|
||||
self._ram_cache_size_bytes = self._calc_ram_available_to_model_cache()
|
||||
|
||||
# A lock applied to all public method calls to make the ModelCache thread-safe.
|
||||
# At the time of writing, the ModelCache should only be accessed from two threads:
|
||||
# - The graph execution thread
|
||||
# - Requests to empty the cache from a separate thread
|
||||
self._lock = threading.RLock()
|
||||
|
||||
self._on_cache_hit_callbacks: set[CacheHitCallback] = set()
|
||||
self._on_cache_miss_callbacks: set[CacheMissCallback] = set()
|
||||
self._on_cache_models_cleared_callbacks: set[CacheModelsClearedCallback] = set()
|
||||
|
||||
# Keep-alive timeout support
|
||||
self._keep_alive_minutes = keep_alive_minutes
|
||||
self._last_activity_time: Optional[float] = None
|
||||
self._timeout_timer: Optional[threading.Timer] = None
|
||||
self._shutdown_event = threading.Event()
|
||||
|
||||
def on_cache_hit(self, cb: CacheHitCallback) -> Callable[[], None]:
|
||||
self._on_cache_hit_callbacks.add(cb)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
self._on_cache_hit_callbacks.discard(cb)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def on_cache_miss(self, cb: CacheMissCallback) -> Callable[[], None]:
|
||||
self._on_cache_miss_callbacks.add(cb)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
self._on_cache_miss_callbacks.discard(cb)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def on_cache_models_cleared(self, cb: CacheModelsClearedCallback) -> Callable[[], None]:
|
||||
self._on_cache_models_cleared_callbacks.add(cb)
|
||||
|
||||
def unsubscribe() -> None:
|
||||
self._on_cache_models_cleared_callbacks.discard(cb)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
@property
|
||||
@synchronized
|
||||
def stats(self) -> Optional[CacheStats]:
|
||||
"""Return collected CacheStats object."""
|
||||
return self._stats
|
||||
|
||||
@stats.setter
|
||||
@synchronized
|
||||
def stats(self, stats: CacheStats) -> None:
|
||||
"""Set the CacheStats object for collecting cache statistics."""
|
||||
self._stats = stats
|
||||
# Populate the cache size in the stats object when it's set
|
||||
if self._stats is not None:
|
||||
self._stats.cache_size = self._ram_cache_size_bytes
|
||||
|
||||
def _record_activity(self) -> None:
|
||||
"""Record model activity and reset the timeout timer if configured.
|
||||
|
||||
Note: This method should only be called when self._lock is already held.
|
||||
"""
|
||||
if self._keep_alive_minutes <= 0:
|
||||
return
|
||||
|
||||
self._last_activity_time = time.time()
|
||||
|
||||
# Cancel any existing timer
|
||||
if self._timeout_timer is not None:
|
||||
self._timeout_timer.cancel()
|
||||
|
||||
# Start a new timer
|
||||
timeout_seconds = self._keep_alive_minutes * 60
|
||||
self._timeout_timer = threading.Timer(timeout_seconds, self._on_timeout)
|
||||
# Set as daemon so it doesn't prevent application shutdown
|
||||
self._timeout_timer.daemon = True
|
||||
self._timeout_timer.start()
|
||||
self._logger.debug(f"Model cache activity recorded. Timeout set to {self._keep_alive_minutes} minutes.")
|
||||
|
||||
@synchronized
|
||||
@record_activity
|
||||
def _on_timeout(self) -> None:
|
||||
"""Called when the keep-alive timeout expires. Clears the model cache."""
|
||||
if self._shutdown_event.is_set():
|
||||
return
|
||||
|
||||
# Double-check if there has been activity since the timer was set
|
||||
# This handles the race condition where activity occurred just before the timer fired
|
||||
if self._last_activity_time is not None and self._keep_alive_minutes > 0:
|
||||
elapsed_minutes = (time.time() - self._last_activity_time) / 60
|
||||
if elapsed_minutes < self._keep_alive_minutes:
|
||||
# Activity occurred, don't clear cache
|
||||
self._logger.debug(
|
||||
f"Model cache timeout fired but activity detected {elapsed_minutes:.2f} minutes ago. "
|
||||
f"Skipping cache clear."
|
||||
)
|
||||
return
|
||||
|
||||
# Check if there are any unlocked models that can be cleared
|
||||
unlocked_models = [key for key, entry in self._cached_models.items() if not entry.is_locked]
|
||||
|
||||
if len(unlocked_models) > 0:
|
||||
self._logger.info(
|
||||
f"Model cache keep-alive timeout of {self._keep_alive_minutes} minutes expired. "
|
||||
f"Clearing {len(unlocked_models)} unlocked model(s) from cache."
|
||||
)
|
||||
# Clear the cache by requesting a very large amount of space.
|
||||
# This is the same logic used by the "Clear Model Cache" button.
|
||||
# Using 1000 GB ensures all unlocked models are removed.
|
||||
self._make_room_internal(1000 * GB)
|
||||
elif len(self._cached_models) > 0:
|
||||
# All models are locked, don't log at info level
|
||||
self._logger.debug(
|
||||
f"Model cache timeout fired but all {len(self._cached_models)} model(s) are locked. "
|
||||
f"Skipping cache clear."
|
||||
)
|
||||
else:
|
||||
self._logger.debug("Model cache timeout fired but cache is already empty.")
|
||||
|
||||
@synchronized
|
||||
def shutdown(self) -> None:
|
||||
"""Shutdown the model cache, cancelling any pending timers."""
|
||||
self._shutdown_event.set()
|
||||
if self._timeout_timer is not None:
|
||||
self._timeout_timer.cancel()
|
||||
self._timeout_timer = None
|
||||
|
||||
@synchronized
|
||||
@record_activity
|
||||
def put(self, key: str, model: AnyModel, execution_device: Optional[torch.device] = None) -> None:
|
||||
"""Add a model to the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key for the model
|
||||
model: The model to cache
|
||||
execution_device: Optional device to use for this specific model. If None, uses the cache's default
|
||||
execution_device. Use torch.device("cpu") to force a model to run on CPU.
|
||||
"""
|
||||
if key in self._cached_models:
|
||||
self._logger.debug(
|
||||
f"Attempted to add model {key} ({model.__class__.__name__}), but it already exists in the cache. No action necessary."
|
||||
)
|
||||
return
|
||||
|
||||
size = calc_model_size_by_data(self._logger, model)
|
||||
self._make_room_internal(size)
|
||||
|
||||
# Inject custom modules into the model.
|
||||
if isinstance(model, torch.nn.Module):
|
||||
apply_custom_layers_to_model(model)
|
||||
|
||||
# Use the provided execution device, or fall back to the cache's default
|
||||
effective_execution_device = execution_device if execution_device is not None else self._execution_device
|
||||
|
||||
# Partial loading only makes sense on CUDA.
|
||||
# - When running on CPU, there is no 'loading' to do.
|
||||
# - When running on MPS, memory is shared with the CPU, so the default OS memory management already handles this
|
||||
# well.
|
||||
running_with_cuda = effective_execution_device.type == "cuda"
|
||||
|
||||
# Wrap model.
|
||||
if isinstance(model, torch.nn.Module) and running_with_cuda and self._enable_partial_loading:
|
||||
wrapped_model = CachedModelWithPartialLoad(
|
||||
model, effective_execution_device, keep_ram_copy=self._keep_ram_copy_of_weights
|
||||
)
|
||||
else:
|
||||
wrapped_model = CachedModelOnlyFullLoad(
|
||||
model, effective_execution_device, size, keep_ram_copy=self._keep_ram_copy_of_weights
|
||||
)
|
||||
|
||||
cache_record = CacheRecord(key=key, cached_model=wrapped_model)
|
||||
self._cached_models[key] = cache_record
|
||||
self._cache_stack.append(key)
|
||||
self._logger.debug(
|
||||
f"Added model {key} (Type: {model.__class__.__name__}, Wrap mode: {wrapped_model.__class__.__name__}, Model size: {size / MB:.2f}MB)"
|
||||
)
|
||||
|
||||
@synchronized
|
||||
def _get_cache_snapshot(self) -> dict[str, CacheEntrySnapshot]:
|
||||
overview: dict[str, CacheEntrySnapshot] = {}
|
||||
for cache_key, cache_entry in self._cached_models.items():
|
||||
total_bytes = cache_entry.cached_model.total_bytes()
|
||||
current_vram_bytes = cache_entry.cached_model.cur_vram_bytes()
|
||||
overview[cache_key] = CacheEntrySnapshot(
|
||||
cache_key=cache_key,
|
||||
total_bytes=total_bytes,
|
||||
current_vram_bytes=current_vram_bytes,
|
||||
)
|
||||
|
||||
return overview
|
||||
|
||||
@synchronized
|
||||
@record_activity
|
||||
def get(self, key: str, stats_name: Optional[str] = None) -> CacheRecord:
|
||||
"""Retrieve a model from the cache.
|
||||
|
||||
:param key: Model key
|
||||
:param stats_name: A human-readable id for the model for the purposes of stats reporting.
|
||||
|
||||
Raises IndexError if the model is not in the cache.
|
||||
"""
|
||||
if key in self._cached_models:
|
||||
if self.stats:
|
||||
self.stats.hits += 1
|
||||
else:
|
||||
for cb in self._on_cache_miss_callbacks:
|
||||
cb(model_key=key, cache_snapshot=self._get_cache_snapshot())
|
||||
if self.stats:
|
||||
self.stats.misses += 1
|
||||
self._logger.debug(f"Cache miss: {key}")
|
||||
raise IndexError(f"The model with key {key} is not in the cache.")
|
||||
|
||||
cache_entry = self._cached_models[key]
|
||||
|
||||
# more stats
|
||||
if self.stats:
|
||||
stats_name = stats_name or key
|
||||
self.stats.high_watermark = max(self.stats.high_watermark, self._get_ram_in_use())
|
||||
self.stats.in_cache = len(self._cached_models)
|
||||
self.stats.loaded_model_sizes[stats_name] = max(
|
||||
self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.cached_model.total_bytes()
|
||||
)
|
||||
|
||||
# This moves the entry to the top (right end) of the stack.
|
||||
self._cache_stack = [k for k in self._cache_stack if k != key]
|
||||
self._cache_stack.append(key)
|
||||
|
||||
self._logger.debug(f"Cache hit: {key} (Type: {cache_entry.cached_model.model.__class__.__name__})")
|
||||
for cb in self._on_cache_hit_callbacks:
|
||||
cb(model_key=key, cache_snapshot=self._get_cache_snapshot())
|
||||
|
||||
return cache_entry
|
||||
|
||||
@synchronized
|
||||
@record_activity
|
||||
def lock(self, cache_entry: CacheRecord, working_mem_bytes: Optional[int]) -> None:
|
||||
"""Lock a model for use and move it into VRAM."""
|
||||
if cache_entry.key not in self._cached_models:
|
||||
self._logger.info(
|
||||
f"Locking model cache entry {cache_entry.key} "
|
||||
f"(Type: {cache_entry.cached_model.model.__class__.__name__}), but it has already been dropped from "
|
||||
"the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code "
|
||||
"(See https://github.com/invoke-ai/InvokeAI/issues/7513)."
|
||||
)
|
||||
# cache_entry = self._cached_models[key]
|
||||
cache_entry.lock()
|
||||
|
||||
self._logger.debug(
|
||||
f"Locking model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})"
|
||||
)
|
||||
|
||||
# Check if the model's specific compute_device is CPU, not just the cache's default execution_device
|
||||
model_compute_device = cache_entry.cached_model.compute_device
|
||||
if model_compute_device.type == "cpu":
|
||||
# Models configured for CPU execution don't need to be loaded into VRAM
|
||||
self._logger.debug(f"Model {cache_entry.key} is configured for CPU execution, skipping VRAM load")
|
||||
return
|
||||
|
||||
try:
|
||||
self._load_locked_model(cache_entry, working_mem_bytes)
|
||||
self._logger.debug(
|
||||
f"Finished locking model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})"
|
||||
)
|
||||
except torch.cuda.OutOfMemoryError:
|
||||
self._logger.warning("Insufficient GPU memory to load model. Aborting")
|
||||
cache_entry.unlock()
|
||||
raise
|
||||
except Exception:
|
||||
cache_entry.unlock()
|
||||
raise
|
||||
|
||||
self._log_cache_state()
|
||||
|
||||
@synchronized
|
||||
@record_activity
|
||||
def unlock(self, cache_entry: CacheRecord) -> None:
|
||||
"""Unlock a model."""
|
||||
if cache_entry.key not in self._cached_models:
|
||||
self._logger.info(
|
||||
f"Unlocking model cache entry {cache_entry.key} "
|
||||
f"(Type: {cache_entry.cached_model.model.__class__.__name__}), but it has already been dropped from "
|
||||
"the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code "
|
||||
"(See https://github.com/invoke-ai/InvokeAI/issues/7513)."
|
||||
)
|
||||
# cache_entry = self._cached_models[key]
|
||||
cache_entry.unlock()
|
||||
self._logger.debug(
|
||||
f"Unlocked model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})"
|
||||
)
|
||||
|
||||
# If `drop_model()` marked this entry stale (e.g. settings changed while a generation
|
||||
# was using it), evict now so the next load rebuilds with the new settings rather than
|
||||
# silently reusing the pre-change cached module.
|
||||
if cache_entry.is_stale and not cache_entry.is_locked and cache_entry.key in self._cached_models:
|
||||
bytes_freed = cache_entry.cached_model.total_bytes()
|
||||
self._delete_cache_entry(cache_entry)
|
||||
if self.stats:
|
||||
self.stats.cleared = (self.stats.cleared or 0) + 1
|
||||
snapshot = self._get_cache_snapshot()
|
||||
for cb in self._on_cache_models_cleared_callbacks:
|
||||
cb(
|
||||
models_cleared=1,
|
||||
bytes_requested=0,
|
||||
bytes_freed=bytes_freed,
|
||||
cache_snapshot=snapshot,
|
||||
)
|
||||
gc.collect()
|
||||
TorchDevice.empty_cache()
|
||||
self._logger.debug(f"Evicted stale cache entry {cache_entry.key} after unlock.")
|
||||
|
||||
def _load_locked_model(self, cache_entry: CacheRecord, working_mem_bytes: Optional[int] = None) -> None:
|
||||
"""Helper function for self.lock(). Loads a locked model into VRAM."""
|
||||
start_time = time.time()
|
||||
|
||||
# Calculate model_vram_needed, the amount of additional VRAM that will be used if we fully load the model into
|
||||
# VRAM.
|
||||
model_cur_vram_bytes = cache_entry.cached_model.cur_vram_bytes()
|
||||
model_total_bytes = cache_entry.cached_model.total_bytes()
|
||||
model_vram_needed = model_total_bytes - model_cur_vram_bytes
|
||||
|
||||
vram_available = self._get_vram_available(working_mem_bytes)
|
||||
self._logger.debug(
|
||||
f"Before unloading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}"
|
||||
)
|
||||
|
||||
# Make room for the model in VRAM.
|
||||
# 1. If the model can fit entirely in VRAM, then make enough room for it to be loaded fully.
|
||||
# 2. If the model can't fit fully into VRAM, then unload all other models and load as much of the model as
|
||||
# possible.
|
||||
vram_bytes_freed = self._offload_unlocked_models(model_vram_needed, working_mem_bytes)
|
||||
self._logger.debug(f"Unloaded models (if necessary): vram_bytes_freed={(vram_bytes_freed / MB):.2f}MB")
|
||||
|
||||
# Check the updated vram_available after offloading.
|
||||
vram_available = self._get_vram_available(working_mem_bytes)
|
||||
self._logger.debug(
|
||||
f"After unloading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}"
|
||||
)
|
||||
|
||||
if vram_available < 0:
|
||||
# There is insufficient VRAM available. As a last resort, try to unload the model being locked from VRAM,
|
||||
# as it may still be loaded from a previous use.
|
||||
vram_bytes_freed_from_own_model = self._move_model_to_ram(cache_entry, -vram_available)
|
||||
vram_available = self._get_vram_available(working_mem_bytes)
|
||||
self._logger.debug(
|
||||
f"Unloaded {vram_bytes_freed_from_own_model / MB:.2f}MB from the model being locked ({cache_entry.key})."
|
||||
)
|
||||
|
||||
# Move as much of the model as possible into VRAM.
|
||||
# For testing, only allow 10% of the model to be loaded into VRAM.
|
||||
# vram_available = int(model_vram_needed * 0.1)
|
||||
# We add 1 MB to the available VRAM to account for small errors in memory tracking (e.g. off-by-one). A fully
|
||||
# loaded model is much faster than a 95% loaded model.
|
||||
model_bytes_loaded = self._move_model_to_vram(cache_entry, vram_available + MB)
|
||||
|
||||
model_cur_vram_bytes = cache_entry.cached_model.cur_vram_bytes()
|
||||
vram_available = self._get_vram_available(working_mem_bytes)
|
||||
loaded_percent = model_cur_vram_bytes / model_total_bytes if model_total_bytes > 0 else 0
|
||||
# Use the model's actual compute_device for logging, not the cache's default
|
||||
model_device = cache_entry.cached_model.compute_device
|
||||
self._logger.info(
|
||||
f"Loaded model '{cache_entry.key}' ({cache_entry.cached_model.model.__class__.__name__}) onto "
|
||||
f"{model_device.type} device in {(time.time() - start_time):.2f}s. "
|
||||
f"Total model size: {model_total_bytes / MB:.2f}MB, "
|
||||
f"VRAM: {model_cur_vram_bytes / MB:.2f}MB ({loaded_percent:.1%})"
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Loaded model onto execution device: model_bytes_loaded={(model_bytes_loaded / MB):.2f}MB, "
|
||||
)
|
||||
self._logger.debug(
|
||||
f"After loading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}"
|
||||
)
|
||||
|
||||
def _move_model_to_vram(self, cache_entry: CacheRecord, vram_available: int) -> int:
|
||||
try:
|
||||
if isinstance(cache_entry.cached_model, CachedModelWithPartialLoad):
|
||||
return cache_entry.cached_model.partial_load_to_vram(vram_available)
|
||||
elif isinstance(cache_entry.cached_model, CachedModelOnlyFullLoad): # type: ignore
|
||||
# Partial load is not supported, so we have not choice but to try and fit it all into VRAM.
|
||||
return cache_entry.cached_model.full_load_to_vram()
|
||||
else:
|
||||
raise ValueError(f"Unsupported cached model type: {type(cache_entry.cached_model)}")
|
||||
except Exception as e:
|
||||
if isinstance(e, torch.cuda.OutOfMemoryError):
|
||||
self._logger.warning("Insufficient GPU memory to load model. Aborting")
|
||||
# If an exception occurs, the model could be left in a bad state, so we delete it from the cache entirely.
|
||||
self._delete_cache_entry(cache_entry)
|
||||
raise
|
||||
|
||||
def _move_model_to_ram(self, cache_entry: CacheRecord, vram_bytes_to_free: int) -> int:
|
||||
try:
|
||||
if isinstance(cache_entry.cached_model, CachedModelWithPartialLoad):
|
||||
return cache_entry.cached_model.partial_unload_from_vram(
|
||||
vram_bytes_to_free, keep_required_weights_in_vram=cache_entry.is_locked
|
||||
)
|
||||
elif isinstance(cache_entry.cached_model, CachedModelOnlyFullLoad): # type: ignore
|
||||
return cache_entry.cached_model.full_unload_from_vram()
|
||||
else:
|
||||
raise ValueError(f"Unsupported cached model type: {type(cache_entry.cached_model)}")
|
||||
except Exception:
|
||||
# If an exception occurs, the model could be left in a bad state, so we delete it from the cache entirely.
|
||||
self._delete_cache_entry(cache_entry)
|
||||
raise
|
||||
|
||||
def _get_vram_available(self, working_mem_bytes: Optional[int]) -> int:
|
||||
"""Calculate the amount of additional VRAM available for the cache to use (takes into account the working
|
||||
memory).
|
||||
"""
|
||||
# If self._max_vram_cache_size_gb is set, then it overrides the default logic.
|
||||
if self._max_vram_cache_size_gb is not None:
|
||||
vram_total_available_to_cache = int(self._max_vram_cache_size_gb * GB)
|
||||
return vram_total_available_to_cache - self._get_vram_in_use()
|
||||
|
||||
working_mem_bytes_default = int(self._execution_device_working_mem_gb * GB)
|
||||
working_mem_bytes = max(working_mem_bytes or working_mem_bytes_default, working_mem_bytes_default)
|
||||
|
||||
if self._execution_device.type == "cuda":
|
||||
# TODO(ryand): It is debatable whether we should use memory_reserved() or memory_allocated() here.
|
||||
# memory_reserved() includes memory reserved by the torch CUDA memory allocator that may or may not be
|
||||
# re-used for future allocations. For now, we use memory_allocated() to be conservative.
|
||||
# vram_reserved = torch.cuda.memory_reserved(self._execution_device)
|
||||
vram_allocated = torch.cuda.memory_allocated(self._execution_device)
|
||||
vram_free, _vram_total = torch.cuda.mem_get_info(self._execution_device)
|
||||
vram_available_to_process = vram_free + vram_allocated
|
||||
elif self._execution_device.type == "mps":
|
||||
vram_reserved = torch.mps.driver_allocated_memory()
|
||||
# TODO(ryand): Is it accurate that MPS shares memory with the CPU?
|
||||
vram_free = psutil.virtual_memory().available
|
||||
vram_available_to_process = vram_free + vram_reserved
|
||||
else:
|
||||
raise ValueError(f"Unsupported execution device: {self._execution_device.type}")
|
||||
|
||||
vram_total_available_to_cache = vram_available_to_process - working_mem_bytes
|
||||
vram_cur_available_to_cache = vram_total_available_to_cache - self._get_vram_in_use()
|
||||
return vram_cur_available_to_cache
|
||||
|
||||
def _get_vram_in_use(self) -> int:
|
||||
"""Get the amount of VRAM currently in use by the cache."""
|
||||
if self._execution_device.type == "cuda":
|
||||
return torch.cuda.memory_allocated()
|
||||
elif self._execution_device.type == "mps":
|
||||
return torch.mps.current_allocated_memory()
|
||||
else:
|
||||
raise ValueError(f"Unsupported execution device type: {self._execution_device.type}")
|
||||
# Alternative definition of VRAM in use:
|
||||
# return sum(ce.cached_model.cur_vram_bytes() for ce in self._cached_models.values())
|
||||
|
||||
def _calc_ram_available_to_model_cache(self) -> int:
|
||||
"""Calculate the amount of RAM available for the cache to use."""
|
||||
# If self._max_ram_cache_size_gb is set, then it overrides the default logic.
|
||||
if self._max_ram_cache_size_gb is not None:
|
||||
self._logger.info(f"Using user-defined RAM cache size: {self._max_ram_cache_size_gb} GB.")
|
||||
return int(self._max_ram_cache_size_gb * GB)
|
||||
|
||||
# Heuristics for dynamically calculating the RAM cache size, **in order of increasing priority**:
|
||||
# 1. As an initial default, use 50% of the total RAM for InvokeAI.
|
||||
# - Assume a 2GB baseline for InvokeAI's non-model RAM usage, and use the rest of the RAM for the model cache.
|
||||
# 2. On a system with a lot of RAM, users probably don't want InvokeAI to eat up too much RAM.
|
||||
# There are diminishing returns to storing more and more models. So, we apply an upper bound. (Keep in mind
|
||||
# that most OSes have some amount of disk caching, which we still benefit from if there is excess memory,
|
||||
# even if we drop models from the cache.)
|
||||
# - On systems without a CUDA device, the upper bound is 32GB.
|
||||
# - On systems with a CUDA device, the upper bound is 1x the amount of VRAM (less the working memory).
|
||||
# 3. Absolute minimum of 4GB.
|
||||
|
||||
# NOTE(ryand): We explored dynamically adjusting the RAM cache size based on memory pressure (using psutil), but
|
||||
# decided against it for now, for the following reasons:
|
||||
# - It was surprisingly difficult to get memory metrics with consistent definitions across OSes. (If you go
|
||||
# down this path again, don't underestimate the amount of complexity here and be sure to test rigorously on all
|
||||
# OSes.)
|
||||
# - Making the RAM cache size dynamic opens the door for performance regressions that are hard to diagnose and
|
||||
# hard for users to understand. It is better for users to see that their RAM is maxed out, and then override
|
||||
# the default value if desired.
|
||||
|
||||
# Lookup the total VRAM size for the CUDA execution device.
|
||||
total_cuda_vram_bytes: int | None = None
|
||||
if self._execution_device.type == "cuda":
|
||||
_, total_cuda_vram_bytes = torch.cuda.mem_get_info(self._execution_device)
|
||||
|
||||
# Apply heuristic 1.
|
||||
# ------------------
|
||||
heuristics_applied = [1]
|
||||
total_system_ram_bytes = psutil.virtual_memory().total
|
||||
# Assumed baseline RAM used by InvokeAI for non-model stuff.
|
||||
baseline_ram_used_by_invokeai = 2 * GB
|
||||
ram_available_to_model_cache = int(total_system_ram_bytes * 0.5 - baseline_ram_used_by_invokeai)
|
||||
|
||||
# Apply heuristic 2.
|
||||
# ------------------
|
||||
max_ram_cache_size_bytes = 32 * GB
|
||||
if total_cuda_vram_bytes is not None:
|
||||
if self._max_vram_cache_size_gb is not None:
|
||||
max_ram_cache_size_bytes = int(self._max_vram_cache_size_gb * GB)
|
||||
else:
|
||||
max_ram_cache_size_bytes = total_cuda_vram_bytes - int(self._execution_device_working_mem_gb * GB)
|
||||
if ram_available_to_model_cache > max_ram_cache_size_bytes:
|
||||
heuristics_applied.append(2)
|
||||
ram_available_to_model_cache = max_ram_cache_size_bytes
|
||||
|
||||
# Apply heuristic 3.
|
||||
# ------------------
|
||||
if ram_available_to_model_cache < 4 * GB:
|
||||
heuristics_applied.append(3)
|
||||
ram_available_to_model_cache = 4 * GB
|
||||
|
||||
self._logger.info(
|
||||
f"Calculated model RAM cache size: {ram_available_to_model_cache / MB:.2f} MB. Heuristics applied: {heuristics_applied}."
|
||||
)
|
||||
return ram_available_to_model_cache
|
||||
|
||||
def _get_ram_in_use(self) -> int:
|
||||
"""Get the amount of RAM currently in use."""
|
||||
return sum(ce.cached_model.total_bytes() for ce in self._cached_models.values())
|
||||
|
||||
def _get_ram_available(self) -> int:
|
||||
"""Get the amount of RAM available for the cache to use."""
|
||||
return self._ram_cache_size_bytes - self._get_ram_in_use()
|
||||
|
||||
def _capture_memory_snapshot(self) -> Optional[MemorySnapshot]:
|
||||
if self._log_memory_usage:
|
||||
return MemorySnapshot.capture()
|
||||
return None
|
||||
|
||||
def _get_vram_state_str(self, model_cur_vram_bytes: int, model_total_bytes: int, vram_available: int) -> str:
|
||||
"""Helper function for preparing a VRAM state log string."""
|
||||
model_cur_vram_bytes_percent = model_cur_vram_bytes / model_total_bytes if model_total_bytes > 0 else 0
|
||||
return (
|
||||
f"model_total={model_total_bytes / MB:.0f} MB, "
|
||||
+ f"model_vram={model_cur_vram_bytes / MB:.0f} MB ({model_cur_vram_bytes_percent:.1%} %), "
|
||||
# + f"vram_total={int(self._max_vram_cache_size * GB)/MB:.0f} MB, "
|
||||
+ f"vram_available={(vram_available / MB):.0f} MB, "
|
||||
)
|
||||
|
||||
def _offload_unlocked_models(self, vram_bytes_required: int, working_mem_bytes: Optional[int] = None) -> int:
|
||||
"""Offload models from the execution_device until vram_bytes_required bytes are available, or all models are
|
||||
offloaded. Of course, locked models are not offloaded.
|
||||
|
||||
Returns:
|
||||
int: The number of bytes freed based on believed model sizes. The actual change in VRAM may be different.
|
||||
"""
|
||||
self._logger.debug(
|
||||
f"Offloading unlocked models with goal of making room for {vram_bytes_required / MB:.2f}MB of VRAM."
|
||||
)
|
||||
vram_bytes_freed = 0
|
||||
# TODO(ryand): Give more thought to the offloading policy used here.
|
||||
cache_entries_increasing_size = sorted(self._cached_models.values(), key=lambda x: x.cached_model.total_bytes())
|
||||
for cache_entry in cache_entries_increasing_size:
|
||||
# We do not fully trust the count of bytes freed, so we check again on each iteration.
|
||||
vram_available = self._get_vram_available(working_mem_bytes)
|
||||
vram_bytes_to_free = vram_bytes_required - vram_available
|
||||
if vram_bytes_to_free <= 0:
|
||||
break
|
||||
if cache_entry.is_locked:
|
||||
# TODO(ryand): In the future, we may want to partially unload locked models, but this requires careful
|
||||
# handling of model patches (e.g. LoRA).
|
||||
continue
|
||||
cache_entry_bytes_freed = self._move_model_to_ram(cache_entry, vram_bytes_to_free)
|
||||
if cache_entry_bytes_freed > 0:
|
||||
self._logger.debug(
|
||||
f"Unloaded {cache_entry.key} from VRAM to free {(cache_entry_bytes_freed / MB):.0f} MB."
|
||||
)
|
||||
vram_bytes_freed += cache_entry_bytes_freed
|
||||
|
||||
TorchDevice.empty_cache()
|
||||
return vram_bytes_freed
|
||||
|
||||
def _log_cache_state(self, title: str = "Model cache state:", include_entry_details: bool = True):
|
||||
if self._logger.getEffectiveLevel() > logging.DEBUG:
|
||||
# Short circuit if the logger is not set to debug. Some of the data lookups could take a non-negligible
|
||||
# amount of time.
|
||||
return
|
||||
|
||||
log = f"{title}\n"
|
||||
|
||||
log_format = " {:<30} Limit: {:>7.1f} MB, Used: {:>7.1f} MB ({:>5.1%}), Available: {:>7.1f} MB ({:>5.1%})\n"
|
||||
|
||||
ram_in_use_bytes = self._get_ram_in_use()
|
||||
ram_available_bytes = self._get_ram_available()
|
||||
ram_size_bytes = ram_in_use_bytes + ram_available_bytes
|
||||
ram_in_use_bytes_percent = ram_in_use_bytes / ram_size_bytes if ram_size_bytes > 0 else 0
|
||||
ram_available_bytes_percent = ram_available_bytes / ram_size_bytes if ram_size_bytes > 0 else 0
|
||||
log += log_format.format(
|
||||
f"Storage Device ({self._storage_device.type})",
|
||||
ram_size_bytes / MB,
|
||||
ram_in_use_bytes / MB,
|
||||
ram_in_use_bytes_percent,
|
||||
ram_available_bytes / MB,
|
||||
ram_available_bytes_percent,
|
||||
)
|
||||
|
||||
if self._execution_device.type != "cpu":
|
||||
vram_in_use_bytes = self._get_vram_in_use()
|
||||
vram_available_bytes = self._get_vram_available(None)
|
||||
vram_size_bytes = vram_in_use_bytes + vram_available_bytes
|
||||
vram_in_use_bytes_percent = vram_in_use_bytes / vram_size_bytes if vram_size_bytes > 0 else 0
|
||||
vram_available_bytes_percent = vram_available_bytes / vram_size_bytes if vram_size_bytes > 0 else 0
|
||||
log += log_format.format(
|
||||
f"Compute Device ({self._execution_device.type})",
|
||||
vram_size_bytes / MB,
|
||||
vram_in_use_bytes / MB,
|
||||
vram_in_use_bytes_percent,
|
||||
vram_available_bytes / MB,
|
||||
vram_available_bytes_percent,
|
||||
)
|
||||
|
||||
if torch.cuda.is_available():
|
||||
log += " {:<30} {:.1f} MB\n".format("CUDA Memory Allocated:", torch.cuda.memory_allocated() / MB)
|
||||
log += " {:<30} {}\n".format("Total models:", len(self._cached_models))
|
||||
|
||||
if include_entry_details and len(self._cached_models) > 0:
|
||||
log += " Models:\n"
|
||||
log_format = (
|
||||
" {:<80} total={:>7.1f} MB, vram={:>7.1f} MB ({:>5.1%}), ram={:>7.1f} MB ({:>5.1%}), locked={}\n"
|
||||
)
|
||||
for cache_record in self._cached_models.values():
|
||||
total_bytes = cache_record.cached_model.total_bytes()
|
||||
cur_vram_bytes = cache_record.cached_model.cur_vram_bytes()
|
||||
cur_vram_bytes_percent = cur_vram_bytes / total_bytes if total_bytes > 0 else 0
|
||||
cur_ram_bytes = total_bytes - cur_vram_bytes
|
||||
cur_ram_bytes_percent = cur_ram_bytes / total_bytes if total_bytes > 0 else 0
|
||||
|
||||
log += log_format.format(
|
||||
f"{cache_record.key} ({cache_record.cached_model.model.__class__.__name__}):",
|
||||
total_bytes / MB,
|
||||
cur_vram_bytes / MB,
|
||||
cur_vram_bytes_percent,
|
||||
cur_ram_bytes / MB,
|
||||
cur_ram_bytes_percent,
|
||||
cache_record.is_locked,
|
||||
)
|
||||
|
||||
self._logger.debug(log)
|
||||
|
||||
@synchronized
|
||||
def make_room(self, bytes_needed: int) -> None:
|
||||
"""Make enough room in the cache to accommodate a new model of indicated size.
|
||||
|
||||
Note: This function deletes all of the cache's internal references to a model in order to free it. If there are
|
||||
external references to the model, there's nothing that the cache can do about it, and those models will not be
|
||||
garbage-collected.
|
||||
"""
|
||||
self._make_room_internal(bytes_needed)
|
||||
|
||||
def _make_room_internal(self, bytes_needed: int) -> None:
|
||||
"""Internal implementation of make_room(). Assumes the lock is already held."""
|
||||
self._logger.debug(f"Making room for {bytes_needed / MB:.2f}MB of RAM.")
|
||||
self._log_cache_state(title="Before dropping models:")
|
||||
|
||||
ram_bytes_available = self._get_ram_available()
|
||||
ram_bytes_to_free = max(0, bytes_needed - ram_bytes_available)
|
||||
|
||||
ram_bytes_freed = 0
|
||||
pos = 0
|
||||
models_cleared = 0
|
||||
while ram_bytes_freed < ram_bytes_to_free and pos < len(self._cache_stack):
|
||||
model_key = self._cache_stack[pos]
|
||||
cache_entry = self._cached_models[model_key]
|
||||
|
||||
if not cache_entry.is_locked:
|
||||
ram_bytes_freed += cache_entry.cached_model.total_bytes()
|
||||
self._logger.debug(
|
||||
f"Dropping {model_key} from RAM cache to free {(cache_entry.cached_model.total_bytes() / MB):.2f}MB."
|
||||
)
|
||||
self._delete_cache_entry(cache_entry)
|
||||
del cache_entry
|
||||
models_cleared += 1
|
||||
else:
|
||||
pos += 1
|
||||
|
||||
if models_cleared > 0:
|
||||
# There would likely be some 'garbage' to be collected regardless of whether a model was cleared or not, but
|
||||
# there is a significant time cost to calling `gc.collect()`, so we want to use it sparingly. (The time cost
|
||||
# is high even if no garbage gets collected.)
|
||||
#
|
||||
# Calling gc.collect(...) when a model is cleared seems like a good middle-ground:
|
||||
# - If models had to be cleared, it's a signal that we are close to our memory limit.
|
||||
# - If models were cleared, there's a good chance that there's a significant amount of garbage to be
|
||||
# collected.
|
||||
#
|
||||
# Keep in mind that gc is only responsible for handling reference cycles. Most objects should be cleaned up
|
||||
# immediately when their reference count hits 0.
|
||||
if self.stats:
|
||||
self.stats.cleared = models_cleared
|
||||
for cb in self._on_cache_models_cleared_callbacks:
|
||||
cb(
|
||||
models_cleared=models_cleared,
|
||||
bytes_requested=bytes_needed,
|
||||
bytes_freed=ram_bytes_freed,
|
||||
cache_snapshot=self._get_cache_snapshot(),
|
||||
)
|
||||
gc.collect()
|
||||
|
||||
TorchDevice.empty_cache()
|
||||
self._logger.debug(f"Dropped {models_cleared} models to free {ram_bytes_freed / MB:.2f}MB of RAM.")
|
||||
self._log_cache_state(title="After dropping models:")
|
||||
|
||||
def _delete_cache_entry(self, cache_entry: CacheRecord) -> None:
|
||||
"""Delete cache_entry from the cache if it exists. No exception is thrown if it doesn't exist."""
|
||||
self._cache_stack = [key for key in self._cache_stack if key != cache_entry.key]
|
||||
self._cached_models.pop(cache_entry.key, None)
|
||||
|
||||
@synchronized
|
||||
def drop_model(self, model_key: str) -> int:
|
||||
"""Drop all cache entries belonging to a model so the next load rebuilds them.
|
||||
|
||||
Cache keys are `<model_key>` or `<model_key>:<submodel>` (see `get_model_cache_key`),
|
||||
so a single model may have multiple entries. Locked entries are marked `is_stale` and
|
||||
evicted by `unlock()` as soon as the last lock releases — without that, a setting
|
||||
toggled during an in-flight generation would survive on the locked entry and quietly
|
||||
get reused by the next generation.
|
||||
|
||||
Returns the number of entries immediately dropped (locked entries that are only marked
|
||||
stale do not count).
|
||||
"""
|
||||
prefix = f"{model_key}:"
|
||||
matching: list[CacheRecord] = [
|
||||
entry for key, entry in self._cached_models.items() if key == model_key or key.startswith(prefix)
|
||||
]
|
||||
|
||||
dropped: list[CacheRecord] = []
|
||||
bytes_freed = 0
|
||||
for entry in matching:
|
||||
if entry.is_locked:
|
||||
entry.is_stale = True
|
||||
continue
|
||||
bytes_freed += entry.cached_model.total_bytes()
|
||||
self._delete_cache_entry(entry)
|
||||
dropped.append(entry)
|
||||
|
||||
if dropped:
|
||||
if self.stats:
|
||||
self.stats.cleared = len(dropped)
|
||||
snapshot = self._get_cache_snapshot()
|
||||
for cb in self._on_cache_models_cleared_callbacks:
|
||||
cb(
|
||||
models_cleared=len(dropped),
|
||||
bytes_requested=0,
|
||||
bytes_freed=bytes_freed,
|
||||
cache_snapshot=snapshot,
|
||||
)
|
||||
gc.collect()
|
||||
TorchDevice.empty_cache()
|
||||
return len(dropped)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
from typing import TypeVar
|
||||
|
||||
import torch
|
||||
|
||||
T = TypeVar("T", torch.Tensor, None, torch.Tensor | None)
|
||||
|
||||
|
||||
def cast_to_device(t: T, to_device: torch.device) -> T:
|
||||
"""Helper function to cast an optional tensor to a target device."""
|
||||
if t is None:
|
||||
return t
|
||||
|
||||
if t.device.type != to_device.type:
|
||||
return t.to(to_device)
|
||||
return t
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
|
||||
This directory contains custom implementations of common torch.nn.Module classes that add support for:
|
||||
- Streaming weights to the execution device
|
||||
- Applying sidecar patches at execution time (e.g. sidecar LoRA layers)
|
||||
|
||||
Each custom class sub-classes the original module type that is is replacing, so the following properties are preserved:
|
||||
- `isinstance(m, torch.nn.OrginalModule)` should still work.
|
||||
- Patching the weights directly (e.g. for LoRA) should still work. (Of course, this is not possible for quantized layers, hence the sidecar support.)
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.utils import (
|
||||
add_nullable_tensors,
|
||||
)
|
||||
|
||||
|
||||
class CustomConv1d(torch.nn.Conv1d, CustomModuleMixin):
|
||||
def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, input.device)
|
||||
bias = cast_to_device(self.bias, input.device)
|
||||
|
||||
# Prepare the original parameters for the patch aggregation.
|
||||
orig_params = {"weight": weight, "bias": bias}
|
||||
# Filter out None values.
|
||||
orig_params = {k: v for k, v in orig_params.items() if v is not None}
|
||||
|
||||
aggregated_param_residuals = self._aggregate_patch_parameters(
|
||||
patches_and_weights=self._patches_and_weights,
|
||||
orig_params=orig_params,
|
||||
device=input.device,
|
||||
)
|
||||
|
||||
weight = add_nullable_tensors(weight, aggregated_param_residuals.get("weight", None))
|
||||
bias = add_nullable_tensors(bias, aggregated_param_residuals.get("bias", None))
|
||||
return self._conv_forward(input, weight, bias)
|
||||
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, input.device)
|
||||
bias = cast_to_device(self.bias, input.device)
|
||||
return self._conv_forward(input, weight, bias)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(input)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.utils import (
|
||||
add_nullable_tensors,
|
||||
)
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
class CustomConv2d(torch.nn.Conv2d, CustomModuleMixin):
|
||||
def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None:
|
||||
tensor = cast_to_device(tensor, input.device)
|
||||
if (
|
||||
tensor is not None
|
||||
and input.is_floating_point()
|
||||
and tensor.is_floating_point()
|
||||
and not isinstance(tensor, GGMLTensor)
|
||||
and tensor.dtype != input.dtype
|
||||
):
|
||||
tensor = tensor.to(dtype=input.dtype)
|
||||
return tensor
|
||||
|
||||
def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = self._cast_tensor_for_input(self.weight, input)
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
|
||||
# Prepare the original parameters for the patch aggregation.
|
||||
orig_params = {"weight": weight, "bias": bias}
|
||||
# Filter out None values.
|
||||
orig_params = {k: v for k, v in orig_params.items() if v is not None}
|
||||
|
||||
aggregated_param_residuals = self._aggregate_patch_parameters(
|
||||
patches_and_weights=self._patches_and_weights,
|
||||
orig_params=orig_params,
|
||||
device=input.device,
|
||||
)
|
||||
|
||||
residual_weight = self._cast_tensor_for_input(aggregated_param_residuals.get("weight", None), input)
|
||||
residual_bias = self._cast_tensor_for_input(aggregated_param_residuals.get("bias", None), input)
|
||||
weight = add_nullable_tensors(weight, residual_weight)
|
||||
bias = add_nullable_tensors(bias, residual_bias)
|
||||
return self._conv_forward(input, weight, bias)
|
||||
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = self._cast_tensor_for_input(self.weight, input)
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
return self._conv_forward(input, weight, bias)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(input)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
elif input.is_floating_point() and (
|
||||
(
|
||||
self.weight.is_floating_point()
|
||||
and not isinstance(self.weight, GGMLTensor)
|
||||
and self.weight.dtype != input.dtype
|
||||
)
|
||||
or (
|
||||
self.bias is not None
|
||||
and self.bias.is_floating_point()
|
||||
and not isinstance(self.bias, GGMLTensor)
|
||||
and self.bias.dtype != input.dtype
|
||||
)
|
||||
):
|
||||
weight = self._cast_tensor_for_input(self.weight, input)
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
return self._conv_forward(input, weight, bias)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import torch
|
||||
from diffusers.models.normalization import RMSNorm as DiffusersRMSNorm
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
|
||||
|
||||
class CustomDiffusersRMSNorm(DiffusersRMSNorm, CustomModuleMixin):
|
||||
"""Custom wrapper for diffusers RMSNorm that supports device autocasting for partial model loading."""
|
||||
|
||||
def _autocast_forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, hidden_states.device) if self.weight is not None else None
|
||||
bias = cast_to_device(self.bias, hidden_states.device) if self.bias is not None else None
|
||||
|
||||
input_dtype = hidden_states.dtype
|
||||
variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True)
|
||||
hidden_states = hidden_states * torch.rsqrt(variance + self.eps)
|
||||
|
||||
if weight is not None:
|
||||
# convert into half-precision if necessary
|
||||
if weight.dtype in [torch.float16, torch.bfloat16]:
|
||||
hidden_states = hidden_states.to(weight.dtype)
|
||||
hidden_states = hidden_states * weight
|
||||
if bias is not None:
|
||||
hidden_states = hidden_states + bias
|
||||
else:
|
||||
hidden_states = hidden_states.to(input_dtype)
|
||||
|
||||
return hidden_states
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
raise RuntimeError("DiffusersRMSNorm layers do not support patches")
|
||||
|
||||
if self._device_autocasting_enabled:
|
||||
return self._autocast_forward(hidden_states)
|
||||
else:
|
||||
return super().forward(hidden_states)
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
|
||||
|
||||
class CustomEmbedding(torch.nn.Embedding, CustomModuleMixin):
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, input.device)
|
||||
return torch.nn.functional.embedding(
|
||||
input,
|
||||
weight,
|
||||
self.padding_idx,
|
||||
self.max_norm,
|
||||
self.norm_type,
|
||||
self.scale_grad_by_freq,
|
||||
self.sparse,
|
||||
)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
raise RuntimeError("Embedding layers do not support patches")
|
||||
|
||||
if self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.flux.modules.layers import RMSNorm
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.patches.layers.set_parameter_layer import SetParameterLayer
|
||||
|
||||
|
||||
class CustomFluxRMSNorm(RMSNorm, CustomModuleMixin):
|
||||
def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor:
|
||||
# Currently, CustomFluxRMSNorm layers only support patching with a single SetParameterLayer.
|
||||
assert len(self._patches_and_weights) == 1
|
||||
patch, _patch_weight = self._patches_and_weights[0]
|
||||
assert isinstance(patch, SetParameterLayer)
|
||||
assert patch.param_name == "scale"
|
||||
|
||||
scale = cast_to_device(patch.weight, x.device)
|
||||
|
||||
# Apply the patch.
|
||||
# NOTE(ryand): Currently, we ignore the patch weight when running as a sidecar. It's not clear how this should
|
||||
# be handled.
|
||||
return torch.nn.functional.rms_norm(x, scale.shape, scale, eps=1e-6)
|
||||
|
||||
def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
scale = cast_to_device(self.scale, x.device)
|
||||
return torch.nn.functional.rms_norm(x, scale.shape, scale, eps=1e-6)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(x)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(x)
|
||||
else:
|
||||
return super().forward(x)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
|
||||
|
||||
class CustomGroupNorm(torch.nn.GroupNorm, CustomModuleMixin):
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, input.device)
|
||||
bias = cast_to_device(self.bias, input.device)
|
||||
return torch.nn.functional.group_norm(input, self.num_groups, weight, bias, self.eps)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
raise RuntimeError("GroupNorm layers do not support patches")
|
||||
|
||||
if self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import bitsandbytes as bnb
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import (
|
||||
autocast_linear_forward_sidecar_patches,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.patches.layers.param_shape_utils import get_param_shape
|
||||
from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
class CustomInvokeLinear8bitLt(InvokeLinear8bitLt, CustomModuleMixin):
|
||||
def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None:
|
||||
tensor = cast_to_device(tensor, input.device)
|
||||
if (
|
||||
tensor is not None
|
||||
and input.is_floating_point()
|
||||
and tensor.is_floating_point()
|
||||
and not isinstance(tensor, GGMLTensor)
|
||||
and tensor.dtype != input.dtype
|
||||
):
|
||||
tensor = tensor.to(dtype=input.dtype)
|
||||
return tensor
|
||||
|
||||
def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
# See the matching method on CustomInvokeLinearNF4 for the rationale. Int8Params doesn't have
|
||||
# the same packed-shape problem as Params4bit, but we still substitute a meta tensor so that
|
||||
# patches don't accidentally read the quantized weight values.
|
||||
weight = torch.empty(get_param_shape(self.weight), device="meta")
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
return weight, bias
|
||||
|
||||
def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return autocast_linear_forward_sidecar_patches(self, x, self._patches_and_weights)
|
||||
|
||||
def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
matmul_state = bnb.MatmulLtState()
|
||||
matmul_state.threshold = self.state.threshold
|
||||
matmul_state.has_fp16_weights = self.state.has_fp16_weights
|
||||
matmul_state.use_pool = self.state.use_pool
|
||||
matmul_state.is_training = self.training
|
||||
# The underlying InvokeInt8Params weight must already be quantized.
|
||||
assert self.weight.CB is not None
|
||||
matmul_state.CB = cast_to_device(self.weight.CB, x.device)
|
||||
matmul_state.SCB = cast_to_device(self.weight.SCB, x.device)
|
||||
|
||||
# weights are cast automatically as Int8Params, but the bias has to be cast manually.
|
||||
if self.bias is not None and self.bias.dtype != x.dtype:
|
||||
self.bias.data = self.bias.data.to(x.dtype)
|
||||
|
||||
# NOTE(ryand): The second parameter should not be needed at all given our expected inference configuration, but
|
||||
# it's dtype field must be accessible, even though it's not used. We pass in self.weight even though it could be
|
||||
# on the wrong device.
|
||||
return bnb.matmul(x, self.weight, bias=cast_to_device(self.bias, x.device), state=matmul_state)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(x)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(x)
|
||||
else:
|
||||
return super().forward(x)
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import copy
|
||||
|
||||
import bitsandbytes as bnb
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import (
|
||||
autocast_linear_forward_sidecar_patches,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.patches.layers.param_shape_utils import get_param_shape
|
||||
from invokeai.backend.quantization.bnb_nf4 import InvokeLinearNF4
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
class CustomInvokeLinearNF4(InvokeLinearNF4, CustomModuleMixin):
|
||||
def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None:
|
||||
tensor = cast_to_device(tensor, input.device)
|
||||
if (
|
||||
tensor is not None
|
||||
and input.is_floating_point()
|
||||
and tensor.is_floating_point()
|
||||
and not isinstance(tensor, GGMLTensor)
|
||||
and tensor.dtype != input.dtype
|
||||
):
|
||||
tensor = tensor.to(dtype=input.dtype)
|
||||
return tensor
|
||||
|
||||
def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
# The NF4 weight is a Params4bit whose .shape reports the *packed-byte* layout, not the logical
|
||||
# (out_features, in_features) shape. We hand patches a meta-device tensor with the correct
|
||||
# logical shape so that shape-only patches (LoRA, LoHA, MergedLayerPatch over LoRA, ...) work.
|
||||
# Patches that read the original weight values (e.g. SetParameterLayer, DoRA) are not supported
|
||||
# on NF4-quantized modules.
|
||||
weight = torch.empty(get_param_shape(self.weight), device="meta")
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
return weight, bias
|
||||
|
||||
def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return autocast_linear_forward_sidecar_patches(self, x, self._patches_and_weights)
|
||||
|
||||
def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
bnb.nn.modules.fix_4bit_weight_quant_state_from_module(self)
|
||||
|
||||
# weights are cast automatically as Int8Params, but the bias has to be cast manually
|
||||
if self.bias is not None and self.bias.dtype != x.dtype:
|
||||
self.bias.data = self.bias.data.to(x.dtype)
|
||||
|
||||
if not self.compute_type_is_set:
|
||||
self.set_compute_type(x)
|
||||
self.compute_type_is_set = True
|
||||
|
||||
inp_dtype = x.dtype
|
||||
if self.compute_dtype is not None:
|
||||
x = x.to(self.compute_dtype)
|
||||
|
||||
bias = None if self.bias is None else self.bias.to(self.compute_dtype)
|
||||
|
||||
# HACK(ryand): Casting self.weight to the device also casts the self.weight.quant_state in-place (i.e. it
|
||||
# does not follow the tensor semantics of returning a new copy when converting to a different device). This
|
||||
# means that quant_state elements that started on the CPU would be left on the GPU, which we don't want. To
|
||||
# avoid this side effect we make a shallow copy of the original quant_state so that we can restore it. Fixing
|
||||
# this properly would require more invasive changes to the bitsandbytes library.
|
||||
|
||||
# Make a shallow copy of the quant_state so that we can undo the in-place modification that occurs when casting
|
||||
# to a new device.
|
||||
weight_was_offloaded = self.weight.device.type != x.device.type
|
||||
old_quant_state = copy.copy(self.weight.quant_state)
|
||||
weight = cast_to_device(self.weight, x.device)
|
||||
self.weight.quant_state = old_quant_state
|
||||
|
||||
# For some reason, the quant_state.to(...) implementation fails to cast the quant_state.code field. We do this
|
||||
# manually here.
|
||||
weight.quant_state.code = cast_to_device(weight.quant_state.code, x.device)
|
||||
|
||||
bias = cast_to_device(self.bias, x.device)
|
||||
if weight_was_offloaded and x.numel() == x.shape[-1]:
|
||||
# bitsandbytes routes single-vector inputs through gemv_4bit, which can fail with CPU-stored,
|
||||
# device-autocasted Params4bit weights on some CUDA/bnb combinations. Use the same dequantized
|
||||
# matmul path that bnb.matmul_4bit uses for batched inputs.
|
||||
dequantized_weight = bnb.functional.dequantize_4bit(weight, weight.quant_state).to(x.dtype)
|
||||
return torch.nn.functional.linear(x, dequantized_weight, bias).to(inp_dtype)
|
||||
return bnb.matmul_4bit(x, weight.t(), bias=bias, quant_state=weight.quant_state).to(inp_dtype)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(x)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(x)
|
||||
else:
|
||||
return super().forward(x)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
|
||||
|
||||
class CustomLayerNorm(torch.nn.LayerNorm, CustomModuleMixin):
|
||||
"""Custom wrapper for torch.nn.LayerNorm that supports device autocasting for partial model loading."""
|
||||
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight = cast_to_device(self.weight, input.device) if self.weight is not None else None
|
||||
bias = cast_to_device(self.bias, input.device) if self.bias is not None else None
|
||||
return F.layer_norm(input, self.normalized_shape, weight, bias, self.eps)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
raise RuntimeError("LayerNorm layers do not support patches")
|
||||
|
||||
if self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import copy
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch
|
||||
from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer
|
||||
from invokeai.backend.patches.layers.lora_layer import LoRALayer
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
def linear_lora_forward(input: torch.Tensor, lora_layer: LoRALayer, lora_weight: float) -> torch.Tensor:
|
||||
"""An optimized implementation of the residual calculation for a sidecar linear LoRALayer."""
|
||||
# up matrix and down matrix have different ranks so we can't simply multiply them
|
||||
if lora_layer.up.shape[1] != lora_layer.down.shape[0]:
|
||||
x = torch.nn.functional.linear(input, lora_layer.get_weight(lora_weight), bias=lora_layer.bias)
|
||||
x *= lora_weight * lora_layer.scale()
|
||||
return x
|
||||
|
||||
x = torch.nn.functional.linear(input, lora_layer.down)
|
||||
if lora_layer.mid is not None:
|
||||
x = torch.nn.functional.linear(x, lora_layer.mid)
|
||||
x = torch.nn.functional.linear(x, lora_layer.up, bias=lora_layer.bias)
|
||||
x *= lora_weight * lora_layer.scale()
|
||||
return x
|
||||
|
||||
|
||||
def autocast_linear_forward_sidecar_patches(
|
||||
orig_module: torch.nn.Linear, input: torch.Tensor, patches_and_weights: list[tuple[BaseLayerPatch, float]]
|
||||
) -> torch.Tensor:
|
||||
"""A function that runs a linear layer (quantized or non-quantized) with sidecar patches for a linear layer.
|
||||
Compatible with both quantized and non-quantized Linear layers.
|
||||
"""
|
||||
# First, apply the original linear layer.
|
||||
# NOTE: We slice the input to match the original weight shape in order to work with FluxControlLoRAs, which
|
||||
# change the linear layer's in_features.
|
||||
orig_input = input
|
||||
input = orig_input[..., : orig_module.in_features]
|
||||
output = orig_module._autocast_forward(input)
|
||||
|
||||
# Then, apply layers for which we have optimized implementations.
|
||||
unprocessed_patches_and_weights: list[tuple[BaseLayerPatch, float]] = []
|
||||
for patch, patch_weight in patches_and_weights:
|
||||
# Shallow copy the patch so that we can cast it to the target device without modifying the original patch.
|
||||
patch = copy.copy(patch)
|
||||
patch.to(input.device)
|
||||
|
||||
if isinstance(patch, FluxControlLoRALayer):
|
||||
# Note that we use the original input here, not the sliced input.
|
||||
output += linear_lora_forward(orig_input, patch, patch_weight)
|
||||
elif isinstance(patch, LoRALayer):
|
||||
output += linear_lora_forward(input, patch, patch_weight)
|
||||
else:
|
||||
unprocessed_patches_and_weights.append((patch, patch_weight))
|
||||
|
||||
# Finally, apply any remaining patches.
|
||||
if len(unprocessed_patches_and_weights) > 0:
|
||||
weight, bias = orig_module._cast_weight_bias_for_input(input)
|
||||
# Prepare the original parameters for the patch aggregation.
|
||||
orig_params = {"weight": weight, "bias": bias}
|
||||
# Filter out None values.
|
||||
orig_params = {k: v for k, v in orig_params.items() if v is not None}
|
||||
|
||||
aggregated_param_residuals = orig_module._aggregate_patch_parameters(
|
||||
unprocessed_patches_and_weights, orig_params=orig_params, device=input.device
|
||||
)
|
||||
residual_weight = orig_module._cast_tensor_for_input(aggregated_param_residuals["weight"], input)
|
||||
residual_bias = orig_module._cast_tensor_for_input(aggregated_param_residuals.get("bias", None), input)
|
||||
assert residual_weight is not None
|
||||
output += torch.nn.functional.linear(input, residual_weight, residual_bias)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class CustomLinear(torch.nn.Linear, CustomModuleMixin):
|
||||
def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None:
|
||||
tensor = cast_to_device(tensor, input.device)
|
||||
if (
|
||||
tensor is not None
|
||||
and input.is_floating_point()
|
||||
and tensor.is_floating_point()
|
||||
and not isinstance(tensor, GGMLTensor)
|
||||
and tensor.dtype != input.dtype
|
||||
):
|
||||
tensor = tensor.to(dtype=input.dtype)
|
||||
return tensor
|
||||
|
||||
def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
weight = self._cast_tensor_for_input(self.weight, input)
|
||||
bias = self._cast_tensor_for_input(self.bias, input)
|
||||
assert weight is not None
|
||||
return weight, bias
|
||||
|
||||
def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor:
|
||||
return autocast_linear_forward_sidecar_patches(self, input, self._patches_and_weights)
|
||||
|
||||
def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
weight, bias = self._cast_weight_bias_for_input(input)
|
||||
return torch.nn.functional.linear(input, weight, bias)
|
||||
|
||||
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if len(self._patches_and_weights) > 0:
|
||||
return self._autocast_forward_with_patches(input)
|
||||
elif self._device_autocasting_enabled:
|
||||
return self._autocast_forward(input)
|
||||
elif input.is_floating_point() and (
|
||||
(self.weight.is_floating_point() and self.weight.dtype != input.dtype)
|
||||
or (
|
||||
self.bias is not None
|
||||
and self.bias.is_floating_point()
|
||||
and not isinstance(self.bias, GGMLTensor)
|
||||
and self.bias.dtype != input.dtype
|
||||
)
|
||||
):
|
||||
weight, bias = self._cast_weight_bias_for_input(input)
|
||||
return torch.nn.functional.linear(input, weight, bias)
|
||||
else:
|
||||
return super().forward(input)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import copy
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch
|
||||
from invokeai.backend.patches.layers.param_shape_utils import get_param_shape
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
|
||||
|
||||
class CustomModuleMixin:
|
||||
"""A mixin class for custom modules that enables device autocasting of module parameters."""
|
||||
|
||||
def __init__(self):
|
||||
self._device_autocasting_enabled = False
|
||||
self._patches_and_weights: list[tuple[BaseLayerPatch, float]] = []
|
||||
|
||||
def set_device_autocasting_enabled(self, enabled: bool):
|
||||
"""Pass True to enable autocasting of module parameters to the same device as the input tensor. Pass False to
|
||||
disable autocasting, which results in slightly faster execution speed when we know that device autocasting is
|
||||
not needed.
|
||||
"""
|
||||
self._device_autocasting_enabled = enabled
|
||||
|
||||
def is_device_autocasting_enabled(self) -> bool:
|
||||
"""Check if device autocasting is enabled for the module."""
|
||||
return self._device_autocasting_enabled
|
||||
|
||||
def add_patch(self, patch: BaseLayerPatch, patch_weight: float):
|
||||
"""Add a patch to the module."""
|
||||
self._patches_and_weights.append((patch, patch_weight))
|
||||
|
||||
def clear_patches(self):
|
||||
"""Clear all patches from the module."""
|
||||
self._patches_and_weights = []
|
||||
|
||||
def get_num_patches(self) -> int:
|
||||
"""Get the number of patches in the module."""
|
||||
return len(self._patches_and_weights)
|
||||
|
||||
def _aggregate_patch_parameters(
|
||||
self,
|
||||
patches_and_weights: list[tuple[BaseLayerPatch, float]],
|
||||
orig_params: dict[str, torch.Tensor],
|
||||
device: torch.device | None = None,
|
||||
):
|
||||
"""Helper function that aggregates the parameters from all patches into a single dict."""
|
||||
# HACK(ryand): If the original parameters are in a quantized format whose weights can't be accessed, we replace
|
||||
# them with dummy tensors on the 'meta' device. This allows patch layers to access the shapes of the original
|
||||
# parameters. But, of course, any sub-layers that need to access the actual values of the parameters will fail.
|
||||
for param_name in orig_params.keys():
|
||||
param = orig_params[param_name]
|
||||
if isinstance(param, torch.nn.Parameter) and type(param.data) is torch.Tensor:
|
||||
pass
|
||||
elif type(param) is torch.Tensor:
|
||||
# Plain tensor (e.g. after cast_to_device moved a Parameter to another device).
|
||||
pass
|
||||
elif type(param) is GGMLTensor:
|
||||
# Move to device and dequantize here. Doing it in the patch layer can result in redundant casts /
|
||||
# dequantizations.
|
||||
orig_params[param_name] = param.to(device=device).get_dequantized_tensor()
|
||||
else:
|
||||
orig_params[param_name] = torch.empty(get_param_shape(param), device="meta")
|
||||
|
||||
params: dict[str, torch.Tensor] = {}
|
||||
|
||||
for patch, patch_weight in patches_and_weights:
|
||||
if device is not None:
|
||||
# Shallow copy the patch so that we can cast it to the target device without modifying the original patch.
|
||||
patch = copy.copy(patch)
|
||||
patch.to(device)
|
||||
|
||||
# TODO(ryand): `self` could be a quantized module. Depending on what the patch is doing with the original
|
||||
# parameters, this might fail or return incorrect results.
|
||||
layer_params = patch.get_parameters(orig_params, weight=patch_weight)
|
||||
|
||||
for param_name, param_weight in layer_params.items():
|
||||
if param_name not in params:
|
||||
params[param_name] = param_weight
|
||||
else:
|
||||
params[param_name] += param_weight
|
||||
|
||||
return params
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
from typing import overload
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@overload
|
||||
def add_nullable_tensors(a: None, b: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def add_nullable_tensors(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: ...
|
||||
|
||||
|
||||
@overload
|
||||
def add_nullable_tensors(a: torch.Tensor, b: None) -> torch.Tensor: ...
|
||||
|
||||
|
||||
@overload
|
||||
def add_nullable_tensors(a: None, b: torch.Tensor) -> torch.Tensor: ...
|
||||
|
||||
|
||||
def add_nullable_tensors(a: torch.Tensor | None, b: torch.Tensor | None) -> torch.Tensor | None:
|
||||
if a is None and b is None:
|
||||
return None
|
||||
elif a is None:
|
||||
return b
|
||||
elif b is None:
|
||||
return a
|
||||
else:
|
||||
return a + b
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
from typing import TypeVar
|
||||
|
||||
import torch
|
||||
from diffusers.models.normalization import RMSNorm as DiffusersRMSNorm
|
||||
|
||||
from invokeai.backend.flux.modules.layers import RMSNorm as FluxRMSNorm
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_conv1d import (
|
||||
CustomConv1d,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_conv2d import (
|
||||
CustomConv2d,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_diffusers_rms_norm import (
|
||||
CustomDiffusersRMSNorm,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_embedding import (
|
||||
CustomEmbedding,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_flux_rms_norm import (
|
||||
CustomFluxRMSNorm,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_group_norm import (
|
||||
CustomGroupNorm,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_layer_norm import (
|
||||
CustomLayerNorm,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import (
|
||||
CustomLinear,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import (
|
||||
CustomModuleMixin,
|
||||
)
|
||||
|
||||
AUTOCAST_MODULE_TYPE_MAPPING: dict[type[torch.nn.Module], type[torch.nn.Module]] = {
|
||||
torch.nn.Linear: CustomLinear,
|
||||
torch.nn.Conv1d: CustomConv1d,
|
||||
torch.nn.Conv2d: CustomConv2d,
|
||||
torch.nn.GroupNorm: CustomGroupNorm,
|
||||
torch.nn.Embedding: CustomEmbedding,
|
||||
torch.nn.LayerNorm: CustomLayerNorm,
|
||||
FluxRMSNorm: CustomFluxRMSNorm,
|
||||
DiffusersRMSNorm: CustomDiffusersRMSNorm,
|
||||
}
|
||||
|
||||
try:
|
||||
# These dependencies are not expected to be present on MacOS.
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_8_bit_lt import (
|
||||
CustomInvokeLinear8bitLt,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_nf4 import (
|
||||
CustomInvokeLinearNF4,
|
||||
)
|
||||
from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt
|
||||
from invokeai.backend.quantization.bnb_nf4 import InvokeLinearNF4
|
||||
|
||||
AUTOCAST_MODULE_TYPE_MAPPING[InvokeLinear8bitLt] = CustomInvokeLinear8bitLt
|
||||
AUTOCAST_MODULE_TYPE_MAPPING[InvokeLinearNF4] = CustomInvokeLinearNF4
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
AUTOCAST_MODULE_TYPE_MAPPING_INVERSE = {v: k for k, v in AUTOCAST_MODULE_TYPE_MAPPING.items()}
|
||||
|
||||
|
||||
T = TypeVar("T", bound=torch.nn.Module)
|
||||
|
||||
|
||||
def wrap_custom_layer(module_to_wrap: torch.nn.Module, custom_layer_type: type[T]) -> T:
|
||||
# HACK(ryand): We use custom initialization logic so that we can initialize a new custom layer instance from an
|
||||
# existing layer instance without calling __init__() on the original layer class. We achieve this by copying
|
||||
# the attributes from the original layer instance to the new instance.
|
||||
custom_layer = custom_layer_type.__new__(custom_layer_type)
|
||||
# Note that we share the __dict__.
|
||||
# TODO(ryand): In the future, we may want to do a shallow copy of the __dict__.
|
||||
custom_layer.__dict__ = module_to_wrap.__dict__
|
||||
|
||||
# Initialize the CustomModuleMixin fields.
|
||||
CustomModuleMixin.__init__(custom_layer) # type: ignore
|
||||
return custom_layer
|
||||
|
||||
|
||||
def unwrap_custom_layer(custom_layer: torch.nn.Module, original_layer_type: type[torch.nn.Module]):
|
||||
# HACK(ryand): We use custom initialization logic so that we can initialize a new custom layer instance from an
|
||||
# existing layer instance without calling __init__() on the original layer class. We achieve this by copying
|
||||
# the attributes from the original layer instance to the new instance.
|
||||
original_layer = original_layer_type.__new__(original_layer_type)
|
||||
# Note that we share the __dict__.
|
||||
# TODO(ryand): In the future, we may want to do a shallow copy of the __dict__ and strip out the CustomModuleMixin
|
||||
# fields.
|
||||
original_layer.__dict__ = custom_layer.__dict__
|
||||
return original_layer
|
||||
|
||||
|
||||
def apply_custom_layers_to_model(module: torch.nn.Module, device_autocasting_enabled: bool = False):
|
||||
for name, submodule in module.named_children():
|
||||
override_type = AUTOCAST_MODULE_TYPE_MAPPING.get(type(submodule), None)
|
||||
if override_type is not None:
|
||||
custom_layer = wrap_custom_layer(submodule, override_type)
|
||||
# TODO(ryand): In the future, we should manage this flag on a per-module basis.
|
||||
custom_layer.set_device_autocasting_enabled(device_autocasting_enabled)
|
||||
setattr(module, name, custom_layer)
|
||||
else:
|
||||
# Recursively apply to submodules
|
||||
apply_custom_layers_to_model(submodule, device_autocasting_enabled)
|
||||
|
||||
|
||||
def remove_custom_layers_from_model(module: torch.nn.Module):
|
||||
for name, submodule in module.named_children():
|
||||
override_type = AUTOCAST_MODULE_TYPE_MAPPING_INVERSE.get(type(submodule), None)
|
||||
if override_type is not None:
|
||||
setattr(module, name, unwrap_custom_layer(submodule, override_type))
|
||||
else:
|
||||
remove_custom_layers_from_model(submodule)
|
||||
@@ -0,0 +1,20 @@
|
||||
import itertools
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def get_effective_device(model: torch.nn.Module) -> torch.device:
|
||||
"""A utility to infer the 'effective' device of a model.
|
||||
|
||||
This utility handles the case where a model is partially loaded onto the GPU, so is safer than just calling:
|
||||
`next(iter(model.parameters())).device`.
|
||||
|
||||
In the worst case, this utility has to check all model parameters, so if you already know the intended model device,
|
||||
then it is better to avoid calling this function.
|
||||
"""
|
||||
# If all parameters are on the CPU, return the CPU device. Otherwise, return the first non-CPU device.
|
||||
for p in itertools.chain(model.parameters(), model.buffers()):
|
||||
if p.device.type != "cpu":
|
||||
return p.device
|
||||
|
||||
return torch.device("cpu")
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team
|
||||
"""
|
||||
This module implements a system in which model loaders register the
|
||||
type, base and format of models that they know how to load.
|
||||
|
||||
Use like this:
|
||||
|
||||
cls, model_config, submodel_type = ModelLoaderRegistry.get_implementation(model_config, submodel_type) # type: ignore
|
||||
loaded_model = cls(
|
||||
app_config=app_config,
|
||||
logger=logger,
|
||||
ram_cache=ram_cache,
|
||||
convert_cache=convert_cache
|
||||
).load_model(model_config, submodel_type)
|
||||
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Dict, Optional, Tuple, Type, TypeVar
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load import ModelLoaderBase
|
||||
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
|
||||
|
||||
class ModelLoaderRegistryBase(ABC):
|
||||
"""This class allows model loaders to register their type, base and format."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def register(
|
||||
cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any
|
||||
) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]:
|
||||
"""Define a decorator which registers the subclass of loader."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_implementation(
|
||||
cls, config: AnyModelConfig, submodel_type: Optional[SubModelType]
|
||||
) -> Tuple[Type[ModelLoaderBase], Config_Base, Optional[SubModelType]]:
|
||||
"""
|
||||
Get subclass of ModelLoaderBase registered to handle base and type.
|
||||
|
||||
Parameters:
|
||||
:param config: Model configuration record, as returned by ModelRecordService
|
||||
:param submodel_type: Submodel to fetch (main models only)
|
||||
:return: tuple(loader_class, model_config, submodel_type)
|
||||
|
||||
Note that the returned model config may be different from one what passed
|
||||
in, in the event that a submodel type is provided.
|
||||
"""
|
||||
|
||||
|
||||
TModelLoader = TypeVar("TModelLoader", bound=ModelLoaderBase)
|
||||
|
||||
|
||||
class ModelLoaderRegistry(ModelLoaderRegistryBase):
|
||||
"""
|
||||
This class allows model loaders to register their type, base and format.
|
||||
"""
|
||||
|
||||
_registry: Dict[str, Type[ModelLoaderBase]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(
|
||||
cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any
|
||||
) -> Callable[[Type[TModelLoader]], Type[TModelLoader]]:
|
||||
"""Define a decorator which registers the subclass of loader."""
|
||||
|
||||
def decorator(subclass: Type[TModelLoader]) -> Type[TModelLoader]:
|
||||
key = cls._to_registry_key(base, type, format)
|
||||
if key in cls._registry:
|
||||
raise Exception(
|
||||
f"{subclass.__name__} is trying to register as a loader for {base}/{type}/{format}, but this type of model has already been registered by {cls._registry[key].__name__}"
|
||||
)
|
||||
cls._registry[key] = subclass
|
||||
return subclass
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def get_implementation(
|
||||
cls, config: AnyModelConfig, submodel_type: Optional[SubModelType]
|
||||
) -> Tuple[Type[ModelLoaderBase], Config_Base, Optional[SubModelType]]:
|
||||
"""Get subclass of ModelLoaderBase registered to handle base and type."""
|
||||
|
||||
key1 = cls._to_registry_key(config.base, config.type, config.format) # for a specific base type
|
||||
key2 = cls._to_registry_key(BaseModelType.Any, config.type, config.format) # with wildcard Any
|
||||
implementation = cls._registry.get(key1) or cls._registry.get(key2)
|
||||
if not implementation:
|
||||
raise NotImplementedError(
|
||||
f"No subclass of LoadedModel is registered for base={config.base}, type={config.type}, format={config.format}"
|
||||
)
|
||||
return implementation, config, submodel_type
|
||||
|
||||
@staticmethod
|
||||
def _to_registry_key(base: BaseModelType, type: ModelType, format: ModelFormat) -> str:
|
||||
return "-".join([base.value, type.value, format.value])
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Init file for model_loaders.
|
||||
"""
|
||||
@@ -0,0 +1,212 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for Anima model loading in InvokeAI."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import accelerate
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base
|
||||
from invokeai.backend.model_manager.configs.controlnet import ControlNet_Checkpoint_Anima_Config
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.configs.main import Main_Checkpoint_Anima_Config
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
logger = InvokeAILogger.get_logger(__name__)
|
||||
|
||||
|
||||
def _strip_anima_bundle_prefix(sd: dict) -> dict:
|
||||
"""Strip the transformer-key prefix from an Anima single-file checkpoint.
|
||||
|
||||
Handles both packaging formats:
|
||||
- Official format: keys prefixed with `net.` (e.g. `net.blocks.0...`)
|
||||
- ComfyUI bundled format: transformer keys prefixed with `model.diffusion_model.`
|
||||
alongside `first_stage_model.*` (VAE) and `cond_stage_model.*` (text encoder).
|
||||
|
||||
Only keys under the detected prefix are kept; unrelated keys from bundled
|
||||
checkpoints (VAE, text encoder) are dropped. If no known prefix is present, the
|
||||
state dict is returned unchanged.
|
||||
"""
|
||||
prefix_to_strip = None
|
||||
for prefix in ["model.diffusion_model.", "net."]:
|
||||
if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)):
|
||||
prefix_to_strip = prefix
|
||||
break
|
||||
|
||||
if prefix_to_strip is None:
|
||||
return sd
|
||||
|
||||
stripped_sd: dict = {}
|
||||
for key, value in sd.items():
|
||||
if isinstance(key, str) and key.startswith(prefix_to_strip):
|
||||
stripped_sd[key[len(prefix_to_strip) :]] = value
|
||||
# Skip non-transformer keys from bundled checkpoints (VAE, text encoder)
|
||||
return stripped_sd
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Anima, type=ModelType.Main, format=ModelFormat.Checkpoint)
|
||||
class AnimaCheckpointModel(ModelLoader):
|
||||
"""Class to load Anima transformer models from single-file checkpoints.
|
||||
|
||||
The Anima checkpoint contains both the MiniTrainDIT backbone and the LLM Adapter
|
||||
under a shared `net.` prefix. The loader strips this prefix and instantiates
|
||||
the AnimaTransformer model with the correct architecture parameters.
|
||||
"""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, Checkpoint_Config_Base):
|
||||
raise ValueError("Only CheckpointConfigBase models are currently supported here.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Transformer:
|
||||
return self._load_from_singlefile(config)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_from_singlefile(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
) -> AnyModel:
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from invokeai.backend.anima.anima_transformer import AnimaTransformer
|
||||
|
||||
if not isinstance(config, Main_Checkpoint_Anima_Config):
|
||||
raise TypeError(
|
||||
f"Expected Main_Checkpoint_Anima_Config, got {type(config).__name__}. "
|
||||
"Model configuration type mismatch."
|
||||
)
|
||||
model_path = Path(config.path)
|
||||
|
||||
# Load the state dict from safetensors
|
||||
sd = load_file(model_path)
|
||||
|
||||
# Strip the transformer-key prefix (`net.` or bundled `model.diffusion_model.`).
|
||||
sd = _strip_anima_bundle_prefix(sd)
|
||||
|
||||
# Create an empty AnimaTransformer with Anima's default architecture parameters
|
||||
with accelerate.init_empty_weights():
|
||||
model = AnimaTransformer(
|
||||
max_img_h=240,
|
||||
max_img_w=240,
|
||||
max_frames=1,
|
||||
in_channels=16,
|
||||
out_channels=16,
|
||||
patch_spatial=2,
|
||||
patch_temporal=1,
|
||||
concat_padding_mask=True,
|
||||
model_channels=2048,
|
||||
num_blocks=28,
|
||||
num_heads=16,
|
||||
mlp_ratio=4.0,
|
||||
crossattn_emb_channels=1024,
|
||||
pos_emb_cls="rope3d",
|
||||
# Anima reuses the Cosmos-Predict2 2B Text2Image DiT, which trains with
|
||||
# rope_scale=(t=1.0, h=4.0, w=4.0). The NTK-scaled spatial RoPE base is
|
||||
# mandatory; omitting it (theta=10000 on all axes) shifts every step's
|
||||
# velocity ~7% off and compounds into degraded images. Matches diffusers
|
||||
# CosmosTransformer3DModel rope_scale via *_extrapolation_ratio.
|
||||
rope_h_extrapolation_ratio=4.0,
|
||||
rope_w_extrapolation_ratio=4.0,
|
||||
rope_t_extrapolation_ratio=1.0,
|
||||
use_adaln_lora=True,
|
||||
adaln_lora_dim=256,
|
||||
extra_per_block_abs_pos_emb=False,
|
||||
image_model="anima",
|
||||
)
|
||||
|
||||
# Determine safe dtype
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
model_dtype = TorchDevice.choose_anima_inference_dtype(target_device)
|
||||
|
||||
# Handle memory management
|
||||
new_sd_size = sum(ten.nelement() * model_dtype.itemsize for ten in sd.values())
|
||||
self._ram_cache.make_room(new_sd_size)
|
||||
|
||||
# Convert to target dtype (skip non-float tensors like embedding indices)
|
||||
for k in sd.keys():
|
||||
if sd[k].is_floating_point():
|
||||
sd[k] = sd[k].to(model_dtype)
|
||||
|
||||
# Filter out tensors that are regenerated at runtime and therefore not part of the
|
||||
# in-memory module state. Some community-trained checkpoints (e.g. animaCatTower_v10)
|
||||
# serialize derived pos_embedder buffers/cached tensors that the official model
|
||||
# registers as non-persistent (or recomputes locally).
|
||||
runtime_only_suffixes = (
|
||||
".inv_freq",
|
||||
"pos_embedder.dim_spatial_range",
|
||||
"pos_embedder.dim_temporal_range",
|
||||
"pos_embedder.seq",
|
||||
)
|
||||
keys_to_remove = [k for k in sd.keys() if k.endswith(runtime_only_suffixes)]
|
||||
for k in keys_to_remove:
|
||||
del sd[k]
|
||||
|
||||
load_result = model.load_state_dict(sd, assign=True, strict=False)
|
||||
if load_result.unexpected_keys:
|
||||
raise RuntimeError(
|
||||
f"Checkpoint contains {len(load_result.unexpected_keys)} unexpected keys. "
|
||||
f"This may indicate a corrupted or incompatible checkpoint. "
|
||||
f"First 5 unexpected keys: {load_result.unexpected_keys[:5]}"
|
||||
)
|
||||
if load_result.missing_keys:
|
||||
logger.warning(
|
||||
f"Checkpoint is missing {len(load_result.missing_keys)} keys "
|
||||
f"(expected for inv_freq buffers). First 5: {load_result.missing_keys[:5]}"
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Anima, type=ModelType.ControlNet, format=ModelFormat.Checkpoint)
|
||||
class AnimaControlNetLLLiteModel(ModelLoader):
|
||||
"""Class to load Anima ControlNet-LLLite adapter models from safetensors checkpoints.
|
||||
|
||||
LLLite adapters are standalone files holding a shared conditioning trunk
|
||||
(lllite_conditioning1) plus tiny per-Linear modules (lllite_dit_blocks_*).
|
||||
Hyperparameters are stored in the safetensors metadata (`lllite.*` keys) with
|
||||
state-dict-shape fallbacks.
|
||||
"""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
from safetensors import safe_open
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from invokeai.backend.anima.control_net_lllite import AnimaControlNetLLLite
|
||||
|
||||
if not isinstance(config, ControlNet_Checkpoint_Anima_Config):
|
||||
raise ValueError("Only ControlNet_Checkpoint_Anima_Config models are supported here.")
|
||||
|
||||
# ControlNet type models don't use submodel_type - load the adapter directly
|
||||
model_path = Path(config.path)
|
||||
|
||||
sd = load_file(model_path)
|
||||
with safe_open(model_path, framework="pt", device="cpu") as f:
|
||||
metadata = f.metadata()
|
||||
|
||||
model = AnimaControlNetLLLite.from_state_dict(sd, metadata)
|
||||
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
model_dtype = TorchDevice.choose_anima_inference_dtype(target_device)
|
||||
model.to(dtype=model_dtype)
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,35 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from transformers import CLIPVisionModelWithProjection
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers)
|
||||
class ClipVisionLoader(ModelLoader):
|
||||
"""Class to load CLIPVision models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, Diffusers_Config_Base):
|
||||
raise ValueError("Only DiffusersConfigBase models are currently supported here.")
|
||||
|
||||
if submodel_type is not None:
|
||||
raise Exception("There are no submodels in CLIP Vision models.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
|
||||
model = CLIPVisionModelWithProjection.from_pretrained(
|
||||
model_path, torch_dtype=self._torch_dtype, local_files_only=True
|
||||
)
|
||||
assert isinstance(model, CLIPVisionModelWithProjection)
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.CogView4, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
class CogView4DiffusersModel(GenericDiffusersLoader):
|
||||
"""Class to load CogView4 main models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if isinstance(config, Checkpoint_Config_Base):
|
||||
raise NotImplementedError("CheckpointConfigBase is not implemented for CogView4 models.")
|
||||
|
||||
if submodel_type is None:
|
||||
raise Exception("A submodel type must be provided when loading main pipelines.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
load_class = self.get_hf_load_class(model_path, submodel_type)
|
||||
repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None
|
||||
variant = repo_variant.value if repo_variant else None
|
||||
model_path = model_path / submodel_type.value
|
||||
|
||||
# We force bfloat16 for CogView4 models. It produces black images with float16. I haven't tracked down
|
||||
# specifically which model(s) is/are responsible.
|
||||
dtype = torch.bfloat16
|
||||
try:
|
||||
result: AnyModel = load_class.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=dtype,
|
||||
variant=variant,
|
||||
local_files_only=True,
|
||||
)
|
||||
except OSError as e:
|
||||
if variant and "no file named" in str(
|
||||
e
|
||||
): # try without the variant, just in case user's preferences changed
|
||||
result = load_class.from_pretrained(model_path, torch_dtype=dtype, local_files_only=True)
|
||||
else:
|
||||
raise e
|
||||
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
@@ -0,0 +1,55 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for ControlNet model loading in InvokeAI."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from diffusers import ControlNetModel
|
||||
|
||||
from invokeai.backend.model_manager.configs.controlnet import ControlNet_Checkpoint_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusion1, type=ModelType.ControlNet, format=ModelFormat.Diffusers
|
||||
)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusion1, type=ModelType.ControlNet, format=ModelFormat.Checkpoint
|
||||
)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusion2, type=ModelType.ControlNet, format=ModelFormat.Diffusers
|
||||
)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusion2, type=ModelType.ControlNet, format=ModelFormat.Checkpoint
|
||||
)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusionXL, type=ModelType.ControlNet, format=ModelFormat.Diffusers
|
||||
)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusionXL, type=ModelType.ControlNet, format=ModelFormat.Checkpoint
|
||||
)
|
||||
class ControlNetLoader(GenericDiffusersLoader):
|
||||
"""Class to load ControlNet models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if isinstance(config, ControlNet_Checkpoint_Config_Base):
|
||||
result = ControlNetModel.from_single_file(
|
||||
config.path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
)
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
else:
|
||||
return super()._load_model(config, submodel_type)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
"""Pure state-dict conversion helpers for FLUX.2 single-file checkpoints.
|
||||
|
||||
These functions translate the BFL (black-forest-labs) key layout used by FLUX.2
|
||||
single-file transformer and VAE checkpoints into the diffusers layout that the
|
||||
`Flux2Transformer2DModel` / `AutoencoderKLFlux2` architectures expect.
|
||||
|
||||
They are intentionally free of any file/model-loading side effects so the key
|
||||
remapping can be unit-tested against a synthetic state dict (see
|
||||
`tests/backend/model_manager/load/`). Both the checkpoint and GGUF FLUX.2 loaders
|
||||
delegate here; GGUF quantized tensors are dequantized only where a fused weight
|
||||
must be split (diffusers uses separate Q/K/V projections).
|
||||
|
||||
Based on the diffusers `convert_flux2_to_diffusers.py` key mappings.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def _flux2_chunk_tensor(tensor, chunks: int):
|
||||
"""Chunk a tensor along dim 0, dequantizing GGUF tensors first.
|
||||
|
||||
diffusers uses separate Q/K/V projections, so a fused GGUF weight cannot stay
|
||||
quantized through the split.
|
||||
"""
|
||||
if hasattr(tensor, "get_dequantized_tensor"):
|
||||
tensor = tensor.get_dequantized_tensor()
|
||||
return tensor.chunk(chunks, dim=0)
|
||||
|
||||
|
||||
def _flux2_malformed_for_chunk(tensor, chunks: int) -> bool:
|
||||
"""Return True if a plain tensor cannot be evenly split into `chunks` along dim 0.
|
||||
|
||||
GGUF quantized tensors are always considered well-formed here (their logical
|
||||
shape is only known after dequantization, matching the original GGUF loader,
|
||||
which split them unconditionally).
|
||||
"""
|
||||
if hasattr(tensor, "get_dequantized_tensor"):
|
||||
return False
|
||||
return tensor.dim() < 1 or tensor.shape[0] % chunks != 0
|
||||
|
||||
|
||||
def _flux2_swap_scale_shift(weight):
|
||||
"""Swap the scale/shift halves of an AdaLayerNorm weight (BFL vs diffusers order)."""
|
||||
if hasattr(weight, "get_dequantized_tensor"):
|
||||
weight = weight.get_dequantized_tensor()
|
||||
elif weight.dim() < 1 or weight.shape[0] % 2 != 0:
|
||||
# Defensive: leave malformed plain tensors untouched.
|
||||
return weight
|
||||
shift, scale = weight.chunk(2, dim=0)
|
||||
return torch.cat([scale, shift], dim=0)
|
||||
|
||||
|
||||
def _convert_flux2_double_block_key(key: str, tensor, converted: dict) -> str | None:
|
||||
"""Convert a `double_blocks.X.*` key to `transformer_blocks.X.*` format.
|
||||
|
||||
Returns the new key, or None if the key was consumed by writing directly into
|
||||
`converted` (fused QKV split into separate projections).
|
||||
"""
|
||||
parts = key.split(".")
|
||||
block_idx = parts[1]
|
||||
rest = ".".join(parts[2:])
|
||||
|
||||
prefix = f"transformer_blocks.{block_idx}"
|
||||
|
||||
# Attention QKV: BFL uses fused qkv, diffusers uses separate Q/K/V.
|
||||
if "img_attn.qkv.weight" in rest:
|
||||
if _flux2_malformed_for_chunk(tensor, 3):
|
||||
return key
|
||||
q, k, v = _flux2_chunk_tensor(tensor, 3)
|
||||
converted[f"{prefix}.attn.to_q.weight"] = q
|
||||
converted[f"{prefix}.attn.to_k.weight"] = k
|
||||
converted[f"{prefix}.attn.to_v.weight"] = v
|
||||
return None
|
||||
elif "txt_attn.qkv.weight" in rest:
|
||||
if _flux2_malformed_for_chunk(tensor, 3):
|
||||
return key
|
||||
q, k, v = _flux2_chunk_tensor(tensor, 3)
|
||||
converted[f"{prefix}.attn.add_q_proj.weight"] = q
|
||||
converted[f"{prefix}.attn.add_k_proj.weight"] = k
|
||||
converted[f"{prefix}.attn.add_v_proj.weight"] = v
|
||||
return None
|
||||
|
||||
# Attention output projection
|
||||
if "img_attn.proj.weight" in rest:
|
||||
return f"{prefix}.attn.to_out.0.weight"
|
||||
elif "txt_attn.proj.weight" in rest:
|
||||
return f"{prefix}.attn.to_add_out.weight"
|
||||
|
||||
# Attention norms
|
||||
if "img_attn.norm.query_norm.scale" in rest or "img_attn.norm.query_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_q.weight"
|
||||
elif "img_attn.norm.key_norm.scale" in rest or "img_attn.norm.key_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_k.weight"
|
||||
elif "txt_attn.norm.query_norm.scale" in rest or "txt_attn.norm.query_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_added_q.weight"
|
||||
elif "txt_attn.norm.key_norm.scale" in rest or "txt_attn.norm.key_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_added_k.weight"
|
||||
|
||||
# MLP layers
|
||||
if "img_mlp.0.weight" in rest:
|
||||
return f"{prefix}.ff.linear_in.weight"
|
||||
elif "img_mlp.2.weight" in rest:
|
||||
return f"{prefix}.ff.linear_out.weight"
|
||||
elif "txt_mlp.0.weight" in rest:
|
||||
return f"{prefix}.ff_context.linear_in.weight"
|
||||
elif "txt_mlp.2.weight" in rest:
|
||||
return f"{prefix}.ff_context.linear_out.weight"
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def _convert_flux2_single_block_key(key: str, tensor, converted: dict) -> str | None:
|
||||
"""Convert a `single_blocks.X.*` key to `single_transformer_blocks.X.*` format."""
|
||||
parts = key.split(".")
|
||||
block_idx = parts[1]
|
||||
rest = ".".join(parts[2:])
|
||||
|
||||
prefix = f"single_transformer_blocks.{block_idx}"
|
||||
|
||||
# linear1 is the fused QKV+MLP projection
|
||||
if "linear1.weight" in rest:
|
||||
return f"{prefix}.attn.to_qkv_mlp_proj.weight"
|
||||
elif "linear2.weight" in rest:
|
||||
return f"{prefix}.attn.to_out.weight"
|
||||
|
||||
# Norms
|
||||
if "norm.query_norm.scale" in rest or "norm.query_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_q.weight"
|
||||
elif "norm.key_norm.scale" in rest or "norm.key_norm.weight" in rest:
|
||||
return f"{prefix}.attn.norm_k.weight"
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def convert_flux2_bfl_to_diffusers(sd: dict) -> dict:
|
||||
"""Convert a FLUX.2 transformer BFL-format state dict to diffusers format."""
|
||||
converted: dict = {}
|
||||
|
||||
# Basic key renames
|
||||
key_renames = {
|
||||
"img_in.weight": "x_embedder.weight",
|
||||
"txt_in.weight": "context_embedder.weight",
|
||||
"time_in.in_layer.weight": "time_guidance_embed.timestep_embedder.linear_1.weight",
|
||||
"time_in.out_layer.weight": "time_guidance_embed.timestep_embedder.linear_2.weight",
|
||||
"guidance_in.in_layer.weight": "time_guidance_embed.guidance_embedder.linear_1.weight",
|
||||
"guidance_in.out_layer.weight": "time_guidance_embed.guidance_embedder.linear_2.weight",
|
||||
"double_stream_modulation_img.lin.weight": "double_stream_modulation_img.linear.weight",
|
||||
"double_stream_modulation_txt.lin.weight": "double_stream_modulation_txt.linear.weight",
|
||||
"single_stream_modulation.lin.weight": "single_stream_modulation.linear.weight",
|
||||
"final_layer.linear.weight": "proj_out.weight",
|
||||
"final_layer.adaLN_modulation.1.weight": "norm_out.linear.weight",
|
||||
}
|
||||
|
||||
for old_key, tensor in sd.items():
|
||||
new_key = old_key
|
||||
|
||||
# Apply basic renames
|
||||
if old_key in key_renames:
|
||||
new_key = key_renames[old_key]
|
||||
# Apply scale-shift swap for adaLN modulation weights (BFL and diffusers use
|
||||
# different parameter ordering for AdaLayerNorm).
|
||||
if old_key == "final_layer.adaLN_modulation.1.weight":
|
||||
tensor = _flux2_swap_scale_shift(tensor)
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Convert double_blocks.X.* to transformer_blocks.X.*
|
||||
if old_key.startswith("double_blocks."):
|
||||
new_key = _convert_flux2_double_block_key(old_key, tensor, converted)
|
||||
if new_key is None:
|
||||
continue # Key was handled specially
|
||||
# Convert single_blocks.X.* to single_transformer_blocks.X.*
|
||||
elif old_key.startswith("single_blocks."):
|
||||
new_key = _convert_flux2_single_block_key(old_key, tensor, converted)
|
||||
if new_key is None:
|
||||
continue # Key was handled specially
|
||||
|
||||
if new_key != old_key or new_key not in converted:
|
||||
converted[new_key] = tensor
|
||||
|
||||
return converted
|
||||
|
||||
|
||||
def _convert_flux2_vae_mid_attention_key(rest: str, tensor, block: str):
|
||||
"""Map a `{enc,dec}.mid.attn_1.*` key to the diffusers mid_block attention layout.
|
||||
|
||||
BFL uses Conv2d (shape `[out, in, 1, 1]`), diffusers uses Linear (`[out, in]`), so
|
||||
weight tensors are squeezed. Returns `(new_key, tensor)`.
|
||||
"""
|
||||
attn_prefix = f"{block}.mid_block.attentions.0"
|
||||
if rest.startswith("q."):
|
||||
new_key = f"{attn_prefix}.to_q.{rest[2:]}"
|
||||
elif rest.startswith("k."):
|
||||
new_key = f"{attn_prefix}.to_k.{rest[2:]}"
|
||||
elif rest.startswith("v."):
|
||||
new_key = f"{attn_prefix}.to_v.{rest[2:]}"
|
||||
elif rest.startswith("proj_out."):
|
||||
new_key = f"{attn_prefix}.to_out.0.{rest[9:]}"
|
||||
elif rest.startswith("norm."):
|
||||
return f"{attn_prefix}.group_norm.{rest[5:]}", tensor
|
||||
else:
|
||||
return f"{attn_prefix}.{rest}", tensor
|
||||
|
||||
if rest.endswith(".weight") and hasattr(tensor, "dim") and tensor.dim() == 4:
|
||||
tensor = tensor.squeeze(-1).squeeze(-1)
|
||||
return new_key, tensor
|
||||
|
||||
|
||||
def convert_flux2_vae_bfl_to_diffusers(sd: dict) -> dict:
|
||||
"""Convert a FLUX.2 VAE BFL-format state dict to diffusers format.
|
||||
|
||||
Key differences:
|
||||
- encoder.down.X.block.Y -> encoder.down_blocks.X.resnets.Y
|
||||
- encoder.down.X.downsample.conv -> encoder.down_blocks.X.downsamplers.0.conv
|
||||
- encoder.mid.block_1/2 -> encoder.mid_block.resnets.0/1
|
||||
- encoder.mid.attn_1.q/k/v -> encoder.mid_block.attentions.0.to_q/k/v
|
||||
- encoder.norm_out -> encoder.conv_norm_out
|
||||
- encoder.quant_conv -> quant_conv (top-level)
|
||||
- decoder.up.X -> decoder.up_blocks.(num_blocks-1-X) (reversed order!)
|
||||
- decoder.post_quant_conv -> post_quant_conv (top-level)
|
||||
- *.nin_shortcut -> *.conv_shortcut
|
||||
"""
|
||||
converted: dict = {}
|
||||
num_up_blocks = 4 # Standard VAE has 4 up blocks
|
||||
|
||||
for old_key, tensor in sd.items():
|
||||
new_key = old_key
|
||||
|
||||
# Encoder down blocks: encoder.down.X.block.Y -> encoder.down_blocks.X.resnets.Y
|
||||
match = re.match(r"encoder\.down\.(\d+)\.block\.(\d+)\.(.*)", old_key)
|
||||
if match:
|
||||
block_idx, resnet_idx, rest = match.groups()
|
||||
rest = rest.replace("nin_shortcut", "conv_shortcut")
|
||||
new_key = f"encoder.down_blocks.{block_idx}.resnets.{resnet_idx}.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Encoder downsamplers: encoder.down.X.downsample.conv -> encoder.down_blocks.X.downsamplers.0.conv
|
||||
match = re.match(r"encoder\.down\.(\d+)\.downsample\.conv\.(.*)", old_key)
|
||||
if match:
|
||||
block_idx, rest = match.groups()
|
||||
new_key = f"encoder.down_blocks.{block_idx}.downsamplers.0.conv.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Encoder mid block resnets: encoder.mid.block_1/2 -> encoder.mid_block.resnets.0/1
|
||||
match = re.match(r"encoder\.mid\.block_(\d+)\.(.*)", old_key)
|
||||
if match:
|
||||
block_num, rest = match.groups()
|
||||
resnet_idx = int(block_num) - 1 # block_1 -> resnets.0, block_2 -> resnets.1
|
||||
new_key = f"encoder.mid_block.resnets.{resnet_idx}.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Encoder mid block attention: encoder.mid.attn_1.* -> encoder.mid_block.attentions.0.*
|
||||
match = re.match(r"encoder\.mid\.attn_1\.(.*)", old_key)
|
||||
if match:
|
||||
new_key, tensor = _convert_flux2_vae_mid_attention_key(match.group(1), tensor, "encoder")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Encoder norm_out -> conv_norm_out
|
||||
if old_key.startswith("encoder.norm_out."):
|
||||
new_key = old_key.replace("encoder.norm_out.", "encoder.conv_norm_out.")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Encoder quant_conv -> quant_conv (move to top level)
|
||||
if old_key.startswith("encoder.quant_conv."):
|
||||
new_key = old_key.replace("encoder.quant_conv.", "quant_conv.")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder up blocks (reversed order!): decoder.up.X -> decoder.up_blocks.(num_blocks-1-X)
|
||||
match = re.match(r"decoder\.up\.(\d+)\.block\.(\d+)\.(.*)", old_key)
|
||||
if match:
|
||||
block_idx, resnet_idx, rest = match.groups()
|
||||
new_block_idx = num_up_blocks - 1 - int(block_idx)
|
||||
rest = rest.replace("nin_shortcut", "conv_shortcut")
|
||||
new_key = f"decoder.up_blocks.{new_block_idx}.resnets.{resnet_idx}.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder upsamplers (reversed order!)
|
||||
match = re.match(r"decoder\.up\.(\d+)\.upsample\.conv\.(.*)", old_key)
|
||||
if match:
|
||||
block_idx, rest = match.groups()
|
||||
new_block_idx = num_up_blocks - 1 - int(block_idx)
|
||||
new_key = f"decoder.up_blocks.{new_block_idx}.upsamplers.0.conv.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder mid block resnets: decoder.mid.block_1/2 -> decoder.mid_block.resnets.0/1
|
||||
match = re.match(r"decoder\.mid\.block_(\d+)\.(.*)", old_key)
|
||||
if match:
|
||||
block_num, rest = match.groups()
|
||||
resnet_idx = int(block_num) - 1
|
||||
new_key = f"decoder.mid_block.resnets.{resnet_idx}.{rest}"
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder mid block attention: decoder.mid.attn_1.* -> decoder.mid_block.attentions.0.*
|
||||
match = re.match(r"decoder\.mid\.attn_1\.(.*)", old_key)
|
||||
if match:
|
||||
new_key, tensor = _convert_flux2_vae_mid_attention_key(match.group(1), tensor, "decoder")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder norm_out -> conv_norm_out
|
||||
if old_key.startswith("decoder.norm_out."):
|
||||
new_key = old_key.replace("decoder.norm_out.", "decoder.conv_norm_out.")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Decoder post_quant_conv -> post_quant_conv (move to top level)
|
||||
if old_key.startswith("decoder.post_quant_conv."):
|
||||
new_key = old_key.replace("decoder.post_quant_conv.", "post_quant_conv.")
|
||||
converted[new_key] = tensor
|
||||
continue
|
||||
|
||||
# Keep other keys as-is (like encoder.conv_in, decoder.conv_in, decoder.conv_out, bn.*)
|
||||
converted[new_key] = tensor
|
||||
|
||||
return converted
|
||||
@@ -0,0 +1,104 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for simple diffusers model loading in InvokeAI."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from diffusers.configuration_utils import ConfigMixin
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers)
|
||||
class GenericDiffusersLoader(ModelLoader):
|
||||
"""Class to load simple diffusers models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
model_path = Path(config.path)
|
||||
model_class = self.get_hf_load_class(model_path)
|
||||
if submodel_type is not None:
|
||||
raise Exception(f"There are no submodels in models of type {model_class}")
|
||||
repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None
|
||||
variant = repo_variant.value if repo_variant else None
|
||||
try:
|
||||
result: AnyModel = model_class.from_pretrained(
|
||||
model_path, torch_dtype=self._torch_dtype, variant=variant, local_files_only=True
|
||||
)
|
||||
except OSError as e:
|
||||
if variant and "no file named" in str(
|
||||
e
|
||||
): # try without the variant, just in case user's preferences changed
|
||||
result = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, local_files_only=True)
|
||||
else:
|
||||
raise e
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
|
||||
# TO DO: Add exception handling
|
||||
def get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin:
|
||||
"""Given the model path and submodel, returns the diffusers ModelMixin subclass needed to load."""
|
||||
result = None
|
||||
if submodel_type:
|
||||
try:
|
||||
config = self._load_diffusers_config(model_path, config_name="model_index.json")
|
||||
module, class_name = config[submodel_type.value]
|
||||
result = self._hf_definition_to_type(module=module, class_name=class_name)
|
||||
except KeyError as e:
|
||||
raise ValueError(f'The "{submodel_type}" submodel is not available for this model.') from e
|
||||
else:
|
||||
try:
|
||||
config = self._load_diffusers_config(model_path, config_name="config.json")
|
||||
if class_name := config.get("_class_name"):
|
||||
result = self._hf_definition_to_type(module="diffusers", class_name=class_name)
|
||||
elif class_name := config.get("architectures"):
|
||||
result = self._hf_definition_to_type(module="transformers", class_name=class_name[0])
|
||||
else:
|
||||
raise RuntimeError("Unable to decipher Load Class based on given config.json")
|
||||
except KeyError as e:
|
||||
raise ValueError("An expected config.json file is missing from this model.") from e
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
# TO DO: Add exception handling
|
||||
def _hf_definition_to_type(self, module: str, class_name: str) -> ModelMixin: # fix with correct type
|
||||
if module in [
|
||||
"diffusers",
|
||||
"transformers",
|
||||
"invokeai.backend.quantization.fast_quantized_transformers_model",
|
||||
"invokeai.backend.quantization.fast_quantized_diffusion_model",
|
||||
]:
|
||||
res_type = sys.modules[module]
|
||||
else:
|
||||
res_type = sys.modules["diffusers"].pipelines
|
||||
result: ModelMixin = getattr(res_type, class_name)
|
||||
return result
|
||||
|
||||
def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> dict[str, Any]:
|
||||
return ConfigLoader.load_config(model_path, config_name=config_name)
|
||||
|
||||
|
||||
class ConfigLoader(ConfigMixin):
|
||||
"""Subclass of ConfigMixin for loading diffusers configuration files."""
|
||||
|
||||
@classmethod
|
||||
def load_config(cls, *args: Any, **kwargs: Any) -> dict[str, Any]: # pyright: ignore [reportIncompatibleMethodOverride]
|
||||
"""Load a diffusrs ConfigMixin configuration."""
|
||||
cls.config_name = kwargs.pop("config_name")
|
||||
# TODO(psyche): the types on this diffusers method are not correct
|
||||
return super().load_config(*args, **kwargs) # type: ignore
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for IP Adapter model loading in InvokeAI."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.ip_adapter.ip_adapter import build_ip_adapter
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load import ModelLoader, ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
from invokeai.backend.raw_model import RawModel
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.Checkpoint)
|
||||
class IPAdapterInvokeAILoader(ModelLoader):
|
||||
"""Class to load IP Adapter diffusers models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("There are no submodels in an IP-Adapter model.")
|
||||
model_path = Path(config.path)
|
||||
model: RawModel = build_ip_adapter(
|
||||
ip_adapter_ckpt_path=model_path,
|
||||
device=torch.device("cpu"),
|
||||
dtype=self._torch_dtype,
|
||||
)
|
||||
return model
|
||||
@@ -0,0 +1,29 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from transformers import LlavaOnevisionForConditionalGeneration
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LlavaOnevision, format=ModelFormat.Diffusers)
|
||||
class LlavaOnevisionModelLoader(ModelLoader):
|
||||
"""Class for loading LLaVA Onevision VLLM models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("Unexpected submodel requested for LLaVA OneVision model.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
model = LlavaOnevisionForConditionalGeneration.from_pretrained(
|
||||
model_path, local_files_only=True, torch_dtype=self._torch_dtype
|
||||
)
|
||||
assert isinstance(model, LlavaOnevisionForConditionalGeneration)
|
||||
return model
|
||||
@@ -0,0 +1,198 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for LoRA model loading in InvokeAI."""
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from invokeai.app.services.config import InvokeAIAppConfig
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.omi.omi import convert_from_omi
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.anima_lora_conversion_utils import lora_model_from_anima_state_dict
|
||||
from invokeai.backend.patches.lora_conversions.flux_aitoolkit_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_aitoolkit_format,
|
||||
lora_model_from_flux_aitoolkit_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_bfl_peft_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_bfl_peft_format,
|
||||
lora_model_from_flux2_bfl_peft_state_dict,
|
||||
lora_model_from_flux_bfl_peft_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import (
|
||||
is_state_dict_likely_flux_control,
|
||||
lora_model_from_flux_control_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import (
|
||||
is_state_dict_flux2_diffusers_format,
|
||||
is_state_dict_likely_in_flux_diffusers_format,
|
||||
lora_model_from_flux2_diffusers_state_dict,
|
||||
lora_model_from_flux_diffusers_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_kohya_format,
|
||||
lora_model_from_flux_kohya_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_onetrainer_bfl_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_onetrainer_bfl_format,
|
||||
lora_model_from_flux_onetrainer_bfl_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_onetrainer_format,
|
||||
lora_model_from_flux_onetrainer_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.flux_xlabs_lora_conversion_utils import (
|
||||
is_state_dict_likely_in_flux_xlabs_format,
|
||||
lora_model_from_flux_xlabs_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.peft_adapter_utils import normalize_peft_adapter_names
|
||||
from invokeai.backend.patches.lora_conversions.qwen_image_lora_conversion_utils import (
|
||||
lora_model_from_qwen_image_state_dict,
|
||||
)
|
||||
from invokeai.backend.patches.lora_conversions.sd_lora_conversion_utils import lora_model_from_sd_state_dict
|
||||
from invokeai.backend.patches.lora_conversions.sdxl_lora_conversion_utils import convert_sdxl_keys_to_diffusers_format
|
||||
from invokeai.backend.patches.lora_conversions.z_image_lora_conversion_utils import lora_model_from_z_image_state_dict
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.LoRA, format=ModelFormat.OMI)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.LoRA, format=ModelFormat.OMI)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LoRA, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LoRA, format=ModelFormat.LyCORIS)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlLoRa, format=ModelFormat.LyCORIS)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlLoRa, format=ModelFormat.Diffusers)
|
||||
class LoRALoader(ModelLoader):
|
||||
"""Class to load LoRA models."""
|
||||
|
||||
# We cheat a little bit to get access to the model base
|
||||
def __init__(
|
||||
self,
|
||||
app_config: InvokeAIAppConfig,
|
||||
logger: Logger,
|
||||
ram_cache: ModelCache,
|
||||
):
|
||||
"""Initialize the loader."""
|
||||
super().__init__(app_config, logger, ram_cache)
|
||||
self._model_base: Optional[BaseModelType] = None
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("There are no submodels in a LoRA model.")
|
||||
model_path = Path(config.path)
|
||||
assert self._model_base is not None
|
||||
|
||||
# Load the state dict from the model file.
|
||||
if model_path.suffix == ".safetensors":
|
||||
state_dict = load_file(model_path.absolute().as_posix(), device="cpu")
|
||||
else:
|
||||
state_dict = torch.load(model_path, map_location="cpu")
|
||||
|
||||
# Strip 'bundle_emb' keys - these are unused and currently cause downstream errors.
|
||||
# To revisit later to determine if they're needed/useful.
|
||||
state_dict = {k: v for k, v in state_dict.items() if not k.startswith("bundle_emb")}
|
||||
|
||||
# Normalize PEFT named-adapter keys (e.g. `lora_A.default.weight` → `lora_A.weight`)
|
||||
# so the downstream format detectors and converters see canonical PEFT keys.
|
||||
state_dict = normalize_peft_adapter_names(state_dict)
|
||||
|
||||
# At the time of writing, we support the OMI standard for base models Flux and SDXL
|
||||
if config.format == ModelFormat.OMI and self._model_base in [
|
||||
BaseModelType.StableDiffusionXL,
|
||||
BaseModelType.Flux,
|
||||
]:
|
||||
state_dict = convert_from_omi(state_dict, config.base) # type: ignore
|
||||
|
||||
# Apply state_dict key conversions, if necessary.
|
||||
if self._model_base == BaseModelType.StableDiffusionXL:
|
||||
state_dict = convert_sdxl_keys_to_diffusers_format(state_dict)
|
||||
model = lora_model_from_sd_state_dict(state_dict=state_dict)
|
||||
elif self._model_base in (BaseModelType.Flux, BaseModelType.Flux2):
|
||||
if config.format is ModelFormat.OMI:
|
||||
# HACK(ryand): We set alpha=None for diffusers PEFT format models. These models are typically
|
||||
# distributed as a single file without the associated metadata containing the alpha value. We chose
|
||||
# alpha=None, because this is treated as alpha=rank internally in `LoRALayerBase.scale()`. alpha=rank
|
||||
# is a popular choice. For example, in the diffusers training scripts:
|
||||
# https://github.com/huggingface/diffusers/blob/main/examples/dreambooth/train_dreambooth_lora_flux.py#L1194
|
||||
#
|
||||
# We assume the same for LyCORIS models in diffusers key format.
|
||||
model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict, alpha=None)
|
||||
elif config.format is ModelFormat.LyCORIS:
|
||||
if is_state_dict_likely_in_flux_diffusers_format(state_dict=state_dict):
|
||||
if is_state_dict_flux2_diffusers_format(state_dict=state_dict):
|
||||
# Flux2 Klein native diffusers naming (to_qkv_mlp_proj, ff.linear_in, etc.)
|
||||
model = lora_model_from_flux2_diffusers_state_dict(state_dict=state_dict, alpha=None)
|
||||
else:
|
||||
# Flux.1 diffusers naming (to_q/to_k/to_v, ff.net.0.proj, etc.)
|
||||
model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict, alpha=None)
|
||||
elif is_state_dict_likely_in_flux_kohya_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_kohya_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_onetrainer_bfl_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_onetrainer_bfl_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_onetrainer_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_onetrainer_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_flux_control(state_dict=state_dict):
|
||||
model = lora_model_from_flux_control_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_aitoolkit_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_aitoolkit_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_xlabs_format(state_dict=state_dict):
|
||||
model = lora_model_from_flux_xlabs_state_dict(state_dict=state_dict)
|
||||
elif is_state_dict_likely_in_flux_bfl_peft_format(state_dict=state_dict):
|
||||
if self._model_base == BaseModelType.Flux2:
|
||||
# FLUX.2 Klein uses Flux2Transformer2DModel (diffusers naming),
|
||||
# so we need to convert BFL keys to diffusers naming.
|
||||
model = lora_model_from_flux2_bfl_peft_state_dict(state_dict=state_dict, alpha=None)
|
||||
else:
|
||||
# FLUX.1 uses BFL Flux class, so BFL keys work directly.
|
||||
model = lora_model_from_flux_bfl_peft_state_dict(state_dict=state_dict, alpha=None)
|
||||
else:
|
||||
raise ValueError("LoRA model is in unsupported FLUX format")
|
||||
else:
|
||||
raise ValueError(f"LoRA model is in unsupported FLUX format: {config.format}")
|
||||
elif self._model_base in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]:
|
||||
# Currently, we don't apply any conversions for SD1 and SD2 LoRA models.
|
||||
model = lora_model_from_sd_state_dict(state_dict=state_dict)
|
||||
elif self._model_base == BaseModelType.ZImage:
|
||||
# Z-Image LoRAs use diffusers PEFT format with transformer and/or Qwen3 encoder layers.
|
||||
# We set alpha=None to use rank as alpha (common default).
|
||||
model = lora_model_from_z_image_state_dict(state_dict=state_dict, alpha=None)
|
||||
elif self._model_base == BaseModelType.QwenImage:
|
||||
model = lora_model_from_qwen_image_state_dict(state_dict=state_dict, alpha=None)
|
||||
elif self._model_base == BaseModelType.Anima:
|
||||
# Anima LoRAs use Kohya-style or diffusers PEFT format targeting Cosmos DiT blocks.
|
||||
model = lora_model_from_anima_state_dict(state_dict=state_dict, alpha=None)
|
||||
else:
|
||||
raise ValueError(f"Unsupported LoRA base model: {self._model_base}")
|
||||
|
||||
model.to(dtype=self._torch_dtype)
|
||||
return model
|
||||
|
||||
def _get_model_path(self, config: AnyModelConfig) -> Path:
|
||||
# cheating a little - we remember this variable for using in the subsequent call to _load_model()
|
||||
self._model_base = config.base
|
||||
|
||||
model_base_path = self._app_config.models_path
|
||||
model_path = model_base_path / config.path
|
||||
|
||||
if config.format == ModelFormat.Diffusers:
|
||||
for ext in ["safetensors", "bin"]: # return path to the safetensors file inside the folder
|
||||
path = model_base_path / config.path / f"pytorch_lora_weights.{ext}"
|
||||
if path.exists():
|
||||
model_path = path
|
||||
break
|
||||
|
||||
return model_path.resolve()
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for Onnx model loading in InvokeAI."""
|
||||
|
||||
# This should work the same as Stable Diffusion pipelines
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.ONNX)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive)
|
||||
class OnnyxDiffusersModel(GenericDiffusersLoader):
|
||||
"""Class to load onnx models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not submodel_type is not None:
|
||||
raise Exception("A submodel type must be provided when loading onnx pipelines.")
|
||||
model_path = Path(config.path)
|
||||
load_class = self.get_hf_load_class(model_path, submodel_type)
|
||||
repo_variant = getattr(config, "repo_variant", None)
|
||||
variant = repo_variant.value if repo_variant else None
|
||||
model_path = model_path / submodel_type.value
|
||||
result: AnyModel = load_class.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
variant=variant,
|
||||
local_files_only=True,
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,556 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import accelerate
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.configs.main import (
|
||||
Main_Checkpoint_QwenImage_Config,
|
||||
Main_GGUF_QwenImage_Config,
|
||||
)
|
||||
from invokeai.backend.model_manager.configs.qwen_vl_encoder import (
|
||||
QwenVLEncoder_Checkpoint_Config,
|
||||
QwenVLEncoder_Diffusers_Config,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
QwenImageVariantType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
|
||||
|
||||
def _strip_comfyui_prefix(sd: dict) -> dict:
|
||||
"""Strip ComfyUI-style `model.diffusion_model.` / `diffusion_model.` prefixes from keys."""
|
||||
prefix_to_strip = None
|
||||
for prefix in ["model.diffusion_model.", "diffusion_model."]:
|
||||
if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)):
|
||||
prefix_to_strip = prefix
|
||||
break
|
||||
if prefix_to_strip is None:
|
||||
return sd
|
||||
stripped: dict = {}
|
||||
for key, value in sd.items():
|
||||
if isinstance(key, str) and key.startswith(prefix_to_strip):
|
||||
stripped[key[len(prefix_to_strip) :]] = value
|
||||
else:
|
||||
stripped[key] = value
|
||||
return stripped
|
||||
|
||||
|
||||
def _dequantize_comfyui_fp8(sd: dict, compute_dtype: torch.dtype) -> int:
|
||||
"""Dequantize ComfyUI-style fp8_scaled weights in-place. Returns count of dequantized tensors.
|
||||
|
||||
Weights are dequantized directly to `compute_dtype` (typically bf16) instead of via a
|
||||
full-precision float32 intermediate. The previous float32 path materialised a complete
|
||||
4-byte/param copy of the model before a separate downcast pass, spiking peak RAM to ~2x the
|
||||
final bf16 size (~80GB for the 20B Qwen-Image transformer). Multiplying in the target dtype
|
||||
keeps the dict at the bf16 model size plus a single transient tensor. fp8 has only 3 mantissa
|
||||
bits and bf16 shares float32's exponent range, so the bf16 multiply loses no meaningful
|
||||
precision here.
|
||||
|
||||
Two key naming schemes are in the wild:
|
||||
- `<path>.weight` + `<path>.weight_scale` (FLUX, Z-Image style)
|
||||
- `<path>.weight` + `<path>.scale_weight` (Qwen2.5-VL fp8_scaled style, also
|
||||
emits `<path>.scale_input` for activation scaling that we discard).
|
||||
"""
|
||||
scale_suffixes = (".weight_scale", ".scale_weight")
|
||||
weight_scale_keys = [k for k in sd.keys() if isinstance(k, str) and k.endswith(scale_suffixes)]
|
||||
count = 0
|
||||
for scale_key in weight_scale_keys:
|
||||
for suffix in scale_suffixes:
|
||||
if scale_key.endswith(suffix):
|
||||
weight_key = scale_key[: -len(suffix)] + ".weight"
|
||||
break
|
||||
if weight_key not in sd:
|
||||
continue
|
||||
weight = sd[weight_key].to(compute_dtype)
|
||||
scale = sd[scale_key].to(compute_dtype)
|
||||
if scale.shape != weight.shape and scale.numel() > 1:
|
||||
for dim in range(len(weight.shape)):
|
||||
if dim < len(scale.shape) and scale.shape[dim] != weight.shape[dim]:
|
||||
block_size = weight.shape[dim] // scale.shape[dim]
|
||||
if block_size > 1:
|
||||
scale = scale.repeat_interleave(block_size, dim=dim)
|
||||
sd[weight_key] = weight * scale
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def _remap_qwen_vl_checkpoint_keys(sd: dict) -> dict:
|
||||
"""Remap legacy ComfyUI Qwen2.5-VL single-file keys to the transformers layout.
|
||||
|
||||
ComfyUI single-file checkpoints use the legacy Qwen2.5-VL key layout
|
||||
(`visual.X`, `model.X`); transformers ≥4.50 expects `model.visual.X` and
|
||||
`model.language_model.X`. This applies the same conversion mapping that
|
||||
`Qwen2_5_VLForConditionalGeneration.from_pretrained` would, since
|
||||
`load_state_dict` does not.
|
||||
|
||||
transformers ≤4.x exposed this as `_checkpoint_conversion_mapping`, but 5.x
|
||||
dropped it (returns `{}`), so we fall back to the legacy mapping ourselves. The
|
||||
negative lookahead keeps already-converted keys untouched, so the remap is safe
|
||||
(and idempotent) for both legacy and new-layout single-file checkpoints.
|
||||
"""
|
||||
import re
|
||||
|
||||
from transformers import Qwen2_5_VLForConditionalGeneration
|
||||
|
||||
key_mapping = Qwen2_5_VLForConditionalGeneration._checkpoint_conversion_mapping or {
|
||||
r"^visual": "model.visual",
|
||||
r"^model(?!\.(language_model|visual))": "model.language_model",
|
||||
}
|
||||
if not key_mapping:
|
||||
return sd
|
||||
|
||||
remapped_sd: dict = {}
|
||||
for old_key, tensor in sd.items():
|
||||
new_key = old_key
|
||||
if isinstance(old_key, str):
|
||||
for pattern, replacement in key_mapping.items():
|
||||
new_key, n_replace = re.subn(pattern, replacement, new_key)
|
||||
if n_replace > 0:
|
||||
break
|
||||
remapped_sd[new_key] = tensor
|
||||
return remapped_sd
|
||||
|
||||
|
||||
def _strip_quantization_metadata(sd: dict) -> None:
|
||||
"""Strip ComfyUI fp8 quantization metadata keys in-place."""
|
||||
keys_to_drop = [
|
||||
k
|
||||
for k in sd.keys()
|
||||
if isinstance(k, str)
|
||||
and (
|
||||
k.endswith(".weight_scale")
|
||||
or k.endswith(".scale_weight")
|
||||
or k.endswith(".scale_input")
|
||||
or "comfy_quant" in k
|
||||
or k == "scaled_fp8"
|
||||
)
|
||||
]
|
||||
for k in keys_to_drop:
|
||||
del sd[k]
|
||||
|
||||
|
||||
def _build_qwen_image_transformer_config(sd: dict, is_edit: bool) -> dict:
|
||||
"""Auto-detect Qwen Image transformer architecture parameters from the state dict.
|
||||
|
||||
Works for both GGUF (GGMLTensor) and plain safetensors (torch.Tensor) state dicts.
|
||||
Mutates nothing.
|
||||
"""
|
||||
from diffusers import QwenImageTransformer2DModel
|
||||
|
||||
def _shape(t):
|
||||
return t.tensor_shape if isinstance(t, GGMLTensor) else t.shape
|
||||
|
||||
num_layers = 0
|
||||
for key in sd.keys():
|
||||
if isinstance(key, str) and key.startswith("transformer_blocks."):
|
||||
parts = key.split(".")
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
num_layers = max(num_layers, int(parts[1]) + 1)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
num_attention_heads = 24
|
||||
attention_head_dim = 128
|
||||
in_channels = 64
|
||||
|
||||
if "img_in.weight" in sd:
|
||||
shape = _shape(sd["img_in.weight"])
|
||||
hidden_dim = shape[0]
|
||||
in_channels = shape[1]
|
||||
num_attention_heads = hidden_dim // attention_head_dim
|
||||
|
||||
joint_attention_dim = 3584
|
||||
if "txt_in.weight" in sd:
|
||||
joint_attention_dim = _shape(sd["txt_in.weight"])[1]
|
||||
|
||||
model_config: dict = {
|
||||
"patch_size": 2,
|
||||
"in_channels": in_channels,
|
||||
"out_channels": 16,
|
||||
"num_layers": num_layers if num_layers > 0 else 60,
|
||||
"attention_head_dim": attention_head_dim,
|
||||
"num_attention_heads": num_attention_heads,
|
||||
"joint_attention_dim": joint_attention_dim,
|
||||
"guidance_embeds": False,
|
||||
"axes_dims_rope": (16, 56, 56),
|
||||
}
|
||||
|
||||
# zero_cond_t enables dual modulation for noisy vs reference patches in edit-variant
|
||||
# models. Setting it on txt2img models produces garbage. Requires diffusers 0.37+.
|
||||
import inspect
|
||||
|
||||
if is_edit and "zero_cond_t" in inspect.signature(QwenImageTransformer2DModel.__init__).parameters:
|
||||
model_config["zero_cond_t"] = True
|
||||
|
||||
return model_config
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
class QwenImageDiffusersModel(GenericDiffusersLoader):
|
||||
"""Class to load Qwen Image Edit main models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if isinstance(config, Checkpoint_Config_Base):
|
||||
raise NotImplementedError("CheckpointConfigBase is not implemented for Qwen Image Edit models.")
|
||||
|
||||
if submodel_type is None:
|
||||
raise Exception("A submodel type must be provided when loading main pipelines.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
load_class = self.get_hf_load_class(model_path, submodel_type)
|
||||
repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None
|
||||
variant = repo_variant.value if repo_variant else None
|
||||
model_path = model_path / submodel_type.value
|
||||
|
||||
# We force bfloat16 for Qwen Image Edit models.
|
||||
# Use `dtype` (newer) with fallback to `torch_dtype` (older diffusers).
|
||||
dtype_kwarg = {"dtype": torch.bfloat16}
|
||||
try:
|
||||
result: AnyModel = load_class.from_pretrained(
|
||||
model_path,
|
||||
**dtype_kwarg,
|
||||
variant=variant,
|
||||
local_files_only=True,
|
||||
)
|
||||
except TypeError:
|
||||
# Older diffusers uses torch_dtype instead of dtype
|
||||
dtype_kwarg = {"torch_dtype": torch.bfloat16}
|
||||
result = load_class.from_pretrained(
|
||||
model_path,
|
||||
**dtype_kwarg,
|
||||
variant=variant,
|
||||
local_files_only=True,
|
||||
)
|
||||
except OSError as e:
|
||||
if variant and "no file named" in str(e):
|
||||
result = load_class.from_pretrained(model_path, **dtype_kwarg, local_files_only=True)
|
||||
else:
|
||||
raise e
|
||||
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.GGUFQuantized)
|
||||
class QwenImageGGUFCheckpointModel(ModelLoader):
|
||||
"""Class to load GGUF-quantized Qwen Image Edit transformer models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, Checkpoint_Config_Base):
|
||||
raise ValueError("Only CheckpointConfigBase models are currently supported here.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Transformer:
|
||||
return self._load_from_singlefile(config)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_from_singlefile(self, config: AnyModelConfig) -> AnyModel:
|
||||
from diffusers import QwenImageTransformer2DModel
|
||||
|
||||
if not isinstance(config, Main_GGUF_QwenImage_Config):
|
||||
raise TypeError(f"Expected Main_GGUF_QwenImage_Config, got {type(config).__name__}.")
|
||||
model_path = Path(config.path)
|
||||
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device)
|
||||
|
||||
sd = gguf_sd_loader(model_path, compute_dtype=compute_dtype)
|
||||
sd = _strip_comfyui_prefix(sd)
|
||||
|
||||
is_edit = getattr(config, "variant", None) == QwenImageVariantType.Edit
|
||||
model_config = _build_qwen_image_transformer_config(sd, is_edit=is_edit)
|
||||
|
||||
with accelerate.init_empty_weights():
|
||||
model = QwenImageTransformer2DModel(**model_config)
|
||||
|
||||
model.load_state_dict(sd, strict=False, assign=True)
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.Checkpoint)
|
||||
class QwenImageCheckpointModel(ModelLoader):
|
||||
"""Loads Qwen Image transformer models from single-file safetensors checkpoints
|
||||
(e.g. ComfyUI fp8_scaled, plain bf16/fp16). Dequantizes ComfyUI fp8 scaling to
|
||||
bf16 at load time; the `default_settings.fp8_storage` toggle then optionally
|
||||
re-casts to fp8 for VRAM savings."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, Checkpoint_Config_Base):
|
||||
raise ValueError("Only CheckpointConfigBase models are currently supported here.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Transformer:
|
||||
model = self._load_from_singlefile(config)
|
||||
return self._apply_fp8_layerwise_casting(model, config, submodel_type)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_from_singlefile(self, config: AnyModelConfig) -> AnyModel:
|
||||
from diffusers import QwenImageTransformer2DModel
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
logger = InvokeAILogger.get_logger(self.__class__.__name__)
|
||||
|
||||
if not isinstance(config, Main_Checkpoint_QwenImage_Config):
|
||||
raise TypeError(f"Expected Main_Checkpoint_QwenImage_Config, got {type(config).__name__}.")
|
||||
model_path = Path(config.path)
|
||||
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device)
|
||||
|
||||
sd = load_file(str(model_path))
|
||||
sd = _strip_comfyui_prefix(sd)
|
||||
|
||||
dequantized = _dequantize_comfyui_fp8(sd, model_dtype)
|
||||
if dequantized > 0:
|
||||
logger.info(f"Dequantized {dequantized} ComfyUI-quantized weights")
|
||||
_strip_quantization_metadata(sd)
|
||||
|
||||
is_edit = getattr(config, "variant", None) == QwenImageVariantType.Edit
|
||||
model_config = _build_qwen_image_transformer_config(sd, is_edit=is_edit)
|
||||
|
||||
with accelerate.init_empty_weights():
|
||||
model = QwenImageTransformer2DModel(**model_config)
|
||||
|
||||
# Dequantized fp8 weights are already at model_dtype; this only casts any remaining
|
||||
# non-quantized float weights (e.g. a plain fp16/fp32 checkpoint) to the compute dtype
|
||||
# so the cache reservation below is sized from the actual post-cast tensors.
|
||||
for k in list(sd.keys()):
|
||||
if sd[k].is_floating_point():
|
||||
sd[k] = sd[k].to(model_dtype)
|
||||
|
||||
new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values())
|
||||
self._ram_cache.make_room(new_sd_size)
|
||||
|
||||
model.load_state_dict(sd, strict=False, assign=True)
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.QwenVLEncoder, format=ModelFormat.QwenVLEncoder)
|
||||
class QwenVLEncoderLoader(ModelLoader):
|
||||
"""Loads a standalone Qwen2.5-VL encoder (text_encoder/ + tokenizer/ + processor/)."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, QwenVLEncoder_Diffusers_Config):
|
||||
raise TypeError(f"Expected QwenVLEncoder_Diffusers_Config, got {type(config).__name__}.")
|
||||
|
||||
from transformers import AutoTokenizer, Qwen2_5_VLForConditionalGeneration
|
||||
|
||||
model_path = Path(config.path)
|
||||
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device)
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Tokenizer:
|
||||
tokenizer_path = model_path / "tokenizer"
|
||||
return AutoTokenizer.from_pretrained(str(tokenizer_path), local_files_only=True)
|
||||
case SubModelType.TextEncoder:
|
||||
encoder_path = model_path / "text_encoder"
|
||||
return Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
str(encoder_path),
|
||||
torch_dtype=model_dtype,
|
||||
low_cpu_mem_usage=True,
|
||||
local_files_only=True,
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Tokenizer and TextEncoder submodels are supported. "
|
||||
f"Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.QwenVLEncoder, format=ModelFormat.Checkpoint)
|
||||
class QwenVLEncoderCheckpointLoader(ModelLoader):
|
||||
"""Loads a single-file Qwen2.5-VL encoder checkpoint (e.g. ComfyUI fp8_scaled).
|
||||
|
||||
The checkpoint bundles the language model and the visual tower into one
|
||||
safetensors file. Tokenizer + processor are pulled from HuggingFace
|
||||
(`Qwen/Qwen2.5-VL-7B-Instruct`) on first use, with offline cache fallback.
|
||||
"""
|
||||
|
||||
DEFAULT_HF_REPO = "Qwen/Qwen2.5-VL-7B-Instruct"
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, QwenVLEncoder_Checkpoint_Config):
|
||||
raise TypeError(f"Expected QwenVLEncoder_Checkpoint_Config, got {type(config).__name__}.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Tokenizer:
|
||||
return self._load_tokenizer_with_offline_fallback()
|
||||
case SubModelType.TextEncoder:
|
||||
return self._load_text_encoder_from_singlefile(config)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Tokenizer and TextEncoder submodels are supported. "
|
||||
f"Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_tokenizer_with_offline_fallback(self) -> AnyModel:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
logger = InvokeAILogger.get_logger(self.__class__.__name__)
|
||||
|
||||
try:
|
||||
return AutoTokenizer.from_pretrained(self.DEFAULT_HF_REPO, local_files_only=True)
|
||||
except OSError:
|
||||
logger.info(
|
||||
f"Tokenizer for single-file Qwen VL encoder not found in HuggingFace cache; "
|
||||
f"downloading from {self.DEFAULT_HF_REPO} (one-time, requires network access)."
|
||||
)
|
||||
try:
|
||||
return AutoTokenizer.from_pretrained(self.DEFAULT_HF_REPO)
|
||||
except OSError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to load Qwen VL tokenizer. Single-file Qwen VL encoder checkpoints do not "
|
||||
f"include the tokenizer; it must be downloaded from HuggingFace ({self.DEFAULT_HF_REPO}) "
|
||||
f"on first use. Either restore network access, or install the encoder in the "
|
||||
f"diffusers folder layout (text_encoder/ + tokenizer/) instead. Original error: {e}"
|
||||
) from e
|
||||
|
||||
def _load_text_encoder_from_singlefile(self, config: QwenVLEncoder_Checkpoint_Config) -> AnyModel:
|
||||
from safetensors.torch import load_file
|
||||
from transformers import AutoConfig, Qwen2_5_VLForConditionalGeneration
|
||||
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
logger = InvokeAILogger.get_logger(self.__class__.__name__)
|
||||
|
||||
model_path = Path(config.path)
|
||||
|
||||
target_device = TorchDevice.choose_torch_device()
|
||||
model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device)
|
||||
|
||||
sd = load_file(str(model_path))
|
||||
|
||||
# Dequantize ComfyUI-style fp8 weights, then strip the now-unused quantization
|
||||
# metadata (`scale_input` is the activation scale ComfyUI's fp8 matmul kernels
|
||||
# use at runtime — we run the encoder in bf16 after dequantization).
|
||||
dequantized_count = _dequantize_comfyui_fp8(sd, model_dtype)
|
||||
if dequantized_count > 0:
|
||||
logger.info(f"Dequantized {dequantized_count} ComfyUI-quantized weights")
|
||||
_strip_quantization_metadata(sd)
|
||||
|
||||
# ComfyUI single-file checkpoints use the legacy Qwen2.5-VL key layout
|
||||
# (`visual.X`, `model.X`); remap to the `model.visual.X` / `model.language_model.X`
|
||||
# layout transformers expects. See `_remap_qwen_vl_checkpoint_keys` for details.
|
||||
sd = _remap_qwen_vl_checkpoint_keys(sd)
|
||||
|
||||
# Cast to compute dtype (skip integer/index tensors)
|
||||
for k in list(sd.keys()):
|
||||
if sd[k].is_floating_point():
|
||||
sd[k] = sd[k].to(model_dtype)
|
||||
|
||||
# Fetch the architecture config from HuggingFace (small, ~5KB).
|
||||
# Offline fallback: tries cache first, downloads only if missing.
|
||||
try:
|
||||
qwen_config = AutoConfig.from_pretrained(self.DEFAULT_HF_REPO, local_files_only=True)
|
||||
except OSError:
|
||||
logger.info(
|
||||
f"Architecture config for single-file Qwen VL encoder not found in HuggingFace cache; "
|
||||
f"downloading from {self.DEFAULT_HF_REPO} (one-time, ~5KB, requires network access)."
|
||||
)
|
||||
try:
|
||||
qwen_config = AutoConfig.from_pretrained(self.DEFAULT_HF_REPO)
|
||||
except OSError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to load Qwen VL architecture config. Single-file Qwen VL encoder checkpoints "
|
||||
f"do not include the model config; it must be downloaded from HuggingFace "
|
||||
f"({self.DEFAULT_HF_REPO}) on first use. Either restore network access, or install the "
|
||||
f"encoder in the diffusers folder layout (text_encoder/config.json + tokenizer/) "
|
||||
f"instead. Original error: {e}"
|
||||
) from e
|
||||
qwen_config.torch_dtype = model_dtype
|
||||
|
||||
new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values())
|
||||
self._ram_cache.make_room(new_sd_size)
|
||||
|
||||
with accelerate.init_empty_weights():
|
||||
model = Qwen2_5_VLForConditionalGeneration(qwen_config)
|
||||
|
||||
# Load weights; allow missing keys for tied lm_head and re-initialised buffers.
|
||||
load_result = model.load_state_dict(sd, strict=False, assign=True)
|
||||
if load_result.unexpected_keys:
|
||||
logger.warning(
|
||||
f"{len(load_result.unexpected_keys)} unexpected keys in checkpoint, "
|
||||
f"first 5: {load_result.unexpected_keys[:5]}"
|
||||
)
|
||||
|
||||
# Tie lm_head ↔ embed_tokens if config requires it and lm_head wasn't loaded
|
||||
if getattr(qwen_config, "tie_word_embeddings", False):
|
||||
try:
|
||||
if hasattr(model, "lm_head") and model.lm_head.weight.is_meta:
|
||||
model.lm_head.weight = model.model.embed_tokens.weight
|
||||
else:
|
||||
model.tie_weights()
|
||||
except AttributeError:
|
||||
model.tie_weights()
|
||||
|
||||
# Re-initialise any leftover meta buffers (RoPE inv_freq etc.)
|
||||
for name, buffer in list(model.named_buffers()):
|
||||
if not buffer.is_meta:
|
||||
continue
|
||||
parts = name.rsplit(".", 1)
|
||||
if len(parts) == 2:
|
||||
parent = model.get_submodule(parts[0])
|
||||
buffer_name = parts[1]
|
||||
else:
|
||||
parent = model
|
||||
buffer_name = name
|
||||
# Replace meta buffer with a real (zero) tensor of the same shape; the model
|
||||
# will recompute or refill these as needed at first forward pass.
|
||||
try:
|
||||
shape = buffer.shape
|
||||
parent.register_buffer(buffer_name, torch.zeros(shape, dtype=model_dtype), persistent=False)
|
||||
except Exception:
|
||||
logger.warning(f"Could not re-initialise meta buffer {name}")
|
||||
|
||||
meta_params = [name for name, p in model.named_parameters() if p.is_meta]
|
||||
if meta_params:
|
||||
raise RuntimeError(f"Failed to load all parameters from checkpoint. Meta tensors remain: {meta_params[:5]}")
|
||||
|
||||
model.eval()
|
||||
return model
|
||||
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from transformers import SiglipVisionModel
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.SigLIP, format=ModelFormat.Diffusers)
|
||||
class SigLIPModelLoader(ModelLoader):
|
||||
"""Class for loading SigLIP models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("Unexpected submodel requested for LLaVA OneVision model.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
model = SiglipVisionModel.from_pretrained(model_path, local_files_only=True, torch_dtype=self._torch_dtype)
|
||||
return model
|
||||
@@ -0,0 +1,39 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.Any, type=ModelType.SpandrelImageToImage, format=ModelFormat.Checkpoint
|
||||
)
|
||||
class SpandrelImageToImageModelLoader(ModelLoader):
|
||||
"""Class for loading Spandrel Image-to-Image models (i.e. models wrapped by spandrel.ImageModelDescriptor)."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("Unexpected submodel requested for Spandrel model.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
model = SpandrelImageToImageModel.load_from_file(model_path)
|
||||
|
||||
torch_dtype = self._torch_dtype
|
||||
if not model.supports_dtype(torch_dtype):
|
||||
self._logger.warning(
|
||||
f"The configured dtype ('{self._torch_dtype}') is not supported by the {model.get_model_type_name()} "
|
||||
"model. Falling back to 'float32'."
|
||||
)
|
||||
torch_dtype = torch.float32
|
||||
model.to(dtype=torch_dtype)
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,160 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for StableDiffusion model loading in InvokeAI."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipeline
|
||||
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_inpaint import StableDiffusionInpaintPipeline
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl import StableDiffusionXLPipeline
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl_inpaint import (
|
||||
StableDiffusionXLInpaintPipeline,
|
||||
)
|
||||
|
||||
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.configs.main import (
|
||||
Main_Checkpoint_SD1_Config,
|
||||
Main_Checkpoint_SD2_Config,
|
||||
Main_Checkpoint_SDXL_Config,
|
||||
Main_Checkpoint_SDXLRefiner_Config,
|
||||
Main_Diffusers_SD1_Config,
|
||||
Main_Diffusers_SD2_Config,
|
||||
Main_Diffusers_SDXL_Config,
|
||||
Main_Diffusers_SDXLRefiner_Config,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_cache.model_cache import get_model_cache_key
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
ModelVariantType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
VARIANT_TO_IN_CHANNEL_MAP = {
|
||||
ModelVariantType.Normal: 4,
|
||||
ModelVariantType.Depth: 5,
|
||||
ModelVariantType.Inpaint: 9,
|
||||
}
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion1, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion2, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusionXLRefiner, type=ModelType.Main, format=ModelFormat.Diffusers
|
||||
)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion3, type=ModelType.Main, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion1, type=ModelType.Main, format=ModelFormat.Checkpoint)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion2, type=ModelType.Main, format=ModelFormat.Checkpoint)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.Main, format=ModelFormat.Checkpoint)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.StableDiffusionXLRefiner, type=ModelType.Main, format=ModelFormat.Checkpoint
|
||||
)
|
||||
class StableDiffusionDiffusersModel(GenericDiffusersLoader):
|
||||
"""Class to load main models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if isinstance(config, Checkpoint_Config_Base):
|
||||
return self._load_from_singlefile(config, submodel_type)
|
||||
|
||||
if submodel_type is None:
|
||||
raise Exception("A submodel type must be provided when loading main pipelines.")
|
||||
|
||||
model_path = Path(config.path)
|
||||
load_class = self.get_hf_load_class(model_path, submodel_type)
|
||||
repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None
|
||||
variant = repo_variant.value if repo_variant else None
|
||||
model_path = model_path / submodel_type.value
|
||||
try:
|
||||
result: AnyModel = load_class.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
variant=variant,
|
||||
local_files_only=True,
|
||||
)
|
||||
except OSError as e:
|
||||
if variant and "no file named" in str(
|
||||
e
|
||||
): # try without the variant, just in case user's preferences changed
|
||||
result = load_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, local_files_only=True)
|
||||
else:
|
||||
raise e
|
||||
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
|
||||
def _load_from_singlefile(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
load_classes = {
|
||||
BaseModelType.StableDiffusion1: {
|
||||
ModelVariantType.Normal: StableDiffusionPipeline,
|
||||
ModelVariantType.Inpaint: StableDiffusionInpaintPipeline,
|
||||
},
|
||||
BaseModelType.StableDiffusion2: {
|
||||
ModelVariantType.Normal: StableDiffusionPipeline,
|
||||
ModelVariantType.Inpaint: StableDiffusionInpaintPipeline,
|
||||
},
|
||||
BaseModelType.StableDiffusionXL: {
|
||||
ModelVariantType.Normal: StableDiffusionXLPipeline,
|
||||
ModelVariantType.Inpaint: StableDiffusionXLInpaintPipeline,
|
||||
},
|
||||
BaseModelType.StableDiffusionXLRefiner: {
|
||||
ModelVariantType.Normal: StableDiffusionXLPipeline,
|
||||
},
|
||||
}
|
||||
assert isinstance(
|
||||
config,
|
||||
(
|
||||
Main_Diffusers_SD1_Config,
|
||||
Main_Diffusers_SD2_Config,
|
||||
Main_Diffusers_SDXL_Config,
|
||||
Main_Diffusers_SDXLRefiner_Config,
|
||||
Main_Checkpoint_SD1_Config,
|
||||
Main_Checkpoint_SD2_Config,
|
||||
Main_Checkpoint_SDXL_Config,
|
||||
Main_Checkpoint_SDXLRefiner_Config,
|
||||
),
|
||||
)
|
||||
try:
|
||||
load_class = load_classes[config.base][config.variant]
|
||||
except KeyError as e:
|
||||
raise Exception(f"No diffusers pipeline known for base={config.base}, variant={config.variant}") from e
|
||||
|
||||
# Without SilenceWarnings we get log messages like this:
|
||||
# site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
|
||||
# warnings.warn(
|
||||
# Some weights of the model checkpoint were not used when initializing CLIPTextModel:
|
||||
# ['text_model.embeddings.position_ids']
|
||||
# Some weights of the model checkpoint were not used when initializing CLIPTextModelWithProjection:
|
||||
# ['text_model.embeddings.position_ids']
|
||||
|
||||
with SilenceWarnings():
|
||||
pipeline = load_class.from_single_file(config.path, torch_dtype=self._torch_dtype)
|
||||
|
||||
if not submodel_type:
|
||||
return pipeline
|
||||
|
||||
# Proactively load the various submodels into the RAM cache so that we don't have to re-load
|
||||
# the entire pipeline every time a new submodel is needed.
|
||||
for subtype in SubModelType:
|
||||
if subtype == submodel_type:
|
||||
continue
|
||||
if submodel := getattr(pipeline, subtype.value, None):
|
||||
self._apply_fp8_layerwise_casting(submodel, config, subtype)
|
||||
self._ram_cache.put(get_model_cache_key(config.key, subtype), model=submodel)
|
||||
result = getattr(pipeline, submodel_type.value)
|
||||
result = self._apply_fp8_layerwise_casting(result, config, submodel_type)
|
||||
return result
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextLLM, format=ModelFormat.Diffusers)
|
||||
class TextLLMModelLoader(ModelLoader):
|
||||
"""Class for loading text causal language models (Llama, Phi, Qwen, Mistral, etc.)."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("Unexpected submodel requested for TextLLM model.")
|
||||
|
||||
# Use float32 for CPU-only models since CPU fp16 is emulated and slow.
|
||||
dtype = self._torch_dtype
|
||||
if getattr(config, "cpu_only", False) is True:
|
||||
dtype = torch.float32
|
||||
|
||||
model_path = Path(config.path)
|
||||
model = AutoModelForCausalLM.from_pretrained(model_path, local_files_only=True, torch_dtype=dtype)
|
||||
return model
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for TI model loading in InvokeAI."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
from invokeai.backend.textual_inversion import TextualInversionModelRaw
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile)
|
||||
@ModelLoaderRegistry.register(
|
||||
base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFolder
|
||||
)
|
||||
class TextualInversionLoader(ModelLoader):
|
||||
"""Class to load TI models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if submodel_type is not None:
|
||||
raise ValueError("There are no submodels in a TI model.")
|
||||
model = TextualInversionModelRaw.from_checkpoint(
|
||||
file_path=config.path,
|
||||
dtype=self._torch_dtype,
|
||||
)
|
||||
return model
|
||||
|
||||
# override
|
||||
def _get_model_path(self, config: AnyModelConfig) -> Path:
|
||||
model_path = self._app_config.models_path / config.path
|
||||
|
||||
if config.format == ModelFormat.EmbeddingFolder:
|
||||
path = model_path / "learned_embeds.bin"
|
||||
else:
|
||||
path = model_path
|
||||
|
||||
if not path.exists():
|
||||
raise OSError(f"The embedding file at {path} was not found")
|
||||
|
||||
return path
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
|
||||
"""Class for VAE model loading in InvokeAI."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
|
||||
|
||||
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
|
||||
from invokeai.backend.model_manager.configs.vae import (
|
||||
VAE_Checkpoint_Anima_Config,
|
||||
VAE_Checkpoint_Config_Base,
|
||||
VAE_Checkpoint_QwenImage_Config,
|
||||
)
|
||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
|
||||
from invokeai.backend.model_manager.taxonomy import (
|
||||
AnyModel,
|
||||
BaseModelType,
|
||||
ModelFormat,
|
||||
ModelType,
|
||||
SubModelType,
|
||||
)
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Diffusers)
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Checkpoint)
|
||||
class VAELoader(GenericDiffusersLoader):
|
||||
"""Class to load VAE models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if isinstance(config, VAE_Checkpoint_Anima_Config):
|
||||
from diffusers.models.autoencoders import AutoencoderKLWan
|
||||
|
||||
return AutoencoderKLWan.from_single_file(
|
||||
config.path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
)
|
||||
elif isinstance(config, VAE_Checkpoint_QwenImage_Config):
|
||||
return self._load_qwen_image_vae(config)
|
||||
elif isinstance(config, VAE_Checkpoint_Config_Base):
|
||||
return AutoencoderKL.from_single_file(
|
||||
config.path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
)
|
||||
else:
|
||||
return super()._load_model(config, submodel_type)
|
||||
|
||||
def _load_qwen_image_vae(self, config: VAE_Checkpoint_QwenImage_Config) -> AnyModel:
|
||||
"""Load a Qwen Image VAE from a single safetensors file.
|
||||
|
||||
The Qwen Image VAE checkpoint is expected to be in the diffusers state-dict
|
||||
layout (i.e. the same keys as `vae/diffusion_pytorch_model.safetensors` from
|
||||
the Qwen-Image repo). `AutoencoderKLQwenImage` does not register a single-file
|
||||
conversion in diffusers, so we instantiate the model with default config and
|
||||
load the state dict directly.
|
||||
"""
|
||||
import accelerate
|
||||
from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage
|
||||
from safetensors.torch import load_file
|
||||
|
||||
sd = load_file(config.path)
|
||||
|
||||
if self._torch_dtype is not None:
|
||||
for k in list(sd.keys()):
|
||||
if sd[k].is_floating_point():
|
||||
sd[k] = sd[k].to(self._torch_dtype)
|
||||
|
||||
new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values())
|
||||
self._ram_cache.make_room(new_sd_size)
|
||||
|
||||
with accelerate.init_empty_weights():
|
||||
model = AutoencoderKLQwenImage()
|
||||
|
||||
model.load_state_dict(sd, strict=True, assign=True)
|
||||
model.eval()
|
||||
return model
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
# Copyright (c) 2024 The InvokeAI Development Team
|
||||
"""Various utility functions needed by the loader and caching system."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import onnxruntime as ort
|
||||
import torch
|
||||
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
||||
from diffusers.schedulers.scheduling_utils import SchedulerMixin
|
||||
from transformers import CLIPTokenizer, PreTrainedTokenizerBase, T5Tokenizer
|
||||
|
||||
from invokeai.backend.image_util.depth_anything.depth_anything_pipeline import DepthAnythingPipeline
|
||||
from invokeai.backend.image_util.grounding_dino.grounding_dino_pipeline import GroundingDinoPipeline
|
||||
from invokeai.backend.image_util.segment_anything.segment_anything_pipeline import SegmentAnythingPipeline
|
||||
from invokeai.backend.ip_adapter.ip_adapter import IPAdapter
|
||||
from invokeai.backend.model_manager.taxonomy import AnyModel
|
||||
from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel
|
||||
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
|
||||
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
|
||||
from invokeai.backend.textual_inversion import TextualInversionModelRaw
|
||||
from invokeai.backend.util.calc_tensor_size import calc_tensor_size
|
||||
|
||||
|
||||
def calc_model_size_by_data(logger: logging.Logger, model: AnyModel) -> int:
|
||||
"""Get size of a model in memory in bytes."""
|
||||
# TODO(ryand): We should create a CacheableModel interface for all models, and move the size calculations down to
|
||||
# the models themselves.
|
||||
if isinstance(model, DiffusionPipeline):
|
||||
return _calc_pipeline_by_data(model)
|
||||
elif isinstance(model, torch.nn.Module):
|
||||
return calc_module_size(model)
|
||||
elif isinstance(model, IAIOnnxRuntimeModel):
|
||||
return _calc_onnx_model_by_data(model)
|
||||
elif isinstance(model, SchedulerMixin):
|
||||
return 0
|
||||
elif isinstance(model, CLIPTokenizer):
|
||||
# TODO(ryand): Accurately calculate the tokenizer's size. It's small enough that it shouldn't matter for now.
|
||||
return 0
|
||||
elif isinstance(
|
||||
model,
|
||||
(
|
||||
TextualInversionModelRaw,
|
||||
IPAdapter,
|
||||
ModelPatchRaw,
|
||||
SpandrelImageToImageModel,
|
||||
GroundingDinoPipeline,
|
||||
SegmentAnythingPipeline,
|
||||
DepthAnythingPipeline,
|
||||
),
|
||||
):
|
||||
return model.calc_size()
|
||||
elif isinstance(model, ort.InferenceSession):
|
||||
if model._model_bytes is not None:
|
||||
# If the model is already loaded, return the size of the model bytes
|
||||
return len(model._model_bytes)
|
||||
elif model._model_path is not None:
|
||||
# If the model is not loaded, return the size of the model path
|
||||
return calc_model_size_by_fs(Path(model._model_path))
|
||||
else:
|
||||
# If neither is available, return 0
|
||||
return 0
|
||||
elif isinstance(
|
||||
model,
|
||||
T5Tokenizer,
|
||||
):
|
||||
# HACK(ryand): len(model) just returns the vocabulary size, so this is blatantly wrong. It should be small
|
||||
# relative to the text encoder that it's used with, so shouldn't matter too much, but we should fix this at some
|
||||
# point.
|
||||
return len(model)
|
||||
elif isinstance(model, PreTrainedTokenizerBase):
|
||||
# Catch-all for other tokenizer types (e.g., Qwen2Tokenizer, Qwen3Tokenizer).
|
||||
# Tokenizers are small relative to models, so returning 0 is acceptable.
|
||||
return 0
|
||||
else:
|
||||
# TODO(ryand): Promote this from a log to an exception once we are confident that we are handling all of the
|
||||
# supported model types.
|
||||
logger.warning(
|
||||
f"Failed to calculate model size for unexpected model type: {type(model)}. The model will be treated as "
|
||||
"having size 0."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _calc_pipeline_by_data(pipeline: DiffusionPipeline) -> int:
|
||||
res = 0
|
||||
assert hasattr(pipeline, "components")
|
||||
for submodel_key in pipeline.components.keys():
|
||||
submodel = getattr(pipeline, submodel_key)
|
||||
if submodel is not None and isinstance(submodel, torch.nn.Module):
|
||||
res += calc_module_size(submodel)
|
||||
return res
|
||||
|
||||
|
||||
def calc_module_size(model: torch.nn.Module) -> int:
|
||||
"""Calculate the size (in bytes) of a torch.nn.Module."""
|
||||
mem_params = sum([calc_tensor_size(param) for param in model.parameters()])
|
||||
mem_bufs = sum([calc_tensor_size(buf) for buf in model.buffers()])
|
||||
return mem_params + mem_bufs
|
||||
|
||||
|
||||
def _calc_onnx_model_by_data(model: IAIOnnxRuntimeModel) -> int:
|
||||
tensor_size = model.tensors.size() * 2 # The session doubles this
|
||||
mem = tensor_size # in bytes
|
||||
return mem
|
||||
|
||||
|
||||
def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, variant: Optional[str] = None) -> int:
|
||||
"""Estimate the size of a model on disk in bytes."""
|
||||
if model_path.is_file():
|
||||
return model_path.stat().st_size
|
||||
|
||||
if subfolder is not None:
|
||||
model_path = model_path / subfolder
|
||||
|
||||
# this can happen when, for example, the safety checker is not downloaded.
|
||||
if not model_path.exists():
|
||||
return 0
|
||||
|
||||
all_files = [f for f in model_path.iterdir() if (model_path / f).is_file()]
|
||||
|
||||
fp16_files = {f for f in all_files if ".fp16." in f.name or ".fp16-" in f.name}
|
||||
bit8_files = {f for f in all_files if ".8bit." in f.name or ".8bit-" in f.name}
|
||||
other_files = set(all_files) - fp16_files - bit8_files
|
||||
|
||||
if not variant: # ModelRepoVariant.DEFAULT evaluates to empty string for compatability with HF
|
||||
files = other_files
|
||||
elif variant == "fp16":
|
||||
files = fp16_files
|
||||
elif variant == "8bit":
|
||||
files = bit8_files
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown variant: {variant}")
|
||||
|
||||
# try read from index if exists
|
||||
index_postfix = ".index.json"
|
||||
if variant is not None:
|
||||
index_postfix = f".index.{variant}.json"
|
||||
|
||||
for file in files:
|
||||
if not file.name.endswith(index_postfix):
|
||||
continue
|
||||
try:
|
||||
with open(model_path / file, "r") as f:
|
||||
index_data = json.loads(f.read())
|
||||
return int(index_data["metadata"]["total_size"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# calculate files size if there is no index file
|
||||
formats = [
|
||||
(".safetensors",), # safetensors
|
||||
(".bin",), # torch
|
||||
(".onnx", ".pb"), # onnx
|
||||
(".msgpack",), # flax
|
||||
(".ckpt",), # tf
|
||||
(".h5",), # tf2
|
||||
(".gguf",), # gguf quantized
|
||||
]
|
||||
|
||||
for file_format in formats:
|
||||
model_files = [f for f in files if f.suffix in file_format]
|
||||
if len(model_files) == 0:
|
||||
continue
|
||||
|
||||
model_size = 0
|
||||
for model_file in model_files:
|
||||
file_stats = (model_path / model_file).stat()
|
||||
model_size += file_stats.st_size
|
||||
return model_size
|
||||
|
||||
return 0 # scheduler/feature_extractor/tokenizer - models without loading to gpu
|
||||
@@ -0,0 +1,31 @@
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Generator
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def _no_op(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def skip_torch_weight_init() -> Generator[None, None, None]:
|
||||
"""Monkey patch several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) to skip weight initialization.
|
||||
|
||||
By default, `torch.nn.Linear` and `torch.nn.ConvNd` layers initialize their weights (according to a particular
|
||||
distribution) when __init__ is called. This weight initialization step can take a significant amount of time, and is
|
||||
completely unnecessary if the intent is to load checkpoint weights from disk for the layer. This context manager
|
||||
monkey-patches common torch layers to skip the weight initialization step.
|
||||
"""
|
||||
torch_modules = [torch.nn.Linear, torch.nn.modules.conv._ConvNd, torch.nn.Embedding]
|
||||
saved_functions = [hasattr(m, "reset_parameters") and m.reset_parameters for m in torch_modules]
|
||||
|
||||
try:
|
||||
for torch_module in torch_modules:
|
||||
assert hasattr(torch_module, "reset_parameters")
|
||||
torch_module.reset_parameters = _no_op
|
||||
yield None
|
||||
finally:
|
||||
for torch_module, saved_function in zip(torch_modules, saved_functions, strict=True):
|
||||
assert hasattr(torch_module, "reset_parameters")
|
||||
torch_module.reset_parameters = saved_function
|
||||
Reference in New Issue
Block a user