JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

阿姆達爾定律與 GPU 運算

你可以買一千顆核心,或一張有上萬個算術單元的顯示卡——但兩者都不會讓一個序列程式快上一千倍。本篇用一條為所有平行夢想封頂的定律,以及一台讓平行避無可避的機器,為本級畫下句點。

再多人手也縮不掉的那頑固一小時

上一篇把執行緒與 MPI 交到你手上,展示了一件工作如何拆給眾多工人——不論他們共用一塊記憶體、還是各自握著自己的一片。自然的期望是線性的:十個工人,十分之一的時間;一千個工人,千分之一。阿姆達爾定律就是那盆說「不」的冷水。想像一件十小時的工作,其中九小時能拆給你想要多少就多少的工人,但有一小時是單一雜務,只能一個人從頭做到尾——譬如讀進輸入,或一個後面每一階段都倚賴的設定步驟。雇一千個工人,那九小時的平行部分融化到幾乎沒有,但那頑固的一小時還在。整件工作永遠低不過一小時:至多 10 倍加速,無論你投入多大的軍隊。

說精確些:若工作中有比例 s 是天生序列的(無法平行化),其餘比例 1 - s 能完美平行到 p 個處理器上,則加速比為 S(p) = 1 / (s + (1 - s) / p)。把 p 推到無窮,平行項消失,只剩 S = 1 / s。整個故事就在這一個分數裡——天花板完全由那塊序列切片決定。一個 95% 可平行(s = 0.05)的程式,無論你買多少核心,都封頂在 1/0.05 = 20 倍;一個 99% 可平行的,封頂在 100 倍。令人清醒的是,只要一點點序列工作,就能把你壓得很低。只要 5% 序列,你那一千顆核心裡就有 999 顆,在最初的二十倍之外什麼也沒替你買到。

Amdahl's law (fixed problem, add processors):

    S(p) = 1 / ( s + (1 - s)/p )        s = serial fraction
    S(infinity) = 1 / s                 the hard ceiling

    s = 0.50  ->  cap   2x      (p=8 already gives ~1.8x)
    s = 0.10  ->  cap  10x      (p=8 gives ~4.7x)
    s = 0.05  ->  cap  20x
    s = 0.01  ->  cap 100x      (need p=100 just to reach 50x)
決定天花板的是序列比例 s,而非核心數。買硬體之前,先量出 s。

強擴展、弱擴展,以及一道誠實的逃生口

阿姆達爾聽起來像是給超級電腦判了死刑,然而有上百萬核心的機器顯然值得建造——所以這幅圖裡有什麼太悲觀了。修正之道在於注意阿姆達爾悄悄凍住了一件事:問題規模。問「固定的工作、更多工人——能多快?」是強擴展,那是嚴酷的局面,因為你每加處理器,每一個分到的工作就縮小,直到那固定的序列一小時主宰一切。但科學家很少把工作固定。給他們更大的機器,他們就模擬更細的網格、或更多粒子。誠實的問題於是變成「相同時間裡做更大的工作——需要多多少工人?」那就是弱擴展,而它仁慈得多。

弱擴展為何更仁慈?通常序列部分是個大致固定的額外開銷,而平行部分隨問題增長。把核心與網格都加倍,每核心的平行工作維持不變,而序列比例 s 作為如今更大總量裡的一份,反而縮小。天花板 1/s 隨著你的成長而抬高。這就是古斯塔夫森的觀察,阿姆達爾那樂觀的雙生兄弟:當工作負載與硬體一同放大時,有用的加速會持續到來。兩條定律都為真;它們回答不同的問題。強擴展告訴你更多核心能否把這件工作做得更快(阿姆達爾把它狠狠封頂)。弱擴展告訴你更多核心能否讓你在相同的牆鐘時間裡,去攻一件更大的工作(這裡的消息是好的)。

備料工的體育場:GPU 是怎麼運作的

如果阿姆達爾警告你平行有極限,GPU 就是把那些極限推到極致、逼你正視本級所教一切的機器。這幅圖:CPU 是幾位才華洋溢的主廚,每位都快速照著複雜食譜做;GPU 是一座擠滿備料工的體育場,每個都慢而簡單,但有數千個,全都同時做著完全相同的切菜動作。要做一頓多變的美食,你想要主廚。但要以一模一樣的方式削一萬顆馬鈴薯,那支備料工大軍轉眼就完工。GPU 運算就是把數值工作改鑄成第二種形狀的藝術——對數千個不同的資料項做同一個運算——好讓繪圖晶片龐大的平行性放手施展。

這正是你在向量化那篇見過的同一個 SIMD 想法,只是被瘋狂地放大了。CPU 的向量單元把一道指令一次套用在或許八個數字上,而 GPU 讓數千條執行緒齊步前進,全都在自己那片資料上執行同一道指令。其陷阱恰恰是 SIMD 的陷阱:當執行緒必須走不同的分支——有些走 if、有些走 else——硬體就再也無法讓它們齊步。它把兩條路徑都跑一遍,並遮掉那些不該動作的執行緒,於是一個滿是依資料而定之分支的核心,就把大半台機器白白丟掉。GPU 鍾愛筆直、整齊的算術,懲罰不規則、多分支的程式。合身的工作是稠密線性代數、樣板掃描、粒子更新——而這些恰好是本級的家常便飯,絕非偶然。

還有第二個更深的微妙之處,它直接連回本級的第一篇。GPU 有驚人的尖峰浮點運算量,但除非資料夠快送達,它的算術單元就會挨餓。GPU 藏匿記憶體延遲,靠的不是大型快取,而是純粹的超額認購:同時有數萬條執行緒在途中,於是只要一條停下等記憶體,另一條就緒的就立刻頂上,算術單元永不空轉。這個把戲只在有海量獨立工作時管用——這又是在說 GPU 要求龐大而整齊的平行性的另一種說法。而正如在 CPU 上一樣,一個以零散、對快取不友善的方式碰記憶體的 GPU 核心,受制於記憶體頻寬、而非浮點運算量。屋頂線模型對 GPU 同樣適用;只是機器更大,而資料區域性差所付的罰款還更陡。

混合精度:刻意拿位數換速度

GPU 把一個本級其餘部分多半視為理所當然的問題磨利了:你究竟需要多少位數?幾乎所有科學計算都跑在雙精度——64 位元,約 16 位十進位數字——因為它是安全的預設。但更窄的格式便宜得多:32 位元的單精度搬一半的位元組、跑快約一倍,而 GPU 為機器學習加入的 16 位元精度可以更快,常達數倍,因為太多工作都是記憶體受限的,而位元組越少,每趟從記憶體取的數字就越多。誘惑顯而易見。危險也同樣顯而易見:半精度只攜帶三到四位可靠的十進位數字,所以天真地用,就是通往無意義答案的一條捷徑。

出路是混合精度:用便宜的低精度做大部分工作,但把少數精度脆弱的步驟用高精度守住。經典樣式是解 A x = b 的迭代精化。把 A 分解,第一次求解用單精度(或半精度)——快,但只大致準。接著用雙精度算出殘差 r = b - A x_hat,再用便宜的精度解出修正量,並加回去。幾個這樣的循環就能找回完整的雙精度準確度,而大半的浮點運算都跑在便宜的檔位。你既拿到低精度的速度,又拿到高精度的答案品質,因為那昂貴、對準確度至關重要的部分——量出殘差——很小。

為本級收尾:速度究竟從何而來

退一步,整個級別便講出一個連貫的故事。浮點運算便宜而記憶體慢,所以好的演算法囤積資料區域性,而記憶體階層決定它的速度。這就是為什麼把每個載入的數字重用許多次的 BLAS-3 矩陣乘矩陣核心能跑近尖峰,而頻寬受限的核心卻爬行。平行性把算術單元加倍,但阿姆達爾把回報封頂在 1/s,溝通又再課它一筆稅。GPU 則是這一切同時的極端展現:數千個便宜的單元、對區域性與整齊性的嚴苛要求,以及和別處一樣的那道記憶體之牆。混合精度是最新的槓桿,拿你省得起的位數,去換你用得上的速度。

注意一件幾乎從不出現在這份清單上的事:降低浮點運算的次數。第一門演算法課帶來的直覺——數運算、把它們最小化——對真機上的效能而言是錯的直覺,因為兩個有相同浮點運算次數的演算法,可能相差 10 倍,只因一個漂亮地串流記憶體、另一個把快取攪得稀爛。這是貫穿這裡每一篇的那條安靜的線索:在一台現代機器上,資料在哪裡比你做多少運算更要緊。能贏的那些重排與分塊,正是讓熱資料保持在近處的那些,而不是省掉幾次乘法的那些。

最後一份誠實,正是這整道階梯所堅持的那種。這一切都不會改動答案正確性理論的任何一位數字。一台 GPU 把向後不穩定的演算法跑得快,照樣是把垃圾產得快;對病態問題用混合精度,也只是更早抵達錯誤的答案。效能與準確度是分開的兩條軸,而一個快速的錯誤答案,是最昂貴的那一種。所以紀律是:為良態問題挑一個穩定的演算法,把它弄快——追逐區域性、倚靠調校過的 BLAS、平行化阿姆達爾所允許的部分、只在條件數許可之處才降精度。把這個次序擺對,那台機器,無論它握著多少核心或多少備料工,終於會替你做事。