SLURM:令人尴尬的并行程序中的令人尴尬的并行程序

SLURM: Embarrassingly parallel program inside an embarrassingly parallel program

我有一个用 Matlab 编写的复杂模型。该模型不是我们编写的,最好将其视为 "black box",即为了从内部解决相关问题,需要重写整个模型,这需要数年时间。

如果我遇到 "embarrassingly parallel" 问题,我可以使用数组提交相同模拟的 X 个变体,选项为 #SBATCH --array=1-X。但是,集群通常对最大数组大小有一个(令人沮丧的小)限制。

在使用 PBS/TORQUE 集群时,我通过强制 Matlab 在单个线程上 运行 来解决这个问题,请求多个 CPU,然后在 运行 中设置多个 Matlab 实例的背景。示例提交脚本是:

#!/bin/bash
<OTHER PBS COMMANDS>
#PBS -l nodes=1:ppn=5,walltime=30:00:00
#PBS -t 1-600

<GATHER DYNAMIC ARGUMENTS FOR MATLAB FUNCTION CALLS BASED ON ARRAY NUMBER>

# define Matlab options
options="-nodesktop -noFigureWindows -nosplash -singleCompThread"

for sub_job in {1..5}
do
    <GATHER DYNAMIC ARGUMENTS FOR MATLAB FUNCTION CALLS BASED ON LOOP NUMBER (i.e. sub_job)>
    matlab ${options} -r "run_model(${arg1}, ${arg2}, ..., ${argN}); exit" &
done
wait
<TIDY UP AND FINISH COMMANDS>

谁能帮我在 SLURM 集群上做同样的事情?

我不是数组作业方面的专家,但我可以帮助您处理内部循环。

我总是会使用 GNU parallel 到 运行 几个并行的串行进程,在一个有多个 CPU 可用的作业中。这是一个简单的 perl 脚本,因此 'install' 并不难,而且它的语法非常简单。它基本上做的是 运行 一些(嵌套的)并行循环。此循环的每次迭代都包含一个(长)过程,就像您的 Matlab 命令一样。与您的解决方案相比,它不会一次提交所有这些进程,但它 运行 仅同时 N 个进程(其中 N 是 CPU 的数量你有空)。一个完成后,立即提交下一个,依此类推,直到整个循环完成。并非所有进程都需要相同的时间,这很好,一旦一个 CPU 被释放,另一个进程就会启动。

然后,您想做的是启动 600 个作业(我在下面用 3 个代替,以显示完整的行为),每个作业有 5 CPUs。为此,您可以执行以下操作(因此我没有包括 matlab 的实际 运行,但可以包括在内):

#!/bin/bash
#SBATCH --job-name example
#SBATCH --out job.slurm.out
#SBATCH --nodes 1
#SBATCH --ntasks 1
#SBATCH --cpus-per-task 5
#SBATCH --mem 512
#SBATCH --time 30:00:00
#SBATCH --array 1-3

cmd="echo matlab array=${SLURM_ARRAY_TASK_ID}"

parallel --max-procs=${SLURM_CPUS_PER_TASK} "$cmd,subjob={1}; sleep 30" ::: {1..5}

使用以下方式提交此职位:

$ sbatch job.slurm

向队列提交了 3 个作业。例如:

$ squeue | grep tdegeus
         3395882_1     debug  example  tdegeus  R       0:01      1 c07
         3395882_2     debug  example  tdegeus  R       0:01      1 c07
         3395882_3     debug  example  tdegeus  R       0:01      1 c07

每个作业获得 5 CPUs。这些被 parallel 命令利用,并行地 运行 你的内部循环。再一次,这个内部循环的范围可能(远)大于 5,parallel 负责在这个作业中的 5 个可用 CPU 之间保持平衡。

让我们检查一下输出:

$ cat job.slurm.out

matlab array=2,subjob=1
matlab array=2,subjob=2
matlab array=2,subjob=3
matlab array=2,subjob=4
matlab array=2,subjob=5
matlab array=1,subjob=1
matlab array=3,subjob=1
matlab array=1,subjob=2
matlab array=1,subjob=3
matlab array=1,subjob=4
matlab array=3,subjob=2
matlab array=3,subjob=3
matlab array=1,subjob=5
matlab array=3,subjob=4
matlab array=3,subjob=5

您现在可以清楚地同时看到 3 x 5 个进程 运行(因为它们的输出是混合的)。

在这种情况下不需要使用 srun。 SLURM 将创造 3 个工作岗位。在每项工作中,一切都发生在单独的计算节点上(即,就好像您在自己的系统上 运行ning 一样)。


安装 GNU Parallel - 选项 1

将 'install' GNU 并行放入您的主文件夹中,例如 ~/opt

  1. Download the latest GNU Parallel.

  2. 创建目录 ~/opt 如果它还不存在

    mkdir $HOME/opt
    
  3. 'Install' GNU 并行:

    tar jxvf parallel-latest.tar.bz2
    cd parallel-XXXXXXXX
    ./configure --prefix=$HOME/opt
    make
    make install
    
  4. ~/opt 添加到您的路径:

    export PATH=$HOME/opt/bin:$PATH
    

    (要使其永久化,将该行添加到您的 ~/.bashrc。)


安装 GNU Parallel - 选项 2

使用conda.

  1. (可选)创建新环境

    conda create --name myenv
    
  2. 加载现有环境:

    conda activate myenv
    
  3. 安装 GNU 并行:

    conda install -c conda-forge parallel 
    

请注意,该命令仅在加载环境时可用。

虽然 Tom 关于使用 GNU Parallel 的建议很好,但我会尝试回答所提出的问题。

如果你想 运行 5 个具有相同参数的 matlab 命令实例(例如,如果它们通过 MPI 通信)那么你会想要 --ncpus-per-task=1 , --ntasks=5 并且您应该在 matlab 行前加上 srun 并摆脱循环。

在您的情况下,由于您对 matlab 的 5 次调用中的每一次都是独立的,因此您想请求 --ncpus-per-task=5--ntasks=1。这将确保您根据需要为每个作业分配 5 CPU 个核心。如果您愿意,可以在 matlab 行前加上 srun,但这不会有什么区别,您只是 运行 完成一项任务。

当然,只有当你的 5 matlab 运行 中的每一个都花费相同的时间时,这才是有效的,因为如果一个比其他 4 CPU 核心花费更长的时间将闲置,等待第五个完成。

您可以使用 python 和子流程来完成,在我下面描述的内容中,您只需设置节点和任务的数量即可,不需要数组,不需要匹配的大小数组到模拟次数等...它只会执行 python 代码直到完成,更多节点执行速度更快。

此外,由于一切都在 python 中准备(比 bash 更容易),因此更容易决定变量。

它假设 Matlab 脚本将输出保存到文件 - 此函数不返回任何内容(它可以更改..)

在 sbatch 脚本中,您需要添加如下内容:

#!/bin/bash
#SBATCH --output=out_cluster.log
#SBATCH --error=err_cluster.log
#SBATCH --time=8:00:00
#SBATCH --nodes=36
#SBATCH --exclusive
#SBATCH --cpus-per-task=2

export IPYTHONDIR="`pwd`/.ipython"
export IPYTHON_PROFILE=ipyparallel.${SLURM_JOBID}

whereis ipcontroller

sleep 3
echo "===== Beginning ipcontroller execution ======"
ipcontroller --init --ip='*' --nodb --profile=${IPYTHON_PROFILE} --ping=30000 & # --sqlitedb
echo "===== Finish ipcontroller execution ======"
sleep 15
srun ipengine --profile=${IPYTHON_PROFILE} --timeout=300 &
sleep 75
echo "===== Beginning python execution ======"

python run_simulations.py

根据您的系统,在此处阅读更多内容:https://ipyparallel.readthedocs.io/en/latest/process.html

和 run_simulations.py 应该包含这样的内容:

import os
from ipyparallel import Client
import sys
from tqdm import tqdm
import subprocess
from subprocess import PIPE
def run_sim(x):
    import os
    import subprocess
    from subprocess import PIPE
    
    # send job!
    params = [str(i) for i in x]
    p1 = subprocess.Popen(['matlab','-r',f'"run_model({x[0]},{x[1]})"'], env=dict(**os.environ))
    p1.wait()

    return

##load ipython parallel
rc = Client(profile=os.getenv('IPYTHON_PROFILE'))
print('Using ipyparallel with %d engines', len(rc))
lview = rc.load_balanced_view()
view = rc[:]
print('Using ipyparallel with %d engines', len(rc))
sys.stdout.flush()
map_function = lview.map_sync

to_send = []
#prepare variables  <-- here you should prepare the arguments for matlab
####################
for param_1 in [1,2,3,4]:
    for param_2 in [10,20,40]:
        to_send.append([param_1, param_2])



ind_raw_features = lview.map_async(run_sim,to_send)
all_results = []

print('Sending jobs');sys.stdout.flush()
for i in tqdm(ind_raw_features,file=sys.stdout):
    all_results.append(i)

您还会在标准输出中看到一个进度条,这很好...您还可以轻松地添加一个检查以查看输出文件是否存在并忽略 运行。