模型推理服务在刚部署时一切正常,吞吐和延迟都符合预期,但运行一段时间后响应时间逐渐拉长,最终触发超时告警,是典型的推理服务内存泄漏表现。排查这类问题不能只盯着GC日志,还得从推理引擎的资源管理机制、输入输出的显存分配策略以及批量请求的调度方式入手。
常规Web服务的内存泄漏,工具链成熟,heap dump抓下来用MAT或JProfiler分析即可定位。模型推理服务则不同——内存被分为两块独立区域,一块是CPU侧的系统内存,存放请求队列、序列化缓存、前后处理中间结果,另一块是GPU侧显存,存放模型权重、KV Cache、激活值。系统内存泄漏的排查思路和普通应用一致,显存泄漏则完全依赖推理引擎的API和CUDA工具链。
显存泄漏的隐蔽性在于,GPU显存管理不像JVM有自动GC,很多推理框架的Allocator实现非常原始,一旦代码路径上申请了显存但没有显式释放,内存分析工具很难给出精确的引用链。更麻烦的是,PyTorch的CUDA缓存机制会让显存占用看起来一直在增长,但实际是正常的缓存行为,需要区分缓存膨胀和真实泄漏。
PyTorch的CUDA内存分配器会缓存已释放的显存块,避免频繁调用cudaFree带来的性能开销。这意味着即使Tensor析构了,nvidia-smi看到的显存占用也不会立刻下降。用torch.cuda.memory_allocated()可以拿到真实分配量,torch.cuda.memory_reserved()则是缓存总量,二者的差值就是缓存池大小。
排查时先跑一个长时间压测,每隔一段时间记录这两个值。如果allocated持续增长且不回落,是真实泄漏。如果allocated稳定但reserved持续增长,是缓存池在扩容,可以通过torch.cuda.empty_cache()手动释放,或者调整PYTORCH_CUDA_ALLOC_CONF环境变量中的max_split_size_mb参数减少碎片化缓存。
自回归模型推理时,每个请求的KV Cache占用会随生成token数线性增长。如果推理引擎未对并发请求的KV Cache总量做硬限制,流量峰值时大量请求同时生成长文本,显存会迅速耗尽。vLLM和SGLang这类框架实现了PagedAttention,将KV Cache分块管理,按需分配,块内碎片也会导致实际占用高于理论值。
排查KV Cache泄漏,先检查推理引擎的并发配置。以vLLM为例,max_num_seqs控制最大并发请求数,max_model_len限制单个请求的上下文长度上限,二者乘积决定了KV Cache的理论最大值。如果gpu_memory_utilization设得过高,KV Cache在请求完成后未被及时回收,显存就会逐渐被占满。观察vLLM日志中的gpu_cache_usage指标,如果这个值在请求量下降后仍不回落,基本可以确定是KV Cache的回收逻辑有问题。
模型推理的输入预处理通常涉及tokenization、embedding查表、图像resize和归一化,这些操作在CPU或GPU上产生大量临时张量。如果一个张量在预处理函数中被创建,但函数返回时只取走了它的视图或切片,原始张量仍被引用,导致此后的每次推理都累积未释放的内存。
排查这个环节,用PyTorch的torch.cuda.memory_summary()抓取当前显存分配的快照,输出会按调用栈聚合,显示每个操作分配了多少显存、释放了多少、还保留了多少。如果某个预处理操作的累计分配量远大于释放量,就该检查这个操作对应的代码。
一个高频的泄漏点是用torch.Tensor.numpy()或torch.Tensor.tolist()转换张量时,原张量在GPU上,转换后的数据在CPU上,但GPU上的原张量没有被显式del或置为None。TensorFlow Serving环境中,则要关注tf.data.Dataset管道中的prefetch和cache配置,不当的配置会导致整个数据集被缓存到内存中。
生产环境通常用多进程或多线程并发处理推理请求。每个子进程或线程的CUDA上下文是独立的,创建和销毁都有显存开销。如果推理服务使用了动态进程池,进程频繁创建销毁,CUDA上下文也跟着反复初始化,不仅性能抖动,还会在某些GPU驱动版本下出现上下文残留。
排查时查看/proc/[pid]/maps中是否有大量未释放的CUDA驱动映射,或者用nvidia-smi查看进程的显存占用是否与进程生命周期一致。如果子进程已经退出,但显存没有回收,属于驱动层面的bug,需要升级驱动或切换为静态进程池的部署方式。
一次性的内存泄漏排查解决的是当前问题,要把监控做进日常巡检才能避免重复踩坑。至少需要采集三个维度的指标。
系统内存维度,从process_resident_memory_bytes和go_memstats_alloc_bytes(Go服务)或jvm_memory_used_bytes(Java服务)取进程内存占用,结合请求量做相关性分析,如果请求量下降但内存不降,触发告警。
GPU显存维度,用nvidia-smi的--query-gpu=memory.used配合Prometheus Node Exporter或DCGM采集,同时用推理框架暴露的kv_cache_usage指标跟踪KV Cache的真实占用,与显存总量做对比,设置75%和90%两级告警。
请求级维度,记录每个请求的端到端延迟和单次推理耗时,模型推理服务的延迟突增往往是内存压力导致的GC耗时或swap操作,这两个指标比内存绝对值更直接反映系统健康状态。
| 监控维度 | 关键指标 | 采集方式 |
|---|---|---|
| 系统内存 | 进程RSS、GC停顿时间 | Prometheus Node Exporter / JVM JMX |
| GPU显存 | 显存使用量、KV Cache占用 | DCGM / 推理框架内置metrics |
| 请求延迟 | P50/P99端到端延迟 | 服务端埋点 + Prometheus Histogram |
内存泄漏的排查最终靠的是把内存的分配与释放路径可视化,与其在灭火时反复dump内存,不如在系统健康时就把这些指标拉起来,让内存曲线的异常趋势先于用户投诉被看到。
评论
加载中...