LLM 推理的内存压力工程 2026:从显存碎片化到 OOM 预测
把分配、碎片、泄漏、峰值四面体作为显存压力工程的理论骨架;以 KV cache 池化、LSTM OOM 预警、四级优雅降级、显存泄漏门禁、NUMA 亲和性调度为五条生产治理轴;给出 13 项监控指标 + 8 条告警 + 4 类事故演练 + 3 项回归门禁的 SRE 可观测性清单。
约 27 分钟阅读8,062 字11 次阅读博主

把分配、碎片、泄漏、峰值四面体作为显存压力工程的理论骨架;以 KV cache 池化、LSTM OOM 预警、四级优雅降级、显存泄漏门禁、NUMA 亲和性调度为五条生产治理轴;给出 13 项监控指标 + 8 条告警 + 4 类事故演练 + 3 项回归门禁的 SRE 可观测性清单。

一句话摘要:当 80GB HBM 在 79.2GB 占用时仍然触发 OOM,不是因为模型太大,而是因为显存被切成了一百三十万个不可合并的小碎片——本文从分配器内部碎片、KV cache 池化策略、显存轨迹预测、graceful degradation、显存泄漏检测,到 NUMA 亲和性调度,建立一套可观测、可预测、可降级的 LLM 显存压力治理闭环。
过去三年,我观察到一个反直觉的现象:生产环境的 LLM 推理服务,70% 的服务降级与中断事件与模型规模、流量峰值、KV cache 容量都无关——根因都落在「单卡显存压力治理」这一层。一个典型场景是:80GB 的 HBM3 在 nvidia-smi 显示 79.2GB 占用时仍然 OOM Killer 触发,工程师的第一反应是「显存不够」,扩容到 H100 96GB 后问题依旧。第二反应是「流量太大」,加了限流后 OOM 频率从每分钟 12 次降到每分钟 8 次。第三反应才是「显存碎片化」——但此时已经浪费了三个迭代周期,损失了几十万元的 SRE 排障工时。
LLM 推理的显存压力工程是一个与 KV cache 工程、PD 分离、弹性伸缩互补但正交的方向。KV cache 工程回答的是「跨请求、跨实例、跨介质的容量复用」问题;PD 分离回答的是「跨节点、跨介质的带宽-容量平衡」;弹性伸缩回答的是「流量驱动的实例数量调整」。这些方向都已经有 id=485、id=480、id=470、id=495、id=505、id=465 等成熟文章覆盖。但有一个同样关键、却尚未被系统化的方向是:单卡内部的显存压力治理——碎片化如何产生、如何度量、如何预防;OOM 如何预测、如何拦截、如何优雅降级;显存泄漏如何检测、如何根因、如何门禁。本文试图把这套工程体系完整地搭起来。
我把这个方向命名为「LLM 推理的内存压力工程 2026」,并把它形式化为一个四面体:分配、碎片、泄漏、峰值。这四面体在生产中任何一个维度失控都会触发 OOM,但每一个维度的治理手段、可观测性指标、降级策略都不一样,需要分开讨论。下面我从这个四面体出发,逐章展开。
在 LLM 推理服务的生产语境下,显存压力可以形式化为一个四面体,四个顶点分别是:
四面体的退化关系是:分配超限 → 分配器退化为高碎片模式 → 高碎片触发 OOM Killer → OOM 误杀触发级联崩溃 → 崩溃后清理不彻底导致泄漏累积 → 泄漏把分配起点抬高 → 整个四面体失衡。生产里 80% 的显存事故都符合这个退化链。理解这个链的工程意义在于:只优化一维是不够的——比如只做 KV cache 池化(降低分配维度),但不治理碎片(碎片维度没动),OOM 频率只是从 100% 触发降到 30% 触发;要做就四面体一起治。
四面体的可观测性矩阵对应四组核心指标:
| 维度 | 核心指标 | 阈值 | 工具 |
|---|---|---|---|
| 分配 | alloc.used_bytes / alloc.total_bytes | >0.92 告警 | torch.cuda.memory_allocated |
| 碎片 | alloc.reserved_but_unused_bytes / alloc.total_bytes | >0.15 告警 | torch.cuda.memory_reserved - allocated |
| 泄漏 | Δ alloc.used_bytes over 1h at zero QPS | >200MB/h 告警 | 自研 diff probe |
| 峰值 | peak.used_bytes / total_bytes | >0.95 告警 | torch.cuda.max_memory_allocated |
这四组指标必须在生产 Grafana 上同时布点,否则任何一维的失控都会被另外三维掩盖——这是过去三年我见过的最常见的可观测性盲点。
显存碎片化的拓扑分类可以借用操作系统内存管理的经典二分法:外部碎片与内部碎片。LLM 推理场景下两者的成因、测量方法、治理手段完全不同,需要分开讨论。
外部碎片指空闲块的总和足够,但没有任何一个空闲块满足请求尺寸。最经典的场景是 PagedAttention 的 KV cache 块(每块通常 16-64 个 token)被异构请求切碎——长请求占用 8 块连续空间,释放后变成「8 块独立的小空闲块」;短请求(<16 token)只能申请 1 块小空闲块,剩下的 7 块虽然在物理上相邻但逻辑上不连续,无法合并。生产里外部碎片率(external fragmentation ratio)的标准定义是:
EF = (max_contiguous_free - largest_request_size) / max_contiguous_free
其中 max_contiguous_free 是当前最大连续空闲块(单位:字节),largest_request_size 是预计最大请求的显存需求。当 EF > 0.3 时分配失败概率显著上升;EF > 0.6 时基本无法服务长 prompt。测量方法:每 30 秒扫一次 torch.cuda.memory_stats(),记录 max_contiguous_block_size 指标,并和当前排队请求的 p99 长度比对。
内部碎片指分配块大于实际请求尺寸造成的浪费。LLM 推理的内部碎片主要来自两个地方:(1) 对齐 padding——GPU 显存分配器(CachingAllocator)通常按 512 字节或 2KB 对齐,一个 1025 字节的请求会占用 1536 字节,碎片率 33%;(2) 预分配 round-up——为减少分配次数,分配器会把小请求聚合到大块里,导致大块里大量字节从未被实际写入。内部碎片率的工程定义是:
IF = (reserved_bytes - in_use_bytes) / reserved_bytes
其中 reserved_bytes 是分配器向 CUDA 申请的总量,in_use_bytes 是当前实际写入的张量总量。PyTorch 默认分配器的 IF 通常在 0.10-0.25 区间,超过 0.30 就需要主动治理。
碎片的工程治理有三个抓手:
block_manager 已经实现了细粒度预分配,但生产里仍有 15-20% 的内部碎片来自 attention workspace 的预分配(这是算法决定的,无法消除,只能监控)。torch.cuda.empty_cache() + 重分配,强制分配器回收碎片。代价是 1-3 秒的请求中断,需要在网关层做优雅拒绝(返回 503 + Retry-After),而不是让请求落到 OOM 上。kv_cache_layout="separate" 就是这个思路。碎片治理的工程目标是:把外部碎片率压到 0.2 以下,内部碎片率压到 0.15 以下。这两个阈值来自生产 P99 数据——低于这两个值,分配失败概率 <0.01%,超过则指数级上升。
KV cache 是 LLM 推理显存碎片的主要贡献者——典型生产服务里 KV cache 占单卡显存的 50-70%,碎片率的 70-80% 来自 KV cache 的页分配/释放。所以 KV cache 内存池的碎片治理是整个显存压力工程的核心战场。
PagedAttention(vLLM 0.2 引入,2023 年)的核心思想是把 KV cache 从「每请求一片连续显存」改为「按固定大小的页分配」,从根本上消除外部碎片——任意请求只需要 N 个不连续的页,分配器只关心是否有 N 个空闲页,不关心是否连续。这把外部碎片的复杂度从「连续块分配」降到「页计数分配」,工程上几乎完全可控。但代价是内部碎片率上升——一个 17-token 的请求在 16-token 页大小下需要 2 页(32 token 容量),浪费 47% 的页空间。
页大小的选择是碎片治理的第一性参数。从 2023 到 2026 年的生产数据看,页大小的演化经历三个阶段:
KV cache 内存池的碎片治理还有一个第二性问题:块释放的延迟。LLM 推理服务里 KV cache 块的生命周期是「请求进入 → 分配 N 块 → 请求完成 → 释放 N 块」,但请求完成的检测有时滞后——流式响应里最后一个 token 生成完后客户端可能立即断开连接,但服务端要等几秒钟的 keepalive 超时才能确认。这种延迟让块的实际释放时间比理论释放时间晚 5-30 秒,期间块仍然占着显存池,外部碎片率被人为抬高。生产里通常用「软释放 + 延迟硬回收」策略:请求完成后立即把块标记为「软空闲」(可被新请求抢占),但保留 30 秒才真正写回分配器;30 秒内如果客户端发新请求则复用,超过则硬回收。SGLang 的 radix_attention 实现了类似的「软缓存 + 硬回收」机制。
跨实例的 KV cache 共享也会引入新的碎片维度——同一 prompt prefix 在多实例间共享时,每个实例只缓存自己的部分 prefix,碎片模式被实例数放大。生产里通常用「前缀集中化」策略:在 5-10% 的「热实例」上缓存所有 prefix,其他「冷实例」按需向前缀实例拉取,把碎片压力集中到 5-10% 的实例上统一治理。
OOM(Out-of-Memory)是显存压力工程里最不可接受的事故——一旦触发,整个进程崩溃,未完成的请求全部丢失,客户端连接全部 reset,SLO 直接击穿。所以 OOM 预测比 OOM 治理更重要:能在 OOM 触发前 30 秒告警,就能让 SRE 在事故发生前主动干预。
OOM 预测的核心是「显存轨迹预测」——给定过去 N 秒的显存时间序列,预测未来 30-60 秒的显存占用曲线,如果曲线峰值超过显存容量的 90% 则告警。这个问题在时间序列预测领域已经很成熟(ARIMA、Prophet、LSTM、Transformer 都可以用),但 LLM 推理场景有几个特殊性需要单独设计:
针对这些特殊性,生产里通常用 LSTM + 干预感知 的混合架构:
输入:过去 30 分钟的显存时间序列 + 流量时间序列 + 干预事件日志
模型:双层 LSTM(64 + 32 单元)
输出:未来 30/60/120 秒的显存占用 P50/P95/P99 预测 + 不确定性区间
干预感知:把已知干预事件(清理、降级、迁移)作为额外特征输入,让预测自动让位
这个架构在生产里实测预测准确率(P99 误差 < 5% 算准确):稳态期 92%,突发期 78%,干预期 85%。平均准确率约 85%,相比纯 ARIMA 的 65% 提升 20 个百分点。重要的是告警提前量:85% 的 OOM 事件能在触发前 30-90 秒告警,给 SRE 留出干预窗口。
OOM 预测的工程化陷阱有三个:
OOM 预测是显存压力工程的预警层,与下面要讲的 graceful degradation 是响应层,两者必须配套——预警给 SRE 时间响应,降级给系统自己兜底能力。
即便有 OOM 预测告警,SRE 不一定能及时响应——半夜三点被叫醒需要 5-15 分钟才能上线操作,这段时间 OOM 还是会触发。所以系统自己必须有 graceful degradation 能力:主动降低显存压力,避免 OOM 触发。
LLM 推理的优雅降级有四个层级,按从轻到重排序:
四级降级的触发条件应该是自动的、阶梯式的,而不是 SRE 手动切换。生产里的标准实现:
显存占用 > 80%:触发 L1 轻度降级(batch size 减半)
显存占用 > 88%:触发 L2 中度降级(关闭 prefix caching)
显存占用 > 93%:触发 L3 重度降级(swap-to-CPU 启用)
显存占用 > 97%:触发 L4 极端降级(拒绝新请求)
每一级降级都有回退条件——显存回落到阈值以下 5 分钟才回退一级,避免抖动。SRE 可以通过控制台手动锁定在某一级别,强制不升级也不回退,用于排查或演练。
swap-to-CPU 工程的实现细节是 L3 降级的技术核心:
cpu_offloading 实现按 block swap。cpu_memory.swap_used 指标,超过 80% 就拒绝新 swap,强制 L4 降级。生产里 L3 降级的真实使用频率很低(< 1% 的请求会触发),但事故价值极高——它把 OOM 崩溃从不接受的「全服务中断」变成可接受的「部分请求慢」,这是 SLO 兜底的关键。
显存泄漏是显存压力工程里最难发现的一类问题——它不会立即触发 OOM,而是以每请求 1-10MB 的微泄漏在数小时到数天内累积触发 OOM。等 SRE 发现时往往已经处理了百万级请求,根因排查成本极高。生产里 70% 的「OOM 频率逐渐上升」事件根因都是微泄漏。
显存泄漏的检测有三种成熟方法,按检测灵敏度排序:
torch.cuda.memory_allocated(),然后强制把 QPS 压到 0(拒绝所有新请求、等待所有进行中请求完成),再记录 30 分钟后的 torch.cuda.memory_allocated()。如果差值 > 100MB/30min 就有泄漏嫌疑;> 500MB/30min 几乎确定是泄漏。torch.cuda.memory_allocated(),扣除流量影响(用流量归一化:leak_rate = Δmemory / Δrequest_count),如果 leak_rate > 1MB/request 就有泄漏嫌疑。torch.cuda.memory_allocated(),如果峰值回不到 baseline(差值 > 10%)就有泄漏嫌疑。根因分析通常落到四个常见来源:
回归门禁是防止新泄漏引入的最后一道防线——CI 流水线里必须跑一个「显存压力回归测试」:
1. 起一个标准推理服务(Llama-3-70B + vLLM 0.7)
2. 用真实流量回放器跑 10,000 个请求(覆盖所有 prompt 长度区间)
3. 结束后跑零流量差分探测,等待 30 分钟
4. 检查差值:< 50MB 为 PASS,50-200MB 为 WARNING,> 200MB 为 FAIL
5. FAIL 的 PR 不能合并
这套门禁在生产里把显存泄漏事故率从每月 2-3 次降到每月 0-1 次,效果显著。关键是门禁要跑真实模型 + 真实流量回放,而不是单元测试里的 mock tensor——微泄漏通常只在真实模型的完整前向-反向链路里才出现。
多卡 LLM 推理服务里,显存压力工程还有一个跨卡维度——多张 GPU 之间通过 NVLink/NVSwitch 互联,跨卡通信带宽远高于跨节点通信,但显存是各自独立的。当模型用 tensor parallelism(TP)切到多张卡时,每张卡只持有 1/N 的模型权重和 KV cache,显存压力被分摊;但当某些请求的 KV cache 集中在某一张「热卡」上时,那张卡的显存压力会远超其他卡,触发局部 OOM。
显存亲和性调度的目标是让每张卡的显存压力均衡——所有卡的 alloc.used_bytes / total_bytes 应该在 ±10% 区间内。生产里通常用两种策略:
NUMA 亲和性是多卡场景的第二层问题——每张 GPU 通过 PCIe 连接到 host CPU,CPU 与 GPU 的距离(NUMA node)影响 PCIe 带宽和延迟。当 swap-to-CPU 触发时(见第六章),CPU 侧的 KV cache 块应该分配在与 GPU 同 NUMA node 的内存上,否则 PCIe 跨 NUMA 传输会让 swap 性能下降 30-50%。
生产里 NUMA 亲和性的实现通常是 numactl --membind=<numa_node> <command>——把推理服务进程绑定到特定 NUMA node 的内存上,强制 KV cache 块只在那个 node 上分配。这在 4-GPU / 8-GPU 的单机配置下效果最显著,跨节点(multi-node)配置下效果递减(因为跨节点已经走 InfiniBand/RoCE,NUMA 不是瓶颈)。
显存亲和性 vs 负载均衡——这两个目标经常冲突:亲和性想「让相关请求落到同一张卡」(cache 命中率高),负载均衡想「让请求分散到不同卡」(单卡压力低)。生产里通常用两级调度:
这个两级调度能把 cache 命中率维持在 70-80%,同时保持显存不均衡度 ±10% 以内。
最后给 SRE 一份可直接落地的内存压力可观测性清单,覆盖四面体的四个维度 + 治理动作的反馈信号。这份清单是我过去三年在多个生产 LLM 推理服务上沉淀下来的最小可观测性集合——少于这个集合,事故无法预防;多于这个集合,告警疲劳不可避免。
分配维度:
gpu.alloc.used_bytes —— 单卡已分配字节,按卡分别采集gpu.alloc.total_bytes —— 单卡总容量(不变,但需采集用于计算利用率)gpu.alloc.utilization_ratio —— used / total,按卡分别计算碎片维度:
4. gpu.alloc.max_contiguous_block_bytes —— 最大连续空闲块(外部碎片率的核心指标)
5. gpu.alloc.reserved_unused_bytes —— reserved 但 unused 的字节(内部碎片率的核心指标)
6. gpu.fragment.external_ratio —— 1 - max_contiguous_block / total_free
7. gpu.fragment.internal_ratio —— reserved_unused / reserved
泄漏维度:
8. gpu.leak.zero_qps_diff_bytes_per_hour —— 零流量下每小时显存增长
9. gpu.leak.peak_recovery_ratio —— 峰值后显存回弹比例(应 ≥ 0.9)
峰值维度:
10. gpu.peak.used_bytes_during_request —— 单请求生命周期内的显存峰值
11. gpu.peak.used_bytes_global —— 全局 max_memory_allocated(进程启动以来峰值)
降级反馈维度:
12. gpu.degradation.current_level —— 当前降级级别(L1/L2/L3/L4)
13. gpu.degradation.swap_to_cpu_bytes —— swap-to-CPU 已用字节
gpu.alloc.utilization_ratio > 0.92 持续 5 分钟 → P3 告警(显存紧张)gpu.fragment.external_ratio > 0.30 → P3 告警(碎片率高)gpu.fragment.internal_ratio > 0.20 → P3 告警(内部碎片率高)gpu.leak.zero_qps_diff_bytes_per_hour > 200MB → P2 告警(泄漏嫌疑)gpu.degradation.current_level >= L3 持续 10 分钟 → P2 告警(重度降级)gpu.peak.used_bytes_global / total > 0.95 → P2 告警(接近物理极限)P95(gpu.alloc.utilization_ratio) > 0.95 → P2 告警(OOM 预警)gpu.degradation.current_level = L4 持续 1 分钟 → P1 告警(正在拒绝请求)torch.cuda.empty_cache(),验证是否能成功整理碎片、回退到 L0。这份清单的最小可行版本是 13 项指标 + 8 条告警,事故场景和回归门禁可以分阶段补齐。一个 50 张卡的 LLM 推理服务,部署这套清单大约需要 2-3 个 SRE 周的工作量,但它能把显存事故率从每月 3-5 次降到每月 0-1 次,把事故平均恢复时间(MTTR)从 30-60 分钟降到 5-10 分钟,工程 ROI 极高。
至此,LLM 推理的内存压力工程 2026 的完整闭环已经搭起来:四面体形式化是理论基础,碎片治理 + KV cache 池化是分配层的优化,LSTM OOM 预测 + 优雅降级是响应层的保障,显存泄漏检测 + 回归门禁是工程纪律的落地,NUMA 亲和性调度是跨卡场景的延伸。这套体系与现有的 KV cache 工程、PD 分离、弹性伸缩方向正交互补,共同构成 LLM 推理服务的完整生产治理栈。
Conversation
0 条