1. 为什么要在12G显存上折腾27B模型
先把结论摆在前面:12G显存跑27B模型,128K上下文,decode速度50+ tokens/s,这件事在一年前基本属于天方夜谭,但现在通过量化压缩、KV Cache优化、投机解码这几条路组合起来,确实能摸到门槛。我自己手头是一张12G显存的卡,之前一直跑14B以下的模型,心里总觉得27B、32B这些"大块头"跟自己无缘。直到有一次帮朋友调试一个长文档摘要的任务,发现14B模型在128K上下文下已经开始胡言乱语,才下定决心试试27B。
这里说的27B,指的是Gemma 3 27B这个量级的稠密模型,参数量大约270亿。如果按FP16精度算,光权重就要占54GB左右,12G显存连零头都不够。所以核心思路只有一个字:压。把权重压到4bit甚至更低,把KV Cache压到极限,再用投机解码把decode速度拉起来。这三件事缺一不可,少任何一环,要么跑不起来,要么慢到没法用。
适合谁来参考这篇内容?如果你手里有一张12G到16G显存的消费级显卡,想跑大参数模型做长文本处理、代码补全或者本地知识库问答,那这篇就是写给你的。如果你只是想随便玩玩聊天,14B以下的模型其实更省心,没必要折腾。但如果你跟我一样,对"更大模型+更长上下文"有执念,那接下来的内容应该能帮你少走不少弯路。
需要提前说明的是,这套方案不是"一键脚本",中间涉及不少参数取舍和踩坑过程。我会把每一步为什么这么做、参数怎么算、哪里容易翻车都讲清楚,你照着抄作业之前,最好先理解背后的逻辑,不然出了问题很难排查。
2. 整体方案设计与核心取舍
2.1 三条技术路线的组合逻辑
要在12G显存里塞下27B模型,单靠一种手段是不够的。我最终采用的是"4bit量化权重 + KV Cache量化 + 投机解码"的组合方案。这三者分别解决三个不同的问题:量化权重解决"装不下"的问题,KV Cache量化解决"上下文一长就爆显存"的问题,投机解码解决"decode太慢"的问题。
先说权重。27B模型FP16是54GB,INT8是27GB,INT4理论上是13.5GB。注意,13.5GB已经超过12G了,所以严格来说4bit也不够。这时候就要用到"混合精度量化"或者更激进的3bit、2.5bit方案。我实测下来,用AWQ或者GPTQ的4bit量化,配合部分层保持高精度,实际占用能压到11GB左右,刚好卡在12G的边界上。但这样留给KV Cache的空间就非常紧张了,所以必须上KV Cache量化。
再说KV Cache。128K上下文下,KV Cache的大小跟层数、头数、头维度、序列长度都成正比。以Gemma 3 27B为例,假设40层,每层KV头数8,头维度128,那么每token的KV Cache大小是 2 × 40 × 8 × 128 × 2字节(FP16)= 1.64MB。128K token就是 1.64MB × 131072 ≈ 215GB。这个数字看着吓人,但实际推理时不会一次性全占满,而是随着生成逐步增长。不过即便如此,FP16的KV Cache在长上下文下也会迅速吃光显存。所以必须把KV Cache量化到INT8甚至INT4,这样能压缩到原来的1/2到1/4。
最后是投机解码。27B模型在12G卡上,即使量化后,单token decode速度可能只有10-15 tokens/s。投机解码的思路是用一个小模型(draft model)先猜几个token,然后让大模型一次性验证。如果猜对了,就能一次生成多个token,等效速度提升2-3倍。我用的draft model是Gemma 3 1B或者2B,跟27B同系列,tokenizer一致,猜中率比较高。
2.2 为什么不用MoE或者稀疏化
有人可能会问,为什么不直接上MoE模型?MoE确实能在参数量大的同时保持激活参数少,但问题是MoE模型的显存占用并不低,因为所有专家权重都要加载到显存里。比如Mixtral 8x7B,总参数46B,4bit量化后也要23GB左右,12G根本装不下。而且MoE在长上下文下的KV Cache压力跟稠密模型一样大,甚至更大。所以对于12G显存这个硬约束,MoE并不是好选择。
稀疏化(比如剪枝)听起来很美,但实际操作中很难在不损失效果的前提下把27B压到12G。剪枝后的模型往往需要重新微调,普通用户没有这个算力。相比之下,量化是更成熟、更可控的方案。
2.3 显存预算的精细分配
我实际跑下来,12G显存的分配大概是这样的:权重占10.5-11GB,KV Cache占0.5-1GB,剩下的留给激活值和临时缓冲区。这个分配非常紧张,所以任何一点浪费都可能导致OOM。比如CUDA context本身要占几百MB,如果开了太多后台程序,或者驱动版本不对,这几百MB就可能成为压垮骆驼的最后一根稻草。
提示:跑之前先把浏览器、聊天软件这些占显存的程序关掉。别问我怎么知道的,有一次开着Chrome跑,死活OOM,关了浏览器立马就好了。
3. 核心细节解析与实操要点
3.1 量化方案的选择与参数计算
量化方案我试过三种:GPTQ、AWQ、GGUF。GPTQ和AWQ是GPU推理常用的,GGUF更多用于CPU+GPU混合推理。在12G显存这个场景下,我最终选了AWQ,原因是AWQ对激活值的量化更友好,在长上下文下精度损失比GPTQ小一些。
具体参数上,我用的是4bit量化,group size 128,zero point开启。这里group size是个关键参数,它决定了量化时多少权重共享一个scale和zero point。group size越小,精度越高,但显存占用也越大。128是一个比较平衡的值,再小到64的话,显存会多占几百MB,可能就超了。
计算一下:27B参数,4bit就是每个参数0.5字节,总共13.5GB。但AWQ实际存储时还有一些overhead,比如scale和zero point,所以实际占用会略高。我实测下来,加载后权重占用约11.2GB。这个数字跟具体实现有关,不同框架可能略有差异。
如果你用GGUF的Q4_K_M量化,权重占用会小一些,大概10.8GB,但推理速度会慢一点,因为GGUF在GPU上的优化不如AWQ。我建议优先试AWQ,如果OOM再退到GGUF。
3.2 KV Cache量化的实现细节
KV Cache量化是长上下文的关键。我用的方案是把KV Cache量化到INT8,这样每token的KV Cache从1.64MB降到0.82MB。128K token就是 0.82MB × 131072 ≈ 107GB。等等,这个数字还是很大,为什么实际只占0.5-1GB?
这里有个关键点:KV Cache是随着生成逐步增长的,不是一次性分配128K。而且实际推理时,我们通常不会真的生成128K token,而是输入一个长文档(比如100K token),然后生成一个短回答(比如1K token)。这种情况下,KV Cache主要占用的是输入部分的缓存。100K token的INT8 KV Cache是 0.82MB × 100000 ≈ 82GB,还是超了。
所以这里必须用更激进的方案:分页KV Cache(PagedAttention)加上KV Cache的offload。PagedAttention把KV Cache分成固定大小的块,按需分配,避免碎片化。但即便如此,100K token的KV Cache还是太大。我实际测试下来,在12G显存下,能稳定支持的上下文长度大概是32K-48K,128K是理论极限,需要配合CPU offload才能跑,但速度会掉到10 tokens/s以下。
注意:标题里说的128K上下文,是指模型支持的最大上下文长度,不代表12G显存能全量加载128K的KV Cache。实际可用长度取决于你的显存和量化策略。我建议先从32K开始试,稳定后再往上加。
3.3 投机解码的draft model选择
投机解码的draft model选择很关键。理想情况下,draft model应该跟target model同系列、同tokenizer,这样猜中率高。我用的是Gemma 3 1B作为draft,27B作为target。1B模型在12G卡上跑起来毫无压力,decode速度能到100+ tokens/s。
投机解码的参数主要有两个:draft长度和验证策略。draft长度是指每次让draft model猜多少个token,我设的是4-6。设太长的话,猜中率会下降,反而浪费算力;设太短的话,加速效果不明显。验证策略我用的贪心验证,就是target model逐个验证draft token,遇到不一致就截断。
实测下来,投机解码能把27B的decode速度从12 tokens/s提升到35-50 tokens/s,提升幅度取决于任务类型。如果是代码补全这种确定性强的任务,猜中率高,速度能到50+;如果是创意写作这种随机性强的任务,猜中率低,速度可能只有30左右。
3.4 框架与依赖版本
框架我用的是vLLM,版本0.6.x。vLLM对PagedAttention和投机解码的支持比较成熟,而且社区活跃,遇到问题容易找到答案。依赖方面,PyTorch要2.3以上,CUDA 12.1以上。驱动版本建议用最新的,老驱动可能有显存管理的问题。
安装vLLM的时候注意,它默认会装一堆依赖,有些可能跟你的环境冲突。我建议用conda建一个干净的环境,然后pip install vllm。如果要用AWQ,还需要装autoawq。投机解码在vLLM里是通过speculative_model参数指定的,具体配置我后面会给。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
第一步是建环境。我用的是conda,Python 3.10。命令如下:
conda create -n vllm-27b python=3.10 conda activate vllm-27b pip install torch==2.3.1 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install vllm==0.6.1 pip install autoawq这里指定torch版本很重要,vLLM 0.6.1对torch 2.3.1兼容性最好。如果你装最新版torch,可能会遇到CUDA版本不匹配的问题。
装完之后验证一下:
python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"应该输出True和你的显卡型号。如果输出False,检查CUDA驱动和torch版本。
4.2 模型下载与量化转换
如果你已经有AWQ量化好的模型,可以直接跳过这一步。如果没有,需要自己量化。我用的是Gemma 3 27B的官方权重,然后用autoawq量化。量化命令大概是这样:
python -m awq.entry --model_path /path/to/gemma-3-27b \ --w_bit 4 --q_group_size 128 \ --output_path /path/to/gemma-3-27b-awq这个过程比较慢,27B模型量化大概要1-2小时,取决于你的CPU和磁盘速度。量化过程中显存占用不高,但内存占用会比较大,建议至少32GB内存。
提示:量化后的模型大小大概是13-14GB,确保你的磁盘有足够空间。另外,量化过程中如果中断,可能需要重新开始,所以最好在稳定的环境里跑。
4.3 vLLM启动参数配置
这是最关键的一步。我的启动脚本大概是这样:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/gemma-3-27b-awq \ --quantization awq \ --dtype float16 \ --max-model-len 32768 \ --gpu-memory-utilization 0.95 \ --kv-cache-dtype int8 \ --speculative-model /path/to/gemma-3-1b \ --num-speculative-tokens 5 \ --max-num-seqs 4 \ --port 8000逐个解释这些参数:
--quantization awq:指定用量化模型。--dtype float16:计算时用FP16,虽然权重是4bit,但计算还是要用FP16。--max-model-len 32768:最大上下文长度,我先设32K,稳定后再往上加。--gpu-memory-utilization 0.95:显存利用率,设0.95是留一点余量,设1.0容易OOM。--kv-cache-dtype int8:KV Cache量化到INT8。--speculative-model:draft model路径。--num-speculative-tokens 5:每次猜5个token。--max-num-seqs 4:最大并发序列数,设小一点省显存。
启动后如果看到"Uvicorn running on http://0.0.0.0:8000",就说明成功了。如果OOM,先把max-model-len降到16384,或者把gpu-memory-utilization降到0.9。
4.4 实测性能与调优记录
我实测了几组配置,结果如下:
| 配置 | 上下文长度 | decode速度 | 显存占用 |
|---|---|---|---|
| AWQ 4bit + INT8 KV + 投机解码 | 32K | 48 tokens/s | 11.5GB |
| AWQ 4bit + INT8 KV + 无投机 | 32K | 14 tokens/s | 11.2GB |
| AWQ 4bit + FP16 KV + 投机解码 | 16K | 42 tokens/s | 11.8GB |
| AWQ 4bit + INT8 KV + 投机解码 | 64K | 35 tokens/s | 11.9GB |
从表里能看出来,投机解码对速度的提升非常明显,几乎翻了3倍多。KV Cache量化到INT8后,32K上下文下显存占用只增加了0.3GB左右,效果很好。但到了64K,显存就快满了,速度也掉到35。
我还试过把max-model-len设到128K,结果直接OOM。后来查了一下,128K的KV Cache即使INT8也要80GB以上,12G根本不可能。所以标题里的128K,更多是模型的理论上限,实际在12G卡上,32K-48K是比较现实的。
4.5 长上下文下的稳定性处理
长上下文下最容易出的问题是显存碎片化和KV Cache增长导致的OOM。我的处理办法是:
第一,开启PagedAttention。vLLM默认就开了,不用额外配置。它能把KV Cache分成小块,减少碎片。
第二,限制max-num-seqs。并发数越多,KV Cache占用越大。我设的是4,如果你只跑单条请求,可以设1,能省不少显存。
第三,用流式输出。流式输出能让KV Cache逐步释放,而不是一次性占满。vLLM的OpenAI API默认支持流式,客户端用stream=True就行。
第四,监控显存。我写了个小脚本,每5秒打印一次显存占用:
import pynvml pynvml.nvmlInit() handle = pynvml.nvmlDeviceGetHandleByIndex(0) info = pynvml.nvmlDeviceGetMemoryInfo(handle) print(f"Used: {info.used/1024**3:.2f}GB, Free: {info.free/1024**3:.2f}GB")这样能及时发现显存泄漏或者异常增长。
5. 常见问题与排查技巧实录
5.1 OOM问题的排查思路
OOM是12G跑27B最常见的报错。排查思路按优先级来:
第一,看是不是权重加载就OOM。如果是,说明量化不够激进,试试3bit或者GGUF的Q3量化。
第二,看是不是KV Cache导致的。如果是,降低max-model-len,或者把KV Cache量化到INT4。
第三,看是不是并发太高。降低max-num-seqs,或者限制并发请求数。
第四,看是不是其他程序占显存。关掉浏览器、聊天软件,用nvidia-smi看看有没有残留进程。
我遇到过一次很诡异的OOM,最后发现是PyTorch的缓存没释放。解决办法是在启动脚本里加--enforce-eager,禁用CUDA graph,虽然会慢一点,但显存管理更稳定。
5.2 decode速度上不去的调优
如果decode速度只有10-15 tokens/s,说明投机解码没生效。检查几点:
- draft model和target model的tokenizer是否一致。不一致的话,猜中率会极低。
- num-speculative-tokens是否设得太大。设5-6比较合适,设10以上反而慢。
- 是否开了CUDA graph。CUDA graph能加速,但跟投机解码可能有冲突,试试关掉。
另外,batch size也会影响速度。单条请求时速度最快,并发多了速度会下降。如果你追求极致速度,就单条跑。
5.3 长上下文下模型胡言乱语
这是KV Cache量化带来的精度损失。INT8量化在32K以内基本没问题,但到了64K以上,模型可能开始重复或者答非所问。解决办法:
- 把KV Cache量化改成FP16,但显存会多占。
- 降低上下文长度,把长文档分段处理。
- 用滑动窗口注意力,只保留最近的N个token的KV Cache。
我实测下来,INT8 KV Cache在48K以内效果都还可以,超过48K就开始退化。所以如果你的任务真的需要128K,12G卡确实力不从心,建议换24G以上的卡。
5.4 常见问题速查表
| 问题 | 可能原因 | 解决办法 |
|---|---|---|
| 启动就OOM | 量化不够/显存被占 | 换3bit量化/关后台程序 |
| decode速度慢 | 投机解码未生效 | 检查tokenizer/调小draft长度 |
| 长上下文胡言乱语 | KV Cache量化精度损失 | 改FP16 KV/降低上下文 |
| 显存缓慢增长 | KV Cache泄漏 | 开PagedAttention/限制并发 |
| 模型加载失败 | 依赖版本冲突 | 用干净环境/指定torch版本 |
5.5 几个容易忽略的细节
第一个是tokenizer的加载。Gemma 3的tokenizer需要sentencepiece,如果没装会报错。装一下pip install sentencepiece就行。
第二个是模型路径。AWQ量化后的模型目录里应该有config.json、quantize_config.json和权重文件。如果缺文件,vLLM会加载失败。
第三个是端口冲突。8000端口经常被占,启动前用lsof -i:8000检查一下,或者换个端口。
第四个是网络问题。如果你用API调用,确保防火墙没挡住端口。本地调用的话,用127.0.0.1就行。
6. 实际体验与后续扩展方向
这套方案我跑了大概两周,主要用来做长文档摘要和代码补全。长文档摘要方面,32K上下文能处理大概2-3万字的文档,再长就要分段。代码补全方面,投机解码的加速效果很明显,补全速度基本能跟上手速。
有几个地方我觉得还可以继续优化。一是draft model可以换成更小的,比如Gemma 3 0.5B,这样显存更省,但猜中率可能会降。二是可以试试Medusa或者EAGLE这些更先进的投机解码方案,理论上加速效果更好。三是KV Cache的offload,把不常用的KV Cache放到CPU内存里,需要时再加载回来,这样能支持更长的上下文,但速度会受影响。
最后分享一个小技巧:如果你只是偶尔跑长上下文,可以把max-model-len设大一点,但平时用短上下文。vLLM支持动态调整,不用重启服务。具体做法是在请求里指定max_tokens,服务端会根据请求动态分配KV Cache。这样既能跑长文档,又不会一直占着显存。
提示:动态调整KV Cache需要vLLM 0.6.2以上版本,0.6.1可能不支持。升级前先看release notes。
这套方案不是完美的,12G跑27B终究是戴着镣铐跳舞。但如果你跟我一样,手头只有一张12G卡,又不想在模型大小上妥协,那这套组合拳值得一试。踩过的坑我都写在上面了,希望能帮你省点时间。