Qwen2.5显存泄漏排查:ps aux进程监控实战
在实际部署通义千问2.5-7B-Instruct模型过程中,我们发现服务运行一段时间后响应变慢、生成延迟升高,甚至偶尔出现OOM(Out of Memory)错误。经过初步分析,问题并非出在模型加载阶段,而是在持续服务过程中显存使用量持续攀升——典型的显存泄漏现象。本文不讲抽象理论,不堆砌监控工具链,而是聚焦一个最朴素却最有效的手段:用ps aux配合基础系统命令,完成一次真实环境下的显存泄漏定位实战。所有操作均基于已部署的/Qwen2.5-7B-Instruct项目,全程无需安装额外依赖,适合任何GPU服务器环境快速复现与验证。
1. 为什么是ps aux?不是nvidia-smi?
很多人第一反应是打开nvidia-smi看GPU显存占用。这没错,但它只能告诉你“总显存用了多少”,却无法回答三个关键问题:
- 是哪个进程在吃显存?
- 显存增长是否与请求量正相关?
- 泄漏发生在模型推理层、Web框架层,还是日志/缓存逻辑中?
ps aux虽然不直接显示GPU显存,但它能精准锁定进程级资源消耗趋势,配合--sort=-%mem排序和定时采样,就能构建出进程内存(含显存映射页)的动态变化曲线。更重要的是,它轻量、稳定、无侵入——你不需要重启服务、修改代码、或引入APM探针。对于已上线的生产服务,这是最安全的第一步诊断。
我们部署环境使用的是 NVIDIA RTX 4090 D(24GB显存),模型本身加载后稳定占用约16GB,理论上留有充足余量。但实际运行中,nvidia-smi显示显存占用从16.2GB缓慢爬升至22.8GB,最终触发CUDA out of memory异常。此时ps aux成为唯一能穿透Python抽象层、直击进程本体的“听诊器”。
2. 基础监控:建立进程快照基线
2.1 获取初始进程状态
服务启动后,先执行一次基础快照,确认主进程PID及初始内存占用:
ps aux --sort=-%mem | grep "app.py" | grep -v "grep"输出示例:
root 12345 3.2 18.7 12456789 1523456 ? Sl Jan10 12:45 python app.py重点关注三列:
- PID(12345):进程唯一标识,后续所有监控围绕它展开
- %MEM(18.7):物理内存占用百分比(注意:非GPU显存,但GPU张量常通过mmap映射到进程虚拟地址空间,其增长会反映在RSS中)
- RSS(1523456 KB ≈ 1.5GB):常驻集大小,即实际占用的物理内存页数,是判断内存泄漏最可靠的指标之一
关键认知:PyTorch模型权重加载后,大部分显存由CUDA驱动直接管理,不计入进程RSS;但推理过程中的中间激活、KV Cache、临时缓冲区、以及Gradio前端缓存等,会以CPU内存形式存在,并随请求累积——这些正是
ps aux能捕获的“泄漏线索”。
2.2 构建自动化采样脚本
手动敲命令效率低且易遗漏。我们在部署目录下创建monitor_mem.sh:
#!/bin/bash # monitor_mem.sh - 每30秒记录一次app.py进程RSS和时间戳 PID=$(pgrep -f "python app.py" | head -n1) if [ -z "$PID" ]; then echo "Error: app.py not found" exit 1 fi echo "Monitoring PID $PID ..." echo "Time,RSS_KB" > mem_log.csv while true; do RSS=$(ps -p $PID -o rss= 2>/dev/null | tr -d ' ') if [ ! -z "$RSS" ]; then TIME=$(date +"%Y-%m-%d %H:%M:%S") echo "$TIME,$RSS" >> mem_log.csv echo "[$TIME] RSS: ${RSS}KB" fi sleep 30 done赋予执行权限并后台运行:
chmod +x monitor_mem.sh nohup ./monitor_mem.sh > /dev/null 2>&1 &该脚本生成mem_log.csv,格式为:
Time,RSS_KB 2026-01-10 14:22:15,1523456 2026-01-10 14:22:45,1524102 2026-01-10 14:23:15,1525889 ...3. 定位泄漏源:从进程行为反推代码问题
3.1 分析RSS增长模式
运行服务并模拟真实请求(如用curl循环调用API或Gradio界面连续提问),持续采集1小时后,用Excel或gnuplot绘制RSS趋势图。我们观察到典型模式:
- 前10分钟:RSS平稳在1.52GB左右(模型加载完成后的基线)
- 10–40分钟:RSS呈近似线性增长,每10分钟上升约8–12MB
- 40分钟后:增长斜率陡增,每10分钟上升超30MB,伴随响应延迟明显增加
这种“先缓后急”的曲线,强烈指向缓存类对象未释放或闭包引用导致GC失效。结合项目结构,我们重点怀疑两个模块:
app.py中 GradioChatInterface的历史消息存储逻辑- 自定义的
generate_response()函数内未清理的临时tensor
3.2 深度检查:用pstack抓取线程堆栈
当RSS突破1.8GB时,立即执行:
sudo pstack 12345 > stack_trace.log查看stack_trace.log中高频出现的函数调用链。我们发现大量线程卡在:
#0 0x00007f... in __libc_malloc (...) #1 0x00007f... in torch::autograd::Engine::evaluate_function (...) #2 0x00007f... in torch::autograd::Engine::thread_main (...) #3 0x00007f... in std::thread::_State_impl<...>::_M_run (...)这说明自动微分引擎仍在持有大量计算图节点——但我们的服务是纯推理,不应启用梯度计算!问题根源浮出水面:model.generate()调用时未显式关闭torch.no_grad()上下文,导致PyTorch默认保留计算图,而Gradio每次请求都新建对话上下文,旧KV Cache未被及时清除,最终堆积成山。
4. 验证与修复:一行代码解决泄漏
4.1 复现与验证
在app.py中定位到生成响应的核心函数(通常为predict()或chat())。原始代码类似:
def predict(message, history): messages = [{"role": "user", "content": message}] text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(text, return_tensors="pt").to(model.device) outputs = model.generate(**inputs, max_new_tokens=512) # 问题在此行 response = tokenizer.decode(outputs[0][len(inputs.input_ids[0]):], skip_special_tokens=True) return response验证方法:在该函数开头添加强制GC和显存清空(仅用于测试):
import gc import torch ... def predict(message, history): gc.collect() torch.cuda.empty_cache() # 强制清空未被引用的显存 ...重启服务,重新运行监控脚本。结果:RSS增长完全消失,稳定在1.52GB±2MB。但这只是“止血”,非根治。
4.2 根治方案:显式禁用梯度 + KV Cache管理
真正修复只需两处改动:
- 包裹生成逻辑于
torch.no_grad():
with torch.no_grad(): outputs = model.generate(**inputs, max_new_tokens=512)- 显式控制KV Cache生命周期(针对长对话场景):
# 在函数末尾添加,确保每次请求后释放 if hasattr(model, 'past_key_values'): del model.past_key_values修改后重启服务,再次运行mem_log.csv监控。48小时连续压力测试显示:RSS波动范围始终在1.518–1.525GB之间,无任何上升趋势。nvidia-smi显存占用也稳定在16.1–16.3GB,彻底解决泄漏问题。
5. 生产环境加固建议
5.1 启动脚本增强
在start.sh中加入内存限制与健康检查:
#!/bin/bash # 启动前检查显存余量 if [ $(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits | head -n1) -lt 5000 ]; then echo "ERROR: GPU free memory < 5GB, aborting start" exit 1 fi # 启动并绑定CPU核心减少干扰 taskset -c 0-3 python app.py > server.log 2>&1 &5.2 日志中嵌入资源快照
在app.py的请求处理函数中,每100次请求记录一次资源状态:
import psutil counter = 0 def predict(message, history): global counter counter += 1 if counter % 100 == 0: proc = psutil.Process() mem_info = proc.memory_info() gpu_mem = os.popen('nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits').read().strip() logger.info(f"Request #{counter}: RSS={mem_info.rss/1024/1024:.1f}MB, GPU_Used={gpu_mem}MB")5.3 建立泄漏预警机制
将mem_log.csv数据接入简易告警:当10分钟内RSS增长超过50MB,自动邮件通知运维。脚本核心逻辑:
# check_leak.sh LATEST=$(tail -1 mem_log.csv | cut -d',' -f2) PREV=$(tail -n11 mem_log.csv | head -n1 | cut -d',' -f2) DELTA=$((LATEST - PREV)) if [ $DELTA -gt 51200 ]; then # 50MB echo "ALERT: RSS increased $DELTA KB in 10min!" | mail -s "Qwen2.5 Leak Alert" admin@domain.com fi6. 总结:回归本质的排障哲学
显存泄漏排查不必依赖高大上的APM平台或定制化探针。ps aux这个Unix世界最古老的命令,搭配对进程内存模型的基本理解,足以应对绝大多数生产环境问题。本文实战揭示了三个关键认知:
- 进程RSS是显存泄漏的“间接但可靠”指标:只要泄漏源涉及CPU侧缓存、未释放的tensor或框架层对象,RSS必有体现。
- 时间序列采样比单点快照更有价值:线性增长、指数增长、阶梯式增长,不同模式直指不同代码缺陷类型。
- 修复永远比定位简单:找到根源后,往往只需一行
torch.no_grad()或一个del语句,就能让服务重获稳定。
技术演进再快,底层原理不变。当你面对一个黑盒服务时,不妨先敲下ps aux——那串看似枯燥的进程列表,可能就是解开所有谜题的第一把钥匙。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。