Skip to content

Commit 59f2651

Browse files
authored
Merge branch 'main' into remove-test-pipelines-common
2 parents 7252c84 + c5469b7 commit 59f2651

25 files changed

Lines changed: 962 additions & 145 deletions

docs/source/en/modular_diffusers/modular_pipeline.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ ModularPipeline {
165165
}
166166
```
167167

168-
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`).
168+
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.
169169

170170
In the example below, the `pretrained_model_name_or_path` will be updated to `"stabilityai/stable-diffusion-xl-base-1.0"`.
171171

@@ -415,6 +415,34 @@ pipeline = ModularPipeline.from_pretrained("stabilityai/stable-diffusion-xl-base
415415
pipeline.save_pretrained("local/path", repo_id="my-username/sdxl-modular", push_to_hub=True)
416416
```
417417

418+
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.
419+
420+
### Save a self-contained copy
421+
422+
Load all the components, then save. Every spec points at the result, so it reloads entirely from one place, including offline.
423+
424+
```py
425+
pipe = ModularPipeline.from_pretrained("MiniMaxAI/MiniMax-H3")
426+
pipe.load_components()
427+
pipe.save_pretrained("path/to/local-copy")
428+
```
429+
430+
The destination recorded in `modular_model_index.json` is wherever [`~ModularPipeline.save_pretrained`] writes: the `save_directory` for a local save, or the `repo_id` when you pass `push_to_hub=True`.
431+
432+
### Keep references to existing components
433+
434+
Load only what's new (or nothing at all). Only loaded components are saved; 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.
435+
436+
```py
437+
pipe = ModularPipeline.from_pretrained("black-forest-labs/FLUX.2-dev")
438+
pipe.update_components(transformer=my_custom_transformer) # the only component in memory
439+
pipe.save_pretrained("local/path", repo_id="my-username/flux2-custom-transformer", push_to_hub=True)
440+
```
441+
442+
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.
443+
444+
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.
445+
418446
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:
419447

420448
```

examples/kandinsky2_2/text_to_image/train_text_to_image_decoder.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -684,17 +684,22 @@ def collate_fn(examples):
684684
)
685685

686686
# Scheduler and math around the number of training steps.
687-
overrode_max_train_steps = False
688-
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
687+
# Check the PR https://github.com/huggingface/diffusers/pull/8312 for detailed explanation.
688+
num_warmup_steps_for_scheduler = args.lr_warmup_steps * accelerator.num_processes
689689
if args.max_train_steps is None:
690-
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
691-
overrode_max_train_steps = True
690+
len_train_dataloader_after_sharding = math.ceil(len(train_dataloader) / accelerator.num_processes)
691+
num_update_steps_per_epoch = math.ceil(len_train_dataloader_after_sharding / args.gradient_accumulation_steps)
692+
num_training_steps_for_scheduler = (
693+
args.num_train_epochs * accelerator.num_processes * num_update_steps_per_epoch
694+
)
695+
else:
696+
num_training_steps_for_scheduler = args.max_train_steps * accelerator.num_processes
692697

693698
lr_scheduler = get_scheduler(
694699
args.lr_scheduler,
695700
optimizer=optimizer,
696-
num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps,
697-
num_training_steps=args.max_train_steps * args.gradient_accumulation_steps,
701+
num_warmup_steps=num_warmup_steps_for_scheduler,
702+
num_training_steps=num_training_steps_for_scheduler,
698703
)
699704
unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
700705
unet, optimizer, train_dataloader, lr_scheduler
@@ -704,8 +709,14 @@ def collate_fn(examples):
704709
vae.to(accelerator.device, dtype=weight_dtype)
705710
# We need to recalculate our total training steps as the size of the training dataloader may have changed.
706711
num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
707-
if overrode_max_train_steps:
712+
if args.max_train_steps is None:
708713
args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
714+
if num_training_steps_for_scheduler != args.max_train_steps * accelerator.num_processes:
715+
logger.warning(
716+
f"The length of the 'train_dataloader' after 'accelerator.prepare' ({len(train_dataloader)}) does not match "
717+
f"the expected length ({len_train_dataloader_after_sharding}) when the learning rate scheduler was created. "
718+
f"This inconsistency may result in the learning rate scheduler not functioning properly."
719+
)
709720
# Afterwards we recalculate our number of training epochs
710721
args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
711722

src/diffusers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,8 @@
569569
"Wan22Image2VideoBlocks",
570570
"Wan22Image2VideoModularPipeline",
571571
"Wan22ModularPipeline",
572+
"Wan22VaceBlocks",
573+
"Wan22VaceModularPipeline",
572574
"WanAnimate2Blocks",
573575
"WanAnimate2DistilledBlocks",
574576
"WanAnimate2DistilledModularPipeline",
@@ -1423,6 +1425,8 @@
14231425
Wan22Image2VideoBlocks,
14241426
Wan22Image2VideoModularPipeline,
14251427
Wan22ModularPipeline,
1428+
Wan22VaceBlocks,
1429+
Wan22VaceModularPipeline,
14261430
WanAnimate2Blocks,
14271431
WanAnimate2DistilledBlocks,
14281432
WanAnimate2DistilledModularPipeline,

src/diffusers/hooks/hooks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ def _set_context(self, name: str | None = None) -> None:
276276
for registry in self._get_child_registries():
277277
registry._set_context(name)
278278

279+
def invalidate_child_registries_cache(self) -> None:
280+
"""Invalidate the cached child-registry list across this module's tree.
281+
282+
`_get_child_registries` caches the registries it finds by walking `named_modules()`, keyed on the registry that
283+
built it. Registering or removing hooks on descendant modules (e.g. block hooks added by `enable_cache`)
284+
changes which modules carry a `_diffusers_hook`, which stales that cache. Call this after any operation that
285+
adds or removes hooks in the subtree so the list is rebuilt on next use. Clears the cache on every registry in
286+
the tree, since the same registries appear in ancestor caches.
287+
"""
288+
for _, module in unwrap_module(self._module_ref).named_modules():
289+
module = unwrap_module(module)
290+
if hasattr(module, "_diffusers_hook"):
291+
module._diffusers_hook._child_registries_cache = None
292+
279293
def _get_child_registries(self) -> list["HookRegistry"]:
280294
"""Return registries of child modules, using a cached list when available.
281295

src/diffusers/models/cache_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def enable_cache(self, config) -> None:
6969
from ..hooks import (
7070
FasterCacheConfig,
7171
FirstBlockCacheConfig,
72+
HookRegistry,
7273
MagCacheConfig,
7374
PyramidAttentionBroadcastConfig,
7475
TaylorSeerCacheConfig,
@@ -101,6 +102,11 @@ def enable_cache(self, config) -> None:
101102
else:
102103
raise ValueError(f"Cache config {type(config)} is not supported.")
103104

105+
# Applying a cache technique registers hooks on child blocks, which stales any
106+
# `_child_registries_cache` built earlier (e.g. by a prior `cache_context`). Invalidate it
107+
# so `_set_context` reaches the freshly-registered block hooks.
108+
HookRegistry.check_if_exists_or_initialize(self).invalidate_child_registries_cache()
109+
104110
self._cache_config = config
105111

106112
def disable_cache(self) -> None:
@@ -144,6 +150,9 @@ def disable_cache(self) -> None:
144150
else:
145151
raise ValueError(f"Cache config {type(self._cache_config)} is not supported.")
146152

153+
# Removing the cache hooks stales any `_child_registries_cache` that included them.
154+
registry.invalidate_child_registries_cache()
155+
147156
self._cache_config = None
148157

149158
def _reset_stateful_cache(self, recurse: bool = True) -> None:

src/diffusers/models/transformers/prior_transformer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,5 +318,8 @@ def forward(
318318
return PriorTransformerOutput(predicted_image_embedding=predicted_image_embedding)
319319

320320
def post_process_latents(self, prior_latents):
321-
prior_latents = (prior_latents * self.clip_std) + self.clip_mean
321+
# `clip_std` / `clip_mean` are parameters of this model, not of a submodule, so group offloading onloads
322+
# them only for the duration of `forward`. This runs after the denoising loop, hence the explicit move.
323+
device = prior_latents.device
324+
prior_latents = (prior_latents * self.clip_std.to(device)) + self.clip_mean.to(device)
322325
return prior_latents

src/diffusers/modular_pipelines/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@
5252
"Wan22Blocks",
5353
"WanImage2VideoAutoBlocks",
5454
"Wan22Image2VideoBlocks",
55+
"Wan22VaceBlocks",
5556
"WanModularPipeline",
5657
"Wan22ModularPipeline",
5758
"WanImage2VideoModularPipeline",
5859
"Wan22Image2VideoModularPipeline",
60+
"Wan22VaceModularPipeline",
5961
]
6062
_import_structure["helios"] = [
6163
"HeliosAutoBlocks",
@@ -226,6 +228,8 @@
226228
Wan22Image2VideoBlocks,
227229
Wan22Image2VideoModularPipeline,
228230
Wan22ModularPipeline,
231+
Wan22VaceBlocks,
232+
Wan22VaceModularPipeline,
229233
WanBlocks,
230234
WanImage2VideoAutoBlocks,
231235
WanImage2VideoModularPipeline,

src/diffusers/modular_pipelines/modular_pipeline.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
_unwrap_model,
3737
simple_get_class_obj,
3838
)
39-
from ..utils import PushToHubMixin, is_accelerate_available, logging
39+
from ..utils import PushToHubMixin, deprecate, is_accelerate_available, logging
4040
from ..utils.dynamic_modules_utils import get_class_from_dynamic_module, resolve_trust_remote_code
4141
from ..utils.hub_utils import _resolve_revision, load_or_create_model_card, populate_model_card
4242
from ..utils.torch_utils import empty_device_cache, is_compiled_module
@@ -135,6 +135,7 @@ def _helios_pyramid_map_fn(config_dict=None):
135135
("wan-animate-2", _create_default_map_fn("WanAnimate2ModularPipeline")),
136136
("wan-animate-2-distilled", _create_default_map_fn("WanAnimate2DistilledModularPipeline")),
137137
("wan-i2v", _wan_i2v_map_fn),
138+
("wan-vace", _create_default_map_fn("Wan22VaceModularPipeline")),
138139
("flux", _create_default_map_fn("FluxModularPipeline")),
139140
("flux-kontext", _create_default_map_fn("FluxKontextModularPipeline")),
140141
("flux2", _create_default_map_fn("Flux2ModularPipeline")),
@@ -1974,10 +1975,13 @@ def save_pretrained(
19741975
push_to_hub (`bool`, *optional*, defaults to `False`):
19751976
Whether to push the pipeline to the Hugging Face model hub after saving it.
19761977
**kwargs: Additional keyword arguments:
1977-
- `overwrite_modular_index` (`bool`, *optional*, defaults to `False`):
1978-
When saving a Modular Pipeline, its components in `modular_model_index.json` may reference repos
1979-
different from the destination repo. Setting this to `True` updates all component references in
1980-
`modular_model_index.json` so they point to the repo specified by `repo_id`.
1978+
- `overwrite_modular_index` (`bool`, *optional*, defaults to `True`):
1979+
Whether to update `modular_model_index.json` so each saved component's loading spec points to the
1980+
destination: `repo_id` when pushing to the Hub, otherwise `save_directory`. Components that are not
1981+
loaded are not saved and always keep their recorded loading specs. Pass `False` to also preserve
1982+
the recorded specs of the components being saved (e.g. for an index that deliberately references
1983+
other repositories); components without a load id (such as custom models added with
1984+
`update_components`) are still rewritten since they have no recorded source.
19811985
- `repo_id` (`str`, *optional*):
19821986
The repository ID to push the pipeline to. Defaults to the last component of `save_directory`.
19831987
- `commit_message` (`str`, *optional*):
@@ -1989,7 +1993,17 @@ def save_pretrained(
19891993
- `token` (`str`, *optional*):
19901994
The Hugging Face token to use for authentication.
19911995
"""
1992-
overwrite_modular_index = kwargs.pop("overwrite_modular_index", False)
1996+
if "overwrite_modular_index" not in kwargs:
1997+
deprecate(
1998+
"overwrite_modular_index",
1999+
"0.43.0",
2000+
"The default of `overwrite_modular_index` in `ModularPipeline.save_pretrained` changed from `False`"
2001+
" to `True`: the saved `modular_model_index.json` now points each saved component at the destination"
2002+
" (the save directory, or `repo_id` when pushing to the Hub). Pass `overwrite_modular_index=False`"
2003+
" to keep the previously recorded loading specs, or pass `True` explicitly to silence this warning.",
2004+
standard_warn=False,
2005+
)
2006+
overwrite_modular_index = kwargs.pop("overwrite_modular_index", True)
19932007
repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1])
19942008

19952009
if push_to_hub:
@@ -2060,6 +2074,9 @@ def save_pretrained(
20602074
library, class_name, component_spec_dict = self.config[component_name]
20612075
component_spec_dict["pretrained_model_name_or_path"] = repo_id if push_to_hub else save_directory
20622076
component_spec_dict["subfolder"] = component_name
2077+
component_spec_dict["variant"] = variant if save_method_accept_variant else None
2078+
# a revision pinned for the original source doesn't exist at the destination
2079+
component_spec_dict["revision"] = None
20632080
self.register_to_config(**{component_name: (library, class_name, component_spec_dict)})
20642081

20652082
self.save_config(save_directory=save_directory)

src/diffusers/modular_pipelines/wan/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
_import_structure["modular_blocks_wan22"] = ["Wan22Blocks"]
2626
_import_structure["modular_blocks_wan22_i2v"] = ["Wan22Image2VideoBlocks"]
2727
_import_structure["modular_blocks_wan_i2v"] = ["WanImage2VideoAutoBlocks"]
28+
_import_structure["modular_blocks_wan_vace"] = ["Wan22VaceBlocks"]
2829
_import_structure["modular_pipeline"] = [
2930
"Wan22Image2VideoModularPipeline",
3031
"Wan22ModularPipeline",
32+
"Wan22VaceModularPipeline",
3133
"WanImage2VideoModularPipeline",
3234
"WanModularPipeline",
3335
]
@@ -43,9 +45,11 @@
4345
from .modular_blocks_wan22 import Wan22Blocks
4446
from .modular_blocks_wan22_i2v import Wan22Image2VideoBlocks
4547
from .modular_blocks_wan_i2v import WanImage2VideoAutoBlocks
48+
from .modular_blocks_wan_vace import Wan22VaceBlocks
4649
from .modular_pipeline import (
4750
Wan22Image2VideoModularPipeline,
4851
Wan22ModularPipeline,
52+
Wan22VaceModularPipeline,
4953
WanImage2VideoModularPipeline,
5054
WanModularPipeline,
5155
)

src/diffusers/modular_pipelines/wan/before_denoise.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,51 @@ def __call__(self, components: WanModularPipeline, state: PipelineState) -> Pipe
402402
return components, state
403403

404404

405+
class WanVaceAdditionalInputsStep(ModularPipelineBlocks):
406+
model_name = "wan-vace"
407+
408+
@property
409+
def description(self) -> str:
410+
return (
411+
"Input processing step that extends `num_frames` with the reference image frames so that the initial "
412+
"noise latents match the frame dimension of the vace conditioning latents.\n\n"
413+
"This block should be placed after the encoder steps and the text input step."
414+
)
415+
416+
@property
417+
def inputs(self) -> list[InputParam]:
418+
return [
419+
InputParam(name="num_videos_per_prompt", default=1),
420+
InputParam(name="batch_size", required=True),
421+
InputParam(name="num_frames", type_hint=int),
422+
InputParam(
423+
name="num_reference_images",
424+
type_hint=int,
425+
default=0,
426+
description="Number of reference images prepended on the frame dimension of the conditioning latents. Can be generated in vace_encoder step.",
427+
),
428+
]
429+
430+
@staticmethod
431+
def check_inputs(block_state):
432+
if block_state.batch_size != 1:
433+
raise ValueError("Passing a list of prompts is not yet supported. This may be supported in the future.")
434+
if block_state.num_videos_per_prompt != 1:
435+
raise ValueError(
436+
"Generating multiple videos per prompt is not yet supported. This may be supported in the future."
437+
)
438+
439+
def __call__(self, components: WanModularPipeline, state: PipelineState) -> PipelineState:
440+
block_state = self.get_block_state(state)
441+
self.check_inputs(block_state)
442+
443+
num_frames = block_state.num_frames or components.default_num_frames
444+
block_state.num_frames = num_frames + block_state.num_reference_images * components.vae_scale_factor_temporal
445+
446+
self.set_block_state(state, block_state)
447+
return components, state
448+
449+
405450
class WanSetTimestepsStep(ModularPipelineBlocks):
406451
model_name = "wan"
407452

0 commit comments

Comments
 (0)