From 7117fd0eb8e241d4e89eb4e207a014fc81540bee Mon Sep 17 00:00:00 2001 From: yiyixuxu Date: Mon, 31 Aug 2026 10:31:05 +0200 Subject: [PATCH 1/7] flipt default for save_pretrained overwrite_modular_index to be True --- .../modular_pipelines/modular_pipeline.py | 27 +++++++++--- .../test_modular_pipeline_loading.py | 43 ++++++++++++++++--- .../testing_utils/loading.py | 2 +- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index f2576b99328d..0338e13cfcef 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -36,7 +36,7 @@ _unwrap_model, simple_get_class_obj, ) -from ..utils import PushToHubMixin, is_accelerate_available, logging +from ..utils import PushToHubMixin, deprecate, is_accelerate_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 @@ -1974,10 +1974,13 @@ def save_pretrained( push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the pipeline to the Hugging Face model hub after saving it. **kwargs: Additional keyword arguments: - - `overwrite_modular_index` (`bool`, *optional*, defaults to `False`): - When saving a Modular Pipeline, its components in `modular_model_index.json` may reference repos - different from the destination repo. Setting this to `True` updates all component references in - `modular_model_index.json` so they point to the repo specified by `repo_id`. + - `overwrite_modular_index` (`bool`, *optional*, defaults to `True`): + Whether to update `modular_model_index.json` so each saved component's loading spec points to the + destination: `repo_id` when pushing to the Hub, otherwise `save_directory`. Components that are + not loaded are not saved and always keep their recorded loading specs. Pass `False` to also + preserve the recorded specs of the components being saved (e.g. for an index that deliberately + references other repositories); components without a load id (such as custom models added with + `update_components`) are still rewritten since they have no recorded source. - `repo_id` (`str`, *optional*): The repository ID to push the pipeline to. Defaults to the last component of `save_directory`. - `commit_message` (`str`, *optional*): @@ -1989,7 +1992,17 @@ def save_pretrained( - `token` (`str`, *optional*): The Hugging Face token to use for authentication. """ - overwrite_modular_index = kwargs.pop("overwrite_modular_index", False) + if "overwrite_modular_index" not in kwargs: + deprecate( + "overwrite_modular_index", + "0.43.0", + "The default of `overwrite_modular_index` in `ModularPipeline.save_pretrained` changed from `False`" + " to `True`: the saved `modular_model_index.json` now points each saved component at the destination" + " (the save directory, or `repo_id` when pushing to the Hub). Pass `overwrite_modular_index=False`" + " to keep the previously recorded loading specs, or pass `True` explicitly to silence this warning.", + standard_warn=False, + ) + overwrite_modular_index = kwargs.pop("overwrite_modular_index", True) repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1]) if push_to_hub: @@ -2060,6 +2073,8 @@ def save_pretrained( library, class_name, component_spec_dict = self.config[component_name] component_spec_dict["pretrained_model_name_or_path"] = repo_id if push_to_hub else save_directory component_spec_dict["subfolder"] = component_name + component_spec_dict["variant"] = variant if save_method_accept_variant else None + component_spec_dict["revision"] = None self.register_to_config(**{component_name: (library, class_name, component_spec_dict)}) self.save_config(save_directory=save_directory) diff --git a/tests/modular_pipelines/test_modular_pipeline_loading.py b/tests/modular_pipelines/test_modular_pipeline_loading.py index 3b9ebcc1cdf3..16d78d797707 100644 --- a/tests/modular_pipelines/test_modular_pipeline_loading.py +++ b/tests/modular_pipelines/test_modular_pipeline_loading.py @@ -16,6 +16,7 @@ import json import os +import pytest import torch from diffusers import AutoModel, ControlNetModel, ModularPipeline, UNet2DConditionModel @@ -120,16 +121,17 @@ def test_load_components_skips_invalid_pretrained_path(self): class TestCustomModelSavePretrained: - def test_save_pretrained_updates_index_for_local_model(self, tmp_path): - """When a component without _diffusers_load_id (custom/local model) is saved, - modular_model_index.json should point to the save directory.""" + @pytest.mark.parametrize("overwrite_modular_index", [True, False]) + def test_save_pretrained_updates_index_for_local_model(self, tmp_path, overwrite_modular_index): + """A component without _diffusers_load_id (custom/local model) is rewritten to the save directory in both + modes; other components' specs follow `overwrite_modular_index`.""" pipe = ModularPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-xl-pipe") pipe.load_components(dtype=torch.float32) pipe.unet._diffusers_load_id = "null" save_dir = str(tmp_path / "my-pipeline") - pipe.save_pretrained(save_dir) + pipe.save_pretrained(save_dir, overwrite_modular_index=overwrite_modular_index) with open(os.path.join(save_dir, "modular_model_index.json")) as f: index = json.load(f) @@ -139,7 +141,8 @@ def test_save_pretrained_updates_index_for_local_model(self, tmp_path): assert unet_spec["subfolder"] == "unet" _library, _cls, vae_spec = index["vae"] - assert vae_spec["pretrained_model_name_or_path"] == "hf-internal-testing/tiny-stable-diffusion-xl-pipe" + expected_vae = save_dir if overwrite_modular_index else "hf-internal-testing/tiny-stable-diffusion-xl-pipe" + assert vae_spec["pretrained_model_name_or_path"] == expected_vae def test_save_pretrained_roundtrip_with_local_model(self, tmp_path): """A pipeline with a custom/local model should be saveable and re-loadable with identical outputs.""" @@ -164,9 +167,10 @@ def test_save_pretrained_roundtrip_with_local_model(self, tmp_path): for key in original_state_dict: assert torch.equal(original_state_dict[key], loaded_state_dict[key]), f"Mismatch in {key}" - def test_save_pretrained_updates_index_for_model_with_no_load_id(self, tmp_path): + @pytest.mark.parametrize("overwrite_modular_index", [True, False]) + def test_save_pretrained_updates_index_for_model_with_no_load_id(self, tmp_path, overwrite_modular_index): """testing the workflow of update the pipeline with a custom model and save the pipeline, - the modular_model_index.json should point to the save directory.""" + the modular_model_index.json should point to the save directory in both modes.""" pipe = ModularPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-xl-pipe") pipe.load_components(dtype=torch.float32) @@ -177,6 +181,26 @@ def test_save_pretrained_updates_index_for_model_with_no_load_id(self, tmp_path) pipe.update_components(unet=unet) + save_dir = str(tmp_path / "my-pipeline") + pipe.save_pretrained(save_dir, overwrite_modular_index=overwrite_modular_index) + + with open(os.path.join(save_dir, "modular_model_index.json")) as f: + index = json.load(f) + + _library, _cls, unet_spec = index["unet"] + assert unet_spec["pretrained_model_name_or_path"] == save_dir + assert unet_spec["subfolder"] == "unet" + + _library, _cls, vae_spec = index["vae"] + expected_vae = save_dir if overwrite_modular_index else "hf-internal-testing/tiny-stable-diffusion-xl-pipe" + assert vae_spec["pretrained_model_name_or_path"] == expected_vae + + def test_save_pretrained_default_writes_self_contained_local_copy(self, tmp_path): + """By default the saved index points at the save directory, so the copy reloads offline; a component + that was never loaded is not saved and keeps its recorded spec.""" + pipe = ModularPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-xl-pipe") + pipe.load_components(names=["unet"], dtype=torch.float32) + save_dir = str(tmp_path / "my-pipeline") pipe.save_pretrained(save_dir) @@ -186,10 +210,15 @@ def test_save_pretrained_updates_index_for_model_with_no_load_id(self, tmp_path) _library, _cls, unet_spec = index["unet"] assert unet_spec["pretrained_model_name_or_path"] == save_dir assert unet_spec["subfolder"] == "unet" + assert unet_spec["revision"] is None _library, _cls, vae_spec = index["vae"] assert vae_spec["pretrained_model_name_or_path"] == "hf-internal-testing/tiny-stable-diffusion-xl-pipe" + loaded_pipe = ModularPipeline.from_pretrained(save_dir) + loaded_pipe.load_components(names=["unet"], dtype=torch.float32, local_files_only=True) + assert loaded_pipe.unet is not None + def test_save_pretrained_overwrite_modular_index(self, tmp_path): """With overwrite_modular_index=True, all component references should point to the save directory.""" pipe = ModularPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-xl-pipe") diff --git a/tests/modular_pipelines/testing_utils/loading.py b/tests/modular_pipelines/testing_utils/loading.py index 64a9dd345008..63b1872f07eb 100644 --- a/tests/modular_pipelines/testing_utils/loading.py +++ b/tests/modular_pipelines/testing_utils/loading.py @@ -88,7 +88,7 @@ def test_modular_index_consistency(self, tmp_path): components_spec = pipe._component_specs components = sorted(components_spec.keys()) - pipe.save_pretrained(str(tmp_path)) + pipe.save_pretrained(str(tmp_path), overwrite_modular_index=False) index_file = tmp_path / "modular_model_index.json" assert index_file.exists() From 6ffb8b4ffdb0ce7249a6d4334539d2274b4414f4 Mon Sep 17 00:00:00 2001 From: yiyixuxu Date: Mon, 31 Aug 2026 10:48:58 +0200 Subject: [PATCH 2/7] update doc --- .../en/modular_diffusers/modular_pipeline.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/modular_pipeline.md b/docs/source/en/modular_diffusers/modular_pipeline.md index 07dc30b078ae..99952404080c 100644 --- a/docs/source/en/modular_diffusers/modular_pipeline.md +++ b/docs/source/en/modular_diffusers/modular_pipeline.md @@ -165,7 +165,7 @@ ModularPipeline { } ``` -If you pass a repository to [`~ModularPipelineBlocks.init_pipeline`], it overrides the loading path by matching your block's components against the pipeline config in that repository (`model_index.json` or `modular_model_index.json`). +If you pass a repository to [`~ModularPipelineBlocks.init_pipeline`], it overrides the loading path by matching your block's components against the pipeline config in that repository (`model_index.json` or `modular_model_index.json`). See [Modular repository](#modular-repository) for how loading specs are recorded and saved. In the example below, the `pretrained_model_name_or_path` will be updated to `"stabilityai/stable-diffusion-xl-base-1.0"`. @@ -415,6 +415,26 @@ pipeline = ModularPipeline.from_pretrained("stabilityai/stable-diffusion-xl-base pipeline.save_pretrained("local/path", repo_id="my-username/sdxl-modular", push_to_hub=True) ``` +By default, [`~ModularPipeline.save_pretrained`] saves the components that are currently loaded, and points each saved component's loading spec in `modular_model_index.json` at the destination — the `repo_id` when pushing to the Hub, otherwise the save directory. A component that isn't loaded isn't saved and keeps its recorded spec, so it is still fetched from its original location later. This gives you two ways to save, depending on what you want: + +- **A self-contained copy** — load all the components, then save. Every spec points at the result, so it reloads entirely from one place, including offline. (A raw download like `hf download ... --local-dir` doesn't do this — the published specs still point at the Hub.) + + ```py + pipe = ModularPipeline.from_pretrained("MiniMaxAI/MiniMax-H3") + pipe.load_components() + pipe.save_pretrained("path/to/local-copy") + ``` + +- **Reuse existing components without saving the weights again** — load only what's new (or nothing at all). Only loaded components are written; everything else stays a pointer to its original repository. For example, to share a single custom transformer while the other components keep loading from the base repo — the same shape as the quantized-transformer repository above: + + ```py + pipe = ModularPipeline.from_pretrained("black-forest-labs/FLUX.2-dev") + pipe.update_components(transformer=my_custom_transformer) # the only component in memory + pipe.save_pretrained("local/path", repo_id="my-username/flux2-custom-transformer", push_to_hub=True) + ``` + +Pass `overwrite_modular_index=False` to also preserve the recorded loading specs of the components being saved. + 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: ``` From 6bddc767bae7832465dda2566ebb4e4861bc0dfe Mon Sep 17 00:00:00 2001 From: yiyixuxu Date: Wed, 2 Sep 2026 21:42:23 +0200 Subject: [PATCH 3/7] make style Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HCdbvRpL9fv3h3WwSUPpfS --- src/diffusers/modular_pipelines/modular_pipeline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index 0338e13cfcef..b7ef8ddf82e8 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -1976,10 +1976,10 @@ def save_pretrained( **kwargs: Additional keyword arguments: - `overwrite_modular_index` (`bool`, *optional*, defaults to `True`): Whether to update `modular_model_index.json` so each saved component's loading spec points to the - destination: `repo_id` when pushing to the Hub, otherwise `save_directory`. Components that are - not loaded are not saved and always keep their recorded loading specs. Pass `False` to also - preserve the recorded specs of the components being saved (e.g. for an index that deliberately - references other repositories); components without a load id (such as custom models added with + destination: `repo_id` when pushing to the Hub, otherwise `save_directory`. Components that are not + loaded are not saved and always keep their recorded loading specs. Pass `False` to also preserve + the recorded specs of the components being saved (e.g. for an index that deliberately references + other repositories); components without a load id (such as custom models added with `update_components`) are still rewritten since they have no recorded source. - `repo_id` (`str`, *optional*): The repository ID to push the pipeline to. Defaults to the last component of `save_directory`. From 51c129a5f8085d60fc9eca7b8ab303aa6e00b80f Mon Sep 17 00:00:00 2001 From: YiYi Xu Date: Wed, 2 Sep 2026 12:17:31 -1000 Subject: [PATCH 4/7] Update docs/source/en/modular_diffusers/modular_pipeline.md Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com> --- docs/source/en/modular_diffusers/modular_pipeline.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/modular_pipeline.md b/docs/source/en/modular_diffusers/modular_pipeline.md index 99952404080c..998403f63c9d 100644 --- a/docs/source/en/modular_diffusers/modular_pipeline.md +++ b/docs/source/en/modular_diffusers/modular_pipeline.md @@ -425,7 +425,9 @@ By default, [`~ModularPipeline.save_pretrained`] saves the components that are c pipe.save_pretrained("path/to/local-copy") ``` -- **Reuse existing components without saving the weights again** — load only what's new (or nothing at all). Only loaded components are written; everything else stays a pointer to its original repository. For example, to share a single custom transformer while the other components keep loading from the base repo — the same shape as the quantized-transformer repository above: +### Keep references to existing components + +Load only what's new (or nothing at all). Only loaded components are written; everything else stays a pointer to its original repository. Use this mode when you want to replace one component while continuing to load the others from their original repository. For example, save a custom transformer while the remaining components continue to load from the base repository. ```py pipe = ModularPipeline.from_pretrained("black-forest-labs/FLUX.2-dev") From 7785b7448930211889c15823ab5a2b2eaf2746d7 Mon Sep 17 00:00:00 2001 From: YiYi Xu Date: Wed, 2 Sep 2026 12:17:55 -1000 Subject: [PATCH 5/7] Update docs/source/en/modular_diffusers/modular_pipeline.md Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com> --- docs/source/en/modular_diffusers/modular_pipeline.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/modular_pipeline.md b/docs/source/en/modular_diffusers/modular_pipeline.md index 998403f63c9d..bcd416ebb3df 100644 --- a/docs/source/en/modular_diffusers/modular_pipeline.md +++ b/docs/source/en/modular_diffusers/modular_pipeline.md @@ -417,7 +417,9 @@ pipeline.save_pretrained("local/path", repo_id="my-username/sdxl-modular", push_ By default, [`~ModularPipeline.save_pretrained`] saves the components that are currently loaded, and points each saved component's loading spec in `modular_model_index.json` at the destination — the `repo_id` when pushing to the Hub, otherwise the save directory. A component that isn't loaded isn't saved and keeps its recorded spec, so it is still fetched from its original location later. This gives you two ways to save, depending on what you want: -- **A self-contained copy** — load all the components, then save. Every spec points at the result, so it reloads entirely from one place, including offline. (A raw download like `hf download ... --local-dir` doesn't do this — the published specs still point at the Hub.) +### Save a self-contained copy + +Load all the components, then save. Every spec points at the result, so it reloads entirely from one place, including offline. Downloading files with `hf download --local-dir` does not rewrite `modular_model_index.json`, so an existing index may still point to the Hub. ```py pipe = ModularPipeline.from_pretrained("MiniMaxAI/MiniMax-H3") From 8b30dd957b6d28554bcc79b93f7c84fcdf9ad526 Mon Sep 17 00:00:00 2001 From: YiYi Xu Date: Wed, 2 Sep 2026 12:18:27 -1000 Subject: [PATCH 6/7] Update docs/source/en/modular_diffusers/modular_pipeline.md Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com> --- docs/source/en/modular_diffusers/modular_pipeline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/modular_pipeline.md b/docs/source/en/modular_diffusers/modular_pipeline.md index bcd416ebb3df..793d858c5cd2 100644 --- a/docs/source/en/modular_diffusers/modular_pipeline.md +++ b/docs/source/en/modular_diffusers/modular_pipeline.md @@ -415,7 +415,7 @@ pipeline = ModularPipeline.from_pretrained("stabilityai/stable-diffusion-xl-base pipeline.save_pretrained("local/path", repo_id="my-username/sdxl-modular", push_to_hub=True) ``` -By default, [`~ModularPipeline.save_pretrained`] saves the components that are currently loaded, and points each saved component's loading spec in `modular_model_index.json` at the destination — the `repo_id` when pushing to the Hub, otherwise the save directory. A component that isn't loaded isn't saved and keeps its recorded spec, so it is still fetched from its original location later. This gives you two ways to save, depending on what you want: +By default, [`~ModularPipeline.save_pretrained`] writes each currently loaded component that Diffusers can serialize. Components that are not loaded, or cannot be serialized, are not written and keep their existing loading specifications. This gives you two ways to save, depending on what you want. ### Save a self-contained copy From 028c12444e1266faf94236ce8b9f88489cc8c2cd Mon Sep 17 00:00:00 2001 From: YiYi Xu Date: Wed, 2 Sep 2026 13:12:09 -1000 Subject: [PATCH 7/7] Update docs/source/en/modular_diffusers/modular_pipeline.md Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com> --- docs/source/en/modular_diffusers/modular_pipeline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/modular_pipeline.md b/docs/source/en/modular_diffusers/modular_pipeline.md index 793d858c5cd2..e5093ae789f5 100644 --- a/docs/source/en/modular_diffusers/modular_pipeline.md +++ b/docs/source/en/modular_diffusers/modular_pipeline.md @@ -437,7 +437,7 @@ Load only what's new (or nothing at all). Only loaded components are written; ev pipe.save_pretrained("local/path", repo_id="my-username/flux2-custom-transformer", push_to_hub=True) ``` -Pass `overwrite_modular_index=False` to also preserve the recorded loading specs of the components being saved. +Pass `overwrite_modular_index=False` to preserve the recorded specs of saved components that already have a load ID. Components without a recorded source, such as models added with `update_components`, are still rewritten to point to the destination. 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: