--- 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== :::