演算法選擇與權衡 (Algorithm Selection and Tradeoffs)

重點總覽 (Overview)

平行程式設計三步驟的第一步是 algorithm selection。同一個問題通常有多個演算法可解,而它們在四個面向上各有優劣,沒有任何一個演算法在所有面向都最好,程式設計者必須為「目標硬體」挑出最佳折衷。

項目 重點
演算法三大基本性質 definiteness(明確)、effective computability(可計算)、finiteness(會終止)
四個比較面向 複雜度 (complexity / work)、平行度 (parallelism)、一般性 (generality)、準確度/數值穩定性 (accuracy / numerical stability)
經典權衡 1 複雜度 vs 平行度 → Kogge-Stone vs Brent-Kung (Ch.11 Scan)
經典權衡 2 一般性 vs 效率 → merge sort vs radix sort (Ch.13 Sorting)
經典權衡 3 複雜度 vs 準確度 → DCS vs cutoff summation (Ch.18 電位圖)
範例演算法 cutoff binning:grid-centric 分解 + 分箱 (binning) + overflow list
量化結果 DCS work 隨體積平方成長;cutoff binning (SmallBin-Overlap) 比 CPU-SSE 序列版快 17× 且可擴充
Important

「最佳演算法」取決於目標平行硬體的特性,以及高複雜度能否用混合策略 (hybrid)thread coarsening(把序列/低複雜度演算法塞進每條 thread)來緩解。


演算法的三大基本性質 (Essential Properties of an Algorithm)

演算法 = 一步步精確描述、且電腦能執行的程序。必須同時具備三性質:

性質 意義
Definiteness 每一步都精確陳述,沒有模糊空間 (no ambiguity)。
Effective computability 每一步都能由電腦實際執行。
Finiteness 保證會終止 (terminate),不會無限執行。
Warning

這三性質是「是不是演算法」的門檻,與平行/序列無關。下面的四面向則是「同樣是合法演算法,但哪個更適合平行硬體」的選擇問題。


平行演算法的四個比較面向 (Four Axes of Algorithm Tradeoffs)

給定問題,通常能想出多個演算法,它們在以下面向各異:

面向 說明 偏好
Algorithmic complexity / work 完成同一計算所需的運算步數;步數少 = work efficient 越低越好
Parallelism exposed 可同時執行的程度;高平行度 → 用更少的 iteration 完成 越高越好
Generality 適用範圍;能處理多少種輸入/key 越廣越好
Accuracy / numerical stability 結果的準確度與數值穩定性 越高越好
Tip

關鍵心法:這四者通常無法兼得。提升一項常以犧牲另一項為代價,這正是「權衡 (tradeoff)」一詞的由來。平行程式設計者的工作就是針對手上的硬體挑出最佳折衷。


三大經典權衡 (Three Classic Tradeoffs)

本書多章已實際比較過演算法,可歸納為三個反覆出現的經典權衡:

權衡 範例 (章節) A 方 B 方
複雜度 vs 平行度 Scan (Ch.11) Brent-Kung:work-efficient (步數/運算少) Kogge-Stone:平行度高、iteration 少
一般性 vs 效率 Sorting (Ch.13) Merge sort:comparison-based,適用任何有比較運算子的 key Radix sort:noncomparison,複雜度低且易平行,但僅限特定 key
複雜度 vs 準確度 電位圖 (Ch.18) DCS:完全準確,但 work 高 Cutoff summation:犧牲少量準確度換大幅降複雜度

複雜度 vs 平行度 (Complexity vs Parallelism)

演算法 Work (運算量) Steps (深度)
Kogge-Stone O(N log N) O(log N)
Brent-Kung O(N) O(2 log N)
Important

誰勝出取決於硬體,且高複雜度可被緩解:用 hybrid(兩個平行演算法結合)或把平行演算法與低複雜度的序列演算法經由 thread coarsening 結合。

一般性 vs 效率 (Generality vs Efficiency)

複雜度 vs 準確度 (Complexity vs Accuracy)

Warning

複雜度 vs 準確度的權衡不是平行程式特有(序列實作也會碰到),但它會給平行程式設計者帶來額外挑戰(見下節 cutoff binning 的分箱與負載不均問題)。


Cutoff Binning 演算法 (Cutoff Binning Algorithm)

把 cutoff 概念真正搬上 GPU,展示「準確度換複雜度」在平行化時的額外挑戰。

序列 vs 平行的分解差異

序列 cutoff 平行 cutoff binning
分解策略 atom-centric(一次處理一個原子) grid-centric(每條 thread 算一個 grid point 的能量)
存取行為 原子向外散佈 → scatter 各 grid point 收集鄰近原子 → gather
是否好平行 否(scatter 不利 GPU)

序列版很直觀:對每個原子,走訪落在其半徑內的 grid point(grid 是可索引的陣列)。但 atom-centric 的 scatter 存取行為讓它難以平行,故 GPU 上改用 grid-centric 分解。

核心構想:分箱 (binning)

  1. 先把輸入原子依座標排序進 bins;每個 bin 對應 grid 空間中的一個 box,裝下座標落在該 box 的所有原子。
  2. 為每個 grid point 定義一個 neighborhood:包含「所有可能對該 grid point 能量有貢獻的原子」的那些 bins。
  3. 執行時:所有 block 各自走訪自己的 neighborhood bins;block 內所有 thread 一起掃描這些 bin 中的原子,但各自判斷該原子是否落在自己的 cutoff radius 內。
// 概念性 pseudocode:grid-centric cutoff binning kernel
__global__ void cutoffBinningKernel(/* bins, neighborhood lists, grid */) {
    int gridPoint = blockIdx.x * blockDim.x + threadIdx.x; // 每 thread 一個 grid point
    float energy = 0.0f;                                   // 結果累積在 register (gather)
    for (bin in neighborhoodBinsOf(blockIdx)) {            // block 走訪自己的 neighborhood
        loadBinAtomsIntoSharedMemory(bin);                 // 協同載入到 shared memory
        __syncthreads();
        for (atom in sharedBinAtoms) {
            if (distance(atom, gridPoint) < cutoff)        // 各 thread 個別判斷半徑
                energy += contribution(atom, gridPoint);
        }
        __syncthreads();
    }
    grid[gridPoint] = energy;
}

ASCII:bins、neighborhood 與 overflow list

Grid 空間切成 bins (每個 bin 容量固定 = capacity)
┌────┬────┬────┬────┐
│ b00│ b01│ b02│ b03│      ● = grid point (一條 thread)
├────┼────┼────┼────┤      neighborhood(●) = ● 半徑內可能有貢獻的所有 bin
│ b10│ b11│●b12│ b13│           = { b01,b02,b03, b11,b12,b13, b21,b22,b23 }
├────┼────┼────┼────┤
│ b20│ b21│ b22│ b23│
└────┴────┴────┴────┘

分箱:每個 atom 依座標放進 home bin
  bin 容量足夠 → 放入 bin
  bin 已滿     → 改放 OVERFLOW LIST ┐
                                     ▼
   [ overflow list ]  → kernel 結束後由 host 序列補算缺漏的貢獻

分箱的隱患:負載不均 (load imbalance)

Tip

SmallBin-Overlap 的關鍵優化:把「序列處理 overflow 原子」與「下一個 kernel 的執行」重疊 (overlap),藉此把這段序列補算的時間藏進 GPU 計算的影子裡。


擴充性與效能比較 (Scalability & Performance, Fig. 19.2)

以「跑多大體積仍划算」對比各實作 (CPU-SSE3 為序列 cutoff 基準):

體積規模 (ų) 誰最快 / 現象 原因
~1,000(小) CPU (SSE) 快過 DCS kernel 工作量不足以餵飽 GPU
2,000 – 500,000(中) DCS kernel 明顯勝 host 大規模平行性發揮
~1,000,000(大) DCS 反而比序列 CPU 還慢! DCS 複雜度高,work 隨體積平方暴增,淹沒硬體資源

三種 binning 實作:

實作 特點 結果
SmallBin neighborhood bin list,讓不同 block 處理不同 neighborhood 多了把原子從 global → shared memory 的指令開銷;中體積下原子本就不多,「少看一些原子」省下的不足以抵銷開銷
SmallBin-Overlap 將序列 overflow 處理與下一個 kernel 執行重疊 比 SmallBin 略有但明顯的改善
→ 整體 比高效序列 CPU-SSE cutoff 快 17×,並在大體積維持同樣的可擴充性
Important

主旨:演算法複雜度決定可擴充性。DCS 平行度雖高,但 O(V²) 的工作量讓它在大體積時崩潰;改用較低複雜度的 cutoff 演算法(犧牲少量準確度)才換到「大規模仍可用」的擴充性。這呼應 19-Parallel-Programming-And-Computational-Thinking/01-Goals-of-Parallel-Computing-and-Amdahls-Law 中「用平行換更大問題 / 更好解」的目標。


考試/面試重點 (Exam / Test Patterns)

情境 / 關鍵字 答案 / 技巧
「演算法必須具備哪三性質?」 definiteness、effective computability、finiteness
「平行演算法選擇要比較哪些面向?」 複雜度(work)、平行度、一般性、準確度/數值穩定性;無單一演算法全勝
Kogge-Stone vs Brent-Kung 屬哪種權衡? 複雜度 vs 平行度;Brent-Kung work-efficient,Kogge-Stone 平行度高(iteration 少)
radix sort vs merge sort 屬哪種權衡? 一般性 vs 效率;radix 快但限特定 key,merge 一般(任何有比較運算子的 key)
DCS vs cutoff summation 屬哪種權衡? 複雜度 vs 準確度;兩者平行度與一般性相同
「DCS 為何在大體積崩潰?」 work 隨體積平方 O(V²) 成長,大體積時甚至慢過序列 CPU
「cutoff 為何能省複雜度?」 物理上遠處粒子貢獻極小,可用隱式低複雜度方法集體處理
「為何 GPU 上用 grid-centric 而非 atom-centric?」 atom-centric 是 scatter 不利 GPU;grid-centric 是 gather(結果累積在 register)
「binning 的隱患與解法?」 bins 原子數不均(load imbalance);設小 bin 容量 + overflow list,kernel 後序列補算
「SmallBin-Overlap 優化什麼?」 把序列 overflow 處理與下個 kernel 執行重疊;對 CPU-SSE 序列版達 17×
「如何緩解高複雜度演算法?」 hybrid(結合兩演算法)或 thread coarsening(每 thread 內塞低複雜度序列演算法)