☰
8GB显卡跑vLLM:从部署到并发压测的显存调优实战
2026/10/19 8:21:36 网站建设 项目流程

1. 为什么非要在 8GB 显卡上折腾 vLLM

显卡只有 8GB,这个数字在本地跑大模型的圈子里一直被当成“入门都勉强”的配置。很多人觉得 vLLM 是给数据中心那种几十 GB 显存准备的推理框架,小卡跑不动,甚至没必要跑。真实情况不是这样。8GB 显存完全可以跑起 vLLM,并且能跑完部署、并发压测、显存调优这一整条链路,关键是你要理解它每一步在跟什么打交道。

vLLM 能解决的,恰好就是小显存用户最痛的问题:模型权重怎么塞进去、并发请求怎么不乱打架、显存碎片怎么少一点。它的核心机制 PagedAttention 把 KV cache 切成固定大小的“页”,按需分配,像操作系统管理内存一样管理显存,这就避免了传统方式下缓存区被请求撑爆或者留下一堆碎片的浪费。再加上 continuous batching(连续批处理),请求结束马上腾位置给新请求,吞吐量比普通脚本高不少。

这篇文章适合两类人。一类是手里刚好有 8GB 显存(老款游戏卡、入门工作站、小显存云主机),想跑一个本地对话模型,同时想弄清楚并发压力下到底能撑住多少请求。另一类是已经在跑 vLLM,但动不动就 CUDA out of memory,想搞清楚哪些参数要动、为什么动、动了之后效果如何。我不会只贴一条“能跑的配置命令”,而是把命令里的参数逐个拆开,讲清楚 8GB 显卡下每个参数背后的显存账。

1.1 vLLM 在小显存场景下比原生推理香在哪

先做个对比。直接用 transformers 这样的原生推理库加载模型,每次请求都要维护一份完整的注意力缓存,请求多了就会出现“都要缓存、谁都不敢释放”的局面,而且显存分配是按最大长度一次性申请的,短请求也占了长请求的地盘,浪费严重。更麻烦的是并发不好做,想多路请求就得自己写任务队列再拼 batch,拼不好还会把单请求延迟拖得很高。

vLLM 的思路是给 KV cache 做“分页管理”。打个比方:传统方式是一条长长的连续货架,来一个请求就要给它一整段固定长度的货架,哪怕它只用了三分之一;PagedAttention 则是把货架拆成一格一格的小抽屉,请求用到哪一格就分配哪一格,用完立刻回收。对 8GB 显存来说,这个差别不是百分之几,而是能不能多同时服务几路请求的区别。

当然,vLLM 也不是魔法。模型权重、中间激活、KV cache、CUDA 上下文、计算图占用的显存,每一块都得从 8GB 里挤出来。这文章就是带你把每一块挤明白,最后给出一个能稳定压测、不轻易 OOM 的方案。

2. 部署前准备:环境、模型选型与显存算术题

2.1 环境安装的几个坑,先排掉

vLLM 的安装整体还算平滑,但有几个点在小显存机器上特别容易踩。我建议先建一个干净的 Python 虚拟环境,避免跟系统里的依赖打架:

python -m venv .venv source .venv/bin/activate pip install vllm

装完立刻验证一下:

python -c "import vllm; print(vllm.__version__)"

能打出版本号就说明基础环境没问题。常见的坑是:本机的 CUDA 驱动太老,或者 Python 版本跟 wheel 包不匹配。vLLM 官方对 Python 版本有要求,一般 3.10 或 3.11 比较稳。如果你装完发现 import 报错,先不用急着手搓源码编译——多数是 torch 版本跟 vLLM 需要的版本错位,这时候单独装一个匹配的 torch 再装 vLLM,通常比编译省一个多小时。

还有一个小细节:不要在同一环境里装太多深度学习库,尤其不要让其他库把 torch 版本改掉。我碰到过一次,装了一个可视化库之后 vLLM 直接起不来,日志里全是 CUDA 版本不一致的报错,最后只能重建虚拟环境。

2.2 8GB 显存到底能装下什么规模的模型

先把模型权重的账算清楚。一个 70 亿参数左右的模型,如果用 FP16 精度加载,光权重就要 14GB 左右,8GB 显存想都不用想。但把权重量化到 4bit(比如 AWQ 或 GPTQ),同样的模型只需要约 3.5~4.5GB。也就是说:8GB 显存的甜点区是“7B 级模型的 4bit 量化版”,或者更小的 1.5B/3B 模型全精度加载。

权重只是第一块,第二块是 KV cache。KV cache 是生成过程中必须存下来的注意力中间结果,大小跟上下文长度线性相关。计算公式大致是:

每 token 的 KV 字节数 = 2 × 层数 × KV头数 × 每头维度 × 精度字节数

拿一个典型的 7B 规模模型举例,假设 28 层、KV 头 8 个、每头维度 128、FP16 精度,那么每个 token 的 KV cache 大约是:

2 × 28 × 8 × 128 × 2 = 114688 字节,约 112KB

4096 上下文长度的 KV cache 就是 112KB × 4096 ≈ 448MB。如果你把上下文拉到 8192,那就直接翻倍到大约 900MB。在小显存机器上,这个数字对并发能力的影响非常直观:上下文越长,能在显存里同时跑的路数越少。

所以选模型的时候,不只要看权重大小,还要看你对上下文长度的需求。如果只是做问答、摘要这些短文本任务,4096 够用;想让它一次性读很长资料,那 8GB 卡上还是选 3B 级模型更从容。

3. vLLM 服务启动:命令逐参数拆解

3.1 一条能跑起来的启动命令

环境就绪、模型选定后,启动服务就是一条命令的事。以“my-org/chat-7b-awq”这样一个 4bit 量化模型为例,我在 8GB 显存机器上常用的启动命令是:

vllm serve my-org/chat-7b-awq \ --port 8000 \ --max-model-len 4096 \ --gpu-memory-utilization 0.85 \ --enforce-eager \ --max-num-seqs 4 \ --swap-space 4 \ --quantization awq

如果你的 vLLM 版本比较老,可能没有vllm serve这个子命令,也可以用等价写法:

python -m vllm.entrypoints.openai.api_server --model my-org/chat-7b-awq ...

启动之后它会输出一个兼容 chat/completions 接口的本地服务地址,默认是http://127.0.0.1:8000。先拿 curl 快速验证一下:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "my-org/chat-7b-awq", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 128 }'

能正常返回带choices字段的 JSON,就说明服务通了。

3.2 每个参数在 8GB 场景下是干什么的

--max-model-len 4096是第一个要重的参数。它同时限制了单请求的最大上下文和 KV cache 的总预算。4096 在 8GB 卡上是一个非常实用的起点,既能应付多数对话场景,又不会把 KV cache 吃掉太多。不要一上来就设置成 32768,那在 8GB 上基本等于自杀,除非你用的是 1.5B 小模型。

--gpu-memory-utilization 0.85的意思是:vLLM 最多使用总显存的 85%。8GB 的 85% 大约是 6.8GB,剩下的留给 CUDA 上下文、驱动开销、其他进程。千万别调成 0.99,看起来是极限压榨,实际上很容易触发显存不足,因为 CUDA 本身的上下文就要占用几百 MB,还有 PyTorch 的缓存机制也会临时占一块。

--enforce-eager非常关键。vLLM 默认会为每个请求形状生成 CUDA graph(CUDA 计算图)来加速推理,但计算图会额外占用显存。在 8GB 这种紧绷环境下,我建议先打开--enforce-eager,让模型老老实实走 eager 模式,用一点速度换稳定性和显存余量。等所有东西跑顺了,再关掉它试试,看能不能换回性能。

--max-num-seqs 4控制的是“最多同时处理多少个序列”。这个数字直接决定了并发承受力。调到 8 也许吞吐更高,但 KV cache 压力也上来了;8GB 下 4 到 8 是合理区间,具体看模型和上下文长度。这里的本质是:你要在“同时处理的请求数”和“每个请求的速度”之间找平衡点。

--swap-space 4是给 CPU 内存换显存用的兜底方案。当 KV cache 不够时,vLLM 会把部分页换到 CPU 内存,代价是访问慢一些。8GB 机器上建议至少留 4GB,相当于给突发流量一个缓冲。

--quantization awq必须跟模型实际量化方式一致。如果模型是 AWQ 而你不指定,或者指定错了,就可能出现权重加载后推理结果疯狂乱蹦,甚至直接报错。如果你的模型是 GPTQ,改成--quantization gptq即可;完全不知道模型怎么量化的,就去模型仓库看它的说明文件。

3.3 启动日志里藏着“还能塞多少并发”的答案

服务启动时,日志里除了 banner 还会打印类似“GPU KV cache size”或者“Maximum concurrency”的信息。这就是当前配置下 KV cache 的总容量。每次调整--max-model-len或者--gpu-memory-utilization之后,主动去看这个数字,它比任何估算都准。

我个人的经验是:KV cache 容量至少能撑住 4 个 4096 上下文的请求,这个配置才算健康。只有 1 个请求的量,说明上下文拉太长或者显存利用率太低,压测的时候并发一上来就会排队或 OOM。

4. 并发压测:写脚本、看指标、解读结果

4.1 自己写一个压测脚本,别靠感觉

服务跑起来之后,最想知道的问题就两个:并发高了还稳不稳?延迟和吞吐各是什么水平?拿 curl 手打不行,得写个一次性压测脚本。下面这个脚本用的是 asyncio + aiohttp,可以按指定并发数持续打请求,并且统计完成时间、生成 token 数和吞吐:

import asyncio import time import statistics import aiohttp API_URL = "http://127.0.0.1:8000/v1/chat/completions" MODEL_NAME = "my-org/chat-7b-awq" N_CONCURRENT = 4 N_REQUESTS = 40 PROMPT = "用一句话解释什么是注意力机制,再写一段关于显存不足的短诗。" MAX_TOKENS = 256 async def one_call(session, results): payload = { "model": MODEL_NAME, "messages": [{"role": "user", "content": PROMPT}], "max_tokens": MAX_TOKENS, "temperature": 0.2, "stream": False, } start = time.perf_counter() async with session.post(API_URL, json=payload) as resp: data = await resp.json() elapsed = time.perf_counter() - start completion_tokens = data.get("usage", {}).get("completion_tokens", 0) results.append({"elapsed": elapsed, "tokens": completion_tokens}) async def worker(session, sem, results): async with sem: await one_call(session, results) async def main(): sem = asyncio.Semaphore(N_CONCURRENT) results = [] async with aiohttp.ClientSession() as session: t0 = time.perf_counter() tasks = [worker(session, sem, results) for _ in range(N_REQUESTS)] await asyncio.gather(*tasks) wall = time.perf_counter() - t0 total_tokens = sum(r["tokens"] for r in results) latencies = sorted(r["elapsed"] for r in results) avg_latency = statistics.mean(latencies) p50 = latencies[len(latencies) // 2] p99 = latencies[int(len(latencies) * 0.99) - 1] qps = N_REQUESTS / wall token_throughput = total_tokens / wall print(f"总耗时: {wall:.2f}s") print(f"QPS: {qps:.2f}") print(f"平均单请求耗时: {avg_latency:.2f}s") print(f"P50: {p50:.2f}s, P99: {p99:.2f}s") print(f"总生成 token: {total_tokens}") print(f"系统吞吐: {token_throughput:.2f} token/s") asyncio.run(main())

如果想测首 token 延迟(TTFT),把请求改成"stream": True,然后记录“收到第一个数据块”的时间点减去发请求的时间点。这个指标在对话场景里比总耗时更敏感,用户感知到的“卡不卡”多半由它决定。

4.2 8GB 场景下,多少数据算正常

先给一个量级参考。7B 模型 4bit 量化,8GB 显存,4096 上下文,4 并发,非流式请求,正常情况下大概落在下面这个区间:

指标含义参考量级
TTFT首 token 延迟400~1500ms
单请求 decode 速度每秒生成 token 数8~15
系统吞吐所有请求综合生成速度30~60 token/s
QPS每秒完成的请求数0.5~2

单请求 decode 速度只有十几 token/s,看着慢,但这是 8GB 卡跑 7B 量化模型的实际水平。压测的意义在于:当并发从 1 涨到 4、8 时,系统吞吐在涨,而 P99 延迟不能涨得太离谱。如果发现并发从 4 加到 8,QPS 没怎么涨,P99 却涨了 3 倍,说明已经到瓶颈了,再往上加并发没有意义,只会让所有请求一起变慢。

还有一个很容易被忽略的信号:压测过程中要同时开一个终端看nvidia-smi的显存占用。如果显存长期顶满,说明 KV cache 已经被压到极限,服务可能随时把请求换到 CPU 内存,延迟会瞬间恶化。这时候你该做的不是继续加并发,而是回头调显存参数。

5. 显存调优:目标不是“能启动”,是“能压住并发”

5.1 调优参数矩阵

8GB 显存下,可动用的调优手段其实就那几样,但每一样都要知道它的代价:

参数作用调大的效果调小的效果
--max-model-len限制最大上下文能处理长文本,KV cache 占用上升KV cache 省下来,能扛更多并发
--gpu-memory-utilization控制 vLLM 可用显存上限给 KV cache 更多空间更稳,不容易触发 CUDA OOM
--max-num-seqs同时处理的序列上限并发吞吐上限提高单请求延迟更稳,显存压力小
--enforce-eager关闭 CUDA graph开启后性能更好但显存更高开启后省显存,速度略降
--swap-space设定 CPU 内存交换空间更能扛突发流量换页能力弱,可能 OOM
--enable-chunked-prefill预填充分块执行长 prompt 更省显存关闭则按整段计算

最后一个--enable-chunked-prefill值得单独说。它把“预填充”阶段的长计算拆成小块,和 decode 阶段的请求混合处理。好处是长 prompt 场景下显存峰值降了,代价是性能调度变复杂。8GB 卡上如果经常遇到“请求一进来显存立刻飙升”,可以试试这个参数。

另外,如果你的 vLLM 版本支持--kv-cache-dtype fp8,并且显卡支持相应指令,可以试着把 KV cache 精度降到 8bit,KV cache 能直接少一半显存。不过这是版本和硬件敏感功能,不是所有环境都能开,我建议把它作为“最后手段”,在稳定跑通之后再去实验。

5.2 一次完整的调优现场

拿一个典型场景举例:7B 模型 4bit 量化,8GB 显存,初始配置是 8192 上下文、不启用 eager、max-num-seqs为 8。启动大概率直接 OOM,或者勉强启动后压测一上并发就崩。

第一步,把上下文降到 4096。这一步立刻省出大约 450MB KV cache,同时让单请求的注意力计算量减少,TTFT 也会降。如果还启动失败,说明模型权重加 CUDA 上下文已经逼近极限,那就把--gpu-memory-utilization从 0.85 降到 0.8,让出更多余量。

第二步,打开--enforce-eager。CUDA graph 在很多 8GB 卡上会吃掉 1GB 左右的显存,这在权重 4GB、上下文 4K 的场景下是致命的。开了 eager 后启动成功率会明显提升,代价是每步推理多一点点调度开销,但对多数对话场景来说体感差距不大。

第三步,压测时把并发从 1 慢慢加到 4、8。观察nvidia-smi的显存曲线和脚本输出的 P99。如果并发加到 4 时显存已经接近满,把--max-num-seqs从 8 降到 4,让 vLLM 不要接受超过处理能力的请求,避免请求在队列里互相踩踏。

第四步,如果并发 4 依然不稳,就把--swap-space调大,给 KV cache 一个 CPU 出口。这是一个保底措施,速度会慢,但至少不会因为瞬间峰值直接崩溃。

调优过程中一定要记得:一次只改一个参数。你同时改了上下文、并发数、显存利用率,出了问题根本不知道是哪一步导致的。我每次调优都会把参数和压测输出存成一个带时间戳的文本,来回改几轮之后,回看记录能清楚看到每个参数的真实影响。

调优策略上,我最终稳定下来的 8GB 配置通常长这样:7B 量化模型用 4096 上下文、0.85 显存利用率、开启 eager、max-num-seqs4、swap-space4;如果换 3B 模型,可以把上下文拉到 8192,max-num-seqs提到 8,整体并发能力会有明显提升。

6. 常见问题速查表

8GB 显存跑 vLLM,踩坑频率最高的几个问题,基本集中在下面这些场景:

现象可能原因排查与解决
启动时直接 CUDA out of memory模型权重 + 上下文太长 + CUDA graph 挤爆显存改小--max-model-len,打开--enforce-eager,调低--gpu-memory-utilization
启动成功但一压测就崩--max-num-seqs太高,KV cache 不够降低并发数,缩短上下文,或调大--swap-space
推理结果胡言乱语量化方式指定错误确认模型量化类型,设置正确的--quantization参数
请求返回 404 或连接失败端口不对,或接口路径写错确认 vLLM 打印的地址,chat/completions 路径应为/v1/chat/completions
首 token 特别慢预填充阶段太长,或请求排队缩短输入 prompt,开启 chunked prefill,降低并发数
服务偶尔响应很慢,但显存没满交换空间把 KV cache 换到 CPU 了看日志中的 swap 指标,减少上下文长度或降低并发
模型加载卡在某个百分比模型文件下载中,或磁盘 IO 慢检查网络与磁盘,本地已缓存可改为离线加载

还有两个值得提的细节。一是监控显存别只看nvidia-smi的占用百分比,要看 vLLM 进程本身的实际显存。二是如果压测中大量请求返回超时,先查--max-num-seqs,它决定了同一个时间段内 vLLM 愿意接受多少请求,超过这个数不代表会拒绝,而是会排队,排队久了自然超时。

7. 最后再分享一点个人体会

在 8GB 显卡上玩 vLLM,我最大的感受是:这卡逼你把显存账算明白,反而比大显存机器上直接无脑堆配置更能理解推理系统的资源结构。权重、KV cache、CUDA graph、激活值,每一样都清清楚楚。

最后给一个小技巧:调试阶段先用最保守的配置把服务拉起来,再一层一层放开限制。比如第一轮就跑 3B 小模型、1024 上下文、并发 2,确认链路没问题后,再逐步换大模型、加上下文、加并发。这样你不会在“到底是模型问题还是参数问题”上浪费时间。等所有参数调顺了,再试--enable-chunked-prefill或者 fp8 KV cache 这类进阶选项。小卡跑 vLLM 不是不能玩,是每一步都得想清楚为什么,想清楚了,8GB 一样能给你稳定可用的对话服务。

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

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

立即咨询