Skip to content

Commit a50c7a2

Browse files
sayakpaulclaude
andauthored
align snapshot_download to respect hfh latest version (#14118)
* align snapshot_download to respect hfh latest version * fix * 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> * match missing-shard test on the shard filename huggingface_hub>=1.22.0 reports a missing cached shard via IncompleteSnapshotError (repo-relative path) before diffusers' own cached-folder check (absolute path) runs, so assert on the shard's filename, which both messages contain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * use get_cached_repo_tree * remove the requirement decorator from tests * force_download=True --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1aadc65 commit a50c7a2

5 files changed

Lines changed: 97 additions & 14 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"ftfy",
104104
"hf-doc-builder>=0.3.0",
105105
"httpx<1.0.0",
106-
"huggingface-hub>=0.34.0,<2.0",
106+
"huggingface-hub>=1.23.0,<2.0",
107107
"requests-mock==1.10.0",
108108
"importlib_metadata",
109109
"invisible-watermark>=0.2.0",

src/diffusers/dependency_versions_table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"ftfy": "ftfy",
1111
"hf-doc-builder": "hf-doc-builder>=0.3.0",
1212
"httpx": "httpx<1.0.0",
13-
"huggingface-hub": "huggingface-hub>=0.34.0,<2.0",
13+
"huggingface-hub": "huggingface-hub>=1.23.0,<2.0",
1414
"requests-mock": "requests-mock==1.10.0",
1515
"importlib_metadata": "importlib_metadata",
1616
"invisible-watermark": "invisible-watermark>=0.2.0",

src/diffusers/pipelines/pipeline_utils.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@
3333
DDUFEntry,
3434
ModelCard,
3535
create_repo,
36+
get_cached_repo_tree,
3637
hf_hub_download,
3738
model_info,
3839
read_dduf_file,
3940
snapshot_download,
4041
)
41-
from huggingface_hub.utils import HfHubHTTPError, OfflineModeIsEnabled, validate_hf_hub_args
42+
from huggingface_hub.errors import CachedRepoTreeNotFoundError
43+
from huggingface_hub.utils import (
44+
HfHubHTTPError,
45+
LocalEntryNotFoundError,
46+
OfflineModeIsEnabled,
47+
validate_hf_hub_args,
48+
)
4249
from packaging import version
4350
from tqdm.auto import tqdm
4451
from typing_extensions import Self
@@ -1658,10 +1665,35 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
16581665
force_download=force_download,
16591666
token=token,
16601667
)
1668+
filenames = {sibling.rfilename for sibling in info.siblings}
1669+
else:
1670+
# We are offline (either the user asked for `local_files_only` or the `model_info` call above
1671+
# failed), so the repo's file listing needed to compute the allow/ignore patterns below cannot be
1672+
# fetched from the Hub. Read it from the local cache with `get_cached_repo_tree` instead: it returns
1673+
# the file listing that a previous online call cached, so both code paths compute the same patterns.
1674+
# If nothing is cached for this repo, skip straight to `snapshot_download` below, which raises the
1675+
# appropriate offline not-found error.
1676+
try:
1677+
config_file = hf_hub_download(
1678+
pretrained_model_name,
1679+
cls.config_name,
1680+
cache_dir=cache_dir,
1681+
revision=revision,
1682+
token=token,
1683+
local_files_only=True,
1684+
)
1685+
filenames = {
1686+
f.path for f in get_cached_repo_tree(pretrained_model_name, revision=revision, cache_dir=cache_dir)
1687+
}
1688+
except (LocalEntryNotFoundError, CachedRepoTreeNotFoundError):
1689+
config_file = None
1690+
1691+
if config_file is not None:
1692+
snapshot_folder = Path(config_file).parent
1693+
16611694
config_dict = cls._dict_from_json_file(config_file)
16621695
ignore_filenames = config_dict.pop("_ignore_files", [])
16631696

1664-
filenames = {sibling.rfilename for sibling in info.siblings}
16651697
if variant is not None and _check_legacy_sharding_variant_format(filenames=filenames, variant=variant):
16661698
warn_msg = (
16671699
f"Warning: The repository contains sharded checkpoints for variant '{variant}' maybe in a deprecated format. "
@@ -1676,9 +1708,11 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
16761708
logger.warning(warn_msg)
16771709

16781710
filenames = set(filenames) - set(ignore_filenames)
1679-
if revision in DEPRECATED_REVISION_ARGS and version.parse(
1680-
version.parse(__version__).base_version
1681-
) >= version.parse("0.22.0"):
1711+
if (
1712+
not local_files_only
1713+
and revision in DEPRECATED_REVISION_ARGS
1714+
and version.parse(version.parse(__version__).base_version) >= version.parse("0.22.0")
1715+
):
16821716
warn_deprecated_model_variant(pretrained_model_name, token, variant, revision, filenames)
16831717

16841718
custom_components, folder_names = _get_custom_components_and_folders(
@@ -1769,7 +1803,6 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
17691803
expected_files = [f for f in filenames if not any(p.match(f) for p in re_ignore_pattern)]
17701804
expected_files = [f for f in expected_files if any(p.match(f) for p in re_allow_pattern)]
17711805

1772-
snapshot_folder = Path(config_file).parent
17731806
pipeline_is_cached = all((snapshot_folder / f).is_file() for f in expected_files)
17741807

17751808
if pipeline_is_cached and not force_download:
@@ -1818,7 +1851,7 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
18181851

18191852
return cached_folder
18201853

1821-
except FileNotFoundError:
1854+
except FileNotFoundError as e:
18221855
# Means we tried to load pipeline with `local_files_only=True` but the files have not been found in local cache.
18231856
# This can happen in two cases:
18241857
# 1. If the user passed `local_files_only=True` => we raise the error directly
@@ -1829,9 +1862,9 @@ def download(cls, pretrained_model_name, **kwargs) -> str | os.PathLike:
18291862
else:
18301863
# 2. we forced `local_files_only=True` when `model_info` failed
18311864
raise EnvironmentError(
1832-
f"Cannot load model {pretrained_model_name}: model is not cached locally and an error occurred"
1833-
" while trying to fetch metadata from the Hub. Please check out the root cause in the stacktrace"
1834-
" above."
1865+
f"Cannot load model {pretrained_model_name}: the model is not fully cached locally ({e}) and an"
1866+
" error occurred while trying to fetch metadata from the Hub. Please check out the root cause in"
1867+
" the stacktrace above."
18351868
) from model_info_call_error
18361869

18371870
@classmethod

tests/models/test_modeling_common.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@ def test_local_files_only_with_sharded_checkpoint(self):
161161
repo_id, subfolder="transformer", cache_dir=tmpdir, local_files_only=True
162162
)
163163

164-
# Verify error mentions the missing shard
164+
# Verify error mentions the missing shard. `huggingface_hub>=1.22.0` reports it via
165+
# `IncompleteSnapshotError` (repo-relative path), older versions via diffusers' own check on the
166+
# cached folder (absolute path), so match on the shard's filename.
165167
error_msg = str(context.value)
166-
assert cached_shard_file in error_msg or "required according to the checkpoint index" in error_msg, (
168+
assert os.path.basename(cached_shard_file) in error_msg, (
167169
f"Expected error about missing shard, got: {error_msg}"
168170
)
169171

tests/pipelines/test_pipelines.py

Lines changed: 48 additions & 0 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,6 +505,53 @@ 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

508+
def test_local_files_only_uses_same_snapshot_download_patterns(self):
509+
# diffusers downloads a filtered subset of a repo (skipping e.g. `.gitattributes`). Newer
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
515+
with tempfile.TemporaryDirectory() as tmpdirname:
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
523+
524+
# `force_download=True` skips the fully-cached early return so `snapshot_download` runs
525+
# offline and validates the computed patterns against the cached snapshot.
526+
offline_folder = DiffusionPipeline.download(
527+
"hf-internal-testing/tiny-stable-diffusion-torch",
528+
cache_dir=tmpdirname,
529+
local_files_only=True,
530+
force_download=True,
531+
)
532+
offline_kwargs = mock_snapshot_download.call_args.kwargs
533+
534+
assert os.path.samefile(offline_folder, cached_folder)
535+
assert set(offline_kwargs["allow_patterns"]) == set(online_kwargs["allow_patterns"])
536+
assert set(offline_kwargs["ignore_patterns"]) == set(online_kwargs["ignore_patterns"])
537+
538+
def test_local_files_only_raises_for_snapshot_with_missing_weights(self):
539+
# An interrupted download leaves a cached snapshot without some weights; loading it offline must
540+
# surface `huggingface_hub`'s incomplete-snapshot error instead of failing later at model load time.
541+
with tempfile.TemporaryDirectory() as tmpdirname:
542+
cached_folder = DiffusionPipeline.download(
543+
"hf-internal-testing/tiny-stable-diffusion-torch", cache_dir=tmpdirname
544+
)
545+
for weights_file in Path(cached_folder).glob("unet/diffusion_pytorch_model*"):
546+
weights_file.unlink()
547+
548+
with self.assertRaisesRegex(OSError, "incomplete"):
549+
DiffusionPipeline.download(
550+
"hf-internal-testing/tiny-stable-diffusion-torch",
551+
cache_dir=tmpdirname,
552+
local_files_only=True,
553+
)
554+
507555
def test_download_from_variant_folder(self):
508556
for use_safetensors in [False, True]:
509557
other_format = ".bin" if use_safetensors else ".safetensors"

0 commit comments

Comments
 (0)