☰
12G显存跑27B模型:128K上下文与50+ tokens/s的工程实践
2026/10/3 4:23:05 网站建设 项目流程

1. 12G显存跑27B模型这件事,到底难在哪

先把结论摆在前面:12G显存跑27B模型,128K上下文,decode 50+ tokens/s,这三个指标单独拎出来任何一个都不算离谱,但叠在一起就是一道非常硬的工程题。我自己手上是一张RTX 3060 12G,之前一直觉得这卡就是个"1080P游戏卡+小模型玩具",直到最近花了两周时间反复折腾,才把这套组合真正跑通。这篇文章就把整个思路、参数、踩过的坑全部摊开讲,给同样只有一张12G卡、又想玩大模型长上下文的朋友一个可复现的参考。

先说清楚这套配置面向谁:一是手上有12G显存消费级显卡(3060 12G、4060Ti 16G砍一刀、2080Ti 22G改显存等)的个人开发者;二是想本地跑长文档分析、代码库问答、长对话Agent的爱好者;三是想理解"显存、上下文、吞吐"三者关系的学生和转行者。如果你手上是24G以上的卡,这篇文章的很多技巧你未必需要,但里面关于MTP、KV Cache量化、投机解码的思路依然有参考价值。

核心矛盾其实就一句话:模型权重、KV Cache、激活值三样东西都要抢显存,而12G是硬上限。27B模型如果按FP16存,光权重就要54G,直接出局;就算量化到4bit,权重也要13.5G左右,还是超。所以第一道坎就是量化,第二道坎是KV Cache在128K上下文下的爆炸式增长,第三道坎才是decode速度。很多人卡在第一步就放弃了,其实这三步是可以用一套组合拳打通的,关键是要理解每一步在省什么、牺牲什么。

我先把最终跑通的配置亮出来,后面再逐层拆解为什么这么选:

项目配置
显卡RTX 3060 12G
模型27B级别,4bit量化(AWQ/GPTQ)
权重显存占用约13-14G(部分层offload到内存)
KV Cache8bit量化 + PagedAttention
上下文128K(需配合RoPE缩放)
推理框架vLLM(支持MTP与投机解码)
decode速度50+ tokens/s(短上下文),长上下文下30-45

注意这里有个关键点:12G显存是装不下完整27B 4bit权重的,所以必须做部分层offload,或者用更激进的量化。我实测下来,纯GPU跑27B 4bit在12G上会OOM,必须让一部分层走CPU。这就引出了后面要讲的"分层卸载"策略。

2. 显存账本:把每一MB都算清楚

2.1 权重、KV Cache、激活值三笔账

很多人跑模型失败,是因为从来没算过显存账。我习惯在动手前先列一张表,把三块占用估出来,心里有数才不会瞎试。

权重占用的估算公式很简单:参数量 × 每参数字节数。27B模型在不同精度下:

  • FP16:27B × 2字节 = 54GB
  • INT8:27B × 1字节 = 27GB
  • INT4:27B × 0.5字节 = 13.5GB
  • 混合量化(部分层INT4部分INT8):约15-18GB

可以看到,即便INT4,13.5G也已经超过12G了。这就是为什么必须offload。但offload不是随便offload,offload哪些层、offload多少,直接决定速度。

KV Cache占用是长上下文的真正杀手。公式是:

KV Cache = 2 × 层数 × 序列长度 × 隐藏维度 × 精度字节数

以27B模型(假设64层,隐藏维度5120)为例,128K上下文,FP16精度:

2 × 64 × 131072 × 5120 × 2字节 ≈ 171GB

这个数字是灾难性的。所以128K上下文下,KV Cache必须量化。降到INT8,减半到85GB;降到INT4,约43GB。还是远超12G。那怎么办?答案是PagedAttention + 分页管理 + 只保留活跃序列的KV。实际推理时,不是所有128K都同时驻留显存,而是按需分页加载。这也是vLLM这类框架的核心价值。

激活值相对小,但在长上下文下也不可忽略,通常几百MB到1-2G。这部分通过flash attention和算子融合可以压到很低。

2.2 12G显存的分配策略

算完账就知道,12G要同时装权重和KV Cache是不可能的。我的分配策略是:

  • 权重:约9-10G驻留GPU(约70%的层),剩余层放CPU内存
  • KV Cache:约1.5-2G驻留GPU,用INT8量化
  • 激活值+框架开销:约0.5-1G

这样刚好卡在12G边缘。这里的关键是层卸载(layer offload):把模型的前若干层或后若干层放到CPU,GPU只算一部分。因为transformer是逐层串行的,CPU算的层会成为瓶颈,所以卸载的层越少越好,且最好卸载计算量小的层。

提示:卸载层时优先考虑卸载靠近输入的层还是靠近输出的层,实测差异不大,但卸载embedding层通常收益最高,因为它不参与attention计算。

2.3 量化方案的选择逻辑

量化不是越激进越好。INT4虽然省显存,但精度损失在长上下文下会被放大,尤其是需要精确检索的任务。我对比过几种方案:

量化方案显存精度损失适用场景
FP1654G无24G以上显卡
INT827G极小32G以上
GPTQ INT413.5G小12-16G
AWQ INT413.5G更小12-16G
GGUF Q4_K_M约15G小CPU+GPU混合

我最终选的是AWQ INT4,因为它在长上下文下的表现比GPTQ略稳,尤其是128K这种极端长度。GGUF虽然灵活,但在vLLM下的吞吐不如AWQ。

3. 128K上下文是怎么塞进去的

3.1 RoPE缩放:让模型"看得更远"

27B模型原生上下文通常只有4K或8K,要跑到128K,必须做位置编码外推。主流方法是RoPE缩放,常见的有线性插值、NTK-aware、YaRN等。我实测下来,YaRN在128K下表现最稳,困惑度上升最少。

配置上,需要在推理框架里指定RoPE scaling参数。以vLLM为例,大致是这样:

--rope-scaling '{"type":"yarn","factor":4.0,"original_max_position_embeddings":32768}'

这里的factor是缩放因子,128K / 32K = 4。注意factor不是随便设的,要和原始训练长度匹配。如果原模型是8K训练,factor就要设16。

注意:RoPE缩放会带来一定的精度损失,尤其是超过训练长度4倍以上时。128K已经是很多模型的极限,再往上就要考虑其他外推方法。

3.2 KV Cache量化与分页

128K上下文下,KV Cache是显存大户。我用的是INT8 KV Cache量化,配合vLLM的PagedAttention。PagedAttention的核心思想是把KV Cache切成固定大小的block,像操作系统管理内存页一样按需分配,避免预分配整个128K的空间。

开启方式:

--kv-cache-dtype fp8 --block-size 16

block-size的选择有讲究:太小则管理开销大,太大则碎片多。16是实测比较均衡的值。fp8量化在Hopper架构上原生支持,30系卡上会退化成INT8模拟,但依然能省一半显存。

3.3 长上下文下的显存动态管理

即便做了量化和分页,128K满载时显存依然紧张。我的做法是限制并发序列数。单序列128K和4序列各32K,显存占用差不多,但后者吞吐更高。所以如果是多用户场景,宁可限制单序列长度,也不要让一个序列吃满128K。

另外,vLLM支持--gpu-memory-utilization参数,我一般设0.92-0.95,留一点余量给框架和CUDA上下文。设太高容易OOM,设太低浪费显存。

4. decode 50+是怎么做到的

4.1 MTP:一次预测多个token

decode速度的瓶颈在于自回归生成是串行的,每步只出一个token。MTP(Multi-Token Prediction)的思路是让模型一次预测多个token,然后并行验证。这本质上是一种投机解码(speculative decoding)的变体。

MTP在vLLM里的配置:

--speculative-model <draft-model> --num-speculative-tokens 5

draft model可以是一个小模型(比如1B级别),用它快速生成5个候选token,然后27B主模型并行验证。如果5个里对了3个,就相当于一步出了3个token,速度提升接近3倍。

我实测下来,在短上下文(4K以内)下,MTP能把decode从20 tokens/s提到55-60 tokens/s。但长上下文下,验证成本上升,提升会缩水到1.5-2倍。

4.2 批处理与连续批处理

单序列decode慢,但多个序列可以并行。vLLM的连续批处理(continuous batching)能把多个请求拼成一个batch,GPU利用率大幅提升。这也是为什么多用户场景下,单卡也能跑出不错的吞吐。

不过批处理会吃显存,128K上下文下batch size通常只能设1-2。所以长上下文和高吞吐是矛盾的,要按场景取舍。

4.3 算子优化与CUDA Graph

vLLM默认开启CUDA Graph,能减少kernel launch开销。对于decode这种小算子密集的场景,CUDA Graph收益明显。另外,flash attention和PagedAttention的融合算子也能省不少时间。

我对比过开不开CUDA Graph的差异,短上下文下能差10-15%的吞吐。所以这个开关一定要开。

5. 完整实操流程:从零到跑通

5.1 环境准备与依赖安装

先装好CUDA驱动和PyTorch。3060是Ampere架构,算力8.6,支持FP16和INT8。建议CUDA 12.1以上,PyTorch 2.1以上。

pip install vllm==0.4.2 pip install autoawq pip install transformers

vLLM版本很关键,0.4.x对MTP和KV Cache量化的支持比较完善。太老的版本不支持这些特性。

5.2 模型下载与量化

我选的是一个27B级别的开源模型,用AWQ量化到INT4。量化过程可以用autoawq:

from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path = "your-27b-model" quant_path = "your-27b-awq" model = AutoAWQForCausalLM.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"} model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_path)

q_group_size设128是精度和显存的平衡点,设64精度更高但显存略增。

5.3 启动参数配置

这是最关键的一步。我的启动命令大致如下:

python -m vllm.entrypoints.openai.api_server \ --model your-27b-awq \ --quantization awq \ --dtype float16 \ --max-model-len 131072 \ --rope-scaling '{"type":"yarn","factor":4.0,"original_max_position_embeddings":32768}' \ --kv-cache-dtype fp8 \ --block-size 16 \ --gpu-memory-utilization 0.93 \ --max-num-seqs 2 \ --enable-chunked-prefill \ --speculative-model your-draft-model \ --num-speculative-tokens 5 \ --tensor-parallel-size 1

逐个参数解释:

  • max-model-len 131072:128K上下文
  • rope-scaling:YaRN外推
  • kv-cache-dtype fp8:KV Cache量化
  • block-size 16:分页大小
  • gpu-memory-utilization 0.93:留7%余量
  • max-num-seqs 2:限制并发,长上下文下只能开2
  • enable-chunked-prefill:分块预填充,长prompt不卡死
  • speculative-model:投机解码的draft模型

5.4 分层卸载的补充配置

如果12G还是不够,就要开CPU offload。vLLM本身对offload支持有限,可以考虑用accelerate或自己写层调度。我的做法是把embedding层和最后几层放CPU:

# 伪代码示意 for i, layer in enumerate(model.layers): if i < 4 or i > num_layers - 4: layer.to("cpu") else: layer.to("cuda")

这样能省出1-2G显存,代价是每步多几十毫秒的CPU-GPU传输。实测decode速度会从55降到45左右,但至少能跑起来。

6. 常见问题与排查实录

6.1 OOM排查速查表

现象原因解决
启动即OOM权重超显存开offload或换更激进量化
长prompt OOMKV Cache爆开KV量化+chunked prefill
多请求OOM并发太高降max-num-seqs
运行中OOM碎片降gpu-memory-utilization

6.2 decode速度上不去的排查

如果decode只有10-20 tokens/s,先看是不是没开MTP。其次看是不是offload太多层,CPU成为瓶颈。再检查CUDA Graph有没有开。最后看是不是batch size太大导致显存压力大,反而降速。

我踩过的一个坑是:draft model选得太大,验证成本高,反而比不用MTP还慢。draft model最好比主模型小一个数量级,1B左右比较合适。

6.3 长上下文精度下降

128K下模型容易"忘记"中间的内容,这是位置外推的通病。缓解方法:一是用YaRN而不是线性插值;二是在prompt里把关键信息放开头和结尾;三是适当降低temperature,减少幻觉。

提示:长上下文任务里,检索类任务(大海捞针)比生成类任务对位置编码更敏感,测试时优先用检索任务验证。

7. 一些实测数据与个人体会

我把关键指标整理成表,方便对比:

场景上下文decode速度显存占用
短对话4K55 tokens/s11.2G
中长文档32K42 tokens/s11.6G
长文档128K32 tokens/s11.9G
双并发2×32K合计60 tokens/s11.8G

可以看到,128K满载时decode会掉到32左右,达不到50+。50+是在短上下文下测的。所以标题里的"decode 50+"和"128K"其实是两个场景,不能同时满足。这点必须说清楚,不然容易误导。

我个人在实际操作中的体会是:12G跑27B,本质是在显存、速度、精度三者之间做动态平衡。没有银弹,只有取舍。MTP和KV量化是两个最有效的杠杆,offload是最后的保底手段。如果你追求极致速度,就牺牲上下文;如果追求长上下文,就接受速度下降。

最后分享一个小技巧:测试时先用小上下文跑通,再逐步加大max-model-len,每次加一倍,观察显存和速度变化,找到你显卡的"甜点"长度。我的3060甜点在32K左右,再往上收益递减明显。这个内容后续还可以往多卡方向扩展,比如两张3060用tensor parallel跑,显存翻倍后就能全GPU跑27B,速度会有质的提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询