显存不是只看够不够:LLM 推理中的显存碎片、分配器与 KV Cache 页管理工程拆解

📅 2026/7/7 3:11:41
显存不是只看够不够:LLM 推理中的显存碎片、分配器与 KV Cache 页管理工程拆解
很多团队做 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多轮对话造成长生命周期 KVprefix cache 造成跨请求复用调度器不断重排、插队、抢占和回收于是显存问题很快从“容量不足”变成“资源形状不合适”。一个 GPU 明明还剩 8GB 空闲显存却可能仍然放不下一个新的长请求不是因为总量不够而是因为可复用的空闲块被切得太碎或者某些 KV block 虽然逻辑上空闲物理上却还无法及时回收。这就是为什么很多团队会遇到一种非常典型的现象压测一开始吞吐正常跑久了延迟开始抖再往后偶发 OOM重启服务后一切恢复如果重启能明显“治好”问题十有八九就不能只盯模型大小而要开始怀疑碎片、回收和分配策略。2. 先把账算清LLM 推理显存到底花在了哪里讨论碎片之前先要把显存账本建立起来。对一个 LLM Serving 进程来说显存大致花在下面几类地方模型权重临时激活和中间 bufferattention / GEMM 等 kernel workspace通信 bufferKV Cacheruntime 元数据和索引结构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 pooladmission control 按 token、按 block 还是按请求数这些决策会直接改变显存对象的生命周期分布。一个很典型的例子是长短请求混跑短请求快速进入、快速退出频繁申请/释放 block长请求持续占住大块 KV不断追加 page最终 page pool 虽然总量未满但高质量空闲块越来越少再比如 prefill burst突然来一批超长 promptprefill 阶段瞬间吃掉大量 blockdecode 侧被迫让路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 vLLMvLLM 最被广泛讨论的就是 PagedAttention 和基于 block 的 KV 管理。它的价值在于把“动态请求 动态长度 多租户共享”这个问题显式拉到 runtime 设计层面。如果你在看 vLLM重点应该理解block/page 抽象如何降低碎片block table 如何组织逻辑连续性调度器如何围绕 block 预算做准入和执行prefix sharing 如何影响回收和引用计数10.2 TensorRT-LLMTensorRT-LLM 更强调 engine、kernel、图优化、量化和执行效率。它在高性能执行路径上很强但工程上同样绕不开 KV 管理、batching 和资源预算问题。理解 TensorRT-LLM 时不要只盯算子性能还要看engine 对动态 shape 的约束workspace 和 execution buffer 的预算长上下文和 paged KV 的支持方式性能最优配置和资源最稳配置是否一致10.3 SGLangSGLang 的价值常体现在执行语义、structured generation、prefix / state 复用与服务编排能力上。它让“状态”变得更显式因此也更需要认真处理状态驻留、复用收益和资源隔离。理解 SGLang 时重点不只是 API而是状态复用如何影响显存生命周期多阶段执行如何影响 prefill / decode 资源切换应用层编排是否会制造新的长生命周期对象把这三个系统放在一起看你会发现它们都在回答同一个问题如何把不规则请求流量变成可管理、可预测的运行时资源行为。11. 线上排障时该盯哪些指标而不是只看 nvidia-sminvidia-smi很重要但远远不够。如果你真的在定位显存碎片和 KV 管理问题至少要建立下面这组观测。11.1 容量类指标权重占用KV 占用workspace 占用allocator reserved bytesallocator active bytes可分配 block 数11.2 碎片类指标空闲 block 总数最大连续可用块不同 block size 桶的空闲分布页内浪费比例block 分配失败次数11.3 生命周期类指标请求平均存活时间长请求占比prefix cache 对象平均驻留时间block 引用计数分布回收延迟11.4 调度类指标admission 拒绝率prefill 等待时间decode 等待时间active sequence 数连续 batching 的 batch occupancy11.5 用户体验类指标TTFTTPOT / tok/sP50 / 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 burst13.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 明明还有显存系统却已经开始不敢接请求了。