正則化儲存格式 (ELL、Hybrid ELL-COO 與 JDS)

重點總覽 (Overview)

本篇承接 14-Sparse-Matrix-Computation/01-Sparse-Matrices-and-Basic-Formats-COO-CSR。CSR 雖省空間又免 atomic,但兩大缺陷:記憶體存取無法 coalescecontrol divergence 嚴重。本篇三種「正則化」格式逐一補救。

格式 核心手法 解決的問題 代價
ELL padding(補零成矩形)+ transpose(column-major) coalescing 空間變差、divergence 未解
Hybrid ELL-COO 把超長 row 的多餘元素抽到 COO ELL 的 padding 爆量 + divergence 失去「依 row 取全部 nonzero」的存取性
JDS 依 row 長度 排序 + column-major divergence(免 padding) 最不易增刪、無法強制對齊邊界
Important

三者都在「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 出發兩步驟:

  1. Padding:找出 nonzero 最多的 row(下例 row 1 有 3 個),其餘 row 在尾端**補 padding(值 0)**到相同長度 → 變成矩形矩陣。colIdx 一併補。
  2. 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)
    }
}
Tip

若沒有 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)
Warning

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 抑制。

Tip

舊架構對齊規則較嚴時,可在轉置前於矩陣尾端多加幾列,強制每次迭代起點對齊到 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
Tip

拆分成本可被攤銷:真實 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 處理長度相近的列 → 迭代次數相近
Warning

JDS 的 coalescing 雖成立,但因沒有 padding,每次迭代的起始位址會任意浮動,無法像 ELL 那樣靠補列把起點對齊到架構邊界(如 64-byte)。這使 JDS 的記憶體存取有時不如 ELL 高效


本章總結:Data-Dependent 效能與 OP/B (Summary: Data-Dependent Performance & OP/B)

為何 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
Important

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 分佈;無單一最優