一個幾十 GB 的資料夾躺在某台配了 GPU 的機器上。裡面是權重檔、一份 tokenizer、一個 config、還有當初隨手寫的 README。現在要讓另外三台機器拿到一模一樣的東西。注意是「一模一樣」,不是「看起來差不多」。

這件事到今天為止,沒有一個大家都同意的做法。

打成 tar 丟過去,然後呢

最直覺的做法是打包。tar 一下,傳過去,對面解開。檔案確實會到。

問題出在你要怎麼稱呼它。

打完的那包,你叫它 model-v2.tar。一週後有人換掉 tokenizer,重打一份,還是叫 model-v2.tar,只是尾巴多黏一個 -new。再過兩週,三台機器上各自躺著一個名字不一樣、內容可能相同也可能不同的壓縮檔,而你沒有任何機械化的辦法判斷它們是不是同一份東西。人腦記不住,ls -la 也告訴不了你。

容器生態早就把這題解掉了:拿內容本身的雜湊值當地址。digest 一樣就是同一份,不用相信檔名,不用相信任何人的口頭保證。

但這招沒辦法直接套到你手工打的 tar 上。tar 會把每個檔案的修改時間、擁有者的 uid 跟 gid、權限模式一起寫進去,而這些欄位是跟著你打包當下那台機器的磁碟現值跑的。同一份模型、同一個人、隔一小時打第二次,出來的位元組就不一樣,雜湊值當然也對不上。你想要的「同樣的東西給同樣的名字」,從第一步就斷了。

換成各家自己的 registry 也沒比較好

第二條路是別自己搬,交給平台。Hugging Face 有 repo,各家雲端有各家的 model registry,每一家都能幫你存、幫你拉。

然後你會發現每多接一家,部署流程就多一個 downloader、多一組憑證、多一種只在半夜炸掉的失敗模式。更痛的是,你公司內部那套已經跑了幾年的東西,Harbor、containerd、CRI-O、映像掃描、上架審核,一個都用不上。它們全都只認容器映像,而你手上這包不長那個樣子。

而且交給平台之後,最初那個問題其實一動也沒動。你還是不知道 A 機器上跑的那份、跟你上週核准過的那份,是不是同一個位元組序列。平台幫你把檔案放好了,沒幫你把「這是哪一份」這件事變成可以驗證的東西。

繞了一大圈才發現,缺的不是儲存空間,是定址方式。

它幾乎沒有發明任何東西

ModelPack 這個規格對自己的描述只有一句話:

“an open standard for packaging and distribution of Artificial Intelligence models as OCI artifacts, adhering to the OCI image specification.”

選 OCI 的理由也寫得很白,就是要吃 “the existing OCI ecosystem, thereby facilitating efficient model management, deployment and serving in cloud-native environments.”

真正值得停下來看的,是它在規格層面做的克制。

一個新格式想擠進既有生態,最常見的做法是替自己定義一種新的 manifest 型別,宣告「我是不同的東西」。ModelPack 沒有走這條。它規定 manifest 的 mediaType 必須還是 OCI 原本那一個:

1
2
3
mediaType:    application/vnd.oci.image.manifest.v1+json
artifactType: application/vnd.cncf.model.manifest.v1+json
config: application/vnd.cncf.model.config.v1+json

分辨「這是模型、不是容器映像」這件事,整個交給 artifactType 那一格。config 物件另外掛一個自己的媒體類型,裝模型專屬的描述資訊。

寄包裹可以拿來想這件事。運送單的格式一個字都不改,所以郵局的分揀機、輸送帶、條碼掃描器全都照舊運作,沒有人需要為了寄一台冰箱重新設計整條產線。改的只有單子上「內容物」那一格填什麼。分揀機看不懂「冰箱」兩個字完全沒差,它本來就只讀條碼;真正需要知道裡面是冰箱的人,在收件端自己看那一格就好。

這個選擇的代價與收益都很直接。代價是你被綁死在 OCI 的結構裡,manifest 能放什麼、layer 怎麼疊,都不是你說了算。收益是你今天就有 digest 定址、有 layer 去重、有既有的權限控管與 mirror 機制,而且不用說服任何人升級他們的 registry。只換掉 tokenizer 的那一版,理論上只需要重推那一層,幾十 GB 的權重不用再走一次網路。

反過來想一下另一條路的樣子會更清楚。如果它替自己定義一種新的 manifest 型別,那每一台 registry 在收到這包東西的時候,第一個動作就是問「這是什麼」,然後多半是拒收或當成不明物件擺著。你得等每一家實作、等每一家升版、等每一家的相容性矩陣把你納進去。一份規格的採用速度,很大一部分由它要求別人改多少東西決定,而這裡要求的接近零。

規格叫你不要壓縮

推 layer 之前先 gzip 一下,這在容器世界幾乎是反射動作,連想都不會想。這份規格叫你不要。

理由拆開只有兩句話。壓縮之所以能省空間,是因為資料裡有重複的樣式可以編碼,文字、原始碼、log 檔滿滿都是重複。模型權重不是那種東西,它是一大片訓練出來的浮點數,位元分布接近隨機,壓縮演算法翻遍整個檔案也找不到什麼可以省。於是壓縮率趨近於一比一,你空間幾乎沒省到,卻讓 runtime 每次啟動都得多燒一輪 CPU 把它解回原狀。純虧。

規格自己的原話是這樣,也是整份文件裡我覺得最好看的一條:

“It is recommended to package model weights files without compression to avoid unnecessary overhead of decompression by the container runtime as model weights files are typically incompressible.”

一份規格願意寫下「這個地方不要照預設做」,而且把理由講到讀的人可以自己驗證,比它在首頁宣稱自己是 open standard 有說服力得多。預設值是會傳染的。容器生態裡有一大堆「因為大家都這樣所以就這樣」的設定,能被具體指出「這一條在你的場景是淨成本」的很少。

同一種克制也出現在可重現打包那一節。規格把檔案排序訂死成字典序,並且要求 mtime、uid、gid、mode 一律填 “known, constant values rather than the current values on disk”,平台專屬的 metadata 一律略掉。

回頭看前面打 tar 的那條死路,它解的就是那一步。同一份模型打包兩次要得到同一個 digest,這被寫成規格層的要求,不是附註裡一句「建議你注意一下」。

一個模型交付物到底裝了什麼

規格把 layer 的媒體類型全部列死了,五個類別,每個類別各四種封裝方式:

這一類裝什麼 媒體類型(後面接 .raw.tar.tar+gzip.tar+zstd
權重 application/vnd.cncf.model.weight.v1
權重設定 application/vnd.cncf.model.weight.config.v1
文件 application/vnd.cncf.model.doc.v1
程式碼 application/vnd.cncf.model.code.v1
資料集 application/vnd.cncf.model.dataset.v1

權重那一類展開來是這四個完整字串,其他四類的形狀完全一樣:

1
2
3
4
application/vnd.cncf.model.weight.v1.raw
application/vnd.cncf.model.weight.v1.tar
application/vnd.cncf.model.weight.v1.tar+gzip
application/vnd.cncf.model.weight.v1.tar+zstd

這張表其實是在回答一個沒被寫成問句的問題:一個模型交付物,到底該包含哪些東西。規格給的答案是五樣,而且 dataset 也在裡面。訓練它的資料集不用另外附一份清冊,也不用寫在 README 裡讓人自己去翻。它跟權重一起裝進同一個 artifact,共用同一套 digest 與同一套散布路徑。

要交代一個模型的來歷,你有兩種做法:把東西全部裝在一起,或是把東西列在一張清單上、各自留指標。裝在一起的好處是拿到就是全套,digest 一算就知道有沒有被動過手腳;壞處是資料集通常比權重還大,而且授權條件往往跟權重不一樣,硬綁在同一包裡不見得每個情境都能推得動。列清單的好處剛好相反,輕、可以分開授權,但你得自己去確認清單上那些指標現在還指得到東西。ModelPack 選了前者,而且是把它寫進媒體類型清單裡,不是留一個「你也可以」的選項。

我的判斷是這個選擇在內部場景成立、對外散布會咬人。公司內部搬自己訓出來的模型,資料集跟權重同一個來源、同一份授權,全裝一包省掉的是一整排對帳工作;換成對外發布公開權重,那個比權重更大、授權條件還可能不一樣的資料集就是推不動的那一層,最後多半還是得分開走。所以「全套」這個前提只有在資料集跟權重歸同一個人管的時候才成立。不歸同一個人管的時候,你拿到的那包東西只會大到推不上去。

.raw 是單檔直接當一層,.tar 是打包不壓,後面兩個才是壓縮版本。前面那條「權重不要壓」的建議,在這裡變成一個你自己要選的欄位,規格沒有從語法層面禁止你壓——它只是把該知道的告訴你,然後讓你選。

它現在還沒到的地方

把宣傳稿放一邊,直接看 gh api repos/modelpack/model-spec 回來的欄位會誠實很多。下面這幾格是寫這篇當下的值,過幾個月自己再抓一次會比較準。授權是 Apache-2.0,archived 是 false,repo 從 2024-08-28 開到現在,最後一次 push 落在 2026-09-07。專案給自己的一句話介紹是 “An Open Standard for Packaging, Distributing and Running LLMs in Cloud-Native Environments”。

然後是不太好看的那半邊:最新的 tag 是 v0.0.7,而且一個 GitHub Release 都沒發過。Docker 那邊確實接了(CNCF 的部落格 2026-08-12 有一篇說明),不過要明講 --format=cncf 才會產出這個格式。它還不是任何人的預設值。

一個自稱 open standard 的東西停在 0.0.x,這兩件事不衝突,但值得你在把它排進技術選型之前先知道。

講清楚這篇的位置:我只讀了規格原文與 repo 的元資料,沒有實際推過任何一個模型上 registry,也沒有量過「不壓縮」在真實權重上省下多少啟動時間。上面所有媒體類型字串都照抄規格,數字來自 GitHub API 回的欄位,不是二手摘要。

我的判斷是這條路走得通,理由不在規格寫得多漂亮,在於它刻意把自己降級成「既有 registry 的一種內容物」,而不是另一座要別人來朝聖的新機場。新標準最常見的死法是要求全世界先改建,這份規格把改建成本壓到接近零。

會讓我改口的條件也很具體:如果再過一年版本號還在 0.0.x、--format=cncf 還是得手動加、主流 registry 對 artifactType 的支援一直卡在半套,那它就只是又一份寫得不錯但沒人用的規格。這種東西每年都有幾份。

順帶一提,這篇談的是封裝與散布,跟前陣子那篇〈你下載的模型權重,是誰簽的名?Model Signing 該不該進你的流程〉不在同一層。那篇問的是「這份權重是誰簽的」,這篇問的是更前面一步:「這份權重要用什麼形狀送出門」。先有形狀,簽章才有東西可以簽。

回到那個資料夾

那個幾十 GB 的資料夾還躺在那台機器上。

如果這套東西真的普及,你要做的事會從「想辦法把檔案搬過去」,變成 push 一次、其他機器各自 pull,而且每一台都能用 digest 確認自己拿到的跟你推上去的是同一份。今天還沒到那一步,你多半仍然得先接受各家各的做法,繼續在部署腳本裡養那幾個 downloader。

不過真正值得帶走的是底下那個框架:這件事從頭到尾的困難,從來不在傳輸。頻寬會變便宜,硬碟會變大,這兩條線都在往好的方向走。難的是定址——你需要一個方法指著某個東西說「就是它」,而且這句話要在三台機器上、三個月後、三個不同的人手上都還成立。檔名做不到。內容雜湊做得到,而世界上早就有一整套基礎設施專門在做內容雜湊,只是它到現在為止都只肯裝容器映像。

ModelPack 在做的事,說穿了就是去問那套基礎設施願不願意多裝一種東西。


參考來源