三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从单机到集群:Slurm作业调度实战指南与核心命令解析

从单机到集群:Slurm作业调度实战指南与核心命令解析

1. 从“单机脚本”到“集群任务”:为什么我们需要Slurm?

如果你还在用nohup python train.py &或者screen来跑一个需要几天的深度学习训练任务,然后把电脑锁屏,祈祷它不要中途崩溃,那么是时候认识一下 Slurm 了。这不仅仅是一个“命令集”,而是一套完整的分布式工作负载管理器。简单来说,它把你手头那几台、几十台甚至上千台服务器,从一堆独立的“计算器”,变成了一个可以统一调度、排队、管理的“超级大脑”。

我第一次接触 Slurm 是在一个高性能计算(HPC)集群上,当时我需要跑一个需要 128 个 CPU 核心、512GB 内存的流体模拟。我本能地想写个脚本用 SSH 把任务分发到各个节点,但立刻被管理员制止了。他告诉我:“用 Slurm 提交,告诉它你要什么资源,它会帮你排队、分配、运行,并在结束后通知你。” 那一刻我意识到,管理集群资源和个人电脑的思维模式完全不同。个人电脑是“独占”的,而集群资源是“共享”的。Slurm 就是那个确保共享公平、高效、有序的“交通警察”和“资源分配器”。

它的核心价值在于资源抽象与任务调度。你不需要关心你的任务具体跑在哪台机器的哪个核心上,你只需要用一套简单的命令,声明你的任务需要多少 CPU、多少 GPU、多少内存、跑多久。Slurm 会根据整个集群的负载情况、队列策略、优先级,在合适的时机把你的任务调度到合适的节点上去执行。这对于从单机开发转向集群计算的工程师和研究员来说,是必须跨越的一道坎。本文的目的,就是帮你把这道坎变成一条清晰、可执行的操作路径,让你能像在本地运行脚本一样,从容地在集群上提交和管理大规模计算任务。

2. 作业提交:你的第一个Slurm脚本长什么样?

提交作业是使用 Slurm 的起点。最核心的命令是sbatch,但它需要一个“剧本”,也就是 Slurm 作业脚本。这个脚本本质上是一个 Bash Shell 脚本,只是在开头加上了一些以#SBATCH开头的特殊注释,用于向 Slurm 申请资源。

2.1 解剖一个标准的作业脚本

让我们从一个最经典的例子开始:提交一个 Python 计算任务。假设我们有一个计算圆周率的脚本pi.py

#!/bin/bash #SBATCH --job-name=pi_calculation # 作业名,在队列中显示 #SBATCH --output=slurm-%j.out # 标准输出重定向到文件,%j会被替换为作业ID #SBATCH --error=slurm-%j.err # 标准错误重定向到文件 #SBATCH --partition=gpu # 指定分区(队列),例如 gpu, cpu, debug #SBATCH --nodes=1 # 申请1个计算节点 #SBATCH --ntasks-per-node=1 # 每个节点上运行1个任务(进程) #SBATCH --cpus-per-task=4 # 每个任务分配4个CPU核心 #SBATCH --mem=8G # 每个节点申请8GB内存 #SBATCH --time=01:00:00 # 作业最大运行时间(时:分:秒),超时会被强制终止 #SBATCH --gres=gpu:1 # 申请1块GPU卡(如果分区支持) # 加载必要的环境模块(取决于集群配置) module load cuda/11.3 module load python/3.9 # 这里是你的实际任务命令 echo "Starting calculation on host: $(hostname)" echo "Using GPU: $CUDA_VISIBLE_DEVICES" python pi.py

逐行解读与避坑指南:

  1. #!/bin/bash: 必须的 Shebang,指明解释器。
  2. #SBATCH指令: 这是关键。所有给 Slurm 调度器的指令都写在这里。必须放在所有可执行命令之前,否则会被当作普通注释忽略。
  3. --job-name: 给你的作业起个名字。在查询时,一个好名字能帮你快速定位。避免使用默认的“bash”。
  4. --output/--error极其重要。指定输出和错误日志文件。%j是作业 ID 的占位符,能保证每次作业的日志不会互相覆盖。如果不指定,输出默认会放在一个名为slurm-<jobid>.out的文件中,但明确指定是好习惯。坑点: 确保你指定的路径有写入权限,否则作业会因无法写日志而失败。
  5. --partition: 指定作业提交到哪个分区(Partition)。分区是集群管理员根据硬件(如 GPU 节点、大内存节点)或用途(如调试、长时作业)划分的逻辑队列。你必须知道你的集群有哪些分区,可以用sinfo命令查看。提交到错误的分区会导致作业一直排队(Pending)或直接失败。
  6. --nodes, --ntasks-per-node, --cpus-per-task: 这是资源申请的核心组合,容易混淆。
    • --nodes=N: 我需要 N 个完整的计算节点。
    • --ntasks-per-node=M: 在每个节点上,我要运行 M 个独立的进程(MPI 任务通常这样用)。
    • --cpus-per-task=C: 为每个任务(进程)分配 C 个 CPU 核心。如果你跑的是 OpenMP 或单纯的多线程程序,这个参数决定了线程数。
    • 常见场景
      • 单节点单进程多线程任务--nodes=1 --ntasks-per-node=1 --cpus-per-task=8。你的程序会在一个节点上启动一个进程,该进程可以使用 8 个 CPU 核心。
      • 单节点多进程任务(如 Python multiprocessing)--nodes=1 --ntasks-per-node=4 --cpus-per-task=2。这会启动 4 个进程,每个进程分配 2 个核心,总共占用 8 个核心。注意: Slurm 会为每个任务分配独立的核心,确保不冲突。
  7. --mem: 申请的内存总量。注意,这是每个节点申请的内存,不是每个任务。如果你申请了--nodes=2 --mem=8G,那么总共会申请 16GB 内存(2节点 * 8G)。大坑: 很多作业失败是因为内存申请不足,被系统 OOM(Out Of Memory)杀死。建议根据程序实际需求稍多申请一些(例如,预估需要6G,就申请8G)。可以用sacct命令查看已结束作业的实际内存使用量来校准。
  8. --time作业的生命线。必须准确预估。Slurm 用它来做调度决策(短作业可能优先)。如果作业超时,会被无情地SIGKILL。对于不确定的任务,可以申请一个较长时间,但注意,有些分区对作业时长有限制。调试时可以用debug分区(通常限时15-30分钟)。
  9. --gres: 申请通用资源,最常见的就是gpu--gres=gpu:2表示申请 2 块 GPU。有些集群还细分类型,如--gres=gpu:v100:1
  10. module load: 在 HPC 集群中,软件环境通常通过 Environment Modules 管理。你需要加载任务所需的编译器、库、软件包。这也是常见失败点:脚本在本地能跑,在集群上失败,往往是缺少对应的模块。用module avail查看可用模块。
  11. 任务命令: 最后,写你真正要执行的命令。环境变量如$CUDA_VISIBLE_DEVICES会被 Slurm 自动设置,你的程序直接读取即可获得可用的 GPU 编号。

2.2 提交作业与立即执行的技巧

写好脚本(比如叫submit.slurm)后,使用sbatch命令提交:

sbatch submit.slurm

提交成功后,会返回一个作业 ID:Submitted batch job 123456

几个实用技巧和高级用法:

  • 依赖作业: 作业 B 必须等作业 A 完成后再开始。

    # 先提交作业A jobid_a=$(sbatch --parsable job_a.slurm) # 作业B依赖作业A的成功完成 sbatch --dependency=afterok:$jobid_a job_b.slurm

    afterok表示 A 成功(状态为 COMPLETED)后才运行 B。还有afternotok(失败后运行)、afterany(结束后无论成功失败都运行)。

  • 数组作业: 如果你要跑 100 个参数类似的独立任务(例如不同的随机种子),用数组作业效率最高,而不是提交 100 个独立作业。

    #SBATCH --array=1-100%10 # 提交ID从1到100的数组作业,%10表示最多同时运行10个

    在脚本中,可以用环境变量$SLURM_ARRAY_TASK_ID来获取当前任务的索引,并据此调整参数。

    python train.py --seed $SLURM_ARRAY_TASK_ID
  • 交互式作业: 用于调试、开发或需要实时交互的场景。这相当于申请一个临时节点供你登录使用。

    # 申请一个节点,1个任务,4个CPU,8G内存,1块GPU,用时1小时 srun --partition=gpu --nodes=1 --ntasks-per-node=1 --cpus-per-task=4 --mem=8G --gres=gpu:1 --time=01:00:00 --pty /bin/bash

    命令执行后,如果你的作业开始运行,你会直接登录到分配的计算节点上,就像 SSH 过去一样。退出 bash shell,作业即结束。

3. 作业查询与监控:你的任务跑到哪一步了?

作业提交后,就进入了调度队列。掌握查询和监控命令,是高效利用集群、排查问题的基础。

3.1 核心查询命令squeue

squeue是查看作业状态的瑞士军刀。最常用的命令是:

squeue -u $USER # 查看自己所有作业的状态

输出类似:

JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON) 123456 gpu pi_calcula alice R 0:05 1 gpu-node-03 123455 cpu data_prep alice PD 0:00 1 (Resources)

关键列解析:

  • ST(状态): 这是最重要的信息。
    • R (RUNNING): 正在运行。
    • PD (PENDING): 排队中。括号里会给出原因(REASON)
    • 常见 PENDING 原因
      • (Resources): 等待所需资源(如GPU)空闲。
      • (Priority): 有更高优先级的作业在前面。
      • (Dependency): 在等待依赖的作业完成。
      • (PartitionTimeLimit): 作业申请的时间超过了分区的最大限制。
  • TIME: 作业已运行时间。
  • NODELIST(REASON): 对于运行中的作业,显示运行的节点;对于排队中的作业,显示排队原因。

高级过滤与格式化:

# 查看所有正在运行(R)的作业 squeue -t RUNNING # 查看特定作业ID的详细信息 squeue -j 123456 # 自定义输出格式,例如只显示作业ID、名称、状态、节点和原因 squeue -u $USER -o "%.10i %.20j %.10T %.20R %.20E"

3.2 历史作业查询sacct

squeue只看当前和未完成的作业。对于已经结束(无论成功失败)的作业,需要用sacct来查看。这是事后分析作业表现(如实际内存消耗、CPU效率)的利器。

# 查看今天的所有作业 sacct --format=JobID,JobName,Partition,State,ExitCode,Elapsed,MaxRSS,AllocCPUS,NodeList -S today # 查看特定作业的详细信息 sacct -j 123456 --format=JobID,JobName,State,ExitCode,Elapsed,MaxRSS,ReqMem,AllocTRES -l

关键字段解析:

  • State: 最终状态,COMPLETED(成功),FAILED(失败),CANCELLED(被取消),TIMEOUT(超时),OUT_OF_MEMORY(内存不足)等。
  • ExitCode: 作业退出码。0:0通常表示成功。非零值表示失败,例如1:0是你的程序返回了1。
  • MaxRSS实际最大常驻内存。这是你调整--mem参数最重要的参考。如果MaxRSS接近你申请的ReqMem,说明申请得刚刚好;如果远小于,说明申请多了,浪费了资源;如果作业因OUT_OF_MEMORY失败,而MaxRSS接近申请值,说明你需要申请更多内存。
  • AllocTRES: 分配的资源详情,可以看到具体分配了哪些 GPU。

一个实战排查案例:你的一个训练作业FAILED了。首先用sacct -j <jobid>查看ExitCodeState。如果StateOUT_OF_MEMORY,那么基本确定是内存爆了。接着去看错误日志文件(slurm-<jobid>.err),通常会有Killed信息。然后,用sacct查看该作业的MaxRSS,对比你申请的--mem,就能确认问题。下次提交时,将--mem申请量提高到MaxRSS的 1.2-1.5 倍。

3.3 节点与分区状态查询sinfo

在提交作业前,或者作业一直排队时,可以用sinfo看看集群的整体状况。

# 查看所有分区状态 sinfo # 更清晰的格式化输出 sinfo -o "%20P %5D %14F %8z %10m %10d %10c %10O %E" # 分别显示分区、节点数、各状态节点数、空闲内存等

输出会显示每个分区有哪些节点,节点是idle(空闲)、alloc(已分配)、mix(部分占用)还是down(宕机)。如果你的作业需要的资源(比如特定型号的 GPU)在所有节点上都处于alloc状态,那自然就会一直PENDING

4. 作业控制:修改、取消与挂起

计划赶不上变化,作业提交后可能需要调整。

4.1 修改排队中的作业参数scontrol

作业一旦开始运行(RUNNING),很多参数就不能改了。但对于还在排队(PENDING)的作业,你可以用scontrol命令进行修改。这个命令功能强大,但需要管理员权限的操作我们这里不讨论。

# 查看作业123456的详细配置 scontrol show job 123456 # 修改排队作业的时间限制(比如发现预估太短) scontrol update jobid=123456 TimeLimit=2-00:00:00 # 改为2天 # 修改排队作业的优先级(需要有相应权限,通常用户不能修改) # scontrol update jobid=123456 Priority=1000 # 修改作业的依赖关系 scontrol update jobid=123456 Dependency=afterany:123455

重要限制: 无法修改正在运行作业的核心资源需求,如--mem--cpus-per-task--gres。这些必须在提交前确定好。能修改的主要是TimeLimitJobNameDependency等非核心调度参数。

4.2 取消作业scancel

这是最常用的控制命令之一。

# 取消单个作业 scancel 123456 # 取消自己所有作业(危险!请确认) scancel -u $USER # 取消自己所有处于排队状态的作业 scancel -t PENDING -u $USER # 取消一个数组作业的所有任务 scancel 123456_* # 假设123456是数组作业ID # 取消数组作业的特定任务 scancel 123456_[1,3,5] # 取消第1,3,5个任务

4.3 挂起与恢复作业scontrol hold/release

有时你可能想让一个排队中的作业暂时不要被调度,但又不想取消它(比如等待一个关键数据文件)。这时可以用挂起。

# 挂起作业123456(状态会变为 PD,Reason显示为 JobHeldUser) scontrol hold 123456 # 释放(恢复)被挂起的作业 scontrol release 123456

5. 深入原理:Slurm如何调度你的作业?

理解了基本命令,我们稍微深入一点,看看 Slurm 背后是怎么工作的。这能帮你更好地编写脚本和排查问题。

5.1 资源请求与分配的逻辑

当你提交一个作业脚本时,Slurm 的slurmctld(中央管理守护进程)会解析你的#SBATCH指令,生成一个资源需求“模板”。这个模板被放入对应分区的队列中。

调度器(例如,默认的backfill调度插件)会持续扫描所有节点slurmd(节点守护进程)报告的状态(空闲 CPU、内存、GPU 等),并尝试将队列中的作业“匹配”到节点上。匹配的原则包括:

  1. 资源满足: 节点必须有足够的 CPU、内存、GPU 等。
  2. 时间满足: 作业的TimeLimit必须在节点/分区允许的范围内。
  3. 优先级策略: 集群通常会配置一套复杂的优先级公式(考虑作业大小、用户公平份额、等待时间等)来决定谁先谁后。
  4. 回填调度: 这是提高资源利用率的关键。假设一个大作业需要 10 个节点跑 24 小时,目前只有 8 个节点空闲,它就需要等待。但此时如果来了一个只需要 2 个节点跑 1 小时的小作业,调度器不会傻等,而是会“回填”这个小作业先运行,充分利用资源。

5.2 环境变量与任务执行

当作业被分配到节点上开始运行时,Slurm 会为你的任务进程设置一系列环境变量,你的程序可以通过它们来感知运行环境:

  • SLURM_JOB_ID: 作业 ID。
  • SLURM_JOB_NODELIST/SLURM_JOB_CPUS_PER_NODE: 分配的节点列表和每个节点的 CPU 数。对于多节点作业,程序需要自己解析这个变量来进行进程间通信(如 MPI)。
  • SLURM_PROCID: 当前任务的进程 ID(在 MPI 作业中很重要)。
  • SLURM_LOCALID: 节点内的本地任务 ID。
  • CUDA_VISIBLE_DEVICES对于 GPU 作业至关重要。Slurm 会将它设置为分配给该作业的 GPU 在物理节点上的本地索引。你的 CUDA 程序应该读取这个变量,而不是试图使用所有 GPU。

5.3 常见失败场景与根因分析

结合sacct和日志文件,我们可以快速定位问题:

  1. 作业状态FAILED,退出码非零

    • 首先看错误日志(.err 文件): 里面通常有 Python 的 Traceback、C++ 的 core dump 信息、找不到文件等直接错误。
    • 检查sacct中的ExitCode: 例如137通常代表SIGKILL(被系统杀死),可能是内存超限(OOM)或超时。
  2. 作业状态OUT_OF_MEMORY

    • 根因: 作业实际内存消耗(MaxRSS)超过了申请值(ReqMem)。
    • 排查: 用sacct对比两者。优化程序内存使用,或增加--mem申请量。注意,如果你用了--mem-per-cpu,计算的是mem-per-cpu * cpus-per-task
  3. 作业状态TIMEOUT

    • 根因: 运行时间超过--time限制。
    • 排查: 程序本身运行慢?还是因为排队导致实际计算时间不足?增加--time,或者优化程序性能。对于长时间作业,可以考虑使用检查点(Checkpoint)机制,让程序能从中断处恢复。
  4. 作业一直PENDING,Reason 为(Resources)

    • 根因: 集群当前没有满足你资源需求的节点。
    • 排查: 用sinfo查看目标分区的资源状态。你是否申请了过于特殊的资源(如特定型号的 GPU、超大的内存)?是否可以调整资源需求(如减少节点数、缩短时间)以更快获得调度?
  5. 作业运行节点上的程序报错“找不到模块”或“命令不存在”

    • 根因: 计算节点的环境与登录节点不同。
    • 排查: 确保作业脚本中通过module load加载了所有必要的软件依赖。永远不要假设计算节点上有你需要的软件或环境变量。对于 Python,强烈建议使用 Conda 虚拟环境,并在作业脚本中source activate你的环境,或者使用--export=ALL参数传递环境变量(但需谨慎)。

掌握这些命令和背后的原理,你就能从“集群小白”成长为可以自主提交、监控、调试和优化大规模计算任务的“集群用户”。Slurm 的命令行接口虽然丰富,但日常使用的核心就是sbatchsqueuescancelsacct这几个。花点时间理解它们,你的科研或工程计算效率将会得到质的提升。

← 返回列表