【问题标题】:SLURM: Embarrassingly parallel program inside an embarrassingly parallel programSLURM:令人尴尬的并行程序中的令人尴尬的并行程序
【发布时间】:2018-03-15 10:57:35
【问题描述】:

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

如果我有一个“令人尴尬的并行”问题,我可以使用一个数组来提交同一个模拟的 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 集群上做同样的事情?

  • par 函数不会在 Matlab 的并行循环中运行我的模型。
  • PBS/TORQUE 语言非常直观,但 SLURM 让我感到困惑。假设我的 PBS 示例具有类似结构的提交脚本,这就是我认为某些命令会导致的结果。
    • --ncpus-per-task=5 对我来说似乎是最明显的一个。我是将 srun 放在循环中的 matlab 命令前面还是将其保留在 PBS 脚本循环中?
    • --ntasks=5 我想会请求 5 个 CPU,但会以串行方式运行,除非程序特别请求它们(即 MPI 或 Python 多线程等)。在这种情况下,我需要将 srun 放在 Matlab 命令前面吗?

【问题讨论】:

    标签: bash matlab slurm


    【解决方案1】:

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

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

    然后,您想做的是启动 600 个作业(我在下面替换 3 个,以显示完整的行为),每个作业有 5 个 CPU。为此,您可以执行以下操作(因此,我没有包括 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 个 CPU。 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 次 5 进程同时运行(因为它们的输出是混合的)。

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


    安装 GNU Parallel - 选项 1

    将 GNU 并行“安装”到您的主文件夹中,例如 ~/opt

    1. Download the latest GNU Parallel.

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

      mkdir $HOME/opt
      
    3. “安装”GNU Parallel:

      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 
      

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

    【讨论】:

    • 非常感谢汤姆的帮助。您的第二段有点令人困惑,我想我知道您的意思,但是如果您可以为了清楚起见重新措辞,那会很棒吗?这是解决问题的好方法,但我暂时将其作为备用选项,因为我真的很想知道如何在没有其他程序的情况下做到这一点,因此希望其他人能提出建议。
    • @ojunk 我试着编辑了一下,我希望它更清楚。不明白的地方能具体一点吗?
    【解决方案2】:

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

    如果您想使用相同的参数运行 5 个 matlab 命令实例(例如,如果它们通过 MPI 进行通信),那么您需要询问 --ncpus-per-task=1--ntasks=5,并且您应该在 @ 987654324@ 与srun 对齐并摆脱循环。

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

    当然,这只有在你的 5 个 matlab 运行中的每一个都花费相同的时间时才有效,因为如果一个需要更长的时间,那么其他 4 个 CPU 内核将处于空闲状态,等待第五个完成。

    【讨论】:

    • 在我的编辑中删除一些类型-o 我不小心在---ncpus-per-task=1 中引入了太多-,也许你可以重新修复它。对不起。
    • @Milliams 您能否确认在--ncpus-per-task=5--ntasks=1 的情况下您在后台运行循环中的作业(即在行尾使用&)?
    【解决方案3】:

    你可以用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)
    

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

    【讨论】: