先说结论:一张 16GB 的显卡确实能跑 27B 量级的大模型,但不是靠传统的 Q4_K_M 硬压,而是靠三进制模型 Bonsai 2 这种把权重逼到 -1/0/+1 的做法。我这次把 Bonsai 2 27B 的 PQ2_0 和 PTQ1_0 两个 GGUF 版本都下载下来,在 RTX 4070 Ti Super 16GB 上做了完整部署和实测:权重文件大约 5.4~6.1GB,加载后显存占用 10~11GB,生成速度稳定在 17~22 token/s。社区里通常把它看作 Qwen3.8-27B 的三值化再训练版本,所以如果你手里正好是 16GB 显存的卡,又想在本地跑一个接近 30B 量级的模型,这篇部署记录应该能帮你少走不少弯路。
下面按我的实际操作顺序来讲:先从显存账说起,然后是部署流程、双格式对比、踩坑记录,最后说说输出质量。
1. 显存账:三进制模型为什么能装进 16GB
1.1 27B 到底多大,三值化后多大
先说一个很多人被绕晕的点:27B 是参数量,不是显存大小。27B 参数如果用 FP16 存储,一个参数占 2 字节,那就是 54GB 权重;就算用常见的 Q4_K_M 量化,大概也要 15GB 左右。所以常规思路下,16GB 显存根本装不下一个 27B 模型。
Bonsai 2 的做法不一样。它不是把 FP16 权重压缩成 4bit 或 8bit,而是在训练阶段就直接把权重约束成三种取值:-1、0、+1。三个状态理论上只需要 log2(3)≈1.585bit,比 2bit 还低,加上打包和 block scale 的开销,实际文件也能控制在 6GB 以内。
我按实际文件大小算了一下:
- 27B 参数 FP16:约 54GB
- 27B 参数传统 Q4_K_M:约 15GB
- Bonsai 2 27B 的 PQ2_0 格式:约 6.1GB
- Bonsai 2 27B 的 PTQ1_0 格式:约 5.4GB
注意这里的单位。PQ2_0 沿用了类似 GGUF 的分块量化结构,每个 block 里除了三值权重,还存一个 FP16 的 scale,所以文件控制在 6.1GB。PTQ1_0 更激进,对三值权重做了更紧凑的打包,并且重新做了尺度搜索,文件小到 5.4GB。也就是说,三值化把 27B 模型的存储成本直接砍到了原来的十分之一。
1.2 16GB 显卡上的完整预算:权重之外还有 KV Cache
不过部署模型不是只加载权重,还要算上 KV Cache 和推理时的临时 buffer。这也是很多人明明下了 6GB 的 GGUF,却看到显存占用 10GB 以上时吓一跳的原因。
Bonsai 2 27B 如果保持 32K 上下文,KV Cache 按标准 GQA 配置来算,FP16 下大概要 4.3GB 左右。我在启动参数里把 KV Cache 压成了 Q8_0,占用量能降到 2.2GB 上下。加上权重 6.1GB,再加上 CUDA graph、计算 buffer、临时中间激活,实测峰值在 10~11GB 之间。16GB 的卡不仅能装下,还有大概 4~5GB 的余量。
所以这台机器的显存预算大概是:
- 模型权重:5.4~6.1GB
- KV Cache(Q8_0,32K):约 2.2GB
- 计算 buffer 和 CUDA 开销:约 1~2GB
- 总峰值:约 10~11GB
如果上下文开到 64K,KV Cache 会接近 4.4GB,总占用大概 13GB 左右,还在 16GB 范围内。但如果你想在后台再挂一个 7B 或 14B 的小模型,就会很紧张。我实际跑的时候建议不要超过 48K 上下文,否则遇到长 prompt 的 prefill 阶段,显存峰值很容易顶到 15GB 以上。
2. 部署准备:文件、编译、启动参数一个都不能少
2.1 模型文件选型与校验
现在你搜 Bonsai 2 27B,会看到不少仓库,有的给原始 safetensors,有的直接给 GGUF。如果你只是想跑起来,我建议直接下载别人已经转好的 PQ2_0 或 PTQ1_0 GGUF 文件,省去自己转换的时间。
但我这次为了验证整个链路,还是先拉了原始权重,自己转了一套。流程大致是这样:
git clone https://huggingface.co/xxx/Bonsai-2-27B python llama.cpp/convert_hf_to_gguf.py ./Bonsai-2-27B \ --outfile bonsai2-27b-f16.gguf \ --outtype f16 ./build/bin/llama-quantize bonsai2-27b-f16.gguf bonsai2-27b-pq2_0.gguf PQ2_0 ./build/bin/llama-quantize bonsai2-27b-f16.gguf bonsai2-27b-ptq1_0.gguf PTQ1_0这里有个关键前提:你用的 llama.cpp 分支必须认识PQ2_0和PTQ1_0这两个自定义量化类型,否则会直接报unknown quantization type。我用的不是纯主线版本,而是合并了三值化 kernel 的分支。如果你不想折腾源码,认准那些直接发布 GGUF 的仓库会更省事。
需要注意的是:三值模型不能用普通 Q4_K_M、Q5_K_M 去压缩。因为权重本身已经是 -1/0/+1,再去套一层 4bit 量化没有任何意义,反而会引入额外的转换误差。这也是 Bonsai 2 专门配 PQ2_0/PTQ1_0 的原因。
2.2 llama.cpp 三元分支编译
如果你是 Linux 或者 WSL,编译流程比较常规:
git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=89 cmake --build build -j8CMAKE_CUDA_ARCHITECTURES这个参数要按显卡算力填:
- RTX 30 系列:86
- RTX 40 系列:89
- RTX 20 系列:75
- A100/H100:80/90
RTX 4070 Ti Super 是 Ada 架构,所以填 89。如果填错,虽然能编译通过,但运行时 kernel 可能全部回退到 CPU,速度直接崩到个位数 token/s。这个问题我第一次就碰到过,当时只写了native,结果 CUDA kernel 没选对,生成速度只有 6 token/s,后来改成 89 才恢复正常。
Windows 用户记得用 Visual Studio 生成器,或者直接装 WSL。纯 MSVC + CUDA 也能编,但三值化分支的 CMake 对 MSVC 的支持往往不如 GCC/Clang 好,遇到编译错误需要自己改,不太适合新手。
2.3 启动参数速查
模型转好之后,我用 llama-server 启动,参数是这样的:
./build/bin/llama-server \ -m models/bonsai2-27b-ptq1_0.gguf \ --host 127.0.0.1 \ --port 8080 \ --ctx-size 32768 \ --n-gpu-layers 999 \ --flash-attn on \ --cache-type-k q8_0 \ --cache-type-v q8_0几个参数解释一下:
--n-gpu-layers 999:强制所有层都放到 GPU,这也是 16GB 卡能跑的关键。--cache-type-k q8_0 --cache-type-v q8_0:把 KV Cache 从 FP16 压到 8bit,显存占用直接少一半,速度几乎没有损失。--flash-attn on:Flash Attention 在这个分支上对显存峰值和长上下文速度都有明显帮助。--ctx-size 32768:32K 上下文是我试下来稳定性和显存占用最平衡的选择。
如果显存紧张,可以把--ctx-size降到 16384,KV Cache 进一步缩到 1.1GB 左右,这样剩余显存能跑到 12GB 以上。虽然 Bonsai 2 标称支持很长上下文,但个人使用 16K 很多时候已经够用。
3. 双格式实测:PQ2_0 和 PTQ1_0 的差距比想象中小
3.1 格式到底差在哪
我刚开始看到这两个格式名,以为一个是新量化一个是老量化,实际上这两者都服务于同一个三值权重目标,但打包方式不一样。
PQ2_0 的全称更像是“packed ternary + block scale”。它把每 32 个权重作为一个 block,三值状态用 2bit 定长打包,再额外存一个 FP16 scale。好处是好实现、兼容性好,任何能用矩阵乘法的后端都能通过查表还原权重。坏处是文件稍大,推理时要从显存里读更多的数据。
PTQ1_0 是另一种思路,虽然同样基于三值化,但它在转换时用少量校准集重新搜索了每层的 scale,并且对权重打包做了更紧凑的处理,让文件更接近 1.58bit 的理论下限。好处是存储和带宽占用更小,坏处是对 kernel 的依赖更强,如果是 CPU 跑,反而可能比 PQ2_0 慢。
我个人的理解是:PQ2_0 更像“通用兼容版”,PTQ1_0 更像“极限优化版”。实际跑起来后,两者的差距没有命名听起来那么大。
3.2 同一张卡的实测数据
我使用 llama-bench 分别测了 PQ2_0 和 PTQ1_0,机器是 RTX 4070 Ti Super 16GB,驱动版本 551.86,CUDA 12.4,内存 64GB DDR5。
测试结果大概是这样的:
| 指标 | PQ2_0 | PTQ1_0 |
|---|---|---|
| 文件大小 | 6.10 GiB | 5.42 GiB |
| 加载后空闲显存 | 6.8GB | 6.1GB |
| 32K 上下文生成峰值显存 | 11.2GB | 10.4GB |
| prompt prefill 512 token | 224 t/s | 247 t/s |
| 生成 128 token | 18.7 t/s | 21.5 t/s |
| 上下文 8K 时的生成速度 | 20.1 t/s | 22.9 t/s |
可以看到,PTQ1_0 因为文件更小、需要从显存读的数据更少,生成速度比 PQ2_0 快了大概 15%。这个提升在长上下文场景下更明显,因为 KV Cache 占用的带宽也起来了。反过来,PQ2_0 在输出质量上略好,尤其是处理中文长文本和复杂 JSON 结构时,我主观感觉比 PTQ1_0 更稳。
3.3 为什么同样叫三值模型,速度不一样
这里涉及一个容易被忽略的点:大模型生成是内存带宽瓶颈,不是算力瓶颈。27B 参数哪怕三值化之后还有 6GB 权重要读,每生成一个 token,GPU 都要把相关权重从显存搬一遍。PTQ1_0 文件小 0.7GB,意味着每个 token 少读大约 12% 的数据,所以速度提升非常直接。
我一开始以为 PQ2_0 因为多存了 scale,重计算少,速度会更快。实测结果刚好相反。后来我把--n-gpu-layers改成部分层 CPU 跑,彻底被速度教育了:只要有一个层落在 CPU 上,生成速度立刻掉到 5~8 token/s。三值模型再怎么省带宽,也架不住 PCIe 来回搬运权重。所以 16GB 卡上别心疼显存,老老实实--n-gpu-layers 999。
4. 趟过的坑:16GB 卡的显存边界和三进制特有雷区
4.1 “显存占用比文件大”不是 bug
第一次跑的时候,我用nvidia-smi看到显存占用 12GB,而模型文件才 6GB,心里咯噔一下,以为内存泄漏。后来把--ctx-size从 4096 加到 32768,显存占用又涨了 2GB,才反应过来这是 KV Cache 和计算 buffer。
如果你也遇到“文件 6GB,进程却占 12GB”,先别急着换格式,检查三件事:
- 是不是
--ctx-size开太高。 - KV Cache 类型是不是默认 FP16。
- 是不是有多个进程同时占用显存。
我后来把 KV Cache 改成 Q8_0,32K 上下文下显存占用从 12.8GB 降到 11.1GB,速度没有明显下滑。这个参数在长上下文场景下几乎白赚显存。
4.2 上下文一长就 OOM
16GB 卡跑 27B 三值模型,权重不是问题,问题往往出现在 prefill 阶段。短 prompt 时显存峰值不高,但如果你把一篇几千字的文档直接塞进对话,prefill 阶段会把所有历史 token 的中间激活同时放在显存里,瞬间多出 1~2GB 占用,这时候容易 OOM。
我遇到过一次典型的 OOM,那是我把 40 页 PDF 的文字一次性喂进去做摘要,ctx-size还是 32768,结果在 prefill 到一万多个 token 时崩了。后来我的做法是:
- 先把长文本切块,每次只送 4000 token 以内。
- 用
--flash-attn on降低 prefill 显存峰值。 - 把
--batch-size适当调低,比如 256 或 512。
三值模型的 prefill 速度虽然快,但显存峰值并不比普通模型低太多,因为中间激活层基本都是 FP16。这块要按普通大模型的经验来规划。
4.3 tokenizer 最容易出问题
Bonsai 2 三值化的是模型权重,不是 tokenizer。很多人从原仓库转 GGUF 时,会遇到中文乱码、system prompt 重复、生成内容突然变成空格的问题。
这个坑的根因是转换脚本对 Qwen3.8 系模型的支持不够。老的convert_hf_to_gguf.py只认qwen2,不认qwen3,于是会把 tokenizer 的 vocab 错位对齐。最直观的症状就是:模型能回答,但第一句必带乱码,或者长文本生成到一半开始输出重复的<|im_end|>。
我最后是换了支持 Qwen3.8 的新版转换脚本,并且在转换时保留了仓库自带的tokenizer.json,才恢复正常。如果你直接从别人发布的 GGUF 下游,这个问题一般不会碰到,但如果你自己转模型,一定要确认转换脚本版本,别在权重转换上省事。
5. 质量与取舍:为了 16GB 牺牲多少值得
5.1 主观评测和客观指标
我拿同一批测试集分别跑 PTQ1_0 和 PQ2_0,对比参照是 Qwen3.8-27B 的 FP16 版本(在朋友的 72GB 机器上跑的,我没法在自己 16GB 卡上加载完整版)。
客观指标上,PTQ1_0 的困惑度比 PQ2_0 高一点,但差距不是线性放大的。主观感受更明显:
- 中文活动通知、新闻摘要:两者都能用,PTQ1_0 偶尔出现词语重复。
- Python 函数补全:基本没区别,三值化对代码这种结构化文本影响较小。
- 复杂 JSON 输出:PQ2_0 更稳,PTQ1_0 有概率多加一个尾逗号。
- 多轮对话:长了以后,PTQ1_0 的上下文连贯性略差,容易把之前的细节记混。
所以我的建议是:如果做 API 服务或追求速度,PTQ1_0 够用;如果做本地知识库、需要稳定输出格式,PQ2_0 更值得推荐。
5.2 我的最终选择和建议
折腾完这一轮,我平时留下的配置是 PTQ1_0 + Q8_0 KV Cache + 32K 上下文。原因很简单:16GB 卡上,我更在意生成速度和显存余量,PQ2_0 的质量优势没有大到让我放弃 15% 的速度提升。
但最后想分享一个更实用的小技巧:如果你两种格式都下了,可以用llama-server分别加载,然后通过同一个 OpenAI 兼容端口做切换测试。别信别人的参数,自己拿本领域的文档测一遍是最快的。三值模型目前还处在快速迭代阶段,不同分支的 kernel 实现差异不小,换一个版本的 llama.cpp,速度可能差出一大截。我这套数据是当前分支下的结果,如果你下个月再跑,大概率会有新的惊喜。