Shell 与 Linux 训练运维:让 GPU 任务跑过整个周末——研究者的生存技能

· 约 4 分钟 ·
本文目录(8 节)

长时间训练不能依赖一个始终在线的 SSH 窗口。远程连接可能中断,磁盘可能写满,进程也可能停止而没有明显提示。下面从 tmux、日志和资源观察讲起,再补上退出状态、检查点和恢复步骤,给训练任务留下一条可追查的运行记录。

前置阅读:建议先读 Linux 与 Python 环境基础、开发环境搭建。

会话保活:tmux 是第一优先级

SSH 断连会向你终端里所有进程发送 SIGHUP,进程默认收到就退出。解决方案是把训练跑在独立于 SSH 会话的容器里。tmux 是标准答案:

# 新建一个名为 train 的会话
tmux new -s train
# 在会话里启动训练
python train.py --epochs 100
# 按下 Ctrl+B 再按 D:脱离会话(detached),训练继续跑
# 重新连上服务器后:
tmux attach -t train    # 回到会话,训练输出原封不动
tmux ls                 # 列出所有会话
tmux kill-session -t train   # 结束会话

tmux 之外还有两层保险,按场景叠加:

# 方案二:nohup + 输出重定向(不需要交互时用)
nohup python train.py --epochs 100 > logs/train_$(date +%m%d_%H%M).log 2>&1 &

# 方案三:setsid 彻底脱离会话组
setsid python train.py > logs/train.log 2>&1 < /dev/null &

2>&1 把标准错误合并到标准输出一起落盘,末尾的 & 放后台。我自己的习惯是:交互式调试用 tmux,批量跑实验用 nohup + 时间戳日志名。

日志:出了问题唯一的现场

训练崩了而没有日志,等于事故没有黑匣子。我的日志纪律有三条:

第一,日志带时间戳,绝不覆盖。 用 $(date +%m%d_%H%M) 生成文件名,每次实验一份。磁盘很便宜,重跑很贵。

第二,Python 端用 logging 而不是 print。 print 有缓冲,崩溃时最后几行可能丢在缓冲区里。如果一定要用 print,加 python -u 强制无缓冲输出。

第三,定期清理。 训练日志加上 TensorBoard 的 events 文件,一个月能吃掉几十 GB。我写了一个每周跑的清理脚本:

#!/bin/bash
# cleanup.sh:删除 30 天前的日志,保留最近 3 个 checkpoint
find ~/logs -name "*.log" -mtime +30 -delete
# checkpoint 按时间排序,只留最新 3 个
ls -t ~/checkpoints/*.pt | tail -n +4 | xargs -r rm -f
echo "$(date): cleanup done" >> ~/logs/cleanup.log

GPU 与进程监控

nvidia-smi 只看一眼不够,要持续观察趋势:

# 每 2 秒刷新一次
watch -n 2 nvidia-smi
# 或者只盯利用率和显存两列,输出更干净
nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.total \
           --format=csv -l 5
# 查清楚是谁占了显卡(多人共享服务器必备)
nvidia-smi --query-compute-apps=pid,used_memory --format=csv
ps -fp <PID>   # 根据 PID 查进程主人和启动命令

CPU 和内存侧用 htop(比 top 直观十倍),磁盘用 df -h 看分区、du -sh */ 找哪个目录在膨胀。有一次训练越来越慢,最后发现是 /tmp 被 dataloader 的缓存塞满导致交换分区狂转——df -h 十秒就定位了。

一个生产级训练启动脚本模板

把上面的纪律固化成一个模板,新项目直接复制:

#!/bin/bash
set -euo pipefail   # 任何命令失败立即退出;未定义变量报错;管道错误不吞

EXP_NAME=${1:-baseline}
LOG_DIR=logs/$(date +%Y%m%d)
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/${EXP_NAME}_$(date +%H%M).log"

echo "[$(date)] 启动实验: $EXP_NAME" | tee -a "$LOG_FILE"

python -u train.py \
    --exp_name "$EXP_NAME" \
    --epochs 100 \
    2>&1 | tee -a "$LOG_FILE"

echo "[$(date)] 实验结束: $EXP_NAME" | tee -a "$LOG_FILE"

set -euo pipefail 是 Shell 脚本最重要的第一行:没有它,数据预处理失败后训练脚本会拿着空数据继续跑,几小时后才发现输出全是 NaN。tee 让日志同时到屏幕和文件,调试体验和留档两不误。

定时任务与数据传输

cron 定时任务:crontab -e 添加一行,比如每天凌晨 4 点跑清理:

0 4 * * * /home/zack/cleanup.sh >> /home/zack/logs/cron.log 2>&1

注意 cron 的环境变量和你登录的 shell 完全不同,脚本里所有路径必须写绝对路径,conda 环境也要在脚本里显式 activate,这是新手踩坑率最高的地方。

rsync 传数据:scp 传大文件夹中断后要从头再来,rsync 支持断点续传和增量同步:

# 把本地数据集推到服务器,-a 保持属性,-v 显示过程,-z 压缩,-P 断点续传+进度
rsync -avzP ./dataset/ zack@server:~/datasets/mydataset/
# 反向把训练结果拉回本地
rsync -avzP zack@server:~/checkpoints/ ./checkpoints/

踩坑与排查

症状可能原因排查方法
SSH 断了训练就停进程挂在 SSH 会话下改用 tmux 或 nohup,检查 ps -ef 的 TTY 列
日志文件是空的print 缓冲未刷新python -u 或 logging;tail -f 实时观察
训练半夜被杀OOM 或管理员清理dmesg | grep -i oom 查 OOM killer 记录
cron 脚本不执行环境变量缺失/相对路径全部改绝对路径,先手动 bash script.sh 验证
磁盘突然满了checkpoint 不删/日志膨胀du -sh */ 定位,部署自动清理脚本
显存占用 100% 但利用率 0%进程僵死没释放显存nvidia-smi 找到 PID 后 kill -9

动手练习

  1. 写一个 gpu_guard.sh:每分钟检查 GPU 利用率,连续 10 分钟为 0 就把训练 PID 和现场信息追加到报警日志。
  2. 用 tmux 跑一个故意写死循环的脚本,detach、退出 SSH、重连 attach,验证输出无损。
  3. 给你的训练脚本加上 set -euo pipefail 和时间戳日志,人为制造一个预处理错误,观察脚本是否立即退出且日志完整。

面试常问

Q:nohup、&、tmux、setsid 有什么区别? & 只放后台,进程仍属于当前会话;nohup 让进程忽略 SIGHUP,但还在会话里;setsid 让进程成为新会话的 leader,彻底脱离终端;tmux 则是造了一个持久的伪终端,支持随时 attach 交互,功能最全。生产训练首选 tmux,批量无人值守任务用 nohup 或 setsid。

Q:怎么查一个训练任务为什么变慢? 自顶向下四层:nvidia-smi 看 GPU 利用率(低则瓶颈在数据加载),htop 看 CPU 和内存(dataloader worker 是否够),iostat 或 df -h 看磁盘 IO 和空间,最后看日志里每个 step 的时间分布。绝大多数「变慢」是数据管线或磁盘问题,不是模型问题。

运维不是 AI 研究的主角,但它决定了你的实验是「跑完」还是「跑了个寂寞」。把这些脚本沉淀下来,是每个研究者从手工时代进入工业化时代的标志。

相关阅读:Linux 与 Python 环境基础、Git 版本控制、Docker 与 CI/CD。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…