用 vLLM 部署一个模型服务
同时服务很多用户时,问题变成怎么用同一张显卡处理尽量多的请求。用小 GPT 量出批量处理的效果,再讲 vLLM 怎样管理 KV 缓存、怎样启动兼容 OpenAI 的服务。
- 约 35 分钟
- 难度:深入
- 实测:2026-09-15 批量实验在 Apple M4 CPU 上运行;vLLM 用法以其官方文档为准
上一课用 Ollama 在自己电脑上运行模型,一个人用很方便。如果要把模型做成服务,给公司里几百个人、或者给你的产品的用户用,问题就不一样了:几十个请求同时到来,怎么在一两张显卡上尽快地处理完?
vLLM 是目前最常用的开源大模型服务框架之一。这一课先用实验看清楚服务端的核心问题,再讲 vLLM 怎么解决它、怎么用。
vLLM 主要面向带 NVIDIA 显卡的 Linux 服务器,我的实验机器(Mac,没有 NVIDIA 显卡)跑不了它,所以这一课的实验用第 09 模块的小 GPT 来做,vLLM 的用法部分只列出官方文档里的命令,不贴我没有亲自运行得到的输出。
一次写一首,还是一次写很多首
第 09 模块第 6 课说过,生成的每一步都要把模型的全部参数读一遍,却只算出一个字。在大模型上,这一步的时间主要花在从显存里读参数上,计算单元大部分时间在等数据。
那么,如果把多个请求放在一起,读一遍参数,同时为多个请求各算出一个字呢?
for batch in [1, 4, 16, 64]:
for _ in range(N_POEMS // batch):
model.generate(torch.full((batch, 1), NEWLINE), N_TOKENS) # batch 首诗同时写
python batch_throughput.py
一共要写 64 首,每首 60 个字,用 KV 缓存
一次写 1 首:总共 1.84 秒,每秒 2085 个字,每一首从开始到写完 0.03 秒
一次写 4 首:总共 0.93 秒,每秒 4110 个字,每一首从开始到写完 0.06 秒
一次写 16 首:总共 0.48 秒,每秒 8053 个字,每一首从开始到写完 0.12 秒
一次写 64 首:总共 0.44 秒,每秒 8706 个字,每一首从开始到写完 0.44 秒
同样写 64 首诗:一首一首写要 1.84 秒;一次写 16 首只要 0.48 秒,每秒写的字数(吞吐量)提高到了约 4 倍。
但请注意最后一列。一次写 1 首时,每一首 0.03 秒就写完了;一次写 64 首时,每一首都要等 0.44 秒才能拿到结果。批次越大,总的效率越高,但每个请求等待的时间(延迟)也越长。
而且从 16 到 64,吞吐量几乎不再提高(8053 到 8706):CPU 的计算能力已经用满了。显卡的计算能力强得多,能从批量处理里得到的好处也大得多。
这就是模型服务要解决的核心问题:在可以接受的延迟之内,把尽可能多的请求放在一起算。
真实服务里的两个难题
我们的实验很理想:64 首诗同时开始,每首都写 60 个字。真实的请求不是这样的。
请求长短不一,而且随时到来。一个请求要写 20 个字,另一个要写 2000 个字。如果把它们放在一批,等最长的写完才处理下一批,短的请求早就写完了,却还占着位置;新来的请求也只能干等。vLLM 的办法是连续批处理(continuous batching):每生成一步,就检查一次,写完的请求立刻移出批次,新来的请求立刻加进来。批次里的请求一直在变,显卡一直满负荷工作。
KV 缓存很占地方,而且大小不确定。第 09 模块第 6 课和上一课都算过,KV 缓存随着文字变长而增长,每个请求最终要占多少事先不知道。传统的做法是按最大长度给每个请求预留一整块连续的显存,而大部分请求根本用不了那么多,大量显存被浪费了,能同时处理的请求就少了。
PagedAttention
vLLM 的核心发明叫 PagedAttention,出自 2023 年的论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》。它借鉴了操作系统管理内存的"分页"办法:
传统做法:每个请求预留一整块,按最大长度
请求 A [■■■■□□□□□□□□□□□□] 用了 4 格,空着 12 格
请求 B [■■■■■■□□□□□□□□□□] 用了 6 格,空着 10 格
分页:KV 缓存切成固定大小的小块,用到哪儿分配到哪儿
块池 [A1][B1][A2][B2][B3][C1][空][空] ...
请求 A 的块表:A1 → A2
请求 B 的块表:B1 → B2 → B3
每个请求的 KV 缓存被切成固定大小的小块,不需要连续存放,写满一块再分配下一块,每个请求只用它实际需要的。这样几乎没有浪费,同样的显存可以同时服务多得多的请求。
多个请求有相同的开头(比如相同的系统提示词)时,这些块还可以共享,不必各存一份。第 06 模块第 4 课讲的"缓存命中的输入更便宜",在服务端就是类似的机制。
论文摘要里报告,在延迟相同的情况下,vLLM 的吞吐量是当时其他系统的 2 到 4 倍,文字越长、模型越大,提升越明显。
安装和启动
以下命令引自 vLLM 的官方文档(截至 2026 年 9 月),需要一台有 NVIDIA 显卡的 Linux 机器。vLLM 也支持 AMD、Intel 的显卡以及一些 CPU 和其他硬件,但安装方式不同,请看官方文档。
uv venv --python 3.12 --seed
source .venv/bin/activate
uv pip install vllm --torch-backend=auto
启动一个服务,只要一条命令:
vllm serve Qwen/Qwen2.5-0.5B-Instruct
默认在 http://localhost:8000 提供一个和 OpenAI 兼容的接口。和上一课的 Ollama 一样,第一部分的代码改三个环境变量就能用:
export LLM_BASE_URL=http://localhost:8000/v1
export LLM_API_KEY=你设置的密钥
export LLM_MODEL=Qwen/Qwen2.5-0.5B-Instruct
服务要给别人用时,一定要加上密钥检查。启动时加 --api-key,或者设置环境变量 VLLM_API_KEY,服务端就会检查请求头里的密钥。第 06 模块第 6 课讲过,一个没有任何限制的接口放到网上,就是在替别人付钱。
同时挂多个 LoRA
第 2、3 课说过 LoRA 的一个好处:一份基座模型加许多个小的适配器。vLLM 支持在一个服务里同时挂多个 LoRA,每个请求指定用哪一个:
vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--enable-lora \
--lora-modules repobot=./.cache/repobot-lora
请求时把模型名写成 LoRA 的名字 repobot,就会用第 3 课微调的那个适配器;写成原模型的名字,就用原模型。多个客户各有自己的 LoRA,却只占一份基座模型的显存。
自己部署,还是调用 API
学到这里,你可以自己部署模型了。但要不要这样做,是另一个问题。
自己部署意味着:买或者租显卡,显卡闲着的时候也在花钱;要自己处理扩容、监控、故障、升级;能用的模型也受限于你的显卡装得下多大。
调用 API 则是按用量付费,没有闲置成本,能用上最好的模型。第 01 模块第 4 课算过,DeepSeek 这类 API 的价格已经很低。
一般来说,下面几种情况值得考虑自己部署:数据不能离开自己的服务器;调用量大而稳定,自己部署的总成本更低;需要用自己微调的模型。在做决定之前,按第 06 模块第 4 课的办法,把两种方案的成本都实际算一遍。
练习
- 在
batch_throughput.py里加上批次 8 和 32,找到你的电脑上吞吐量不再明显提高的那个批次大小。 - 修改实验,让一批里的诗长短不一(比如一半写 20 个字,一半写 120 个字),并且整批等最长的写完才结束。和全部写 60 个字相比,吞吐量下降了多少?这就是连续批处理要解决的问题。
- 如果你能用上一台有 NVIDIA 显卡的机器(比如云服务器),按本课的命令启动 vLLM,用第 06 模块的 RepoBot v4 调用它。
自测
1. 为什么把多个请求放在一起处理,吞吐量会提高?代价是什么?
生成每一步都要把模型参数读一遍,大模型上主要时间花在读参数上。多个请求一起处理,读一遍参数就能为每个请求各算出一个字,计算单元得到了充分利用。代价是每个请求的延迟变长了:它要和其他请求一起算,而且批次大到一定程度后,计算能力用满,吞吐量也不再提高。
2. 连续批处理解决了什么问题?
真实的请求长短不一、随时到来。如果一批请求要等最长的那个写完才换下一批,短请求写完了还占着位置,新请求只能等待。连续批处理在每一步都把写完的请求移出、把新请求加入,让显卡一直满负荷工作。
3. PagedAttention 是怎样节省显存的?
它把每个请求的 KV 缓存切成固定大小的小块,按需分配,不需要按最大长度预留一整块连续的显存,几乎没有浪费。相同前缀的块还可以在请求之间共享。省下的显存可以用来同时处理更多的请求。