
今年我把大量業(yè)余時間投進了一個叫ai-engineering-from-scratch的個人項目。簡單說就是給自己立了條規(guī)矩凡是跟 AI 相關的環(huán)節(jié)能自己動手實現(xiàn)的絕不直接調封裝好的接口。從手寫張量運算開始到訓練一個微型語言模型再到把模型量化部署成一個能用的服務整個過程走下來收獲最大的不是某個模型效果變好了而是終于弄明白了一個大語言模型從數(shù)據(jù)集到上線中間到底經(jīng)歷了什么出了問題時該往哪個方向查。這篇文章就圍繞這個項目把我踩過的坑、驗證過確實有效的路線、以及現(xiàn)在還保留在收藏夾里的參考材料整理出來。不吹不黑直接上干貨。適合已經(jīng)會用 PyTorch、會調 Transformers 庫但對底層實現(xiàn)還有點心里沒底的人也適合準備進入大模型應用開發(fā)、想從底層建立手感的新手。看完你至少能少走我一半的彎路。1. 為什么我決定從零開始學AI工程先別急著調API1.1 框架用久了心里不踏實先說個真實感受。我最早做 NLP 項目時基本就是transformers庫一把梭加載模型、寫個訓練循環(huán)、調參數(shù)、完事。模型跑不動就換更大的顯卡效果不好就換更強的預訓練權重。直到有一次模型在驗證集上的損失怎么都降不下去我把學習率調低、把層數(shù)減少、把 dropout 拉滿全都沒用。當時我能做的只有百度搜索關鍵詞然后挨個試網(wǎng)上流傳的玄學改法。那天之后我開始反思我不是在用 AI而是在調一個巨大的不透明的黑盒子。梯度怎么傳播的、學習率策略在內部到底起了什么作用、顯存為什么爆、推理為什么慢這些問題我都答不上來。而這些問題恰恰是 AI 工程里最核心的問題。框架用久了AI 工程變成了一種配置工程這對工程師來說是非常危險的。一旦遇到模型異常、數(shù)據(jù)泄漏、顯存瓶頸這類問題不懂底層就只能靠猜而靠猜的代價是極其昂貴的。所以我才決定做from scratch這件事。不是去重寫 PyTorch也不是去發(fā)明新一代算法而是把現(xiàn)代語言模型里最關鍵的模塊一個個抽出來用最樸素的方式實現(xiàn)一遍。注意力機制自己寫、分詞器自己寫、訓練循環(huán)自己寫、量化腳本自己寫。寫完之后再回到框架里你會發(fā)現(xiàn)自己不是在使用框架而是在理解框架心態(tài)完全不一樣。1.2 從零手搓到底要學什么范圍怎么劃剛開始我也很迷茫因為AI 工程這個范圍太大了。做 CV 的需要懂卷積、數(shù)據(jù)增強、分布式訓練做推薦的需要懂特征工程、Embedding、在線學習做 NLP / LLM 的需要懂分詞、Transformer、微調、推理優(yōu)化。如果不能明確邊界這個項目很可能變成一個永遠完不成的大坑。我的做法是先把全鏈路畫成一個流程圖再從里面選出必須親手做一遍的核心節(jié)點。我這里所謂的流程圖并不是畫給別人看的那種而是寫給自己確認用的清單。我的清單大致是數(shù)據(jù)處理分詞器、批次構建、數(shù)據(jù)采樣模型構建Embedding、多頭注意力、前饋網(wǎng)絡、層歸一化訓練系統(tǒng)損失函數(shù)、反向傳播、優(yōu)化器、學習率調度推理系統(tǒng)自回歸生成、KV Cache、采樣策略部署優(yōu)化量化、批處理、服務化接口模型進階在基礎模型之上嘗試先思考、再回答的推理行為每個節(jié)點只追求能跑通、能解釋、能量化效果不做過度擴展。比如我不會去手寫 CUDA kernel因為那已經(jīng)超出一般的 AI 工程范圍屬于系統(tǒng)底層優(yōu)化跟當前目標不對齊。先把上層的每一個工程環(huán)節(jié)摸透等將來遇到 perf 瓶頸時再下沉也不遲。這個范圍劃完之后我給自己定了三個里程碑第一用 NumPy 手寫一個能訓練 MNIST 的迷你神經(jīng)網(wǎng)絡第二用 PyTorch 從零實現(xiàn)一個小型 GPT在幾十 MB 的數(shù)據(jù)上訓練出能生成文本的模型第三對第二個模型做量化壓縮和推理加速部署成一個 HTTP 服務。后面所有的工作都圍繞這三個里程碑展開。2. 我的學習路線把黑盒拆成白盒的六個階段2.1 六個階段一層層揭開隔層我給自己制定了遞進式路線不是上來就寫 Transformer而是按照造一臺機器的順序去推進。如果你也想照這條路走我建議你嚴格按順序來不要貪快。第一個階段是 Python 編程與數(shù)據(jù)操作。重點不是語法而是對數(shù)據(jù)到底長什么樣有直覺。列表、字典、字符串處理、文件讀寫配合collections.Counter做詞頻統(tǒng)計這樣后面寫分詞器時不會發(fā)怵。第二個階段是手寫張量與自動求導。現(xiàn)階段不建議直接上 PyTorch而是先用 NumPy 自己實現(xiàn)一個極簡的張量類只支持標量、向量、矩陣的加法乘法以及對某個參數(shù)的梯度記錄。這個階段的核心任務是理解反向傳播到底在做什么而不是把所有細節(jié)都搞完。我做了一個非常粗糙的自動求導只有幾十行代碼但它讓我第一次看懂了鏈式法則在程序里如何落地。第三個階段是手寫 MLP 和簡單 CNN。用自己寫的自動求導工具去訓練一個識別手寫數(shù)字的模型把梯度下降、過擬合、正則化這些基礎概念體驗一遍。很多人跳過這個階段直接學大模型但我強烈不建議。因為大模型也依賴同樣的數(shù)學機制如果連一個隱藏層的 MLP 都調不明白后面遇到 Transformer 的異常會更難定位。第四個階段是學習和實現(xiàn)注意力機制。注意力是任何現(xiàn)代語言模型的靈魂它做的事本質上就是根據(jù)相關性從其他位置上取信息。先手寫單頭注意力再擴展成多頭最后加上因果掩碼。第五個階段是把注意力堆成 Transformer實現(xiàn)一個微型 GPT。從 token 化文本開始訓練一個幾百萬參數(shù)的小模型讓它生成看起來像模像樣的句子。第六個階段是推理優(yōu)化與部署。把訓練好的權重量化成半精度或整數(shù)精度加上 KV Cache寫一個流式生成接口再封裝成一個所有人都能通過 HTTP 調用的服務。每個階段我都有對應的完成標準。比如第四階段的完成標準不是理解了注意力公式而是寫出一個能跑、能反向傳播的多頭注意力模塊并在小數(shù)據(jù)集上訓練收斂。這樣就不會陷入漫無目的的看論文狀態(tài)。2.2 參考書怎么用讀一遍不如改一遍在我動手的過程中最常被問到的一句話是你到底用什么學的我的答案很直接《Build a Large Language Model From Scratch》這本書給了我很重要的路線參考。這本書從數(shù)據(jù)準備開始一步步搭建類似 GPT 的結構包含分詞、注意力、訓練、微調等完整內容。書里的代碼風格非常樸素沒有花里胡哨的高級封裝很適合配合這個項目來理解每一個環(huán)節(jié)。但我還得提醒一點讀這本書千萬不要只停留在讀上。我第一次讀的時候以為看懂了結果合上書自己寫連masked_fill都寫錯了位置。后來我換了個策略把書里的每段代碼都當成參考實現(xiàn)然后關掉示例代碼自己重新實現(xiàn)一遍遇到卡殼再回去對照。這樣讀一遍書等于自己寫了三遍代碼效果完全不一樣。除了這本書我還會配合看一些原始論文和開源項目。書不會覆蓋所有細節(jié)比如混合精度訓練、KV Cache 的顯存優(yōu)化、量化感知訓練這些工程問題需要從零散的源碼和文章中補全。我的經(jīng)驗是以書為主線建立骨架再以論文和源碼為枝葉填充細節(jié)最后用自己的代碼驗證理解。3. 從零手搓一個微型語言模型的完整流程3.1 先解決詞的問題手寫 BPE 分詞器語言模型處理的是 token不是原始字符串。所以從零搭建模型第一步就是解決怎么把一段話切成 token。按空格切詞雖然簡單但會得到一個巨大的詞表而且無法處理沒見過的新詞所以現(xiàn)代模型普遍使用字節(jié)對編碼BPE。BPE 的核心思想是先把文本看成 UTF-8 字節(jié)序列然后反復統(tǒng)計相鄰字節(jié)對的出現(xiàn)頻率把最高頻的對合并成一個新符號直到詞表達到目標大小。手寫 BPE 時最關鍵的數(shù)據(jù)結構是一個計數(shù)用的字典。首先是統(tǒng)計相鄰字節(jié)對頻率def get_stats(ids): counts {} for pair in zip(ids, ids[1:]): counts[pair] counts.get(pair, 0) 1 return counts然后在每一輪迭代中找到頻次最高的 pair把它的兩個 id 合并成一個新的 iddef merge(ids, pair, new_id): result [] i 0 while i len(ids): if i len(ids) - 1 and ids[i] pair[0] and ids[i1] pair[1]: result.append(new_id) i 2 else: result.append(ids[i]) i 1 return result這個過程初看很簡單但真正實現(xiàn)時有一個坑隨著合并的進行id 長度會變化新的高頻 pair 必須基于上一輪合并后的序列重新統(tǒng)計。如果理解錯了合并順序就會亂掉分詞結果會有問題。我當時就是在這里猶豫了很久后來意識到每次合并后全局重新統(tǒng)計才是標準做法而不是在一個固定列表上連續(xù)改。3.2 核心模塊手寫多頭注意力的幾個細節(jié)注意力模塊是整個模型中最容易寫錯、也最值得親手實現(xiàn)的部分。常見的公式非常簡單Q 和 K 做點積、除以根號 d、加上掩碼、Softmax、再乘 V。但工程實現(xiàn)時有幾個細節(jié)特別值得注意。第一個是因果掩碼。語言模型生成時只能看前面的 token不能偷看后面的內容。實現(xiàn)方式是把未來位置的值設成負無窮經(jīng)過 Softmax 之后概率趨近于零。我在第一次實現(xiàn)時用的掩碼矩陣是對的但維度 broadcast 沒對齊結果訓練損失直接亂跳。排查了很久才發(fā)現(xiàn)是掩碼形狀錯誤。手動實現(xiàn)時建議把每一步的張量形狀打印出來別嫌麻煩。第二個是縮放因子的作用。除以sqrt(d_k)不只是為了讓數(shù)值范圍更好看而是防止點積結果過大導致 Softmax 進入飽和區(qū)梯度變得極小。如果不縮放模型在小數(shù)據(jù)集上也能跑但訓練會明顯變慢損失曲線的尾巴還會抖動。第三點是多頭注意力中頭的意義。多頭不是簡單地疊加多個注意力結果而是讓每個頭學到不同的相關性模式。有的頭可能關注前一個詞有的頭可能關注句法成分。雖然我們在代碼層面只是把 d_model 切成幾段分別計算但正是這種參數(shù)獨立性讓模型表達力變強了。手寫時我建議先按循環(huán)每個頭的方式寫一版跑通后再改成矩陣并行版這樣對分塊的印象會特別深刻。第四點是殘差連接和層歸一化的位置。Transformer 每一層結構是注意力 - 殘差 - 層歸一化 - 前饋網(wǎng)絡 - 殘差 - 層歸一化。殘差連接讓梯度有一條高速公路可以直達底層層歸一化穩(wěn)定每一層的激活分布。順序如果搞錯了模型的訓練穩(wěn)定性和最終效果都有明顯差距。3.3 訓練循環(huán)里的關鍵決策學習率、批次和損失模型搭完了訓練循環(huán)也不像想象中那么簡單。我最初寫的訓練循環(huán)只是取數(shù)據(jù)、算 loss、反向傳播、更新結果模型一直不收斂。后來我逐項排查發(fā)現(xiàn)真正影響訓練效果的是幾個細節(jié)隨機種子是否固定、數(shù)據(jù)是否被隨機打亂、學習率調度是否合理、梯度有沒有做裁剪。學習率我很推薦使用 warmup cosine 的調度方式。一開始用一個很小的學習率熱身讓參數(shù)更新不會太激進然后再逐步降到接近零讓模型在后期做好精細調整。我用的配置大致是warmup 步數(shù)約占總步數(shù)的 3% 到 5%峰值學習率在1e-3到3e-4之間具體看模型規(guī)模和 batch size。批次大小也很關鍵。我的顯卡資源有限單批放不了太多樣本就用梯度累積來模擬更大的批次。注意梯度累積不是直接改batch_size而是在多個小批次上累計梯度然后再做一次優(yōu)化器更新。梯度累積的步數(shù)需要根據(jù)顯存實測來定我試過累積 4 步效果最穩(wěn)。數(shù)據(jù)處理上我加了一條保護線驗證集絕對不參與訓練而且驗證集的數(shù)據(jù)順序每次都要固定。有一次我偷懶直接在數(shù)據(jù)類里給訓練集和驗證集用了同一個 shuffler結果驗證損失一直往下掉訓練損失卻不降后來才發(fā)現(xiàn)是驗證集里混進了訓練樣本數(shù)據(jù)泄漏的坑差點沒把我逼瘋。訓練過程中我習慣每 20 個 step 打印一次 loss 和當前學習率每 200 個 step 在驗證集上算一次困惑度。損失曲線的形態(tài)非常有信息量如果訓練 loss 下降但驗證 loss 上升那是過擬合如果訓練 loss 不降那多半是學習率太高、數(shù)據(jù)有泄漏或者模型結構寫錯了。3.4 采樣策略怎么讓模型說得像人話模型訓練完之后生成階段同樣有很多坑。語言模型是逐步生成 token 的每次預測下一個 token 的概率分布然后從分布中采樣。如果每次都選概率最大的 token就是貪心解碼雖然穩(wěn)定但容易重復、死板。如果每次都純隨機采樣句子會變得前言不搭后語。我實測下來配合使用temperature、top-k 和 top-p效果最好。Temperature 控制分布的尖銳程度溫度小于 1 的時候概率集中到高概率 token 上生成的文本更保守溫度大于 1 的時候分布更平緩文本更大膽。Top-k 是只從概率最高的 k 個 token 中采樣避免給那些幾乎不可能的 token 太多機會。Top-p 則是動態(tài)選擇累計概率超過閾值的 token 集合比 top-k 更靈活。我常用的一個組合是temperature0.8, top_k40, top_p0.9。這個組合在我訓練的幾百萬參數(shù)小模型上生成結果既有一定的多樣性也不會很快陷入重復循環(huán)。如果你發(fā)現(xiàn)模型總是重復同樣的句子可以適當把 temperature 調高一點或者增大 top-p 閾值如果發(fā)現(xiàn)上下文不連貫則降低 temperature 會更有效。4. 從生成到推理小模型也能學會思考鏈4.1 推理模型與傳統(tǒng)模型到底差在哪里當我把基礎語言模型跑通之后我注意到行業(yè)里的討論熱點開始轉向另一個方向讓模型在回答之前先做一段內部推理再給出最終答案。傳統(tǒng)語言模型是輸入問題立刻輸出答案而推理模型的生成結果是先輸出一系列思考過程再輸出最終結論。這個過程類似于人在紙上打草稿草稿越長越有機會通過逐步演算推導出正確答案。從工程角度看兩者最主要的差別不在模型結構而在訓練方式。一個普通模型在訓練時看到的是問題 - 答案的配對而一個具備推理能力的模型在訓練時會看到問題 - 思考過程 - 答案的完整鏈路。思考過程被當作可學習的文本參與訓練模型因此學會了在回答前主動進行推理。另一個關鍵點是強化學習在這種訓練中的應用。為了讓模型學會找到更好的思考路徑社區(qū)里出現(xiàn)了很多從零構建推理模型的項目。核心做法是讓模型對同一個問題生成多條不同的推理路徑和答案然后根據(jù)答案是否正確以及推理路徑是否合理給予不同強度的更新信號。做得好的路徑會得到正向強化做得差的路徑會被抑制。這個過程不依賴人工標注的標準推理文本而是通過模型自身的探索來進化成本低很多效果卻可能很好。我個人的立場是如果你想搞懂 modern LLM 的完整能力邊界從生成到推理這一步非常值得復現(xiàn)。它也是從會做 Next Token Prediction向會解決問題進階的關鍵一步。4.2 我在小模型上復現(xiàn)推理能力的實操嘗試理論清晰之后我決定在自己的小模型上進行嘗試。我先準備了一批帶思考鏈的訓練數(shù)據(jù)。數(shù)據(jù)來源不是去爬什么高不可攀的內容而是自己寫規(guī)則生成對于數(shù)學加減法、字符串反轉、簡單邏輯題先生成問題再人工寫一段分步驟的思考文本最后給出答案。數(shù)據(jù)量控制在幾萬條以內一方面是因為我算力有限另一方面也足以驗證小模型是否能學到推理行為。訓練時我用的是監(jiān)督微調在已經(jīng)訓練好的基礎模型之上用這批帶思考鏈的數(shù)據(jù)繼續(xù)訓練。關鍵技巧是思考過程的特殊 token 分隔。我在文本中插入[THINK]和[ANSWER]兩個特殊標記讓模型明確知道哪里是思考區(qū)、哪里是答案區(qū)。推理時生成到[ANSWER]之前的部分就是模型的思考過程。實測結果很有意思。一個參數(shù)量不到兩百萬的小模型經(jīng)過幾千步有思考鏈數(shù)據(jù)的微調之后面對簡單加法問題時確實會先輸出類似先把十位和個位分別相加然后處理進位這類的過程文本再給出最終答案。雖然思考過程經(jīng)常有廢話但正確率比起微調前有明顯提升。不過也有明顯的局限性模型在陌生題型上基本不具備泛化推理能力它只是記住了遇到問題要先輸出一段過程的模式。這說明 from scratch 復現(xiàn)推理模型不是簡單加數(shù)據(jù)就行可能還需要引入更多的強化學習策略。但作為工程入門走一遍這個流程已經(jīng)讓我很清楚地理解了思考鏈的本質它不神秘只是把未顯式建模的中間計算過程變成可訓練的文本序列。做這一步時還有一個非常重要的提醒數(shù)據(jù)合規(guī)和版權問題永遠不能繞開。不要從不明渠道下載所謂的全套數(shù)據(jù)或者盜版資源自己寫規(guī)則生成、使用合規(guī)的開源數(shù)據(jù)才是能長期復用的做法。模型能力可以慢慢提升合規(guī)底線是絕對不能突破的。5. 工程化落地模型能跑只是第一步5.1 量化、KV Cache 與推理加速在本地把模型訓練出來之后真正讓人頭疼的是怎么讓它跑得更快、占用資源更少。我一開始直接用 PyTorch 逐 token 生成文本結果慢得離譜。后來才發(fā)現(xiàn)我連最基礎的 KV Cache 都沒有加。KV Cache 的核心思想非常樸素生成下一個 token 時前面所有 token 的 Key 和 Value 矩陣其實已經(jīng)算過了沒必要重新計算。把這些結果緩存起來每次生成只需要算新 token 的相關部分推理速度能提升一個量級。我的小模型加上 KV Cache 之后生成 200 token 的耗時直接降為原來的三分之一。除了 KV Cache量化是另一個大頭。訓練好的模型參數(shù)默認是 32 位浮點數(shù)占內存大、計算慢。把權重轉成 16 位、8 位甚至 4 位整數(shù)就能非常顯著地壓縮模型體積。我做的是一種比較簡單的訓練后量化方法先統(tǒng)計每一層權重的數(shù)值范圍然后把浮點數(shù)映射到整數(shù)范圍推理時再反量化回浮點數(shù)。對于我的小模型從 FP32 壓到 INT8 后模型體積減小了接近四倍生成結果的質量幾乎沒有肉眼可見的下降。量化過程里最容易踩的坑是校準。直接拿模型權重的 min/max 做映射有時候會被離群點帶偏導致量化后某些層輸出異常。我當時用一批驗證集數(shù)據(jù)觀察每層的激活分布選擇合理的截斷范圍之后再量化效果立刻穩(wěn)定了很多。簡單說量化不是簡單的數(shù)學映射它需要數(shù)據(jù)來輔助確定映射參數(shù)。5.2 微調階段的數(shù)據(jù)配比與防災難性遺忘工程化之后我開始嘗試在自己的基礎模型上做微調。初始階段最容易犯的錯誤是用單一任務的數(shù)據(jù)把整個模型反復訓練結果只要訓練步數(shù)稍微多一點模型原有的通用能力就被大幅破壞了。這個現(xiàn)象有一個專門術語叫災難性遺忘我一開始天真地以為只要學習率夠小就沒事后來發(fā)現(xiàn)遠遠不夠。解決災難性遺忘我驗證有效的方法有三個。第一是混入通用數(shù)據(jù)微調數(shù)據(jù)中至少保留 30% 到 50% 的基礎語料讓模型一邊學習新任務一邊維持舊知識。第二是降低微調階段的學習率比預訓練階段低一個數(shù)量級。第三是使用 LoRA 這類參數(shù)高效微調方法只訓練一小部分附加參數(shù)原有參數(shù)盡量不動。LoRA 的原理是把要更新的權重矩陣分解成兩個低秩矩陣訓練時只更新這兩個小矩陣推理時再合并回原權重。這樣不僅顯存占用大幅降低而且對原有參數(shù)的擾動很小。我在實驗中發(fā)現(xiàn)LoRA 加上混合數(shù)據(jù)能把新任務的表現(xiàn)提升不少同時依然能流暢生成普通文本效果比全量微調穩(wěn)定得多。數(shù)據(jù)配比這件事沒有萬能公式但有一個通用的排查方法每次微調完成后在上一個版本模型的測試集上做一次回歸評測如果得分明顯下降就說明數(shù)據(jù)配比或學習率可能有問題。我前前后后調整了三次配比最終確定了一個能兼顧兩邊的比例這個經(jīng)驗值只對我的場景有效但添加回歸驗證集這個方法對任何人都有用。5.3 從單機實驗到服務化部署模型調好之后要把模型給別人用就得做服務化部署。我沒有引入過于復雜的架構而是用了一個非常簡單的思路把模型加載到一個常駐進程里通過 HTTP 接口接收文本請求生成結果后返回。這樣做的好處是模型只在啟動時加載一次不用每次請求都重新初始化。這里有一個非常容易踩的坑服務進程必須設置超時機制。我的模型雖然只有幾百萬參數(shù)但在無 GPU 的環(huán)境下生成一段長文本仍然可能耗時幾秒。如果客戶端超時時間設得太短就會頻繁報錯。解決辦法有兩個一是用流式返回模型每生成一個 token 就推給客戶端一部分二是把生成請求丟到任務隊列里異步處理客戶端去輪詢結果。我實際項目中先用了流式返回交互體驗好很多。另外多進程服務一定要小心模型副本占用的顯存或內存。如果使用gunicorn這類多進程服務器每個 worker 都會加載一份模型副本。我試過默認配置結果服務啟動后內存直接翻了幾倍。解決辦法是根據(jù)實際內存限制 worker 數(shù)量或者改用多線程。這個細節(jié)不親自部署一次是很難從文檔里學到的。6. 我踩過的坑問題排查與避坑速查表6.1 訓練不收斂三個最常見的元兇我的模型第一次訓練時loss 一路降不下去我很確定自己代碼沒有語法錯誤就把鍋甩給了數(shù)據(jù)量太小。后來我系統(tǒng)排查了一遍發(fā)現(xiàn)根本原因和學習率有關我用了條很低的學習率導致模型幾乎在原地踏步。把學習率從1e-4提到1e-3之后loss 立刻開始下降。檢查學習率絕對是排查不收斂的第一動作。第二個常見元兇是數(shù)據(jù)順序沒有打亂。如果每個 epoch 內樣本順序完全不變模型會學到順序上的偽特征訓練 loss 也能降但驗證 loss 非常不穩(wěn)定。打亂數(shù)據(jù)之后問題基本就消失了。第三個元兇是初始化。使用 PyTorch 默認初始化的 Transformer在小模型上訓練時有時會遇到嚴重的梯度異常。我后來把每個線性層的權重標準差按照隱藏層維度的倒數(shù)來縮放也就是非常流行的 small initialization 技巧訓練穩(wěn)定性明顯提升。不要小看這幾行初始化代碼它對整個訓練過程影響巨大。還有一個容易被忽略的元兇損失函數(shù)和標簽的對齊。我在實現(xiàn)交叉熵損失時一開始把 labels 設置成了形狀不匹配的 tensorPyTorch 沒有直接報錯而是靜默廣播了導致模型學到了一個非常奇怪的分布。發(fā)現(xiàn)這個問題后我在訓練循環(huán)里加了一個assert檢查張量形狀遇到形狀不一致的情況立刻中斷。6.2 顯存爆炸與訓練速度過慢的排查思路當我把模型從幾百萬參數(shù)擴到一兩千萬參數(shù)時顯存瞬間告急。第一個原因是注意力矩陣的平方復雜度序列長度為 512 時注意力矩陣是 512 乘以 512顯存開銷隨序列長度平方增長。解決辦法是把最長序列控制在 256或者改用梯度累積減少單批顯存峰值。第二個原因是優(yōu)化器狀態(tài)比想象中更占內存。Adam 優(yōu)化器本身要保存每個參數(shù)的動量變量和方差變量再加上模型參數(shù)和梯度實際顯存占用接近參數(shù)的 7 到 12 倍。所以不要看到模型只有幾百萬參數(shù)就覺得內存肯定夠用?;旌暇扔柧毷呛芎玫木徑馐侄蔚⒁馓荻瓤s放和 loss scaling否則很容易出現(xiàn)溢出導致 loss 變成 NaN。我在排查顯存問題時寫了一個簡單腳本利用 PyTorch 的顯存統(tǒng)計工具在訓練循環(huán)前后打印reserved和allocated內存很快就定位出哪個模塊占用了大頭。這種可視化統(tǒng)計方法比肉眼猜測高效得多。如果你也遇到 OOM建議先用這個思路搞清楚內存分配在哪里再決定是減小序列長度、減小 batch還是換一種注意力實現(xiàn)。6.3 常見問題速查表問題現(xiàn)象可能原因排查方向解決方案訓練 loss 不降學習率過低或過高打印每個 step 的 loss 和學習率使用 warmup cosine 調度調峰值學習率訓練 loss 下降但驗證 loss 不降數(shù)據(jù)泄漏或過擬合檢查驗證集來源觀察訓練/驗證差距重建驗證集混入通用數(shù)據(jù)驗證 loss 劇烈震蕩數(shù)據(jù)順序未打亂 / batch 太小檢查數(shù)據(jù)加載器是否 shuffle開啟 shuffle或增大批次推理速度極慢沒有 KV Cache打印每 token 生成耗時實現(xiàn) KV Cache復用歷史 Key/Value模型生成總是重復采樣策略太保守觀察生成文本的重復率調高 temperature禁用重復 n-gram量化后效果暴跌校準數(shù)據(jù)不足 / 離群點影響對比量化前后每層輸出分布用驗證集做校準設置合理的截斷范圍服務啟動后內存翻倍多進程加載多份模型副本查看進程內存占用限制 worker 數(shù)或改用線程這些坑都是我實際踩過的。第 4 條 KV Cache 的坑尤其隱蔽因為模型小的時候速度差異不明顯一旦把序列長度拉長差距立刻顯現(xiàn)出來。第 6 條量化校準則直接關系到模型能不能在低資源環(huán)境下部署值得多花時間仔細調。最后再分享一個我現(xiàn)在仍然在用的習慣每跑一次實驗之前先把隨機種子固定好然后把數(shù)據(jù)處理流程寫成可復現(xiàn)的腳本任何一步出錯都能從緩存重放。這個習慣在 ai-engineering-from-scratch 項目的后半段幫了我大忙因為改動一旦多了你根本無法判斷效果變化到底來自數(shù)據(jù)、模型版本還是訓練參數(shù)。固定種子、緩存數(shù)據(jù)、記錄每次實驗的配置這三件事堅持做下來整個項目才真正算得上工程而不是一次性的代碼練習。如果你也打算從零開始走一遍 AI 工程我建議你從寫一個最簡單的手寫數(shù)字分類器起步再一步步走到語言模型和部署。這條路不易但走完之后的底氣是任何框架封裝都給不了的。