1. 问题现象:那个让你抓狂的“资源耗尽”报错
如果你在用VSCode跑机器学习实验,尤其是长时间、多轮次地调试和训练模型,那么下面这个错误你很可能不陌生。某一天,你的代码跑得好好的,突然就给你甩过来一个冷冰冰的提示:
RuntimeError: Resource temporarily unavailable
OMP: Error #34: System unable to allocate necessary resources for OMP thread:
OMP: System error #11: Resource temporarily unavailable
OMP: Hint Try decreasing the value of OMP_NUM_THREADS.
第一次看到这个错误,我跟你一样懵。字面意思是“资源暂时不可用”,提示让你减少 OMP_NUM_THREADS 这个环境变量的值。我当时的反应是:“我机器内存和CPU都够啊,也没开多少线程,怎么就资源不足了?” 然后就开始在网上疯狂搜索,结果发现相关的讨论少得可怜,大多都是泛泛而谈调整环境变量,根本没说在点上。
这个错误的本质,是操作系统告诉你:“兄弟,我没办法再给你创建新的线程(或进程)了。” 在Linux/Unix系统中,每个用户能创建的进程/线程数量是有限制的(可以通过 ulimit -u 查看)。当你触达这个上限时,任何试图创建新进程的操作(比如用PyTorch开多线程数据加载,或者启动一个新的Python子进程)都会失败,并抛出上述错误。
那么问题来了,我明明只是关掉了VSCode窗口,或者只是停止了代码运行,我并没有手动启动一大堆后台任务,这些“资源”是被谁占用的呢?答案就藏在VSCode那个看似方便无比的TensorBoard集成插件里。这个插件让我们能在编辑器里直接可视化训练曲线,体验丝滑,但它却在背后悄悄埋下了一颗“进程地雷”。
2. 根因剖析:VSCode TensorBoard插件的“隐形”进程
VSCode的TensorBoard插件设计初衷是好的——无缝集成,一键启动,无需在终端单独敲命令。你点击“启动TensorBoard”,它就在后台为你运行 tensorboard --logdir=./runs 之类的命令,并把端口映射到VSCode内置的浏览器标签页里。体验确实很棒。
但坑就坑在它的生命周期管理上。当你进行以下操作时,问题就产生了:
- 直接关闭VSCode窗口:你以为关掉编辑器,里面跑的所有东西都会随之结束。对不起,TensorBoard进程可能不会。
- 点击VSCode里TensorBoard面板的“停止”按钮(如果有的话):这个按钮有时并不能彻底终止后台进程。
- 反复运行和停止训练脚本:如果你的脚本里每次训练都调用
tensorboard启动,而之前的又没有正确关闭,那么每次都会残留一个进程。
为什么这些进程关不掉?核心原因在于,这些TensorBoard进程是以独立子进程的形式启动的,它们与VSCode主进程的父子关系可能并不紧密,或者VSCode插件在发送终止信号时不够“强硬”(比如用了SIGTERM而非SIGKILL),导致一些进程变成了“孤儿进程”或“僵尸进程”,继续占用着系统的进程槽位(PID)。
更糟糕的是,这些残留进程不仅是TensorBoard的主进程,它还可能连带启动了一些工作线程。在Linux下,你通过 ps 命令看到的可能是一串相关的进程树。它们不占什么CPU和内存,所以平时你感觉不到,但它们就像“幽灵”一样,默默消耗着你宝贵的进程数上限。当你跑的实验越来越复杂,启动的线程越来越多时,这个上限很快就会被这些“幽灵进程”填满,最终触发那个令人崩溃的 RuntimeError。
3. 实战排查:如何揪出这些“幽灵进程”
光知道原因没用,我们得学会自己动手把它找出来。排查过程就像破案,一步步缩小范围。
3.1 第一步:确认是否是进程数超限
首先,打开你的终端,输入以下命令,看看当前用户允许的最大进程数是多少,以及已经用了多少。
# 查看当前用户的进程数限制
ulimit -u
# 查看当前用户实际运行的进程数量(一个粗略的统计)
ps -u $USER | wc -l
如果第二个数字已经接近第一个数字,那基本就是这个问题了。但 ps -u $USER 会列出很多其他进程,不够精确。我们需要精准定位到元凶。
3.2 第二步:使用ps命令进行地毯式搜索
ps 命令是进程排查的瑞士军刀。我们用它来专门查找TensorBoard相关的进程。
基础排查命令:
# 查看所有包含‘tensorboard’关键词的进程
ps aux | grep tensorboard
这个命令会列出所有进程信息,并通过 grep 过滤出包含“tensorboard”的行。你可能会看到类似这样的输出:
yourname 12345 0.0 0.1 123456 7890 pts/0 Sl 10:00 0:00 /usr/bin/python /usr/local/bin/tensorboard --logdir=./runs --port=6006
yourname 12346 0.0 0.0 12345 678 ? S 10:01 0:00 [tensorboard] <defunct>
第一行是一个正常的TensorBoard进程。第二行状态显示为 <defunct>,这就是所谓的“僵尸进程”,它已经死了,但还没被父进程“收尸”,依然占着PID。
进阶排查命令(查看进程层级):
有时候进程名字可能不完全是“tensorboard”,或者我们想看到进程树结构,可以用:
# 显示进程树,并高亮包含tensorboard的进程
ps -xHf | grep -A 5 -B 5 tensorboard
# 或者用pstree命令更直观(如果系统已安装)
pstree -p $USER | grep -i tensorboard
ps -xH 中的 -H 参数可以显示进程的层级关系(哪个进程是哪个进程的父进程)。当你运行这个命令后,很可能会倒吸一口凉气——屏幕上可能密密麻麻列出了一大堆 tensorboard 或其子进程,数量远超你的想象。这就是资源被耗尽的直接证据。
4. 清理战场:手动终止残留进程
找到目标后,就要果断清理。这里提供几种从温和到暴力的方法。
方法一:精准打击(推荐先尝试)
首先,尝试用 SIGTERM (信号15) 正常终止进程,允许它们进行一些清理工作。
# 使用pkill命令,按进程名终止
pkill -f tensorboard
# 或者,如果知道具体的端口,可以用lsof找到PID再kill
lsof -ti:6006 | xargs kill
方法二:强制清除(当方法一无效时)
如果正常终止不了,或者进程已经处于异常状态,那就需要动用 SIGKILL (信号9) 了。这个信号无法被进程捕获或忽略,操作系统会直接强制移除进程。
# 组合命令:查找、提取PID、强制杀死
ps -ef | grep tensorboard | grep -v grep | awk '{print $2}' | xargs kill -9
我们来拆解一下这个命令管道:
ps -ef:列出所有进程的完整信息。grep tensorboard:过滤出包含“tensorboard”的行。grep -v grep:排除掉grep命令自身产生的进程行。awk '{print $2}':提取第二列,也就是进程PID。xargs kill -9:将前面得到的PID列表作为参数,传递给kill -9命令。
还有一个更直接的变体,使用 ps -xH 并配合 grep:
ps -xH | grep tensorboard | awk '{print $1}' | xargs kill -9
注意,这里 ps -xH 输出的PID列可能是第一列($1),具体取决于你的系统,使用前最好先手动运行 ps -xH | grep tensorboard 确认一下PID在第几列。
执行完这些命令后,再次运行 ps aux | grep tensorboard,应该看不到任何相关进程了。此时再去运行你的训练代码,那个“Resource temporarily unavailable”的错误应该就消失了。
5. 治本之道:在代码中正确关闭TensorBoard
手动清理是补救措施,是“治标”。要“治本”,必须在我们的Python代码里养成好习惯,确保TensorBoard进程随着脚本结束而正确关闭。
核心就一句话:记得调用 tensorboard.close()!
很多教程和示例代码,包括一些早期版本的PyTorch或TensorFlow官方示例,可能只告诉你怎么启动TensorBoard,却没强调关闭。下面我们看一个正确的代码范式。
5.1 使用PyTorch的SummaryWriter
如果你用的是PyTorch,通常结合 torch.utils.tensorboard.SummaryWriter 使用。
import torch
from torch.utils.tensorboard import SummaryWriter
import time
# 创建SummaryWriter实例
writer = SummaryWriter(log_dir='./runs/exp1')
try:
for epoch in range(100):
# ... 你的训练逻辑 ...
loss = torch.randn(1).item()
# 向TensorBoard写入数据
writer.add_scalar('Loss/train', loss, epoch)
time.sleep(0.1)
print(f"Epoch {epoch}, Loss: {loss}")
finally:
# 关键步骤:在finally块中确保writer被关闭
writer.close()
print("TensorBoard writer已关闭。")
把 writer.close() 放在 finally 块里是个好习惯,这样即使训练过程中发生异常,也能保证资源被释放。
5.2 直接使用TensorBoard库
如果你直接使用 tensorboard 库来启动一个独立的TensorBoard服务器,关闭方式略有不同。
from tensorboard import program
import threading
import time
def run_tensorboard(logdir, port=6006):
tb = program.TensorBoard()
tb.configure(argv=[None, '--logdir', logdir, '--port', str(port)])
url = tb.launch()
print(f"TensorBoard started at {url}")
return tb
# 启动TensorBoard
log_dir = "./runs"
tb_instance = run_tensorboard(log_dir)
try:
# 模拟你的主程序运行
for i in range(10):
print(f"Main program running... step {i}")
time.sleep(1)
finally:
# 关键步骤:关闭TensorBoard程序
if tb_instance:
# 方法一:尝试优雅关闭(可能需要根据tensorboard版本调整)
# tb_instance.server.close() # 旧版本可能这样用
# 方法二:更通用的做法是记录下PID,然后用信号终止
# 但更简单的做法是:确保它运行在子进程,并在主进程结束时终止。
# 实际上,上面`tb.launch()`通常是非阻塞的,会启动子进程。
# 最可靠的方式是使用atexit注册清理函数,或者用上下文管理器。
print("主程序结束,请手动检查并确保TensorBoard进程已终止。")
# 在实际项目中,你可能需要保存tb_instance启动的子进程PID,然后在这里os.kill(pid, signal.SIGTERM)
直接使用 tensorboard 库时,生命周期管理更复杂,因为它通常以守护进程形式运行。一个更现代、更推荐的做法是不在训练脚本内启动TensorBoard服务器,而是:
- 在单独的终端手动运行
tensorboard --logdir=./runs --port=6006。 - 你的训练脚本只负责通过
SummaryWriter写日志文件。 - 训练结束后,直接
Ctrl+C关闭那个独立终端里的TensorBoard进程。
这样职责分离,进程管理最清晰,也避免了本文讨论的所有问题。
6. 自动化防御:编写进程清理脚本与监控告警
对于需要长期运行实验的开发者,尤其是使用远程服务器的情况,手动排查终究麻烦。我们可以建立一些自动化机制来防御和预警。
6.1 创建一键清理脚本
在你的用户目录(如 ~/bin/)下创建一个脚本文件,比如叫 clean_tb.sh,并赋予执行权限。
#!/bin/bash
# clean_tb.sh - 一键清理残留TensorBoard进程
echo "正在查找TensorBoard相关进程..."
PIDS=$(ps -ef | grep tensorboard | grep -v grep | awk '{print $2}')
if [ -z "$PIDS" ]; then
echo "未找到TensorBoard进程。"
else
echo "找到以下PID: $PIDS"
echo "正在终止进程..."
echo $PIDS | xargs kill -9
echo "进程已终止。"
fi
# 顺便也检查一下常见的端口占用(例如6006)
echo "检查端口6006占用情况..."
PORT_PID=$(lsof -ti:6006)
if [ -z "$PORT_PID" ]; then
echo "端口6006未被占用。"
else
echo "端口6006被PID $PORT_PID占用,正在清理..."
echo $PORT_PID | xargs kill -9
fi
以后遇到问题,只需要在终端运行 ~/bin/clean_tb.sh 即可。
6.2 将清理步骤加入实验脚本
你可以在每次训练脚本开始前,先自动清理可能存在的残留进程。但这有一定风险,可能会误杀他人或其他任务启动的TensorBoard。更安全的方法是在你自己的实验目录下,使用特定的、唯一的端口号。
# 在你的train.py开头可以加入
import os
import signal
import subprocess
def kill_process_on_port(port):
try:
# 使用lsof查找占用指定端口的PID
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True)
if result.stdout:
pids = result.stdout.strip().split('\n')
for pid in pids:
if pid:
os.kill(int(pid), signal.SIGKILL)
print(f"已终止占用端口{port}的进程 {pid}")
except Exception as e:
print(f"清理端口{port}时发生错误: {e}")
# 在启动你的TensorBoard或训练前,先清理你计划使用的端口
my_tb_port = 6007 # 使用一个不那么通用的端口
kill_process_on_port(my_tb_port)
6.3 设置资源监控告警
对于服务器管理员或个人,可以配置简单的监控。例如,使用 crontab 定期检查用户进程数,接近上限时发送警告。
# 一个简单的检查脚本 check_proc_limit.sh
#!/bin/bash
USER=$(whoami)
MAX_PROC=$(ulimit -u)
CURR_PROC=$(ps -u $USER | wc -l)
USAGE_PERCENT=$(( CURR_PROC * 100 / MAX_PROC ))
if [ $USAGE_PERCENT -gt 80 ]; then
echo "警告:用户 $USER 的进程数已达 $CURR_PROC/$MAX_PROC ($USAGE_PERCENT%)" | mail -s "进程数告警" your-email@example.com
# 或者可以打印到日志文件
echo "$(date): 进程数告警 - $CURR_PROC/$MAX_PROC" >> /tmp/proc_alert.log
fi
然后将这个脚本加入cron任务,每小时运行一次:0 * * * * /path/to/check_proc_limit.sh。
7. VSCode插件使用的最佳实践与替代方案
最后,我们来聊聊如何从根本上避免这个问题,以及一些备选方案。
VSCode TensorBoard插件使用建议:
- 显式停止,而非直接关闭窗口:在VSCode中,找到TensorBoard视图(通常在侧边栏或底部面板),寻找明确的“Stop TensorBoard”或关闭按钮,点击它。
- 一个会话,一个TensorBoard:尽量避免在同一个VSCode工作区中反复启动多个TensorBoard实例。如果需要对比不同实验,使用不同的
--logdir路径或--port端口。 - 养成代码习惯:无论是否使用插件,只要你在代码中实例化了
SummaryWriter或启动了TensorBoard服务,就一定在脚本末尾或异常处理中调用对应的close()方法。 - 定期检查:在长时间、多轮实验后,习惯性地在终端里运行一下
ps aux | grep tensorboard,防患于未然。
替代方案:完全脱离VSCode插件
如果你觉得插件带来的麻烦大于便利,完全可以回归传统方式:
- 终端独立运行:在项目根目录打开一个独立的终端,运行
tensorboard --logdir=./runs --host=0.0.0.0 --port=6006。 - 使用浏览器访问:打开浏览器,输入
http://localhost:6006(或你的服务器IP:6006)。 - 训练脚本独立:你的Python训练脚本只负责写日志文件到
./runs目录。 - 优点:进程生命周期完全由你手动控制,清晰无残留。关闭时,只需在运行TensorBoard的终端按
Ctrl+C即可。 - 缺点:需要多开一个终端窗口,失去了在VSCode内集成的便捷性。
我个人的经验是,在本地开发调试时,可以使用VSCode插件,但务必注意关闭。在远程服务器上进行长期训练时,强烈建议使用独立的终端运行TensorBoard,并通过端口转发在本地浏览器查看,这样环境最干净,问题最少。



被折叠的 条评论
为什么被折叠?



