feat(mm): use relative paths for invoke-managed models

We switched all model paths to be absolute in #5900. In hindsight, this is a mistake, because it makes the `models_dir` non-portable.

This change reverts to the previous model pathing:
- Invoke-managed models (in the `models_dir`) are stored with relative paths
- Non-invoke-managed models (outside the `models_dir`, i.e. in-place installed models) still have absolute paths.

## Why absolute paths make things non-portable

Let's say my `models_dir` is `/media/rhino/invokeai/models/`. In the DB, all model paths will be absolute children of this path, like this:

- `/media/rhino/invokeai/models/sd-1/main/model1.ckpt`

I want to change my `models_dir` to `/home/bat/invokeai/models/`. I update my `invokeai.yaml` file and physically move the files to that directory.

On startup, the app checks for missing models. Because all of my model paths were absolute, they now point to a nonexistent path. All models are broken.

There are a couple options to recover from this situation, neither of which are reasonable:

1. The user must manually update every model's path. Unacceptable UX.
2. On startup, we check for missing models. For each missing model, we compare its path with the last-known models dir. If there is a match, we replace that portion of the path with the new models dir. Then we re-check to see if the path exists. If it does, we update the models DB entry. Brittle and requires a new DB entry for last-known models dir.

It's better to use relative paths for Invoke-managed models.
This commit is contained in:
psychedelicious 2024-03-29 22:09:42 +11:00 committed by Kent Keirsey
parent 3409711ed3
commit c5d1bd1360

View File

@ -368,11 +368,13 @@ class ModelInstallService(ModelInstallServiceBase):
def delete(self, key: str) -> None: # noqa D102 def delete(self, key: str) -> None: # noqa D102
"""Unregister the model. Delete its files only if they are within our models directory.""" """Unregister the model. Delete its files only if they are within our models directory."""
model = self.record_store.get_model(key) model = self.record_store.get_model(key)
models_dir = self.app_config.models_path model_path = self.app_config.models_path / model.path
model_path = models_dir / Path(model.path) # handle legacy relative model paths
if model_path.is_relative_to(models_dir): if model_path.is_relative_to(self.app_config.models_path):
# If the models is in the Invoke-managed models dir, we delete it
self.unconditionally_delete(key) self.unconditionally_delete(key)
else: else:
# Else we only unregister it, leaving the file in place
self.unregister(key) self.unregister(key)
def unconditionally_delete(self, key: str) -> None: # noqa D102 def unconditionally_delete(self, key: str) -> None: # noqa D102
@ -500,9 +502,9 @@ class ModelInstallService(ModelInstallServiceBase):
def _scan_for_missing_models(self) -> list[AnyModelConfig]: def _scan_for_missing_models(self) -> list[AnyModelConfig]:
"""Scan the models directory for missing models and return a list of them.""" """Scan the models directory for missing models and return a list of them."""
missing_models: list[AnyModelConfig] = [] missing_models: list[AnyModelConfig] = []
for x in self.record_store.all_models(): for model_config in self.record_store.all_models():
if not Path(x.path).resolve().exists(): if not (self.app_config.models_path / model_config.path).resolve().exists():
missing_models.append(x) missing_models.append(model_config)
return missing_models return missing_models
def _register_orphaned_models(self) -> None: def _register_orphaned_models(self) -> None:
@ -548,10 +550,11 @@ class ModelInstallService(ModelInstallServiceBase):
May raise an UnknownModelException. May raise an UnknownModelException.
""" """
model = self.record_store.get_model(key) model = self.record_store.get_model(key)
old_path = Path(model.path).resolve() models_dir = self.app_config.models_path
models_dir = self.app_config.models_path.resolve() old_path = self.app_config.models_path / model.path
if not old_path.is_relative_to(models_dir): if not old_path.is_relative_to(models_dir):
# The model is not in the models directory - we don't need to move it.
return model return model
new_path = (models_dir / model.base.value / model.type.value / model.name).with_suffix(old_path.suffix) new_path = (models_dir / model.base.value / model.type.value / model.name).with_suffix(old_path.suffix)
@ -561,7 +564,7 @@ class ModelInstallService(ModelInstallServiceBase):
self._logger.info(f"Moving {model.name} to {new_path}.") self._logger.info(f"Moving {model.name} to {new_path}.")
new_path = self._move_model(old_path, new_path) new_path = self._move_model(old_path, new_path)
model.path = new_path.as_posix() model.path = new_path.relative_to(models_dir).as_posix()
self.record_store.update_model(key, ModelRecordChanges(path=model.path)) self.record_store.update_model(key, ModelRecordChanges(path=model.path))
return model return model
@ -600,12 +603,19 @@ class ModelInstallService(ModelInstallServiceBase):
model_path = model_path.resolve() model_path = model_path.resolve()
# Models in the Invoke-managed models dir should use relative paths.
if model_path.is_relative_to(self.app_config.models_path):
model_path = model_path.relative_to(self.app_config.models_path)
info.path = model_path.as_posix() info.path = model_path.as_posix()
# Checkpoints have a config file needed for conversion - resolve this to an absolute path
if isinstance(info, CheckpointConfigBase): if isinstance(info, CheckpointConfigBase):
legacy_conf = (self.app_config.legacy_conf_path / info.config_path).resolve() # Checkpoints have a config file needed for conversion. Same handling as the model weights - if it's in the
info.config_path = legacy_conf.as_posix() # invoke-managed legacy config dir, we use a relative path.
legacy_config_path = Path(info.config_path).resolve()
if legacy_config_path.is_relative_to(self.app_config.legacy_conf_path):
legacy_config_path = legacy_config_path.relative_to(self.app_config.legacy_conf_path)
info.config_path = legacy_config_path.as_posix()
self.record_store.add_model(info) self.record_store.add_model(info)
return info.key return info.key