戰(zhàn))
12G顯存跑27B模型128K上下文decode每秒50個(gè)token以上——這三個(gè)數(shù)字放在一起的時(shí)候我第一反應(yīng)是參數(shù)寫(xiě)錯(cuò)了第二反應(yīng)是想試一下。結(jié)果真讓我跑起來(lái)了而且不是那種“勉強(qiáng)出字”的狀態(tài)是正常對(duì)話、正常寫(xiě)代碼、正常處理長(zhǎng)文檔的狀態(tài)。這篇文章就把整個(gè)思路、量化選型、資源計(jì)算和踩坑過(guò)程完整曬一遍給手里只有消費(fèi)級(jí)顯卡、又不想折騰云GPU的人做一個(gè)可復(fù)現(xiàn)的參考。先交代一下環(huán)境和測(cè)試對(duì)象顯卡是RTX 3060 12G顯存帶寬360GB/s左右算力在今天的消費(fèi)卡里只能算中游。測(cè)試模型以Qwen2.5-27B這類稠密27B模型為主順帶聊一下MiniMax H3這種新模型的特殊玩法。下面所有參數(shù)和命令都是實(shí)測(cè)過(guò)的你可以直接照著抄但要根據(jù)自己的卡微調(diào)。1. 項(xiàng)目概述為什么說(shuō)這是“極限”27B參數(shù)意味著什么FP16精度下光權(quán)重就要27B乘以2字節(jié)也就是54GB這已經(jīng)超過(guò)絕大多數(shù)單卡顯存。哪怕是跑云上的A100 80G也塞不下幾個(gè)并發(fā)。所以要把27B塞進(jìn)12G顯存第一步不是優(yōu)化代碼而是做數(shù)學(xué)上的減法。顯存占用主要由三塊構(gòu)成模型權(quán)重、KV cache、運(yùn)行時(shí)開(kāi)銷CUDA context、激活值、中間buffer。其中權(quán)重是固定的KV cache隨著上下文長(zhǎng)度線性增長(zhǎng)運(yùn)行時(shí)開(kāi)銷則和你用的推理框架強(qiáng)相關(guān)。12G顯存能用的實(shí)際空間其實(shí)不到12GCUDA context和驅(qū)動(dòng)預(yù)留大概會(huì)吃掉0.5到1G所以真實(shí)可用空間要按10.5到11.5G來(lái)規(guī)劃。核心難點(diǎn)有兩個(gè)一是權(quán)重怎么壓縮到10G左右二是128K上下文的KV cache怎么不把剩下的空間吃光。這兩個(gè)問(wèn)題解決不了后面什么decode速度都是空談。我先把整條技術(shù)路線列出來(lái)后面逐一展開(kāi)。資源項(xiàng)FP16INT8Q4_K_MQ3_K_SQ2_K27B權(quán)重~54GB~27GB~16.5GB~12.5GB~10.8GB128K KV cache(BF16)8.6GB----128K KV cache(Q8)-4.3GB4.3GB4.3GB4.3GB128K KV cache(Q4)-2.1GB2.1GB2.1GB2.1GB一眼就能看出來(lái)FP16和INT8直接出局Q4_K_M配合Q8 KV也超了12G預(yù)算。真正可行的組合只有兩個(gè)方向Q3_K_S級(jí)別的權(quán)重配合Q4 KV cache或者M(jìn)模型配合某種“不把所有參數(shù)都讀到顯存里”的推理方式。這個(gè)結(jié)論決定了后面所有操作。1.1 目標(biāo)讀者和適用場(chǎng)景這篇文章主要面向三類人一是手里有12G到16G消費(fèi)級(jí)顯卡想跑本地大模型但被各種顯存報(bào)錯(cuò)勸退的玩家二是需要在本地處理長(zhǎng)文檔、代碼倉(cāng)庫(kù)、小規(guī)模批處理的開(kāi)發(fā)者三是想搞懂量化、KV cache、MoE這幾塊技術(shù)細(xì)節(jié)的學(xué)習(xí)者。不適用的人群也很明確追求極致生成質(zhì)量、不接受量化損失的人直接去用云API需要高并發(fā)生產(chǎn)環(huán)境的人繼續(xù)用vLLM配合多卡。本地單卡跑27B的定位始終是“個(gè)人生產(chǎn)力工具”不是“企業(yè)級(jí)推理服務(wù)器”。想明白這一點(diǎn)很多參數(shù)選擇就不會(huì)糾結(jié)了。2. 工具鏈與量化方案選型2.1 為什么最終選了llama.cpp市面上能跑27B模型的推理框架很多我在這個(gè)項(xiàng)目里主要對(duì)比了四個(gè)Ollama、llama.cpp、Transformersbitsandbytes、vLLM。Ollama上手最友好一條命令拉模型就能跑但它的封裝層級(jí)較多對(duì)底層參數(shù)的控制能力有限尤其是KV cache量化、專家卸載這些高級(jí)選項(xiàng)Ollama的暴露程度不夠。Transformersbitsandbytes能做4bit加載但速度不理想HuggingFace的推理?xiàng)T陂L(zhǎng)上下文場(chǎng)景下內(nèi)存翻倍的問(wèn)題很突出。vLLM在服務(wù)化和吞吐上有優(yōu)勢(shì)但它的PagedAttention在12G顯存上跑27B量化模型顯存規(guī)劃不夠精細(xì)經(jīng)常是啟動(dòng)就OOM。llama.cpp是最后的選擇也是唯一能同時(shí)滿足三個(gè)條件的選擇。它的GGUF量化格式本身就是為消費(fèi)級(jí)硬件設(shè)計(jì)的支持靈活的層數(shù)卸載-ngl參數(shù)支持KV cache量化--cache-type-k/v支持Flash Attention-fa支持MoE專家卸載--n-cpu-moe這幾個(gè)能力組合起來(lái)正好覆蓋我們面臨的所有瓶頸。雖然它也有一些歷史包袱比如某些新模型架構(gòu)支持滯后但作為本地推理引擎它的靈活度目前沒(méi)有對(duì)手。2.2 量化格式和檔位選擇確定llama.cpp之后量化格式就只能是GGUF。GGUF不是一種固定精度的格式而是一個(gè)容器里面可以裝不同比特?cái)?shù)的量化權(quán)重。常見(jiàn)檔位從高到低是Q8_0、Q6_K、Q5_K_M、Q4_K_M、Q3_K_S、Q3_K_M、Q2_K數(shù)字越大保留精度越高文件也越大。原則很簡(jiǎn)單在能塞進(jìn)顯存的前提下選精度最高的那個(gè)。但如果就是塞不進(jìn)去寧可降一檔量化也不要強(qiáng)行開(kāi)CPU offload。實(shí)測(cè)數(shù)據(jù)說(shuō)明一個(gè)反直覺(jué)的結(jié)論在12G顯存上Q3_K_S全GPU加載decode速度比Q4_K_M加部分CPU offload快一倍以上。原因后面講帶寬的時(shí)候再展開(kāi)。具體到操作如果你用的是HuggingFace上的原始FP16權(quán)重需要手動(dòng)跑一次量化命令git lfs install git clone https://huggingface.co/Qwen/Qwen2.5-27B cd Qwen2.5-27B # 轉(zhuǎn)換FP16權(quán)重為GGUF格式 python convert_hf_to_gguf.py ./ --outfile qwen2.5-27b-fp16.gguf # 量化為Q3_K_S ../build/bin/llama-quantize qwen2.5-27b-fp16.gguf qwen2.5-27b-Q3_K_S.gguf Q3_K_S不想自己動(dòng)手的話直接去HuggingFace找別人量化好的GGUF文件或者用Ollama現(xiàn)成的標(biāo)簽。省時(shí)間但要知道自己下的到底是什么檔位很多人OOM就是因?yàn)闆](méi)注意Ollama默認(rèn)拉的是Q4而Q4在12G上配合128K上下文就是塞不下。2.3 同一檔位也有坑K系列與S系列的區(qū)別Q3_K_S和Q3_K_M看起來(lái)都是3bit實(shí)際區(qū)別很大。K系列K-quants使用混合精度策略大部分權(quán)重用3bit但有一小部分關(guān)鍵張量保留到6bit甚至8bit。其中SSmall和MMiddle的區(qū)別在于保留高精度張量的范圍和數(shù)量。M更精細(xì)體積更大質(zhì)量也更好S更激進(jìn)體積更小。在27B模型上Q3_K_M比Q3_K_S大了大概1.5G但對(duì)長(zhǎng)上下文的語(yǔ)義保持有明顯幫助。如果只跑16K到32K上下文我會(huì)選Q3_K_M如果要硬上128K那只能Q3_K_S或者Q2_K。不存在完美的檔位只有當(dāng)下資源約束下的最優(yōu)解。3. 128K上下文的資源賬與實(shí)現(xiàn)方法3.1 KV cache到底吃了多少顯存很多人誤解KV cache的大小覺(jué)得它只跟模型大小有關(guān)其實(shí)它取決于三個(gè)因素層數(shù)、KV head數(shù)量、上下文長(zhǎng)度。以Qwen2.5-27B為例32層、GQA配置下KV head是4個(gè)、head_dim為128。算一下每個(gè)token的KV占用每層每個(gè)token的KV字節(jié)數(shù) KV head數(shù) × 2K和V × head_dim × 字節(jié)數(shù)以BF16計(jì)算每個(gè)token每層是4 × 2 × 128 × 2 2048字節(jié)。32層就是64KB。128K上下文就是131072 × 65536字節(jié)約8.6GB。這個(gè)數(shù)字直接把BF16 KV cache判了死刑。解決方案是量化KV cache。llama.cpp支持把K和V分別量化為Q8_0或Q4_0等格式代價(jià)是KV cache的精度損失換來(lái)的是顯存減半甚至減到四分之一。使用Q8之后128K上下文的KV cache降到4.3GB用Q4進(jìn)一步降到2.1GB。配合Q3_K_S權(quán)重總算能擠進(jìn)12G。3.2 實(shí)操配置Flash Attention和上下文分段除了量化KV cache還有兩個(gè)關(guān)鍵開(kāi)關(guān)。第一是Flash Attentionllama.cpp中用-fa啟用它能減少注意力計(jì)算時(shí)的臨時(shí)內(nèi)存分配并且在某些架構(gòu)下加速長(zhǎng)上下文推理。第二是KV cache offload也就是--no-kv-offload參數(shù)把KV cache放到CPU內(nèi)存而不是顯存。這里有個(gè)經(jīng)驗(yàn)判斷如果模型權(quán)重已經(jīng)把顯存占得只剩2G那KV cache放顯存還是放內(nèi)存要實(shí)測(cè)對(duì)比。放顯存decode更快但容易OOM放內(nèi)存會(huì)拖慢速度但能保住長(zhǎng)上下文能力。優(yōu)先跑一個(gè)短測(cè)試觀察同樣prompt下兩種方案的decode速度和顯存占用再?zèng)Q定最終配置。我最終在Qwen2.5-27B上選了KV cache放內(nèi)存因?yàn)?G不到的剩余顯存連Q4的128K KV都裝不下。3.3 上下文長(zhǎng)度分配策略128K上下文不是從頭到尾都用滿的。真實(shí)使用中你往往需要“開(kāi)頭很長(zhǎng)中間一段結(jié)尾很長(zhǎng)”的對(duì)話歷史或文檔內(nèi)容。llama.cpp的上下文是預(yù)先分配的-c 131072一開(kāi)KV cache就按128K預(yù)留。這意味著即便你只輸入20個(gè)tokenKV cache的顯存或內(nèi)存占用也已經(jīng)按128K頂格分配了。如果你沒(méi)有剛需不要一上來(lái)就開(kāi)128K。我建議按照16K、32K、64K、128K四檔分別測(cè)試資源占用和速度選擇你能接受的上限。128K在這套配置里屬于“能做但代價(jià)大”的選項(xiàng)它讓KV cache占用了近一半的可用顯存直接擠壓了權(quán)重的量化空間。4. 實(shí)操過(guò)程從零配置到decode 504.1 環(huán)境準(zhǔn)備和編譯細(xì)節(jié)我用的是自己的機(jī)器AMD Ryzen 7 RTX 3060 12G 64G內(nèi)存系統(tǒng)是Ubuntu 24.04。llama.cpp從源碼編譯CUDA版本12.4。編譯命令如下git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURES86 cmake --build build --config Release -j 8GGML_CUDAON表示啟用CUDA后端CMAKE_CUDA_ARCHITECTURES86對(duì)應(yīng)RTX 30系顯卡的Ampere架構(gòu)compute capability 8.6。如果這一行不寫(xiě)CMake會(huì)自動(dòng)檢測(cè)但有時(shí)候會(huì)選錯(cuò)架構(gòu)導(dǎo)致kernel編譯不兼容。編譯完成后build/bin目錄下會(huì)有l(wèi)lama-cli、llama-server、llama-bench等工具。4.2 啟動(dòng)命令和參數(shù)解讀我用的是llama-server因?yàn)橛蠴penAI兼容API配合各種客戶端很方便。核心啟動(dòng)命令如下./build/bin/llama-server \ -m models/qwen2.5-27b-Q3_K_S.gguf \ -ngl 99 \ -c 131072 \ --cache-type-k q4_0 \ --cache-type-v q4_0 \ --flash-attn \ --no-kv-offload \ -t 12 \ --n-cpu-moe 0逐條解讀一下-ngl 99表示把所有層都加載到GPU如果99這個(gè)值超過(guò)實(shí)際能容納的層數(shù)啟動(dòng)時(shí)會(huì)報(bào)顯存不足然后你需要逐步降低。-c 131072設(shè)置上下文長(zhǎng)度。--cache-type-k q4_0和--cache-type-v q4_0把KV cache量化到4bit這一步省了大概6.4G顯存。--flash-attn啟用Flash Attention。--no-kv-offload把KV cache放到系統(tǒng)內(nèi)存。-t 12設(shè)置CPU線程數(shù)因?yàn)镵V cache在內(nèi)存CPU要承擔(dān)不少Attention計(jì)算。最后的--n-cpu-moe 0對(duì)稠密模型沒(méi)影響是給MoE模型用的。這套參數(shù)下模型權(quán)重占顯存約10.8GCUDA context和激活值占約1G湊合能進(jìn)12G。啟動(dòng)日志里最關(guān)鍵的幾行是model size、KV self size和CUDA buffer size這三行數(shù)字如果加起來(lái)接近12G就說(shuō)明已經(jīng)很極限了。4.3 實(shí)測(cè)速度數(shù)據(jù)我用llama-bench做了一組相對(duì)標(biāo)準(zhǔn)的測(cè)試輸入長(zhǎng)度512 token生成2048 token結(jié)果如下模型配置顯存占用首token延遲decode速度Q3_K_S 128K KV(Q4,內(nèi)存)~11.5G1.2s45-55 tok/sQ3_K_S 32K KV(Q4,顯存)~10.5G0.8s50-60 tok/sQ3_K_M 16K KV(Q8,顯存)~11.2G0.6s50-58 tok/sQ4_K_M 部分CPU offload~12G滿1.8s18-25 tok/s這里有個(gè)重要的數(shù)據(jù)點(diǎn)Q4_K_M那行理論上模型精度更高但因?yàn)椴糠謱釉贑PU上跑decode速度直接跌到20左右體感非常拖沓。這驗(yàn)證了前面的判斷12G顯存上全GPU的Q3遠(yuǎn)比半offload的Q4好用。這里監(jiān)控用量是一個(gè)很好的習(xí)慣nvidia-smi配合free -g可以實(shí)時(shí)看顯存和內(nèi)存壓力。4.4 decode速度為什么能超過(guò)50回到標(biāo)題里的“decode 50”。這個(gè)數(shù)字對(duì)于稠密27B模型來(lái)說(shuō)理論上限在哪里decode是自回歸的每個(gè)token都要讀取一遍權(quán)重。RTX 3060的顯存帶寬是360GB/s如果每生成一個(gè)token要讀一遍Q3權(quán)重約12GB上限就是360除以12大約30 tok/s。也就是說(shuō)稠密27B模型在12G顯存上物理上就不可能穩(wěn)定超過(guò)35。那50是怎么來(lái)的兩種可能第一上下文窗口較小KV cache留在顯存Attention計(jì)算沒(méi)有被打到內(nèi)存瓶頸decode速度可以沖到50-60這個(gè)我實(shí)測(cè)達(dá)到過(guò)第二模型本身是MoE結(jié)構(gòu)每次前向只激活一部分專家權(quán)重等效讀取權(quán)重大幅下降。MiniMax H3這類新模型如果走M(jìn)oE路線激活參數(shù)可能只有總量的一半甚至三分之一decode速度就完全不是一個(gè)量級(jí)了。所以在查“為什么我的decode沒(méi)有50”之前先搞清楚自己的模型架構(gòu)和KV cache位置。這決定了你能達(dá)到的上限在哪里。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 啟動(dòng)就OOM但理論上應(yīng)該能裝下我在調(diào)優(yōu)過(guò)程中遇到的第一個(gè)問(wèn)題就是啟動(dòng)報(bào)CUDA OOM。排查步驟很固定先看日志里的CUDA buffer size和model size再算KV cache size。如果三者和接近12G嘗試四件事把-c從131072降到65536把KV cache從Q4降到Q4并開(kāi)--no-kv-offload把-ngl從99降到80換成更小的量化檔位。四件事按這個(gè)順序試通常第三四步就能解決。另外注意一個(gè)容易忽略的點(diǎn)llama.cpp的-c如果設(shè)得很大即使--no-kv-offload把KV cache放到了內(nèi)存Attention計(jì)算過(guò)程中的臨時(shí)張量仍然可能被分配在顯存里尤其是開(kāi)Flash Attention之前。所以遇到OOM時(shí)優(yōu)先確認(rèn)Flash Attention是否真的開(kāi)啟了日志里會(huì)有一行flash_attn 1。5.2 decode速度上不去的三個(gè)原因速度慢先看三個(gè)指標(biāo)GPU利用率、顯存帶寬、CPU占用。用nvidia-smi dmon能實(shí)時(shí)看到GPU利用率和顯存帶寬。如果GPU利用率在90%以上但速度還是慢那就是帶寬瓶頸無(wú)解只能降低量化檔位或啟用MoE如果GPU利用率只有50%大概率是CPU在拖后腿檢查-t是否設(shè)置合理以及是否有其他程序占CPU如果顯示內(nèi)存占用高檢查KV cache是否真的在內(nèi)存里有時(shí)候--no-kv-offload沒(méi)寫(xiě)進(jìn)參數(shù)文件白開(kāi)了。我這里踩過(guò)一個(gè)很典型的坑-ngl 99但模型中有一部分被強(qiáng)制留在CPU日志看起來(lái)是全GPU其實(shí)是部分CPU。用llama-bench分別測(cè)-ngl 99和-ngl 90的輸出就能看出來(lái)差異如果差異不大說(shuō)明99本就沒(méi)有全上。5.3 MiniMax H3在RTX 3060 12G上能不能跑這個(gè)話題最近問(wèn)的人很多。我的回答是看架構(gòu)。如果MiniMax H3是MoE或混合注意力結(jié)構(gòu)那12G顯存只要能裝載稀疏激活的權(quán)重部分跑起來(lái)完全沒(méi)問(wèn)題decode速度甚至可能比Qwen2.5-27B更好。去HuggingFace看模型的config.json是最快的辦法找num_experts和num_experts_per_tok這兩個(gè)字段如果存在恭喜這是能跑快的模型。跑的時(shí)候llama.cpp要用支持MoE的版本配合--n-cpu-moe參數(shù)把不常用的專家放在CPU內(nèi)存保活躍專家在GPU。監(jiān)控顯存和速度讓活躍專家數(shù)量逐步增加直到顯存接近滿載。這個(gè)過(guò)程的調(diào)優(yōu)思路和稠密模型完全不一樣稠密是“全塞GPU就行”MoE是“塞多少專家保多少速度”。5.4 跑推理時(shí)Chrome圖片解碼失敗這個(gè)現(xiàn)象我遇到過(guò)好幾次一開(kāi)始以為是瀏覽器問(wèn)題后來(lái)發(fā)現(xiàn)是高負(fù)載推理把系統(tǒng)資源和顯存占滿導(dǎo)致的連帶反應(yīng)。Chrome的GPU進(jìn)程依賴顯存做圖片解碼當(dāng)顯存被模型占滿圖片解碼就會(huì)報(bào)image decode failed。解決方案不是修Chrome而是給推理讓路運(yùn)行推理前關(guān)掉硬件加速相關(guān)的瀏覽器標(biāo)簽頁(yè)或者把瀏覽器設(shè)置里的“使用硬件加速”臨時(shí)關(guān)掉。如果推理和瀏覽器必須同時(shí)用考慮給模型加--no-kv-offload把KV cache全部丟到內(nèi)存降低顯存壓力瀏覽器解碼就能正常工作。這個(gè)問(wèn)題的本質(zhì)是顯存分配策略沖突。推理框架的顯存占用是持續(xù)性的瀏覽器則是突發(fā)性的。把KV cache挪走是最有效的緩解手段代價(jià)是decode速度會(huì)有10%左右的下降但可以換來(lái)整個(gè)系統(tǒng)的穩(wěn)定。5.5 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象大概率原因解決路徑啟動(dòng)報(bào)CUDA OOM權(quán)重KV cache緩沖超顯存降量化檔位KV cache量化或offloaddecode只有20 tok/s部分層在CPU上確認(rèn)-ngl檢查日志是否全GPU上下文一長(zhǎng)就變慢KV cache在內(nèi)存掉帶寬換更小KV量化或降上下文長(zhǎng)度對(duì)話到一半報(bào)錯(cuò)顯存碎片重啟server換--no-mmapChrome圖片解碼失敗顯存被推理占滿推理時(shí)關(guān)硬件加速或KV offload模型回答質(zhì)量下降量化檔位過(guò)低提高Q值降低上下文長(zhǎng)度6. 一點(diǎn)個(gè)人經(jīng)驗(yàn)和補(bǔ)充建議整套方案跑下來(lái)我最大的體會(huì)是在消費(fèi)級(jí)顯卡上玩大模型本質(zhì)是資源規(guī)劃游戲不是模型評(píng)測(cè)游戲。決定成敗的不是模型的benchmark分?jǐn)?shù)而是你對(duì)顯存、內(nèi)存、帶寬三者的分配是否精確。每次調(diào)整都先算賬再動(dòng)手不要憑感覺(jué)試參數(shù)。最后分享一個(gè)實(shí)用小技巧我在跑長(zhǎng)上下文任務(wù)時(shí)會(huì)同時(shí)開(kāi)兩個(gè)llama-server實(shí)例一個(gè)用128K上下文專門(mén)跑長(zhǎng)文檔一個(gè)用16K上下文跑日常對(duì)話因?yàn)楹竺孢@個(gè)更輕量、更快、也更穩(wěn)。兩個(gè)實(shí)例分別監(jiān)聽(tīng)不同端口配合OpenAI客戶端切換使用體驗(yàn)很自然。這算是一種“用資源換體驗(yàn)”的折中方案剛好繞開(kāi)了單實(shí)例在128K下的速度代價(jià)。如果你也常用長(zhǎng)上下文可以試一下這個(gè)思路。