1. 云端训练任务为什么会“跑不完”1.1 从一次真实的翻车说起去年帮一个朋友调一个图像分割的模型数据集不大大概两万多张图backbone 用的是 SegFormer 的轻量版本单卡 RTX 4090 跑 80 个 epoch 大概需要 14 个小时。当时想得很简单租了一台按量计费的 GPU 服务器晚上十点挂上去第二天早上起来收结果。结果第二天打开终端一看SSH 会话早就断了进程也没了日志停在 epoch 7。那一瞬间的心情相信跑过云端训练的人都懂。后来复盘问题其实一点都不复杂SSH 连接本身不是为长时间任务设计的。网络抖动、本地电脑休眠、公司网络策略调整、甚至笔记本合盖任何一个环节出问题会话一断挂在会话里的前台进程就会收到 SIGHUP 信号然后被系统回收。这不是 PyTorch 的问题也不是 GPU 的问题而是进程生命周期管理的问题。这篇文章想聊的就是这件事怎么让一个 PyTorch 训练任务在云端 GPU 上真正“跑完”。核心手段有两个一个是断点续训Checkpoint Resume保证任务即使中断也能从最近的存档继续另一个是后台保活tmux/screen/nohup保证会话断开时进程不被杀。两者配合起来才算是一套完整的方案。适合所有需要在远程 GPU 上跑长任务的人不管你是租的云服务器、实验室的共享机器还是公司内部的训练集群。1.2 断点续训和后台保活到底解决什么问题先把这两个概念掰开说清楚因为很多人会把它们混在一起。后台保活解决的是“进程别被杀”的问题。它不关心训练本身跑到哪了只关心进程能不能在 SSH 断开后继续活着。常用的工具有tmux、screen、nohup其中tmux是最推荐的因为它支持会话分离和重新附着还能开多个窗口管理多个任务。断点续训解决的是“中断了也能接着跑”的问题。它要求训练脚本在运行过程中定期把模型权重、优化器状态、学习率调度器状态、当前 epoch/step 等信息保存到磁盘。任务重启时脚本先读这些文件把状态恢复回来然后从断点继续。这两者的关系是后台保活是第一道防线能挡住 90% 的意外中断断点续训是第二道防线挡住剩下的 10%比如机器重启、进程 OOM 被杀、GPU 驱动崩溃、云厂商迁移宿主机等。只做后台保活不做断点续训遇到机器重启就全白跑只做断点续训不做后台保活那你得一直守着 SSH体验极差。两个都做才是工程上靠谱的做法。1.3 一个典型的失败场景拆解我见过太多人踩的坑基本可以归成三类第一类是直接前台跑。python train.py敲下去SSH 一断进程跟着走。这种最常见也最冤。第二类是用了 nohup 但没重定向输出。nohup python train.py 看起来没问题但 stdout 默认写到nohup.out如果脚本里有大量 tqdm 进度条输出这个文件会迅速膨胀到几个 G把磁盘写满然后训练崩掉。而且没有日志轮转排查问题的时候翻都翻不动。第三类是保存了 checkpoint 但没保存优化器状态。只存model.state_dict()恢复的时候优化器动量、Adam 的一二阶矩全丢了学习率调度器也从头开始。这种“假续训”会让模型在恢复后出现明显的 loss 抖动甚至比从头训练还慢。下面我会把这三类问题对应的正确做法一步步拆开讲。2. 后台保活tmux 的正确打开方式2.1 为什么首选 tmux 而不是 nohupnohup是最轻量的方案一行命令就能让进程忽略 SIGHUP。但它有几个硬伤不能重新附着到进程的终端、不能实时看输出、不能交互式调试、多任务管理很麻烦。对于训练任务来说你总得时不时看一眼 loss 曲线、看一眼 GPU 占用nohup只能靠tail -f看日志文件体验很差。screen比nohup好支持分离和附着但它的会话管理比较原始窗口切换、分屏、复制粘贴都不如tmux顺手而且项目活跃度也不如tmux。tmux的优势在于会话可以随时 detach 和 attachSSH 断了会话还在一个会话里可以开多个 window 和 pane同时跑多个任务或者一边跑训练一边看 GPU支持配置文件可以把常用设置固化下来社区活跃遇到问题好搜。所以我的建议很明确只要不是极端受限的环境一律用 tmux。2.2 tmux 安装与基础会话管理大部分 Linux 发行版直接包管理器装就行# Ubuntu / Debian sudo apt update sudo apt install -y tmux # CentOS / RHEL sudo yum install -y tmux # 验证版本 tmux -V装完之后最核心的几个操作# 新建一个名为 train 的会话 tmux new -s train # 在会话里跑训练下面会讲具体命令 # 分离会话按 Ctrlb 然后按 d # 此时回到普通 shell训练继续在后台跑 # 重新附着到 train 会话 tmux attach -t train # 列出所有会话 tmux ls # 杀掉某个会话 tmux kill-session -t train这里有个细节很多人不知道tmux的默认前缀键是Ctrlb所有快捷键都要先按前缀键再按功能键。比如Ctrlb d是 detachCtrlb c是新建 windowCtrlb n是切到下一个 windowCtrlb %是垂直分屏Ctrlb 是水平分屏。刚开始会不习惯用两天就顺了。提示如果你用的是 macOStmux通过 Homebrew 装brew install tmux。Windows 用户建议直接在 WSL2 里操作原生 Windows 下 tmux 体验不好。2.3 一个生产级的 tmux 配置默认配置用起来有几个不舒服的地方窗口编号从 0 开始、鼠标不能滚动、状态栏信息太少。我一般会在~/.tmux.conf里加这么几行# 窗口编号从 1 开始 set -g base-index 1 setw -g pane-base-index 1 # 开启鼠标支持滚动、选择 pane set -g mouse on # 状态栏显示更丰富的信息 set -g status-left [#S] set -g status-right #{pane_current_path} | %Y-%m-%d %H:%M # 增大回滚缓冲区方便翻日志 set -g history-limit 50000 # 用 Ctrla 作为前缀键可选看个人习惯 # set -g prefix C-a # unbind C-b # bind C-a send-prefix改完配置后在 tmux 里执行tmux source-file ~/.tmux.conf生效或者直接重开会话。history-limit这个参数特别重要默认只有 2000 行训练日志一多就翻不到前面的内容了调到 50000 基本够用。2.4 训练命令怎么写才不会被误杀在 tmux 会话里跑训练命令本身不需要加nohup因为 tmux 已经帮你处理了信号隔离。但输出重定向还是要做的原因有两个一是方便事后排查二是避免终端缓冲区被撑爆。我常用的写法是这样cd /path/to/project python -u train.py \ --config configs/segformer_b0.yaml \ --batch-size 16 \ --epochs 80 \ 21 | tee -a logs/train_$(date %Y%m%d_%H%M%S).log几个关键点解释一下python -u强制 stdout 和 stderr 不缓冲这样日志能实时写进文件不会因为缓冲导致你看不到最新进度。这个参数在排查“训练卡住”类问题时特别有用。21把 stderr 合并到 stdout保证报错信息也进日志文件。tee -a同时输出到终端和文件-a是追加模式。这样你在 tmux 里能实时看到进度日志文件里也有完整记录。日志文件名带上时间戳避免多次运行互相覆盖。如果磁盘空间紧张可以配合logrotate或者写个定时清理脚本。注意千万不要在训练脚本里用print打大量无意义的内容尤其是每个 step 都打一行。日志文件会迅速膨胀IO 也会拖慢训练。建议用logging模块按 epoch 或者每 N 个 step 打一次。3. 断点续训Checkpoint 到底该存什么3.1 一个完整的 checkpoint 包含哪些内容很多人以为 checkpoint 就是模型权重其实远不止。一个能支撑“无缝续训”的 checkpoint至少要包含以下几项内容作用不保存的后果model.state_dict()模型参数无法恢复模型optimizer.state_dict()优化器状态动量、Adam 矩loss 抖动收敛变慢scheduler.state_dict()学习率调度状态学习率重置训练节奏乱epoch/global_step当前进度不知道从哪继续best_metric历史最优指标无法判断是否刷新最优scaler.state_dict()AMP 混合精度缩放因子混合精度训练不稳定rng_state随机数种子状态数据增强、dropout 不可复现前四项是必须的后三项在特定场景下才需要。比如你用了torch.cuda.amp那scaler状态一定要存否则恢复后梯度缩放会从头开始前几个 step 容易出问题。如果你对实验可复现性要求高rng_state也要存包括 Python 的random、NumPy 的np.random、PyTorch 的torch.random三套状态。3.2 保存策略按 epoch 还是按 step这是个很实际的问题。按 epoch 存文件少、管理简单但如果一个 epoch 要跑两小时中断一次最多损失两小时。按 step 存粒度细但文件多、IO 频繁而且如果保存逻辑写得不好会明显拖慢训练。我的经验是这样默认按 epoch 存同时保留一个“最近 N 个”的滚动窗口。比如每个 epoch 结束存一个epoch_{n}.pt同时维护last.pt和best.pt两个固定文件并且只保留最近 3 个 epoch 的文件更早的自动删掉。这样既不会丢太多进度也不会把磁盘撑爆。如果单个 epoch 特别长比如超过 1 小时可以改成按 step 存比如每 2000 个 step 存一次。但要注意保存操作本身是有开销的尤其是模型大的时候一次torch.save可能要好几百毫秒甚至几秒。所以不要在 step 循环里无脑存要加个计数器控制频率。import os import torch def save_checkpoint(state, save_dir, is_bestFalse, max_keep3): os.makedirs(save_dir, exist_okTrue) epoch state[epoch] # 保存当前 epoch 的 checkpoint ckpt_path os.path.join(save_dir, fepoch_{epoch}.pt) torch.save(state, ckpt_path) # 更新 last.pt last_path os.path.join(save_dir, last.pt) torch.save(state, last_path) # 更新 best.pt if is_best: best_path os.path.join(save_dir, best.pt) torch.save(state, best_path) # 清理旧文件只保留最近 max_keep 个 all_ckpts sorted( [f for f in os.listdir(save_dir) if f.startswith(epoch_)], keylambda x: int(x.split(_)[1].split(.)[0]) ) for old in all_ckpts[:-max_keep]: os.remove(os.path.join(save_dir, old))这段代码有几个细节值得说。torch.save默认用的是 pickle 协议保存的是整个 state 字典恢复的时候直接torch.load就行。但要注意如果 state 里包含了不能 pickle 的对象比如某些自定义的 lambda会报错。所以 state 里尽量只放 tensor、数字、字符串这些基础类型。另外torch.save是同步阻塞的保存期间训练会暂停。如果模型特别大比如 7B 以上可以考虑用torch.save的异步版本或者先把 state 拷到 CPU 再存避免占用 GPU 显存。3.3 恢复逻辑怎么保证“无缝”恢复逻辑比保存更容易出错因为要考虑的边界情况多。一个健壮的恢复函数大概长这样def load_checkpoint(model, optimizer, scheduler, scaler, ckpt_path, device): if not os.path.exists(ckpt_path): print(f[Resume] checkpoint not found: {ckpt_path}, training from scratch) return 0, 0.0 print(f[Resume] loading checkpoint from {ckpt_path}) ckpt torch.load(ckpt_path, map_locationdevice) model.load_state_dict(ckpt[model]) optimizer.load_state_dict(ckpt[optimizer]) if scheduler is not None and scheduler in ckpt: scheduler.load_state_dict(ckpt[scheduler]) if scaler is not None and scaler in ckpt: scaler.load_state_dict(ckpt[scaler]) start_epoch ckpt[epoch] 1 best_metric ckpt.get(best_metric, 0.0) print(f[Resume] resume from epoch {start_epoch}, best_metric{best_metric:.4f}) return start_epoch, best_metric这里有几个坑要重点说。第一个坑是map_location。如果你在 GPU 上存的 checkpoint换到另一台机器或者 CPU 上加载不加map_location会报错。加上map_locationdevice之后PyTorch 会自动把 tensor 映射到指定设备。第二个坑是start_epoch的计算。如果你存的时候epoch是从 0 开始的那恢复的时候要1如果从 1 开始就不用加。这个一定要和保存逻辑对齐否则会重复跑一个 epoch 或者跳过一个 epoch。我见过有人因为这个 bug训练了 100 个 epoch 实际只跑了 50 个。第三个坑是优化器状态和模型参数的设备一致性。optimizer.load_state_dict之后优化器里的状态 tensor 会自动跟着模型参数走但前提是你先model.to(device)再 load 优化器。顺序反了会出问题。第四个坑是 scheduler 的恢复时机。有些 scheduler比如ReduceLROnPlateau需要在每个 epoch 结束后根据指标更新恢复的时候如果直接 load 状态可能会跳过第一次更新。这个要结合具体 scheduler 的行为来调。3.4 一个完整的训练循环模板把保存和恢复串起来一个可以直接抄的训练循环大概是这样import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR from torch.cuda.amp import GradScaler, autocast def train(config): device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(config).to(device) optimizer AdamW(model.parameters(), lrconfig.lr, weight_decayconfig.wd) scheduler CosineAnnealingLR(optimizer, T_maxconfig.epochs) scaler GradScaler() start_epoch, best_metric load_checkpoint( model, optimizer, scheduler, scaler, ckpt_pathos.path.join(config.save_dir, last.pt), devicedevice ) for epoch in range(start_epoch, config.epochs): model.train() for step, batch in enumerate(train_loader): inputs, targets batch inputs inputs.to(device, non_blockingTrue) targets targets.to(device, non_blockingTrue) optimizer.zero_grad(set_to_noneTrue) with autocast(): outputs model(inputs) loss criterion(outputs, targets) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update() scheduler.step() # 验证 val_metric evaluate(model, val_loader, device) is_best val_metric best_metric if is_best: best_metric val_metric # 保存 checkpoint state { epoch: epoch, model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), scaler: scaler.state_dict(), best_metric: best_metric, } save_checkpoint(state, config.save_dir, is_bestis_best) print(fEpoch {epoch} done, val_metric{val_metric:.4f}, best{best_metric:.4f})这个模板里optimizer.zero_grad(set_to_noneTrue)比默认的zero_grad()更省显存是 PyTorch 官方推荐的做法。non_blockingTrue配合pin_memoryTrue的 DataLoader 能加速数据搬运。clip_grad_norm_在 AMP 下要先unscale_再裁剪顺序不能反。4. 实操全流程从零到稳定跑完4.1 环境准备与依赖检查在开始之前先把环境确认一遍。这一步看起来啰嗦但能省掉后面 80% 的玄学问题。# 确认 GPU 可用 nvidia-smi # 确认 PyTorch 能识别 GPU python -c import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.device_count()) # 确认 CUDA 版本匹配 python -c import torch; print(torch.version.cuda)如果torch.cuda.is_available()返回False先别急着改代码八成是驱动或者 CUDA 版本不匹配。nvidia-smi右上角显示的 CUDA 版本是驱动支持的最高版本PyTorch 自带的 CUDA 版本不能超过这个。比如驱动显示 CUDA 12.1那你装 PyTorch 的时候选 cu121 或更低选 cu124 就会出问题。另外如果你用的是 WSL2要注意 WSL 里的 CUDA 是透传 Windows 驱动的不需要在 WSL 里单独装驱动但 Windows 侧的驱动要够新。这个坑我踩过折腾了半天才发现是 Windows 驱动太旧。4.2 目录结构与日志规范一个清晰的目录结构能让后续排查省很多事。我一般这么组织project/ ├── configs/ │ └── segformer_b0.yaml ├── checkpoints/ │ ├── last.pt │ ├── best.pt │ └── epoch_10.pt ├── logs/ │ ├── train_20250101_220000.log │ └── tensorboard/ ├── src/ │ ├── train.py │ ├── model.py │ └── dataset.py └── scripts/ └── run_train.shcheckpoints和logs分开方便清理。scripts/run_train.sh把启动命令固化下来避免每次手敲出错#!/bin/bash set -e EXP_NAMEsegformer_b0_$(date %Y%m%d_%H%M%S) LOG_FILElogs/train_${EXP_NAME}.log mkdir -p logs checkpoints python -u src/train.py \ --config configs/segformer_b0.yaml \ --save-dir checkpoints \ --exp-name ${EXP_NAME} \ 21 | tee -a ${LOG_FILE}set -e让脚本遇到错误立即退出避免错误被吞掉。日志文件名带实验名和时间戳方便追溯。4.3 启动、监控与恢复的完整操作假设现在要启动一个训练任务完整流程是这样# 1. 登录服务器 ssh usergpu-server # 2. 新建 tmux 会话 tmux new -s train_segformer # 3. 激活环境 conda activate pytorch_env cd /path/to/project # 4. 启动训练 bash scripts/run_train.sh # 5. 按 Ctrlb 然后 d 分离会话 # 此时可以关掉 SSH训练继续 # 6. 过一会儿重新登录附着会话查看进度 tmux attach -t train_segformer # 7. 如果发现训练挂了检查日志 tail -n 100 logs/train_*.log # 8. 修复问题后重新启动脚本会自动从 last.pt 恢复 bash scripts/run_train.sh监控方面除了看日志我强烈建议开一个 TensorBoardtensorboard --logdir logs/tensorboard --port 6006 --host 0.0.0.0然后在本地用 SSH 端口转发访问这里只讲本地端口转发用于查看训练曲线ssh -L 6006:localhost:6006 usergpu-server浏览器打开http://localhost:6006就能看到 loss 曲线。这样即使不在服务器旁边也能随时掌握训练状态。4.4 一个真实的续训现场记录说个具体的例子。上个月跑一个检测模型训练到 epoch 23 的时候云厂商发通知说宿主机要维护实例会被迁移。当时心里一紧赶紧去检查 checkpoint。打开checkpoints目录看到last.pt的时间戳是 20 分钟前说明最近一次保存是 epoch 23 结束时。best.pt是 epoch 19 的说明 epoch 20 到 23 没有刷新最优。实例迁移完成后重新登录tmux attach发现会话没了因为宿主机重启tmux 会话也丢了这是正常的。重新tmux new跑bash scripts/run_train.sh日志里打出[Resume] loading checkpoint from checkpoints/last.pt [Resume] resume from epoch 24, best_metric0.8732训练从 epoch 24 继续loss 曲线和中断前完全接得上没有出现抖动。最终跑到 epoch 80best_metric 是 0.8915比中断前的 0.8732 还高了一点。整个过程除了损失了不到 20 分钟最后一次保存到中断之间的时间其他都无缝衔接。这个例子里如果只做了 tmux 没做 checkpoint那 23 个 epoch 全白跑如果只做了 checkpoint 没做 tmux那每次 SSH 断开都要手动重启体验极差。两者配合才是完整的方案。5. 常见问题与排查技巧实录5.1 checkpoint 相关的典型报错报错一RuntimeError: Error(s) in loading state_dict for Model: Missing key(s) in state_dict这个通常是模型结构变了比如你改了 backbone 或者加了新层但加载的是旧 checkpoint。解决办法有两个一是用strictFalse加载让 PyTorch 忽略不匹配的 key二是写个映射函数把旧 key 名转成新 key 名。missing, unexpected model.load_state_dict(ckpt[model], strictFalse) print(fMissing keys: {missing}) print(fUnexpected keys: {unexpected})报错二RuntimeError: CUDA out of memory在加载 checkpoint 时出现这是因为torch.load默认会把 tensor 加载到保存时的设备上。如果保存时在 GPU 0加载时 GPU 0 显存不够就会 OOM。加map_locationcpu先加载到内存再按需搬到 GPU。报错三pickle.UnpicklingError: invalid load keycheckpoint 文件损坏了通常是保存过程中进程被杀导致的。这种情况只能回退到上一个完好的 checkpoint。所以前面说的“保留最近 N 个”策略很重要就是防这个。5.2 tmux 会话丢失的排查tmux ls显示no server running on /tmp/tmux-1000/default说明 tmux 服务端进程没了。原因可能是机器重启、tmux 进程被 OOM killer 杀了、或者/tmp被清理了。如果是机器重启那没办法只能靠 checkpoint 恢复。如果是 OOM那要检查是不是训练进程占内存太多或者系统内存本身不够。/tmp被清理的情况比较少见但有些云厂商会定期清理/tmp这时候可以把 tmux socket 目录改到别的地方# 在 ~/.bashrc 里加 export TMUX_TMPDIR$HOME/.tmux_socket mkdir -p $TMUX_TMPDIR5.3 训练卡住但进程还在的情况有时候nvidia-smi显示 GPU 利用率 0%但进程还在日志也不更新。这种“假死”状态最让人头疼。常见原因有几个一是 DataLoader 的num_workers设太大worker 进程之间死锁。解决办法是把num_workers调小或者加persistent_workersFalse。二是分布式训练里某个 rank 挂了其他 rank 在等它。这种情况要看torch.distributed的日志通常会有一个 rank 报错。三是 IO 卡住比如读的数据在慢速网络存储上。用iostat或者iotop看一下磁盘 IO 就知道。排查的时候可以用py-spy直接 dump 进程栈pip install py-spy py-spy dump --pid PID这个工具能在不中断进程的情况下看到每个线程在干什么非常实用。5.4 常见问题速查表现象可能原因排查方法解决方案SSH 断开后进程消失前台运行收到 SIGHUPps aux | grep python用 tmux 或 nohup恢复后 loss 抖动优化器状态没存检查 checkpoint 内容保存 optimizer.state_dict()恢复后学习率重置scheduler 状态没存打印 lr 对比保存 scheduler.state_dict()checkpoint 文件损坏保存时进程被杀torch.load报错保留多个历史 checkpointGPU 利用率 0% 但进程在DataLoader 死锁py-spy dump调小 num_workers日志文件暴涨每 step 都 printdu -sh logs/用 logging 控制频率tmux 会话丢失机器重启或 OOMtmux ls靠 checkpoint 恢复加载 checkpoint OOM默认加载到原设备看报错栈加 map_locationcpu5.5 几个我踩过的坑和对应技巧坑一checkpoint 存到网络存储上保存速度极慢。有一次把save_dir设成了 NFS 挂载的目录每个 epoch 保存要等十几秒训练效率大打折扣。后来改成先存本地 SSD再异步同步到网络存储问题解决。如果非要用网络存储建议用torch.save存到本地临时文件再用shutil.move移过去。坑二多个实验共用同一个 save_dircheckpoint 互相覆盖。这个错误很低级但很常见。解决办法是每个实验用独立的子目录目录名带上实验名和时间戳。坑三恢复训练后忘了重置 DataLoader 的随机种子。如果 DataLoader 用了shuffleTrue恢复后数据顺序和中断前不一致虽然不影响最终收敛但会让实验不可复现。要完全复现的话得把torch.Generator的状态也存下来。坑四AMP 的 scaler 状态没存恢复后前几个 step loss 异常。这个前面提过GradScaler的状态一定要存。而且恢复的时候scaler.load_state_dict要在第一次scaler.step之前调用。坑五用torch.save存了整个模型对象而不是 state_dict。存整个模型对象torch.save(model, path)虽然方便但依赖模型类的定义换环境容易加载失败。正确做法是只存state_dict加载时先实例化模型再load_state_dict。6. 进阶让续训更稳的几个工程手段6.1 原子写入避免 checkpoint 损坏前面提到 checkpoint 可能在保存过程中损坏一个有效的防护手段是原子写入先写到临时文件写完再重命名。重命名在大多数文件系统上是原子操作要么成功要么失败不会出现半个文件的情况。import os import torch def atomic_save(state, path): tmp_path path .tmp torch.save(state, tmp_path) os.replace(tmp_path, path) # 原子替换os.replace在 POSIX 系统上是原子的Windows 上也是。这样即使保存过程中进程被杀last.pt要么是旧的完整版本要么是新的完整版本不会损坏。6.2 用信号处理做优雅退出有时候你需要主动停止训练比如发现超参设错了但又不想丢掉当前进度。可以在脚本里注册信号处理函数收到SIGTERM或SIGINT时先保存 checkpoint 再退出。import signal import sys class GracefulExit: def __init__(self): self.should_stop False signal.signal(signal.SIGTERM, self._handler) signal.signal(signal.SIGINT, self._handler) def _handler(self, signum, frame): print(f[Signal] received {signum}, will save checkpoint and exit) self.should_stop True graceful GracefulExit() # 在训练循环里检查 for epoch in range(start_epoch, config.epochs): for step, batch in enumerate(train_loader): if graceful.should_stop: save_checkpoint(state, config.save_dir) sys.exit(0) # ... 训练逻辑这样你kill PID的时候进程会先存 checkpoint 再退出不会丢进度。6.3 自动重启用 supervisor 或 systemd如果你希望进程挂了能自动拉起来可以用supervisor或systemd管理。以systemd为例写一个 service 文件[Unit] DescriptionPyTorch Training Job Afternetwork.target [Service] Typesimple Useryour_user WorkingDirectory/path/to/project ExecStart/bin/bash scripts/run_train.sh Restarton-failure RestartSec30 StandardOutputappend:/path/to/project/logs/systemd.log StandardErrorappend:/path/to/project/logs/systemd.err [Install] WantedBymulti-user.targetRestarton-failure让进程异常退出时自动重启RestartSec30是重启前等 30 秒。配合断点续训进程重启后会自动从last.pt恢复基本实现无人值守。不过要注意systemd管理的是系统级服务需要 root 权限。如果没有 root可以用supervisor的用户模式或者写个简单的守护脚本。6.4 磁盘空间监控与自动清理长任务跑久了checkpoint 和日志会占满磁盘。可以在训练脚本里加个磁盘检查空间不足时自动清理旧文件。import shutil def check_disk_space(path, min_free_gb10): total, used, free shutil.disk_usage(path) free_gb free / (1024 ** 3) if free_gb min_free_gb: print(f[Warning] only {free_gb:.2f} GB free on {path}) return False return True在每个 epoch 保存前检查一次空间不够就删掉最旧的 checkpoint。这个逻辑可以和前面的max_keep策略结合双保险。7. 一些个人经验和收尾建议跑了这么多年的云端训练我最大的体会是稳定性不是靠某一个工具而是靠一套组合拳。tmux 解决会话问题checkpoint 解决中断问题原子写入解决损坏问题systemd 解决自动重启问题磁盘监控解决空间问题。每一层都不复杂但叠起来就能把“跑不完”的概率压到很低。如果只能记住三件事我建议是这三件第一永远用 tmux 跑训练不要图省事直接前台跑第二checkpoint 一定要存优化器和 scheduler 状态只存模型权重等于没存第三保留最近 N 个 checkpoint不要只留一个last.pt损坏的时候你会感谢自己。最后分享一个小技巧在训练脚本启动的时候把当前的 git commit hash、CUDA 版本、PyTorch 版本、GPU 型号这些信息一起写进日志。等过几个月回头看某个实验能快速定位当时的环境省去很多“这个结果是哪台机器跑的”的困惑。import subprocess import torch def log_env_info(logger): try: commit subprocess.check_output([git, rev-parse, HEAD]).decode().strip() except Exception: commit unknown logger.info(fGit commit: {commit}) logger.info(fPyTorch: {torch.__version__}) logger.info(fCUDA: {torch.version.cuda}) logger.info(fGPU: {torch.cuda.get_device_name(0)})这套东西搭好之后你就可以安心睡觉第二天起来收结果了。训练任务稳定跑完本质上就是把所有可能出问题的环节都提前想到、提前兜住。工程上的事大多如此。