如何用Bash脚本让8卡服务器GPU始终满负荷运行训练任务?
保持8-GPU服务器满负载的动态任务调度方案
你的原始脚本确实会因为任务时长不一致导致GPU空闲浪费,要实现GPU一空闲就立刻启动新任务,我们可以用Bash的进程管理特性来做动态调度,核心是用wait -n监听任意后台进程的结束,然后及时给空闲GPU分配新任务。
下面是完整的改进脚本,我会一步步解释逻辑:
完整脚本
#!/bin/bash # 第一步:生成所有待执行的任务列表(把所有超参数组合存起来) tasks=() for l1 in {1e-4,2e-4,5e-4,1e-3}; do for attr in {0..7}; do tasks+=("$l1 $attr") done done # 第二步:初始化每个GPU的第一个任务,记录进程ID declare -A gpu_pids # 用关联数组存储GPU编号对应的进程ID for gpu in {0..7}; do if [ ${#tasks[@]} -gt 0 ]; then # 取出列表第一个任务 task="${tasks[0]}" tasks=("${tasks[@]:1}") # 从列表中移除已分配的任务 # 解析任务参数 read lr attr <<< "$task" # 启动任务,可选:把输出重定向到单独日志文件 python train.py --lr "$lr" --attr "$attr" --device "$gpu" > "train_gpu${gpu}_lr${lr}_attr${attr}.log" 2>&1 & # 记录该GPU对应的进程ID gpu_pids[$gpu]=$! echo "GPU $gpu 启动任务:lr=$lr, attr=$attr (PID: ${gpu_pids[$gpu]})" fi done # 第三步:动态补充任务,直到所有任务分配完毕 while [ ${#tasks[@]} -gt 0 ]; do # 等待任意一个后台进程结束,获取结束的进程PID finished_pid=$(wait -n) # 找出这个PID对应的GPU(哪个GPU空闲了) for gpu in "${!gpu_pids[@]}"; do if [ "${gpu_pids[$gpu]}" -eq "$finished_pid" ]; then echo "GPU $gpu 任务完成(PID: $finished_pid),准备分配新任务..." # 取出下一个待执行任务 task="${tasks[0]}" tasks=("${tasks[@]:1}") read lr attr <<< "$task" # 在空闲GPU上启动新任务 python train.py --lr "$lr" --attr "$attr" --device "$gpu" > "train_gpu${gpu}_lr${lr}_attr${attr}.log" 2>&1 & gpu_pids[$gpu]=$! echo "GPU $gpu 启动新任务:lr=$lr, attr=$attr (PID: ${gpu_pids[$gpu]})" break # 找到对应的GPU后退出循环,处理下一个结束的进程 fi done done # 第四步:等待最后一批任务全部完成 wait echo "所有训练任务已完成!"
关键逻辑解释
- 任务预生成:先把所有需要运行的
(lr, attr)组合存入数组,避免循环嵌套导致的调度混乱,方便后续按需取用。 - GPU进程跟踪:用关联数组
gpu_pids记录每个GPU当前运行的进程ID,这样能精准定位哪个GPU空闲了。 - 动态等待与补位:
wait -n是Bash 4.3+的特性,它会等待任意一个后台进程结束并返回其PID。我们通过PID找到对应的空闲GPU,立刻给它分配新任务,确保8块GPU始终处于满负载状态。 - 日志分离:把每个任务的输出重定向到单独的日志文件,避免多个任务的输出混在一起,方便后续排查问题。
兼容性注意
如果你的服务器Bash版本低于4.3(可以用bash --version查看),wait -n无法使用,这时可以用循环检查每个进程的存活状态来替代:
# 替代wait -n的逻辑,适合低版本Bash while [ ${#tasks[@]} -gt 0 ]; do # 遍历所有GPU,检查进程是否存活 for gpu in "${!gpu_pids[@]}"; do if ! kill -0 "${gpu_pids[$gpu]}" 2>/dev/null; then # 进程已结束,分配新任务 echo "GPU $gpu 任务完成,准备分配新任务..." task="${tasks[0]}" tasks=("${tasks[@]:1}") read lr attr <<< "$task" python train.py --lr "$lr" --attr "$attr" --device "$gpu" > "train_gpu${gpu}_lr${lr}_attr${attr}.log" 2>&1 & gpu_pids[$gpu]=$! echo "GPU $gpu 启动新任务:lr=$lr, attr=$attr (PID: ${gpu_pids[$gpu]})" # 处理完一个GPU后,重新开始循环(避免漏查其他可能空闲的GPU) continue 2 fi done sleep 1 # 每秒检查一次,避免占用过多CPU done
这种方式效率稍低,但能兼容旧版本Bash。
内容的提问来源于stack exchange,提问作者Hammer. Wang
相关产品推荐
相关产品推荐

