1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/modular_pipelines/testing_utils/loading.py
YiYi Xu 7643c48266 ModularPipeline.save_pretrained: change the default overwrite_modular_index to be True (#14659)
* flipt default for save_pretrained overwrite_modular_index to be True

* update doc

* make style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCdbvRpL9fv3h3WwSUPpfS

* Update docs/source/en/modular_diffusers/modular_pipeline.md

Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>

* Update docs/source/en/modular_diffusers/modular_pipeline.md

Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>

* Update docs/source/en/modular_diffusers/modular_pipeline.md

Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>

* Update docs/source/en/modular_diffusers/modular_pipeline.md

Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>

* address review comments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCdbvRpL9fv3h3WwSUPpfS

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
2026-09-03 13:50:58 -10:00

183 lines
7.9 KiB
Python

# coding=utf-8
# Copyright 2026 HuggingFace Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import weakref
import pytest
import torch
import diffusers
from diffusers import ComponentsManager, ModularPipeline
from diffusers.utils import logging
from ...testing_utils import CaptureLogger, torch_device
from .common import BaseModularPipelineOutputMixin
from .utils import get_specified_components
class ModularLoadingTesterMixin(BaseModularPipelineOutputMixin):
"""
Serialization and component lifecycle tests for a modular pipeline: `save_pretrained`/`from_pretrained`
round-trips, what `modular_model_index.json` records, and `load_components`/`unload_components`.
Tests that assert on *device memory* after unloading live in `memory.py` instead.
"""
def test_save_from_pretrained(self, tmp_path, base_pipe_output):
base_pipe = self.get_pipeline().to(torch_device)
base_pipe.save_pretrained(str(tmp_path))
pipe = ModularPipeline.from_pretrained(tmp_path)
pipe.load_components(dtype=torch.float32)
pipe.to(torch_device)
image = pipe(**self.get_dummy_inputs(), output=self.output_name)
expected_slice = base_pipe_output[0, -3:, -3:, -1].flatten()
image_slice = image[0, -3:, -3:, -1].flatten()
assert torch.abs(expected_slice - image_slice).max() < 1e-3
def test_load_expected_components_from_pretrained(self, tmp_path):
pipe = self.get_pipeline()
expected = get_specified_components(self.pretrained_model_name_or_path, cache_dir=tmp_path)
if not expected:
pytest.skip("Skipping test as we couldn't fetch the expected components.")
actual = {
name
for name in pipe.components
if getattr(pipe, name, None) is not None
and getattr(getattr(pipe, name), "_diffusers_load_id", None) not in (None, "null")
}
assert expected == actual, f"Component mismatch: missing={expected - actual}, unexpected={actual - expected}"
def test_load_expected_components_from_save_pretrained(self, tmp_path):
pipe = self.get_pipeline()
save_dir = str(tmp_path / "saved-pipeline")
pipe.save_pretrained(save_dir)
expected = get_specified_components(save_dir)
loaded_pipe = ModularPipeline.from_pretrained(save_dir)
loaded_pipe.load_components(dtype=torch.float32)
actual = {
name
for name in loaded_pipe.components
if getattr(loaded_pipe, name, None) is not None
and getattr(getattr(loaded_pipe, name), "_diffusers_load_id", None) not in (None, "null")
}
assert expected == actual, (
f"Component mismatch after save/load: missing={expected - actual}, unexpected={actual - expected}"
)
def test_modular_index_consistency(self, tmp_path):
pipe = self.get_pipeline()
components_spec = pipe._component_specs
components = sorted(components_spec.keys())
pipe.save_pretrained(str(tmp_path), overwrite_modular_index=False)
index_file = tmp_path / "modular_model_index.json"
assert index_file.exists()
with open(index_file) as f:
index_contents = json.load(f)
compulsory_keys = {"_blocks_class_name", "_class_name", "_diffusers_version"}
for k in compulsory_keys:
assert k in index_contents
to_check_attrs = {"pretrained_model_name_or_path", "revision", "subfolder"}
for component in components:
spec = components_spec[component]
for attr in to_check_attrs:
if getattr(spec, "pretrained_model_name_or_path", None) is not None:
for attr in to_check_attrs:
assert component in index_contents, f"{component} should be present in index but isn't."
attr_value_from_index = index_contents[component][2][attr]
assert getattr(spec, attr) == attr_value_from_index
def test_unload_components(self):
pipe = ModularPipeline.from_pretrained(self.pretrained_model_name_or_path)
pipe.load_components()
name = next(name for name in pipe.pretrained_component_names if pipe.components.get(name) is not None)
spec_before = pipe._component_specs[name]
pipe.unload_components(name)
assert getattr(pipe, name) is None
# `components` is the mapping most callers iterate over: the entry stays, its value becomes None
assert pipe.components[name] is None
# unloading an already unloaded component is a no-op, not an error
pipe.unload_components(name)
assert pipe.components[name] is None
# the spec survives, so the component can be loaded again
assert pipe._component_specs[name] is spec_before
pipe.load_components(names=name)
assert getattr(pipe, name) is not None
# with a ComponentsManager attached, unloading also removes the component from the manager
manager = ComponentsManager()
pipe = ModularPipeline.from_pretrained(self.pretrained_model_name_or_path, components_manager=manager)
pipe.load_components(names=name)
assert len(manager._lookup_ids(name=name)) == 1
pipe.unload_components(name)
assert getattr(pipe, name) is None
assert len(manager._lookup_ids(name=name)) == 0
def test_unload_components_multiple_names(self):
pipe = ModularPipeline.from_pretrained(self.pretrained_model_name_or_path)
pipe.load_components()
names = [name for name in pipe.pretrained_component_names if pipe.components.get(name) is not None]
if len(names) < 2:
pytest.skip("Skipping test as the pipeline has fewer than two loaded pretrained components.")
pipe.unload_components(names)
assert all(pipe.components[name] is None for name in names)
pipe.load_components(names=names)
assert all(pipe.components[name] is not None for name in names)
def test_unload_components_invalid_names(self):
pipe = ModularPipeline.from_pretrained(self.pretrained_model_name_or_path)
pipe.load_components()
name = next(name for name in pipe.pretrained_component_names if pipe.components.get(name) is not None)
with pytest.raises(ValueError, match="Invalid type for names"):
pipe.unload_components((name,))
assert pipe.components[name] is not None
# an unknown name is warned about and skipped; the known names are still unloaded
logger = logging.get_logger("diffusers.modular_pipelines.modular_pipeline")
logger.setLevel(diffusers.logging.WARNING)
with CaptureLogger(logger) as cap_logger:
pipe.unload_components([name, "not_a_component"])
assert "not_a_component" in cap_logger.out
assert pipe.components[name] is None
def test_unload_components_releases_component(self):
pipe = ModularPipeline.from_pretrained(self.pretrained_model_name_or_path)
pipe.load_components()
name = next(
name for name in pipe.pretrained_component_names if isinstance(pipe.components.get(name), torch.nn.Module)
)
# a weakref keeps no strong reference, so it goes dead only if nothing in the pipeline holds the
# component anymore — which is what makes the memory actually reclaimable
component_ref = weakref.ref(pipe.components[name])
pipe.unload_components(name)
assert component_ref() is None