同一份星表資料,zstd -3 壓到 1.31 倍、xz -9 壓到 1.64 倍,OpenZL 壓到 2.06 倍——而且壓縮吞吐量比 zstd 還快。讓它同時拿下這兩件事的,不是更兇的 entropy coder,而是有人先把資料的格式講給壓縮器聽了。
OpenZL——把格式講給壓縮器聽
通用壓縮器看到的永遠是一串 byte。zstd、xz、gzip 不知道你餵進去的是一張星表、一份 Parquet、還是一段 protobuf——它們在 byte stream 上找重複、做 entropy coding,能榨出多少全看資料碰巧長得多有規律。OpenZL 是 Meta 在 2025 年 10 月開源、並已在內部 production 大量使用的壓縮框架,它換了一個前提:使用者用一份格式描述告訴它「這份資料的結構長這樣」,OpenZL 就照這份結構建一個專門針對該格式的壓縮器。Meta 的說法是「OpenZL takes a description of your data and builds from it a specialized compressor optimized for your specific format」。關鍵在於,所有這些長相各異的專用壓縮器,產出的檔案都能被同一個通用解壓器讀回來。
底下幾節依序拆開這個框架的幾個構件:先是把結構交給壓縮器的那份描述(SDDL),再是壓縮的真正引擎(codec 的有向無環圖),接著是讓「一個壓縮器、無數種格式」得以成立的那個通用解壓器,最後用 SAO 星表資料集上的實測數字說明「高壓縮比 + 高吞吐」為什麼同時成立、為什麼對資料中心與 AI workload 重要。先從一個問題開始:把結構講得越清楚,壓縮比到底能往上走多少?下面這個滑桿掃過四個真實的設定點。
drag the slider across four real benchmark setups · 4 設定點
SDDL:把二進位格式寫成一份描述
第一個構件是描述本身。OpenZL 提供 SDDL(Simple Data Description Language),文件給的定義很直白:「SDDL is a domain-specific language for describing binary file formats.」它的目的是讓你「formally specify the structure of binary data so it can be efficiently processed by compression algorithms, transformation tools, and other downstream systems」。換句話說,SDDL 不是壓縮演算法,是一份說明書——告訴系統「這份檔案前 N byte 是 header、後面是一連串固定欄位的 record、第三欄是排序過的整數」。
SDDL 的構件是工程師熟悉的那一套:type、record、array、variable、expression,再加上條件邏輯的 when 區塊與驗證約束的 expect 述句。重點不在語法新不新,而在於它把「資料的形狀」變成壓縮器的一個明確輸入。Meta 在發表文裡點出通用方法的代價:「Using generic methods on structured data leaves compression gains on the table.」一串混了 header、整數欄、字串欄的 byte,在 zstd 眼裡是均質的;但只要你說清楚哪段是什麼,壓縮就能「focus on a sequence of reversible steps that surface patterns before coding」——先用一連串可逆變換把規律攤開,再交給 coder。
when 與 expect 這兩個構件值得多看一眼,因為它們把 SDDL 從「靜態 schema」推向「能描述真實格式」的工具。真實的二進位格式很少是一張固定表格:某個欄位的存在與否,往往取決於前面某個 flag 的值;某段資料的長度,寫在前面的 length 欄裡。when 區塊讓描述能表達「滿足某條件時這段才存在」的條件欄位,expect 述句則是驗證約束——宣告「這裡的 magic number 應該是某值」「這個長度欄不能超過某上界」。對壓縮器而言,這些不只是防呆:每多一條被 expect 確認過的不變式,壓縮器就多一分可以倚賴的結構假設,少一分得保守處理的不確定性。把資料的形狀講得越完整,可逆變換能站穩的地基就越大。
描述不一定要全手寫。除了 SDDL,OpenZL 也接受自訂 parser;更進一步,它有一個離線的 training 環節,會去探索不同的變換選擇,「builds an effective compression config that can be re-employed for similar data」——也就是替一類資料找出一份夠好的壓縮計畫,之後同類資料重複套用。quick-start 文件裡的 sra0 樣本就示範了這個落差:只給一個現成 profile,OpenZL 壓到 1.50×(同檔 zstd-19 是 1.22×、gzip-9 是 1.19×);經過 training,同一份資料壓到 3.47×,文件原話是「Training improved compression ratio by 130.48%」。描述講得越精確、訓練探索得越深,壓縮器對這份資料的理解就越貼。
graph model:codec 串成的有向無環圖
有了描述,真正幹活的是壓縮引擎。OpenZL 的白皮書把它的核心抽象稱為「the 'graph model' of compression, a new theoretical framework for representing compression as a directed acyclic graph of modular codecs」——把一次壓縮表示成一張由模組化 codec 構成的有向無環圖。每個 codec 是一個可逆的小變換:bitpack、delta、tokenize、entropy coding,各做一件事。描述告訴系統資料怎麼拆,graph 決定每一段資料流過哪些 codec、以什麼順序。
SAO 星表是 Meta 自己舉的例子,也最能說明 graph 怎麼運作。流程是:先把 header 跟 record 分開;把每筆 record 拆解成一條條 field stream(同一欄的值聚成一條流);再對每條流套上適合它的變換——排序過的座標欄用 delta coding(只存相鄰差值)、低基數的欄位用 tokenization(把重複值換成短 token);最後每條流各自再做最適合自己的 entropy 壓縮。重點在「各自」:通用壓縮器只有一條 pipeline 套在整串 byte 上,OpenZL 的 graph 讓每條 field stream 走自己最佳的那條路徑。文件把這張圖的地位講得很重:「The compression graph is the ultimate driver of both compression and decompression.」它同時決定怎麼壓、也決定怎麼解。下面這個 widget 把 SAO 這條 graph 攤開成幾個 codec 階段,點任一階段看它負責什麼。
click any codec stage to read its job · 5 stages
SAO 星表的 codec graph——資料先拆解,每條 stream 走自己的路徑
click any codec node above
split header · 職責
把 header 與 record body 分成兩條獨立的流。header 通常是少量、結構固定的 metadata,record body 是大量同構的資料——兩者的統計分布不一樣,混著壓會互相拖累。
不負責的事:怎麼壓。它只做切分,把後面的決策留給下游節點。
decompose records · 職責
把列式的 record 轉成欄式的 field stream——第一欄全部聚在一起、第二欄全部聚在一起。同一欄的值統計性質相近,規律(單調、低基數、固定步長)這時才看得見。
不負責的事:判斷每條流該用哪個 codec,那是下游 delta / tokenize 節點的事。
delta · 職責
對排序過或近單調的欄位(星表裡的座標就是這種),只存相鄰元素的差值。一串遞增的大整數變成一串接近零的小差值,entropy coder 面對「多半是小數字」的分布能壓得更兇。
適用前提:欄位本身要有順序性。對亂序欄位 delta 反而會放大數值範圍。
tokenize · 職責
對低基數欄位(distinct 值少、重複多)建一張字典,把原值換成短 token。寬欄位塌縮成「一張小字典 + 一串索引」,索引本身又是低基數、好壓。
適用前提:distinct 值要遠少於總筆數,否則字典本身就跟原資料一樣大。
entropy · 職責
graph 的葉節點。每條被前面變換攤平過的 stream,各自做最適合自己分布的 entropy 壓縮。關鍵是「各自」——delta 過的座標流與 tokenize 過的索引流分布不同,套不同參數,而不是一條共用 pipeline 一視同仁。
不負責的事:產生規律。它吃的是上游已經攤平好的資料,只負責把剩下的 redundancy 編掉。
這裡有個容易混淆的點:graph 不是「先全套變換、再壓縮」的兩段式流程,變換本身就是壓縮的一部分。delta、tokenize 這些 codec 不直接縮小資料,它們做的是把 redundancy 從「coder 看不見的形式」搬成「coder 看得見的形式」。一串遞增整數的資訊量其實很低,但 zstd 在原始 byte 上看不出這點,只能在表面找重複的位元組序列;delta 之後同一串資料變成一連串接近零的小差值,redundancy 攤在明面上,coder 就壓得動了。這正是 Meta 說的「Compression can then focus on a sequence of reversible steps that surface patterns before coding」——壓縮的主力工作從「直接編碼」前移到「先攤開規律」。OpenZL 的價值不在發明新的 entropy coder,而在於它讓你能為每一段資料、用一張組合圖,把規律提前攤開到通用 coder 吃得下的地方;同一套標準 codec 庫,靠不同的接法服務天差地別的格式。
通用解壓器:自描述 wire format
到這裡會冒出一個明顯的問題:如果每種格式、甚至每次 training 都產出一個不同的壓縮器,那解壓端怎麼辦?要為每個壓縮器配一個對應的解壓器嗎?OpenZL 的答案是不用——所有檔案共用同一個解壓器。Meta 的原話:「all OpenZL files can be decompressed using the same universal OpenZL decompressor」,而且不管它當初是用哪個壓縮設定壓的。CLI 文件講得更具體:「decompression doesn't require any --profile: data compressed with openzl is always decodable, regardless of which --profile was used for compression.」
能做到這件事,靠的是把壓縮計畫本身寫進檔案。白皮書的講法是:「OpenZL compresses data into a self-describing wire format, any configuration of which can be decompressed by a universal decoder.」壓縮時所用的那張 codec graph——哪些節點、什麼順序、各自的參數——被序列化進壓縮後的 frame;解壓器讀出這張圖,反向把每個 codec 逐一逆轉回去。因為前面提過「The compression graph is the ultimate driver of both compression and decompression」,同一張圖正向走是壓縮、反向走是解壓。解壓器不需要事先認得你的格式,它只需要認得 codec 庫裡那組標準構件,剩下的接法從 frame 裡讀。下面的 widget 把同一份 SAO 資料兩種看法疊在一起,拖動分隔線比較。
drag the divider · generic byte stream vs. OpenZL field streams
左半是通用壓縮器看到的——一串均質 byte;右半是 OpenZL 看到的——header 與三條各有結構的 fiel…
同份 SAO 資料,zstd 看成一串 byte 壓 1.31×,OpenZL 拆成多條 field stream 各走 codec,壓到 2.06×。
通用解壓器的好處不只是省一個 binary。Meta 列了三個它在 production 重視的後果:因為全公司只有一個解壓器,安全稽核只要審這一份;壓縮端的改進可以 fleet-wide 推開,不必擔心解壓端跟不上;向後相容因此有了硬保證。GitHub 上 README 給的承諾很具體:「payloads compressed with any release-tagged version of the library will remain decompressible by new releases of the library for at least the next several years.」壓縮端可以年年換更聰明的 graph、更好的 training,舊資料照樣解得開——因為解壓邏輯不在壓縮器裡,而在資料自己攜帶的那張圖加上那組固定的標準 codec 庫。白皮書把這層效益總結成「its universal decoder eliminates deployment lag」:新壓縮器一寫好就能用,不必等解壓端先佈署到每一台機器。
值得停下來想清楚這個分工為什麼成立。傳統上「壓縮器」與「解壓器」是一對綁死的程式:你換了壓縮邏輯,就得同步換解壓邏輯,否則舊解壓器讀不懂新格式。OpenZL 把這層耦合切斷的辦法,是讓壓縮後的 frame 自己攜帶「該怎麼被解開」的全部資訊——也就是那張 codec graph 的序列化形式。解壓器不認得你的格式、不認得你這次用了什麼 training,它只認得標準 codec 庫裡那組固定的可逆構件,再按 frame 裡讀到的圖把它們逐一逆轉。這也解釋了 CLI 那句「decompression doesn't require any --profile」:解壓端不需要被告知任何配置,因為配置就寫在資料裡。白皮書還點出這個設計的另一個副作用——把所有壓縮策略的多樣性收斂到「不同的圖」這一層,真正要長期維護、要嚴格稽核的程式碼就只剩那組標準 codec 庫,安全風險集中在一塊小而被反覆驗證的表面上,而不是散在每個團隊各自寫的解壓器裡。對一個要在整個機隊上跑十幾年的壓縮系統,這種「壓縮可以一直演進、解壓器保持單一且穩定」的形狀,才是它敢給出數年向後相容承諾的底氣。
SAO 數字:為什麼「高壓縮比 + 高吞吐」一起拿下
把機制講完,回到那組讓人多看一眼的數字。傳統的直覺是壓縮比與速度互斥——xz -9 用 1.64× 的壓縮比換來了慘烈的 3.1 MB/s 壓縮速度,就是這條 trade-off 曲線的典型。OpenZL 在 SAO 星表資料集上的官網數字打破了這個直覺:壓縮比 2.06×,壓縮速度 203 MB/s(比 zstd -3 的 115 MB/s 還快),解壓 822 MB/s。Meta 對 OpenZL 的一句話定位是「lossless compression for structured data, with performance comparable to specialized compressors」——拿通用框架的可維護性,換到專用壓縮器等級的表現。下面這張表把三者擺在一起,點欄位標題排序。
速度能跟上的原因,回頭看就是前面那張 graph。通用壓縮器把整串 byte 餵進一條較重的 pipeline,要榨出更高壓縮比往往得開更大的 search window、更慢的 match finder——xz 的 3.1 MB/s 就是這樣換來的。OpenZL 的 graph 反過來:因為描述已經把資料拆成幾條結構單純的 field stream,每條流上跑的是輕量、針對性的 codec(delta 就是逐元素相減、tokenize 就是查字典),重活在「拆解」這一步用結構知識省掉了,留給 entropy coder 的是已經攤平、好啃的資料。換句話說,高壓縮比不是靠在 coder 那端加碼算力拿到的,而是靠前段的結構變換把問題變簡單——這就是為什麼壓縮比往上走的同時,吞吐量還能守在 zstd 的量級。對一條每秒要吞下幾百 MB 的 ingestion pipeline,這個性質決定了 OpenZL 能不能站在熱路徑上,而不是只能當離線壓縮工具。
click column header to sort · 4 columns × 3 rows
| 壓縮器 | 壓縮比 (×) | 壓縮 (MB/s) | 解壓 (MB/s) |
|---|---|---|---|
| zstd -3 | 1.31 | 115 | 890 |
| xz -9 | 1.64 | 3.1 | 30 |
| OpenZL | 2.06 | 203 | 822 |
一個必要的誠實標註:Meta 的發表文裡,同一個 SAO 例子給的速度是另一組數字——壓縮比一樣 2.06×,但壓縮速度 340 MB/s、解壓 1200 MB/s,量測平台註明是 M1 CPU、clang-17。官網與發表文的壓縮比一致、速度量級不同,合理的推測是兩者跑在不同硬體或不同建置設定上(發表文明寫 M1/clang-17,官網未註明平台),所以這裡把兩組數字分開引、不混算。要點不在哪一組絕對值,而在兩組都呈現同一件事:壓縮比明顯高過 zstd,吞吐量卻維持在同個量級甚至更快。兩組數字在壓縮比這一項完全一致、都是 2.06×——壓縮比只取決於資料與壓縮策略,不隨硬體變動,會跟著平台跑的只有速度。所以拿來支撐「結構感知贏過通用壓縮」這個論點的,其實是那個跨兩份來源都站得住的壓縮比,而速度數字我只用來說明它沒有為了高壓縮比犧牲吞吐,量級對得上即可,不去比較哪一台機器跑得快。
這件事為什麼重要,得看資料中心的成本結構。在一條高吞吐的儲存或傳輸 pipeline 上,壓縮器若慢到變成瓶頸,省下的空間就被吐不出去的吞吐量吃掉了——xz 的 3.1 MB/s 就是這種情況,壓縮比好看,但只能拿去壓冷資料。OpenZL 想站的位置是「壓縮比往 xz 靠、速度往 zstd 靠」的那個角落:既值得放進熱路徑,又真的省下可觀的儲存與頻寬。Meta 點名的目標使用者是「engineers that deal with large quantities of specialized datasets (like AI workloads for example) and require high speed for their processing pipelines」——AI workload 的訓練資料、特徵檔、模型 checkpoint 多半是高度結構化又體積龐大的資料,正好落在「通用壓縮器留了一截 gain 在桌上、又吃不起慢速壓縮」的夾縫裡。
另一個對採用者實際的數字是工程成本。白皮書說 Meta 內部佈署「consistent improvements in size and/or speed, with development timelines reduced from months to days」——做一個專用壓縮器從幾個月縮到幾天。傳統上要為某個格式寫專用壓縮器,等於要連配套的專用解壓器一起寫、測試、佈署、長期維護;OpenZL 把「設計壓縮策略」與「維護解壓器」這兩件事拆開——前者用描述加 training 快速迭代,後者由那個共用的通用解壓器一次解決。GitHub README 也把成熟度講清楚:「we consider the core to have reached production-readiness, and OpenZL is used extensively in production at Meta.」這不是一個論文 demo,是已經在正式環境跑的東西。
What this enables:把「資料的結構」變成壓縮器的一個顯式輸入後,你能用一張 codec 的有向無環圖、針對每一種格式快速組出專用壓縮器——而它們全部共用一個自描述、向後相容的通用解壓器,於是專用壓縮器的高壓縮比與高吞吐,第一次能在不犧牲可維護性的前提下同時拿下。