Skip to content

[Download] Resolve the revision once at the beginning of from_pretrained - #14340

Merged
sayakpaul merged 4 commits into
huggingface:mainfrom
Wauplin:use-resolve-revision
Aug 28, 2026
Merged

[Download] Resolve the revision once at the beginning of from_pretrained#14340
sayakpaul merged 4 commits into
huggingface:mainfrom
Wauplin:use-resolve-revision

Conversation

@Wauplin

@Wauplin Wauplin commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Follow-up on huggingface_hub v1.26.0, which shipped resolve_revision / ResolvedRevision. Same change as vllm-project/vllm#49990, which has been running in production since.

What

Loading a model or a pipeline fetches several files from the same repo one by one (model_index.json, config.json, the weight index, each checkpoint shard, custom code, ...). Each of those calls resolves revision="main" into a commit hash on its own — one HTTP call per file, and no guarantee that two calls land on the same commit if the repo is updated in between.

This PR resolves the revision once, at the top of the loading entrypoints, and passes the resulting ResolvedRevision down. Since it is a str subclass whose string value stays the user-facing revision ("main"), nothing else had to change: error messages keep saying main, and the download helpers (hf_hub_download, snapshot_download, get_cached_repo_tree) pick up .resolved transparently.

A single _resolve_revision helper in utils/hub_utils.py, called from DiffusionPipeline.download, ModelMixin.from_pretrained, AutoModel.from_pretrained, the four AutoPipelineFor*.from_pretrained, ModularPipeline.from_pretrained and ModularPipelineBlocks.from_pretrained.

Left out, since they fetch a single file from the repo and resolving would only add a request: ConfigMixin.load_config / SchedulerMixin.from_pretrained, _fetch_state_dict (LoRA), from_single_file and textual inversion. load_ip_adapter too, because a single revision there can span several repos.

The helper is best-effort by design: local folders are returned untouched and, if the Hub can't answer (repo/revision not found, offline with an empty cache, invalid repo id), the original revision is returned so the download that follows raises its usual, diffusers-flavored error. Checked against main: identical error messages for a bad revision, a missing repo and an empty offline cache, on all three entrypoints.

Measurements

HTTP calls (telemetry excluded), counted by wrapping httpx.Client.send:

cold cache warm cache
DiffusionPipeline.from_pretrained (tiny-stable-diffusion-torch) 34 → 34 3 → 2
AutoPipelineForText2Image.from_pretrained (same repo) 35 → 33 5 → 2
ModelMixin.from_pretrained (single-file unet) 6 → 7 4 → 3
ModelMixin.from_pretrained (2-shard transformer) 10 → 10 4 → 2
AutoModel.from_pretrained (unet subfolder) 9 → 10 6 → 3

The win is on reuse; a cold single-file load pays one extra repo_info. The commit-consistency guarantee applies in all cases.

Also in this PR

  • Bumped the minimum huggingface_hub to 1.26.0 (from 1.23.0) in setup.py and dependency_versions_table.py.
  • Two test fixes made necessary by the new code path:
    • test_kwargs_local_files_only wrote <sha>hug into refs/main; that value is now read back by resolve_revision and passed around as a commit hash, so it has to stay a syntactically valid one.
    • test_local_files_only_with_sharded_checkpoint raised HfHubHTTPError(response=mock.Mock()); resolve_revision reads error.response.status_code to tell a Hub outage from a definitive answer, so the mock now carries the real response.

🤖 Generated with Claude Code

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@Wauplin

Wauplin commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: everything green except Hub tests for models, schedulers, and pipelines, which fails on TestModelPushToHub::test_push_to_hub / test_push_to_hub_in_organization with 504 Gateway Timeout from hub-ci on /api/repos/create and /api/repos/delete. Unrelated to this PR — push_to_hub never goes through resolve_revision, and the same two tests time out the same way on other branches (e.g. run 30535166896, branch lora-tests-migration-pipelines, which predates this PR). Re-running the job kept the same signature.

Green: check_code_quality, check_repository_consistency, check_auto_docs, check_dependencies, check_torch_dependencies, Fast PyTorch Pipeline / Models & Schedulers / Modular Pipeline CPU tests, PyTorch Example CPU tests, Torch CUDA Tests (models, others, schedulers, lora), Torch Pipelines CUDA Tests, doc build.

Use `huggingface_hub.resolve_revision` (new in huggingface_hub 1.26.0) at the
top of the loading entrypoints so that every file fetched afterwards is pinned
to the same commit and can be served from the cache without re-resolving the
revision on each call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Wauplin
Wauplin force-pushed the use-resolve-revision branch from f97fcd2 to f6fd21f Compare August 27, 2026 13:40

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, can merge pretty soon. Just some questions.

model = model_cls.from_pretrained(pretrained_model_or_path, **kwargs)

load_id_kwargs = {"pretrained_model_name_or_path": pretrained_model_or_path, **kwargs}
# the load id records the revision the user asked for, not the commit it was resolved to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could I get an explanation on why this is needed (to pass the revision here)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not mandatory but doing so the model._diffusers_load_id is strictly the same as before

Comment on lines -122 to +125
error_response = mock.Mock(
status_code=500,
headers={},
raise_for_status=mock.Mock(side_effect=HfHubHTTPError("Server down", response=mock.Mock())),
json=mock.Mock(return_value={}),
)
error_response = mock.Mock(status_code=500, headers={}, json=mock.Mock(return_value={}))
# `resolve_revision` inspects `error.response.status_code` to tell a Hub outage from a definitive answer,
# so the raised error has to carry the response itself.
error_response.raise_for_status = mock.Mock(side_effect=HfHubHTTPError("Server down", response=error_response))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of doing raise_for_status this way?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because before we were doing response=mock.Mock() in the raise_for_status mock. Not a problem because the response was never read anyway but now that it is, we need to make sure the response mock is properly passed to the error mock. Another solution would have been to do

        error_response = mock.Mock(
            status_code=500,
            headers={},
            raise_for_status=mock.Mock(side_effect=HfHubHTTPError("Server down", response=mock.Mock(status_code=500))),
            json=mock.Mock(return_value={}),
        )

but that created 2 mocks for the same logical thing (the "response mock")

Still fine for me to revert, would you prefer that?

# `refs/main` is read back by `resolve_revision` and passed around as a commit hash.
commit_id = tmpdirname.name
new_commit_id = commit_id + "hug"
new_commit_id = "0" * len(commit_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

@Wauplin

Wauplin commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @sayakpaul , thanks for the review! I've answered the questions above. I'm fine with making some changes if you prefer to, otherwise I think we're good :)

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work!

@sayakpaul
sayakpaul merged commit c1bf18c into huggingface:main Aug 28, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants