戰(zhàn):RTX 4090部署27B大模型完整實(shí)錄)
最近一直在折騰本地大模型部署這臺 RTX 4090 被我前前后后塞進(jìn)過好幾個(gè)模型從 7B、13B 到 27B 都有。4bit 量化下的 27B 模型能跑但 24GB 顯存被權(quán)重、KV Cache 和 CUDA 上下文瓜分得滿滿當(dāng)當(dāng)稍微把上下文拉長一點(diǎn)就 OOM體驗(yàn)其實(shí)挺憋屈。后來我拿到一個(gè)很特別的項(xiàng)目Ternary-Bonsai-2-27B它采用PTQ1_0后訓(xùn)練量化方案把 27B 參數(shù)直接壓到三元權(quán)重-1/0/1表示GGUF 單文件只有 6.1GB。這個(gè)體積一出來我就知道4090 這次終于可以真正從容地跑 27B 了。這篇文章不是我寫的評測是完整部署實(shí)錄從可行性分析、環(huán)境準(zhǔn)備、兩條部署路徑、調(diào)優(yōu)參數(shù)、性能實(shí)測到各類踩坑記錄全部按實(shí)操順序展開打算上手的朋友可以直接照著抄作業(yè)。1. 動手前先算賬27B 憑什么能塞進(jìn) 24GB1.1 三元量化比 4bit 量化省在哪里很多人看到 27B 模型第一反應(yīng)是 4090 跑不動這個(gè)直覺沒錯(cuò)但前提是沒算量化的賬。27B 參數(shù)如果用 BF16 原生存儲一個(gè)參數(shù)占 2 字節(jié)算下來 54GB24GB 顯存連一半都放不下即使壓縮到 4bit比如 GPTQ、AWQ權(quán)重也要 13.5GB 左右加上 KV Cache、CUDA context 和激活值長上下文下依然提心吊膽。而 Ternary-Bonsai-2-27B 走的是更極端的路子把每個(gè)權(quán)重映射到{-1, 0, 1}三個(gè)離散值。這樣理論上每個(gè)權(quán)重只需要 1.58 bit按 2bit 打包計(jì)算27B 參數(shù)約 6.75GB我拿到的實(shí)際 GGUF 文件甚至只有 6.1GB說明文件內(nèi)還有稀疏化帶來的額外壓縮收益。這個(gè)數(shù)字意味著權(quán)重占 6GB 多KV Cache 預(yù)留 2GBCUDA context 和激活值再占 2GB 左右整體負(fù)載不到 14GB。24GB 的 4090 不但裝得下還留下充足余量。這里的邏輯可以類比成把一整本書壓縮成大綱4bit 量化相當(dāng)于保留完整的句子結(jié)構(gòu)但刪去修飾詞三元量化則只保留章節(jié)標(biāo)題和關(guān)鍵論點(diǎn)。信息密度確實(shí)下降了但骨架還在對很多任務(wù)來說骨架級別的信息已經(jīng)夠用。1.2 PTQ1.0 和 GPTQ/AWQ 的本質(zhì)區(qū)別很多朋友對量化方案的理解停留在數(shù)字越小體積越小但 PTQ1.0 和當(dāng)前主流方案有幾處關(guān)鍵差異。GPTQ、AWQ 這類方法本質(zhì)上是在做校準(zhǔn)感知的數(shù)值映射拿一批校準(zhǔn)數(shù)據(jù)去統(tǒng)計(jì)權(quán)重和激活分布然后決定在哪個(gè)區(qū)間用多少個(gè)離散值去近似原權(quán)重通常是 16 個(gè)4bit。PTQ1.0 走的是后訓(xùn)練量化路線里的極端分支校準(zhǔn)邏輯類似但映射目標(biāo)只有 3 個(gè)離散值。它不要求反向傳播、不需要重訓(xùn)練校準(zhǔn)速度快但代價(jià)是數(shù)值表達(dá)能力的斷崖式下降——3 個(gè)值沒法表達(dá)精細(xì)的梯度信息模型輸出的概率分布會相對鈍一些。所以在部署之前我先給自己定了個(gè)預(yù)期這個(gè)模型的定位是能跑、夠快、日常夠用而不是全面替代原版 27B。實(shí)際用下來也確實(shí)如此。代碼生成、翻譯、結(jié)構(gòu)化總結(jié)這類任務(wù)它的表現(xiàn)比我預(yù)期好不少但復(fù)雜數(shù)學(xué)推理、多步邏輯鏈條任務(wù)質(zhì)量衰減非常明顯。這一點(diǎn)我會在采樣調(diào)優(yōu)部分拿實(shí)測案例展開。1.3 部署路徑選型llama.cpp、Ollama 還是 bitnet.cpp模型方案清楚了接下來是工具選型。我實(shí)際試了三條路線簡單做個(gè)對比部署路徑核心優(yōu)勢主要劣勢我的最終選擇llama.cpp llama-server原生兼容 GGUF參數(shù)可控性強(qiáng)性能穩(wěn)定需要編譯學(xué)習(xí)成本略高主力方案Ollama一條命令導(dǎo)入模型管理方便底層參數(shù)暴露有限調(diào)優(yōu)上限低備用方案bitnet.cpp 類專用推理器針對 1.58bit 模型有理論最優(yōu)速度生態(tài)窄對通用 GGUF 兼容不穩(wěn)定僅做實(shí)驗(yàn)驗(yàn)證llama.cpp 的 GGUF 生態(tài)是目前覆蓋最廣的。雖然三元量化格式比較新早期版本未必認(rèn)識但只要把框架更新到支持TQ1_0量化類型的較新版本就能直接加載。Ollama 本質(zhì)上是封裝了一層 llama.cpp上手最舒服但我想調(diào)--parallel、--batch-size、Flash Attention 這類底層參數(shù)時(shí)它就有些使不上勁。bitnet.cpp 是專門為 1.58bit 模型設(shè)計(jì)的開源推理項(xiàng)目理論性能最好但它和通用 GGUF 格式不通用我試跑過一次就當(dāng)技術(shù)驗(yàn)證沒有作為日常主力。選型結(jié)論很簡單要省心用 Ollama要性能和可調(diào)性用 llama.cpp。我最后以 llama.cpp 為主力后面所有調(diào)優(yōu)數(shù)據(jù)都是在這套環(huán)境里測出來的。2. 環(huán)境準(zhǔn)備與模型文件體檢2.1 驅(qū)動、CUDA 和 llama.cpp 編譯先交代硬件RTX 4090 24GB 128GB 內(nèi)存 Ubuntu 22.04驅(qū)動 550.120CUDA 12.4。部署前先花兩分鐘確認(rèn)環(huán)境nvidia-smi nvcc --version gcc --versionllama.cpp 我強(qiáng)烈建議源碼編譯不要直接使用部分發(fā)行版自帶的舊包因?yàn)槿炕阕邮呛髞砑尤氲呐f版本大概率不支持。編譯命令git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDAON -DCMAKE_BUILD_TYPERelease cmake --build build --config Release -j 16這里有個(gè)容易踩的坑如果你正在使用 Conda 環(huán)境cmake 可能會錯(cuò)誤地找到 Conda 自帶的 CUDA 庫導(dǎo)致編譯出來的程序沒法正確調(diào)用 GPU 上的 nvcc 算子。解決辦法是在干凈的系統(tǒng)環(huán)境編譯或者強(qiáng)制指定編譯器cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_COMPILER/usr/local/cuda/bin/nvcc -DCMAKE_BUILD_TYPERelease編譯完成后先用自帶的llama-bench跑一下確認(rèn) GPU 是活躍狀態(tài)。如果輸出結(jié)果后面帶(CPU)標(biāo)記趕緊回去檢查編譯選項(xiàng)否則后面速度測試會很慘。2.2 GGUF 文件上任前的體檢流程拿到模型文件后我沒有直接開跑而是先做了三件事校驗(yàn)文件完整性、讀取 GGUF 頭部信息、核對量化類型。sha256sum ternary-bonsai-2-27b-ptq1_0.gguf再用 llama.cpp 自帶的 Python 腳本讀取 GGUF 元數(shù)據(jù)python3 llama.cpp/gguf-py/gguf/gguf_dump.py ternary-bonsai-2-27b-ptq1_0.gguf --no-tensors輸出里能直接看到模型參數(shù)量、層數(shù)、KV 頭數(shù)組、上下文長度上限、詞表大小和量化類型。我這份文件的幾個(gè)關(guān)鍵信息參數(shù)量 27B層數(shù) 48KV 頭數(shù)組{8, 4}最大上下文 131072量化類型TQ1_0文件 6.1GB。這個(gè)體檢步驟一定別省。有一次我圖省事直接加載結(jié)果 GGUF 頭部版本和 llama.cpp 能識別的版本不一致加載到一半就崩了。提前用gguf_dump.py確認(rèn)元數(shù)據(jù)能規(guī)避掉好幾類異常。2.3 如果拿到 safetensors如何轉(zhuǎn)成 GGUF我這次拿到的直接是 GGUF但如果你從別的渠道拿到的是 Hugging Face 格式權(quán)重就需要自己轉(zhuǎn)換。llama.cpp 提供了轉(zhuǎn)換腳本python3 llama.cpp/convert_hf_to_gguf.py models/ternary-bonsai-2-27b \ --outfile models/ternary-bonsai-2-27b-ptq1_0.gguf \ --outtype tq1_0注意--outtype tq1_0這個(gè)參數(shù)在較新版本才支持。轉(zhuǎn)換前確認(rèn)一下config.json里的quantization_config字段確實(shí)標(biāo)注了 PTQ1.0 方案否則腳本可能把你原來的 BF16 權(quán)重原樣轉(zhuǎn)成普通 FP16 GGUF文件不但沒變小加載后算子不匹配還會跑不起來。2.4 一個(gè)隱蔽但很重要的檢查量化算子是否真的生效這里想提一個(gè)容易被忽略的檢查點(diǎn)。當(dāng) GGUF 加載成功后llama.cpp 日志里會出現(xiàn)每層 offload 的信息。如果你看到模型的量化類型確實(shí)是TQ1_0但顯存占用卻比預(yù)期大了接近一倍很可能是推理時(shí)矩陣計(jì)算被回退到了更高精度。這種情況下需要檢查編譯時(shí)GGML_CUDA開關(guān)以及是否有部分算子走了 CPU 回退。解決辦法是重新編譯并確認(rèn)日志中沒有出現(xiàn) fallback 之類字樣。這一步不檢查后面的性能數(shù)據(jù)全是失真。3. 部署實(shí)操先把模型跑起來再說3.1 用 llama-cli 跑通第一句話跑通是從命令行開始的。我用的第一條命令./build/bin/llama-cli -m models/ternary-bonsai-2-27b-ptq1_0.gguf \ -ngl 999 -c 8192 -fa -t 8 \ --temp 0.7 --top-p 0.9 --repeat-penalty 1.1 \ -p 寫一段關(guān)于本地大模型部署的總結(jié)每個(gè)參數(shù)都值得解釋清楚-ngl 999把所有層都放到 GPU。如果改成-ngl 40表示 48 層里只放 40 層剩下 8 層在 CPU 跑速度立刻掉一個(gè)量級。-c 8192上下文長度。27B 模型在 4090 上8K 是穩(wěn)妥起點(diǎn)。-fa啟用 Flash Attention。長上下文下顯存占用和速度改善非常明顯后面有實(shí)測對比。-t 8CPU 線程數(shù)在全量 GPU offload 時(shí)影響不大但部分算子落到 CPU 時(shí)管用。采樣參數(shù)先給一個(gè)保守組合具體怎么調(diào)我放到第 4 節(jié)專門講。第一次加載時(shí)日志會輸出逐層 offload 信息。我的觀察是加載階段顯存峰值約 13GB穩(wěn)定運(yùn)行后約 9GB。首 token 出現(xiàn)在約 1.5 秒后解碼速度約 45 tokens/s這個(gè)數(shù)據(jù)對 27B 模型在 4090 上來說是相當(dāng)舒服的水平。3.2 不想碰編譯就用 Ollama 導(dǎo)入如果不想折騰編譯Ollama 是很好的一條路。安裝好 Ollama 后準(zhǔn)備一個(gè) ModelfileFROM ./ternary-bonsai-2-27b-ptq1_0.gguf TEMPLATE {{- if .System }}|sys|{{ .System }}/|sys|{{ end }} |user|{{ .Prompt }}/|user| |assistant|{{ .Response }}/|assistant| PARAMETER num_ctx 8192 PARAMETER temperature 0.7 PARAMETER top_p 0.9 PARAMETER stop |user| PARAMETER stop |assistant|然后執(zhí)行ollama create ternary-bonsai -f Modelfile ollama run ternary-bonsaiOllama 的ollama serve自帶 API很多前端工具直接就能對接。但它暴露的底層參數(shù)不如 llama.cpp 多比如 Flash Attention 開關(guān)、--parallel并發(fā)數(shù)、--batch-size這些都沒法直接調(diào)。所以如果你只是本地日常用Ollama 夠如果你想壓榨 4090 的推理性能還是得回到 llama.cpp 甚至 vLLM 這條路。3.3 跑成常駐服務(wù)兼容 OpenAI API為了讓模型能被各類應(yīng)用調(diào)用我最終把 llama-server 跑成了常駐服務(wù)./build/bin/llama-server -m models/ternary-bonsai-2-27b-ptq1_0.gguf \ -ngl 999 -c 8192 -fa --host 127.0.0.1 --port 8080 \ --parallel 2 --batch-size 512啟動后先用 curl 驗(yàn)證curl http://127.0.0.1:8080/v1/chat/completions \ -H Content-Type: application/json \ -d {model:local,messages:[{role:user,content:你好}],max_tokens:128}返回正常后這套服務(wù)就能被任何 OpenAI 兼容客戶端接入。我把 Open WebUI 和幾個(gè) RAG 工具都接到了這個(gè)端口上體驗(yàn)基本和在線 API 一致只是速度由本地硬件決定。4. 調(diào)優(yōu)過程速度、顯存與生成質(zhì)量4.1 先用基準(zhǔn)測試把底數(shù)摸清楚調(diào)優(yōu)不能靠感覺第一步要定量。我用 llama-bench 分別測了 prompt processing輸入處理和 generation逐 token 生成兩個(gè)階段的性能./build/bin/llama-bench -m models/ternary-bonsai-2-27b-ptq1_0.gguf \ -ngl 999 -c 8192 -fa -t 8在 RTX 4090 上的實(shí)測數(shù)據(jù)場景Prompt ProcessingGeneration顯存占用c 4096986 tokens/s52 tokens/s約 8GBc 8192912 tokens/s47 tokens/s約 9GBc 16384703 tokens/s38 tokens/s約 11GB我還專門關(guān)掉 Flash Attention 做了對照c 8192 場景下 generation 跌到 41 tokens/s顯存占用反而多了約 1.5GB。所以 Flash Attention 在這個(gè)配置下屬于必須開的優(yōu)化項(xiàng)沒有之一。為什么上下文變長后速度下降這么明顯一方面是 KV Cache 占用的顯存變多另一方面是解碼階段需要頻繁讀取 KV Cache三元量化讓權(quán)重本身的讀取帶寬降得很低KV Cache 的讀取占比就相對上升了所以上下文長度對最終速度的影響會比普通 4bit 模型更明顯。4.2 采樣參數(shù)調(diào)優(yōu)實(shí)錄速度只是調(diào)優(yōu)的一半生成質(zhì)量才是花時(shí)間大頭。三元量化模型的輸出分布相對平緩采樣參數(shù)對結(jié)果的敏感度比普通模型更高。我做了幾組對比默認(rèn)的temp0.8, top_p0.95輸出流暢但偶爾出現(xiàn)車轱轆話同一句式的重復(fù)概率明顯偏高。temp0.5, top_p0.85邏輯更穩(wěn)但文風(fēng)偏干創(chuàng)意類任務(wù)表現(xiàn)一般。temp0.65, top_p0.9, repeat_penalty1.15最終保留的組合日常對話和代碼生成兼顧。這里有一個(gè)重要的實(shí)操經(jīng)驗(yàn)如果你發(fā)現(xiàn)模型總在重復(fù)同一句話不要急著猛提repeat_penalty。三元量化模型的概率分布本身比較平低 temperature 下容易收斂到重復(fù)循環(huán)這時(shí)你把懲罰系數(shù)調(diào)到 1.3 以上反而會讓輸出變得更加奇怪。我的處理順序是先把repeat_penalty降到 1.05再把temperature提到 0.7 以上讓采樣分布帶一點(diǎn)隨機(jī)性然后再觀察。多數(shù)情況下這個(gè)組合比單純懲罰重復(fù)有效得多。代碼生成場景我推薦單獨(dú)一組參數(shù)temp0.2, top_p0.9, min_p0.05輸出更穩(wěn)定。多步推理類任務(wù)建議temp0.6, top_p0.8適當(dāng)降低 top_p 可以防止分布過散導(dǎo)致邏輯漂移。4.3 顯存占用與并發(fā)平衡一臺 4090 只跑單會話多少有點(diǎn)浪費(fèi)所以我把注意力放到并發(fā)上。先手工估算 KV Cache 體積KV Cache 大小 ≈ 層數(shù) × KV 頭數(shù) × 頭維度 × 上下文長度 × 緩存元素字節(jié)數(shù) × 2以這個(gè)模型為例48 層、8 組 KV 頭、128 頭維度、8192 上下文4bit 緩存下約 2GB。加上權(quán)重 6.1GB、CUDA context 和激活值單實(shí)例約 9GB。于是我把--parallel設(shè)為 2實(shí)測兩個(gè)并發(fā)會話穩(wěn)定運(yùn)行每個(gè)會話仍然是 8192 上下文總顯存約 14GB。繼續(xù)開到 3 個(gè)并發(fā)偶發(fā) OOM把上下文降到 6144 后能跑但生成速度互相擠占實(shí)際總吞吐提升非常有限。所以在 4090 上這個(gè)模型的合理并發(fā)配置是 2再多意義不大。順帶說一句llama-server 的/metrics端點(diǎn)會暴露 KV Cache 使用率、請求數(shù)和推理時(shí)間等指標(biāo)用 Prometheus 接上做監(jiān)控很方便。我后來就是靠這個(gè)監(jiān)控發(fā)現(xiàn)高峰期顯存不足才把并發(fā)從 3 降回 2。4.4 長上下文實(shí)戰(zhàn)取舍對三元量化模型來說長上下文是個(gè)很微妙的話題。理論上 24GB 顯存能開到 32K 上下文但實(shí)測解碼速度會掉到 20 tokens/s 左右體驗(yàn)非常差。試了幾輪后我的配置策略是日常保持 8K需要做長文檔總結(jié)時(shí)臨時(shí)重啟為 16K不常駐。如果你確實(shí)需要 32K 以上可以考慮-ngl 35部分 offload把 13 層放到 CPU 上顯存能騰出約 2.5GB上下文推高到 32K但 generation 會掉到 15 tokens/s 上下。這個(gè)值只適合后臺批處理場景不適合實(shí)時(shí)對話。我的實(shí)際建議還是先問自己這個(gè)任務(wù)真的需要 32K 上下文嗎大多數(shù)場景 8K 到 16K 完全夠用沒必要為了一個(gè)邊緣需求犧牲日常體驗(yàn)。5. 踩坑記錄與排查速查5.1 GGUF 版本不匹配導(dǎo)致加載失敗第一次加載時(shí)我遇到了GGUF metadata版本不識別的問題?,F(xiàn)象是 llama.cpp 日志報(bào)unknown GGUF quantization type然后直接退出。原因很簡單我用的 llama.cpp 版本太舊還不認(rèn)識TQ1_0這個(gè)新增量化類型。解法git pull origin master cmake --build build --config Release -j 16升級之后就能正常識別。所以遇到加載失敗先檢查 llama.cpp 版本一星半點(diǎn)都別看直接拉最新別急著懷疑模型文件損壞。5.2 生成速度只有幾 token/s問題出在哪有幾天我把模型接到某個(gè)遠(yuǎn)程前端用出字速度慢到令人崩潰。排查順序很關(guān)鍵nvidia-smi看 GPU 利用率。如果利用率接近 0%大概率權(quán)重沒有完全 offload日志里的offloaded N/48 layers是一眼定乾坤的指標(biāo)。檢查-t線程數(shù)。如果部分算子落到 CPU-t 8和-t 16的差距非常明顯。確認(rèn)-fa是否開啟長上下文下收益巨大。檢查是否發(fā)生內(nèi)存交換。如果 GGUF 權(quán)重因?yàn)橄到y(tǒng)內(nèi)存不足被 swap 到磁盤模型會邊讀盤邊推理這時(shí)需要降低上下文長度和并發(fā)數(shù)。5.3 輸出內(nèi)容大量重復(fù)怎么辦前面寫過不推薦一上來就高懲罰。處理順序是先降repeat_penalty到 1.05temperature提到 0.7跑十組對話觀察如果還有重復(fù)再逐步提高repeat_penalty每次只加 0.05。實(shí)測這個(gè)方式比直接拉滿懲罰得到的輸出自然得多。5.4 明明有顯存卻報(bào) CUDA OOM有兩次我明明看到nvidia-smi顯示空閑 10GBllama-server 依然報(bào) CUDA OOM。原因是 CUDA context 和 KV Cache 會在啟動階段預(yù)分配顯存nvidia-smi看到的空閑并不等于進(jìn)程可申請的顯存。排查方法是看 llama-server 日志里的 buffer 分配信息或者在啟動時(shí)降低-c和--parallel把預(yù)分配空間降下來。如果你需要精準(zhǔn)控制還可以用--no-mmap配合顯存優(yōu)化參數(shù)但代價(jià)是加載時(shí)間變長多數(shù)情況下沒必要。這一整套流程走下來我對三元量化模型的定位有了更實(shí)際的判斷它不適合作為唯一的生產(chǎn)模型去承擔(dān)高精度推理任務(wù)但做日常對話、代碼補(bǔ)全、長文檔初稿生成它用不到原生模型一半的顯存跑出了可以接受的可用性和遠(yuǎn)高于同顯存場景的速度這個(gè)性價(jià)比在單卡環(huán)境里非常難得。最后分享一個(gè)我的習(xí)慣本地保留一份原版 BF16 權(quán)重和一份 PTQ1.0 權(quán)重遇到對質(zhì)量沒有把握的任務(wù)先用三元量化跑一遍拿整體思路再決定要不要切換到原版復(fù)核。這種低比特出草稿、高比特來復(fù)核的組合實(shí)際用下來比單一模型硬扛效率高得多。