mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-23 09:09:01 +03:00
* 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>
183 lines
7.9 KiB
Python
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
|