
前幾天一個(gè)朋友拎著一張 24GB 顯存的顯卡來找我開門見山想跑 70B 模型。我第一反應(yīng)是先問他“你準(zhǔn)備用哪個(gè)量化方案”因?yàn)樵趩慰ōh(huán)境下70B 模型的 FP16 原始權(quán)重就要超過 140GB不量化根本連起點(diǎn)都摸不到。而量化之后又會牽扯出一連串推理優(yōu)化問題上下文窗口能開多大、批量能塞多少、KV cache 會不會爆、用 llama.cpp 還是 vLLM。這幾層問題疊在一起其實(shí)不是“跑一個(gè)模型”而是一整套五層技術(shù)棧的選擇題。這篇內(nèi)容就是針對“讓 70B 跑在單卡”這件事的完整拆解從顯存預(yù)算、量化方案、模型格式、推理引擎一直到 KV cache 和運(yùn)行參數(shù)每一層我都把原理和實(shí)操放到一起講。適合想在單張顯卡上部署大模型的工程師、做私有化推理的同學(xué)也適合手里只有一張 4090 或 48GB 顯卡卻想折騰大參數(shù)模型的玩家。看完你至少能回答三個(gè)問題為什么非要量化、量化到多少合適、跑起來之后哪些參數(shù)最容易讓顯存爆炸。1. 先算一筆顯存賬70B 為什么天然排斥單卡1.1 光權(quán)重就已經(jīng) 140GB其它開銷還沒算模型占用顯存的第一大頭永遠(yuǎn)是權(quán)重本身。70B 參數(shù)指的是參數(shù)量不是字節(jié)數(shù)。顯存占用取決于每個(gè)參數(shù)用多少位來存FP32 是 4 字節(jié)所以 70B 參數(shù)就要 70B × 4B 280GBFP16/BF16 是 2 字節(jié)要 140GBINT8 是 1 字節(jié)要 70GBINT4 是 0.5 字節(jié)理論上只要 35GB。單卡環(huán)境里常見的顯存是 24GB4090、3090 級別和 48GBA6000、L40S 級別前者連 INT8 權(quán)重都裝不下后者也只能勉強(qiáng)容納 INT4 級別的權(quán)重。所以“讓 70B 跑在單卡”這句話的潛臺詞其實(shí)就是“把權(quán)重壓到 INT4 或等效精度再把其它開銷壓縮到極致”。但這只是權(quán)重賬。真正跑推理的時(shí)候還有三塊開銷KV cache注意力緩存的中間結(jié)果、激活值前向計(jì)算圖的中間張量、以及 CUDA context 和框架本身的基本占用。后兩項(xiàng)加起來隨架構(gòu)不同能吃掉 2GB 到 8GB 不等有時(shí)候你明明算好權(quán)重只要 40GB實(shí)際一加載就報(bào) OOM就是因?yàn)橥私o這些“隱形住戶”留位置。1.2 單卡不是沒顯存是沒有“臨時(shí)空間”把 70B 壓縮進(jìn)單卡很多人第一反應(yīng)是“量化到 INT4 就完事了”。實(shí)際上權(quán)重塞進(jìn)去之后真正限制你的是臨時(shí)空間。推理和訓(xùn)練不一樣訓(xùn)練是一次性把整個(gè)批次灌進(jìn)顯存推理是逐 token 生成。每生成一個(gè) token注意力層的 K 和 V 緩存都會累計(jì)增加。也就是說輸入越長、對話輪次越多、批量越大KV cache 就越大。權(quán)重是固定成本KV cache 是隨時(shí)間增長的可變成本。單卡環(huán)境里這兩者在爭奪同一塊顯存。所以“能裝下”和“跑得動”是兩回事。你用 INT4 把權(quán)重壓到 38GB 左右看著 48GB 顯卡剩余 10GB似乎挺寬??梢坏┥舷挛脑O(shè)到 16K、批量推到 4KV cache 直接吃掉 5GB 以上再疊加激活值和 CUDA contextOOM 就像定時(shí)炸彈。這也是后面每一節(jié)都要圍繞“顯存總預(yù)算”來設(shè)計(jì)的原因。1.3 五層技術(shù)棧的整體劃分我把單卡跑 70B 這件事拆成五層每層解決一類獨(dú)立問題層級解決的核心問題常用手段對應(yīng)章節(jié)第 1 層權(quán)重精度讓權(quán)重體積從 140GB 降到可接受范圍FP16、BF16、INT8、INT4、FP8第 2 節(jié)第 2 層量化方法在壓體積的同時(shí)盡量保住質(zhì)量GPTQ、AWQ、GGUF Q4、校準(zhǔn)集第 2 節(jié)第 3 層模型格式與轉(zhuǎn)換讓模型能被指定推理框架讀取HuggingFace 權(quán)重、GGUF、ONNX第 3 節(jié)第 4 層推理引擎與內(nèi)存調(diào)度分配顯存、管理 KV cache、控制批處理llama.cpp、vLLM、PagedAttention第 4 節(jié)第 5 層運(yùn)行參數(shù)調(diào)優(yōu)在顯存預(yù)算內(nèi)平衡質(zhì)量、速度和并發(fā)context length、batch、gpu offload第 5 節(jié)這五層是層層依賴的關(guān)系權(quán)重精度決定你剩下多少顯存給 KV cache量化方法決定壓縮后的模型質(zhì)量格式必須匹配框架框架的調(diào)度方式?jīng)Q定并發(fā)能力而最后一層參數(shù)設(shè)置則是前面所有選擇的最終校驗(yàn)。很多人失敗的原因就是把精力全放在第一層權(quán)重一量化就著急跑結(jié)果在第四、第五層瘋狂撞墻。2. 量化選型FP8、INT8、INT4 與三種主流方法2.1 位寬不是越低越好得看精度損失曲線先列一張我常用的位寬對照表精度權(quán)重占用70B相對 FP16 的壓縮率主要問題FP16/BF16140GB1x單卡放不下FP870GB2x受硬件和框架支持限制消費(fèi)卡收益一般INT870GB2x可以跑但精度損失比 FP8 略明顯需校準(zhǔn)INT4約 35-40GB約 3.5x壓縮率高但敏感層容易崩質(zhì)量FP8 是最容易被誤會的一檔。它在 H100 這類原生支持 FP8 的卡上有收益但在消費(fèi)級顯卡上經(jīng)常是“名義上支持、實(shí)際跑得慢”甚至框架里根本沒有優(yōu)化要讓 70B 跑到單卡FP8 的 70GB 權(quán)重體積已經(jīng)超出了 48GB 的承受范圍所以真正的主力是 INT8 和 INT4。INT8 適合 48GB 顯卡跑一些對質(zhì)量敏感但又想保住體面的場景權(quán)重 70GB 對 48GB 還是放不下因此單卡 70B 幾乎唯一現(xiàn)實(shí)的路線就是 INT4 系列。需要注意INT4 不是把每個(gè)參數(shù)機(jī)械地砍掉一半而是用少量高位寬參數(shù)去補(bǔ)償?shù)捅忍亓炕瘞淼恼`差。這個(gè)“補(bǔ)償”體現(xiàn)在 group size、scale 和 zero point 上直接決定了實(shí)際文件會比剛才的“35GB理論值”多出幾 GB。2.2 GPTQ、AWQ 和 GGUF 的 Q4三套量化方法該怎么選常見的 INT4 量化方法有三種我簡單說說它們的設(shè)計(jì)思路和適用場景。GPTQ 是一套基于 Hessian 矩陣的逐層量化方法。它會在量化每一層時(shí)用一小批校準(zhǔn)數(shù)據(jù)來估計(jì)權(quán)重的重要程度把量化誤差分配到不那么重要的參數(shù)上。優(yōu)點(diǎn)是量化速度快、社區(qū)生態(tài)成熟vLLM 和 HuggingFace 的集成度都高缺點(diǎn)是如果你后續(xù)跑的任務(wù)類型和校準(zhǔn)數(shù)據(jù)差異很大質(zhì)量會略受影響。AWQ 的思路不一樣它不看權(quán)重而看激活值。AWQ 認(rèn)為某些通道的激活值波動大這些通道對應(yīng)的權(quán)重應(yīng)該保留更高的精度。它不需要反向傳播用激活值統(tǒng)計(jì)信息就能完成量化。我實(shí)測下來AWQ 在長文本任務(wù)里往往比同精度的 GPTQ 更穩(wěn)特別是代碼生成和數(shù)學(xué)題這種對細(xì)節(jié)敏感的場景。GGUF 里的 Q4 又不太一樣。它是 llama.cpp 體系下的量化格式Q4_K_M 這類名字里的“K”代表 k-quants 混合量化。簡單說它會根據(jù)每個(gè)張量內(nèi)部的結(jié)構(gòu)分配不同位寬注意力層的部分張量用 6bitFFN 的某些部分用 4bit這樣整體文件還是約 4bit 的量級但關(guān)鍵部分精度更高。對比起來方法配套框架量化依據(jù)適合場景GPTQvLLM、Transformers權(quán)重重要性 校準(zhǔn)集服務(wù)化部署、離線批量推理AWQvLLM、部分服務(wù)框架激活值統(tǒng)計(jì)長文本、代碼、高精度要求GGUF Q4llama.cpp混合比特分配本地部署、邊緣端、單機(jī)調(diào)試一句話總結(jié)你想用 vLLM 做并發(fā)服務(wù)優(yōu)先 GPTQ 或 AWQ你想在單機(jī)上用最簡單可靠的方式跑通GGUF 加 llama.cpp 最省心。2.3 量化參數(shù)怎么設(shè)group size、校準(zhǔn)數(shù)據(jù)和 KV cache 量化選了方法之后還有一組參數(shù)要定其中最容易踩坑的是 group size。group size 是指多少個(gè)權(quán)重參數(shù)共享一個(gè)量化縮放因子。常見值是 128想做更精細(xì)的量化可以降到 32。數(shù)值越小量化顆粒度越細(xì)精度越好但存儲開銷越大速度也可能變慢因?yàn)橐x更多 scale 信息。對 70B 模型來說group size 從 128 降到 32文件體積可能從 38GB 跳到 41GB 以上這多出來的 3GB 很可能直接擠爆你的顯存預(yù)算。校準(zhǔn)數(shù)據(jù)也值得認(rèn)真對待。它通常是一批文本用來統(tǒng)計(jì)權(quán)重或激活的數(shù)值分布。這里有一個(gè)很反直覺的點(diǎn)校準(zhǔn)數(shù)據(jù)不需要很多但一定要貼近你的真實(shí)任務(wù)。比如你要做的是代碼補(bǔ)全卻用長篇新聞文本做校準(zhǔn)那么生成代碼時(shí)精度就可能不如預(yù)期。我習(xí)慣從目標(biāo)任務(wù)的真實(shí)數(shù)據(jù)里隨機(jī)抽一二百條樣本截成 512 或 1024 token 的片段用作校準(zhǔn)集不貪多。還有不少人主動把 KV cache 也量化掉。llama.cpp 里有專門的 KV cache 量化選項(xiàng)vLLM 也支持 kv cache 的 FP8/INT8 存儲。這件事有利有弊它能立刻省出接近一半的緩存顯存但代價(jià)是長上下文下的質(zhì)量下降。我的建議是只有在上下文長度或并發(fā)量確實(shí)需要那部分顯存時(shí)才開 KV cache 量化并且先在目標(biāo)長度下做一輪質(zhì)量對比不要默認(rèn)開啟。3. 實(shí)操鏈路從原始權(quán)重到 GGUF再到 llama.cpp / vLLM3.1 一條能跑的鏈路先用 GGUF 打通再考慮框架實(shí)戰(zhàn)里最容易讓人心態(tài)崩的時(shí)刻不是模型太大而是“所有安裝都正確但就是跑不出結(jié)果”。所以我推薦的第一條鏈路是 GGUF llama.cpp原因是它有最完整的單機(jī)工具鏈從權(quán)重轉(zhuǎn)換到量化再到運(yùn)行每一步都有獨(dú)立命令出了問題也好定位。第一步先把模型權(quán)重格式統(tǒng)一。如果你手頭是 HuggingFace 格式的模型可以用 llama.cpp 倉庫里的轉(zhuǎn)換腳本導(dǎo)出 GGUFpython convert_hf_to_gguf.py 你的模型目錄 \ --outfile 模型名-f16.gguf \ --outtype f16轉(zhuǎn)換時(shí)注意不要跳過參數(shù)核查特別是 hidden_size、head 數(shù)這些腳本本身能大部分自動識別但你至少要確認(rèn)輸出日志里的維度和你模型架構(gòu)對得上不然量化出來的文件跑起來會全是亂碼。第二步量化。llama.cpp 自帶的 quantize 工具會把 F16 的 GGUF 壓成 Q4_K_M 或其它比特檔位./llama-quantize 模型名-f16.gguf 模型名-q4_k_m.gguf q4_k_m這里 q4_k_m 后面還能跟 group size 參數(shù)比如 q4_k_m 的變體但先用默認(rèn)即可。第三步驗(yàn)證。量化完先別急著上服務(wù)跑一次文本生成觀察輸出質(zhì)量./llama-cli -m 模型名-q4_k_m.gguf \ -c 4096 \ -ngl 999 \ -p 寫一段關(guān)于單卡推理的代碼建議如果輸出通順、沒有大量重復(fù)字符或亂碼說明鏈路是通的如果輸出明顯劣化先回查校準(zhǔn)集或 group size而不是懷疑顯卡。我見過很多新手一看到輸出差就認(rèn)為是量化方法的問題其實(shí)是命令行里少了關(guān)鍵的重復(fù)懲罰參數(shù)。3.2 llama.cpp 運(yùn)行參數(shù)ngl、ctx、batch、mlock 的作用跑通之后真正決定體驗(yàn)的是幾個(gè)運(yùn)行參數(shù)。-ngl控制有多少層放到 GPU。設(shè)為 999 表示“盡量全放 GPU”它會自動把能放下的層都放進(jìn)去如果顯存不夠剩余層會回退到 CPU。24GB 顯卡跑 40GB 的 Q4 權(quán)重時(shí)-ngl的值決定了你會看到多少 offload 警告。-c是上下文長度也就是 KV cache 的基礎(chǔ)大小。默認(rèn)可能比較小但當(dāng)你把 70B 模型真正用于長對話時(shí)這里必須按第 4 節(jié)的計(jì)算公式來設(shè)。-b是 batch size也就是單次前向傳播處理多少個(gè) token。這個(gè)參數(shù)調(diào)大可以提升吞吐但會顯著增加激活值顯存。單卡上我一般從 256 開始試OOM 就往 128 降。--mlock是鎖內(nèi)存防止進(jìn)程的內(nèi)存被換出到 swap。這個(gè)參數(shù)在不 offload 的情況下影響不大但只要開了 CPU offload它能讓速度穩(wěn)定很多。命令行參數(shù)在不同版本可能改名比如有些版本用llama-cli有些老版本還用main。如果你查閱的資料和你安裝的版本輸出對不上先用--help看當(dāng)前版本實(shí)際支持的參數(shù)名不要照抄過時(shí)教程。3.3 vLLM 跑量化模型離線批處理入口如果你要做的是并發(fā)推理服務(wù)或想在同一張卡上服務(wù)多個(gè)請求llama.cpp 的串行方式就不太夠用了。vLLM 是更好選擇它對量化模型有原生支持入口代碼很簡潔from vllm import LLM, SamplingParams llm LLM( model/path/to/awq-or-gptq-weights, quantizationawq, # 可選 awq 或 gptq dtypefloat16, gpu_memory_utilization0.85, max_model_len4096, enforce_eagerTrue, ) params SamplingParams( max_tokens512, temperature0.6, top_p0.9, ) outputs llm.generate( [寫一篇關(guān)于單卡推理優(yōu)化的短文], params, ) print(outputs[0].outputs[0].text)這里gpu_memory_utilization0.85的含義是為模型和 KV cache 預(yù)留最多 85% 顯存剩下的留給 CUDA context 和激活值。max_model_len是一把雙刃劍設(shè)得越大能處理的長文本越多但 KV cache 預(yù)留就越大。很多人在 vLLM 上遇到 OOM首先不是量化問題而是max_model_len默認(rèn)或誤設(shè)得太大。第一次跑 vLLM 時(shí)建議用enforce_eagerTrue它會禁用 CUDA graph 優(yōu)化雖然吞吐略低但好處是顯存占用更可預(yù)測、更容易定位問題。等確認(rèn)穩(wěn)定后再關(guān)掉它把速度榨出來。4. 顯存管理的隱形大頭KV cache、上下文窗口與批處理4.1 一條 KV cache 公式讓你不再靠直覺設(shè)置參數(shù)我見過太多人把“上下文長度”當(dāng)成靈感參數(shù)隨便填一個(gè)然后看著 OOM 一臉無辜。KV cache 的計(jì)算有明確公式先記住這個(gè)KV cache 大小 2 × 層數(shù) × KV頭數(shù) × 頭維度 × 上下文長度 × 批量大小 × 每值字節(jié)數(shù)乘以 2 是因?yàn)?Key 和 Value 各一份。以一個(gè) 70B 級別的模型為例通常有 80 層、8 個(gè) KV 頭、每個(gè)頭 128 維如果上下文長度 4096、批量 1、FP16 存儲那么2 × 80 × 8 × 128 × 4096 × 1 × 2 字節(jié) ≈ 1.34GB單個(gè)請求就吃掉 1.34GB看起來還好。但把這四個(gè)變量放大就不一樣了上下文長度翻到 8192KV cache 變 2.68GB批量從 1 變成 8又是 8 倍增長。在 48GB 卡上Q4 權(quán)重約 40GB剩下 8GB如果 KV cache 已經(jīng)吃了 5GB激活值再占 2GB剩下的空間就非常緊張。這就是為什么“70B 量化后能裝下但一開長上下文就崩”。4.2 上下文與批處理性能提升的另一面是內(nèi)存翻倍很多人調(diào)參數(shù)時(shí)總想讓吞吐更高于是把批處理大小調(diào)大但忽略了批量對內(nèi)存的影響。在 Transformers 這類非連續(xù)批處理實(shí)現(xiàn)里批量從 1 升到 4KV cache 直接乘以 4如果你做的是動態(tài)批處理峰值內(nèi)存會陡然上升然后掉下來再上升很容易在某個(gè)瞬間觸頂。vLLM 引入的 Continuous Batching連續(xù)批處理比傳統(tǒng)方案聰明得多每個(gè)序列只要沒結(jié)束就繼續(xù)占用 KV cache結(jié)束時(shí)立刻釋放不需要等整批結(jié)束。這讓吞吐明顯提升但總 KV cache 依然是所有并發(fā)請求之和。單卡部署 70B 時(shí)我一般會把并發(fā)上限壓到很低很多時(shí)候“跑得穩(wěn)”比“跑得多”更值得優(yōu)先考慮畢竟單個(gè)用戶一次對話就可能在長上下文下占用數(shù) GB 緩存。4.3 PagedAttention、offload 與顯存碎片vLLM 的高吞吐還得益于 PagedAttention。它的思路和操作系統(tǒng)虛擬內(nèi)存很像把 KV cache 劃分成固定大小的塊按需分配而不是給每個(gè)請求預(yù)留一整塊連續(xù)空間。這讓顯存利用率高了不少也減少了碎片問題。對于單卡 70B 場景這一點(diǎn)特別有用因?yàn)楸緛砜捎蔑@存就緊巴巴的碎片再多一點(diǎn)就徹底跑不動了。如果 24GB 卡連 INT4 權(quán)重都放不滿就必須面對 offload 策略。offload 是把部分層放到 CPU 內(nèi)存GPU 只放核心層。llama.cpp 的-ngl設(shè)成某個(gè)層數(shù)就是在做這件事。CPU 和 GPU 之間反復(fù)搬數(shù)據(jù)的代價(jià)是速度大幅下降一個(gè) 70B 模型可能出現(xiàn)幾分鐘才生成一個(gè) token 的情況。所以如果你的目標(biāo)是要可用盡量選 48GB 以上顯存或者選更小尺寸的模型硬要在 24GB 上跑 70B更多是“證明能跑”而不是“實(shí)際能用”。有人會問張量并行能不能解決單卡放不下的問題可以但標(biāo)題限制是“單卡”張量并行需要多卡通信不在討論范圍。真正單卡場景能做的是把權(quán)重量化、把 KV cache 精打細(xì)算、把不需要的層 offload這三板斧用完還是不行就該考慮換硬件或換模型而不是繼續(xù)迷信“優(yōu)化到極致就能塞下”。5. 單卡推理的實(shí)戰(zhàn)排坑從 OOM 到質(zhì)量劣化的一次性檢查5.1 開跑前的自查清單每次在單卡上部署大模型前我都會花兩分鐘過一遍清單這比反復(fù)試錯(cuò)高效得多權(quán)重顯存是否在預(yù)算內(nèi)INT4 的 70B 通常 38GB 到 42GB不要只看理論值要按實(shí)際文件大小算。KV cache 是否按公式算過根據(jù)目標(biāo)上下文長度、批量大小和存儲精度算出精確占用。CUDA context 和激活值是否有預(yù)留至少留 2GBvLLM 場景要留 10% 到 15%。校準(zhǔn)集是否貼近真實(shí)任務(wù)如果不是量化模型的輸出質(zhì)量可能無法接受??蚣馨姹臼欠衿ヅ鋖lama.cpp、vLLM 的更新速度極快量化格式和參數(shù)都有變動老版本可能讀不出新格式。5.2 高頻故障速查表癥狀最常見原因解決方向啟動即 OOM權(quán)重 KV cache 超過顯存減小上下文長度或批量開 KV cache 量化運(yùn)行一段后 OOMKV cache 隨序列增長到峰值降低 max_model_len減少并發(fā)請求數(shù)生成速度極慢大量層被 offload 到 CPU提高 -ngl / 減少 offload或換更大顯存卡輸出明顯變差量化過度或校準(zhǔn)集不匹配換 group size32換 AWQ或重做校準(zhǔn)長文本后輸出亂碼位置編碼緩存或 KV cache 溢出檢查上下文長度設(shè)置檢查重復(fù)懲罰參數(shù)多請求時(shí)吞吐驟降批處理過大、碎片過多開 PagedAttentionvLLM限制最大并發(fā)這里說一個(gè)我踩過很多次的坑有時(shí)不是模型不行而是把真實(shí)任務(wù)里的文本喂進(jìn)去后上下文長度觸到了顯存天花板于是系統(tǒng)把舊 token 截?cái)嗟⒁饬C(jī)制并不清楚該截?cái)嗄男┪恢媒Y(jié)果就是輸出質(zhì)量和邏輯突然崩壞。排查時(shí)先用一個(gè)短 prompt 驗(yàn)證模型本身再慢慢加長輸入就能區(qū)分是量化質(zhì)量問題還是上下文截?cái)鄦栴}。5.3 穩(wěn)定運(yùn)行的實(shí)測配置參考最后給兩組我實(shí)際用過的配置作為大家調(diào)試的起點(diǎn)。48GB 單卡例如 A6000 / L40S跑 70B Q4_K_M 權(quán)重llama.cpp 里我會用-ngl 999 -c 8192 -b 512 --mlock這個(gè)配置能保證權(quán)重全量進(jìn) GPU8K 上下文對大多數(shù)問答和文檔摘要都夠用如果后續(xù)要上 vLLM我會把gpu_memory_utilization設(shè)為 0.9max_model_len設(shè)為 8192并設(shè)置單請求并發(fā)數(shù)不超過 4。24GB 單卡例如 4090只適合“體驗(yàn)型”部署llama.cpp 中用-ngl 80 -c 4096 -b 256 --mlock這里的-ngl 80不是固定值而是根據(jù) 38GB 權(quán)重和 24GB 顯存算出來的一個(gè)“能塞多少塞多少”的量實(shí)際層數(shù)以日志為準(zhǔn)。注意這種配置下生成速度會明顯低于 GPU 全量加載所以它更適合驗(yàn)證推理流程不適合對外提供穩(wěn)定服務(wù)。我自己折騰單卡 70B 的過程里最大的感受是優(yōu)化順序比優(yōu)化技巧更重要。先把顯存預(yù)算按公式算清楚再決定量化精度、上下文長度和批量大小最后才去調(diào)框架參數(shù)反過來先堆量化再亂調(diào)上下文問題會像海浪一樣一波接一波?,F(xiàn)在每次跑大模型我第一件事永遠(yuǎn)是打開一個(gè)計(jì)算表格把參數(shù)填進(jìn)去而不是先敲運(yùn)行命令這會省掉一大半排錯(cuò)時(shí)間。