[Download] Resolve the revision once at the beginning of from_pretrained - #14340
Conversation
|
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. |
|
CI status: everything green except Hub tests for models, schedulers, and pipelines, which fails on Green: |
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>
f97fcd2 to
f6fd21f
Compare
sayakpaul
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Could I get an explanation on why this is needed (to pass the revision here)?
There was a problem hiding this comment.
it's not mandatory but doing so the model._diffusers_load_id is strictly the same as before
| 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)) |
There was a problem hiding this comment.
What is the advantage of doing raise_for_status this way?
There was a problem hiding this comment.
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) |
|
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 :) |
Follow-up on
huggingface_hubv1.26.0, which shippedresolve_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 resolvesrevision="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
ResolvedRevisiondown. Since it is astrsubclass whose string value stays the user-facing revision ("main"), nothing else had to change: error messages keep sayingmain, and the download helpers (hf_hub_download,snapshot_download,get_cached_repo_tree) pick up.resolvedtransparently.A single
_resolve_revisionhelper inutils/hub_utils.py, called fromDiffusionPipeline.download,ModelMixin.from_pretrained,AutoModel.from_pretrained, the fourAutoPipelineFor*.from_pretrained,ModularPipeline.from_pretrainedandModularPipelineBlocks.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_fileand textual inversion.load_ip_adaptertoo, because a singlerevisionthere 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
revisionis returned so the download that follows raises its usual, diffusers-flavored error. Checked againstmain: 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:DiffusionPipeline.from_pretrained(tiny-stable-diffusion-torch)AutoPipelineForText2Image.from_pretrained(same repo)ModelMixin.from_pretrained(single-file unet)ModelMixin.from_pretrained(2-shard transformer)AutoModel.from_pretrained(unet subfolder)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
huggingface_hubto1.26.0(from1.23.0) insetup.pyanddependency_versions_table.py.test_kwargs_local_files_onlywrote<sha>hugintorefs/main; that value is now read back byresolve_revisionand passed around as a commit hash, so it has to stay a syntactically valid one.test_local_files_only_with_sharded_checkpointraisedHfHubHTTPError(response=mock.Mock());resolve_revisionreadserror.response.status_codeto tell a Hub outage from a definitive answer, so the mock now carries the real response.🤖 Generated with Claude Code