Skip to content

Commit 3e0fb45

Browse files
lsteinLincoln SteinRyanJDick
authored
Load single-file checkpoints directly without conversion (#6510)
* use model_class.load_singlefile() instead of converting; works, but performance is poor * adjust the convert api - not right just yet * working, needs sql migrator update * rename migration_11 before conflict merge with main * Update invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py Co-authored-by: Ryan Dick <[email protected]> * Update invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py Co-authored-by: Ryan Dick <[email protected]> * implement lightweight version-by-version config migration * simplified config schema migration code * associate sdxl config with sdxl VAEs * remove use of original_config_file in load_single_file() --------- Co-authored-by: Lincoln Stein <[email protected]> Co-authored-by: Ryan Dick <[email protected]>
1 parent aba1608 commit 3e0fb45

File tree

21 files changed

+227
-488
lines changed

21 files changed

+227
-488
lines changed

invokeai/app/api/routers/model_manager.py

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import io
55
import pathlib
6-
import shutil
76
import traceback
87
from copy import deepcopy
8+
from tempfile import TemporaryDirectory
99
from typing import Any, Dict, List, Optional, Type
1010

1111
from fastapi import Body, Path, Query, Response, UploadFile
@@ -19,7 +19,6 @@
1919
from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
2020
from invokeai.app.services.model_install.model_install_common import ModelInstallJob
2121
from invokeai.app.services.model_records import (
22-
DuplicateModelException,
2322
InvalidModelException,
2423
ModelRecordChanges,
2524
UnknownModelException,
@@ -30,7 +29,6 @@
3029
MainCheckpointConfig,
3130
ModelFormat,
3231
ModelType,
33-
SubModelType,
3432
)
3533
from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
3634
from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
@@ -174,18 +172,6 @@ async def get_model_record(
174172
raise HTTPException(status_code=404, detail=str(e))
175173

176174

177-
# @model_manager_router.get("/summary", operation_id="list_model_summary")
178-
# async def list_model_summary(
179-
# page: int = Query(default=0, description="The page to get"),
180-
# per_page: int = Query(default=10, description="The number of models per page"),
181-
# order_by: ModelRecordOrderBy = Query(default=ModelRecordOrderBy.Default, description="The attribute to order by"),
182-
# ) -> PaginatedResults[ModelSummary]:
183-
# """Gets a page of model summary data."""
184-
# record_store = ApiDependencies.invoker.services.model_manager.store
185-
# results: PaginatedResults[ModelSummary] = record_store.list_models(page=page, per_page=per_page, order_by=order_by)
186-
# return results
187-
188-
189175
class FoundModel(BaseModel):
190176
path: str = Field(description="Path to the model")
191177
is_installed: bool = Field(description="Whether or not the model is already installed")
@@ -746,39 +732,36 @@ async def convert_model(
746732
logger.error(f"The model with key {key} is not a main checkpoint model.")
747733
raise HTTPException(400, f"The model with key {key} is not a main checkpoint model.")
748734

749-
# loading the model will convert it into a cached diffusers file
750-
try:
751-
cc_size = loader.convert_cache.max_size
752-
if cc_size == 0: # temporary set the convert cache to a positive number so that cached model is written
753-
loader._convert_cache.max_size = 1.0
754-
loader.load_model(model_config, submodel_type=SubModelType.Scheduler)
755-
finally:
756-
loader._convert_cache.max_size = cc_size
757-
758-
# Get the path of the converted model from the loader
759-
cache_path = loader.convert_cache.cache_path(key)
760-
assert cache_path.exists()
761-
762-
# temporarily rename the original safetensors file so that there is no naming conflict
763-
original_name = model_config.name
764-
model_config.name = f"{original_name}.DELETE"
765-
changes = ModelRecordChanges(name=model_config.name)
766-
store.update_model(key, changes=changes)
767-
768-
# install the diffusers
769-
try:
770-
new_key = installer.install_path(
771-
cache_path,
772-
config={
773-
"name": original_name,
774-
"description": model_config.description,
775-
"hash": model_config.hash,
776-
"source": model_config.source,
777-
},
778-
)
779-
except DuplicateModelException as e:
780-
logger.error(str(e))
781-
raise HTTPException(status_code=409, detail=str(e))
735+
with TemporaryDirectory(dir=ApiDependencies.invoker.services.configuration.models_path) as tmpdir:
736+
convert_path = pathlib.Path(tmpdir) / pathlib.Path(model_config.path).stem
737+
converted_model = loader.load_model(model_config)
738+
# write the converted file to the convert path
739+
raw_model = converted_model.model
740+
assert hasattr(raw_model, "save_pretrained")
741+
raw_model.save_pretrained(convert_path)
742+
assert convert_path.exists()
743+
744+
# temporarily rename the original safetensors file so that there is no naming conflict
745+
original_name = model_config.name
746+
model_config.name = f"{original_name}.DELETE"
747+
changes = ModelRecordChanges(name=model_config.name)
748+
store.update_model(key, changes=changes)
749+
750+
# install the diffusers
751+
try:
752+
new_key = installer.install_path(
753+
convert_path,
754+
config={
755+
"name": original_name,
756+
"description": model_config.description,
757+
"hash": model_config.hash,
758+
"source": model_config.source,
759+
},
760+
)
761+
except Exception as e:
762+
logger.error(str(e))
763+
store.update_model(key, changes=ModelRecordChanges(name=original_name))
764+
raise HTTPException(status_code=409, detail=str(e))
782765

783766
# Update the model image if the model had one
784767
try:
@@ -791,8 +774,8 @@ async def convert_model(
791774
# delete the original safetensors file
792775
installer.delete(key)
793776

794-
# delete the cached version
795-
shutil.rmtree(cache_path)
777+
# delete the temporary directory
778+
# shutil.rmtree(cache_path)
796779

797780
# return the config record for the new diffusers directory
798781
new_config = store.get_model(new_key)

invokeai/app/services/config/config_default.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import annotations
55

6+
import copy
67
import locale
78
import os
89
import re
@@ -25,14 +26,13 @@
2526
LEGACY_INIT_FILE = Path("invokeai.init")
2627
DEFAULT_RAM_CACHE = 10.0
2728
DEFAULT_VRAM_CACHE = 0.25
28-
DEFAULT_CONVERT_CACHE = 20.0
2929
DEVICE = Literal["auto", "cpu", "cuda", "cuda:1", "mps"]
3030
PRECISION = Literal["auto", "float16", "bfloat16", "float32"]
3131
ATTENTION_TYPE = Literal["auto", "normal", "xformers", "sliced", "torch-sdp"]
3232
ATTENTION_SLICE_SIZE = Literal["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8]
3333
LOG_FORMAT = Literal["plain", "color", "syslog", "legacy"]
3434
LOG_LEVEL = Literal["debug", "info", "warning", "error", "critical"]
35-
CONFIG_SCHEMA_VERSION = "4.0.1"
35+
CONFIG_SCHEMA_VERSION = "4.0.2"
3636

3737

3838
def get_default_ram_cache_size() -> float:
@@ -85,7 +85,7 @@ class InvokeAIAppConfig(BaseSettings):
8585
log_tokenization: Enable logging of parsed prompt tokens.
8686
patchmatch: Enable patchmatch inpaint code.
8787
models_dir: Path to the models directory.
88-
convert_cache_dir: Path to the converted models cache directory. When loading a non-diffusers model, it will be converted and store on disk at this location.
88+
convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).
8989
download_cache_dir: Path to the directory that contains dynamically downloaded models.
9090
legacy_conf_dir: Path to directory of legacy checkpoint config files.
9191
db_dir: Path to InvokeAI databases directory.
@@ -102,7 +102,6 @@ class InvokeAIAppConfig(BaseSettings):
102102
profiles_dir: Path to profiles output directory.
103103
ram: Maximum memory amount used by memory model cache for rapid switching (GB).
104104
vram: Amount of VRAM reserved for model storage (GB).
105-
convert_cache: Maximum size of on-disk converted models cache (GB).
106105
lazy_offload: Keep models in VRAM until their space is needed.
107106
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 only enable this feature if you are actively inspecting the model cache's behaviour.
108107
device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.<br>Valid values: `auto`, `cpu`, `cuda`, `cuda:1`, `mps`
@@ -148,7 +147,7 @@ class InvokeAIAppConfig(BaseSettings):
148147

149148
# PATHS
150149
models_dir: Path = Field(default=Path("models"), description="Path to the models directory.")
151-
convert_cache_dir: Path = Field(default=Path("models/.convert_cache"), description="Path to the converted models cache directory. When loading a non-diffusers model, it will be converted and store on disk at this location.")
150+
convert_cache_dir: Path = Field(default=Path("models/.convert_cache"), description="Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).")
152151
download_cache_dir: Path = Field(default=Path("models/.download_cache"), description="Path to the directory that contains dynamically downloaded models.")
153152
legacy_conf_dir: Path = Field(default=Path("configs"), description="Path to directory of legacy checkpoint config files.")
154153
db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.")
@@ -170,9 +169,8 @@ class InvokeAIAppConfig(BaseSettings):
170169
profiles_dir: Path = Field(default=Path("profiles"), description="Path to profiles output directory.")
171170

172171
# CACHE
173-
ram: float = Field(default_factory=get_default_ram_cache_size, gt=0, description="Maximum memory amount used by memory model cache for rapid switching (GB).")
174-
vram: float = Field(default=DEFAULT_VRAM_CACHE, ge=0, description="Amount of VRAM reserved for model storage (GB).")
175-
convert_cache: float = Field(default=DEFAULT_CONVERT_CACHE, ge=0, description="Maximum size of on-disk converted models cache (GB).")
172+
ram: float = Field(default_factory=get_default_ram_cache_size, gt=0, description="Maximum memory amount used by memory model cache for rapid switching (GB).")
173+
vram: float = Field(default=DEFAULT_VRAM_CACHE, ge=0, description="Amount of VRAM reserved for model storage (GB).")
176174
lazy_offload: bool = Field(default=True, description="Keep models in VRAM until their space is needed.")
177175
log_memory_usage: bool = Field(default=False, description="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 only enable this feature if you are actively inspecting the model cache's behaviour.")
178176

@@ -357,14 +355,14 @@ def settings_customise_sources(
357355
return (init_settings,)
358356

359357

360-
def migrate_v3_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig:
361-
"""Migrate a v3 config dictionary to a current config object.
358+
def migrate_v3_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
359+
"""Migrate a v3 config dictionary to a v4.0.0.
362360
363361
Args:
364362
config_dict: A dictionary of settings from a v3 config file.
365363
366364
Returns:
367-
An instance of `InvokeAIAppConfig` with the migrated settings.
365+
An `InvokeAIAppConfig` config dict.
368366
369367
"""
370368
parsed_config_dict: dict[str, Any] = {}
@@ -398,32 +396,41 @@ def migrate_v3_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig:
398396
elif k in InvokeAIAppConfig.model_fields:
399397
# skip unknown fields
400398
parsed_config_dict[k] = v
401-
# When migrating the config file, we should not include currently-set environment variables.
402-
config = DefaultInvokeAIAppConfig.model_validate(parsed_config_dict)
403-
404-
return config
399+
parsed_config_dict["schema_version"] = "4.0.0"
400+
return parsed_config_dict
405401

406402

407-
def migrate_v4_0_0_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig:
408-
"""Migrate v4.0.0 config dictionary to a current config object.
403+
def migrate_v4_0_0_to_4_0_1_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
404+
"""Migrate v4.0.0 config dictionary to a v4.0.1 config dictionary
409405
410406
Args:
411407
config_dict: A dictionary of settings from a v4.0.0 config file.
412408
413409
Returns:
414-
An instance of `InvokeAIAppConfig` with the migrated settings.
410+
A config dict with the settings migrated to v4.0.1.
415411
"""
416-
parsed_config_dict: dict[str, Any] = {}
417-
for k, v in config_dict.items():
418-
# autocast was removed from precision in v4.0.1
419-
if k == "precision" and v == "autocast":
420-
parsed_config_dict["precision"] = "auto"
421-
else:
422-
parsed_config_dict[k] = v
423-
if k == "schema_version":
424-
parsed_config_dict[k] = CONFIG_SCHEMA_VERSION
425-
config = DefaultInvokeAIAppConfig.model_validate(parsed_config_dict)
426-
return config
412+
parsed_config_dict: dict[str, Any] = copy.deepcopy(config_dict)
413+
# precision "autocast" was replaced by "auto" in v4.0.1
414+
if parsed_config_dict.get("precision") == "autocast":
415+
parsed_config_dict["precision"] = "auto"
416+
parsed_config_dict["schema_version"] = "4.0.1"
417+
return parsed_config_dict
418+
419+
420+
def migrate_v4_0_1_to_4_0_2_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
421+
"""Migrate v4.0.1 config dictionary to a v4.0.2 config dictionary.
422+
423+
Args:
424+
config_dict: A dictionary of settings from a v4.0.1 config file.
425+
426+
Returns:
427+
An config dict with the settings migrated to v4.0.2.
428+
"""
429+
parsed_config_dict: dict[str, Any] = copy.deepcopy(config_dict)
430+
# convert_cache was removed in 4.0.2
431+
parsed_config_dict.pop("convert_cache", None)
432+
parsed_config_dict["schema_version"] = "4.0.2"
433+
return parsed_config_dict
427434

428435

429436
def load_and_migrate_config(config_path: Path) -> InvokeAIAppConfig:
@@ -437,27 +444,31 @@ def load_and_migrate_config(config_path: Path) -> InvokeAIAppConfig:
437444
"""
438445
assert config_path.suffix == ".yaml"
439446
with open(config_path, "rt", encoding=locale.getpreferredencoding()) as file:
440-
loaded_config_dict = yaml.safe_load(file)
447+
loaded_config_dict: dict[str, Any] = yaml.safe_load(file)
441448

442449
assert isinstance(loaded_config_dict, dict)
443450

451+
migrated = False
444452
if "InvokeAI" in loaded_config_dict:
445-
# This is a v3 config file, attempt to migrate it
453+
migrated = True
454+
loaded_config_dict = migrate_v3_config_dict(loaded_config_dict) # pyright: ignore [reportUnknownArgumentType]
455+
if loaded_config_dict["schema_version"] == "4.0.0":
456+
migrated = True
457+
loaded_config_dict = migrate_v4_0_0_to_4_0_1_config_dict(loaded_config_dict)
458+
if loaded_config_dict["schema_version"] == "4.0.1":
459+
migrated = True
460+
loaded_config_dict = migrate_v4_0_1_to_4_0_2_config_dict(loaded_config_dict)
461+
462+
if migrated:
446463
shutil.copy(config_path, config_path.with_suffix(".yaml.bak"))
447464
try:
448-
# loaded_config_dict could be the wrong shape, but we will catch all exceptions below
449-
migrated_config = migrate_v3_config_dict(loaded_config_dict) # pyright: ignore [reportUnknownArgumentType]
465+
# load and write without environment variables
466+
migrated_config = DefaultInvokeAIAppConfig.model_validate(loaded_config_dict)
467+
migrated_config.write_file(config_path)
450468
except Exception as e:
451469
shutil.copy(config_path.with_suffix(".yaml.bak"), config_path)
452470
raise RuntimeError(f"Failed to load and migrate v3 config file {config_path}: {e}") from e
453-
migrated_config.write_file(config_path)
454-
return migrated_config
455-
456-
if loaded_config_dict["schema_version"] == "4.0.0":
457-
loaded_config_dict = migrate_v4_0_0_config_dict(loaded_config_dict)
458-
loaded_config_dict.write_file(config_path)
459471

460-
# Attempt to load as a v4 config file
461472
try:
462473
# Meta is not included in the model fields, so we need to validate it separately
463474
config = InvokeAIAppConfig.model_validate(loaded_config_dict)

invokeai/app/services/model_load/model_load_base.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType
99
from invokeai.backend.model_manager.load import LoadedModel, LoadedModelWithoutConfig
10-
from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase
1110
from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase
1211

1312

@@ -28,11 +27,6 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo
2827
def ram_cache(self) -> ModelCacheBase[AnyModel]:
2928
"""Return the RAM cache used by this loader."""
3029

31-
@property
32-
@abstractmethod
33-
def convert_cache(self) -> ModelConvertCacheBase:
34-
"""Return the checkpoint convert cache used by this loader."""
35-
3630
@abstractmethod
3731
def load_model_from_path(
3832
self, model_path: Path, loader: Optional[Callable[[Path], AnyModel]] = None

invokeai/app/services/model_load/model_load_default.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
ModelLoaderRegistry,
1818
ModelLoaderRegistryBase,
1919
)
20-
from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase
2120
from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase
2221
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
2322
from invokeai.backend.util.devices import TorchDevice
@@ -33,7 +32,6 @@ def __init__(
3332
self,
3433
app_config: InvokeAIAppConfig,
3534
ram_cache: ModelCacheBase[AnyModel],
36-
convert_cache: ModelConvertCacheBase,
3735
registry: Optional[Type[ModelLoaderRegistryBase]] = ModelLoaderRegistry,
3836
):
3937
"""Initialize the model load service."""
@@ -42,7 +40,6 @@ def __init__(
4240
self._logger = logger
4341
self._app_config = app_config
4442
self._ram_cache = ram_cache
45-
self._convert_cache = convert_cache
4643
self._registry = registry
4744

4845
def start(self, invoker: Invoker) -> None:
@@ -53,11 +50,6 @@ def ram_cache(self) -> ModelCacheBase[AnyModel]:
5350
"""Return the RAM cache used by this loader."""
5451
return self._ram_cache
5552

56-
@property
57-
def convert_cache(self) -> ModelConvertCacheBase:
58-
"""Return the checkpoint convert cache used by this loader."""
59-
return self._convert_cache
60-
6153
def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel:
6254
"""
6355
Given a model's configuration, load it and return the LoadedModel object.
@@ -76,7 +68,6 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo
7668
app_config=self._app_config,
7769
logger=self._logger,
7870
ram_cache=self._ram_cache,
79-
convert_cache=self._convert_cache,
8071
).load_model(model_config, submodel_type)
8172

8273
if hasattr(self, "_invoker"):

invokeai/app/services/model_manager/model_manager_default.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing_extensions import Self
88

99
from invokeai.app.services.invoker import Invoker
10-
from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache, ModelLoaderRegistry
10+
from invokeai.backend.model_manager.load import ModelCache, ModelLoaderRegistry
1111
from invokeai.backend.util.devices import TorchDevice
1212
from invokeai.backend.util.logging import InvokeAILogger
1313

@@ -86,11 +86,9 @@ def build_model_manager(
8686
logger=logger,
8787
execution_device=execution_device or TorchDevice.choose_torch_device(),
8888
)
89-
convert_cache = ModelConvertCache(cache_path=app_config.convert_cache_path, max_size=app_config.convert_cache)
9089
loader = ModelLoadService(
9190
app_config=app_config,
9291
ram_cache=ram_cache,
93-
convert_cache=convert_cache,
9492
registry=ModelLoaderRegistry,
9593
)
9694
installer = ModelInstallService(

0 commit comments

Comments
 (0)