Skip to content

Commit cee1cd6

Browse files
[Remote code] Add functionality to run remote models, schedulers, pipelines (huggingface#5472)
* upload custom remote poc * up * make style * finish * better name * Apply suggestions from code review * Update tests/pipelines/test_pipelines.py * more fixes * remove ipdb * more fixes * fix more * finish tests --------- Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
1 parent 5b448a5 commit cee1cd6

3 files changed

Lines changed: 172 additions & 14 deletions

File tree

src/diffusers/configuration_utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,10 +485,18 @@ def extract_init_dict(cls, config_dict, **kwargs):
485485

486486
# remove attributes from orig class that cannot be expected
487487
orig_cls_name = config_dict.pop("_class_name", cls.__name__)
488-
if orig_cls_name != cls.__name__ and hasattr(diffusers_library, orig_cls_name):
488+
if (
489+
isinstance(orig_cls_name, str)
490+
and orig_cls_name != cls.__name__
491+
and hasattr(diffusers_library, orig_cls_name)
492+
):
489493
orig_cls = getattr(diffusers_library, orig_cls_name)
490494
unexpected_keys_from_orig = cls._get_init_keys(orig_cls) - expected_keys
491495
config_dict = {k: v for k, v in config_dict.items() if k not in unexpected_keys_from_orig}
496+
elif not isinstance(orig_cls_name, str) and not isinstance(orig_cls_name, (list, tuple)):
497+
raise ValueError(
498+
"Make sure that the `_class_name` is of type string or list of string (for custom pipelines)."
499+
)
492500

493501
# remove private attributes
494502
config_dict = {k: v for k, v in config_dict.items() if not k.startswith("_")}

src/diffusers/pipelines/pipeline_utils.py

Lines changed: 111 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
from requests.exceptions import HTTPError
3434
from tqdm.auto import tqdm
3535

36-
import diffusers
37-
3836
from .. import __version__
3937
from ..configuration_utils import ConfigMixin
4038
from ..models.modeling_utils import _LOW_CPU_MEM_USAGE_DEFAULT
@@ -305,13 +303,23 @@ def maybe_raise_or_warn(
305303
)
306304

307305

308-
def get_class_obj_and_candidates(library_name, class_name, importable_classes, pipelines, is_pipeline_module):
306+
def get_class_obj_and_candidates(
307+
library_name, class_name, importable_classes, pipelines, is_pipeline_module, component_name=None, cache_dir=None
308+
):
309309
"""Simple helper method to retrieve class object of module as well as potential parent class objects"""
310+
component_folder = os.path.join(cache_dir, component_name)
311+
310312
if is_pipeline_module:
311313
pipeline_module = getattr(pipelines, library_name)
312314

313315
class_obj = getattr(pipeline_module, class_name)
314316
class_candidates = {c: class_obj for c in importable_classes.keys()}
317+
elif os.path.isfile(os.path.join(component_folder, library_name + ".py")):
318+
# load custom component
319+
class_obj = get_class_from_dynamic_module(
320+
component_folder, module_file=library_name + ".py", class_name=class_name
321+
)
322+
class_candidates = {c: class_obj for c in importable_classes.keys()}
315323
else:
316324
# else we just import it from the library.
317325
library = importlib.import_module(library_name)
@@ -323,19 +331,35 @@ def get_class_obj_and_candidates(library_name, class_name, importable_classes, p
323331

324332

325333
def _get_pipeline_class(
326-
class_obj, config, load_connected_pipeline=False, custom_pipeline=None, cache_dir=None, revision=None
334+
class_obj,
335+
config,
336+
load_connected_pipeline=False,
337+
custom_pipeline=None,
338+
repo_id=None,
339+
hub_revision=None,
340+
class_name=None,
341+
cache_dir=None,
342+
revision=None,
327343
):
328344
if custom_pipeline is not None:
329345
if custom_pipeline.endswith(".py"):
330346
path = Path(custom_pipeline)
331347
# decompose into folder & file
332348
file_name = path.name
333349
custom_pipeline = path.parent.absolute()
350+
elif repo_id is not None:
351+
file_name = f"{custom_pipeline}.py"
352+
custom_pipeline = repo_id
334353
else:
335354
file_name = CUSTOM_PIPELINE_FILE_NAME
336355

337356
return get_class_from_dynamic_module(
338-
custom_pipeline, module_file=file_name, cache_dir=cache_dir, revision=revision
357+
custom_pipeline,
358+
module_file=file_name,
359+
class_name=class_name,
360+
repo_id=repo_id,
361+
cache_dir=cache_dir,
362+
revision=revision if hub_revision is None else hub_revision,
339363
)
340364

341365
if class_obj != DiffusionPipeline:
@@ -383,11 +407,18 @@ def load_sub_model(
383407
variant: str,
384408
low_cpu_mem_usage: bool,
385409
cached_folder: Union[str, os.PathLike],
410+
revision: str = None,
386411
):
387412
"""Helper method to load the module `name` from `library_name` and `class_name`"""
388413
# retrieve class candidates
389414
class_obj, class_candidates = get_class_obj_and_candidates(
390-
library_name, class_name, importable_classes, pipelines, is_pipeline_module
415+
library_name,
416+
class_name,
417+
importable_classes,
418+
pipelines,
419+
is_pipeline_module,
420+
component_name=name,
421+
cache_dir=cached_folder,
391422
)
392423

393424
load_method_name = None
@@ -414,14 +445,15 @@ def load_sub_model(
414445
load_method = getattr(class_obj, load_method_name)
415446

416447
# add kwargs to loading method
448+
diffusers_module = importlib.import_module(__name__.split(".")[0])
417449
loading_kwargs = {}
418450
if issubclass(class_obj, torch.nn.Module):
419451
loading_kwargs["torch_dtype"] = torch_dtype
420-
if issubclass(class_obj, diffusers.OnnxRuntimeModel):
452+
if issubclass(class_obj, diffusers_module.OnnxRuntimeModel):
421453
loading_kwargs["provider"] = provider
422454
loading_kwargs["sess_options"] = sess_options
423455

424-
is_diffusers_model = issubclass(class_obj, diffusers.ModelMixin)
456+
is_diffusers_model = issubclass(class_obj, diffusers_module.ModelMixin)
425457

426458
if is_transformers_available():
427459
transformers_version = version.parse(version.parse(transformers.__version__).base_version)
@@ -501,7 +533,8 @@ class DiffusionPipeline(ConfigMixin, PushToHubMixin):
501533

502534
def register_modules(self, **kwargs):
503535
# import it here to avoid circular import
504-
from diffusers import pipelines
536+
diffusers_module = importlib.import_module(__name__.split(".")[0])
537+
pipelines = getattr(diffusers_module, "pipelines")
505538

506539
for name, module in kwargs.items():
507540
# retrieve library
@@ -1080,11 +1113,21 @@ def from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.P
10801113

10811114
# 3. Load the pipeline class, if using custom module then load it from the hub
10821115
# if we load from explicit class, let's use it
1116+
custom_class_name = None
1117+
if os.path.isfile(os.path.join(cached_folder, f"{custom_pipeline}.py")):
1118+
custom_pipeline = os.path.join(cached_folder, f"{custom_pipeline}.py")
1119+
elif isinstance(config_dict["_class_name"], (list, tuple)) and os.path.isfile(
1120+
os.path.join(cached_folder, f"{config_dict['_class_name'][0]}.py")
1121+
):
1122+
custom_pipeline = os.path.join(cached_folder, f"{config_dict['_class_name'][0]}.py")
1123+
custom_class_name = config_dict["_class_name"][1]
1124+
10831125
pipeline_class = _get_pipeline_class(
10841126
cls,
10851127
config_dict,
10861128
load_connected_pipeline=load_connected_pipeline,
10871129
custom_pipeline=custom_pipeline,
1130+
class_name=custom_class_name,
10881131
cache_dir=cache_dir,
10891132
revision=custom_revision,
10901133
)
@@ -1223,6 +1266,7 @@ def load_module(name, value):
12231266
variant=variant,
12241267
low_cpu_mem_usage=low_cpu_mem_usage,
12251268
cached_folder=cached_folder,
1269+
revision=revision,
12261270
)
12271271
logger.info(
12281272
f"Loaded {name} as {class_name} from `{name}` subfolder of {pretrained_model_name_or_path}."
@@ -1542,6 +1586,10 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
15421586
will never be downloaded. By default `use_onnx` defaults to the `_is_onnx` class attribute which is
15431587
`False` for non-ONNX pipelines and `True` for ONNX pipelines. ONNX weights include both files ending
15441588
with `.onnx` and `.pb`.
1589+
trust_remote_code (`bool`, *optional*, defaults to `False`):
1590+
Whether or not to allow for custom pipelines and components defined on the Hub in their own files. This
1591+
option should only be set to `True` for repositories you trust and in which you have read the code, as
1592+
it will execute code present on the Hub on your local machine.
15451593
15461594
Returns:
15471595
`os.PathLike`:
@@ -1569,6 +1617,7 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
15691617
use_safetensors = kwargs.pop("use_safetensors", None)
15701618
use_onnx = kwargs.pop("use_onnx", None)
15711619
load_connected_pipeline = kwargs.pop("load_connected_pipeline", False)
1620+
trust_remote_code = kwargs.pop("trust_remote_code", False)
15721621

15731622
allow_pickle = False
15741623
if use_safetensors is None:
@@ -1604,15 +1653,34 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
16041653
)
16051654

16061655
config_dict = cls._dict_from_json_file(config_file)
1607-
16081656
ignore_filenames = config_dict.pop("_ignore_files", [])
16091657

16101658
# retrieve all folder_names that contain relevant files
1611-
folder_names = [k for k, v in config_dict.items() if isinstance(v, list)]
1659+
folder_names = [k for k, v in config_dict.items() if isinstance(v, list) and k != "_class_name"]
16121660

16131661
filenames = {sibling.rfilename for sibling in info.siblings}
16141662
model_filenames, variant_filenames = variant_compatible_siblings(filenames, variant=variant)
16151663

1664+
diffusers_module = importlib.import_module(__name__.split(".")[0])
1665+
pipelines = getattr(diffusers_module, "pipelines")
1666+
1667+
# optionally create a custom component <> custom file mapping
1668+
custom_components = {}
1669+
for component in folder_names:
1670+
module_candidate = config_dict[component][0]
1671+
1672+
if module_candidate is None:
1673+
continue
1674+
1675+
candidate_file = os.path.join(component, module_candidate + ".py")
1676+
1677+
if candidate_file in filenames:
1678+
custom_components[component] = module_candidate
1679+
elif module_candidate not in LOADABLE_CLASSES and not hasattr(pipelines, module_candidate):
1680+
raise ValueError(
1681+
f"{candidate_file} as defined in `model_index.json` does not exist in {pretrained_model_name} and is not a module in 'diffusers/pipelines'."
1682+
)
1683+
16161684
if len(variant_filenames) == 0 and variant is not None:
16171685
deprecation_message = (
16181686
f"You are trying to load the model files of the `variant={variant}`, but no such modeling files are available."
@@ -1636,12 +1704,21 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
16361704

16371705
model_folder_names = {os.path.split(f)[0] for f in model_filenames if os.path.split(f)[0] in folder_names}
16381706

1707+
custom_class_name = None
1708+
if custom_pipeline is None and isinstance(config_dict["_class_name"], (list, tuple)):
1709+
custom_pipeline = config_dict["_class_name"][0]
1710+
custom_class_name = config_dict["_class_name"][1]
1711+
16391712
# all filenames compatible with variant will be added
16401713
allow_patterns = list(model_filenames)
16411714

16421715
# allow all patterns from non-model folders
16431716
# this enables downloading schedulers, tokenizers, ...
16441717
allow_patterns += [f"{k}/*" for k in folder_names if k not in model_folder_names]
1718+
# add custom component files
1719+
allow_patterns += [f"{k}/{f}.py" for k, f in custom_components.items()]
1720+
# add custom pipeline file
1721+
allow_patterns += [f"{custom_pipeline}.py"] if f"{custom_pipeline}.py" in filenames else []
16451722
# also allow downloading config.json files with the model
16461723
allow_patterns += [os.path.join(k, "config.json") for k in model_folder_names]
16471724

@@ -1652,12 +1729,32 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
16521729
CUSTOM_PIPELINE_FILE_NAME,
16531730
]
16541731

1732+
load_pipe_from_hub = custom_pipeline is not None and f"{custom_pipeline}.py" in filenames
1733+
load_components_from_hub = len(custom_components) > 0
1734+
1735+
if load_pipe_from_hub and not trust_remote_code:
1736+
raise ValueError(
1737+
f"The repository for {pretrained_model_name} contains custom code in {custom_pipeline}.py which must be executed to correctly "
1738+
f"load the model. You can inspect the repository content at https://hf.co/{pretrained_model_name}/blob/main/{custom_pipeline}.py.\n"
1739+
f"Please pass the argument `trust_remote_code=True` to allow custom code to be run."
1740+
)
1741+
1742+
if load_components_from_hub and not trust_remote_code:
1743+
raise ValueError(
1744+
f"The repository for {pretrained_model_name} contains custom code in {'.py, '.join([os.path.join(k, v) for k,v in custom_components.items()])} which must be executed to correctly "
1745+
f"load the model. You can inspect the repository content at {', '.join([f'https://hf.co/{pretrained_model_name}/{k}/{v}.py' for k,v in custom_components.items()])}.\n"
1746+
f"Please pass the argument `trust_remote_code=True` to allow custom code to be run."
1747+
)
1748+
16551749
# retrieve passed components that should not be downloaded
16561750
pipeline_class = _get_pipeline_class(
16571751
cls,
16581752
config_dict,
16591753
load_connected_pipeline=load_connected_pipeline,
16601754
custom_pipeline=custom_pipeline,
1755+
repo_id=pretrained_model_name if load_pipe_from_hub else None,
1756+
hub_revision=revision,
1757+
class_name=custom_class_name,
16611758
cache_dir=cache_dir,
16621759
revision=custom_revision,
16631760
)
@@ -1754,9 +1851,10 @@ def download(cls, pretrained_model_name, **kwargs) -> Union[str, os.PathLike]:
17541851

17551852
# retrieve pipeline class from local file
17561853
cls_name = cls.load_config(os.path.join(cached_folder, "model_index.json")).get("_class_name", None)
1757-
cls_name = cls_name[4:] if cls_name.startswith("Flax") else cls_name
1854+
cls_name = cls_name[4:] if isinstance(cls_name, str) and cls_name.startswith("Flax") else cls_name
17581855

1759-
pipeline_class = getattr(diffusers, cls_name, None)
1856+
diffusers_module = importlib.import_module(__name__.split(".")[0])
1857+
pipeline_class = getattr(diffusers_module, cls_name, None) if isinstance(cls_name, str) else None
17601858

17611859
if pipeline_class is not None and pipeline_class._load_connected_pipes:
17621860
modelcard = ModelCard.load(os.path.join(cached_folder, "README.md"))

tests/pipelines/test_pipelines.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,58 @@ def test_run_custom_pipeline(self):
862862
# compare output to https://huggingface.co/hf-internal-testing/diffusers-dummy-pipeline/blob/main/pipeline.py#L102
863863
assert output_str == "This is a test"
864864

865+
def test_remote_components(self):
866+
# make sure that trust remote code has to be passed
867+
with self.assertRaises(ValueError):
868+
pipeline = DiffusionPipeline.from_pretrained("hf-internal-testing/tiny-sdxl-custom-components")
869+
870+
# Check that only loading custom componets "my_unet", "my_scheduler" works
871+
pipeline = DiffusionPipeline.from_pretrained(
872+
"hf-internal-testing/tiny-sdxl-custom-components", trust_remote_code=True
873+
)
874+
875+
assert pipeline.config.unet == ("diffusers_modules.local.my_unet_model", "MyUNetModel")
876+
assert pipeline.config.scheduler == ("diffusers_modules.local.my_scheduler", "MyScheduler")
877+
assert pipeline.__class__.__name__ == "StableDiffusionXLPipeline"
878+
879+
pipeline = pipeline.to(torch_device)
880+
images = pipeline("test", num_inference_steps=2, output_type="np")[0]
881+
882+
assert images.shape == (1, 64, 64, 3)
883+
884+
# Check that only loading custom componets "my_unet", "my_scheduler" and explicit custom pipeline works
885+
pipeline = DiffusionPipeline.from_pretrained(
886+
"hf-internal-testing/tiny-sdxl-custom-components", custom_pipeline="my_pipeline", trust_remote_code=True
887+
)
888+
889+
assert pipeline.config.unet == ("diffusers_modules.local.my_unet_model", "MyUNetModel")
890+
assert pipeline.config.scheduler == ("diffusers_modules.local.my_scheduler", "MyScheduler")
891+
assert pipeline.__class__.__name__ == "MyPipeline"
892+
893+
pipeline = pipeline.to(torch_device)
894+
images = pipeline("test", num_inference_steps=2, output_type="np")[0]
895+
896+
assert images.shape == (1, 64, 64, 3)
897+
898+
def test_remote_auto_custom_pipe(self):
899+
# make sure that trust remote code has to be passed
900+
with self.assertRaises(ValueError):
901+
pipeline = DiffusionPipeline.from_pretrained("hf-internal-testing/tiny-sdxl-custom-all")
902+
903+
# Check that only loading custom componets "my_unet", "my_scheduler" and auto custom pipeline works
904+
pipeline = DiffusionPipeline.from_pretrained(
905+
"hf-internal-testing/tiny-sdxl-custom-all", trust_remote_code=True
906+
)
907+
908+
assert pipeline.config.unet == ("diffusers_modules.local.my_unet_model", "MyUNetModel")
909+
assert pipeline.config.scheduler == ("diffusers_modules.local.my_scheduler", "MyScheduler")
910+
assert pipeline.__class__.__name__ == "MyPipeline"
911+
912+
pipeline = pipeline.to(torch_device)
913+
images = pipeline("test", num_inference_steps=2, output_type="np")[0]
914+
915+
assert images.shape == (1, 64, 64, 3)
916+
865917
def test_local_custom_pipeline_repo(self):
866918
local_custom_pipeline_path = get_tests_dir("fixtures/custom_pipeline")
867919
pipeline = DiffusionPipeline.from_pretrained(

0 commit comments

Comments
 (0)