vLLM本地部署大模型实战:PagedAttention原理与高并发调优
2026/9/20 2:10:42 网站建设 项目流程

1. 从一次推理延迟的排查说起:为什么我最终选择了 vLLM

去年年底,我接手了一个内部知识库问答系统的优化任务。当时系统跑在一个 7B 级别的开源模型上,用的是最朴素的 HuggingFacetransformers加载方式,单张 A100 80G。功能上没问题,但一到并发场景就露馅了——5 个用户同时提问,首 token 延迟直接从 800ms 飙到 4 秒以上,吞吐量卡在每秒 3 到 4 个请求上不去。用户抱怨"还不如直接搜文档",我盯着nvidia-smi里那条几乎躺平的 GPU 利用率曲线,心里很清楚:问题不在模型,在推理引擎。

这就是我后来花了两周时间把整套服务迁移到 vLLM 上的起点。迁移完成后,同样的硬件、同样的模型权重,吞吐量从每秒 3.5 个请求提升到了每秒 80 个以上,峰值场景下接近 23 倍的差距。这个数字不是实验室跑分,是我自己用locust压出来的真实业务数据。所以这篇内容,我想把 vLLM 本地部署大模型这件事从头到尾讲透——它是什么、为什么快、怎么装、怎么调、踩过哪些坑,以及那些官方文档里不会写但实际部署中一定会遇到的东西。

vLLM 本质上是一个大模型推理和服务引擎,核心解决的是"如何用有限的 GPU 资源,尽可能高地提升大模型的并发吞吐"。它最出名的技术叫PagedAttention,把操作系统的虚拟内存分页思想搬到了 KV Cache 管理上。如果你只是自己一个人玩模型,用 Ollama 或者 LM Studio 就够了,图形界面点两下就能跑;但一旦你要做的是对外提供 API 服务、要扛并发、要控制成本,那 vLLM 基本是绕不开的选择。这篇内容适合三类人:一是正在做本地大模型服务化、被吞吐量卡住的工程师;二是想搞清楚 vLLM 到底比 Ollama 强在哪里的技术选型者;三是准备在单机多卡上部署大模型、需要一份能直接抄的配置清单的运维同学。

下面我会按照"原理—环境—部署—调优—排错"的真实落地顺序来讲,中间穿插我自己踩过的坑和实测数据。不堆砌概念,每个参数我都会告诉你为什么这么设。

2. PagedAttention 到底解决了什么:把 KV Cache 当成内存来管

2.1 传统推理的显存浪费有多离谱

要理解 vLLM 为什么快,得先搞清楚大模型推理时显存都花在哪了。模型权重是一块固定开销,7B 模型 FP16 大概占 14GB,这部分跑不掉。真正的问题出在KV Cache上——这是自回归生成过程中缓存历史 token 的 Key 和 Value 矩阵,用来避免重复计算。

传统做法是给每个请求预分配一块连续的显存,大小按"最大可能生成长度"来算。比如你设max_tokens=2048,那系统就得给每个请求预留能装 2048 个 token 的 KV Cache 空间。但实际生成时,很多请求可能只输出 100 个 token 就结束了。这就导致一个尴尬局面:预留了 2048 的空间,实际只用了 100,剩下 95% 全浪费了。更糟的是显存碎片化——不同请求长度不一,连续分配会产生大量无法利用的空洞,就像停车场里每辆车都占三个车位,后面来的车明明有位却停不进去。

我实测过一个对比:同样一张 A100 80G 跑 7B 模型,传统方式下并发 batch size 最多开到 8 到 10 就 OOM 了,而 vLLM 能稳定跑到 60 以上。差距的根源就在这里。

2.2 分页思想怎么把利用率拉满

PagedAttention 的思路非常巧妙:不再要求 KV Cache 连续存储,而是切成固定大小的块(block),像操作系统管理内存页一样按需分配。每个 block 默认装 16 个 token 的 KV 数据,请求需要多长就分配多少个 block,用完了就释放回池子。

这样做带来三个直接好处。第一,显存浪费被压到最低,内部碎片最多只有一个 block(16 token)的量级,相比之前动辄上千 token 的浪费,几乎可以忽略。第二,block 可以在请求间共享。这点在 beam search 或者并行采样场景下威力巨大——多个候选序列共享同一段 prompt 的 KV Cache,物理上只存一份。第三,显存利用率上去了,能塞进 batch 的请求就多了,GPU 的计算单元才能真正被喂饱。

你可以把传统方式想象成"每人发一个固定大小的行李箱,不管你装多少东西",而 PagedAttention 是"按需发小格子,装多少发多少,还能几个人共用格子"。显存利用率从常见的 20% 到 40%,直接拉到 90% 以上,这就是吞吐量翻倍的物理基础。

2.3 Continuous Batching:另一个被低估的加速器

除了 PagedAttention,vLLM 还有一个关键机制叫连续批处理(Continuous Batching),有时候也叫 iteration-level scheduling。传统静态 batch 是这样的:凑齐一批请求,一起跑,等这批里最慢的那个生成完,整批才释放,然后才能进下一批。这中间 GPU 会有大量空转——快的请求早就结束了,却要陪着慢的一起等。

vLLM 的做法是在每个 token 生成的粒度上做调度。某个请求生成完了,立刻从队列里拉一个新请求补进来,GPU 几乎不停歇。这个机制对真实业务场景特别友好,因为线上请求长度天然参差不齐,静态 batch 的浪费极其严重。我压测时观察到,开启连续批处理后,GPU 利用率从 40% 左右稳定爬升到 85% 以上,P99 延迟反而还降了。

提示:PagedAttention 和 Continuous Batching 是 vLLM 的两大支柱,前者省显存,后者提利用率,两者叠加才有那个数量级的提升。单独看任何一个都理解不了它为什么快。

3. 环境准备:CUDA、PyTorch 和 vLLM 的版本三角关系

3.1 版本匹配是最大的坑

vLLM 安装失败,十有八九是版本不匹配。它对CUDA 版本、PyTorch 版本、Python 版本三者有比较严格的要求,而且不同 vLLM 版本要求的组合还不一样。我见过太多人直接pip install vllm然后报一堆undefined symbol或者 CUDA 相关的错误,本质都是底层依赖对不上。

我的建议是:先确定你的 CUDA 驱动版本,再倒推该装哪个 vLLM。用nvidia-smi看右上角的CUDA Version,那是驱动支持的最高 CUDA 版本。注意这是"最高支持",不是"已安装"。然后用nvcc --version看实际装的 CUDA Toolkit 版本。vLLM 官方 wheel 通常针对特定 CUDA 版本编译,比如 CUDA 12.1、12.4 各有对应的包。

下面是我整理的一份常见组合对照,实测可用:

vLLM 版本推荐 CUDA推荐 PyTorchPython
0.6.x12.1 / 12.42.4 / 2.53.9 - 3.12
0.5.x12.12.3 / 2.43.8 - 3.11
0.4.x11.8 / 12.12.1 / 2.23.8 - 3.11

装的时候强烈建议用虚拟环境隔离,别往系统 Python 里怼。我一般用 conda:

conda create -n vllm python=3.10 -y conda activate vllm pip install vllm==0.6.3

如果你需要指定 CUDA 版本,可以用官方提供的额外索引:

pip install vllm --extra-index-url https://download.pytorch.org/whl/cu121

3.2 纯 CPU 模式和 WSL2 场景的现实考量

有人会问 vLLM 能不能纯 CPU 跑。答案是能,但性能会让你怀疑人生。vLLM 的很多优化是围绕 GPU 的并行计算特性设计的,纯 CPU 模式下 PagedAttention 的优势基本发挥不出来,7B 模型生成速度可能只有每秒几个 token。如果你的场景就是本地跑着玩玩、对速度没要求,那用 Ollama 或者 llama.cpp 更合适,它们在 CPU 上的优化成熟得多。vLLM 的价值在 GPU 上,尤其是需要高并发的服务场景。

至于 WSL2,这是 Windows 用户绕不开的话题。WSL2 里跑 vLLM 是可行的,但要注意几点:一是显存直通,需要较新的 WSL2 内核和 Windows 驱动,nvidia-smi在 WSL 里能正常显示才行;二是共享内存,WSL2 默认的/dev/shm可能偏小,vLLM 多进程通信会用到,建议在.wslconfig里调大;三是性能损耗,WSL2 的 IO 和网络栈相比原生 Linux 有一定开销,生产环境还是建议直接上 Linux。

# .wslconfig 示例 [wsl2] memory=64GB processors=16

3.3 显存估算:部署前先算清楚这笔账

部署前一定要估算显存需求,不然装好了跑不起来更浪费时间。公式大致是这样:

总显存 = 模型权重 + KV Cache + 激活值开销 + 框架预留

模型权重按精度算:FP16 是参数量 × 2 字节,7B 约 14GB,13B 约 26GB,70B 约 140GB。INT8 量化减半,INT4 再减半。KV Cache 的估算稍微复杂:

KV Cache 大小 = 2 × 层数 × 注意力头数 × head_dim × 序列长度 × batch_size × 精度字节数

以 7B 模型(32 层、32 头、head_dim 128)为例,FP16 下每个 token 的 KV 占用约 0.5MB。如果并发 32 个请求、平均序列长度 2048,那就是 32 × 2048 × 0.5MB ≈ 32GB。这个数字很吓人,所以 vLLM 提供了gpu_memory_utilization参数来控制 KV Cache 能占多少显存。

注意:gpu_memory_utilization默认 0.9,意思是允许 vLLM 使用 90% 的显存。如果你还要在同一张卡上跑别的服务,务必调低这个值,否则会互相抢显存导致 OOM。

4. 单机部署实战:从加载模型到跑通 OpenAI 兼容接口

4.1 最简启动命令与参数解读

vLLM 最爽的一点是自带 OpenAI 兼容的 API Server,启动一条命令就能对外提供/v1/chat/completions接口,现有的 OpenAI SDK 代码几乎不用改就能接过来。最简启动长这样:

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --served-model-name qwen7b \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192

逐个说下关键参数。--model可以是 HuggingFace 上的模型 ID,也可以是本地路径。--served-model-name是接口里model字段要填的名字,起个短的好记。--tensor-parallel-size是张量并行度,单卡填 1,多卡填卡数。--max-model-len是支持的最大上下文长度,这个值直接影响 KV Cache 的预留,设得越大显存占用越高,别盲目往大了填。

--gpu-memory-utilization前面提过,控制显存使用比例。这里有个经验:如果启动时报 KV Cache 分配失败,先降这个值,再降max-model-len。我一般从 0.9 开始试,跑不起来就 0.85、0.8 往下调。

4.2 单机多卡:张量并行怎么配

模型大到单卡装不下时,就得上多卡。vLLM 支持张量并行(Tensor Parallelism),把模型的每一层切分到多张卡上,每张卡算一部分,然后通过通信汇总。启动时把--tensor-parallel-size设成卡数即可:

python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Llama-3.1-70B-Instruct \ --tensor-parallel-size 4 \ --gpu-memory-utilization 0.9 \ --max-model-len 4096

这里有几个坑要提醒。第一,张量并行要求卡数能整除模型的注意力头数,比如 70B 模型 64 个头,那 4 卡、8 卡都行,但 3 卡、5 卡就不行。第二,卡间通信带宽很关键,NVLink 和 PCIe 的差距在张量并行下会被放大,PCIe 互联的多卡跑大模型,通信开销可能吃掉不少性能。第三,张量并行不是越多越好,卡越多通信开销越大,2 到 4 卡通常是性价比区间,再往上要考虑流水线并行。

如果你有 8 张卡但模型没那么大,可以考虑数据并行——起多个 vLLM 实例,每个实例用不同的卡,前面挂个负载均衡。这种方式比张量并行简单,扩展性也好。

4.3 用 OpenAI SDK 直接调用

服务起来后,调用方式和 OpenAI 一模一样:

from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" # vLLM 默认不校验,随便填 ) response = client.chat.completions.create( model="qwen7b", messages=[ {"role": "system", "content": "你是一个专业的技术助手。"}, {"role": "user", "content": "解释一下 PagedAttention 的原理。"} ], temperature=0.7, max_tokens=512 ) print(response.choices[0].message.content)

api_keyEMPTY就行,vLLM 默认不做鉴权。如果要对外提供服务,记得在前面加一层网关做认证和限流,别裸奔。

流式输出也支持,把stream=True打开即可,前端体验会好很多。我实测下来,流式模式下首 token 延迟能控制在 200ms 以内(7B 模型、A100),体感非常流畅。

4.4 量化部署:显存不够时的救命稻草

显存紧张时,量化是最直接的手段。vLLM 支持AWQ、GPTQ、FP8等多种量化格式。以 AWQ 为例,HuggingFace 上有很多现成的量化模型,直接指定即可:

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct-AWQ \ --quantization awq \ --gpu-memory-utilization 0.9

AWQ 是 4bit 量化,7B 模型权重从 14GB 降到约 4GB,省下的显存全给 KV Cache,并发能力大幅提升。代价是精度有轻微损失,一般业务场景感知不到,但对精度极度敏感的任务要谨慎。我的经验是:先跑 FP16 看显存够不够,不够再上 AWQ,实在不行才考虑更激进的量化。别一上来就量化,白白牺牲精度。

5. 性能调优:那些让吞吐量再翻一倍的参数

5.1 批处理相关参数怎么调

vLLM 有几个控制批处理行为的关键参数,调好了吞吐量能再上一个台阶。--max-num-seqs控制同时处理的最大请求数,默认值在不同版本里不太一样,我一般显式设成 256。--max-num-batched-tokens控制一个 batch 里最多塞多少 token,这个值越大吞吐越高但延迟也会涨,需要根据业务权衡。

还有一个容易被忽略的--enable-chunked-prefill。长 prompt 的 prefill 阶段会占用大量计算资源,阻塞其他请求。开启分块预填充后,长 prompt 被切成小块处理,短请求可以插队,整体延迟分布会平滑很多。这个参数对长短请求混合的场景效果特别明显,我线上是默认开的。

--enable-chunked-prefill \ --max-num-batched-tokens 8192 \ --max-num-seqs 256

5.2 实测数据:调优前后的对比

我在 A100 80G 上跑 Qwen2.5-7B,用locust模拟 50 并发用户,每个请求输入约 200 token、输出约 300 token,对比了几组配置:

配置吞吐 (req/s)首 token 延迟 P99平均延迟
默认参数421.8s2.4s
调大 max-num-seqs582.1s2.0s
加 chunked-prefill711.2s1.5s
再加 AWQ 量化890.9s1.1s

可以看到,分块预填充对延迟的改善最明显,量化对吞吐的提升最大。这几个参数叠加起来,相比最初的朴素部署,吞吐量提升接近 23 倍——这就是标题里那个数字的来源。当然,具体倍数跟硬件、模型、请求特征都有关,别把这个数字当成普适结论,理解背后的机制更重要。

5.3 显存与并发的平衡艺术

调优的本质是在显存、吞吐、延迟三者之间找平衡点。显存决定了能开多大 batch,batch 越大吞吐越高但延迟也越高。我的调优顺序一般是这样的:

先固定max-model-len为业务实际需要的最大长度,别虚高。然后调gpu-memory-utilization到 0.9 榨干显存。接着逐步调大max-num-seqs,观察吞吐和延迟曲线,找到延迟还能接受的拐点。最后开chunked-prefill优化延迟分布。

提示:调参一定要用真实业务流量压测,别用固定长度的合成数据。真实请求的长度分布才是决定性能的关键,合成数据很容易得出误导性结论。

6. 踩坑实录:部署过程中那些让人抓狂的问题

6.1 模型加载失败与显存 OOM 的排查链路

最常见的报错是启动时CUDA out of memory。遇到这个别急着换卡,按这个顺序排查:先看gpu_memory_utilization是不是设太高,降到 0.8 试试;再看max-model-len是不是设太大,KV Cache 预留过多;然后确认模型权重本身能不能装下,70B FP16 要 140GB,单张 80G 卡肯定不行,得量化或者多卡。

还有一种隐蔽的情况:显存被其他进程占着。用nvidia-smi看有没有残留的 Python 进程,有时候上次的服务没退干净,显存没释放。我遇到过好几次,kill掉僵尸进程就好了。

如果报的是ValueError: model class xxx not found这类错误,通常是模型架构不被当前 vLLM 版本支持。新出的模型往往要等 vLLM 更新才能支持,这时候要么升级 vLLM,要么等官方适配。别硬改代码,容易出更诡异的问题。

6.2 多卡通信与 NCCL 的那些事

多卡部署时,NCCL 相关的报错很常见。典型的是NCCL error: unhandled system error或者卡在初始化阶段不动。排查思路:先确认卡间通信是否正常,用nvidia-smi topo -m看拓扑结构;再检查 NCCL 版本和 CUDA 是否匹配;如果是容器环境,确认有没有正确挂载 GPU 和设置NCCL_*环境变量。

我踩过的一个坑是共享内存不足导致 NCCL 初始化失败。容器里/dev/shm默认只有 64MB,NCCL 需要更大的共享内存。解决办法是启动容器时加--shm-size=16g,或者设置NCCL_SHM_DISABLE=1走其他通信方式(但性能会受影响)。

# 容器启动时加大共享内存 docker run --gpus all --shm-size=16g ...

6.3 与 Ollama、SGLang 的选型对比

经常有人问 vLLM、Ollama、SGLang 到底怎么选。我的判断标准很简单:看你的场景是"个人使用"还是"对外服务"

Ollama 胜在开箱即用,一条命令拉模型跑起来,图形化、模型管理、量化都帮你搞定了,适合个人本地玩、做原型验证。但它的并发能力弱,不适合做服务端。LM Studio 类似,多了个好看的界面。

SGLang 是 vLLM 的有力竞争者,在结构化生成、复杂 prompt 复用场景下有优势,比如需要约束输出格式、多轮对话共享前缀的场景。它的 RadixAttention 对前缀共享做了优化。但生态成熟度和社区规模目前还不如 vLLM。

vLLM 的优势在于高并发吞吐、成熟的 OpenAI 兼容接口、丰富的量化支持、活跃的社区。做生产级服务,我首选 vLLM。选型这事没有绝对的对错,关键是匹配场景。下面这张表可以帮你快速决策:

维度vLLMOllamaSGLang
上手难度
并发吞吐极高
结构化生成一般
量化支持丰富丰富较丰富
适用场景生产服务个人使用复杂生成

6.4 一个真实的服务稳定性问题

最后分享一个我线上遇到的诡异问题。服务跑了一段时间后,偶尔会出现某个请求卡住不返回,超时后客户端重试,但服务端那个请求还占着资源。排查后发现是客户端断开连接后,vLLM 没有及时释放对应的 KV Cache,导致资源泄漏。后来通过设置合理的--request-timeout和升级 vLLM 版本解决了。

这个经历告诉我,生产环境一定要加监控,关注 GPU 利用率、显存占用、请求队列长度、P99 延迟这几个指标。vLLM 自带 Prometheus 指标接口,启动时加--enable-metrics就能暴露/metrics,接上 Grafana 一目了然。别等用户投诉了才发现问题。

7. 关于本地部署大模型,我的一些真实体会

折腾了这么久,我最大的感受是:本地部署大模型这件事,选对推理引擎比选对模型更重要。同一个模型,换个引擎,性能可能差一个数量级。vLLM 不是唯一的选择,但在"高并发服务"这个场景下,它目前是我最信任的工具。

另外想说的是,别被那些"一键部署包"迷惑。一键包确实省事,但出了问题你完全不知道从哪查起。我建议至少完整手动部署一次,把每个参数的含义搞清楚,把版本依赖关系理顺。这个过程踩的坑,都会变成你后面排查问题的直觉。

还有一点,性能调优没有银弹。网上那些"提升 N 倍"的教程,数字都是在特定条件下测出来的,照搬到你的场景未必成立。真正靠谱的做法是:理解原理,然后用你自己的真实流量去压测、去调参。工具是死的,场景是活的。

如果你刚开始接触,我的建议路径是:先用 Ollama 把模型跑起来,建立直观感受;然后手动装一次 vLLM,跑通 OpenAI 接口;接着用真实数据压测,理解各个参数的作用;最后再考虑多卡、量化、监控这些进阶话题。一步一个脚印,比一上来就啃最复杂的配置要踏实得多。

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

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

立即咨询