跳到主要內容

【2026 最新】Hugging Face 離線備援怎麼做?Commit、SHA-256 與斷網冷還原 7 步教學

最後更新: ·
Hugging Face 離線備援:Commit、SHA-256 與斷網冷還原教學首圖

如果明天 Hugging Face Hub 暫時連不上,你手上的模型能不能在一台乾淨、沒有網路的機器重新跑起來?只存一份權重不夠;tokenizer、設定檔、License 證據、runtime 與驗收收據少任何一項,都可能在復原當天才爆雷。

這篇 Hugging Face 離線備援教學會帶你做一條可演練的冷還原流程:固定完整 commit、下載實體檔案、建立 SHA-256 manifest、封存可離線安裝的 runtime,再從第二份儲存體拉回來斷網推論。本文不預測平台會發生什麼,也不把「可下載」誤寫成「可任意散布」;若你關心平台中立性,可另讀 Hugging Face 平台風險分析

先說結論:Hugging Face 離線備援要封存整個 release bundle

先記住這條式子:

可復原模型 = 完整 Commit × 實體檔案 × 授權證據 × SHA-256 × 鎖定 Runtime × 斷網 Smoke Test

乘號代表其中一關為零,整包就不能標成「已驗收」。這也劃出本文和舊模型保留/封存決策的界線:那篇幫你決定哪些模型值得留;這篇要求遠端完全不可用時,仍能從空白環境冷還原。

Hugging Face 離線備援的五關冷還原流程圖
線上來源比對是封存前檢查;真正的復原證據,是停網後仍能從空白環境載入並完成固定測試。

開始前:先定義 RPO、RTO 與目標機器

  • RPO:你最多能接受模型資產落後到哪個已驗收 commit,例如只保證回到上一個正式 release。
  • RTO:從宣布演練開始,到固定 smoke test 通過,允許花多久。
  • 目標平台:先寫死 OS、CPU 架構、Python 小版本與 CPU/CUDA/Metal 路線。不同平台要做不同 runtime 包。

本文的「乾淨環境」是已預置相容 OS、Bash/Zsh、Python 3.10+(含 venv/pip)與 tar,但沒有模型、Hub cache、token 或 Python 推論依賴的空白應用環境,不是完全沒有作業系統的裸機。若你的 RTO 要涵蓋裸機重建,還要另存 OS/Python 安裝媒體或固定 platform digest 的 base image,並把它加入同一場演練。

以下命令以 Bash/Zsh、Python 3.10 以上、Transformers CPU 路線示範,並把 huggingface_hub 固定在 2026-08-29 查核的 1.29.0。範例模型是 HuggingFaceTB/SmolLM2-135M-Instruct;工作流程可換模型,但 commit、License、base model 與檔案閉包都要重查。

第 1 步:把 main 解析成完整 commit

set -euo pipefail
python3 --version
python3 -c 'import sys; assert sys.version_info >= (3, 10), "Python 3.10+ required"'
python3 -m venv .hf-dr-online
source .hf-dr-online/bin/activate
python -m pip install --upgrade pip
python -m pip install "huggingface_hub==1.29.0"

export REPO_ID="HuggingFaceTB/SmolLM2-135M-Instruct"
export DR_ROOT="$PWD/hf-dr"
mkdir -p "$DR_ROOT/repo" "$DR_ROOT/evidence" "$DR_ROOT/runtime"

python - <<'PY'
from huggingface_hub import resolve_revision
print(resolve_revision(
    "HuggingFaceTB/SmolLM2-135M-Instruct",
    revision="main",
).resolved)
PY

下列 shell 區塊要依序在同一個 Bash/Zsh session 執行,才能沿用前面 export 的值;每個可獨立執行的區塊都先開啟 set -euo pipefail,讓前置檢查或下載一失敗就停止。若重開終端機,先從 provenance/復原紀錄重新匯入變數。

Hugging Face 下載文件要求用完整 commit hash 固定一個 revision,短版 7 碼不適合這個用途。把輸出抄進環境變數;本文範例固定在下列不可變 commit

export COMMIT="12fd25f77366fa6b3b4b768ec3050bf629380bac"

main 與 tag 都可能移動;完整 commit 只回答「哪一版」,不代表檔案已下載完整、程式可執行或授權允許鏡像。

第 2 步:先 dry-run,再下載完整實體檔案

set -euo pipefail
hf download "$REPO_ID" \
  --revision "$COMMIT" \
  --local-dir "$DR_ROOT/repo" \
  --dry-run

hf download "$REPO_ID" \
  --revision "$COMMIT" \
  --local-dir "$DR_ROOT/repo"

--dry-run 先顯示檔案與下載量,讓你在佔用儲存空間前審核。災難復原的預設做法是下載整個 repo;若要用 --include--exclude 縮小體積,必須把選取規則寫進 provenance,並證明該 runtime 不需要被排除的 shard、tokenizer、processor、chat template、adapter、base model 或 custom code。

不要直接把一般 Hub cache 的 snapshots/<commit> 當成備份。官方 cache 文件說明 snapshot 會以 symlink 指向 blobs;只搬 snapshot 可能留下斷掉的連結。Git/Xet 的 pointer 或傳輸快取也不等於已 materialize 的權重,因此本文使用獨立 --local-dir

第 3 步:在線上做來源比對,保存 License 證據

set -euo pipefail
hf cache verify "$REPO_ID" \
  --revision "$COMMIT" \
  --local-dir "$DR_ROOT/repo" \
  --fail-on-missing-files

export META_HOLD="$PWD/hf-local-dir-metadata-$COMMIT"
if [ -d "$DR_ROOT/repo/.cache/huggingface" ]; then
  mv "$DR_ROOT/repo/.cache/huggingface" "$META_HOLD"
fi

hf cache verify "$REPO_ID" \
  --revision "$COMMIT" \
  --local-dir "$DR_ROOT/repo" \
  --fail-on-missing-files \
  --fail-on-extra-files

hf cache verify 會向 Hub 比對,因此要在來源仍可用時跑;它不是離線驗證器。在 1.29.0 的實際查核中,--local-dir 產生的 .cache/huggingface 會被 strict 模式算成額外檔案,所以先移出 repo 再驗。未來升級 CLI 時,這段行為要重新驗證。

set -euo pipefail
hf --version > "$DR_ROOT/evidence/hf-version.txt"
printf '%s\n' \
  "repo_id=$REPO_ID" \
  "source=https://huggingface.co/$REPO_ID" \
  "resolved_commit=$COMMIT" \
  > "$DR_ROOT/evidence/provenance.txt"
date -u +"checked_at_utc=%Y-%m-%dT%H:%M:%SZ" \
  >> "$DR_ROOT/evidence/provenance.txt"

python - <<'PY'
import json, os
from pathlib import Path
from huggingface_hub import ModelCard

root = Path(os.environ["DR_ROOT"])
card = ModelCard.load(str(root / "repo" / "README.md"))
(root / "evidence" / "model-card-metadata.json").write_text(
    json.dumps(card.data.to_dict(), ensure_ascii=False, indent=2),
    encoding="utf-8",
)
PY

把 pinned README.mdLICENSENOTICE、自訂條款與 gated 核准證據一起封存;不要放 token。Model Cardlicense 欄位是重要線索,但 adapter、量化版、merge 與 base model 仍可能有各自條款。Gated access 是個人存取機制,而且作者能撤銷存取;它本身不是第三方再散布授權。

第 4 步:把 runtime 也變成離線資產

權重能讀不代表環境能重建。先在與復原目標相同的 OS、CPU 架構與 Python 小版本建立乾淨環境,再鎖版並下載 wheelhouse:

set -euo pipefail
python3 -m venv .runtime-build
source .runtime-build/bin/activate
python -m pip install --upgrade pip
python -m pip install torch transformers tokenizers safetensors

python -m pip freeze > "$DR_ROOT/runtime/requirements.lock.txt"
python -m pip download \
  --only-binary=:all: \
  --dest "$DR_ROOT/runtime/wheelhouse" \
  --requirement "$DR_ROOT/runtime/requirements.lock.txt"

python - <<'PY' > "$DR_ROOT/runtime/platform.json"
import json, platform, sys
print(json.dumps({
    "python": sys.version,
    "platform": platform.platform(),
    "machine": platform.machine(),
}, ensure_ascii=False, indent=2))
PY

--only-binary=:all: 若失敗,先讀取 resolver 的錯誤;常見原因之一是某個依賴沒有符合目標平台的 wheel。這時應先建出可審核的 wheel 或改用鎖定 digest 與 platform 的 OCI image,而不是等斷網後才臨時編譯。GPU 路線還要記錄驅動、CUDA/ROCm 與硬體需求;Linux 容器也不會自動變成 macOS Metal runtime。

如果你走 GGUF/llama.cpp,請同時保存上游模型 commit、轉檔工具 commit、量化命令、llama.cpp build commit 與 backend;多模態模型還可能需要 mmproj。可先參考 Qwen GGUF 本機驗收,再把相同變因納入本篇的離線 bundle。

第 5 步:建立固定 smoke test 與 golden receipt

把下列程式存成 hf-dr/runtime/smoke.py。它只從本地路徑載入 safetensors,預設不執行 repo 的 remote code,並以固定 prompt 產生 token ID 收據:

import argparse, json
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

p = argparse.ArgumentParser()
p.add_argument("mode", choices=["record", "verify"])
p.add_argument("--model-dir", required=True)
p.add_argument("--receipt", required=True)
a = p.parse_args()

model_dir = Path(a.model_dir)
receipt = Path(a.receipt)
prompt = "Reply with only DR_OK."

tokenizer = AutoTokenizer.from_pretrained(
    model_dir, local_files_only=True, trust_remote_code=False
)
model = AutoModelForCausalLM.from_pretrained(
    model_dir,
    local_files_only=True,
    trust_remote_code=False,
    use_safetensors=True,
)
model.eval()
torch.manual_seed(1)
inputs = tokenizer(prompt, return_tensors="pt")
with torch.inference_mode():
    output = model.generate(**inputs, max_new_tokens=8, do_sample=False)

new_ids = output[0, inputs["input_ids"].shape[1]:].tolist()
result = {
    "prompt": prompt,
    "generated_token_ids": new_ids,
    "decoded": tokenizer.decode(new_ids, skip_special_tokens=True),
}

if a.mode == "record":
    receipt.write_text(json.dumps(result, indent=2), encoding="utf-8")
elif json.loads(receipt.read_text(encoding="utf-8"))["generated_token_ids"] != new_ids:
    raise SystemExit("SMOKE_FAIL: token IDs differ")
else:
    print("SMOKE_PASS", result["decoded"])
set -euo pipefail
HF_HUB_OFFLINE=1 python "$DR_ROOT/runtime/smoke.py" record \
  --model-dir "$DR_ROOT/repo" \
  --receipt "$DR_ROOT/runtime/golden-receipt.json"

Golden token ID 先當成 regression signal,不是跨環境重現保證。只有在你已固定並驗過模型 commit、wheel hash、OS image、硬體、kernel、thread、deterministic 設定與推論參數,而且同一 stack 重跑穩定後,才把 exact ID equality 當成這個 profile 的阻擋門檻;換平台時,至少要求 manifest 相同、本地載入成功、tokenizer/輸出 schema 符合且輸出非空。要做更完整的品質 parity,可接著使用本機 LLM Parity A/B Test

第 6 步:用跨平台 manifest 封住缺檔、多檔與 hash 漂移

把這段存成 hf-dr/runtime/manifest.py。它拒絕 symlink、未完成下載與 Git LFS pointer,並對每個實體檔案記錄相對路徑、bytes、SHA-256:

import argparse, hashlib, json
from pathlib import Path

p = argparse.ArgumentParser()
p.add_argument("mode", choices=["create", "verify"])
p.add_argument("root")
a = p.parse_args()
root = Path(a.root).resolve()
manifest = root / "SHA256SUMS.json"

def digest(path):
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()

def inventory():
    rows = []
    for path in sorted(root.rglob("*"), key=lambda x: x.as_posix()):
        rel = path.relative_to(root).as_posix()
        if path.is_symlink():
            raise SystemExit(f"SYMLINK_REJECTED: {rel}")
        if not path.is_file() or path == manifest:
            continue
        if path.name.endswith(".incomplete"):
            raise SystemExit(f"INCOMPLETE_REJECTED: {rel}")
        with path.open("rb") as f:
            if f.read(128).startswith(b"version https://git-lfs.github.com/spec/v1"):
                raise SystemExit(f"LFS_POINTER_REJECTED: {rel}")
        rows.append({
            "path": rel,
            "bytes": path.stat().st_size,
            "sha256": digest(path),
        })
    return rows

if a.mode == "create":
    manifest.write_text(json.dumps({
        "schema": "alphalab.model-dr.v1",
        "files": inventory(),
    }, ensure_ascii=False, indent=2), encoding="utf-8")
    print("MANIFEST_CREATED", manifest)
else:
    expected = json.loads(manifest.read_text(encoding="utf-8"))["files"]
    actual = inventory()
    if actual != expected:
        e, g = {x["path"]: x for x in expected}, {x["path"]: x for x in actual}
        print("missing", sorted(e.keys() - g.keys()))
        print("extra", sorted(g.keys() - e.keys()))
        print("changed", sorted(k for k in e.keys() & g.keys() if e[k] != g[k]))
        raise SystemExit("MANIFEST_FAIL")
    print("MANIFEST_PASS", len(actual), "files")
set -euo pipefail
python "$DR_ROOT/runtime/manifest.py" create "$DR_ROOT"

python - <<'PY'
import hashlib, os
from pathlib import Path
p = Path(os.environ["DR_ROOT"]) / "SHA256SUMS.json"
print(hashlib.sha256(p.read_bytes()).hexdigest())
PY

把最後輸出的 manifest hash 存到另一個權限域,例如變更單、密鑰管理系統或另一個不可由同一組憑證覆寫的位置。SHA-256 能檢查 bytes 是否改變;如果攻擊者能同時改 bundle 與 manifest,它不會替你證明來源、License 或惡意程式安全。對 pickle 與 custom code 的風險,可延伸閱讀 vLLM/SGLang 本機模型安全硬化;Hugging Face 也明確說明 pickle scanner 是 best effort

第 7 步:完成 Hugging Face 離線備援的鏡像、冷還原與回滾

第二份 copy 要跨帳號或跨供應商憑證邊界,並用完整 commit 與 archive hash 當 release key;這個命名本身不會阻止覆寫。若要形成不可變保護,先在 Amazon S3 設定 Versioning、適當的 Object Lock retention 與限制覆寫的 IAM,再記錄實際 object version ID。以下把 bundle 封成一個 tar,讓還原端能指定版本取回;bucket 名稱請換成自己的:

set -euo pipefail
export BUCKET="your-model-dr-bucket"
export ARCHIVE="$PWD/hf-dr-$COMMIT.tar"

tar -C "$(dirname "$DR_ROOT")" \
  -cf "$ARCHIVE" \
  "$(basename "$DR_ROOT")"

ARCHIVE_SHA256="$(python - <<'PY'
import hashlib, os
from pathlib import Path
p = Path(os.environ["ARCHIVE"])
if p.stat().st_size > 5_000_000_000:
    raise SystemExit("PUT_OBJECT_LIMIT_EXCEEDED: use multipart upload")
h = hashlib.sha256()
with p.open("rb") as f:
    for chunk in iter(lambda: f.read(1024 * 1024), b""):
        h.update(chunk)
print(h.hexdigest())
PY
)"
export ARCHIVE_SHA256

export KEY="model-dr/$REPO_ID/$COMMIT/$ARCHIVE_SHA256.tar"
PUT_RESULT="$(aws s3api put-object \
  --bucket "$BUCKET" \
  --key "$KEY" \
  --body "$ARCHIVE" \
  --checksum-algorithm SHA256 \
  --if-none-match '*')"
export PUT_RESULT

VERSION_ID="$(python - <<'PY'
import json, os
result = json.loads(os.environ["PUT_RESULT"])
version = result.get("VersionId")
if version in (None, "", "null", "None"):
    raise SystemExit("VERSIONING_REQUIRED")
print(version)
PY
)"
export VERSION_ID
unset PUT_RESULT
printf 'bucket=%s\nkey=%s\narchive_sha256=%s\nversion_id=%s\n' \
  "$BUCKET" "$KEY" "$ARCHIVE_SHA256" "$VERSION_ID"

把輸出的 archive SHA-256 與 version ID 存到 bundle 之外的復原紀錄。這個 put-object 分支刻意在超過 5 GB 時停止;更大的 bundle 要走 multipart upload,並直接從 CompleteMultipartUpload 的成功回應取得 VersionId。不要退回「上傳後再 HEAD 猜版本」的競態流程。

AWS checksum 會檢查傳輸,但拉回後仍要用自己的 archive hash 與 manifest 驗整棵檔案樹。S3 VersioningObject Lock 可以成為保護層,但 Object Lock 保護的是指定 object version,仍可建立新版本或 delete marker;如果跨區副本仍由同一帳號與管理憑證控制,也沒有形成獨立的憑證失效域。

先在可連 Amazon S3 的還原站指定 sealed version 取回、驗 archive hash、解開到空目錄;之後切斷網路,再建立 runtime:

set -euo pipefail
export BUCKET="<recovery-record-bucket>"
export KEY="<recovery-record-key>"
export VERSION_ID="<recovery-record-version-id>"
export ARCHIVE_SHA256="<recovery-record-archive-sha256>"
export RESTORE_ROOT="$PWD/restore-drill"
export RESTORED_ARCHIVE="$RESTORE_ROOT/hf-dr.tar"
mkdir -p "$RESTORE_ROOT"

test ! -e "$RESTORE_ROOT/hf-dr" || {
  echo "RESTORE_DIRECTORY_MUST_BE_EMPTY"
  exit 1
}

aws s3api get-object \
  --bucket "$BUCKET" \
  --key "$KEY" \
  --version-id "$VERSION_ID" \
  --checksum-mode ENABLED \
  "$RESTORED_ARCHIVE"

python - <<'PY'
import hashlib, os
from pathlib import Path
p = Path(os.environ["RESTORED_ARCHIVE"])
h = hashlib.sha256()
with p.open("rb") as f:
    for chunk in iter(lambda: f.read(1024 * 1024), b""):
        h.update(chunk)
if h.hexdigest() != os.environ["ARCHIVE_SHA256"]:
    raise SystemExit("ARCHIVE_SHA256_FAIL")
print("ARCHIVE_SHA256_PASS")
PY

tar -xf "$RESTORED_ARCHIVE" -C "$RESTORE_ROOT"
export BUNDLE="$RESTORE_ROOT/hf-dr"
python "$BUNDLE/runtime/manifest.py" verify "$BUNDLE"

python3 -m venv "$RESTORE_ROOT/offline-venv"
source "$RESTORE_ROOT/offline-venv/bin/activate"
python -m pip install \
  --no-index \
  --find-links "$BUNDLE/runtime/wheelhouse" \
  --requirement "$BUNDLE/runtime/requirements.lock.txt"

HF_HUB_OFFLINE=1 python "$BUNDLE/runtime/smoke.py" verify \
  --model-dir "$BUNDLE/repo" \
  --receipt "$BUNDLE/runtime/golden-receipt.json"

Transformers 離線文件中的 HF_HUB_OFFLINE=1local_files_only=True 會阻止 Hugging Face client 回源;它們不會替第三方程式證明整台機器沒有外連,所以正式演練還是要真的斷網,或把工作負載放進 --network none 的隔離容器。

記錄開始/結束時間、manifest hash、模型 commit、runtime lock hash、目標平台與 smoke 結果,才有 RTO 收據。更新時讓新 commit 與舊版並存,冷還原通過後只切換一個 release pointer;回滾就是把 pointer 指回上一個已驗收 bundle,而不是重新上網抓舊版。若你仍在評估 API 或本機部署的完成成本,可搭配 GLM API-first/本機部署指南

冷還原常見的 6 個疏漏

  1. 只存權重:漏掉 tokenizer、config、chat template、processor 或 shard index。
  2. 把 cache 當 vault:snapshot 是 symlink forest,備份工具還可能因 CACHEDIR.TAG 跳過它。
  3. 只寫 main:分支移動後,下一次抓到的是另一版。
  4. 只存 requirements:斷網時沒有相容 wheel、base image、驅動或 native library。
  5. 只比 hash:manifest 與 bundle 放在同一權限域,兩者可能一起被替換。
  6. 只跑線上 smoke:程式悄悄回源成功,讓你誤以為本地包完整。

FAQ:Hugging Face 離線備援常見問題

1. 為什麼不能只用 git clone?

大型檔案可能只留下 Git LFS/Xet pointer。備援要驗證 materialized bytes,而不是只看 Git history 存在。

2. 可以直接備份 Hugging Face cache 嗎?

可以,但必須連同 blobs、refs 與 snapshot symlink 一起正確還原,也要確認備份工具沒有跳過 cache。對初學者,經驗證的 --local-dir release bundle 較容易稽核。

3. Hub 的 blob_id、Xet hash 與本地 SHA-256 相同嗎?

用途不同。Git blob OID、Xet 識別碼與 materialized file 的 SHA-256 不能混用;本文另算本地 SHA-256,讓還原端有一致的比較基準。

4. Gated model 下載後就能放到公開鏡像嗎?

不能從「已獲存取」直接推論「可公開再散布」。應按該模型的 LICENSE、自訂條款、base model 條款與 gated agreement 決定備份位置和可分享對象。

5. 什麼時候能排除 repo 裡的大檔?

只有當你已固定單一 runtime、列出需要的所有檔案,並用斷網 smoke test 證明選取集合完整時。選取規則本身也要進 provenance。

6. 設定 HF_HUB_OFFLINE=1 就算 air-gapped 嗎?

不算。它限制 Hugging Face client 的 HTTP 行為,不限制自訂 Python、遙測或其他第三方套件。真正演練要切斷網路並觀察整個程序。

7. llama.cpp 和 Transformers 要備份同一套 runtime 嗎?

不用。llama.cpp 以 GGUF、build commit、編譯旗標與 backend 為核心;Transformers 以原生 checkpoint、Python/PyTorch 與 wheels 或 OCI image 為核心。兩條路要各自封存、各自驗收。

8. 多久要演練一次?

依模型更新頻率與 RPO 設定。至少每次換 commit、runtime、硬體平台或備份後端後,都要把新組合當成未驗收,重新做 read-back、斷網載入與收據保存。

新手完成清單

  • 已把 branch/tag 解析為完整 commit。
  • 已保存實體權重、tokenizer、config、Model Card、License 與依賴關係。
  • 已封存相容平台的 runtime、wheelhouse 或固定 digest 的 image。
  • manifest 會拒絕缺檔、多檔、symlink、pointer 與 SHA-256 不符。
  • 第二份備份使用獨立憑證邊界,且 commit 路徑不覆寫。
  • 乾淨環境真的斷網,golden smoke test 已通過並留下 RTO 收據。

接著閱讀

左右滑動查看更多推薦

下一步:把網路拔掉,才算完成第一份復原收據

今天先挑一個小模型完成整條流程,不要一開始就搬最大的權重。當你能從第二份儲存體拉回固定 commit、驗過 manifest、離線安裝 runtime,並在乾淨環境通過 smoke test,這個 bundle 才有資格進入「可復原」清單。之後每次升級模型或 runtime,就以同一張收據決定 promote 或 rollback。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多三封:一封 Weekly 週報與最多兩封關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。