實(shí)戰(zhàn):從環(huán)境搭建到推理部署全流程指南)
簡(jiǎn)介大語(yǔ)言模型微調(diào)是行業(yè)落地的核心技術(shù)環(huán)節(jié)。LoRA作為一種高效參數(shù)微調(diào)方法通過(guò)凍結(jié)基座模型權(quán)重并訓(xùn)練低秩增量矩陣顯著降低顯存占用使6B級(jí)別模型在普通顯卡上也能完成業(yè)務(wù)定制。ChatGLM3-6B作為中文場(chǎng)景中表現(xiàn)優(yōu)秀的基礎(chǔ)模型配合LoRA可在知識(shí)問(wèn)答、領(lǐng)域?qū)υ挼热蝿?wù)上快速適配。本文完整梳理了從環(huán)境搭建、數(shù)據(jù)清洗、訓(xùn)練參數(shù)配置到推理驗(yàn)證的工程流程并針對(duì)顯存溢出、過(guò)擬合、loss異常等常見(jiàn)問(wèn)題給出可操作的排查方案為開(kāi)發(fā)者提供一套可直接復(fù)現(xiàn)的微調(diào)實(shí)踐路徑。 做AI應(yīng)用落地這幾年我最大的感受是模型底座要選對(duì)但真正決定業(yè)務(wù)體驗(yàn)的往往是最后那一步定向調(diào)教。ChatGLM3-6B是我常用的底座之一通用對(duì)話、知識(shí)問(wèn)答都能打可一旦牽扯到具體的行業(yè)術(shù)語(yǔ)、輸出格式、角色人設(shè)直接拿原版模型上線就會(huì)顯得官方而空洞。這篇就把我的完整跑通方案記錄下來(lái)——基于ChatGLM3-6B模型用LoRA方法做微調(diào)從環(huán)境搭建、數(shù)據(jù)處理到訓(xùn)練、推理每一步都給出可復(fù)現(xiàn)的源碼和參數(shù)并且把我在實(shí)操中踩過(guò)的坑一并講清楚。先說(shuō)這套方案適合誰(shuí)。如果你準(zhǔn)備在自己的顯卡上把一個(gè)大模型拉向垂直場(chǎng)景手頭有一張24G顯存左右的卡或者幾張消費(fèi)級(jí)卡做數(shù)據(jù)并行想把模型從什么都能聊變成你的領(lǐng)域?qū)<夷沁@篇文章就是給你準(zhǔn)備的。LoRA的核心價(jià)值在于不跟那60億參數(shù)硬碰硬而是訓(xùn)練一組規(guī)模極小的增量矩陣讓模型在保持原有能力的同時(shí)學(xué)會(huì)你要的說(shuō)話方式。我的實(shí)測(cè)結(jié)論是在1千到1萬(wàn)條高質(zhì)量業(yè)務(wù)數(shù)據(jù)下LoRA微調(diào)的效果跟全參微調(diào)已經(jīng)非常接近但訓(xùn)練資源需求卻低了一個(gè)數(shù)量級(jí)。1. 項(xiàng)目背景與整體設(shè)計(jì)思路1.1 為什么選ChatGLM3-6B當(dāng)?shù)鬃疫xChatGLM3-6B有幾個(gè)非常實(shí)際的考量。首先是參數(shù)規(guī)模卡位很合適6B這個(gè)量級(jí)既不像70B那樣需要多卡集群伺候也不會(huì)像幾百M(fèi)的小模型那樣微調(diào)完還是顯得笨。它在中文上的表現(xiàn)尤其是在指令遵循和上下文理解方面明顯好于同體量的多數(shù)開(kāi)源模型這對(duì)我做的業(yè)務(wù)問(wèn)答、文本潤(rùn)色、結(jié)構(gòu)化抽取這類(lèi)任務(wù)來(lái)說(shuō)特別重要。其次ChatGLM3的對(duì)話協(xié)議是固定的一套模版格式。官方微調(diào)腳本里已經(jīng)對(duì)這種格式做了完整支持包括system prompt、多輪歷史、工具調(diào)用這些字段。這意味著你在微調(diào)時(shí)不需要自己發(fā)明輪子只要把數(shù)據(jù)整理成對(duì)應(yīng)的對(duì)話結(jié)構(gòu)剩下的交給模型。最后社區(qū)的生態(tài)成熟度也是個(gè)隱性優(yōu)勢(shì)。transformers、peft、datasets這些主流庫(kù)對(duì)ChatGLM3支持得很到位踩坑時(shí)隨便一搜就能找到答案這一點(diǎn)對(duì)新手來(lái)說(shuō)價(jià)值很大。說(shuō)到生態(tài)我還要多提一句很多人以為選底座只看榜單分?jǐn)?shù)但實(shí)際開(kāi)發(fā)里庫(kù)的兼容性和社區(qū)活躍度往往是決定能否按時(shí)交付的關(guān)鍵。我早期用過(guò)一些小眾模型文檔不全、加載報(bào)錯(cuò)、社區(qū)沒(méi)人回答折騰一周還在環(huán)境階段。后來(lái)統(tǒng)一收斂到ChatGLM3這類(lèi)有官方微調(diào)倉(cāng)庫(kù)、有大量第三方教程的模型上交付效率明顯上來(lái)了。1.2 選LoRA而不選全參微調(diào)核心邏輯在哪這里我要把方案選型講透因?yàn)楹芏喑鯇W(xué)者上來(lái)就糾結(jié)我到底該用LoRA還是全參微調(diào)。我自己的判斷標(biāo)準(zhǔn)很簡(jiǎn)單先看你的硬件再看你的數(shù)據(jù)量。全參微調(diào)意味著6B模型的所有參數(shù)都要參與梯度更新。以AdamW優(yōu)化器為例光優(yōu)化器狀態(tài)就要占掉兩倍模型參數(shù)量的內(nèi)存加上模型本身、梯度、激活值一張24G的卡根本塞不下通常需要A100 80G級(jí)別的設(shè)備。LoRA的做法是凍結(jié)所有原始參數(shù)只訓(xùn)練插入在attention層里的低秩矩陣。以r8為例ChatGLM3-6B可訓(xùn)練參數(shù)大概只有800萬(wàn)到1000萬(wàn)級(jí)別只占全部參數(shù)的1%左右。訓(xùn)練時(shí)的顯存大頭是模型本身的權(quán)重和推理帶來(lái)的激活值所以24G單卡就能很舒服地跑起來(lái)。再?gòu)臄?shù)據(jù)量的角度說(shuō)全參微調(diào)在數(shù)據(jù)量不足時(shí)特別容易災(zāi)難性遺忘——模型把原來(lái)的通用能力全忘了只知道你的業(yè)務(wù)話術(shù)。LoRA因?yàn)樵紮?quán)重不動(dòng)相當(dāng)于在保持常識(shí)基礎(chǔ)的同時(shí)疊加一個(gè)業(yè)務(wù)適配層一千條高質(zhì)量數(shù)據(jù)就能看到效果而且訓(xùn)練完的LoRA權(quán)重只有幾十MB切換任務(wù)時(shí)只要把a(bǔ)dapter換掉就行不需要維護(hù)多個(gè)完整模型副本。P-Tuning v2也是官方推薦的一種輕量方案它通過(guò)在模型輸入側(cè)加連續(xù)型prompt來(lái)實(shí)現(xiàn)微調(diào)。但它對(duì)長(zhǎng)文本、復(fù)雜推理場(chǎng)景的適配不如LoRA靈活而且每次推理都要多走一段連續(xù)prompt的學(xué)習(xí)路徑。我綜合對(duì)比后LoRA在大多數(shù)業(yè)務(wù)場(chǎng)景下是性價(jià)比最優(yōu)的選擇這也是這篇文章為什么用LoRA來(lái)實(shí)戰(zhàn)的原因。2. 環(huán)境準(zhǔn)備與依賴安裝2.1 硬件評(píng)估顯存到底怎么算在你開(kāi)始敲pip install之前先把機(jī)器的情況摸清楚。我的運(yùn)行環(huán)境是這樣的單張RTX 4090 24G系統(tǒng)是Ubuntu 22.04CUDA 12.1PyTorch 2.1。這個(gè)配置可以很輕松地跑ChatGLM3-6B的LoRA微調(diào)實(shí)際峰值顯存約21GB左右。如果你用的是16G顯存的卡比如RTX 4080或者4090 Laptop也能跑但要開(kāi)啟gradient checkpointing并調(diào)小batch size。這里我給大家一個(gè)粗略的顯存估算公式。6B模型在FP16下權(quán)重占用約為12GB60億參數(shù) × 2字節(jié)。LoRA訓(xùn)練時(shí)這12GB是固定的因?yàn)槟銉鼋Y(jié)了原始權(quán)重額外的開(kāi)銷(xiāo)來(lái)自LoRA參數(shù)、梯度緩存、優(yōu)化器狀態(tài)和激活值。以per_device_train_batch_size1、max_length1024為例激活值大約占3~5GBLoRA參數(shù)及其優(yōu)化器狀態(tài)不到1GB模型梯度凍結(jié)層不需要存基本可忽略。所以整體算下來(lái)18~23GB是一個(gè)比較現(xiàn)實(shí)的區(qū)間。你準(zhǔn)備硬件之前可以拿這個(gè)公式粗算一下別等啟動(dòng)訓(xùn)練才發(fā)現(xiàn)OOM。如果你只有一張12G的卡也并非完全不行??梢試L試量化為4bit加載用bitsandbytes配合peft的load_in_4bitTrue把模型權(quán)重壓到3GB左右騰出空間給訓(xùn)練。當(dāng)然4bit量化會(huì)稍微損失精度訓(xùn)練出來(lái)的效果比16bit略差但確實(shí)能讓你在低端卡上跑通整個(gè)流程。2.2 環(huán)境搭建和依賴清單依賴這塊我直接給出經(jīng)過(guò)我驗(yàn)證的版本組合避免大家被各庫(kù)之間的版本沖突折磨Python 3.10transformers 4.36.0低于4.30會(huì)有ChatGLM3的tokenizer兼容問(wèn)題peft 0.7.0datasets 2.16.0accelerate 0.26.0bitsandbytes 0.42.04bit量化用可選項(xiàng)sentencepiece 0.1.99protobuf 3.20.3這個(gè)版本很關(guān)鍵后面的版本會(huì)跟ChatGLM的tokenizer依賴沖突創(chuàng)建虛擬環(huán)境后用pip安裝這些包即可。有條件的話我建議用conda裝PyTorch然后pip裝其他庫(kù)。conda create -n glm3-lora python3.10 conda activate glm3-lora pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cu121 pip install transformers4.36.0 peft0.7.0 datasets2.16.0 accelerate0.26.0 sentencepiece0.1.99 protobuf3.20.3裝完之后先把模型下載到本地。注意模型文件不小6B的FP16權(quán)重大約12GB提前留好磁盤(pán)空間。git lfs install git clone https://huggingface.co/THUDM/chatglm3-6b提示如果網(wǎng)絡(luò)不穩(wěn)定也可以從ModelScope的鏡像倉(cāng)庫(kù)下載代碼上只要把model_name_or_path換成對(duì)應(yīng)的本地路徑就行。這一步踩過(guò)的坑也順便說(shuō)一下protobuf版本不對(duì)時(shí)tokenizer加載會(huì)報(bào)TypeError: Descriptors cannot not be created directly我當(dāng)時(shí)卡了差不多半天最后鎖定到3.20.3才正常。別小看依賴版本微調(diào)項(xiàng)目大部分時(shí)間都耗在這些不起眼的小問(wèn)題上。3. 數(shù)據(jù)準(zhǔn)備與預(yù)處理3.1 數(shù)據(jù)結(jié)構(gòu)讓模型看懂你的業(yè)務(wù)LoRA微調(diào)成敗的第一決定因素是數(shù)據(jù)而不是模型。ChatGLM3-6B的官方微調(diào)腳本期望的數(shù)據(jù)是對(duì)話格式的JSON。我通常使用ShareGPT格式每個(gè)樣本是一個(gè)多輪對(duì)話數(shù)組[ { conversations: [ { role: system, content: 你是云運(yùn)維助手熟悉Kubernetes、Docker、Prometheus等工具。 }, { role: user, content: Pod一直處于Pending狀態(tài)我該怎么排查 }, { role: assistant, content: 首先用 kubectl describe pod pod-name 查看事件重點(diǎn)看調(diào)度失敗原因。常見(jiàn)情況包括節(jié)點(diǎn)資源不足、節(jié)點(diǎn)被污點(diǎn)污化、PVC沒(méi)有綁定等。 } ] } ]注意這里有幾個(gè)細(xì)節(jié)。第一system字段可以顯式設(shè)定人設(shè)這是ChatGLM3相對(duì)早期模型的一大增強(qiáng)如果你要的是某個(gè)垂直領(lǐng)域的問(wèn)答助手建議每個(gè)樣本都保留這個(gè)字段。第二多輪對(duì)話要保留完整的上下文不要只給單輪切出來(lái)的片段否則模型學(xué)不到記住前文的能力。第三字段名必須是conversations里面每輪的role只能是system、user、assistant三種之一連大小寫(xiě)都不能錯(cuò)。對(duì)于數(shù)據(jù)量我給個(gè)經(jīng)驗(yàn)區(qū)間。如果只是做風(fēng)格遷移或角色扮演兩三百條高質(zhì)量樣本就夠看到明顯變化如果是行業(yè)知識(shí)問(wèn)答建議至少準(zhǔn)備一千條覆蓋主要問(wèn)法的樣本最好是三千到一萬(wàn)條的效果更穩(wěn)。數(shù)據(jù)再多的話普通LoRA可能就有點(diǎn)吃力了需要配合增量預(yù)訓(xùn)練或者調(diào)整數(shù)據(jù)采樣策略。3.2 數(shù)據(jù)清洗與質(zhì)量檢查這一步極其容易被忽略但恰恰決定了微調(diào)效果的天花板。我拿到原始數(shù)據(jù)后的處理順序是這樣的先做格式清洗再做內(nèi)容去重最后做質(zhì)量抽檢。格式清洗包括JSON解析驗(yàn)證、剔除空content的樣本、把內(nèi)容中的多余空白和不可見(jiàn)字符清掉、統(tǒng)一全角半角標(biāo)點(diǎn)。很多從業(yè)務(wù)數(shù)據(jù)庫(kù)里摳出來(lái)的數(shù)據(jù)格式亂得讓人頭大但模型學(xué)的就是這些文本垃圾進(jìn)垃圾出所以這一步不能省。內(nèi)容去重我用的是datasets庫(kù)的Dataset.from_list配合簡(jiǎn)單的hash去重。因?yàn)橹貜?fù)樣本會(huì)在訓(xùn)練時(shí)被反復(fù)加權(quán)導(dǎo)致模型對(duì)某幾條回答過(guò)擬合降低泛化能力。實(shí)際操作中我還遇到過(guò)一種隱蔽問(wèn)題文本內(nèi)容不同但語(yǔ)義高度重復(fù)的樣本比如K8s是什么和Kubernetes是什么同時(shí)大量出現(xiàn)這會(huì)讓模型對(duì)特定說(shuō)法過(guò)度敏感。處理這類(lèi)問(wèn)題沒(méi)有捷徑只能靠人工抽樣看所以我的習(xí)慣是第一步先按字面去重第二步再抽檢語(yǔ)義重復(fù)率實(shí)在太多就做聚類(lèi)后挑代表樣本。最后的人工抽檢是必須的。我習(xí)慣從清洗后的數(shù)據(jù)里隨機(jī)抽20~30條逐條看答案是否準(zhǔn)確、是否包含明顯錯(cuò)誤信息。大模型微調(diào)最怕的就是數(shù)據(jù)本身有毒——模型一旦學(xué)進(jìn)去錯(cuò)誤知識(shí)很難通過(guò)后續(xù)手段擦除。這塊用一句話總結(jié)就是寧可少十頁(yè)數(shù)據(jù)不要一條臟數(shù)據(jù)。4. LoRA微調(diào)核心實(shí)現(xiàn)4.1 LoRA參數(shù)到底怎么設(shè)在寫(xiě)代碼之前先得對(duì)LoRA的幾個(gè)關(guān)鍵參數(shù)心里有數(shù)不然一行代碼都看不懂。LoRA的核心思想是把權(quán)重更新量拆成一個(gè)低秩矩陣的乘積即ΔW BA其中B的維度是d×rA的維度是r×k。訓(xùn)練時(shí)只更新B和A這個(gè)r就是秩。參數(shù)名推薦值作用備注r8低秩矩陣的秩決定可學(xué)習(xí)容量數(shù)據(jù)量大可加大到16新手別一上來(lái)就64lora_alpha32縮放因子控制LoRA影響強(qiáng)度和r搭配實(shí)際縮放系數(shù)為alpha/rlora_dropout0.1隨機(jī)失活比例防過(guò)擬合數(shù)據(jù)量小、過(guò)擬合時(shí)加到0.15target_modulesquery_key_value指定插入LoRA的網(wǎng)絡(luò)層ChatGLM3核心attention模塊biasnone是否訓(xùn)練偏置項(xiàng)一般設(shè)為none省參數(shù)省顯存這些參數(shù)不是拍腦袋定的我在不同任務(wù)上做過(guò)對(duì)比。r8配上alpha32在客服對(duì)答、知識(shí)抽取、文案改寫(xiě)這些任務(wù)上都能拿到不錯(cuò)的效果。如果你想追求更極致的推理速度可以把r降到4alpha調(diào)成16模型體積會(huì)小不少但表達(dá)能力的上限也會(huì)相應(yīng)降低。另外特別提醒一下target_modules不要亂加。有人為了充分微調(diào)把dense、dense_h_to_4h全都加上結(jié)果訓(xùn)練時(shí)間翻倍、顯存暴漲效果卻沒(méi)有明顯提升因?yàn)榇蟛糠挚蛇w移的知識(shí)就集中在attention層的value投影里。4.2 訓(xùn)練主流程代碼下面直接上核心代碼。首先是加載基座模型和tokenizerimport torch from transformers import AutoTokenizer, AutoModel, TrainingArguments, Trainer from peft import LoraConfig, get_peft_model model_name THUDM/chatglm3-6b tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModel.from_pretrained( model_name, torch_dtypetorch.float16, trust_remote_codeTrue, device_mapauto ) # 如果顯存緊張用下面的方式加載4bit量化模型 # from transformers import BitsAndBytesConfig # bnb_config BitsAndBytesConfig(load_in_4bitTrue, bnb_4bit_compute_dtypetorch.float16) # model AutoModel.from_pretrained(model_name, quantization_configbnb_config, trust_remote_codeTrue)然后配置LoRA并注入模型lora_config LoraConfig( r8, lora_alpha32, lora_dropout0.1, target_modules[query_key_value], biasnone, task_typeCAUSAL_LM ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 輸出類(lèi)似: trainable params: 8,388,608 || all params: 6,258,278,400 || trainable%: 0.134數(shù)據(jù)處理部分需要把對(duì)話樣本拼成ChatGLM3的prompt格式。這里的關(guān)鍵是讓tokenizer正確處理特殊token。我建議自己實(shí)現(xiàn)一個(gè)拼接函數(shù)這樣對(duì)batch處理更友好ChatGLM3的chat模板長(zhǎng)這樣def build_prompt(conversations): system 你是一個(gè)智能助手 if conversations[0][role] system: system conversations[0][content] conversations conversations[1:] prompt [gMASK]sop system \n for msg in conversations: if msg[role] user: prompt |user|\n msg[content] |assistant|\n else: prompt msg[content] \n return prompt訓(xùn)練時(shí)我們對(duì)整條拼接后的文本做tokenize把user部分對(duì)應(yīng)的token在labels里設(shè)為-100忽略loss只讓模型學(xué)習(xí)assistant的回答部分。實(shí)現(xiàn)時(shí)可以用tokenizer(prompt, return_tensorspt)然后手動(dòng)構(gòu)造labels數(shù)組凡是user位置的token label都置為-100。接著配置TrainingArgumentstraining_args TrainingArguments( output_dir./glm3-lora-checkpoints, per_device_train_batch_size1, gradient_accumulation_steps8, num_train_epochs3, learning_rate2e-4, warmup_ratio0.03, logging_steps10, save_steps500, evaluation_strategysteps, eval_steps500, fp16True, gradient_checkpointingTrue, report_tonone ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, tokenizertokenizer, ) trainer.train()這些參數(shù)里per_device_train_batch_size1加gradient_accumulation_steps8等效batch size是8但顯存壓力卻被控制得很好。gradient_checkpointingTrue會(huì)以少量計(jì)算換顯存實(shí)測(cè)能省下4GB左右的占用。learning_rate用2e-4是LoRA微調(diào)常見(jiàn)的起點(diǎn)比全參微調(diào)的1e-5到5e-5要大一兩個(gè)量級(jí)原因很簡(jiǎn)單可訓(xùn)練參數(shù)太少學(xué)習(xí)率太小根本學(xué)不動(dòng)。這個(gè)經(jīng)驗(yàn)我在多個(gè)模型上驗(yàn)證過(guò)用1e-5訓(xùn)練LoRAloss基本貼著原地不動(dòng)。這里插一句非常重要的經(jīng)驗(yàn)不要盲目開(kāi)大batch size。LoRA微調(diào)時(shí)batch size影響的是LoRA那一小部分參數(shù)的梯度估計(jì)8的等效batch已經(jīng)足夠。過(guò)大反而讓模型傾向于復(fù)刻訓(xùn)練集的常見(jiàn)回答損失多樣性模型會(huì)變得油嘴滑舌但缺乏真正的泛化能力。4.3 訓(xùn)練過(guò)程中的幾個(gè)坑這塊是純實(shí)操經(jīng)驗(yàn)了。第一個(gè)坑是loss不下降或下降極慢。我遇到過(guò)最典型的原因就是learning_rate設(shè)成了全參微調(diào)的1e-5LoRA參數(shù)更新本身就小1e-5基本等于沒(méi)訓(xùn)練。改成2e-4甚至5e-4后loss明顯開(kāi)始往下走。排查時(shí)先看learning_rate再看target_modules是否正確注入最后看數(shù)據(jù)加載是否真的打到了模型上。第二個(gè)坑是顯存突然爆炸。有時(shí)候開(kāi)始訓(xùn)練正常跑了幾百步之后OOM。這一般是激活值累積或者某個(gè)batch的文本特別長(zhǎng)導(dǎo)致的。我后來(lái)習(xí)慣在數(shù)據(jù)集里加一個(gè)長(zhǎng)度上限過(guò)濾比如超過(guò)2048 tokens的樣本直接截?cái)嗷騺G棄訓(xùn)練穩(wěn)定性會(huì)好很多。具體做法是在preprocess函數(shù)里判斷l(xiāng)en(input_ids) max_length就跳過(guò)。第三個(gè)坑是梯度檢查點(diǎn)跟某些庫(kù)的兼容問(wèn)題。如果你在加載模型時(shí)用了prepare_model_for_kbit_training又同時(shí)開(kāi)啟gradient_checkpointing在模型反向傳播時(shí)可能報(bào)RuntimeError: None of the inputs have requires_gradTrue。這個(gè)問(wèn)題的根源是4bit量化后部分參數(shù)被設(shè)為不需要梯度解決方法是確保你只對(duì)model.base_model調(diào)用gradient_checkpointing_enable()并檢查所有需要梯度的參數(shù)是否都在LoRA的adapter里。這個(gè)報(bào)錯(cuò)信息看著嚇人其實(shí)原因非常具體按這個(gè)思路排查基本都能解決。5. 模型評(píng)估與推理部署5.1 模型保存與合并訓(xùn)練完成后trainer會(huì)直接把LoRA adapter保存在output_dir里。這里面有幾個(gè)文件adapter_config.json、adapter_model.bin還有tokenizer相關(guān)文件。adapter_model.bin通常只有幾十MB這就是你辛苦訓(xùn)練的成果。保存LoRA其實(shí)就夠了因?yàn)橥评頃r(shí)你可以在基座模型上臨時(shí)加載這個(gè)adapter。但如果你的最終目的是部署到生產(chǎn)環(huán)境不希望每次啟動(dòng)都多一步加載adapter的邏輯那就需要把a(bǔ)dapter合并回原始模型from peft import PeftModel model PeftModel.from_pretrained(model, ./glm3-lora-checkpoints/checkpoint-1500) merged_model model.merge_and_unload() merged_model.save_pretrained(./glm3-lora-merged) tokenizer.save_pretrained(./glm3-lora-merged)合并后的模型跟普通ChatGLM3-6B的結(jié)構(gòu)完全一樣只是權(quán)重已經(jīng)被業(yè)務(wù)數(shù)據(jù)微調(diào)過(guò)可以用常規(guī)方式加載和部署。這樣在推理服務(wù)里就不需要依賴peft庫(kù)也能省掉adapter疊加的運(yùn)行時(shí)開(kāi)銷(xiāo)。如果你的服務(wù)框架不支持peft合并導(dǎo)出幾乎是唯一的選擇。需要提醒的是合并操作會(huì)把LoRA權(quán)重加到原始權(quán)重上結(jié)果是一個(gè)新的6B完整模型磁盤(pán)占用又回到12GB左右。如果你同時(shí)維護(hù)多個(gè)業(yè)務(wù)場(chǎng)景的LoRA建議保留adapter文件按需加載合并而不是每個(gè)場(chǎng)景都存一份完整模型磁盤(pán)開(kāi)銷(xiāo)會(huì)大很多。5.2 推理驗(yàn)證效果好不好自己先聊幾輪模型微調(diào)完光看loss曲線是不夠的loss低不代表效果好可能是過(guò)擬合了。我自己習(xí)慣先做一組固定的測(cè)試問(wèn)題集蓋上訓(xùn)練分布專(zhuān)門(mén)測(cè)模型的遷移能力。比如我訓(xùn)練了一個(gè)客服助手測(cè)試時(shí)會(huì)問(wèn)幾個(gè)訓(xùn)練數(shù)據(jù)里完全沒(méi)出現(xiàn)過(guò)的同義問(wèn)題看它能否給出合理回答。推理測(cè)試的代碼很簡(jiǎn)單model AutoModel.from_pretrained(./glm3-lora-merged, trust_remote_codeTrue, devicecuda) response, history model.chat(tokenizer, 你熟悉Kubernetes嗎請(qǐng)用一個(gè)例子說(shuō)明Pod調(diào)度原理。, history[]) print(response)測(cè)試時(shí)重點(diǎn)看三個(gè)維度一是回答是否跟業(yè)務(wù)口徑一致二是多輪對(duì)話是否還能記得前面的內(nèi)容三是是否出現(xiàn)通用能力退化比如原本能做的數(shù)學(xué)題現(xiàn)在做不出來(lái)了。第三個(gè)維度很多人會(huì)忽略但恰恰是衡量LoRA微調(diào)是否健康的關(guān)鍵指標(biāo)。我在一個(gè)項(xiàng)目里就遇到過(guò)微調(diào)后模型對(duì)領(lǐng)域問(wèn)題的回答非常漂亮但一讓它算11都開(kāi)始胡說(shuō)這就是典型的災(zāi)難性遺忘。解決方案是訓(xùn)練時(shí)在數(shù)據(jù)里摻10%左右的基礎(chǔ)通用數(shù)據(jù)效果立竿見(jiàn)影。后來(lái)我每次準(zhǔn)備數(shù)據(jù)都會(huì)刻意留出這個(gè)比例確保模型既懂業(yè)務(wù)又不丟常識(shí)。我還會(huì)把微調(diào)前后的回答放在一起做對(duì)比。用同一組問(wèn)題先問(wèn)原版ChatGLM3再問(wèn)微調(diào)后的模型輸出差異一目了然。這個(gè)對(duì)比過(guò)程不只是看結(jié)果好壞還能幫你理解LoRA到底改了什么。比如我看到過(guò)原版模型回答你怎么看云原生時(shí)會(huì)給出標(biāo)準(zhǔn)百科式的定義而微調(diào)后模型的回答變成了結(jié)合業(yè)務(wù)場(chǎng)景的具體建議這就是LoRA在起作用。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 顯存不足怎么辦這個(gè)問(wèn)題出現(xiàn)的頻率最高。如果你遇到CUDA out of memory按下面的順序排查確認(rèn)已經(jīng)開(kāi)啟gradient_checkpointing這是性價(jià)比最高的手段。把per_device_train_batch_size降到1。檢查max_length設(shè)置很多數(shù)據(jù)里有個(gè)別超長(zhǎng)樣本會(huì)在某個(gè)step突然撐爆顯存可以先把max_length設(shè)成512或768試跑??紤]4bit量化加載模型。我在一張24G卡上用上面的組合可以跑batch_size1 grad_accum8 max_length1024的訓(xùn)練顯存峰值不到22GB。如果這么調(diào)還是OOM那基本得換更大的卡或者減少數(shù)據(jù)長(zhǎng)度了不要指望靠玄學(xué)優(yōu)化繞過(guò)物理限制。6.2 過(guò)擬合與欠擬合怎么判斷過(guò)擬合看eval loss。如果訓(xùn)練loss持續(xù)下降但eval loss先降后升那就是過(guò)擬合的典型信號(hào)。LoRA微調(diào)數(shù)據(jù)量一兩千條時(shí)過(guò)擬合非常容易發(fā)生。我的對(duì)策是加大lora_dropout到0.15降低學(xué)習(xí)率到1e-4訓(xùn)練輪數(shù)減少到2輪同時(shí)用early stopping回調(diào)來(lái)早停。如果過(guò)擬合嚴(yán)重到訓(xùn)練集loss都快到0了建議先別急著調(diào)參回到數(shù)據(jù)處理環(huán)節(jié)看是不是重復(fù)樣本太多、對(duì)話模式太單一。欠擬合則相反訓(xùn)練loss降不下去模型的回答還是跟原版差不多。這多半是學(xué)習(xí)率太小、訓(xùn)練輪數(shù)不夠或者LoRA的r設(shè)得太小??梢韵仍囍裭earning_rate提到5e-4觀察幾輪再?zèng)Q定。注意欠擬合和過(guò)擬合的調(diào)參方向是相反的別搞混。我見(jiàn)過(guò)有人把欠擬合誤判成過(guò)擬合加了dropout、降了學(xué)習(xí)率結(jié)果問(wèn)題越來(lái)越嚴(yán)重。6.3 Loss變成NaN或訓(xùn)練崩潰Loss出現(xiàn)NaN最常見(jiàn)的原因是學(xué)習(xí)率過(guò)大導(dǎo)致梯度爆炸或者是fp16混合精度下的loss scale出了問(wèn)題。處理方式先把lr降到1e-5確認(rèn)穩(wěn)定后再往上調(diào)。如果還不行把fp16關(guān)掉用純fp32訓(xùn)練顯存壓力會(huì)大但能排除精度問(wèn)題。另外數(shù)據(jù)里如果混入了特別臟的文本比如超長(zhǎng)亂碼也可能導(dǎo)致數(shù)值異常清洗數(shù)據(jù)時(shí)要把這類(lèi)內(nèi)容過(guò)濾掉。還有一個(gè)容易被忽略的問(wèn)題tokenizer的pad token沒(méi)設(shè)置好。ChatGLM3的tokenizer默認(rèn)沒(méi)有pad_token而Trainer在batch時(shí)需要對(duì)樣本pad到相同長(zhǎng)度。解決辦法是在訓(xùn)練前顯式設(shè)置tokenizer.pad_token tokenizer.eos_token不設(shè)置的話可能報(bào)錯(cuò)或訓(xùn)練行為異常這是我見(jiàn)很多人卡住的點(diǎn)。另外如果用了DataCollatorForSeq2Seq記得把paddingTrue開(kāi)啟否則pad邏輯不會(huì)生效。這些小細(xì)節(jié)看著不起眼但往往就是它們決定了你能不能順利跑通一次訓(xùn)練。6.4 訓(xùn)練速度太慢怎么辦如果你覺(jué)得訓(xùn)練速度慢先別懷疑顯卡性能大概率是數(shù)據(jù)長(zhǎng)度太長(zhǎng)或者小batch拖累的。LoRA訓(xùn)練的計(jì)算量主要集中在attention的前向反向傳播上輸入token越長(zhǎng)計(jì)算復(fù)雜度增長(zhǎng)越快。我建議先統(tǒng)計(jì)一下訓(xùn)練數(shù)據(jù)的平均長(zhǎng)度如果大部分樣本都能控制在512 token以內(nèi)就不要把max_length設(shè)成2048白白增加計(jì)算量。另外檢查一下數(shù)據(jù)加載是否有瓶頸比如每次都在內(nèi)存里重新讀寫(xiě)大JSON換成datasets庫(kù)的內(nèi)存映射機(jī)制會(huì)快很多。還有一個(gè)訓(xùn)練加速的技巧在數(shù)據(jù)預(yù)處理時(shí)提前把所有樣本tokenize好并緩存到磁盤(pán)訓(xùn)練時(shí)直接加載預(yù)處理后的token可以省掉運(yùn)行時(shí)的tokenize開(kāi)銷(xiāo)。數(shù)據(jù)量幾千條時(shí)這個(gè)優(yōu)化不明顯但如果數(shù)據(jù)量到了幾萬(wàn)條能省下不少時(shí)間。我的習(xí)慣是第一次跑數(shù)據(jù)預(yù)處理時(shí)花些時(shí)間緩存之后每次調(diào)參訓(xùn)練都不用重復(fù)處理數(shù)據(jù)。這些坑回頭看看都不復(fù)雜但確實(shí)會(huì)一個(gè)接一個(gè)地消耗時(shí)間。我把它們整理成一張速查表方便你排查現(xiàn)象首要懷疑點(diǎn)處理建議顯存OOM激活值過(guò)大開(kāi)gradient checkpointing、降batch、裁長(zhǎng)文本Loss不降學(xué)習(xí)率太小調(diào)大到2e-4乃至5e-4Eval loss反彈過(guò)擬合加dropout、減訓(xùn)練輪數(shù)、查數(shù)據(jù)重復(fù)LossNaN學(xué)習(xí)率過(guò)大或fp16不穩(wěn)降lr、關(guān)fp16試跑、清理臟數(shù)據(jù)訓(xùn)練報(bào)pad相關(guān)錯(cuò)誤未設(shè)置pad_token顯式設(shè)置tokenizer.pad_token自己在實(shí)際項(xiàng)目里把這一整套流程跑下來(lái)之后我最大的體會(huì)是LoRA微調(diào)這個(gè)事代碼層面還真不難難的是數(shù)據(jù)整理和對(duì)效果的系統(tǒng)驗(yàn)證。源碼和參數(shù)照著抄都能跑通可如果你的數(shù)據(jù)是臟的或者壓根沒(méi)有一套效果評(píng)估方法那微調(diào)出來(lái)的模型很可能只是看起來(lái)在訓(xùn)練實(shí)際離上線還有很大距離。我現(xiàn)在的固定流程是先花一天整理數(shù)據(jù)再花半天配環(huán)境訓(xùn)練一晚上第二天上午做定向測(cè)試對(duì)比微調(diào)前后在業(yè)務(wù)問(wèn)題上的回答差異。這個(gè)節(jié)奏在我做過(guò)的幾個(gè)項(xiàng)目里都挺穩(wěn)定。如果你準(zhǔn)備在自己的場(chǎng)景里上手ChatGLM3-6B的LoRA微調(diào)我建議從小數(shù)據(jù)量開(kāi)始跑通全流程再逐步加數(shù)據(jù)、調(diào)參數(shù)這樣可以省下很多不必要的試錯(cuò)時(shí)間。本文還有配套的精品資源點(diǎn)擊獲取