在 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 由兩個方向的指令組成:
| 方向 | 觸發時機 | 作用 |
|---|---|---|
| clean | git add:工作目錄 → 暫存區 / 物件庫 | 把工作目錄中的內容「清理」成要存進 Git 的版本 |
| smudge | git checkout:物件庫 → 工作目錄 | 把 Git 中存的版本「弄髒」成要放在工作目錄的內容 |
名稱的意象是:存進 Git 的版本是「乾淨的」,工作目錄裡的版本是被「抹上」本地內容的。兩個指令都從 stdin 讀入檔案內容、從 stdout 輸出轉換後的內容,Git 只負責呼叫,完全不在意中間做了什麼。
設定一個 filter 需要兩個部分:
- 在 Git 設定中定義 filter 的指令(
.git/config或~/.gitconfig); - 在
.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,要求客戶端上傳完成後回報確認。
這個設計有兩個好處:
- 批次處理:一次請求可以詢問數百個物件,減少往返。
- 控制面與資料面分離: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。具體來說,它做兩件事:
- 在全域設定
~/.gitconfig中寫入 2.2 節提到的[filter "lfs"]區段,讓 Git 知道遇到filter=lfs的檔案時要呼叫git-lfs。 - 如果當下位於某個倉庫中,順便為這個倉庫安裝
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 設定錯誤。 - 驗證失敗:反向代理沒有正確轉發
Authorizationheader,或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 時下載失敗。依序檢查:
- 網路與驗證:
git lfs env確認端點,GIT_TRACE=1 git lfs pull看詳細錯誤。 - 物件是否真的存在於伺服器:有人提交了 pointer 卻沒有成功推送物件(例如在沒有安裝 LFS 的環境中操作,或繞過了
pre-pushhook)。 - 配額是否用盡。
想先完成 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 | 大量檔案、多種儲存後端、分散式備份 | 以符號連結管理檔案,可追蹤每份內容存在哪些位置,功能強大但學習曲線陡 |
| DVC | ML 資料集與實驗管線 | 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 LFS Batch API
- Git LFS 檔案鎖定 API
- Git LFS migrate 指令手冊
- Git LFS prune 指令手冊
- GitHub:Git LFS 配額與限制
- GitHub Actions checkout:
lfs輸入 - GitLab:LFS 物件儲存
- Gitea:Git LFS 設定
- Hugging Face Hub:Xet 儲存後端
- GitHub:Git 屬性與 filter driver
結語
Git LFS 的設計本質上很樸素:用 Git 原生的 filter 機制把大檔案換成 pointer,再用一套簡單的 HTTP API 把內容搬到別處。理解了 clean/smudge 與 Batch API 這兩個核心,大部分的使用問題與部署問題都能推理出原因。日常使用時,記得先寫好 .gitattributes 再加入檔案、在 CI 中善用快取、留意平台的計量方式;若選擇自架,應把儲存、備份、頻寬、反向代理和權限管理一併納入設計;自架可改變成本與容量的控制方式,但不會消除成本。