LLM 推理为什么这么慢?
想象你用 ChatGPT 提了一个问题,等了 10 秒才看到第一个字——这背后发生了什么?
你每次问 AI,都在消耗昂贵的 GPU 内存
当你向 AI 发送一条消息,服务器上的 GPU 要为你的这条对话存储一份"记忆草稿"——技术上叫 KV Cache(键值缓存)。
问题来了:KV Cache 会随着对话变长而不断增长,而且每个用户的增长速度和长度都不一样。
内存碎片问题
传统方式为每个请求预留一整块连续内存,用不满就浪费,用完就崩。就像订了一排座位却只来了两人。
低并发问题
一台 GPU 同时只能服务很少几个用户。其他人只能排队等待,吞吐量极低。
成本极高
低效率意味着需要更多 GPU 来服务同样数量的用户。每张 A100 的租金约 2-3 美元/小时。
vLLM 的诞生:伯克利的工程突破
2023 年,加州大学伯克利分校的研究团队发布了 vLLM。它的核心创新只有一个词:PagedAttention(分页注意力)。
这个名字来自操作系统里的"虚拟内存分页"技术——借鉴了 50 年前解决内存碎片的方法,把它用在了 AI 推理上。
操作系统的"虚拟内存分页"把 RAM 切成小页按需分配,解决了内存碎片。vLLM 把同样的思路用在 GPU 显存上——这就是 PagedAttention 的本质。当你指导 AI 构建新功能时,"借鉴成熟领域的已知解决方案"是一个非常实用的架构模式。
用户发送请求
vLLM 分配内存块
模型计算注意力
生成 token 返回
用几行代码启动 vLLM
看看真实使用 vLLM 的代码长什么样:
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3-8B",
tensor_parallel_size=2
)
params = SamplingParams(
temperature=0.8,
max_tokens=256
)
outputs = llm.generate(
["什么是 PagedAttention?"],
params
)
从 vLLM 库导入两个工具:LLM(模型加载器)和 SamplingParams(生成控制器)
加载 Meta 的 Llama-3-8B 模型,参数 80 亿个——使用 2 张 GPU 并行运行
设置生成参数:temperature=0.8 表示"有点创意但不随机"
最多生成 256 个 token(约 200 个中文字)
发送问题,等待 vLLM 生成答案
测一下你的理解
你接手了一个 AI 聊天服务,发现并发用户超过 20 人就开始报 OOM(内存溢出)错误。根据你学到的知识,最可能的原因是什么?
PagedAttention:像图书馆借书一样管理显存
vLLM 最核心的创新——把 GPU 内存切成一页一页按需分配,从根本上消灭内存浪费。
旧方式:给每人预留一整间书房
想象一座图书馆为每位读者预留了一间独立书房。有人要读一本薄书,给了他一间大书房;有人要读百科全书,书房又不够用了。大多数书房半空着,偶尔有人书放不下。
这就是传统 LLM 框架的 KV Cache 分配策略——预分配固定大小的连续内存块,要么浪费,要么溢出。
PagedAttention 就像图书馆改成按需借书:读者需要哪本书就借哪本,书架按"页"管理,用完立即归还。不同读者的书可以放在同一排书架上,不必连续存放。
KV Cache 如何被切分成"页"
vLLM 把 GPU 显存划分为固定大小的 Block(块),每个块存储 16 个 token 的 KV 数据。
GPU 显存池(按块管理)
用户 A
用户 B
用户 A
空闲
用户 C
空闲
用户 A 的对话在逻辑上是连续的,但在 GPU 物理内存里,块可以分散存储。vLLM 用一张"块表"记录这种映射关系,就像图书馆的借书记录单。这个设计把内存利用率从 20-40% 提升到了 90%+。
块管理器的真实代码
这是 vLLM 源码中 KV Cache 管理器的核心数据结构(来自 vllm/v1/core/kv_cache_manager.py):
@dataclass
class KVCacheBlocks:
"""块分配结果,是调度器和
KV Cache 管理器之间的接口"""
blocks: tuple[
Sequence[KVCacheBlock], ...
]
def get_block_ids(self) ->
tuple[list[int], ...]:
return tuple(
[blk.block_id for blk in group]
for group in self.blocks
)
@dataclass:Python 的语法糖,自动生成初始化方法,让代码更简洁
KVCacheBlocks 类:代表一次内存分配的结果,像一张"块使用清单"
blocks 字段:一个嵌套结构——外层是 GPU 分组,内层是每组分配到的块列表
get_block_ids 方法:把"块对象"转成"块编号列表",方便后续 GPU 计算使用
实际作用:让调度器知道"用户 A 的第 3 段对话放在哪几个块里"
内存分配协商:调度器 vs 块管理器
当一个新请求到来时,调度器和块管理器如何协商分配内存:
理解检验
你的 AI 服务用了新的推理框架后,GPU 内存利用率从 35% 提升到了 91%。你的技术同事说这是 PagedAttention 的功劳。他说的原理是什么?
连续批处理:永不空转的流水线
vLLM 如何让 GPU 一直保持满负荷运行,而不是等一个请求完成再处理下一个。
传统批处理:等待所有人到齐才发车的班车
传统 批处理就像一辆班车:等所有座位坐满(或等时间到)才出发,所有乘客一起到达目的地,然后全部下车,再等下一批。
问题在于:LLM 生成的回答长度差异巨大。有人问"今天几号?"——2个字就够。有人要写一篇文章——1000个字。短答案的用户早就等好了,却不得不等长答案的用户。
所有请求同时开始,同时结束。短请求完成后 GPU 空置等待长请求。利用率低。
就像地铁站:一个乘客下车,立刻有新乘客上车。GPU 永远满载运行,无需等待。
一条请求经历了什么?
从 API 调用到生成第一个 token,数据流经 vLLM 的各个组件:
调度器:谁先谁后的决策者
vLLM 的调度器 (vllm/v1/engine/llm_engine.py) 核心工作是决定每次 GPU 迭代处理哪些请求:
class LLMEngine:
def __init__(self, vllm_config, ...):
# 输入处理:文本 → tokens
self.input_processor =
InputProcessor(vllm_config)
# 输出处理:tokens → 文本
self.output_processor =
OutputProcessor(
renderer.tokenizer,
log_stats=self.log_stats,
)
# 核心引擎:管理 GPU 工作
self.engine_core =
EngineCoreClient.make_client(
vllm_config=vllm_config,
)
LLMEngine 是 vLLM 的"总指挥",负责协调所有组件
input_processor:把用户的文字变成 GPU 能理解的数字序列(token IDs)
output_processor:把 GPU 输出的数字还原成人类可读的文字,支持流式输出
engine_core:真正干活的引擎核心,可以在同一进程或独立进程中运行
这种分层设计让每个组件各司其职,方便测试和替换
前缀缓存:重用已经算过的内容
前缀缓存是 vLLM 另一个重要优化。当多个用户的请求有相同的系统提示时,第一个用户计算过的 KV Cache 可以直接给后续用户用。
共享系统提示
100 个用户都带相同的"你是一个专业客服..."前缀,只需计算一次,节省 100 倍计算量。
文档 RAG 场景
同一份文档被多次查询时,文档内容的 KV Cache 可以被所有查询共享。
首 token 加速
有了前缀缓存,用户看到第一个字的等待时间(TTFT)大幅降低。
架构思考
你正在用 vLLM 构建一个企业知识库问答系统(RAG)。用户每次问问题,都会带上一大段公司文档作为上下文。你发现有很多用户在查询同一篇文档。你应该如何优化这个场景?
张量并行:让多块 GPU 变成一块超级 GPU
单张 GPU 装不下 70B 参数的模型怎么办?vLLM 把计算工作切成几份同时进行。
就像一本大书由多人同时翻译
想象要翻译一部百万字的百科全书,一个人翻译需要 10 年。但 10 个人每人翻 10 万字,只需 1 年。
张量并行就是这个道理:把模型的每一层神经网络切分成若干份,分配给多张 GPU 同时计算,最后把结果合并。
神经网络的计算本质是矩阵乘法。把大矩阵按列或按行切分,每张 GPU 负责一部分列/行的计算。
每张 GPU 计算完自己的部分后,通过高速互联(如 NVLink)把结果汇总,得到完整输出。
理想情况下,4 张 GPU 张量并行比单张快约 4 倍(实际受通信开销影响,约 3-3.5 倍)。
vLLM 支持多种并行策略
不同场景选不同策略,就像运货可以选轿车、货车或集装箱船:
张量并行 (TP)
切分单层计算。适合单台机器多卡。延迟最低,但需要 GPU 间高速互联(NVLink)。
流水线并行 (PP)
按层切分模型。不同机器负责不同层。适合跨机器部署,适合超大模型(1000B+)。
数据并行 (DP)
每台机器跑完整模型,处理不同批次的请求。适合高吞吐量场景,横向扩展方便。
当你让 AI 帮你配置 vLLM 时,可以说:"我有 4 张 A100 在同一台机器,模型是 70B,请给我推荐 tensor_parallel_size 和 pipeline_parallel_size 的配置,并说明理由。"有了这些词汇,AI 能给出更精确的答案。
配置多 GPU 推理
from vllm import LLM
# 4 张 GPU 张量并行
llm = LLM(
model="meta-llama/Llama-3-70B",
tensor_parallel_size=4,
)
# 或使用 OpenAI 兼容 API 服务
# python -m vllm.entrypoints.openai.api_server \
# --model meta-llama/Llama-3-70B \
# --tensor-parallel-size 4 \
# --port 8000
加载 Meta 的 Llama-3-70B 模型(参数量 700 亿,单张 A100 放不下)
tensor_parallel_size=4:把模型切成 4 份分给 4 张 GPU,每张只需 ~20GB 显存
第二种方式:启动 HTTP 服务器,提供和 OpenAI API 完全兼容的接口
--port 8000:服务监听的端口,你的代码只需把 base_url 指向这里
好处:原本调用 OpenAI 的代码,只需改 base_url 就能切换到 vLLM
量化:用更少显存跑更大模型
量化让你在有限显存中跑更大的模型,vLLM 支持多种量化格式:
AWQ
激活感知量化,4 位整数,精度损失极小,推荐首选
GPTQ
后训练量化,4-8 位,社区模型最广泛支持的格式
FP8
8 位浮点数,保留更多精度,H100/A100 原生支持,速度快
GGUF
llama.cpp 格式,社区模型多,适合 CPU 推理场景
架构决策练习
你的公司有一台配备 4 张 A100(每张 80GB)的服务器,需要部署一个 70B 参数的模型。你应该给 AI 开发助手描述怎样的配置方案?
生产部署:让 vLLM 稳定服务真实用户
从"能跑起来"到"稳定服务数千用户",你需要了解的关键配置和常见陷阱。
生产环境的完整系统结构
一个真实的 vLLM 生产部署不只是启动一个进程,它是一套完整系统:
你需要监控的三个核心指标
TTFT(首 token 时间)
用户发请求到看到第一个字的延迟。用户感知最敏感的指标。目标:<1 秒。
吞吐量(Tokens/s)
每秒生成多少个 token。决定你能服务多少用户。A100 单卡通常 2000-5000 tokens/s。
KV Cache 命中率
前缀缓存的命中比例。越高代表重复计算越少,直接影响成本和速度。
发现问题:vLLM 服务为什么突然变慢?
当你的 AI 助手陷入 bug 循环说"不知道为什么变慢了",这是你的调试清单:
vLLM 日志里的 gpu_cache_usage_perc 如果持续接近 100%,说明内存压力过大,调度器被迫驱逐请求。
num_waiting_reqs 如果持续增长,说明并发量超过了你的处理能力,需要扩容或限流。
如果 num_running_reqs 始终很小,可以调大 max_num_seqs 让每次迭代处理更多请求。
用 nvidia-smi 检查 GPU 使用率。如果低于 70%,说明 CPU 或网络成了瓶颈,不是 GPU 问题。
一个请求的完整生命周期对话
最终测验:从瓶颈到解决方案
你的 AI 客服系统反馈说"用户总是等很久才看到第一个字"(TTFT 高),但后续生成速度还好。你会优先检查和优化什么?
从 PagedAttention 的内存革命,到连续批处理的流水线,再到张量并行的多卡扩展——这些知识让你能够向 AI 助手提出更精确的需求,在遇到 LLM 服务性能问题时快速定位根因。