From 12ceecf077e5289e0e4a55fe2150b9b85087bb70 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Wed, 20 Aug 2025 11:04:28 +0530 Subject: [PATCH 1/8] feat: implement requirements validation for custom blocks. --- src/diffusers/commands/custom_blocks.py | 2 - .../modular_pipelines/modular_pipeline.py | 66 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/diffusers/commands/custom_blocks.py b/src/diffusers/commands/custom_blocks.py index 43d9ea88577a..953240c5a2c3 100644 --- a/src/diffusers/commands/custom_blocks.py +++ b/src/diffusers/commands/custom_blocks.py @@ -89,8 +89,6 @@ def run(self): # automap = self._create_automap(parent_class=parent_class, child_class=child_class) # with open(CONFIG, "w") as f: # json.dump(automap, f) - with open("requirements.txt", "w") as f: - f.write("") def _choose_block(self, candidates, chosen=None): for cls, base in candidates: diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index 8a05cce209c5..e45dc5068a40 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -32,6 +32,7 @@ from ..utils import PushToHubMixin, is_accelerate_available, logging from ..utils.dynamic_modules_utils import get_class_from_dynamic_module, resolve_trust_remote_code from ..utils.hub_utils import load_or_create_model_card, populate_model_card +from ..utils.import_utils import _is_package_available from .components_manager import ComponentsManager from .modular_pipeline_utils import ( ComponentSpec, @@ -231,6 +232,7 @@ class ModularPipelineBlocks(ConfigMixin, PushToHubMixin): config_name = "modular_config.json" model_name = None + _requirements: Union[List[Tuple[str, str], Tuple[str, str]]] = None @classmethod def _get_signature_keys(cls, obj): @@ -270,6 +272,28 @@ def _get_required_inputs(self): return input_names + def _get_requirements(self): + if getattr(self, "_requirements", None) is not None: + defined_reqs = self._requirements + if not isinstance(defined_reqs): + defined_reqs = [defined_reqs] + + final_reqs = [] + for pkg, specified_ver in defined_reqs: + pkg_available, pkg_actual_ver = _is_package_available(pkg) + if not pkg_available: + raise ValueError( + f"{pkg} was specified in the requirements but wasn't found. Please check your environment." + ) + if specified_ver != pkg_actual_ver: + logger.warning( + f"Version for {pkg} was specified to be {specified_ver} whereas the actual version found is {pkg_actual_ver}. Ignore if this is not concerning." + ) + final_reqs.append((pkg, specified_ver)) + + else: + return None + @property def required_inputs(self) -> List[InputParam]: return self._get_required_inputs() @@ -293,6 +317,31 @@ def from_pretrained( trust_remote_code: Optional[bool] = None, **kwargs, ): + config = cls.load_config(pretrained_model_name_or_path) + has_remote_code = "auto_map" in config and cls.__name__ in config["auto_map"] + trust_remote_code = resolve_trust_remote_code( + trust_remote_code, pretrained_model_name_or_path, has_remote_code + ) + if not (has_remote_code and trust_remote_code): + raise ValueError( + "Selected model repository does not happear to have any custom code or does not have a valid `config.json` file." + ) + + if "requirements" in config and config["requirements"] is not None: + requirements: Union[List[Tuple[str, str]], Tuple[str, str]] = config["requirements"] + if not isinstance(requirements, list): + requirements = [requirements] + for pkg, fetched_ver in requirements: + pkg_available, pkg_actual_ver = _is_package_available(pkg) + if not pkg_available: + raise ValueError( + f"{pkg} was specified in the requirements but wasn't found in the current environment." + ) + if fetched_ver != pkg_actual_ver: + logger.warning( + f"Version of {pkg} was specified to be {fetched_ver} in the configuration. However, the actual installed version if {pkg_actual_ver}. Things might work unexpected." + ) + hub_kwargs_names = [ "cache_dir", "force_download", @@ -305,16 +354,6 @@ def from_pretrained( ] hub_kwargs = {name: kwargs.pop(name) for name in hub_kwargs_names if name in kwargs} - config = cls.load_config(pretrained_model_name_or_path) - has_remote_code = "auto_map" in config and cls.__name__ in config["auto_map"] - trust_remote_code = resolve_trust_remote_code( - trust_remote_code, pretrained_model_name_or_path, has_remote_code - ) - if not (has_remote_code and trust_remote_code): - raise ValueError( - "Selected model repository does not happear to have any custom code or does not have a valid `config.json` file." - ) - class_ref = config["auto_map"][cls.__name__] module_file, class_name = class_ref.split(".") module_file = module_file + ".py" @@ -340,8 +379,13 @@ def save_pretrained(self, save_directory, push_to_hub=False, **kwargs): module = full_mod.rsplit(".", 1)[-1].replace("__dynamic__", "") parent_module = self.save_pretrained.__func__.__qualname__.split(".", 1)[0] auto_map = {f"{parent_module}": f"{module}.{cls_name}"} - self.register_to_config(auto_map=auto_map) + + # resolve requirements + requirements = self._get_requirements() + if requirements is not None: + self.register_to_config(requirements=requirements) + self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs) config = dict(self.config) self._internal_dict = FrozenDict(config) From 127e9a39d885dea964e054e7335932550e05f51a Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Wed, 20 Aug 2025 11:51:15 +0530 Subject: [PATCH 2/8] up --- src/diffusers/modular_pipelines/modular_pipeline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index e45dc5068a40..5171223a6ff8 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -232,7 +232,7 @@ class ModularPipelineBlocks(ConfigMixin, PushToHubMixin): config_name = "modular_config.json" model_name = None - _requirements: Union[List[Tuple[str, str], Tuple[str, str]]] = None + _requirements: Union[List[Tuple[str, str]], Tuple[str, str]] = None @classmethod def _get_signature_keys(cls, obj): @@ -275,7 +275,7 @@ def _get_required_inputs(self): def _get_requirements(self): if getattr(self, "_requirements", None) is not None: defined_reqs = self._requirements - if not isinstance(defined_reqs): + if not isinstance(defined_reqs, list): defined_reqs = [defined_reqs] final_reqs = [] @@ -290,6 +290,7 @@ def _get_requirements(self): f"Version for {pkg} was specified to be {specified_ver} whereas the actual version found is {pkg_actual_ver}. Ignore if this is not concerning." ) final_reqs.append((pkg, specified_ver)) + return final_reqs else: return None From 37d3887194c4b54c145101a38ad703331aa6a0b8 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Wed, 20 Aug 2025 12:09:33 +0530 Subject: [PATCH 3/8] unify. --- .../modular_pipelines/modular_pipeline.py | 71 +++++++++---------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index 5171223a6ff8..0fdfd8735550 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -272,29 +272,6 @@ def _get_required_inputs(self): return input_names - def _get_requirements(self): - if getattr(self, "_requirements", None) is not None: - defined_reqs = self._requirements - if not isinstance(defined_reqs, list): - defined_reqs = [defined_reqs] - - final_reqs = [] - for pkg, specified_ver in defined_reqs: - pkg_available, pkg_actual_ver = _is_package_available(pkg) - if not pkg_available: - raise ValueError( - f"{pkg} was specified in the requirements but wasn't found. Please check your environment." - ) - if specified_ver != pkg_actual_ver: - logger.warning( - f"Version for {pkg} was specified to be {specified_ver} whereas the actual version found is {pkg_actual_ver}. Ignore if this is not concerning." - ) - final_reqs.append((pkg, specified_ver)) - return final_reqs - - else: - return None - @property def required_inputs(self) -> List[InputParam]: return self._get_required_inputs() @@ -329,19 +306,7 @@ def from_pretrained( ) if "requirements" in config and config["requirements"] is not None: - requirements: Union[List[Tuple[str, str]], Tuple[str, str]] = config["requirements"] - if not isinstance(requirements, list): - requirements = [requirements] - for pkg, fetched_ver in requirements: - pkg_available, pkg_actual_ver = _is_package_available(pkg) - if not pkg_available: - raise ValueError( - f"{pkg} was specified in the requirements but wasn't found in the current environment." - ) - if fetched_ver != pkg_actual_ver: - logger.warning( - f"Version of {pkg} was specified to be {fetched_ver} in the configuration. However, the actual installed version if {pkg_actual_ver}. Things might work unexpected." - ) + _ = _validate_requirements(config["requirements"]) hub_kwargs_names = [ "cache_dir", @@ -383,8 +348,8 @@ def save_pretrained(self, save_directory, push_to_hub=False, **kwargs): self.register_to_config(auto_map=auto_map) # resolve requirements - requirements = self._get_requirements() - if requirements is not None: + requirements = _validate_requirements(getattr(self, "_requirements", None)) + if requirements: self.register_to_config(requirements=requirements) self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs) @@ -2489,3 +2454,33 @@ def __call__(self, state: PipelineState = None, output: Union[str, List[str]] = return state.get(output) else: raise ValueError(f"Output '{output}' is not a valid output type") + + +def _validate_requirements(reqs): + normalized_reqs = _normalize_requirements(reqs) + if not normalized_reqs: + return [] + + final: List[Tuple[str, str]] = [] + for req, specified_ver in normalized_reqs: + req_available, req_actual_ver = _is_package_available(req) + if not req_available: + raise ValueError(f"{req} was specified in the requirements but wasn't found in the current environment.") + if specified_ver != req_actual_ver: + logger.warning( + f"Version of {req} was specified to be {specified_ver} in the configuration. However, the actual installed version if {req_actual_ver}. Things might work unexpected." + ) + + final.append((req, specified_ver)) + + return final + + +def _normalize_requirements(reqs): + if not reqs: + return [] + if isinstance(reqs, tuple) and len(reqs) == 2 and isinstance(reqs[0], str): + req_seq: List[Tuple[str, str]] = [reqs] # single pair + else: + req_seq = reqs + return req_seq From 1de4402c2678febb9db4e8f609981c5341d1018c Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Mon, 27 Oct 2025 13:55:17 +0530 Subject: [PATCH 4/8] up --- .../modular_pipelines/modular_pipeline.py | 42 +++------ .../modular_pipeline_utils.py | 85 +++++++++++++++++++ 2 files changed, 95 insertions(+), 32 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index 8d5a943f458d..02232c7f609e 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -32,7 +32,6 @@ from ..utils import PushToHubMixin, is_accelerate_available, logging from ..utils.dynamic_modules_utils import get_class_from_dynamic_module, resolve_trust_remote_code from ..utils.hub_utils import load_or_create_model_card, populate_model_card -from ..utils.import_utils import _is_package_available from .components_manager import ComponentsManager from .modular_pipeline_utils import ( ComponentSpec, @@ -40,6 +39,7 @@ InputParam, InsertableDict, OutputParam, + _validate_requirements, format_components, format_configs, make_doc_string, @@ -240,7 +240,7 @@ class ModularPipelineBlocks(ConfigMixin, PushToHubMixin): config_name = "modular_config.json" model_name = None - _requirements: Union[List[Tuple[str, str]], Tuple[str, str]] = None + _requirements: Optional[Dict[str, str]] = None @classmethod def _get_signature_keys(cls, obj): @@ -1143,6 +1143,14 @@ def doc(self): expected_configs=self.expected_configs, ) + @property + def _requirements(self) -> Dict[str, str]: + requirements = {} + for block_name, block in self.sub_blocks.items(): + if getattr(block, "_requirements", None): + requirements[block_name] = block._requirements + return requirements + class LoopSequentialPipelineBlocks(ModularPipelineBlocks): """ @@ -2547,33 +2555,3 @@ def __call__(self, state: PipelineState = None, output: Union[str, List[str]] = return state.get(output) else: raise ValueError(f"Output '{output}' is not a valid output type") - - -def _validate_requirements(reqs): - normalized_reqs = _normalize_requirements(reqs) - if not normalized_reqs: - return [] - - final: List[Tuple[str, str]] = [] - for req, specified_ver in normalized_reqs: - req_available, req_actual_ver = _is_package_available(req) - if not req_available: - raise ValueError(f"{req} was specified in the requirements but wasn't found in the current environment.") - if specified_ver != req_actual_ver: - logger.warning( - f"Version of {req} was specified to be {specified_ver} in the configuration. However, the actual installed version if {req_actual_ver}. Things might work unexpected." - ) - - final.append((req, specified_ver)) - - return final - - -def _normalize_requirements(reqs): - if not reqs: - return [] - if isinstance(reqs, tuple) and len(reqs) == 2 and isinstance(reqs[0], str): - req_seq: List[Tuple[str, str]] = [reqs] # single pair - else: - req_seq = reqs - return req_seq diff --git a/src/diffusers/modular_pipelines/modular_pipeline_utils.py b/src/diffusers/modular_pipelines/modular_pipeline_utils.py index b15126868634..50190305be5e 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline_utils.py +++ b/src/diffusers/modular_pipelines/modular_pipeline_utils.py @@ -19,9 +19,11 @@ from typing import Any, Dict, List, Literal, Optional, Type, Union import torch +from packaging.specifiers import InvalidSpecifier, SpecifierSet from ..configuration_utils import ConfigMixin, FrozenDict from ..utils import is_torch_available, logging +from ..utils.import_utils import _is_package_available if is_torch_available(): @@ -670,3 +672,86 @@ def make_doc_string( output += format_output_params(outputs, indent_level=2) return output + + +def _validate_requirements(reqs): + if reqs is None: + normalized_reqs = {} + else: + if not isinstance(reqs, dict): + raise ValueError( + "Requirements must be provided as a dictionary mapping package names to version specifiers." + ) + normalized_reqs = _normalize_requirements(reqs) + + if not normalized_reqs: + return {} + + final: Dict[str, str] = {} + for req, specified_ver in normalized_reqs.items(): + req_available, req_actual_ver = _is_package_available(req) + if not req_available: + logger.warning(f"{req} was specified in the requirements but wasn't found in the current environment.") + + if specified_ver: + try: + specifier = SpecifierSet(specified_ver) + except InvalidSpecifier as err: + raise ValueError(f"Requirement specifier '{specified_ver}' for {req} is invalid.") from err + + if req_actual_ver == "N/A": + logger.warning( + f"Version of {req} could not be determined to validate requirement '{specified_ver}'. Things might work unexpected." + ) + elif not specifier.contains(req_actual_ver, prereleases=True): + logger.warning( + f"{req} requirement '{specified_ver}' is not satisfied by the installed version {req_actual_ver}. Things might work unexpected." + ) + + final[req] = specified_ver + + return final + + +def _normalize_requirements(reqs): + if not reqs: + return {} + + normalized: "OrderedDict[str, str]" = OrderedDict() + + def _accumulate(mapping: Dict[str, Any]): + for pkg, spec in mapping.items(): + if isinstance(spec, dict): + # This is recursive because blocks are composable. This way, we can merge requirements + # from multiple blocks. + _accumulate(spec) + continue + + pkg_name = str(pkg).strip() + if not pkg_name: + raise ValueError("Requirement package name cannot be empty.") + + spec_str = "" if spec is None else str(spec).strip() + if spec_str and not spec_str.startswith(("<", ">", "=", "!", "~")): + spec_str = f"=={spec_str}" + + existing_spec = normalized.get(pkg_name) + if existing_spec is not None: + if not existing_spec and spec_str: + normalized[pkg_name] = spec_str + elif existing_spec and spec_str and existing_spec != spec_str: + try: + combined_spec = SpecifierSet(",".join(filter(None, [existing_spec, spec_str]))) + except InvalidSpecifier: + logger.warning( + f"Conflicting requirements for '{pkg_name}' detected: '{existing_spec}' vs '{spec_str}'. Keeping '{existing_spec}'." + ) + else: + normalized[pkg_name] = str(combined_spec) + continue + + normalized[pkg_name] = spec_str + + _accumulate(reqs) + + return normalized From 7b43d0e4096b811ade3c40280a7438fa2554236e Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Tue, 20 Jan 2026 09:29:32 +0530 Subject: [PATCH 5/8] add tests --- .../test_modular_pipelines_common.py | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/modular_pipelines/test_modular_pipelines_common.py b/tests/modular_pipelines/test_modular_pipelines_common.py index 661fcc253795..35855d39b6ca 100644 --- a/tests/modular_pipelines/test_modular_pipelines_common.py +++ b/tests/modular_pipelines/test_modular_pipelines_common.py @@ -1,4 +1,6 @@ import gc +import json +import os import tempfile from typing import Callable, Union @@ -8,9 +10,16 @@ import diffusers from diffusers import ComponentsManager, ModularPipeline, ModularPipelineBlocks from diffusers.guiders import ClassifierFreeGuidance +from diffusers.modular_pipelines import SequentialPipelineBlocks from diffusers.utils import logging -from ..testing_utils import backend_empty_cache, numpy_cosine_similarity_distance, require_accelerator, torch_device +from ..testing_utils import ( + CaptureLogger, + backend_empty_cache, + numpy_cosine_similarity_distance, + require_accelerator, + torch_device, +) class ModularPipelineTesterMixin: @@ -335,3 +344,53 @@ def test_guider_cfg(self, expected_max_diff=1e-2): assert out_cfg.shape == out_no_cfg.shape max_diff = torch.abs(out_cfg - out_no_cfg).max() assert max_diff > expected_max_diff, "Output with CFG must be different from normal inference" + + +class TestCustomBlockRequirements: + def get_dummy_block_pipe(self): + class DummyBlockOne: + # keep two arbitrary deps so that we can test warnings. + _requirements = {"xyz": ">=0.8.0", "abc": ">=10.0.0"} + + class DummyBlockTwo: + # keep two dependencies that will be available during testing. + _requirements = {"transformers": ">=4.44.0", "diffusers": ">=0.2.0"} + + pipe = SequentialPipelineBlocks.from_blocks_dict( + {"dummy_block_one": DummyBlockOne, "dummy_block_two": DummyBlockTwo} + ) + return pipe + + def test_custom_requirements_save_load(self): + pipe = self.get_dummy_block_pipe() + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + config_path = os.path.join(tmpdir, "modular_config.json") + with open(config_path, "r") as f: + config = json.load(f) + + assert "requirements" in config + requirements = config["requirements"] + + expected_requirements = { + "xyz": ">=0.8.0", + "abc": ">=10.0.0", + "transformers": ">=4.44.0", + "diffusers": ">=0.2.0", + } + assert expected_requirements == requirements + + def test_warnings(self): + pipe = self.get_dummy_block_pipe() + with tempfile.TemporaryDirectory() as tmpdir: + logger = logging.get_logger("diffusers.modular_pipelines.modular_pipeline_utils") + logger.setLevel(30) + + with CaptureLogger(logger) as cap_logger: + pipe.save_pretrained(tmpdir) + + template = "{req} was specified in the requirements but wasn't found in the current environment" + msg_xyz = template.format(req="xyz") + msg_abc = template.format(req="abc") + assert msg_xyz in str(cap_logger.out) + assert msg_abc in str(cap_logger.out) From 29273538d13d036e65de964511a7084d07618550 Mon Sep 17 00:00:00 2001 From: Sayak Paul Date: Tue, 17 Feb 2026 23:27:52 +0530 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Dhruv Nair --- src/diffusers/modular_pipelines/modular_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index d28c42fec5fd..7ff51854f8d9 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -389,7 +389,7 @@ def from_pretrained( trust_remote_code = resolve_trust_remote_code( trust_remote_code, pretrained_model_name_or_path, has_remote_code ) - if not (has_remote_code and trust_remote_code): + if not has_remote_code and trust_remote_code: raise ValueError( "Selected model repository does not happear to have any custom code or does not have a valid `config.json` file." ) From e8d4612a251360ab402f3b1245a14752e399e87e Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Tue, 17 Feb 2026 23:37:04 +0530 Subject: [PATCH 7/8] reviewer feedback. --- .../modular_pipelines/modular_pipeline.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/diffusers/modular_pipelines/modular_pipeline.py b/src/diffusers/modular_pipelines/modular_pipeline.py index 7ff51854f8d9..b95d35a41e36 100644 --- a/src/diffusers/modular_pipelines/modular_pipeline.py +++ b/src/diffusers/modular_pipelines/modular_pipeline.py @@ -384,19 +384,6 @@ def from_pretrained( trust_remote_code: bool = False, **kwargs, ): - config = cls.load_config(pretrained_model_name_or_path) - has_remote_code = "auto_map" in config and cls.__name__ in config["auto_map"] - trust_remote_code = resolve_trust_remote_code( - trust_remote_code, pretrained_model_name_or_path, has_remote_code - ) - if not has_remote_code and trust_remote_code: - raise ValueError( - "Selected model repository does not happear to have any custom code or does not have a valid `config.json` file." - ) - - if "requirements" in config and config["requirements"] is not None: - _ = _validate_requirements(config["requirements"]) - hub_kwargs_names = [ "cache_dir", "force_download", @@ -409,6 +396,19 @@ def from_pretrained( ] hub_kwargs = {name: kwargs.pop(name) for name in hub_kwargs_names if name in kwargs} + config = cls.load_config(pretrained_model_name_or_path, **hub_kwargs) + has_remote_code = "auto_map" in config and cls.__name__ in config["auto_map"] + trust_remote_code = resolve_trust_remote_code( + trust_remote_code, pretrained_model_name_or_path, has_remote_code + ) + if not has_remote_code and trust_remote_code: + raise ValueError( + "Selected model repository does not happear to have any custom code or does not have a valid `config.json` file." + ) + + if "requirements" in config and config["requirements"] is not None: + _ = _validate_requirements(config["requirements"]) + class_ref = config["auto_map"][cls.__name__] module_file, class_name = class_ref.split(".") module_file = module_file + ".py" From f274df4fef2af625098bf225c1b5f25368946ef9 Mon Sep 17 00:00:00 2001 From: Steven Liu <59462357+stevhliu@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:54:13 -0800 Subject: [PATCH 8/8] [docs] validation for custom blocks (#13156) validation --- .../en/modular_diffusers/custom_blocks.md | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/source/en/modular_diffusers/custom_blocks.md b/docs/source/en/modular_diffusers/custom_blocks.md index b412e0e58abc..66e1de172b34 100644 --- a/docs/source/en/modular_diffusers/custom_blocks.md +++ b/docs/source/en/modular_diffusers/custom_blocks.md @@ -332,4 +332,49 @@ Make your custom block work with Mellon's visual interface. See the [Mellon Cust Browse the [Modular Diffusers Custom Blocks](https://huggingface.co/collections/diffusers/modular-diffusers-custom-blocks) collection for inspiration and ready-to-use blocks. - \ No newline at end of file + + +## Dependencies + +Declaring package dependencies in custom blocks prevents runtime import errors later on. Diffusers validates the dependencies and returns a warning if a package is missing or incompatible. + +Set a `_requirements` attribute in your block class, mapping package names to version specifiers. + +```py +from diffusers.modular_pipelines import PipelineBlock + +class MyCustomBlock(PipelineBlock): + _requirements = { + "transformers": ">=4.44.0", + "sentencepiece": ">=0.2.0" + } +``` + +When there are blocks with different requirements, Diffusers merges their requirements. + +```py +from diffusers.modular_pipelines import SequentialPipelineBlocks + +class BlockA(PipelineBlock): + _requirements = {"transformers": ">=4.44.0"} + # ... + +class BlockB(PipelineBlock): + _requirements = {"sentencepiece": ">=0.2.0"} + # ... + +pipe = SequentialPipelineBlocks.from_blocks_dict({ + "block_a": BlockA, + "block_b": BlockB, +}) +``` + +When this block is saved with [`~ModularPipeline.save_pretrained`], the requirements are saved to the `modular_config.json` file. When this block is loaded, Diffusers checks each requirement against the current environment. If there is a mismatch or a package isn't found, Diffusers returns the following warning. + +```md +# missing package +xyz-package was specified in the requirements but wasn't found in the current environment. + +# version mismatch +xyz requirement 'specific-version' is not satisfied by the installed version 'actual-version'. Things might work unexpected. +```