---
tags: linux2026
---
# [2026q1](https://wiki.csie.ncku.edu.tw/linux/schedule) 第 6 週測驗題
> [作答表單](https://forms.gle/MXL7Xb6LyXF3YAoj7)
:::warning
:warning: 回答延伸問題時,務必優先以[課程教材](https://wiki.csie.ncku.edu.tw/linux/schedule)為主要出處,隨後參照 C 語言規格書、Linux 核心原始程式碼及其 git log 和 LKML (Linux Kernel Mailing-List),和權威素材 (如 GCC 和 glibc 手冊,但絕對不是 CSDN 或者尋常的網路日誌/筆記)
AI 工具是輔助性質,可用來撰寫測試程式碼和收集資訊,但主要的推測、查證、分析,和討論,都該由人類進行。
:::
檢驗學員對於 CS:APP 第 2 章、數值系統、C 語言規格、Linux 核心風格、紅黑樹實作與程式開發等議題的掌握。
注意須知 (Linux 核心合規標準):本測驗的作答規範仿照 Linux 核心的 [`checkpatch.pl`](https://docs.kernel.org/dev-tools/checkpatch.html) 風格檢查與巨集定義慣例,要求學員以系統程式設計的嚴謹度撰寫答案。
時代脈絡補充:C23 已正式將有號整數的表示法明定為 two's complement,讓 `time_after()`、`ERR_PTR()` 這類長期依賴位元保持性的系統程式技巧有了更清楚的語言背景。不過,這不表示 signed overflow 與所有有號位移都已自動變成可攜且安全;實作仍必須避免未定義或 implementation-defined 的邊界案例。換言之,現代標準吸收了核心世界的實務現實,但工程紀律並沒有因此放鬆。
| 約束類別 | 規定格式 | 技術理由 | 範例 |
|---|---|---|---|
| 運算式語法 | 填空答案採最精簡形式;嚴格不含空白、`?`、`;` | 評估 bitwise 操作或巨集層級運算式的關鍵邏輯,排除語法填充;題目內程式碼可保留排版空白以利閱讀 | `a^b` |
| 十六進位表示 | 大寫字母;32 位元值以 8 位數字表示 (含前導零) | 確保記憶體對齊視覺化的一致性,避免 endianness 或截斷歧義 | `00200000`、`ABC` |
| 數值填空 | 依題目指定基底作答;十六進位不含 `0x` 字首且以大寫表示 | 讓評分聚焦於數值本身,不混入語法或格式噪音 | `3F801000`、`112` |
| 邏輯運算式填空 | 允許合法 C 語法;維持最短且語義直接的表達 | 這類題目要檢查的是位元操作邏輯,不是常數抄寫能力 | `~3`、`1u` |
| 常數與型別字尾 | 預設十進位整數;嚴格小寫型別字尾 | 遵循 ISO C 標準表示法,防止核心空間中的隱式型別提升錯誤 | `112`、`1u`、`-1.0f` |
| 記憶體存取 | 嚴格以 `[]` 陣列索引表示 | 區分高階陣列迭代與原始指標算術,提高語義清晰度 | `a[i]` |
| C 語言術語 | 全部小寫,至多一個半形空白 | 與 C 語言規格書 (如 C99/C11) 的正式術語格式一致 | `unsigned int` |
### 背景:TurboQuant 向量量化
本測驗的程式碼取材自 [TurboQuant](https://arxiv.org/pdf/2504.19874) 高維向量的量化系統。
> $\to$ [過度依賴 AI 幫我統整,卻沒有盡到回去翻閱原始論文查證的義務,最後導出錯誤的結論](https://youtu.be/YlxQuyy3Mu0), [很 drama 的論文](https://www.facebook.com/thegiivee/posts/pfbid0QDYDWa98XKrE3iuqH1kZkLGTMbhF37tz5zWZece7FsSTNqgtq5jd4ecU934Zem1nl)
> 新聞報導:
> * [TurboQuant 衝擊記憶體 外媒曝 : DDR5 價格出現下跌](https://ec.ltn.com.tw/article/breakingnews/5386988)
> * [Google 新技術 TurboQuant 嚇崩記憶體股?](https://www.wealth.com.tw/articles/f8a6ce0f-dd06-49b9-8f11-cb6685392eec)
閱讀前,學員應當先掌握以下:
* 線性代數面:向量、內積、L2 norm、正交矩陣與基底變換
* 系統面:知道 Linux 核心為何在意位元寬度、記憶體配置、快取區域性與有號/無號轉換
雖以 TurboQuant 為背景,但每題真正要驗證的仍是 CS:APP 第 2 章的基本功與 Linux 核心程式設計常見陷阱。
[向量量化](https://en.wikipedia.org/wiki/Vector_quantization) (vector quantization, VQ) 的主體想法:把原本連續的浮點向量 $x \in \mathbb{R}^d$,改寫成少量代表點加上一個索引。做法是先挑出有限個代表點 (centroid) $\{c_1, \ldots, c_K\}$,再把每個輸入向量對映到最接近的代表點。於是原本必須儲存整個浮點向量,現在只要儲存代表點的索引即可。線性代數上,這等於把空間切成多個 [Voronoi 區域](https://en.wikipedia.org/wiki/Voronoi_diagram),每個區域共用一個代表點。代價是一定會有失真,常見的衡量方式是重建誤差 $\mathbb{E}[\|x - c_{q(x)}\|_2^2]$。
經典的 [Lloyd-Max 演算法](https://en.wikipedia.org/wiki/Lloyd%27s_algorithm) 可視為反覆調整代表點、讓平均誤差愈來愈小的過程:先依目前代表點分群,再更新每群的中心,重複直到收斂。LBG 演算法則把這件事推廣到高維向量。若學員曾學過 [k-means 分群](https://en.wikipedia.org/wiki/K-means_clustering),可把它視為同一族方法在不同語境下的表述。
對高維向量有二個極端做法。第一個極端是逐座標各自量化,也就是[純量量化](https://en.wikipedia.org/wiki/Quantization_(signal_processing)#Scalar_quantization)。這樣做最簡單,但若不同座標彼此相關,就會浪費很多表示能力。第二個極端是直接對整個 $d$ 維向量做完整的高維 VQ。這在理論上最好,但 codebook 大小會隨維度快速爆炸,實作上通常不可行。
線性代數提供的折衷方法是先做基底變換。若向量的統計相關性可由[共變異數矩陣](https://en.wikipedia.org/wiki/Covariance_matrix) $\Sigma$ 描述,則可用[正交對角化](https://en.wikipedia.org/wiki/Orthogonal_diagonalization) 找到一組較適合量化的新座標系:$\Sigma = P \Lambda P^\top$,其中 $P$ 是[正交矩陣](https://en.wikipedia.org/wiki/Orthogonal_matrix),$\Lambda$ 是對角矩陣。把向量旋轉到新基底後,新的座標 $y = P^\top x$ 彼此較不相關,此時再逐座標量化,通常比直接在原座標系逐座標量化更有效。TurboQuant 的核心精神就是先做把座標轉成較容易量化形式的步驟,再做後續量化。
TurboQuant 的運作原理可類比於 [JPEG/MPEG 等 transform coding](https://hackmd.io/@sysprog/linux2026-quiz4):
```
JPEG TurboQuant
pixel block (8×8) embedding vector (d-dim)
│ │
▼ ▼
┌────────────┐ ┌──────────────────┐
│ DCT │ orthogonal │ random rotation │ orthogonal
│ (discrete │ transform │ y = Π · x │ transform
│ cosine) │ (decorrelate) └──────────────────┘ (decorrelate)
└────────────┘ │
│ ▼
▼ ┌──────────────────┐
┌────────────┐ │ Lloyd-Max scalar │
│ quantize │ per-coefficient │ quantize │ per-coordinate
│ matrix │ (non-uniform) │ per coordinate │ (uniform)
└────────────┘ └──────────────────┘
│ │
▼ ▼
compressed bits b-bit packed indices
```
小結:
* JPEG 以 DCT (離散餘弦轉換) 將空間域的畫素區塊轉為頻域係數,利用影像能量集中於低頻的特性,對高頻係數分配較少位元。DCT 的基底向量是固定的餘弦函式族,構成 $\mathbb{R}^n$ 的標準正交基底 (orthonormal basis)。TurboQuant 以隨機正交矩陣 $\Pi$ 將 embedding 向量旋轉至新基底,利用高維空間中 concentration of measure 的性質,使旋轉後的座標近乎獨立且服從已知分布。二者的共同數學結構是基底變換 (change of basis):以正交矩陣 $Q$ 將向量從原始座標系轉至新座標系 $y = Qx$,在新座標系中逐座標處理後,再以 $Q^\top y$ 轉回原始座標系。
* 二者皆為 lossy compression (失真壓縮),無法完美還原原始資料,但皆提供可控的失真上界,使品質損失極低且可預測。正交轉換保範 (norm-preserving),這是正交矩陣 $Q^\top Q = I$ 的直接推論:$\|Qx\|_2^2 = x^\top Q^\top Q x = x^\top x = \|x\|_2^2$。DCT 對應的特例即 Parseval 定理 ($\sum |x_i|^2 = \sum |X_k|^2$)。保範性質確保轉換本身不引入任何失真,所有失真僅來自後續的量化步驟。
* TurboQuant 不區分 token 的重要性,每個 KV embedding 一視同仁地經過旋轉、量化、打包,品質保證是對所有向量的 worst-case bound。這與 SnapKV、PyramidKV 等 token-level 壓縮方法 (選擇性丟棄低重要性的 token) 形成對比。
此問題的理論根基可溯至 Shannon 的 [source coding theory](https://en.wikipedia.org/wiki/Source_coding_theorem) (1948)。Shannon 的 [rate-distortion theory](https://en.wikipedia.org/wiki/Rate%E2%80%93distortion_theory) 建立失真壓縮的基本極限:對任何來源分布與失真度量,存在 distortion-rate function $D(R)$,給定位元率 $R$ 時,任何編碼方案的失真不可能低於 $D(R)$;反之,只要位元率足夠,總存在達到 $D(R)$ 的編碼。此定理為量化器設計劃定不可突破的物理極限,類似 Carnot 效率之於熱機。TurboQuant 論文 Theorem 3 藉由 [Shannon lower bound](https://en.wikipedia.org/wiki/Rate%E2%80%93distortion_theory) 與 [Yao's minimax principle](https://en.wikipedia.org/wiki/Yao%27s_principle) 證明,對任意 randomized $b$-bit quantizer $Q$,存在 hard instances $x, y \in \mathbb{S}^{d-1}$ 使 MSE 下界 $D_{\text{mse}}(Q) \geq 1/4^b$ 成立。TurboQuant 的實際失真與此下界僅差 $\sqrt{3}\pi / 2 \approx 2.7$ 倍常數,在工程意義上已逼近理論極限。
VQ 在大型語言模型 (LLM) 推論中扮演關鍵角色。decoder-based transformer 模型在推論時以 key/value (KV) 快取儲存先前 token 的 attention embedding,大小計算如下:
```
KV cache size = 2 x n_kv_heads x head_dim x n_layers x seq_len x sizeof(dtype)
^ ^
key + value context length
Llama-3.1-8B (GQA, 8 KV heads, head_dim=128, 32 layers, 128K context, FP16):
= 2 x 8 x 128 x 32 x 131072 x 2 bytes ≈ 16 GiB
```
16 GiB 已接近消費級 GPU 的 HBM 容量上限 (NVIDIA RTX 4090: 24 GB),而更大的模型 (70B 以上) 或更長的 context 會使此數字進一步膨脹。可將 KV 快取視為一個三維結構:
```
KV 快取的三個壓縮維度:
┌─────────────────────────────────────────────────┐
│ │ ↕ 寬:資料精度
│ context length │ (每個數值佔幾 bit)
│ ◄──────────────────────────────────────────► │ FP16 = 16 bit
│ │ 量化後 = 2~4 bit
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ t₁│ t₂│ t₃│ t₄│ t₅│ t₆│ t₇│ t₈│...│ tₙ│ │ ↕ 高:狀態大小
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │ (head_dim × n_heads
│ │ × n_layers)
└─────────────────────────────────────────────────┘
三種壓縮策略分別對應不同維度:
長 (context length) ── sliding window attention / token pruning
限制保留的 token 數量,丟棄過舊的記憶
(SnapKV, PyramidKV, Scissorhands)
高 (state size) ── recurrent / state-space compression
將歷史記錄壓縮為固定大小的隱狀態
(Linear Attention, Mamba, RWKV)
寬 (data precision) ── vector quantization ← TurboQuant
降低每個數值的位元寬度
FP16 (16-bit) → 2~4 bit
```
TurboQuant 專攻資料精度維度的壓縮:將每個 $d$ 維 FP16 向量壓縮為 $b$-bit 整數索引 (每座標僅 2 至 4 位元),可將 KV 快取縮減 $4.5\times$ 以上 (論文 Table 1)。壓縮帶來的加速並非源自計算量減少,因為 GPU 的 Tensor Core 有最低精度要求 (如 NVIDIA H100 原生支援 FP8/INT8),低於此精度的量化索引仍需反量化後才能參與矩陣運算。真正的加速來自記憶體頻寬的節省。
LLM 推論的記憶體頻寬瓶頸源自 decoder-based transformer 的自迴歸 (autoregressive) 生成特性:每個新 token 的產生需讀取整個 KV 快取以計算 attention,但每次僅產出一個 token 的 logits。以 NVIDIA H100 SXM 為例,其 dense FP16 Tensor Core 峰值為 989 TFLOPS (不含 2:4 sparsity 加速),HBM3 頻寬為 3.35 TB/s,[算術強度](https://en.wikipedia.org/wiki/Roofline_model) (arithmetic intensity) 的平衡點約為 $989 \times 10^{12} / (3.35 \times 10^{12}) \approx 295$ FLOP/byte。然而 autoregressive decoding 的 attention 計算中,每個 FP16 KV 元素 (2 bytes) 從 HBM 讀入後僅參與一次乘加 (2 FLOP),算術強度僅約 $2 / 2 = 1$ FLOP/byte,遠低於平衡點,GPU 算力利用率不到 0.5%。這正是 [roofline model](https://en.wikipedia.org/wiki/Roofline_model) 所描述的 memory-bound 區域:效能完全受限於 HBM 至 SRAM 的資料搬運速度,而非計算單元的吞吐量。KV 快取存於 HBM,每次 attention 計算都需將其載入 SRAM,將精度從 16-bit 降至 3-bit 可使搬運的資料量縮減至約 $1/5$,在此 memory-bound 場景下直接轉化為等比例的延遲降低。
延續以 CPU 運算換取記憶體頻寬的主題,Linux 核心同樣採用積極的壓縮策略,以減緩記憶體 swap 至磁碟時的極端延遲。Zswap 與 Zram 皆壓縮本應被 swap 至實體磁碟的記憶體 page,以相對廉價的 CPU 週期換取大幅降低的磁碟存取延遲。但二者的底層架構代表根本不同的記憶體壓力處理方法。
[zram](https://docs.kernel.org/admin-guide/blockdev/zram.html) 是壓縮 RAM 區塊裝置,作為完全包含在實體 RAM 中的獨立 swap partition 運作。對無磁碟嵌入式系統或傳統 swap 不可用的高度記憶體受限環境極有助益。較新的核心提供 `CONFIG_ZRAM_WRITEBACK` 選項,允許將不可壓縮或閒置的 page 寫回後端磁碟,但此功能預設關閉且需手動設定 backing device。在預設組態下,Zram 與次要實體磁碟 swap 配對時仍存在 LRU Inversion 風險:當 Zram 達到容量上限時,缺乏自動逐出至後端磁碟的整合機制,核心可能被迫從系統其餘部分回收活躍的熱 file-backed page,而非回收困在 Zram 中的過時 anonymous page,造成效能劣化。
Zswap 則是輕量的 write-behind 壓縮快取,無縫位於實際磁碟 swap 裝置之前。它透過 frontswap API 直接掛入核心記憶體管理子系統。當系統嘗試將 page swap 至磁碟時,Zswap 攔截該 page、嘗試壓縮、並儲存於動態配置的 RAM pool 中。
關鍵結構差異在於 Zswap 與核心逐出邏輯的深度整合。Zswap 維護索引樹並透過 `zswap_entry` 結構追蹤元資料 (含 allocation handle、offset 與 length)。當 Zswap 記憶體池達到最大閾值時,內建的 pool-full 逐出機制會將最舊的壓縮 page 解壓縮並寫回後端磁碟 swap,避免鎖死。此外,可選的 shrinker worker (`queue_work(shrink_wq, &zswap_shrink_work)`) 提供主動式回收,在記憶體壓力升高時提前逐出冷 page (此功能預設關閉,需明確啟用)。二者共同形成在壓力下優雅退化的分層系統,不破壞系統整體的 LRU 邏輯。
| 特性 | Zswap | Zram |
|---|---|---|
| 主要功能 | 磁碟 swap 前的 write-back 快取 | 獨立壓縮區塊裝置 |
| 整合方式 | 透過 frontswap API | 格式化為獨立 swap 空間 |
| 逐出機制 | 透過 shrinker 自動 LRU 逐出至磁碟 | 硬性上限;無自動逐出至磁碟 |
| 失效模式 | 優雅分層至磁碟儲存 | 潛在 LRU inversion 與系統 thrashing |
此動態壓縮機制與 TurboQuant 的設計原則一致:在運算資源充裕而記憶體/儲存資源受限時,以確定性的壓縮轉換交換容量。TurboQuant 以隨機旋轉與位元打包繞過 GPU HBM 容量瓶頸,Zswap 以 CPU 週期繞過 NAND 儲存延遲。
既有 VQ 方法面臨兩難:
```
Offline (Product Quant.) Online (RabitQ etc.)
┌───────────────────────┐ ┌──────────────────────┐
│ ✚ low distortion │ │ ✚ no preprocessing │
│ ✖ needs codebook │ │ ✖ loose bounds │
│ ✖ data-dependent │ │ ✖ not GPU-friendly │
│ ✖ slow (494 s, d=3072)│ │ │
└───────────┬───────────┘ └──────────┬───────────┘
│ │
└──────────┐ ┌───────────────┘
▼ ▼
┌────────────────────┐
│ TurboQuant │
├────────────────────┤
│ ✚ data-oblivious │
│ ✚ online │
│ ✚ near Shannon LB │
│ ✚ accelerator- │
│ friendly │
│ ✚ ~0 s (0.002 s, │
│ d=3072) │
└────────────────────┘
```
TurboQuant 的設計強調 accelerator-friendly 的程式碼結構。在使用者空間,AVX-512 等 SIMD (Single Instruction, Multiple Data) 指令可自由用於處理大量浮點資料。但在 Linux 核心中,浮點運算單元 (FPU) 與 SIMD 暫存器的使用受嚴格管控。
context switch 是核心中極頻繁的操作。若核心不加限制地使用 AVX/ZMM 暫存器 (涵蓋數百位元組的狀態),每次 context switch、中斷或系統呼叫都需儲存與還原此狀態,效能將嚴重劣化。因此標準核心程式碼以 `-mno-avx` 等旗標編譯,明確禁止編譯器產生 SIMD 指令。若編譯器在核心程式碼中自行產出 AVX 指令,會在使用者空間的浮點變數尚未儲存前將其靜默覆寫,後果不堪設想。
然而,對極端運算密集的任務,核心允許隔離的 SIMD 臨界區段。RAID6 同位元組計算是典型案例。RAID6 需產生二個同位元組區塊 (P 與 Q) 以抵禦二個同時的磁碟故障。P 同位元組是簡單的 bitwise XOR,但 Q 同位元組需要在有限伽羅瓦體 (Galois field) $GF(2^8)$ 上進行複雜的乘法運算。以標準純量整數指令逐位元組計算,在計算上極為昂貴,成為儲存吞吐量的瓶頸。
為達極致吞吐量,核心在 `lib/raid6` 架構樹中提供特化實作。安全執行 SIMD 程式碼的流程如下:
1. 儲存裝置驅動程式呼叫 `kernel_fpu_begin()`:此函式主動儲存目前使用者空間的 FPU 暫存器狀態並停用核心 preemption,防止行程在持有 FPU 狀態期間被排程切換。
2. 以 inline assembly 使用寬 `ymm` 或 `zmm` 暫存器處理資料。演算法針對特定微架構嚴格最佳化,例如 LoongArch LSX/LASX 實作將 伽羅瓦體處理迴圈明確展開至每次操作 64 位元組,精確對齊CPU core 的 L1 cache line 大小,並避免分支以維持 SIMD pipeline 的連續資料流。演算法同時避開 null page 處理,確認 SIMD pipeline 不受稀疏資料干擾。
3. 計算完成後呼叫 `kernel_fpu_end()` 還原原始狀態並重新啟用 preemption。
此案例與 TurboQuant 的向量化距離計算直接對照:二者皆須確保運算對映至硬體的向量執行單元,刻意避免分支以維持 pipeline 吞吐量,同時遵守作業系統或加速器要求的嚴格執行協定。
TurboQuant 要解決的根本問題可形式化為:設計量化映射 $Q : \mathbb{R}^d \to \{0,1\}^B$ (其中 $B = b \cdot d$) 與反量化映射 $Q^{-1} : \{0,1\}^B \to \mathbb{R}^d$,使以下二個失真指標最小化。MSE 失真衡量的是 $\ell_2$ 空間中原始向量與重建向量的距離平方;inner product 失真衡量的是以量化向量近似原始內積 $\langle y, x \rangle$ 時的誤差平方,此指標直接對應 attention 機制中 query-key 相似度計算的精度:
$$D_{\text{mse}} := \mathbb{E}_Q\left[\|x - Q^{-1}(Q(x))\|_2^2\right] \quad \text{(MSE 失真)}$$
$$D_{\text{prod}} := \mathbb{E}_Q\left[|\langle y, x \rangle - \langle y, Q^{-1}(Q(x)) \rangle|^2\right] \quad \text{(inner product 失真)}$$
對內積量化器,TurboQuant 額外要求 unbiasedness:$\mathbb{E}_Q[\langle y, Q^{-1}(Q(x)) \rangle] = \langle y, x \rangle$。此性質確保量化後的 attention score 在期望值上等於原始值,不引入系統性偏差。
TurboQuant 提出二個演算法,分別最佳化 MSE 與內積失真。注意:TurboQuant 不使用極座標 (polar coordinates) 轉換,亦不將資料搬移至頻域。論文中作為比較對象的 [PolarQuant](https://arxiv.org/abs/2502.02617) 才是以極座標 (半徑 + 角度) 編碼的方法。PolarQuant 將向量兩兩配對轉為極座標 $(r, \theta)$,再對半徑遞迴配對,最終僅剩一個總距離和一組角度;角度範圍固定,因此不需額外的 normalization constant。TurboQuant 的做法截然不同,以正交轉換去相關後在旋轉座標系中逐座標量化,類似 JPEG 以 DCT 去相關後逐係數量化。二者的本質差異在於:(1) JPEG 的 DCT 是固定轉換,TurboQuant 的旋轉矩陣 $\Pi$ 是隨機產生的;(2) JPEG 對不同頻率係數分配不同精度 (低頻精、高頻粗),TurboQuant 對所有座標使用相同的 codebook (因為旋轉後各座標的統計分布相同)。網路上流傳的部分科普文章誤將 PolarQuant 的遞迴極座標轉換描述為 TurboQuant 的第一步,這是張冠李戴。
第 1 階段:隨機旋轉 + Lloyd-Max 純量量化 (Algorithm 1, TurboQuant_mse)
```
input: x ∈ ℝᵈ
│
▼
┌─────────┐ ┌─────────────────┐ ┌───────────┐ ┌──────────┐
│ L2 norm │ │ random rotation │ │ scalar │ │ bit-pack │
│ ‖x‖ │───▶│ y = Π·(x/‖x‖) │───▶ │ quantize │───▶│ b-bit │
└─────────┘ └─────────────────┘ │ per coord │ │ indices │
│ │ └───────────┘ └──────────┘
▼ ▼ │ │
norm (FP32) Lemma 1: ▼ ▼
│ y_j ~ Beta idx[j] ∈ [0, 2ᵇ) packed bytes
│ → N(0, 1/d) for each j ∈ [d]
│ (coords ~indep.)
│ codebook: {c₁, …, c_{2ᵇ}}
│ (Lloyd-Max centroids)
│ │
└───────▶ stored together ◀──────────────┘
in block_tq3 {norm, indices[]}
```
* 隨機旋轉 (Lemma 1):先對隨機矩陣 $G \in \mathbb{R}^{d \times d}$ (各元素 i.i.d. $\mathcal{N}(0,1)$) 施行 [QR 分解](https://en.wikipedia.org/wiki/QR_decomposition) $G = QR$,再以 $\Pi = Q \cdot \text{diag}(\text{sign}(R_{ii}))$ 校正符號,確保 $\Pi$ 為 [Haar 測度](https://en.wikipedia.org/wiki/Haar_measure)下均勻分布於正交群 $O(d)$ 上的隨機正交矩陣。QR 分解是線性代數的基礎工具,將任意矩陣分解為正交矩陣 $Q$ ($Q^\top Q = I$) 與上三角矩陣 $R$ 的乘積;單獨取 $Q$ 因子並不保證 Haar 均勻性 (因為 QR 分解的符號約定使 $R$ 對角元素為正,引入偏差),乘上 $\text{diag}(\text{sign}(R_{ii}))$ 才消除此偏差。旋轉後的座標 $y_j = (\Pi \cdot \hat{x})_j$ (其中 $\hat{x} = x/\|x\|_2$ 為單位向量) 服從 scaled Beta 分布 $f_X(x) = \frac{\Gamma(d/2)}{\sqrt{\pi} \cdot \Gamma((d-1)/2)} (1 - x^2)^{(d-3)/2}$。此分布隨維度 $d$ 增大而趨近 $\mathcal{N}(0, 1/d)$ (中央極限定理的推論),且各座標間並非獨立但恰好不相關 ($\mathbb{E}[x_i x_j] = 0$ 對 $i \neq j$),相依性僅表現在高階約束 (如單位範數條件 $\sum x_i^2 = 1$) 而非成對統計量。直觀理解:單位球面 $\mathbb{S}^{d-1}$ 上均勻分布的向量,其各座標投影集中於零附近且漸近 Gaussian,這是 concentration of measure 現象的具體展現。此旋轉將座標間的相關性打散,使 $d$ 維問題近似分解為 $d$ 個獨立的一維量化問題。
* Lloyd-Max codebook:旋轉後各座標服從相同的已知分布,可以單一 codebook 處理所有座標。對此分布求解一維 continuous k-means 最佳化 (Eq. 4),得到 $2^b$ 個最佳 centroid。以 $b = 1, 2$ 為例:$b = 1$ 時 centroid 為 $\{\pm\sqrt{2/\pi} / \sqrt{d}\}$;$b = 2$ 時為 $\{\pm 0.453/\sqrt{d},\ \pm 1.510/\sqrt{d}\}$。codebook 僅依賴維度 $d$ 和位元寬度 $b$,可預計算後儲存,不需要訓練資料。
* 逐座標量化:對每個 $y_j$ 找最近 centroid 並記錄 $b$-bit 索引。反量化時以索引查表還原 centroid 值,再乘以 $\Pi^\top$ 旋轉回原始基底。
第 2 階段:1-bit QJL 殘差校正 (Algorithm 2, TurboQuant_prod)
第 1 階段的 MSE 量化器雖然重建誤差最小,但用於估計內積 $\langle y, x \rangle$ 時會產生系統性偏差。$b = 1$ 時乘法偏差為 $2/\pi \approx 0.637$ (論文 Section 3.2),意味著 attention score 被系統性縮減約 36%;$b$ 增大時偏差遞減但不為零。如同一把校準不良的尺,每次量測都偏向同一方向,累積後會扭曲 attention score 的排序。
```
原始向量 x ∈ ℝᵈ
│
├───▶ Q_mse (b−1 bits) ───▶ idx (第 1 階段: MSE 量化)
│ │
│ x̃ = DeQuant(idx) (反量化重建)
│ │
└───────▶ r = x − x̃ (殘差向量: 第 1 階段的量化誤差)
│
├───▶ sign(S·r) ───▶ qjl (第 2 階段: QJL 1-bit sign hash)
│ 每個座標僅用 1 bit: +1 或 −1
└───▶ ‖r‖ ───▶ gamma (殘差 L2 norm)
output: (idx, qjl, gamma)
反量化: x̂ = x̃_mse + (√(π/2) / d) · γ · Sᵀ · qjl
```
* QJL 的角色:QJL (Quantized Johnson-Lindenstrauss) 的數學基礎是 [Johnson-Lindenstrauss 引理](https://en.wikipedia.org/wiki/Johnson%E2%80%93Lindenstrauss_lemma),該引理指出將 $n$ 個高維點以隨機線性映射 $S \in \mathbb{R}^{m \times d}$ 投影至 $m = O(\varepsilon^{-2} \log n)$ 維子空間時,所有成對距離以 $(1 \pm \varepsilon)$ 倍率保持。QJL 更進一步:對殘差 $r = x - \tilde{x}_{\text{mse}}$ 計算隨機投影 $Sr$ 後僅取 sign,每個座標只花 1 bit 記錄正負號 ($+1$ 或 $-1$)。反量化時以 $\sqrt{\pi/2}$ 為 scale factor,補償 sign 函式丟失幅值的效應 (因為 $\mathbb{E}[|Z|] = \sqrt{2/\pi}$ 對 $Z \sim \mathcal{N}(0,1)$)。Lemma 4 證明組合後的內積估計器是 unbiased 的:$\mathbb{E}[\langle y, \hat{x} \rangle] = \langle y, x \rangle$。
偏差消除的數學推導 (論文 Theorem 2 證明的關鍵,以條件期望展開):
```
僅用 MSE 量化器估計內積 (biased, 含偏差):
┌──────────────────────────────────────────────────────────┐
│ E[⟨y, x̃_mse⟩] = (2/π) · ⟨y, x⟩ (b=1 時) │
│ ~~~~~~ │
│ 乘法偏差:內積被系統性縮減至 63.7% │
└──────────────────────────────────────────────────────────┘
加入 QJL 殘差校正後 (unbiased, 不含偏差):
E[⟨y, x̃⟩] = E[⟨y, x̃_mse + x̃_qjl⟩]
= E[⟨y, x̃_mse⟩] + E[⟨y, x̃_qjl⟩]
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
含偏差的 MSE 項 QJL 殘差校正項
其中 QJL 校正項:
E[⟨y, x̃_qjl⟩ | x̃_mse] = ⟨y, r⟩ (Lemma 4: QJL unbiased)
= ⟨y, x - x̃_mse⟩ (殘差定義)
= ⟨y, x⟩ - ⟨y, x̃_mse⟩
合計:E[⟨y, x̃⟩] = E[⟨y, x̃_mse⟩] + ⟨y, x⟩ - E[⟨y, x̃_mse⟩]
= ⟨y, x⟩ ✓ 完美消除偏差
```
此推導是 Theorem 2 (p13) 的關鍵:無論 MSE 量化器引入多少偏差,QJL 對殘差的無偏估計恰好補上缺口,此性質不依賴 $b$ 值或 codebook 選擇。注意:無偏性嚴格成立的前提是 $S$ 的元素為 i.i.d. $\mathcal{N}(0,1)$ (Definition 1、Lemma 4)。後續 Problem C 改用確定性 Rademacher 投影 ($\pm 1$) 以簡化實作,犧牲嚴格無偏性但在高維度下漸近等價 (詳見 Problem C 的驗證說明)。
$Q_{\text{prod}}$ 的量化映射定義為 (論文 Algorithm 2):
$$
Q_{\text{prod}}(x) = \left[Q_{\text{mse}}(x),\ \text{sign}(S \cdot r),\ \|r\|_2\right], \quad r = x - Q_{\text{mse}}^{-1}(Q_{\text{mse}}(x))
$$
反量化映射為:
$$
Q_{\text{prod}}^{-1}(\text{idx}, \text{qjl}, \gamma) = Q_{\text{mse}}^{-1}(\text{idx}) + \frac{\sqrt{\pi/2}}{d} \cdot \gamma \cdot S^\top \cdot \text{qjl}
$$
* Theorem 2 (論文 p12-13) 證明此內積量化器滿足:(1) 無偏性 $\mathbb{E}[\langle y, \tilde{x} \rangle] = \langle y, x \rangle$;(2) 內積失真上界 $D_{\text{prod}} \leq \frac{\sqrt{3}\pi^2 \cdot \|y\|_2^2}{d} \cdot \frac{1}{4^b}$。具體數值:$b = 1, 2, 3, 4$ 時 $D_{\text{prod}} \approx \frac{1.57}{d}, \frac{0.56}{d}, \frac{0.18}{d}, \frac{0.047}{d}$。
* 品質保證:論文在 Needle-In-A-Haystack 測試 (Figure 4) 中展示 $4\times$ 壓縮達到與 full-precision 相同的 recall score (0.997),在 LongBench-V1 (Table 1) 上以 2.5-bit 量化達到與 16-bit full cache 相當的平均分數 (49.44 vs 50.06)。損失極低的原因在於內積估計的 unbiasedness:個別估計有 variance,但不會系統性偏向特定方向,在大量 attention head 與 layer 的統計平均下,整體品質趨近無損。
* Theorem 1 證明 MSE 失真上界為 $D_{\text{mse}} \leq \frac{\sqrt{3}\pi}{2} \cdot \frac{1}{4^b}$,具體數值如下:
| $b$ (bits) | $D_{\text{mse}}$ (上界) | $D_{\text{mse}}$ (下界 $1/4^b$) | 倍率 |
|---|---|---|---|
| 1 | 0.36 | 0.25 | 1.44 |
| 2 | 0.117 | 0.0625 | 1.87 |
| 3 | 0.03 | 0.0156 | 1.92 |
| 4 | 0.009 | 0.0039 | 2.31 |
以下三題分別探討此管線中的關鍵環節:FP32/FP16 轉換 (Problem A)、位元打包與 codebook 量化 (Problem B)、sign hash 與內積估計 (Problem C)。
---
## Problem A: 浮點格式轉換與 denormalized 捨入
> 延伸 CS:APP 2.87 / Practice Problem 2.52 / Figure 2.32 / Figure 2.35
題目設計要點:
* 核心在於把 Figure 2.32 與 Figure 2.35 轉成可執行的位元操作,而不是背誦 FP16 規格。
* 學員只要掌握 normalized / denormalized、bias、hidden bit 與 round-to-even,即可完整作答。
* 延伸閱讀刻意連到 Linux 核心中的浮點模擬與格式轉換,讓同一組規則落到真實程式碼。
- [ ] Part 1: Half-Precision 編碼
IEEE 754-2008 的 16 位元 half-precision 格式由 1 個 sign bit、$k = 5$ 個 exponent bits、$n = 10$ 個 fraction bits 組成,exponent bias 為 $2^{5-1} - 1 = 15$。CS:APP Practice Problem 2.52 要求在不同浮點格式間轉換位元樣式,並在必要時套用 round-to-even 規則。
本題真正會用到的規格常數:
| 格式 | Exponent Bits ($k$) | Fraction Bits ($n$) | Bias |
|---|---:|---:|---:|
| FP32 | 8 | 23 | 127 |
| FP16 | 5 | 10 | 15 |
CS:APP Figure 2.32 定義 IEEE 754 浮點數由三個欄位組成:sign bit `s`、$k$-bit exponent field `exp`、$n$-bit fraction field `frac`。以下圖示對照二種格式的欄位配置:
```
FP32 (single precision, k=8, n=23):
31 30 23 22 0
┌──┬─────────────┬──────────────────────────────────┐
│ s│ exp │ frac │
│ │ (8 bit) │ (23 bit) │
└──┴─────────────┴──────────────────────────────────┘
FP16 (half precision, k=5, n=10):
15 14 10 9 0
┌──┬───────┬───────────────┐
│ s│ exp │ frac │
│ │(5 bit)│ (10 bit) │
└──┴───────┴───────────────┘
```
FP32 → FP16 轉換需將 8-bit exponent 轉為 5-bit (調整 bias),23-bit fraction 截斷為 10-bit (捨入)。Figure 2.33 進一步將浮點值分為四類:
* normalized ($\text{exp} \neq 0$ 且 $\text{exp} \neq 2^k - 1$):significand $M = 1 + f$,exponent $E = e - \text{Bias}$
* denormalized ($\text{exp} = 0$):significand $M = f$,exponent $E = 1 - \text{Bias}$
* infinity ($\text{exp} = 2^k - 1, \text{frac} = 0$)
* NaN ($\text{exp} = 2^k - 1, \text{frac} \neq 0$)
Figure 2.35 以 8-bit 浮點格式 ($k = 4, n = 3, \text{bias} = 7$) 列出所有非負可表示值。注意 denormalized 與 normalized 的交界處:最大 denormalized value $= 7/512$ 與最小 normalized value $= 8/512 = 1/64$ 之間的平滑銜接,歸功於 denormalized 使用 $E = 1 - \text{Bias}$ 而非 $E = -\text{Bias}$。
CS:APP 2.87 (homework problem) 要求填寫 half-precision 的 hex/M/E/V/D 表格,包含 $-0$、最小正 denormalized、$512$、最大 denormalized、$-\infty$ 以及 $0\text{x3BB0}$ 所代表的值。此表格驗證學員對 exponent bias、hidden bit、denormalized 編碼規則的理解。
- [ ] Part 2: 向量量化中的 FP32 → FP16 轉換 (含 denormalized 捨入)
大型語言模型的 key/value 快取在推論期間持續膨脹,記憶體用量隨 context length 與 attention head 數量線性成長。TurboQuant 等向量量化方法會把高維浮點向量壓縮成低位元整數索引,但每個量化區塊仍要保存一些輔助資訊,例如 scale 與 offset。若這些輔助資訊仍用 FP32 儲存,壓縮收益會被吃掉一部分;改用 FP16 儲存,才能把整體空間再往下壓。換言之,本題不是孤立的格式轉換練習,而是量化系統中真實會遇到的空間成本問題。
Linux 核心的 MIPS FPU 軟體模擬器 ([`arch/mips/math-emu/`](https://github.com/torvalds/linux/tree/master/arch/mips/math-emu)) 在不具備硬體 FPU 的嵌入式處理器上以純整數運算模擬浮點計算。其 double → single 轉換 ([`sp_fdp.c`](https://github.com/torvalds/linux/blob/master/arch/mips/math-emu/sp_fdp.c)) 與本題的 single → half 轉換邏輯相同:擷取 sign/exponent/fraction 欄位 → 轉換 bias → 處理 denormalized/overflow/underflow → round-to-even 捨入。Linux 核心以 `union` 結構體對映 IEEE 754 欄位:
```c
/* arch/mips/math-emu/ieee754.h (節錄,經簡化) */
union ieee754sp {
struct {
unsigned sign:1;
unsigned bexp:8;
unsigned mant:23;
};
u32 bits;
};
```
此 `union` 型別雙關受 GCC/Clang 明確支援,Linux 核心廣泛依賴此慣例。C99 本文對透過 `union` 讀取非最後寫入成員的行為未明確定義 (附註 82 為非規範性),WG14 Defect Report 283 (TC3 腳註) 後續澄清位元組以新型別重新詮釋,但結果依賴底層表示 (representation),當存取產生 trap representation 時為未定義行為,其餘情況屬 implementation-defined behavior。反之,若以指標強制轉型 `*(uint32_t *)&v` 取代 `union`,則違反 C99 6.5 §7 的 strict aliasing rule,屬未定義行為。本題及後續 Problem B、C 皆以 `union` 實作型別雙關。
以下 FP32 → FP16 轉換函式取自向量量化實作,處理完整的 normalized、denormalized 與 special value 路徑,包含 CS:APP Section 2.4.4 定義的 round-to-even 捨入。讀這段程式碼時,建議先把它分成三件事看:先拆欄位,再依 exponent 決定走哪條路,最後才做捨入。這樣較不容易在細節中迷失。
偏移量轉換可一步完成:$\text{hexp} = e_{\text{FP32}} - (\text{Bias}_{\text{FP32}} - \text{Bias}_{\text{FP16}})$。填空 __ A01 __ 為此淨偏移量。學員須依 CS:APP Figure 2.32 查閱 single precision 的 $k$ 值 (8) 與 half-precision 的 $k$ 值 (5),分別以 $2^{k-1} - 1$ 計算各格式的 bias 後求差。
注意 `int hexp = (int)exp - __A01__` 的 `(int)` 強制轉型。`exp` 的型別為 `unsigned`,若省略 `(int)` 而寫 `exp - 112`,依 C99 6.3.1.8 的 usual arithmetic conversions,`112` 先轉為 `unsigned` 再進行減法,當 `exp < 112` 時無號減法 wrap around 產生大正數 (例如 `exp = 0` 時結果為 $2^{32} - 112$)。此 unsigned 值賦予 `int hexp` 時,依 C99 6.3.1.3 §3 屬 implementation-defined behavior。在 two's complement 實作上 `hexp` 取得負值,`hexp <= 0` 成立,但 `shift = 14 - hexp` 會產生極大的右移量,導致結果錯誤。加上 `(int)` 強制轉型後,減法以 signed 算術執行,`hexp` 正確取得負值且量值合理,後續邏輯才能正確分流。此 signed/unsigned 轉換陷阱見 CS:APP Section 2.2.4。
當 $\text{hexp} \leq 0$ 時結果落入 FP16 denormalized 區間。此時 FP32 的 normalized mantissa ($1.f_{22}f_{21}\ldots f_0$,含 hidden bit) 須右移以對齊 denormalized 的固定指數。依 CS:APP Section 2.4.2,denormalized 的 exponent 為 $E = 1 - \text{Bias}$,因此 FP16 的 denormalized exponent 為 $1 - 15 = -14$。右移量的基底為 $\text{Bias}_{\text{FP16}} - 1$,填空 __ A02 __。
Normalized 路徑截斷 FP32 fraction 的低位元以符合 FP16 的 $n$-bit fraction。學員須依 CS:APP Figure 2.32 查閱兩格式的 $n$ 值差 ($23 - 10 = 13$),截斷時以 round-to-even 判斷:低 13 位元的中點值為 $2^{12}$。填空 __ A03 __ 為此中點位移量。
```c
#include <stdio.h>
#include <stdint.h>
typedef uint32_t u32;
typedef uint16_t u16;
#define FP32_EXP_SHIFT 23
#define FP16_EXP_SHIFT 10
#define FP16_EXP_MAX 31
#define FP32_MANT_WIDTH 24
#define FP32_SIGN_MASK 0x80000000u
#define FP32_EXP_MASK 0x7F800000u
#define FP32_FRAC_MASK 0x007FFFFFu
#define FP16_SIGN_MASK 0x8000u
#define FP16_EXP_MASK 0x7C00u
#define FP16_QNAN 0x7E00u
#define unlikely(x) __builtin_expect(!!(x), 0)
u16 fp32_to_fp16(float v) {
union { float f; u32 u; } bits;
bits.f = v;
unsigned sign = (bits.u & FP32_SIGN_MASK) >> 16;
unsigned exp = (bits.u & FP32_EXP_MASK) >> FP32_EXP_SHIFT;
unsigned frac = bits.u & FP32_FRAC_MASK;
unsigned hsign = sign;
if (exp == 0xFF) {
if (frac == 0)
return (u16)(hsign | FP16_EXP_MASK); /* ±∞ */
/* 簡化實作:統一回傳 canonical quiet NaN,不保留原本 payload */
return (u16)(hsign | FP16_QNAN); /* NaN */
}
if (exp == 0)
return (u16)hsign; /* FP32 denorm → FP16 ±0,保留 -0 的 sign bit */
int hexp = (int)exp - __A01__;
if (unlikely(hexp >= FP16_EXP_MAX))
return (u16)(hsign | FP16_EXP_MASK); /* overflow → ±∞ */
if (hexp <= 0) {
/* denormalized 路徑:還原 hidden bit 後右移 */
unsigned mant = frac | 0x800000;
int shift = __A02__ - hexp;
if (unlikely(shift > FP32_MANT_WIDTH))
return (u16)hsign;
unsigned val = mant >> shift;
unsigned lost = mant & ((1u << shift) - 1);
unsigned half = 1u << (shift - 1);
/* round-to-even */
if (lost > half || (lost == half && (val & 1)))
val++;
return (u16)(hsign | val);
}
/* normalized 路徑:截斷 fraction 並捨入 */
unsigned hfrac = frac >> 13;
unsigned lost = frac & 0x1FFF;
unsigned half = 1u << __A03__;
if (lost > half || (lost == half && (hfrac & 1))) {
hfrac++;
if (hfrac == 0x400) { /* fraction 進位溢至 exponent */
hfrac = 0;
hexp++;
if (unlikely(hexp >= FP16_EXP_MAX))
return (u16)(hsign | FP16_EXP_MASK);
}
}
return (u16)(hsign | ((unsigned)hexp << FP16_EXP_SHIFT) | hfrac);
}
```
若想快速自我檢查,可用最小測試程式驗證 `1.0f` 是否轉成 FP16 的 `3C00`:
```c
int main(void)
{
u16 h = fp32_to_fp16(1.0f);
/* 預期輸出 3C00 */
printf("%04X\n", h);
return 0;
}
```
驗證 (normalized):`float v = 1.0f` 的 FP32 為 `0x3F800000` (exp=127, frac=0)。hexp $= 127 - 112 = 15$,hfrac=0,結果 `0x3C00`。查 CS:APP Figure 2.36,value 1.0 的 exp 欄位 = Bias,$E = 0, M = 1$,符合 FP16 `0x3C00` 的 $e = 15, E = 0, M = 1$。
驗證 (denormalized 邊界):FP32 `0x387FE000` (exp=112, frac=`0x7FE000`) 轉換時 hexp $= 112 - 112 = 0$,進入 denormalized 路徑。mant $= \text{0x7FE000} | \text{0x800000} = \text{0xFFE000}$,shift $= 14 - 0 = 14$,右移後 val $= \text{0x3FF}$。lost $= \text{0x2000}$ 恰等於 half $= \text{0x2000}$,且 val 末位為 1,觸發 round-to-even 進位,val $= \text{0x400}$。結果 `0x0400` 恰為 FP16 最小 normalized 值 ($e = 1, f = 0, M = 1, E = 1 - 15 = -14$),越過 denormalized/normalized 邊界,正是 CS:APP Section 2.4.2 強調的平滑銜接。
驗證 (round-to-even 截斷):FP32 `0x` __ A04 __ 代表值 $1.0 + 2^{-11}$。其 frac = `0x001000` (第 12 位元為 1,其餘為 0)。學員須依 CS:APP Figure 2.32 的 single precision 欄位配置,驗證 $1.0 + 2^{-11}$ 的 frac 欄位:fraction 的第 $i$ 位元 (從 MSB,$i = 0$ 起) 代表 $2^{-(i+1)}$,故 $2^{-11}$ 對應 frac 的第 $i = 10$ 位元,即 bit 位置 $22 - 10 = 12$,hex 為 `0x001000`。hexp = 15,hfrac $= 0x001000 >> 13 = 0$,lost $= 0x1000$,half $= 0x1000$。lost == half 且 hfrac 末位為 0 (偶數),不進位。FP16 結果為 `0x3C00` (value 1.0),精度損失 $= 2^{-11} \approx 0.049\%$。此行為與 CS:APP Section 2.4.4 的 round-to-even 規則一致:中點處向偶數方向捨入。
驗證 (overflow 路徑):FP16 最大 normalized 值的 biased exponent 為 $2^5 - 2 = 30$,fraction 全 1 ($10$ 個 1),significand $M = 2 - 2^{-10}$,$E = 30 - 15 = 15$,值為 $(2 - 2^{-10}) \times 2^{15} = 65504$。此值的 FP32 表示為 `0x` __ A05 __。學員須依 CS:APP Figure 2.32 建構 FP32 位元樣式:$65504 = 1.1111111111_2 \times 2^{15}$,故 FP32 exp $= 15 + 127 = 142$,frac 高 10 位元全 1,低 13 位元全 0。介於 65504 與 65536 之間的 FP32 值 (hexp = 30) 在 normalized 路徑中截斷或捨入至 `0x7BFF` (65504);但若捨入使 fraction 進位溢位 (hfrac 從 `0x3FF` 進位至 `0x400`),hexp 遞增至 31,進入 overflow 路徑回傳 `0x7C00` ($+\infty$)。FP32 exp $\geq 143$ (即 hexp $\geq 31$,值 $\geq 2^{16}$) 則直接 overflow。
延伸閱讀:數值截斷屬 [CWE-197](https://cwe.mitre.org/data/definitions/197.html) (Numeric Truncation Error)。[CVE-2021-33200](https://nvd.nist.gov/vuln/detail/CVE-2021-33200) 中 BPF verifier 的 pointer arithmetic limit 追蹤 (`aux->alu_limit`) 存在值域精度不足的 corner case,允許攻擊者繞過安全檢查,存取越界記憶體。Linux 核心雖禁止在一般路徑使用浮點運算,但 MIPS FPU 軟體模擬器與 BPF verifier 的數值截斷邏輯仍需嚴格處理精度損失。CS:APP Practice Problem 2.49 指出 $n$-bit fraction 浮點格式無法精確表示的最小正整數為 $2^{n+1} + 1$。在 FP16 ($n = 10$) 下此值為 $2^{11} + 1 = 2049$。量化系統中以 FP16 儲存的 codebook centroid 精度同樣受此限制。
除 MIPS FPU 外,GPU 裝置驅動程式亦涉及 FP16 轉換。DRM/AMDGPU 的色彩管理子系統 ([`drivers/gpu/drm/amd/display/dc/basics/`](https://github.com/torvalds/linux/tree/master/drivers/gpu/drm/amd/display/dc/basics)) 以定點數實作 gamma 校正與色彩空間轉換,內部使用的查表值 (LUT) 常以 FP16 格式儲存。若 round-to-even 處理有誤,會在色彩梯度上產生 banding 瑕疵 (量化雜訊),使用者可在螢幕上直接觀察到精度損失的後果。
### 延伸問題
* MIPS FPU 軟體模擬器的 double → single 轉換 (Section 2.4.4 round-to-even) 與 Part 2 的 `fp32_to_fp16` 捨入邏輯直接對應。以此為出發點:
* (a) 閱讀 [`arch/mips/math-emu/sp_fdp.c`](https://github.com/torvalds/linux/blob/master/arch/mips/math-emu/sp_fdp.c) 的 `ieee754sp_fdp()`,追蹤 52-bit fraction 截斷至 23-bit 時的 guard ($G$) / round ($R$) / sticky ($S$) bit 計算。以 $G$、$R$、$S$ 與截斷後最低位元 $L$ 推導 round-to-even 進位條件 $G \land (R \lor S \lor L)$,驗證其與 Part 2 的 `lost > half || (lost == half && (val & 1))` 等價。對照 Section 2.4.4 Figure 2.37 的四種捨入模式,說明 round-to-even 在 discarded bits 均勻分布假設下期望捨入誤差為零的數學依據
* (b) Practice Problem 2.49 指出 $n$-bit fraction 浮點格式無法精確表示的最小正整數為 $2^{n+1}+1$,FP16 ($n=10$) 為 2049。閱讀 [`kernel/time/clocksource.c`](https://github.com/torvalds/linux/blob/master/kernel/time/clocksource.c) 的 `clocks_calc_mult_shift()`,此函式以 $t_{\text{ns}} = (\text{cycles} \times \text{mult}) \gg \text{shift}$ 將 TSC 週期轉為奈秒,須在 64 位元溢位約束下最大化 `shift`。推導 `shift` 與相對轉換誤差 ($\leq 2^{-\text{shift}}$) 的關係,並類比 IEEE 754 的精度/範圍權衡:定點的 `shift` 對應 fraction 寬度 $n$,增大 $n$ 提升精度但壓縮動態範圍
* \(c) [CVE-2021-33200](https://nvd.nist.gov/vuln/detail/CVE-2021-33200) 中 BPF verifier 的 `aux->alu_limit` 在計算指標算術邊界時,對 masking 方向的處理存在 corner case,使攻擊者可繞過範圍檢查存取越界記憶體。閱讀 [`kernel/bpf/verifier.c`](https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c) 的修補差異,追蹤 `sanitize_ptr_alu()` 如何修正 `alu_limit` 在負偏移時的值域追蹤。更廣泛地,BPF verifier 的 `coerce_reg_to_size()` 在 64→32 位元截斷時亦曾因未同步更新有號值域 (`smin_value`/`smax_value`) 而被利用。此類漏洞與 Part 2 的 FP32→FP16 轉換在概念上對應:二者皆為寬精度到窄精度的截斷,若未正確追蹤截斷後的值域,下游邏輯會基於錯誤假設做出不安全決策
* Part 2 的 `(int)exp - 112` 涉及 Section 2.2.6 的 usual arithmetic conversions (C99 6.3.1.8)。以此為出發點:
* (a) 若省略 `(int)` 而寫 `exp - 112`,當 `exp < 112` 時無號減法 wrap around 產生大正數 (Section 2.3.1)。以 GCC `-O2` 編譯有/無 `(int)` 轉型兩版,比較 x86-64 組合語言的分支指令差異 (`jle` vs `jbe`)。[CVE-2017-16995](https://nvd.nist.gov/vuln/detail/CVE-2017-16995) 的 BPF verifier `check_alu_op()` 正是因相同的 signed/unsigned 混用陷阱而被利用,攻擊者得以用負值繞過範圍檢查
* (b) 閱讀 [`arch/x86/kernel/fpu/core.c`](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/fpu/core.c),追蹤 `kernel_fpu_begin()` 如何以 `fpregs_lock()` 停用 preemption 並儲存 FPU 狀態 (現代 x86 以 `TIF_NEED_FPU_LOAD` 管理)。說明核心為何禁止一般路徑使用浮點:若 SIMD 區段中允許 context switch,被切入行程的浮點狀態會被靜默破壞。閱讀 [`lib/raid6/avx2.c`](https://github.com/torvalds/linux/blob/master/lib/raid6/avx2.c),追蹤 RAID6 如何以 `vpshufb` 實作 $GF(2^8)$ 的 4-bit 查表乘法 (Section 2.1.3 的位元操作在 SIMD 層級的擴充);
* \(c) DRM/AMDGPU 的 [`drivers/gpu/drm/amd/display/dc/basics/conversion.c`](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/display/dc/basics/conversion.c) 以純整數位元操作轉換 FP16 色彩值。在 HDR 內容中,FP16 denormalized 區間 ($[2^{-24}, 2^{-14})$) 對應暗部畫素,Figure 2.35 顯示 denormalized 為等差數列;若 round-to-even 有誤,會在暗部梯度產生 banding 瑕疵。設計一組落在 FP16 denormalized 邊界的 FP32 輸入,至少涵蓋 tie case 與非 tie case,比較 round-to-even 與 round-towards-zero 的輸出差異
---
## Problem B: 位元打包、codebook 量化與反量化迴路
> 延伸 CS:APP Section 2.1.3 (Bit-Level Operations) / Problem 2.67 / Practice Problem 2.49 / Figure 2.35
題目設計要點:
* 表面主題是 TurboQuant,實際驗證的是固定寬度欄位的打包、解包與位元安全性
* `pack_indices` / `unpack_indices` 可視為手動實作一個極小型 bitstream format,
* 延伸問題把 shift/mask、節點打包與走訪安全性連到 Linux 核心內部資料結構設計
- [ ] Part 1: 位元操作與量化的對應
CS:APP Section 2.1.3 介紹 C 語言的 bitwise 運算子 (`&`, `|`, `~`, `^`)。Figure 2.35 的 8-bit 浮點格式與本題的 codebook 量化,雖然表面形式不同,但都在回答同一件事:位元數有限時,如何用少量離散值去近似連續世界。差別只在於 Figure 2.35 討論的是浮點格式本身,本題討論的是量化後索引如何儲存。
TurboQuant 的關鍵觀察是:若先把高維向量旋轉到較合適的座標系,每個座標就能近似視為一個獨立的一維隨機變數。這樣一來,高維量化問題就能拆成很多個簡單的一維量化問題。對本題而言,學員不需要完整重建論文證明,只要理解 codebook 是把連續值映到少數代表值,索引則是代表值的編號即可。
論文 (p10) 指出,在足夠高的維度 $d$ 下,旋轉後座標的 Beta 分布趨近 $\mathcal{N}(0, 1/d)$,最佳 Lloyd-Max centroid 以 $1/\sqrt{d}$ 縮放。以標準 $\mathcal{N}(0, 1)$ 分布為參照 (實際 centroid 為以下值除以 $\sqrt{d}$),1-bit 與 2-bit 最佳 centroid 分別為:
* 1-bit ($b = 1$, 2 個 centroid):$\{\pm\sqrt{2/\pi}\} \approx \{-0.7979, +0.7979\}$,縮放後 $\{\pm\sqrt{2/\pi}/\sqrt{d}\}$
* 2-bit ($b = 2$, 4 個 centroid):$\{-1.510, -0.453, +0.453, +1.510\}$,縮放後各除以 $\sqrt{d}$
CS:APP Problem 2.67 揭示位移量等於型別寬度時的未定義行為 (`1 << 32`)。此問題在位元打包中尤為相關:當 `bits` 等於或超過 `int` 寬度時,`1 << bits` 觸發 C99 6.5.7 §3 的未定義行為。
- [ ] Part 2: 完整的量化-反量化迴路
以下程式碼實作完整的 TurboQuant MSE 量化-反量化迴路 (簡化版),結合 codebook 查表、3-bit 位元打包、L2 norm 保存與反量化重建。若只看控制流程,可以把它拆成先把浮點值對映到索引,再把索引塞進位元組陣列,最後再把索引還原回代表值三步。
Linux 核心的位元操作巨集定義於 [`include/linux/bitops.h`](https://github.com/torvalds/linux/blob/master/include/linux/bitops.h) 與 [`include/vdso/bits.h`](https://github.com/torvalds/linux/blob/master/include/vdso/bits.h)。`BIT(nr)` 巨集以 `UL(1) << (nr)` 取代 `1 << (nr)`,避免有號左移的未定義行為:
```c
/* include/vdso/bits.h (節錄) */
#define BIT(nr) (UL(1) << (nr))
/* include/linux/bitops.h (節錄,經簡化) */
static inline void set_bit(int nr, volatile unsigned long *addr)
{
unsigned long mask = BIT(nr % BITS_PER_LONG);
addr[nr / BITS_PER_LONG] |= mask;
}
static inline int test_bit(int nr, const volatile unsigned long *addr)
{
return 1UL & (addr[nr / BITS_PER_LONG] >> (nr % BITS_PER_LONG));
}
```
`set_bit` 和 `test_bit` 的邏輯與本題 `pack_indices` / `unpack_indices` 的逐位元操作完全對應:`nr / BITS_PER_LONG` 等同 `bp >> 3` (選取位元組),`nr % BITS_PER_LONG` 等同 `bp & 7` (位元組內偏移)。差別在於 Linux 核心操作 `unsigned long` 陣列 (每元素 32 或 64 位元),量化系統操作 `u8` 陣列 (每元素 8 位元)。本題刻意在 `pack_indices` 保留一處 `__B05__ << b`,讓學員直接面對「位移起點應為哪種型別」這個核心問題;`unpack_indices` 的 `(1 << bits)` 則保留作為遮罩推導題,讓位移風險與遮罩邏輯分開觀察。
量化流程 (Algorithm 1 of TurboQuant):
1. 計算輸入向量的 L2 norm 並正規化
2. 以預計算的隨機正交矩陣旋轉 (此處簡化為 identity)
3. 對每個座標找最近的 codebook centroid,記錄索引
4. 將索引以 $b$-bit 打包至位元組陣列
其中第 3 步可視為把連續值換成離它最近的桶子編號,第 4 步則是把這些編號緊密排進位元流。Problem B 的填空主要都落在這兩步。
```c
#include <stdint.h>
#include <string.h>
#include <math.h>
typedef uint8_t u8;
#define HEAD_DIM 128
#define BITS 3
/* 注意:論文的 codebook 是對 Beta 分布 f_X (Lemma 1) 求解
* Lloyd-Max 最佳化 (Eq. 4) 而得,centroid 值域在 [-1, 1] 且
* 隨維度 d 縮放 (近似 N(0, 1/d) 的 centroid)。以下值為
* 標準 N(0,1) 的近似 3-bit centroid,未經 1/sqrt(d) 縮放,
* 僅作為位元打包教學範例使用。填空答案不受此簡化影響。 */
static const float CODEBOOK_3[8] = {
-1.748f, -1.050f, -0.500f, -0.069f,
0.069f, 0.500f, 1.050f, 1.748f
};
typedef struct {
float norm; /* L2 norm (FP32) */
u8 indices[(HEAD_DIM*BITS+7)/8]; /* packed 3-bit indices */
} block_tq3;
/* 打包:將 n 個 b-bit 索引寫入位元組陣列 */
static void pack_indices(const u8 *idx, u8 *packed,
int n, int bits) {
memset(packed, 0, (n * bits + 7) / 8);
int bp = 0;
for (int i = 0; i < n; i++) {
for (int b = 0; b < bits; b++) {
if (idx[i] & (__B05__ << b))
packed[bp >> 3] |= (1u << (bp & __B01__));
bp++;
}
}
}
/* 解包:從位元組陣列讀出 n 個 b-bit 索引 */
static void unpack_indices(const u8 *packed, u8 *idx,
int n, int bits) {
int bp = 0;
u8 mask = (1 << bits) - __B02__;
for (int i = 0; i < n; i++) {
u8 val = 0;
for (int b = 0; b < bits; b++) {
if (packed[bp >> 3] & (1u << (bp & 7)))
val |= (1u << b);
bp++;
}
idx[i] = val & mask;
}
}
void tq3_quantize(const float *src, block_tq3 *blk) {
/* Step 1: L2 norm */
float norm_sq = 0.0f;
for (int i = 0; i < HEAD_DIM; i++)
norm_sq += src[i] * src[i];
float norm = sqrtf(norm_sq);
blk->norm = norm;
float inv_norm = (norm > 1e-15f) ? 1.0f / norm : 0.0f;
/* Step 2: 正規化 + 最近 centroid 查表 */
u8 idx[HEAD_DIM];
for (int i = 0; i < HEAD_DIM; i++) {
float y = src[i] * inv_norm;
float best_dist = __builtin_huge_valf();
u8 best = 0;
for (int c = 0; c < (1 << BITS); c++) {
float d = (y - CODEBOOK_3[c]);
d *= d;
if (d < best_dist) { best_dist = d; best = (u8)c; }
}
idx[i] = best;
}
/* Step 3: 打包 */
pack_indices(idx, blk->indices, HEAD_DIM, BITS);
}
void tq3_dequantize(const block_tq3 *blk, float *dst) {
u8 idx[HEAD_DIM];
unpack_indices(blk->indices, idx, HEAD_DIM, BITS);
for (int i = 0; i < HEAD_DIM; i++)
dst[i] = blk->norm * CODEBOOK_3[__B03__];
}
```
填空說明:
打包的 `bp & __B01__` 計算位元在位元組內的偏移量。`bp >> 3` 選取目標位元組 (等同 `bp / 8`),`bp & __B01__` 取得位元組內偏移 (等同 `bp % 8`)。學員須依 CS:APP Section 2.1.3 的位元遮罩性質推導:$x \bmod 2^k = x\ \&\ (2^k - 1)$,此處 $k = 3$。
解包的 `mask = (1 << bits) - __B02__`。$b$-bit 索引的有效值域為 $[0, 2^b - 1]$,遮罩須涵蓋低 $b$ 位元全為 1 的樣式。學員須依 CS:APP Section 2.1.3 推導 $(1 \ll b) - 1$ 產生 $b$ 個 1 位元的遮罩。
反量化的 `CODEBOOK_3[__B03__]` 以索引查表還原 centroid 值,再乘以 `norm` 還原尺度。此處 __ B03 __ 為第 $i$ 個座標的 codebook 索引變數。
`pack_indices` 中的 `__B05__ << b` 是本題刻意保留的風險點。若直接寫成 `1 << b`,會以有號 `int` 型別的 `1` 參與運算。本題 `bits = 3`,`b` 至多為 2,實際上不會溢位;但若泛化至 `bits = 31`,`1 << 31` 使 $1 \times 2^{31}$ 超出 32 位元 `int` 的可表示範圍 $[-2^{31}, 2^{31}-1]$,依 C99 6.5.7 §4 屬未定義行為 (CS:APP Problem 2.67)。因此 __ B05 __ 應填入無號常數,使左移在無號型別上執行。`unpack_indices` 的 `mask = (1 << bits) - __B02__` 則保留原貌,因為該處要先驗證學員是否能從 $(1 \ll b) - 1$ 正確推得低位元遮罩。
驗證 (打包/解包對稱性):令 `idx = {5, 3, 7}`,bits $= 3$。以二進位表示:$5 = 101_2, 3 = 011_2, 7 = 111_2$。打包順序為 LSB-first per index,位元串依 `bit_pos` 遞增排列:
```text
Index packing visualization (3-bit, LSB-first):
[ MSB <------------------- Byte 0 ------------------- LSB ]
Bit weight: 7 6 5 4 3 2 1 0
Byte 0 content: I2b1 I2b0 I1b2 I1b1 I1b0 I0b2 I0b1 I0b0
[ MSB <------------------- Byte 1 ------------------- LSB ]
Bit weight: 7 6 5 4 3 2 1 0
Byte 1 content: 0 0 0 0 0 0 I2b2 -
Index 0 = 5 = 101b -> bit positions 0,1,2
Index 1 = 3 = 011b -> bit positions 3,4,5
Index 2 = 7 = 111b -> bit positions 6,7,8
```
| bit_pos | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 來源 | 5 bit0 | 5 bit1 | 5 bit2 | 3 bit0 | 3 bit1 | 3 bit2 | 7 bit0 | 7 bit1 | 7 bit2 |
| 值 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 |
* packed[0]:位元 [7:0] = `11011101` = `0x` __ B04 __
* packed[1]:位元 [8] = `00000001` = `0x01`
解包後應還原 `{5, 3, 7}`。此打包/解包構成 bijection (一對一映射),每個 $3 \times \text{HEAD\_DIM} = 384$ 位元的位元串恰好對映一組 128 個 3-bit 索引,無歧義。
驗證 (量化誤差):假設某座標正規化後 $y = 0.3$。最近 centroid 為 `CODEBOOK_3[5]` $= 0.500$,量化誤差 $= |0.3 - 0.5| = 0.2$。此誤差乘以 norm 後回到原始尺度:$\Delta = 0.2 \times \text{norm}$。TurboQuant 論文 Theorem 1 證明,3-bit MSE 量化的期望失真上界為 $D_{\text{mse}} \approx 0.03$。
驗證 (壓縮率):原始向量 $128 \times 4 = 512$ 位元組。量化區塊:$4$ (norm) $+ 48$ (indices) $= 52$ 位元組。壓縮比 $= 512 / 52 \approx 9.8\times$。
延伸閱讀:位元操作錯誤屬 [CWE-682](https://cwe.mitre.org/data/definitions/682.html) (Incorrect Calculation)。Linux 核心自 2017 年起大規模將裝置驅動程式中的 `1 << N` 改為 `BIT(N)`,消除數百處有號位移未定義行為。UBSan 與 Coverity 持續偵測此類缺陷。[`include/linux/bitmap.h`](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) 的 `bitmap_read()` / `bitmap_write()` 是本題 `pack_indices` / `unpack_indices` 在 Linux 核心中最直接的對應,處理跨 word 邊界的任意位元寬度讀寫。
位元寬度不足的風險同樣適用於引用計數。[CVE-2016-0728](https://nvd.nist.gov/vuln/detail/CVE-2016-0728) 是經典案例:`security/keys/keyring.c` 中 keyring 的引用計數以 `atomic_t` (本質為 `int`) 儲存,攻擊者透過反覆呼叫 `keyctl()` 系統呼叫使計數溢位歸零,觸發 premature free,進而達成 local privilege escalation。
此漏洞促使核心引入 [`include/linux/refcount.h`](https://github.com/torvalds/linux/blob/master/include/linux/refcount.h) 的 `refcount_t` 型別,取代裸 `atomic_t` 用於引用計數:
```c
/* include/linux/refcount.h (節錄,經簡化) */
typedef struct refcount_struct {
atomic_t refs;
} refcount_t;
static inline void refcount_inc(refcount_t *r)
{
int old = atomic_fetch_add_relaxed(1, &r->refs);
if (unlikely(old < 0 || old + 1 < 0))
refcount_warn_saturate(r, REFCOUNT_ADD_OT_OVERFLOW);
}
```
`refcount_t` 在每次增減時檢查溢位與 underflow,若偵測到異常則觸發 `WARN` 並將計數飽和 (saturate) 至安全值。此設計以明確的邊界檢查取代隱式假設,自 2016 年起核心中數千處 `atomic_inc` / `atomic_dec_and_test` 已轉換為 `refcount_inc` / `refcount_dec_and_test`,顯著降低 use-after-free 類漏洞的攻擊面。此案例直接延伸 CS:APP Section 2.3 的整數算術溢位討論。
HID (Human Interface Device) 子系統提供另一個經典案例。[`drivers/hid/hid-core.c`](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-core.c) 的 `extract()` 函式從 HID report 中擷取任意位元寬度的欄位 (例如 3-bit 按鍵狀態接著 5-bit 填充),手動處理位元組邊界的跨越,邏輯與本題的 `unpack_indices` 幾乎相同。HID 規範允許欄位以任意位元偏移排列,因此不能使用 C 的 bit-field (其記憶體配置為 implementation-defined),必須如 CS:APP Section 2.1.3 所述以 shift/mask 手動操作。
Problem B 的 `pack_indices` / `unpack_indices` 以手動 shift/mask 操作位元,而非使用 C 語言的 bit-field。Linux 核心在網路協定標頭與硬體暫存器定義中採取相同策略,原因是 C 語言 bit-field 的記憶體配置為 implementation-defined (C99 6.7.2.1 §11):
* bit-field 在配置單元 (allocation unit) 中的排列方向 (高位元優先或低位元優先) 由實作決定
* 相鄰 bit-field 是否跨越配置單元邊界由實作決定
* 配置單元的大小由實作決定
[`include/uapi/linux/ip.h`](https://github.com/torvalds/linux/blob/master/include/uapi/linux/ip.h) 的 `struct iphdr` 是經典案例,以條件編譯處理 bit-field 順序:
```c
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#endif
__u8 tos;
__be16 tot_len;
/* ... */
};
```
此做法的代價是每個含 bit-field 的結構都需要兩份定義。更現代的核心程式碼傾向完全避免 bit-field,改用明確的型別標記 (`__be16`、`__le32`) 搭配位元組序轉換巨集 (`cpu_to_be32()`、`le16_to_cpu()` 等,定義於 [`include/linux/byteorder/generic.h`](https://github.com/torvalds/linux/blob/master/include/linux/byteorder/generic.h))。Sparse 靜態分析工具 (`__CHECK_ENDIAN__`) 可在編譯時期偵測位元組序混用錯誤。
Problem B 的 `pack_indices` 採用 LSB-first 排列並以 `uint8_t` 為最小單元,天然為 endian-neutral:無論主機位元組序為何,`packed[bp >> 3]` 的語義不變。這與 Linux 核心 [`include/linux/bitmap.h`](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) 的 `bitmap_read()` / `bitmap_write()` 不同,後者操作 `unsigned long` 陣列,在 big-endian 架構上需額外處理 word 內的位元組序。
C/C++ 標準將 bit-field 的記憶體配置、對齊與排列順序定義為 implementation-defined。依 Application Binary Interface 不同 (如 x86-64 的 System V ABI 與 AArch64 的 AAPCS),編譯器可能從高位元或低位元開始填充,導致與硬體暫存器或網路協定互動時產生 endianness 不匹配。更嚴重的是,若二個 bit-field 共用同一個自然對齊的 word,從不同 CPU core 同時修改相鄰欄位可能引發 read-modify-write tearing,破壞並行安全性。因此 Linux 核心大量採用明確的 bitwise 操作搭配標準化 API。
`cpumask` 子系統追蹤各種拓撲下的 CPU core 狀態 (`cpu_online_mask`、`cpu_possible_mask`、`cpu_active_mask`):
```c
/* include/linux/cpumask.h (節錄) */
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
```
`DECLARE_BITMAP` 巨集展開為精確以 `BITS_TO_LONGS` 除法 (向上取整至最近的 word 邊界) 計算大小的 `unsigned long` 陣列。對這些 mask 的操作使用 `set_bit()`、`clear_bit()`、`test_bit()` 等 atomic 或 non-atomic bitwise 函式,在並行 SMP 環境中提供跨平台的一致性與 atomics。此邏輯與 Problem B 的 `pack_indices` / `unpack_indices` 逐位元操作完全對應。
`struct page` 則展示核心中最極端的位元打包案例。實體記憶體被分割為個別 page,每個 page 以一個 `struct page` 描述子表示。由於典型系統中存在數百萬個 page,`struct page` 每增加一個位元組就會浪費數 GiB 的 RAM。核心將大量狀態資訊塞入結構頂端的單一 `unsigned long flags;` 欄位,此 word 被多工 (multiplex) 以容納:
* Page 狀態旗標
> 個別位元指示 page 是否 dirty、locked、處於 writeback、或正參與 LRU 串列。
* Zone 與 Node ID
> Non-Uniform Memory Access (NUMA) 節點與記憶體 zone 資訊。
* Section 細節
> 啟用特定功能時,資料透過 inline 函式與位移操作動態對映至既有結構空間,在不破壞 ABI 的前提下擷取精確的 mask。
核心提供 [`include/linux/packing.h`](https://github.com/torvalds/linux/blob/master/include/linux/packing.h) API,讓開發者穩健地擷取與打包自訂位元欄位,避免編譯器特定的填充與對齊假設。此 API 與 Problem B 的 `pack_indices` / `unpack_indices` 在概念上直接對應,皆以手動 shift/mask 操作確保位元配置的可預測性。
[Linux 核心的紅黑樹](https://hackmd.io/@sysprog/linux-rbtree)實作 ([`include/linux/rbtree_types.h`](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_types.h)) 展示另一種位元打包策略,將指標與旗標欄位合併至單一 word,與本題量化索引打包在概念上相通。以下程式碼取自 Linux 核心,以填空形式測驗學員對位元打包、指標對齊與 C 語言規格的理解:
```c
/* include/linux/rbtree_types.h (節錄) */
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
#define RB_RED 0
#define RB_BLACK 1
```
`__rb_parent_color` 將 parent 指標與節點顏色 (紅/黑) 打包至一個 `unsigned long`。`__attribute__((aligned(sizeof(long))))` 強制每個 `rb_node` 對齊至 `sizeof(long)` 邊界 (32 位元系統為 4 位元組、64 位元系統為 8 位元組),確保任何合法 `rb_node *` 位址的低 2 位元恆為零。Linux 核心利用 bit 0 儲存顏色,高位元儲存 parent 指標:
```
__rb_parent_color 的位元配置:
MSB 2 1 0
┌────────────────────────────────────────────┬───┬───┐
│ parent pointer (高位元) │ 0 │ C │
└────────────────────────────────────────────┴───┴───┘
bit 2 以上:parent 指標 ▲
(對齊保證低 2 位元為 0) color: 0=RED, 1=BLACK
```
擷取 parent 指標與設定 parent/color 的操作 ([`include/linux/rbtree.h`](https://github.com/torvalds/linux/blob/master/include/linux/rbtree.h)、[`include/linux/rbtree_augmented.h`](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_augmented.h)):
```c
/* 擷取 parent 指標:清除低 2 位元 */
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & __B06__))
/* 設定 parent 與 color */
static inline void rb_set_parent_color(struct rb_node *rb,
struct rb_node *p, int color)
{
rb->__rb_parent_color = (unsigned long)p + color;
}
/* 新節點連結 (隱含初始顏色為紅色,因為對齊指標 bit 0 = 0 = RB_RED) */
static inline void rb_link_node(struct rb_node *node, struct rb_node *parent,
struct rb_node **rb_link)
{
node->__rb_parent_color = (unsigned long)parent;
node->rb_left = node->rb_right = NULL;
*rb_link = node;
}
```
填空 __ B06 __ 為 `rb_parent()` 的遮罩,須清除 bit 0 和 bit 1 以還原對齊指標。學員須依 CS:APP Section 2.1.3 推導:`~3` 在 C 語言中以 `int` 型別表示為 `...11111100`,但在與 `unsigned long` 做 `&` 運算時,依 C99 6.3.1.8 的 usual arithmetic conversions 隱式轉為 `unsigned long`,所有高位元補 1。填空仍以最精簡的 C 運算式作答,canonical form 為 `~3`。若從核心實作習慣出發,`~3UL` 更能直接表達「此遮罩工作在 unsigned long 寬度」;本題刻意保留 `~3` 作答,是為了先驗證學員是否理解 usual arithmetic conversions 的補寬過程。
`rb_set_parent_color` 使用 `+` 而非 `|`。二者在此等價,因為對齊指標低位元為零,加上 0 或 1 不會進位至 bit 2。這是 CS:APP Section 2.3 的無號算術性質:當加數的非零位元不重疊時,加法與 OR 結果相同。
`rb_link_node` 中 `node->__rb_parent_color = (unsigned long)parent` 將新節點隱式著色為紅色 (RED=0),因為對齊指標的 bit 0 恆為 0,不需額外設定顏色。此設計呼應紅黑樹插入時新節點預設為紅色的性質。
驗證:令 `parent = (struct rb_node *)0x7FFE0040` (低 2 位元為 0,符合對齊),`color = RB_BLACK (1)`。呼叫 `rb_set_parent_color(node, parent, 1)` 後,`node->__rb_parent_color = 0x7FFE0040 + 1 =` `0x` __ B07 __。隨後 `rb_parent(node)` 回傳 `(struct rb_node *)(0x7FFE0041 & ~3) = (struct rb_node *)0x7FFE0040`,正確還原 parent 指標。
此技巧涉及的 C 語言規格與效能議題:
* 指標與整數的轉換 (C11 6.3.2.3 §5-6) 屬 implementation-defined behavior。C 標準僅保證 `uintptr_t` (C11 7.20.1.4) 可無損保存指標值,不保證 `unsigned long` 亦可。Linux 核心在其支援的平面位址空間 (flat address space) 架構上額外假設 `(unsigned long)ptr` 可保留並還原位址資訊,此假設由 GCC 與 Linux 核心的型別系統共同保證 (`sizeof(unsigned long) == sizeof(void *)`)
* `__attribute__((aligned(sizeof(long))))` 是 GCC 擴展 (extension),強化結構體的對齊需求。C 標準 (C11 6.2.8) 僅規定對齊值為 implementation-defined 的 2 的冪,不保證特定值。Linux 核心原始碼的註解指出,CRIS 架構是最初促使加入此強制對齊的原因
* `__rb_change_child()` 以 `WRITE_ONCE()` 更新子節點指標,防止編譯器將指標寫入拆分為多次 store 或最佳化消除。此巨集在 [`include/asm-generic/rwonce.h`](https://github.com/torvalds/linux/blob/master/include/asm-generic/rwonce.h) 以 `volatile` 語義實作,約束編譯器對該存取產生單次 C 語言層級的寫入,對自然對齊且大小在架構支援範圍內的存取可防止 store tearing,確保與 RCU (Read-Copy-Update) 讀取端的正確配對。ISO C 的 `volatile` 語義 (C11 6.7.3 §7) 僅保證對 volatile 物件的存取不被最佳化消除,但不保證存取的 atomics 或排序。Linux 核心的 `WRITE_ONCE` / `READ_ONCE` 在此基礎上搭配編譯器屏障 (`barrier()`),提供比 ISO C `volatile` 更精確的語義
除了在低位元塞入顏色旗標,核心也把錯誤碼編進不可能為合法指標的位址區段。[`include/linux/err.h`](https://github.com/torvalds/linux/blob/master/include/linux/err.h) 定義的 `ERR_PTR` / `IS_ERR` / `PTR_ERR` 三件組將 errno 值編碼在指標的高位元區段。
```c
/* include/linux/err.h (節錄,經簡化) */
#define MAX_ERRNO 4095
#define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO)
static inline void * ERR_PTR(long error) {
return (void *) error;
}
static inline long PTR_ERR(const void *ptr) {
return (long) ptr;
}
static inline bool IS_ERR(const void *ptr) {
return IS_ERR_VALUE((unsigned long)ptr);
}
```
此設計利用 Linux 核心虛擬位址空間的頂端 4095 位元組 ($-1$ 至 $-4095$) 在所有支援的架構上永遠不會是合法的核心指標 (此區域為保留位址範圍),因此可安全地將這些值重新詮釋為錯誤碼。`IS_ERR` 的判斷以無號比較 `>= (unsigned long)-MAX_ERRNO` 實作,等同檢查指標值是否落在 `[0xFFFFF001, 0xFFFFFFFF]` (32 位元) 或對應的 64 位元區間。這與 Problem B 的 `rb_parent()` 遮罩在概念上對稱:後者從低位元擷取額外資訊 (顏色),前者從高位元區段擷取額外資訊 (errno)。
此機制使核心函式以單一指標回傳值同時傳遞成功結果 (合法指標) 與失敗原因 (錯誤碼),避免額外的 out-parameter 或全域 `errno` 變數。呼叫端的典型模式為:
```c
struct inode *inode = iget_locked(sb, ino);
if (IS_ERR(inode))
return PTR_ERR(inode); /* 傳播錯誤碼 */
```
若未檢查 `IS_ERR` 而直接解參考回傳的指標,會觸發 kernel oops (存取非法位址)。Sparse 靜態分析工具以 `__must_check` 標註強制呼叫端檢查回傳值。此模式與 CS:APP Section 2.2.4 的 signed/unsigned 轉換直接相關:`ERR_PTR(-ENOMEM)` 將負的 `long` 轉為 `void *`,`PTR_ERR` 再轉回 `long`,整個迴路依賴 two's complement 表示法在 signed/unsigned 間的位元保持性質。
快取與記憶體配置:相較於將 `parent`、`left`、`right`、`color` 分開儲存 (4 欄位,64 位元系統需 32 位元組含填充),打包後僅 3 欄位 (24 位元組),每個節點省 8 位元組 (25%)。在 64 位元組的 cache line 上,packed 版可容納 2 個節點加部分第 3 個 ($24 \times 2 = 48$,剩 16 位元組放第 3 個節點的 `__rb_parent_color` 與 `rb_right`)。CFS 排程器的每個 task 的 `sched_entity` 內嵌 `rb_node`,伺服器或容器化環境中 $n$ 可達數千至數萬,$n = 10000$ 時省下 80 KB;加上 netfilter 規則集 ([`nft_set_rbtree`](https://github.com/torvalds/linux/blob/master/net/netfilter/nft_set_rbtree.c))、I/O 排程器、`procfs` ([`fs/proc/generic.c`](https://github.com/torvalds/linux/blob/master/fs/proc/generic.c)) 等使用處,節省量與系統複雜度成正比。歷史上 VMA 管理亦以 augmented rbtree 追蹤位址區間,Linux v6.1 起已改用 maple tree 取代。
Linux 核心以 augmented 紅黑樹搭配雙向鏈結串列管理每個行程的虛擬定址空間 (`vm_area_struct`)。紅黑樹提供 $O(\log n)$ 的搜尋、插入與刪除,核心實作以侵入式設計 (`rb_node` 內嵌於資料結構) 消除一層指標間接存取,改善 CPU 快取局部性。為對映至 order-4 B-tree 表示,紅黑樹強制 black height 一致與相鄰連結顏色交替的不變式,確保最長路徑不超過最短路徑的兩倍。
然而,隨著 CPU core 數量指數成長,紅黑樹設計暴露出記憶體管理子系統的嚴重可擴充性瓶頸:
* 快取效率不足
> 紅黑樹為二元樹,樹高相對較深,走訪過程中產生大量 cache line miss。
* lock contention
> 依序走訪節點需透過紅黑樹與輔助鏈結串列,須以寫入模式持有高度爭搶的 `mmap_sem` (現為 `mmap_lock`)。
Linux v6.1 引入 [Maple Tree](https://docs.kernel.org/core-api/maple_tree.html) 取代紅黑樹,這是種 RCU-safe (Read-Copy-Update) 的 range-based B-tree 變體,專為現代處理器快取架構設計:
* 分支因子與密度
> VMA 管理使用的 allocation node (`maple_arange_64`) 非葉節點分支因子為 10,葉節點為 16。一般 Maple 節點的葉節點與非葉節點分支因子皆為 16。相較二元樹,樹高大幅降低,CPU 可將大量連續索引資料拉入 L1/L2 快取,有效減少 cache miss。
* 無鎖走訪
> Maple Tree 為 RCU-safe,特定讀取端路徑 (如 page fault 處理中的 VMA 查詢) 無需取得 `mmap_lock` 即可走訪結構。此無鎖特性適用於 RCU 讀取端臨界區段內的唯讀走訪,寫入端仍需持有適當的鎖。
* 消除輔助結構
> Maple Tree 淘汰並移除 augmented 紅黑樹、雙向鏈結串列 (`vm_next` 與 `vm_prev`)、以及 VMA 快取。迭代改以狀態機 (`struct ma_state`) 與 `vma_iterator` API 實作。
此架構轉換並非毫無風險。過渡期間引入 "StackRot" 漏洞 ([CVE-2023-3269](https://nvd.nist.gov/vuln/detail/CVE-2023-3269),影響 Linux 6.1 至 6.4),這是一個 Use-After-Free-By-RCU (UAFBR) 缺陷:在 stack 擴充期間,Maple Tree 可能在未正確取得 MM write lock 的情況下進行節點替換。由於 Maple 節點以 RCU callback 釋放記憶體,實際解除配置延遲至 RCU grace period 之後,使精心設計的 local privilege escalation exploit 得以操縱記憶體管理子系統。
此案例展示硬體特性如何主導軟體資料結構設計:Maple Tree 以高分支因子換取快取效率,與 TurboQuant 以 data-oblivious codebook 換取 accelerator-friendly 執行路徑的思路一致。非同步記憶體釋放 (RCU) 引入的新型安全風險亦值得關注。
[CVE-2023-53566](https://nvd.nist.gov/vuln/detail/CVE-2023-53566) 涉及 `nft_set_rbtree` 的節點插入與 garbage collection 路徑:插入時可能觸發 null dereference,GC 走訪時未正確快取下一節點導致 use-after-free,說明紅黑樹操作的正確性對系統安全至關重要。
Problem B 的紅黑樹將 `rb_node` 內嵌於 `sched_entity` 等結構,核心透過 [`container_of`](https://github.com/torvalds/linux/blob/master/include/linux/container_of.h) 巨集從 `rb_node *` 反推外層結構位址。此巨集仰賴 `offsetof` (C99 7.17):
```c
/* include/linux/container_of.h (節錄,經簡化) */
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
((type *)(__mptr - offsetof(type, member))); })
```
`offsetof` 產生編譯時期常數,計算成員在結構中的位元組偏移。此技巧與 Problem B 的位元打包概念互補:後者在單一 word 中塞入額外資訊 (指標 + 顏色),前者從嵌入的子結構還原外層容器。二者皆依賴對記憶體配置的精確控制。
侵入式設計的優勢在於零額外記憶體配置:`rb_node` 直接佔用外層結構的空間,不需另行 `kmalloc` 節點容器。缺點是物件生命週期管理更困難:若內嵌節點所屬的外層結構已被釋放,殘留的指標透過 `container_of` 會計算出指向已釋放記憶體的位址,導致 use-after-free。前述 [CVE-2023-53566](https://nvd.nist.gov/vuln/detail/CVE-2023-53566) 即屬此類:`nft_set_rbtree` 的 garbage collection 路徑在走訪期間呼叫 `rb_prev()` 可能回傳 `NULL`,且未在釋放目前節點前快取下一個節點,導致 null dereference 或 use-after-free。修正方式為在 GC 迴圈中先以 `rb_prev()` 取得前一節點並快取,再對目前節點執行移除操作,確保迭代器不指向已釋放的記憶體。
Linux 核心的所有侵入式資料結構 (`list_head`、`hlist_node`、`rb_node`、`maple_node`) 皆依賴此模式,約有數千處 `container_of` 呼叫。與 C++ 的 `std::set<T>` 等外部容器相比,侵入式設計避免每次插入的額外 heap allocation,降低記憶體碎片化並改善快取局部性。
紅黑樹的高度上界 $h \leq 2\log_2(n+1)$ 保證 $n$ 個節點的搜尋、插入、刪除皆為 $O(\log n)$,且插入至多 2 次旋轉、刪除至多 3 次旋轉。此確定性旋轉次數上界使 worst-case 延遲可預測,是 Linux 核心選擇紅黑樹 (而非高度更緊的 AVL 樹,$h \leq 1.44\log_2(n+2)$) 的考量之一。紅黑樹與 TurboQuant 的共同設計哲學在於可證明的上界:前者以 $2\log_2(n+1)$ 控制動態資料結構的更新成本,後者以 $D_{\text{mse}} \leq \frac{\sqrt{3}\pi}{2} \cdot \frac{1}{4^b}$ (Theorem 1) 控制量化誤差。二者皆透過數學分析給出確定性的品質保證,而非依賴啟發式 (heuristic) 的平均表現。
對 TurboQuant 的典型 codebook ($2^b$ 個 centroid,$b \in \{2, 3, 4\}$),最佳資料結構不是紅黑樹,而是固定大小陣列搭配 branchless 線性掃描。在 $2^b \leq 16$ 的場景下,陣列查表充分利用 CPU 快取與 SIMD 向量化,符合 TurboQuant 論文強調的 accelerator-friendly 設計。
### 延伸問題
* `pack_indices` 的逐位元操作對應 Section 2.1.3 的 bitwise operations,`rb_parent()` 的遮罩 `~3` 對應 $x\ \&\ (2^k - 1)$ 的位元遮罩性質。以此為出發點:
* (a) 閱讀 [`include/linux/bitmap.h`](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) 的 `bitmap_read()`/`bitmap_write()`,追蹤跨 `unsigned long` word 邊界的讀寫邏輯。與 `pack_indices` 比較:後者逐位元迭代 ($128 \times 3 = 384$ 次分支),`bitmap_read()` 以 word 為單位操作 ($\approx 6$ 次 word 存取)。閱讀 [`drivers/hid/hid-core.c`](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-core.c) 的 `extract()`,比較 HID report 欄位擷取的跨位元組邊界處理
* (b) 若 `BITS` 恰為 2、4 或 8,索引欄位可整齊切分位元組。設計一版以 byte 或 word 為單位搬移的 `pack_indices`,比較其與逐位元版本在分支數、記憶體存取次數與可讀性上的差異,並說明為何核心在 hot path 常偏好 aligned access
* \(c) `struct page` 的 `flags` 將 node ID、zone ID、page flags 以 shift/mask 打包至一個 `unsigned long`。閱讀 [`include/linux/page-flags-layout.h`](https://github.com/torvalds/linux/blob/master/include/linux/page-flags-layout.h),追蹤 `NODES_SHIFT`、`ZONES_SHIFT` 如何依 `CONFIG_*` 在編譯時期計算位元配置。從 C99 6.7.2.1 §11 (bit-field 配置為 implementation-defined) 說明核心為何不用 bit-field。此固定位元預算與 `block_tq3` 的空間約束相同
* (d) `rb_set_parent_color` 以 `+` 而非 `|` 合併指標與顏色 (Section 2.3 的無號算術)。從 $\mathbb{Z}/2^{64}\mathbb{Z}$ 證明:$a$ 低 $k$ 位元為零且 $b < 2^k$ 時 $a + b = a \lor b$。閱讀 [`include/linux/rbtree_augmented.h`](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_augmented.h) 的 `__rb_rotate_set_parents()`,追蹤旋轉中 parent/color 的更新。對比 [`include/linux/err.h`](https://github.com/torvalds/linux/blob/master/include/linux/err.h) 的 `ERR_PTR`:前者在低位元夾帶資訊 (對齊保證),後者在高位元夾帶 errno (保留位址範圍)。分析若 slab corruption 破壞 `rb_node` 對齊,`+` 產生進位如何破壞 parent 指標
* [CVE-2023-3269](https://nvd.nist.gov/vuln/detail/CVE-2023-3269) (StackRot) 與 [CVE-2023-53566](https://nvd.nist.gov/vuln/detail/CVE-2023-53566) (`nft_set_rbtree`) 展示紅黑樹與 Maple Tree 的並行安全風險。以此為出發點:
* (a) 閱讀 [`include/linux/maple_tree.h`](https://github.com/torvalds/linux/blob/master/include/linux/maple_tree.h) 的 `maple_arange_64` 節點結構。VMA 管理使用分支因子 10 (非葉) / 16 (葉) 的 Maple Tree,$n = 10000$ 時樹高 $\leq 4$,紅黑樹 $\leq 2\log_2(10001) \approx 27$。以 L2 miss penalty $\approx 10$ ns 估算此差異對 page fault VMA 查詢延遲的影響,並說明此估算隱含每層至少一次指標追逐與一次快取未命中的假設
* (b) StackRot 的根因:`expand_stack()` 未持有 MM write lock 即修改 Maple Tree,舊節點以 `call_rcu()` 延遲釋放,RCU grace period 結束後殘留指標成為 dangling pointer。閱讀 [`include/linux/rcupdate.h`](https://github.com/torvalds/linux/blob/master/include/linux/rcupdate.h),說明 RCU 不變式如何被違反。此 UAFBR 比傳統 UAF 更難偵測:物件存活狀態依全域 grace period 時序而定,非局部可判定
* \(c) CVE-2023-53566 中 `nft_set_rbtree` GC 走訪時未在 `kfree` 前快取 `rb_prev()`,致 `container_of` 推算出無效位址。侵入式資料結構的 `container_of` 前提是 `rb_node` 所屬物件仍存活,這點可與 `unpack_indices` 的 bijection 類比:底層記憶體無效時,解碼仍可能成功,但結果無意義。設計正確的 GC 走訪虛擬碼,明確寫出先快取迭代器、再 unlink、最後釋放節點的順序
---
## Problem C: 型別雙關、sign hash 與浮點算術性質
> 延伸 CS:APP Practice Problem 2.54 / Section 2.4.5 (Floating-Point Operations) / Section 2.2.6 / Figure 2.33
題目設計要點:
* 本題把三類常見陷阱放在同一條線上看:浮點非結合性、有號/無號混算,及 sign hash 的幅值補償
* 作答時不需重建 QJL 全部證明,只要掌握先投影、再保留正負號、最後按比例補回幅值的流程
* 延伸問題刻意導向 RTT 平滑、EWMA、`jiffies` 與雜湊等核心內部設計,讓數值性質對應到核心程式碼
- [ ] Part 1: 浮點算術性質與量化
CS:APP Section 2.4.5 指出浮點算術不滿足結合律。以 single-precision 為例:
```
(3.14 + 1e10) - 1e10 → 0.0
3.14 + (1e10 - 1e10) → 3.14
```
Practice Problem 2.54 要求判斷以下運算式是否恆成立 (假設 `x` 為 `int`,`f` 為 `float`,`d` 為 `double`,且 `f`, `d` 非 NaN/∞):
* `x == (int)(double) x`:恆真,因為 32-bit int 可精確表示為 `double` 的 52-bit fraction
* `x == (int)(float) x`:非恆真,反例為 $x = 2^{24} + 1$,`float` 的 23-bit fraction 不足以精確表示
* `d == (double)(float) d`:非恆真,`float` 精度低於 `double`
* `f == -(-f)`:恆真,IEEE 754 的 negation 僅翻轉 sign bit,不損失精度
* `(f+d) - f == d`:非恆真,浮點加法不滿足結合律,cancellation 損失精度
CS:APP Section 2.2.6 討論有號與無號整數的轉換規則。C99 6.3.1.8 定義的 usual arithmetic conversions 規定:當 `int` 與 `unsigned` 運算元出現在同一運算式中,`int` 先轉換為 `unsigned` 再進行運算。此規則在混合型別的位元操作中尤為重要。
這些性質直接影響向量量化的正確性:
* quantize-dequantize 迴路的往返誤差 (round-trip error) 類似 `x == (int)(float) x` 的精度損失
* 浮點加法的非結合性意味著內積 $\sum x_i y_i$ 的累加順序會影響結果精度
* TurboQuant 的 QJL 以 $\sqrt{\pi/2}$ scale factor 補償 sign 函式的幅值損失,使內積估計滿足無偏性 $\mathbb{E}[\langle y, \hat{x} \rangle] = \langle y, x \rangle$ (Lemma 4)
- [ ] Part 2: QJL sign hash 量化與內積估計
TurboQuant 的 inner product quantizer (Algorithm 2) 採兩階段壓縮。第一階段先用 $(b-1)$-bit MSE quantizer (Problem B) 壓縮向量的大致形狀;第二階段再對剩下的誤差,也就是殘差 (residual),施加 1-bit QJL sign hash。直觀上,可把它理解成先做一個粗略版本,再用很便宜的額外資訊補回內積估計最在意的部分。反量化時把兩部分相加,就能得到 inner product estimator。完整的 Algorithm 2 管線已在前述背景中以圖示呈現。
以下程式碼單獨實作 QJL 的 sign hash 與內積估計環節 (Algorithm 2 的第二階段),不包含 MSE 量化部分 (已在 Problem B 實作)。hash 函式以確定性 Rademacher 投影避免儲存完整的 $d \times d$ 投影矩陣,僅需 $O(1)$ 額外空間。讀程式碼時,建議把它拆成兩段:`qjl_quantize()` 負責把每個投影方向壓成一個 sign bit,`qjl_inner_product()` 則把這些 sign bit 重新解讀成對內積的近似。
Linux 核心中與此型別雙關技巧相關的實作見 [`include/linux/swab.h`](https://github.com/torvalds/linux/blob/master/include/linux/swab.h),其 `__swab32` 在早期版本以 `union` 在 `__u32` 與 4 個 `__u8` 之間轉換位元組序,現代核心已改用 `__builtin_bswap32` 或輔助函式實作。Linux 核心的 [`include/linux/average.h`](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 定義的 `DECLARE_EWMA` 巨集則展示另一種數值壓縮策略,以無號定點數搭配明確精度參數儲存指數加權移動平均值,用於 TCP RTT 平滑與無線訊號強度估計。
此技巧在 TCP RTT 估計中扮演關鍵角色。Jacobson/Karels 演算法 (1988, [RFC 6298](https://datatracker.ietf.org/doc/html/rfc6298)) 以定點右移取代浮點乘法:
```c
/* net/ipv4/tcp_input.c (節錄,經簡化) */
static void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
struct tcp_sock *tp = tcp_sk(sk);
long m = mrtt_us;
long srtt = tp->srtt_us;
if (srtt != 0) {
m -= (srtt >> 3); /* m = 量測值 - srtt/8 */
srtt += m; /* srtt = srtt + (量測值 - srtt)/8 */
/* = 7/8 * srtt + 1/8 * 量測值 */
/* ... mdev (mean deviation) 計算省略 ... */
} else {
srtt = m << 3; /* 首次量測:srtt = 量測值 * 8 */
}
tp->srtt_us = max(1U, srtt);
}
```
`srtt_us` 以 8 倍放大儲存 (即 3 位元定點小數),避免浮點除法。`>> 3` 等同乘以 $1/8$,平滑因子 $\alpha = 1/8$ 為 2 的冪,使指數平滑完全以整數位移實作。此設計與 TurboQuant 的 $\sqrt{\pi/2}$ scale factor 概念相通:二者皆以確定性的數學轉換補償資訊壓縮帶來的幅值偏差,前者補償定點截斷損失,後者補償 sign 函式對幅值的損失。
[`include/linux/average.h`](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 的 `DECLARE_EWMA` 巨集將此模式泛化:
```c
/* include/linux/average.h (節錄,經簡化) */
#define DECLARE_EWMA(name, _precision, _weight_rcp) \
struct ewma_##name { \
unsigned long internal; \
}; \
static inline void ewma_##name##_add( \
struct ewma_##name *e, unsigned long val) { \
unsigned long internal = READ_ONCE(e->internal); \
unsigned long weight_rcp = ilog2(_weight_rcp); \
unsigned long precision = _precision; \
if (internal) { \
internal = internal - \
(internal >> weight_rcp) + \
(val << precision >> weight_rcp); \
} else { \
internal = val << precision; \
} \
WRITE_ONCE(e->internal, internal); \
}
```
`_precision` 指定定點小數位元數 (類似 `srtt_us` 的 3 位元),`_weight_rcp` 為平滑因子的倒數 (必須為 2 的冪)。無線訊號強度 (RSSI) 估計 ([`net/mac80211/rx.c`](https://github.com/torvalds/linux/blob/master/net/mac80211/rx.c)) 以 `DECLARE_EWMA(rssi, 10, 16)` 定義 10 位元精度、$\alpha = 1/16$ 的平滑器,用於判斷漫遊 (roaming) 時機。磁碟 I/O 延遲統計 ([`block/blk-wbt.c`](https://github.com/torvalds/linux/blob/master/block/blk-wbt.c)) 以類似的 EWMA 追蹤寫入延遲,當延遲超過閾值時啟動寫入節流 (write-back throttling),防止 buffered write 淹沒儲存裝置。
定點數算術的風險在於精度累積誤差。若 `_precision` 不足,長期執行的 EWMA 會因反覆右移截斷而趨向零 (定點 underflow)。`DECLARE_EWMA` 以 `unsigned long` 儲存,在 64 位元系統上提供 64 位元寬度,但若 `val` 的有效位元數與 `_precision` 之和超過型別寬度,`val << precision` 仍可能溢位。此限制與 Problem A 的 FP16 精度上限 ($2^{11} + 1 = 2049$ 為不可精確表示的最小正整數) 呼應:有限位元寬度下,數值表示的精度與範圍互相制約。
TurboQuant 論文 Definition 1 定義 QJL 使用 i.i.d. Gaussian 投影矩陣 $S_{i,j} \sim \mathcal{N}(0,1)$:
$$Q_{\text{qjl}}(x) := \text{sign}(S \cdot x), \quad Q_{\text{qjl}}^{-1}(z) := \frac{\sqrt{\pi/2}}{d} \cdot S^\top \cdot z$$
Lemma 4 在 Gaussian $S$ 的假設下證明此估計器是 unbiased 的:$\mathbb{E}[\langle y, Q_{\text{qjl}}^{-1}(Q_{\text{qjl}}(x))\rangle] = \langle y, x \rangle$。以下實作以確定性 Rademacher 投影 ($\pm 1$) 近似 Gaussian $S$,犧牲嚴格的無偏性保證以換取 $O(1)$ 空間 (不需儲存完整的投影矩陣)。此近似在高維度下表現良好,因為 Rademacher 與 Gaussian 投影的 [Johnson-Lindenstrauss 性質](https://en.wikipedia.org/wiki/Johnson%E2%80%93Lindenstrauss_lemma) 漸近等價。scale factor $\sqrt{\pi/2}$ 仍用於補償 sign 函式對幅值的損失。
```c
#include <stdint.h>
#include <string.h>
#include <math.h>
typedef uint32_t u32;
typedef uint8_t u8;
#define BIT(nr) (1u << (nr))
#define DIM 128
#define SDIM 256 /* sketch 維度,可大於 DIM 以降低 variance */
/* 確定性 Rademacher 投影:以 Knuth's multiplicative hash 常數
* 2654435761u 混合索引;此值近似 2^32 / phi,與 2^32 互質,
* 在 32-bit 空間上能提供良好的均勻分布與 bit diffusion。
* 注意:這裡混合了 signed/unsigned 運算,需留意隱式型別轉換。 */
static float rademacher(int dim, int sketch) {
u32 h = (u32)(dim * 2654435761u + sketch * 340573321u);
h ^= h >> 16;
h *= 0x45d9f3b;
h ^= h >> 16;
return (h & 1) ? 1.0f : -1.0f;
}
/* 量化:計算 sign hash */
void qjl_quantize(const float *x, u8 *hash, int d) {
memset(hash, 0, SDIM / 8);
for (int s = 0; s < SDIM; s++) {
float proj = 0.0f;
for (int i = 0; i < d; i++)
proj += x[i] * rademacher(i, s);
if (proj >= 0.0f)
hash[s >> 3] |= BIT(s & 7);
}
}
/* 以 sign hash 估計 <query, key> 內積 */
float qjl_inner_product(const float *query, const u8 *key_hash,
float key_norm, int d) {
/* QJL scale = 1 / E[|Z|] / SDIM, where E[|Z|] = sqrt(2/pi) for Z ~ N(0,1) */
float scale = key_norm / (sqrtf((float)M_2_PI) * (float)SDIM);
float sum = 0.0f;
for (int s = 0; s < SDIM; s++) {
/* 將 key 的 sign bit 還原為 ±1 */
float key_sign = (key_hash[s >> 3] & BIT(s & 7))
? 1.0f : __C01__;
/* 計算 query 在第 s 個投影方向上的分量 */
float q_proj = 0.0f;
for (int i = 0; i < d; i++)
q_proj += query[i] * rademacher(i, s);
sum += q_proj * key_sign;
}
return scale * sum;
}
```
填空說明:
QJL 的反量化 scale factor 為 $\sqrt{\pi/2}$,用於補償 sign 函式丟失幅值資訊的效應。此因子的來源是 half-normal 分布:對 $Z \sim \mathcal{N}(0,1)$,$\mathbb{E}[|Z|] = \sqrt{2/\pi}$。sign 函式將幅值壓為 $\pm 1$,平均損失 $\mathbb{E}[|Z|]$ 倍的幅值。反量化時需除以此期望值以補償,即乘以 $1/\mathbb{E}[|Z|] = 1/\sqrt{2/\pi} = \sqrt{\pi/2}$。程式碼以 `key_norm / (sqrtf((float)M_2_PI) * (float)SDIM)` 實作此補償,其中 `M_2_PI` 是 `<math.h>` 定義的常數 $2/\pi$,`sqrtf((float)M_2_PI)` 即 $\sqrt{2/\pi} = \mathbb{E}[|Z|]$。
key 的 sign bit 為 0 時對應 $-1$ (QJL 以 $\{-1, +1\}$ 而非 $\{0, 1\}$ 編碼方向)。__ C01 __ 為此負值的浮點常數。
`rademacher()` 中的 `dim * 2654435761u`:`dim` 為 `int`,`2654435761u` 為 `unsigned` 常數。此常數來自 Knuth 的 multiplicative hash,可視為 $2^{32}/\varphi$ 的整數近似,目的不是隨機亂選,而是利用黃金比例乘法在有限字寬下提供良好的 bit diffusion。依 C99 6.3.1.8 的 usual arithmetic conversions (CS:APP Section 2.2.6),`dim` 先轉為 __ C02 __ 型別再進行乘法,結果亦為該型別 (填空以最短型別拼寫作答:`unsigned`;`unsigned int` 為同義詞,亦可接受)。此隱式轉換將有號值以模 $2^{32}$ 算術重新詮釋為無號值。當 `dim` 為非負值時數值不變,但當 `dim` 為負值時,轉換後會先變成一個很大的無號值,再參與乘法與 XOR-shift 混合。`d` 與 `SDIM` 在本題語境都應是非負且小於陣列長度的維度參數;若上游誤把負值傳入維度參數,這段程式未必立刻 crash,卻會產生型別正確但語義錯誤的 hash,正是 signed/unsigned 混算最危險的地方。
驗證 (內積估計):令 $x = (1, 0, 0, \ldots)$,$y = (1, 0, 0, \ldots)$,真實內積 $\langle x, y \rangle = 1$。在此退化情況下,每個 sketch 維度的 `q_proj` $= \text{rademacher}(0, s)$,`key_sign` $= \text{sign}(\text{rademacher}(0, s))$。因為 $\text{rademacher} \in \{-1, +1\}$,乘積恆為 1,sum $= \text{SDIM}$,估計值 $= \sqrt{\pi/2} \approx 1.253$。此系統性偏差源於 Rademacher 投影與 Lemma 4 假設的 Gaussian 投影之差異:Gaussian $S_{i,j}$ 的幅值服從 half-normal 分布,期望值恰為 $\sqrt{2/\pi}$,使 $\sqrt{\pi/2}$ 的 scale factor 精確補償;Rademacher 投影的幅值恆為 1,無法被同一常數精確補償。在高維度且向量分量均勻分散 (非退化) 的場景下,中央極限定理使 `proj` 的分布趨近 Gaussian,此偏差大幅縮減。實務上以大量 sketch 維度 (SDIM) 降低 variance,內積估計的相對誤差隨 $1/\sqrt{\text{SDIM}}$ 衰減。
驗證 (C 語言型別安全):`hash[s >> 3]` 的索引型別為 `int` (因 `s` 為 `int`,右移結果為 `int`)。C99 6.5.7 §5 規定有號整數右移的行為屬 implementation-defined (算術右移或邏輯右移,見 CS:APP Section 2.1.8),但 `s` 為非負整數,二者結果相同。`qjl_quantize` 中的 `1u << (s & 7)` 使用 `1u` (無號) 避免有號左移溢位,與 Problem B 討論的 `BIT()` 巨集同理。
驗證 (浮點累加順序):`sum += q_proj * key_sign` 逐項累加,其精度受浮點加法非結合性影響 (CS:APP Section 2.4.5)。當 SDIM 很大時,累加的中間結果幅值可能遠大於個別項,導致 cancellation。實務上以 Kahan summation 或分組累加可改善此問題,但 QJL 的 unbiased 性質是在數學期望 (而非有限精度算術) 下成立的。浮點累加引入的 round-off error 是額外的雜訊來源,不影響無偏性但增加 variance。
延伸問題 (非填空):CS:APP Practice Problem 2.54 的 `f == -(-f)` 恆真 (sign bit 翻轉兩次)。在 QJL 語境中,若 key 向量取負 $x' = -x$,其 sign hash 的每個位元都翻轉:`hash'[s] = 1 - hash[s]`。因此 $\langle y, Q^{-1}(Q(-x)) \rangle = -\langle y, Q^{-1}(Q(x)) \rangle$,內積估計值取負。sign hash 完美保留向量的正負號資訊,與 IEEE 754 的 sign bit 語義一致。此性質對 attention score 的正確性至關重要:key 向量的方向翻轉必須忠實反映在內積估計中。
延伸閱讀:混合有號/無號運算屬 [CWE-195](https://cwe.mitre.org/data/definitions/195.html) (Signed to Unsigned Conversion Error)。Linux 核心的 [`include/linux/compiler.h`](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h) 定義 `__same_type()` 巨集以 `__builtin_types_compatible_p` 在編譯時期檢查型別一致性,防止隱式轉換引入的非預期行為。`-Wsign-conversion` 編譯旗標可偵測此類問題,但 Linux 核心基於歷史相容性尚未全面啟用。
核心處理時間先後時也面臨相同的無號算術陷阱。CS:APP Section 2.2.4 討論有號與無號整數的比較陷阱。Linux 核心的 `jiffies` 計數器是此議題最直接的實務案例。`jiffies` 為全域 `unsigned long` 變數,每次 timer interrupt 遞增 1,在 32 位元系統上以 `HZ=1000` 約 49.7 天後環繞 (wrap around) 回零。
若以直覺的 `jiffies > saved_jiffies` 判斷時間先後,環繞時會產生錯誤結果。核心以 [`include/linux/jiffies.h`](https://github.com/torvalds/linux/blob/master/include/linux/jiffies.h) 的 `time_after()` 巨集正確處理:
```c
/* include/linux/jiffies.h (節錄) */
#define time_after(a,b) (typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
```
此巨集將無號減法結果強制轉為有號 `long` 再與 0 比較。當 `a` 在時間上晚於 `b` 時 (即使 `a` 的數值因環繞而小於 `b`),`b - a` 在無號算術下產生大值,轉為 `long` 後為負數,`< 0` 成立。此技巧依賴 C99 6.3.1.3 的 implementation-defined 有號/無號轉換行為:在 two's complement 架構上,無號值的高位元被重新詮釋為符號位元。Linux 核心假設所有目標架構皆為 two's complement (此假設在 C23 中已成為標準要求)。
`typecheck()` 巨集以指標相等比較 (`&__dummy == &__dummy2`,其中 `__dummy` 與 `__dummy2` 分別宣告為目標型別與實際型別) 觸發編譯時期型別不匹配警告,確保二個運算元型別相同,防止不同寬度的無號型別在比較時因隱式提升產生非預期行為。此防禦性設計與 Problem C 討論的 `dim * 2654435761u` 型別提升陷阱直接對應。
歷史上,不使用 `time_after()` 而直接比較 jiffies 的驅動程式在系統連續執行數十天後出現 timer 失效的 bug,是 unsigned wrap-around 在實務中造成嚴重後果的經典案例。
BPF verifier 是此類問題的經典教訓。[CVE-2017-16995](https://nvd.nist.gov/vuln/detail/CVE-2017-16995) 中 [`kernel/bpf/verifier.c`](https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c) 的 `check_alu_op()` 在處理 ALU 運算時未正確處理 sign extension,使負的 32-bit `int` 被詮釋為大的 `unsigned` 值,攻擊者藉此繞過範圍檢查存取越界記憶體。此漏洞直接對應 CS:APP Section 2.2.4 的 signed/unsigned 比較陷阱與 Section 2.2.6 的 integer promotion 規則。類似地,[CVE-2024-58017](https://nvd.nist.gov/vuln/detail/CVE-2024-58017) 中 `kernel/printk/printk.c` 的 `LOG_BUF_LEN_MAX` 計算以有號 `1 << N` 觸發溢位,修正方式為將 `1` 轉型為 `u32` 後再進行位移,消除有號左移的未定義行為,與本卷 Problem B 的 B05 填空同理。
### 延伸問題
* `rademacher()` 的 `dim * 2654435761u` 與 `time_after()` 的 `(long)((b)-(a)) < 0` 皆依賴 Section 2.3 的無號算術環繞性質。以此為出發點:
* (a) 閱讀 [`include/linux/seqlock.h`](https://github.com/torvalds/linux/blob/master/include/linux/seqlock.h) 的 `read_seqcount_retry()`:sequence counter 以 LSB 奇偶性標記寫入進行中 (與 Problem B `rb_node` 的 bit 0 編碼顏色相同手法)。`seqcount` 環繞不影響正確性 (判斷僅用奇偶與等值),但 `jiffies` 環繞需 `time_after()` 的有號重新詮釋 (Section 2.2.4)。從 $\mathbb{Z}/2^{32}\mathbb{Z}$ 證明等值比較在環繞後仍正確,大小比較則失效
* (b) `rademacher()` 使用 Knuth 黃金比例常數 ($2654435761 \approx 2^{32}/\phi$)。$\gcd(2654435761, 2^{32}) = 1$,故乘法構成 $\mathbb{Z}/2^{32}\mathbb{Z}$ 上的雙射。閱讀 [`include/linux/hash.h`](https://github.com/torvalds/linux/blob/master/include/linux/hash.h) 的 `hash_long()`,說明其以 `(val * GOLDEN_RATIO_PRIME) >> (BITS_PER_LONG - bits)` 提取高位元的原因 (高位元 avalanche 品質優於低位元)。再追蹤 Linux 核心 2016 年對 32-bit 黃金比例常數的調整脈絡:早期常數 `0x9e370001UL` 在低位元混合品質不佳,後續改用 `0x61C88647` 以改善 avalanche。若從模運算的對稱性看,`0x61C88647` 最著名的性質其實是 `0x9e3779b9` 的加法逆元,也就是兩者相加在模 $2^{32}$ 下回到 0;這也提醒學員區分 additive inverse 與 multiplicative inverse,不要把二種對稱性混為一談。對照本題 `rademacher()`:QJL 最終取 `h & 1`,因此不能只靠單次乘法,還需要後續三輪 XOR-shift-multiply 混合來補強低位元品質
* \(c) TCP SRTT 以 `>> 3` 實現 $\alpha = 1/8$ 的 EWMA (Section 2.3.7 的整數除以 2 的冪)。閱讀 [`net/ipv4/tcp_input.c`](https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_input.c) 的 `tcp_rtt_estimator()`,追蹤 `srtt_us` 的 8 倍放大定點更新。此截斷偏差 (towards zero) 與 Section 2.4.4 的 round-to-even 不同。計算 RTT 從 $100\ \mu\text{s}$ 跳變至 $200\ \mu\text{s}$ 時,收斂至 95% 新穩態需 $n_{95} = \lceil \log(0.05) / \log(7/8) \rceil = 23$ 個樣本。對比 QJL 的無偏性保證:EWMA 截斷偏差有界 ($\leq 1\ \mu\text{s}$) 但不為零,QJL 的偏差在高維度統計平均下趨近零
* Section 2.4.5 指出浮點加法不滿足結合律 (Practice Problem 2.54 的 `(f+d)-f == d` 非恆真),QJL 的 `sum` 逐項累加受此影響。以此為出發點:
* (a) 對 $d = 128$, $\text{SDIM} = 256$,樸素累加的 round-off error $\approx \sqrt{\text{SDIM}} \cdot 2^{-24} \approx 10^{-6}$,QJL 本身 variance $\approx 1/\sqrt{\text{SDIM}} \approx 0.063$,差距約 4 個數量級。閱讀 [`lib/math/reciprocal_div.c`](https://github.com/torvalds/linux/blob/master/lib/math/reciprocal_div.c),此模組以 `(a * m) >> 32` 取代除法 (CFS `inv_weight` 同理),截斷誤差至多 1 且確定性有界;核心選擇定點而非浮點除法的原因,除了停用 FPU 的限制,還在於定點誤差可靜態分析
* (b) 閱讀 [`include/linux/average.h`](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 的 `DECLARE_EWMA`。`ewma_add()` 以 `val << precision >> weight_rcp` 更新,其中區域變數 `weight_rcp = ilog2(_weight_rcp)` 是實際右移量。以 cfg80211 RSSI (`precision=10`, `_weight_rcp=16`) 為例:`weight_rcp = 4`,故 `val << 10 >> 4 = val << 6`,無精度損失。推導避免退化的必要條件應寫為 `precision ≥ ilog2(_weight_rcp)`,並說明此約束如何呼應 IEEE 754 在固定位元預算下對 fraction 與 exponent 的權衡 (Figure 2.32)
* \(c) 閱讀 [`include/linux/hash.h`](https://github.com/torvalds/linux/blob/master/include/linux/hash.h),`hash_long()` 與 `rademacher()` 皆以乘法雜湊產生偽隨機位元。核心的 [`net/core/flow_dissector.c`](https://github.com/torvalds/linux/blob/master/net/core/flow_dissector.c) 以 `jhash` 將封包分類至 RSS 佇列,對 hash 品質的要求 (低碰撞,負載均衡) 與 QJL 的投影品質需求 (低相關,低 variance) 互為映照。設計測試:計算 `rademacher(i, s)` 對 $i \in [0, 127]$, $s \in [0, 255]$ 的 $128 \times 256$ 矩陣行列相關係數,並說明你會用哪些統計量判斷其是否趨近零
---
## 參考題解
:::spoiler
* A01: ==112==
* A02: ==14==
* A03: ==12==
* A04: ==3F801000==
* A05: ==477FE000==
* B01: ==7==
* B02: ==1==
* B03: ==idx[i]==
* B04: ==DD==
* B05: ==1u==
* B06: ==~3==
* B07: ==7FFE0041==
* C01: ==-1.0f==
* C02: ==unsigned==
:::