在連續批次中解鎖非同步性
同步批次使 GPU 有四分之一時間處於閒置狀態
在同步的連續批次中,CPU 與 GPU 交替工作:GPU 計算前向傳遞時 CPU 必須等待,而 CPU 準備下一批次時 GPU 也必須等待。對一個產生 8K 令牌、批次大小為 32 的 8B 模型進行效能分析,總耗時 300.6 秒,其中有 24.0% 的時間是 GPU 閒置等待 CPU。這段閒置時間代表了吞吐量的損失,而僅靠連續批次無法消除。
建立併發需要非預設的 CUDA 串流
要讓 CPU 與 GPU 工作重疊,我們必須在不阻塞 CPU 的情況下啟動 GPU 操作。在 PyTorch 中,未指定串流的操作會在預設串流上執行,該串流會與所有其他串流同步,迫使 CPU 必須等待 GPU 完成。使用非預設串流可讓 CPU 立即將工作排入佇列並重新取得控制權。我們需要三條獨立的串流:一條負責 host‑to‑device(H2D)傳輸、一條負責 GPU 計算、以及一條負責 device‑to‑host(D2H)傳輸,因為這些操作彼此獨立,放在不同串流中即可同時執行。
在串流之間強制順序需要 CUDA 事件
獨立啟動這三條串流會導致競爭條件:計算可能在 H2D 傳輸完成之前就開始,而 D2H 可能在計算結束前就傳輸結果。CUDA 事件會在串流中記錄一個標記;另一條串流可以在繼續之前等待該標記。透過在每次 H2D 傳輸後記錄事件,並在計算串流中等待該事件;同樣地,在計算結束後記錄事件,並在 D2H 串流中等待,我們建立了一條管線,使 GPU 能在保持正確順序的同時,CPU 在排入所有工作後即可自由。
防止資料損毀與處理攜帶需要雙緩衝與遮罩
在 batch N 計算期間準備 batch N+1 可能會覆寫 GPU 仍在讀取的輸入緩衝區。解決方法是保留兩組 host 端與 device 端的張量(槽位 A 與 B),交替使用,雖然會使記憶體使用量加倍,但能防止競爭條件。由於同時出現在兩個批次中的請求需要將其新產生的 token 作為下一批次的輸入,我們在 batch N+1 的輸入緩衝區中插入一個佔位 token(值為 0),稍後再以 batch N 輸出的實際 token 替換。這種替換稱為攜帶(carry‑over),透過一個指明目標位置的攜帶遮罩(carry‑over mask)執行;該遮罩在 CUDA graph 內應用,成本可忽略不計。
完整的非同步迴圈將 CPU 批次準備與 GPU 計算重疊
步驟 0(冷啟動)使用同步路徑派發 batch 0。從步驟 1 起,CPU 在 GPU 於槽位 A 計算 batch N 的同時,於槽位 B 準備 batch N+1:驅逐已完成的請求、接納新請求、更新 KV 快取,並建立攜帶遮罩。當輸入就緒後,CPU 在 H2D 串流上排入 H2D 傳輸,記錄事件,於計算串流中等待該事件,啟動前向傳遞,再記錄另一個事件,並在 D2H 串流中等待該事件後才啟動輸出傳輸。CPU 隨後僅在最終同步事件上阻塞,以將結果拷貝回來,之後立即開始準備下一批次。此模式不斷重複,只要 batch N+1 的輸入在 batch N 結束時已就緒,GPU 就能持續忙碌。
實驗結果顯示 22% 加速
在相同工作負載(8K 令牌、批次大小 32、8B 模型)下使用非同步管線執行,得到的時間線顯示 CPU 與 GPU 幾乎持續重疊。GPU 的活躍時間提升至總執行時間的 99.4%,高於同步情況下的 76.0%。總生成時間從 300.6 秒下降至 234.5 秒,約提升 22%。剩餘的差距對應於不可避免的同步點,即 CPU 必須等待 D2H 傳輸完成;不需要新增 kernel 或模型變更。
結論
透過使用 CUDA 串流與事件將基於排程的相依性改為基於資料的相依性,並以雙緩衝與攜帶遮罩解決競爭條件,我們在連續批次中將 CPU 與 GPU 的工作解耦。這使 GPU 能保持飽和,為 LLM 推論帶來顯著的吞吐量提升,同時維持模型精度。