當通用機器無路可走
現代 CPU 是通用性的奇蹟。它推測性地越過分支、重排指令、預取資料、餵飽好幾個算術單元——這一切只為了讓你丟給它的*任何*程式都跑得體面。但通用性是有代價的。為了執行未知的指令流,CPU 必須把大部分電晶體與大部分能量花在*控制*上:擷取、解碼、預測、排程。真正的算術——你真正想要的那部分——只佔了矽晶極薄的一片,也只佔了功耗預算極薄的一片。
現在假設你*確實*知道工作負載。深度學習推論、科學模擬、圖形著色——這些都不是任意纏繞的義大利麵。它們是同一小撮運算,多半是一個矩陣乘法或一個模板(stencil),在浩瀚的資料上重複,幾乎沒有分支。對這麼規律的工作負載而言,通用 CPU 是個悲劇性的錯配:你付錢買了從不啟用的分支預測器,付錢買了從不需要的亂序機制。逃生出口就是領域專用架構(DSA)——一塊與某一類演算法共同設計的晶片,幾乎每一顆電晶體都在做有用的算術。
GPU:成千上萬條執行緒齊步行進
脫離 CPU 的第一步,不是把*算術*專用化,而是把*控制*專用化。GPU下了一個粗暴的賭注:我的工作大多是同一條指令套用在海量不同資料上。因此與其用一個精緻的控制單元去駕馭一條執行緒,不如用一個樸素的控制單元一次駕馭三十二條執行緒。這三十二條一束稱為 *warp*(NVIDIA)或 *wavefront*(AMD),其執行模型就是 SIMT——單指令、多執行緒(Single Instruction, Multiple Threads)。
想像一支划艇隊。一名舵手喊出划槳節奏;三十二名槳手一齊划。你把*一次*擷取與解碼的成本,攤提到三十二條算術通道上,於是每次有用運算所需的控制開銷便崩塌下來。一個高階 GPU 的串流多處理器可以同時讓數十個 warp 常駐,而這裡就是隱藏記憶體延遲的訣竅:當某個 warp 因等資料而停頓,排程器立刻換進另一個就緒的 warp。只要有足夠多執行緒在飛行中,算術單元就永不閒置。GPU 並不像快取那樣試圖*避免*那趟漫長的記憶體之旅——它靠著手邊永遠有別的活可幹來*遮蓋*那趟旅程。
每個 GPU 程式設計師都會痛苦地學到一個陷阱。因為三十二條執行緒共用一個程式計數器,一個把*部分*通道導向某邊、其餘導向另一邊的分支,會迫使硬體兩條路徑都跑,每次把閒置的通道遮蔽掉。這就是 *warp 分歧(divergence)*,它能讓你的吞吐量砍半甚至砍到四分之一。SIMT 偏愛寬廣、規律、無分支的程式碼——這正是它為何如此擅長線性代數,又為何如此笨拙於——比方說——剖析(parsing)的原因。
點名真正的敵人:記憶體高牆
在我們打造更專用的東西之前,必須誠實面對矩陣乘法硬體*為何*長成那副模樣。瓶頸幾乎從來不是乘法器。在現代製程中,一次乘加(MAC)只花幾分之一皮焦耳。搬移運算元的代價則高得多。從晶片外的 DRAM 讀一個數,可能要花掉消耗它的那次乘法一百倍的能量——而且要花上數百個週期。便宜的算術與昂貴的資料搬移之間的這道鴻溝,就是記憶體高牆,它正是現代加速器設計*那個*決定性的約束。
Rough energy per operation (45 nm-class, order-of-magnitude): 32-bit integer ADD ............. 0.1 pJ <- arithmetic is nearly free 32-bit float MULTIPLY .......... 4 pJ read 32 bits from on-chip SRAM . 5 pJ <- the register/cache file read 32 bits from on-chip cache . ~50 pJ read 32 bits from off-chip DRAM . 640 pJ <- ~100x a multiply! Lesson: the joules are in the WIRES, not the math. A datum you fetch once and reuse 100 times is 100x cheaper per result than a datum you fetch fresh every multiply.
由此導出兩個後果,它們塑造了這一關的一切。第一,你用頻寬對抗高牆:堆疊式高頻寬記憶體(HBM)距離運算晶粒只有幾吋——其實是幾毫米——透過數千條連線每秒輸送數 TB,而非普通 DRAM 那條狹窄的匯流排。第二,更聰明地,你用重用對抗高牆:設計資料路徑,讓一個數一旦上了晶片,在離開之前就被乘上許多次。脈動陣列正是圍繞這第二個想法所打造過最優雅的機器。
脈動陣列:把算術變成心跳
H. T. Kung 以人類心臟為脈動陣列命名:資料被有節奏地泵送,流過一格格相同的微小單元,每一格都按著同一個時脈跳動。這裡沒有共用的暫存器檔,幾乎沒有控制邏輯。每個單元每個週期只做一件事——把兩個進來的數相乘、把乘積加進一個累加總和、再把輸入傳給鄰居。其精妙之處在於運算元在單元之間漣漪般傳遞,於是一個在陣列邊緣只載入一次的值,會在需要再次從記憶體取回之前,被一整列或一整行的乘法器重用。
這是最純粹形態的資料流架構。CPU 是*控制驅動*的:由程式計數器決定下一步做什麼。脈動陣列則是*資料驅動*的:運算在資料到達之處、到達之時發生。沒有指令要擷取,沒有分支要預測,沒有運算元要命名——那些連線*就是*程式。這就是為何一個 256×256 的 MAC 陣列,像 Google 第一代 TPU 裡的那個,能在每一個時脈完成 65,536 次乘加,而每個週期只從記憶體讀進寥寥幾個值。
One processing element (PE) of a weight-stationary array.
The weight W is loaded ONCE and stays put; activations flow through.
a_in (activation, flows left -> right)
|
psum v a_out -> (to PE on the right)
_in -> [ * + ] -------->
| ^
| W (weight: loaded once, held for the whole matmul)
v
psum_out (partial sum, flows top -> bottom)
each clock: psum_out = psum_in + (a_in * W)
a_out = a_in // hand activation to neighbour實作範例:串流一個 2×2 矩陣乘法
讓我們在最能展現訣竅的最小陣列上,把這心跳化為具體:一個 2×2 網格計算 C = A · B。四個單元各自累加 C 的一個元素。權重 B *固定*坐在單元裡;A 的各列從左邊流入,並在時間上錯開(skew),使得對的運算元在對的週期遇上對的單元。那刻意的對角線錯位,正是每個脈動陣列暗藏的編舞。
Compute C = A . B with stationary weights B in a 2x2 array.
A = | a11 a12 | B = | b11 b12 |
| a21 a22 | | b21 b22 |
Weights pinned in cells: PE[r][c] holds B[r][c]
PE00<-b11 PE01<-b12
PE10<-b21 PE11<-b22
Activations a-row enters from the LEFT, skewed by one cycle per row
so column 2 arrives one beat after column 1:
row1 feed: a11, a12 (start t=1)
row2 feed: a21, a22 (start t=2, one cycle later)
Each cell: psum += a_in * W , then forward a_in downward/rightward.
After the wave passes, the partial sums have summed exactly:
c11 = a11*b11 + a12*b21
c12 = a11*b12 + a12*b22
c21 = a21*b11 + a22*b21
c22 = a21*b12 + a22*b22 <-- a full matrix multiply, no register file- 載入權重。每個 B 元素只串流進來一次,鎖存進它的單元。從此 B 不再移動——這正是我們買到的重用。
- 錯位餵入 A。第 1 週期推入 A 的第 1 列,第 2 週期推入第 2 列。這一週期的交錯,使每個激活值對齊它必須加入的部分和。
- 敲擊時脈。每個週期,每個單元把進來的激活值乘上它儲存的權重、加進它的累加器、再把激活值傳給鄰居。波一旦抵達,沒有單元會閒置。
- 排出結果。激活波掃過之後,每個累加器都持有一個完成的 C 元素。在下一塊(tile)已經串流進來的同時,從陣列邊緣把它們讀出。
去數數記憶體流量,因為那才是重點所在。為了產出四個結果,我們載入四個權重與八個激活值——共十二個值——並執行了八次乘加。把這 2×2 玩具放大成 256×256 陣列,算術以 N² 成長,每週期 65,536 次 MAC,而你餵入的運算元只以 N 成長。你載入的每個權重會被一整行重用;每個激活值會被一整列重用。陣列把一趟記憶體之旅變成數百次乘法——這正是記憶體高牆所要求的解藥。
組裝一台真正的加速器——並認清它的極限
一台出貨的 NPU 或 TPU,是脈動陣列加上把它餵飽的所有支架。在 MAC 網格周圍,你會找到一塊大型的晶片內 SRAM 暫存區(快取的軟體管理表親,是被明確載入而非猜測得來的)、一疊裝模型權重的 HBM,以及處理矩陣乘法做不到的事的特殊功能單元——夾在各層之間的激活函數、正規化與池化。整塊晶片在宏觀尺度上就是一條管線:串流進一塊、相乘、套用激活、把結果寫回、重複。
留意這三台機器如何構成一道專用化的階梯。多核 CPU給你數十條完全獨立、樂於分支的執行緒。GPU給你數萬條鎖步前進的執行緒,並保有通用的彈性。脈動陣列則完全拋棄彈性,把一個運算的資料流硬接死。每往上一階,都是以付出通用性來換取能效——又是那個領域專用的交易,如今顯現為一道光譜,而非單一抉擇。
這就是本軌道架構半部的應用高潮。我們從一條在 CPU 裡爬行的單一指令開始,以一道每秒數 TB 流過矽晶的算術波前作結。串起它們的不是更聰明的數學——而是那個無情的、物理的真相:運算便宜,資料搬移昂貴,而勝出的設計,正是那些重排連線、讓一個數一旦被取來便能加倍償還其代價的設計。