訓(xùn)練模型微調(diào)與部署實(shí)戰(zhàn):顯存優(yōu)化、LoRA、量化加速與排錯指南)
前面四篇我順著注意力機(jī)制、Transformer結(jié)構(gòu)、預(yù)訓(xùn)練語言模型一路寫過來理論層面的東西講了不少。到了第五篇我想換一個更貼近生產(chǎn)環(huán)境的話題拿到一個預(yù)訓(xùn)練模型之后怎么把它真正用起來怎么在顯存、速度和效果之間找到一個可以接受的平衡點(diǎn)。這篇沒有太多數(shù)學(xué)推導(dǎo)大部分是實(shí)驗(yàn)記錄、參數(shù)選擇和排錯過程適合那些已經(jīng)能跑通基礎(chǔ)代碼、但還沒有完整做過微調(diào)和部署的同學(xué)。先說一個常見誤區(qū)。很多人以為微調(diào)就是把模型加載進(jìn)來跑幾個epoch然后保存權(quán)重。但真上手之后會發(fā)現(xiàn)光是一個batch size設(shè)置不對顯卡就直接報OOM訓(xùn)練到一半loss變成NaN查了一下午也不知道是哪里出了問題模型好不容易跑通了上線之后第一次推理請求卻要等好幾秒直接被調(diào)用方投訴。這些坑我?guī)缀醵疾冗^。這篇就圍繞“選型、微調(diào)、加速、壓測、排錯”這條主線來寫看完整套流程你至少能把一個文本分類任務(wù)從代碼變成可服務(wù)的狀態(tài)并且知道每一步為什么這樣做。1. 先從顯存說起選模型比調(diào)參更重要微調(diào)之前的第一件事不是寫訓(xùn)練腳本而是想清楚手里這塊顯卡到底能扛住多大的模型。很多項(xiàng)目翻車不是調(diào)參的問題是模型選大了卡里根本放不下。1.1 參數(shù)規(guī)模與實(shí)際需求的錯位我見過不少人一開始就奔著超大模型去覺得參數(shù)越多效果越好。但在實(shí)際業(yè)務(wù)里這個邏輯往往不成立。一個短文本分類任務(wù)幾百個類別訓(xùn)練數(shù)據(jù)幾萬條用基礎(chǔ)規(guī)模的模型已經(jīng)能跑出不錯的效果強(qiáng)行上大模型只會讓訓(xùn)練周期邊長、部署成本變高、推理延遲變大。給一個選型時可以參考的粗粒度對照表模型規(guī)模參數(shù)量級別訓(xùn)練階段顯存參考適用場景基礎(chǔ)級1億到3億8GB到16GB文本分類、短文本匹配、抽取式問答進(jìn)階級3億到10億16GB到40GB生成式任務(wù)、長文本理解、多任務(wù)微調(diào)大模型級百億以上多卡并行才能訓(xùn)練通用對話、復(fù)雜推理一般走API更劃算這張表不是精確值因?yàn)轱@存占用還和序列長度、batch size、是否用混合精度直接相關(guān)但它能幫你快速判斷方向。我在某個文本分類項(xiàng)目里最初選的模型比實(shí)際需要的大了一號訓(xùn)練時頻繁O(jiān)OM后來換成小一號的模型效果幾乎沒有下降訓(xùn)練時間縮短了一半以上推理速度也上來了。1.2 環(huán)境準(zhǔn)備先把版本匹配問題解決選好模型之后下一步是搭環(huán)境。很多人在這上面浪費(fèi)了大量時間問題往往出在版本不匹配上而且報錯信息經(jīng)常是莫名其妙的不會直接告訴你“版本不兼容”。我自己常用的環(huán)境組合是Python 3.10PyTorch 2.xCUDA 11.8或12.1配合對應(yīng)版本的PyTorch預(yù)編譯包transformers 4.x的近期版本注意它和tokenizers有聯(lián)動更新最好一起升級。一個建議是盡量用容器鏡像來固化環(huán)境。我通常的做法是寫一個Dockerfile把基礎(chǔ)鏡像、PyTorch版本、Python依賴一次裝好然后把訓(xùn)練、驗(yàn)證、推理都放在同一個環(huán)境里跑。這樣做的直接好處是不管換到哪臺服務(wù)器跑出來的結(jié)果都是一樣的不會出現(xiàn)“在我電腦上好好的一上線就報錯”的情況。裝完之后可以用這段代碼確認(rèn)基礎(chǔ)環(huán)境是否正常import torch print(torch.__version__) print(torch.cuda.is_available()) from transformers import AutoModel # 能打印出模型參數(shù)量就說明加載階段沒問題 model AutoModel.from_pretrained(your/model/path) print(sum(p.numel() for p in model.parameters()))如果torch.cuda.is_available()返回的是False先別急著改代碼優(yōu)先檢查PyTorch的CUDA版本是否和驅(qū)動匹配。很多新手在這里反復(fù)重裝包其實(shí)問題出在PyTorch本身是CPU版本或者安裝時沒有選擇帶CUDA的wheel包。1.3 數(shù)據(jù)處理里的隱藏影響面模型能加載之后還要確認(rèn)數(shù)據(jù)預(yù)處理沒有埋雷。同樣是文本分類不同文本的長度差異可能非常大有的只有幾個字有的上千個字。如果所有樣本都按同一個最大長度截?cái)喽痰臅速M(fèi)大量計(jì)算長的會丟失大量信息。有一個穩(wěn)妥的實(shí)踐方式先統(tǒng)計(jì)訓(xùn)練數(shù)據(jù)的長度分布再決定截?cái)嚅L度。對大多數(shù)中文短文本任務(wù)來說128到256的序列長度已經(jīng)覆蓋了絕大部分情況如果做長文本任務(wù)比如閱讀理解或者文檔分類就需要考慮切塊或者滑窗策略而不是簡單把長度上限調(diào)大因?yàn)門ransformer的注意力復(fù)雜度是序列長度的平方長度翻倍計(jì)算量會變成四倍。另外還要注意padding_side的選擇。對于單向語言模型來說padding應(yīng)當(dāng)放在左邊這樣生成時不會受右側(cè)padding影響而對于雙向編碼器模型左右兩側(cè)padding沒有本質(zhì)區(qū)別但要在預(yù)處理階段保持一致否則訓(xùn)練和推理的結(jié)果會不一致。2. 微調(diào)策略全參微調(diào)與LoRA的取舍環(huán)境準(zhǔn)備好、數(shù)據(jù)處理完成之后才進(jìn)入真正的微調(diào)環(huán)節(jié)。這里的核心問題不是“用什么框架”而是“用哪種微調(diào)方式”。我在實(shí)際項(xiàng)目里對比過三種全參微調(diào)、凍結(jié)部分層微調(diào)、低秩適配微調(diào)它們的應(yīng)用場景和成本差別很大。2.1 三種微調(diào)方式的適用邊界全參微調(diào)在數(shù)據(jù)量足夠大的時候效果最好因?yàn)槟P退袇?shù)都能根據(jù)新任務(wù)進(jìn)行調(diào)整。但它有兩個明顯的痛點(diǎn)一是顯存占用大因?yàn)椴粌H要存模型參數(shù)還要存所有參與訓(xùn)練的梯度二是容易過擬合如果訓(xùn)練數(shù)據(jù)只有幾千條全參微調(diào)往往會記住訓(xùn)練集中的噪聲在驗(yàn)證集上表現(xiàn)反而更差。凍結(jié)部分層微調(diào)是一種折中方案常見的做法是凍結(jié)模型的底層編碼層只訓(xùn)練頂層和最后的分類頭。底層學(xué)到的是通用語言特征遷移到新任務(wù)時不需要大改頂層更接近任務(wù)相關(guān)的語義空間調(diào)整的價值更大。這種做法比全參微調(diào)省顯存但缺點(diǎn)是模型容量仍然沒有變化而且在有些任務(wù)上效果不夠穩(wěn)定。低秩適配微調(diào)也就是常說的LoRA是目前實(shí)踐中很流行的一種方式。它的思路是保持原模型權(quán)重不變只額外添加一個低秩矩陣來模擬權(quán)重的增量??梢岳斫鉃樵P褪且粭澭b修好的房子我們不去推倒重裝而是往墻上加幾塊預(yù)制板用很小的代價調(diào)整房間的布局。訓(xùn)練時只更新這幾塊預(yù)制板的參數(shù)顯存占用大幅下降而且在數(shù)據(jù)量不大的場景下效果經(jīng)常不輸全參微調(diào)。2.2 訓(xùn)練腳本里的關(guān)鍵參數(shù)下面是一段我常用的一般性訓(xùn)練配置框架以句對分類任務(wù)為例只保留關(guān)鍵部分from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer model_name your/base/model tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels10) training_args TrainingArguments( output_dir./output, learning_rate3e-5, per_device_train_batch_size16, gradient_accumulation_steps4, num_train_epochs3, warmup_ratio0.1, weight_decay0.01, logging_steps50, fp16True, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, )里面對效果影響比較大的幾個參數(shù)我逐個說一下我的理解。學(xué)習(xí)率learning_rate預(yù)訓(xùn)練模型微調(diào)時一般從2e-5到5e-5之間開始找。這個數(shù)值比隨機(jī)初始化模型的學(xué)習(xí)率小很多因?yàn)轭A(yù)訓(xùn)練模型已經(jīng)處于一個比較平滑的收斂區(qū)域太大的學(xué)習(xí)率會把參數(shù)直接推離原來的有利位置效果反而變差。batch size和gradient_accumulation_steps是配套的。小顯存顯卡上一個batch塞不了多少樣本如果每次都按小batch更新梯度的噪聲會很大訓(xùn)練不穩(wěn)定。一個有效的辦法是用較小的per_device_train_batch_size比如8或16再配合梯度累積比如4步累積等效batch size就變成了32或64這樣既緩解了顯存壓力又保持了較大batch的穩(wěn)定性。fp16也就是混合精度訓(xùn)練可以把模型和梯度的一部分用半精度存儲顯存占用降低訓(xùn)練速度也快但要注意某些數(shù)值敏感的場景里可能出現(xiàn)精度問題后面我會專門講一個踩坑案例。2.3 為什么要設(shè)置warmup剛才代碼里有一個warmup_ratio0.1很多人不理解它的意義直接設(shè)成0。Warmup的意思是訓(xùn)練最開始的一小段步數(shù)里學(xué)習(xí)率從很小的值逐步升到設(shè)定值之后再按計(jì)劃衰減。理由其實(shí)不復(fù)雜預(yù)訓(xùn)練模型的權(quán)重分布已經(jīng)比較穩(wěn)定訓(xùn)練一開始就施加完整學(xué)習(xí)率可能會讓模型參數(shù)產(chǎn)生劇烈擾動。用warmup先“試探”一小段時間讓參數(shù)緩慢進(jìn)入任務(wù)適配的軌道往往能比直接跑完整學(xué)習(xí)率更穩(wěn)定。我在幾個任務(wù)上對比過加了10%的warmup之后驗(yàn)證集的loss曲線通常更平滑最終效果也稍有提升。3. 推理加速從FP32到INT8的量化實(shí)踐與部署驗(yàn)證訓(xùn)練完模型之后真正到部署階段才是考驗(yàn)工程能力的地方。這一節(jié)我會重點(diǎn)講推理加速里最容易見效的一條路量化。量化不改變模型結(jié)構(gòu)只改變參數(shù)存儲和計(jì)算的精度格式能讓模型更小、跑得更快。3.1 量化為什么能加速模型里的參數(shù)原本以FP32格式存儲一個數(shù)占4字節(jié)如果把它映射成INT8一個數(shù)只占1字節(jié)模型的內(nèi)存占用直接降到四分之一。而且大部分現(xiàn)代GPU對低精度整數(shù)運(yùn)算有專門優(yōu)化同樣的計(jì)算INT8往往比FP32快不少。你可能會擔(dān)心精度降這么多效果會不會崩這取決于模型的權(quán)重分布。Transformer類模型的權(quán)重在經(jīng)過訓(xùn)練之后大多數(shù)數(shù)值集中在零附近一個較窄的范圍內(nèi)分布比較規(guī)整映射成INT8時引入的損失相對可控。我實(shí)測過一些文本模型量化后F1分?jǐn)?shù)下降不到一個百分點(diǎn)但推理速度能提升一到兩倍這個性價比在業(yè)務(wù)上是劃算的。3.2 動態(tài)量化與靜態(tài)量化的選擇量化按執(zhí)行方式分為動態(tài)量化和靜態(tài)量化兩種適用范圍不同。動態(tài)量化在推理時實(shí)時計(jì)算激活值的范圍并完成映射不需要準(zhǔn)備額外的校準(zhǔn)數(shù)據(jù)實(shí)現(xiàn)最簡單。它的缺點(diǎn)是每次推理都有額外的計(jì)算開銷但因?yàn)橹涣炕瘷?quán)重、激活值仍然按浮點(diǎn)計(jì)算在NLP場景往往已經(jīng)能提升不少效果。靜態(tài)量化需要先用一批有代表性的校準(zhǔn)數(shù)據(jù)跑一遍模型統(tǒng)計(jì)出激活值的分布范圍然后把這個范圍固定下來。這樣推理時就不用再實(shí)時計(jì)算范圍速度快進(jìn)一步加快但前提是你有一批能代表真實(shí)輸入分布的校準(zhǔn)數(shù)據(jù)。我的使用經(jīng)驗(yàn)是NLP任務(wù)優(yōu)先嘗試動態(tài)量化因?yàn)檩斎腴L度變化大靜態(tài)量化的激活范圍很難用一個固定值覆蓋所有情況如果動態(tài)量化后的速度和顯存已經(jīng)能滿足要求就沒有必要上復(fù)雜的靜態(tài)量化。只有當(dāng)延遲和模型大小都壓不住需求時再認(rèn)真做靜態(tài)量化加校準(zhǔn)流程。下面給出一段基于一般PyTorch環(huán)境的動態(tài)量化示例import torch from transformers import AutoModelForSequenceClassification model AutoModelForSequenceClassification.from_pretrained(your/finetuned/model) model_dynamic torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, # 只量化線性層 dtypetorch.qint8 ) # 分段保存避免一次寫入占滿內(nèi)存 torch.save(model_dynamic.state_dict(), model_int8.pt)3.3 用ONNX與推理器做進(jìn)一步提速如果你的部署環(huán)境對時延要求嚴(yán)格還可以把模型導(dǎo)出成ONNX格式然后在支持ONNX Runtime的推理器上運(yùn)行。PyTorch的torch.onnx.export可以做到這一點(diǎn)。為什么導(dǎo)出之后可能更快因?yàn)橥评砥髟趦?yōu)化圖上做了不少文章比如算子融合、內(nèi)存復(fù)用、多線程調(diào)度優(yōu)化等。在普通文本分類任務(wù)上我測過的結(jié)果是ONNX Runtime相比原生PyTorch推理延遲普遍能再降20%到40%顯卡和CPU上的表現(xiàn)不一樣但趨勢是一致的。導(dǎo)出時有一個特別容易踩的坑onnx模型的可變輸入長度。如果輸入張量維度里寫死了某個具體數(shù)字后續(xù)推理就鎖定在固定長度上遇到更長的文本就報錯。正確做法是導(dǎo)出時把序列長度維度設(shè)為動態(tài)import torch model.eval() dummy_input torch.randint(0, 1000, (1, 128), dtypetorch.long) torch.onnx.export( model, dummy_input, model.onnx, input_names[input_ids], output_names[logits], dynamic_axes{input_ids: {0: batch_size, 1: seq_len}}, opset_version17, )導(dǎo)出成功之后用推理器測一下速度我一般會寫一個簡單的計(jì)時腳本比較原模型和量化導(dǎo)出模型的差異import onnxruntime as ort import time import numpy as np session ort.InferenceSession(model.onnx, providers[CPUExecutionProvider]) inputs {input_ids: np.random.randint(0, 1000, (1, 128)).astype(int64)} # 預(yù)熱 for _ in range(10): session.run(None, inputs) times [] for _ in range(100): start time.perf_counter() session.run(None, inputs) times.append((time.perf_counter() - start) * 1000) print(fp50: {np.percentile(times, 50):.2f} ms) print(fp95: {np.percentile(times, 95):.2f} ms)3.4 部署方式的選擇邏輯模型導(dǎo)出來之后往哪里部署也有講究。如果只是內(nèi)網(wǎng)離線批量處理數(shù)據(jù)CPU加ONNX Runtime的搭配通常就夠了好處是部署簡單、不依賴GPU機(jī)器。如果要在線上提供實(shí)時接口并且單個請求的語義復(fù)雜程度較高建議保留GPU推理因?yàn)镚PU在高并發(fā)場景下的吞吐能力確實(shí)比CPU強(qiáng)很多但如果業(yè)務(wù)是短文本、低延遲要求不高CPU方案能省下不少硬件成本。我的經(jīng)驗(yàn)是先量化再壓測數(shù)據(jù)出來之后再決定要不要上GPU。很多時候你以為必須上GPU結(jié)果量化加優(yōu)化之后CPU上跑得完全夠用預(yù)算就省下來了。4. 性能觀察延遲、吞吐與顯存的實(shí)際數(shù)據(jù)部署前后有幾個數(shù)據(jù)一定要測不是大概看看而是要認(rèn)真記錄和對比延遲、吞吐、顯存占用。我見過不少項(xiàng)目上線后出問題原因都是上線前沒有一個統(tǒng)一的性能基線。4.1 延遲看P95而不是平均值很多第一次接觸部署的人習(xí)慣看平均延遲實(shí)際上在生產(chǎn)環(huán)境里平均延遲往往會掩蓋問題。假設(shè)你有100個請求99個都很快但有一個因?yàn)榕抨?duì)或者服務(wù)端抖動特別慢平均延遲看起來還行但用戶體驗(yàn)已經(jīng)崩了。大家更關(guān)心的是絕大多數(shù)請求有多快所以頭部延遲指標(biāo)更重要一般看P95也就是95%的請求都低于這個耗時。我一般在壓測時寫一個簡單的并發(fā)請求腳本記錄每個請求的耗時然后計(jì)算分位數(shù)。核心代碼如下import asyncio import time import numpy as np import httpx async def send_one(client, url, text): start time.perf_counter() await client.post(url, json{text: text}) return (time.perf_counter() - start) * 1000 async def main(): url http://your-service:8000/predict texts [測試文本 for _ in range(300)] async with httpx.AsyncClient(timeout30) as client: tasks [send_one(client, url, t) for t in texts] times await asyncio.gather(*tasks) times np.array(times) print(fp50: {np.percentile(times, 50):.2f} ms) print(fp95: {np.percentile(times, 95):.2f} ms) print(fp99: {np.percentile(times, 99):.2f} ms) asyncio.run(main())注意壓測不能只跑幾輪就下結(jié)論至少要模擬貼近業(yè)務(wù)的請求量并且關(guān)注服務(wù)在持續(xù)壓力下有沒有出現(xiàn)超時或者內(nèi)存上漲。4.2 吞吐與批處理的權(quán)衡吞吐量衡量的是單位時間內(nèi)能處理多少請求它和延遲并不是永遠(yuǎn)同向變化。把多個請求合成一個batch喂給模型GPU利用率會上升吞吐量變大但單個請求的等待時間也可能變長。這就需要一個取舍在線對話場景里尾延遲重要優(yōu)先保證單個請求快離線批量判重場景里卡口寬可以接受單請求稍慢一些把吞吐拉滿。拿我做過的一個文本分類服務(wù)來說調(diào)整了batch大小和量化方式后得到過類似這樣的對比數(shù)據(jù)配置平均延遲(ms)P95(ms)吞吐(請求/秒)顯存占用(GB)全精度FP3228452106.2動態(tài)量化INT816263803.1動態(tài)量化大Batch推理21385204.8這個表并不是某個模型的基準(zhǔn)測試報告它只表示縱向變化的趨勢。關(guān)鍵結(jié)論是不同配置各有勝負(fù)如果你只盯著平均延遲選方案可能會選到吞吐很低的配置如果只盯吞吐又可能在線的延遲扛不住。所以壓測之前一定要先想清楚業(yè)務(wù)到底在乎什么指標(biāo)。4.3 顯存監(jiān)控不能只看一眼訓(xùn)練階段看顯存大家習(xí)慣用nvidia-smi隨手看一眼但部署階段的顯存觀測需要更精細(xì)。因?yàn)橥评矸?wù)的顯存占用往往是波動的峰值出現(xiàn)的時間點(diǎn)可能很短暫。我的做法是在壓測期間用一段小循環(huán)持續(xù)采集顯存數(shù)據(jù)而不是只抓一個瞬間while true; do nvidia-smi --query-gpumemory.used --formatcsv gpu_mem.log; sleep 0.5; done然后用一個小腳本分析日志里的最大值、平均值、末尾回落情況。如果顯存在連續(xù)壓測后緩慢上升、回落不下去往往說明存在內(nèi)存泄漏或者緩存管理問題如果峰值超過限額但沒有OOM也最好留出至少20%的余量否則業(yè)務(wù)流量一波動就很容易報警。5. 排查實(shí)錄五個曾經(jīng)讓我熬夜的坑與對策這一節(jié)寫的都是我自己真實(shí)踩過的坑拿出來說一說是因?yàn)樗鼈兲写硇粤藥缀趺扛粢欢螘r間就會有人在社區(qū)里問同樣的問題。每個坑我都會先從現(xiàn)象說起再講排查思路和最終解決方案。5.1 訓(xùn)練到一半loss變成NaN先說一個最讓人崩潰的問題訓(xùn)練一切正常跑了幾個epoch之后loss突然變成NaN然后整個訓(xùn)練狀態(tài)就廢了只能從頭再跑。我第一次遇到時是手忙腳亂的后來總結(jié)了一個排查順序效率比亂試高很多第一步檢查數(shù)據(jù)里有沒有臟數(shù)據(jù)。文本字段里如果全是空串、超長串或者存在異常token模型很容易計(jì)算出極端數(shù)值。可以先打印一批樣本看有沒有明顯異常。第二步檢查學(xué)習(xí)率是否過大或者warmup階段是否缺失。學(xué)習(xí)率偏高時梯度更新幅度會把參數(shù)推出正常范圍loss在某個節(jié)點(diǎn)直接爆炸。第三步檢查混合精度。FP16訓(xùn)練時梯度值如果太小會自動被縮放到一個可表示的范圍內(nèi)但如果某個梯度的數(shù)值實(shí)在太小有可能在縮放過程中被截?cái)喑?或者反過來產(chǎn)生溢出。此時可以臨時關(guān)閉fp16用純FP32重跑同樣的配置觀察問題是否消失。我的經(jīng)驗(yàn)是遇到NaN先不要懷疑模型結(jié)構(gòu)90%以上的情況來自數(shù)據(jù)或者混合精度數(shù)值問題先做排除比改結(jié)構(gòu)快得多。5.2 動態(tài)輸入長度導(dǎo)致顯存忽高忽低還有一個容易被忽略的隱患是動態(tài)長度的padding策略。模型對每個batch都會按batch內(nèi)最大長度補(bǔ)齊如果同一batch內(nèi)的兩個文本長度差異巨大一個幾十字一個上千字這個batch的內(nèi)存占用就完全被長文本主導(dǎo)了導(dǎo)致顯存峰值飄忽不定。我后來采取的做法是在數(shù)據(jù)加載階段按長度分桶把長度相近的樣本放進(jìn)同一個batch短的和短的在一起長的和長的在一起。這樣每個batch的內(nèi)部長度都比較接近顯存曲線的尖峰就被削平了訓(xùn)練也穩(wěn)定不少。這個做法在工具層面實(shí)現(xiàn)并不復(fù)雜就是先按文本長度排序再按batch大小切分然后給每個batch做padding。對比同樣數(shù)據(jù)量下兩種策略的效果顯存峰值差別能到20%甚至更多而且訓(xùn)練速度也更快因?yàn)椴淮嬖凇白屢欢讯虡颖镜纫粋€長樣本”的情況。5.3 量化后某些算子悄悄回退成浮點(diǎn)量化的一個隱蔽坑是看起來你已經(jīng)對模型做了INT8量化但實(shí)際跑到某些算子時推理器會把計(jì)算回退成浮點(diǎn)精度導(dǎo)致速度沒有提升多少還白白承受了精度損失。怎么發(fā)現(xiàn)這個問題我在用推理器跑模型時會開啟profiling能力打印出每個算子的執(zhí)行時間和數(shù)據(jù)類型。如果發(fā)現(xiàn)某些關(guān)鍵算子在配置了量化后依然以FP32運(yùn)行就需要檢查是否使用了不支持量化的自定義算子或者把模型結(jié)構(gòu)里某些模塊替換成推理器更友好的實(shí)現(xiàn)。如果你的模型里加了自定義的pooling層或者特殊激活函數(shù)這幾乎是必踩的坑。我的建議是自定義層盡量用基礎(chǔ)算子拼接實(shí)現(xiàn)這比用高級接口容易優(yōu)化也更方便量化工具識別。5.4 長文本截?cái)嘣斐尚Ч挛⒄{(diào)階段一個容易犯的錯是為了控制顯存和訓(xùn)練速度把所有文本統(tǒng)一截到128個token但業(yè)務(wù)里明明有大量長文本。上線之后發(fā)現(xiàn)長文本樣本的表現(xiàn)明顯比訓(xùn)練數(shù)據(jù)里的短文本差一開始還以為是模型泛化能力不行后來一查才發(fā)現(xiàn)是截?cái)嗖呗园殃P(guān)鍵信息丟掉了。Transformer對長文本的處理有個客觀限制注意力復(fù)雜度隨序列長度平方增長序列越長計(jì)算越貴而且長距離依賴的建模也不一定友好。前幾篇我細(xì)致拆解過多頭注意力機(jī)制時提到它對位置信息的感知依賴位置編碼過長的序列會讓后續(xù)位置的關(guān)系變得稀疏。針對這個問題的實(shí)踐方案是先分析業(yè)務(wù)文本的長度分布再決定截?cái)嗷蛘咔蟹帧φ嬲枰L文本建模的任務(wù)可以按固定步長切成多個片段每段分別過模型最后融合各段的結(jié)果。這樣既控制了單段長度又保住了全文信息。注意融合方式也要驗(yàn)證有時候簡單取平均效果就不錯有時候需要用一個小模型學(xué)習(xí)組合權(quán)重。5.5 服務(wù)冷啟動后第一個請求特別慢這個坑特別小但上線時很容易造成問題。模型服務(wù)剛啟動時第一次來的推理請求經(jīng)常慢得離譜甚至可能超時但熱身后的請求就正常了。原因是推理器在第一次運(yùn)行時需要完成某些算子的初始化、顯存的分配、緩存預(yù)熱等工作這些開銷一次性的但會對監(jiān)控系統(tǒng)產(chǎn)生“尖刺”。我現(xiàn)在的做法是在服務(wù)啟動后做一次主動預(yù)熱用幾條代表性文本先跑一遍推理確認(rèn)模型正常后再對外宣告服務(wù)就緒。如果是用容器部署就在健康檢查接口里加上模型加載完成和預(yù)熱完成的標(biāo)志如果使用進(jìn)程管理器則用一個單獨(dú)的啟動腳本先跑預(yù)熱邏輯再拉起對外服務(wù)??紤]到監(jiān)控告警通常會有一個看P95指標(biāo)的趨勢邏輯如果連健康檢查的請求都因?yàn)槔鋯映瑫r調(diào)度器可能就會把它當(dāng)異常實(shí)例摘除非常影響發(fā)布流程。這個小坑踩過之后我現(xiàn)在所有推理服務(wù)都會把預(yù)熱放在啟動流程里當(dāng)成標(biāo)準(zhǔn)步驟而不是可選項(xiàng)。跑完這一整套流程之后我還有一個習(xí)慣性動作把每次實(shí)驗(yàn)的模型規(guī)模、數(shù)據(jù)集大小、batch設(shè)置、量化方式、延遲和顯存數(shù)據(jù)記錄在一個固定的表格里形成一個自己的基線庫。這樣每次接手新項(xiàng)目我不是憑感覺猜配置而是直接翻歷史實(shí)驗(yàn)記錄從最接近的場景抄一個起點(diǎn)再小步調(diào)整。最后分享一個小技巧如果你在兩個配置之間拿不定主意不要同時改多個變量。比如先固定模型結(jié)構(gòu)不變單獨(dú)對比量化和非量化再固定量化方案調(diào)整batch大小。每次只挪一個變量結(jié)果才可解釋不然出了問題你根本不知道是哪個改動引起的。這個習(xí)慣幫我省下的時間大概是寫這篇內(nèi)容用時的好幾倍。