在 AI/ML 專案裡,我們經常需要把模型權重、資料集、圖片或音訊檔放進版本控制。直接 git add 一個幾 GB 的 .safetensors 檔案,很快就會發現倉庫越來越肥、clone 越來越慢,甚至被託管平台直接拒絕推送。Git LFS(Large File Storage)正是為了解決這個問題而生。本文會從原理講到實戰,整理遷移、CI、託管配額、自架部署與替代方案。

1. 為什麼需要 Git LFS

1.1 Git 處理大檔案的困境

Git 是一套內容定址(content-addressable)的快照系統:每個檔案的每個版本都會以 blob 物件的形式存進 .git/objects,並以內容雜湊作為索引。這個設計對原始碼非常有效,但遇到大型二進位檔就會暴露幾個問題:

  • 歷史永久保留:只要某個版本曾被提交,它就永遠留在歷史中。即使後來刪除檔案,每個 clone 仍然要下載它。一個 2 GB 的模型改了十次,倉庫裡就有約 20 GB 的資料。
  • 難以差異壓縮:Git 在打包(packfile)時會嘗試對相近的 blob 做 delta 壓縮。文字檔的效果很好,但壓縮過的二進位格式(圖片、影片、模型權重)往往改一點就整個檔案位元組都不同,delta 幾乎無效。
  • clone 必須拿到全部歷史:預設的 git clone 會下載所有版本的所有物件,與你實際需要的那一版無關。
  • 平台限制:多數託管平台對單一檔案有硬性上限。例如 GitHub 會直接拒絕一般 Git 歷史中大於 100 MiB 的檔案。

1.2 Git LFS 的核心思路

Git LFS 的想法很簡單:Git 倉庫裡只放一個很小的文字指標檔(pointer file),真正的檔案內容另外存放在 LFS 伺服器上。

這裡的「LFS 伺服器」常讓人困惑:它不是 Git 本身內建的,Git 只負責版本控制,並不具備存放 LFS 物件的能力。LFS 伺服器是一個實作了 Git LFS API 的獨立服務,實務上有兩種來源:

  • 託管平台內建(最常見):GitHub、GitLab、Gitea / Forgejo、Bitbucket、Hugging Face 等平台都已經內建 LFS 伺服器,與你的倉庫使用同一個位址與帳號權限。使用者完全不需要另外架設或設定,Git LFS 會根據 remote URL 自動推導出 LFS 端點(細節見 2.3 節)。
  • 自己架設:如果你自架 Git 服務,可以選擇內建 LFS 的軟體(例如 Gitea / Forgejo、GitLab CE),或另外架設獨立的 LFS 伺服器(見 5.2 節)。

要注意的是,如果你的遠端只是一個透過 SSH 存取的裸倉庫(例如 ssh://server/srv/repo.git),那麼它只有 Git,沒有 LFS 伺服器,推送 LFS 檔案會失敗,必須另外提供 LFS 服務。

  • 提交時,大檔案被替換成指標檔,原始內容存入本地 LFS 快取,推送時再上傳到 LFS 伺服器。
  • 檢出(checkout)時,Git LFS 根據指標檔向伺服器下載對應內容,再還原到工作目錄。

這樣一來,Git 歷史保存的是小型指標檔,因此一般 Git 物件的下載量與倉庫大小會下降。檢出時是否立即下載 LFS 內容,取決於用戶端設定;也可以先略過,再用 git lfs pull 下載需要的檔案。

2. 運作原理

2.1 Pointer 檔格式

被 LFS 追蹤的檔案,在 Git 物件庫中實際存放的是這樣一份文字檔:

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345

三個欄位的意義:

欄位說明
version指標檔規格版本,目前固定為 spec/v1
oid原始檔案內容的 SHA-256 雜湊,也是 LFS 物件的唯一識別
size原始檔案的位元組數

由於 oid 是內容雜湊,LFS 物件天生具有檔案層級的去重:同一份內容無論出現在幾個路徑、幾個 commit,伺服器都只存一份。但反過來說,檔案只要改了一個位元組,就會產生一個全新的完整物件,這一點在後面談配額與替代方案時很重要。

你可以用以下指令觀察倉庫中實際存的是什麼:

# 查看 HEAD 中某檔案在 Git 物件庫裡的真實內容(即 pointer)
git cat-file -p HEAD:models/model.safetensors

# 或者用 LFS 指令解析
git lfs pointer --file=models/model.safetensors

2.2 Clean 與 Smudge Filter

Git LFS 並沒有修改 Git 本身,而是利用 Git 原生的 filter 機制。在看 LFS 怎麼用它之前,先了解這個機制本身。

Git 的 filter 是什麼

Git 在「工作目錄」與「物件庫」之間搬運檔案時,允許插入外部程式對內容做轉換,這就是 filter(官方文件稱為 filter driver,見 man gitattributes)。一個 filter 由兩個方向的指令組成:

方向觸發時機作用
cleangit add:工作目錄 → 暫存區 / 物件庫把工作目錄中的內容「清理」成要存進 Git 的版本
smudgegit checkout:物件庫 → 工作目錄把 Git 中存的版本「弄髒」成要放在工作目錄的內容

名稱的意象是:存進 Git 的版本是「乾淨的」,工作目錄裡的版本是被「抹上」本地內容的。兩個指令都從 stdin 讀入檔案內容、從 stdout 輸出轉換後的內容,Git 只負責呼叫,完全不在意中間做了什麼。

設定一個 filter 需要兩個部分:

  1. 在 Git 設定中定義 filter 的指令(.git/config 或 ~/.gitconfig);
  2. 在 .gitattributes 中指定哪些路徑使用這個 filter。

舉一個與 LFS 無關的小例子:假設我們希望設定檔裡的密碼永遠不要被提交,但本地工作目錄中仍保有真實密碼:

# .git/config
[filter "hidepass"]
    clean = sed -e 's/^password = .*/password = REDACTED/'
    smudge = cat
# .gitattributes
config.ini filter=hidepass

此後 git add config.ini 時,存進 Git 的內容中密碼會被替換成 REDACTED,而工作目錄中的檔案保持原樣。常見的實際應用還有:提交前自動格式化程式碼、清除 Jupyter Notebook 的輸出(例如 nbstripout 就是以 clean filter 實作),以及透明加密(git-crypt)。

這個例子也說明了 filter 的兩個特性:

  • filter 定義不會隨倉庫散佈。 .gitattributes 會被提交,但 .git/config 不會。所以協作者 clone 後,必須自己在本機定義同名的 filter,若本機沒有安裝或設定對應的 filter,Git 可能會把檔案原樣加入一般 Git 歷史,造成應為 pointer 的檔案被存成原始 blob。因此協作者需安裝 Git LFS 並執行 git lfs install。
  • Git 看到的永遠是 clean 之後的內容。 diff、雜湊、歷史紀錄都基於 clean 的輸出,工作目錄中的實際內容對 Git 物件庫而言是不可見的。

Git LFS 如何使用 filter

理解了這個機制,Git LFS 的做法就很直接了:clean 把大檔案換成 pointer 存進 Git,smudge 再把 pointer 換回真實內容。執行 git lfs install 後,全域設定 ~/.gitconfig 會多出:

[filter "lfs"]
    clean = git-lfs clean -- %f
    smudge = git-lfs smudge -- %f
    process = git-lfs filter-process
    required = true

而倉庫中的 .gitattributes 負責指定哪些路徑套用這個 filter:

*.safetensors filter=lfs diff=lfs merge=lfs -text

兩個方向的轉換如下:

  • clean(工作目錄 → 暫存區):git add 時,Git 把檔案內容交給 git-lfs clean。它計算 SHA-256、把原始內容複製到 .git/lfs/objects/<oid前兩碼>/<oid次兩碼>/<oid>,然後輸出 pointer 給 Git 存成 blob。
  • smudge(暫存區 → 工作目錄):git checkout 時,Git 把 pointer 交給 git-lfs smudge。它先查本地快取,沒有的話向 LFS 伺服器下載,再輸出原始內容寫入工作目錄。

process 是長駐行程版本(filter process protocol),讓 Git 在處理大量檔案時不必每個檔案都啟動一次 git-lfs,效能好很多。required = true 表示本機已設定這個 filter 時,filter 執行失敗應使 Git 操作失敗;它不能替未安裝 Git LFS 的協作者補上 filter。

另外三個屬性:

  • diff=lfs:讓 git diff 比較 pointer,而不是嘗試對大型二進位內容做 diff。
  • merge=lfs:合併時使用 LFS 的策略。
  • -text:關閉換行符號轉換,避免二進位檔被改壞。

除了 filter,git lfs install 也會在倉庫中安裝幾個 hook,其中最關鍵的是 pre-push:在 git push 把 commit 推上去之前,先把這些 commit 引用到、而伺服器尚未擁有的 LFS 物件上傳。這保證遠端不會出現「有 pointer 卻沒有內容」的狀態。

2.3 傳輸流程與 Batch API

LFS 物件的上傳與下載走的是獨立於 Git 協定的 HTTP API。預設的 LFS 端點由 Git remote URL 推導,例如:

https://git.example.com/user/repo.git  →  https://git.example.com/user/repo.git/info/lfs

也可以在倉庫根目錄的 .lfsconfig 中用 lfs.url 明確指定(例如讓 Git 走 SSH、LFS 走另一台 HTTP 伺服器)。

核心是 Batch API。以下載為例,完整流程如下:

  Git 客戶端                   LFS 伺服器                 物件儲存(S3 等)
      │                           │                           │
      │ 1. checkout 遇到 pointer   │                           │
      │──── POST /objects/batch ─▶│                           │
      │   {operation: download,   │                           │
      │    objects: [{oid,size}]} │                           │
      │                           │ 2. 驗證權限、查詢物件         │
      │◀─── 200 actions.download ─│                           │
      │   {href, header,          │                           │
      │    expires_in}            │                           │
      │                           │                           │
      │──────────── 3. GET href(可能是預簽名 URL)────────────▶│
      │◀──────────────────── 4. 檔案內容 ──────────────────────│
      │                           │                           │
      │ 5. 校驗 SHA-256,寫入 .git/lfs/objects 與工作目錄          │

請求內容大致如下:

POST /user/repo.git/info/lfs/objects/batch
Accept: application/vnd.git-lfs+json
Content-Type: application/vnd.git-lfs+json

{
  "operation": "download",
  "transfers": ["basic"],
  "ref": { "name": "refs/heads/main" },
  "objects": [
    { "oid": "4d7a2146...", "size": 12345 }
  ]
}

伺服器針對每個物件回傳 actions:

  • 下載時給 download,包含 href、需要附帶的 header 與過期時間。
  • 上傳時給 upload(伺服器已經有的物件則不回傳 action,客戶端就會跳過),有時還會附帶 verify,要求客戶端上傳完成後回報確認。

這個設計有兩個好處:

  1. 批次處理:一次請求可以詢問數百個物件,減少往返。
  2. 控制面與資料面分離:LFS 伺服器只負責驗證與發放 URL,實際的位元組可以直接在客戶端與 S3、CDN 之間傳輸。這也是自架時能把 LFS 物件放到物件儲存的關鍵。

幾個補充:

  • transfers 欄位用於協商傳輸方式,標準是 basic(單一 HTTP PUT/GET),也支援自訂傳輸代理(custom transfer agent),例如直接對接 S3 的分段上傳工具。
  • 使用 SSH remote 時,客戶端會先透過 SSH 執行 git-lfs-authenticate 取得 HTTP 憑證;Git LFS 3.0 起也支援純 SSH 的傳輸協定(伺服器端需實作 git-lfs-transfer)。
  • 除錯時可以用 GIT_TRACE=1 GIT_CURL_VERBOSE=1 git lfs pull 看到完整的請求與回應。

3. 基本使用與遷移

3.1 安裝與追蹤檔案

安裝:多數套件管理員都有提供:

# macOS
brew install git-lfs

# Debian / Ubuntu
sudo apt install git-lfs

# Fedora / RHEL
sudo dnf install git-lfs

# Windows:Git for Windows 已內建

初始化設定:安裝套件之後,還需要執行:

git lfs install

這個指令的名稱容易讓人誤會:它並不是安裝 Git LFS(安裝是上一步套件管理員做的事),而是設定 Git 使用 Git LFS。具體來說,它做兩件事:

  1. 在全域設定 ~/.gitconfig 中寫入 2.2 節提到的 [filter "lfs"] 區段,讓 Git 知道遇到 filter=lfs 的檔案時要呼叫 git-lfs。
  2. 如果當下位於某個倉庫中,順便為這個倉庫安裝 pre-push 等 hook。

一般使用者可在每個帳號執行一次,讓 Git 全域設定知道如何呼叫 LFS filter。hook 則是倉庫層級設定;在既有倉庫中執行 git lfs install 可安裝或更新它。若 clone 後推送 LFS 檔案失敗,先在該倉庫執行此命令,再確認 hook 與 remote 設定。

可以用以下指令確認設定是否已經生效:

git config --global --get-regexp '^filter\.lfs\.'

如果有輸出 filter.lfs.clean、filter.lfs.smudge 等項目,就代表已經設定過了。另外幾個常用的變體:

指令作用範圍
git lfs install目前使用者(寫入 ~/.gitconfig),最常用
git lfs install --system整台機器的所有使用者(寫入系統層級設定,通常需要 root)
git lfs install --local只對目前倉庫生效(寫入 .git/config)
git lfs install --skip-repo只寫入全域設定,不在目前倉庫安裝 hook
git lfs uninstall移除上述設定

--system 會寫入系統層級 Git 設定;--local 只影響目前倉庫。CI runner 可在映像或工作目錄中明確設定,避免依賴某個帳號的全域狀態。

追蹤檔案:

# 依副檔名追蹤(注意要加引號,避免 shell 展開 *)
git lfs track "*.safetensors"
git lfs track "*.parquet"

# 依目錄追蹤
git lfs track "datasets/**"

# 查看目前追蹤規則
git lfs track

git lfs track 實際上只是在 .gitattributes 中新增一行。這個檔案必須提交進倉庫,否則其他協作者 clone 後不會知道哪些檔案要走 LFS:

git add .gitattributes
git add models/model.safetensors
git commit -m "Add model weights via LFS"
git push

常用查詢指令:

# 列出目前檢出版本中被 LFS 管理的檔案(* 表示已下載內容,- 表示只有 pointer)
git lfs ls-files

# 顯示檔案大小
git lfs ls-files --size

# 查看暫存區中的檔案是否以 LFS pointer 形式存入
git lfs status

# 檢查本地 LFS 物件與 pointer 是否一致
git lfs fsck

# 顯示 LFS 端點、快取路徑等環境資訊
git lfs env

3.2 .gitattributes 的常見陷阱

LFS 的大部分問題都出在 .gitattributes,以下是最常見的幾個:

一、規則必須先於檔案加入。 追蹤規則只影響之後的 git add。如果檔案已經以一般 blob 提交過,事後才加規則,歷史中的舊版本仍然是一般物件。若只是想讓「目前的版本」轉為 LFS:

git lfs track "*.bin"
git add --renormalize .
git commit -m "Move *.bin to LFS"

但這不會縮小歷史,要清理歷史需要用 3.3 節的 migrate。

二、樣式語法與 .gitignore 不完全相同。 .gitattributes 的樣式不支援否定(!),而且以 / 開頭表示相對於 .gitattributes 所在目錄。要比對任意深度的目錄需要用 **:

# 只比對根目錄下的 data/ 中的 csv
/data/*.csv filter=lfs diff=lfs merge=lfs -text

# 比對任何位置的 csv
*.csv filter=lfs diff=lfs merge=lfs -text

# 比對 assets 目錄下的所有內容
assets/** filter=lfs diff=lfs merge=lfs -text

三、大小寫敏感。 *.png 不會比對到 IMAGE.PNG。在 macOS 和 Windows 這種大小寫不敏感的檔案系統上特別容易踩雷,必要時兩種都寫上。

四、不要用 LFS 追蹤小型文字檔。 例如設定檔、JSON、CSV 樣本。它們用 Git 原生存放更好,還能保留可讀的 diff。

五、.gitattributes 本身不能被 LFS 追蹤。 例如 track "*" 這類過度寬泛的規則會把它也卷進去,導致整個機制失效。

3.3 遷移既有倉庫

如果倉庫已經有大量大檔案混在一般歷史中,git lfs migrate 可以改寫歷史,把它們轉成 LFS 物件。

第一步:分析現況。

# 依副檔名統計所有分支、所有歷史中佔空間最多的檔案類型
git lfs migrate info --everything --top=10

第二步:匯入到 LFS(改寫歷史)。

# 改寫所有分支與 tag 上的歷史,把指定類型轉成 LFS
git lfs migrate import --everything --include="*.safetensors,*.bin,*.parquet"

# 也可以依大小篩選
git lfs migrate import --everything --above=50MB

這個指令會重新產生每一個相關 commit,因此 commit 雜湊全部改變。完成後需要強制推送:

git push --force --all
git push --force --tags

改寫歷史前務必注意:

  • 先備份(例如 git clone --mirror 一份)。
  • 通知所有協作者。他們手上的舊分支與新歷史無法直接合併,最乾淨的做法是重新 clone。
  • 已經開啟的 PR / MR 會失效,需要重新建立。
  • 託管平台上舊物件可能仍佔用空間,必要時需執行伺服器端的垃圾回收或聯繫平台處理。

不想改寫歷史的情況:

# 只在最新 commit 之上新增一個 commit,把目前的檔案轉成 LFS
git lfs migrate import --no-rewrite path/to/large.bin

歷史不變、舊版本仍佔空間,但適合多人協作中途才導入 LFS、又不想打亂所有人的情況。

反向遷移:如果決定不再使用 LFS(例如改用其他方案),可以把 LFS 物件轉回一般 Git 物件:

git lfs migrate export --everything --include="*.png"

4. 進階技巧

4.1 選擇性下載

當倉庫中有大量 LFS 物件,而你只需要其中一部分(例如只想拿某一個模型的權重),可以避免全部下載。

clone 時跳過所有 LFS 內容,之後按需下載:

GIT_LFS_SKIP_SMUDGE=1 git clone https://git.example.com/user/models.git
cd models

# 此時工作目錄中都是 pointer,只下載需要的部分
git lfs pull --include="llama-8b/*"

持久的包含與排除規則:設定後,之後的 pull、checkout 都會遵守。

git config lfs.fetchinclude "configs/*,small-model/*"
git config lfs.fetchexclude "datasets/raw/*"

只下載近期的物件:git lfs fetch --recent 會額外取得近期分支與近期 commit 引用到的物件,範圍可以用 lfs.fetchrecentrefsdays、lfs.fetchrecentcommitsdays 調整。適合預先抓好接下來可能切換到的分支,以便離線工作。

Git 原生的部分 clone也可以搭配使用,減少一般 Git 物件的下載量:

git clone --filter=blob:none https://git.example.com/user/repo.git

4.2 快取清理

本地的 .git/lfs/objects 會保留你曾經檢出過的每一個版本,久了可能比工作目錄本身還大。git lfs prune 會刪除本機快取中不再需要的物件。它會保留目前檢出的版本、近期引用、stash、其他 worktree,以及尚未推送的物件;細節受近期保留設定和 remote 影響。

# 先看看會刪掉什麼
git lfs prune --dry-run --verbose

# 刪除前向遠端確認物件確實存在,最安全
git lfs prune --verify-remote

要特別注意:prune 只清理本地快取,伺服器端的儲存用量不會因此減少。 伺服器端的物件只要曾經被任何歷史引用,就會持續計入用量;改寫歷史後,舊物件是否真的被刪除取決於平台的垃圾回收策略,部分平台需要刪除整個倉庫或聯繫客服才能釋放。

4.3 檔案鎖定

二進位檔無法合併。兩個人同時修改同一個 .psd 或 Unity 場景檔,其中一人的成果注定要丟棄。Git LFS 提供了檔案鎖定機制來避免這種情況:

# 標記為可鎖定
git lfs track "*.psd" --lockable

這會在 .gitattributes 中加上 lockable 屬性。之後被標記的檔案在檢出時會變成唯讀,提醒使用者先取得鎖:

git lfs lock design/banner.psd      # 取得鎖,檔案變為可寫入
git lfs locks                       # 查看目前所有鎖
git lfs unlock design/banner.psd    # 修改並推送後釋放
git lfs unlock --force design/banner.psd   # 管理員強制解鎖

鎖定檢查需伺服器支援 Locking API,並由用戶端啟用 lock verification;可依 Git LFS 文件用 lfs.<url>.locksverify 設定驗證行為。啟用後,Git LFS 的 pre-push hook 會向伺服器檢查即將推送的檔案是否由其他人鎖定。先確認所用平台支援 Locking API,再把它納入團隊流程。對純程式或 ML 專案通常用不到,但在遊戲、設計類專案中很實用。

4.4 在 CI 中使用

CI 每次都可能重新下載 LFS 物件,應只在工作確實需要內容時啟用 LFS checkout。以 GitHub Actions 為例,actions/checkout 提供 lfs: true:

steps:
  - uses: actions/checkout@v4
    with:
      lfs: true

若要快取 .git/lfs,快取鍵必須反映該工作需要的 LFS 物件;單純按檔名或分支快取可能在檔案更新後重用舊內容。先確保 checkout 與快取步驟順序正確,再用 git lfs fsck 或工作流程中的雜湊檢查確認內容。對不需要大檔案的 lint、文件檢查工作,跳過 LFS 下載通常最省流量。

其他建議:

  • 只下載 job 真正需要的檔案,例如 git lfs pull --include="tests/fixtures/*"。
  • 不需要大檔案的 job(lint、單元測試)完全不要 pull。
  • 自架 CI runner 時,可以在 runner 上維持一份持久的 LFS 快取目錄,透過 lfs.storage 設定指向它。

5. 託管與自架

5.1 主流平台比較

GitHub

GitHub LFS 的單檔限制、儲存與頻寬額度依方案而異,也可能調整。下載流量會計入倉庫擁有者的配額;透過 fork 存取時也可能由上游倉庫承擔用量。使用前請查看 GitHub 當前的 LFS 配額與限制,並留意 CI 下載會累積頻寬。

GitLab

GitLab 支援 Git LFS;自架執行個體可設定 LFS 物件儲存,包括物件儲存服務。GitLab.com 的儲存與流量規則依方案和命名空間設定而異,請查閱當前官方文件。

Hugging Face Hub

Hugging Face Hub 現在採用 Xet 作為大型檔案儲存後端,同時維持 Git 與 Git LFS pointer 的相容路徑。Git LFS 以檔案為單位識別物件,Xet 則支援區塊層級去重。要使用 Xet 的傳輸最佳化,請依 Hub 文件安裝並設定支援 Xet 的用戶端;不同工具和版本的行為可能不同。

5.2 自架 LFS 伺服器

自架 Git 服務可省去第三方方案的特定配額,但儲存、備份、頻寬、可用性與維運成本仍由自己承擔。常見選擇包括:

方案適合情境
Gitea / Forgejo輕量部署,並使用其內建 LFS 支援與可配置的物件儲存
GitLab Self-Managed需要 GitLab 的整合功能,並自行管理 LFS 儲存
獨立 LFS 伺服器Git 主機不提供 LFS,且願意自行整合認證、儲存與備份

請依部署版本查看伺服器的 Gitea LFS、Forgejo 或 GitLab LFS 設定文件。實際設定名稱、支援的儲存後端和直連下載行為會隨產品與版本改變;不要直接複製另一版本的 app.ini 範例。上線前測試 clone、checkout、push、權限控制與備份還原,並確認產生的下載 URL 可由客戶端連線。

5.3 反向代理設定要點

自架 LFS 最常見的失敗原因不在 Git 伺服器本身,而在前面的反向代理。LFS 上傳可能包含大型 HTTP 請求;反向代理的請求大小限制、逾時、緩衝和上游連線設定都可能影響傳輸。應依實際伺服器與代理文件設定,並用接近預期大小的檔案驗證。

Nginx:

server {
    listen 443 ssl;
    server_name git.example.com;

    # 取消請求大小上限(預設僅 1 MB)
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # 不要先把整個請求緩衝到磁碟再轉發,直接串流
        proxy_request_buffering off;
        proxy_buffering off;

        # 放寬逾時,大檔案上傳可能需要很久
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Caddy 若設定了請求本文大小限制,請確認 LFS 上傳路徑允許預期大小;其他代理也需檢查其限制和逾時。

CDN 與 tunnel:確認服務方案對請求大小、連線時間和上傳方式的限制。若代理不適合承載大型 LFS 物件,可依 Git 伺服器支援的方式配置獨立 LFS endpoint 或物件儲存直連;檢查產生的 URL、認證和 TLS,避免把私有儲存端點暴露給不該存取的客戶端。

檢查清單:

  • 上傳失敗且回應 413:請求大小限制。
  • 上傳進行到一半中斷、回應 504:逾時設定。
  • 下載 URL 指向內網位址:ROOT_URL 或物件儲存的對外 endpoint 設定錯誤。
  • 驗證失敗:反向代理沒有正確轉發 Authorization header,或 X-Forwarded-Proto 造成 URL 方案不一致。

6. 疑難排解與替代方案

6.1 常見問題

clone 後檔案內容是 pointer 文字,而不是真正的檔案

通常是這台機器沒有安裝 Git LFS、沒有完成本機設定,或工作目錄中的 LFS 下載被略過。

git lfs install
git lfs pull

如果是刻意用 GIT_LFS_SKIP_SMUDGE=1 clone 的,用 git lfs pull 下載內容。若 pull 失敗,檢查認證、LFS endpoint、物件是否已上傳,以及託管平台回報的配額狀態。

smudge filter lfs failed

checkout 時下載失敗。依序檢查:

  1. 網路與驗證:git lfs env 確認端點,GIT_TRACE=1 git lfs pull 看詳細錯誤。
  2. 物件是否真的存在於伺服器:有人提交了 pointer 卻沒有成功推送物件(例如在沒有安裝 LFS 的環境中操作,或繞過了 pre-push hook)。
  3. 配額是否用盡。

想先完成 checkout、稍後再處理,可以暫時跳過:

GIT_LFS_SKIP_SMUDGE=1 git checkout <branch>

Encountered N file(s) that should have been pointers, but weren't

符合 .gitattributes 規則的檔案,卻以一般 blob 的形式存在於倉庫中。常見原因是某位協作者在沒有安裝 LFS 的環境下提交了檔案。修正方式:

# 在最新 commit 中轉正,不改寫歷史
git add --renormalize .
git commit -m "Fix files that should be LFS pointers"

# 或改寫歷史,徹底清理
git lfs migrate import --everything --include="<pattern>"

不小心把大檔案以一般物件提交了,而且已經推送

只是後續再刪除檔案並不會讓倉庫變小。需要用 git lfs migrate import 改寫歷史(見 3.3 節)。如果還沒推送,git reset --soft HEAD~1 之後重新加入追蹤規則再提交即可。

推送極慢或卡住

  • 用 git lfs push --dry-run origin main 檢查實際要上傳的物件數量與大小。
  • 調整並行度:git config lfs.concurrenttransfers 8。
  • 自架環境檢查反向代理的緩衝設定(5.3 節)。

排查工具總整理

指令用途
git lfs env顯示版本、端點、快取路徑與設定
git lfs status暫存區中 LFS 檔案的狀態
git lfs ls-files --size列出 LFS 檔案與大小
git lfs fsck驗證本地物件完整性
git lfs logs last查看最近一次的錯誤日誌
GIT_TRACE=1 GIT_CURL_VERBOSE=1輸出完整的 HTTP 請求

6.2 何時不該用 LFS

Git LFS 不是萬靈丹。它擅長的是「與程式碼緊密綁定、需要跟著 commit 一起版本化、數量與大小適中的二進位檔」,例如遊戲素材、測試資料、小型模型。以下情況可以考慮其他方案:

方案適合情境特點
Git LFS中等規模二進位檔,需與程式碼同步版本化生態系最成熟,幾乎所有平台都支援,對使用者透明
git-annex大量檔案、多種儲存後端、分散式備份以符號連結管理檔案,可追蹤每份內容存在哪些位置,功能強大但學習曲線陡
DVCML 資料集與實驗管線Git 只存 .dvc 中繼資料,資料放在 S3 / GCS 等遠端;內建管線與實驗追蹤,與 Git 託管平台無關
Hugging Face Hub(Xet)公開或團隊共享的模型與資料集區塊層級去重,頻繁迭代大型檔案時遠比 LFS 省空間與流量
物件儲存 + 清單檔超大型資料(TB 級)、不需要細粒度版本倉庫中只記錄 URL 與雜湊,由腳本下載與校驗,最簡單也最便宜

幾個判斷原則:

  • 檔案頻繁小幅修改(例如持續微調的模型 checkpoint):LFS 每次都存一份完整副本,成本會線性增長。區塊去重的方案(Xet)或只保留關鍵版本的做法更適合。
  • 資料量達到數百 GB 以上:託管平台的 LFS 計費通常比直接使用物件儲存昂貴得多,DVC 或物件儲存加清單檔更划算。
  • 需要資料血緣與實驗管理:DVC 提供的不只是儲存。
  • 檔案根本不需要版本化:例如 build 產物,應該放在 Release 或套件倉庫,而不是 LFS。

參考資料

結語

Git LFS 的設計本質上很樸素:用 Git 原生的 filter 機制把大檔案換成 pointer,再用一套簡單的 HTTP API 把內容搬到別處。理解了 clean/smudge 與 Batch API 這兩個核心,大部分的使用問題與部署問題都能推理出原因。日常使用時,記得先寫好 .gitattributes 再加入檔案、在 CI 中善用快取、留意平台的計量方式;若選擇自架,應把儲存、備份、頻寬、反向代理和權限管理一併納入設計;自架可改變成本與容量的控制方式,但不會消除成本。