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

開始前:先定義 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.md、LICENSE/NOTICE、自訂條款與 gated 核准證據一起封存;不要放 token。Model Card 的 license 欄位是重要線索,但 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 Versioning/Object 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=1 與 local_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 個疏漏
- 只存權重:漏掉 tokenizer、config、chat template、processor 或 shard index。
- 把 cache 當 vault:snapshot 是 symlink forest,備份工具還可能因
CACHEDIR.TAG跳過它。 - 只寫 main:分支移動後,下一次抓到的是另一版。
- 只存 requirements:斷網時沒有相容 wheel、base image、驅動或 native library。
- 只比 hash:manifest 與 bundle 放在同一權限域,兩者可能一起被替換。
- 只跑線上 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。






