正則化儲存格式 (ELL、Hybrid ELL-COO 與 JDS)
重點總覽 (Overview)
本篇承接 14-Sparse-Matrix-Computation/01-Sparse-Matrices-and-Basic-Formats-COO-CSR。CSR 雖省空間又免 atomic,但兩大缺陷:記憶體存取無法 coalesce、control divergence 嚴重。本篇三種「正則化」格式逐一補救。
| 格式 | 核心手法 | 解決的問題 | 代價 |
|---|---|---|---|
| ELL | padding(補零成矩形)+ transpose(column-major) | coalescing | 空間變差、divergence 未解 |
| Hybrid ELL-COO | 把超長 row 的多餘元素抽到 COO | ELL 的 padding 爆量 + divergence | 失去「依 row 取全部 nonzero」的存取性 |
| JDS | 依 row 長度 排序 + column-major | divergence(免 padding) | 最不易增刪、無法強制對齊邊界 |
三者都在「compaction(壓縮)↔ regularization(正則化)」之間做取捨:ELL/JDS 甚至故意重新引入零元素或重排,以換取規則的記憶體存取與負載平衡。沒有一種格式全面最佳——最佳選擇取決於 nonzero 的分佈(data-dependent),這正是本章 Summary 的核心訊息。
ELL 格式:Padding + Transposition 達成 Coalescing (Improving Memory Coalescing with the ELL Format)
名稱源自 ELLPACK(解 elliptic boundary value problems 的套件)。從 CSR 出發兩步驟:
- Padding:找出 nonzero 最多的 row(下例 row 1 有 3 個),其餘 row 在尾端**補 padding(值 0)**到相同長度 → 變成矩形矩陣。
colIdx一併補。 - Transposition:把矩形矩陣以 column-major(逐欄連續) 排放 = C 語言 row-major 下的轉置。
rowPtrs不再需要(row r 的首元素就在value[r])。
原矩陣 (4x4) CSR 分組 padding 成矩形(*=補零)
c0 c1 c2 c3 row0: 1 7 row0: 1 7 *
r0 1 7 . . row1: 5 3 9 row1: 5 3 9 <- 最長 row, max=3
r1 5 . 3 9 --> row2: 2 8 --> row2: 2 8 *
r2 . 2 8 . row3: 6 row3: 6 * *
r3 . . . 6
transpose 成 column-major(逐欄攤平)
┌── col0 ──┐┌─ col1 ─┐┌─ col2 ─┐
value = [ 1 5 2 6 , 7 3 8 *, * 9 * * ]
colIdx = [ 0 0 1 3 , 1 2 2 *, * 3 * * ]
index i: 0 1 2 3 4 5 6 7 8 9 10 11
└thr0..thr3┘ (第 t 次迭代 = 第 t 欄)
__global__ void spmv_ell_kernel(ELLMatrix ellMatrix, float *x, float *y) {
unsigned int row = blockIdx.x*blockDim.x + threadIdx.x; // 02 一 thread 一 row
if (row < ellMatrix.numRows) { // 03 邊界
float sum = 0.0f; // 04
for (unsigned int t = 0; t < ellMatrix.nnzPerRow[row]; ++t) { // 05
unsigned int i = t*ellMatrix.numRows + row; // 06 column-major 索引
unsigned int col = ellMatrix.colIdx[i]; // 07
float value = ellMatrix.value[i]; // 08
sum += x[col]*value; // 09
}
y[row] += sum; // 11 免 atomic(一 row 一 thread)
}
}
若沒有 nnzPerRow 向量,kernel 可改成走完整列(含 padding):padding 值為 0,x[col]*0 不影響結果——只是多算幾次。
Coalescing 為何成立:索引 i = t*numRows + row,row 直接綁 threadIdx.x → 同一迭代(同 t),相鄰 thread 的 i 相鄰 → 連續位址 → coalesced。對比 CSR(row-major)第一次迭代 thread 0/1/2/3 讀 value[0],[2],[5],[7](相隔遠、不 coalesced)。
ELL 迭代 t=0: thr0 thr1 thr2 thr3
v[0] v[1] v[2] v[3] <- 相鄰 => COALESCED ✓
CSR 迭代 0 : thr0 thr1 thr2 thr3
v[0] v[2] v[5] v[7] <- 跳躍 => NOT coalesced ✗
Accessibility:ELL 同時具備 CSR(給 row → 取該 row nonzeros)與 COO(給 nonzero → 反推 row/col)的存取性。反推公式:
正向(算 i): i = t*numRows + row
反推(算 row): row = i % numRows (因 row < numRows,故 row%numRows == row)
ELL 沒有解決 control divergence:每個 thread 仍依自己 row 的 nnz 迭代不同次數,divergence 與 CSR 一樣糟。ELL 只修好了 coalescing。
此外 padding 空間開銷高度依賴分佈:若 1000×1000、1% nonzero(平均每列 10 個),但某列有 200 個 → 全部列被 pad 到 200,ELL 約佔未壓縮的 40%,比 CSR(約 2%)大 20 倍。下一節用 hybrid 抑制。
舊架構對齊規則較嚴時,可在轉置前於矩陣尾端多加幾列,強制每次迭代起點對齊到 64-byte 等邊界 → 更佳 coalescing。(JDS 因無 padding 而失去此選項。)
Hybrid ELL-COO 格式:抑制過量 Padding (Regulating Padding with the Hybrid ELL-COO Format)
問題根源:ELL 的 padding 爆量與 divergence,都來自少數超長 row。對策:轉 ELL 前,把超長 row 尾端的多餘 nonzero 抽出來放進獨立的 COO,剩餘部分做 ELL,最後兩部分結果相加(hybrid method)。
僅用 ELL(row1、row6 超長)→ max=5,大量 padding(22 個)
┌──────── 抽走 row 尾端多餘元素 ────────┐
ELL 部分(max 降到 2,padding 22→3) COO 部分(收容多餘元素)
row : [ a b ] value : [ ... 被抽走的 nnz ... ]
row1: [ a b ] (其餘 3 個 -> COO) rowIdx: [ 1 1 1 6 6 ]
... colIdx: [ ... ]
row6: [ a b ] (其餘 2 個 -> COO)
=> 所有 thread 只需迭代 2 次 (divergence 大減)
兩段各自跑既有 kernel:SpMV/ELL(規則的多數元素)+ SpMV/COO(少量長尾元素,用 atomic 累加,見 sibling 筆記)。
| 設計考量 | Hybrid vs 純 ELL |
|---|---|
| Space efficiency | 更好:padding 大減 |
| Flexibility | 更好:可換 padding 加,也可在無 padding 時 append 到 COO |
| Accessibility | 變差:若某 row 溢位到 COO,要取它的全部 nonzero 須搜尋 COO(昂貴) |
| Memory coalescing | 維持(ELL、COO 兩段皆 coalesced) |
| Control divergence | 減少:長尾移走後 ELL 各列趨近等長;COO 本就無 divergence |
拆分成本可被攤銷:真實 iterative solver 中,每次迭代只有 x、y 變,矩陣係數不變,故「拆 ELL + COO」只需做一次,成本分攤到上千次迭代。若矩陣只用一次,額外成本可能划不來("it depends")。
JDS 格式:排序消除 Control Divergence (Reducing Control Divergence with the JDS Format)
Jagged Diagonal Storage:不靠 padding,而是依 row 長度排序(例如長→短),排序後矩陣呈鋸齒三角狀,故名。
步驟:(1) 依 row 分組(同 CSR);(2) 依 row 的 nnz 排序,同時用 row 陣列記錄原始 row 索引;(3) column-major 存放;(4) 用 iterPtr 標記每次迭代(每欄)nonzeros 的起點。
排序前 nnz 排序後(長→短) column-major + iterPtr
r0: 2 s0(原r1): x x x iter0(col0): r1 r0 r2 r3 <- 4 個
r1: 3 sort s1(原r0): x x iter1(col1): r1 r0 r2 <- 3 個
r2: 2 -----> s2(原r2): x x iter2(col2): r1 <- 1 個
r3: 1 s3(原r3): x
row 陣列 = [1,0,2,3] value = [r1c0 r0c0 r2c0 r3c0 | r1c1 r0c1 r2c1 | r1c2]
(保留原始順序) iterPtr = [0, 4, 7, 8]
物理視圖(第一次迭代):thr0 thr1 thr2 thr3 讀 value[0..3] 相鄰 => COALESCED ✓
每個 thread 認領一 row,用 iterPtr 找各迭代起點走 dot product(kernel 留作習題)。
為何排序不影響解:重排 row = 重排線性方程式;只要 y 隨之重排即等價於換方程順序,解不變。最後再用 row 陣列把解 x 排回原順序即可。
JDS 變體(分段 ELL):排序後把列切成數段(section),因已排序,同段內各列長度接近;對每段各做一份 ELL,只需 pad 到該段最長列 → padding 遠少於整體單一 ELL。此變體用 section pointer 取代 iterPtr。
| 設計考量 | JDS 表現 |
|---|---|
| Space efficiency | 優於 ELL:免 padding(分段-ELL 變體有少量 padding,仍少於純 ELL) |
| Flexibility | 最差:增刪 nonzero 改變列長 → 可能要重新排序;比 CSR 還不易 |
| Accessibility | 類 CSR:給 row 可取其 nonzeros;但不易由 nonzero 反推 row/col |
| Memory coalescing | 是(column-major);但無 padding → 各迭代起點任意,無法強制對齊 → 可能略遜 ELL |
| Control divergence | 有效降低:同一 warp 內各 thread 處理長度相近的列 → 迭代次數相近 |
JDS 的 coalescing 雖成立,但因沒有 padding,每次迭代的起始位址會任意浮動,無法像 ELL 那樣靠補列把起點對齊到架構邊界(如 64-byte)。這使 JDS 的記憶體存取有時不如 ELL 高效。
本章總結:Data-Dependent 效能與 OP/B (Summary: Data-Dependent Performance & OP/B)
- 正則化的兩大手法:hybrid method(混用兩格式)與 sorting/partitioning(JDS)。兩者都可能重新引入零元素;hybrid 用來緩解「padding 爆量」的病態案例。
- Data-dependent performance:SpMV 的執行效率與頻寬效率取決於輸入矩陣 nonzero 的分佈——這與先前多數 kernel 不同,卻是真實大型應用的常態,也是 SpMV 作為 parallel pattern 的重要性所在。
為何 SpMV 的 FLOPS 很低——關鍵在 arithmetic intensity OP/B:
每個 nonzero 的工作:
記憶體流量 = value(4B) + colIdx(4B) = 8 bytes (稀疏矩陣無 data reuse!)
運算量 = 1 個 multiply + 1 個 add = 2 ops
=> OP/B = 2 ops / 8 bytes = 0.25 op/byte
SpMV 的 OP/B ≈ 0.25,極低 → 計算被 memory bandwidth 限制(memory-bound),只能達到峰值 FLOPS 的一小部分。CPU、GPU 皆然——這就是為何各種儲存格式(省頻寬)如此關鍵。理解 OP/B 後,就不會再對 SpMV 的低 FLOPS 感到意外。(roofline 觀點見 22-Advanced-Practices-And-Future-Evolution/03-Memory-Bandwidth-and-Compute-Throughput)
五格式總對照(承 sibling 的 COO/CSR):
| 格式 | Space | Flexibility(增刪) | Accessibility | Coalescing | Control Divergence |
|---|---|---|---|---|---|
| COO | 中(3 陣列) | 最佳(append) | 給 nonzero → row/col | ✓ | 無(但需 atomic) |
| CSR | 最佳 | 差(須位移後列) | 給 row → nonzeros | ✗ | 高 |
| ELL | 差(padding) | 中(換 padding) | row 與 nonzero 兩者皆可 | ✓ | 高(同 CSR) |
| Hybrid | 中 | 中(可 append COO) | 溢位列較差 | ✓ | 減少 |
| JDS | 良(免 pad) | 最差(須重排) | 給 row → nonzeros | ✓(難對齊) | 減少(排序) |
考試/面試重點 (Exam / Test Patterns)
| 情境 / 關鍵字 | 答案 / 技巧 |
|---|---|
| ELL 兩大手法 | padding(補零成矩形)+ transpose 成 column-major |
| ELL 為何能 coalesce | i = t*numRows + row,同迭代相鄰 thread 索引相鄰 → 連續位址 |
| ELL column-major 索引公式 | i = t*numRows + row;反推 row = i % numRows |
| ELL 解決什麼、沒解決什麼 | 解決 coalescing;未解 control divergence、空間更差 |
| ELL padding 何時爆量 | 少數 row 超長時,全列被 pad 到最長 → 空間可達 CSR 的 20 倍 |
| Hybrid ELL-COO 動機 | 把超長 row 尾端 nonzero 抽到 COO,降低 ELL padding 與 divergence |
| Hybrid 犧牲什麼 | accessibility:溢位到 COO 的 row,取其全部 nonzero 須搜尋 COO |
| 拆分/排序成本如何划算 | iterative solver 矩陣不變 → 一次預處理攤銷到多次迭代 |
| JDS 用什麼降 divergence | 依 row 長度排序,同 warp 內列長相近 → 迭代次數相近(免 padding) |
| JDS 排序為何不改解 | 重排列=重排方程;y 隨之重排即等價,最後用 row 陣列把解排回原序 |
JDS 的 row / iterPtr 用途 |
row 記原始列索引(排回解);iterPtr 標每次迭代(欄)起點 |
| JDS coalescing 的缺點 | 無 padding → 起點任意 → 無法強制對齊 → 可能遜於 ELL |
| SpMV 的 OP/B 與意義 | 0.25(2 ops / 8 bytes,矩陣無 reuse)→ memory-bound,FLOPS 低 |
| 「最佳格式」怎麼選 | data-dependent,看 nonzero 分佈;無單一最優 |
Related Notes
- 14-Sparse-Matrix-Computation/01-Sparse-Matrices-and-Basic-Formats-COO-CSR
- 06-Performance-Considerations/01-Memory-Coalescing
- 04-Compute-Architecture-And-Scheduling/02-Warps-SIMD-and-Control-Divergence
- 09-Parallel-Histogram/01-Atomic-Operations-and-Basic-Histogram
- 22-Advanced-Practices-And-Future-Evolution/03-Memory-Bandwidth-and-Compute-Throughput
- 06-Performance-Considerations/04-Optimization-Checklist-and-Bottlenecks