單機多卡訓練調(diào)優(yōu):破解“8卡7閑”的GPU利用率瓶頸 發(fā)布時間:2026/9/11 9:25:07 建盈捌捌建站 我們做 AI Infra 的朋友可能都有過這種體驗明明買的是 8 卡機器一看nvidia-smi只有 0 號卡在忙剩下 7 張卡 GPU-Util 是 0%。這時候很多人第一反應是“是不是我沒配好分布式框架”“是不是該上 Kubernetes 或者 Slurm 了”。但我想說一句可能不太中聽的話大概率不是集群調(diào)度的問題而是你單機內(nèi)部就已經(jīng)在“空轉(zhuǎn)”了。這就像一個人連自己手頭的算力都沒理順就急著去搞“全局統(tǒng)籌”那調(diào)度器再智能也救不了你。這篇是 AI Infra 系列里訓練與調(diào)度章節(jié)的上半部分聚焦一個非常不起眼、但一旦出問題就極其致命的話題單機多卡訓練調(diào)優(yōu)。我會從自己實際調(diào)過的訓練任務出發(fā)講清楚“8 張卡 7 張閑”背后的三種典型瓶頸給出可操作的觀測手段和參數(shù)調(diào)整方案也會附上完整的排查流程和踩坑記錄。如果你正在做大模型微調(diào)、LoRA 訓練、YOLO 系列目標檢測訓練或任何跑在單機多卡上的深度學習任務這篇文章應該能幫你省下好幾個星期的無效加班。1. “8 張卡 7 張閑”先把問題看透很多人一看到 7 張卡閑著就本能地把問題歸到“并行策略”上是不是沒裝 NCCL是不是 DDP 沒寫對是不是網(wǎng)卡沒起來實際上在我接觸的大量訓練任務里單機多卡利用率低的根因根本不在“多卡通信”那一層而是單卡本身就沒吃飽。1.1 這不是調(diào)度器的鍋很可能是你親手埋的單機雷我們先把概念理清楚。集群調(diào)度器比如 Kubernetes、Slurm管的是“哪臺機器上跑哪個任務”它解決的是多任務、多用戶、多節(jié)點之間的資源分配。而單機上的 GPU 利用率由你的訓練進程內(nèi)部決定數(shù)據(jù)能不能喂夠、計算內(nèi)核有沒有被正確啟動、梯度同步會不會互相等待。有個很形象的類比你把一棟樓的幾十間房分給幾十戶人住這是物業(yè)調(diào)度器的職責但每一戶人家里水管通不通、電閘有沒有跳那得你自己修。8 卡機器 7 張卡閑著就好比 7 戶鄰居家的水龍頭一打開根本沒水物業(yè)再公平也沒用。在我排過的問題里出現(xiàn)“8 卡 7 閑”最常見的原因其實是這幾個代碼里把模型放在了顯存最大的 0 號卡其他卡的模型雖然創(chuàng)建了但因為數(shù)據(jù)加載線程卡死根本沒有有效的計算任務喂過去。訓練循環(huán)里用了同步操作比如每個 step 都調(diào)用一次torch.cuda.synchronize()導致 7 張卡都在等某一張卡完成。DataLoader 的num_workers設置過低CPU 根本來不及把數(shù)據(jù)搬運過來GPU 只能干等著。單機內(nèi) PCIe 帶寬不夠數(shù)據(jù)在 CPU 和 GPU 之間搬運的時間比 GPU 計算時間還長表現(xiàn)為 GPU 一會兒忙一會兒停。這些問題的共同點是你從表面上看是“GPU 利用率低”但根子全在單機訓練鏈路的下游。不等你理清這一層上什么調(diào)度器都是白搭。1.2 三種最常見的“卡閑”剖面我做了一個簡單的分類法方便大家對照自己遇到的情況。每當有人截圖給我說“8 張卡 7 張閑”我都會先問一句那個“閑”是持續(xù)性的還是周期性脈沖這會直接影響排查方向。第一類是持續(xù)饑餓型。GPU-Util 始終很低比如一直 0% 或者個位數(shù)。這種最常見的原因就是數(shù)據(jù)供給斷了CPU 預處理跟不上GPU 每個 step 大部分時間都在空轉(zhuǎn)等數(shù)據(jù)。你可以通過nvidia-smi反復看會發(fā)現(xiàn) GPU 顯存占用正常但利用率極低。這種時候要查 DataLoader而不是查網(wǎng)絡。第二類是周期性打嗝型。GPU-Util 在 0% 和 90% 之間來回跳像心電圖一樣。這種通常發(fā)生在每個 step 結(jié)束后的梯度同步階段8 張卡算完各自的梯度后要等所有卡都算完再做 all-reduce。如果各卡的計算負載不均衡或者通信鏈路有瓶頸就會表現(xiàn)為每分鐘出現(xiàn)若干次“利用率塌下去再彈回來”。第三類是靜默啞火型。顯存占用很高但利用率也是 0%看起來像卡住了。這種情況往往是顯存碎片化、OOM 后重試或者代碼里某個分支邏輯錯誤導致根本沒有調(diào)度內(nèi)核。有時也和 NCCL 超時、通信組初始化卡住有關(guān)。這種最容易誤判為“多卡通信問題”其實 8 張卡連數(shù)據(jù)都沒開始算。你只有先看清楚當前屬于哪一類后續(xù)的優(yōu)化動作才有針對性。我見過有人花了一整周去調(diào) NCCL 參數(shù)結(jié)果最后發(fā)現(xiàn)是num_workers0導致 CPU 只用一個線程在跑數(shù)據(jù)加載真是典型的“方向錯了越努力越尷尬”。2. 觀測先行先量化再動手很多人在調(diào)優(yōu)時有個壞習慣憑感覺。感覺 GPU 利用率低就隨手把batch_size調(diào)大感覺數(shù)據(jù)加載慢就把num_workers從 4 改成 8。這種“盲調(diào)”偶爾能碰對但大多數(shù)時候會引入新問題而且你根本說不清楚優(yōu)化到底有沒有效果。2.1 五類必看的指標我在實際調(diào)優(yōu)時會固定收集五類指標。第一類是 GPU 利用率與顯存用nvidia-smi或nvidia-smi dmon看每張卡的GPU-Util、memory-used和溫度。第二類是 CPU 與內(nèi)存用top或htop看 CPU 核心占用率和內(nèi)存剩余量CPU 占用率如果長時間超過 90%大概率是預處理瓶頸。第三類是數(shù)據(jù)加載時延在 DataLoader 的__getitem__里加時間戳或直接用 PyTorch 自帶的 Profiler 看DataLoader的耗時占比。第四類是 GPU 內(nèi)核時間分布用nsys采集看計算內(nèi)核、內(nèi)存拷貝、通信分別占了多少。第五類是網(wǎng)絡與通信時間在 DDP 訓練腳本里記錄每個 step 的總耗時和 all-reduce 耗時。這五類指標不需要全部常駐收集但它們組合起來能幫你快速畫出整個數(shù)據(jù)流的水位圖。以我個人的經(jīng)驗很多問題的答案在指標觀測階段就已經(jīng)浮出水面了根本不需要真正動手改代碼。2.2 一套可以直接照搬的排查腳本和工具鏈如果你手頭條件比較緊張不想一上來就上重型 Profiler可以先從幾條命令行開始。我自己最常用的組合是這樣的# 實時看 GPU 利用率和顯存 watch -n 1 nvidia-smi # 看每張卡的指標變化采樣間隔 1 秒 nvidia-smi dmon -s pucvmet -d 1 # 看 CPU 每個核心的使用率 htop # 定位 Python 進程里哪個線程在占用 CPU py-spy top --pid 訓練進程 pid # 更細粒度看 CPU 上的函數(shù)調(diào)用棧 py-spy dump --pid 訓練進程 pid我特別想推薦py-spy這個工具。它不需要重啟訓練進程就能把 Python 側(cè)當前所有線程的調(diào)用棧打出來。當 GPU 利用率掉到 0% 而你懷疑是數(shù)據(jù)加載線程卡住時直接py-spy dump往往能一眼看到線程卡在PIL.Image.open還是cv2.imdecode省去了到處打日志的痛苦。在深入優(yōu)化之前我還會用 PyTorch Profiler 做一次完整采集。它的寫法非常簡單能直接給出每個算子和 DataLoader 的耗時占比from torch.profiler import profile, ProfilerActivity with profile( activities[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapesTrue, profile_memoryTrue, ) as prof: for step, batch in enumerate(train_loader): # 正常訓練邏輯 ... if step 50: break print(prof.key_averages().table( sort_bycuda_time_total, row_limit20 ))這里要注意Profiler 本身會帶來額外開銷別在生產(chǎn)訓練里一直開。一般我是先跑五六十個 step采集一輪記錄基線然后關(guān)掉 Profiler 再跑正式訓練。觀測這件事核心目的不是好看的圖表而是幫你把“感覺慢”變成“具體哪里慢”。量化永遠先于優(yōu)化這句話在 AI Infra 里怎么強調(diào)都不過分。3. 數(shù)據(jù)供給瓶頸優(yōu)化 DataLoader 的關(guān)鍵參數(shù)排在我碰到過的問題榜首的永遠是數(shù)據(jù)供給。你要知道GPU 的計算速度是以“每秒多少億次浮點運算”計的而 CPU 讀圖、解碼、縮放、歸一化用的是每秒多少 MB 的 I/O 和單核指令。兩者之間天然存在掉鏈子的可能。3.1 四個參數(shù)和背后的數(shù)學PyTorch 的 DataLoader 是我調(diào)優(yōu)的重災區(qū)。很多人的默認寫法是DataLoader(dataset, batch_size64, shuffleTrue)這一行代碼等于在告訴 PyTorch請用主線程、不預取任何數(shù)據(jù)、不要把數(shù)據(jù)鎖在頁鎖定內(nèi)存里。然后就奢望 4 張甚至 8 張 GPU 能跑滿。要優(yōu)化先得理解四個關(guān)鍵參數(shù)。第一個是num_workers。它決定起多少個 worker 子進程來做數(shù)據(jù)預處理。注意是把數(shù)據(jù)集讀到一個 Python 列表里再shuffle還是直接訪問磁盤文件會造成巨大差異。一般經(jīng)驗是設為 CPU 核心數(shù)的一半到三分之二給訓練主進程留點余量。第二個是prefetch_factor它控制每個 worker 在內(nèi)存里預取多少個 batch 的數(shù)據(jù)。默認是 2如果訓練腳本對數(shù)據(jù)順序不太敏感可以適當調(diào)大比如 4 甚至 8。第三個是pin_memoryTrue它把數(shù)據(jù)分配到頁鎖定內(nèi)存page-locked memory里GPU 從這種內(nèi)存拷貝數(shù)據(jù)走的是高速通道能減少一部分 H2D 拷貝開銷。第四個是persistent_workersTrue意思是訓練完一個 epoch 后 worker 不銷毀重建省掉頻繁初始化子進程的損耗。這中間還有一層數(shù)學關(guān)系。假設單個 batch 在 CPU 上的預處理時間是t_prepGPU 上一個 step 的計算時間是t_train。如果你想 GPU 不閑等就必須滿足num_workers * (單 worker 每 batch 時間) * prefetch_factor t_train更通俗地說CPU 每生產(chǎn)一個 batch 的時間必須小于等于 GPU 每消費一個 batch 的時間。如果 CPU 側(cè)生產(chǎn)一個 batch 要 20msGPU 算一個 batch 只要 5ms那不管你num_workers開多少如果 Python GIL 或磁盤 I/O 成為上限GPU 也得原地等 15ms。所以關(guān)鍵不是某個參數(shù)開多大而是讓整條流水線的瓶頸盡量往 GPU 側(cè)移動。3.2 訓練腳本里直接能用的配置基于上面的數(shù)學關(guān)系我一般會把 DataLoader 寫成這樣作為多卡訓練里的初始配置from torch.utils.data import DataLoader train_loader DataLoader( dataset, batch_size64, shuffleTrue, num_workers8, prefetch_factor4, persistent_workersTrue, pin_memoryTrue, drop_lastTrue, )這里drop_lastTrue也是我比較偏愛的選項。多卡訓練時如果最后一個 batch 不夠分配DDP 會報錯與其處理那些邊界情況不如直接丟掉不足整數(shù)倍的批次。當然如果你的數(shù)據(jù)集非常小丟掉可能會影響訓練效果這種情況下你可以用torch.utils.data.distributed.DistributedSampler自己控制補全邏輯而不是簡單粗暴地drop_last。我特別想強調(diào)一點num_workers不是越大越好。因為每個 worker 都是獨立進程它們之間如果需要共享內(nèi)存比如共享某個查找表可能會引發(fā)大量拷貝和序列化開銷。另外當 worker 數(shù)量超過 CPU 物理核心數(shù)后操作系統(tǒng)會做大量上下文切換反而拖慢整體速度。我見過有人把num_workers設成 128結(jié)果 CPU 被打滿數(shù)據(jù)加載時間比原來還長。3.3 數(shù)據(jù)預處理優(yōu)化的三個方向如果調(diào)完 DataLoader 參數(shù)后CPU 還是忙不過來那就得往數(shù)據(jù)預處理本身動刀了。我按性價比從高到低排一下。第一個方向是做緩存。如果你的數(shù)據(jù)不是無限流而是固定的訓練集可以提前把所有樣本做完 decode、resize、歸一化之后以 tensor 形式緩存到內(nèi)存或本地磁盤。這樣訓練時少了解碼和預處理CPU 壓力會小很多。缺點是占用大量內(nèi)存適合單機顯存比較大而 CPU 比較弱的場景。第二個方向是換數(shù)據(jù)格式。比如把幾萬張小圖片打包成一個 TFRecord 或者 WebDataset 格式的文件利用順序讀減少隨機 I/O 的尋址開銷。尤其是在機械硬盤或者網(wǎng)絡文件系統(tǒng)上這種優(yōu)化收益非常明顯。我在一個目標檢測項目里把 JPEG 文件從散裝改成 TFRecord訓練吞吐直接提升了 30% 以上。第三個方向是異步卸載。把預處理放到 GPU 上做比如用 DALI 這類庫讓 GPU 直接讀圖片、解碼、做增強。這看起來有點“奢侈”但 GPU 大部分時間都在閑著等數(shù)據(jù)你無非是把它的空閑時間利用起來而已。DALI 的上手成本略高但對視頻、圖像解碼這類重負載任務效果是立竿見影的。這些都是我在單機調(diào)優(yōu)中真正用過的方案沒有哪一個是銀彈關(guān)鍵還是要看你卡在哪一層。4. 通信與混合精度讓 8 張卡真正并行起來數(shù)據(jù)供給問題解決之后GPU 利用率通常會好很多但“8 卡 7 閑”還有一個常見因素就是多卡之間的通信等待。這種問題在單機場景下也經(jīng)常出現(xiàn)因為即便是在同一臺機器里PCIe 帶寬和 NVLink 拓撲也會明顯影響通信開銷。4.1 all-reduce 開銷如何影響全局等待分布式訓練里最核心的通信原語是 all-reduce每張卡算完自己的梯度之后需要把所有卡的梯度加總再廣播回每一張卡。在 DDP 中這個操作在每個 step 結(jié)束都會發(fā)生。如果通信鏈路比較慢或者梯度張量特別大那么整個訓練就會被卡在“等同步”上。那要怎么判斷是不是通信瓶頸簡單方法是在代碼里手動記錄每個 step 的總耗時和只計算梯度、不做通信的耗時import time train_loader iter(train_loader) batch next(train_loader) t0 time.time() output model(batch) loss loss_fn(output, target) loss.backward() # 手動調(diào)用 all-reduce 前的同步 torch.cuda.synchronize() t_compute time.time() - t0 t1 time.time() # DDP 內(nèi)部的梯度 all-reduce 在這里進行 optimizer.step() torch.cuda.synchronize() t_comm time.time() - t1 print(fcompute: {t_compute:.3f}s, comm: {t_comm:.3f}s)如果t_comm占比超過 30%通信就是你要優(yōu)先優(yōu)化的問題。這個時候我們有幾個可以操作的杠桿。第一個杠桿是減少通信頻率。梯度累積gradient accumulation可以讓多個 step 的梯度先累計再統(tǒng)一做一次 all-reduce。比如原來是每 step 同步一次改成每 4 步同步一次通信次數(shù)就直接降到原來的 1/4。代價是顯存不變的情況下等效 batch size 變大了可能會影響收斂。后面我會詳細算這筆賬。第二個杠桿是使用梯度壓縮或者稀疏化。對某些模型來說梯度變化很小的參數(shù)可以延遲同步或者只同步部分重要梯度。這種做法在訓練初期通常有效但實現(xiàn)復雜度高需要謹慎調(diào)參。第三個杠桿是檢查拓撲。nvidia-smi topo -m可以查看 GPU 之間的連接方式。如果是 PCIe 互聯(lián)通信帶寬一般沒法和 NVLink 相比。如果 8 張卡里有部分卡是通過 PCIe 交換機跨接通信開銷會很高這時你可以通過環(huán)境變量NCCL_P2P_DISABLE1或NCCL_SOCKET_IFNAME等參數(shù)做實驗看哪種配置實際最快。不要盲信默認值很多廠家給的默認 NCCL 配置并不適合你的具體場景。4.2 混合精度訓練性能與收益的權(quán)衡聊完通信再聊聊另一種極其常見的情況顯存足夠但 GPU 利用率依然不高原因可能是你沒有開啟混合精度訓練AMP。混合精度訓練的核心思路是用 FP16 做前向和反向計算用 FP32 保存主權(quán)重和累加梯度。這樣既能把計算速度提上去又能把顯存占用降下來。很多人以為 AMP 只是省顯存其實它對計算速度的提升也不可忽視。特別是 Ampere 及更新的 GPU 架構(gòu)FP16 的吞吐往往是 FP32 的兩倍甚至更多。在大模型訓練里開啟 AMP 后8 張卡的利用率可能會有明顯改善因為每個 step 的計算時間變短了流水線更容易跑滿。但這里有個大坑FP16 的數(shù)值范圍比較窄很容易出現(xiàn)梯度下溢或溢出導致訓練不穩(wěn)定。所以 PyTorch 的torch.cuda.amp里默認用了動態(tài)損失縮放dynamic loss scaling核心是先用一個縮放因子放大損失反向傳播后再縮小梯度。這個過程是自動的但如果你用了自定義優(yōu)化器或者自定義損失函數(shù)有可能會干擾它的縮放邏輯。我的建議是盡量使用 PyTorch 官方支持的GradScaler不要省事手動去對梯度做縮放。另一個細節(jié)是混合精度對某些算子特別不友好比如 LayerNorm、Softmax 等它們在 FP16 下的精度損失會被放大。PyTorch 的 autocast 會自動對這類算子保持 FP32 精度所以你不必太過擔心但如果是自定義算子就要小心了。從我實測的經(jīng)驗看開啟 AMP 往往是一步“低垂的果實”改動少提升明顯。如果你目前還沒有做我建議先把這一步補上再考慮更復雜的優(yōu)化策略。4.3 梯度累積和 batch size 的數(shù)學關(guān)系梯度累積是一個非常實用的手段適合在顯存不足、又想把 batch size 變大的場景。簡單說就是用多個小 batch 的梯度累加代替一個大批次的梯度。假設你顯存只夠放下batch_size16但你想用等效batch_size64的效果那就把累積步數(shù)設為 4。這個過程要注意兩點。第一optimizer.zero_grad()只應該在累積結(jié)束時調(diào)用一次第二loss.backward()每次都會累加梯度所以累積步數(shù)越多梯度值就越大需要配合損失縮放來平衡。在實際代碼里它長這樣accum_steps 4 for step, batch in enumerate(train_loader): loss model(batch) / accum_steps # 先把 loss 縮小 loss.backward() if (step 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()這里把 loss 除以accum_steps是為了保證累積后的梯度量級和真正的大 batch 一致。你可能覺得這是小細節(jié)但如果你不除瞬間的梯度范數(shù)就會變大造成優(yōu)化器動一步過大很容易訓飛。梯度累積還直接影響通信頻率如果配合 DDP你每累積 N 個 step 才做一次 all-reduce通信開銷能顯著下降。我遇到過的一個案例是在 8 卡單機上訓練一個中等規(guī)模的檢測模型原來是每個 step 同步一次通信耗時占比接近 40%。改成accum_steps4后通信占比降到 15% 以內(nèi)整體吞吐提升了近 20%。代價是梯度更新頻率變低模型收斂變慢但在很多任務上這種取舍是劃算的。需要注意的是如果你用的是 PyTorch 2.0 之后的版本它引入了torch.compile和圖優(yōu)化這也會與梯度累積產(chǎn)生一些交互。比如某些融合內(nèi)核在累積模式下沒法被完全緩存反而會變慢。所以我每次調(diào)完參數(shù)后都會用 Profiler 重新測一遍而不是直接套理論。5. 單機調(diào)優(yōu)實戰(zhàn)一個完整案例前面講了不少原理和參數(shù)這章我?guī)Т蠹彝暾咭槐槲以趯嶋H項目中做單機調(diào)優(yōu)的流程。案例背景很簡單一臺 8 卡 A100 的機器跑一個多卡微調(diào)任務數(shù)據(jù)是幾萬張圖片模型是一個 base 級別的視覺模型。初始狀態(tài)是GPU-Util 大部分時間在 15%-25% 之間徘徊顯存占用到是挺滿但就是算得慢loss 下降也慢。5.1 初始狀態(tài)與瓶頸定位我先用nvidia-smi dmon和py-spy做了一次快照發(fā)現(xiàn)每張卡都存在明顯的周期性利用率塌陷每過幾秒鐘GPU-Util 會從 80% 掉到 0%持續(xù)幾百毫秒再彈回來。這個脈沖形態(tài)非常典型說明問題大概率不在計算本身而在數(shù)據(jù)供給或通信同步上。接著我用 PyTorch Profiler 采集了 50 個 step表格里最刺眼的幾行是 DataLoader 的耗時占了整個 step 的 60% 以上。具體到函數(shù)層面cv2.imdecode和PIL.Image.resize各吃掉了一大半 CPU 時間。然后我又看了一眼 CPU 占用8 個物理核心全部被打滿。哦原來這個任務根本沒開多進程數(shù)據(jù)加載就是單線程在那兒一張一張讀圖。怪不得 GPU 利用率上不去CPU 每生產(chǎn)一個 batch 的時間比 GPU 算完一個 batch 的時間長好幾倍GPU 只能干等。5.2 一步步優(yōu)化與效果對比我做的第一步優(yōu)化很簡單重寫 DataLoader把num_workers設為 12、prefetch_factor4、pin_memoryTrue、persistent_workersTrue。這一個改動之后GPU-Util 從 20% 提到了 60% 左右整個訓練時長縮短到原來的二分之一到三分之一。但我還不滿足因為 CPU 依然接近滿負荷說明它已經(jīng)成為新的瓶頸只是比原來緩解了。第二步優(yōu)化是針對圖像預處理。因為這些圖片都是固定尺寸的小圖不需要在每次加載時做隨機裁縮放了的。我把所有樣本在訓練開始前做了預處理先 decode 和 resize 到統(tǒng)一尺寸再緩存為一個內(nèi)存張量列表。這樣訓練時 DataLoader 只做 tensor 切片和歸一化CPU 的壓力大幅下降。這一步又把 GPU-Util 拉到了 85% 左右。第三步優(yōu)化是開啟 AMP同時把torch.compile打開。AMP 讓每個 step 的計算時間顯著變短而torch.compile做了算符融合也能稍微提速。這時候 GPU-Util 已經(jīng)能穩(wěn)定跑到 90% 以上了。不過這里有個經(jīng)驗教訓torch.compile第一次調(diào)用需要編譯特別慢所以別直接在生產(chǎn)腳本里開先在驗證集上跑幾十個 step看沒問題再正式啟用。我最后又做了一次通信檢查發(fā)現(xiàn)這個模型相對不大all-reduce 開銷只有 10% 左右所以我沒有上梯度累積。如果模型再大一些參數(shù)再多一些我大概率會考慮加上accum_steps2或者 4來壓縮通信開銷。最終效果對比大概是這樣優(yōu)化階段GPU 利用率每 epoch 耗時主要瓶頸初始狀態(tài)15%-25%72 分鐘CPU 數(shù)據(jù)加載調(diào)優(yōu) DataLoader 參數(shù)后約 60%31 分鐘CPU 預處理緩存預處理結(jié)果后約 85%22 分鐘顯存帶寬有限開啟 AMP torch.compile 后90%18 分鐘小幅通信開銷從這個案例里你可以看到單機調(diào)優(yōu)的價值是巨大的從 72 分鐘到 18 分鐘四倍加速。很多時候你以為需要升級機器或者上調(diào)度器其實不過是把單機內(nèi)部的“水龍頭”擰開而已。5.3 常見問題速查表最后給出一張速查表是我在實際操作中總結(jié)出來的高頻問題和解決思路方便大家直接對照。現(xiàn)象常見原因檢查手段推薦方案GPU-Util 持續(xù)為 0%顯存卻很高數(shù)據(jù)加載卡死 / 進程掛起py-spy dump 看線程棧檢查 DataLoader、數(shù)據(jù)路徑是否可讀GPU-Util 周期塌陷像心電圖CPU 預處理慢 / 通信等待Profiler 看 DataLoader 耗時調(diào)大 num_workers、prefetch_factor考慮緩存單卡占用 90% 以上其他卡接近 0%DDP 初始化失敗 / 模型只放在單卡查看進程數(shù)量和啟動命令用 torch.distributed.launch 能正確拉起多進程訓練跑到一半卡住GPU 利用率 0%NCCL 超時 / 通信組不完整查看 NCCL 報錯日志檢查網(wǎng)卡、NCCL_P2P_DISABLE 等環(huán)境變量顯存 OOM但顯存曲線有明顯碎片動態(tài) shape / 顯存碎片用 PyTorch Profiler 看 memory 分布固定輸入 shape關(guān)掉 cudnn.benchmark 或開啟 max_split_size_mbCPU 占用極高GPU 占用上不去DataLoader worker 太多或 CPU 資源不足htop 看各核心占用降低 num_workers用更高效的預處理庫這里面要特別提醒一句NCCL 報錯是很嚇人的動不動就是 “Timeout” 或者 “unhandled system error”但它不一定代表你的網(wǎng)絡或者集群有問題。單機多卡訓練中NCCL 超時常常是因為某張卡的訓練進程因為 DataLoader 異常死掉或者卡住了導致其他卡等不到同步最后超時。所以遇到 NCCL 報錯先從 CPU 側(cè)和 Python 進程狀態(tài)查起很多時候根子不在通信。我個人在實際操作中還有一個習慣每次訓練啟動后先不急著跑完整流程而是把train_loader單獨拿出來跑幾個 epoch打印每個 epoch 的耗時。如果數(shù)據(jù)加載本身就要花 20 秒一個 epoch而 GPU 上訓練 1000 張圖只要 2 秒那問題已經(jīng)很明確了根本不用上 Profiler。這個習慣幫我省了很多時間尤其是在接手別人的老舊訓練腳本時。另外我還想分享一個關(guān)于“先單機后集群”的心得。做 AI Infra 很容易被“集群調(diào)度”“云原生”“大規(guī)模分布式”這些詞吸引好像不搞點大架構(gòu)就不夠高級。但真正穩(wěn)定的系統(tǒng)一定是先把單機這塊地基打牢再談橫向擴展。如果你的單機多卡任務都跑不出應有的利用率那即使上了 Kubernetes也只是把一個低效的任務復制到更多機器上浪費的資源只會成倍增加。先把 8 張卡都調(diào)動起來再去想如何調(diào)度 1000 張卡這才是正確的順序。這篇文章里講到的 DataLoader 調(diào)優(yōu)、AMP、通信開銷觀測、梯度累積其實都屬于單機調(diào)優(yōu)的入門動作。后面我會繼續(xù)寫訓練與調(diào)度的中篇和下篇講講集群調(diào)度器選型、任務排隊、資源配額這些偏系統(tǒng)的內(nèi)容。但不管后面寫得多復雜我都會以“單機內(nèi)部是否真正跑滿”作為第一準繩。你也可以這樣記看到 8 張卡閑著先別急著懷疑調(diào)度器很可能只是你欠下了單機調(diào)優(yōu)這筆賬該還了。 商務建站 企業(yè)官網(wǎng) 運營推廣 返回列表 PREV 查看更多資訊 NEXT 返回資訊列表