Apptainer 方式运行 intel 2016 编译出的程序

一、问题起源

系统从 CentOS7 升级到 Rocky8 后由于 intel 2016 版的 mpi 依赖的 glibc 与系统的 2.28 版的 ABI 不兼容,导致之前在 CentOS7 下编译出的 vasp 在 Rocky8 下 mpirun 运行时出错,一运行就报 *** buffer overflow detected *** 错误。虽然串行运行时正常,但是无法并行就相当于无法使用。这样就带来了两个问题:

  • intel 2016 版编译器与 Rocky8 的 glibc ABI 不兼容,而且所依赖的 gcc 头文件版本也不对,导致编译出错。并不是一定无法编译,而是当涉及与高版本不兼容的内容时会出错。比如编译一个简单的源文件时通常没什么问题,但是编译 vasp 就会出错!同样的设置在 CentOS7 下就能正常编译。可见在 Rocky8 下直接用 intel 2016 编译大型程序通常会遇到问题。
  • 即使是之前在 CentOS7 下编译好的程序,在 Rocky8 下 mpirun 并行运行时也会出错!

那么就彻底不能用了吗? 也不是!

二、容器

1. podman 容器中编译

上面第一个问题可通过 podman 容器解决,用 podman 创建一个 CentOS7 的容器,映射宿主机上的 intel 2016 及 vasp 源文件,然后在容器中编译。或者干脆在别的装有 CentOS7 的机器上用 intel 2016 编译好拷过来。总之,就是还是要在 CentOS7 下编译。如果不需要重新编译的话,那么已有的程序不用动。

2. apptainer 容器中运行

podman 容器更像 docker,虽然不需要后台服务程序,但它仍不方便在计算集群上供多人使用。因为文件权限、个人设置等等各种原因吧。而 apptainer (epel 源里就有)则是专门针对高性能计算集群用户量身定做的容器方案,比 podman 更轻量级且更透明。它不像 podman 或 docker 一样区分镜像与容器的概念,它只有镜像(当然叫容器也行,反正都一样),用户不能修改,只能直接调用。总之,apptainer 就是专门为特定程序提供运行环境打包而设计的

(1) 创建镜像

通常通过 .def 文件创建镜像,比如 centos7_base.def 内容如下:

Bootstrap: docker
From: centos:7

%post
    # 使用 USTC 科大 vault 源代替已失效的官方源
    sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-Base.repo
    sed -i 's|#baseurl=http://mirror.centos.org/centos/|baseurl=https://mirrors.ustc.edu.cn/centos-vault/|g' /etc/yum.repos.d/CentOS-Base.repo

    yum -y clean all
    yum -y makecache

    yum -y install \
        which tar gzip bzip2 \
        gcc gcc-c++ gcc-gfortran \
        numactl numactl-devel \
        environment-modules \
        openssh-clients

    # 用于挂载 /public
    mkdir -p /public

%environment
    export MODULEPATH=/public/software/modules
    source /usr/share/Modules/init/bash

然后用如下命令就可以创建镜像文件 centos7.sif

apptainer build centos7.sif centos7_base.def

有了镜像文件后普通用户就可以直接使用该镜像文件运行了,其中 xxxx 是用户想在容器里运行的命令,--bind 用于映射文件或目录,类似 docker 里的 -v。

apptainer exec --bind /public:/public centos7.sif xxxx

不过,上面创建镜像的过程会失败,因为他要到 docker-hub 上去下载,然而却连不上。好在 apptainer 可以直接导入 podman 或 docker 导出的镜像文件。Rocky 8 下可先用 podman 拉取镜像,然后再导出为 centos7.tar:

podman pull    quay.io/centos/centos:7
podman save -o centos7.tar centos:7

有了 centos7 的镜像,可以如下创建 apptainer 的镜像 centos7.sif

apptainer build centos7.sif docker-archive://centos7.tar

但是这样的 centos7.sif 里的内容是未经配置的,通常并符合我们的需求,但它又无法修改了。所以,最理想的办法是先创建一个 “沙箱” 镜像,其实就是一个目录,用于存放镜像的文件:

apptainer build --sandbox centos7_sbox docker-archive://centos7.tar

然后就有了一个 centos7_sbox 目录。此时可以通过如下命令进入该沙箱,对其进行修改(需加 --writable 选项)。

apptainer shell --writable centos7_sbox

执行上述命令后会进入一个以 Apptainer> 开头的 shell 中,这其实就是 centos7_sbox 所代表的沙箱容器(或镜像)的 shell。先修改 yum 源,改为科大的 http://mirrors.ustc.edu.cn/centos-vault,这样就可以 yum 安装所需程序了,比如 gcc gcc-c++ gcc-gfortran environment-modules openssh-clients 等。然后设置 module,在 /etc/profile.d 目录下创建 module.sh 文件,内容如下(此处需根据自己实际目录调整):

export MODULEPATH=/public/software/modules
source /usr/share/Modules/init/bash

再装一些可能用到的程序就基本差不多了。需注意:对于镜像中本没有的路径,apptainer 会自动映射宿主机的路径!比如,Apptainer> 下的 /root 路径就是我真实宿主机的 /root!所以千万不要不小心删了宿主机的东西!沙箱镜像设置完后就可以退出了,然后就可根据此沙箱镜像创建 .sif 只读镜像了:

apptainer build centos7.sif centos7_sbox

(2) 运行镜像

把 centos7.sif 放到一个普通用户能访问的地方,比如 /public/apptainers/,然后普通用户就可以运行容器中的命令了,比如运行 lsb_release -a 命令会输出 CentOS7 信息(前提是在配置沙箱时安装了 redhat-lsb-core 包)

apptainer exec --bind /public:/public  /public/apptainers/centos7.sif  lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 7.9.2009 (Core)
Release:    7.9.2009
Codename:   Core

需注意的是,若运行的命令是 uname -a 则显示的是宿主机的内核信息,这一点和 podman 以及 docker 等是一样的。有一点需要注意,虽然 moudle 已经安装并配置好了,但如果直接运行

apptainer exec --bind /public:/public  /public/apptainers/centos7.sif  module list

会提示 FATAL: "module": executable file not found in $PATH 错误,这是因为 apptainer exec 进入的是一个 非登录的 shell,并不会触发加载 /etc/profile.d 下的脚本,需使用 bash -l 主动登录才行,并将运行的命令以 -c 参数传入,所以应该如下使用:

apptainer exec --bind /public:/public  /public/apptainers/centos7.sif  bash -lc "module list"
Currently Loaded Modulefiles:
  1) compiler/intel/2016u3   2) apps/vasp/5.4.4-i16 

它会自动从宿主机上我的用户目录下读取 ~/.bashrc 设置,容器中的工作目录就是宿主机里的当前工作目录。所以将上述命令中的 "module list" 直接替换为 "mpirun -n 16 vasp_std" 它运行的就是 intel 2016 版在 CentOS7 下编译出的 vasp_std 文件,可以正常运行!为了便于使用,将 apptainer 命令封装在 runi16 脚本里,内容如下(这样运行有问题,见第(3)部分):

#!/bin/bash
apptainer exec --bind /public:/public /public/apptainers/centos7.sif bash -lc "$*"

在每个节点上都装上 apptainer,把 runi16 拷到每个节点的 /usr/local/bin 下,就可以直接通过 runi16 vasp_std 直接通过容器提供的 CentOS7 环境来运行之前的 intel 2016 编译的 vasp_std 了。如果是在脚本中提交并行任务,只需如下运行即可

mpirun -n 64 runi16 vasp_std   #核数根据实际情况调整

注意需使用 mpirun(实际调用的是 mpiexec.hydra,不要使用基于 mpd 的 mpiexec,也不要使用 slurm 的 srun,它用的应该是 mpiexec,会提示 mpd 进程未运行等问题)。然后我到一个计算节点上去运行试一下,结果发现提示如下错误:

INFO: Cleanup error: while unmounting /var/lib/apptainer/mnt/session/final directory: 
      no such file or directory, while unmounting /var/lib/apptainer/mnt/session/rootfs directory: no such file or directory 
FATAL: container creation failed: open /etc/resolv.conf: no such file or directory

原来是因为计算节点上没有 /etc/resolv.conf 文件,apptainer 会自动挂载宿主机的该文件,若宿主机没有则报此错误。解决办法也很简单,用 root touch 一个空的 /etc/resolv.conf 即可。然后再运行就正常!

至此,通过 apptainer 部署 CentOS7 运行环境的容器完美运行之前 CentOS7 遗留下来的由 intel 2016 编译的程序。用户只需在相应程序前用 runi16 脚本封装一下来运行即可,不用关心容器的细节!

(3) 并不完美

明明早上已成功运行 mpirun -n 64 runi16 vasp_std,而且还提交了任务试了一下,成功算完。但是,下午再试同样的命令又不能运行了。此时不论是上面命令还是 runi16 mpirun -n 64 vasp_std 都报同样的 buffer overflow 错误,显然是又出现了 glibc ABI 不兼容的问题!可我明明已经用了 apptainer 容器了啊!系统设置也没有变!到了晚上,莫名其妙的 runi16 mpirun -n 64 vasp_std 又能运行了!看来这样运行并不稳定!问 AI,它说:

Apptainer 采用 “集成而非隔离” 的理念,默认情况下,容器内的应用程序会直接使用宿主机系统的动态链接库,包括GLIBC。这是 Apptainer 针对高性能计算(HPC)环境的一项核心设计特性。Apptainer 容器的设计使其默认优先使用宿主机的 GLIBC,这带来了出色的性能和兼容性,但也需要注意宿主机与容器内软件对GLIBC版本的依赖关系。

由此可见,默认情况下 apptainer 并不能很好的隔离 glibc。但奇怪的是为啥早上的时候能运行?是环境变量变了还是什么变了?而且一阵好一阵坏的,不得而知!

我的目的就是要隔离宿主机的 gblic,好在 apptainer 提供了 --contailall 选项,它是一个非常有用的安全与隔离选项,它能为 apptainer 容器运行环境提供一个最高级别的隔离

功能类别 具体控制项 默认情况 (无--containall) 使用 --containall
文件系统挂载 自动挂载宿主机的$HOME/tmp/proc等目录 自动挂载 禁止自动挂载
环境变量 继承宿主机的环境变量 全部继承 不继承
临时文件系统 在容器内挂载/tmp/var/tmp 使用宿主机的目录 使用容器内隔离的临时目录

使用 --containall 参数后可以屏蔽宿主机环境变量可能带来的干扰,让容器的运行环境更加纯净和一致,完全使用容器内的 gblic 而与宿主机无关,这样才能解决我的问题。不过,使用该参数后默认不会保持工作目录,需要手动传递工作目录,此时将 runi16 脚本内容改为如下:

#!/bin/bash
apptainer exec --containall --pwd="$(pwd)" --bind /public:/public /public/apptainers/centos7.sif bash -lc "$*"

而且,即使这样修改后依然无法 mpirun -n 64 runi16 vasp_std 这样计算,这样用时虽然不会报 buffer overflow 错误了,但是无法正常并行,看到的是四个独立的 vasp_std 进程在计算,这就是完全隔离所需的代价,系统内外的消息传递、网络模式、内存共享等等无法保持一致。此时只能在容器内部进行 mpi 并行, 即 runi16 mpirun -n 64 vasp_std,这样带来的一个限制就是 只能单个节点并行,不能跨节点并行! 不过好在单个节点核数也不少,通常情况下也够用了。

由于使用了 --containall 后,宿主机环境变量无法直接传入容器中,若没有在 ~/.bashrc 中设置 module,在脚本中 runi16 之前的设置将不管用。此外,即使 ~/.bashrc 中有设置,但与所需设置冲突也会失效。最保险的办法是在运行 runi16 时清除原有设置并重新设置,即:

runi16 "
     module purge
     module load xxx  yyy
     mpirun -n 64 vasp_std   # 或把核数替换成自动的 $SLURM_NTASKS_PER_NODE
"

上面 runi16 运行的命令可以放在一行并以 ; 分隔,也可以使用 SLURM 的环境变量,但前提是两边得用双引号 ",这样在传给 runi16 时就会替换为实际值;若用单引号 ' 则直接将 $SLURM_NTASKS_PER_NODE 以字面形式传给 runi16, 而 runi16 的容器内部是没有 slurm 的,这些变量的值就是空的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容