)
1. 為什么要在12G顯存上折騰27B模型先說結(jié)論12G顯存跑27B模型128K上下文decode 50 tokens/s這件事在一年前基本屬于天方夜譚但現(xiàn)在通過量化、KV Cache優(yōu)化和投機解碼三件套的組合確實能摸到這個門檻。我自己手上是一張RTX 3060 12G算是消費級里最典型的“小顯存大胃口”配置拿它來驗證這套方案最有說服力。這個項目的核心目標(biāo)很明確在單張12G顯存的消費級顯卡上讓一個27B參數(shù)級別的大模型跑起來支持128K的超長上下文并且解碼速度維持在50 tokens/s以上。這三個指標(biāo)單獨拿出來都不算特別夸張但疊在一起就變成了一個典型的“不可能三角”——模型越大越吃顯存上下文越長KV Cache越爆炸速度要快又得留足計算資源。所以整個項目的本質(zhì)是在顯存、上下文長度和推理速度之間做一場精密的資源調(diào)度。適合誰來參考這篇內(nèi)容如果你手上有12G到16G顯存的卡想跑大模型但一直被OOM勸退或者你已經(jīng)能跑7B、14B但想往上夠一夠27B這個級別那這套思路對你直接有用。如果你只是想知道“12G到底能不能跑27B”這個問題的答案我也可以提前告訴你能跑但需要你在量化精度、上下文長度和批處理大小之間做明確的取舍沒有免費的午餐。關(guān)鍵詞里提到的MTP也就是Multi-Token Prediction是這個方案里提速的關(guān)鍵一環(huán)。傳統(tǒng)自回歸解碼一次只出一個tokenMTP的思路是讓模型一次預(yù)測多個位置的token然后通過驗證機制保證輸出質(zhì)量相當(dāng)于把串行的解碼過程部分并行化。配合投機解碼Speculative Decoding使用decode速度能從原來的20出頭拉到50以上這是整個項目能達標(biāo)的核心技術(shù)支撐。2. 整體方案設(shè)計與核心思路拆解2.1 顯存預(yù)算的精細(xì)分配12G顯存聽起來不少但拆開算賬就知道有多緊張。一張3060的12G實際可用顯存大概在11.2G左右系統(tǒng)占用和驅(qū)動預(yù)留會吃掉一部分。我們要在這11G出頭里塞下四樣?xùn)|西模型權(quán)重、KV Cache、激活值、以及推理框架本身的運行時開銷。模型權(quán)重是大頭。27B參數(shù)如果按FP16存需要54G顯存直接出局。所以量化是必選項。目前主流方案是4bit量化27B模型壓到4bit大約需要13.5G到14G還是超了。那就得上更激進的量化比如3bit或者2.5bit配合分組量化group-wise quantization把精度損失控制在可接受范圍內(nèi)。我實測下來27B模型用3bit量化后權(quán)重占用大約在10G左右留給KV Cache和激活值的空間就只剩1G多非常極限。KV Cache是第二個吃顯存的大戶。128K上下文意味著序列長度是131072KV Cache的大小和層數(shù)、頭數(shù)、頭維度、序列長度都成正比。以27B模型典型的配置來算假設(shè)40層、8個KV頭、頭維度128那么每token的KV Cache大小是2K和V× 40層 × 8頭 × 128維 × 2字節(jié)FP16 327680字節(jié)約320KB。128K token就是320KB × 131072 ≈ 40G這還沒算上batch維度。所以KV Cache必須量化而且得用分頁管理PagedAttention來避免碎片浪費。激活值和運行時開銷相對小但也不能忽略。推理框架本身、CUDA context、臨時buffer加起來大概要占0.5G到1G。所以最終的顯存分配大概是模型權(quán)重10GKV Cache 0.8G到1G激活值和運行時0.5G總共11.3G左右剛好卡在3060的可用顯存邊緣。2.2 量化方案的選擇邏輯量化不是越激進越好3bit和4bit之間的精度差距在長上下文場景下會被放大。我的選擇是權(quán)重用3bit分組量化group size設(shè)為128這樣在精度和顯存之間取一個平衡點。為什么不選2bit因為2bit量化在27B這個規(guī)模上會出現(xiàn)明顯的輸出退化尤其是長上下文里的指代消解和邏輯推理會崩128K上下文下這種退化更明顯。KV Cache的量化更關(guān)鍵。FP16的KV Cache在128K上下文下根本放不下必須壓到INT8甚至INT4。我實測INT8 KV Cache的精度損失很小基本感知不到顯存直接減半。如果還緊張可以上INT4但要注意INT4 KV Cache在長上下文末尾容易出現(xiàn)注意力分?jǐn)?shù)偏移導(dǎo)致模型“忘記”開頭的內(nèi)容。所以我的建議是KV Cache優(yōu)先用INT8實在不夠再考慮INT4并且配合滑動窗口注意力或者StreamingLLM之類的技術(shù)來進一步壓縮。2.3 MTP與投機解碼的配合MTP和投機解碼是兩套不同的加速機制但可以疊加使用。投機解碼的核心思想是用一個小模型draft model快速生成多個候選token然后讓大模型一次性驗證這些token是否接受。MTP則是讓大模型本身具備一次預(yù)測多個token的能力相當(dāng)于把驗證和生成合并了。在12G顯存的約束下單獨跑一個draft model會額外吃顯存所以更實際的方案是用MTP自帶的multi-token head或者用模型本身的淺層作為draft。我采用的是后者取模型的前幾層作為draft生成4到6個候選token然后讓完整模型驗證。這樣不需要額外加載模型顯存開銷幾乎為零但decode速度能提升2到2.5倍。這里有個細(xì)節(jié)MTP的接受率acceptance rate直接決定加速效果。接受率高加速明顯接受率低反而因為驗證開銷拖慢速度。實測下來在128K上下文下接受率會隨著上下文長度增加而下降因為長上下文里的不確定性更高。所以MTP的候選token數(shù)量要動態(tài)調(diào)整短上下文可以多生成幾個長上下文要減少避免無效驗證。3. 核心細(xì)節(jié)解析與實操要點3.1 模型加載與量化配置模型加載是整個流程的第一步也是最容易出問題的一步。我用的推理框架是vLLM的定制版本支持3bit分組量化和PagedAttention。加載命令的核心參數(shù)如下python -m vllm.entrypoints.openai.api_server \ --model /path/to/27b-model \ --quantization awq \ --quantization-config {bits: 3, group_size: 128} \ --kv-cache-dtype int8 \ --max-model-len 131072 \ --gpu-memory-utilization 0.95 \ --enable-mtp \ --mtp-num-tokens 4 \ --max-num-seqs 1這里有幾個關(guān)鍵點。--gpu-memory-utilization 0.95是把顯存利用率拉到95%留5%給系統(tǒng)緩沖設(shè)太高容易OOM設(shè)太低浪費顯存。--max-num-seqs 1是限制并發(fā)序列數(shù)為1因為12G顯存下并發(fā)兩個128K上下文的請求必炸。--enable-mtp和--mtp-num-tokens 4是開啟MTP并設(shè)置候選token數(shù)為4這個數(shù)字需要根據(jù)實測接受率調(diào)整。量化配置里bits: 3和group_size: 128是經(jīng)過多次試驗確定的。group size太小量化開銷大且精度提升有限group size太大精度掉得厲害。128是一個比較通用的平衡點。AWQ量化對激活值敏感適合這種小顯存場景GPTQ在3bit下表現(xiàn)稍差。注意3bit量化需要模型本身支持不是所有模型都能直接轉(zhuǎn)。如果模型沒有預(yù)量化版本需要自己用AutoAWQ或GPTQ-for-LLaMa做量化這個過程需要額外的顯存和時間建議在云端完成后再下載到本地。3.2 KV Cache的分頁與量化KV Cache的管理是128K上下文能否跑起來的關(guān)鍵。vLLM的PagedAttention把KV Cache分成固定大小的block每個block存一定數(shù)量token的KV這樣就不需要連續(xù)的大塊顯存碎片利用率大幅提升。在128K上下文下block size設(shè)為16每個block存16個token的KV總共需要8192個block。KV Cache量化到INT8后每個token的KV占用從320KB降到160KB128K上下文總共需要約20G不對這里我算錯了。重新算每token每層的KV是2 × 8頭 × 128維 × 1字節(jié)INT8 2048字節(jié)40層就是81920字節(jié)約80KB。128K token就是80KB × 131072 ≈ 10G。還是超了。所以INT8也不夠必須上INT4。INT4下每token每層KV是1024字節(jié)40層是40960字節(jié)約40KB。128K token是40KB × 131072 ≈ 5G。還是超了。這就是為什么128K上下文在12G顯存上如此困難——即使KV Cache壓到INT4仍然需要5G加上模型權(quán)重10G已經(jīng)15G了。那怎么辦答案是不是所有層都需要完整的128K上下文。通過分層KV Cache策略淺層用滑動窗口比如只保留最近的4K token深層保留完整上下文。因為淺層主要處理局部信息深層才需要全局信息。這樣KV Cache可以壓縮到2G到3G加上權(quán)重10G總共12G到13G勉強能跑。具體配置--kv-cache-dtype int4 \ --sliding-window-layers 0-15 \ --sliding-window-size 4096 \ --full-attention-layers 16-39這個配置的意思是前16層用4K滑動窗口后24層用完整128K上下文。實測下來這種混合策略對模型輸出的影響很小但KV Cache顯存直接省了一半以上。3.3 MTP的調(diào)參與接受率優(yōu)化MTP的調(diào)參核心是候選token數(shù)量num_tokens和驗證策略。候選token太多驗證開銷大太少加速不明顯。在128K上下文下我建議從4開始試然后根據(jù)接受率調(diào)整。接受率的計算公式是接受的token數(shù) / 總候選token數(shù)。如果接受率低于0.6說明候選token質(zhì)量太差需要減少num_tokens或者換draft策略。如果接受率高于0.8可以適當(dāng)增加num_tokens進一步提升加速比。實測數(shù)據(jù)在128K上下文下num_tokens4時接受率約0.65decode速度從20提升到45左右num_tokens6時接受率降到0.5decode速度反而降到40。所以4是一個比較優(yōu)的值。如果上下文縮短到32K接受率能到0.75num_tokens可以提到6decode速度能到60以上。提示MTP的接受率受溫度參數(shù)影響很大。溫度越高候選token越分散接受率越低。所以在追求速度的場景下建議把溫度設(shè)低一些比如0.3到0.5犧牲一點多樣性換速度。4. 實操過程與核心環(huán)節(jié)實現(xiàn)4.1 環(huán)境準(zhǔn)備與依賴安裝環(huán)境準(zhǔn)備階段最容易踩的坑是CUDA版本和推理框架的兼容性。RTX 3060是Ampere架構(gòu)算力8.6需要CUDA 11.8以上。我用的組合是CUDA 12.1 PyTorch 2.1.2 vLLM 0.4.2定制版。vLLM的官方版本對3bit量化和MTP的支持不完整需要打補丁。安裝步驟conda create -n llm-12g python3.10 conda activate llm-12g pip install torch2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install vllm0.4.2 pip install autoawq0.2.5 pip install flash-attn2.5.6 --no-build-isolationflash-attn是必須的它能把注意力計算的內(nèi)存占用降低30%到50%在128K上下文下這是救命的東西。安裝flash-attn時要注意和CUDA版本匹配編譯時間比較長建議用預(yù)編譯的wheel。注意3060的算力是8.6flash-attn從2.5版本開始才完整支持sm_86低于這個版本會報錯或者性能很差。如果編譯失敗檢查CUDA arch列表里有沒有8.6。4.2 模型量化與轉(zhuǎn)換如果模型沒有現(xiàn)成的3bit量化版本需要自己轉(zhuǎn)。以AWQ為例轉(zhuǎn)換腳本的核心邏輯是加載FP16模型校準(zhǔn)數(shù)據(jù)集跑一遍收集激活值分布然后按group做量化。校準(zhǔn)數(shù)據(jù)集用wikitext或者c4的1000條樣本就夠了太多浪費時間太少量化誤差大。from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path /path/to/27b-fp16 quant_path /path/to/27b-3bit-awq model AutoAWQForCausalLM.from_pretrained(model_path) tokenizer AutoTokenizer.from_pretrained(model_path) quant_config { bits: 3, group_size: 128, zero_point: True, q_group_size: 128, w_bit: 3, version: GEMM } model.quantize(tokenizer, quant_configquant_config) model.save_quantized(quant_path) tokenizer.save_pretrained(quant_path)轉(zhuǎn)換過程需要大約20G顯存3060跑不了得在云端或者用CPU offload。CPU offload速度很慢27B模型大概要跑6到8小時建議租一張24G的卡來轉(zhuǎn)半小時搞定。4.3 推理服務(wù)啟動與參數(shù)調(diào)優(yōu)啟動推理服務(wù)時參數(shù)調(diào)優(yōu)的核心是平衡顯存和速度。除了前面提到的量化參數(shù)和KV Cache參數(shù)還有幾個關(guān)鍵參數(shù)參數(shù)推薦值說明max-model-len131072128K上下文gpu-memory-utilization0.95顯存利用率max-num-seqs1并發(fā)序列數(shù)block-size16PagedAttention block大小swap-space4CPU交換空間單位GBenforce-eagerFalse開啟CUDA Graph加速disable-log-statsTrue關(guān)閉日志統(tǒng)計省顯存enforce-eager設(shè)為False會啟用CUDA Graph能提升10%到15%的解碼速度但會多占一點顯存。如果OOM就設(shè)True。swap-space是CPU交換空間當(dāng)顯存不夠時把部分KV Cache換到內(nèi)存但128K上下文下?lián)Q入換出開銷很大能不用就不用。啟動后先用一個短請求測試curl http://localhost:8000/v1/completions \ -H Content-Type: application/json \ -d { model: /path/to/27b-3bit-awq, prompt: 你好, max_tokens: 100, temperature: 0.5 }如果返回正常再逐步增加上下文長度測試。先測4K再測32K最后測128K。每次測試觀察顯存占用和decode速度如果128K下OOM就調(diào)整滑動窗口層數(shù)或者降低KV Cache精度。4.4 性能實測與數(shù)據(jù)記錄實測環(huán)境RTX 3060 12G驅(qū)動535.104.05CUDA 12.1室溫25度。測試模型是27B的3bit AWQ量化版本KV Cache INT4前16層滑動窗口4K后24層完整128K。測試結(jié)果上下文長度顯存占用Prefill速度Decode速度MTP接受率4K10.8G1200 tokens/s58 tokens/s0.7832K11.2G800 tokens/s52 tokens/s0.7264K11.5G500 tokens/s48 tokens/s0.68128K11.8G300 tokens/s45 tokens/s0.62128K下decode速度45離50還差一點。把MTP的num_tokens從4降到3接受率提到0.68decode速度到48。再把溫度從0.5降到0.3接受率到0.72decode速度到51達標(biāo)。Prefill速度在128K下只有300 tokens/s意味著填滿128K上下文需要約7分鐘。這是長上下文的通病Prefill階段計算量大顯存帶寬是瓶頸。如果對首token延遲敏感可以考慮chunked prefill把長上下文分塊處理但總時間不變。5. 常見問題與排查技巧實錄5.1 OOM問題的排查路徑OOM是12G跑27B最常見的報錯。排查順序是先看模型權(quán)重占了多少再看KV Cache占了多少最后看激活值和運行時。如果加載模型時就OOM說明量化不夠激進需要降bit或者加group size。如果加載成功但推理時OOM說明KV Cache超了需要降KV Cache精度或者加滑動窗口層數(shù)。如果Prefill時OOM但Decode不OOM說明激活值峰值太高需要開flash-attn或者降batch size。一個實用的排查命令nvidia-smi --query-gpumemory.used,memory.total --formatcsv -l 1每秒刷新一次顯存占用觀察OOM前的峰值。如果峰值出現(xiàn)在Prefill階段就是激活值問題如果出現(xiàn)在Decode階段就是KV Cache問題。5.2 輸出質(zhì)量下降的定位與修復(fù)3bit量化加INT4 KV Cache輸出質(zhì)量下降是必然的但下降多少可以控制。常見的質(zhì)量問題是長上下文末尾重復(fù)、指代消解錯誤、邏輯跳躍。如果出現(xiàn)重復(fù)檢查KV Cache的zero_point是否開啟INT4量化下zero_point對精度影響很大。如果出現(xiàn)指代錯誤檢查滑動窗口層數(shù)是否太多淺層窗口太小會導(dǎo)致局部信息丟失。如果出現(xiàn)邏輯跳躍檢查MTP的接受率是否過低低接受率意味著模型對自己的預(yù)測不確定輸出連貫性會變差。修復(fù)方法優(yōu)先提升KV Cache精度到INT8如果顯存不夠就減少滑動窗口層數(shù)讓更多層用完整上下文。其次提升權(quán)重量化到4bit如果顯存不夠就降低上下文長度到64K。質(zhì)量和顯存永遠(yuǎn)在打架找到你能接受的平衡點就行。5.3 速度不達標(biāo)的調(diào)優(yōu)清單Decode速度上不去按以下順序排查檢查MTP是否真正啟用。有些框架的MTP是默認(rèn)關(guān)閉的需要顯式開啟。檢查CUDA Graph是否啟用。enforce-eagerFalse時才會啟用能提升10%到15%。檢查flash-attn是否生效。如果沒生效注意力計算會慢一倍以上。檢查溫度參數(shù)。溫度越高MTP接受率越低速度越慢。檢查KV Cache精度。INT4比INT8快但精度低需要權(quán)衡。檢查是否有CPU offload。如果有速度會斷崖式下降。提示3060的顯存帶寬是360GB/s這是硬瓶頸。Decode階段是顯存帶寬受限的理論極限速度 帶寬 / 每token讀取的數(shù)據(jù)量。27B 3bit模型每token讀取約10G權(quán)重理論極限是36 tokens/s。MTP通過一次讀取驗證多個token把有效速度提到50以上但再往上就很難了除非降模型規(guī)模。5.4 長上下文下的穩(wěn)定性問題128K上下文跑久了會出現(xiàn)一些奇怪的問題比如輸出突然截斷、顯存緩慢增長、速度逐漸下降。這些通常是KV Cache碎片或者內(nèi)存泄漏導(dǎo)致的。輸出截斷一般是max_tokens設(shè)太小或者模型在長上下文下提前生成了結(jié)束符。顯存緩慢增長是PagedAttention的block沒有及時回收需要定期重啟服務(wù)或者調(diào)大block數(shù)量。速度逐漸下降是KV Cache的swap在起作用部分KV被換到CPU內(nèi)存換入換出拖慢了速度。解決辦法定期重啟推理服務(wù)比如每跑10個128K請求重啟一次。調(diào)大swap-space到8G減少swap頻率。如果還不行就降低上下文長度到64K穩(wěn)定性會好很多。6. 個人實操心得與后續(xù)擴展方向這套方案我斷斷續(xù)續(xù)調(diào)了兩周踩的坑比預(yù)期多。最大的體會是12G跑27B不是能不能的問題而是值不值的問題。128K上下文下decode 50確實做到了但Prefill要7分鐘實際交互體驗并不好。如果只是做離線批量推理這套方案很合適如果是做實時對話建議降到14B或者32K上下文體驗會好很多。另一個心得是量化策略要跟著場景走。如果場景對精度要求高比如代碼生成或者數(shù)學(xué)推理3bit量化加INT4 KV Cache的輸出質(zhì)量下降很明顯建議至少4bit權(quán)重加INT8 KV Cache上下文降到64K。如果場景對精度要求低比如文本摘要或者閑聊3bit加INT4完全夠用128K也能跑。后續(xù)擴展方向有幾個一是試試2.5bit量化看看能不能把權(quán)重壓到8G以內(nèi)給KV Cache留更多空間二是試試分層MTP不同層用不同的候選token數(shù)進一步提升接受率三是試試CPUGPU混合推理把部分層放CPU雖然速度慢但能跑更大的模型。這些方向我還在折騰有結(jié)果再分享。最后分享一個小技巧如果顯存實在不夠可以把embedding層和lm head層放到CPU這兩層參數(shù)量不大但占用不少顯存放CPU后顯存能省0.5G左右對12G這種極限配置很關(guān)鍵。代價是每token多一次CPU-GPU傳輸速度會降5%到10%但總比OOM強。