戰(zhàn):配置驅(qū)動(dòng)的大模型微調(diào)與分布式訓(xùn)練指南)
很多人第一次看見 OpenRig 這個(gè)名字第一反應(yīng)多半是Rig 不是鉆井平臺(tái)的意思嗎一個(gè)開源項(xiàng)目起個(gè)鉆機(jī)的名字做什么我當(dāng)初也這么嘀咕過(guò)直到自己動(dòng)手用這個(gè)開源的大模型微調(diào)框架跑完一輪任務(wù)才明白這個(gè)名字有多貼切。鉆井平臺(tái)是把埋在地層深處的油抽到地面OpenRig 這個(gè)項(xiàng)目做的事也很像——把分布式訓(xùn)練這套原本藏在 DeepSpeed 和各種底層基礎(chǔ)設(shè)施里的重型裝備組裝成一個(gè)能讓你直接操作的完整的訓(xùn)練平臺(tái)。你只需要準(zhǔn)備好數(shù)據(jù)、寫好配置剩下的多卡并行、梯度同步、斷點(diǎn)保存這些事情它都替你扛下來(lái)。這篇文章就直接講我的實(shí)操經(jīng)歷從選型、環(huán)境準(zhǔn)備、配置拆解到單卡跑通、多卡訓(xùn)練、踩坑排錯(cuò)。如果你想用自己的業(yè)務(wù)數(shù)據(jù)微調(diào)一個(gè)開源底座模型又不想一上來(lái)就去啃 DeepSpeed 那套晦澀的文檔那這篇內(nèi)容應(yīng)該能幫你少走很多彎路。后面涉及的所有步驟都是基于我在實(shí)際項(xiàng)目里的用法配置思路同樣適用于其他配置驅(qū)動(dòng)型的微調(diào)框架OpenRig 只是我把整個(gè)鏈路跑通的那個(gè)載體。1. 為什么是 OpenRig微調(diào)工具鏈的現(xiàn)狀與選型邏輯1.1 微調(diào)一個(gè)模型通常有哪幾條路現(xiàn)在的開源大模型微調(diào)生態(tài)基本分成三個(gè)流派各有各的取舍。第一類是全家桶式的整合工具比如市面上常見的微調(diào)工廠類項(xiàng)目。這類工具的好處是開箱即用界面化或命令行化內(nèi)置了大量數(shù)據(jù)集格式和模型適配連 LoRA 的 rank 都幫你預(yù)設(shè)好。缺點(diǎn)是封裝層比較厚一旦訓(xùn)練過(guò)程出了奇怪的問(wèn)題你想深入到分布式那一層去看會(huì)發(fā)現(xiàn)自己被框架擋住了很多底層參數(shù)改不動(dòng)出了問(wèn)題也不容易定位。第二類是自己手寫訓(xùn)練腳本。很多資深工程師習(xí)慣用 HuggingFace 的 Trainer 或者自己寫一個(gè) PyTorch 訓(xùn)練循環(huán)只依賴 transformers 庫(kù)再加一個(gè)推理腳本。這條路最靈活你能控制每一步。但它的成本也很明顯分布式支持要自己接梯度累積、梯度檢查點(diǎn)、混合精度、斷點(diǎn)續(xù)訓(xùn)這些機(jī)制都要自己搭等這些基建全部就緒真正調(diào)模型的時(shí)間已經(jīng)被壓縮掉一大半。第三類是直接使用 DeepSpeed 這類分布式引擎提供的接口。它的能力是最完整的像 ZeRO 分片、CPU offload、NVMe offload、張量并行這些重型能力都有。但 DeepSpeed 的配置文件即使到今天也仍然給人一種可編程基礎(chǔ)設(shè)施的感覺(jué)——你必須非常清楚每個(gè)字段背后的內(nèi)存模型和通信模式才敢動(dòng)手改。它面向的是平臺(tái)工程師而不是每天要跑好幾版實(shí)驗(yàn)的微調(diào)算法工程師。1.2 OpenRig 的定位一臺(tái)配置驅(qū)動(dòng)的訓(xùn)練鉆井平臺(tái)OpenRig 給我的感覺(jué)是它刻意站在了全家桶和裸 DeepSpeed中間的位置。項(xiàng)目的名字取得很直白Open 是開放Rig 是鉆井平臺(tái)上那種重型設(shè)備的總稱。它不是一個(gè)用來(lái)發(fā)明新訓(xùn)練算法的框架而是把分布式訓(xùn)練里那些高頻、易錯(cuò)的動(dòng)作標(biāo)準(zhǔn)化成配置和命令。你面對(duì)的仍然是一個(gè)相對(duì)簡(jiǎn)單的 YAML 文件但在這個(gè)配置文件里你能感受到 DeepSpeed 的影子——ZeRO 的級(jí)別、offload 的策略、梯度累積的步數(shù)這些都沒(méi)有被藏起來(lái)而是明明白白地?cái)[在配置項(xiàng)里讓你決定。這種設(shè)計(jì)解決了我的一個(gè)很現(xiàn)實(shí)的痛點(diǎn)實(shí)驗(yàn)的可復(fù)現(xiàn)性。之前我用腳本方式訓(xùn)練時(shí)經(jīng)常出現(xiàn)這種情況——一個(gè)實(shí)驗(yàn)跑完過(guò)兩周再想復(fù)現(xiàn)發(fā)現(xiàn)當(dāng)時(shí)的命令行參數(shù)已經(jīng)不全了或者代碼已經(jīng)改得面目全非。用 OpenRig 這類配置驅(qū)動(dòng)的方式整個(gè)實(shí)驗(yàn)的全部要素都固化在了一份配置文件里。模型路徑、數(shù)據(jù)路徑、訓(xùn)練參數(shù)、分布式策略全都寫在同一處代碼基本不需要改。跑完一個(gè)實(shí)驗(yàn)把配置文件歸檔就等于把這次實(shí)驗(yàn)的 DNA 保存下來(lái)了。1.3 我最終選擇它的三個(gè)判斷標(biāo)準(zhǔn)這里順帶說(shuō)一下我挑微調(diào)框架的三個(gè)標(biāo)準(zhǔn)供你參考。第一是可讀性。配置文件的每一行我自己能不能不看文檔就理解它是在做什么。如果一份配置里充滿了只有框架作者才懂的縮寫那我用它的熱情會(huì)立刻減半。OpenRig 的配置項(xiàng)命名接近 DeepSpeed 和 transformers 的習(xí)慣經(jīng)歷過(guò)這兩個(gè)生態(tài)的人幾乎可以零成本上手。第二是可控性。訓(xùn)練中途我想切換到 ZeRO stage 3或者想臨時(shí)打開 CPU offload或者想調(diào)整梯度累積步數(shù)這些改動(dòng)是否只需要改配置而不需要改代碼。OpenRig 在這點(diǎn)上是滿足我的——分布式策略的切換確實(shí)只動(dòng) YAML 就行。第三是可監(jiān)控性。訓(xùn)練期間我要能拿到每個(gè) GPU 的顯存占用、loss 曲線、樣本吞吐量。這一點(diǎn)我單獨(dú)放在后面一章節(jié)里展開講因?yàn)樗鼪Q定了你排障的效率。2. 把 rig 立起來(lái)環(huán)境準(zhǔn)備與顯存估算2.1 先算賬你的顯卡夠不夠跑全參數(shù)微調(diào)這一步是整個(gè)項(xiàng)目里最不該跳過(guò)的地方。很多人上來(lái)就 pip install結(jié)果數(shù)據(jù)跑起來(lái)幾分鐘后直接 OOM才回頭研究顯存浪費(fèi)時(shí)間。顯存估算其實(shí)有一個(gè)很簡(jiǎn)單的經(jīng)驗(yàn)公式。全參數(shù)微調(diào)的時(shí)候顯存消耗主要來(lái)自四塊項(xiàng)目量級(jí)估算以 7B 模型、FP16 為例模型權(quán)重參數(shù)量 × 2 字節(jié)約 14GB梯度參數(shù)量 × 2 字節(jié)約 14GBAdam 優(yōu)化器狀態(tài)參數(shù)量 × 12 字節(jié)FP32 副本 動(dòng)量 方差約 84GB激活值前向計(jì)算中間結(jié)果取決于 batch size 和序列長(zhǎng)度通常數(shù) GB 到幾十 GB看這個(gè)表你就明白了7B 模型做全參數(shù)微調(diào)光權(quán)重、梯度和優(yōu)化器狀態(tài)加起來(lái)就已經(jīng)超過(guò) 100GB 顯存單張 24GB 的消費(fèi)級(jí)顯卡根本放不下。所以如果你手頭只有一兩張消費(fèi)卡又想跑 7B 以上模型第一反應(yīng)應(yīng)該是做 LoRA 或 QLoRA而不是硬扛全參數(shù)微調(diào)。LoRA 只需要訓(xùn)練低秩適配器優(yōu)化器狀態(tài)只跟 LoRA 參數(shù)有關(guān)顯存需求可以壓到原來(lái)的幾分之一。反過(guò)來(lái)如果你有 4 張或 8 張 80GB 的加速卡且模型在 7B 到 13B 這個(gè)量級(jí)那全參數(shù)微調(diào)就是可行的配合 ZeRO stage 2 或 stage 3顯存壓力會(huì)進(jìn)一步緩解。我當(dāng)時(shí)就是先拿 OpenRig 在一張卡上用 LoRA 把流程跑通然后再上多卡做全參數(shù)微調(diào)這樣每一步的變量都足夠少出了問(wèn)題容易定位。2.2 軟件環(huán)境版本搭配決定了你能走多遠(yuǎn)OpenRig 本身不是一個(gè)獨(dú)立于 PyTorch 生態(tài)的項(xiàng)目它的底座仍然是標(biāo)準(zhǔn)的技術(shù)棧。我用的環(huán)境是 Python 3.10、CUDA 12.1、PyTorch 2.1、DeepSpeed 0.12 左右transformers 保持在一個(gè)相對(duì)較新的版本。這里我踩過(guò)一個(gè)坑transformers 版本如果和底座模型的 tokenizer 格式不匹配會(huì)出現(xiàn)加載模型時(shí)報(bào)莫名其妙的 key 錯(cuò)誤而且錯(cuò)誤信息通常不會(huì)直接告訴你版本不兼容而是報(bào)某個(gè) shape 對(duì)不上。我的建議是首先把 PyTorch 和 CUDA 版本對(duì)齊這兩者不一致會(huì)導(dǎo)致整個(gè)訓(xùn)練在第一步就出錯(cuò)。其次把 transformers 的版本固定在某個(gè)你測(cè)過(guò)的版本上不要隨手升級(jí)到最新版。最后OpenRig 的安裝我用的是本地源碼方式也就是 git clone 下來(lái)之后 pip install -r requirements.txt。這樣做的理由很實(shí)際微調(diào)框架本身迭代快升級(jí)有可能改變配置項(xiàng)的行為固定源碼版本能讓實(shí)驗(yàn)真正可復(fù)現(xiàn)。2.3 目錄規(guī)劃別把所有東西都堆在一個(gè)文件夾里訓(xùn)練項(xiàng)目的目錄結(jié)構(gòu)看起來(lái)是小事但做實(shí)驗(yàn)做到后面你就會(huì)知道一個(gè)干凈的目錄結(jié)構(gòu)能救你多少時(shí)間。我習(xí)慣這樣組織project/ ├── configs/ # 每次實(shí)驗(yàn)的 YAML 配置歸檔 ├── data/ # train.jsonl / valid.jsonl ├── models/ # 底座模型權(quán)重本地路徑 ├── output/ # 訓(xùn)練產(chǎn)物checkpoint、tokenizer、日志 └── scripts/ # 啟動(dòng)腳本、評(píng)估腳本每次實(shí)驗(yàn)跑完我會(huì)把對(duì)應(yīng)的 YAML 配置復(fù)制到 configs 目錄以時(shí)間名稱命名。這個(gè)習(xí)慣讓我在三個(gè)月后仍然能準(zhǔn)確判斷這次實(shí)驗(yàn)到底用的是什么學(xué)習(xí)率和什么數(shù)據(jù)分布而不是靠聊天記錄里的只言片語(yǔ)去猜。3. 配置文件里的門道核心字段逐個(gè)拆解3.1 模型與數(shù)據(jù)讓訓(xùn)練有東西可學(xué)OpenRig 的配置里模型部分通常會(huì)指定一個(gè)model_name_or_path。我強(qiáng)烈建議你把底座模型下載到本地目錄然后用本地路徑加載。理由有兩個(gè)一是訓(xùn)練時(shí)如果每次都走 HuggingFace 下載網(wǎng)絡(luò)抖動(dòng)會(huì)導(dǎo)致加載時(shí)間變得很長(zhǎng)二是本地路徑天然就是不可變快照模型版本不會(huì)在你訓(xùn)練到一半時(shí)突然變化。數(shù)據(jù)集方面最常用的是 JSONL 格式每一行是一條樣本。以指令微調(diào)為例行內(nèi)通常包含 instruction、input、output 這幾個(gè)字段OpenRig 在加載數(shù)據(jù)時(shí)會(huì)根據(jù)你配置的 prompt 模板來(lái)拼接。舉個(gè)例子一條樣本可能是{instruction: 解釋一下什么是梯度累積, input: , output: 梯度累積是將多個(gè)小 batch 的梯度累加后再進(jìn)行一次參數(shù)更新的策略。}這里有個(gè)非常關(guān)鍵但容易被忽略的細(xì)節(jié)拼接模板的時(shí)候output部分一定要作為標(biāo)簽被參與 loss 計(jì)算而instruction部分通常要 mask 掉也就是說(shuō)模型在生成答復(fù)之前看到的所有文本都不應(yīng)該反向傳播誤差。如果你的框架沒(méi)有自動(dòng)處理這一點(diǎn)你需要自己檢查拼出來(lái)的訓(xùn)練樣本確認(rèn) loss 只在 output 部分計(jì)算。我見過(guò)太多人因?yàn)橛?xùn)練模板拼錯(cuò)模型最后學(xué)會(huì)的是復(fù)述問(wèn)題而不是回答問(wèn)題。3.2 訓(xùn)練超參數(shù)從 lr 到 batch size 的聯(lián)動(dòng)關(guān)系很多人第一次接觸配置文件會(huì)一個(gè)個(gè)參數(shù)單獨(dú)去理解這樣容易忽略它們之間的聯(lián)動(dòng)關(guān)系。舉一個(gè)最典型的例子global batch size。global_batch_size per_device_batch_size × gradient_accumulation_steps × 顯卡數(shù)量這公式?jīng)Q定了每一次參數(shù)更新的實(shí)際樣本量。假設(shè)你每張卡per_device_batch_size2梯度累積步數(shù)為 8使用 4 張卡那么 global batch size 就是 2 × 8 × 4 64。這個(gè)值會(huì)直接影響訓(xùn)練的穩(wěn)定性以及學(xué)習(xí)率的選擇。常見的做法是當(dāng) global batch size 變大時(shí)learning rate 也可以適當(dāng)調(diào)大但不要一次調(diào)太多否則 loss 曲線直接起飛。學(xué)習(xí)率的量級(jí)全參數(shù)微調(diào)我一般從 1e-5 到 2e-5 起步LoRA 微調(diào)則通常從 1e-4 到 3e-4 起步。這里面的道理在于LoRA 每次只更新一小部分新增參數(shù)可以承受更大的更新步長(zhǎng)而全參數(shù)微調(diào)動(dòng)的是整個(gè)模型的全部權(quán)重步子邁大了很容易破壞底座模型已經(jīng)學(xué)到的能力。max_length這個(gè)參數(shù)也要好好選。它決定了每條樣本在 token 化后會(huì)被截?cái)嗟蕉嚅L(zhǎng)。我實(shí)戰(zhàn)中的經(jīng)驗(yàn)是訓(xùn)練數(shù)據(jù)里大約 5% 到 10% 的樣本會(huì)超過(guò)你設(shè)定的長(zhǎng)度這是正常的但如果超過(guò)一半的樣本都被截?cái)嗟?max_length說(shuō)明這個(gè)值設(shè)得太短模型根本看不完你給它的完整上下文。你需要先對(duì)數(shù)據(jù)集做一次長(zhǎng)度分布統(tǒng)計(jì)再確定 max_length 的數(shù)值。3.3 分布式策略ZeRO 的每一級(jí)都在做什么既然 OpenRig 定位是鉆井平臺(tái)那么分布式策略就是它身上最核心的機(jī)械設(shè)備。DeepSpeed 的 ZeRO 分為幾個(gè) stage很多人每次都靠死記硬背我換個(gè)說(shuō)法讓你一次記住。ZeRO stage 1把優(yōu)化器狀態(tài)切分到多張卡上。每張卡只管一部分參數(shù)的優(yōu)化器狀態(tài)算完梯度后需要跨卡做一次通信。這是性價(jià)比最高的起步選項(xiàng)顯存省得不多但實(shí)現(xiàn)簡(jiǎn)單、通信開銷低。ZeRO stage 2在 stage 1 基礎(chǔ)上把梯度也做切分。每張卡只保存自己負(fù)責(zé)的那部分梯度進(jìn)一步降低顯存。ZeRO stage 3把模型參數(shù)本身也切分到多張卡。每一層只存在于某一張卡上用到時(shí)才通過(guò)通信把參數(shù)廣播給其他卡。顯存省得最多但通信開銷也最大訓(xùn)練吞吐量往往會(huì)有明顯下降。配置時(shí)你需要根據(jù)顯存壓力來(lái)選擇。我的經(jīng)驗(yàn)是如果顯存足夠優(yōu)先 stage 2因?yàn)樗∠碌娘@存足以支撐全參數(shù)微調(diào)而且訓(xùn)練速度比 stage 3 快很多。只有當(dāng)模型大到 stage 2 也放不下時(shí)才上 stage 3或者配合 CPU offload。offload 是把優(yōu)化器狀態(tài)或參數(shù)搬到內(nèi)存里顯存是省了但訓(xùn)練速度會(huì)受到明顯影響能不用就盡量不用。4. 實(shí)操記錄從單卡驗(yàn)證到多卡訓(xùn)練4.1 先跑通再求快單卡小模型驗(yàn)證整個(gè)鏈路我在正式提交大規(guī)模訓(xùn)練任務(wù)之前有一個(gè)雷打不動(dòng)的習(xí)慣先用單卡、小模型、小數(shù)據(jù)量把整條鏈路跑一遍。具體來(lái)說(shuō)選一個(gè)比目標(biāo)模型小一兩檔的模型數(shù)據(jù)只取幾百條把 epoch 設(shè)為 1跑幾個(gè) step 看日志輸出是否正常。這一步的目標(biāo)不是訓(xùn)練出什么效果而是確認(rèn)三件事——數(shù)據(jù)加載正常、prompt 模板拼出來(lái)的樣本內(nèi)容確實(shí)正確、loss 在第一步后是下降的而不是變成 NaN。這里有個(gè)小技巧我會(huì)在配置里打開一個(gè)打印樣本的開關(guān)把 token 化之后拼接出來(lái)的文本直接打印到控制臺(tái)。肉眼確認(rèn)一下 instruction 和 output 中間沒(méi)有混入奇怪的換行符或特殊 token。很多排錯(cuò)工作在這一步就能提前終結(jié)。4.2 多卡啟動(dòng)命令幾個(gè)容易寫錯(cuò)的地方鏈路驗(yàn)證完畢就可以上多卡了。OpenRig 這類框架通常依賴 DeepSpeed 的啟動(dòng)器來(lái)分配進(jìn)程。我常用的啟動(dòng)命令長(zhǎng)這樣deepspeed --num_gpus 4 ./run_train.py --config ./configs/sft_7b.yaml也可以使用 torchrun 的方式OpenRig 基于 PyTorch 生態(tài)通常兩種都能支持torchrun --nproc_per_node4 ./run_train.py --config ./configs/sft_7b.yaml第一次跑多卡時(shí)最容易出的問(wèn)題反而不是命令本身而是環(huán)境變量。比如CUDA_VISIBLE_DEVICES設(shè)錯(cuò)了會(huì)導(dǎo)致明明有 8 張卡實(shí)際只有 2 張可用。排查這類問(wèn)題啟動(dòng)前先用nvidia-smi確認(rèn)當(dāng)前機(jī)器上卡的編號(hào)和顯存占用情況再設(shè)置對(duì)應(yīng)的可見變量能省很多事。多機(jī)訓(xùn)練時(shí)還需要額外注意節(jié)點(diǎn)之間的網(wǎng)絡(luò)互通。第一次跑分布式訓(xùn)練我不建議直接開多機(jī)先把單機(jī)多卡跑穩(wěn)。單機(jī)多卡的通信走的是 PCIe 或 NVLink穩(wěn)定性和速度都有保障多機(jī)多卡一旦涉及網(wǎng)卡、防火墻、主機(jī)名解析問(wèn)題的復(fù)雜度會(huì)瞬間上一個(gè)量級(jí)。4.3 訓(xùn)練過(guò)程的監(jiān)控loss 之外還要看什么很多人只看 lossloss 一降就覺(jué)得萬(wàn)事大吉。實(shí)際上分布式訓(xùn)練中有三個(gè)指標(biāo)應(yīng)該時(shí)刻盯著。第一是 GPU 利用率。如果你發(fā)現(xiàn)某張卡的利用率一直很低而其他卡很高很可能數(shù)據(jù)加載成了瓶頸或者數(shù)據(jù)在卡間分配不均。第二是顯存占用。如果顯存占用在訓(xùn)練過(guò)程中一路緩慢上漲而不是穩(wěn)定在某個(gè)值附近大概率有顯存泄漏跑兩三個(gè)小時(shí)后 OOM 幾乎是必然的。第三是吞吐量比如每秒處理多少樣本或者每秒處理多少 token。這個(gè)指標(biāo)尤其重要它能告訴你當(dāng)前配置下的訓(xùn)練成本是多少方便你決定是否要做調(diào)整。我習(xí)慣用一個(gè)終端專門開一個(gè)監(jiān)控面板實(shí)時(shí)刷新每張卡的利用率、顯存、溫度然后在訓(xùn)練日志里觀察 loss 和吞吐量。訓(xùn)練的前一個(gè)小時(shí)不建議走開因?yàn)榍耙粋€(gè)小時(shí)往往是問(wèn)題的高發(fā)期。4.4 checkpoint 的管理斷電斷網(wǎng)都不怕OpenRig 這類框架在做 checkpoint 保存時(shí)會(huì)同時(shí)保存模型權(quán)重、優(yōu)化器狀態(tài)、學(xué)習(xí)率調(diào)度器狀態(tài)、當(dāng)前步數(shù)這些內(nèi)容。這樣才能做到真正的斷點(diǎn)續(xù)訓(xùn)。你在配置里設(shè)置好保存間隔比如每 500 步保存一次。訓(xùn)練中斷后重啟命令里指定從最近的 checkpoint 恢復(fù)即可。有一個(gè)細(xì)節(jié)值得留意如果你改了配置里的模型結(jié)構(gòu)相關(guān)參數(shù)比如改了 max_length 或者換了一個(gè)不同的數(shù)據(jù)集那從舊 checkpoint 恢復(fù)時(shí)可能因?yàn)?shape 不匹配而報(bào)錯(cuò)。斷點(diǎn)續(xù)訓(xùn)的前提是你恢復(fù)的是一個(gè)相同實(shí)驗(yàn)的現(xiàn)場(chǎng)而不是一個(gè)半路改了配置的新實(shí)驗(yàn)。我在項(xiàng)目里會(huì)為每次實(shí)驗(yàn)單獨(dú)建目錄checkpoint 按實(shí)驗(yàn)?zāi)夸浉綦x這樣就不會(huì)出現(xiàn)恢復(fù)錯(cuò)了現(xiàn)場(chǎng)的低級(jí)錯(cuò)誤。5. 踩坑記錄OOM、loss 不降、訓(xùn)練卡死5.1 OOM 的三種常見場(chǎng)景顯存溢出大概是微調(diào)過(guò)程中出現(xiàn)頻率最高的問(wèn)題。它有三個(gè)主要來(lái)源處理方式完全不同。第一種是激活值導(dǎo)致的 OOM。前向計(jì)算中每一層的中間結(jié)果都需要占用顯存序列越長(zhǎng)、batch 越大激活值占用越高。解決方法是打開梯度檢查點(diǎn)gradient checkpointing用重計(jì)算的方式騰出顯存或者減小max_length與per_device_batch_size。第二種是優(yōu)化器狀態(tài)導(dǎo)致的 OOM。這一般發(fā)生在全參數(shù)微調(diào)場(chǎng)景權(quán)重和優(yōu)化器狀態(tài)加起來(lái)超過(guò)了顯存。解決方法是升級(jí) ZeRO stage或開啟 offload或者換成 LoRA 方案。第三種是數(shù)據(jù)長(zhǎng)度極端導(dǎo)致的 OOM。假設(shè)你的max_length設(shè)為 2048數(shù)據(jù)里絕大多數(shù)樣本都是幾百 token但偏偏有極少數(shù)樣本長(zhǎng)度逼近 2048那這少數(shù)幾個(gè)樣本就會(huì)讓顯存占用出現(xiàn)尖峰導(dǎo)致 OOM。這種問(wèn)題的特點(diǎn)是不穩(wěn)定——有時(shí)跑幾十步?jīng)]事忽然某一步就炸了。我的經(jīng)驗(yàn)和做法是在數(shù)據(jù)處理階段就按長(zhǎng)度做直方圖統(tǒng)計(jì)把長(zhǎng)度明顯超標(biāo)的尾巴樣本單獨(dú)過(guò)濾掉而不是把風(fēng)險(xiǎn)留在訓(xùn)練過(guò)程中。5.2 loss 不降或者震蕩先查數(shù)據(jù)再查參數(shù)訓(xùn)練開始后最讓人焦慮的莫過(guò)于 loss 長(zhǎng)時(shí)間不降。這種問(wèn)題我的排障順序永遠(yuǎn)是先查數(shù)據(jù)再查代碼最后才動(dòng)訓(xùn)練參數(shù)。數(shù)據(jù)層面第一個(gè)要確認(rèn)的是模板拼接是否正確。把打印出來(lái)的樣本逐條人工檢查確認(rèn) input 和 output 沒(méi)有倒置。第二個(gè)是確認(rèn)數(shù)據(jù)是否被隨機(jī)打亂。如果訓(xùn)練數(shù)據(jù)是純按類別排列的前面幾千條都是同一類樣本模型在早期只會(huì)看到單一分布loss 曲線就會(huì)呈現(xiàn)出奇怪的周期性波動(dòng)甚至長(zhǎng)時(shí)間下不去。代碼層面確認(rèn)是否只有 output 部分參與了 loss 計(jì)算。如果你把整個(gè)拼好的文本都拿去做交叉熵模型的目標(biāo)函數(shù)會(huì)變成預(yù)測(cè)問(wèn)題本身訓(xùn)練出來(lái)的模型效果會(huì)非常差表現(xiàn)為生成時(shí)大量復(fù)述。如果數(shù)據(jù)和代碼都沒(méi)問(wèn)題再考慮學(xué)習(xí)率。全參數(shù)微調(diào)學(xué)習(xí)率過(guò)大時(shí)loss 會(huì)劇烈震蕩過(guò)小時(shí)loss 則下降得極其緩慢。有一個(gè)比較實(shí)用的判據(jù)第一個(gè) step 的 loss 應(yīng)該和隨機(jī)初始化的困惑度差不太多如果第一個(gè) step 就出現(xiàn)極大或極小的數(shù)值通常是數(shù)據(jù)或精度設(shè)置出了問(wèn)題。5.3 分布式訓(xùn)練里的假死不是卡死是等待多卡訓(xùn)練時(shí)最讓人崩潰的往往不是報(bào)錯(cuò)而是整個(gè)訓(xùn)練看起來(lái)完全停住日志半天不動(dòng)。這里我先提醒一句分布式訓(xùn)練里進(jìn)程通信的等待是常態(tài)并不是真的死機(jī)所以不要立刻 kill 進(jìn)程。你首先要確定的是它在通信等待還是真的無(wú)響應(yīng)。如果是第一次多卡通信NCCL 初始化階段經(jīng)常要花一兩分鐘建立連接日志停留時(shí)間較長(zhǎng)是正常的。如果超過(guò)十分鐘仍然一動(dòng)不動(dòng)通常問(wèn)題出在網(wǎng)絡(luò)層面——比如主機(jī)名無(wú)法互相解析、防火墻阻擋了通信端口或者網(wǎng)卡選錯(cuò)。這時(shí)可以把NCCL_DEBUGINFO打開重新啟動(dòng)日志里會(huì)輸出非常詳細(xì)的通信過(guò)程你能看到是哪個(gè)節(jié)點(diǎn)連不上。還有一個(gè)容易被忽略的問(wèn)題多卡訓(xùn)練時(shí)各卡的工作負(fù)載不均衡。DeepSpeed 在 ZeRO 下會(huì)把各層分配到不同卡上如果數(shù)據(jù)長(zhǎng)度分布不均勻某些卡負(fù)責(zé)的層激活值特別大就會(huì)成為顯存瓶頸導(dǎo)致其他卡在等它算完整體吞吐量上不去。這種隱性不均衡不像 OOM 那樣直接報(bào)錯(cuò)但會(huì)表現(xiàn)為訓(xùn)練速度遠(yuǎn)低于預(yù)期。應(yīng)對(duì)辦法是盡量讓數(shù)據(jù)長(zhǎng)度均衡或者在數(shù)據(jù)處理階段按長(zhǎng)度做分桶 padding。6. 跑通之后的收尾實(shí)驗(yàn)管理、效果評(píng)估與下一步擴(kuò)展6.1 實(shí)驗(yàn)管理讓每個(gè)結(jié)果都經(jīng)得起回溯訓(xùn)練跑完僅僅是開始的一半。我會(huì)為每一次成功實(shí)驗(yàn)做三件事第一把最終使用的配置文件復(fù)制一份存檔文件名加上實(shí)驗(yàn)編號(hào)和日期第二把訓(xùn)練日志保存下來(lái)確保 loss 曲線可以被后續(xù)可視化第三記錄一張簡(jiǎn)短的結(jié)果卡包括數(shù)據(jù)集規(guī)模、訓(xùn)練步數(shù)、最終 loss、驗(yàn)證集上的評(píng)測(cè)指標(biāo)、模型輸出示例。這三樣?xùn)|西合起來(lái)才是一次實(shí)驗(yàn)的完整閉環(huán)。為什么要強(qiáng)調(diào)這一點(diǎn)因?yàn)槲⒄{(diào)項(xiàng)目里你大概率要面對(duì)數(shù)十次乃至上百次實(shí)驗(yàn)。如果沒(méi)有這套歸檔習(xí)慣你很容易陷入這次效果好但不知道為什么好的境地。配置驅(qū)動(dòng)框架的最大優(yōu)勢(shì)就是所有變量都在配置里只要你同步歸檔任何一組結(jié)果都能精確復(fù)現(xiàn)。6.2 評(píng)估模型不要只信 loss要跑到生成那里去看很多初學(xué)者的通病是看到訓(xùn)練 loss 下降就覺(jué)得模型已經(jīng)訓(xùn)練好了。實(shí)際做指令微調(diào)時(shí)loss 和真實(shí)生成質(zhì)量之間并不總是完全一致。原因很簡(jiǎn)單loss 是一個(gè) token 級(jí)別的平均交叉熵它反映的是模型對(duì)訓(xùn)練分布的整體擬合程度但不直接代表模型在真實(shí)請(qǐng)求上的表現(xiàn)。我的做法是保留一份專門的評(píng)估集里面的樣本在訓(xùn)練時(shí)絕對(duì)沒(méi)有出現(xiàn)過(guò)。訓(xùn)練結(jié)束后我用這批樣本的 instruction 部分去觸發(fā)模型生成然后人工檢查生成結(jié)果。重點(diǎn)看三件事格式是否符合預(yù)期、是否出現(xiàn)復(fù)述問(wèn)題而不是回答問(wèn)題、生成內(nèi)容是否安全可控。做完這輪人工評(píng)估才敢把模型交給下游推理鏈路。6.3 下一步擴(kuò)展從 SFT 到繼續(xù)預(yù)訓(xùn)練、長(zhǎng)文本、部署銜接跑通一輪 SFT 后OpenRig 這類配置驅(qū)動(dòng)框架的擴(kuò)展空間還是挺大的。最常見的方向有三個(gè)。方向一是繼續(xù)預(yù)訓(xùn)練。SFT 用的是指令問(wèn)答數(shù)據(jù)繼續(xù)預(yù)訓(xùn)練用的是大量領(lǐng)域文檔數(shù)據(jù)這兩種任務(wù)的配置差異主要在數(shù)據(jù)格式、學(xué)習(xí)率和訓(xùn)練步數(shù)。你只需要準(zhǔn)備符合格式的語(yǔ)料調(diào)整訓(xùn)練參數(shù)框架層面基本不用改動(dòng)。方向二是更長(zhǎng)上下文的訓(xùn)練。底座模型的默認(rèn)上下文長(zhǎng)度往往有限如果你想針對(duì)長(zhǎng)文檔場(chǎng)景做適配需要把max_length調(diào)大同時(shí)開啟 DeepSpeed 的序列并行相關(guān)能力顯存壓力會(huì)增加不少可能需要配合梯度檢查點(diǎn)來(lái)平衡。我記得自己在做長(zhǎng)文本適配時(shí)光是顯存估算就來(lái)回調(diào)了好幾輪最后還是靠減小 batch size 才穩(wěn)定跑起來(lái)。方向三和部署銜接。微調(diào)產(chǎn)物如果是 LoRA 這類增量權(quán)重推理前需要把增量權(quán)重合并回底座模型或者用支持 LoRA 的推理框架直接加載。這一步看似簡(jiǎn)單卻經(jīng)常因?yàn)榘姹静灰恢庐a(chǎn)生詭異的問(wèn)題。給底座模型和微調(diào)框架做版本鎖定的價(jià)值在部署那一刻會(huì)體現(xiàn)得淋漓盡致只要版本一致合并動(dòng)作就是確定的不會(huì)有任何意外。我自己在跑完第一輪完整的 OpenRig 微調(diào)項(xiàng)目時(shí)最大的感受其實(shí)不是工具好用而是終于不用把大量精力花在分布式訓(xùn)練的基建上了。之前我至少要花上兩三天去配置 DeepSpeed、調(diào)試多卡啟動(dòng)、處理 checkpoint 恢復(fù)用配置驅(qū)動(dòng)的框架之后這些時(shí)間被壓縮到半天以內(nèi)。省下來(lái)的時(shí)間全都投在了數(shù)據(jù)清洗、樣本審查和生成效果評(píng)估上——而這些恰恰是微調(diào)項(xiàng)目里真正決定最終質(zhì)量的部分。如果你正準(zhǔn)備開始自己的微調(diào)項(xiàng)目我的建議很簡(jiǎn)單一開始不要追求最大的模型、最多的卡先把一套小模型、小數(shù)據(jù)量、完整鏈路的方案跑通。把 rig 架穩(wěn)了再往上加設(shè)備才不會(huì)被各種地基問(wèn)題反復(fù)絆倒。