卸載模型(offload model)
當 CPU 與像 GPU 這樣的加速器搭檔時,誰當家,工作又是怎麼真正送到加速器上的?卸載模型就是標準答案:CPU(主機)仍是老大、執行主程式,並把選定的繁重、平行的區塊卸載給裝置(GPU 或其他加速器),由它做數字運算再把結果交回。主機統籌;裝置加速。
具體而言,一次卸載有一個一再出現的形狀,而每一步都由主機驅動。首先主機在裝置上配置記憶體(裝置通常有自己獨立的記憶體)。接著它把輸入資料從主機記憶體跨越連接匯流排複製到裝置記憶體。然後它啟動核心——告訴裝置跨越許多執行緒去執行那段平行運算。裝置執行,主機通常等待或做其他工作,最後主機把結果從裝置記憶體複製回主機記憶體。想像把一大批表格寄到一個快速處理中心:你得把它們打包寄過去(複製進),他們平行地處理整批,再把結果寄回來(複製出)。關鍵在於主機與裝置往往不共用一個定址空間,所以一個主機指標在裝置上毫無意義、反之亦然——這些複製並非可有可無的瑣事,它們正是資料跨越鴻溝的方式。
它之所以重要,是因為這趟來回是異質運算的主導成本模型,也解釋了一個恆久的實務教訓:相對於運算,傳輸是慢的。一個只跑幾微秒的核心,若你花了好幾毫秒把它的資料經匯流排寄過去,就毫無意義;所以卸載模型只在那塊工作夠大、夠平行、夠被重複使用以攤平複製成本時才划算——或者當你讓資料常駐在裝置上、跨越許多核心都不必複製時。「統一」或「共享」記憶體方案試圖藉由讓兩邊看見同一個定址空間來隱藏複製,但資料終究得實際搬移,所以它們減輕的是程式設計,而非頻寬的定律。
主機:cudaMalloc(&d_a, bytes); // 1. 在裝置上配置 cudaMemcpy(d_a, a, bytes, H2D); // 2. 把輸入從主機複製到裝置 kernel<<<grid, block>>>(d_a, d_c); // 3. 啟動(裝置運算) cudaMemcpy(c, d_c, bytes, D2H); // 4. 把結果從裝置複製回主機 // 步驟 2 與 4 跨越匯流排,常常比步驟 3 花費更多
四步驟的卸載:配置、複製進、啟動、複製出。那兩次複製跨越主機-裝置匯流排,常常主導了總時間。
主機與裝置通常有各自獨立的記憶體,所以一個主機指標在裝置上毫無意義——這些複製是強制的,不是瑣事。常見的錯誤是只計時核心、忽略傳輸;對於小型或一次性的工作,複製進/複製出可能遠大於運算本身,使卸載反而淨虧。