Skip to content

Commit c2f9647

Browse files
sayakpaulclaude
andcommitted
compute the same snapshot_download patterns offline as online
Instead of returning the cached snapshot folder without validation when offline, compute the same allow/ignore patterns as the online path (sourcing the file listing from the cached snapshot instead of model_info) and let snapshot_download validate the cached snapshot against them, so an interrupted download surfaces hfh's IncompleteSnapshotError instead of failing later at model load time. Also revert the _get_checkpoint_shard_files divergence: its patterns were already identical in both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fd5a5bb commit c2f9647

5 files changed

Lines changed: 116 additions & 89 deletions

File tree

examples/instruct_pix2pix/train_instruct_pix2pix_sdxl.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,9 @@ def log_validation(pipeline, args, accelerator, generator, global_step, is_final
8585
os.makedirs(val_save_dir)
8686

8787
original_image = (
88-
lambda image_url_or_path: (
89-
load_image(image_url_or_path)
90-
if urlparse(image_url_or_path).scheme
91-
else Image.open(image_url_or_path).convert("RGB")
92-
)
88+
lambda image_url_or_path: load_image(image_url_or_path)
89+
if urlparse(image_url_or_path).scheme
90+
else Image.open(image_url_or_path).convert("RGB")
9391
)(args.val_image_url_or_path)
9492

9593
if torch.backends.mps.is_available():

src/diffusers/loaders/peft.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
logger = logging.get_logger(__name__)
4747

4848
_SET_ADAPTER_SCALE_FN_MAPPING = defaultdict(
49-
lambda: lambda model_cls, weights: weights,
49+
lambda: (lambda model_cls, weights: weights),
5050
{
5151
"UNet2DConditionModel": _maybe_expand_lora_scales,
5252
"UNetMotionModel": _maybe_expand_lora_scales,

src/diffusers/pipelines/pipeline_utils.py

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,10 +1662,37 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
16621662
force_download=force_download,
16631663
token=token,
16641664
)
1665+
filenames = {sibling.rfilename for sibling in info.siblings}
1666+
else:
1667+
# We are offline (either the user asked for `local_files_only` or the `model_info` call above
1668+
# failed), so the repo's file listing needed to compute the allow/ignore patterns below cannot be
1669+
# fetched. Source the listing from the cached snapshot instead: it holds exactly the (already
1670+
# filtered) files a previous online call downloaded, so both code paths compute the same patterns
1671+
# and `snapshot_download` validates the cached snapshot against them (catching e.g. a download
1672+
# that was interrupted midway). If the config isn't cached, skip straight to `snapshot_download`
1673+
# below, which raises the appropriate offline not-found error.
1674+
try:
1675+
config_file = hf_hub_download(
1676+
pretrained_model_name,
1677+
cls.config_name,
1678+
cache_dir=cache_dir,
1679+
revision=revision,
1680+
token=token,
1681+
local_files_only=True,
1682+
)
1683+
except LocalEntryNotFoundError:
1684+
config_file = None
1685+
1686+
if config_file is not None:
1687+
snapshot_folder = Path(config_file).parent
1688+
if local_files_only:
1689+
filenames = {
1690+
f.relative_to(snapshot_folder).as_posix() for f in snapshot_folder.rglob("*") if f.is_file()
1691+
}
1692+
16651693
config_dict = cls._dict_from_json_file(config_file)
16661694
ignore_filenames = config_dict.pop("_ignore_files", [])
16671695

1668-
filenames = {sibling.rfilename for sibling in info.siblings}
16691696
if variant is not None and _check_legacy_sharding_variant_format(filenames=filenames, variant=variant):
16701697
warn_msg = (
16711698
f"Warning: The repository contains sharded checkpoints for variant '{variant}' maybe in a deprecated format. "
@@ -1680,9 +1707,11 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
16801707
logger.warning(warn_msg)
16811708

16821709
filenames = set(filenames) - set(ignore_filenames)
1683-
if revision in DEPRECATED_REVISION_ARGS and version.parse(
1684-
version.parse(__version__).base_version
1685-
) >= version.parse("0.22.0"):
1710+
if (
1711+
not local_files_only
1712+
and revision in DEPRECATED_REVISION_ARGS
1713+
and version.parse(version.parse(__version__).base_version) >= version.parse("0.22.0")
1714+
):
16861715
warn_deprecated_model_variant(pretrained_model_name, token, variant, revision, filenames)
16871716

16881717
custom_components, folder_names = _get_custom_components_and_folders(
@@ -1767,40 +1796,20 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
17671796

17681797
# Don't download index files of forbidden patterns either
17691798
ignore_patterns = ignore_patterns + [f"{i}.index.*json" for i in ignore_patterns]
1770-
re_ignore_pattern = [re.compile(fnmatch.translate(p)) for p in ignore_patterns]
1771-
re_allow_pattern = [re.compile(fnmatch.translate(p)) for p in allow_patterns]
17721799

1773-
expected_files = [f for f in filenames if not any(p.match(f) for p in re_ignore_pattern)]
1774-
expected_files = [f for f in expected_files if any(p.match(f) for p in re_allow_pattern)]
1800+
if not local_files_only:
1801+
re_ignore_pattern = [re.compile(fnmatch.translate(p)) for p in ignore_patterns]
1802+
re_allow_pattern = [re.compile(fnmatch.translate(p)) for p in allow_patterns]
17751803

1776-
snapshot_folder = Path(config_file).parent
1777-
pipeline_is_cached = all((snapshot_folder / f).is_file() for f in expected_files)
1804+
expected_files = [f for f in filenames if not any(p.match(f) for p in re_ignore_pattern)]
1805+
expected_files = [f for f in expected_files if any(p.match(f) for p in re_allow_pattern)]
17781806

1779-
if pipeline_is_cached and not force_download:
1780-
# if the pipeline is cached, we can directly return it
1781-
# else call snapshot_download
1782-
return snapshot_folder
1807+
pipeline_is_cached = all((snapshot_folder / f).is_file() for f in expected_files)
17831808

1784-
else:
1785-
# We are offline (either the user asked for `local_files_only` or `model_info` failed above).
1786-
# diffusers only ever caches a filtered subset of a repo (skipping e.g. `.gitattributes`), but
1787-
# newer `huggingface_hub` versions make `snapshot_download` validate that the cached snapshot is
1788-
# complete and raise for the missing files. The resolved config file lives inside the cached
1789-
# snapshot folder, so return that folder directly. If the config isn't cached, fall through to
1790-
# `snapshot_download` below, which raises the not-found error handled together with a failed
1791-
# `model_info` call.
1792-
try:
1793-
config_file = hf_hub_download(
1794-
pretrained_model_name,
1795-
cls.config_name,
1796-
cache_dir=cache_dir,
1797-
revision=revision,
1798-
token=token,
1799-
local_files_only=True,
1800-
)
1801-
return Path(config_file).parent
1802-
except LocalEntryNotFoundError:
1803-
pass
1809+
if pipeline_is_cached and not force_download:
1810+
# if the pipeline is cached, we can directly return it
1811+
# else call snapshot_download
1812+
return snapshot_folder
18041813

18051814
user_agent = {"pipeline_class": cls.__name__}
18061815
if custom_pipeline is not None and not custom_pipeline.endswith(".py"):
@@ -1843,7 +1852,7 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
18431852

18441853
return cached_folder
18451854

1846-
except FileNotFoundError:
1855+
except FileNotFoundError as e:
18471856
# Means we tried to load pipeline with `local_files_only=True` but the files have not been found in local cache.
18481857
# This can happen in two cases:
18491858
# 1. If the user passed `local_files_only=True` => we raise the error directly
@@ -1854,9 +1863,9 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
18541863
else:
18551864
# 2. we forced `local_files_only=True` when `model_info` failed
18561865
raise EnvironmentError(
1857-
f"Cannot load model {pretrained_model_name}: model is not cached locally and an error occurred"
1858-
" while trying to fetch metadata from the Hub. Please check out the root cause in the stacktrace"
1859-
" above."
1866+
f"Cannot load model {pretrained_model_name}: the model is not fully cached locally ({e}) and an"
1867+
" error occurred while trying to fetch metadata from the Hub. Please check out the root cause in"
1868+
" the stacktrace above."
18601869
) from model_info_call_error
18611870

18621871
@classmethod

src/diffusers/utils/hub_utils.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -416,18 +416,14 @@ def _get_checkpoint_shard_files(
416416
return shard_filenames, sharded_metadata
417417

418418
# At this stage pretrained_model_name_or_path is a model identifier on the Hub
419-
if local_files_only:
420-
# We are offline: the index file has already been resolved from the local cache and the shards live
421-
# alongside it, so use that folder directly. Skip `snapshot_download`, whose snapshot-completeness
422-
# check in newer `huggingface_hub` versions raises a less specific error for a missing shard than
423-
# the check below.
424-
cached_folder = os.path.dirname(index_filename)
425-
else:
426-
allow_patterns = original_shard_filenames
427-
if subfolder is not None:
428-
allow_patterns = [os.path.join(subfolder, p) for p in allow_patterns]
419+
allow_patterns = original_shard_filenames
420+
if subfolder is not None:
421+
allow_patterns = [os.path.join(subfolder, p) for p in allow_patterns]
422+
423+
ignore_patterns = ["*.json", "*.md"]
429424

430-
# If the repo doesn't have the required shards, error out early even before downloading anything.
425+
# If the repo doesn't have the required shards, error out early even before downloading anything.
426+
if not local_files_only:
431427
model_files_info = model_info(pretrained_model_name_or_path, revision=revision, token=token)
432428
for shard_file in original_shard_filenames:
433429
shard_file_present = any(shard_file in k.rfilename for k in model_files_info.siblings)
@@ -437,28 +433,29 @@ def _get_checkpoint_shard_files(
437433
"required according to the checkpoint index."
438434
)
439435

440-
try:
441-
# Load from URL
442-
cached_folder = snapshot_download(
443-
pretrained_model_name_or_path,
444-
cache_dir=cache_dir,
445-
proxies=proxies,
446-
token=token,
447-
revision=revision,
448-
allow_patterns=allow_patterns,
449-
ignore_patterns=["*.json", "*.md"],
450-
user_agent=user_agent,
451-
)
452-
if subfolder is not None:
453-
cached_folder = os.path.join(cached_folder, subfolder)
454-
455-
# We have already dealt with RepositoryNotFoundError and RevisionNotFoundError when getting the index,
456-
# so we don't have to catch them here. We have also dealt with EntryNotFoundError.
457-
except HfHubHTTPError as e:
458-
raise EnvironmentError(
459-
f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load {pretrained_model_name_or_path}. You should try"
460-
" again after checking your internet connection."
461-
) from e
436+
try:
437+
# Load from URL
438+
cached_folder = snapshot_download(
439+
pretrained_model_name_or_path,
440+
cache_dir=cache_dir,
441+
proxies=proxies,
442+
local_files_only=local_files_only,
443+
token=token,
444+
revision=revision,
445+
allow_patterns=allow_patterns,
446+
ignore_patterns=ignore_patterns,
447+
user_agent=user_agent,
448+
)
449+
if subfolder is not None:
450+
cached_folder = os.path.join(cached_folder, subfolder)
451+
452+
# We have already dealt with RepositoryNotFoundError and RevisionNotFoundError when getting the index, so
453+
# we don't have to catch them here. We have also dealt with EntryNotFoundError.
454+
except HfHubHTTPError as e:
455+
raise EnvironmentError(
456+
f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load {pretrained_model_name_or_path}. You should try"
457+
" again after checking your internet connection."
458+
) from e
462459

463460
cached_filenames = [os.path.join(cached_folder, f) for f in original_shard_filenames]
464461
for cached_file in cached_filenames:

tests/pipelines/test_pipelines.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import unittest
2626
import unittest.mock as mock
2727
import warnings
28+
from pathlib import Path
2829

2930
import numpy as np
3031
import PIL.Image
@@ -504,28 +505,50 @@ def test_local_files_only_are_used_when_no_internet(self):
504505
if p1.data.ne(p2.data).sum() > 0:
505506
assert False, "Parameters not the same!"
506507

507-
def test_local_files_only_returns_cached_snapshot_without_snapshot_download(self):
508+
def test_local_files_only_uses_same_snapshot_download_patterns(self):
508509
# diffusers downloads a filtered subset of a repo (skipping e.g. `.gitattributes`). Newer
509-
# `huggingface_hub` versions validate that a cached snapshot is complete under
510-
# `local_files_only=True`, so an offline `snapshot_download(allow_patterns=None)` would wrongly
511-
# expect the whole repo. The offline path must instead compute `allow_patterns` from the local
512-
# cache and return the cached snapshot directly. See
513-
# https://github.com/huggingface/diffusers/issues/14117
510+
# `huggingface_hub` versions validate that a cached snapshot contains every file matching the
511+
# requested patterns under `local_files_only=True`, so an offline
512+
# `snapshot_download(allow_patterns=None)` would wrongly expect the whole repo. The offline path
513+
# must compute the same patterns as the online one (from the cached snapshot instead of
514+
# `model_info`). See https://github.com/huggingface/diffusers/issues/14117
514515
with tempfile.TemporaryDirectory() as tmpdirname:
515-
cached_folder = DiffusionPipeline.download(
516-
"hf-internal-testing/tiny-stable-diffusion-torch", cache_dir=tmpdirname
517-
)
516+
with mock.patch(
517+
"diffusers.pipelines.pipeline_utils.snapshot_download", side_effect=snapshot_download
518+
) as mock_snapshot_download:
519+
cached_folder = DiffusionPipeline.download(
520+
"hf-internal-testing/tiny-stable-diffusion-torch", cache_dir=tmpdirname
521+
)
522+
online_kwargs = mock_snapshot_download.call_args.kwargs
518523

519-
with mock.patch("diffusers.pipelines.pipeline_utils.snapshot_download") as mock_snapshot_download:
520524
offline_folder = DiffusionPipeline.download(
521525
"hf-internal-testing/tiny-stable-diffusion-torch",
522526
cache_dir=tmpdirname,
523527
local_files_only=True,
524528
)
529+
offline_kwargs = mock_snapshot_download.call_args.kwargs
525530

526-
# A fully cached pipeline returns early; `snapshot_download` must not be called at all.
527-
mock_snapshot_download.assert_not_called()
528531
assert os.path.samefile(offline_folder, cached_folder)
532+
assert set(offline_kwargs["allow_patterns"]) == set(online_kwargs["allow_patterns"])
533+
assert set(offline_kwargs["ignore_patterns"]) == set(online_kwargs["ignore_patterns"])
534+
535+
@require_hf_hub_version_greater("1.21.0")
536+
def test_local_files_only_raises_for_snapshot_with_missing_weights(self):
537+
# An interrupted download leaves a cached snapshot without some weights; loading it offline must
538+
# surface `huggingface_hub`'s incomplete-snapshot error instead of failing later at model load time.
539+
with tempfile.TemporaryDirectory() as tmpdirname:
540+
cached_folder = DiffusionPipeline.download(
541+
"hf-internal-testing/tiny-stable-diffusion-torch", cache_dir=tmpdirname
542+
)
543+
for weights_file in Path(cached_folder).glob("unet/diffusion_pytorch_model*"):
544+
weights_file.unlink()
545+
546+
with self.assertRaisesRegex(OSError, "incomplete"):
547+
DiffusionPipeline.download(
548+
"hf-internal-testing/tiny-stable-diffusion-torch",
549+
cache_dir=tmpdirname,
550+
local_files_only=True,
551+
)
529552

530553
def test_download_from_variant_folder(self):
531554
for use_safetensors in [False, True]:

0 commit comments

Comments
 (0)