从一个请求开始:LLM 推理系统如何完成一次生成?
摘要
学习 vLLM 或其他 LLM 推理系统时,我发现最容易卡住的地方并不是某一个 CUDA kernel,也不是某个具体优化技巧,而是对整个请求生命周期缺少整体认识。
一个用户请求进入推理服务后,并不是简单地执行一次模型 forward 然后返回结果。LLM 是自回归生成模型,请求会经历 tokenizer、prefill、KV Cache 初始化、decode loop、scheduler 调度、streaming output 等多个阶段。推理系统要同时处理计算、显存、调度和用户体验之间的平衡。
这篇文章作为 vLLM 源码阅读笔记的第一篇,先从一个请求的生命周期出发,梳理 LLM 推理系统的基本流程,为后续继续理解 Scheduler、PagedAttention、Prefix Cache、Continuous Batching 和 Decode 阶段性能瓶颈做铺垫。
1. 为什么要从请求生命周期开始看推理系统
我最开始学习 LLM 推理系统时,最困惑的问题不是某一个 CUDA kernel 怎么写,也不是 vLLM 某个模块的具体实现,而是一个更基础的问题:
一个用户请求进入推理服务后,系统到底经历了什么?
在普通应用开发里,请求通常是一次性处理并返回结果。比如图像分类、文本分类或者普通后端接口,请求进入服务,模型或业务逻辑执行一次,然后返回结果。
但 LLM 推理不一样。
大语言模型是自回归生成的。它不是一次 forward 就直接返回完整答案,而是先处理用户输入的 prompt,然后一个 token、一个 token 地生成输出。每生成一个 token,这个 token 又会成为下一步生成的输入。
所以 LLM 推理系统本质上不是一个简单的模型调用,而是一个持续运行的生成过程。
一个请求进入系统后,大致会经历下面几个阶段:

这篇文章先不深入某一个 kernel,而是从请求生命周期出发,理解 LLM 推理系统的整体流程。
2. 从文本到 token:请求进入系统
用户输入的内容通常是一段自然语言,例如:
text
请解释一下 vLLM 中 KV Cache 的作用。
对推理系统来说,这段文本不能直接送进模型。模型真正处理的是 token id,也就是数字序列。
因此,请求进入服务后,第一步通常是 tokenizer。
Tokenizer 会把文本切分并编码成 token,例如:
text
"请解释一下 vLLM 中 KV Cache 的作用"
→ [token_1, token_2, token_3, ...]
这些 token 会作为模型的输入。
在推理服务中,请求本身通常还会携带一些生成参数,比如:
text
max_tokens
temperature
top_p
stop words
streaming
request_id
这些参数会影响后续生成过程,例如最多生成多少 token、采样是否随机、是否流式返回等。
LLM 推理不是一次 forward
传统模型推理往往是:
text
输入 → 模型 forward → 输出
但 LLM 的生成过程更像这样:
text
输入 prompt
↓
模型处理 prompt
↓
生成第一个 token
↓
把新 token 接回输入
↓
生成下一个 token
↓
重复直到结束
这就是自回归生成。
如果用更抽象的方式表示,就是:
text
P(next_token | previous_tokens)
模型每一步都根据已有 token 预测下一个 token。
这会带来一个很重要的系统问题:
一个 LLM 请求不会只执行一次模型计算,而是会在系统中停留很多轮。
如果用户要求生成 200 个 token,那么这个请求至少要经历 200 轮 decode。
这也是为什么 LLM Serving 需要调度器、KV Cache、显存管理和持续批处理。它不是简单地把模型封装成一个接口,而是要管理一个持续生成、不断读写状态的过程。
3. Prefill、KV Cache 与 Decode

LLM 推理通常可以分成两个阶段:
text
Prefill 阶段
Decode 阶段
这两个阶段的计算特征和系统瓶颈不同,也是理解推理优化的基础。
Prefill:处理用户输入的 prompt
Prefill 是第一个阶段。它的任务是处理用户输入的整段 prompt。
假设用户输入长度是 S,隐藏维度是 D,那么 prefill 阶段会把整段 prompt 一次性送入模型,计算每一层的 attention、MLP 等结果。
在 attention 里,每个 token 都会生成自己的 Q、K、V。对于后续生成来说,最重要的是 K 和 V,因为它们会被保存下来,作为历史上下文。
这就是 KV Cache 的来源。
Prefill 的特点是:
TTFT,也就是 Time To First Token,指的是用户发出请求后,到系统返回第一个 token 所需的时间。
如果 prompt 很长,prefill 就会很重,TTFT 通常也会变高。
KV Cache:把历史上下文保存下来
如果没有 KV Cache,每生成一个新 token,模型都需要重新处理前面所有 token。
比如 prompt 有 1000 个 token,现在要生成第 1001 个 token。下一步生成第 1002 个 token 时,如果没有缓存,就要再次重新计算前 1001 个 token 的 K/V。
这会造成大量重复计算。
KV Cache 的核心思想是:
已经计算过的历史 token 的 K/V 不要重复算,保存下来,后续 decode 阶段直接读取。
在 prefill 阶段,系统会为 prompt 中的 token 生成并保存 KV。
在 decode 阶段,每生成一个新 token,也会把这个 token 对应的 KV 追加到 cache 中。
所以 KV Cache 是 LLM 推理系统中非常核心的状态。
它同时影响:
text
显存占用
decode 速度
batching 能力
最大上下文长度
并发请求数量
这也是为什么 vLLM 会围绕 KV Cache 做 PagedAttention、block table、prefix cache 等设计。
Decode:逐 token 生成
Prefill 完成后,请求进入 decode 阶段。
Decode 阶段每一轮通常只处理一个新 token。流程大概是:
text
当前 token 输入模型
↓
读取历史 KV Cache
↓
计算 attention
↓
得到 logits
↓
采样或选择下一个 token
↓
把新 token 的 KV 写入 KV Cache
↓
继续下一轮
Decode 的特点和 prefill 很不一样:
TPOT,也就是 Time Per Output Token,表示每生成一个输出 token 的平均时间。
从用户体验看:
- TTFT 决定"多久看到第一个 token"
- TPOT 决定"后续 token 输出得快不快"
一个推理服务系统不能只优化其中一个。
如果 TTFT 很低但 TPOT 很高,用户会觉得后续输出很慢。
如果 TPOT 很低但 TTFT 很高,用户会觉得系统迟迟没有响应。
Decode 为什么容易成为瓶颈
Decode 每一步只生成一个 token,看起来计算量比 prefill 小很多,但它有一个关键问题:
每一步都要读取历史 KV Cache。
随着上下文变长,历史 KV 越来越大。每一轮 decode 都要让当前 token 的 Q 去和历史 K 做 attention,再和历史 V 做加权求和。
所以 decode 阶段经常不是单纯的计算瓶颈,而是容易受到显存读取、cache layout、batch size 和 GPU 利用率影响。
这也是为什么很多 LLM 推理优化会关注:
text
KV Cache 布局
PagedAttention
GQA / MQA
KV Cache Quantization
FlashDecoding
Continuous Batching
这些技术本质上都在试图降低 decode 阶段的访存压力,或者提高 GPU 在 decode 阶段的利用率。
4. 多请求场景:Scheduler 与 Continuous Batching
如果一个系统里只有一个请求,那么流程很简单:
text
prefill → decode → decode → decode → finished
但真实推理服务中,请求是不断到来的。
比如:
text
请求 A:prompt 很长,要生成 200 token
请求 B:prompt 很短,要生成 30 token
请求 C:刚刚进入系统
请求 D:已经 decode 到一半
系统必须决定:
下一轮到底让哪些请求运行?
这就是 scheduler 的作用。
Scheduler 需要考虑很多因素:
text
哪些请求还在 waiting
哪些请求正在 running
KV Cache block 是否足够
这一轮 token budget 是否够
prefill 和 decode 谁优先
是否有请求已经 finished
如何平衡吞吐和延迟
所以 vLLM 这样的系统不是简单地调用一次模型,而是在每一个 iteration 里动态决定哪些请求进入执行。
这就是 continuous batching 的基础。
Continuous Batching:动态维护 batch
传统 batching 往往是一批请求一起开始,一起结束。
但 LLM 请求长度不同、生成长度不同,如果强行 static batching,会出现明显浪费:
text
短请求已经结束,但还要等长请求
新请求来了,但进不了当前 batch
GPU 一部分资源空着
整体吞吐下降
Continuous Batching 的思想是:
每一轮 decode 都重新维护 batch。完成的请求可以退出,新来的请求可以加入,正在生成的请求继续执行。
这样系统就不要求所有请求同生共死,而是动态地利用 GPU 资源。
这也是 vLLM 相比普通推理封装更复杂的地方。它需要把请求调度、KV Cache 管理、block table、attention kernel 都组织起来。
Streaming Output:边生成边返回
很多 LLM 服务会一边生成,一边把 token 返回给用户。
这就是 streaming output。
Decode 每生成一个 token,系统就可以把它解码成文本片段并返回:
text
token_1 → 返回
token_2 → 返回
token_3 → 返回
...
这对用户体验很重要。即使完整答案需要几秒钟,用户只要很快看到第一个 token,就会感觉系统响应更快。
所以流式输出和 TTFT、TPOT 都有关:
text
TTFT:多久看到第一个 token
TPOT:后续 token 输出间隔
5. 总结:LLM 推理系统是在平衡计算、显存和调度

从一个请求的生命周期看,LLM 推理系统至少包含几条主线:
vLLM 的核心价值,就在于它不是简单封装一个 Transformer forward,而是围绕 LLM 推理的真实瓶颈做系统设计。
后续继续阅读 vLLM 源码时,可以沿着这条线展开:
text
请求如何进入 engine?
Scheduler 如何选择请求?
KV Cache 如何分块管理?
PagedAttention 如何读取非连续 KV?
Prefix Cache 如何复用相同前缀?
Decode 阶段为什么容易 memory-bound?
对我来说,理解 vLLM 的第一步,不是直接看 PagedAttention kernel,而是先理解:
一个请求进入系统后,为什么会变成一个持续调度、持续读写 KV Cache、持续生成 token 的过程。
这也是整个 LLM 推理系统的起点。
一句话串讲请求生命周期
如果要用一段话把整条链路串起来:
一个请求进入 LLM 推理服务后,首先会经过 tokenizer,把文本转成 token id。然后进入 prefill 阶段,模型会一次性处理整段 prompt,并生成每一层的 KV Cache。Prefill 主要影响首 token 延迟,也就是 TTFT。
Prefill 之后,请求进入 decode 阶段。Decode 是逐 token 生成的,每一轮会用当前 token 的 query 去读取历史 KV Cache,计算 attention,得到 logits,然后采样出下一个 token。新 token 对应的 KV 会继续追加到 KV Cache 中。
在多请求场景下,系统还需要 scheduler。因为不同请求的 prompt 长度和生成长度不同,vLLM 这类系统会使用 continuous batching,在每一轮 iteration 动态决定哪些请求进入执行。完成的请求退出,新请求加入,从而提高 GPU 利用率和整体吞吐。
所以 LLM 推理服务不只是一次模型 forward,而是一个同时涉及请求调度、KV Cache 管理、显存分配、decode 循环和流式输出的系统。