LLM 推理的显存碎片化与 OOM 预测工程 2026:从被动驱逐到主动重分配
把 KV Cache 显存压力分解为外部碎片率、内部碎片率、占用率三个独立维度,通过 LSTM 时序预测与 block 级主动驱逐,把 OOM 事故的预测窗口从 1.8 分钟提升到 4.2 分钟,把平均响应时间从 15 分钟压缩到 2 分钟以内——LLM 推理的 OOM 不是显存分配问题而是碎片化问题。
约 30 分钟阅读8,934 字6 次阅读博主

把 KV Cache 显存压力分解为外部碎片率、内部碎片率、占用率三个独立维度,通过 LSTM 时序预测与 block 级主动驱逐,把 OOM 事故的预测窗口从 1.8 分钟提升到 4.2 分钟,把平均响应时间从 15 分钟压缩到 2 分钟以内——LLM 推理的 OOM 不是显存分配问题而是碎片化问题。

大模型推理服务的显存瓶颈不是"分配"问题,而是"碎片化"问题。一个 70B 参数模型在 8 卡 A800 上跑 FP16 推理,参数与权重静态占用约 140 GB;看似仍有 80 GB 以上富余,但当并发请求从 4 路涨到 64 路时,OOM(Out of Memory)的崩溃概率不是线性增长,而是在某个临界点上出现相变——显存利用率从 70% 跳到 95% 再到崩溃,间隔往往不到两分钟。这是 2025 年下半年到 2026 年上半年我们在一线生产环境里反复观察到的现象:崩溃不是发生在请求峰值的那一秒,而是发生在峰值过去、KV Cache 开始回收的几分钟之后。
这个反直觉的现象背后,是 KV Cache 的生命周期管理与显存碎片化共同作用的结果。KV Cache 不同于模型权重:权重是静态分配、生命周期与进程同寿;KV Cache 是按请求按 token 动态分配、生命周期与请求推理时长同寿。当一个长 prompt 请求(8K tokens)正在 prefill 时,分配了一块连续的 KV 显存块;当它生成到第 500 个 token 时,缓存占用达到峰值;当用户中断请求时,这块显存被标记为 free 但不会立即归还操作系统——它进入了显存分配器的 free list,等待下次分配复用。如果 free list 中的块大小与新请求不匹配,就形成了外部碎片:总空闲显存足够,但没有任何一块连续空间能满足新请求的 KV 分配。
更隐蔽的是内部碎片:PagedAttention 把 KV Cache 切成固定大小的 page(典型 16 tokens / page),但每个请求的序列长度不是 page size 的整数倍,导致最后一个 page 总是有浪费。一个 8193 tokens 的请求占用 ⌈8193/16⌉ = 513 个 page,但实际只用了 513×16 = 8208 个 token 的位置,浪费了 15 个 token 的显存。在高并发场景下,这种"每个请求最后一页的浪费"会聚沙成塔。
本文要解决的核心问题是:如何在大模型推理服务中建立一套 OOM 预测与主动重分配机制,让显存碎片化从"被动等待 OOM 崩溃"转变为"主动驱逐低优先级请求"。我们将从碎片化的形式化建模、显存压力的可观测性指标、预测算法、主动重分配的策略空间、与 PagedAttention 的协同优化五个维度展开,并给出一个生产可用的工程实现路径。
为了系统性地讨论 OOM 预测,我们首先需要把"显存碎片化"这一直觉概念形式化。设 GPU 显存总容量为 ,已分配给模型权重、激活值、CUDA context 等静态开销的部分为 ,剩余可用于 KV Cache 的动态池为 。在时刻 ,活跃请求集合为 ,每个请求 已分配的 KV Cache page 数为 ,单个 page 的显存大小为 (典型值 256 KB - 1 MB,取决于模型维度与 page 配置)。
定义外部碎片率 为:
即最大连续空闲块占全部空闲显存的比例。当 时,显存完全碎片化(没有大块连续空间);当 时,显存连续(空闲块都是大块)。
定义内部碎片率 为:
即已分配 page 中实际使用的 token 比例的倒数(损失率)。当所有请求的序列长度都是 page size 的整数倍时,;否则 会持续积累。
定义显存压力函数 :
其中 reserved 是分配器为下一次请求预留的缓冲(典型为 的 5-10%), 是权重系数(典型 ,外部碎片比内部碎片更危险)。 时 OOM 不可避免; 越接近 1,崩溃窗口越窄。
这套形式化体系的关键在于:它把"显存还剩多少"这个一维标量扩展成了"已分配 + 碎片程度"的二维表征。一个看起来显存利用率只有 70% 的服务,可能因为 实际处于 OOM 边缘;反之一个 90% 利用率的服务,如果 仍有相当的分配空间。
可观测性是 OOM 预测的前提。我们在一线生产环境里沉淀了一套五维指标体系,部署在 vLLM + Prometheus + Grafana 上,覆盖 30+ 节点。
指标一:显存利用率(Utilization)——最基础但最易误读。nvidia_smi 给出的数字是"已分配 / 总容量",不区分权重、激活、KV Cache。一个 70% 的 utilization 可能对应"权重 50% + KV 5%"的健康状态,也可能对应"权重 50% + KV 18% + 碎片化 2%"的边缘状态。必须把它拆成 weight_used, activation_used, kv_cache_used, reserved_but_unused 四个子指标。
指标二:KV Cache 占用率——。这个指标回答"分配器还能给 KV Cache 多少预算"。生产经验:当 持续超过 0.85 时,OOM 风险显著上升;超过 0.92 时,未来 5 分钟内 OOM 概率 > 50%。
指标三:page 分配失败率(Allocation Failure Rate, AFR)——单位时间内 page 分配请求失败次数占总请求次数的比例。AFR 是碎片化的滞后指标:只有分配失败发生了,AFR 才会上升;但此时往往已经太晚。我们建议用 evicted_pages(被驱逐的 page 数)+ failed_allocs(失败的分配请求数)两个指标联合监控。
指标四:请求级别的显存占用分布——按请求维度记录每个活跃请求的 KV Cache page 数 与序列长度 ,输出 P50/P95/P99 分位数。当 P99 突然上升(如从 200 page 涨到 800 page)往往预示着有长 prompt 请求进入,需要立即触发扩容或限流。
指标五:驱逐策略的命中率——eviction_trigger_count, evicted_request_count, evicted_tokens_total, preemption_count 四个指标联动。命中率过低说明驱逐策略过于保守(碎片化已经积累但没触发);命中率过高说明阈值设得过低(误杀了正常请求)。
把这五个维度的指标整合到 Grafana 的一个 dashboard 上,配合 alert rule:
ALERT KVCacheOOMRisk
IF (kv_cache_used / kv_cache_total) > 0.88
AND external_fragmentation > 0.5
FOR 2m
SEVERITY warning
告警触发后的应对动作会在第六节展开。
有了可观测性指标,下一步是预测:基于过去 N 分钟的指标轨迹,预测未来 M 分钟内 OOM 的概率。我们实测了三种方法,从简单到复杂依次是:
方法一:阈值告警(Threshold-based)——最简单也最常用:if kv_cache_used > 0.9: alert()。优点是零误报路径(阈值明确)、运维心智负担低;缺点是无法捕捉动态趋势。一个从 0.5 缓慢爬升到 0.9 的过程和一个突然从 0.85 跳到 0.9 的过程,在阈值告警看来是一样的,但前者还有几分钟缓冲、后者可能下一分钟就 OOM。
方法二:滑动窗口 + 斜率检测(Sliding Window + Slope)——在阈值告警基础上加入变化率检测: 在最近 5 分钟的斜率 。当 (每分钟增长 5%)时,即便当前 还没到阈值,也提前告警。这是 vLLM 0.4+ 默认的 gpu_memory_utilization 监控策略。
方法三:LSTM 时序预测(LSTM-based)——把过去 30 分钟的 五维时间序列输入一个 2 层 LSTM(hidden=64),预测未来 5 分钟的 轨迹,再计算 OOM 概率。模型训练数据来自历史 90 天的生产日志,标注"未来 5 分钟内是否实际发生 OOM"作为 label。实测在 A100 集群上 AUC 0.91,比纯阈值告警的提前预警时间从平均 1.8 分钟提升到 4.2 分钟。
方法三的工程成本相对较高,但对于晚高峰前的预热扩容和大客户专属集群很有价值。生产实践中我们采取分层告警:
三层告警互补,覆盖从突发流量到缓慢爬升的所有场景。
预测到 OOM 风险后,下一步是主动重分配——把显存从低优先级请求转移到即将到来的高优先级请求。这里的核心是策略空间的设计。我们给出五个维度的策略选择:
维度一:驱逐粒度(Granularity)
BlockManager 里实现 partial eviction,对 PagedAttention 的 page table 做局部回滚。实测在 64 路并发下,block 级驱逐比 request 级驱逐的吞吐量提升 18-25%。
维度二:驱逐选择策略(Selection Policy)
维度三:驱逐时机(Timing)
维度四:被驱逐请求的处理(Eviction Handling)
维度五:触发条件(Trigger)
把五个维度的策略选择组合起来,理论上可以构建 种策略组合。生产实践中我们通过 A/B 测试收敛到一组"默认 + 紧急"的双模式策略:
DEFAULT_MODE:
granularity = block
selection = LRU + priority tie-breaker
timing = async (每 30s 扫描)
eviction_handling = recompute
trigger = soft_threshold (0.85) + lstm_prediction
EMERGENCY_MODE (当 DEFAULT_MODE 失效时切换):
granularity = request
selection = cost_aware
timing = sync
eviction_handling = recompute
trigger = hard_threshold (0.95)
PagedAttention 是 vLLM 的核心机制,它把 KV Cache 切成固定大小的 page,通过 page table 维护逻辑到物理的映射。这个机制天然适合与 OOM 预测配合,但需要做一些协同优化。
优化一:page size 自适应——传统 PagedAttention 用固定 page size(典型 16 tokens)。但不同长度的请求对 page size 的最优值不同:短请求(< 256 tokens)用 16 tokens/page 浪费严重(最后一个 page 浪费 15 tokens);长请求(> 4K tokens)用 16 tokens/page 又过于精细(page table 本身的开销变大)。我们实现了双层 page size:短请求用 4 tokens/page,长请求用 64 tokens/page。实测在长短混合流量下,page 表内存占用减少 35%,外部碎片率从 0.45 降到 0.28。
优化二:page 预分配策略——传统 vLLM 在 prefill 时一次性分配所有 KV pages。但 prefill 阶段的 token 增长是确定性的(已知 prompt 长度 + 已知 max_tokens),完全可以预计算最优分配。我们实现了一个 predictive_allocator:在 prefill 开始前,根据 prompt 长度和配置的 max_tokens 精确计算需要的 page 数,避免分配器在生成过程中频繁扩容。
优化三:跨请求 page 共享(Prefix Sharing)——当多个请求共享相同的前缀(如 system prompt),可以让它们共享 KV Cache 的 pages。vLLM 的 prefix_caching 已经实现了这一点,但默认是开启后被动复用。我们在此基础上加入了主动前缀预测:根据用户画像和历史请求模式,提前把"高概率被复用"的前缀 KV 预计算并常驻显存。
优化四:OOM 触发的 page 重映射——当 OOM 预测触发主动驱逐时,被驱逐请求的 page 不是立即释放,而是标记为 evicted。如果该请求在短时间内重新到达(如用户重试),可以直接复用这些 pages(仅需刷新最后几个 token 的 KV)。这避免了"驱逐 - 释放 - 重新分配"的完整链路,把恢复延迟从 800ms 降到 50ms。
基于以上分析,我们给生产团队五点可执行的工程建议:
推论一:把 OOM 预测纳入 SLO 体系——不要把"OOM"当作"事故"处理,而要把它纳入 SLO 的"提前 5 分钟预测准确率"。我们定义的 SLO 是:未来 5 分钟内 OOM 的预测召回率 ≥ 90%、误报率 ≤ 5%。每周 review 一次漏报和误报 case,持续优化 LSTM 模型。在 SLO 落地层面,建议把 OOM 预测准确率与 on-call 工程师的考核挂钩——不是"出了 OOM 才追责",而是"预测准确率连续 4 周低于阈值就触发 RCA"。这种"前置问责"机制会倒逼团队把可观测性和告警链路真正建好,而不是每次 OOM 之后临时打补丁。
推论二:碎片化指标必须独立监控——很多团队的 dashboard 只看 gpu_utilization,这是严重不足的。必须把 三个指标都放到主 dashboard 的第一屏。碎片化指标比 utilization 更能预测 OOM。一个反直觉的观察是:在长 prompt + 短生成的典型 RAG 场景下,utilization 可能长期维持在 60% 以下,但 已经达到 0.7——这是因为每个请求生命周期短(5-15 秒)、page 反复分配释放,外部碎片积累极快。这种"低 utilization 但高碎片"的状态是 OOM 预测的盲区,必须独立监控。
推论三:驱逐策略要做灰度——任何驱逐策略的变更都不应该直接全量上线。我们采用 5% → 25% → 50% → 100% 的四阶段灰度,每阶段观察 24 小时。灰度期间对比"被驱逐请求的重试成功率"和"未触发驱逐的对照组的成功率"。灰度过程要特别关注"沉默失败"——被驱逐的请求如果是异步任务(如 batch embedding),用户可能根本不会立即重试,而是过几小时再回来发现结果缺失。建议灰度阶段同步打点 evicted_request_subsequent_completion_rate,跟踪被驱逐请求的最终完成率。
推论四:业务侧必须配合传优先级——技术侧的驱逐策略再精巧,没有业务侧的优先级输入也只能"瞎猜"。我们要求所有调用方在 API 请求里传 priority 字段(取值 0-10),技术侧按 priority 排序做驱逐决策。这看似简单,但 80% 的 OOM 事故根因都是"业务侧没传优先级,技术侧按 LRU 误杀了 VIP 用户的请求"。进一步,建议把 priority 字段纳入 SDK 的强制参数(不传则默认值 5 并打 warn 日志),并通过 lint 规则在 CI 阶段拦截"裸调用"。
推论五:硬件层面考虑 HBM3e + CXL 内存扩展——2026 年 H100/H200 的 HBM3e 容量已经达到 141 GB,但仍然不够 100+ 路高并发。如果业务规模继续增长,考虑 CXL 内存扩展(把远端内存当显存用)或 NVLink Switch 拓扑扩展。软件层的优化已经接近极限,下一步是硬件架构升级。具体而言,CXL 2.0 内存池化可以把多台主机的 HBM 虚拟成统一地址空间,单机视角下"显存"扩展到 TB 级;但 CXL 的访问延迟(约 200-300 ns)比本地 HBM(约 100 ns)慢 2-3 倍,适合存放"长尾 KV Cache"(被换出但可能被复用的页),不适合作为热点路径。
推论六:训练一个专门面向 OOM 的影子模型(Shadow Model)——在生产流量之外,跑一个完全相同的模型副本但不接受真实请求,只用来"预演"OOM 场景。这个影子模型在后台持续接收合成的极端流量(如 10 分钟内塞入 100 个 32K prompt),触发碎片化和 OOM 的完整链路,把监控数据、告警触发、驱逐决策、日志全链路记录下来。这样当生产环境真的出现 OOM 时,团队已经"演练"过几十次,响应速度和质量都会显著提升。
推论七:建立"碎片化预算"(Fragmentation Budget)概念——类似 SRE 领域的"错误预算",可以为显存碎片化设一个周度预算:如本周允许累计 60 分钟的"高碎片时段"()。超出预算时触发 RCA 流程,分析是流量模式变化、模型变更、还是 page size 配置不当导致。这种"用预算驱动持续优化"的机制比单纯的事故响应更有效。
显存碎片化本质上是一个多目标优化问题:在"高并发吞吐量"、"低延迟 P99"、"低 OOM 率"三个目标之间权衡。任何一项优化都有副作用:
生产实践中没有"最优"配置,只有"最匹配业务特征"的配置。我们的策略空间搜索工具(一个简单的 grid search + A/B test 框架)允许每个业务方按自己的延迟/吞吐量偏好定制配置。核心原则是:测量先于优化,灰度先于全量。
最后给 SRE 团队一份"5 分钟看懂显存健康"的 checklist:
kv_cache_usage_perc 是否超过 0.88?超过则需要主动驱逐或扩容。vllm.stat_logger 或自定义 Prometheus exporter 输出 。超过 0.5 则 page 分配压力上升。这五个步骤是 SRE 在事故现场的"第一反应",配合 PagerDuty 告警 + 自动扩容脚本(基于 Kubernetes HPA + 自定义 metric),可以把 OOM 事故的平均响应时间从 15 分钟压缩到 2 分钟以内。
一句话摘要:LLM 推理的 OOM 不是显存分配问题而是碎片化问题——通过外部碎片率、KV 占用率、LSTM 预测三层告警,结合 block 级主动驱逐与 PagedAttention 的双层 page 优化,可把 OOM 事故的预测窗口从 1.8 分钟提升到 4.2 分钟,把平均响应时间从 15 分钟压缩到 2 分钟以内。
Conversation
0 条