☰
单卡部署70B大模型:量化、显存与KV Cache全解析
2026/10/26 8:03:50 网站建设 项目流程

前几天一个朋友拎着一张 24GB 显存的显卡来找我,开门见山:想跑 70B 模型。我第一反应是先问他,“你准备用哪个量化方案?”因为在单卡环境下,70B 模型的 FP16 原始权重就要超过 140GB,不量化根本连起点都摸不到。而量化之后又会牵扯出一连串推理优化问题:上下文窗口能开多大、批量能塞多少、KV cache 会不会爆、用 llama.cpp 还是 vLLM。这几层问题叠在一起,其实不是“跑一个模型”,而是一整套五层技术栈的选择题。

这篇内容就是针对“让 70B 跑在单卡”这件事的完整拆解,从显存预算、量化方案、模型格式、推理引擎,一直到 KV cache 和运行参数,每一层我都把原理和实操放到一起讲。适合想在单张显卡上部署大模型的工程师、做私有化推理的同学,也适合手里只有一张 4090 或 48GB 显卡却想折腾大参数模型的玩家。看完你至少能回答三个问题:为什么非要量化、量化到多少合适、跑起来之后哪些参数最容易让显存爆炸。

1. 先算一笔显存账:70B 为什么天然排斥单卡

1.1 光权重就已经 140GB,其它开销还没算

模型占用显存的第一大头永远是权重本身。70B 参数指的是参数量,不是字节数。显存占用取决于每个参数用多少位来存:

FP32 是 4 字节,所以 70B 参数就要 70B × 4B = 280GB;FP16/BF16 是 2 字节,要 140GB;INT8 是 1 字节,要 70GB;INT4 是 0.5 字节,理论上只要 35GB。

单卡环境里常见的显存是 24GB(4090、3090 级别)和 48GB(A6000、L40S 级别),前者连 INT8 权重都装不下,后者也只能勉强容纳 INT4 级别的权重。所以“让 70B 跑在单卡”这句话的潜台词,其实就是“把权重压到 INT4 或等效精度,再把其它开销压缩到极致”。

但这只是权重账。真正跑推理的时候,还有三块开销:KV cache(注意力缓存的中间结果)、激活值(前向计算图的中间张量)、以及 CUDA context 和框架本身的基本占用。后两项加起来随架构不同能吃掉 2GB 到 8GB 不等,有时候你明明算好权重只要 40GB,实际一加载就报 OOM,就是因为忘了给这些“隐形住户”留位置。

1.2 单卡不是没显存,是没有“临时空间”

把 70B 压缩进单卡,很多人第一反应是“量化到 INT4 就完事了”。实际上权重塞进去之后,真正限制你的是临时空间。

推理和训练不一样,训练是一次性把整个批次灌进显存,推理是逐 token 生成。每生成一个 token,注意力层的 K 和 V 缓存都会累计增加。也就是说,输入越长、对话轮次越多、批量越大,KV cache 就越大。权重是固定成本,KV cache 是随时间增长的可变成本。单卡环境里,这两者在争夺同一块显存。

所以“能装下”和“跑得动”是两回事。你用 INT4 把权重压到 38GB 左右,看着 48GB 显卡剩余 10GB,似乎挺宽裕,可一旦上下文设到 16K、批量推到 4,KV cache 直接吃掉 5GB 以上,再叠加激活值和 CUDA context,OOM 就像定时炸弹。这也是后面每一节都要围绕“显存总预算”来设计的原因。

1.3 五层技术栈的整体划分

我把单卡跑 70B 这件事拆成五层,每层解决一类独立问题:

层级解决的核心问题常用手段对应章节
第 1 层:权重精度让权重体积从 140GB 降到可接受范围FP16、BF16、INT8、INT4、FP8第 2 节
第 2 层:量化方法在压体积的同时尽量保住质量GPTQ、AWQ、GGUF Q4、校准集第 2 节
第 3 层:模型格式与转换让模型能被指定推理框架读取HuggingFace 权重、GGUF、ONNX第 3 节
第 4 层:推理引擎与内存调度分配显存、管理 KV cache、控制批处理llama.cpp、vLLM、PagedAttention第 4 节
第 5 层:运行参数调优在显存预算内平衡质量、速度和并发context length、batch、gpu offload第 5 节

这五层是层层依赖的关系:权重精度决定你剩下多少显存给 KV cache;量化方法决定压缩后的模型质量;格式必须匹配框架;框架的调度方式决定并发能力;而最后一层参数设置,则是前面所有选择的最终校验。

很多人失败的原因就是把精力全放在第一层,权重一量化就着急跑,结果在第四、第五层疯狂撞墙。

2. 量化选型:FP8、INT8、INT4 与三种主流方法

2.1 位宽不是越低越好,得看精度损失曲线

先列一张我常用的位宽对照表:

精度权重占用(70B)相对 FP16 的压缩率主要问题
FP16/BF16140GB1x单卡放不下
FP870GB2x受硬件和框架支持限制,消费卡收益一般
INT870GB2x可以跑,但精度损失比 FP8 略明显,需校准
INT4约 35-40GB约 3.5x压缩率高,但敏感层容易崩质量

FP8 是最容易被误会的一档。它在 H100 这类原生支持 FP8 的卡上有收益,但在消费级显卡上经常是“名义上支持、实际跑得慢”,甚至框架里根本没有优化;要让 70B 跑到单卡,FP8 的 70GB 权重体积已经超出了 48GB 的承受范围,所以真正的主力是 INT8 和 INT4。

INT8 适合 48GB 显卡跑一些对质量敏感但又想保住体面的场景,权重 70GB 对 48GB 还是放不下,因此单卡 70B 几乎唯一现实的路线就是 INT4 系列。需要注意,INT4 不是把每个参数机械地砍掉一半,而是用少量高位宽参数去补偿低比特量化带来的误差。这个“补偿”体现在 group size、scale 和 zero point 上,直接决定了实际文件会比刚才的“35GB理论值”多出几 GB。

2.2 GPTQ、AWQ 和 GGUF 的 Q4:三套量化方法该怎么选

常见的 INT4 量化方法有三种,我简单说说它们的设计思路和适用场景。

GPTQ 是一套基于 Hessian 矩阵的逐层量化方法。它会在量化每一层时,用一小批校准数据来估计权重的重要程度,把量化误差分配到不那么重要的参数上。优点是量化速度快、社区生态成熟,vLLM 和 HuggingFace 的集成度都高;缺点是如果你后续跑的任务类型和校准数据差异很大,质量会略受影响。

AWQ 的思路不一样,它不看权重而看激活值。AWQ 认为某些通道的激活值波动大,这些通道对应的权重应该保留更高的精度。它不需要反向传播,用激活值统计信息就能完成量化。我实测下来,AWQ 在长文本任务里往往比同精度的 GPTQ 更稳,特别是代码生成和数学题这种对细节敏感的场景。

GGUF 里的 Q4 又不太一样。它是 llama.cpp 体系下的量化格式,Q4_K_M 这类名字里的“K”代表 k-quants 混合量化。简单说,它会根据每个张量内部的结构分配不同位宽,注意力层的部分张量用 6bit,FFN 的某些部分用 4bit,这样整体文件还是约 4bit 的量级,但关键部分精度更高。对比起来:

方法配套框架量化依据适合场景
GPTQvLLM、Transformers权重重要性 + 校准集服务化部署、离线批量推理
AWQvLLM、部分服务框架激活值统计长文本、代码、高精度要求
GGUF Q4llama.cpp混合比特分配本地部署、边缘端、单机调试

一句话总结:你想用 vLLM 做并发服务,优先 GPTQ 或 AWQ;你想在单机上用最简单可靠的方式跑通,GGUF 加 llama.cpp 最省心。

2.3 量化参数怎么设:group size、校准数据和 KV cache 量化

选了方法之后还有一组参数要定,其中最容易踩坑的是 group size。

group size 是指多少个权重参数共享一个量化缩放因子。常见值是 128,想做更精细的量化可以降到 32。数值越小,量化颗粒度越细,精度越好,但存储开销越大,速度也可能变慢,因为要读更多 scale 信息。对 70B 模型来说,group size 从 128 降到 32,文件体积可能从 38GB 跳到 41GB 以上,这多出来的 3GB 很可能直接挤爆你的显存预算。

校准数据也值得认真对待。它通常是一批文本,用来统计权重或激活的数值分布。这里有一个很反直觉的点:校准数据不需要很多,但一定要贴近你的真实任务。比如你要做的是代码补全,却用长篇新闻文本做校准,那么生成代码时精度就可能不如预期。我习惯从目标任务的真实数据里随机抽一二百条样本,截成 512 或 1024 token 的片段用作校准集,不贪多。

还有不少人主动把 KV cache 也量化掉。llama.cpp 里有专门的 KV cache 量化选项,vLLM 也支持 kv cache 的 FP8/INT8 存储。这件事有利有弊:它能立刻省出接近一半的缓存显存,但代价是长上下文下的质量下降。我的建议是,只有在上下文长度或并发量确实需要那部分显存时才开 KV cache 量化,并且先在目标长度下做一轮质量对比,不要默认开启。

3. 实操链路:从原始权重到 GGUF,再到 llama.cpp / vLLM

3.1 一条能跑的链路:先用 GGUF 打通,再考虑框架

实战里最容易让人心态崩的时刻,不是模型太大,而是“所有安装都正确,但就是跑不出结果”。所以我推荐的第一条链路是 GGUF + llama.cpp,原因是它有最完整的单机工具链,从权重转换到量化再到运行,每一步都有独立命令,出了问题也好定位。

第一步,先把模型权重格式统一。如果你手头是 HuggingFace 格式的模型,可以用 llama.cpp 仓库里的转换脚本导出 GGUF:

python convert_hf_to_gguf.py 你的模型目录 \ --outfile 模型名-f16.gguf \ --outtype f16

转换时注意不要跳过参数核查,特别是 hidden_size、head 数这些,脚本本身能大部分自动识别,但你至少要确认输出日志里的维度和你模型架构对得上,不然量化出来的文件跑起来会全是乱码。

第二步,量化。llama.cpp 自带的 quantize 工具会把 F16 的 GGUF 压成 Q4_K_M 或其它比特档位:

./llama-quantize 模型名-f16.gguf 模型名-q4_k_m.gguf q4_k_m

这里 q4_k_m 后面还能跟 group size 参数,比如 q4_k_m 的变体,但先用默认即可。

第三步,验证。量化完先别急着上服务,跑一次文本生成,观察输出质量:

./llama-cli -m 模型名-q4_k_m.gguf \ -c 4096 \ -ngl 999 \ -p "写一段关于单卡推理的代码建议"

如果输出通顺、没有大量重复字符或乱码,说明链路是通的;如果输出明显劣化,先回查校准集或 group size,而不是怀疑显卡。我见过很多新手一看到输出差就认为是量化方法的问题,其实是命令行里少了关键的重复惩罚参数。

3.2 llama.cpp 运行参数:ngl、ctx、batch、mlock 的作用

跑通之后,真正决定体验的是几个运行参数。

  • -ngl控制有多少层放到 GPU。设为 999 表示“尽量全放 GPU”,它会自动把能放下的层都放进去;如果显存不够,剩余层会回退到 CPU。24GB 显卡跑 40GB 的 Q4 权重时,-ngl的值决定了你会看到多少 offload 警告。
  • -c是上下文长度,也就是 KV cache 的基础大小。默认可能比较小,但当你把 70B 模型真正用于长对话时,这里必须按第 4 节的计算公式来设。
  • -b是 batch size,也就是单次前向传播处理多少个 token。这个参数调大可以提升吞吐,但会显著增加激活值显存。单卡上我一般从 256 开始试,OOM 就往 128 降。
  • --mlock是锁内存,防止进程的内存被换出到 swap。这个参数在不 offload 的情况下影响不大,但只要开了 CPU offload,它能让速度稳定很多。

命令行参数在不同版本可能改名,比如有些版本用llama-cli,有些老版本还用main。如果你查阅的资料和你安装的版本输出对不上,先用--help看当前版本实际支持的参数名,不要照抄过时教程。

3.3 vLLM 跑量化模型:离线批处理入口

如果你要做的是并发推理服务,或想在同一张卡上服务多个请求,llama.cpp 的串行方式就不太够用了。vLLM 是更好选择,它对量化模型有原生支持,入口代码很简洁:

from vllm import LLM, SamplingParams llm = LLM( model="/path/to/awq-or-gptq-weights", quantization="awq", # 可选 "awq" 或 "gptq" dtype="float16", gpu_memory_utilization=0.85, max_model_len=4096, enforce_eager=True, ) params = SamplingParams( max_tokens=512, temperature=0.6, top_p=0.9, ) outputs = llm.generate( ["写一篇关于单卡推理优化的短文"], params, ) print(outputs[0].outputs[0].text)

这里gpu_memory_utilization=0.85的含义是为模型和 KV cache 预留最多 85% 显存,剩下的留给 CUDA context 和激活值。max_model_len是一把双刃剑,设得越大能处理的长文本越多,但 KV cache 预留就越大。很多人在 vLLM 上遇到 OOM,首先不是量化问题,而是max_model_len默认或误设得太大。

第一次跑 vLLM 时建议用enforce_eager=True,它会禁用 CUDA graph 优化,虽然吞吐略低,但好处是显存占用更可预测、更容易定位问题。等确认稳定后再关掉它,把速度榨出来。

4. 显存管理的隐形大头:KV cache、上下文窗口与批处理

4.1 一条 KV cache 公式,让你不再靠直觉设置参数

我见过太多人把“上下文长度”当成灵感参数,随便填一个,然后看着 OOM 一脸无辜。KV cache 的计算有明确公式,先记住这个:

KV cache 大小 = 2 × 层数 × KV头数 × 头维度 × 上下文长度 × 批量大小 × 每值字节数

乘以 2 是因为 Key 和 Value 各一份。以一个 70B 级别的模型为例,通常有 80 层、8 个 KV 头、每个头 128 维,如果上下文长度 4096、批量 1、FP16 存储,那么:

2 × 80 × 8 × 128 × 4096 × 1 × 2 字节 ≈ 1.34GB

单个请求就吃掉 1.34GB,看起来还好。但把这四个变量放大就不一样了:上下文长度翻到 8192,KV cache 变 2.68GB;批量从 1 变成 8,又是 8 倍增长。在 48GB 卡上,Q4 权重约 40GB,剩下 8GB,如果 KV cache 已经吃了 5GB,激活值再占 2GB,剩下的空间就非常紧张。这就是为什么“70B 量化后能装下,但一开长上下文就崩”。

4.2 上下文与批处理:性能提升的另一面是内存翻倍

很多人调参数时总想让吞吐更高,于是把批处理大小调大,但忽略了批量对内存的影响。在 Transformers 这类非连续批处理实现里,批量从 1 升到 4,KV cache 直接乘以 4;如果你做的是动态批处理,峰值内存会陡然上升,然后掉下来再上升,很容易在某个瞬间触顶。

vLLM 引入的 Continuous Batching(连续批处理)比传统方案聪明得多:每个序列只要没结束就继续占用 KV cache,结束时立刻释放,不需要等整批结束。这让吞吐明显提升,但总 KV cache 依然是所有并发请求之和。单卡部署 70B 时,我一般会把并发上限压到很低,很多时候“跑得稳”比“跑得多”更值得优先考虑,毕竟单个用户一次对话就可能在长上下文下占用数 GB 缓存。

4.3 PagedAttention、offload 与显存碎片

vLLM 的高吞吐还得益于 PagedAttention。它的思路和操作系统虚拟内存很像:把 KV cache 划分成固定大小的块,按需分配,而不是给每个请求预留一整块连续空间。这让显存利用率高了不少,也减少了碎片问题。对于单卡 70B 场景,这一点特别有用,因为本来可用显存就紧巴巴的,碎片再多一点就彻底跑不动了。

如果 24GB 卡连 INT4 权重都放不满,就必须面对 offload 策略。offload 是把部分层放到 CPU 内存,GPU 只放核心层。llama.cpp 的-ngl设成某个层数就是在做这件事。CPU 和 GPU 之间反复搬数据的代价是速度大幅下降,一个 70B 模型可能出现几分钟才生成一个 token 的情况。所以如果你的目标是要可用,尽量选 48GB 以上显存,或者选更小尺寸的模型;硬要在 24GB 上跑 70B,更多是“证明能跑”,而不是“实际能用”。

有人会问,张量并行能不能解决单卡放不下的问题?可以,但标题限制是“单卡”,张量并行需要多卡通信,不在讨论范围。真正单卡场景能做的是把权重量化、把 KV cache 精打细算、把不需要的层 offload,这三板斧用完还是不行,就该考虑换硬件或换模型,而不是继续迷信“优化到极致就能塞下”。

5. 单卡推理的实战排坑:从 OOM 到质量劣化的一次性检查

5.1 开跑前的自查清单

每次在单卡上部署大模型前,我都会花两分钟过一遍清单,这比反复试错高效得多:

  • 权重显存是否在预算内:INT4 的 70B 通常 38GB 到 42GB,不要只看理论值,要按实际文件大小算。
  • KV cache 是否按公式算过:根据目标上下文长度、批量大小和存储精度,算出精确占用。
  • CUDA context 和激活值是否有预留:至少留 2GB,vLLM 场景要留 10% 到 15%。
  • 校准集是否贴近真实任务:如果不是,量化模型的输出质量可能无法接受。
  • 框架版本是否匹配:llama.cpp、vLLM 的更新速度极快,量化格式和参数都有变动,老版本可能读不出新格式。

5.2 高频故障速查表

症状最常见原因解决方向
启动即 OOM权重 + KV cache 超过显存减小上下文长度或批量,开 KV cache 量化
运行一段后 OOMKV cache 随序列增长到峰值降低 max_model_len,减少并发请求数
生成速度极慢大量层被 offload 到 CPU提高 -ngl / 减少 offload,或换更大显存卡
输出明显变差量化过度或校准集不匹配换 group size=32,换 AWQ,或重做校准
长文本后输出乱码位置编码缓存或 KV cache 溢出检查上下文长度设置,检查重复惩罚参数
多请求时吞吐骤降批处理过大、碎片过多开 PagedAttention(vLLM),限制最大并发

这里说一个我踩过很多次的坑:有时不是模型不行,而是把真实任务里的文本喂进去后,上下文长度触到了显存天花板,于是系统把旧 token 截断,但注意力机制并不清楚该截断哪些位置,结果就是输出质量和逻辑突然崩坏。排查时先用一个短 prompt 验证模型本身,再慢慢加长输入,就能区分是量化质量问题还是上下文截断问题。

5.3 稳定运行的实测配置参考

最后给两组我实际用过的配置,作为大家调试的起点。

48GB 单卡,例如 A6000 / L40S,跑 70B Q4_K_M 权重,llama.cpp 里我会用:

-ngl 999 -c 8192 -b 512 --mlock

这个配置能保证权重全量进 GPU,8K 上下文对大多数问答和文档摘要都够用;如果后续要上 vLLM,我会把gpu_memory_utilization设为 0.9,max_model_len设为 8192,并设置单请求并发数不超过 4。

24GB 单卡,例如 4090,只适合“体验型”部署,llama.cpp 中用:

-ngl 80 -c 4096 -b 256 --mlock

这里的-ngl 80不是固定值,而是根据 38GB 权重和 24GB 显存算出来的一个“能塞多少塞多少”的量,实际层数以日志为准。注意这种配置下,生成速度会明显低于 GPU 全量加载,所以它更适合验证推理流程,不适合对外提供稳定服务。

我自己折腾单卡 70B 的过程里,最大的感受是:优化顺序比优化技巧更重要。先把显存预算按公式算清楚,再决定量化精度、上下文长度和批量大小,最后才去调框架参数;反过来先堆量化再乱调上下文,问题会像海浪一样一波接一波。现在每次跑大模型,我第一件事永远是打开一个计算表格,把参数填进去,而不是先敲运行命令,这会省掉一大半排错时间。

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

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

立即咨询