大语言模型(Large Language Model,LLM)的推理部署是其从技术验证走向工程化落地的核心环节,直接决定服务稳定性、资源利用率与业务承载能力。本文的上篇推理部署框架llama.cpp与Ollama使用指北已介绍轻量级本地推理框架的基本用法,包括模型量化、本地部署和基础对话体验。这类工具适合在单机上快速运行和体验模型,但在真实的生产环境中,模型常常需要以API服务的形式同时服务多个用户。这时,显存碎片化、静态批处理导致的GPU算力浪费,以及高并发下首Token延迟飙升等问题就会暴露出来。

上述问题根源于LLM自回归生成机制中的KVCache管理缺陷与静态调度策略,而vLLM正是为解决这些问题而设计的专用推理引擎。vLLM凭借PagedAttention与Continuous Batching两项核心技术,分别从显存管理和计算调度两个维度突破传统方案的瓶颈,显著提升显存利用率与吞吐能力,成为当前工业界应用最广的高并发推理方案之一。

延续上篇的主题,本文将逐步介绍vLLM的基础使用方法,主要参考vLLM官方文档vLLM docs的实践指南,更深入的内容可查阅该官方文档的详细说明。本文将从vLLM快速入门讲起,依次说明如何进行安装部署、模型加载、服务配置、参数调优以及运行监控与性能观察。

1 核心概念

1.1 背景与问题

1.1.1 LLM推理的挑战

LLM正加速从云端走向边缘与本地部署,背后的驱动力主要来自更低的推理延迟、更强的隐私保护,以及更可控的部署成本。与此同时,随着模型参数规模和上下文长度不断增长,传统推理方案的瓶颈也越来越明显:显存利用率低、GPU算力利用率低、并发能力不足,并且请求量一旦增加,响应延迟就会迅速恶化。

这类问题在LLM工程落地中非常常见。模型在单用户或低并发环境下通常表现正常,但一旦进入真实生产场景,面对十几个甚至几十个并发请求,往往就会出现首Token延迟飙升、显存耗尽、吞吐量下降等现象,而GPU利用率却始终不高。

根本原因在于LLM的自回归生成机制。LLM需要逐Token生成结果,并持续维护KVCache来保存历史上下文。KVCache的显存占用会随着上下文长度线性增长,而生成长度在推理前又无法准确预知,这就使得资源分配高度动态,传统优化手段很难奏效。

传统推理方案,例如直接使用Hugging Face Transformers的model.generate()方法,通常采用预先分配策略,为每个请求预先分配一整块连续显存空间,即便该空间大部分未被使用。这种预留机制造成显存浪费和严重碎片化,GPU计算单元频繁等待显存数据访问,最终导致吞吐量下降、延迟升高和算力成本上升。

vLLM是加州大学伯克利分校团队在2023年开源的高性能推理框架,核心目标是提升LLM推理的吞吐量、显存利用率,并降低推理延迟。它已经成为Mistral、Qwen、Llama、DeepSeek等主流模型常用的推理后端之一,也常被视为面向服务器级GPU的主流LLM推理方案。

它的核心性能优势主要来自两项关键技术:

  • PagedAttention:借鉴操作系统的分页内存管理思想,将KVCache切分为固定大小的块,统一调度和复用,使不同请求能够复用非连续的显存资源,从而显著降低碎片率。在实际生产中,这种机制通常能减少大量显存浪费。

  • Continuous Batching:从调度层面提升GPU利用率。传统推理往往要等一个批次中的所有请求都结束后才能进入下一轮计算,而vLLM允许新请求持续加入正在执行的批次,各请求独立推进、独立结束,不必同步等待。这样一来,在相同硬件条件下通常可以带来数倍吞吐提升。

除性能优化外,vLLM在工程可用性方面也很突出。它原生兼容Hugging Face模型格式,并提供OpenAI风格的API接口。开发者只需少量代码或一条vllm serve命令,就能快速部署支持流式输出、高并发请求和批处理的推理服务。

关于vLLM的定位与适用场景,有两点需要明确:

  1. vLLM是高性能推理引擎,不是完整的模型服务平台。监控、日志、自动扩缩容、安全认证、模型版本管理等能力,需要由外部系统补齐。vLLM专注于推理优化,不涉及模型训练。在轻量级或纯CPU离线推理场景中,llama.cpp等方案往往更具性价比。

  2. vLLM更适合实时对话系统、多租户模型服务、高并发生成任务,以及需要长上下文和流式输出的在线推理服务。

在LLM落地实践中,模型本身的能力固然重要,但承载业务流量的推理系统架构,才是LLM工程化落地的核心底座。相关工程问题与解决方案可参考:vLLM核心原理与生产实践

https://nebius.com/blog/posts/serving-llms-with-vllm-practical-guide

1.1.2 KVCache原理与局限

原理

LLM以自回归方式生成文本,也就是逐个Token依次输出。当模型准备生成第N个Token时,必须参考此前N-1个Token所包含的全部信息,这一能力由注意力机制承担。

注意力计算可视为一种查询匹配过程:当前待生成的token发出一组Query(查询向量),意在询问此前内容中哪些部分与当下最相关;已存在的每个token均提供一组Key(索引标签)与Value(实际内容)。以Query与各个Key的匹配程度作为权重,对所有Value进行加权求和,所得结果即为当前token应当重点关注的上下文信息。

以翻译“The cat sat on the mat”为例。模型已经生成了两个中文词:“那只”、“猫”。现在模型要决定第三个中文词是什么。

第三个中文词在生成之前,先有一个“空位”代表它。这个空位被称为“当前待生成的Token”。模型为这个空位计算出一个Query向量,用来询问之前的内容。

具体计算:这个空位有自己的位置编码和初始向量,乘以权重矩阵W_Query,得到Query = [0.2, 0.8]。这个Query的含义相当于“猫做了什么动作”。
每个已存在的英文单词(The、cat、sat、on、the、mat)都有各自的Key和Value(2维向量):

单词 Key Value
cat [0.8, 0.1] [0.9, 0.2]
sat [0.2, 0.9] [0.1, 0.9]
其他 忽略 忽略

匹配度计算(点积:q·k = q1×k1 + q2×k2):

  • Query与cat的Key:0.2×0.8 + 0.8×0.1 = 0.24
  • Query与sat的Key:0.2×0.2 + 0.8×0.9 = 0.76

加权求和(Value × 匹配度,再相加):

  • cat贡献:[0.9×0.24, 0.2×0.24] = [0.216, 0.048]
  • sat贡献:[0.1×0.76, 0.9×0.76] = [0.076, 0.684]
  • 结果:[0.292, 0.732]

这个结果向量被送回空位所在的层。输出层根据这个向量判断概率最高的词是“坐”。于是空位被填充为“坐”。

在多层Transformer结构中,每一层都会为每个Token分别计算Key和Value。如果每生成一个新Token都重新计算一次全部历史Token的Key和Value,计算量会随着序列长度快速膨胀,接近平方级增长。为避免这种重复计算,vLLM会将每个Token在每一层产生的Key和Value保存下来,在后续生成过程中直接复用这些结果。这部分用于存储历史Key和Value的数据结构,就是KVCache,通常驻留在GPU显存中。

生成过程因此可以拆成两个阶段:

  • 预填充(Prefill)阶段:将整个提示词一次性输入模型,并行计算所有Token对应的Key和Value,写入KVCache,然后生成首个回复Token。
  • 解码(Decode)阶段:之后每生成一个新Token,只需为新Token计算Query、Key和Value,并将新的Key和Value追加到KVCache末尾,再基于Query与更新后的缓存计算注意力,生成下一个Token。

到这里,解码阶段的单步计算已经显著降低,但新的问题也随之出现。

局限

随着对话序列长度增长,KVCache的显存占用会持续升高。以70B规模模型为例,在处理3.2万Token长上下文时,单条请求的KVCache可能占用几十GB显存,具体数值会受到精度、层数、头数等配置影响。一块NVIDIA H100显卡的总显存约80GB,这意味着一条长序列请求几乎可以独占整张卡。

传统框架通常为每条请求预先分配一整块连续显存来容纳KVCache,这会引发两个明显问题:

  • 空间浪费与显存溢出并存:若按最大长度预分配,大部分空间在多数时间内处于空闲状态;若按平均长度分配,一旦实际输出长度超出预期,便会因显存耗尽而触发显存溢出崩溃。
  • 显存碎片化:每条请求各自占据一块连续显存,不同请求的长度和结束时间都不同。当部分请求完成并释放显存后,留下的是大小不一、彼此分散的空闲区域。即使剩余显存总量足够,新请求也可能因为缺乏足够长度的连续空间而无法启动。

这就像一家旅馆按照每位客人可能住的最长天数,一次性锁死整层楼的房间。结果大多数客人只住几天,大量房间在大部分时间里白白空着;偶尔遇到长住客,整层楼倒是刚好能被占满。更麻烦的是,当一位长住客退房后,留下的房间散落在不同楼层,彼此不连续。而新来的团队需要的恰恰是一整层连续楼层。于是就会出现一种尴尬的局面:总空房间不少,但因为不连续,怎么也塞不进一个需要整层楼的团队。

https://monashdeepneuron.medium.com/2023-apac-hpc-ai-competition-fc8689acdfe3

1.2 vLLM关键技术

1.2.1 PagedAttention

在LLM中,注意力机制需要缓存Key和Value(KVCache),显存占用很大。如果直接连续分配,容易产生碎片,浪费空间,并降低性能。vLLM的PagedAttention借鉴了操作系统的虚拟内存与分页机制,有效解决了这个问题。
操作系统管理内存的基本流程如下,可以把操作系统的内存管理想象成书架:

  1. 分块存储
    内存被切成固定大小的页(Page),就像书架分成统一格子。

  2. 按需分配,不要求连续
    程序只分配需要的页,这些页在物理内存中可以散落,不必连续。

  3. 映射关系维护
    系统维护页表,记录逻辑页与物理页的对应关系。程序看到的是连续空间,但底层可能分散。

  4. 统一回收消除碎片
    程序结束后,占用的页会被回收放回空闲池。因为分配和回收都是固定大小的页,从而避免碎片产生。

PagedAttention将这一思想平移到KVCache管理中,核心设计包括:

  1. 固定大小的块(Block)
    将显存切成大小相同的块,每块可存固定数量Token的KV数据。

  2. 按需分配,物理块可分散
    序列开始时只分配少量块,随着Token生成再动态追加。这些块在显存中不必连续。

  3. 块表(Block Table)映射
    每条序列维护一张块表,记录逻辑块与物理块的对应关系。注意力计算时通过查表即可找到真实位置。

  4. 即时回收+全局空闲池
    序列结束或暂停时,其占用的块会立即回收并放入全局空闲池,供其他请求复用。

  5. 写时复制(Copy-on-Write)实现前缀共享
    多条序列若拥有相同前缀,可先共享同一组物理块;只有当某条序列需要修改数据时,才复制出专属的新块,从而避免重复占用显存。

凭借这套机制,KVCache的显存利用率显著提升,分配与回收都变成了轻量级的块操作,系统吞吐能力也随之大幅提高。该技术的详细原理与实现可参考:Deploy LLMs at Scale with vLLM

https://x.com/_avichawla/status/2031827029570854928

1.2.2 Continuous Batching

解决显存瓶颈后,vLLM进一步把重点放在计算效率上。传统静态批处理通常是先凑齐一批请求,统一完成预填充,再逐Token生成,并且必须等批次内所有请求都结束后才能进入下一轮。这意味着短请求经常要等待长请求,GPU也会因为批次内某些序列结束而被动浪费算力。

vLLM的连续批处理(Continuous Batching)更像一个动态拼车系统:

  • GPU持续运行一个生成循环,每一步只为当前批次中仍然活跃的序列生成一个Token。
  • 某条序列完成后,立即从批次中移除,并归还它占用的KVCache块。
  • 同一步调度中,系统会从等待队列里拉入新的请求,直接插入当前批次。
  • 下一步生成会在新旧混合批次上继续执行,GPU始终保持较高利用率。

在这种情况下,短请求不必等待长请求结束,完成后也能立刻释放资源。配合PagedAttention灵活的块分配机制,新请求可以随时进入执行流程,不会因为KVCache无法连续分配而被阻塞。最终,vLLM的吞吐量往往能达到传统方法的数倍,在线服务成本也会明显下降。高并发调度优化细节可参考:vLLM高并发推理优化详解

https://github.com/huggingface/blog/blob/main/continuous_batching.md

1.2.3 其他优化措施

除了上述两大核心设计,vLLM还集成了多项在生产环境中很实用的优化能力:

  • 自动前缀缓存(Prefix Caching):自动识别多个请求中的相同前缀,例如System Prompt、工具说明或少样本示例,并复用对应的KVCache,减少重复计算。
  • 分块预填充(Chunked Prefill):将超长提示文本拆成多个小块,穿插在解码阶段交替执行,避免长输入一次性占满GPU计算资源。
  • FlashAttention-3:使用高效注意力内核,减少显存读写开销,加速Attention计算。
  • PagedAttention专用CUDA内核:直接在GPU侧读取块表,减少CPU参与调度的开销。
  • 多LoRA批量服务:在同一批次中并行服务多个不同LoRA适配器的请求,适合多任务、多租户场景。
  • 投机解码(Speculative Decoding):先用小模型快速生成候选Token,再由大模型验证,以提升生成速度。
  • 张量并行与流水线并行:通过一行参数即可将模型切分到多张GPU上运行,便于扩展到多卡甚至多节点环境。
  • 智能调度与抢占:系统高负载时优先处理短交互式请求,并对长序列请求进行适度抢占与恢复,以维持整体体验。

这些技术协同起来,使vLLM不只是一个跑得快的推理工具,而是一套更适合生产环境的高效推理引擎。vLLM核心技术与实战落地详解可参考:vLLM Tutorial for Beginners

https://www.datadoghq.com/monitoring/vllm-monitoring/

2 快速上手

2.1 安装说明

版本兼容

vLLM本质上是一个GPU-first推理框架,底层高度依赖CUDA Runtime、Triton Kernel、FlashAttention以及PyTorch CUDA等关键组件,因此它与NVIDIA GPU生态绑定很深。由于Windows在CUDA工具链、C++扩展编译链以及部分底层依赖上的兼容性不如Linux成熟,部署时更容易出现依赖缺失、编译失败或运行不稳定等问题。

在标准Linux + NVIDIA GPU环境下,只要完成CUDA和PyTorch等基础依赖安装,vLLM通常就能比较顺利地部署。官方一般已经提供集成主要CUDA扩展和推理内核的预编译wheel,大多数Linux发行版可以直接通过pip安装,无需额外编译。

相比之下,纯CPU环境难以发挥vLLM的核心优势。PagedAttention主要面向GPU显存带宽和高并发吞吐优化,在CPU上很难获得同等级别收益,而这一差距在Windows + CPU环境中通常会更明显。不过,CPU环境仍然可以安装vLLM,只是过程更复杂。由于预编译wheel支持有限,很多情况下需要从源码编译C++和Python扩展模块,涉及GCC、CMake、Ninja以及PyTorch等多重依赖约束,部分GPU相关算子也会被禁用或降级。

在无GPU的CPU推理场景中,更适合选择专门面向CPU优化的推理框架,例如:

  • llama.cpp:针对AVX2/AVX512等SIMD指令集优化,支持GGUF量化格式,在低资源环境下具备更高效率与更低延迟
  • Ollama:基于llama.cpp封装,提供更友好的模型管理与开箱即用体验
  • Hugging Face Optimum:面向Intel生态的系统级CPU推理优化方案,可提供更高效的推理加速

安装

在Linux+CUDA环境下安装vLLM时,需要重点关注PyTorch、vLLM与CUDA之间的版本兼容关系。vLLM依赖其支持范围内的PyTorch与CUDA组合运行,并基于PyTorch提供GPU计算能力和算子体系进行推理加速。因此,一旦PyTorch版本变化,底层CUDA相关接口和二进制兼容性也可能变化,进而导致vLLM编译失败或运行时无法正常加载。

很多表面上的安装问题,本质上通常来自PyTorch与CUDA或相关依赖版本不匹配。不同版本的vLLM会限定其支持的PyTorch版本范围,而PyTorch本身又与CUDA版本强绑定,并进一步影响驱动版本兼容性。在这种链式依赖关系下,直接执行:

pip install vllm

该命令会默认安装最新版本的vLLM。如果当前环境中的PyTorch版本不匹配,pip在解析依赖时可能会自动调整或升级PyTorch,但新版本PyTorch未必与当前CUDA环境完全兼容,从而导致运行错误。

在安装前应重点核对vLLM与PyTorch的兼容关系,可以参考vLLM GitHub Releases来确认具体版本对应关系。如果当前PyTorch版本较低,通常需要选择较旧的vLLM版本进行匹配。如果需使用较新的vLLM版本,则往往需要同步升级PyTorch与CUDA环境,以保证整体兼容。例如,PyTorch 2.5.1通常与vLLM 0.7.3版本适配,可以这样安装:

pip install vllm==0.7.3

对于生产环境,更推荐直接使用官方Docker镜像或经过验证的版本组合,以降低编译与兼容性风险。如果需要从源码构建vLLM,应明确指定release tag,并确保当前PyTorch与CUDA组合在官方支持范围内。

2.2 模型加载与调用

在vLLM中,从模型到服务的完整流程通常分为三步:模型获取→服务启动→API调用验证。详细内容可参考,vLLM快速部署与实战教程

1. 模型获取

vLLM支持直接加载Hugging Face或ModelScope上的模型,也支持从本地路径加载已经下载好的模型。如果指定在线模型,启动服务时会自动下载。生产环境中更推荐提前将模型下载到本地。国内环境常用ModelScope下载,示例如下:

pip install modelscope
modelscope download --model Qwen/Qwen3-0.6B --local_dir ./Qwen3-0.6B

https://blog.ovhcloud.com/reference-architecture-deploying-a-vision-language-model-with-vllm-on-ovhcloud-mks-for-high-performance-inference-and-full-observability/

2. 服务启动

无论是本地模型还是在线模型,vLLM的启动方式是统一的,只需要指定不同的路径。

本地模型启动:

vllm serve ./Qwen3-0.6B --served-model-name Qwen3-0.6B

在线模型启动,例如直接使用Hugging Face模型时,会自动从远程拉取:

vllm serve Qwen/Qwen3-0.6B --served-model-name Qwen3-0.6B

--served-model-name用于指定模型在API中的暴露名称,方便后续调用。

服务启动后,vLLM通常会执行以下操作:

  • 自动加载tokenizer和模型权重
  • 初始化GPU显存分配,基于PagedAttention机制
  • 启动OpenAI兼容API服务,默认端口为8000
  • 捕获CUDA Graph形状,用虚拟数据预先打包一组GPU操作快照,后续直接重放

启动后,可先查询可用模型列表:

curl http://localhost:8000/v1/models

https://techcommunity.microsoft.com/blog/bff5f527-af54-4e01-be5c-609acdd7f285/the-rise-of-vllm-in-modern-cloud-development-revolutionizing-ai-inference/4499179

3. API调用验证

vLLM提供OpenAI兼容接口,可以直接使用OpenAI SDK进行调用:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy"  # vLLM默认不校验API key,可任意填写
)

response = client.chat.completions.create(
    model="Qwen3-0.6B",  # 需与启动时指定的模型名称一致
    messages=[
        {"role": "user", "content": "你好,请介绍一下自己"}
    ]
)

print(response.choices[0].message.content)

也可以使用curl快速验证:

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3-0.6B",
    "messages": [{"role": "user", "content": "你好"}]
  }'

4. Python本地直接调用

如果不需要启动服务,也可以直接在Python中使用vLLM进行离线推理:

# SamplingParams: 配置文本生成策略的参数类
from vllm import LLM, SamplingParams

# 加载模型
llm = LLM(
    model="Qwen3-0.6B"
)

# 批量处理多个prompt,提高推理效率
# 使用chat风格prompt
prompts = [
    "<|im_start|>user\n你好,请介绍一下自己<|im_end|>\n<|im_start|>assistant\n",
    "<|im_start|>user\n2的9次方等于多少<|im_end|>\n<|im_start|>assistant\n"
]

# 稳定采样参数
params = SamplingParams(
    temperature=0.2,
    top_p=0.9,
    max_tokens=1024,
    repetition_penalty=1.2,
    stop=["<|im_end|>"]
)

# 批量推理
outputs = llm.generate(prompts, params)

# 输出结果
for index, output in enumerate(outputs):
    print(f"问题{index}回答:\n")
    print(output.outputs[0].text.strip())

2.3 vLLM Serve配置与调优

vLLM Serve提供了丰富的CLI参数,用于灵活配置LLM服务。不过实际上并不需要掌握全部参数,真正影响效果的通常只有少数几个核心配置项。更多参数配置规范可以参考:vLLM Official Documentation

2.3.1 基础参数

最基础的一层是服务本身的网络与接口配置,用于定义对外访问方式和基础鉴权能力。

  • --host:指定服务绑定的IP地址,默认值为0.0.0.0,表示监听所有网络接口。
  • --port:设置服务监听端口,默认值为8000
  • --served-model-name:指定对外暴露的模型名称,该名称只影响接口返回中的模型标识,不影响实际加载的模型。
  • --api-key:用于接口鉴权,未配置时默认不启用鉴权,生产环境建议显式开启。
  • --trust-remote-code:允许执行模型仓库中的自定义代码,部分模型依赖该选项才能正常加载。

例如:

vllm serve ./Qwen3-0.6B \
  --host 0.0.0.0 \
  --port 9000 \
  --served-model-name my-custom-model \
  --trust-remote-code

以上基础参数可覆盖日常绝大多数部署场景。如果需要快速查看完整参数说明,可以使用帮助命令:

  • 全量参数查询
    执行vllm serve --help=all可输出完整的参数手册,涵盖模型加载、并行策略、网络配置等全部配置模块。

  • 单点精准查询
    日常调试仅需核对单项参数时,可使用vllm serve --help=参数名精准检索。例如执行vllm serve --help=port,只会返回该参数的作用、默认值与使用方式。

2.3.2 显存与并发

gpu-memory-utilization

真正影响性能上限的是显存管理能力和并发调度参数。vLLM的核心机制基于KVCache与Continuous Batching,因此其性能调优的本质,是在避免显存溢出的前提下,尽可能提升GPU利用率和并发承载能力。

其中最关键的参数是--gpu-memory-utilization。例如设置为0.9时,表示vLLM会将可用显存中90%分配给KVCache等运行时数据,而不是占用GPU总显存的90%。

在实际部署中,如果同一GPU上还运行其他CUDA进程,或者存在显存碎片和波动风险,通常需要将该值适当调低到0.8~0.85,以预留安全空间,避免高并发场景下因KVCache增长导致OOM(Out Of Memory)。

vllm serve ./Qwen3-0.6B --gpu-memory-utilization 0.9

max-model-len

另一个非常关键的参数是--max-model-len,用于限制模型支持的最大上下文长度,即输入与输出Token的总和。虽然部分模型标称支持128K甚至更长上下文,但上下文越长,KVCache占用越大,显存消耗增长越快,并发能力也会随之下降。

因此在生产环境中,不应盲目追求超长上下文,而应结合业务需求进行取舍。如果大多数请求集中在4K Token以内,那么保留128K上下文不仅收益有限,还会显著浪费显存并降低整体吞吐能力。

vllm serve ./Qwen3-0.6B --max-model-len 8192

--max-model-len强相关的参数是--max-num-seqs,它控制vLLM同一时刻能并行处理的请求数量,即最大并发上限。--max-num-seqs设置过小会限制GPU利用率,无法发挥Continuous Batching的优势;设置过大会导致KVCache增长过快甚至引发OOM风险。

dtype

--dtype用于指定模型权重的数据类型,常见选项包括:

  • float16:标准半精度格式,显存占用约为FP32的一半。
  • bfloat16:在A100、H100等新架构GPU上数值稳定性更好。
  • auto:根据模型特性自动选择适配类型。

一般而言,新架构GPU更推荐bfloat16,消费级显卡或部分兼容环境多使用float16

2.3.3 多卡与量化

当单张GPU无法容纳模型时,需要使用多GPU并行。vLLM主要支持两种方式:Tensor Parallelism(张量并行)和Pipeline Parallelism(流水线并行)。前者在单层内部做张量切分,适合单机多卡;后者按层切分计算流程,更适合多机多卡。

https://jarvislabs.ai/blog/scaling-llm-inference-dp-pp-tp

--tensor-parallel-size(简称TP)属于张量并行方式,它将模型权重与计算按张量维度拆分到多张GPU上共同参与计算。

vllm serve ./Qwen3-0.6B --tensor-parallel-size 4

--pipeline-parallel-size(简称PP)属于流水线并行方式,它将模型按层切分,不同GPU分别负责不同层的计算流程,更适合跨节点的多机部署场景。

vllm serve ./Qwen3-0.6B --pipeline-parallel-size 2

量化方案

显存不足时,可以使用量化模型降低占用。--quantization用于指定已量化模型的格式,例如AWQ、GPTQ、FP8等,vLLM会按对应方式加载。

  • AWQ:激活感知的4-bit量化,稳定性较好。
  • GPTQ:主流权重量化方法。
  • FP8:8-bit浮点格式,在H100等新硬件上精度与性能更均衡。

该参数仅适用于已量化模型。如果对全精度模型使用该参数,通常会报错。

vllm serve Qwen2.5-7B-Instruct-AWQ --quantization awq

https://docs.vllm.ai/projects/llm-compressor/en/0.8.1/

2.3.4 高级特性

除了基础性能参数外,vLLM还提供了一些对线上体验非常关键的高级优化能力。

--enable-prefix-caching用于开启前缀缓存。当多个请求共享相同System Prompt时,vLLM会缓存对应的KVCache,后续请求可以直接复用,从而减少重复计算。对于对话系统、Agent系统或RAG场景,这一优化尤其有效。

vllm serve ./Qwen3-0.6B --enable-prefix-caching

--enable-chunked-prefill用于优化长文本请求对GPU资源的占用。推理通常分为Prefill和Decode阶段,长输入会导致Prefill耗时过长并阻塞其他请求的处理。开启Chunked Prefill后,vLLM会将长输入拆分成多个小块,与生成过程交替执行,从而改善整体延迟表现。

另一个重要方向是投机解码(Speculative Decoding)。该方法使用小模型先生成候选Token,再由大模型进行验证,从而提升生成速度。

vllm serve ./Qwen3-0.6B \
  --speculative-model ./Qwen2.5-0.5B-Instruct \
  --num-speculative-tokens 5

其中:

  • --speculative-model用于指定草稿模型
  • --num-speculative-tokens用于控制每轮预生成Token数量

在合适场景下,这种方法可以明显提升吞吐量,同时几乎不影响生成质量。

2.4 vLLM Python调用示例

2.4.1 交互式对话客户端

本示例实现了一个vLLM交互式对话客户端,支持多轮对话上下文管理、流式与非流式输出、连接错误和HTTP异常处理,以及命令行参数配置。

import argparse
import json
from argparse import Namespace
from collections.abc import Iterable
import requests
import sys

def post_chat_request(
    messages: list[dict], api_url: str, model: str, stream: bool = False
) -> requests.Response:
    headers = {"Content-Type": "application/json"}
    # 对话接口标准请求体
    payload = {
        "model": model,
        "messages": messages,
        "temperature": 0,
        "max_tokens": 1024,
        "stream": stream,
    }
    try:
        response = requests.post(
            api_url, headers=headers, json=payload, stream=stream, timeout=15
        )
        response.raise_for_status()
        return response
    except requests.exceptions.ConnectionError:
        print("\n❌ 连接失败:请确认 vLLM 已启动在 localhost:8000")
        sys.exit(1)
    except requests.exceptions.HTTPError as e:
        print(f"\n❌ API 错误:{e}")
        if 'response' in locals():
            print(f"返回:{response.text}")
        sys.exit(1)

def get_streaming_chat_response(response: requests.Response) -> Iterable[str]:
    """解析对话流式返回"""
    for chunk in response.iter_lines(chunk_size=8192, delimiter=b"\n"):
        if chunk:
            try:
                chunk_str = chunk.decode("utf-8").lstrip("data: ")
                if chunk_str == "[DONE]":
                    break
                data = json.loads(chunk_str)
                delta = data["choices"][0]["delta"].get("content", "")
                yield delta
            except Exception:
                continue

def get_chat_response(response: requests.Response) -> str:
    """解析非流式对话返回"""
    try:
        data = json.loads(response.content)
        return data["choices"][0]["message"]["content"]
    except Exception:
        print("❌ 返回格式错误")
        return ""

def parse_args():
    parser = argparse.ArgumentParser(description="vLLM 交互式对话客户端")
    parser.add_argument("--host", default="localhost")
    parser.add_argument("--port", type=int, default=8000)
    parser.add_argument("--model", default="Qwen3-0.6B")
    parser.add_argument("--stream", action="store_true", help="开启流式输出")
    return parser.parse_args()

def main(args: Namespace):
    # 对话接口地址(对话专用接口)
    api_url = f"http://{args.host}:{args.port}/v1/chat/completions"
    # 历史消息,保存上下文实现多轮对话
    chat_history = []
    print("===== 交互式对话模式 =====")
    print("输入 quit / exit 退出对话\n")

    while True:
        # 接收用户输入
        user_input = input("你:")
        if user_input.lower() in ("quit", "exit"):
            print("对话结束")
            break
        if not user_input.strip():
            print("请输入有效内容!")
            continue

        # 追加用户消息到历史
        chat_history.append({"role": "user", "content": user_input})
        response = post_chat_request(chat_history, api_url, args.model, args.stream)

        print("AI:", end="", flush=True)
        ai_reply = ""
        if args.stream:
            # 流式逐字输出
            for content in get_streaming_chat_response(response):
                ai_reply += content
                print(content, end="", flush=True)
        else:
            # 非流式一次性输出
            ai_reply = get_chat_response(response)
            print(ai_reply, end="")
        
        print("\n")
        # 追加AI回复到历史,保留上下文
        chat_history.append({"role": "assistant", "content": ai_reply})

if __name__ == "__main__":
    args = parse_args()
    main(args)

2.4.2 Gradio聊天机器人

本示例实现了一个基于Gradio的聊天机器人Web服务器,通过OpenAI SDK调用vLLM API服务。

import argparse
import time

import gradio as gr
from openai import OpenAI

def predict(message, history, client, model_name, temp, stop_token_ids):
    # 记录请求开始时间
    start_time = time.time()
    
    messages = [
        {"role": "system", "content": "你是一位优秀的AI助手。"},
        *history,
        {"role": "user", "content": message},
    ]

    # 向OpenAI API(vLLM 服务端)发送请求
    stream = client.chat.completions.create(
        model=model_name,
        messages=messages,
        temperature=temp,
        stream=True,
        extra_body={
            "repetition_penalty": 1,
            "stop_token_ids": [int(id.strip()) for id in stop_token_ids.split(",")]
            if stop_token_ids
            else [],
        },
    )
    elapsed_time = time.time() - start_time
    # 收集所有数据块并拼接成完整消息
    full_message = ""
    for chunk in stream:
        full_message += chunk.choices[0].delta.content or ""

    # 在返回消息末尾添加耗时信息
    full_message += f"\n\n---\n⏱️ 推理耗时: {elapsed_time:.2f} 秒"
    
    # 返回完整消息作为单次响应
    return full_message

def parse_args():
    parser = argparse.ArgumentParser(
        description="带可调参数的聊天机器人界面"
    )
    parser.add_argument(
        "--model-url", type=str, default="http://localhost:8000/v1", help="模型服务地址"
    )
    parser.add_argument(
        "-m", "--model", type=str, required=True, help="聊天机器人使用的模型名称"
    )
    parser.add_argument(
        "--temp", type=float, default=0.8, help="文本生成的温度参数"
    )
    parser.add_argument(
        "--stop-token-ids", type=str, default="", help="逗号分隔的停止token ID列表"
    )
    parser.add_argument("--host", type=str, default=None, help="服务监听地址")
    parser.add_argument("--port", type=int, default=8001, help="服务监听端口")
    return parser.parse_args()


def build_gradio_interface(client, model_name, temp, stop_token_ids):
    def chat_predict(message, history):
        return predict(message, history, client, model_name, temp, stop_token_ids)

    return gr.ChatInterface(
        fn=chat_predict,
        title="聊天机器人界面",
        description="基于vLLM的简易聊天机器人",
    )

def main():
    # 解析命令行参数
    args = parse_args()

    # 使用vLLM的API服务
    openai_api_key = "EMPTY"
    openai_api_base = args.model_url

    # 创建 OpenAI 客户端
    client = OpenAI(api_key=openai_api_key, base_url=openai_api_base)

    # 使用 predict 函数定义 Gradio 聊天机器人界面
    gradio_interface = build_gradio_interface(
        client, args.model, args.temp, args.stop_token_ids
    )

    gradio_interface.queue().launch()

if __name__ == "__main__":
    main()

2.4.3 监控与可观测性

在生产环境中,如果缺少监控,服务状态就很难感知。vLLM启动后会自动暴露一个指标接口,地址为http://localhost:8000/metrics,通常无需额外配置即可使用。通过以下命令,可以查看vLLM核心推理监控指标及其说明:

curl -s http://localhost:8000/metrics | grep -E "^# HELP vllm" | awk '{print $3, $NF}'

这条命令会静默访问本地vLLM的监控接口,筛选并输出指标名称与释义。之所以只保留以vllm开头的指标,是因为这些指标直接对应LLM推理的核心性能,而非vllm开头的指标多属于系统底层指标,日常运维中通常无需关注。

也可以通过Python读取运行指标。调用llm.get_metrics()后,可以获取当前LLM实例的监控指标列表。这些指标类型包括GaugeCounterHistogramVector,分别对应瞬时值、累计计数、分布统计和向量类指标。这种方式适合用于实验记录、性能调试、压测分析,或者在批量任务结束后自动输出统计报告。

下面是一份简化示例,展示如何创建LLM实例、执行推理任务,并打印采集到的指标。脚本执行完成后,可以看到本次推理任务中的缓存使用情况、请求延迟、Token生成量等关键数据,适合在开发和测试阶段快速定位性能问题。

from vllm import LLM, SamplingParams
from vllm.v1.metrics.reader import Counter, Gauge, Histogram, Vector

# ==================== 1. 准备输入 ====================
prompts = [
    "周末休息的时候适合做些什么",
    "夏天解暑又好吃的零食有",
    "出去玩首选的地方是",
]

# ==================== 2. 配置生成参数 =================
sampling_params = SamplingParams(
    temperature=0.8, 
    top_p=0.95,         # 核采样:只从累积概率达到95%的词中选择,过滤低概率词
)

def print_metrics_info(llm):
    """打印所有监控指标的说明和当前值"""
    
    metrics = llm.get_metrics()
    
    # 初始化计数器
    gauge_count = 0
    counter_count = 0
    vector_count = 0
    histogram_count = 0
    
    print("\n" + "="*70)
    print("vLLM 运行监控指标详解")
    print("="*70)
    
    for metric in metrics:
        # -------- Gauge 仪表盘指标:瞬时值,会上下波动 --------
        if isinstance(metric, Gauge):
            gauge_count += 1
            print(f"\n📊 Gauge 仪表盘指标 ({gauge_count}):")
            print(f"   名称: {metric.name}")
            print(f"   类型: 瞬时值(可增可减)")
            print(f"   当前值: {metric.value}")
            # 常见指标含义解释
            if "cache_usage" in metric.name:
                print(f"   💡 含义: GPU显存缓存使用百分比,越高说明显存占用越多")
            elif "num_requests_running" in metric.name:
                print(f"   💡 含义: 当前正在处理的请求数量")
            elif "num_requests_waiting" in metric.name:
                print(f"   💡 含义: 等待队列中排队的请求数量")
            elif "gpu_prefix_cache_queries" in metric.name:
                print(f"   💡 含义: GPU缓存查询总数")
            else:
                print(f"   💡 含义: 其他瞬时状态指标")
        
        # -------- Counter 计数器指标:累计值,只增不减 --------
        elif isinstance(metric, Counter):
            counter_count += 1
            print(f"\n🔢 Counter 计数器指标 ({counter_count}):")
            print(f"   名称: {metric.name}")
            print(f"   类型: 累计值(只增不减)")
            print(f"   累计值: {metric.value}")
            # 常见指标含义解释
            if "request_success" in metric.name:
                print(f"   💡 含义: 成功完成的请求总数(本次共{len(prompts)}个)")
            elif "prompt_tokens" in metric.name:
                print(f"   💡 含义: 处理的总输入token数")
            elif "generation_tokens" in metric.name:
                print(f"   💡 含义: 生成的总输出token数")
            else:
                print(f"   💡 含义: 其他累计统计指标")
        
        # -------- Vector 向量指标:多维数值 --------
        elif isinstance(metric, Vector):
            vector_count += 1
            print(f"\n📈 Vector 向量指标 ({vector_count}):")
            print(f"   名称: {metric.name}")
            print(f"   类型: 多维数值(键值对)")
            print(f"   当前值: {metric.values}")
            # 根据values内容推断含义
            if metric.values:
                keys = list(metric.values.keys())
                if all(str(k).isdigit() for k in keys):
                    print(f"   💡 含义: 可能是各GPU编号的统计值,如GPU{keys}的显存使用量(MB)")
                elif "gpu" in metric.name.lower():
                    print(f"   💡 含义: 多个GPU维度的监控数据")
                else:
                    print(f"   💡 含义: 多维度统计指标,键值对表示不同维度的值")
        
        # -------- Histogram 直方图指标:分布统计 --------
        elif isinstance(metric, Histogram):
            histogram_count += 1
            print(f"\n📉 Histogram 直方图指标 ({histogram_count}):")
            print(f"   名称: {metric.name}")
            print(f"   类型: 分布统计(桶计数)")
            print(f"   总次数(count): {metric.count} 次")
            print(f"   总和(sum): {metric.sum}")
            if metric.count > 0:
                print(f"   平均值: {metric.sum/metric.count:.4f}")
            print(f"   分布详情:")
            
            # 解释各桶的含义
            if "latency" in metric.name or "time" in metric.name:
                print(f"   💡 含义: {'延迟' if 'latency' in metric.name else '时间'}分布,每个桶表示≤该时间的请求数")
                unit = "秒"
            elif "tokens" in metric.name:
                print(f"   💡 含义: Token数量分布,每个桶表示≤该数量的请求数")
                unit = "个"
            else:
                print(f"   💡 含义: 数据分布统计")
                unit = ""
            
            # 打印每个桶
            for bucket_boundary, count in metric.buckets.items():
                if bucket_boundary == float('inf'):
                    print(f"      大于所有桶的值: {count} 次")
                else:
                    percentage = (count / metric.count * 100) if metric.count > 0 else 0
                    print(f"      ≤ {bucket_boundary}{unit}: {count} 次 ({percentage:.1f}%)")
    
    # 打印汇总信息
    print("\n" + "="*70)
    print("指标汇总")
    print("="*70)
    print(f"  Gauge(仪表盘)指标: {gauge_count} 个")
    print(f"  Counter(计数器)指标: {counter_count} 个")
    print(f"  Vector(向量)指标: {vector_count} 个")
    print(f"  Histogram(直方图)指标: {histogram_count} 个")
    print(f"  总计: {gauge_count + counter_count + vector_count + histogram_count} 个指标")
    print("="*70 + "\n")

def main():
    # ==================== 3. 初始化模型 ====================
    print("正在加载模型...")
    llm = LLM(
        model="./Qwen3-0.6B",      
        disable_log_stats=False,   # 启用统计日志(设为True则关闭)
    )
    print("模型加载完成!\n")

    # ==================== 4. 执行批量文本生成 ====================
    print(f"开始批量生成文本,共{len(prompts)}个提示词...")
    outputs = llm.generate(prompts, sampling_params)
    
    # ==================== 5. 打印生成结果 ====================
    print("\n" + "="*70)
    print("生成结果")
    print("="*70)
    for i, output in enumerate(outputs, 1):
        # 获取原始输入提示词
        prompt = output.prompt
        # 获取模型生成的文本
        # outputs[0] 表示取第一个生成结果(可以配置生成多个候选回复)
        generated_text = output.outputs[0].text
        
        print(f"\n--- 第{i}条结果 ---")
        print(f"提示词: {prompt}")
        print(f"生成文本: {generated_text}")
        # 打印生成统计
        if hasattr(output.outputs[0], 'token_ids'):
            print(f"生成token数: {len(output.outputs[0].token_ids)}")
    
    # ==================== 6. 获取并打印监控指标 ====================
    print_metrics_info(llm)

if __name__ == "__main__":
    main()

3 总结

vLLM的定位是服务端LLM推理引擎,而不是个人本地试玩工具。如果只是本地体验模型,或者主要使用CPU推理、GGUF模型,llama.cpp和Ollama通常更合适。

vLLM的核心场景,是将开源LLM稳定部署为在线服务。它的优势主要体现在GPU推理、高并发处理、监控指标和生产部署能力上,适用于企业内部模型服务、RAG系统、Agent平台,以及需要多人同时调用的API服务。不过vLLM本身并未提供官方图形化Web界面,但可以通过Open WebUI框架进一步搭建。

如果对外提供API服务,不能只关注模型能否跑通,还需要为vLLM配套做好鉴权、限流、超时控制、监控告警、日志审计、压测验证和降级回滚。否则在突发流量、异常请求、长Prompt或长输出场景下,很容易影响整体服务稳定性。

使用vLLM时,通常需要重点关注以下几点:

  • 显存是首要约束。模型大小、上下文长度(max-model-len)、输出长度(max_tokens)和并发数共同决定显存占用。显存不足可考虑量化(AWQ、GPTQ、INT4/8、FP8),但须用真实数据验证效果。
  • 参数从保守值起步。通过压测逐步优化max-model-lenmax_tokensgpu-memory-utilization、并发上限和队列长度,在显存、吞吐、延迟与稳定性之间取得平衡。
  • 优先选择Linux或Docker部署。Windows不推荐直接用于生产环境。
  • 多机多卡需评估通信开销。超大LLM场景下,节点间网络带宽很容易成为瓶颈。

上线前必须进行贴近真实业务的阶梯式压测,覆盖短问答、长Prompt、多轮对话、RAG和流式输出等场景,重点观察P99延迟、首Token延迟、显存、KVCache使用率、排队时间和错误率,逐步加压直至指标异常。同时,还要提前准备降级回滚预案,例如降低max_tokensmax-model-len,限制并发与队列,限制长Prompt和长输出,减少RAG检索返回量,切换更轻量的模型或量化版本,或者回退到稳定镜像等。

本文主要介绍基于vLLM部署通用LLM的方法。若需了解多模态相关实践,例如vLLM在多模态OCR场景中的落地应用,可参考文章:What Makes DeepSeek OCR So Powerful

4 参考文献

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐