8GB 显存到底能不能正经跑开源大模型?以前我的答案是"能跑,但是憋屈",直到我把 vLLM 这套推理框架在 8GB 显存上完整部署、并发压测、再做一轮显存调优跑通之后,发现之前的判断错了一半。问题根本不在于显存小,而在于你有没有把显存的每一块都用到刀刃上。这篇文章把我从零到一的操作过程完整复盘一遍,包括模型选型和量化方案、部署命令的每个参数为什么这么设、并发压测脚本怎么写、结果怎么解读,以及最后那几板斧是怎么把显存榨干的。适合手里只有一张 8GB 显卡、又想让本地大模型服务扛住并发请求的开发者和自托管爱好者。
1. 8GB显存不是跑不动,是以前的推理方式太浪费
1.1 传统推理方案为什么在8GB上寸步难行
如果不引入任何优化框架,直接用常见的深度学习推理流程加载一个 7B 参数规模的大模型,光是模型权重就要占掉一大片显存。以 FP16 精度计算,每个参数占 2 字节,7B 就是大约 14GB,这已经超过了 8GB 显卡的全部显存。所以很多人第一反应是"8GB 卡直接告别大模型",这个结论在传统推理路径下基本成立。
更麻烦的还在后面。传统方案在服务端推理时,显存分配是静态的、整块的。每次请求进来,系统要为它单独留出一块完整的输入输出缓冲区,请求结束才释放。这种设计在小模型低并发时没毛病,但大模型的每个请求都要维护独立的 KV cache(键值缓存),显存碎片化非常严重。8GB 显存本来就紧,一个请求占 1GB,碎片再吃掉几百 MB,并发稍微一上来就 OOM。
还有一个容易忽略的点:传统批处理(static batching)会把一个 batch 里所有请求绑到同一批结束。如果其中一个请求序列特别长,其他已经完成的请求也得干等着,GPU 算力在大量空转。这批请求不结束,显存就一直被占着,整体吞吐和显存利用率双双拉胯。
1.2 vLLM 到底优化了什么
vLLM 能在这类消费级显卡上翻身,靠的核心是两件事:PagedAttention 和 Continuous Batching。
PagedAttention 的思路很像操作系统里的虚拟内存分页。它把 KV cache 切成固定大小的物理块,不需要为每个请求预留一整块连续空间,而是按需分配、动态映射。请求之间甚至可以共享同一个物理块。8GB 显存上这招太关键了——显存碎片被消掉,KV cache 利用率大幅提高,同样的物理显存能容纳更长的上下文和更多并发请求。
Continuous Batching(连续批处理)则是把"一批请求必须一起结束"这个限制拆掉。一个请求如果已经生成了完整的回复,它的 GPU 计算插槽立刻释放,新请求马上补进来。每个 step 都在动态调整 batch 组成,GPU 几乎不会有空闲等待。这套机制叠加下来,吞吐量比传统方案高出数倍,显存占用却更少。
1.3 8GB 显存的真实预算
一张 8GB 显存的显卡,扣除驱动、桌面环境、CUDA 上下文这些固定开销,vLLM 实际能支配的显存大约就是 7.5GB 左右。这 7.5GB 要同时装下三样东西:
- 模型权重
- 激活值(计算过程中的中间张量)
- KV cache(推理过程中不断累积的键值缓存)
理论上这块卡能接受的模型规模大致是这样的:一个 3B 左右的小模型,FP16 权重约 6GB,加少量 KV cache,可以跑但余量不大;一个 7B 模型,FP16 权重约 14GB,完全没戏;如果换成 INT4 量化,权重压到 4GB 左右,剩下的 3GB+ 都留给 KV cache 和激活值,就比较从容了。所以我在这篇文章里锁定的路线很明确:8GB 显卡 + 7B 级量化模型,或者 3B 级 FP16 模型。
2. 动手之前的显存账本与量化选择
2.1 先算一笔显存账,再决定跑哪个模型
部署之前我会先做一次笔算,别急着敲命令。给定一个模型,显存需求主要来自权重、激活和 KV cache。权重部分最好估,参数量乘精度字节数就行。比如 7B 模型,FP16 是 7×2=14GB,INT8 是 7×1=7GB,INT4 大约 7×0.5=3.5GB。
KV cache 的估算稍微复杂一点,但它决定了你能支持多长的上下文和多大的并发。记忆公式大概是这样的:
KV cache 大小 ≈ 2(K 和 V 两份)× 层数 × KV 头数 × 每头维度 × 序列长度 × batch 大小 × 每个元素字节数
比如一个 32 层、KV 头数 8、每头维度 128 的模型,在 FP16 下每个 token 产生的 KV cache 大约是:
2 × 32 × 8 × 128 × 2 = 131072 字节 ≈ 128KB
看着不多,但 4096 个 token 的上下文、4 个并发请求,就是 128KB × 4096 × 4 ≈ 2GB。这就是为什么同样一张卡,上下文长度开大之后 KV cache 会迅速反超权重成为显存大头。
激活值的估算最不稳定,它跟 batch size、序列长度和具体算子实现都有关。这也是为什么我不建议在 8GB 卡上把 batch 设置得太激进,vLLM 会自动控制并发,但如果你同时把并发拉满、上下文拉满,激活值会让显存瞬间爆掉。
2.2 量化路线的取舍:INT4 和 INT8 怎么选
8GB 显存跑 7B 模型,量化是绕不开的。我在实际操作中把 INT8 和 INT4 都试了,结论是看你的优先级。
INT8 量化更保守。7B 模型权重大约 7GB,推理质量损失很小,很多场景下跟 FP16 几乎看不出差别。但在 8GB 卡上,7GB 权重 + 上下文 + 显存开销,KV cache 只剩一点点,稍微开几个并发请求就会碰到显存天花板。所以 INT8 在 8GB 卡上适合短上下文、低并发的场景,跑起来很稳,但别指望能同时支撑大量会话。
INT4 量化则是 8GB 卡的常规答案。权重降到 4GB 左右,留出 3GB+ 的空间给 KV cache,7B 模型才能在 8GB 卡上真正用起来,能承受的上下文长度和并发数也才像样子。代价是质量损失比 INT8 明显一些,复杂推理任务上偶尔会出现逻辑跳跃,另外 INT4 在某些 GPU 上没有 INT8 的加速优化,生成速度可能略慢。
我的个人偏好是:如果 3B 模型能覆盖业务需求,优先用 FP16 的 3B,省心质量好;如果必须上 7B,那就 INT4,但最好用 AWQ 或 GPTQ 这类针对性的量化方式,而不是简单粗暴的低精度转换。对于绝大多数问答、总结、代码生成场景,INT4 的 7B 模型在 8GB 卡上是最均衡的选择。
2.3 我不建议在8GB卡上做的事
有几种组合我在实测里踩过,替各位划个红线:
- 不量化直接跑 7B 模型的 FP16:权重都装不下,加载阶段就会报显存不足。
- 7B 模型 + 超长上下文(比如 32K):即便 INT4,KV cache 需求也会迅速膨胀,最后服务能跑,但一条长请求进来就 OOM。
- 盲目调高 gpu-memory-utilization 到接近 1.0:vLLM 预分配显存时如果留的余量太少,一旦碰到短时显存尖峰,进程会直接崩溃,而且不是报错优雅退出,是整卡卡死。
3. 完整部署流程:一条命令背后的每一处取舍
3.1 环境准备:比想象中容易翻车的一步
我建议用独立的虚拟环境装 vLLM。我用的是 Python 3.10 左右的版本,新一些的 vLLM 版本在 Python 3.11、3.12 上也没问题,但太老的系统 Python 容易在编译依赖时踩坑。安装命令本身不复杂:
python -m venv vllm-env source vllm-env/bin/activate pip install vllm这一步看似简单,但有几个隐性要求。首先是显卡驱动不能太旧,vLLM 依赖新版 GPU 加速库,老驱动往往会导致内置算子无法编译。其次是建议直接用 vLLM 官方准备好的预编译轮子,而不是从源码编译,8GB 卡编译一次要等非常久,而且内存不足时还会被编译器干掉。如果你发现安装过程在编译某些自定义算子,第一反应应该是去把 CUDA 相关的编译工具链检查一遍,而不是干等。
也就是说,真正容易翻车的不是 vLLM 本身,而是它的依赖链:PyTorch 版本、GPU 加速库版本、驱动版本三者必须互相兼容。我在某次升级驱动之后,旧虚拟环境里的加速库直接失效,重新装了对应版本才恢复。建议部署前先跑一句python -c "import torch; print(torch.cuda.is_available())",确认环境链路是通的。
3.2 启动服务的命令逐参数解读
环境就绪后,加载模型用一条 vLLM 的 serve 命令就行。我实际使用的启动命令长这样:
vllm serve /data/models/7b-int4-model \ --quantization awq \ --max-model-len 4096 \ --gpu-memory-utilization 0.92 \ --enforce-eager \ --swap-space 4 \ --port 8000 \ --served-model-name local-7b这里每一个参数都不是随便写的,它们就是后面显存调优的核心旋钮。
第一个是--quantization awq,告诉 vLLM 用 AWQ 量化的方式加载模型。如果你的模型是 GPTQ 量化格式,就换成--quantization gptq。这个参数直接决定了权重在显存里的摆放方式,错了会显示格式不匹配。
然后是--max-model-len 4096。我为什么显式把上下文长度压到 4096?因为很多开源模型默认的 maximum context 是 8192 甚至更长,在 8GB 卡上如果按默认值来,KV cache 池会被预分配得巨大,还没开始推理显存就快满了。我宁可主动缩短上下文,换取更宽裕的 KV cache 池和并发能力。
--gpu-memory-utilization 0.92的意思是让 vLLM 预分配显卡 92% 的显存用来放权重和 KV cache,剩余 8% 留给激活值和突发开销。这个值我之前试过 0.98,结果在第一次并发请求时就炸了,所以 8GB 卡我建议在 0.90~0.95 之间取值,别贪。
--enforce-eager是很多人不知道的好东西。它关闭 vLLM 的 CUDA graph 优化,换来的是加载模型时的显存尖峰大幅降低。代价是每个请求多一点点调度开销,实测吞吐会掉一些,但在 8GB 卡上,稳定不 OOM 优先于极限吞吐。
--swap-space 4表示允许使用 4GB 系统内存作为 KV cache 的 swap 区域。这是 8GB 卡上应急用的安全阀,后面我会详细说。
3.3 验证部署成功的三种方式
服务启动后,不要急着上压测,先做三个快速验证。
第一,看启动日志。vLLM 会打印显存分配的详细报告,里面有模型权重占用的字节数、KV cache 池的大小、剩余显存。如果日志里 KV cache 显示为几十 MB,说明某个参数设得不合理,先不要跑请求。
第二,用 curl 请求一次 OpenAI 兼容接口:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"local-7b","messages":[{"role":"user","content":"你好"}],"max_tokens":64}'能正常返回而且首 token 延迟合理,说明服务链路是通的。
第三,打开 vLLM 自带的监控端点。它在启动日志里会给一个端口,能看到当前的吞吐量、显存使用、请求排队数这些指标。这也是后面压测时判断瓶颈在哪里的重要依据。
4. 把并发压测跑起来:工具、脚本与指标解读
4.1 压测要达到的目标和工具选型
我跑并发压测的目的只有两个:验证在 8GB 显存上这个服务到底能扛住多大的并发,以及并发上来之后延迟和吞吐怎么变化。所以压测方案不能只看"能不能通",要看三个指标:
- 成功率:并发请求里有几个真的返回了完整结果,有没有 OOM。
- TTFT(Time To First Token):从发请求到第一个 token 出来要多久,反映排队和调度效率。
- 生成吞吐:每秒能生成多少个 token,用 tokens/s 计。
工具方面我没有选重量级的压测平台,而是用一个轻量的 Python 脚本,直接打 vLLM 兼容的接口。脚本思路很直接:固定并发数,每个并发循环发送一批请求,统计耗时和 token 数。我试过用通用 HTTP 压测工具,但大模型接口的响应是流式的,关注指标不止 QPS,自己写脚本更顺手。
4.2 一个可复现的并发压测脚本
下面这个脚本是我实际用过的简化版,核心逻辑都在。
import asyncio import aiohttp import time API_URL = "http://localhost:8000/v1/chat/completions" MODEL = "local-7b" CONCURRENCY = 8 # 并发数 TOTAL_REQUESTS = 40 # 总请求数 MAX_TOKENS = 256 async def send_one(session, req_id): payload = { "model": MODEL, "messages": [{"role": "user", "content": "请用三句话解释什么是操作系统"}], "max_tokens": MAX_TOKENS, "temperature": 0.2, } start = time.perf_counter() first_token_time = None gen_tokens = 0 async with session.post(API_URL, json=payload) as resp: async for line in resp.content: if line.strip(): if first_token_time is None: first_token_time = time.perf_counter() - start gen_tokens += 1 return { "ok": True, "ttft": first_token_time, "tokens": gen_tokens, "total": time.perf_counter() - start, } async def main(): async with aiohttp.ClientSession() as session: sem = asyncio.Semaphore(CONCURRENCY) async def worker(i): async with sem: return await send_one(session, i) tasks = [worker(i) for i in range(TOTAL_REQUESTS)] results = await asyncio.gather(*tasks, return_exceptions=True) success = [r for r in results if isinstance(r, dict) and r.get("ok")] total_tokens = sum(r["tokens"] for r in success) elapsed = max(r["total"] for r in success) if success else 0 print(f"并发: {CONCURRENCY} 成功: {len(success)}/{TOTAL_REQUESTS}") print(f"TTFT均值: {sum(r['ttft'] for r in success) / len(success):.2f}s") print(f"总生成token: {total_tokens} 吞吐: {total_tokens / elapsed:.2f} tokens/s") asyncio.run(main())这个脚本我故意写得简单,便于各位修改。实际使用时我会把 CONCURRENCY 从 1 调到 2、4、8、16 分别跑一轮,每轮之间留出半分钟让服务稳定下来。要注意每轮用相同 prompt 和 max_tokens,否则结果没有可比性。
4.3 结果怎么解读
我在这张 8GB 卡上跑 INT4 7B 模型时,得到的典型数据长这样(具体数值因模型而异,但变化趋势是稳定的):
| 并发数 | 成功率 | 平均TTFT(s) | 整体吞吐(tokens/s) |
|---|---|---|---|
| 1 | 100% | 0.30 | 18 |
| 4 | 100% | 0.45 | 52 |
| 8 | 100% | 0.70 | 75 |
| 16 | 87% | 1.50 | 60 |
重点看两个转折点。并发从 1 涨到 8,吞吐是明显上升的,这说明连续批处理在起作用,单个请求并不是独立占满 GPU。但是并发到 16 的时候,吞吐不升反降,成功率还掉了,说明显存里的 KV cache 池已经被打满,后来的请求只能排队,排队时间过长就超时。8GB 卡上这个模型的真实甜点区间就在并发 8 左右。
这个结论在我调大 max-model-len 之后又被验证了一次:上下文从 4096 改成 8192,同并发下吞吐立刻下降,原因就是 KV cache 池被压缩,能够同时在跑的请求变少。所以说压测的价值在"找边界",找到了边界才知道接下来的显存调优往哪个方向走。
5. 显存调优三板斧:KV cache、上下文长度与SWAP
5.1 KV cache 与 gpu-memory-utilization 的调配
显存调优的第一件事是理解 gpu-memory-utilization 和 KV cache 池的关系。vLLM 在启动时会按这个比例预分配显存,减去模型权重的固定占用后,剩下的全部划给 KV cache 池。所以权重越大、预分配比例越低,KV cache 池就越小。
在 8GB 卡上,我建议一步步往下调:先 0.95,如果压测出现 OOM 就降到 0.92,再不行到 0.90。不要一上来就 0.98。gpu-memory-utilization 太高会导致显存预分配时几乎没有余量,激活值稍有抖动就崩。调低之后确实会少一点 KV cache 空间,但稳定性和成功率上来了,这笔交易是划算的。
还有一个小技巧:如果模型支持 KV cache 的低精度存储,可以考虑把 KV cache 的 dtype 调成更低精度,可以显著增大相同显存里的 KV cache 容量。这个选项不是所有模型都支持,支持的话收益在 8GB 卡上非常明显,能让并发上限高一截。
5.2 max-model-len 本质上是显存设置而不是功能设置
很多新手把 max-model-len 当成"模型最多能处理多少字"的功能选项,这是误解。在 vLLM 里,它直接决定 KV cache 池预分配的大小。vLLM 要按照 max-model-len 乘以最大 batch 数来预留 KV cache 空间。你把它设得很大,KV cache 池就很大,同一时间能缓存的请求变少,并发能力反而下降。
所以在调优时要把它当作显存选项来用:如果业务场景大多数对话都在 2000 token 以内,就没必要给 8192。我实测在 8GB 卡上,INT4 7B 模型配合 max-model-len 4096,并发 8 是稳定区间;调到 2048 的话,KV cache 池更宽裕,并发能到 12 左右,但这时候请求内容稍微长一点就会被截断。要根据业务最大输入长度去反向设这个值,而不是追求大。
5.3 swap-space、enforce-eager 和量化的最后权衡
swap-space 是 8GB 卡上的安全阀。它允许 vLLM 在 KV cache 满的时候把一部分缓存换到系统内存里。开这个选项之后,并发高了不容易 OOM,但有代价:swap 出去的请求再被调度回来时,要从内存重新搬回显存,生成速度会明显变慢。所以我会把 swap-space 当成"防止崩掉的保底",而不是指望它在高并发下还能保持速度。想让服务稳定扛并发,优先还是把 gpu-memory-utilization 和 max-model-len 调好。
enforce-eager 我上面说过了,它能大幅度降低加载时的显存尖峰。当 8GB 卡在启动阶段就 OOM 的时候,第一个要检查的就是它。至于它带来的调度开销,我实测在低并发下几乎无感,高并发下会损失一部分吞吐,但如果不开它服务都起不来,那止损是值得的。
最后一板斧是回到量化方案本身。如果调完 KV cache 和上下文还是不够用,就得回头审视模型:同样 7B 能力档位,不同量化格式实际占用差距可能在 0.5~1GB 之间。换一个更紧凑的量化格式,比在现有配置上锱铢必较更容易见效。
5.4 我的最终参数组合
多次压测对比之后,我在这张 8GB 卡上固定下来的生产组合是:
vllm serve /data/models/7b-int4-model \ --quantization awq \ --max-model-len 4096 \ --gpu-memory-utilization 0.92 \ --enforce-eager \ --swap-space 4 \ --port 8000配合并发限制在 8 以内使用。这套配置牺牲了理论峰值吞吐,换来了较高的成功率、可接受的 TTFT,以及相对平稳的显存水位。如果你跑的是 3B 模型,可以不开 enforce-eager,gpu-memory-utilization 也可以往上调一点,因为权重占用小,KV cache 池天然宽裕。
6. 实测中踩过的坑和最终建议
6.1 三个容易忽略的坑
第一个坑是显存监控只看 nvidia-smi。vLLM 预分配的显存并不等于正在用的显存,KV cache 池是预分配好的,nvidia-smi 显示的高占用不代表它已经满了。真正的判断依据应该是 vLLM 自己的监控端点和压测结果,而不是盯着显卡占用率看。
第二个坑是日志里出现 "No available memory for KV cache" 这种报错时,第一反应不应该是加 swap-space,而是检查 max-model-len。前面说了,这个参数直接决定 KV cache 池大小,把 8192 改回 4096 往往立刻解决问题。加 swap 是治标,调整长度才是治本。
第三个坑是 OpenAPI 兼容接口里的 max_tokens 参数。即便你在服务启动时设了 max-model-len 4096,请求时如果传了 max_tokens 超过这个值,vLLM 会直接拒掉请求。所以客户端侧也要把 max_tokens 控制在启动配置之内,否则压测时会出现一批莫名的 4xx 错误,误导你以为是并发导致的问题。
6.2 我的最终建议
8GB 显存跑 vLLM 这件事,我的结论是:不仅能跑,而且能背着并发跑很稳,前提是不贪。不要试图跑最大的模型、最长的上下文、最高的并发,这三样最多同时要两样。我现在的默认路径是 7B 模型 INT4 量化,max-model-len 4096,并发控制在 8 附近,gpu-memory-utilization 0.92,enforce-eager 保平安。这套组合在本地开发、内部工具、甚至小流量的个人服务上都够用。
最后分享一个小技巧:压测脚本里的语义负载不要用太简单的 prompt。比如全是"你好"这种短请求,KV cache 压力很小,测出来的并发能力虚高。我在实际测试中故意用长一点的输入(几百 token 的文本)去压,才能暴露出真实使用时的显存水位和排队情况。在 8GB 卡上,短请求和长请求的压测结果差距可能是两倍以上,这一点你自己测一次就会深有体会。