一个请求在 vLLM 里的一生:从 HTTP 到 token streaming
TL;DR
- 场景:想把 vLLM 当成「给模型包个 HTTP 包装」用,结果线上并发上来后才发现它在干一件完全不同的事 ------ 围绕 token、显存和调度持续运转的系统。
- 结论 :一个请求的真实链路是
API Server → Tokenizer → Waiting Queue → Scheduler → Prefill → KV Cache Manager → Decode Loop → Sampler → Streamer,每一段都有自己的瓶颈;理解这段链路,比死记--max-num-seqs、--gpu-memory-utilization重要得多。 - 产出 :一张覆盖 12 节的请求生命周期导图 + 25 项能力版本矩阵 + 10 行错误速查卡,可直接对照
vllm serve启动参数、curl /v1/chat/completions请求、SSE 流式返回做排障。
版本矩阵
| # | 功能 / 主题 | 状态 | 说明 |
|---|---|---|---|
| 1 | OpenAI-Compatible API(/v1/chat/completions、/v1/completions、/v1/embeddings) |
✅ 已验证 | vLLM 完整映射 Chat/Completion/Embedding,流式 + 采样参数全覆盖 |
| 2 | Continuous Batching(iteration-level) | ✅ 已验证 | vLLM 核心,Scheduler 每 step 动态决定 running set |
| 3 | PagedAttention | ✅ 已验证 | 默认 block size 16 token,block table 维护逻辑→物理映射 |
| 4 | Chunked Prefill | ✅ 已验证 | 0.6.5+ 默认开启,--enable-chunked-prefill 显式开关;与 decode step 混批 |
| 5 | Automatic Prefix Caching | ✅ 已验证 | v1 zero-overhead prefix caching,长文本命中率极低也不会回退 |
| 6 | Guided Decoding(JSON Schema / Regex / EBNF / choice) | ✅ 已验证 | --guided-decoding-backend 可选 FSM/xgrammar/outlines |
| 7 | Speculative Decoding | ✅ 已验证 | EAGLE / Medusa / n-gram prompt lookup 三种草稿方式 |
| 8 | Multi-LoRA | ✅ 已验证 | enable_lora=True + max_lora_rank,支持运行时切换 adapter |
| 9 | OpenAI Streaming SSE(stream=true) |
✅ 已验证 | 流式 chunk + chat template 多轮 |
| 10 | Function Calling / Tool Use | ✅ 已验证 | ≥0.8.3 内置,结构化 tool 描述 |
| 11 | 多 GPU 并行(TP / PP / SP) | ✅ 已验证 | 张量并行 + 流水线并行 + 序列并行,万亿参数模型可扩展 |
| 12 | TTFT / TPOT / P95/P99 / queue time metrics | ✅ 已验证 | 内置 Prometheus 指标,可对接 Grafana |
| 13 | Preemption(recompute / swap) | ✅ 已验证 | 显存不足时按优先级抢占 |
| 14 | P/D Disaggregation | ✅ 已验证 | 0.7+ 独立 prefill / decode 节点,KV 跨节点传输 |
| 15 | Multi-Step 调度 | ✅ 已验证 | 减少 GPU→CPU 同步次数,提升吞吐但 TTFT/ITL 略增 |
| 16 | KV Cache 量化(FP8 / INT8 / INT4) | ✅ 已验证 | --kv-cache-dtype 可选 fp8 / int8 |
| 17 | Tokenizer 并行化 | ✅ 已验证 | --tokenizer-pool-size 起多个 tokenizer 进程 |
| 18 | Streaming 取消(客户端断开 / 超时) | ✅ 已验证 | 取消请求时及时释放 KV block,避免泄漏 |
| 19 | Prefix Cache 哈希 + LRU 淘汰 | ✅ 已验证 | hash 复用已计算 block,LRU 驱逐冷 block |
| 20 | vLLM V1 引擎 | ✅ 已验证 | 2025 默认 V1,V0 已弃用;V1 = zero-overhead prefix caching + 统一调度 |
| 21 | token budget(--max-num-batched-tokens) |
✅ 已验证 | 限制每轮 prefill+decode 总 token 数 |
| 22 | Logits Processor(repetition / presence / frequency penalty) | ✅ 已验证 | SamplingParams 内置 |
| 23 | Cancel / timeout / 抢占 链路 | ✅ 已验证 | Engine Core 统一处理 |
| 24 | usage 统计(prompt_tokens / completion_tokens / total_tokens) |
✅ 已验证 | 流式尾部 chunk 返回 usage 字段 |
| 25 | Anatomy of vLLM 系统拆解博客(2025-09-05) | ✅ 已验证 | Aleksa Gordić 主笔,commit 42172ad 为分析基准 |
备注:版本矩阵全部基于 vLLM 官方文档、Anatomy of vLLM 博客(2025-09-05)、vllm-project/vllm GitHub 主分支(commit 42172ad 之后)核查;未单独标注「⚠️」的条目均为多源印证后的稳定事实。

理解 vLLM 最好的方式,不是先背配置参数,而是跟踪一个请求从进入服务到流式返回 token 的全过程。
一个 LLM 请求表面上只是 HTTP 调用,实际内部会经过 API Server、Tokenizer、请求队列、Scheduler、KV Cache Manager、Model Executor、Decode Loop、Sampler、Streamer 等多个模块。vLLM 的价值就在于把这些环节组织成一个持续运行的高吞吐系统。
如果只把 vLLM 看成"把模型包成 OpenAI API 的服务",很容易低估它真正解决的问题。普通 HTTP wrapper 做的是请求转发和模型调用;vLLM 这类推理框架做的是持续调度、动态 batching、KV Cache 管理和 token streaming。
这篇文章就沿着一个请求走一遍:它从 HTTP 入口进来,如何被 tokenizer 变成 token,如何进入 waiting queue,如何被 scheduler 安排 prefill 和 decode,如何占用和释放 KV Cache,最后又如何通过 streaming 返回给客户端。
一、入口:请求进入 API Server

用户请求通常通过 OpenAI-compatible API 进入服务,比如 /v1/chat/completions 或 /v1/completions。
请求里包含模型名、messages 或 prompt、temperature、top_p、max_tokens、stream、stop、seed 等参数。API Server 首先做协议层处理:解析请求、校验参数、构造内部 request object。如果是 chat 接口,还要把 messages 应用 chat template,转成模型实际输入文本。
这里有一个容易被忽略的点:HTTP 层不是瓶颈时,它看起来不重要;一旦并发上来,API Server、JSON 序列化、SSE 返回、连接数、网关超时和客户端取消都会影响尾延迟。
所以高性能 LLM Serving 不是只有 GPU。入口层也要稳定。
API Server 之后,请求文本要经过 tokenizer。模型不能直接处理字符串,必须先转成 token ids。Tokenizer 往往运行在 CPU 侧。对于高并发短请求,tokenizer 本身也可能成为瓶颈,尤其是 chat template 很复杂、messages 很长、并发很高时。
Tokenizer 完成后,系统就知道了输入 token 数。这个数字对调度非常关键:
- 输入 token 数决定 prefill 成本;
- 输入 token 数影响 KV Cache 初始占用;
- 输入 token 数决定是否超过最大上下文长度;
- 输入 token 数影响是否应该做 chunked prefill;
- 输入 token 数会进入 usage 统计和容量规划。
好的 Serving 系统不会只把请求看成"一个请求",而会把它看成"多少 prefill token + 预计多少 decode token + 什么优先级 + 什么采样约束"。
二、等待队列:请求不一定马上上 GPU

完成预处理后,请求进入 waiting queue。
这一步很关键。请求进入队列,不代表已经获得 GPU 执行资源,也不代表已经拿到完整 KV Cache。Scheduler 会在每一轮调度时决定哪些 waiting 请求可以进入执行。
等待队列不是简单 FIFO。生产系统里常见策略包括:
| 策略 | 目的 |
|---|---|
| 交互式请求优先 | 控制用户感知延迟 |
| 短 prompt 优先 | 降低平均 TTFT |
| 高优先级租户优先 | 支持业务等级和 SLA |
| 接近超时的请求优先 | 减少无意义排队 |
| 长 prompt 分块进入 | 避免阻塞 decode |
| 超过 token budget 的请求继续等待 | 防止本轮 GPU 负载失控 |
vLLM 的调度器会结合 token budget、running requests、KV Cache 可用 block 等因素决定本轮执行内容。也就是说,请求能不能执行,不只取决于"GPU 现在忙不忙",还取决于"这一轮还有多少 token 预算""KV Cache block 是否够""已有 running 请求是否需要继续 decode"。
这也是普通 Web 服务和 LLM Serving 的差异之一。普通 Web 服务里,请求通常进入 worker 后一次性处理完成;LLM Serving 里,请求可能在系统中存在很久,每个 decode step 都要重新参与调度。
三、Scheduler 是整个系统的大脑

Scheduler 每一轮要做的事情很多。
它要决定哪些 running 请求继续 decode。它要决定是否从 waiting queue 中加入新请求做 prefill。它要检查 KV Cache block 是否足够。它要控制本轮总 token 数,不让 GPU 负载失控。它要处理完成请求,释放资源。它还要处理被取消、超时、抢占的请求。
普通 HTTP wrapper 往往是"一个请求进来,调用一次 model.generate(),等结果返回"。而成熟 LLM Serving 的调度发生在 token 级别或者 step 级别。因为每个请求不是一次性完成,而是在 decode loop 中持续存在。
可以把 Scheduler 看成系统的大脑:
- waiting queue 提供候选请求;
- running set 保存正在生成的请求;
- KV Cache Manager 告诉它显存 block 是否够;
- token budget 限制本轮 prefill / decode 工作量;
- policy 决定优先级、抢占、拒绝和超时;
- metrics 反馈 TTFT、TPOT、queue time 和 P99。
Scheduler 的好坏,直接决定吞吐、首 token 延迟、流式稳定性和尾延迟。很多 vLLM 参数看似零散,本质上都是在影响这套调度系统:允许多少 running requests、每轮 token budget 多大、prefill 是否切块、是否开启 prefix caching、KV cache 使用率到什么程度开始抢占。
理解 Scheduler,比死记参数更重要。
四、Prefill:建立请求的 KV Cache

新请求第一次进入 GPU 执行时,通常先做 prefill。
Prefill 会把输入 prompt 喂给模型,计算所有输入 token 的隐藏状态,并为每一层 attention 建立 KV Cache。Prefill 结束后,模型可以产生第一个输出 token,或者至少准备好进入 decode 阶段。
TTFT 很大程度受 prefill 影响。prompt 越长,prefill 越重。RAG、Agent、长对话、代码仓库问答、长文档分析都会让 TTFT 增加。
如果一个超长 prompt 一次性 prefill,会阻塞其他 decode 请求。对于已经在流式输出的用户来说,这会表现为 token 间隔突然变长;对于新请求来说,会表现为首 token 等待变久。
为了缓解这个问题,vLLM 支持 chunked prefill,把长 prompt 切成多个块,让调度器可以在 prefill 块之间插入 decode。这样长 prompt 不会一次性独占 GPU,在线交互的 P95/P99 更容易控制。
对于语音对话系统,chunked prefill 很关键。用户听到首字的时间比总吞吐更敏感。一个长上下文请求不应该把整张 GPU 卡住,让所有人的语音回复都变慢。
Prefill 过程中,还需要为输入 token 分配 KV Cache block。Decode 过程中,每生成新 token,也要追加新的 KV block 或写入已有 block。
这就进入了 KV Cache Manager 的职责范围。
五、KV Cache Manager:调度必须看显存账本
KV Cache Manager 的职责包括:
- 维护全局可用 block pool;
- 为请求分配物理 block;
- 维护请求的 block table;
- 请求结束后回收 block;
- 支持共享前缀缓存;
- 在资源不足时配合 scheduler 做抢占、等待、swap/offload 或拒绝。
如果 KV Cache block 不够,请求不能继续执行。此时系统可能让请求等待,或者抢占低优先级请求,或者触发 swap/offload,或者直接返回资源不足。
所以调度器不是只看 GPU 是否空闲,还必须看 KV Cache 是否够。
这也是很多自研系统容易踩的坑。只看 GPU util,可能以为"还有算力";但 KV Cache 已经接近上限,新的长上下文请求根本进不来。只看显存总占用,也不够;你还需要看 used/free blocks、prefix cache 命中率、preemption 次数、waiting queue 长度和 token 长度分布。
LLM Serving 的资源不是静态的。请求进入、增长、取消、结束、共享前缀、分叉采样,都会改变 KV Cache 账本。Scheduler 和 KV Cache Manager 必须协同工作。
六、Decode Loop:逐 token 生成

Prefill 后,请求进入 decode 阶段。
Decode 是一个循环。每一轮,每个 running 请求通常生成一个 token。生成后,系统更新 KV Cache,把 token 送入采样逻辑,再判断是否结束。
结束条件包括:
- 生成了 EOS token;
- 命中 stop sequence;
- 达到
max_tokens; - 客户端取消;
- 请求超时;
- 策略约束停止。
每一轮 decode 后,Scheduler 都可以重新组织 batch。完成的请求被移除,新请求可以加入。这就是 continuous batching。
对用户来说,decode 阶段表现为 token streaming。模型一边生成,服务一边通过 SSE 或其他流式协议返回。
这里也能看到 vLLM 和普通 wrapper 的差异。普通 wrapper 往往把生成看成一次函数调用;vLLM 把生成看成持续循环中的很多 step。每个 step 都可能重新组合 batch、释放完成请求、加入新请求、追加 KV block、处理取消和 stop 条件。
这就是高吞吐 Serving 的核心:让 GPU 持续有活干,同时不让长输出请求把短请求拖死。
七、Sampler:采样不是小事
模型输出 logits 后,还要经过采样策略产生 token。
常见参数包括 temperature、top_p、top_k、repetition penalty、presence penalty、frequency penalty、guided decoding、grammar constraint 等。
采样通常不是最大的算力瓶颈,但会影响系统复杂度。尤其是结构化输出、JSON schema、正则约束、函数调用格式约束,会给 decode 阶段增加额外逻辑。
如果不同请求的采样参数差异很大,batching 的实现也会更复杂。成熟框架需要支持同一 batch 中不同请求有不同采样参数。
这也是为什么"所有请求一起 batch"在 LLM Serving 里并不简单。请求的 prompt 长度、输出长度、采样参数、stop 条件、streaming 状态、优先级都可能不同。
八、Streamer:把 token 持续返回给客户端

如果 stream=true,服务端不会等完整回答生成完,而是每产生一个 token 或一小段文本就返回。
这对用户体验很重要。即使总生成时间是 5 秒,只要 300ms 内看到首 token,用户感知会好很多。
但流式返回也增加系统负担:
- 连接会持续占用;
- 网络层要处理大量小包或 SSE chunk;
- 客户端取消后,服务端要及时停止生成并释放 KV Cache;
- 如果后端生成快但前端消费慢,可能出现背压;
- 网关、负载均衡和客户端超时都要处理长连接。
所以 Streamer 不是简单 print(token),而是 Serving 系统的一部分。
请求结束后,系统还要做清理:释放 KV Cache block,从 running set 移除请求,关闭 stream,记录 metrics,返回最终 usage 信息,如 prompt_tokens、completion_tokens、total_tokens。
如果启用了 prefix caching,请求的一部分 KV Cache 不一定立即释放,而是进入缓存池等待复用。如果启用了 LoRA 或多模型服务,还可能释放 adapter 相关状态。
资源释放是否及时,直接影响系统能否承载下一批请求。很多自研系统的线上问题不是"执行慢",而是取消请求、异常请求、超时请求后资源没有正确释放。
九、vLLM 请求链路的简化图
可以把整个链路理解成:
text
Client
-> API Server
-> Tokenizer
-> Waiting Queue
-> Scheduler
-> Prefill
-> KV Cache Manager
-> Decode Loop
-> Sampler
-> Streamer
-> Client
但这条链路不是一次性直线,而是一个循环系统。
Prefill 把请求带入系统。Decode 让请求在系统中持续存在。Scheduler 每轮重新决定执行集合。KV Cache Manager 决定资源是否足够。Streamer 持续把结果吐出去。
真正理解 vLLM,就要理解这条循环。
十、哪些地方最容易成为瓶颈

第一,长 prompt 导致 TTFT 高。解决方向是 chunked prefill、prefix caching、prompt 压缩、RAG 控制和上下文裁剪。
第二,decode 并发过高导致 TPOT 变差。解决方向是限制 running requests、优化 batch size、扩 decode GPU、降低上下文长度。
第三,KV Cache 不足导致排队和抢占。解决方向是减少 max_model_len、使用 paged cache、限制 max_tokens、量化 KV、增加显存。
第四,CPU 侧 tokenizer 或 scheduler 成为瓶颈。解决方向是 tokenizer 并行化、减少 chat template 开销、优化调度器实现。
第五,流式连接过多导致网关压力。解决方向是连接池、网关限流、超时控制、合理 chunk flush 和及时处理取消。
这些瓶颈对应到指标上,可以看:
| 指标 | 可能问题 |
|---|---|
| TTFT 高 | prefill 重、排队久、tokenizer 慢 |
| TPOT 高 | decode 拥挤、上下文长、KV 读取压力大 |
| queue time 高 | scheduler 预算不足、KV block 不够 |
| preemption 多 | KV Cache 压力大或优先级策略激进 |
| stream cancel 多 | 前端超时、用户等待过久、网关策略不匹配 |
十一、对自研系统的启发
如果自己设计 Serving,不要只写一个 model.generate() 包装 HTTP。
至少要拆出这些模块:请求解析、tokenizer、等待队列、调度器、KV Cache 管理器、执行器、采样器、streamer、metrics、限流和取消机制。
其中最关键的是 Scheduler 和 KV Cache Manager。前者决定吞吐和延迟,后者决定并发上限和稳定性。
好的系统还需要把指标和链路对齐。不要只看 GPU util,也不要只看平均 tokens/s。要把 TTFT、TPOT、P95/P99、queue time、KV Cache usage、waiting/running requests、token 长度分布、取消率和错误率放在一起看。
否则调参只是在碰运气。
十二、核心结论
vLLM 的请求生命周期不是简单的函数调用,而是一个持续调度系统。
请求进入后,先经过 API Server、Tokenizer 和 waiting queue,再由 Scheduler 安排 prefill 和 decode。KV Cache Manager 管理显存 block,Decode Loop 每轮生成 token,Sampler 决定下一个 token,Streamer 持续返回结果。请求结束后,系统还要及时释放资源、记录 metrics、处理 usage 和缓存复用。
理解这条链路,才能真正理解 vLLM 的参数、瓶颈和优化方向。
当你把一个请求从 HTTP 追踪到 token streaming,再追踪到资源释放,你会发现:LLM Serving 不是"模型调用",而是一个围绕 token、显存、调度和连接持续运转的系统。
参考资料
- vLLM Documentation: docs.vllm.ai/
- vLLM Blog, Inside vLLM: Anatomy of a High-Throughput LLM Inference System: vllm.ai/blog/2025-0...
- vLLM Documentation, Chunked Prefill: docs.vllm.ai/en/latest/p...
- vLLM Documentation, Automatic Prefix Caching: docs.vllm.ai/en/latest/f...
- vLLM Documentation, OpenAI-Compatible Server: docs.vllm.ai/en/latest/s...
- vLLM Documentation, Offline Inference / LLM Engine Examples: docs.vllm.ai/en/latest/g...
- Paper, Efficient Memory Management for Large Language Model Serving with PagedAttention: arxiv.org/abs/2309.06...
- Hugging Face Text Generation Inference: github.com/huggingface...
3. 错误速查卡
| # | 症状 | 根因 | 定位方法 | 修复方案 |
|---|---|---|---|---|
| 1 | TTFT P99 飙到秒级 | prefill 重 / 排队久 / tokenizer 慢 | 看 vllm:request_success_total{finished_reason="stop",ttft_bucket} 分位 + Prometheus TTFT 曲线 |
开 chunked prefill、--enable-prefix-caching、限制 max_model_len、--tokenizer-pool-size 提到 4-8 |
| 2 | TPOT 抖动 / decode step 间隔变大 | decode 拥挤 / 长上下文 KV 读压 / prefill 抢断 decode | 监控 TPOT P95/P99 + 观察单请求 decode step 间隔 | 限制 --max-num-seqs、降低 max_model_len、扩 decode GPU |
| 3 | queue time 累计上升 / 新请求迟迟不进 | scheduler token budget 不够 / KV block 不足 | 看 e2e_request_latency_seconds - ttft - tpot、queue depth 指标 |
提 --gpu-memory-utilization 到 0.9、量化 KV、降并发或加卡 |
| 4 | preemption 频繁(recompute / swap) | KV 压力大 / 优先级策略激进 | 监控 preemptions_total{reason=}、swap / recompute 计数 |
加 max_tokens 上限、启用 prefix caching、扩显存 |
| 5 | stream cancel 率飙升 | 网关超时 / 用户等待过久 / 客户端 SDK retry | cancel reason 分布 + 网关 504 / 499 计数 | 调网关 timeout、流式 chunk flush、加速 TTFT |
| 6 | tokenizer CPU 单核满 | 高并发 + 复杂 chat template | 单核 CPU util 100% + tokenizer 阶段耗时陡升 | --tokenizer-pool-size 提到 4-8、简化 chat template |
| 7 | 长 prompt 一次性 prefill 阻塞 decode | 没开 chunked prefill + max_num_batched_tokens 偏大 |
decode step 间隔出现几十秒级长尾 | 开 chunked prefill、限制 --max-num-batched-tokens |
| 8 | prefix cache 命中率长期低于 10% | prompt 结构不稳定 / prompt 模板随机化 | prefix cache hit rate 指标 + hash 分布 | 稳定 system prompt + 用户前缀,开启 prefix caching |
| 9 | OOM / KV block 不足报错 | 显存规划不足 / max_model_len 太大 |
KV cache usage > 90%、OOM 异常 |
减 max_model_len、限 max_tokens、--kv-cache-dtype fp8 |
| 10 | 网关 / LB 连接耗尽 | 流式连接占用过多 + 客户端慢消费 | 网关 active connection + SSE chunk 大小 | 网关超时、限流、客户端 SDK backoff |
作者:武子康的个人博客