~/blog/quantization-lab

LLM Deep Dive · part 6

[LLM Deep Dive] 量化實驗室:什麼是量化、怎麼量化、為什麼會有損失

cat --toc

TL;DR

做了一個可以點的量化實驗室:model.ai-muninn.com/zh-TW/quantization/。選格式、追一個權重走完四步,看它在哪一步壞掉。同一組 64 個權重從 128 bytes 壓到 36 bytes,代價是 INT4 下四個教學值裡有兩個直接變成 0。誤差有三個來源,其中兩個我做之前沒搞懂:刻度多粗由整組最大值決定,以及 scale 自己也是近似值

深色終端機風格的社群封面。左側是一條 INT4 數線,兩邊各七格;−15.875 落在最左端並標成綠色,−0.875 與 0.375 兩個紅點擠在中央同一個紅色虛線方框裡,方框下方寫著「這一格裡的兩個權重都變 0」,4.25 是另一個青色點。右側四張青框資訊卡由上而下寫著 0.375 → 0(INT4 下直接消失)、15.875 ÷ 7(最大值決定所有人的刻度)、4 個值 2 個歸零(同一組共用一把尺)、128 → 36 bytes(省 71.9%)。標題是「為什麼量化會有損失」,底部寫著「一個特別大的數,會拖累整組」。

前言

量化的算式短到可以寫在一行:除以 scale、取最近的刻度、乘回 scale。(ONNX 的規格還多一個 zero_point 位移項;本頁走對稱量化,那一項是 0,所以看不到它。)

我把這行字讀懂過很多次。但真要說「誤差是在哪一步生出來的」,我會說「取整數那邊吧」——然後就講不下去了。所以我做了一個可以點的東西:量化實驗室,選一個格式、追一個權重,看它在四步裡怎麼變形。

做完之後,我對「誤差從哪來」的答案從一個變成三個。

什麼是量化:用更少的位元裝同一組數字

模型的權重原本是浮點數,BF16 每個佔 16 bits。量化就是改用更少的位元存同一組數字——INT4 每個只佔 4 bits。

實驗室用 64 個權重當一組來算這筆帳:

儲存量
64 個 BF16 權重128 bytes
64 個 INT4 權重32 bytes
+1 個共用 scale(FP32)4 bytes
合計36 bytes

省 71.9%。左邊那個立方體畫的就是這筆帳:切格式的時候 64 塊會散開、重新編碼、再組回去,體積跟著縮。

代價在哪?4 bits 只有 16 種組合,而這裡用的對稱範圍取 −7 到 7、共 15 格(捨掉 −8 換來正負對稱)。原本可以是任意浮點數的權重,現在只能落在這 15 格裡。落不上去的,就要挪到最近的那一格。

怎麼量化:四個步驟

進頁面預設是 BF16,什麼都還沒發生。點 INT8、FP8、INT4 或 FP4 其中一個,下面才會帶出四個步驟。

  1. 算 scale——找出整組 64 個權重裡的最大絕對值,除以格式的正整數上限。INT4 上限是 7。
  2. 換成刻度——把權重除以 scale,取最近的整數,超出範圍就壓到邊界。
  3. 儲存——存的是「一個共用 scale + 每個權重的整數代碼」。代碼 3 不是權重 3。
  4. 還原——代碼乘回 scale,跟原值比。

頁面上那四個數字按鈕(0.375−0.8754.25−15.875)可以換追蹤對象。建議第一次選 INT4、追 0.375

誤差在第二步生出來——但它有多大,是第一步決定的。

損失來源一就是這個取整數:落不到刻度上的權重,被挪到最近的一格。這個大家都知道。下面兩節是另外兩個,我做之前都沒想到。

損失來源二:刻度有多粗,由整組的最大值決定

教學組只有四個值:−15.875−0.8750.3754.25。第一個比其他三個大了一個量級。

INT4 兩邊各七格,scale 是 15.875 ÷ 7 ≈ 2.2679。一格就跨 2.2679。於是:

同一把 INT4 尺的兩種情況。上排組裡有 −15.875,一格跨 2.2679,−0.875 與 0.375 兩個紅點落在中央同一格裡,兩個都變 0;下排拿掉 −15.875,一格跨 0.6071,同樣兩個權重各自落在不同格

0.375 ÷ 2.2679 = 0.165,四捨五入是 0−0.875 也是 0。四個值裡有兩個在 INT4 直接消失。

拿掉 −15.875,同組最大變成 4.25,scale 變 0.60710.375 就還原成 0.6071——仍然有誤差,但不是歸零。差別不在 0.375 本身,在它跟誰分在同一組。

這種明顯偏離同組其他數字的值叫離群值。它為什麼是量化演算法的頭號敵人、K-quant 的 super-block 與 TurboQuant 的旋轉各自怎麼對付它,Part 1 寫得比這裡完整,我不重講。實驗室能補的只有一件事:那些演算法在躲的東西,你可以點一下看它發生。

這也是為什麼要分組。 把 64 個權重切成幾小組、各組算各自的 scale,離群值的影響就被關在它那一組裡。代價是每組都要多存一個 scale。實驗室左側矩陣的「每組權重數」可以直接調來看這個取捨。

損失來源三:scale 自己也是近似值

這件事我做之前沒想過。

直覺上,落在刻度正上方的權重應該完美還原。組裡的最大值最該如此——它定義了 scale,代碼剛好是格式上限。

選 INT4、追 −15.875,還原值是:

-15.874999523162842

差了 4.77e-7取整數那一步沒有誤差−15.875 ÷ 2.267857… = −7 剛剛好。誤差在別的地方:15.875 ÷ 7 這個數在 FP32 裡存不下,實際存的是 2.267857074737549。乘回這個近似 scale,就回不到原來的位置。

同一個權重在 FP8 與 FP4 也是同樣的 4.77e-7。

這個差距小到不影響任何事,但它打破了「誤差只來自取整數」的直覺:還原用的那把尺,本身也是被量化過的。

那「還原」在推論時到底發生什麼?

第四步叫「還原」,我一開始被自己的設計誤導:如果每次用模型都要把權重還原成 BF16,那省下來的空間不是又吐回去了嗎?

不是。以只量化權重的做法來說,計算時確實要把權重換回浮點,但不是把整個模型還原成一份 BF16 再拿去算。反量化融合在矩陣乘法的 kernel 裡,一小塊一小塊做完就用掉,完整的 BF16 weight tensor 從頭到尾不會同時存在記憶體裡。所以省下的容量與記憶體頻寬在推論時仍然成立。

這條路徑在站上的效能文裡出現過,只是當時沒有特別把它抽出來講。NVFP4 那篇裡那行 NVFP4 weights → Marlin dequant → BF16 → BF16 GEMM 講的就是它:dequant 是 kernel 內的一站。

所以實驗室第四步的「還原」純粹是為了算誤差:把原值和還原值擺在一起才比得出差距,不是推論時每個 token 都要付的 overhead。

(本頁只量化權重,activation 全程保持浮點。activation 也一起量化是另一回事,之後再寫。)

哪些數字可以引用,哪些只是示意

想把實驗室的數字引用到別的地方,差別就在這裡。

可以引用的:scale、還原值、誤差、bytes。這些都是實際算出來的。

只是示意的:方塊大小不代表 tensor 真實維度、cache 格數不是實測、動畫速度跟真實速度無關、彩色路由連線是固定種子產生的。

另外,拿不到的數字會標成「未核對」而不是 0——0 看起來像一個測量結果。

進階:為什麼這些數字可以信

只想玩工具的話可以跳過這節。

node verify.cjs 現在跑 53 項,測的是資料一致性不是畫面:FP4 E2M1 的每一個有限編碼都要對、FP8 E4M3FN 的 subnormal 與 NaN 要對、ties-to-even 在 0 的兩側都要成立、scale 要用儲存後的 FP32 值而不是理論值去挑整數代碼。

最後那條就是上面 4.77e-7 的來源。如果測試偷懶用理論 scale,那個誤差就會消失——但它明明真的存在。

有一個教學組的巧合值得知道:INT8 在這四個值上誤差全是 0。因為 15.875 ÷ 127 = 0.125 剛好整除,而四個值都是 0.125 的整數倍。實驗室裡有標這是刻意挑的教學資料,不能解讀成 INT8 無損。

自己點一次

model.ai-muninn.com/zh-TW/quantization/,不用登入。

建議的順序:選 INT4、追 0.375,看它在第二步變成 0;再追 −15.875,看那個 4.77e-7 從哪裡來。兩個都看完,量化的誤差來源就不只是「取整數」了。

參考:ONNX QuantizeLinear 規格Part 1:量化演算法在對付什麼

常見問題

什麼是量化?
用更少的位元表示同一組數字。原本每個權重佔 16 bits,改成 4 bits,同一組 64 個權重從 128 bytes 變成 36 bytes(含一個共用的 scale),省 71.9%。代價是這些數字只能落在少數幾個刻度上,落不上去的就要四捨五入。
量化怎麼做?
四步:先用整組的最大絕對值算出 scale,把每個權重除以 scale 後取最近的刻度,儲存「共用 scale + 每個權重的代碼」,要用的時候再乘回 scale。實驗室把這四步拆開,可以追著單一權重看。
量化完之後,每次推論都要把權重還原成 BF16 嗎?
以只量化權重的做法來說,計算時確實要把權重換回浮點,但不是把整個模型還原成一份 BF16 再拿去算。反量化融合在矩陣乘法的 kernel 裡,一塊一塊做完就用掉,完整的 BF16 weight tensor 不會同時存在記憶體裡,所以省下的容量與頻寬在推論時仍然成立。
一個權重剛好落在量化的刻度上,還原回來就會完全一樣嗎?
不一定。scale 本身也用有限的位元儲存。以實驗室的教學組為例,INT4 的 scale 是 15.875 ÷ 7,這個數在 FP32 裡存不下,所以連定義這個 scale 的 −15.875 都還原成 −15.874999523162842,差 4.77e-7。
為什麼同一組裡有一個很大的數,其他權重就會變不準?
因為 scale 由整組的最大絕對值決定。教學組裡有 −15.875,INT4 的 scale 就是 2.2679,一格跨 2.2679;0.375 和 −0.875 連半格都不到,四捨五入之後都變成 0。拿掉那個大數,scale 變 0.6071,兩個權重就各自落在不同格。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。