如何整活闲置超算

本文所述操作基于仅有 40 台双 EPYC 7H12 家用服务器组成的家用土超算,据咱分析,和主流超算差别应该不大(其实文中的例子更适合 Kubernetes 集群,至于为何用超算系统,老板喜欢╮(╯▽╰)╭。

# 超级计算机

超级计算机不是单台单个系统,而是由上千上万台高性能服务器(HPC)组成,每台服务器运行单独的操作系统,并作为一个计算节点,独立或协同工作。其设计难点在数据传输带宽和延迟,共享内存,并行存储,散热等方面。表面上看,计算节点是个服务器,可以 SSH 登录。节点间使用 InfiniBand 协议通信,共享 home 目录。

传统超算主要用于并行计算。实际使用时我们不可能亲自 SSH 上去部署服务,而是通过专用的任务调度工具。这里介绍的是 Slurm (Simple Linux Utility for Resource Management),一个开源超算资源管理软件,包括天河二号在内,在 TOP500 中占有率超过 50%。

# 食材

# Slurm

Slurm 用于管理计算资源,主要用于运行 Linux 的超算,简单介绍下部署任务时常用的三个命令。

  • salloc 先抢占资源,后部署任务。执行 salloc 命令后排队等待资源分配,排到后开始计费。此时可以使用 ssh 登录到计算节点,手动执行命令。当用户退出登录节点后任务自动终止。适合测试程序。
  • srun 交互式提交任务。任务提交后会在当前终端实时返回程序输出,程序退出自动释放资源。适合交互式单节点任务。
  • sbatch 批量提交任务。最常用的任务提交命令,将节点类型,任务数量,资源需求等参数和执行程序的命令写在 Bash 脚本中,在所有任务执行完成后释放资源,默认情况下,任何任务失败会终止全部任务并释放资源。适合提交并行或多步任务。

超算上运行的程序一般都会针对任务调度系统定制优化,比如将大规模优化问题通过数学方法分解成数个子问题,或者 FFT 之类的适合分解的算法,配合 Slurm 并行计算出结果再迭代解出原问题。

现代程序一般会有很多运行时依赖。在服务器上可以用 pacman / apt 安装,而超算出于安全和稳定的考虑,禁止用户获取 root 权限,无法自行安装应用。不过我们可以通过 environment modules 运行预装的程序。

# Modules

Modules 是一个 shell 初始化工具,modulefiles 用于会话期间修改运行环境。配合 sbatch 使用,可在 bash 脚本中动态加载依赖的软件/库。

查看并过滤可用软件。

module avail >> module.log 2>&1
cat module.log | grep gcc

gcc/4.4.7-kd
gcc/4.9.2-fenggl
gcc/7.3.0-wzm
gcc/8.1.0-wjl
gcc/8.3.0-wzm
gcc/9.1.0-fenggl
...

复制软件全称,载入到当前环境。

module load gcc/8.1.0-wjl

之后就能用 GCC 8.1 编译运行程序了。超算专用程序的依赖不多,可以一个个添加。但像咱这样的搬砖家写出来的缝合怪,若是没有包管理工具,没人能跑起来。不如换个思路,说起依赖问题,不知道大家有没有想到容器呢?

# Singularity

Singularity 是专为超算打造的容器,对性能的损耗可以忽略不计。支持 InfiniBand 和 GPU 等 PCIe 设备,兼容 Docker 镜像,也有 Kubernetes CRI 的官方支持。我们可以在本地安装 Singularity 打包程序和运行环境,再到超算上用 environment modules 加载 Singularity 运行程序镜像。

创建 singularity 定义文件 screenfetch.def,写入如下内容。

Bootstrap: docker
From: ubuntu:18.04

%help
    App: screenfetch
    Usage: singulaity exec <image name> <command>

%files
    /etc/apt/sources.list
    /etc/ssl/certs /etc/ssl/certs

%post
    apt-get update && apt-get install -y screenfetch
    rm -rf /var/lib/apt/lists/*

%environment
    export LC_ALL=C

%runscript
    exec screenfetch

Bootstrap 指定镜像源,From 指定镜像名,%help 是帮助信息,%files 是需要打包到镜像的文件(如不指定路径,则使用源文件相同路径)。

这个栗子中,我们从 Docker 源获取 Ubuntu 18.04 镜像,将本地的 apt 软件源和 ssl 证书拷贝到容器的相同位置,然后安装 screenfetch 并清理安装缓存,最后设置环境变量和启动容器时运行的脚本。

保存文件,使用如下命令编译镜像。

singularity build run.sif screenfetch.def

执行后,当前目录下多出的 run.sif 就是镜像本体了,这个镜像可以丢到官方仓库也可以直接运行。从镜像启动容器有多种方式,比如,使用 run 指令会运行 runscript 中的语句,而 exec 指令允许用户自定义容器运行的程序,比如。

singularity run run.sif
singularity exec run.sif screenfetch

以上两条命令返回的结果是相同的。如果你对 Dockerfile 比较熟悉,也可以打包 Docker 镜像,上传到 Docker Hub,然后使用 Singularity 运行 Docker 镜像,这样的镜像体积小很多,比如。

singularity exec docker://ubuntu:latest lsb_release -a

# 料理

那没有优化过的程序可以跑在超算上吗?就是那种不管加再多的核心,再多的内存都不会变快的垃圾代码?もちろんです,有了 bash 和容器,就算把博客挂到超算上也没问题呦!

# 多节点部署(相同参数)

此次测试的 Golang 程序采用微服务架构,根据功能拆分为主模块和子模块,一对多的关系,通信协议是 RESTful API。子模块部署在多台服务器上,相互之间没有依赖关系。使用 sbatch 的简单栗子就像下面这样。

#!/bin/sh
#SBATCH --job-name worker
#SBATCH --error=job-%j.err
#SBATCH --partition=amd
#SBATCH --nodes=3
#SBATCH --ntasks=3
#SBATCH --cpus-per-task=64
#SBATCH --no-kill=on

module load singularity/3.6

singularity exec bin.sif <command>

按需修改,保存到job-worker.sh中。配置中的 nodes 和 ntasks 保持一致,cpus-per-task 是单个节点的核心数,以保证每个节点运行一个程序(只设定 ntasks 也可以),另外我们指定 no-kill 参数,使得单一节点故障不会影响其他节点。此方法操作简单,只需执行sbatch job-worker.sh一条命令,~~但没办法修改任务的运行参数。~~在文档 (opens new window)中 filename pattern 的说明,通配符 %A 和 %a 代指作业编号和索引编号,有兴趣可以尝试下能否用在命令参数中。

# 多节点部署(不同参数)

为解决上述问题,我们将 nodes 和 ntasks 改为 1,这样 sbatch 脚本只会部署单个节点,然后通过外部脚本循环执行 sbatch 命令,在每次循环中修改 sbatch 脚本的命令参数,以此实现动态传参的效果。

  • sbatch 脚本
#!/bin/sh
#SBATCH --job-name worker
#SBATCH --error=job-%j.err
#SBATCH --partition=amd
#SBATCH --ntasks=1

module load singularity/3.6

ip=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1 -d '/')

singularity exec bin.sif <command> -i $ip -p port
  • 外部脚本
#!/bin/bash

set -o pipefail

for i in {23300..23333};
do
    sed -i 's/port/$i/g' job-worker.sh
    sbatch job-worker.sh
    echo "Task $i works."
done

在这个栗子中我们部署了 33 个任务,分别监听不同的端口(23300 ~ 23333)。

# 单节点部署

有些模块适合放在一起运行,它们会突发性的占用宿主机全部资源,但一般不会同时触发。接下来我们申请一个完整计算节点,尝试在单节点一次提交多个不同的程序。

想法是可行的,但 Slurm 好像没有这种方法。不过 sbatch 命令使用 bash 脚本,那我们用 bash 的一些方法也是可以的吧。

#!/bin/sh
#SBATCH --job-name worker
#SBATCH --error=job-%j.err
#SBATCH --partition=amd
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=64

module load singularity/3.6

nohup singularity exec bin.sif <module1> <command1> >> module1.log 2>&1 &
sleep 30s
nohup singularity exec bin.sif <module2> <command2> >> module2.log 2>&1 &
wait

后台运行了两个任务,其中 module2 依赖 module1,为确保 module1 已经启动,简单的间隔 30s 后再启动 module2。由于 sbatch 脚本执行完成后 Slurm 会认为任务结束并释放资源,所以最后通过 wait 指令,等待所有后台任务。

基于上述方法(主要是容器),咱们可以对超算为所欲为,至于代价嘛。hhh

超级计算机
食材
Slurm
Modules
Singularity
料理
多节点部署(相同参数)
多节点部署(不同参数)
单节点部署