戰(zhàn):量化、剪枝與編譯優(yōu)化加速推理部署)
1. 模型優(yōu)化器到底在優(yōu)化什么從一次推理延遲排查說起第一次認(rèn)真審視Model-Optimizer這個(gè)詞是在一個(gè)推薦系統(tǒng)的線上問題復(fù)盤會上。當(dāng)時(shí)模型離線指標(biāo)一切正常AUC 穩(wěn)在 0.78但線上 P99 延遲從 120ms 一路漲到 480ms機(jī)器擴(kuò)容了兩輪也沒壓住。排查到最后發(fā)現(xiàn)問題不在模型結(jié)構(gòu)也不在特征服務(wù)而是推理階段的計(jì)算圖里塞滿了冗余算子算子融合沒做量化也沒上顯存帶寬被白白吃掉了一大半。那次之后我才真正意識到Model-Optimizer 不是一個(gè)可選項(xiàng)而是模型從實(shí)驗(yàn)室走向生產(chǎn)環(huán)境的必經(jīng)環(huán)節(jié)。所謂 Model-Optimizer直白講就是一套針對模型本身做“瘦身、提速、省資源”的技術(shù)體系。它要解決的問題非常具體模型太大裝不進(jìn)目標(biāo)設(shè)備、推理太慢扛不住并發(fā)、顯存占用太高導(dǎo)致 batch size 上不去、功耗太高讓端側(cè)設(shè)備發(fā)燙降頻。它服務(wù)的對象也很明確——算法工程師、推理部署工程師、以及所有需要把模型真正跑起來的人。不管你是做 CV、NLP、推薦還是語音只要模型要上線優(yōu)化器這一關(guān)就繞不過去。我見過太多團(tuán)隊(duì)把 90% 的精力花在調(diào)模型結(jié)構(gòu)上最后上線時(shí)才發(fā)現(xiàn)推理成本高得離譜。一個(gè) 7B 參數(shù)的模型FP16 精度下光權(quán)重就要占 14GB 顯存如果再加上 KV Cache 和中間激活值單卡 24GB 都未必跑得動(dòng)。這時(shí)候 Model-Optimizer 的價(jià)值就體現(xiàn)出來了通過量化把 FP16 壓到 INT8 甚至 INT4顯存直接砍到原來的四分之一到八分之一通過算子融合把幾十個(gè)小算子合并成幾個(gè)大算子kernel launch 開銷大幅下降通過剪枝把不重要的權(quán)重去掉計(jì)算量跟著降。這些手段組合起來往往能在精度損失不到 1% 的前提下把推理吞吐提升 2 到 4 倍。這篇文章我會從實(shí)際工程角度出發(fā)把 Model-Optimizer 涉及的核心技術(shù)點(diǎn)、實(shí)操步驟、參數(shù)選擇邏輯、以及我踩過的坑盡可能完整地拆開講清楚。內(nèi)容會覆蓋量化、剪枝、蒸餾、算子融合、圖優(yōu)化、編譯加速這幾大塊每一塊都會給出可復(fù)現(xiàn)的操作路徑和參數(shù)建議。適合已經(jīng)有一定模型訓(xùn)練基礎(chǔ)、正準(zhǔn)備做推理部署的讀者也適合想系統(tǒng)了解模型優(yōu)化全貌的同行參考。2. 模型優(yōu)化的整體設(shè)計(jì)思路與方案選型2.1 優(yōu)化的三個(gè)核心目標(biāo)延遲、吞吐、內(nèi)存做模型優(yōu)化之前必須先想清楚到底在優(yōu)化什么。很多團(tuán)隊(duì)一上來就說“我要量化”結(jié)果量化完發(fā)現(xiàn)延遲沒降多少反而精度掉了兩個(gè)點(diǎn)這就是目標(biāo)沒對齊。Model-Optimizer 的優(yōu)化目標(biāo)其實(shí)就三個(gè)維度而且這三個(gè)維度經(jīng)常互相沖突需要根據(jù)業(yè)務(wù)場景做取舍。延遲Latency指的是單次推理從輸入到輸出花的時(shí)間對實(shí)時(shí)交互類應(yīng)用最關(guān)鍵比如語音助手、自動(dòng)駕駛感知、在線廣告排序。這類場景對 P99 延遲極其敏感用戶等 200ms 和等 500ms 的體驗(yàn)完全是兩回事。吞吐Throughput指的是單位時(shí)間內(nèi)能處理多少請求對離線批處理、大規(guī)模推薦、內(nèi)容審核這類場景更重要吞吐上去了單次成本才能降下來。內(nèi)存Memory則是前兩者的約束條件顯存不夠batch size 就上不去吞吐自然受限內(nèi)存帶寬不夠延遲也會被拖累。我通常的做法是先畫一張三維權(quán)衡圖把業(yè)務(wù)對這三個(gè)指標(biāo)的硬性要求標(biāo)出來。比如在線廣告排序P99 延遲必須小于 150ms那量化和算子融合就是必選項(xiàng)離線視頻審核吞吐優(yōu)先那就可以用更大的 batch size 配合 INT8 量化延遲放寬到秒級也沒關(guān)系。這個(gè)判斷做在前面后面選技術(shù)方案就不會跑偏。2.2 量化、剪枝、蒸餾、編譯四條主流路線怎么選Model-Optimizer 的技術(shù)路線大致可以分成四條量化、剪枝、知識蒸餾、編譯優(yōu)化。這四條路線不是互斥的實(shí)際工程中經(jīng)常組合使用但每條路線的適用場景和投入產(chǎn)出比差別很大。量化是把模型權(quán)重和激活值從高精度浮點(diǎn)FP32/FP16轉(zhuǎn)成低精度整數(shù)INT8/INT4或低精度浮點(diǎn)FP8/BF16。它的優(yōu)勢是通用性強(qiáng)、工具鏈成熟、壓縮比和加速比都很可觀。INT8 量化通常能帶來 2 到 4 倍的推理加速顯存占用降到原來的四分之一。缺點(diǎn)是低精度下精度損失需要仔細(xì)控制尤其是 INT4 量化對校準(zhǔn)數(shù)據(jù)和方法很敏感。剪枝是把模型中不重要的權(quán)重或結(jié)構(gòu)去掉分為非結(jié)構(gòu)化剪枝和結(jié)構(gòu)化剪枝。非結(jié)構(gòu)化剪枝把單個(gè)權(quán)重置零壓縮率高但需要稀疏計(jì)算庫支持實(shí)際加速效果依賴硬件。結(jié)構(gòu)化剪枝直接砍掉整個(gè)通道或注意力頭硬件友好但精度損失相對更大。剪枝更適合參數(shù)量巨大、冗余度高的模型比如早期的 BERT 和 ResNet。知識蒸餾是用一個(gè)大模型教師去指導(dǎo)一個(gè)小模型學(xué)生訓(xùn)練讓小模型學(xué)到教師模型的泛化能力。它的優(yōu)勢是能直接得到一個(gè)結(jié)構(gòu)更小、推理更快的新模型不依賴特殊硬件支持。缺點(diǎn)是訓(xùn)練成本高需要重新訓(xùn)練學(xué)生模型而且蒸餾效果和教師模型質(zhì)量強(qiáng)相關(guān)。編譯優(yōu)化是通過圖優(yōu)化、算子融合、內(nèi)存復(fù)用、自動(dòng)調(diào)優(yōu)等手段把模型計(jì)算圖編譯成針對特定硬件高度優(yōu)化的執(zhí)行代碼。TVM、TensorRT、OpenVINO、ONNX Runtime 都屬于這一類。它的優(yōu)勢是不改模型精度純靠工程手段提速通常能拿到 1.5 到 3 倍的加速。缺點(diǎn)是和硬件綁定較深跨平臺遷移需要重新編譯。下面這張表是我根據(jù)實(shí)際項(xiàng)目經(jīng)驗(yàn)整理的選型參考可以幫你快速判斷該走哪條路優(yōu)化路線典型加速比精度影響實(shí)現(xiàn)難度適用場景INT8 量化2-4x極小0.5%中通用推理加速首選方案INT4 量化3-6x中等1-3%高大模型端側(cè)部署顯存極度受限結(jié)構(gòu)化剪枝1.5-3x中等1-2%中冗余度高的 CNN/Transformer知識蒸餾2-5x可控高需要重新訓(xùn)練小模型編譯優(yōu)化1.5-3x無低所有場景通常和量化組合我的建議是先上編譯優(yōu)化再做 INT8 量化這兩步能解決 80% 的問題。如果還不夠再考慮剪枝和蒸餾。INT4 量化留到最后因?yàn)樗鼘鹊挠绊懶枰屑?xì)評估不是所有模型都扛得住。2.3 精度與速度的平衡如何設(shè)定可接受的精度損失做優(yōu)化最怕的就是“優(yōu)化完精度掉了業(yè)務(wù)方不認(rèn)”。所以在動(dòng)手之前一定要和業(yè)務(wù)方對齊一個(gè)可接受的精度損失范圍。我的經(jīng)驗(yàn)是分類任務(wù)精度損失控制在 0.5% 以內(nèi)檢測任務(wù) mAP 損失控制在 1% 以內(nèi)生成任務(wù)用人工評估或 BLEU/ROUGE 等指標(biāo)控制在 2% 以內(nèi)這個(gè)范圍大多數(shù)業(yè)務(wù)都能接受。設(shè)定好閾值之后優(yōu)化過程就有了明確的停止條件。比如 INT8 量化后精度掉了 0.3%在閾值內(nèi)那就繼續(xù)往下做如果掉了 0.8%超了那就得回頭調(diào)整量化策略比如改用逐通道量化、換校準(zhǔn)數(shù)據(jù)集、或者對敏感層保留 FP16。這個(gè)“量化-評估-調(diào)整”的循環(huán)是 Model-Optimizer 實(shí)操中最耗時(shí)間也最考驗(yàn)經(jīng)驗(yàn)的部分。還有一個(gè)容易被忽略的點(diǎn)精度評估必須用和線上一致的驗(yàn)證集。我見過團(tuán)隊(duì)用訓(xùn)練集的一個(gè)子集做校準(zhǔn)和評估結(jié)果線上效果差很多。校準(zhǔn)數(shù)據(jù)一定要從真實(shí)業(yè)務(wù)分布里采樣覆蓋各種邊界情況否則量化參數(shù)會偏精度損失在線上會被放大。3. 量化實(shí)操從 FP16 到 INT8 的完整落地路徑3.1 量化基本原理為什么 INT8 能加速還不怎么掉精度量化的本質(zhì)是用更少的比特位來表示數(shù)值。FP16 有 16 位其中 1 位符號、5 位指數(shù)、10 位尾數(shù)能表示的數(shù)值范圍很廣但精度有限。INT8 只有 8 位表示范圍是 -128 到 127但它把所有的比特位都用來表示數(shù)值的“精細(xì)度”所以在有限范圍內(nèi)精度反而更高。量化的核心公式是real_value scale * (quantized_value - zero_point)。其中 scale 是縮放因子zero_point 是零點(diǎn)偏移。對于對稱量化zero_point 為 0公式簡化為real_value scale * quantized_value。scale 的計(jì)算方式是scale max(abs(real_value)) / 127也就是把浮點(diǎn)數(shù)的最大絕對值映射到 INT8 的最大值。為什么 INT8 量化精度損失小因?yàn)樯窠?jīng)網(wǎng)絡(luò)對數(shù)值的微小擾動(dòng)有很強(qiáng)的魯棒性。權(quán)重和激活值的分布通常集中在某個(gè)范圍內(nèi)量化只是把這個(gè)范圍線性映射到整數(shù)空間信息損失主要來自舍入誤差。只要 scale 選得合理舍入誤差相對于權(quán)重的整體分布來說很小對最終輸出的影響就有限。但這里有個(gè)關(guān)鍵點(diǎn)激活值的分布是動(dòng)態(tài)的不同輸入下最大值可能差很多。如果用一個(gè)固定的 scale遇到異常大的激活值就會導(dǎo)致大量數(shù)值被截?cái)嗑缺赖?。所以?shí)際量化時(shí)激活值通常采用動(dòng)態(tài)量化每次推理時(shí)實(shí)時(shí)計(jì)算 scale或者用校準(zhǔn)數(shù)據(jù)集統(tǒng)計(jì)一個(gè)合理的范圍。權(quán)重是靜態(tài)的可以直接離線量化。3.2 訓(xùn)練后量化PTQ實(shí)操校準(zhǔn)集怎么選、參數(shù)怎么調(diào)訓(xùn)練后量化Post-Training QuantizationPTQ是最常用的量化方式不需要重新訓(xùn)練只需要一個(gè)校準(zhǔn)數(shù)據(jù)集跑一遍前向傳播統(tǒng)計(jì)激活值分布就能生成量化參數(shù)。它的優(yōu)點(diǎn)是快、成本低缺點(diǎn)是精度損失相對大一些尤其是對激活值分布復(fù)雜的模型。校準(zhǔn)集的選擇是 PTQ 成敗的關(guān)鍵。我的經(jīng)驗(yàn)是校準(zhǔn)集樣本數(shù)在 100 到 500 之間比較合適太少統(tǒng)計(jì)不準(zhǔn)太多收益遞減。樣本要從真實(shí)業(yè)務(wù)數(shù)據(jù)里隨機(jī)采樣覆蓋各種類別和邊界情況。比如做圖像分類校準(zhǔn)集里每個(gè)類別至少要有幾個(gè)樣本做 NLP要覆蓋不同長度、不同領(lǐng)域的文本。校準(zhǔn)方法主要有三種MinMax 校準(zhǔn)、Moving Average MinMax 校準(zhǔn)、Entropy 校準(zhǔn)。MinMax 直接用校準(zhǔn)集里的最大最小值算 scale簡單但容易受異常值影響。Moving Average 對多個(gè) batch 的最大值做滑動(dòng)平均更穩(wěn)定。Entropy 校準(zhǔn)通過最小化量化前后分布的 KL 散度來選 scale精度最好但計(jì)算量大。我一般先用 Entropy 校準(zhǔn)如果速度太慢再換 Moving Average。以 PyTorch 為例PTQ 的典型流程是這樣的import torch from torch.quantization import get_default_qconfig, prepare, convert # 1. 加載訓(xùn)練好的模型并切換到 eval 模式 model MyModel() model.load_state_dict(torch.load(model.pth)) model.eval() # 2. 指定量化配置x86 平臺用 fbgemmARM 平臺用 qnnpack model.qconfig get_default_qconfig(fbgemm) # 3. 插入觀察器準(zhǔn)備量化 model_prepared prepare(model) # 4. 用校準(zhǔn)集跑前向傳播統(tǒng)計(jì)激活值分布 with torch.no_grad(): for batch in calib_loader: model_prepared(batch) # 5. 轉(zhuǎn)換為量化模型 model_quantized convert(model_prepared) # 6. 保存量化模型 torch.save(model_quantized.state_dict(), model_int8.pth)這段代碼看起來簡單但有幾個(gè)坑必須注意。第一prepare之前一定要把模型切到 eval 模式否則 BatchNorm 和 Dropout 的行為不對校準(zhǔn)統(tǒng)計(jì)會出錯(cuò)。第二校準(zhǔn)集不要用訓(xùn)練集訓(xùn)練集已經(jīng)被模型“見過”激活值分布和線上不一致。第三量化后的模型要在真實(shí)驗(yàn)證集上評估不能只看校準(zhǔn)集上的表現(xiàn)。3.3 量化感知訓(xùn)練QAT實(shí)操什么時(shí)候必須上 QAT如果 PTQ 后精度損失超過閾值那就得上量化感知訓(xùn)練Quantization-Aware TrainingQAT。QAT 是在訓(xùn)練過程中模擬量化誤差讓模型學(xué)會適應(yīng)低精度表示。它的精度通常比 PTQ 高 0.5 到 1 個(gè)百分點(diǎn)但需要重新訓(xùn)練成本高不少。QAT 的核心是在前向傳播時(shí)插入偽量化節(jié)點(diǎn)Fake Quantization模擬量化的舍入誤差反向傳播時(shí)用直通估計(jì)器Straight-Through EstimatorSTE把梯度直接傳過去。這樣模型在訓(xùn)練時(shí)就能“感知”到量化的影響權(quán)重會朝著對量化更友好的方向調(diào)整。QAT 的實(shí)操流程比 PTQ 多兩步先做 PTQ 得到初始量化參數(shù)再加載這個(gè)參數(shù)做微調(diào)訓(xùn)練。微調(diào)的學(xué)習(xí)率要設(shè)得很小通常是原始訓(xùn)練學(xué)習(xí)率的十分之一到百分之一訓(xùn)練輪數(shù)也不用太多幾個(gè) epoch 就夠。下面是一個(gè)典型的 QAT 配置# 1. 先做 PTQ 得到初始量化模型 model.qconfig torch.quantization.get_default_qat_qconfig(fbgemm) model_prepared prepare_qat(model, inplaceFalse) # 2. 加載 PTQ 的量化參數(shù)可選能加速收斂 # model_prepared.load_state_dict(ptq_state_dict, strictFalse) # 3. 微調(diào)訓(xùn)練學(xué)習(xí)率設(shè)為原始學(xué)習(xí)率的 1/100 optimizer torch.optim.SGD(model_prepared.parameters(), lr1e-5, momentum0.9) for epoch in range(5): for batch in train_loader: optimizer.zero_grad() output model_prepared(batch) loss criterion(output, target) loss.backward() optimizer.step() # 4. 訓(xùn)練完成后轉(zhuǎn)換為量化模型 model_prepared.eval() model_quantized convert(model_prepared)QAT 最需要注意的是訓(xùn)練和推理的一致性。訓(xùn)練時(shí)用的偽量化節(jié)點(diǎn)推理時(shí)必須真正轉(zhuǎn)成 INT8 算子否則精度對不上。另外QAT 對學(xué)習(xí)率非常敏感學(xué)習(xí)率大了模型會發(fā)散小了收斂太慢需要多試幾組。3.4 逐通道量化 vs 逐張量量化精度差多少、性能差多少量化粒度是另一個(gè)關(guān)鍵選擇。逐張量量化Per-Tensor是整個(gè)張量共用一個(gè) scale實(shí)現(xiàn)簡單、硬件友好但精度損失大。逐通道量化Per-Channel是每個(gè)通道單獨(dú)算 scale精度高但需要更多存儲和計(jì)算。以卷積層為例權(quán)重張量的形狀是[out_channels, in_channels, kH, kW]逐通道量化就是給每個(gè) out_channel 算一個(gè) scale。這樣不同通道的權(quán)重分布差異能被更好地捕捉量化誤差更小。實(shí)測下來逐通道量化比逐張量量化精度高 0.3 到 0.8 個(gè)百分點(diǎn)但推理速度可能慢 5% 到 10%因?yàn)橐嘁淮?scale 查表。我的建議是權(quán)重用逐通道量化激活值用逐張量量化。權(quán)重是靜態(tài)的逐通道量化的額外開銷可以接受激活值是動(dòng)態(tài)的逐張量量化實(shí)現(xiàn)更簡單而且激活值的通道間差異通常沒有權(quán)重那么大。這個(gè)組合在精度和性能之間取得了比較好的平衡。4. 剪枝與蒸餾結(jié)構(gòu)瘦身的兩種思路4.1 結(jié)構(gòu)化剪枝實(shí)操怎么判斷哪些通道可以砍剪枝的思路和量化完全不同。量化是“降低每個(gè)數(shù)值的精度”剪枝是“直接去掉一部分計(jì)算”。結(jié)構(gòu)化剪枝因?yàn)橛布押迷趯?shí)際部署中更常用。它的核心問題是怎么判斷哪些通道或注意力頭不重要可以安全砍掉最常用的判斷標(biāo)準(zhǔn)是權(quán)重的 L1 或 L2 范數(shù)。一個(gè)通道的權(quán)重范數(shù)越小說明它對輸出的貢獻(xiàn)越小越可以砍。具體做法是對每個(gè)卷積層的每個(gè)輸出通道計(jì)算其權(quán)重的 L1 范數(shù)然后按范數(shù)排序砍掉最小的那部分。砍的比例通常從 10% 開始試逐步增加到 30% 或 50%每次砍完都要評估精度。但單純按范數(shù)剪枝有個(gè)問題它沒有考慮通道之間的相關(guān)性。有些通道單獨(dú)看范數(shù)很小但它和別的通道組合起來對輸出影響很大砍了就會掉精度。所以更精細(xì)的做法是用泰勒展開或者 Fisher 信息來評估通道的重要性考慮梯度信息。不過這些方法計(jì)算量大工程上不一定劃算。我一般用“范數(shù)剪枝 微調(diào)”的組合先按 L1 范數(shù)砍掉 20% 到 30% 的通道然后在訓(xùn)練集上微調(diào)幾個(gè) epoch 恢復(fù)精度。微調(diào)的學(xué)習(xí)率設(shè)小一點(diǎn)1e-4 到 1e-5 之間讓模型慢慢適應(yīng)新的結(jié)構(gòu)。實(shí)測下來ResNet-50 砍掉 30% 通道后微調(diào)ImageNet top-1 精度損失能控制在 1% 以內(nèi)。4.2 非結(jié)構(gòu)化剪枝稀疏度上去了為什么速度沒上去非結(jié)構(gòu)化剪枝是把單個(gè)權(quán)重置零理論上能獲得很高的壓縮率比如 90% 的稀疏度。但很多人剪完發(fā)現(xiàn)模型大小是小了推理速度卻沒變甚至更慢。原因在于GPU 和專用加速器對稀疏計(jì)算的支持有限稠密計(jì)算庫遇到零值還是要走一遍乘加運(yùn)算。要讓非結(jié)構(gòu)化剪枝真正加速需要硬件和軟件棧的配合。NVIDIA 的 Ampere 架構(gòu)支持 2:4 稀疏模式也就是每 4 個(gè)權(quán)重里最多 2 個(gè)非零這種結(jié)構(gòu)化稀疏能被 Tensor Core 直接加速理論加速比 2 倍。但 2:4 稀疏對剪枝算法有約束不能隨便砍需要專門訓(xùn)練。所以我的建議是除非你的硬件明確支持稀疏加速否則優(yōu)先做結(jié)構(gòu)化剪枝。非結(jié)構(gòu)化剪枝更適合研究場景或者配合專門的稀疏推理引擎使用。工程落地時(shí)結(jié)構(gòu)化剪枝的收益更確定調(diào)試成本也更低。4.3 知識蒸餾落地教師模型怎么選、溫度參數(shù)怎么調(diào)知識蒸餾適合那種“必須用小模型但精度要求高”的場景。它的核心思想是教師模型輸出的軟標(biāo)簽soft label比真實(shí)硬標(biāo)簽包含更多信息學(xué)生模型學(xué)軟標(biāo)簽?zāi)軐W(xué)到更好的泛化能力。教師模型的選擇很關(guān)鍵。教師模型不一定要最大最強(qiáng)但一定要和目標(biāo)任務(wù)匹配。我見過用 BERT-large 蒸餾 BERT-small 效果很好但用 GPT-3 蒸餾一個(gè)小分類模型就沒什么收益因?yàn)槿蝿?wù)差異太大。教師模型比學(xué)生模型大 3 到 10 倍比較合適太大了蒸餾信號反而不好傳遞。溫度參數(shù) T 是蒸餾的核心超參。T 越大軟標(biāo)簽的分布越平滑學(xué)生模型能學(xué)到的“暗知識”越多。但 T 太大也會導(dǎo)致軟標(biāo)簽過于均勻失去區(qū)分度。我的經(jīng)驗(yàn)是 T 取 3 到 10 之間具體看任務(wù)。分類任務(wù) T4 左右比較常見生成任務(wù) T 可以取大一點(diǎn)。蒸餾損失通常是軟標(biāo)簽損失和硬標(biāo)簽損失的加權(quán)和loss alpha * KL(student_soft, teacher_soft) (1-alpha) * CE(student_logits, hard_label)。alpha 一般取 0.5 到 0.9偏向軟標(biāo)簽。如果教師模型質(zhì)量很高alpha 可以取大一點(diǎn)如果教師模型本身有噪聲alpha 要小一點(diǎn)多依賴硬標(biāo)簽。5. 編譯優(yōu)化與算子融合不改模型也能提速5.1 算子融合原理為什么融合能省時(shí)間算子融合是編譯優(yōu)化里最有效的手段之一。它的原理是把多個(gè)小算子合并成一個(gè)大的 kernel減少 kernel launch 開銷和中間結(jié)果的顯存讀寫。舉個(gè)例子一個(gè)典型的 Transformer 層里有 LayerNorm、QKV 投影、注意力計(jì)算、輸出投影、殘差連接、FFN 等多個(gè)算子。如果不融合每個(gè)算子都要單獨(dú)啟動(dòng)一個(gè) CUDA kernel每個(gè) kernel 都要從顯存讀數(shù)據(jù)、算完再寫回顯存。kernel launch 本身有開銷顯存讀寫更是大頭。融合之后多個(gè)算子在一個(gè) kernel 里完成中間結(jié)果留在寄存器或共享內(nèi)存里不用來回讀寫顯存速度能提升 20% 到 50%。常見的融合模式有Conv BN ReLU 融合、LayerNorm 殘差融合、QKV 投影融合、注意力 Softmax 融合。這些融合在 TensorRT、TVM、ONNX Runtime 里都有現(xiàn)成的實(shí)現(xiàn)通常不需要手寫只要把模型導(dǎo)出成對應(yīng)的格式編譯時(shí)自動(dòng)就會做。5.2 TensorRT 部署實(shí)操從 ONNX 到 engine 的完整流程TensorRT 是 NVIDIA 平臺上最成熟的推理加速方案它把模型編譯成針對特定 GPU 高度優(yōu)化的 engine能拿到 2 到 5 倍的加速。下面是從 ONNX 到 TensorRT engine 的完整流程# 1. 先把 PyTorch 模型導(dǎo)出成 ONNX python export_onnx.py --model model.pth --output model.onnx --opset 13 # 2. 用 trtexec 編譯 ONNX 到 TensorRT engine trtexec --onnxmodel.onnx \ --saveEnginemodel.engine \ --fp16 \ --int8 \ --calibcalibration.cache \ --workspace4096 \ --minShapesinput:1x3x224x224 \ --optShapesinput:8x3x224x224 \ --maxShapesinput:32x3x224x224這里有幾個(gè)參數(shù)需要重點(diǎn)解釋。--fp16開啟 FP16 精度能直接拿到 2 倍加速精度損失幾乎為零建議默認(rèn)開啟。--int8開啟 INT8 量化需要配合--calib指定校準(zhǔn)緩存文件。--workspace是編譯時(shí)可用顯存設(shè)大一點(diǎn)能讓 TensorRT 嘗試更多優(yōu)化策略4096MB 是個(gè)比較穩(wěn)妥的值。--minShapes、--optShapes、--maxShapes指定動(dòng)態(tài) shape 的范圍TensorRT 會針對 optShapes 做最優(yōu)優(yōu)化。導(dǎo)出 ONNX 時(shí)最容易踩的坑是算子不支持。PyTorch 的一些自定義算子、動(dòng)態(tài)控制流、復(fù)雜的索引操作ONNX 可能不支持。解決辦法是盡量用標(biāo)準(zhǔn)算子重寫模型或者用 ONNX 的自定義算子擴(kuò)展。導(dǎo)出后一定要用onnxruntime跑一遍確認(rèn)輸出和 PyTorch 一致再做后續(xù)編譯。5.3 動(dòng)態(tài) shape 與 batch size 調(diào)優(yōu)吞吐和延遲的取舍動(dòng)態(tài) shape 是推理部署里繞不開的問題。線上請求的 batch size 和序列長度都是變化的如果只支持固定 shape要么浪費(fèi)算力要么頻繁重編譯。TensorRT 的動(dòng)態(tài) shape 機(jī)制通過 profile 來支持你指定 min、opt、max 三個(gè) shapeTensorRT 會為 opt shape 做最優(yōu)優(yōu)化其他 shape 走 fallback 路徑。batch size 的選擇是吞吐和延遲的經(jīng)典權(quán)衡。batch size 越大吞吐越高但單次延遲也越大。因?yàn)榇?batch 能更好地利用 GPU 的并行能力但每個(gè)請求要等整個(gè) batch 湊齊才能開始算。在線服務(wù)通常用較小的 batch size1 到 8保證低延遲離線批處理用大 batch size32 到 256追求高吞吐。我的調(diào)優(yōu)方法是先固定一個(gè)可接受的延遲上限比如 P99 小于 200ms然后逐步增大 batch size測吞吐和延遲的變化找到吞吐最高且延遲不超標(biāo)的那個(gè)點(diǎn)。這個(gè)點(diǎn)通常不是最大 batch size而是某個(gè)中間值。另外動(dòng)態(tài) batch 配合請求隊(duì)列能進(jìn)一步提升 GPU 利用率但隊(duì)列長度要控制好太長會導(dǎo)致延遲抖動(dòng)。6. 常見問題與排查技巧實(shí)錄6.1 量化后精度暴跌的五個(gè)常見原因量化后精度暴跌是最常見的問題我整理了一個(gè)排查清單按可能性從高到低排列問題現(xiàn)象可能原因排查方法解決方案精度掉 5% 以上校準(zhǔn)集分布不對對比校準(zhǔn)集和驗(yàn)證集的激活值分布重新采樣校準(zhǔn)集覆蓋真實(shí)分布某些層精度異常敏感層未保護(hù)逐層對比量化前后輸出對敏感層保留 FP16整體精度略降量化粒度太粗檢查是否用了逐張量量化權(quán)重改用逐通道量化輸出全亂scale 計(jì)算溢出檢查激活值是否有 inf/nan加 clip 或換校準(zhǔn)方法特定類別精度差類別不平衡分析各類別精度變化校準(zhǔn)集按類別均衡采樣其中校準(zhǔn)集分布不對是最容易被忽略的。我遇到過一次校準(zhǔn)集用的是公開數(shù)據(jù)集但線上數(shù)據(jù)是手機(jī)拍攝的光照和角度差異很大量化后精度掉了 8%。后來換成線上采樣的校準(zhǔn)集精度損失降到 0.5% 以內(nèi)。這個(gè)教訓(xùn)很深刻校準(zhǔn)集必須來自真實(shí)業(yè)務(wù)分布。6.2 推理速度沒提升反而變慢的排查思路優(yōu)化完速度反而變慢這種情況也不少見。排查思路是先定位瓶頸在哪再針對性解決。用nsight systems或者torch.profiler抓一下推理的 timeline看看時(shí)間花在哪里。常見的變慢原因有第一量化后的算子沒有被硬件加速比如某些自定義算子沒有 INT8 實(shí)現(xiàn)框架 fallback 到 FP32反而多了一次類型轉(zhuǎn)換。第二算子融合沒生效模型里有一些不支持的算子打斷了融合。第三內(nèi)存拷貝開銷太大輸入輸出在 CPU 和 GPU 之間來回拷。第四batch size 太小GPU 利用率上不去優(yōu)化收益被 launch 開銷吃掉。我的經(jīng)驗(yàn)是優(yōu)化后一定要用 profiler 驗(yàn)證不能只看端到端時(shí)間。端到端時(shí)間受很多因素影響profiler 能告訴你每個(gè) kernel 的實(shí)際耗時(shí)定位問題更準(zhǔn)。6.3 跨平臺部署的兼容性坑跨平臺部署是另一個(gè)大坑。在 x86 上量化好的模型放到 ARM 上可能跑不了因?yàn)榱炕阕訉?shí)現(xiàn)不一樣。在 NVIDIA GPU 上編譯的 TensorRT engine換到另一張卡上可能不兼容因?yàn)?engine 和 GPU 架構(gòu)綁定。解決兼容性問題的原則是盡量在目標(biāo)平臺上做優(yōu)化和編譯。如果做不到那就用中間格式ONNX做轉(zhuǎn)換在目標(biāo)平臺上重新編譯。量化方面x86 用 fbgemm 后端ARM 用 qnnpack 后端兩者生成的量化模型不通用需要分別量化。TensorRT engine 更是和 GPU 型號、驅(qū)動(dòng)版本、TensorRT 版本都綁定跨環(huán)境必須重新編譯。還有一個(gè)容易忽略的點(diǎn)不同框架的量化語義可能不一致。PyTorch 的 INT8 和 TensorFlow 的 INT8zero_point 和 scale 的定義可能不同直接轉(zhuǎn)換會出錯(cuò)??缈蚣苻D(zhuǎn)換時(shí)一定要用官方工具并且做數(shù)值對齊驗(yàn)證。7. 我個(gè)人的優(yōu)化經(jīng)驗(yàn)與踩坑記錄做了這么多模型優(yōu)化項(xiàng)目我最大的體會是優(yōu)化不是一次性的工作而是一個(gè)持續(xù)迭代的過程。模型在更新數(shù)據(jù)分布在變化硬件在升級優(yōu)化策略也要跟著調(diào)整。我現(xiàn)在的習(xí)慣是每次模型上線前都跑一遍完整的優(yōu)化 pipeline量化、剪枝、編譯都過一遍用自動(dòng)化腳本保證一致性。另一個(gè)體會是不要追求極致的壓縮比要追求性價(jià)比。INT4 量化能把模型壓到四分之一但精度損失和調(diào)試成本可能讓整個(gè)項(xiàng)目延期。INT8 量化加算子融合通常能拿到 3 到 4 倍加速精度損失不到 0.5%這個(gè)投入產(chǎn)出比是最高的。除非顯存實(shí)在不夠否則沒必要上 INT4。最后分享一個(gè)實(shí)用技巧優(yōu)化前先做 baseline profiling優(yōu)化后再做一次對比每個(gè)環(huán)節(jié)的耗時(shí)變化。這樣你能清楚知道每個(gè)優(yōu)化手段貢獻(xiàn)了多少哪些手段值得繼續(xù)投入哪些可以放棄。我見過團(tuán)隊(duì)花兩周做剪枝結(jié)果只提速 5%而量化一天就提速 2 倍這就是沒有做 profiling 的后果。數(shù)據(jù)驅(qū)動(dòng)才能讓優(yōu)化工作有的放矢。