
1. 从登录节点走到队列qsub 真正解决的是什么我第一次接触集群是在学校超算中心做计算流体力学模拟的时候。当时什么都不懂拿到账号第一反应就是 SSH 登上去把编译好的可执行文件直接往命令行里一敲然后看着它跑。前五分钟很顺畅五分钟之后管理员就发消息来了“同学请不要在登录节点跑任务请用 qsub 提交到计算节点。”我那时候才意识到原来集群的用法跟一台普通 Linux 服务器完全不一样。登录节点是给你编辑代码、编译程序、管理文件用的真正的计算资源都在后面的计算节点上。你的任务想要跑到计算节点上去就得通过作业调度系统来“递送”而 qsub 就是最核心的那个“递送工具”。1.1 为什么集群上不能“直接跑命令”集群和单机服务器最大的区别在于资源是共享且受限的。一台超算可能有几百个计算节点每个节点有几十个核但登录节点通常只有几个核它的职责是让你做轻量级操作而不是承担大规模计算。想象一下如果所有人都直接在登录节点上跑自己的大任务登录节点瞬间就会被塞满这时候其他人连敲一个ls都会卡半天。所以集群管理员会在调度系统里配置好资源分区你的计算任务必须通过 qsub 提交到队列里由调度器统一安排放到空闲的计算节点上执行。这其实是一种非常合理的分工逻辑登录节点管“人的操作”计算节点管“算的活”。而 qsub 就是连接这两者的那座桥。1.2 qsub 的适用阵营PBS/Torque/SGE 都是一个思路qsub 这个命令最常出现在 Torque/PBS Pro 和 Sun Grid EngineSGE这类作业调度系统中这些系统在高校超算中心、科研机构和企业 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 nodes1:ppn4 #PBS -l walltime01: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 nodes1:ppn4是我见过最容易被写错的一行。它的意思是申请 1 个节点每个节点用 4 个核。很多人会把 nodes 和 ppn 搞反写成nodes4:ppn1结果变成了 4 个节点各用 1 个核。这两者的区别非常大。如果你的程序是 MPI 并行程序需要跨节点通信那nodes4:ppn1可能是合理的但如果你的程序是 OpenMP 多线程或者只是多个独立任务并行跑那它应该尽可能在一个节点内完成nodes1:ppn8才是合适的写法。跨节点通信的开销远高于节点内通信尤其是对通信密集型任务来说盲目分散到多个节点可能反而更慢。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、memnodes1:ppn16申请 1 个节点上的 16 个核心。mem32gb申请 32GB 内存。很多集群不会强制限制内存但申请了最好按申请的量来用超了你可能直接触发节点上的 OOM Killer作业被杀死还会影响同节点上其他用户的任务。walltime24:00:00作业允许运行的最长墙钟时间格式是小时:分钟:秒。超过这个时间调度器会强制杀掉作业。如果你的任务经常跑到 25 小时而你申请了 24 小时那每次都会在最后一步失败。关于 walltime我的建议是宁可多申请一点也不要卡得太紧。因为调度器在排队时会把你的作业放进一个“预期运行时间段”里如果申请时间太长调度器可能会延长你的排队时间但申请太短导致任务中途被掐死损失更大。一般按真实运行时间的 1.2 到 1.5 倍申请比较稳妥。3.3 传递环境变量、工作目录与模块加载qsub -v可以显式传递环境变量给作业比如qsub -v MY_VAR123,INPUT_FILEdata.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_ARRAYIDTorque/PBS Pro 环境或$SGE_TASK_IDSGE 环境获取当前子任务编号然后用这个编号去拼接不同的输入文件路径#!/bin/bash #PBS -N array_job #PBS -l nodes1:ppn1 #PBS -t 1-100 INPUT_FILEinput_${PBS_ARRAYID}.dat OUTPUT_FILEoutput_${PBS_ARRAYID}.dat ./my_program -i $INPUT_FILE -o $OUTPUT_FILE作业依赖则是用来表达“任务之间有先后顺序”的。比如任务 B 必须等任务 A 成功结束之后才能开始JOB_A_ID$(qsub job_a.pbs) qsub -W dependafterok:$JOB_A_ID job_b.pbs这在串联多个阶段的流水线任务时非常有用前处理、主计算、后处理可以分别写成三个脚本用依赖关系串起来提交一次就够了。4. 交互模式用 qsub -I 进入一颗计算节点批处理作业适合那些能完全自动化跑完的任务但实际开发过程中经常遇到程序需要调试、脚本需要交互操作的情况。这时候你就可以用交互模式直接申请一颗计算节点然后把你的 shell 搬到那个节点上去。4.1 什么时候需要交互式作业程序编译完成后第一次跑之前想手动测试是否能运行。需要交互式调参比如调试 Python 脚本、运行 Jupyter Notebook。排查程序崩溃问题时手动执行一系列命令观察输出。交互模式的好处是你能实时看到输出随时打断、修改、重跑。缺点是它会长期占用一个计算资源而且如果你断线了交互会话也就没了作业可能被清理掉。4.2 交互模式的实际用法与注意事项申请交互式节点的命令qsub -I -l nodes1:ppn4,walltime02: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 workqS列是状态R表示正在运行Q表示排队中。Time Use是作业已经运行的时间。Queue是作业所在的队列。如果你需要更详细的信息用qstat -f 123456.master01查看完整状态里面有资源使用情况、运行节点、提交时间、调度原因等。5.2 排队时间为什么那么久排队时间过长的原因有很多最常见的几种你请求的资源数量太大比如申请 128 核但集群上当前没有足够的空闲资源满足你调度器只能等。集群上其他用户的作业比你优先级更高调度器会优先调度高优先级作业。队列本身的资源配额已经满了管理员设置了用户或组的配额上限。遇到长时间排队我的建议是先qstat -f看看作业的状态和限制原因。如果是因为申请资源过大可以适当缩小资源规模如果是因为优先级那只能换低峰时段提交。另外很多集群的调度器默认会做“回填”调度意思是队列里如果有小作业它会在不影响前面大作业的前提下插空运行。所以如果你的作业很小反而可能比预期更早跑起来。5.3 作业异常退出的排查链路作业提交后如果很快就变成“E”且没有产生输出八成是作业脚本本身有问题。排查的完整链路一般是看错误日志文件.err绝大多数线索都在这里。确认脚本是否有执行权限虽然qsub一般是读取脚本内容而不是直接执行文件但有些调度器要求脚本必须有x权限。确认脚本的换行符是 LF 而不是 CRLF从 Windows 编辑过再上传到 Linux 集群的脚本经常栽在这里。在脚本开头临时加一行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 nodes1:ppn8-pe smp 8--nodes1 --ntasks-per-node8取工作目录环境变量$PBS_O_WORKDIR$SGE_O_WORKDIR$SLURM_SUBMIT_DIR这张表不是完全一一对应各调度器之间还是有一些细节差异但核心逻辑是一致的申请资源、提交作业、排队运行、查看状态。6.2 从 qsub 迁移到 sbatch 时需要改什么如果你的集群环境换成了 Slurm旧脚本迁移起来并不难# Torque 风格 #PBS -N my_job #PBS -l nodes1:ppn8,walltime01:00:00,mem16gb # Slurm 风格 #SBATCH --job-namemy_job #SBATCH --nodes1 #SBATCH --ntasks-per-node8 #SBATCH --time01:00:00 #SBATCH --mem16G另外脚本里凡是依赖$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 和作业脚本的交互关系然后再慢慢扩展功能。你会发现作业调度其实不复杂它只是一套把你和计算资源隔开、又帮你管理资源的规则。搞清楚这套规则后面的路就顺畅了。