
最近一直在折腾本地大模型部署这台 RTX 4090 被我前前后后塞进过好几个模型从 7B、13B 到 27B 都有。4bit 量化下的 27B 模型能跑但 24GB 显存被权重、KV Cache 和 CUDA 上下文瓜分得满满当当稍微把上下文拉长一点就 OOM体验其实挺憋屈。后来我拿到一个很特别的项目Ternary-Bonsai-2-27B它采用PTQ1_0后训练量化方案把 27B 参数直接压到三元权重-1/0/1表示GGUF 单文件只有 6.1GB。这个体积一出来我就知道4090 这次终于可以真正从容地跑 27B 了。这篇文章不是我写的评测是完整部署实录从可行性分析、环境准备、两条部署路径、调优参数、性能实测到各类踩坑记录全部按实操顺序展开打算上手的朋友可以直接照着抄作业。1. 动手前先算账27B 凭什么能塞进 24GB1.1 三元量化比 4bit 量化省在哪里很多人看到 27B 模型第一反应是 4090 跑不动这个直觉没错但前提是没算量化的账。27B 参数如果用 BF16 原生存储一个参数占 2 字节算下来 54GB24GB 显存连一半都放不下即使压缩到 4bit比如 GPTQ、AWQ权重也要 13.5GB 左右加上 KV Cache、CUDA context 和激活值长上下文下依然提心吊胆。而 Ternary-Bonsai-2-27B 走的是更极端的路子把每个权重映射到{-1, 0, 1}三个离散值。这样理论上每个权重只需要 1.58 bit按 2bit 打包计算27B 参数约 6.75GB我拿到的实际 GGUF 文件甚至只有 6.1GB说明文件内还有稀疏化带来的额外压缩收益。这个数字意味着权重占 6GB 多KV Cache 预留 2GBCUDA context 和激活值再占 2GB 左右整体负载不到 14GB。24GB 的 4090 不但装得下还留下充足余量。这里的逻辑可以类比成把一整本书压缩成大纲4bit 量化相当于保留完整的句子结构但删去修饰词三元量化则只保留章节标题和关键论点。信息密度确实下降了但骨架还在对很多任务来说骨架级别的信息已经够用。1.2 PTQ1.0 和 GPTQ/AWQ 的本质区别很多朋友对量化方案的理解停留在数字越小体积越小但 PTQ1.0 和当前主流方案有几处关键差异。GPTQ、AWQ 这类方法本质上是在做校准感知的数值映射拿一批校准数据去统计权重和激活分布然后决定在哪个区间用多少个离散值去近似原权重通常是 16 个4bit。PTQ1.0 走的是后训练量化路线里的极端分支校准逻辑类似但映射目标只有 3 个离散值。它不要求反向传播、不需要重训练校准速度快但代价是数值表达能力的断崖式下降——3 个值没法表达精细的梯度信息模型输出的概率分布会相对钝一些。所以在部署之前我先给自己定了个预期这个模型的定位是能跑、够快、日常够用而不是全面替代原版 27B。实际用下来也确实如此。代码生成、翻译、结构化总结这类任务它的表现比我预期好不少但复杂数学推理、多步逻辑链条任务质量衰减非常明显。这一点我会在采样调优部分拿实测案例展开。1.3 部署路径选型llama.cpp、Ollama 还是 bitnet.cpp模型方案清楚了接下来是工具选型。我实际试了三条路线简单做个对比部署路径核心优势主要劣势我的最终选择llama.cpp llama-server原生兼容 GGUF参数可控性强性能稳定需要编译学习成本略高主力方案Ollama一条命令导入模型管理方便底层参数暴露有限调优上限低备用方案bitnet.cpp 类专用推理器针对 1.58bit 模型有理论最优速度生态窄对通用 GGUF 兼容不稳定仅做实验验证llama.cpp 的 GGUF 生态是目前覆盖最广的。虽然三元量化格式比较新早期版本未必认识但只要把框架更新到支持TQ1_0量化类型的较新版本就能直接加载。Ollama 本质上是封装了一层 llama.cpp上手最舒服但我想调--parallel、--batch-size、Flash Attention 这类底层参数时它就有些使不上劲。bitnet.cpp 是专门为 1.58bit 模型设计的开源推理项目理论性能最好但它和通用 GGUF 格式不通用我试跑过一次就当技术验证没有作为日常主力。选型结论很简单要省心用 Ollama要性能和可调性用 llama.cpp。我最后以 llama.cpp 为主力后面所有调优数据都是在这套环境里测出来的。2. 环境准备与模型文件体检2.1 驱动、CUDA 和 llama.cpp 编译先交代硬件RTX 4090 24GB 128GB 内存 Ubuntu 22.04驱动 550.120CUDA 12.4。部署前先花两分钟确认环境nvidia-smi nvcc --version gcc --versionllama.cpp 我强烈建议源码编译不要直接使用部分发行版自带的旧包因为三元量化算子是后来加入的旧版本大概率不支持。编译命令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这里有个容易踩的坑如果你正在使用 Conda 环境cmake 可能会错误地找到 Conda 自带的 CUDA 库导致编译出来的程序没法正确调用 GPU 上的 nvcc 算子。解决办法是在干净的系统环境编译或者强制指定编译器cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_COMPILER/usr/local/cuda/bin/nvcc -DCMAKE_BUILD_TYPERelease编译完成后先用自带的llama-bench跑一下确认 GPU 是活跃状态。如果输出结果后面带(CPU)标记赶紧回去检查编译选项否则后面速度测试会很惨。2.2 GGUF 文件上任前的体检流程拿到模型文件后我没有直接开跑而是先做了三件事校验文件完整性、读取 GGUF 头部信息、核对量化类型。sha256sum ternary-bonsai-2-27b-ptq1_0.gguf再用 llama.cpp 自带的 Python 脚本读取 GGUF 元数据python3 llama.cpp/gguf-py/gguf/gguf_dump.py ternary-bonsai-2-27b-ptq1_0.gguf --no-tensors输出里能直接看到模型参数量、层数、KV 头数组、上下文长度上限、词表大小和量化类型。我这份文件的几个关键信息参数量 27B层数 48KV 头数组{8, 4}最大上下文 131072量化类型TQ1_0文件 6.1GB。这个体检步骤一定别省。有一次我图省事直接加载结果 GGUF 头部版本和 llama.cpp 能识别的版本不一致加载到一半就崩了。提前用gguf_dump.py确认元数据能规避掉好几类异常。2.3 如果拿到 safetensors如何转成 GGUF我这次拿到的直接是 GGUF但如果你从别的渠道拿到的是 Hugging Face 格式权重就需要自己转换。llama.cpp 提供了转换脚本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这个参数在较新版本才支持。转换前确认一下config.json里的quantization_config字段确实标注了 PTQ1.0 方案否则脚本可能把你原来的 BF16 权重原样转成普通 FP16 GGUF文件不但没变小加载后算子不匹配还会跑不起来。2.4 一个隐蔽但很重要的检查量化算子是否真的生效这里想提一个容易被忽略的检查点。当 GGUF 加载成功后llama.cpp 日志里会出现每层 offload 的信息。如果你看到模型的量化类型确实是TQ1_0但显存占用却比预期大了接近一倍很可能是推理时矩阵计算被回退到了更高精度。这种情况下需要检查编译时GGML_CUDA开关以及是否有部分算子走了 CPU 回退。解决办法是重新编译并确认日志中没有出现 fallback 之类字样。这一步不检查后面的性能数据全是失真。3. 部署实操先把模型跑起来再说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 写一段关于本地大模型部署的总结每个参数都值得解释清楚-ngl 999把所有层都放到 GPU。如果改成-ngl 40表示 48 层里只放 40 层剩下 8 层在 CPU 跑速度立刻掉一个量级。-c 8192上下文长度。27B 模型在 4090 上8K 是稳妥起点。-fa启用 Flash Attention。长上下文下显存占用和速度改善非常明显后面有实测对比。-t 8CPU 线程数在全量 GPU offload 时影响不大但部分算子落到 CPU 时管用。采样参数先给一个保守组合具体怎么调我放到第 4 节专门讲。第一次加载时日志会输出逐层 offload 信息。我的观察是加载阶段显存峰值约 13GB稳定运行后约 9GB。首 token 出现在约 1.5 秒后解码速度约 45 tokens/s这个数据对 27B 模型在 4090 上来说是相当舒服的水平。3.2 不想碰编译就用 Ollama 导入如果不想折腾编译Ollama 是很好的一条路。安装好 Ollama 后准备一个 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|然后执行ollama create ternary-bonsai -f Modelfile ollama run ternary-bonsaiOllama 的ollama serve自带 API很多前端工具直接就能对接。但它暴露的底层参数不如 llama.cpp 多比如 Flash Attention 开关、--parallel并发数、--batch-size这些都没法直接调。所以如果你只是本地日常用Ollama 够如果你想压榨 4090 的推理性能还是得回到 llama.cpp 甚至 vLLM 这条路。3.3 跑成常驻服务兼容 OpenAI API为了让模型能被各类应用调用我最终把 llama-server 跑成了常驻服务./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 验证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}返回正常后这套服务就能被任何 OpenAI 兼容客户端接入。我把 Open WebUI 和几个 RAG 工具都接到了这个端口上体验基本和在线 API 一致只是速度由本地硬件决定。4. 调优过程速度、显存与生成质量4.1 先用基准测试把底数摸清楚调优不能靠感觉第一步要定量。我用 llama-bench 分别测了 prompt processing输入处理和 generation逐 token 生成两个阶段的性能./build/bin/llama-bench -m models/ternary-bonsai-2-27b-ptq1_0.gguf \ -ngl 999 -c 8192 -fa -t 8在 RTX 4090 上的实测数据场景Prompt ProcessingGeneration显存占用c 4096986 tokens/s52 tokens/s约 8GBc 8192912 tokens/s47 tokens/s约 9GBc 16384703 tokens/s38 tokens/s约 11GB我还专门关掉 Flash Attention 做了对照c 8192 场景下 generation 跌到 41 tokens/s显存占用反而多了约 1.5GB。所以 Flash Attention 在这个配置下属于必须开的优化项没有之一。为什么上下文变长后速度下降这么明显一方面是 KV Cache 占用的显存变多另一方面是解码阶段需要频繁读取 KV Cache三元量化让权重本身的读取带宽降得很低KV Cache 的读取占比就相对上升了所以上下文长度对最终速度的影响会比普通 4bit 模型更明显。4.2 采样参数调优实录速度只是调优的一半生成质量才是花时间大头。三元量化模型的输出分布相对平缓采样参数对结果的敏感度比普通模型更高。我做了几组对比默认的temp0.8, top_p0.95输出流畅但偶尔出现车轱辘话同一句式的重复概率明显偏高。temp0.5, top_p0.85逻辑更稳但文风偏干创意类任务表现一般。temp0.65, top_p0.9, repeat_penalty1.15最终保留的组合日常对话和代码生成兼顾。这里有一个重要的实操经验如果你发现模型总在重复同一句话不要急着猛提repeat_penalty。三元量化模型的概率分布本身比较平低 temperature 下容易收敛到重复循环这时你把惩罚系数调到 1.3 以上反而会让输出变得更加奇怪。我的处理顺序是先把repeat_penalty降到 1.05再把temperature提到 0.7 以上让采样分布带一点随机性然后再观察。多数情况下这个组合比单纯惩罚重复有效得多。代码生成场景我推荐单独一组参数temp0.2, top_p0.9, min_p0.05输出更稳定。多步推理类任务建议temp0.6, top_p0.8适当降低 top_p 可以防止分布过散导致逻辑漂移。4.3 显存占用与并发平衡一台 4090 只跑单会话多少有点浪费所以我把注意力放到并发上。先手工估算 KV Cache 体积KV Cache 大小 ≈ 层数 × KV 头数 × 头维度 × 上下文长度 × 缓存元素字节数 × 2以这个模型为例48 层、8 组 KV 头、128 头维度、8192 上下文4bit 缓存下约 2GB。加上权重 6.1GB、CUDA context 和激活值单实例约 9GB。于是我把--parallel设为 2实测两个并发会话稳定运行每个会话仍然是 8192 上下文总显存约 14GB。继续开到 3 个并发偶发 OOM把上下文降到 6144 后能跑但生成速度互相挤占实际总吞吐提升非常有限。所以在 4090 上这个模型的合理并发配置是 2再多意义不大。顺带说一句llama-server 的/metrics端点会暴露 KV Cache 使用率、请求数和推理时间等指标用 Prometheus 接上做监控很方便。我后来就是靠这个监控发现高峰期显存不足才把并发从 3 降回 2。4.4 长上下文实战取舍对三元量化模型来说长上下文是个很微妙的话题。理论上 24GB 显存能开到 32K 上下文但实测解码速度会掉到 20 tokens/s 左右体验非常差。试了几轮后我的配置策略是日常保持 8K需要做长文档总结时临时重启为 16K不常驻。如果你确实需要 32K 以上可以考虑-ngl 35部分 offload把 13 层放到 CPU 上显存能腾出约 2.5GB上下文推高到 32K但 generation 会掉到 15 tokens/s 上下。这个值只适合后台批处理场景不适合实时对话。我的实际建议还是先问自己这个任务真的需要 32K 上下文吗大多数场景 8K 到 16K 完全够用没必要为了一个边缘需求牺牲日常体验。5. 踩坑记录与排查速查5.1 GGUF 版本不匹配导致加载失败第一次加载时我遇到了GGUF metadata版本不识别的问题。现象是 llama.cpp 日志报unknown GGUF quantization type然后直接退出。原因很简单我用的 llama.cpp 版本太旧还不认识TQ1_0这个新增量化类型。解法git pull origin master cmake --build build --config Release -j 16升级之后就能正常识别。所以遇到加载失败先检查 llama.cpp 版本一星半点都别看直接拉最新别急着怀疑模型文件损坏。5.2 生成速度只有几 token/s问题出在哪有几天我把模型接到某个远程前端用出字速度慢到令人崩溃。排查顺序很关键nvidia-smi看 GPU 利用率。如果利用率接近 0%大概率权重没有完全 offload日志里的offloaded N/48 layers是一眼定乾坤的指标。检查-t线程数。如果部分算子落到 CPU-t 8和-t 16的差距非常明显。确认-fa是否开启长上下文下收益巨大。检查是否发生内存交换。如果 GGUF 权重因为系统内存不足被 swap 到磁盘模型会边读盘边推理这时需要降低上下文长度和并发数。5.3 输出内容大量重复怎么办前面写过不推荐一上来就高惩罚。处理顺序是先降repeat_penalty到 1.05temperature提到 0.7跑十组对话观察如果还有重复再逐步提高repeat_penalty每次只加 0.05。实测这个方式比直接拉满惩罚得到的输出自然得多。5.4 明明有显存却报 CUDA OOM有两次我明明看到nvidia-smi显示空闲 10GBllama-server 依然报 CUDA OOM。原因是 CUDA context 和 KV Cache 会在启动阶段预分配显存nvidia-smi看到的空闲并不等于进程可申请的显存。排查方法是看 llama-server 日志里的 buffer 分配信息或者在启动时降低-c和--parallel把预分配空间降下来。如果你需要精准控制还可以用--no-mmap配合显存优化参数但代价是加载时间变长多数情况下没必要。这一整套流程走下来我对三元量化模型的定位有了更实际的判断它不适合作为唯一的生产模型去承担高精度推理任务但做日常对话、代码补全、长文档初稿生成它用不到原生模型一半的显存跑出了可以接受的可用性和远高于同显存场景的速度这个性价比在单卡环境里非常难得。最后分享一个我的习惯本地保留一份原版 BF16 权重和一份 PTQ1.0 权重遇到对质量没有把握的任务先用三元量化跑一遍拿整体思路再决定要不要切换到原版复核。这种低比特出草稿、高比特来复核的组合实际用下来比单一模型硬扛效率高得多。