Skip to content

Conversation

psychedelicious
Copy link
Collaborator

@psychedelicious psychedelicious commented Sep 18, 2025

Summary

Normalize model file storage to a flat directory structure.

Currently, models were stored in a nested hierarchy of folders like this:

  • <models_dir>/<base>/<type>/model_name.ext for single-file models
  • <models_dir>/<base>/<type>/model_name/**.* for folder models

This has approach has a couple associated unpleasantries:

  • Model names are easily clobbered. For example, maaaany models are called simply model.safetensors. How do we work around that? Well, right now, we throw when a candidate model install would clobber an existing file.
  • Model file storage is tightly coupled to its config attributes. When a model's base or type is changed, we need to move the model files, else the directory hierarchy no longer accurately indicates the model's base and type.
  • It encourages users to fiddle with their model files and implies that users can simply drop models into the appropriate folder and Invoke will be able to use the models - which is not the case.

This PR normalizes/flattens model storage paths, storing each model in its own dir which is named with the model's unique key:

  • <models_dir>/<key>/file_name.ext for single-file models
  • <models_dir>/<key>/**.* for folder models

This neatly addressing those unpleasantries:

  • No chance of clobbering, because keys are UUIDs.
  • No coupling of model base/type to file storage.
  • It's immediately clear that the models_dir is not something you should fiddle with.

This PR includes the simple code change for new model installs and a rather cautious migration to migrate to the new directory structure and update the model records in the DB. It uses SQLite save points and records all file operations, effectively making the DB and FS operations into a single transaction that can be rolled back.

It also removes some now-unused methods in the MM codebase.

Related Issues / Discussions

Numerous issues scattered throughout discord and GH

QA Instructions

First, test the migration with your models_dir at the default setting (e.g. no models_dir in invokeai.yaml):

  • Make a backup of your install dir (e.g. cp -r /path/to/invokeai/ /path/to/invokeai_backup/).
    • You could just back up the databases/ and models/ dirs I suppose.
  • Start up Invoke to run the migration. You'll see log messages as it does its thing.
  • Generate, have a play, nothing should break.
  • Install a model. Should work.
  • Review your install dir; it should look the same except models/ should have a UUID folder for each model in there.

Next, do the same tests, but move models_dir to some other location (e.g. mv /path/to/invokeai/models/ /home/username/models/ and update invokeai.yaml accordingly before starting up the app.

I've tested both scenarios and tested invalid model configs in the DB. Everything working fine for me. We don't expect any generation issues, because we aren't doing anything that Invoke didn't already do. It already can load models from any location; we are just shuffling files around.

Merge Plan

n/a

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

Store models in a flat directory structure. Each model is in a dir named
its unique key (a UUID). Inside that dir is either the model file or the
model dir.
@github-actions github-actions bot added api python PRs that change python files services PRs that change app services python-tests PRs that change python tests labels Sep 18, 2025
@psychedelicious psychedelicious force-pushed the psyche/feat/mm/normalized-model-storage branch from 05f9551 to 5e6b52d Compare September 19, 2025 02:55
@psychedelicious psychedelicious marked this pull request as draft September 19, 2025 11:42
- Add concept of match certainty to new probe
- Port CLIP Embed models to new API
- Fiddle with stuff
Previously, we had a multi-phase strategy to identify models from their
files on disk:
1. Run each model config classes' `matches()` method on the files. It
checks if the model could possibly be an identified as the candidate
model type. This was intended to be a quick check. Break on the first
match.
2. If we have a match, run the config class's `parse()` method. It
derive some additional model config attrs from the model files. This was
intended to encapsulate heavier operations that may require loading the
model into memory.
3. Derive the common model config attrs, like name, description,
calculate the hash, etc. Some of these are also heavier operations.

This strategy has some issues:
- It is not clear how the pieces fit together. There is some
back-and-forth between different methods and the config base class. It
is hard to trace the flow of logic until you fully wrap your head around
the system and therefore difficult to add a model architecture to the
probe.
- The assumption that we could do quick, lightweight checks before
heavier checks is incorrect. We often _must_ load the model state dict
in the `matches()` method. So there is no practical perf benefit to
splitting up the responsibility of `matches()` and `parse()`.
- Sometimes we need to do the same checks in `matches()` and `parse()`.
In these cases, splitting the logic is has a negative perf impact
because we are doing the same work twice.
- As we introduce the concept of an "unknown" model config (i.e. a model
that we cannot identify, but still record in the db; see #8582), we will
_always_ run _all_ the checks for every model. Therefore we need not try
to defer heavier checks or resource-intensive ops like hashing. We are
going to do them anyways.
- There are situations where a model may match multiple configs. One
known case are SD pipeline models with merged LoRAs. In the old probe
API, we relied on the implicit order of checks to know that if a model
matched for pipeline _and_ LoRA, we prefer the pipeline match. But, in
the new API, we do not have this implicit ordering of checks. To resolve
this in a resilient way, we need to get all matches up front, then use
tie-breaker logic to figure out which should win (or add "differential
diagnosis" logic to the matchers).
- Field overrides weren't handled well by this strategy. They were only
applied at the very end, if a model matched successfully. This means we
cannot tell the system "Hey, this model is type X with base Y. Trust me
bro.". We cannot override the match logic. As we move towards letting
users correct mis-identified models (see #8582), this is a requirement.

We can simplify the process significantly and better support "unknown"
models.

Firstly, model config classes now have a single `from_model_on_disk()`
method that attempts to construct an instance of the class from the
model files. This replaces the `matches()` and `parse()` methods.

If we fail to create the config instance, a special exception is raised
that indicates why we think the files cannot be identified as the given
model config class.

Next, the flow for model identification is a bit simpler:
- Derive all the common fields up-front (name, desc, hash, etc).
- Merge in overrides.
- Call `from_model_on_disk()` for every config class, passing in the
fields. Overrides are handled in this method.
- Record the results for each config class and choose the best one.

The identification logic is a bit more verbose, with the special
exceptions and handling of overrides, but it is very clear what is
happening.

The one downside I can think of for this strategy is we do need to check
every model type, instead of stopping at the first match. It's a bit
less efficient. In practice, however, this isn't a hot code path, and
the improved clarity is worth far more than perf optimizations that the
end user will likely never notice.
@psychedelicious psychedelicious marked this pull request as ready for review October 8, 2025 21:01
@psychedelicious psychedelicious merged commit 05cc3b1 into psyche/feat/mm/unknown-model Oct 8, 2025
9 of 19 checks passed
@psychedelicious psychedelicious deleted the psyche/feat/mm/normalized-model-storage branch October 8, 2025 21:01
@github-actions github-actions bot added invocations PRs that change invocations backend PRs that change backend files frontend PRs that change frontend files labels Oct 8, 2025
@psychedelicious psychedelicious mentioned this pull request Oct 9, 2025
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api backend PRs that change backend files frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants