☰
qsub 作业提交完全指南:从 PBS 脚本编写到 Slurm 迁移
2026/10/2 15:20:53 网站建设 项目流程

1. 从登录节点走到队列:qsub 真正解决的是什么

我第一次接触集群是在学校超算中心做计算流体力学模拟的时候。当时什么都不懂,拿到账号第一反应就是 SSH 登上去,把编译好的可执行文件直接往命令行里一敲,然后看着它跑。前五分钟很顺畅,五分钟之后管理员就发消息来了:“同学,请不要在登录节点跑任务,请用 qsub 提交到计算节点。”

我那时候才意识到,原来集群的用法跟一台普通 Linux 服务器完全不一样。登录节点是给你编辑代码、编译程序、管理文件用的,真正的计算资源都在后面的计算节点上。你的任务想要跑到计算节点上去,就得通过作业调度系统来“递送”,而 qsub 就是最核心的那个“递送工具”。

1.1 为什么集群上不能“直接跑命令”

集群和单机服务器最大的区别在于资源是共享且受限的。一台超算可能有几百个计算节点,每个节点有几十个核,但登录节点通常只有几个核,它的职责是让你做轻量级操作,而不是承担大规模计算。

想象一下,如果所有人都直接在登录节点上跑自己的大任务,登录节点瞬间就会被塞满,这时候其他人连敲一个ls都会卡半天。所以集群管理员会在调度系统里配置好资源分区,你的计算任务必须通过 qsub 提交到队列里,由调度器统一安排,放到空闲的计算节点上执行。

这其实是一种非常合理的分工逻辑:登录节点管“人的操作”,计算节点管“算的活”。而 qsub 就是连接这两者的那座桥。

1.2 qsub 的适用阵营:PBS/Torque/SGE 都是一个思路

qsub 这个命令最常出现在 Torque/PBS Pro 和 Sun Grid Engine(SGE)这类作业调度系统中,这些系统在高校超算中心、科研机构和企业 HPC 环境里非常常见。虽然各家调度器的具体实现有差异,但 qsub 的基本语义是共通的:提交一个作业、申请资源、排队等待、在计算节点上执行任务。

后来我转到用 Slurm 的环境,发现它用的提交命令是sbatch,不是qsub。但只要你理解了“批处理作业”这个核心概念,从 qsub 迁移到 sbatch 其实就是参数名的翻译题,而不是重新学一遍。这个后面我会专门讲。

建议:接到任何集群账号的第一件事,先敲qsub --help或者man qsub,很多管理员会定制集群的策略,默认参数不一定是通用值。

2. 一个能跑的作业脚本,先从这些行说起

很多人第一次用 qsub 会误以为它是一个自带所有参数的命令行工具,直接在命令后面加各种选项就能跑任务。实际上,更常见、更规范的做法是写一个“作业脚本”,然后用 qsub 提交这个脚本。

为什么推荐脚本方式?因为作业脚本里有资源申请、环境加载、执行命令、日志输出这些信息,它构成了一个可复现的“任务描述文件”。你这次跑通之后,下次改改参数就能重新用,别人拿到你的脚本也能理解你当时是怎么跑的。比在命令行里敲一长串参数要清晰得多。

2.1 一个最小作业脚本的骨架

最简单的作业脚本长这样:

#!/bin/bash #PBS -N my_test_job #PBS -l nodes=1:ppn=4 #PBS -l walltime=01:00:00 #PBS -q workq #PBS -o my_test_job.out #PBS -e my_test_job.err cd $PBS_O_WORKDIR echo "Job started at $(date)" echo "Running on host: $(hostname)" # 模拟一个计算任务 sleep 60 echo "Job finished at $(date)"

把它存成test.pbs,然后执行:

qsub test.pbs

如果提交成功,调度系统会返回一串作业 ID,类似123456.master01,有了这个 ID 你就能用qstat查状态、用qdel删作业。

2.2 申请多少资源才不算浪费

脚本里#PBS -l nodes=1:ppn=4是我见过最容易被写错的一行。它的意思是申请 1 个节点,每个节点用 4 个核。很多人会把 nodes 和 ppn 搞反,写成nodes=4:ppn=1,结果变成了 4 个节点各用 1 个核。

这两者的区别非常大。如果你的程序是 MPI 并行程序,需要跨节点通信,那nodes=4:ppn=1可能是合理的;但如果你的程序是 OpenMP 多线程,或者只是多个独立任务并行跑,那它应该尽可能在一个节点内完成,nodes=1:ppn=8才是合适的写法。

跨节点通信的开销远高于节点内通信,尤其是对通信密集型任务来说,盲目分散到多个节点可能反而更慢。

2.3 为什么总有人忘记工作目录这一行

作业脚本提交到计算节点后,默认的工作目录往往不是你提交命令时所在的目录,而是提交用户的 home 目录,或者是一个默认目录。如果脚本里的程序需要读取当前目录下的输入文件,你就会发现程序报错“找不到文件”。

解决办法就是在脚本里加上这行:

cd $PBS_O_WORKDIR

$PBS_O_WORKDIR是 PBS 系统自动注入的环境变量,它记录了你在哪个目录下执行了 qsub,也就是“作业提交目录”。脚本里写上cd $PBS_O_WORKDIR之后,计算节点上的工作目录就会被切换到你提交作业的地方,输入输出文件的位置就和你的预期一致了。

提醒:很多集群上这种环境变量的名字和语义大同小异,但个别 SGE 环境里这个变量不叫$PBS_O_WORKDIR,需要先env | grep WORK确认。我遇到过在 SGE 上脚本直接报错的情况,查了才发现变量名不生效。

3. qsub 参数逐个拆解:从队列选择到作业数组

作业脚本里的#PBS注释是最常用的参数配置方式,但 qsub 的命令行参数也能实现同样的功能,两边可以互相覆盖。实际工作中,我习惯把稳定的资源配置写在脚本里,把每次需要改的参数(比如输入文件路径、输出日志名)放在命令行里覆盖,这样更灵活。

3.1 队列、作业名与日志文件

#PBS -q workq指定队列。集群管理员会配置多个队列,有的队列节点多、排队时间长,有的队列节点少、响应快。如果你不指定队列,调度器会用默认队列,但默认队列的优先级往往不是最优的。

#PBS -N指定作业名,它只影响你在qstat输出里看到的名称,不影响程序和代码。取一个能一眼分辨的名字很重要,尤其是你同时挂了很多作业时,job1、job2这种名字会让后续管理变得非常痛苦。

#PBS -o指定标准输出日志文件,#PBS -e指定错误日志文件。如果不指定,调度系统会把输出放在一个默认位置,可能是一个混合了所有作业输出的目录,也可能直接丢到你的 home 目录下。我强烈建议显式指定这两个文件,并且把日志放在和输入文件相同的目录里,方便排查问题。

需要注意:某些调度器版本里,如果-o指定的目录不存在,作业并不会报错,但日志文件就是静默丢失了。我踩过一次,折腾了半天才发现输出目录因为脚本清理没建出来。

3.2 资源限制参数:nodes、ppn、walltime、mem

  • nodes=1:ppn=16:申请 1 个节点上的 16 个核心。
  • mem=32gb:申请 32GB 内存。很多集群不会强制限制内存,但申请了最好按申请的量来用,超了你可能直接触发节点上的 OOM Killer,作业被杀死,还会影响同节点上其他用户的任务。
  • walltime=24:00:00:作业允许运行的最长墙钟时间,格式是小时:分钟:秒。超过这个时间调度器会强制杀掉作业。如果你的任务经常跑到 25 小时,而你申请了 24 小时,那每次都会在最后一步失败。

关于 walltime,我的建议是宁可多申请一点,也不要卡得太紧。因为调度器在排队时会把你的作业放进一个“预期运行时间段”里,如果申请时间太长,调度器可能会延长你的排队时间;但申请太短导致任务中途被掐死,损失更大。一般按真实运行时间的 1.2 到 1.5 倍申请比较稳妥。

3.3 传递环境变量、工作目录与模块加载

qsub -v可以显式传递环境变量给作业,比如:

qsub -v MY_VAR=123,INPUT_FILE=data.txt test.pbs

如果你的程序依赖某些环境变量,但你又不想把它们写死在脚本里,-v是很好的方式。还有一种做法是在脚本里用export声明,但-v更灵活,每次提交时可以随时改。

计算节点上的软件环境往往和登录节点不完全一样。很多集群需要你手动加载某些软件模块,比如:

module load intel-mpi module load python/3.8

如果脚本里没有加载这些模块,程序运行时会提示找不到动态库或者命令不存在。所以写作业脚本时,一定要在计算部分启动之前完成所有环境加载。

3.4 数组任务与作业依赖

数组任务是我特别喜欢的一个功能。假设你有 100 个独立的输入文件,每个文件需要一个单独的计算任务,如果手动提交 100 次作业就太蠢了。数组任务可以一次提交 100 个子任务:

qsub -t 1-100 job_array.pbs

在脚本里通过$PBS_ARRAYID(Torque/PBS Pro 环境)或$SGE_TASK_ID(SGE 环境)获取当前子任务编号,然后用这个编号去拼接不同的输入文件路径:

#!/bin/bash #PBS -N array_job #PBS -l nodes=1:ppn=1 #PBS -t 1-100 INPUT_FILE="input_${PBS_ARRAYID}.dat" OUTPUT_FILE="output_${PBS_ARRAYID}.dat" ./my_program -i $INPUT_FILE -o $OUTPUT_FILE

作业依赖则是用来表达“任务之间有先后顺序”的。比如任务 B 必须等任务 A 成功结束之后才能开始:

JOB_A_ID=$(qsub job_a.pbs) qsub -W depend=afterok:$JOB_A_ID job_b.pbs

这在串联多个阶段的流水线任务时非常有用:前处理、主计算、后处理可以分别写成三个脚本,用依赖关系串起来,提交一次就够了。

4. 交互模式:用 qsub -I 进入一颗计算节点

批处理作业适合那些能完全自动化跑完的任务,但实际开发过程中,经常遇到程序需要调试、脚本需要交互操作的情况。这时候你就可以用交互模式,直接申请一颗计算节点,然后把你的 shell 搬到那个节点上去。

4.1 什么时候需要交互式作业

  • 程序编译完成后,第一次跑之前想手动测试是否能运行。
  • 需要交互式调参,比如调试 Python 脚本、运行 Jupyter Notebook。
  • 排查程序崩溃问题时,手动执行一系列命令观察输出。

交互模式的好处是你能实时看到输出,随时打断、修改、重跑。缺点是它会长期占用一个计算资源,而且如果你断线了,交互会话也就没了,作业可能被清理掉。

4.2 交互模式的实际用法与注意事项

申请交互式节点的命令:

qsub -I -l nodes=1:ppn=4,walltime=02:00:00 -q workq

提交成功后你会发现自己“被传送”到了一台计算节点上,命令行提示符的主机名会变化。这时候你可以正常运行命令、跑程序。退出交互模式就输入exit,资源随即释放。

需要注意的事项:

  • 交互模式下没有完整的作业脚本,环境变量的处理需要格外小心,尤其是工作目录,登录节点的工作目录不一定在计算节点上存在。
  • 部分集群的交互模式申请需要额外授权,管理员没开权限的话,qsub -I会直接报错。
  • 交互节点上不要跑超出申请时间的任务,时间到了会被强制断掉。

我在调试 MPI 程序时特别喜欢用交互模式,先申请一个 4 核小分区,然后自己手动执行mpirun -np 4 ./app,就能实时看到输出和报错,调试效率比提交批处理作业再翻日志高很多。

5. 作业提交之后:状态判断与排队黑盒

qsub 提交完作业只是开始,真正的等待和排查才是日常。qsub 之后一般会有三种结果:作业排队(Q)、作业运行(R)、作业结束(E)。但实际使用中,你还会看到 H(挂起)、C(完成)、X(子任务已完成)等状态。

5.1 看懂 qstat 的一行输出

最常用的查看命令是qstat,它会输出一行行作业信息:

Job id Name User Time Use S Queue ---------------- --------------- -------- -------- - ----- 123456.master01 my_test_job zhangsan 00:00:30 R workq
  • S列是状态,R表示正在运行,Q表示排队中。
  • Time Use是作业已经运行的时间。
  • Queue是作业所在的队列。

如果你需要更详细的信息,用qstat -f 123456.master01查看完整状态,里面有资源使用情况、运行节点、提交时间、调度原因等。

5.2 排队时间为什么那么久

排队时间过长的原因有很多,最常见的几种:

  1. 你请求的资源数量太大(比如申请 128 核),但集群上当前没有足够的空闲资源满足你,调度器只能等。
  2. 集群上其他用户的作业比你优先级更高,调度器会优先调度高优先级作业。
  3. 队列本身的资源配额已经满了,管理员设置了用户或组的配额上限。

遇到长时间排队,我的建议是先qstat -f看看作业的状态和限制原因。如果是因为申请资源过大,可以适当缩小资源规模;如果是因为优先级,那只能换低峰时段提交。

另外,很多集群的调度器默认会做“回填”调度,意思是队列里如果有小作业,它会在不影响前面大作业的前提下插空运行。所以如果你的作业很小,反而可能比预期更早跑起来。

5.3 作业异常退出的排查链路

作业提交后如果很快就变成“E”且没有产生输出,八成是作业脚本本身有问题。排查的完整链路一般是:

  1. 看错误日志文件.err,绝大多数线索都在这里。
  2. 确认脚本是否有执行权限,虽然qsub一般是读取脚本内容而不是直接执行文件,但有些调度器要求脚本必须有x权限。
  3. 确认脚本的换行符是 LF 而不是 CRLF,从 Windows 编辑过再上传到 Linux 集群的脚本经常栽在这里。
  4. 在脚本开头临时加一行echo "start",配合#PBS -o输出,确认作业是否真的进入了运行阶段。

最常见的坑是脚本忘了#!/bin/bash这一行。虽然大部分情况下调度器会默认用/bin/sh执行,但一旦你的脚本里用了[[ ]]这类 bash 特有语法,它就会报语法错误。我第一次提交自己的作业脚本就遇到过这种情况,卡了快两个小时才发现是 shebang 的问题。

6. 这种调度思路在其他平台上的迁移

学习 qsub 不只是为了在一个集群上用,更重要的是理解作业调度的通用思想。现在很多新集群已经从 Torque 转向 Slurm 了,Slurm 用sbatch、squeue、scancel来替代 qsub、qstat、qdel。

6.1 Torque/PBS、SGE 与 Slurm 的对应关系

下面这张表是我自己整理的常用命令对照,平时迁移作业时会直接拿它做参考:

功能Torque/PBSSGESlurm
提交批处理作业qsubqsubsbatch
提交交互作业qsub -Iqrshsrun
删除作业qdelqdelscancel
查看作业状态qstatqstatsqueue
查看节点状态pbsnodesqhostsinfo
作业详细信息qstat -fqstat -jscontrol show job
申请资源参数-l nodes=1:ppn=8-pe smp 8--nodes=1 --ntasks-per-node=8
取工作目录环境变量$PBS_O_WORKDIR$SGE_O_WORKDIR$SLURM_SUBMIT_DIR

这张表不是完全一一对应,各调度器之间还是有一些细节差异,但核心逻辑是一致的:申请资源、提交作业、排队运行、查看状态。

6.2 从 qsub 迁移到 sbatch 时需要改什么

如果你的集群环境换成了 Slurm,旧脚本迁移起来并不难:

# Torque 风格 #PBS -N my_job #PBS -l nodes=1:ppn=8,walltime=01:00:00,mem=16gb # Slurm 风格 #SBATCH --job-name=my_job #SBATCH --nodes=1 #SBATCH --ntasks-per-node=8 #SBATCH --time=01:00:00 #SBATCH --mem=16G

另外,脚本里凡是依赖$PBS_*环境变量的地方都要替换掉。比如cd $PBS_O_WORKDIR通常换成cd $SLURM_SUBMIT_DIR,$PBS_ARRAYID换成$SLURM_ARRAY_TASK_ID。

我的经验是,迁移作业脚本最大的工作量不是改写参数,而是排查脚本里所有隐式依赖的路径和环境变量。很多人写作业脚本时从没意识到$PATH在某台计算节点上可能不包含某个软件的安装目录,结果换平台后就各种找不到命令。

7. 我在集群上踩过的坑与最后的建议

从第一次被管理员提醒“不要直接跑命令”到现在,我在集群上跑了上万次作业,踩过的坑数都数不清。挑几个最典型的分享出来,算是替大家排雷。

第一个坑是#PBS指令必须写在脚本顶部的 shebang 之后,而且必须是“完整的一行注释”,不能在#PBS前面有空格或者缩进。有些编辑器会自动加缩进,一保存作业脚本就废了。

第二个坑是:不要在作业脚本里用cd切换到不存在的目录。很多人的输入文件目录是在登录节点上临时建的,提交后不小心删了,结果作业运行时目录不存在,程序直接报错。

第三个坑更隐蔽:作业脚本里用到相对路径,但脚本运行时的当前目录不是$PBS_O_WORKDIR。这其实是很多人栽过跟头的“隐形坑”,所以我每次写作业脚本的第一条命令基本都是cd $PBS_O_WORKDIR。

第四个坑是整个作业逻辑依赖多个目录下的文件,但你没有把所有需要的环境变量传过去。比如脚本里用到了某个自定义的 Python 虚拟环境,却没有激活它,程序直接用了系统自带的 Python,版本不同,跑出来的结果完全不对。

至于最后的建议,我觉得只要记住一句话就够了:qsub不是“运行命令”,而是“提交一份任务计划”。你写作业脚本的时候,不要只想着“我要跑这个程序”,要多想一步“程序在哪里、输入文件在哪里、依赖库在哪里、结果输出到哪里”。想通了这几个问题,你的作业脚本基本一次就能跑通,排队等待的时间也能少一半。

如果你刚开始接触集群,不用急着把所有参数背下来,先跑通一个最简单的作业,理解 qsub 和作业脚本的交互关系,然后再慢慢扩展功能。你会发现,作业调度其实不复杂,它只是一套把你和计算资源隔开、又帮你管理资源的规则。搞清楚这套规则,后面的路就顺畅了。

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

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

立即咨询