很多团队做 LLM Serving 时,第一反应总是模型多大、量化到几 bit、KV Cache 占多少显存、单机能扛多少并发。但线上系统真正跑起来之后,经常遇到一种更难受的问题:明明总显存看起来还够,服务却还是 OOM、吞吐抖动、TTFT 变长,甚至越跑越不稳定。
这背后往往不是“显存总量不足”这么简单,而是显存碎片、动态分配策略、KV Cache 管理方式、请求调度模式一起作用的结果。你看到的是一次分配失败,真正的问题可能是页块太碎、长短请求混跑、block 回收不及时、prefix 复用带来生命周期错配,或者 allocator 和 serving runtime 的粒度设计不一致。
本文从工程视角系统拆解一个常被低估、但又极其影响 LLM 稳定性的主题:LLM 推理为什么不能只盯着“剩余显存”,还必须理解分配器、碎片、页式 KV 和调度之间的耦合关系。如果你在看 vLLM、TensorRT-LLM、SGLang、PagedAttention、prefix cache 或推理稳定性问题,这篇文章应该能把很多零散概念串起来。
目录
- 为什么线上最难的问题常常不是“显存不够”,而是“显存不好用”
- 先把账算清:LLM 推理显存到底花在了哪里
- 显存碎片到底是什么,它为什么会让服务越来越不稳
- 为什么 LLM Serving 比普通推理更容易产生碎片
- 连续 KV Cache 为什么在真实服务里很快失去优势
- PagedAttention 和 block manager 到底解决了什么
- 分配器视角:你以为在分 token,其实是在分生命周期
- 调度为什么会直接影响显存稳定性
- Prefix Cache、长上下文和多轮对话为什么会放大问题
- vLLM、TensorRT-LLM、SGLang 应该分别怎么理解这件事
- 线上排障时该盯哪些指标,而不是只看 nvidia-smi
- 几种最常见、也最容易误判的工程场景
- 工程落地建议:怎么把“能跑”变成“稳跑”
- 面试里怎么把这个题讲得像真的做过
- 总结
1. 为什么线上最难的问题常常不是“显存不够”,而是“显存不好用”
很多人刚接触 LLM 推理时,会默认把显存问题理解成一道静态容量题:
- 模型权重占多少
- KV Cache 占多少
- batch size 能开多大
- 剩余显存还能不能再塞请求
这种理解在离线 benchmark 里通常没问题,但在线服务不是静态世界。真实流量会同时带来:
- 不同长度的 prompt
- 不同长度的生成
- 请求随时进入和退出 batch
- 多轮对话造成长生命周期 KV
- prefix cache 造成跨请求复用
- 调度器不断重排、插队、抢占和回收
于是显存问题很快从“容量不足”变成“资源形状不合适”。
一个 GPU 明明还剩 8GB 空闲显存,却可能仍然放不下一个新的长请求;不是因为总量不够,而是因为可复用的空闲块被切得太碎,或者某些 KV block 虽然逻辑上空闲,物理上却还无法及时回收。
这就是为什么很多团队会遇到一种非常典型的现象:
- 压测一开始吞吐正常
- 跑久了延迟开始抖
- 再往后偶发 OOM
- 重启服务后一切恢复
如果重启能明显“治好”问题,十有八九就不能只盯模型大小,而要开始怀疑碎片、回收和分配策略。
2. 先把账算清:LLM 推理显存到底花在了哪里
讨论碎片之前,先要把显存账本建立起来。对一个 LLM Serving 进程来说,显存大致花在下面几类地方:
- 模型权重
- 临时激活和中间 buffer
- attention / GEMM 等 kernel workspace
- 通信 buffer
- KV Cache
- runtime 元数据和索引结构
- allocator 自身的保留与缓存
其中最容易被低估的是最后两项。
很多人觉得“显存就是权重加 KV”,但真实系统里往往还存在:
- 框架级缓存分配器保留的显存
- CUDA graph / execution plan 相关 workspace
- 不同 shape 触发的临时 buffer 扩张
- block table、page table、索引映射等元数据
如果你只算理论 KV 容量,最后很容易得到一个过于乐观的并发预算。
更重要的是,权重通常是静态的,KV 和临时 buffer 才是动态的。真正制造不稳定性的,往往不是静态部分,而是动态部分的生命周期高度不规则。
3. 显存碎片到底是什么,它为什么会让服务越来越不稳
显存碎片本质上不是“总量减少”,而是“可用空间被切散了”。
可以把它想象成酒店房间管理:
- 你总共有 100 个房间
- 现在空着 20 个
- 但这 20 个分散在很多层、很多边角
- 来了一支需要连续 8 间房的团队,结果接待不了
显存分配也是类似道理。一次新的分配请求不只关心“还有多少空闲”,还关心:
- 空闲块是否足够大
- 块大小是否匹配
- 生命周期是否能和已有对象兼容
- 是否会触发额外的搬迁或回收
在线 LLM 服务里,碎片的危害主要有四个:
- 新请求准入失败,出现 OOM 或 admission failure
- 调度器被迫保守,吞吐下降
- 回收动作变频繁,延迟抖动增大
- 长时间运行后稳定性下降,服务表现出“老化”特征
所以碎片不是一个“底层实现细节”,而是直接影响 SLA 和成本的线上问题。
4. 为什么 LLM Serving 比普通推理更容易产生碎片
传统 CV/NLP 推理很多是静态 shape、固定 batch、短生命周期 tensor。即使有动态 shape,生命周期也相对整齐。
但 LLM Serving 有几个天然更容易制造碎片的特点。
4.1 请求长度高度不一致
一个 256 token 的请求和一个 16K token 的请求,会消耗完全不同规模的 KV 空间,而且释放时间也不同。
4.2 decode 是持续增长型内存
随着生成继续,每个请求的 KV 会不断追加。它不是“一次分完一次释放”,而是边跑边长。
4.3 continuous batching 让生命周期交错
请求不断进入和退出 batch,显存对象的生死顺序被打乱,天然不利于形成整齐的分配/释放模式。
4.4 prefix cache 和复用带来共享引用
某些 KV 不再只属于单个请求,而会被多个请求共享。这会让回收条件更复杂,不再是“请求结束就释放”。
4.5 长对话和长上下文容易形成“长尾占坑”
少量超长会话可能长期占住大块 KV 空间,而大量短请求在旁边频繁进出,最终把空闲区域切得很碎。
所以 LLM Serving 的内存管理,本质上不是简单的 tensor 分配,而是一个带有明显系统特征的在线资源管理问题。
5. 连续 KV Cache 为什么在真实服务里很快失去优势
从直觉上看,连续内存似乎总是更好:
- 地址简单
- 访问局部性好
- 元数据更少
- 看起来更“规整”
这在单请求、固定长度、离线推理场景里可能成立。但到了多租户在线服务,连续 KV 往往很快暴露两个问题。
5.1 预留过大会浪费
如果你为了避免扩容,给每个请求预留较大的连续空间,那么短请求会造成大量内部浪费。
5.2 动态扩容很困难
如果一开始只给较小空间,后续生成变长时就可能需要扩容。连续内存一旦后面没有足够空间,要么搬迁,要么分配失败。
这就是连续 KV 的核心矛盾:
预留太多会浪费,预留太少会扩不动。
而真实 LLM 服务恰恰最不确定的就是请求最终会长到哪里。你没法在请求进入时,准确知道它会生成 50 token 还是 2000 token。
所以连续 KV 更像是静态世界里的好方案,而不是动态世界里的稳方案。
6. PagedAttention 和 block manager 到底解决了什么
PagedAttention 被广泛提起,但很多人只记住了“把 KV 分页”,没有真正理解它解决的是哪一层问题。
它的核心思想可以概括成一句话:
不要要求每个请求的 KV 在物理显存中连续,而是把它拆成固定大小的 block,通过映射表组织成逻辑连续。
这样做的直接收益有三类。
6.1 降低外部碎片
请求不再需要申请一整段大块连续空间,而是按 block 逐步增长。只要还有足够 page/block,就能继续扩展。
6.2 提高回收灵活性
请求结束后,可以按 block 回收;共享前缀场景下,也可以基于引用计数精细释放。
6.3 让调度和内存更容易协同
调度器可以按 block 预算做 admission control,而不是只看粗糙的“剩余显存百分比”。
但分页不是白送收益。它也会带来:
- page table / block table 元数据开销
- 地址映射与 gather 成本
- block size 选择的 trade-off
- 内部碎片问题从“连续段浪费”变成“页内浪费”
所以 block manager 的价值不只是“把 KV 管起来”,而是为在线服务提供一个更适合动态生命周期的资源抽象。
7. 分配器视角:你以为在分 token,其实是在分生命周期
很多人谈 KV 管理时只盯 token 数量,但 allocator 真正在意的是对象生命周期。
为什么?
因为如果两个对象大小类似,但生命周期完全不同,它们混放在同一类资源池中,就很容易制造碎片。
例如:
- 短请求的 decode block,生命周期可能只有几百毫秒
- 长对话的 block,生命周期可能持续几十秒甚至几分钟
- prefix cache block,可能跨多个请求共享
- workspace buffer,可能在一次 kernel 执行后立刻释放
如果这些对象全都走同一套粗粒度分配路径,系统很容易出现:
- 短命对象频繁打洞
- 长命对象占住关键位置
- 回收节奏和申请节奏不匹配
因此成熟系统往往会引入分层管理思路:
- 静态权重与动态 KV 分离
- KV block 与临时 workspace 分离
- 可共享前缀与私有 decode block 分离
- 不同大小等级的 block 分桶
从 allocator 角度看,真正高级的优化不是“把显存榨干”,而是让不同生命周期的对象尽量少互相污染。
8. 调度为什么会直接影响显存稳定性
很多人把调度问题和显存问题分开看,这在 LLM Serving 里经常是错的。
调度器决定了:
- 哪些请求一起进入 batch
- 谁先 prefill,谁先 decode
- 长请求和短请求是否混跑
- 是否允许超长请求占满 page pool
- admission control 按 token、按 block 还是按请求数
这些决策会直接改变显存对象的生命周期分布。
一个很典型的例子是长短请求混跑:
- 短请求快速进入、快速退出,频繁申请/释放 block
- 长请求持续占住大块 KV,不断追加 page
- 最终 page pool 虽然总量未满,但高质量空闲块越来越少
再比如 prefill burst:
- 突然来一批超长 prompt
- prefill 阶段瞬间吃掉大量 block
- decode 侧被迫让路
- TTFT 和吞吐同时恶化
所以优秀的调度器不只是“排队器”,而必须具备基本的显存意识。至少要回答三个问题:
- 当前 admission 看的是 token、block 还是 bytes
- 哪类请求会放大碎片和抖动
- 遇到资源紧张时,是限流、降级、分池还是抢占
9. Prefix Cache、长上下文和多轮对话为什么会放大问题
Prefix Cache 看起来是在省算力,但它同时也改变了内存对象的生命周期。
9.1 prefix 复用会让“该释放”的东西不再容易释放
如果一个前缀被多个请求共享,那么它的释放时机就从“请求结束”变成“最后一个引用离开”。这会带来更长的驻留时间。
9.2 长上下文会放大 block 占用
上下文越长,单请求的 page 链越长;一旦并发上来,page pool 很容易被少量长请求吃掉。
9.3 多轮对话天然偏向长生命周期
会话式应用不像一次性问答。它会不断追加历史上下文,导致部分 KV 在系统中停留得更久,更像“热状态”而不是“临时缓存”。
9.4 RAG/Agent 会让前缀复用更不稳定
工具定义、检索片段、system prompt、格式模板可能部分固定、部分动态。这意味着前缀不是“完全不复用”,也不是“完全复用”,而是会在边界处形成很多不规则对象。
这就是为什么很多团队做完 prefix cache 后,会发现命中率、显存占用和回收行为之间出现新的 trade-off。它省掉了重复 prefill,但也引入了更复杂的状态驻留和释放逻辑。
10. vLLM、TensorRT-LLM、SGLang 应该分别怎么理解这件事
这几个系统不应该只从“谁更快”来理解,更应该从它们对推理运行时抽象的选择来理解。
10.1 vLLM
vLLM 最被广泛讨论的,就是 PagedAttention 和基于 block 的 KV 管理。它的价值在于把“动态请求 + 动态长度 + 多租户共享”这个问题显式拉到 runtime 设计层面。
如果你在看 vLLM,重点应该理解:
- block/page 抽象如何降低碎片
- block table 如何组织逻辑连续性
- 调度器如何围绕 block 预算做准入和执行
- prefix sharing 如何影响回收和引用计数
10.2 TensorRT-LLM
TensorRT-LLM 更强调 engine、kernel、图优化、量化和执行效率。它在高性能执行路径上很强,但工程上同样绕不开 KV 管理、batching 和资源预算问题。
理解 TensorRT-LLM 时,不要只盯算子性能,还要看:
- engine 对动态 shape 的约束
- workspace 和 execution buffer 的预算
- 长上下文和 paged KV 的支持方式
- 性能最优配置和资源最稳配置是否一致
10.3 SGLang
SGLang 的价值常体现在执行语义、structured generation、prefix / state 复用与服务编排能力上。它让“状态”变得更显式,因此也更需要认真处理状态驻留、复用收益和资源隔离。
理解 SGLang 时,重点不只是 API,而是:
- 状态复用如何影响显存生命周期
- 多阶段执行如何影响 prefill / decode 资源切换
- 应用层编排是否会制造新的长生命周期对象
把这三个系统放在一起看,你会发现它们都在回答同一个问题:如何把不规则请求流量变成可管理、可预测的运行时资源行为。
11. 线上排障时该盯哪些指标,而不是只看 nvidia-smi
nvidia-smi很重要,但远远不够。
如果你真的在定位显存碎片和 KV 管理问题,至少要建立下面这组观测。
11.1 容量类指标
- 权重占用
- KV 占用
- workspace 占用
- allocator reserved bytes
- allocator active bytes
- 可分配 block 数
11.2 碎片类指标
- 空闲 block 总数
- 最大连续可用块
- 不同 block size 桶的空闲分布
- 页内浪费比例
- block 分配失败次数
11.3 生命周期类指标
- 请求平均存活时间
- 长请求占比
- prefix cache 对象平均驻留时间
- block 引用计数分布
- 回收延迟
11.4 调度类指标
- admission 拒绝率
- prefill 等待时间
- decode 等待时间
- active sequence 数
- 连续 batching 的 batch occupancy
11.5 用户体验类指标
- TTFT
- TPOT / tok/s
- P50 / P95 / P99 延迟
- OOM 次数
- 服务重启前后的性能差
如果没有这套指标,很多团队会在“是模型太大”“是流量太高”“是 GPU 不够”“是框架不稳定”之间来回猜,最后根本定位不到根因。
12. 几种最常见、也最容易误判的工程场景
场景一:总显存没打满,但 admission 持续失败
这通常不是“还有优化空间”,而可能是:
- 空闲块不够连续
- block 池被长请求占住
- prefix 共享对象释放滞后
- allocator reserved 过高但 active 不高
场景二:压测一小时后延迟明显变差
这类问题经常和运行时老化有关。常见原因包括:
- 长短请求混跑导致 pool 质量下降
- 小对象频繁分配释放制造碎片
- block 回收路径慢于申请路径
- 缓存命中提升了驻留,但没有同步升级淘汰策略
场景三:量化后理论上省了显存,但并发没涨多少
可能的原因是:
- 节省的是权重空间,不是动态 KV 瓶颈
- workspace 或 page pool 才是上限
- 调度策略过于保守,没有把新增空间转化成更多 admission
场景四:Prefix Cache 命中率提升了,但 OOM 更频繁
这听起来矛盾,其实很常见。因为命中率高意味着更多共享状态被保留,如果回收和淘汰策略没跟上,驻留时间会变长,最终挤压私有 decode 空间。
场景五:重启服务后指标立刻恢复
这类问题一定要优先检查:
- allocator 缓存膨胀
- pool 碎片累积
- page 回收不及时
- 长尾会话导致的状态滞留
如果每次重启都“治标有效”,那就说明问题高度可重复,只是之前没有合适的指标把它看见。
13. 工程落地建议:怎么把“能跑”变成“稳跑”
如果你真的要把系统从 demo 推到生产,下面这些原则比单点 benchmark 更重要。
13.1 别只做静态容量预算,要做动态生命周期预算
除了算模型和理论 KV,还要估计:
- 长短请求分布
- 平均会话时长
- prefix 复用带来的驻留增长
- 峰值时段的 prefill burst
13.2 用 page/block 思维管理 KV,而不是连续段思维
在线服务里,灵活回收和渐进扩容,通常比“理论最规整布局”更重要。
13.3 给不同生命周期对象分池
至少考虑把:
- 共享前缀
- 私有 decode KV
- 临时 workspace
- 超长会话
做逻辑隔离或预算隔离,减少互相污染。
13.4 调度器要显存感知
不要只按请求数限流。更合理的是按 token、block、上下文长度、会话类型联合做 admission。
13.5 观测一定要能看到“运行时老化”
只看瞬时吞吐没有意义。至少要补:
- 长时间 soak test
- 混合长短请求压测
- prefix 开关对比实验
- 重启前后 allocator / block 分布对比
13.6 优化目标要从“跑满 GPU”改成“稳定兑现 SLA”
线上系统最怕的不是平均值不高,而是尾延迟失控、偶发 OOM 和不可预测抖动。对业务来说,可预测性往往比单次峰值更重要。
14. 面试里怎么把这个题讲得像真的做过
这是一个非常适合拉开差距的话题,因为它天然带有系统味道。
如果面试官问你“KV Cache 怎么优化”,很多人只会回答:
- 做 PagedAttention
- 做量化
- 提高命中率
这样的回答太浅。更好的讲法应该是:
- 先区分静态显存和动态显存
- 说明线上瓶颈不只是容量,还有碎片和生命周期错配
- 解释连续 KV 为什么在动态请求下不稳
- 解释 page/block 抽象如何降低外部碎片
- 再补充调度、prefix cache、长会话如何改变回收行为
- 最后讲你会怎么看指标、怎么验证优化是否真的生效
如果再进一步,你甚至可以主动抛出这几个面试加分点:
- 为什么命中率高不等于资源效率高
- 为什么量化后并发不一定同步提升
- 为什么重启后恢复正常往往指向碎片或 allocator 问题
- 为什么 admission control 应该按 block/token 而不是只按请求数
能把这些 trade-off 讲清楚,面试官通常会判断你不只是看过论文,而是真的理解推理系统的工程约束。
15. 总结
LLM 推理里的显存问题,远不只是“够不够”这么简单。
更准确地说,它是四件事共同决定的结果:
- 总容量够不够
- 分配形状合不合适
- 生命周期是否规整
- 调度策略是否和内存抽象匹配
这也是为什么 vLLM、TensorRT-LLM、SGLang 这些系统虽然切入点不同,最后都会落到同一个核心矛盾上:
如何让高度不规则的请求流量,在有限 GPU 显存上以足够稳定、足够高效、足够可预测的方式运行。
如果你正在做 LLM Serving,建议尽早把“显存碎片、分配器、KV 页管理、调度耦合”作为一个完整专题来看。它不是边角知识,而是决定系统能否长期稳跑的主干能力。
很多时候,真正把服务从“能 demo”拉到“能生产”的,不是再多压出 5% 峰值吞吐,而是先回答清楚:
为什么这台 GPU 明明还有显存,系统却已经开始不敢接请求了。