Select Page

核心數增加,程式不一定會等比例變快。真正的限制,可能是工作不能同時做、資料送不進來,或有人早已做完,卻還在等最慢的那一份。

要改善平行運算效能,先找出時間花在哪裡,再決定要加核心、減少資料搬移,還是重新分配工作。

想像一間廚房:增加廚師,只有在食材供應、料理分工和出餐順序都配合時才有用。

如果所有人都擠在同一個冰箱前,爐子再多也救不了。這個觀點同樣適用於 CPU、GPU,以及把工作分到多台電腦的系統。

先看三種「人多卻更慢」的情況

把一疊數字分給兩個人計算,算的部分確實可以縮短,但如果兩人坐在教室兩端,還得走過去交付小計,最後的總時間就包含計算、傳遞與合併。省下的時間,可能全被溝通吃掉。

換成四個人,也不保證改善,如果其中一份特別難,另外三人先完成後只能等待,完成時間仍由最慢的一份決定,再把全班都拉進來數人頭,如果要先搬椅子、換座位、協調隊形,光是準備工作就可能比直接清點更久。

這三種情況,分別對應通訊成本、負載不均,以及拆分工作的額外開銷,Stanford CS149 第一講用課堂活動說明這些限制,實際程式裡則可能表現成資料複製、同步等待、任務建立與排程。

「有變快」和「用得有效率」是兩回事

加速比是原本的執行時間除以新的執行時間。同一件事從一分鐘變成半分鐘,就是兩倍加速。這可能已經有商業價值,但如果用了大量額外硬體,仍要另外看資源利用率與成本。

比較時要固定資料量、演算法、輸出品質與計時範,。拿最佳化過的多核心版本,去對照刻意寫得很慢的單核心版本,得到的漂亮倍數無法說明平行化本身貢獻多少。

加核心有上限:用 Amdahl 定律算一個例子

假設一項固定工作在單核心需要 100 秒,其中 20 秒必須依序做,剩下 80 秒可以完美分工。

即使忽略排程與通訊成本,增加核心也只能縮短那 80 秒。這是本文另外設計的理論示意,不是影片實測,也不是任何 CPU 的跑分。

總時間=20+80 ÷ 核心數。加速比=100 ÷ 總時間。這個簡化模型對應 Amdahl 定律,假設各核心能力相同,工作量固定,而且可平行的部分能均勻分配。

核心數理論執行時間相對單核心加速
1100.0 秒1.00 倍
260.0 秒1.67 倍
440.0 秒2.50 倍
830.0 秒3.33 倍
1625.0 秒4.00 倍
3222.5 秒4.44 倍
Amdahl 定律理論示意,20% 工作無法平行時,32 核心約加速 4.44 倍
固定工作量、20% 依序執行、忽略額外開銷的理論示意。橫軸採類別刻度,並非硬體實測。

即使核心數持續增加,這個例子的加速也只會逐漸逼近五倍,實際程式還有其他開銷,所以診斷時要同時看「哪些部分不能拆」和「拆開後多付了什麼成本」,如果增加的是工作量而不只是核心數,就要另看吞吐量或弱擴展。

超純量、多核心、SIMD,分別在平行什麼?

處理器早就不只靠提高時脈來加速。功耗、散熱和指令相依關係,限制了單一執行流程的成長。

多放運算資源只是起點,軟體還要提供足夠、可同時執行的工作。

超純量:在同一條指令流裡找出可同時做的事

超純量處理器可以在條件允許時同時發出多道指令,例如兩筆互不依賴的乘法有機會重疊執行,但下一步若要用到前一步的結果,就必須尊重相依關係。硬體能尋找現有的平行性,卻不能憑空消除資料依賴。

多核心:讓不同工作各自前進

多核心讓不同執行緒有機會同時執行,各核心可以處理不同的指令流。

前提是程式、函式庫或執行環境有把工作拆開。只開一條主要計算執行緒,不會因為買到更多核心就自動平均分散。

課程以縮小複雜控制邏輯、換取更多核心來解釋設計取捨,但這是幫助理解的模型

。實際 CPU 常同時具備複雜核心、多核心與向量單元,不應理解成現代多核心都取消了亂序執行,或每個核心必然比較慢。

SIMD:同一個動作,一次處理多筆資料

SIMD 是單一指令、多重資料,例如把一批像素的亮度做相同調整,向量指令能同時處理多個元素,一次能處理幾筆,取決於指令集、向量寬度與資料型別,「一次八筆」只是常見教學例子。

如果不同元素需要走不同分支,部分運算通道可能被遮蔽,等待另一條路完成,這就是分支分歧造成的浪費,多核心的各個核心可以走不同指令流,SIMD 組內則有更強的共同執行限制,兩者不能當成同一回事,參考 CS149 的多核心架構講義。

NumPy 等工具可以把批次運算交給最佳化實作,但一個陣列加號不代表整個陣列只需要一道機器指令。

通常仍有分批載入、向量運算、寫回及邊界處理,是否使用 SIMD 也受資料型別與平台影響,NumPy 官方 SIMD 說明介紹了依 CPU 能力選擇運算核心的機制。

快取解決什麼?讓同一份資料少跑幾趟

記憶體階層可以想成倉庫、車庫與工作桌,空間越靠近操作的人,通常越有限,卻也越方便拿取,快取的價值,就是讓近期或附近會用到的資料留在靠近核心的位置。

  • 時間區域性:剛用過的資料,稍後還會再用,保留它能減少重新讀取。
  • 空間區域性:這筆資料旁邊的內容很快也會用到,連續存取能更有效利用一次帶回的快取列。

所以同樣數量的運算,不同資料排列方式可能有明顯速度差,連續掃描陣列,和沿著分散的指標到處跳,給記憶體系統的壓力不同。把工作切成適合快取的小區塊,完成區塊內的多個步驟再往下一塊,常比每個步驟都重掃整份資料更值得試。

快取不會保證所有存取都快,工作集合太大、資料重用不足,或不同核心反覆改動同一快取列,都可能增加傳輸,甚至兩個執行緒修改的是不同變數,只要它們碰巧位於同一快取列,也可能產生偽共享,需要用量測確認。

延遲與頻寬不同,多執行緒也不能取代頻寬

延遲是等一份資料需要多久,頻寬是單位時間內能送多少資料。

增加車道可以讓更多車通過,卻不必然縮短每台車從起點到終點的行程,相同地,記憶體頻寬提高,不代表每一次零散讀取都會更快。

硬體多執行緒的想法,是某條執行緒等待時,先安排另一條準備好的執行緒使用運算單元,它可以隱藏部分等待,增加整體吞吐量,但不會讓原本那份資料提早到達,也不等於多出完整的實體核心。

這招需要足夠的獨立工作和可用資源,如果記憶體通道早已滿載,再增加執行緒只是在增加排隊者,把「多執行緒能隱藏延遲」直接說成「多執行緒能解決所有記憶體問題」,就會選錯最佳化方向。

為什麼高階 GPU 也可能吃不滿?

做兩個大陣列的逐元素乘法時,每筆結果只做少量算術,卻要讀取輸入並寫回輸出,這種工作可能先用完記憶體傳輸能力,運算單元仍有餘裕,CS149 第三講以 V100 時代的假設硬體與運算模型說明這件事,低利用率是特定工作負載相對算術峰值的估算,不能解讀成所有 GPU 都只有固定百分比有效能。

這裡的關鍵是算術強度:相對於搬動的資料量,完成了多少運算,若能把資料載入後多用幾次,或避免不必要的中間結果寫回,就可能提高效率,不過增加無意義的計算,只會讓數字好看,並沒有更快完成使用者的工作。

跑本地模型時也要區分「放得下」和「送得快」,容量決定可容納的資料與模型規模,頻寬影響搬運速度,實際瓶頸還會隨批次與階段改變。可以接著看站內的 本地 AI 記憶體容量與頻寬整理,理解為什麼只看 GPU 核心數不夠。

工作怎麼分,決定大家會不會互相等待

靜態分配是在開始前先切好每個工作者負責的範圍。它簡單、開銷低,也容易保留資料區域性,適合每份工作成本接近的情況,若有些影像區塊特別難算,平均分配像素數不等於平均分配計算時間。

動態分配則讓工作者完成一批後,再領下一批,降低有人空等的機會,代價是領取任務、同步與管理佇列都要成本,每批太大,仍可能負載不均。每批太小,則可能忙著領工作而非真正計算。

一個實用折衷,是把大任務切成數量多於工作者的區塊,同時讓每個區塊保有足夠的連續資料,再比較不同區塊大小的總耗時,而不是預設動態排程一定比較快,CS149 的工作分配與排程講義也把負載平衡和管理開銷放在一起討論。

判斷一批整數是否為質數,就是很直觀的例子,使用逐一試除的做法時,不同數字可能花費不同時間,平均分配數字個數不代表負載均衡,如果能事先估計成本,提早安排較大的任務,也可能減少最後只剩一人忙碌的長尾。

ISPC 與 Cilk:表達可平行的工作,交給系統安排

ISPC 讓你用接近逐筆處理的方式描述運算,再以一組 program instances,也就是 gang,執行相同程式。

要特別分清楚:在 CPU 的常見用法中,gang 主要對應向量化,並不代表每個 instance 都是一條作業系統執行緒。

跨核心並行還需要 task 或呼叫端的多執行緒安排,入門可參考 ISPC 官方文件與 CS149 第一份程式作業。

Cilk 則以分支、匯合的方式表達任務相依關係,當一個工作者沒有事做,可以透過 work stealing 從其他工作者取得尚待執行的工作。重點是暴露可平行的任務,再由執行系統排程,而不是為每個小任務都建立一條新執行緒。想動手學習,可以從 OpenCilk 的平行任務教學開始。

快速排序則呈現另一種情況:先把資料按基準值分成兩側,接著左右可以分別排序,可平行的子問題隨遞迴逐步出現,是否能順利利用多核心,還受分區均衡與最小任務大小影響,Cilk 的關鍵字也不是所有標準 C++ 編譯器都直接支援,實作時要使用支援它的工具鏈。

資料搬運也要分:必要交換,還是實作多出的往返?

有些通訊是目前演算法與工作分配必須付出的成本,例如把影像分區後,相鄰區域需要交換邊界資料。

有些則來自實作細節,例如快取容量不足、傳輸粒度太大、重複複製中間結果。CS149 第六講將兩者區分為 inherent communication 與 artifactual communication。

區分的目的,是找到可以改的地方。必要交換不代表完全無法減少,重新選擇分區方式可能縮小邊界,實作造成的往返,也可能透過更好的資料布局、合併步驟和重用快取來降低。

把模型或工作拆到多台機器時,這些原則仍成立,除了各台機器的能力,還要看互連、分片方式與交接資料量,站內的 EXO 與本機 AI 分工整理,可以延伸理解多機合作為什麼不是把規格直接相加。

同樣地,顯存不足時把部分模型卸載到系統記憶體,可能讓工作成功執行,卻增加資料搬移。像 LTX 的低顯存工作流程,就適合用「容量換取可執行性,再付出搬運成本」的角度評估,不能只看是否成功啟動。

遇到程式跑不快,我會按這個順序檢查

  • 固定基準:使用同一份輸入、相同輸出品質與完整計時範圍,保留可重現的單核心或原始版本。
  • 找出熱點:先確認時間花在計算、I/O、記憶體、同步還是外部服務,別直接從增加執行緒開始。
  • 逐步增加並行度:比較少量與較多工作者的總時間,也看各工作者是否平均忙碌。
  • 檢查資料路徑:是否反覆複製、跨裝置來回、產生巨大暫存,或無法利用連續存取。
  • 一次改一項:調整區塊大小、合併運算或排程策略後,重新檢查正確性與總耗時。

不要只追求 CPU 或 GPU 的使用率數字。忙碌可能代表重複計算,低使用率也可能是合理地等待磁碟或網路。最終要回答的是:在相同品質與限制下,完成同一件事花了多久,用掉多少資源。

平行運算常見問題

核心越多,任何程式都會更快嗎?

不會。程式必須有足夠可同時執行的工作,而且不能被序列部分、記憶體頻寬、同步或負載不均限制。更多核心也可能增加額外開銷。

SIMD 和多核心有什麼不同?

SIMD 讓一個指令同時作用於多筆資料,多核心則讓不同核心執行各自的指令流。兩者可以並用,但工作必須符合各自的執行方式。

快取越大,效能一定越好嗎?

不一定。資料是否會重用、存取是否連續、工作集合大小與其他核心的存取方式,都會影響快取是否真的減少等待與搬運。

多執行緒可以解決記憶體頻寬不足嗎?

多執行緒能在有獨立工作時隱藏部分延遲,但不會增加實際可用頻寬。通道滿載後,增加執行緒可能只增加排隊與競爭。

應該先升級硬體,還是先改程式?

先量測。若主要成本是重複搬移、任務分配或序列熱點,改善程式可能更有效。確認瓶頸確實對應硬體能力後,才有依據選擇升級項目。

先找到等待發生在哪裡,再談加速

平行運算的核心問題,是讓可同時做的工作真正同時進行,同時控制搬資料、分工與等待的成本。多核心提供空間,SIMD 提高相同運算的吞吐,快取減少遠端存取,多執行緒利用等待空檔,排程則避免工作分配失衡。它們各自解決不同問題,沒有哪一招能包辦全部。

如果手上就有一段跑不快的程式,先記錄它在不同工作者數量下的完整耗時,找出哪一步開始不再改善。這個轉折點,往往比硬體規格表更能告訴你下一步該改什麼。

延伸學習:Stanford CS149 前六講

想沿著相同脈絡深入,可以從 Stanford CS149 官方課程播放清單開始,並搭配 課程講義目錄。