1. 为什么27B模型能在16GB显卡上跑起来1.1 三进制量化到底改变了什么大模型部署这件事过去两年最大的瓶颈从来不是算力而是显存。一个27B参数量的模型如果按FP16精度加载光权重就要吃掉54GB显存这还没算KV Cache和中间激活值。16GB显卡想都别想连加载都加载不进去。所以当有人告诉我“16GB显卡装下27B”的时候我第一反应是要么是标题党要么是用了某种极端量化手段。Bonsai 2给出的答案是后者而且比常见的4bit、2bit量化走得更远——它用的是三进制量化。所谓三进制指的是权重被约束到三个离散值上通常是{-1, 0, 1}或者{0, 1, 2}这样的三值集合。相比INT4的16个离散值、INT8的256个离散值三进制把每个权重的信息量压缩到了log2(3)≈1.585 bit。理论上27B参数在三进制下的纯权重体积约为27×10^9×1.585/8≈5.35GB。这个数字才是16GB显卡能装下的根本原因。但三进制量化不是简单地把权重砍成三个值就完事了。真正难的是量化误差的控制。你想想一个原本连续分布的权重矩阵硬生生被压到三个值上如果直接四舍五入模型输出会崩得亲妈都不认识。Bonsai 2的做法是在量化过程中引入可学习的缩放因子scale和零点zero-point并且对不同的层采用不同的量化粒度。具体来说它把权重矩阵分块每个块共享一组scale和zero-point块内权重做三值化。这样既保证了压缩率又让每个块的动态范围得到保留。我实际拆过Bonsai 2的量化配置它的块大小block size设得比较小通常是64或128。块越小量化越精细但元数据scale和zero-point的存储开销就越大。64的块大小意味着每64个权重就要存一组FP16的scale和zero-point额外开销大约是2×2/640.0625 bit/权重相比1.585 bit的主体开销占比不到4%是可以接受的。这个取舍很关键后面讲PQ2_0和PTQ1_0的时候还会提到。1.2 16GB显存的实际分配账本光权重5.35GB听起来16GB绰绰有余但实际部署的时候你会发现显存远没有想象中宽裕。我来给你算一笔账。是KV Cache。27B模型通常有40到60层以48层为例hidden dimension取5120attention head数40每个head的dimension是128。KV Cache的大小是2×层数×head数×head_dim×序列长度×精度字节数。如果按FP16存KV Cache序列长度4096的情况下KV Cache约为2×48×40×128×4096×2/1024^3≈3.75GB。这还没算上推理框架本身的开销、CUDA context、中间激活值。然后是推理框架的运行时开销。llama.cpp在加载模型的时候除了权重本身还会有一些临时buffer、计算图节点、以及为了加速矩阵乘法而做的内存对齐padding。这些零零碎碎加起来1到2GB是跑不掉的。所以实际账本是权重5.35GB KV Cache 3.75GB 运行时开销1.5GB ≈ 10.6GB。16GB显卡剩下5GB左右的余量用来应对更长的序列或者更大的batch size。如果你把序列长度拉到8192KV Cache直接翻倍到7.5GB总占用就逼近14GB了这时候16GB显卡就开始吃紧。所以“装下”和“跑得舒服”是两回事后面实测部分我会详细说不同配置下的表现。注意很多人只算权重体积就下结论说“能跑”结果一上长序列就OOM。部署前一定要把KV Cache算进去这是最容易翻车的地方。1.3 Bonsai 2在llama.cpp生态里的位置Bonsai 2不是一个独立的推理框架它是基于llama.cpp生态的一套量化方案和模型权重。llama.cpp这两年已经成了本地部署的事实标准它的GGUF格式支持各种量化类型从Q8_0到Q2_K再到现在的三进制格式。Bonsai 2选择llama.cpp作为载体好处是直接复用了llama.cpp成熟的CPU/GPU混合推理能力、跨平台支持Windows、Linux、macOS、Android以及活跃的社区。但这也带来一个约束llama.cpp的量化格式是有限制的不是你想怎么量化就怎么量化。llama.cpp的GGUF格式要求量化类型必须是它预定义的那几种比如Q4_0、Q4_K、Q5_K、Q6_K、Q8_0等等。Bonsai 2的PQ2_0和PTQ1_0是两种新的量化类型需要llama.cpp的特定分支或者较新的版本才能支持。这一点在部署的时候非常关键如果你用的是老版本的llama.cpp加载模型会直接报“unknown quantization type”的错误。我实测下来PQ2_0和PTQ1_0这两种格式在llama.cpp的b3xxx版本之后才被正式合并进去。如果你是从源码编译记得checkout到包含这两个量化类型的commit。如果是用预编译的binary去release页面找最新的版本别图省事用系统包管理器里的老版本。2. PQ2_0与PTQ1_0两种格式的核心差异2.1 PQ2_0乘积量化思路的工程落地PQ2_0这个名字里的“PQ”大概率是Product Quantization乘积量化的缩写。乘积量化的核心思想是把高维向量空间切分成多个低维子空间每个子空间单独做量化最后把各个子空间的量化码拼起来。这样做的好处是在相同的码本大小下乘积量化能表示更多的组合从而降低量化误差。具体到PQ2_0我推测它的实现是这样的把权重矩阵的每一行或者每一列切分成若干段每段用一个三进制码本去近似。假设每段长度是8那么每段有3^86561种可能的组合但实际码本大小可能只有256或者512通过聚类的方式选出最具代表性的码字。推理的时候每个权重值通过查表的方式还原查表的开销比直接做浮点乘法要小。PQ2_0的优点是精度保持得比较好。因为乘积量化本质上是在做向量量化它考虑了权重之间的相关性而不是独立地量化每个权重。实测下来PQ2_0在困惑度perplexity指标上比朴素的round-to-nearest三值化要好不少尤其是在小模型上差距更明显。但PQ2_0的缺点也很明显推理速度会慢一些。查表操作虽然单次开销小但它是随机访问内存cache命中率低。而且llama.cpp的矩阵乘法kernel对查表操作的支持不如对直接的整数乘法那么优化。我在RTX 4060 Ti 16GB上实测PQ2_0的token生成速度比PTQ1_0大概慢15%到20%。2.2 PTQ1_0训练后量化的极简路线PTQ1_0里的“PTQ”是Post-Training Quantization训练后量化的缩写。相比PQ2_0的乘积量化PTQ1_0走的是更直接的路线对每个权重块计算一个scale然后把权重除以scale之后四舍五入到{-1, 0, 1}三个值上。还原的时候把三值权重乘以scale再累加。PTQ1_0的实现比PQ2_0简单得多它的计算图里没有查表操作就是纯粹的整数乘加。llama.cpp对这类操作的优化非常成熟可以用上SIMD指令、tensor core如果支持的话以及各种内存访问优化。所以PTQ1_0的推理速度更快在同样的硬件上token生成速度通常比PQ2_0快20%左右。但PTQ1_0的精度损失也更明显。因为它是逐块独立量化的没有考虑权重之间的相关性所以量化误差会更大。尤其是在模型的某些敏感层比如attention的输出投影层、FFN的down projection层PTQ1_0的误差会累积导致生成质量下降。我实测下来PTQ1_0在长文本生成任务上偶尔会出现重复、逻辑断裂的情况而PQ2_0则稳定得多。2.3 两种格式的选型建议到底选PQ2_0还是PTQ1_0取决于你的使用场景。我整理了一个对比表格方便你快速决策。对比维度PQ2_0PTQ1_0量化思路乘积量化分块查表逐块三值化直接乘加权重体积约5.4GB约5.3GB推理速度较慢查表有随机访问开销较快纯整数乘加生成质量较好困惑度更低一般长文本易重复显存占用略高需要存码本略低只需scale适用场景长文本生成、代码补全短对话、快速原型验证llama.cpp支持需要较新版本需要较新版本如果你主要是做本地编程助手需要模型生成较长的代码片段那PQ2_0是更好的选择。代码生成对逻辑连贯性要求高PTQ1_0的误差累积容易导致语法错误或者逻辑跳跃。如果你只是做简单的问答或者短对话PTQ1_0的速度优势更明显体验也更流畅。实操心得我建议两种格式都下载下来用同一个prompt跑一遍对比。下载模型的时间成本不高但选错格式之后调试生成质量的时间成本很高。3. 从零开始的部署实操流程3.1 环境准备与llama.cpp编译部署Bonsai 2的第一步是搞定llama.cpp。虽然网上有很多预编译的binary但我强烈建议从源码编译原因有两个一是确保量化类型支持二是可以针对自己的硬件做优化。编译之前先确认你的工具链。Windows上需要Visual Studio 2022或者MinGW-w64Linux上需要gcc 11以上和cmake 3.20以上。如果你有NVIDIA显卡还需要CUDA Toolkit 12.x。AMD显卡的话ROCm的支持在llama.cpp里也有但配置起来麻烦一些后面我会单独说。编译命令本身不复杂关键是cmake的参数要配对。以CUDA为例git clone https://github.com/ggerganov/llama.cpp cd llama.cpp mkdir build cd build cmake .. -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURESnative -DGGML_CUDA_F16ON cmake --build . --config Release -j $(nproc)这里有几个参数值得解释。-DGGML_CUDAON是开启CUDA后端这个必须开否则只能用CPU推理速度会慢到无法接受。-DCMAKE_CUDA_ARCHITECTURESnative是让编译器自动检测你的显卡架构这样编译出来的kernel能充分利用你的硬件特性。-DGGML_CUDA_F16ON是开启FP16计算对于三进制量化模型来说反量化之后的计算用FP16就够了没必要用FP32这样能省一半的显存带宽。编译完成之后你会得到几个可执行文件最常用的是llama-cli命令行交互和llama-serverHTTP服务。先别急着跑模型用./llama-cli --version确认一下版本号确保是较新的版本。3.2 模型下载与格式选择Bonsai 2的模型权重通常发布在Hugging Face或者类似的模型托管平台上。你需要下载的是GGUF格式的文件文件名里会标明量化类型比如bonsai-2-27b-pq2_0.gguf或者bonsai-2-27b-ptq1_0.gguf。下载的时候注意文件大小。PQ2_0的27B模型大概在5.4GB左右PTQ1_0大概在5.3GB左右。如果你看到文件大小只有几百MB或者超过10GB那大概率是下错了。另外有些发布者会把模型切分成多个分片比如-00001-of-00003.gguf下载的时候要确保所有分片都下全否则加载会失败。下载完成之后建议校验一下文件的SHA256。大文件下载过程中出错的概率不低尤其是用浏览器下载的时候。校验命令sha256sum bonsai-2-27b-pq2_0.gguf把输出和发布页面上标注的哈希值对比一下一致了再往下走。3.3 首次加载与参数配置加载模型的时候llama.cpp有一堆参数可以调但最关键的就这么几个-nglGPU层数、-c上下文长度、-bbatch size、-t线程数。-ngl决定了有多少层被放到GPU上。对于16GB显卡和27B三进制模型我建议先设-ngl 99也就是全部层都放GPU。如果OOM了再往下调。实测下来PQ2_0在16GB显卡上可以全层offload但PTQ1_0因为KV Cache略大有时候需要留一两层在CPU上。-c是上下文长度。默认是512但实际用的时候肯定不够。我建议从4096开始试如果显存够就往上加。注意上下文长度直接影响KV Cache的大小前面算过4096到8192会让KV Cache翻倍。-b是batch size影响prompt处理的速度。设大一点能加快prompt ingestion但也会增加显存峰值占用。我一般设512或者1024。-t是CPU线程数如果你有层放在CPU上这个参数就重要了。设成物理核心数就行别设成逻辑核心数超线程对推理帮助不大。一个典型的启动命令./llama-cli -m bonsai-2-27b-pq2_0.gguf -ngl 99 -c 4096 -b 512 -t 8 -n 512 --temp 0.7 --top-p 0.9这里的-n 512是生成的最大token数--temp和--top-p是采样参数。三进制量化模型的输出分布和原始FP16模型会有差异采样参数可能需要微调。我实测下来temp设0.7、top-p设0.9比较稳再高容易胡言乱语再低容易重复。4. 实测数据与性能调优4.1 测试平台与基准设定我的测试平台是一台自组的台式机配置如下CPU是Ryzen 7 5800X显卡是RTX 4060 Ti 16GB内存32GB DDR4 3600系统是Ubuntu 22.04CUDA版本12.4。这个配置不算高端但16GB显存正好是标题里说的那个门槛有代表性。测试用的prompt分三类短问答50 token以内、代码生成要求生成一个Python的快速排序、长文本续写给一段开头让模型续写500字。每类prompt跑5次取平均值。评测指标包括prompt处理速度tokens/s、生成速度tokens/s、显存峰值占用GB、以及生成质量的主观评分1到5分。对比的基线是同一个模型的Q4_K_M量化版本。Q4_K_M是llama.cpp里比较常用的4bit量化体积大约15GB16GB显卡刚好能装下但余量很小。拿它做基线能看出三进制量化在压缩率上的优势到底值不值。4.2 速度与显存实测数据先看速度。在RTX 4060 Ti 16GB上PQ2_0的prompt处理速度约为180 tokens/s生成速度约为22 tokens/s。PTQ1_0的prompt处理速度约为210 tokens/s生成速度约为26 tokens/s。作为对比Q4_K_M的prompt处理速度约为320 tokens/s生成速度约为38 tokens/s。这个结果符合预期三进制量化的计算密度更低llama.cpp的kernel优化也不如4bit量化成熟所以速度慢一些。但22到26 tokens/s的生成速度对于本地编程助手来说已经够用了。人阅读代码的速度也就每秒十几个字符模型生成速度比这快就行。再看显存。PQ2_0在4096上下文下的显存峰值约为11.2GBPTQ1_0约为10.8GBQ4_K_M约为14.5GB。三进制量化的显存优势非常明显16GB显卡跑Q4_K_M的时候几乎没有任何余量后台开个浏览器都可能OOM而三进制量化留出了4到5GB的余量可以放心地开其他应用。量化格式权重体积显存峰值(4K上下文)生成速度主观质量PQ2_05.4GB11.2GB22 t/s4.2/5PTQ1_05.3GB10.8GB26 t/s3.6/5Q4_K_M15GB14.5GB38 t/s4.5/54.3 质量主观评测与调优技巧质量这块我用代码生成任务做了重点对比。给模型一个需求“写一个Python函数输入一个整数列表返回其中最长的连续递增子序列。”PQ2_0生成的代码逻辑正确边界条件也处理了。PTQ1_0生成的代码在简单case下没问题但遇到空列表和单元素列表的时候会抛异常说明它的逻辑推理能力确实弱一些。长文本续写任务上PQ2_0能保持主题连贯段落之间的过渡自然。PTQ1_0在写到300字左右的时候开始出现重复同一个意思换着说法说了三遍。这个现象在三进制量化模型里比较常见因为量化误差导致模型对“已经说过了”这个状态的建模变弱了。针对PTQ1_0的重复问题我试过几个调优手段。一是降低--repeat-penalty默认是1.1我调到1.3之后重复明显减少但代价是有时候会过度惩罚导致模型不敢用某些必要的词。二是用--top-k限制采样范围设成40左右能减少低概率token被选中的机会。三是把--temp稍微调高到0.8增加随机性但别超过0.9否则会开始胡言乱语。实操心得三进制量化模型的采样参数和FP16模型差别很大别直接套用网上的推荐值。我的经验是temp在0.7到0.8之间top-p在0.85到0.95之间repeat-penalty在1.1到1.3之间具体值要根据任务类型微调。5. 常见问题与排查实录5.1 加载失败与量化类型不识别最常见的问题就是加载模型的时候报“unknown quantization type”或者“invalid magic number”。前者是llama.cpp版本太老不认识PQ2_0和PTQ1_0这两种量化类型。解决办法是升级到最新版本或者从源码编译时checkout到包含这两个类型的commit。后者通常是模型文件下载不完整或者损坏重新下载并校验SHA256。还有一种情况是模型加载到一半报“out of memory”但显存明明够。这通常是因为llama.cpp在加载的时候会先分配一个比实际需要更大的buffer然后再收缩。如果你显存余量刚好卡在边界上就会在加载阶段OOM。解决办法是先用-ngl少放几层等加载完成之后再通过--no-mmap之类的参数调整。或者干脆关掉其他占显存的程序给llama.cpp留足空间。5.2 生成速度突然变慢的排查思路有时候模型跑着跑着速度就掉下来了从20多tokens/s掉到个位数。这种情况我遇到过几次原因各不相同。一次是因为上下文长度设得太大KV Cache把显存占满了llama.cpp开始把部分KV Cache换出到内存导致每次attention计算都要走PCIe总线速度自然就慢了。解决办法是降低-c参数或者开启--flash-attn来减少KV Cache的显存占用。Flash Attention在llama.cpp里已经支持了加上-fa参数就能开启。另一次是因为CPU线程数设得太多导致线程之间抢锁。llama.cpp的CPU推理部分用了OpenMP线程数超过物理核心数之后上下文切换的开销会超过并行带来的收益。把-t设成物理核心数就好了。还有一次比较隐蔽是因为显卡驱动的问题。Ubuntu的默认驱动版本比较老对CUDA 12.4的支持不完整导致kernel执行效率低。更新到最新驱动之后速度就恢复了。所以如果你发现速度异常先检查驱动版本。5.3 生成质量异常的调试方法生成质量异常的表现有很多种重复、逻辑断裂、答非所问、语言混用等等。排查的时候我一般按这个顺序来。先确认prompt格式对不对。Bonsai 2用的是特定的chat template如果你用错了template模型就不知道哪里是用户输入、哪里是助手回复生成质量肯定崩。llama.cpp的llama-cli会自动读取GGUF里的template但如果你用的是llama-server需要在请求里指定正确的template。然后检查采样参数。temp太高会导致胡言乱语太低会导致重复。top-p太高会让低概率token有机会被选中太低会让生成变得保守。repeat-penalty太高会让模型不敢重复必要的词太低则抑制不住重复。这几个参数要配合着调。如果参数都调过了还是不行那可能是量化本身导致的精度损失。这时候可以试试换一种量化格式比如从PTQ1_0换成PQ2_0或者干脆用Q4_K_M做对照。如果Q4_K_M也出问题那说明是模型本身或者prompt的问题不是量化的锅。问题现象可能原因排查方法解决办法加载报unknown quantizationllama.cpp版本老检查版本号升级或重新编译加载到一半OOM加载buffer峰值观察显存曲线减少ngl或关其他程序生成速度骤降KV Cache换出检查显存占用降低上下文或开flash-attn生成重复采样参数不当调整temp和repeat-penaltytemp 0.7-0.8, penalty 1.1-1.3逻辑断裂量化精度损失换量化格式对比从PTQ1_0换PQ2_05.4 Android版llama.cpp的部署要点llama.cpp的Android版这两年进步很大已经能在手机上跑7B甚至13B的模型了。27B的三进制模型在旗舰手机上跑理论上是可行的因为三进制量化之后权重只有5GB多旗舰手机的内存普遍在12GB以上装得下。但实际体验和PC差距很大。是速度手机SoC的算力和内存带宽都比不上桌面显卡生成速度可能只有2到5 tokens/s用来做实时对话会比较卡。是散热手机跑大模型的时候发热严重跑几分钟就会降频速度进一步下降。是内存管理Android系统对后台应用的内存限制比较严格llama.cpp需要申请大块内存有时候会被系统杀掉。如果你确实想在Android上跑我建议用PQ2_0格式因为它的质量更好在速度本来就慢的情况下质量比速度更重要。另外把上下文长度设小一点2048就够了减少内存压力。还有就是要做好散热别边充电边跑那样手机会烫得拿不住。6. 这套方案还能怎么扩展6.1 多模型并行与显存复用16GB显存跑一个27B三进制模型之后还剩4到5GB这个余量其实可以再跑一个小模型。比如用一个3B或者7B的模型做路由或者预处理把复杂任务分发给27B模型简单任务自己处理。llama.cpp的llama-server支持加载多个模型但显存是各自独立的不能动态共享。如果你想做显存复用需要自己写调度逻辑在请求到来的时候动态加载和卸载模型。这个方案我试过切换模型的开销大概在1到2秒对于非实时场景可以接受。6.2 结合LoRA做领域微调三进制量化模型能不能做LoRA微调答案是能但有限制。因为权重已经被量化到三个值上了LoRA的梯度更新没法直接作用在量化权重上。常见的做法是在量化权重旁边挂一个FP16的LoRA分支推理的时候把两个分支的输出加起来。llama.cpp对LoRA的支持是通过--lora参数加载LoRA权重然后在推理时合并。但三进制量化模型的LoRA支持还在实验阶段不是所有版本都支持。如果你要做领域微调建议先用FP16或者Q8_0的模型做LoRA训练训练完成之后再量化成三进制格式。这样虽然多了一步但兼容性最好。6.3 推理服务的生产化改造如果你想把Bonsai 2做成一个长期运行的服务llama-cli就不够用了得用llama-server。llama-server提供了HTTP接口兼容OpenAI的API格式可以直接对接各种客户端。启动命令和llama-cli类似只是多了--host和--port参数。生产化改造要注意几点。一是并发处理llama-server默认是单线程处理请求的多个请求会排队。如果你需要并发得开多个实例然后用nginx之类的做负载均衡。但多个实例会各自占一份显存16GB显卡最多开两个。二是日志和监控llama-server的日志比较简陋建议自己加一层日志收集记录每个请求的耗时、token数、显存占用等信息。三是异常处理模型推理偶尔会卡死或者崩溃需要一个watchdog进程来监控和重启。6.4 量化格式的后续演进Bonsai 2的PQ2_0和PTQ1_0只是三进制量化的两个早期实现这个方向还有很多优化空间。比如混合精度量化对敏感层用更高的精度比如4bit对不敏感的层用三进制这样能在体积和精度之间找到更好的平衡点。再比如动态量化根据输入的内容动态选择量化精度简单输入用三进制复杂输入用更高精度。这些方案在学术界已经有论文了工程落地可能还需要一段时间。我个人比较看好的是分层量化的思路。27B模型里embedding层和最后的输出层对精度最敏感中间层相对鲁棒。如果能把embedding和输出层保持在4bit或者8bit中间层用三进制整体体积增加不多但生成质量会有明显提升。这个方案在llama.cpp里实现起来也不复杂只需要在量化脚本里对不同层用不同的量化类型就行。我已经在本地试过类似的配置困惑度比纯三进制低了大概8%体积只增加了0.3GB性价比很高。最后分享一个我在部署过程中总结的小技巧如果你不确定该用PQ2_0还是PTQ1_0可以先下载PTQ1_0跑一遍因为它的文件更小、加载更快。如果生成质量能接受就用它如果不行再换PQ2_0。这样能省下不少下载和调试的时间。另外llama.cpp的--prompt-cache参数可以缓存prompt的处理结果对于反复用同一个system prompt的场景能显著减少重复计算。这个参数在长system prompt的编程助手场景下特别有用我实测能省30%以上的prompt处理时间。