☰
深度学习跑实验必备Linux命令:GPU监控、后台运行与可复现
2026/10/17 5:02:03 网站建设 项目流程

搞深度学习这些年,我发现一个挺有意思的现象:很多人栽在模型结构里,也有很多人栽在命令行上。代码写得头头是道,一到真正跑实验的时候,就开始反复敲错命令。有的是不知道怎么看显卡占用,有的是训到一半终端断开了不知道该不该直接重跑,还有的是跑完一组参数想复现,结果死活想不起来当时用的什么配置。我自己也经历过一段手忙脚乱的时期,后来每次踩坑就把命令记下来,慢慢整理成一份跑实验指令合集。这篇不想写成一本Linux手册,只聊那些我在日常训练模型、跑对比实验、排查训练故障时真实用过的命令,以及每条命令背后为什么要这么写。内容默认环境是Linux服务器加Python训练脚本,前提是你能用conda或者pip安装依赖。适合刚入门的同学做一份自己的速查表,也适合经常在多个实验之间切换的老手检查和补充细节。

1. 先建整体认知:跑实验的指令究竟在解决什么问题

1.1 我把指令分成了三个层次

跑实验不是只有一条python train.py。如果只会前台跑这样一个命令,遇到稍微复杂一点的场景就会卡住。我习惯把跑实验涉及的命令分成三个层次:第一层是"能跑起来",包括进入虚拟环境、确认显卡可用、启动训练脚本;第二层是"可控可观测",包括输出日志、后台运行、监控GPU、清理残留进程;第三层是"可复现可批量",包括参数注入、随机种子固定、循环跑不同组合、记录环境版本。

这三个层次其实对应了做实验的三个阶段。刚上手时往往只需要第一层,能出个loss曲线就很开心。但实验量一上来,同样的脚本要跑十几个超参组合,还在前台保持终端开着就不现实了。再往后,当你需要回头解释"当时这个结果怎么来的",第三层的那几条命令反而会救你一命。很多教程把命令一条条列出来,我觉得不如先理解层次,遇到新问题时至少知道该去哪个层找解决办法。

1.2 为什么值得专门整理一份指令集

把命令整理成一份清单,表面上是省去每次重新搜索的时间,本质上是在降低实验的不可控因素。举个例子:同样的python train.py --lr 1e-3,你在自己的电脑上跑得好好的,换到另一台服务器上,如果Python版本变了、CUDA版本变了,你以为还是同样的实验,其实已经不一样了。整理指令集的时候,你会被迫去确认这些细节。

还有一个容易被忽略的价值:整理过程等于把整个工作流固化下来了。今天你可能记得nohup后面要加&,一个月后就未必记得。如果不记下来,每次都要重新读文档、重新试错。我把这套指令写在项目的README里,或者直接写在Makefile里,换台机器照着手敲一遍,十分钟就能把工作环境恢复到八九不离十。这比我脑子里的记忆可靠得多。

1.3 这份合集的适用场景和边界

需要先说明边界:我这边主要聊的是单机多卡、或者几台机器通过常规SSH和tmux管理的场景,不涉及大规模集群调度系统。如果你用的是专门的作业提交工具,很多场景会由调度器统一管理,指令思路仍然可以参考,但具体写法要换成对应的提交脚本。容器化和云环境也不在本文主线上,不过环境创建、日志跟踪、GPU监控这些思路迁移过去依然成立。

我觉得边界想清楚反而更有用。一个人在最常见的Linux服务器上跑深度学习实验,需要的指令其实就是这个合集覆盖的范围。后面每一节都是围绕这条主线展开,从环境搭建一直讲到最后故障排查。

2. 环境与数据:用几条命令把地基铺稳

2.1 固定Python和核心库版本,别让环境漂移

开始一个新实验之前,我通常先建一个独立的conda环境,而不是在base环境里直接装包。原因是不同实验之间经常出现依赖冲突,今天为了跑A升级了某个库,明天B实验可能就莫名其妙的报错。创建环境我一般这样敲:

conda create -n exp_env python=3.9 -y conda activate exp_env pip install torch==1.13.1 torchvision==0.14.1

这里的版本号是举例,实际按自己项目需求填。关键不是装了什么,而是要把版本钉死。PyTorch这种框架,大版本之间API会有差异,小版本之间也可能有算子行为的变化,同一个模型在不同版本下跑出不一样的结果是很正常的。装完之后先用一条命令确认环境是否正常,再开始写训练脚本:

python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__, torch.cuda.is_available())"

如果打印出False,别急着怀疑显卡坏了,先查驱动和PyTorch的CUDA版本是否匹配。这个检查动作我已经养成习惯了,几乎每次换环境都会做一次,能省掉后面大量莫名其妙的诡异报错。

固定环境版本之后,还需要把依赖固化下来。我见过很多人用pip freeze > requirements.txt把几百个包全导出来,看起来稳妥,实际上把很多无关的传递依赖也锁进去了,换机器安装时反而容易出问题。更推荐的做法是只锁定你直接依赖的顶层包版本,比如torch、torchvision、numpy这些,然后在README里写明Python版本和CUDA版本。这样别人拿到项目,用指定版本建环境,比生搬硬套一个巨大的requirements要干净得多。

2.2 软链接、磁盘权限与数据读取的检查命令

数据准备阶段我最不喜欢的操作是复制几百GB数据集。正确的做法是建软链接,指向数据所在的真实路径:

ln -s /path/to/dataset /home/user/project/data

软链接的好处是节省磁盘空间,同时让代码里的路径保持稳定。即使真实数据被换到别的盘,只需要改链接地址,训练脚本不用动。需要注意的是,如果项目是别人共享的,建链接前先确认目标路径有没有读取权限。分布式训练时,多台机器可能挂载的是不同的数据路径,用软链接统一之后,代码里就少了一堆判断机器名的脏逻辑。

跑大数据实验之前,我习惯先敲两个命令看看磁盘余量:

df -h du -sh /path/to/dataset

原因很现实:训练到一半,checkpoint写不进去,报错No space left on device,这种事故我见过不止一次。磁盘满的时候,损失往往是一整夜的训练进度。提前看一眼,把输出目录放到空间充足的挂载盘上,比事后追悔有用得多。

数据读取性能在命令行也能看出端倪。训练时如果GPU利用率很低,但CPU占用率很高,往往是数据加载跟不上。可以先看htop或者top -u $USER确认CPU忙不忙,再决定调整num_workers还是优化数据读取逻辑。这些指令和训练本身没有直接关系,但排查起来特别顺手。

3. 训练启动、后台托管与日志:把训练从"前台裸奔"变成可控任务

3.1 第一次启动训练脚本,我坚持前台加 -u

写好训练脚本后,第一次跑我从来不会直接丢到后台。前台运行,终端里会实时打印日志,出问题直接看到traceback,Ctrl+C也能干净利落地停下。这个阶段我用的命令很简单:

python -u train.py --config configs/exp1.yaml

这里值得多说一句-u参数。Python默认对标准输出是有缓冲的,尤其是当你把输出重定向到文件时,print的内容不会立刻写入文件,会攒到一定量才flush一次。如果训练中途崩溃,你很可能发现日志文件里缺失了最后一段输出,原因就是缓冲区还没来得及写出去。加上-u让Python进入无缓冲模式,每条print实时落盘。调试的时候,这往往是能不能看到关键错误的第一道防线。

前台跑通之后,确认模型能正常出loss,再考虑后台化。第一次就跑后台,出问题还得kill掉、改代码、重新启动,来回折腾的时间远多于前台那几分钟。所以我一直坚持一个原则:新脚本第一次必须前台跑,稳定之后再后台。

3.2 nohup、tmux怎么选:核心是"要不要交互"

把训练放到后台,最经典的是nohup加日志重定向:

nohup python -u train.py --config configs/exp1.yaml > logs/exp1.log 2>&1 &

拆开看:nohup让进程忽略挂断信号,终端关掉也不影响训练;> logs/exp1.log把标准输出重定向到文件;2>&1把标准错误也送进同一个文件;最后的&让命令在后台运行。忘了写2>&1是新手最容易犯的问题,报错信息去了哪儿根本不知道,排查时看着一个空日志发呆。注意如果日志文件已存在,>会直接覆盖,想保留之前的记录用>>追加。

但nohup方案有一个痛点:启动完就脱离了交互。你想进去看看运行状态,除非看日志,否则进不了Python进程。这时我会选tmux:

tmux new -s exp1 tmux attach -t exp1

在tmux窗口里前台运行训练脚本,即使关闭SSH连接,训练也不会断;重新连上服务器后,执行tmux attach -t exp1就能回到那个终端,看到完整的运行上下文。需要临时停一下、进pdb、改几个参数再重跑,都比nohup方便。

我给自己的选择规则很简单:如果只是单次训练,跑完看结果,用nohup;如果需要在训练过程中时不时干预,或者同时开多个实验窗口,用tmux。有人在两者之间纠结半天,其实都行,关键是日志和进程的对应关系要清晰,别让五个后台任务输出全混在一起。

3.3 动态跟踪日志的三板斧:tail、grep、watch

训练跑起来之后,最常用的指令就是看日志。tail -f是实时滚动输出,适合盯loss变化:

tail -f logs/exp1.log

如果日志已经很长了,只看最后五十行:

tail -n 50 logs/exp1.log

有时候我们只关心某个阶段的信息,比如每个epoch结束时的val_loss,直接用grep过滤:

grep -n "val_loss" logs/exp1.log | tail -n 20

-n显示行号,方便定位;管道符把grep的输出再交给tail,只保留最近20条。想看指标随着时间的变化,还能用watch循环刷新一个命令:

watch -n 5 'tail -n 30 logs/exp1.log'

每隔5秒刷新一次,屏幕上就是动态更新的训练状态。类似地,GPU监控我后面会讲,也可以用watch组合。习惯了这套组合,跑训练时就不用一直盯着终端,偶尔瞟一眼就能确认任务是否健康。

4. GPU占用、监控与清理:别让显存成为薛定谔的资源

4.1 nvidia-smi的几个实用形态

服务器上跑GPU任务,最基础的一定是nvidia-smi。但我发现很多人只会裸敲这一个命令,然后对着刷屏的输出找自己那个进程,效率很低。如果只想持续刷新,可以:

nvidia-smi -l 2

等价于每2秒刷新一次,有点像全屏监控界面。不过我更推荐用查询模式,只提取关心的字段,输出更干净:

nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.total --format=csv -l 5

这条命令会以CSV格式循环打印每张卡的利用率、显存占用和总量,一眼就能看出哪张卡有空闲。需要记录数据时,把输出重定向到文件:

nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.total --format=csv -f logs/gpu.log

训练结束后用tail打开这个文件,结合时间点能看出显存在哪个阶段开始异常升高。这种细致到字段的监控,在排查显存泄漏时比满屏的默认输出有用得多。还可以加入temperature.gpu和power.draw,如果发现利用率不高但温度很高,说明散热或功耗限制可能成了瓶颈。

4.2 找进程、确认身份、再决定怎么kill

nvidia-smi默认输出的末尾会列出每个GPU上的进程,但只有PID、类型、显存用量,看不到命令行。很多时候我们拿到一个PID,想知道它到底是谁,需要配合ps:

ps -ef | grep 12345

这样能看到对应的进程全名、启动用户、启动命令。确认是残留的训练进程后,再考虑清理。我的习惯是先普通kill,等几秒看它是否退出,不行再补-9:

kill 12345 kill -9 12345

直接kill -9的问题在于进程没有机会清理缓存、释放锁、保存状态,容易留下半截结果,下次启动时反而出现奇怪的冲突。对于多进程DataLoader引起的残留,父进程杀了子进程还可能活着,我通常先查一下全部同名进程:

pgrep -f train.py ps -ef | grep train.py | grep -v grep

确认所有相关进程都在列表里,再一次性清理。清理时要小心pkill -f这种命令,匹配规则太宽容易误杀其他合法任务,我原则上只在目标明确时才用。

另外有一种更隐蔽的情况是僵尸进程。用ps aux | grep defunct能看到状态为Z的进程,这种进程用kill -9也干不掉,因为进程本身已经结束了,只是等待父进程回收。如果父进程是你起的训练主进程,就杀掉父进程;如果父进程变成了1号系统进程,往往只能等节点重启或者找管理员处理。训练场景里出现僵尸进程,多和数据加载器崩溃、父进程没有正确回收子进程有关,尽量避免使用timeout、pkill这类粗暴方式结束训练。

5. 参数遍历、配置注入与随机种子:把几百组实验变成生产线

5.1 用Shell循环批量跑不同超参

当实验从一个配置变成十几个配置时,手工逐条启动是不可能的。我会在bash里直接写一个循环,把参数组合全部跑完:

for lr in 1e-4 3e-4 1e-3; do for seed in 42 43; do python -u train.py --lr $lr --seed $seed \ --out_dir "results/lr${lr}_seed${seed}" \ > "logs/lr${lr}_seed${seed}.log" 2>&1 done done

这个循环最关键的一点:每个组合的输出目录和日志文件都用参数本身来命名,而不是统一叫result或log。否则上一组结果会被下一组覆盖,跑完发现自己只剩最后一组的结果,那种想砸电脑的心情我经历过。${lr}和$lr作用一样,但在字符串拼接时,用大括号能让变量边界更清晰,避免把lr1e-4之类的字符误当成变量名的一部分。

如果想多个组合并行跑,可以在每个python命令后加&,最后跟一个wait:

for ...; do python -u train.py ... & done wait

并行能明显节省时间,但前提是显卡显存和CPU核数都允许。我的建议是:先用串行方式把单组实验跑通,确认没有隐性问题,再决定要不要并行。并行任务之间如果互相抢显存,速度反而会一起降下来。

5.2 配置文件与参数注入:命令行别变成裹脚布

超参数全写在命令行里不是长久之计。一个实验动辄十几个参数,命令行长不说,还容易敲错。我习惯让train.py支持读YAML配置文件,命令行只负责覆盖少数需要变化的参数:

python train.py --config configs/base.yaml --lr 1e-3 --out_dir results/exp1

这样改一个超参不需要动脚本,只要在命令行里临时覆盖就行。--help也是一个被低估的命令:

python train.py --help

它能列出所有可传参数,新接手项目的小伙伴再也不用靠猜。每次启动实验之前,我会把当前使用的配置复制到结果目录,作为那个实验的存档:

cp configs/base.yaml results/exp1/config_used.yaml

同时用Shell写一个简单的meta信息,记录启动时间和Git版本:

echo "$(date) $(git rev-parse HEAD)" > results/exp1/meta.txt

等到一个月后回看实验结果,你能清楚知道用的是哪份配置、哪次提交的代码,而不需要在记忆里打捞。这个习惯的成本近乎为零,收益却极大。

5.3 随机种子、可复现性与依赖快照

深度学习实验里,随机种子是复现的底线。启动命令里加上--seed 42已经是基本功,更完整一点,还要保证Python、NumPy、PyTorch三个层面的随机源都被固定。如果追求更严格的确定性,可以设置环境变量:

CUBLAS_WORKSPACE_CONFIG=:4096:8 python train.py --seed 42

这个变量会影响cuBLAS内部的工作区分配方式,能在一定程度上让卷积、矩阵乘法等操作的结果不再随机抖动,但会牺牲部分性能。只在做对照实验、需要严格可复现时才建议开启,平时跑大规模实验没必要为了这个把训练拖慢。

另外,命令要记,但环境也要存档。启动实验时顺手记录代码提交状态:

git rev-parse HEAD git diff --stat pip freeze | grep -E "torch|numpy|scipy"

把这几条输出一起放进实验结果目录,是判断"实验能不能复现"的核心证据。我之前遇到过一种情况:模型结果跑出来后隔两周再复现,loss对不上了,最后发现是PyTorch小版本被人升级了。从那以后,我开始把依赖版本和Git提交哈希作为每个实验的必填元数据,而不是等到出了问题再追溯。

6. 翻车现场诊断:崩溃恢复、显存泄漏和诡异报错先敲哪些命令

6.1 别急着重跑,先看退出码和日志尾巴

训练崩了,最忌讳的是立刻改代码重跑,因为前面的错误往往还没看明白。我通常按顺序做三件事:第一步,看上一次命令的退出码:

echo $?

0表示正常,非0表示异常。第二步,打开日志最后面:

tail -n 100 logs/exp1.log

第三步,从日志里筛出关键错误行:

grep -n -E "Error|Traceback|RuntimeError|CUDA" logs/exp1.log | tail -n 50

grep加正则一次过滤多个关键字,配合-n显示行号,能快速定位错误发生在哪个epoch、哪个模块。有些进程被系统杀掉的情况,Python日志里可能什么线索都没有,这时候可以看系统日志:

dmesg | tail -n 30

如果进程因为内存不足被OOM Killer杀掉,通常能在dmesg里看到对应记录。这个命令在部分云服务器上需要权限,没有权限就先看日志和应用层的错误,不要死磕系统层。

6.2 显存逐秒爬升的定位思路

显存泄漏的一大特征是nvidia-smi里显存占用随着训练的进行逐步上升,但显存总量并没有立刻爆掉。为了捕捉这个爬升曲线,我会提前把监控命令的输出写到文件:

nvidia-smi --query-gpu=memory.used --format=csv -l 1 > logs/gpu_mem.log

运行一段时间后,打开这个文件看数字变化趋势。如果每步都在涨,基本可以判断是显存泄漏,而不是单次偶发问题。Python侧也可以加一条打印指令,在自己的训练循环里输出峰值显存:

print(torch.cuda.max_memory_allocated() / 1024**3, "GB")

把峰值和当前显存对比,能帮助判断是模型本身占显存大,还是历史张量没有释放。诊断时还可以设置CUDA_LAUNCH_BLOCKING=1:

CUDA_LAUNCH_BLOCKING=1 python -u train.py --config configs/exp1.yaml

这个环境变量会让GPU计算同步执行,这样哪一行代码触发错误时,Python traceback能直接指到具体位置。代价是速度明显变慢,所以只在定位问题时临时用,找到问题就关掉。

6.3 保留现场、断点续训和崩溃信息采集

每次训练崩溃,我先做的是把现场原样保存下来,而不是急着清空目录。用带时间戳的文件名把崩溃日志和当前结果复制出来:

cp logs/exp1.log logs/exp1_error_$(date +%Y%m%d_%H%M%S).log

这样事后回溯时,你能清楚区分什么时候是崩溃现场,什么时候是修复后的输出。如果训练脚本实现了断点保存,通常可以用--resume从最近一次checkpoint继续:

python train.py --config configs/exp1.yaml --resume checkpoints/latest.ckpt

恢复后建议先盯一个epoch,确认loss没有突变,再放心离开。

有些崩溃是Python层面没有报错、直接进程消失,比如段错误。遇到这种情况,我会加上Python的faulthandler环境变量再跑一次:

PYTHONFAULTHANDLER=1 python -u train.py --config configs/exp1.yaml

它能在进程崩溃时打印出更底层的调用栈,把问题范围缩小到某个扩展库或CUDA调用附近。需要深入调试单步执行时,使用python -m pdb打开调试器,逐步查看变量状态。这些命令不一定每次都能解决问题,但能帮你把"神秘崩溃"变成"可定位的崩溃"。

我在实际使用中还养成一个习惯:把最常用的几条命令放进Makefile,比如make run、make log、make gpu,不同实验只需要替换对应的配置文件名。这样既减少记忆负担,也确保启动方式在每个实验里保持一致。这份指令集不是要背下来,而是要在一次次跑实验的过程中,慢慢变成你自己的肌肉记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询