GPU显存不释放怎么办?从进程排查到kill信号全攻略
发布时间:2026/10/1 19:20:14来源:尧图网络
刚把训练脚本CtrlC终止一跑nvidia-smi显存还是满满当当地被吃着新任务直接OOM。我相信每个做深度学习的人都被这个场景折磨过。更气人的是你用ps aux | grep python查到一堆残留进程kill -9打过去PID倒是没了显存依然一动不动。这篇文章就把这个坑彻底讲透GPU显存为什么在进程死后不释放怎么用ps、lsof这类命令把占用源揪出来kill -2、kill -15、kill -9三种信号到底什么区别以及怎么看一个进程是不是已经变成僵尸(defunct)了遇到僵尸该怎么办。这篇内容适合所有用GPU跑训练或推理的开发者不管你是用PyTorch、TensorFlow还是自己撸CUDA显存管理的问题早晚会碰到。把下面的排查逻辑和信令机制搞明白下次遇到显存莫名其妙少了几个G或者kill完进程还卡着显存你就能自己动手解决而不是靠重启机器续命。1. GPU显存被占用的底层原因先搞清楚谁是凶手1.1 显存生命周期进程退出后显存到底由谁回收很多人默认进程死了显存就自动释放这句话在大多数情况下是成立的但成立的前提是驱动正确地收到了进程退出的通知。GPU显存和CPU内存不一样它的分配、释放由显存驱动栈统一管理。当你的Python进程正常退出或崩溃退出内核里的GPU驱动模块会清理这个进程对应的地址空间显存会被标记为可用。但这里有个致命前提进程必须是真的死透了。现实里经常出现的情况是你CtrlC杀掉的是终端的前台进程但训练脚本里用了multiprocessing或torch.distributed启动了多个子进程这些子进程虽然由同一个父进程衍生但它们各自持有独立的CUDA context。父进程挂了子进程成了孤儿进程被init收养如果子进程没有被正确终止它们的显存占用就一直挂在那边。这就是为什么你杀了主进程显存纹丝不动——因为你杀的根本不是全部。另一个隐蔽的情况是进程还活着只是你没能找到它。用ps aux | grep python输出的结果里真正需要关注的是第二列的PID和第八列的STAT状态。很多人在一个多机多卡的训练任务结束后会残留大量训练进程但他们的grep命令写得太宽松匹配出了一堆自己的命令行实际上的目标进程躲在更后面。1.2 僵尸进程和显存占用的关系别把脏水泼给僵尸说到僵尸进程很多人就直接联想到杀不死、显存不释放这个概念需要澄清。僵尸进程zombieSTAT标记为Z的本质是子进程已经退出但父进程没有调用wait()系统调用来获取它的退出状态所以内核保留了进程描述符。换句话说僵尸进程本身是死的它不占用CPU不占用显存也不占用内存——它只占用一个进程表项和一个PID。所以如果你看到ps输出里有一个defunct或者标记为Z的进程显存还占着那一定另有原因。要么是还有别的活着的进程在占显存要么是这个僵尸进程对应的真正GPU上下文没有释放但这种情况极其罕见。更常见的坑是你看到的僵尸进程是某个多进程训练框架的残留而真正持有显存的worker进程还欢快地跑着只是你没从ps的输出里识别出来。1.3 CUDA context与显存镜像为什么进程没了显存还在再往深挖一层驱动并不是简单地在进程退出时立刻把显存清空。现代GPU驱动为了提高效率在进程内会缓存一些内存块而CUDA runtime也在进程地址空间里维护着一批显存池。正常情况下这些缓存会在进程退出时随上下文销毁。但如果你是在容器里跑训练情况会更复杂——容器内的进程退出容器本身还活着GPU上下文可能还挂在容器对应的命名空间里。还有一种场景是使用了CUDA Managed Memory或者多进程共享显存导致同一块显存有多个引用计数。当一个进程退出时因为它不是唯一的持有者显存不会被立即释放而另一个持有者如果一直不退出显存就一直被占用。这一点和Linux文件删除后仍然占用磁盘空间的逻辑很像文件在、inode在、只是目录项没了只有所有fd关闭后空间才真正释放。2. 三步锁定占用GPU的进程从nvidia-smi到ps的全链路排查2.1 第一步用nvidia-smi看的是当前会话还是所有进程排查显存占用第一个命令永远是nvidia-smi。默认输出的底部Processes表格会列出当前使用GPU的进程包含PID、进程名、显存占用。但这里有个细节如果同一个GPU被多个用户或者多个容器共享nvidia-smi默认可能只显示当前用户/当前容器的进程你需要用sudo nvidia-smi才能看到所有进程或者用nvidia-smi --query-compute-appspid,process_name,used_memory --formatcsv来拿到所有计算进程清单。实际排查中我建议直接这样nvidia-smi --query-compute-appspid,used_memory,process_name --formatcsv输出会长这样pid, used_memory, process_name 12345, 8120MiB, python 12467, 8120MiB, python如果这里还有python进程在那说明确实有残留。注意两列显存占用相同的现象很常见尤其是用PyTorch DDP启动的多个进程每个进程会各自预留一部分显存。这时候你要判断的是这些PID到底是不是真的活着。2.2 第二步用ps aux加grep python看清进程状态和父子关系拿到PID之后下一步就是用ps去确认进程的真实状态。很多人直接ps aux | grep python看到一堆结果就懵了。这里的关键在于理解ps输出每一列的含义USER哪个用户跑的、PID进程号、PPID父进程号、%CPU、%MEM、VSZ、RSS、STAT进程状态、START、TIME、COMMAND。排查时我习惯加一个完整格式ps -ef | grep python # 或 ps aux | grep python | grep -v grep注意那个grep -v grep是为了把自己这条grep命令行过滤掉。很多新手在ps输出里看到一个包含grep python的进程还以为自己有什么残留程序。更推荐的做法是直接根据PID查询ps -o pid,ppid,stat,cmd -p PID这条命令会把某个具体PID的状态、父进程、完整命令行显示出来。STAT一列如果显示Z那就是僵尸如果显示D代表不可中断的睡眠这时候kill也杀不掉如果显示S或R说明进程还活着可以正常kill。2.3 第三步用fuser和lsof找到GPU设备上的所有进程ps只解决看到进程的问题但要确认一个进程是否真的在使用GPU更直接的方法是查GPU设备文件。NVIDIA驱动在/dev/nvidia*上创建设备节点任何使用GPU的进程都会打开这些设备文件。所以fuser命令可以直接列出谁在占用设备sudo fuser -v /dev/nvidia*输出会列出每个设备文件对应的进程PID和用户。如果某个PID在fuser列表里但不在nvidia-smi的列表里说明这个进程只是加载了驱动上下文还没有实际申请显存。反过来如果nvidia-smi里有显存占用但fuser里找不到多半是容器或者权限隔离导致看不到。lsof也可以用来补充查询sudo lsof /dev/nvidia*这条命令能看到哪个进程打开了NVIDIA设备文件对于定位到底是谁在偷偷用GPU非常有用特别是你怀疑有非python程序在占显存的时候。比如有些数据加载库会拉起多进程这些worker进程名可能不叫python而是叫别的可执行文件。3. kill命令信号机制kill -2、kill -15、kill -9到底干了什么3.1 三个信号的底层逻辑优雅退出和强制终止的区别很多人把kill -9当成万能杀招觉得信号越大越厉害这是个误区。kill默认发的是SIGTERM15号信号意思就是请终止信号本身不含强制属性进程可以自行决定是否处理。比如Python里注册了signal.signal(signal.SIGTERM, handler)就能在退出前做清理而如果你没注册自定义handlerPython解释器默认的行为就是直接退出。kill -2发的是SIGINT也就是中断信号和你在终端按CtrlC效果一样。它只是比SIGTERM更偏向用户主动打断很多程序会针对SIGINT做一些特殊处理比如保存检查点、打印当前loss再退出。所以你在训练的时候按CtrlC通常触发的是SIGINT而用kill 默认触发SIGTERM。kill -9发的是SIGKILL相当于立刻处决。这个信号不能被进程捕获、不能被忽略、不能注册handler内核直接把这进程从运行队列里摘掉回收CPU和内存。注意回收内存说的是CPU内存GPU显存由另一套驱动机制管理SIGKILL之后驱动会收到进程释放事件但清理动作可能异步进行极端情况下接口卡住显存就卡住了。3.2 为什么不建议上来就kill -9从GPU训练的角度讲直接kill -9最大的问题是不会给进程任何清理机会。如果这个进程持有文件锁、写入训练日志或者正在保存模型权重SIGKILL会把这些操作打断留下一个写到一半的模型文件下次训练加载时直接报错。更麻烦的是如果它正在写TensorBoard的event文件可能会留下损坏的日志TensorBoard解析的时候会警告。信号优先级应该是先kill -2SIGINT相当于CtrlC让程序保存现场过几秒检查不行就kill -15SIGTERM再给一次机会让有handler的脚本做清理最后才kill -9。给进程几秒钟的宽限期比自己事后修补文件要省事得多。这里有一个我自己的习惯在写训练脚本时会在signal.SIGTERM里注册一个钩子把当前epoch的模型参数保存下来。这样无论是我自己kill还是别人kill模型都不会丢。这个钩子在线上出故障的时候救命过好几次后面细说。3.3 kill -9之后显存还是没释放的几种真凶用kill -9杀掉进程后nvidia-smi还是显示显存占用这时候需要冷静。大多数情况下真正的问题不是显存没释放而是你没杀干净。常见的原因有三种第一种是进程变成了僵尸。子进程已经退出父进程没回收所以你看到ps里还有一条僵尸记录它的显存其实早就归还了只是进程表里还挂着一个死壳。这种不用管只需要处理父进程就行。第二种是多进程残留。你kill了主进程但子进程还活着显存自然不释放。排查方法前面写过fuser -v /dev/nvidia*能看到所有进程一个个核对。第三种是驱动本身卡住了。这种情况比较少见通常伴随dmesg里大量NVRM相关报错。如果确认所有进程都没了、显存还不释放可以试试重新初始化GPU先卸载nvidia_uvm模块再加载这条操作在服务器上影响面比较大生产环境别乱试。sudo rmmod nvidia_uvm sudo modprobe nvidia_uvm实测在驱动正常的情况下这个操作可以清掉驱动层卡住的显存引用。3.4 补充一个信号传递的坑kill的其实是错误进程ps aux | grep python输出的列表里有时候你会看到PID很小的python进程比如PID 1或者PID 7之类。这种是容器里的init进程或者系统初始化脚本里拉起的常驻进程。盲目kill -9 PID 1在某些容器环境里直接等于shutdown整个容器。所以在kill之前务必用ps -o pid,ppid,cmd -p PID确认这个进程到底是什么来头。特别是看到PPID为0或者1的进程除非你有十足把握否则不要轻易动。4. 僵尸进程完全解读识别、产生原因与清理4.1 僵尸进程的产生过程为什么它会一直赖在ps里用一个通俗的比喻进程结束的时候内核会给父进程发一个SIGCHLD通知父进程接收到通知后需要调用wait()来领取子进程的死亡报告然后内核才会把子进程的占位符从进程表里清掉。如果父进程自己很忙一直没空调用wait()或者父进程自己也挂了但新的父进程通常是init进程没有及时接管那个占位符就一直留着这就是僵尸。僵尸进程的STAT状态在ps里显示为ZCOMMAND尾常常跟着defunct标记。它不占CPU也不占内存但会占一个PID。Linux的PID数量有上限默认一般是32768或者更大如果僵尸进程堆积最终会导致系统无法创建新进程这个后果在GPU服务器上很常见因为训练脚本频繁拉子进程、父进程又没管理好。确认一个进程是不是僵尸的最简单方法ps aux | grep -w Z # 或者 ps -ef | grep defunct | grep -v grep如果输出里有内容那些就是僵尸。4.2 为什么kill -9杀不掉僵尸进程这是很多人最费解的地方我一个kill -9打过去为什么进程还在原因很简单——僵尸进程已经死了。你能kill掉的只是活着的进程僵尸进程已经处于退出状态SIGKILL再暴力也只是对一个已经不存在的运行体发信号不会有任何效果。想要清掉僵尸进程得收拾它的父进程。你有两个选择要么把父进程杀掉让僵尸进程被init进程收养init会周期性调用wait()把它们收回要么找到父进程后给父进程发送SIGCHLD信号让它去处理自己的子进程但这个只对写了handler的程序有效大多数Python脚本没有。所以最有效的做法就是杀掉父进程ps -o ppid -p 僵尸PID # 得到父进程PID后 kill -9 父进程PID注意如果父进程本身是PID 1那就是init进程理论上init会自动回收僵尸只是可能没那么及时。如果容器环境里PID 1是业务的init脚本kill -9 PID 1会把整个容器搞挂要小心。4.3 从根源上减少僵尸进程父进程的wait与管理与其每次手动清僵尸不如从代码层面减少僵尸的产生。在Python多进程训练中最典型的问题是使用了multiprocessing.Pool或者concurrent.futures.ProcessPoolExecutor但主进程异常退出导致worker子进程变成孤儿。正确的做法是给主进程加超时管理和优雅退出逻辑。另一个实际工程经验是使用torch.multiprocessing的时候尽量用spawn启动方式不要用fork。fork方式在子进程里如果继承了父进程已初始化的CUDA context会导致显存分配异常。用spawn的方式会重新导入模块每个子进程自己初始化CUDA显存管理更干净但也更慢。在显存灵敏的场景里这个trade-off是值得的。4.4 僵尸进程和显存泄漏一次真实的排障过程我之前遇到过一次很典型的案例一个长期运行的推理服务每天固定时间用multiprocessing启动一批worker任务结束后workers正常退出但服务进程没有写wait回收逻辑。连续跑了一周后ps aux | grep defunct列出来一百多个僵尸进程虽然显存没爆但服务开始报OSError: [Errno 11] Resource temporarily unavailable。原因就是PID被僵尸占光了新的worker拉不起来。那次排障花了大半天最后定位到问题就是父进程的join逻辑写错了位置。修好后服务连续三个月没出过问题。所以遇到显存相关的问题第一步看活进程第二步看僵尸数量两步分开排查会少走很多弯路。5. 实操从发现显存占用到彻底清理的完整流程5.1 一条龙排查命令我把整个排查到清理的流程整理成一个可复制的操作序列按顺序执行即可# 第1步查看GPU整体占用情况 nvidia-smi # 第2步列出GPU上的所有计算进程 nvidia-smi --query-compute-appspid,used_memory,process_name --formatcsv # 第3步查看GPU设备上的所有文件持有者 sudo fuser -v /dev/nvidia* # 第4步根据第2步拿到的PID确认进程状态、父进程、命令行 ps -o pid,ppid,stat,etime,cmd -p PID # 第5步确认其中没有需要保留的按顺序kill # 先给一个SIGTERM等5秒 kill -15 PID sleep 5 # 还没死再给SIGKILL kill -9 PID # 第6步看看有没有僵尸进程 ps -ef | grep defunct | grep -v grep # 第7步有僵尸就看父进程是谁 ps -o pid,ppid,stat,cmd -p 僵尸PID # 第8步杀掉僵尸的父进程确认父进程可杀后 kill -9 父PID这套流程跑完90%的显存残留问题都能解决。剩下没解决的多半是驱动级问题重启或者重装驱动。5.2 常见问题与排查速查表为了节省时间我整理了一张速查表可以直接对照症状找方案现象可能原因处理方式nvidia-smi显示大量python进程占用显存训练进程残留逐个ps核对后kill -15再kill -9ps里能看到PID但kill后仍在进程处于D状态等待或排查IO问题必要时重启ps里显示Z和defunct僵尸进程杀掉其父进程确认父进程可杀再操作kill -9后显存仍然不释放进程没杀干净或驱动卡住fuser核对残留rmmod/modprobe nvidia_uvm容器内杀进程导致整个容器退出误杀PID 1新容器尽量用tini或s6作为init进程显存使用量持续缓慢增长代码显存泄漏用pytorch的显存快照或torch.cuda.memory_summary排查5.3 防患于未然显存使用的好习惯最后说几个我认为值得养成的习惯。第一训练脚本的入口加上信号处理逻辑让SIGTERM也能触发模型保存和进程优雅退出第二使用torch.cuda.empty_cache()的场景要理解清楚它只是释放CUDA缓存不是真正的进程退出的显存回收不要指望它解决进程残留第三给每个训练任务设置环境变量CUDA_VISIBLE_DEVICES这样即使进程残留也至少能隔离到特定显卡不会影响其他任务。我现在每次排查显存问题都会在nvidia-smi之后顺手看一眼ps -ef | grep defunct这个习惯帮我提前避免了好几次PID耗尽的事故。5.4 一条实用的清理技巧按条件批量kill时先print再执行批量kill残留进程的诱惑很大比如经常有人网上找一条命令ps aux | grep python | awk {print $2} | xargs kill -9我强烈不建议直接这么干。这条命令会把你自己当前跑的脚本也杀了如果它也是python还会误伤系统里其他用户的python服务。如果非要批量必须先把进程列出来看清楚确认哪些可以杀再写进命令。我习惯的做法是先把PID输出到文件里检查一遍ps -eo pid,user,cmd | grep -E train.py|inference.py | grep -v grep /tmp/gpu_procs.txt cat /tmp/gpu_procs.txt # 人工确认后 awk {print $1} /tmp/gpu_procs.txt | xargs -r kill -15多花两分钟做安全确认比起误杀后重新拉起服务省太多时间。6. 遇到驱动级显存泄漏最后的手段与工具6.1 显存泄漏的复现与定位思路如果代码有显存泄漏你会看到显存占用在多个epoch后不断上涨最终OOM。这跟进程残留是两码事。定位代码级泄漏优先用PyTorch自带的工具import torch print(torch.cuda.memory_summary())memory_summary会把缓存块、活跃块、保留内存池都列出来。如果发现某个Tensor的size持续不变但显存一路涨优先怀疑loss或者中间变量没有释放。再进阶一点用torch.cuda.set_per_process_memory_fraction设置显存上限让泄漏程序在达到上限时直接报错而不是拖垮整机。6.2 重建GPU环境的兜底方案当你确认所有进程都清了、僵尸也没了驱动层也重刷过显存还是被占着最后的手段就是重置GPU。如果是NVIDIA Tesla卡有些型号支持GPU复位sudo nvidia-smi --gpu-reset -i 显卡编号如果这个命令报错不支持那就只能重启机器了。实际上如果一台GPU服务器出现反复的显存不释放说明驱动和CUDA环境可能已经不稳定这时候备份数据和环境配置重装驱动往往比反复排查更高效。我在生产环境里遇到过一次驱动卡死所有进程都杀了、显存还是显示占用12G最后重启解决。重启之后把驱动从535升到了550这个问题再也没出现过——所以有些时候该升级驱动就升级别老是在应用层打转。6.3 AI框架内的显存复用机制与进程级隔离稍微聊深一层很多AI框架内部是有显存复用机制的。PyTorch的CachingAllocator会缓存释放的显存块而不是立刻还给驱动。这就是为什么你在一个长生命周期进程里反复创建和销毁Tensornvidia-smi里看到的显存使用量只升不降——因为缓存不释放。这个特性本身是为了加速但容易让人误判为泄漏。要验证是不是框架缓存导致的显存虚高可以用torch.cuda.empty_cache()看nvidia-smi显存是否下降。下降说明是缓存不下降说明有Tensor引用没释放。容器场景下如果多个任务共享一张卡建议每个任务限制显存上限避免互相影响export PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128这个参数能把显存碎片化问题压下去一些实测在频繁创建小Tensor的任务里效果明显。关于显存碎片我踩过不少坑后来统一在训练脚本开头设置好PYTORCH_CUDA_ALLOC_CONFOOM的概率降了一大截。整个排查显存问题的过程说穿了就三件事确认活进程、清理僵尸、理解信号机制。ps aux | grep python只是第一层真正的功夫在判断每个进程该不该杀、怎么杀、杀了之后有没有副作用。把第一张速查表打印出来贴在工位上踩坑的时候翻一下比想象中有用。
网站建设高端定制企业官网