Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/en/modular_diffusers/modular_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ pipe.save_pretrained("local/path", repo_id="my-username/flux2-custom-transformer

Pass `overwrite_modular_index=False` to keep the loading specs in `modular_model_index.json` as they are. A saved component whose loading spec is empty is still filled in with the destination, since there is nothing to preserve.

Note that moving the files any other way (uploading with `hf upload`, downloading a repository with `hf download --local-dir`) doesn't rewrite the index, so the copy still points to the old location; update the index manually in that case.
Moving the files any other way doesn't rewrite the index. A copy downloaded with `hf download --local-dir` still works: when a pipeline is loaded from a local directory, every component whose files are present in that directory is loaded from it instead of the recorded repository. A copy uploaded with `hf upload` keeps pointing at the old location, so update the index manually in that case.

A modular repository can also include custom pipeline blocks as Python code. This allows you to share specialized blocks that aren't native to Diffusers. For example, [diffusers/Florence2-image-Annotator](https://huggingface.co/diffusers/Florence2-image-Annotator) contains custom blocks alongside the loading configuration:

Expand Down
51 changes: 50 additions & 1 deletion src/diffusers/modular_pipelines/modular_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@
from typing_extensions import Self

from ..configuration_utils import ConfigMixin, FrozenDict
from ..models.auto_model import AutoModel
from ..models.modeling_utils import ModelMixin
from ..pipelines.pipeline_loading_utils import (
LOADABLE_CLASSES,
_fetch_class_library_tuple,
_unwrap_model,
filter_model_files,
simple_get_class_obj,
)
from ..utils import PushToHubMixin, deprecate, is_accelerate_available, logging
from ..utils import (
TRANSFORMERS_COMPONENT_AUX_FILES,
PushToHubMixin,
deprecate,
is_accelerate_available,
is_transformers_available,
logging,
)
from ..utils.dynamic_modules_utils import get_class_from_dynamic_module, resolve_trust_remote_code
from ..utils.hub_utils import _resolve_revision, load_or_create_model_card, populate_model_card
from ..utils.torch_utils import empty_device_cache, is_compiled_module
Expand All @@ -59,12 +69,46 @@
)


# classes whose components are loaded from weight files; a component without a type hint is loaded with `AutoModel`
_MODEL_CLASSES = (ModelMixin, AutoModel)
if is_transformers_available():
from transformers import PreTrainedModel

_MODEL_CLASSES = (*_MODEL_CLASSES, PreTrainedModel)

if is_accelerate_available():
import accelerate

logger = logging.get_logger(__name__) # pylint: disable=invalid-name


def _is_local_component(
pretrained_model_name_or_path: str | os.PathLike | None, component_spec: ComponentSpec
) -> bool:
"""
Whether the component's files are in `pretrained_model_name_or_path`, a local pipeline directory: weight files for
a model, the config file its class saves for a diffusers component without weights (schedulers, guiders, ...), one
of `TRANSFORMERS_COMPONENT_AUX_FILES` for a transformers one (tokenizers, processors, ...).
"""
if pretrained_model_name_or_path is None:
return False
component_dir = os.path.join(pretrained_model_name_or_path, component_spec.subfolder or "")
if not os.path.isdir(component_dir):
return False
filenames = os.listdir(component_dir)

class_obj = component_spec.type_hint
is_model = class_obj is None or issubclass(class_obj, _MODEL_CLASSES)

if is_model:
return len(filter_model_files(filenames)) > 0

if issubclass(class_obj, ConfigMixin):
return class_obj.config_name in filenames

return any(filename in filenames for filename in TRANSFORMERS_COMPONENT_AUX_FILES)


# map regular pipeline to modular pipeline class name


Expand Down Expand Up @@ -1765,6 +1809,11 @@ def __init__(
library, class_name, component_spec_dict = value
component_spec = self._dict_to_component_spec(name, component_spec_dict)
component_spec.default_creation_method = "from_pretrained"
# a local copy of the repo (e.g. `hf download --local-dir`) keeps the original index, which
# points at the Hub; load the components whose files are present locally from the copy
if _is_local_component(pretrained_model_name_or_path, component_spec):
component_spec.pretrained_model_name_or_path = pretrained_model_name_or_path
component_spec.revision = None
self._component_specs[name] = component_spec

elif name in self._config_specs:
Expand Down
13 changes: 0 additions & 13 deletions src/diffusers/pipelines/pipeline_loading_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,6 @@
TRANSFORMERS_DUMMY_MODULES_FOLDER = "transformers.utils"
CONNECTED_PIPES_KEYS = ["prior"]

# Auxiliary (non-weight) files a transformers component saves next to its weights. Repos with a flat,
# transformers-style layout host a component's files at the repo root instead of in a subfolder, where the
# folder-based allow patterns of `DiffusionPipeline.download` would miss them. Root-hosted weights and
# `config.json` are matched by their own patterns, so only these auxiliary filenames need listing.
# Currently the set needed by DiffusionGemma — extend as new flat-layout pipelines require it.
TRANSFORMERS_COMPONENT_AUX_FILES = [
"chat_template.jinja",
"generation_config.json",
"processor_config.json",
"tokenizer.json",
"tokenizer_config.json",
]

logger = logging.get_logger(__name__)

LOADABLE_CLASSES = {
Expand Down
2 changes: 1 addition & 1 deletion src/diffusers/pipelines/pipeline_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from ..utils import (
CONFIG_NAME,
DEPRECATED_REVISION_ARGS,
TRANSFORMERS_COMPONENT_AUX_FILES,
BaseOutput,
PushToHubMixin,
_get_detailed_type,
Expand Down Expand Up @@ -92,7 +93,6 @@
CONNECTED_PIPES_KEYS,
CUSTOM_PIPELINE_FILE_NAME,
LOADABLE_CLASSES,
TRANSFORMERS_COMPONENT_AUX_FILES,
_fetch_class_library_tuple,
_get_custom_components_and_folders,
_get_custom_pipeline_class,
Expand Down
1 change: 1 addition & 0 deletions src/diffusers/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SAFE_WEIGHTS_INDEX_NAME,
SAFETENSORS_FILE_EXTENSION,
SAFETENSORS_WEIGHTS_NAME,
TRANSFORMERS_COMPONENT_AUX_FILES,
USE_PEFT_BACKEND,
WEIGHTS_INDEX_NAME,
WEIGHTS_NAME,
Expand Down
11 changes: 11 additions & 0 deletions src/diffusers/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
FLASHPACK_FILE_EXTENSION = "flashpack"
GGUF_FILE_EXTENSION = "gguf"
ONNX_EXTERNAL_WEIGHTS_NAME = "weights.pb"
# Auxiliary (non-weight) files a transformers component saves next to its weights, or as its only files for tokenizers
# and processors. `DiffusionPipeline.download` uses them to fetch components hosted at the root of a flat,
# transformers-style repo, and `ModularPipeline` to tell that such a component is present in a local directory.
TRANSFORMERS_COMPONENT_AUX_FILES = [
"chat_template.jinja",
"generation_config.json",
"preprocessor_config.json",
"processor_config.json",
"tokenizer.json",
"tokenizer_config.json",
]
HUGGINGFACE_CO_RESOLVE_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
DIFFUSERS_DYNAMIC_MODULE_NAME = "diffusers_modules"
HF_MODULES_CACHE = os.getenv("HF_MODULES_CACHE", os.path.join(HF_HOME, "modules"))
Expand Down
68 changes: 68 additions & 0 deletions tests/modular_pipelines/test_modular_pipeline_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

import json
import os
import shutil

import pytest
import torch
from huggingface_hub import snapshot_download

from diffusers import AutoModel, ControlNetModel, ModularPipeline, UNet2DConditionModel
from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec
Expand Down Expand Up @@ -276,3 +278,69 @@ def test_init_raises_without_resolvable_blocks(self):
# The base class has no `default_blocks_name`, so with no `blocks` there is nothing to build from.
with pytest.raises(ValueError, match="No pipeline blocks could be resolved"):
ModularPipeline()


class TestLoadFromLocalCopy:
def test_local_copy_loads_present_components_locally(self, tmp_path):
"""`hf download --local-dir` keeps the index pointing at the Hub; components whose subfolder is present in
the local copy load from it, the rest keep their recorded spec."""
local_dir = str(tmp_path / "local-copy")
cache_dir = str(tmp_path / "cache")
snapshot_download("hf-internal-testing/tiny-anima-modular-pipe", local_dir=local_dir)

pipe = ModularPipeline.from_pretrained(local_dir)
for name in ("vae", "transformer", "text_encoder", "scheduler"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding the name of the components, we could call pipe.components.keys() here?

spec = pipe._component_specs[name]
assert spec.pretrained_model_name_or_path == local_dir, f"{name} should load from the local copy"
assert spec.revision is None
assert (
pipe._component_specs["t5_tokenizer"].pretrained_model_name_or_path == "hf-internal-testing/tiny-random-t5"
)

pipe.load_components(names=["vae"], dtype=torch.float32, local_files_only=True, cache_dir=cache_dir)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it not error out when cache_dir doesn't have any weight copies?

I ran the following

from diffusers import ModularPipeline
from huggingface_hub import snapshot_download
import tempfile
import pathlib
import torch 


with tempfile.TemporaryDirectory() as tmpdir:
    tmpdir = pathlib.Path(tmpdir)
    local_dir = tmpdir / "local_dir"
    cache_dir = tmpdir / "cache_dir"

    snapshot_download("hf-internal-testing/tiny-anima-modular-pipe", local_dir=local_dir)
    pipe = ModularPipeline.from_pretrained(local_dir)
    print(pipe.components.keys())

    pipe.load_components(names=["vae"], dtype=torch.float32, local_files_only=True, cache_dir=cache_dir)
    print(pipe.vae is not None)

And I got:

Logs
Fetching 12 files:   0%|          | 0/12 [00:00<?, ?it/s]
Fetching 12 files:   8%|| 1/12 [00:00<00:06,  1.63it/s]
Fetching 12 files:  17%|█▋        | 2/12 [00:00<00:04,  2.27it/s]
Fetching 12 files:  42%|████▏     | 5/12 [00:01<00:01,  6.66it/s]
Fetching 12 files:  58%|█████▊    | 7/12 [00:01<00:00,  8.11it/s]
Fetching 12 files:  75%|███████▌  | 9/12 [00:03<00:01,  2.14it/s]
Fetching 12 files:  83%|████████▎ | 10/12 [00:04<00:01,  1.96it/s]
Fetching 12 files:  92%|█████████▏| 11/12 [00:04<00:00,  2.00it/s]
Fetching 12 files: 100%|██████████| 12/12 [00:05<00:00,  1.34it/s]
Fetching 12 files: 100%|██████████| 12/12 [00:05<00:00,  2.01it/s]
Guiders are currently an experimental feature under active development. The API is subject to breaking changes in future releases.
/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/utils/_validators.py:205: UserWarning: The `local_dir_use_symlinks` argument is deprecated and ignored in `hf_hub_download`. Downloading to a local directory does not use symlinks anymore.
  warnings.warn(
Failed to create component vae:
- Component spec: ComponentSpec(name='vae', type_hint=<class 'diffusers.models.autoencoders.autoencoder_kl_qwenimage.AutoencoderKLQwenImage'>, description=None, config=None, pretrained_model_name_or_path='hf-internal-testing/tiny-anima-modular-pipe', subfolder='vae', variant=None, revision=None, default_creation_method='from_pretrained', repo=None)
- load() called with kwargs: {'dtype': torch.float32, 'local_files_only': True, 'cache_dir': PosixPath('/var/folders/wg/2xcyr_6j3lgc_y5k0344x2b80000gn/T/tmp2xh3y9ma/cache_dir')}
If this component is not required for your workflow you can safely ignore this message.

Traceback:
Traceback (most recent call last):
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/configuration_utils.py", line 414, in load_config
    config_file = hf_hub_download(
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/utils/_validators.py", line 88, in _inner_fn
    return fn(*args, **kwargs)
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/file_download.py", line 1035, in hf_hub_download
    return _hf_hub_download_to_cache_dir(
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/file_download.py", line 1182, in _hf_hub_download_to_cache_dir
    _raise_on_head_call_error(head_call_error, force_download, local_files_only)
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/file_download.py", line 1910, in _raise_on_head_call_error
    raise LocalEntryNotFoundError(
huggingface_hub.errors.LocalEntryNotFoundError: Cannot find the requested files in the disk cache and outgoing traffic has been disabled. To enable hf.co look-ups and downloads online, set 'local_files_only' to False.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/modular_pipelines/modular_pipeline_utils.py", line 347, in load
    component = load_method(pretrained_model_name_or_path, **load_kwargs, **kwargs)
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/utils/_validators.py", line 88, in _inner_fn
    return fn(*args, **kwargs)
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/models/modeling_utils.py", line 1144, in from_pretrained
    config, unused_kwargs, commit_hash = cls.load_config(
  File "/Users/sayakpaul/miniconda3/envs/diffusers/lib/python3.10/site-packages/huggingface_hub/utils/_validators.py", line 88, in _inner_fn
    return fn(*args, **kwargs)
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/configuration_utils.py", line 441, in load_config
    raise EnvironmentError(
OSError: hf-internal-testing/tiny-anima-modular-pipe does not appear to have a file named config.json.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/modular_pipelines/modular_pipeline.py", line 2501, in load_components
    components_to_register[name] = spec.load(**component_load_kwargs)
  File "/Users/sayakpaul/Downloads/diffusers/src/diffusers/modular_pipelines/modular_pipeline_utils.py", line 349, in load
    raise ValueError(f"Unable to load {self.name} using load method: {e}")
ValueError: Unable to load vae using load method: hf-internal-testing/tiny-anima-modular-pipe does not appear to have a file named config.json.

dict_keys(['text_encoder', 'tokenizer', 't5_tokenizer', 'guider', 'vae', 'image_processor', 'text_conditioner', 'transformer', 'scheduler'])
pipe.vae is not None=False

Like the assert just right after should fail. What am I missing?

assert pipe.vae is not None
cached_weights = [p for p in (tmp_path / "cache").rglob("*") if p.suffix in (".safetensors", ".bin")]
assert cached_weights == [], f"weights should not be in the Hub cache: {cached_weights}"

def test_local_copy_missing_files_keeps_recorded_spec(self, tmp_path):
"""A missing subfolder, or a model subfolder without weight files (e.g. a partial download), keeps the
recorded spec instead of shadowing it with an unloadable folder."""
local_dir = str(tmp_path / "local-copy")
snapshot_download("hf-internal-testing/tiny-anima-modular-pipe", local_dir=local_dir)
shutil.rmtree(os.path.join(local_dir, "transformer"))
for filename in os.listdir(os.path.join(local_dir, "vae")):
if filename.endswith((".safetensors", ".bin")):
os.remove(os.path.join(local_dir, "vae", filename))

Comment on lines +311 to +314

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not remove the VAE subfolder directly like the transformer?

pipe = ModularPipeline.from_pretrained(local_dir)
assert (
pipe._component_specs["transformer"].pretrained_model_name_or_path
== "hf-internal-testing/tiny-anima-modular-pipe"
)
assert (
pipe._component_specs["vae"].pretrained_model_name_or_path == "hf-internal-testing/tiny-anima-modular-pipe"
)
assert pipe._component_specs["text_encoder"].pretrained_model_name_or_path == local_dir

def test_local_copy_loads_components_at_root(self, tmp_path):
"""A component recorded without a subfolder is at the root of its repo; when the local copy has its files
there it is loaded from the copy: weights for a model, the saved config file for anything else."""
local_dir = str(tmp_path / "local-copy")
snapshot_download("hf-internal-testing/tiny-cosmos3-modular-pipe", local_dir=local_dir)
index_path = os.path.join(local_dir, "modular_model_index.json")
with open(index_path) as f:
index = json.load(f)
root_components = ["transformer", "scheduler", "text_tokenizer"]
for name in root_components:
for filename in os.listdir(os.path.join(local_dir, name)):
shutil.move(os.path.join(local_dir, name, filename), os.path.join(local_dir, filename))
index[name][2]["subfolder"] = None
with open(index_path, "w") as f:
json.dump(index, f)

pipe = ModularPipeline.from_pretrained(local_dir)
for name in root_components:
assert pipe._component_specs[name].pretrained_model_name_or_path == local_dir, f"{name} not local"
pipe.load_components(names=root_components, dtype=torch.float32, local_files_only=True)
for name in root_components:
assert getattr(pipe, name) is not None, f"{name} did not load from the local copy"
Loading