GPU 为什么能快,也为什么容易被大量小请求拖慢?
这篇是整个 GPU Serving 系列的地基。在讨论 vLLM、SGLang、TensorRT-LLM、TGI、PagedAttention、continuous batching 这些框架之前,先回到一个更底层的问题:GPU 到底喜欢什么样的工作负载?LLM 请求为什么和普通 Web 请求不一样?为什么线上来了大量请求后,不能简单地"一个请求开一个线程,然后丢给 GPU"?
TL;DR
- 场景:LLM 在线推理服务上线后,GPU 利用率波动大、TTFT / P99 失控、显存监控看似还有余量但请求被卡住。
- 结论:GPU 快不快,不取决于"单个请求被处理得多快",而取决于"调度系统是否把随机请求组织成了 GPU 擅长的执行形状"------大批量、低空转、规整张量、可控 KV Cache、稳定节奏。
- 产出:CPU/GPU 节奏对照表 + 五类瓶颈定位指标 + Serving Loop 心智模型;以及一份把 8 个常见反模式一次性扫掉的错误速查卡。
版本矩阵
| 机制 / 动作 | 状态 | 说明 |
|---|---|---|
| dynamic batching(短窗口凑批) | ✅ 已验证 | vLLM / TGI / SGLang 均默认开启;HF Transformers 文档将其列为标准推理加速手段 |
| continuous batching(请求级迭代 batch) | ✅ 已验证 | HF Transformers 官方文档 + vLLM blog 2025-09-05 明确说明 |
| PagedAttention / paged KV Cache | ✅ 已验证 | vLLM 论文 arXiv:2309.06180(2023 起);2025--2026 仍是 KV 管理事实标准 |
| chunked prefill(长 prompt 切块) | ✅ 已验证 | vLLM / SGLang 当前主推;用于防止长 prefill 拉爆 TTFT |
| prefix caching(重复上下文复用) | ✅ 已验证 | vLLM / SGLang / TGI 均原生支持;系统提示词 / RAG 文档场景收益最明显 |
| prefill / decode 分离(disaggregated) | ✅ 已验证 | vLLM 官方文档 disagg_prefill 章节;2024--2025 落地加速 |
| TTFT / TPOT / P95 / P99 作为线上核心指标 | ✅ 已验证 | vLLM / TGI / TensorRT-LLM / SGLang 通用 |
| KV Cache 用量作为真正的动态瓶颈 | ✅ 已验证 | vLLM / SGLang 文档反复强调"显存监控不等于 KV 余量" |
| 仅以"GPU Util"判断服务健康 | ⚠️ 反模式 | 文章第五节列出 5 类瓶颈,GPU Util 仅是其中之一 |
| 单请求独立 generate,线上生产可用 | ❌ 反模式 | 文章第六节给出具体崩塌路径 |

这篇是整个 GPU Serving 系列的地基。很多人一讨论大模型推理服务,就会很快跳到 vLLM、SGLang、TensorRT-LLM、TGI、PagedAttention、continuous batching 这些框架和名词。但在理解框架之前,更值得先问一个底层问题:GPU 到底喜欢什么样的工作负载?LLM 请求为什么和普通 Web 请求不一样?为什么线上来了大量请求后,不能简单地"一个请求开一个线程,然后丢给 GPU"?
结论先放在前面:GPU 高效处理大量请求的关键,不是让每个请求单独跑得更快,而是把很多随机到达、长度不一、生命周期不一致的请求,组织成 GPU 喜欢的执行形状。这个"形状"主要包含四件事:
- 足够大的并行度;
- 较少的空转和调度碎片;
- 可控的显存和 KV Cache 占用;
- 稳定、可预测、能被策略控制的调度节奏。
如果把 LLM Serving 看成"HTTP 服务收到请求,然后调用模型 generate",很容易低估它的复杂性。它真正难的地方,不在于调用一次模型,而在于持续地把随机请求整理成 GPU 能高效执行的批次,同时又不让用户的首 token 等太久、不让尾延迟爆掉、不让 KV Cache 被长上下文和高并发挤穿。
一、CPU 和 GPU 处理请求的思路完全不同

CPU 擅长处理复杂控制流。一个普通 Web 服务收到请求后,可以为每个请求分配线程、协程或者事件循环任务。请求之间的分支逻辑、IO 等待、数据库访问、异常处理、超时重试,都可以由 CPU 比较灵活地调度。对于 CPU 来说,任务可以很碎,控制流可以很复杂,单个请求的生命周期也可以非常不规则。
GPU 的优势不在这里。GPU 擅长的是大规模数据并行。它希望一次性拿到足够大的矩阵乘法、向量运算或者张量操作,然后让大量计算单元同时工作。换句话说,GPU 喜欢的是"同一类工作一起做",而不是"每个请求带着自己独立的分支和节奏来敲门"。
所以一个朴素误区是:既然 GPU 很快,那每来一个请求就立刻丢给 GPU 执行。这个思路在低并发、demo 或者离线调试里可能看起来没问题,但在线上服务里,效果往往很差。因为单个请求的矩阵规模不够大,kernel launch 的固定开销占比变高,GPU SM 利用率上不去,显存带宽也无法形成稳定吞吐。
可以把它想成大货车送货。GPU 像一辆大货车,它能一次运很多货,所以单位成本很低。但如果每次只让它送一包纸巾,还要求它不断启动、停车、掉头、再启动,那大货车的优势就被浪费掉了。
CPU 的调度粒度可以很细;GPU 的效率来自批量、规整、连续和高并行。做 GPU Serving 的第一件事,就是放下"请求线程"的直觉,改用"执行批次"的直觉。
二、为什么大量小请求会拖慢 GPU

在线推理中的请求通常是碎片化的。用户不是整齐地每 100 毫秒来一批,而是随机到达。每个请求的 prompt 长度不同、输出长度不同、采样参数不同、是否流式返回也不同。
如果每个请求单独执行,GPU 会遇到几个问题。
第一,矩阵乘法规模太小。很多算子在 batch size 太小时无法吃满 GPU。单请求推理不是不能跑,而是成本结构很差。硬件能力在那里,但并行度没有被组织出来。
第二,kernel launch 过于频繁。GPU 执行不是普通函数调用那么轻量。每次从 CPU 侧发起 GPU kernel,都有调度、同步和提交成本。请求越碎,这些固定开销越容易在总耗时里占据可见比例。
第三,内存访问不连续。LLM 推理大量依赖权重读取和 KV Cache 读取。如果请求分布凌乱,batch 结构不断变化,显存访问的局部性就会变差。GPU 不是只怕算不动,也怕数据喂不上。
第四,请求生命周期不一致。A 请求生成 20 个 token 就结束,B 请求要生成 800 个 token,C 请求 prompt 很长但输出很短,D 请求短 prompt 但流式输出持续很久。传统固定 batch 会被最长请求拖住,短请求提前结束后,也不能立刻释放出全部效率。
这就是 GPU Serving 的核心矛盾:
| 一端 | 另一端 |
|---|---|
| 线上请求随机到达 | GPU 希望批量执行 |
| prompt / output 长度不齐 | GPU 希望张量形状规整 |
| 流式请求生命周期长短不同 | GPU 希望节奏稳定 |
| 用户关心首 token 和尾延迟 | 系统关心吞吐与成本 |
Serving 系统的工作,就是在这两端之间搭桥。
三、LLM 推理不是普通模型推理

图像分类模型通常是"一次输入,一次输出"。Embedding 模型也类似:输入一段文本,输出一个向量。这类任务天然适合动态批处理。服务端等待几毫秒,把多个请求合成一个 batch,跑一次前向计算,然后返回结果。
LLM 生成不一样。它是自回归的。模型不是一次性生成完整回答,而是每次生成一个 token,再把这个 token 接回上下文,继续生成下一个 token。
所以 LLM 请求至少分成两个阶段。
第一个阶段叫 Prefill。模型读取用户输入的 prompt,一次性计算整个 prompt 的隐藏状态,并建立 KV Cache。这个阶段通常计算量较大,特别是长 prompt、RAG 文档、多轮对话、Agent 工具上下文都会让 prefill 变重。Prefill 更接近"把已有上下文读懂并铺好后续生成所需的缓存"。
第二个阶段叫 Decode。模型每次只生成一个新 token。Decode 会反复读取已有 KV Cache,然后追加新的 KV。这个阶段的单步计算相对小,但要持续很多轮。很多时候 decode 不是纯计算瓶颈,而是显存带宽和 KV Cache 访问瓶颈。
因此 LLM Serving 不是:
text
request -> forward -> response
而更像:
text
request -> prefill -> decode step 1 -> stream token
-> decode step 2 -> stream token
-> decode step 3 -> stream token
-> ...
-> stop
这会带来一个很重要的工程后果:调度器不仅要决定"哪些新请求一起 prefill",还要持续决定"哪些正在生成的请求一起 decode"。新请求不断进来,旧请求不断生成,短请求不断结束,长请求还在占用 KV Cache。Serving 框架要在这个动态系统里保持吞吐、延迟和显存稳定。
这也是 continuous batching、chunked prefill、prefill/decode 分离这些技术出现的根源。它们不是在炫技,而是在处理 LLM 自回归生成带来的服务复杂性。
四、吞吐和延迟天然冲突

GPU Serving 永远绕不开两个指标:吞吐和延迟。
吞吐关注单位时间内系统能生成多少 token,比如 tokens/s、requests/s。吞吐高,意味着 GPU 被充分利用,单位成本低。
延迟关注单个用户等多久。LLM 里常见几个细分指标:
- TTFT:Time To First Token,用户等到第一个 token 的时间;
- TPOT:Time Per Output Token,后续每个 token 的平均生成间隔;
- P95 / P99:尾延迟,反映最慢那部分用户的体验;
- queue time:请求进入系统后,真正开始执行前等待了多久。
想提高吞吐,最直接的方法是扩大 batch,让 GPU 一次处理更多请求。但 batch 需要等待请求凑齐,也会让短请求被长请求影响。想降低延迟,最直接的方法是请求来了就立刻处理,但这样 GPU 可能吃不满。
所以 Serving 系统不是单纯追求"最快",而是在吞吐、TTFT、TPOT、P95、显存占用、成本之间做权衡。
一个面向内部批量任务的系统,可以牺牲 TTFT 换吞吐。用户不在屏幕前等待第一个 token,系统就可以更积极地凑 batch,让 GPU 尽可能满载。
一个语音对话系统,则不能让用户等 3 秒才听到首字。这里 TTFT 和流式稳定性会比极限 tokens/s 更重要。
一个 Agent 系统可能请求链路很长,单次 LLM 调用还会触发工具调用、检索、数据库读写和二次推理,因此更关心稳定尾延迟。一个 LLM 调用的 P99 爆掉,可能会被整个 Agent 链路放大成明显的用户等待。
所以同样是 GPU Serving,在线聊天、语音对话、离线批处理、Agent 工具链、RAG 问答的调度策略都不应该完全一样。
五、"显存没满"不代表 GPU 没瓶颈

很多工程师看 GPU 监控时,容易盯着两个数字:显存占用和 GPU Util。显存没满,不代表系统还有大量余量。GPU Util 高,也不代表系统效率高。
LLM 推理里常见几类瓶颈。
第一是计算瓶颈。Prefill 阶段经常更接近计算密集,因为它要对整个 prompt 做并行计算。长上下文、RAG 文档、多轮历史都可能把 prefill 压力拉高。
第二是显存带宽瓶颈。Decode 阶段需要频繁读取模型权重和 KV Cache。batch 较小、上下文较长、并发较高时,带宽压力会很明显。此时不是"算力不够",而是"数据搬运跟不上"。
第三是调度瓶颈。CPU 侧 scheduler、tokenizer、请求队列、网络流式返回都有可能成为瓶颈。GPU 很快,但 GPU 前面喂不饱,后面接不住,也会浪费。
第四是 KV Cache 容量瓶颈。不是模型权重放不下,而是并发请求的 KV Cache 放不下。请求越多、上下文越长、输出越长,KV Cache 越大。显存监控里看到的"还有余量",未必能支撑下一批长上下文请求。
第五是尾延迟瓶颈。平均延迟看起来很好,但部分长 prompt 或长输出请求把 P99 拉爆,用户体验仍然很差。
判断 GPU Serving 是否健康,至少要同时看:
| 指标 | 代表什么 |
|---|---|
| TTFT / TPOT / P95 / P99 | 用户体验和尾延迟 |
| tokens/s | 系统吞吐 |
| running requests / waiting requests | 当前并发与排队状态 |
| KV cache usage | 显存里真正稀缺的动态资源 |
| preemption 次数 | 调度器是否在频繁抢占和腾挪 |
| prefix cache hit rate | 重复上下文是否被复用 |
| batch size 分布 | GPU 是否稳定吃到足够批次 |
| 输入 / 输出 token 长度分布 | 负载是否被长尾请求拖慢 |
只看显存和 GPU Util,就像只看餐厅厨房面积和厨师是否在动。真正要看的是:前厅排队多长、备菜区是否堵住、出餐节奏是否稳定、最慢那桌客人等了多久。
六、最朴素的 GPU Serving 系统会怎么崩

假设我们自己写一个最简单的 LLM Serving:
text
HTTP 服务收到请求;
请求进入队列;
后端拿一个请求调用模型 generate;
生成完返回。
这个方案在低并发下能跑,但并发稍微上来就会暴露问题。
首先,GPU 没有被批量利用。每个请求单独执行,吞吐很低。看起来每个请求都被"及时处理",但 GPU 的并行能力没有真正释放。
其次,长请求会阻塞短请求。一个用户传了 20K token 的 RAG prompt,后面一堆短问题都要排队。对于用户来说,这种等待非常难解释:明明只是问了一个很短的问题,却被前面的长上下文拖住。
再次,显存管理粗糙。每个请求的 KV Cache 动态增长,释放时间不一致,很容易出现碎片和浪费。并发高时,KV Cache 往往比模型权重更像真正的动态瓶颈。
然后,流式输出不好做。请求不是一次性完成,而是 decode 循环中不断产生 token。服务端既要调度 GPU,又要持续向客户端推流,还要处理客户端断开、网络波动、限速和超时。
最后,系统无法做稳定的 SLO 控制。没有 scheduler,就不能控制谁先执行、batch 多大、长 prompt 是否切块、请求是否要抢占、超长输出是否要限制、不同优先级请求是否要分流。
这也是 vLLM、SGLang、TGI、TensorRT-LLM 这类 Serving 框架存在的原因。它们不是简单包装模型,而是在解决 GPU 推理服务的调度、显存、批处理和流式输出问题。
七、一个更准确的 mental model

把 LLM Serving 想成餐厅,比想成普通 Web 服务更准确。
用户请求是客人点单。GPU 是厨房。单个客人来了就立刻开火,厨房会被频繁打断。把相似步骤合并处理,厨房效率才高。但如果为了凑一大批客人,让第一个客人等太久,体验又会变差。
Prefill 像备菜:输入越长,备菜越重。Decode 像一道菜一道菜持续出餐:每一步不大,但要持续很多轮。KV Cache 像半成品存储区:存得好,后面出餐快;存不好,厨房通道被堵死。
Serving 系统的任务,就是把随机到达的请求,调度成 GPU 能高效处理的执行序列,同时保证用户感知的延迟不失控。
后续很多优化,本质上都在修这座桥:
- dynamic batching:等待极短时间,把多个请求合成更有价值的 batch;
- continuous batching:已有请求结束后,及时把新请求补进 decode 循环;
- PagedAttention / paged KV:把 KV Cache 管理从粗放分配变成可分页、可复用;
- chunked prefill:把长 prompt 的 prefill 切块,避免长上下文拖垮尾延迟;
- prefix caching:复用重复上下文,减少重复 prefill;
- prefill/decode 分离:按两个阶段不同资源特征分别扩展和调度;
- 多 GPU 并行:在模型太大或吞吐要求太高时,用张量并行、流水并行、专家并行等方式拆分压力。
这些技术看起来分散,但都在回答同一个问题:如何在随机在线请求和规整 GPU 执行之间搭一座桥。
八、核心结论
GPU 高效处理大量请求,不是靠"多开线程",而是靠调度系统把请求组织成适合 GPU 的批量执行模式。
LLM Serving 的复杂性来自自回归生成。Prefill 和 Decode 资源特征不同,KV Cache 生命周期复杂,请求长度不一致,流式输出持续占用系统资源。
判断一个 Serving 系统是否健康,也不能只看显存和 GPU Util。至少要同时看 TTFT、TPOT、tokens/s、running requests、waiting requests、KV cache usage、prefix cache hit rate、preemption 次数、batch size 分布、输入输出 token 长度分布。
如果只记住一句话,就是:
GPU 快的前提,是你把工作组织成它擅长的形状。LLM Serving 的工程价值,正是在随机请求和规整执行之间完成这次组织。
参考资料
- vLLM Documentation: docs.vllm.ai/
- vLLM Blog, Anatomy of a High-Throughput LLM Inference System: vllm.ai/blog/2025-0...
- vLLM Documentation, Disaggregated Prefilling: docs.vllm.ai/en/latest/f...
- Paper, Efficient Memory Management for Large Language Model Serving with PagedAttention: arxiv.org/abs/2309.06...
- Hugging Face Transformers, Continuous Batching: huggingface.co/docs/transf...
- Hugging Face Text Generation Inference: github.com/huggingface...
- NVIDIA TensorRT-LLM Documentation: nvidia.github.io/TensorRT-LL...
- NVIDIA TensorRT-LLM, GPT Attention / KV Cache: nvidia.github.io/TensorRT-LL...
- SGLang Documentation: sgl-project.github.io/
- SGLang GitHub: github.com/sgl-project...
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| GPU Util 看着不高,但 tokens/s 也低 | 单请求执行,batch 太小,kernel launch 摊薄不了 | 看 batch size 分布、kernel launch 频率 | 开 dynamic batching + continuous batching |
| 平均延迟还行,P99 偶尔爆掉 | 长 prompt prefill 阻塞 decode 循环,或长输出拖累 batch | 看 P99 命中样本的 prompt/output 长度 | 开 chunked prefill;对超长 prompt 单独调度 |
| 显存监控还有余量,新请求却被拒 / 排队 | KV Cache 被长上下文挤穿,权重显存和 KV 显存混淆 | 单独看 KV cache usage 而不是 total memory | 上 PagedAttention / paged KV;设 KV 预留水位 |
| GPU Util 90%+,但用户体验仍差 | 显存带宽瓶颈 / 调度瓶颈,CPU 喂不饱或网络接不住 | 看 prefill/decode 占比、显存带宽、scheduler 队列 | 调 max_num_seqs;考虑 prefill/decode 分离 |
| 系统提示词 / RAG 文档每次都重新算 | 相同 prefix 没被复用 | 看 prefix cache hit rate | 开 prefix caching,按 prefix hash 复用 KV |
| TTFT 经常 > 1s,请求立刻排队 | batch 凑得太久 / 长 prefill 没切块 | 看 queue time、waiting requests、prefill step 时长 | 缩凑批窗口;chunked prefill;prefill/decode 分离 |
| 用户量上来后吞吐反而下降 | 朴素 Serving 没有调度器,长请求阻塞短请求 | 看 waiting requests 是否随 running requests 同步增长 | 上 continuous batching;按 token budget 调度 |
| 流式输出断断续续、客户端频繁超时 | decode 节奏被 prefill 抢占、调度抖动 | 看 decode step 间隔、preemption 次数 | 设 prefill/decode 优先级;长 prompt 走独立调度通道 |
作者:武子康的个人博客