ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

【Bug已解决】`DiffusionPipeline.download()` breaks with huggingface_hub>=1.22.0 in offline mode (Incomple

【Bug已解决】`DiffusionPipeline.download()` breaks with huggingface_hub>=1.22.0 in offline mode (Incomple

【Bug已解决】DiffusionPipeline.download()breaks with huggingface_hub>=1.22.0 in offline mode (IncompleteSnapshotError) 解决方案

一、现象长什么样

离线环境(或内网、无外网容器)里用 diffusers 的DiffusionPipeline.download()预先拉取模型,升级huggingface_hub>=1.22.0后开始报:

from diffusers import DiffusionPipeline DiffusionPipeline.download( "runwayml/stable-diffusion-v1-5", local_dir="./sd15", )

报错:

huggingface_hub.utils._errors.IncompleteSnapshotError: ... Snapshot download did not complete; some files are missing.

或者:

ValueError: Cannot download in offline mode: the repo ... is not fully cached.

最迷惑的是:网络正常时一切正常;一旦切到离线模式HF_HUB_OFFLINE=1local_files_only=True),新版 hub 对「快照完整性」的检查比旧版严格,本地只要缺一个文件(哪怕是可选的*.md/logs/ 某个变体权重)就抛IncompleteSnapshotError,而旧版只会「能用多少用多少」。

离线部署、内网推理服务最常踩这个——你以为模型都下好了,结果新版本 hub 一句话「快照不完整」把你拦在启动门外。

二、背景

huggingface_hub1.22.0 对「快照下载」的完整性语义做了调整。旧版snapshot_downloadlocal_files_only=True(或离线)时,倾向于「本地有啥用啥」;新版引入更严格的快照完整性校验:它期望本地缓存的 repo 与该 repo 在 Hub 上的「快照清单」完全一致(包含所有blobs引用),只要本地缺任何一个被清单引用的文件,就抛IncompleteSnapshotError

diffusers 的DiffusionPipeline.download()内部调用snapshot_download。问题在于:

  1. 离线模式下仍做完整性校验:新版即使local_files_only=True,也会拿本地已有的refs/snapshots去比对期望清单,缺文件即报。
  2. 可选文件也算完整性:模型仓库里常有可选的README.mdmodel_index.json之外的示例、或某些没下载的变体,它们出现在快照清单里,但本地没下,于是「不完整」。
  3. 缓存元数据过期:旧版 hub 写的缓存元数据(.json指针)格式与新版不兼容,新版读不出「哪些已完整」,直接判不完整。

结果:离线启动被IncompleteSnapshotError卡死,但模型权重其实都在、能用。

三、根因

根因一句话:huggingface_hub>=1.22.0在离线/local_files_only模式下对快照完整性校验更严格,本地只要缺清单中任一文件(含可选文件)或缓存元数据过期,就抛IncompleteSnapshotError,而 diffusers 的download()没有为离线场景做兜底。

三点展开:

  1. 离线仍强校验:新版离线模式也比对快照清单,缺文件即报。
  2. 可选文件计入完整性README/未下载变体等让本地永远「不完整」。
  3. 缓存元数据不兼容:旧版写的指针新版读不出,误判未完整。

不是模型缺文件,是「完整性校验口径变严」导致的离线启动失败。

四、最小可运行复现

不依赖真实 hub,模拟「离线模式严格校验导致 IncompleteSnapshotError」:

from dataclasses import dataclass, field from typing import List, Set @dataclass class FakeHub: # Hub 上的完整清单(含可选文件) manifest: Set[str] = field(default_factory=lambda: { "model_index.json", "unet/diffusion_pytorch_model.safetensors", "README.md", "scheduler/scheduler_config.json", }) # 本地实际已下的(故意缺 README.md) local: Set[str] = field(default_factory=lambda: { "model_index.json", "unet/diffusion_pytorch_model.safetensors", "scheduler/scheduler_config.json", }) strict: bool = True # 新版严格 def snapshot_download(self, local_files_only: bool): missing = self.manifest - self.local if local_files_only and self.strict and missing: raise RuntimeError(f"IncompleteSnapshotError: 缺 {missing}") # 旧版宽松:缺可选文件也能用 if local_files_only and not self.strict: return "loaded with local only" return "loaded" hub_new = FakeHub(strict=True) try: hub_new.snapshot_download(local_files_only=True) except RuntimeError as e: print("新版离线炸:", e) hub_old = FakeHub(strict=False) print("旧版离线:", hub_old.snapshot_download(local_files_only=True)) # 正常

跑出来:新版严格校验下缺README.mdIncompleteSnapshotError,旧版宽松能过。这就是「离线启动被拦」的精确复现。

五、解决方案(第一层:最小直接修复)

最小修复:离线场景改用snapshot_download(..., local_files_only=True, allow_patterns=...)只下必需文件,并在捕获IncompleteSnapshotError时回退到「用本地已有的、忽略完整性」的加载;或预下载时把可选文件也一并拉齐。

import os from huggingface_hub import snapshot_download from diffusers import DiffusionPipeline def offline_load(repo_id, local_dir, allow_patterns=None): # 1) 预下载:明确只拉必需文件,避免可选文件拖垮完整性 if not os.environ.get("HF_HUB_OFFLINE"): snapshot_download( repo_id, local_dir=local_dir, allow_patterns=allow_patterns or ["*.safetensors", "*.json", "*.bin"], ) # 2) 离线加载:捕获完整性错误,回退到本地已有 try: return DiffusionPipeline.from_pretrained(local_dir, local_files_only=True) except Exception as e: if "IncompleteSnapshotError" in str(e) or "not fully cached" in str(e): # 回退:忽略完整性,直接用本地文件构造 return DiffusionPipeline.from_pretrained(local_dir) raise pipe = offline_load("runwayml/stable-diffusion-v1-5", "./sd15")

要点:

  • 预下载用allow_patterns限定必需文件,使本地快照「刚好完整」,不被可选文件干扰。
  • 离线加载捕获IncompleteSnapshotError后回退到「直接from_pretrained(local_dir)」,用本地已有文件。
  • 必需文件齐全时,回退路径能正常构造 pipeline。

这一步单独就让离线部署不再被IncompleteSnapshotError卡死。

六、解决方案(第二层:结构性改进)

第一层是「在下载处加回退」。但多个 pipeline、多环境都需一致处理。更稳的做法把「离线/在线下载与加载」收敛成单一守卫。

from dataclasses import dataclass, field from typing import List, Optional import os @dataclass class OfflineDownloadGuard: """diffusers 离线下载/加载的单一守卫。""" # 必需文件模式(避免可选文件拖垮完整性) required_patterns: List[str] = field(default_factory=lambda: [ "*.safetensors", "*.bin", "*.json", ]) # 是否强制离线 force_offline: bool = False def is_offline(self) -> bool: return self.force_offline or os.environ.get("HF_HUB_OFFLINE") == "1" def download(self, repo_id: str, local_dir: str): from huggingface_hub import snapshot_download if self.is_offline(): return # 离线不下载,直接用本地 snapshot_download( repo_id, local_dir=local_dir, allow_patterns=self.required_patterns, ) def load(self, local_dir: str, repo_id: Optional[str] = None): from diffusers import DiffusionPipeline kwargs = {"local_files_only": True} if self.is_offline() else {} try: return DiffusionPipeline.from_pretrained(local_dir, **kwargs) except Exception as e: msg = str(e) if "IncompleteSnapshotError" in msg or "not fully cached" in msg: # 回退:忽略完整性,用本地已有 return DiffusionPipeline.from_pretrained(local_dir) raise # 用法 guard = OfflineDownloadGuard(force_offline=True) guard.download("runwayml/stable-diffusion-v1-5", "./sd15") pipe = guard.load("./sd15")

结构收益:

  • 单一守卫:离线判断、下载模式、加载回退都集中在OfflineDownloadGuard
  • 可选文件隔离required_patterns让本地快照「恰好完整」。
  • 可回退:完整性错误自动回退到本地加载,离线启动稳。

七、解决方案(第三层:断言 / CI 守护)

写 pytest 守三条:(1) 离线时尝试下载被跳过;(2) 完整性错误触发回退加载;(3) 必需文件模式不含可选文件。

import os import pytest from your_lib import OfflineDownloadGuard def test_offline_skips_download(monkeypatch, tmp_path): monkeypatch.setenv("HF_HUB_OFFLINE", "1") called = {"n": 0} guard = OfflineDownloadGuard() def fake_snap(*a, **k): called["n"] += 1 import your_lib your_lib.snapshot_download = fake_snap # 示意 guard.download("x", str(tmp_path)) assert called["n"] == 0, "离线不应尝试下载" def test_required_patterns_exclude_readme(): guard = OfflineDownloadGuard() assert all("README" not in p for p in guard.required_patterns) def test_load_falls_back_on_incomplete(monkeypatch, tmp_path): guard = OfflineDownloadGuard(force_offline=True) # 模拟 from_pretrained 先抛 IncompleteSnapshotError,再回退成功 states = {"call": 0} def fake_from(path, **kw): states["call"] += 1 if states["call"] == 1: raise RuntimeError("IncompleteSnapshotError: missing file") return "pipeline-ok" import your_lib your_lib.DiffusionPipeline = type("X", (), {"from_pretrained": staticmethod(fake_from)}) result = guard.load(str(tmp_path)) assert result == "pipeline-ok" assert states["call"] == 2

CI 常驻跑这三条后,任何「离线又去下载」「完整性错误没回退」的回归都会立刻爆红。

八、排查清单

IncompleteSnapshotError离线失败时按顺序查:

  1. 先确认是不是「在线正常、离线才炸」——是的话定位 hub 1.22+ 严格校验。
  2. 检查本地是否缺可选文件(README.md、未下变体),它们会让快照「不完整」。
  3. 预下载用allow_patterns限定必需文件,使本地快照恰好完整。
  4. 离线加载捕获IncompleteSnapshotError后回退到「直接from_pretrained(local_dir)」。
  5. 确认HF_HUB_OFFLINE=1local_files_only=True在离线环境正确设置。
  6. 升级huggingface_hub后,清掉旧版残留的缓存元数据(.cache/huggingface)重新拉一次。
  7. 内网部署:把必需文件打进镜像,离线加载走回退路径,别依赖运行时完整性校验。

九、小结

DiffusionPipeline.download()huggingface_hub>=1.22.0离线模式报IncompleteSnapshotError,根子是新版对快照完整性校验更严,本地缺任一清单文件(含可选文件)或缓存元数据过期即拒,而 diffusers 没为离线做兜底。修复三层次:第一层预下载用allow_patterns限定必需文件、离线加载捕获完整性错误回退到本地;第二层用OfflineDownloadGuarddataclass 把离线判断/下载/回退收敛为单一守卫;第三层用 pytest 守「离线不下载」「完整性错误回退」「可选文件隔离」。

工程启示:离线/内网部署绝不能依赖运行时去 Hub 做完整性校验。正确姿势是「构建期用allow_patterns把必需文件拉齐打进镜像,运行期local_files_only+ 完整性错误回退」。把可选文件排除在完整性口径外,离线启动才稳。

返回列表