©PaperWeekly 原創 · 作者 | 張逸驊
單位 | 密歇根州立大學博士生
研究方向 | 可信人工智慧
過去的兩週裡,DeepSeek 在社交媒體上宣告這是他們的開源周(OpenSourceWeek),並連續五天放出了多款軟體庫。
前段時間分別釋出了 FlashMLA(高效 Hopper GPU MLA 解碼核)、DeepEP(面向 MoE 的專家並行通訊庫)以及 DeepGEMM(支援 FP8 的 GEMM 庫)。而就在第 4 天,他們一口氣開源了三大元件:DualPipe、EPLB 以及 profile-data,其中的 DualPipe 因為引入了“雙向流水線並行”這一核心理念,引起了廣泛討論。
本文將聚焦於 DualPipe 的核心思路:如何在大模型的訓練階段,實現前向(forward)與後向(backward)的完全重疊,從而大幅降低流水線中的「空閒時間(bubble)」。
為了讓讀者們更好地理解這些概念,本文從一個通俗易懂的類比——“機械加工中的工藝最佳化”——切入,並在每個部分先講清楚比喻場景,再與深度學習中的並行訓練一一對應,讓讀者可以在腦海中形成清晰的“具象”畫面。
同時,文末我們將深入到DualPipe 技術的原始碼層面,探討它如何進一步減少流水線氣泡、實現前後向交疊、將通訊帶來的壓力減小到極致,且如何在較複雜的混合並行場景下落地。

引言
在大語言模型(Large Language Model)如 GPT-3、PaLM、LLama 等火熱的當下,分散式訓練已成為突破單卡 GPU 極限、成功訓練超大模型的必備手段。
我們經常聽到諸如“資料並行”“模型並行”“流水線並行”等名詞,但對初學者來說,很難直觀把握它們之間的區別與聯絡。尤其是當大家閱讀到一些高階用法,如 DeepSeek-V3 裡採用的 DualPipe 技術,可能會覺得晦澀難懂。
與此同時,在工業領域,最佳化生產工藝需要經過無數次試錯;在人工智慧領域,訓練大語言模型同樣需要反覆調整引數。這兩件事看似毫不相關,卻有著驚人的相似性。讓我們跟隨老王機械加工廠的故事,看看如何用車間裡的機床與工序,理解大模型訓練中的四大並行技術。
1.1 單卡時代:從手工小作坊說起
在蘇州工業園區,老王擁有一個不大不小的機械公司,他的公司的主要業務是為機械產品的最佳化加工工藝,比如鑄造溫度、淬火時間、切削角度等等。
每當一個新的訂單到來時,老王會根據經驗設計一份初始工藝手冊,按照這個工藝手冊進行加工,對加工後的零件進行質量檢測,並根據當前加工出來的零件缺陷,從後往前,對每一道工藝進行引數調整:比如當前加工出來的產品有空隙缺陷,就告訴我們鑄造溫度應該升高一些。
在創業初期,老王最初只接螺絲釘加工訂單。這類零件加工簡單,只需在一臺多功能機床上完成切削、打磨兩道工序。每當出現次品,老師傅就會對著成品倒推問題:如果是打磨不勻,就調整打磨引數;若是切削誤差,就修改刀具角度。
整個過程都在同一臺機床上閉環完成。老王的工廠就像一個手工小作坊:不成體系不成規模,但對於簡單的零件還是夠用的。
1.2 模型並行(Model Parallelism):工藝手冊的拆分藝術
有一天老王接到了一個以前沒有遇到過的大單子:發動機曲軸的工藝最佳化,老王發現自己的單臺機床根本無法勝任這個工作。他將鑄造、熱處理、精密加工等工序拆分到三臺專業機床上。每臺機床的操作手冊都只記錄對應工序的引數標準,且調整鑄造引數時必須同步考慮對後續工序的影響。
此時,老王發現了一個以前沒有遇到過的問題:機床的閒置問題。在零件進行鑄造階段,熱處理和精密加工的機床好像沒有事做;於此同時,把一個機床的加工結果搬運到另一個機床上加工的過程好像也需要時間,而在“搬運”的過程中如果不做合理的安排可能會使機床閒置的問題更加嚴重。
在大語言模型中,這對應“模型並行”(Model Parallel)。當模型的體量過大,單個 GPU 無法容納所有引數時,就把模型本身(如不同層、不同模組)拆分到多個 GPU 上。
在上邊的例子中,鑄造就像是模型的輸入層、熱處理是中間層、而精密加工是輸出層。模型訓練時每個 GPU 負責特定層的計算,必須透過裝置間通訊傳遞中間結果。這種方法的代價是不可避免的 GPU 閒置以及需要頻繁的跨裝置資料傳輸。老王遇到的問題正是 GPU 之間的排程和通訊的問題。
1.3 資料並行 (Data Parallelism):克隆車間計劃
為加速工藝引數最佳化,資金充裕的老王在隔壁建了三個完全相同的車間。每個車間都配備全套四條產線,分別加工不同批次的渦輪盤(資料分片)。收工時,四個車間的工藝主任會開會比對資料,統一修訂工藝標準。原本收到的 10,000 塊原材料加工最佳化需要一個月,現在只需要半個月了。
老王很好奇,為什麼我的車間數量翻了四倍,而速度只提升了兩倍呢?和車間主任們溝通了解到,原來雖然工藝相同,不同的原材料在加工的過程中會遇到一些獨特的問題,導致四個車間的收工時間不一樣,你等等我、我等等你,時間就這麼被浪費了。
這對應“資料並行”(Data Parallel)。每個車間(GPU)都持有完整的工藝手冊(模型引數副本),但加工不同的原材料批次(資料分片)。當遇到次品(計算損失)時,各車間獨立推導本批次的工藝調整方案(本地梯度),但需要將所有調整方案彙總平均(All-Reduce)後,才能更新統一的工藝標準(全域性引數)。
這種方法的瓶頸在於:最慢的車間(straggler GPU)會拖累整體會議進度(通訊同步開銷),且車間間的溝通耗時(All-Reduce 頻寬)隨著車間數量增加而顯著上升。
1.4 張量並行(Tensor Parallelism):協作加工超大單件
有一天老王出息了,接到了一個超級訂單:飛機機體制造的工藝最佳化。這種訂單加工的零件本身極其巨大,比如飛機機翼。儘管機翼只是某一道工序裡的一個大部件,但仍然需要呼叫很多臺同樣的機床進行協同。
也就是說,這個單件雖然屬於某個特定的工序模組,但其體量依然超出了單臺機床的處理能力。因此,老王就安排多臺相同功能的機床一同來完成同一個大零件的加工。
在大語言模型訓練中,這對應“張量並行”(Tensor Parallel)。即便將模型分割成不同模組後,某些單個模組本身依然很大。
此時,需要把這部分網路層對應的張量再進行更細粒度地拆分,分別放到多個 GPU 進行平行計算。比如把一個巨大的矩陣分割成不同的塊,分別放到不同 GPU 上並行做矩陣乘法,再把結果合併。這樣,單層的計算負載也能夠透過多卡來分攤。
這時,老王發現對機床的排程已經成了提升他工作效率的重中之重,因為此時不同機床間的協作非常密切,大部分機床都只負責某一部分或者某一小部分的零件加工,零件需要頻繁地運送到某個機床又運送出去,後邊工序的機床必須要前一個工序結束後才能工作,在質檢後的反饋階段,前一個工序的引數調整又必須等待後一個工序的引數調整完畢。
這樣就留下了大量的閒置問題:同時加工同一個零件的機床在等著他的兄弟機床結束任務、後一個機床在等前一個機床的工件送過來,又或是前一個機床在等後一個機床的反饋報告。老王覺得必須做點什麼才能讓自己公司的效率更高了。
這對應”張量並行”(Tensor Parallel)的深層邏輯。當單個工序(模型層)的處理需求超過單臺機床(GPU)的承載能力時,就需要將巨型零件(大張量)拆分為多個部件,分發給相同功能的機床組(GPU 組)協同加工。比如機翼的拋光工序需要四臺機床分別處理不同區域的表面,再透過拼接(張量通訊)確保接縫處的完美銜接。
但這類協作帶來了三重挑戰:零件拆分/組裝需要額外運輸時間(張量切分與合併的通訊開銷)、任何機床的延遲都會拖慢整組進度(同步等待)、質檢反饋需要在機床組內部完成多輪協商(梯度同步),這些因素共同導致了新的閒置形態——”協作氣泡”。
1.5 流水線並行(Pipeline Parallelization):讓不同機床同時高效運轉
針對不同工序間的協作困境,老王設計了一套精巧的流水線系統:如果原始的加工路線是:鑄造 – 鍛造 – 熱處理 – 拋光,當鑄造機床完成第一批零件的初加工時,這批零件立即被送往鍛造,而此時鑄造機床已開始處理第二批零件;當第一批零件完成鍛造進入熱處理線時,第二批零件恰好在鍛造線就位——就像多米諾骨牌般環環相扣。

▲ 不採用流水線並行時,資料的傳輸流程:資料在模型的不同部位依次傳遞,每次只有一個 GPU 在工作,大量的等待時間使得 GPU 的利用效率非常低。
具體得講,在未實行流水線系統之前,四個車間的工作模式就像上圖展示的這樣,第一批原料在被加工成成品等待檢驗前(T1~T4),每個時刻僅有一臺機床在工作,而當質檢報告生成後,引數調整又按順序原路返回,導致在引數調整階段(T5-T12),仍然只有一臺裝置沒有處於閒置狀態。
在上圖彙總,灰色區域表示在對應時間段,該車間處於閒置狀態。
聰明的老王一眼就看出來問題:在第一批原料送給第二道工序的時候,第一道工序完全可以運送第二批原料了,同理,在第一批原料送給第三道工序的時候,第二批原來也送到了第二道工序,此時第一道工序已經開始進入第三批原料了。如此一來,這個生產線路就變成了下邊這個樣子:

▲ 1F1B 流水線並行方案示意圖
上圖展示的正是著名的 1F1B(one-forward-one-backward)流水線並行方案。其基本原則是:當某個 GPU(或機床)發現可對最近一批資料執行梯度回傳時,便優先進行後向傳播。
例如,在 T5 時刻,Device 4 面臨兩個選擇——要麼開始執行第二批資料的前向傳播,要麼對第一批資料進行後向傳播;按照規則,它會優先處理第一批資料的後向傳播。
與此同時,所有資料均按批次順序依次回傳,後續批次的後向傳播永遠在前一批次的後向傳播全部啟動後才開始。此外,每個裝置在前向傳播過程中最多隻能累積一定數量的啟用資料(在圖中為 4),以確保每次前向計算儲存的中間資料(activation)足以支撐後續的梯度回傳,回到我們的例子,就好像機械加工的過程中記錄的中間資料,都是在質檢報告生成後用於輔助判斷工藝引數調整的重要資料。
舉例來說,在 T5 時刻,GPU 1 雖有機會處理第五批資料的前向傳播,但為了避免啟用資料過多而影響後向傳播所需的儲存和效率,它選擇不再累積新的前向任務。
顯然,即使在 1F1B 流水線方案下,部分裝置仍可能出現短暫的閒置狀態——這就是我們在大語言模型訓練中所稱的“氣泡”。氣泡的存在意味著裝置資源沒有被充分利用,降低了整體訓練效率,而這正是我們迫切希望透過更先進的排程策略來解決的問題。
為了進一步最佳化流水線排程,老王對整個過程進行了深入觀察,發現氣泡較大的根本原因在於各車間組織質檢報告與引數調整的反饋時間過長——對一批材料進行反饋調整的耗時大約是該批加工時間的兩倍。這導致第一道工序在加工完第四批材料後,必須長時間等待才能完成第一批資料的反饋調整。
為此,老王提出了一個開創性的思路:既然反饋過程耗時如此,不妨將其拆分為兩個獨立部分,實現解耦。比如,每一道工藝可能同時涉及夾具設計和加工方案設計;如果每臺機床能先根據質檢報告對夾具設計進行引數調整,再對加工方案進行調整,而這兩者又相互獨立(即上一工序的夾具調整無需等待當前工序的加工方案反饋),則整體氣泡率便能大幅降低。
基於這一理念,老王設計瞭如下圖所示的改進版流水線系統:

▲ ZB1P 流水線並行方案示意圖
在該設計中,每批材料的引數調整被分為兩步——淺藍色代表夾具設計調整,深藍色代表加工方案調整。
在同一工序中,同一步驟的引數調整存在依賴關係(例如,在鑄造-鍛造-熱處理-拋光的流程中,只有當鍛造工藝的夾具設計完成後,鑄造工藝才能啟動對應的調整);而不同步驟之間則相互獨立(例如,鍛造工藝的夾具設計調整無需等待拋光工藝的加工方案反饋)。
因此,與最初的 1F1B 方案相比,該設計在保證相同啟用資料數量(圖中為 4)的前提下,有效減少了氣泡的數量。
上圖所示正是 DeepSeek-V3 技術報告中,與 DualPipe 進行比較時採用的第二個基線——ZB1P 方案。上圖就是在 DeepSeek-V3 技術報告中,和 DualPipe 做比較的第二個基線:ZB1P,他在保證了和 1F1B 相同 activation 數量的情況下(圖中是 4)進一步得減少了 bubble 的數量。
在大語言模型訓練中,梯度回傳通常分為兩大步驟:
-
輸入梯度計算:將梯度從當前層傳遞至上一層;
-
引數梯度計算:計算當前層引數的梯度以便更新。
以一個線性層為例,其前向計算為 ,當損失 傳回時,我們獲得 ;接著需計算兩個梯度:(1) —— 用於梯度回傳至上一層;(2) —— 用於當前層引數更新。
令人有趣的是,這兩項計算在邏輯上並不直接相關:即便某層只完成了(1)而暫未執行(2),梯度依然能夠自然地向上回傳。
正是基於這一特性,ZB1P 方案將每一層的(1)與(2)解耦,使得輸入梯度的回傳(1)能夠提前完成,而引數梯度的計算(2)則可以稍後啟動,從而大幅提升了流水線的排程自由度和整體效率。
現在,你已經全面瞭解了 DeepSeek-V3 技術報告中與 DualPipe 比較的兩種流水線並行演算法的原理。報告中還提供了下表,對不同流水線並行演算法的效率進行了量化比較:

核心引數說明:
-
: 流水線深度,即參與平行計算的工序數量; -
: 前向傳播所需時間,對應各工序進行初步加工的時長; -
: 後向傳播所需時間,對應各工序完成反饋調整的時長; -
: 啟用資料累積視窗大小,即在梯度回傳過程中用於儲存中間啟用資料的上限。
從上表可以看出,ZB1P 方案在保持與 1F1B 相同啟用資料數量的前提下,透過將梯度回傳過程拆分並解耦,顯著降低了氣泡數量,從而縮短了裝置等待時間,提升了整體訓練效率。更先進的排程策略(如 DualPipe)正是在這一最佳化思路基礎上進一步拓展,致力於最大限度地提高大模型訓練時的資源利用率和並行效能。
綜上所述,老王透過不斷最佳化流水線排程策略——從最初的 1F1B,到改進後的 ZB1P,再到最新的 DualPipe 技術——正如大語言模型訓練中的不斷演進與突破,每一次創新都在減少“氣泡”對整體效率的影響,推動著系統向更高的效能和更優的資源利用率邁進。
1.6 當流水線依然有“死角”:ZB1P 的侷限性
雖然 ZB1P 的解耦思路有效地縮短了氣泡,但在老王的機床車間中,仍然存在一些“死角”式的空閒時間。
試想一下:在鑄造 – 鍛造 – 熱處理 – 拋光這個工藝路線中,老王的鑄造機床剛完成了某批材料的「夾具設計調整」就將夾具引數交給下一道鍛造工序,但此時後面某道工序的“加工方案調整”卻無法立刻開始,因為它依賴於前一步驟的全部調整完成後才會逐層傳遞下來。
由於零件的流動和多道工序的耦合關係依舊較緊密,一旦中間某道工藝的加工速度出現輕微延遲,就可能再次在流水線中累積出空閒時間,形成新的“氣泡”。
在大模型訓練中,ZB1P 方案雖然將輸入梯度與引數梯度進行了解耦,讓前向與後向的 overlap(重疊)程度比 1F1B 高出了不少,但依舊無法達到“前向與後向完美並行”這一理想狀態。原因在於:
1. 流水線氣泡仍然存在:在以往的實現中,前向和後向是兩個完全分開的階段:先把所有微批資料完成前向,再做後向。這樣一來,後傳階段就會浪費前面機床資源,氣泡現象嚴重。
2. 手工排程複雜:常規的流水線並行實現,需要手動編寫大量的邏輯,比如在第幾個微批的哪個時刻去傳送啟用值?什麼時候再接收梯度?每個階段如果不精心安排,整體就會出現等待或通訊瓶頸。
2. 前後相互擠壓:前向傳播雖然先行一步,但當批次數量非常大、模型層數較多時,後向計算啟動多輪後,可能“擠佔”前向計算所需的硬體資源(例如視訊記憶體、頻寬等);若資源管理不當,也會引發意外的等待和空閒。
3. 缺少前後向交疊:在深度學習裡,如果能讓後向計算和前向計算(針對其他微批次)同時在不同 GPU 上並行,就能極大地提高利用率。然而,手動實現這套邏輯往往很繁瑣。
拿回到老王的機床車間舉例:當鑄造機床與鍛造機床在同一時刻幾乎滿載工作時,熱處理機床或拋光機床一旦因為某個機械故障(類似於 GPU 運算負載不均)而被迫降低產能,那麼整個流水線又會變得“前等後、後等前”。儘管 ZB1P 已經讓排程靈活了不少,但這種多機床、多道工藝序列協作的空閒死角仍然無法完全避免。
於是,老王又琢磨出了更新的升級方案——透過更深層次地“打散”前向與後向的依賴,讓兩者在同一流水線的相鄰節點中能夠充分地相互交錯與重疊。
換句話說,假如能讓各個機床在處理後向時,依然能接收甚至處理下一批次或下一道工序的前向任務,那麼流水線的並行度便能進一步加大,從而將空閒時間壓縮得更小。
基於這種思路,老王開發出了一個在模型訓練中同樣適用的新排程策略,也就是本文的重點——DualPipe。接下來,我們將從高層視角介紹 DualPipe 的核心思想。

雙管齊下的流水線排程:DualPipe
2.1 雙向排程(Bidirectional Scheduling):把“前後”同時推上生產線
在傳統的流水線(即單向流水線)中,如 1F1B 或 ZB1P,一臺機床要麼處於“前向加工”狀態,要麼被動等待上游機床的反饋資訊以進行“後向調整”,二者通常是互斥的。
在 DualPipe 中,老王為機床配備了一個“分時工作模式”和“靈活的前後道資訊傳輸系統”,讓同一臺機床可以同時進行前向和後向的操作:一方面可以執行原材料的加工任務,同時可以接受從後方傳遞過來的質檢報告並對當前站點進行對應的引數調整。
有了這個雙向運輸系統,一個機床在同一時刻可以不斷地接收從上游送來的原材料(對應前向輸入)和從下游傳來的成品反饋(對應後向梯度),真正做到“雙管齊下”。
這麼做有什麼好處呢?DualPipe 的出現,正是為了讓流水線裡的前向與後向能真正做到同時進行,在大語言模型的訓練當中,這麼做可以最大限度地利用 GPU 資源。和傳統的“單向流水線”不同,DualPipe 允許從流水線的兩端同時注入微批次(Micro-batches)。
如果把流水線想象成一個長長的傳送帶,過去我們只能從傳送帶的一頭放入原材料、依次透過多臺機床到達終點;而 DualPipe 則在“傳送帶的左端和右端”同時進行傳輸,讓機床既能對“左端”(上游)送來的原材料進行前向加工,又能在稍後對“右端”(下游)送來的半成品進行後向調整。
這個做法極大地提升了流水線整體的利用率,使得 GPU 既能處理來自“前段”的前向任務,也能接收“後段”的反饋並執行部分後向計算,避免了單向流動帶來的等待。
下面這個表展示了三種流水線排程方案在“氣泡”(Bubble)和啟用資料(Activation)方面的差異,直觀地反映了不同方法在隱藏通訊延遲與排程資源方面的成效:

其中, 表示流水線的深度,即參與計算的階段數; 和 分別代表前向與後向計算所需的時間,而 則是啟用資料累積的視窗大小。
通過對比三個方法,我們發現:
-
1F1B 方法:這種最基礎的單向流水線排程方式,其氣泡數量為 。也就是說,每個階段在前向和後向之間都有固定的等待時間,整個流水線幾乎是嚴格序列的。啟用資料則為 ,說明每個流水線階段僅需儲存一份啟用資料。
-
ZB1P 方法:透過將後向傳播的部分計算(例如輸入梯度與權重梯度的拆分)解耦,ZB1P 成功減少了氣泡數量,將其降低到 。不過,啟用資料的需求並未改變,仍然是 。
-
DualPipe 方法:DualPipe 採用雙向流水線和計算與通訊重疊的策略,將前向和後向同時注入流水線,從而大幅度降低了流水線中的空閒等待(氣泡)。表中顯示,其氣泡數量為 。可以看到,相較於傳統方法,氣泡數明顯減少,但為實現這種重疊排程,啟用資料的儲存需求相應提升到了 。
透過這個表格,我們可以直觀地看到 DualPipe 在排程上所作的最佳化:
-
氣泡減少:雙向排程和通訊重疊使得前向和後向可以並行進行,從而減少了等待時間; -
啟用資料增加:為了支援這種高效的重疊機制,需要在每個 GPU 上額外保留一份啟用資料,從而使得啟用儲存量比傳統方案略有增加。換句話說,DualPipe 是在“犧牲”少量視訊記憶體的前提下,極大地提高了流水線的利用率,使得 GPU 在大規模分散式訓練中能以更高的吞吐率持續運轉。這正如老王在車間裡透過引入雙向運輸系統,不僅提高了機床的利用率,還確保了在高負載下依然能保持穩定高效的生產。

▲ DualPipe 流水線並行方案示意圖
2.2 GPU 裡的“計算-通訊”交錯:實現“邊運邊算”
“分段運輸”是 DualPipe 將 GPU 的運算潛力挖掘到極致的另一大殺器。那麼,為什麼分段運輸這麼重要呢?
在大規模分散式訓練中,“計算”和“通訊”往往是兩大耗時主力。如果它們都按照“整塊、順序”來執行,就會變成:先等通訊把整個資料從上一臺 GPU(或機床)傳完,再進行計算;計算完成後,再一次性把下一批資料傳過來……週而復始。這樣一來,等待是不可避免的:在通訊時計算資源閒置,在計算時網路資源又閒置。
DualPipe 之所以要把大塊通訊“拆分”成若干個小段(我們可以稱之為“小份運輸”),並在每一小段傳輸過程中穿插一部分計算,核心目的就是讓 GPU 不必傻等所有資料都到齊才能開始工作。
下面透過一個更具體的比喻和技術邏輯,來解釋為啥這樣做能“邊運輸、邊加工”,從而減少(甚至隱藏)通訊帶來的空閒時間。
為什麼“分段運輸”更高效
設想老王有一臺“鑄造機床”要把一車零件(比如 1000 個)運送給下一臺“鍛造機床”做下一步加工。如果採用“整批一次性運輸”——也就是等到把全部 1000 個零件搬運過去之後,鍛造機床才能開始幹活。那麼:
-
在運送的那段時間裡,鍛造機床一直處於無事可做(idle)的狀態; -
鑄造機床送完這 1000 個零件後,還要額外等待一下,看看鍛造機床是否能及時消化完; -
如果鍛造機床處理速度比運輸速度快或慢,兩邊都容易形成 “你等我、我等你” 的浪費。
分段運輸的思路:若把這 1000 個零件拆成 4 次或 10 次小批次運輸,只要第一批貨物到達鍛造機床,這臺機床馬上就能動工對前幾百個零件進行鍛造;與此同時,鑄造機床(或搬運車)可以繼續把第二批、第三批、第四批材料在後臺運過來。
-
對鍛造機床來說,它不用等所有材料都送達才能開工;它開始鍛造第一批的同時,後幾批尚在運輸。 -
搬運車(通訊)也不需要等待鍛造機床“空出來”再去送下一批,而是能根據狀態繼續分段運輸。 -
等到鍛造機床處理完第一批零件之際,第二批零件可能也剛好運達,機床可以緊接著鍛造第二批,如此交替推進,幾乎沒有空閒。
核心邏輯是:只要我們把大批次的工作“分塊”(或稱“流水化”),就能讓計算和通訊在時間上相互穿插。通訊並非一次性把所有東西運來,而是一段段地送;計算也不用一直等到全部資料到齊才執行,而是收到一點就先幹一點。雙方像齒輪一樣咬合在一起,從而把可能的等待時間“壓縮”到最小。

▲ DualPipe 流水線並行的通訊方案設計
GPU 裡的“計算-通訊”交錯:如何實現“邊運邊算”
把這個思路放到 GPU(尤其是多 GPU、跨節點)環境下,具體會涉及幾點:
1. 在計算和通訊之間進行資源劃分 GPU 有多個 SM(Streaming Multiprocessors)。當我們做大規模 all-to-all 或者 pipeline 傳輸時,可以讓部分 SM 負責發包/收包(通訊),另一些 SM 或流(Stream)負責進行前向或後向計算。
只要通訊包不是太大,或者被拆分成較多小包,那些專門負責計算的 SM 就可以在“通訊流”幹活時依然執行運算元,不用整塊阻塞。
2. 資料按微批次或更小塊切割:比如有一個模型的若干個層的特徵張量需要傳輸,DualPipe 並不會一次性把這幾個層全部發完再進行運算元計算,而是分成若干小塊(如甚至拆分到某個 attention 模組這種粒度)。
GPU 只要收到第一小塊資料,就可以在計算流中先啟動一部分前向或後向運算。這樣就像“先送兩箱貨,機床馬上開工;後面繼續分批往下送”。
3. 通訊與計算的並行排程:在 PyTorch 裡,cuda 提供多流(multi-stream)非同步執行:發起通訊(P2P ops, all-to-all ops)後,程式無需立刻 wait(),可以讓計算流直接開始幹別的活。
只有在真的需要用到通訊結果的那一刻,才去等待對應的事件。就像老王給搬運車下達運輸命令(非同步通訊)之後,機床就去幹別的事情,直到下一批材料實際用得到的時候,才會等待搬運車把貨送達。
這就是“在運輸過程中穿插加工計算”的本質:發起運輸命令後,立刻讓 GPU 去計算之前已經到達的那部分資料,而不是卡在那邊乾等傳輸結束。
4. 流水線機制:考慮到上邊提到的 DualPipe 額外加入的雙向流水線的思路,讓當前 GPU 既能等待來自“前面的輸出”,也能把後向的梯度送往“前面的 GPU”。在宏觀排程上,多個微批次與多個 GPU 形成一個環環相扣的管線,一邊傳送資料,一邊接受下一部分資料,並行執行。
為什麼能避免閒置?根本是“時間重疊”與“分塊解耦”
最終,我們把“整塊”的通訊和計算分解成多次小規模的交替操作。從時間線上來看,通訊和計算往往呈下列交叉圖案:
時間軸: ==================================>通訊流: [comm chunk1] [comm chunk2] ...計算流: [compute chunk1] [compute chunk2] ...
-
當 comm chunk2 開始傳送時,compute chunk1 可能已經在算前向/後向了;
-
當 compute chunk2 需要資料時, comm chunk2 大機率已快傳完了; -
雙方就像齒輪一樣咬合,不會出現長時間空轉。
如果是大塊整體傳輸,則類似:
(整塊通訊) [comm all data] (等待結束) -> [compute all data]
-
計算必須等通訊完全結束才能開始,通訊完成後又可能很快結束,導致通訊通道閒置;
-
期間 GPU 做不了別的事情——浪費了大量潛在計算時間。
也就是說,透過分塊 + 並行排程,我們讓通訊過程與計算過程在時間線上儘量重疊(overlap),於是之前被浪費的空閒時段被“填補”了。當通訊在進行時,計算也在進行;當計算需要更多資料時,新一批資料往往已經在路上或已送達。
正因為此,DualPipe 在模型規模“爆炸式”增長、並行度加大時,依然能保持較高的吞吐率。因為即使通訊時長在增大,只要管理好分配給通訊的 GPU SM 資源比例,就可以將大部分通訊操作隱藏到併發的計算中。
這與流水線工廠在接單量暴增時,只要車間產線設定得好,運輸環節和工藝環節兩條線“各司其職、相互交錯”,就能穩定地“吃下”更多訂單並保證整體效率。

原始碼如何實現“雙管齊下”:DualPipe 關鍵邏輯解析
在上一個章節裡,我們用機械加工的類比方式,說明了 DualPipe 如何讓前向(Forward)和後向(Backward)真正地在同一個流水線上“同時上陣”,以最大化地利用 GPU 資源。
下面,我們來看看 DualPipe 的核心原始碼如何將這一思路落到實處,並透過拆分批次、管理通訊、以及巧妙呼叫“前向-後向”協同來實現高度的重疊和極小化的流水線氣泡。
3.1 類的基本結構與初始化
class DualPipe(nn.Module): def __init__( self, modules: Tuple[nn.Module, nn.Module], ... ) -> None: super().__init__() ... self.module = nn.ModuleList(modules) self.overlaped_forward_backward = ... ...
-
modules:這裡傳入的是兩個 nn.Module 的元組,通常代表流水線的“前半段”和“後半段”。想象一下,在機械加工的流水線裡,這可能對應“前向加工”的組合機床(如鑄造+鍛造)和“後向調整”的另一個組合機床(如質量檢測+引數調整)。
-
overlaped_forward_backward:用於判斷這兩個 Module 是否支援前後向重疊的專用函式。只有當傳入的 Module 有 overlaped_forward_backward 這個方法(且型別相同),才會在後續流程裡真正地進行前後向同一批次的緊密交織。
與此同時,程式碼中還設定了一系列與分散式訓練流程相關的引數和標記,如:
-
self.group 和 self.rank:與 torch.distributed 相關,用於管理當前節點在流水線中的PP Rank(哪個階段)。 -
self.is_first_rank, self.is_last_rank, self.is_in_second_half:標記當前節點(機床)在“流水線”中的位置,是在“左端”還是在“右端”、是前半段還是後半段。
這些標誌位和對映表,類似於老王在車間裡給每臺機床貼上的“這是鑄造線”“這是拋光線”“這是倒數第二工序”等標籤。
3.2 狀態管理與重置
_reset_states 方法
def _reset_states(self) -> None: WeightGradStore.clear() self.input_chunks = ([], []) self.output_chunks = ([], []) self.input_grad_chunks = ([], []) self.output_grad_chunks = ([], []) self.labels = None self.loss_chunks = [] self.criterion = None ...# 各種計數器,用於追蹤當前處理到第幾個 chunk self.current_f_chunk_id = [0, 0] self.current_b_chunk_id = [0, 0] ... self.comm_ops = [] self.to_free = []
-
_reset_states 就像每次老王接到新訂單之前都會清空生產線、重新擺放刀具和記錄簿一樣:
-
WeightGradStore.clear():將之前儲存的需要延遲執行的“引數梯度計算”回撥函式全清空,為後續的 Zero-Bubble 或部分重疊做準備。 -
input_chunks, output_chunks, input_grad_chunks, output_grad_chunks:相當於流水線裡的“在製品”和它們的“梯度資訊”。初始化為空的列表是為了逐步把每一批(micro-batch)塞進來再往後傳。 -
一系列計數器:用來跟蹤當前處理到第幾個前向批次、第幾個後向批次,或者是已經發送/接收了多少次資料等。
3.3 前向與後向計算:如何在同一裝置上實現交疊
在大模型訓練裡,“前向計算”與“後向計算”的核心邏輯大多是對張量執行一次前傳或後傳,然後將梯度往上一層傳遞。對於機械類比而言,前向就像“對零件進行加工”,後向就像“對零件的質量缺陷進行追溯並調整生產引數”。DualPipe 裡分別封裝了下面這些方法來完成這個過程:
3.3.1 _forward_compute_chunk(self, phase)
def _forward_compute_chunk(self, phase: int) -> None: phase ^= self.is_in_second_half # 動態修正 phase chunk_id = self.current_f_chunk_id[phase] self.current_f_chunk_id[phase] += 1 inputs = self.input_chunks[phase][chunk_id] ... outputs = self.module[phase](*inputs) ...
-
這裡先根據 phase 計算出當前實際使用的是哪個模組(如前段模組 or 後段模組)。然後從 input_chunks[phase] 中取出對應批次的資料做前向計算。
-
outputs 最終被存進 self.output_chunks[phase]` 中。 -
如果這是最後一個階段(is_last_stage)並且設定了 criterion,就把損失(Loss)放進 self.loss_chunks 中。想象一下,當車間裡把一批零件送到“最後工序”——如果這是老王最信任的質檢環節,就同時得出“損失分數”以便後續調整。
3.3.2 _backward_compute_chunk(self, phase, enable_zb: bool = False)
def _backward_compute_chunk(self, phase: int, enable_zb: bool = False) -> None:if self.forward_only:return phase ^= self.is_in_second_half chunk_id = self.current_b_chunk_id[phase] ...if is_last_stage:# 在最後一段,直接對 loss 進行 backward loss = self.loss_chunks[chunk_id] loss.backward() loss.detach_()else:# 對輸出和輸出梯度執行 run_backward outputs = self.output_chunks[phase][chunk_id] ... run_backward(outputs, output_grads) ...if enable_zb: WeightGradStore.flush()# 更新 input_grads ...
-
對於“後向階段”,最重要的是:如果在最末尾的階段,就直接對 loss 呼叫 backward();否則對中間層的 outputs 呼叫 run_backward,並把梯度傳回上一層。
-
enable_zb 用於啟動 Zero-Bubble(如 ZB1P)策略,即將一些引數梯度的計算快取在 WeightGradStore 裡,並在合適的時機 flush()。這部分正好與我們前面講的分離“輸入梯度計算”和“權重梯度計算”相呼應。 -
當後向回傳完畢,拿到上一層(或上一工序)的梯度,就把它存到 self.input_grad_chunks[phase] 裡——類似於老王在質量檢驗後,把修改意見“傳回”上一個機床
3.3.3 _forward_backward_compute_chunk(self, phase0, phase1)
def _forward_backward_compute_chunk(self, phase0: int, phase1: int) -> None:if self.forward_only: self._forward_compute_chunk(phase0)returnif not self.overlaped_forward_backward: self._forward_compute_chunk(phase0) self._backward_compute_chunk(phase1)return# 1) pre-forward# 2) pre-backward# 3) overlaped_forward_backward(...)# 4) post-forward# 5) post-backward
這是 DualPipe 裡最核心的部分:如果 overlapped_forward_backward 為 True,就代表同一個 GPU 支援把前向和後向融合在一起的一種特殊方法(類似“左右手一起做事”)。
-
函數里先從 input_chunks 拿到本批次前向需要的資料,再從 output_chunks 中拿到後向需要的資料,然後呼叫 module0.overlaped_forward_backward(…)`。 -
這一步好比在車間裡,工人先準備好要加工的新零件,同時也把上一批零件的質檢報告和所需的調整一併拿來,然後運用同一個機床的“聯合加工功能”去完成“前向+後向”的混合操作。 -
最後把新的輸出以及梯度分別存回 output_chunks 和 input_grad_chunks。這樣就能在同一階段內,完成對前向與後向的部分操作,而無需像 ZB1P 那樣分兩次呼叫或等待另外的階段。
3.4 通訊管理:讓“運輸時間”隱藏在計算中
在整個大模型流水線並行中,每個階段的 GPU 都需要頻繁地與前一個/後一個 GPU 通訊(如在車間裡把半成品從鑄造機床運給鍛造機床)。DualPipe 透過以下幾個函式對通訊做了拆分和排程,讓計算過程與通訊過程能夠大幅重疊:
-
_recv_forward(self, phase) / _send_forward(self, phase) 用於接收/傳送“前向輸出”或“前向輸入”。在車間比喻中,這相當於把加工好的半成品移交到下一臺機床。 -
_recv_backward(self, phase) / _send_backward(self, phase) 用於接收/傳送“後向梯度”或“後向輸入”。就像檢驗報告在車間間的回傳。 -
_commit_and_wait_comm(self) 先透過 dist.batch_isend_irecv(self.comm_ops) 一次性把所有的非阻塞通訊發出去,然後 wait()。這意味著通訊和計算可以在不同時間片並行,只有當我們真的需要資料時才會等待。這就把運輸時間“隱藏”在機床空閒或機床可以“分配給傳輸”的那部分時間裡了。
3.5 WeightGradStore:延遲梯度更新設計
在後向傳播時,我們可能會多次對同樣的權重做梯度累加。WeightGradStore 透過一個靜態佇列快取了這些更新操作,只有在需要時 pop() 統一執行,有兩大好處:
-
減少頻繁同步或寫記憶體:不必每個微批都做一次引數更新,可以攢到一個合適的時間點統一處理。 -
搭配流水線:避免打斷流水線的併發計算,也方便在某些階段利用通訊空閒時再同步。
class WeightGradStore: enabled: bool = False cache: List[Callable] = [] funcs_queue = queue.Queue() @classmethod def put(cls, func: Callable) -> None: cls.cache.append(func) @classmethod def flush(cls) -> None: cls.funcs_queue.put(cls.cache) cls.cache = [] @classmethod def pop(cls) -> None: funcs = cls.funcs_queue.get()for func in funcs: func()
注意:原始碼裡 phase ^= self.is_in_second_half 是一個巧妙的小技巧,根據當前 PP Rank 是否在後半段來“翻轉” phase,讓同一個函式既能適用於從左到右又能適用於從右到左的傳輸。
3.6 整體排程:step(…) 方法中的 8 大步驟
最核心的應用邏輯在 step(…) 裡。這函式就像老王給所有機床派發指令的“總排程”——為了實現 DualPipe 的雙向流水線,需要做以下階段性動作(簡化理解,可結合原始碼的註釋):
-
Step 1:nF0 在流水線開始時,讓一端的機床率先處理一定數量的前向批次。這就像在傳送帶還沒正式轉動前,前面幾道工序先把一部分原材料“初加工”起來,後面工序暫時閒置。 -
Step 2:nF0F1 逐步讓另一端也開始前向操作,使兩個方向都被啟用,透過 _forward_chunk(0) 和 _forward_chunk(1) 的交替來實現雙向注入。 -
Step 3:nB1W1F1 在某些批次開始出現後向時,先將後向(_backward_chunk(1))和前向(_forward_chunk(1))混合進行,並呼叫 _weight_chunk() 來執行延遲的權重更新操作(ZeroBubble 相關)。 -
Step 4:nF0B1F1B0(主迴圈) DualPipe 透過 _forward_backward_chunk(0,1) / _forward_backward_chunk(1,0) 實現對前向和後向的同步排程,也就是最主要的“互相交織”步驟。 -
Step 5:nB1F1B0 後向與前向繼續往返推進,做進一步交錯。 -
Step 6:nB1B0 更集中地執行後向,讓之前拆分在 WeightGradStore 的梯度處理也得以不斷重新整理。 -
Step 7:nWB0 繼續執行“權重更新 + 後向計算”。 -
Step 8:nW 收尾階段的權重更新,確保所有操作都順利結束。
在車間的類比裡,這就像老王的“排程表”分成多個時段:先讓左側機床做一波初加工,等到一定時間後,右側機床也開始送入原材料;兩邊持續地傳送/接收半成品、檢驗報告,並在最後進行一些統一的調整或收尾。
3.7 程式碼總結
從 step(…) 的排程流程到 _forward_backward_compute_chunk(…) 的“前後向合體”,我們可以看到 DualPipe 利用了大量細粒度的分段和雙向注入策略,在程式碼層面極大地隱藏了通訊成本,也允許同一個 GPU 在合適的時機一邊做前向,一邊做後向。
這樣做帶來的好處是顯而易見的:
-
流水線氣泡最小:DualPipe 真正實現了“前後向同時上陣”,降低了序列等待。 -
通訊被大幅重疊:藉助 _commit_and_wait_comm() 的非同步傳送/接收,很多跨節點的 all-to-all 或 pipeline 通訊在 GPU 計算“間隙”裡就完成了。 -
可擴充套件性:即便分散式訓練規模變大(更多 GPU、更多節點),只要保持一定計算-通訊比例,就能繼續讓這種“重疊”有效發揮。 -
視訊記憶體最佳化:把最淺層與最深層放在同一個 PP Rank 並做必要的 Zero-Bubble,減少中間儲存浪費。
這樣的 “雙管齊下” 方案大大加速了像 MoE 這種需要大量跨節點 Expert 並行通訊的大模型訓練,讓在有限 GPU 資源(例如 H800)上也能跑起 1000 億級別的網路成為可能。

總結與展望
從機床組加工零件的直觀場景引申到大語言模型並行訓練,我們先後介紹了無並行、模型並行、資料並行、流水線並行這幾個基礎策略。在流水線並行中,難點往往是減少“流水線氣泡”、壓縮通訊等待、使前後向交疊執行。
DualPipe 就是一種高階技術封裝,透過對微批次以及前向-後向時序的巧妙排程,實現了零氣泡的理想狀態或者接近理想的狀態,極大地提升了流水線並行的訓練速度與資源利用率。
綜上所述,DualPipe 不僅在概念層面做到了前後向交疊,更在工程實現上封裝了通訊與排程細節,讓使用者能夠更輕鬆地進行復雜的流水線並行訓練。對於想要深入大模型分散式訓練細節的從業者或研究者來說,研讀其原始碼並結合實踐,對開發更高效的並行方案大有裨益。
一句話總結:像流水線裝配車間一樣,DualPipe 讓大模型的前向、後向加工同步流動,極大地提升了多卡並行效率,為大模型時代奠定了重要的技術基石。
更多閱讀

#投 稿 通 道#
讓你的文字被更多人看到
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋樑,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學術熱點剖析、科研心得或競賽經驗講解等。我們的目的只有一個,讓知識真正流動起來。
• 文章確係個人原創作品,未曾在公開渠道發表,如為其他平臺已發表或待發表的文章,請明確標註
• 稿件建議以 markdown 格式撰寫,文中配圖以附件形式傳送,要求圖片清晰,無版權問題
• PaperWeekly 尊重原作者署名權,並將為每篇被採納的原創首發稿件,提供業內具有競爭力稿酬,具體依據文章閱讀量和文章質量階梯制結算
• 投稿郵箱:[email protected]
• 來稿請備註即時聯絡方式(微信),以便我們在稿件選用的第一時間聯絡作者
• 您也可以直接新增小編微信(pwbot02)快速投稿,備註:姓名-投稿

△長按新增PaperWeekly小編
現在,在「知乎」也能找到我們了
進入知乎首頁搜尋「PaperWeekly」
點選「關注」訂閱我們的專欄吧
·
·
·
