習(xí)系統(tǒng)崗筆試通關(guān)指南:從反向傳播到分布式訓(xùn)練)
說實話看到“網(wǎng)易2018實習(xí)生招聘筆試題-深度學(xué)習(xí)系統(tǒng)實習(xí)生”這個標(biāo)題我第一反應(yīng)不是“又是一套題”而是這套題背后藏著的崗位定位和考察邏輯。深度學(xué)習(xí)系統(tǒng)這個方向處在算法和工程的交叉點上既要懂模型怎么訓(xùn)又要懂系統(tǒng)怎么跑是很多做AI基礎(chǔ)設(shè)施團(tuán)隊的標(biāo)配崗位。如果你正在準(zhǔn)備算法崗或者AI系統(tǒng)崗的面試這套題背后的考察思路非常值得拆開揉碎了看。這篇文章我不僅會還原題目背后的考點還會把每個考點對應(yīng)的原理、實操細(xì)節(jié)、常見坑全部講透幫你把“會做題”變成“真理解”。1. 題目整體設(shè)計與崗位定位拆解1.1 “深度學(xué)習(xí)系統(tǒng)實習(xí)生”到底在招什么樣的人網(wǎng)易2018年這個崗位掛在深度學(xué)習(xí)系統(tǒng)團(tuán)隊下面和純算法崗有本質(zhì)區(qū)別。純算法崗考察的是模型設(shè)計、損失函數(shù)調(diào)優(yōu)、實驗分析而系統(tǒng)實習(xí)生崗考察的是“你知不知道一個訓(xùn)練任務(wù)從數(shù)據(jù)到模型參數(shù)更新中間經(jīng)歷了什么”。說白了這個崗位要的人是能把一條訓(xùn)練命令跑起來之后清楚知道GPU顯存被誰占了、數(shù)據(jù)加載為什么慢、梯度更新為什么不同步、分布式訓(xùn)練時通信開銷從哪里來的人。這也是為什么這套題會有大量關(guān)于內(nèi)存管理、數(shù)據(jù)流水線、同步策略、算子實現(xiàn)的內(nèi)容。我當(dāng)時帶過不少實習(xí)生最大的感受是算法基礎(chǔ)好的學(xué)生很多但能把一個自定義Op接入框架、把數(shù)據(jù)流水線壓滿、把分布式訓(xùn)練調(diào)穩(wěn)的人非常少。網(wǎng)易這套題的考察重點恰恰就瞄準(zhǔn)了這個缺口。1.2 筆試真題的核心模塊與考察意圖根據(jù)當(dāng)年的題目結(jié)構(gòu)和同類崗位的考察風(fēng)格這套題基本覆蓋了以下幾個模塊模塊典型考點考察意圖深度學(xué)習(xí)基礎(chǔ)前向傳播、反向傳播、梯度推導(dǎo)確認(rèn)你真正理解訓(xùn)練過程而不是只會調(diào)API系統(tǒng)設(shè)計數(shù)據(jù)加載、內(nèi)存復(fù)用、流水線并行判斷你有沒有工程化思維框架原理計算圖、自動求導(dǎo)、算子注冊驗證你對底層機(jī)制的熟悉程度分布式訓(xùn)練參數(shù)同步、AllReduce、通信開銷考察系統(tǒng)級視野編程能力C/Python實現(xiàn)、調(diào)試確認(rèn)動手能力匹配從考點分布可以看出這套題不是死記硬背能通過的它需要你在腦子里建立一張完整的訓(xùn)練系統(tǒng)全景圖。下面我逐個模塊展開把每個考點背后的原理和實操細(xì)節(jié)講透。2. 核心考點深度解析從前向傳播到反向傳播2.1 前向傳播不只是一個“算一遍”的過程很多同學(xué)一看到前向傳播就覺得是“輸入經(jīng)過網(wǎng)絡(luò)得到輸出”這個理解沒錯但太粗糙了。在系統(tǒng)層面前向傳播涉及的是張量在內(nèi)存中的流轉(zhuǎn)每一層的輸入輸出張量什么時候分配、什么時候釋放、中間結(jié)果要不要緩存、緩存多久。以卷積層為例輸入特征圖經(jīng)過im2col變換后矩陣乘法的中間結(jié)果會占據(jù)大量顯存。如果你在前向過程中沒有規(guī)劃好中間張量的生命周期一個batch稍大一點顯存就爆了。這就是為什么面試官喜歡問“你是怎么估計一個模型訓(xùn)練所需顯存的”。我的建議是你要在大腦里維護(hù)一張“張量生命周期表”哪個張量被創(chuàng)建、被誰引用、被誰釋放、能否原地操作。比如ReLU的原地版本inplaceTrue就能省一份輸出張量的內(nèi)存但在反向傳播時需要額外標(biāo)記這就是典型的系統(tǒng)級細(xì)節(jié)。2.2 反向傳播與梯度計算筆試中出現(xiàn)頻率最高的推導(dǎo)題反向傳播的核心是鏈?zhǔn)椒▌t但筆試不會只考你鏈?zhǔn)椒▌t公式它通常會給你一個具體的復(fù)合函數(shù)比如 $f \text{softmax}(\text{linear}(x))$讓你手動推導(dǎo)每個中間變量的梯度。這種題考察的是你對“梯度流向”的敏感度。我當(dāng)時帶人時總結(jié)過一個口訣反向傳播就是反向跑一遍前向每個算子的反向函數(shù)接收上游梯度結(jié)合本地的輸入算出對輸入的梯度再傳給下游。理解這個循環(huán)比背出100個公式都重要。以Softmax為例它前向輸出 $p_i \frac{e^{z_i}}{\sum_j e^{z_j}}$反向時如果上游梯度是 $g$那么對 $z_k$ 的梯度是 $p_k(g_k - \sum_j p_j g_j)$。推導(dǎo)過程不復(fù)雜但筆試容易在“分母那個求和是常量還是變量”上卡住。記住在反向時求和項是前向已經(jīng)算好的常量不是變量。2.3 梯度下降與學(xué)習(xí)率別只背公式這一塊筆試很少直接考“SGD公式”而是結(jié)合系統(tǒng)問題考察比如學(xué)習(xí)率太大會發(fā)生什么損失震蕩、不收斂為什么要用動量對抗梯度方向的抖動學(xué)習(xí)率衰減的策略有哪些在系統(tǒng)層面學(xué)習(xí)率還牽扯到“參數(shù)更新在哪一步完成”。同步訓(xùn)練里所有worker都算完梯度統(tǒng)一更新異步訓(xùn)練里一個worker算完就直接更新不需要等別人。這直接決定了訓(xùn)練的收斂性和系統(tǒng)的復(fù)雜度。3. 真題實戰(zhàn)一套完整的模擬題與詳細(xì)解答3.1 真題一實現(xiàn)一個全連接層的前向與反向題目描述請用Python實現(xiàn)一個全連接層的前向傳播和反向傳播要求支持任意batch size輸入。這個題是基本功。前向很簡單$Y XW b$。反向需要分別計算對輸入、權(quán)重、偏置的梯度。我給出一個參考實現(xiàn)注意以下幾點import numpy as np class FullyConnected: def __init__(self, in_features, out_features): self.W np.random.randn(in_features, out_features) * 0.01 self.b np.zeros((1, out_features)) self.x None def forward(self, x): self.x x # 緩存輸入反向傳播要用 return np.dot(x, self.W) self.b def backward(self, grad_output): # grad_output shape: (batch_size, out_features) grad_x np.dot(grad_output, self.W.T) grad_W np.dot(self.x.T, grad_output) grad_b np.sum(grad_output, axis0, keepdimsTrue) return grad_x, grad_W, grad_b這里最容易錯的就是grad_b很多人會忘記對batch維求和。因為偏置是對每個輸出節(jié)點共享的反向時要把所有樣本的梯度累加起來。這個細(xì)節(jié)筆試經(jīng)常考值得留意。3.2 真題二手動推導(dǎo)Softmax交叉熵的反向公式這道題的完整版本一般是請推導(dǎo)softmax和交叉熵組合的反向傳播公式。很多人會分開推導(dǎo)過程復(fù)雜容易出錯。有個更優(yōu)雅的做法交叉熵對softmax輸入 $z$ 的梯度恰好是 $p - y$其中 $p$ 是softmax輸出$y$ 是one-hot標(biāo)簽。這個結(jié)果之所以成立是因為交叉熵的梯度恰好抵消了softmax雅可比矩陣中的耦合項。推導(dǎo)時建議分兩步先求 $L$ 對 $p_i$ 的梯度再求 $p_i$ 對 $z_j$ 的梯度用鏈?zhǔn)椒▌t合起來化簡后就是 $p - y$。如果你在筆試時時間緊張可以先寫出結(jié)論再用兩步推導(dǎo)驗證。這個公式推導(dǎo)清楚后很多框架的CrossEntropyLoss實現(xiàn)你就能一眼看穿。3.3 真題三從零實現(xiàn)mini-batch SGD這個題考察的不只是“更新參數(shù)”還考察你是否考慮過“梯度累積”和“batch采樣”class SGD: def __init__(self, params, lr0.01, momentum0.9): self.params params self.lr lr self.momentum momentum self.velocity [np.zeros_like(p) for p in params] def step(self, grads): for i, (p, g) in enumerate(zip(self.params, grads)): self.velocity[i] self.momentum * self.velocity[i] - self.lr * g p self.velocity[i]注意這里返回的是更新后的參數(shù)引用不是新對象。實際框架里優(yōu)化器通過.step()原地更新參數(shù)這樣外部保存的參數(shù)引用始終有效。這個細(xì)節(jié)在面試時經(jīng)常被追問你可以主動提到。3.4 真題四數(shù)據(jù)加載為什么慢如何優(yōu)化數(shù)據(jù)加載是系統(tǒng)崗筆試題里的常客。常規(guī)回答是“用多線程/多進(jìn)程”但面試官真正想聽的是更深層的流水線思路。建議按四層來回答I/O層從磁盤讀取數(shù)據(jù)用SSD能顯著提速但更關(guān)鍵的是減少小文件隨機(jī)讀改成順序讀大文件如TFRecord、recordIO。解碼層圖片解碼是CPU密集操作用TurboJPEG、libjpeg-turbo或GPU解碼可以大幅降耗時。預(yù)處理層隨機(jī)裁剪、翻轉(zhuǎn)、歸一化這些操作放在worker進(jìn)程中做不和主訓(xùn)練進(jìn)程搶GIL。傳輸層數(shù)據(jù)從CPU拷貝到GPU要走PCIe這里可以用CUDA Stream和內(nèi)存池來隱藏拷貝延遲。很多框架的DataLoader支持num_workers和prefetch_factor本質(zhì)就是在每一層都做流水線。筆試中如果能把上面四層結(jié)構(gòu)答出來就已經(jīng)超過大部分候選人了。3.5 真題五分布式訓(xùn)練中的同步與異步這個問題主要考察你對分布式訓(xùn)練系統(tǒng)的理解。同步訓(xùn)練SyncSGD是等所有worker算完梯度做一次AllReduce后再更新異步訓(xùn)練AsyncSGD是每個worker算完就更新到參數(shù)服務(wù)器。難點在于同步訓(xùn)練里如果某個worker掉隊整體訓(xùn)練速度就被它拖累。這時候有幾種解法備份workerbackup worker每次只等前k個完成即可彈性同步動態(tài)調(diào)整同步門檻梯度壓縮減少通信量。實操中我通常會先做通信時間占比分析。如果你的模型計算時間遠(yuǎn)大于通信時間那么同步帶來的staleness問題就不嚴(yán)重如果通信占大頭就需要梯度壓縮或者改用異步方案。4. 從筆試到工程落地深度學(xué)習(xí)系統(tǒng)的核心實現(xiàn)細(xì)節(jié)4.1 計算圖與自動求導(dǎo)機(jī)制筆試回答到這個層面就進(jìn)入了加分區(qū)。深度學(xué)習(xí)框架的基本功就是計算圖和自動求導(dǎo)。PyTorch是動態(tài)圖每次前向都會重新建圖靈活但有一定開銷TensorFlow 1.x是靜態(tài)圖先構(gòu)圖后執(zhí)行利于部署和優(yōu)化但對調(diào)試不友好。你可以類比理解動態(tài)圖是“邊寫邊執(zhí)行”靜態(tài)圖是“先寫劇本再開演”。自動求導(dǎo)的實現(xiàn)有兩種主流方式符號微分在圖上做數(shù)學(xué)推導(dǎo)拿到精確的梯度表達(dá)式反向模式自動微分本質(zhì)上是鏈?zhǔn)椒▌t的機(jī)械執(zhí)行每次前向多存一些中間結(jié)果反向時按圖走一遍。筆試中如果被問到“框架的autograd是怎么工作的”你可以說前向過程中每個Op記錄了輸入輸出和grad_fn反向時按grad_fn組成的鏈依次調(diào)用形成一條執(zhí)行軌跡。能把這個流程講清楚說明你真的用過框架而不是只調(diào)了loss.backward()。4.2 顯存優(yōu)化算得動比算得快更重要顯存優(yōu)化這塊筆試有時候會給場景題分析訓(xùn)練某個模型時的顯存占用分布或者如何優(yōu)化顯存。實際上一個訓(xùn)練任務(wù)的顯存占用主要來自幾塊模型參數(shù)本身優(yōu)化器狀態(tài)Adam要額外存一階動量和二階動量是參數(shù)量的2倍前向中間激活值通信緩沖區(qū)分布式時。優(yōu)化思路也有很多混合精度訓(xùn)練FP16存儲FP32累積梯度能讓顯存減半激活值重計算activation checkpointing用時間換空間只存部分層的激活值反向時重新算一次梯度累加能在不改變batch大小的情況下模擬大batch效果。最值得一提的是顯存碎片問題。我實際遇到過一個模型怎么調(diào)batch size都會報OOM后來發(fā)現(xiàn)是碎片導(dǎo)致的。解決辦法是給PyTorch設(shè)置環(huán)境變量或用CUDA緩存分配器的配置讓顯存分塊更規(guī)整。這種問題只有真正跑過訓(xùn)練的人才會經(jīng)驗豐富。4.3 數(shù)據(jù)流水線把GPU餓死是最大的浪費訓(xùn)練系統(tǒng)的性能瓶頸很多時候不是計算而是數(shù)據(jù)供給。我們常說的“GPU利用率低”背后往往是CPU來不及準(zhǔn)備數(shù)據(jù)。我見過一個典型的案例一個目標(biāo)檢測模型在2080Ti上訓(xùn)練GPU利用率只有60%左右排查后發(fā)現(xiàn)是數(shù)據(jù)加載線程數(shù)不夠圖片解碼占滿了CPU導(dǎo)致GPU常常等待。把DataLoader的num_workers從4調(diào)到12之后GPU利用率穩(wěn)定在95%以上訓(xùn)練速度直接提升了1.5倍。這個案例說明一個道理深度學(xué)習(xí)訓(xùn)練是一個流水線系統(tǒng)從硬盤讀取到預(yù)處理再到GPU計算任一段堵住了整體就慢了。筆試時候如果遇到這類問題你可以按“I/O、解碼、預(yù)處理、傳輸”四層去分析再講具體優(yōu)化手段回答會非常系統(tǒng)。4.4 分布式訓(xùn)練的通信開銷與AllReduce當(dāng)模型大到單卡放不下或者數(shù)據(jù)量太大單卡訓(xùn)練太慢時就需要分布式訓(xùn)練。這里的核心不是“多卡跑一下”這么簡單而是通信效率。AllReduce是分布式訓(xùn)練最常用的通信原語它把不同GPU上的梯度先規(guī)約求和再把結(jié)果廣播回去。最簡單的實現(xiàn)是Ring-AllReduce把GPU排成一個環(huán)每個節(jié)點只和鄰居通信數(shù)據(jù)分成N份依次傳遞和累加。這種方式把通信量與GPU數(shù)量解耦比參數(shù)服務(wù)器方式更容易擴(kuò)展到大規(guī)模集群。筆試中如果問“為什么千卡訓(xùn)練沒有百倍加速”你可以從加速比的角度分析假設(shè)每輪迭代計算時間為C通信時間為T單卡迭代時間為CN卡情況下的理想迭代時間為C/N T。當(dāng)N增大到一定程度T成為主導(dǎo)加速比就上不去了。實操中的經(jīng)驗是先評估通信計算比Communication to Computation Ratio。如果通信占比較大優(yōu)先考慮梯度壓縮量化到8bit或者梯度累積減少通信頻率。5. 常見問題與面試避坑實戰(zhàn)指南5.1 筆試中最容易翻車的三個細(xì)節(jié)我面試過不少候選人發(fā)現(xiàn)筆試最容易丟分的地方往往不在難題而在基礎(chǔ)細(xì)節(jié)。這里盤點三個高頻翻車點第一個是反向傳播里忘了緩存前向的輸入。如果你在forward里沒有把輸入存下來反向時拿不到x梯度就算不了。這是實現(xiàn)的常識但緊張時容易漏。第二個是SGD實現(xiàn)里用錯了更新順序。標(biāo)準(zhǔn)的momentum SGD是先算velocity再更新參數(shù)。有些人先更新參數(shù)再用舊參數(shù)算velocity順序反了收斂就出問題。第三個是Softmax數(shù)值穩(wěn)定性。很多人在筆試?yán)镏苯訉憂p.exp(z)當(dāng) $z$ 里有大數(shù)時指數(shù)會溢出。正確做法是減掉每行的最大值z_max np.max(z, axis-1, keepdimsTrue) exp_z np.exp(z - z_max) p exp_z / np.sum(exp_z, axis-1, keepdimsTrue)這是工程和理論之間的典型差異理論上softmax(z) softmax(z - c)但數(shù)值上差異巨大。我一直覺得能主動寫出這一步的候選人說明真踩過訓(xùn)練不穩(wěn)定的坑。5.2 如何展現(xiàn)你的系統(tǒng)級理解筆試時除了寫出正確答案你還可以在答題旁邊加一段“系統(tǒng)備注”展示你的工程思考。比如全連接層實現(xiàn)里可以注明“該實現(xiàn)未考慮in-place更新實際框架中會使用更高效的內(nèi)存復(fù)用策略”。面試官看重的不是你背了多少知識點而是你能否把算法和系統(tǒng)連接起來。舉個常見的面試問題“你如何檢測數(shù)據(jù)加載是否是訓(xùn)練瓶頸”你要給出可執(zhí)行的方案用nvidia-smi看GPU利用率用top或perf看CPU占用率用py-spy看Python進(jìn)程卡在哪個調(diào)用再針對性優(yōu)化。這種“問題定位→數(shù)據(jù)佐證→方案落地→效果驗證”的路徑就是系統(tǒng)崗需要的思維方式。筆試雖然只考紙面但你展現(xiàn)的思維鏈面試官是看得出來的。5.3 經(jīng)典追問為什么批量歸一化在訓(xùn)練和推理時不一樣這道題是筆試的“隱藏考點”它考察你能否區(qū)分訓(xùn)練和推理的系統(tǒng)差異。訓(xùn)練時BN用當(dāng)前batch的均值和方差來歸一化同時更新滑動均值推理時沒有batch的概念只能使用訓(xùn)練階段積累的滑動均值。系統(tǒng)層面上這一步在推理框架里往往被融合到卷積層里變成一組額外的縮放和偏置參數(shù)避免單獨算均值方差。如果你知道“BN融合”這個操作就可以補(bǔ)充推理階段可以把BN的縮放系數(shù)和偏置融入前一層的權(quán)重中減少一次kernel launch這在移動端推理中很常見。這個補(bǔ)充能明顯體現(xiàn)出你的工程深度。5.4 應(yīng)對“你最熟悉的深度學(xué)習(xí)框架”這類開放題這類題看似送分實則最容易暴露問題。推薦按“框架的架構(gòu)設(shè)計、自動求導(dǎo)實現(xiàn)、適用的業(yè)務(wù)場景、踩過的坑”四個維度來組織回答。比如你選PyTorch可以這樣說動態(tài)圖機(jī)制讓調(diào)試非常方便適合研究和快速迭代torch.autograd用Tensor和Function各自的backward方法構(gòu)建反向執(zhí)行圖在生產(chǎn)的推理場景中我會用torch.jit.script或onnx導(dǎo)出模型配合TensorRT加速。避免只回答“我用過PyTorch挺好用的”。面試官想聽的是你對框架有體系化認(rèn)知而不只是“會用”。6. 實操經(jīng)驗我整理的一套筆試復(fù)習(xí)清單6.1 核心知識自查表這套清單是我自己帶實習(xí)生時常用的基本覆蓋了深度學(xué)習(xí)系統(tǒng)方向的大部分考點。你可以用它自測看看哪些地方還有盲區(qū)知識點自測標(biāo)準(zhǔn)是否掌握反向傳播推導(dǎo)能獨立推導(dǎo)softmax交叉熵的梯度是/否自動求導(dǎo)原理能解釋PyTorch的backward鏈?zhǔn)秸{(diào)用過程是/否顯存分析能估算出常見模型訓(xùn)練時的顯存開銷是/否數(shù)據(jù)加載優(yōu)化能說出四層流水線結(jié)構(gòu)與優(yōu)化手段是/否分布式訓(xùn)練能對比同步/異步、解釋AllReduce是/否數(shù)值穩(wěn)定性知道softmax的max-subtraction技巧是/否BN訓(xùn)練/推理區(qū)別知道滑動均值與BN融合是/否調(diào)試工具鏈會用nvidia-smi、perf、py-spy定位瓶頸是/否如果有一半以上“否”說明該重點補(bǔ)了。6.2 手寫實現(xiàn)推薦練習(xí)筆試終究要落到代碼上建議你花一周時間手寫以下內(nèi)容寫一遍勝過看十遍用numpy實現(xiàn)全連接層、ReLU、SGD用numpy實現(xiàn)softmax交叉熵的前向和反向用自己的“迷你框架”搭一個兩層MLP在MNIST上跑通給這個迷你框架加一個batch維度對齊的check確認(rèn)梯度正確gradient check實現(xiàn)一個簡單的DataLoader理解shuffle和batch的交互。梯度檢查這塊我多說一句用數(shù)值法近似梯度和你的解析梯度比較誤差落在1e-6量級基本就是對的。這是一個非常實用的自檢手段筆試時也可以主動提一下。6.3 我的時間分配建議如果你只有兩周準(zhǔn)備時間可以這樣分配前3天把反向傳播的所有常見推導(dǎo)手寫一遍第4-7天完成迷你框架的代碼實現(xiàn)第8-10天重點攻克數(shù)據(jù)加載、顯存優(yōu)化、分布式這三塊系統(tǒng)知識第11-14天做真題模擬整理錯題復(fù)盤表達(dá)。時間緊的話優(yōu)先保證全連接層和softmax的實現(xiàn)能力因為這是最高頻的基礎(chǔ)題。7. 寫在后面這套題真正教會我的事我在實際帶人和做項目的過程中反復(fù)體會到一道筆試題的答案只是表面它背后真正考的是你有沒有建立起“算法到系統(tǒng)”的完整映射。能手動推導(dǎo)softmax反向公式的人很多但能說清楚為什么要在實現(xiàn)里減掉最大值、知道推理時BN要融合進(jìn)卷積的人不多后者才是深度學(xué)習(xí)系統(tǒng)崗真正需要的能力。如果你正在準(zhǔn)備這類面試我建議你不要只刷題而是真的把一個兩層的網(wǎng)絡(luò)從零寫出來把顯存占用打印出來把數(shù)據(jù)加載的瓶頸測出來。這些親手做過的事情才是在筆試和面試中真正能打動面試官的東西。這套網(wǎng)易2018年的題雖然過去了幾年但它考察的能力模型到現(xiàn)在依然是深度學(xué)習(xí)系統(tǒng)方向的核心主線。