简介:面向国科大超算中心用户编写的Slurm作业调度系统使用指南,重点介绍超算平台上作业提交、队列查看与资源管理的基本流程,适合高性能计算入门者与科研人员快速上手。内容从基本概念讲起,系统梳理了sinfo显示队列与节点、squeue查看作业、scontrol查询详细分区/节点/作业信息等常用命令,并对三种作业提交方式——交互式srun、批处理sbatch、分配式salloc——给出参数说明、环境变量解析与典型实例,还覆盖GPU作业提交、sbcast文件同步、sattach作业步吸附等进阶操作,基本涵盖日常超算使用的完整链路。压缩包内共1个文件,为PDF格式电子手册,整包约595KB,便于离线查阅。已有3344人学习下载,适合作为超算入门和日常查询的参考手册。
1. 高校超算中心的 Slurm 作业调度系统:先弄懂它管的是什么
高校超算中心里,最容易被新手当成黑匣子的就是 Slurm 作业调度系统。你登录上去,敲命令提交一个脚本,调度器就把任务排到成百上千个运算核心上,看起来像魔法一样。实际上,绝大多数人浪费的机时、踩过的坑,都和没搞懂“调度器到底怎么想”有关。这篇文章把 Slurm 从提交到排队、再到跑完排查这一整条链路讲清楚,新手能照着脚本一步步复现,熟手也能在参数边界和踩坑记录里找到对应自己问题的解法。
Slurm 管的事只有三件:收作业、排作业、跑作业。你在登录节点写号脚本交给它,它来决定你的任务什么时候上哪台节点、占用多少资源、跑多久、日志写到哪里。适合什么人读?刚把程序迁到超算上的同学、总在排队和超时之间反复挣扎的工程师,以及在 GPU 节点上报 CUDA 错误却查不出原因的人。
2. 提交链路:从登录节点到 SBATCH 脚本的最小闭环
2.1 登录节点与计算节点之间隔着层调度器
每个超算中心的物理架构基本一样:一堆计算节点组成一个或几个分区,一个控制节点负责调度决策,登录节点让你做轻量交互。登录节点是公共入口,不是用来跑计算的。A 同学直接在登录节点上跑过大矩阵分解,还没到半分钟,终端就被系统警告,说已经影响了登录节点的响应,再继续就会强杀进程。
正确做法是把计算任务写进作业脚本,通过 sbatch 提交。Slurm 收到后,会把脚本安排到某个计算节点上去执行。登录节点只需要处理一件事:把脚本和输入数据准备好,然后等待结果写回工作目录。判断自己在哪台节点上,可以用 hostname,绝大多数超算中心登录节点的主机名都会带有 login 字样。
2.2 最小可运行的 SBATCH 脚本:第一次提交的完整示例
第一次提交作业,不要急着上复杂参数。先写一个最小脚本,确认整条链路是通的。下面这个例子在任何配置了 Slurm 的集群上都能用:
#!/bin/bash #SBATCH --job-name=hello_slurm #SBATCH --partition=cpu #SBATCH --nodes=1 #SBATCH --cpus-per-task=4 #SBATCH --mem=8G #SBATCH --time=00:30:00 #SBATCH --output=logs/hello_%j.log mkdir -p logs module load python/3.9 python -c "import time; print('hello, slurm'); time.sleep(10)"提交命令就一行:sbatch run.sh。脚本里以#SBATCH开头的行是调度器指令,bash 会把它们当注释忽略掉,但 Slurm 会读这些行来决定作业属性。--partition=cpu指定提交到 cpu 分区,--nodes=1表示只申请一个计算节点,--cpus-per-task=4表示申请 4 个 CPU 核心,--mem=8G表示这个作业最多使用 8G 内存,--time=00:30:00是墙钟时间上限,超时会被系统直接杀掉。%j是作业号的占位符,最终日志文件名会变成hello_123456.log这种形式。
注意mkdir -p logs写在脚本里而不是靠调度器帮你建,这是一个常见的坑:--output指定的目录不存在时,有些集群的 slurmd 会静默失败或者报错,导致你等了半天看不到日志文件。
2.3 用 squeue、sacct、scontrol 掌握作业的每个阶段
提交之后最常用的三个命令,分别负责当前状态、历史记录和完整属性,我会把它们组合起来用:
squeue -u $USER # 当前在排队和运行中的作业列表,STATE 列是 PENDING 或 RUNNING sacct -j 123456 --format=JobID,JobName,State,ExitCode,Elapsed,End -X # 查看作业的完整生命周期,State 显示 COMPLETED、FAILED 等 scontrol show job 123456 # 查看单个作业的完整调度属性,包括排队原因、分区、节点列表squeue -u $USER只看自己提交的任务。PENDING表示在排队,RUNNING表示正在跑。理解作业卡在哪,需要用scontrol show job 123456 | grep Reason,这一行的值会直接告诉你排队原因,常见的是Resources(资源不够)、Priority(优先级被压住)、Dependency(等待依赖任务)。sacct -j适合跑完复盘,作业被杀了、异常退出了,看ExitCode比看日志更直接。
3. 排队、分区与并行:理解调度器后,你的任务才算真正属于集群
3.1 分区与 QOS:为什么有的作业能插队,有的作业只能等
每个超算中心都会划出几个分区。cpu 分区是最基础的通用计算池,gpu 分区里有带显卡的节点,large 或 highmem 分区提供大内存节点。分区不是摆设,它对作业能申请的资源上限、最大运行时长都有硬限制。你提交作业时如果不写--partition,系统会读默认分区;如果默认分区和你的需求不匹配,就会出现排队很久或者提交直接被拒的情况。
QOS(Quality of Service)比分区细一层,管的是限额和优先级。有的 QOS 允许你跑 48 小时,有的只允许 4 小时;有的允许一次提交 100 个作业,有的只允许 2 个。查看这些限制,用下面两条命令:
sinfo -s # 只看分区列表、节点数和分区状态,确认分区名拼写 scontrol show partition=large # 看某个分区的 MaxTime、QOS、节点列表、默认内存如果提交时提示Invalid partition,先用sinfo -s确认分区名,不要凭记忆写。如果提示QOSMaxCpuPerUserLimit,说明你的用户级配额满了,不是集群没资源,而是你的 QOS 限制到了上限。这个时候不是去催管理员,而是看自己有没有提交大量重复作业占着配额。
优先级这个东西,决定了同等条件下谁先跑。它由多个维度计算,包括 QOS 级别、作业等待时间、用户过往公平份额(FairShare)使用情况。你会发现刚提交的作业 Priority 值可能是负数,这很常见,尤其是最近跑了很多任务之后。等一段时间,作业的 Priority 会随时间增长,慢慢被调度。
3.2 从单机到多节点:srun 与 mpirun 的正确用法
多数人第一次跑并行程序时会困惑一件事:脚本里到底用 srun 还是 mpirun。Slurm 生态下,最推荐的是 srun。它由调度器直接管理任务启动,能正确感知当前作业申请的节点列表和任务数,不会出现“作业申请了 2 个节点,但 mpirun 只在本机拉起 8 个进程”这种诡异问题。
一个典型的多节点 MPI 作业脚本长这样:
#!/bin/bash #SBATCH --job-name=mpi_job #SBATCH --partition=large #SBATCH --nodes=2 #SBATCH --ntasks-per-node=8 #SBATCH --cpus-per-task=1 #SBATCH --time=01:00:00 #SBATCH --output=logs/mpi_%j.log module load openmpi/4.1 srun ./a.out这里--nodes=2申请两台节点,--ntasks-per-node=8指定每个节点跑 8 个进程,总共 16 个 MPI 进程。srun 会根据这些参数自动把进程分布到两台节点上,不需要你自己写 hostfile。如果你的代码是 OpenMP 多线程,而不是 MPI,那么应该调--cpus-per-task,而不是--ntasks-per-node。两种并行模式混在一起是另一套玩法,但超算中心的规范通常是:每个节点的进程数 × 每个进程的线程数,要小于等于该节点允许的核心数。
3.3 申请资源与实际占用:为什么不能默认整节点独占
看过很多新手的脚本,只写--nodes=1,不写--mem和--cpus-per-task。这时候调度器按分区默认值处理,有些集群的默认内存就是整节点的物理内存。你只跑一个单线程程序,却把整台 64 核 128G 的节点占住了,后果就是排队时间暴涨,系统资源大量闲置。
查看节点实际资源分配情况,可以用scontrol show node:
scontrol show node | grep -E "NodeName|CPUs|RealMemory|AllocMem|Gres" # 看节点总资源与已分配资源,确认节点当前负载申请资源的黄金法则是:只申请你实际会用的量,再加上一点点冗余。这既是对其他人负责,也是对你自己的排队时间负责。申请 8 核 32G 能跑的任务,不要申请 16 核 64G,除非你确定编译器优化和中间数据会让你吃下这么多。
4. 把脚本写到能上线的程度:CPU、内存、GPU 与环境的参数组合
4.1 CPU 与内存:cpus-per-task、mem、mem-per-cpu 怎么选
资源申请参数里,最容易写错的就是--mem和--cpus-per-task的搭配。下面这张表是我每次都拿来对照的:
| 参数 | 作用 | 常见写法 | 常见误用 |
|---|---|---|---|
--cpus-per-task | 每个任务分配的 CPU 核心数 | --cpus-per-task=8 | 与--ntasks混用 |
--mem | 整个作业的内存上限 | --mem=32G | 忘记写导致默认整节点内存 |
--mem-per-cpu | 每个 CPU 核心的内存 | --mem-per-cpu=4G | 和--mem同时写,冲突 |
--ntasks | 进程数 | --ntasks=1 | 用--ntasks=16跑单线程程序 |
我的习惯是:纯串行或多线程程序用--cpus-per-task和--mem;MPI 程序用--nodes加--ntasks-per-node,内存用--mem-per-cpu来估算。如果一个任务运行时间比较长,比如超过 10 小时,--time一定要留出余量。写--time=08:00:00却跑了 8 小时 5 分钟的作业,最后 5 分钟会被强制 kill,之前的计算全部白费。
计算内存需求有一个经验公式:先看输入数据的大小,再估峰值内存,最后加上 20% 的余量。不要用自己的笔记本电脑上的内存表现直接推断超算节点上的表现,数据加载方式和并行库的缓冲策略会带来明显差异。
4.2 GPU 作业:申请一张卡时,还要带上 CPU 和内存
GPU 节点的调度是 Slurm 里最容易翻车的地方。很多第一次用 GPU 分区的人,直接沿用 CPU 作业的脚本,跑起来之后报 CUDA error,原因是作业压根没有申请 GPU。在 GPU 分区,显存不是自动分配的,必须用--gres=gpu:1显式声明:
#!/bin/bash #SBATCH --partition=gpu #SBATCH --job-name=torch_train #SBATCH --gres=gpu:1 #SBATCH --nodes=1 #SBATCH --ntasks=1 #SBATCH --cpus-per-task=8 #SBATCH --mem=32G #SBATCH --time=12:00:00 #SBATCH --output=logs/train_%j.log module load cuda/11.8 conda activate pytorch python train.py --epochs 100--gres=gpu:1表示申请一张 GPU 卡。注意这行不能写成--gres=gpu:1 --gpus=1,两套语法只需要用一个。申请 2 张卡就写--gres=gpu:2,但在一个节点有 4 张卡的情况下,--gres=gpu:2有可能被调度到两个不同节点,所以最好加上--nodes=1,确保 2 张卡在同一台节点上。
GPU 作业的 CPU 和内存配额很容易被忽略。数据预处理在 CPU 上做,数据加载用到内存,如果只申请了 4 核 8G,预处理阶段会成为瓶颈。常见配置是每张 GPU 配 6 到 8 个 CPU 核心和 16 到 32G 内存,具体取决于你的模型大小和 batch size。
4.3 环境初始化:module 与 conda 必须在脚本里自己加载
很多同学在登录节点上手动module load python/3.9、conda activate pytorch,然后直接提交脚本,脚本里却不带这些命令。结果作业运行时报Command not found,花半天也找不到原因。原因很简单:计算节点上的环境是干净的,你登录时加载的模块不会自动传递过去。
正确做法是把环境初始化写进脚本:
#!/bin/bash #SBATCH --job-name=env_test #SBATCH --output=logs/env_%j.log source ~/.bashrc module load cuda/11.8 conda activate myenv which python python train.py如果你用 conda,建议在脚本里写source ~/.bashrc然后再conda activate,不要只写conda activate。有些集群的 bash 环境默认不加载 conda init 的内容,只写 activate 会报错。module load和conda activate的先后顺序也有讲究,先加载 CUDA 驱动相关的 module,再激活 conda 环境,避免两个 Python 的库路径互相干扰。
另外,不要在你的代码里硬编码绝对路径,比如/home/yourname/project/...这种写死的东西换成相对路径,或者使用环境变量$SLURM_SUBMIT_DIR。这个变量指向你提交作业时所在的目录,很多集群在计算节点上运行时的工作目录会指向临时目录,导致脚本找不到输入文件。我一般会在脚本开头加上cd $SLURM_SUBMIT_DIR,保证执行目录正确。
5. 避坑:看起来能跑、实际跑不动的五个高频场景
5.1 现象:提交瞬间失败,提示 Invalid partition
提交命令刚跑完,系统直接弹出一个错误,内容是sbatch: error: Batch job submission failed: Invalid partition。原因基本是两个:分区名拼写错误,或者你的账号没有该分区的访问权限。解决方法:先用sinfo -s看集群上有哪些分区,再用scontrol show partition=分区名查看是否包含你的账号。
不要让“刚才明明还能提”这种经验干扰判断。有些集群在维护期间会临时隐藏某个分区,sinfo -s里的 STATE 会是down或drain。我一个朋友把当年的模拟项目X的作业提到 large 分区,排了很久才注意节点状态变成了 drain,作业一直 PENDING,最后取消了在重提的时候选了 cpu 分区。第一次提交之前先看sinfo -s花不了十秒钟,能省下大量等错节点的时间。
5.2 现象:作业一直 PENDING,Reason 写着 Resources
squeue -u $USER显示作业一直 PENDING,用scontrol show job 123456 | grep Reason看到Resources。这个原因不是特权问题,是集群里暂时没有足够匹配你请求的空闲资源。你申请了 4 张 GPU 卡但整个分区只剩下 2 张,那就一直等着。这种属于资源分配不当,引发的问题是自己跟别的作业互相卡位。
解决思路是减小资源申请量。把--gres=gpu:4改成--gres=gpu:2,或者把--nodes=2改成--nodes=1,大概率立刻就能跑。如果任务确实必须要那么多资源,那就没有捷径,只能等。另外可以换个 QOS 再提一次,不同 QOS 的优先级不同,有时低优先级分区反而更容易排队排上。
5.3 现象:GPU 程序报错 CUDA error,原因竟是根本没申请 GPU
这是 GPU 分区最尴尬的翻车现场。脚本里没有写--gres=gpu:1,作业被调度到普通的 CPU 节点上,程序运行到一半,CUDA runtime 找不到设备,报出CUDA error: no kernel image is available for execution on the GPU或者直接段错误。很多人以为是代码问题,在代码里加了无数条检查,结果白折腾。
排查方法很简单:在脚本里加上nvidia-smi,让作业自己把 GPU 的信息打印出来。如果输出里没有显卡信息,说明资源申请本来就没带 GPU。另一种可能性是申请到了 GPU 节点但显存不足导致的 OOM,这种情况看报错信息里有没有CUDA out of memory,有的话就是 batch size 太大,或者--gres=gpu:1不够用。
5.4 现象:作业正常结束,但日志文件一个字节都没有
sbatch提交后很快变成COMPLETED,但logs/目录下就是看不到输出文件。可能原因有三个:第一,日志目录logs/在提交时并不存在,--output=logs/xxx_%j.log路径创建失败;第二,脚本启动后cd到了别的目录,--output里写的相对路径相对于提交目录;第三,程序输出被某个缓冲区缓存了,作业结束时缓冲区内容没有刷到文件里。
我的习惯是:提交前在命令行里先手动执行一次mkdir -p logs,不要依赖脚本去建目录。同时,用相对路径时,在脚本开头先cd $SLURM_SUBMIT_DIR,这样日志路径就一定落在你预期的地方。如果验证作业输出确实没内容但作业又是 COMPLETED,先看一眼sacct -j 作业号 --format=JobID,State,ExitCode,End,看 ExitCode 是不是 0,不是 0 说明程序非正常退出,只是没打印出错误到日志里。
5.5 现象:作业跑一半被杀,日志末行写着 DUE TO TIME LIMIT
日志文件末尾会出现DUE TO TIME LIMIT或者Killed by signal 0。原因是--time申请的时间小于程序实际运行时间,到了墙钟时间上限,调度器强制终止作业。这个问题在长训练任务里最常见,尤其是需要 20 小时但只申请了 12 小时的任务,半夜作业被杀,第二天起来才发现。
如果作业还在运行中,可以用scontrol update JobId=123456 TimeLimit=02:00:00延长运行时间,前提是集群 QOS 允许。如果作业已经被杀了,没有后悔药,只能调整--time后重新提交。对训练任务,我会先跑一个小规模测试,用进度条估算总时间,再乘 1.5 倍写进--time。记住一句话:时间申请太多只是排队久一点,申请太少则是白跑一次。
6. 进阶玩法:数组作业、作业依赖与排队卡住的后悔药
真正把 Slurm 用到得心应手,靠的是三类技巧:数组作业、作业依赖和运行中的调度干预。数组作业适合成百上千个相互独立的小任务,比如参数扫描、超参数搜索,一次提交就搞定:
#!/bin/bash #SBATCH --job-name=param_scan #SBATCH --array=1-100%10 #SBATCH --output=logs/scan_%A_%a.log #SBATCH --partition=cpu #SBATCH --cpus-per-task=2 python run_exp.py --param-idx ${SLURM_ARRAY_TASK_ID}--array=1-100%10含义是共 100 个子任务,同时最多跑 10 个,%10这个并发限制很重要,既避免一次抢占全部资源,又不会让排队时间无限拉长。%A是主作业号,%a是子任务号,日志文件按子任务单独命名,很好排查哪个任务出了问题。
作业依赖适合有明确上下游关系的流程。比如要等前面那些参数扫描跑完后,再做聚合分析:
sbatch --dependency=afterok:123456 aggregate.shafterok表示只有 123456 号作业成功结束才启动后续任务。同理还有afterany(无论成功失败都跑)和afternotok(前序失败才跑)。依赖写错了也不会报错,只会让后面的作业一直 PENDING,Reason 里写着Dependency。
排队卡住的时候还有后悔药可用。发现--time写少了,作业还在 RUNNING,用scontrol update JobId=123456 TimeLimit=02:00:00直接改。改了脚本之后想重新开始,用scontrol requeue 123456把作业放回排队队列,下次调度会按新脚本重新执行。这两个命令在我日常工作中比重新提交整个作业省事得多,因为它们保留了作业号和历史记账记录,排查问题不至于乱成一团。
我现在的习惯是:每次提交前先sinfo -s确认分区,再scontrol show partition看一眼限制,最后检查脚本里有没有cd $SLURM_SUBMIT_DIR。这些动作加起来不到一分钟,但已经帮我避开了绝大部分低级错误。Slurm 的报错信息不一定直接告诉你问题在哪,但你愿意多花十秒去查状态、看 Reason,它其实把所有答案都摆在队列里了。希望帮到你。
本文还有配套的精品资源,点击获取