MPI Apps with Singularity and Docker

Running MPI applications in Singularity and Docker containers.

The Message Passing Interface (MPI) is one of the frameworks used in high-performance computing (HPC) for a wide variety of parallel computing architectures. It can be used for running applications on multicore systems and on multicore/multinode systems, where you map a “process” to a processing unit, typically a CPU core. MPI has been around for many years and is used by a great number of applications to scale, in terms of performance.

Classically, the way to run an MPI application is something like:

$ mpirun -np 16 --hostfile  ./ []

Note that this command does not really correspond to a specific MPI implementation; rather, it is meant to illustrate the basics of most MPI implementations.

The mpirun command launches and coordinates the application (<application>), the -np option is the number of processes to be used, and --hostfile names a simple text file (<list_of_hosts>)that has a list of hostnames (servers) to be used in running the application.

With the development of containers and their use in HPC and artificial intelligence (AI), the challenge becomes how to run MPI applications that are in the container. In this article, I show an approach to accomplishing this goal.

Test Software

I'm sure a number of people have come up with several ways to run MPI applications in containers. The methods presented in this article are the best ways I’ve found.

Although these tests are not performance tests that require detailed specifications about the hardware and software, I offer a brief description. The system ran Linux Mint 19.3 and used the latest PGI Community Edition compilers, version 19.10. Open MPI 3.1.3, which came prebuilt with the PGI compilers, was used in the tests.

The Docker-CE (Community Edition) v19.03.8 build afacb8b7f0, Ubuntu version (on which Linux Mint is based), was downloaded. For Singularity, the latest version as of the writing of this article, 3.5.3, was built and installed. It uses Go 1.13, which was installed from binaries on the golang website.

The simple MPI used is the classic 2D Poisson equation solver, poisson_mpi, which I procured from an online set of Fortran 90 examples.

HPCCM

HPC Container Maker (HPCCM) was used to create the specification files for creating the container images. It was installed in the Anaconda distribution (Python plus R programming language), which included conda (a package and environment manager) version 4.8.2 and Python 3.7. HPCCM version 20.2.0 was installed (the latest as of the writing of this article).

HPCCM was discussed in a previous HPC article. The tool, written in Python, is very easy to use and allows you to specify a basic description of how you want your container built. This HPCCM “recipe” can then create a Dockerfile for Docker or a singularity description file for Singularity, which you can modify. These files are then used to create the container images. For this example, the same HPCCM recipe is used for both Docker and Singularity.

Creating Container Specification Files

The first step in creating the container specification file is to create the HPCCM recipe, for which you can find the instructions in the HPCCM tutorial. The recipe file used in this article (Listing 1) also can be found in that tutorial.

Listing 1: poisson.py

import hpccm
 
Stage0 += baseimage(image='ubuntu:18.04')
Stage0 += pgi(eula=True, mpi=True)
 
Stage0 += copy(src='poisson_mpi.f90', dest='/var/tmp/poisson_mpi.f90')
Stage0 += shell(commands=['mpif90 /var/tmp/poisson_mpi.f90 -o /usr/local/bin/poisson_mpi'])

From the recipe, you can see that Ubuntu 18.04 is the base image and that the PGI compilers, version 19.10, are installed. Note that the PGI compilers require you to accept the end-user license agreement (EULA). (If you install the compilers on your home system, for example, you are presented the EULA to read and accept.) Also notice that I’m installing the prebuilt MPI libraries that come with the compiler.

The next line in the recipe copies the application source code into the image, and the last line is a shell directive to compile the code and put the binary in /usr/local/bin. The location of the binary was chosen arbitrarily.

Taking the poisson.py recipe in Listing 1, you can create a Singularity definition file or Dockerfile with just one command:

$ hpccm --recipe poisson.py --format singularity > Singularity.def
$ hpccm --recipe poisson.py --format docker > Dockerfile

After the specification files are created, be sure to examine them, even if you aren’t modifying anything. Alternatively, if you remove the redirection to an output file, HPCCM prints the specification file to the screen.

For Singularity, I made a small modification to the recipe file because I wanted to try pulling the PGI compilers from the local host rather than over the Internet (Listing 2). The resulting Singularity definition file is shown in Listing 3, and the Dockerfile is shown in Listing 4.

Listing 2: Modified poisson.py File

import hpccm
 
Stage0 += baseimage(image='ubuntu:18.04')
Stage0 += pgi(eula=True, tarball='/home/laytonjb/pgilinux-2019-1910-x86-64.tar.gz', mpi=True)
 
Stage0 += copy(src='poisson_mpi.f90', dest='/var/tmp/poisson_mpi.f90')
Stage0 += shell(commands=['mpif90 /var/tmp/poisson_mpi.f90 -o /usr/local/bin/poisson_mpi'])

Listing 3: Singularity Definition File

From: ubuntu:18.04
%post
    . /.singularity.d/env/10-docker*.sh
 
# PGI compiler version 19.10
%files
    /home/laytonjb/pgilinux-2019-1910-x86-64.tar /var/tmp/pgilinux-2019-1910-x86-64.tar
%post
    apt-get update -y
    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        g++ \
        gcc \
        libnuma1 \
        openssh-client \
        perl
    rm -rf /var/lib/apt/lists/*
%post
    cd /
    mkdir -p /var/tmp/pgi && tar -x -f /var/tmp/pgilinux-2019-1910-x86-64.tar -C /var/tmp/pgi
    cd /var/tmp/pgi && PGI_ACCEPT_EULA=accept PGI_INSTALL_DIR=/opt/pgi PGI_INSTALL_MPI=true PGI_INSTALL_NVIDIA
      =true PGI_MPI_GPU_SUPPORT=true PGI_SILENT=true ./install
    echo "variable LIBRARY_PATH is environment(LIBRARY_PATH);" >> /opt/pgi/linux86-64/19.10/bin/siterc
    echo "variable library_path is default(\$if(\$LIBRARY_PATH,\$foreach(ll,\$replace(\$LIBRARY_PATH,":",), -L \$ll)));" >> /opt/pgi/linux86-64/19.10/bin/siterc
    echo "append LDLIBARGS=\$library_path;" >> /opt/pgi/linux86-64/19.10/bin/siterc
    ln -sf /usr/lib/x86_64-linux-gnu/libnuma.so.1 /opt/pgi/linux86-64/19.10/lib/libnuma.so
    ln -sf /usr/lib/x86_64-linux-gnu/libnuma.so.1 /opt/pgi/linux86-64/19.10/lib/libnuma.so.1
    rm -rf /var/tmp/pgilinux-2019-1910-x86-64.tar /var/tmp/pgi
%environment
    export LD_LIBRARY_PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/lib:/opt/pgi/linux86-64/19.10/lib:$LD_LIBRARY_PATH
    export PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/bin:/opt/pgi/linux86-64/19.10/bin:$PATH
%post
    export LD_LIBRARY_PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/lib:/opt/pgi/linux86-64/19.10/lib:$LD_LIBRARY_PATH
    export PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/bin:/opt/pgi/linux86-64/19.10/bin:$PATH
 
%files
    poisson_mpi.f90 /var/tmp/poisson_mpi.f90
 
%post
    cd /
    mpif90 /var/tmp/poisson_mpi.f90 -o /usr/local/bin/poisson_mpi

Listing 4: Dockerfile

FROM ubuntu:18.04
 
# PGI compiler version 19.10
RUN apt-get update -y && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        g++ \
        gcc \
        libnuma1 \
        openssh-client \
        perl \
        wget && \
    rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/tmp && wget -q -nc --no-check-certificate -O /var/tmp/pgi-community-linux-x64-latest.tar.gz 
--referer https://www.pgroup.com/products/community.htm?utm_source=hpccm\&utm_medium=wgt\&utm_campaign=CE\&nvi
d=nv-int-14-39155 -P /var/tmp https://www.pgroup.com/support/downloader.php?file=pgi-community-linux-x64 && \
    mkdir -p /var/tmp/pgi && tar -x -f /var/tmp/pgi-community-linux-x64-latest.tar.gz -C /var/tmp/pgi -z && \
    cd /var/tmp/pgi && PGI_ACCEPT_EULA=accept PGI_INSTALL_DIR=/opt/pgi PGI_INSTALL_MPI=true PGI_INSTALL_NVIDIA
      =true PGI_MPI_GPU_SUPPORT=true PGI_SILENT=true ./install && \
    echo "variable LIBRARY_PATH is environment(LIBRARY_PATH);" >> /opt/pgi/linux86-64/19.10/bin/siterc && \
    echo "variable library_path is default(\$if(\$LIBRARY_PATH,\$foreach(ll,\$replace(\$LIBRARY_PATH,":",), -L\$ll)));" >> /opt/pgi/linux86-64/19.10/bin/siterc && \
    echo "append LDLIBARGS=\$library_path;" >> /opt/pgi/linux86-64/19.10/bin/siterc && \
    ln -sf /usr/lib/x86_64-linux-gnu/libnuma.so.1 /opt/pgi/linux86-64/19.10/lib/libnuma.so && \
    ln -sf /usr/lib/x86_64-linux-gnu/libnuma.so.1 /opt/pgi/linux86-64/19.10/lib/libnuma.so.1 && \
    rm -rf /var/tmp/pgi-community-linux-x64-latest.tar.gz /var/tmp/pgi
ENV LD_LIBRARY_PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/lib:/opt/pgi/linux86-64/19.10/lib:$LD_LIBRARY_PATH \
    PATH=/opt/pgi/linux86-64/19.10/mpi/openmpi-3.1.3/bin:/opt/pgi/linux86-64/19.10/bin:$PATH
 
COPY poisson_mpi.f90 /var/tmp/poisson_mpi.f90
 
RUN mpif90 /var/tmp/poisson_mpi.f90 -o /usr/local/bin/poisson_mpi

Building the Images

I leave the details on how to build images to you. The build command for the Singularity image is:

$ sudo singularity build poisson.sif Singularity.def

Note that sudo was used instead of fakeroot, which would have allowed the image to be built as a non-privileged user. At the time of writing, I did not build fakeroot capability into Singularity. I also did not use encryption, so the steps for running MPI applications in containers would be more clear.

You can easily check whether the build was successful by listing the files in the directory (Listing 5).

Listing 5: Checking the Singularity Build

$ ls -s
total 2444376
      4 buildit.docker        4 poisson-docker.py        4 runit.docker
      4 buildit.sing         20 poisson_mpi.f90          4 runit.sing
      4 Dockerfile            4 poisson_mpi.txt          4 Singularity.def
      4 hosts                 4 poisson.py              16 summary-notes.txt
      4 ompi.env        2444296 poisson.sif

The build command for the Docker image is:

$ sudo docker build -t poisson -f Dockerfile .

Notice that a version was not used with the image tag (the image is named poisson).To check whether the build was successful, display a list of images on the host system (Listing 6).

Listing 6: Checking the Docker Build

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
poisson             latest              0a7e2fad652e        2 hours ago         9.83GB
                                        49cbd14ae32f        3 hours ago         269MB
ubuntu              18.04               72300a873c2c        3 weeks ago         64.2MB
hello-world         latest              fce289e99eb9        14 months ago       1.84kB

Running the Containers

In this section, I show how to run MPI applications that are in containers for both Singularity and Docker.

Singularity

Rather than show the output from the MPI application for each command-line options, Listing 7 shows sample output from a Singularity container run that represents the output for all options.

Listing 7: Singularity Container Run

POISSON_MPI - Master process:
  FORTRAN90 version
  
  A program to solve the Poisson equation.
  The MPI message passing library is used.
  
  The number of interior X grid lines is        9
  The number of interior Y grid lines is        9
  
  The number of processes is   2
  
INIT_BAND - Master Cartesian process:
  The X grid spacing is   0.100000    
  The Y grid spacing is   0.100000    
  
INIT_BAND - Master Cartesian process:
  Max norm of boundary values =   0.841471    
  
POISSON_MPI - Master Cartesian process:
  Max norm of right hand side F at interior nodes =    1.17334
  
Step    ||U||         ||Unew||     ||Unew-U||
  
       1  0.403397      0.497847      0.144939
       2  0.570316      0.604994      0.631442E-01
       3  0.634317      0.651963      0.422323E-01
       4  0.667575      0.678126      0.308531E-01
       5  0.687708      0.694657      0.230252E-01
       6  0.701077      0.705958      0.185730E-01
       7  0.710522      0.714112      0.154310E-01
       8  0.717499      0.720232      0.131014E-01
       9  0.722829      0.724966      0.111636E-01
      10  0.727009      0.728716      0.955921E-02
      11  0.730356      0.731745      0.825889E-02
      12  0.733083      0.734229      0.726876E-02
      13  0.735336      0.736293      0.641230E-02
      14  0.737221      0.738029      0.574042E-02
      15  0.738814      0.739502      0.514485E-02
      16  0.740172      0.740762      0.461459E-02
      17  0.741339      0.741849      0.414240E-02
      18  0.742348      0.742791      0.372162E-02
      19  0.743225      0.743613      0.334626E-02
      20  0.743993      0.744333      0.301099E-02
      21  0.744667      0.744967      0.271109E-02
      22  0.745261      0.745526      0.244257E-02
      23  0.745787      0.746022      0.220180E-02
      24  0.746254      0.746463      0.198567E-02
      25  0.746669      0.746855      0.179151E-02
      26  0.747039      0.747205      0.161684E-02
      27  0.747369      0.747518      0.145969E-02
      28  0.747665      0.747799      0.131971E-02
      29  0.747930      0.748050      0.119370E-02
      30  0.748168      0.748276      0.107971E-02
      31  0.748382      0.748479      0.976622E-03
  
POISSON_MPI - Master process:
  The iteration has converged
  
POISSON_MPI:
  Normal end of execution.

You can execute an MPI application in Singularity containers in two ways, both of which execute the application, which is the ultimate goal. Note that in both cases, being a privileged user (e.g.,root) is not required.

The first way to run the MPI code in the Singularity container is:

$ singularity exec poisson.sif mpirun --mca mpi_cuda_support 0 -n 2 /usr/local/bin/poisson_mpi

In this case, any support for CUDA is turned off explicitly with the --mca mpi_cuda_support 0 option, which is also used in the next method.

The second way to execute MPI code in a Singularity container is:

$ mpirun -verbose --mca mpi_cuda_support 0 -np 2 singularity exec poisson.sif /usr/local/bin/poisson_mpi

Running the MPI code with mpirun, uses the Singularity container as an application, just like any other application.

If you examine the command a little closer, you will notice that mpirun is used “outside” the container and is run in the host operating system (OS). Make sure (1) the command in $PATH and (2) the MPI version on the host OS are compatible with the MPI library in the container. On examination of the command, you can see that the MPI initialization is done in the host OS, not in the container. The command line looks just like you run any other MPI application.

Overall, this approach could make the container more portable, because if the MPI versions are compatible, you don’t have to worry about which version of the MPI implementation is in the container, compared with on the host. It just all works, as long as the MPI versions are compatible.

Docker

Docker was not originally designed for HPC, but over time, it has adopted HPC and AI capabilities. The method for running MPI applications requires running as a privileged user (root):

$ sudo docker run --rm -it poisson mpirun --allow-run-as-root -n 2 /usr/local/bin/poisson_mpi

Note that although you can run an MPI application in a Docker container other ways, here, the container is run as a privileged user (root), because in my opinion, this is the simplest method I’ve found. You don’t treat the container as an application, but it does get the job of running the MPI application done.

Summary

When containers broke into the mainstream (I'm not counting chroot), HPC was ignored. However, some container ideals are very useful in the HPC environment, and one key HPC capability is running MPI-based applications.

In this article, I explained how to run MPI applications in Singularity and Docker containers. The best way to get started is to use HPCCM to write a simple recipe that can then be used to create the appropriate specification file for Singularity or Docker. Then, you can keep the HPCCM recipe as the “source” for the container spec files, making container life just a bit easier.

I also illustrated how to execute the containers so that the MPI application can run. Although the application was very simple, the examples did show how the process works and how straightforward and, dare I say, simple, the process can be.