Example 5: Containerized MPI Multi-Process Job

This example shows how to run a multi-process MPI job with Slurm and Apptainer. It uses a small Python program with mpi4py to estimate pi using a distributed Monte Carlo calculation.

The example follows a hybrid launch model: the host MPI launcher starts one Apptainer container per MPI rank. The container provides Python and mpi4py; the host MPI stack coordinates the ranks across the allocated nodes.

1. Connect to the CoSTAR cluster

Open a terminal and connect to the CoSTAR login node:

ssh ab1234@costar-login01

Replace ab1234 with your University username.

2. Create the Apptainer definition file

Download the definition file, or create a file called example-05-mpi.def in your working directory and copy in the recipe below.

example-05-mpi.def (click to collapse / view)
 1Bootstrap: docker
 2From: ubuntu:22.04
 3
 4%post
 5    set -e
 6    export DEBIAN_FRONTEND=noninteractive
 7
 8    apt-get update
 9    apt-get install -y --no-install-recommends \
10        python3 python3-pip python3-dev build-essential \
11        libopenmpi-dev openmpi-bin
12
13    python3 -m pip install --upgrade pip
14    python3 -m pip install --no-cache-dir mpi4py
15
16    apt-get clean
17    rm -rf /var/lib/apt/lists/* /root/.cache/pip || true
18
19%environment
20    export PATH=/usr/bin:$PATH
21    export PYTHONUNBUFFERED=1
22
23%labels
24    Author CoSTAR
25    Description "Python mpi4py example for Slurm multi-process jobs"

3. Create the MPI Python script

Download the Python script, or create a file called example-05-mpi.py in your working directory and copy in the script below.

example-05-mpi.py (click to collapse / view)
 1from mpi4py import MPI
 2
 3import random
 4import socket
 5
 6
 7comm = MPI.COMM_WORLD
 8rank = comm.Get_rank()
 9size = comm.Get_size()
10host = socket.gethostname()
11
12if rank == 0:
13    print(f"[INFO] World size = {size}")
14
15comm.Barrier()
16print(f"Rank {rank:02d} on host {host}")
17
18total_samples = 2_000_000
19local_samples = total_samples // size
20random.seed(1234 + rank)
21
22hits = 0
23for _ in range(local_samples):
24    x = random.random()
25    y = random.random()
26    if x * x + y * y <= 1.0:
27        hits += 1
28
29total_hits = comm.reduce(hits, op=MPI.SUM, root=0)
30
31if rank == 0:
32    samples_used = local_samples * size
33    pi_estimate = 4.0 * total_hits / samples_used
34    print(f"[RESULT] pi ~= {pi_estimate:.6f} using {samples_used} samples")

4. Build the Apptainer image

Note

On CoSTAR, Apptainer is available on compute nodes, not on the login node. If you want to build the image on the cluster, start an interactive Slurm session on a compute node first.

Start an interactive Slurm session:

srun -N 1 --time=00-01:00:00 --pty bash

Once the session starts on a compute node, build the image:

apptainer build example-05-mpi.sif example-05-mpi.def

When the build finishes, exit the interactive session:

exit

5. Run interactively

For a quick interactive test, first request an allocation across 2 nodes with 1 MPI rank per node:

salloc -N 2 --ntasks-per-node=1 -t 02:00:00

When the allocation starts, run the MPI program inside the Apptainer image:

mpirun -np 2 apptainer exec --cleanenv example-05-mpi.sif python3 example-05-mpi.py

This launches 2 MPI ranks in total. When you finish testing, leave the interactive allocation with:

exit

6. Create the batch submission script

Download the example submission script, or create a file called example-05-mpi.sh in your working directory and copy in the script below.

example-05-mpi.sh (click to collapse / view)
 1#!/bin/bash
 2
 3#SBATCH --job-name=mpi-container
 4#SBATCH --partition=main
 5#SBATCH --nodes=2
 6#SBATCH --ntasks-per-node=1
 7#SBATCH --time=00:10:00
 8#SBATCH --hint=nomultithread
 9#SBATCH --output=slurm.%N.%j.out
10#SBATCH --error=slurm.%N.%j.err
11
12module purge
13module load NVHPC/26.3-CUDA-13.1.0
14module load UCX-CUDA/1.19.0-GCCcore-14.3.0-CUDA-13.1.0
15
16IMAGE=$PWD/example-05-mpi.sif
17SCRIPT=$PWD/example-05-mpi.py
18
19echo "Job started on $(date)"
20echo "Nodes allocated:"
21scontrol show hostnames "$SLURM_NODELIST"
22
23mpirun -np "$SLURM_NTASKS" apptainer exec --cleanenv "$IMAGE" python3 "$SCRIPT"
24
25echo "Job finished on $(date)"

The job requests 2 nodes with 1 MPI rank per node, for 2 MPI ranks in total. The mpirun command launches those ranks and runs the Python script inside the Apptainer image.

7. Submit as a batch job

Submit the script to the Slurm scheduler:

sbatch example-05-mpi.sh

You can check the status of your job by running:

squeue --me

8. Check the output

When the job finishes, Slurm writes the standard output and error streams to files in the submission directory:

  • slurm.<node>.<job_id>.out contains the allocated node list, MPI rank placement, and the final pi estimate.

  • slurm.<node>.<job_id>.err contains error messages, if any were produced.

The output file should include lines similar to:

Nodes allocated:
costar01
costar02
[INFO] World size = 2
Rank 00 on host costar01
Rank 01 on host costar02
[RESULT] pi ~= 3.141820 using 2000000 samples

The rank order may differ between runs, but the world size should match the number of MPI ranks requested by Slurm.

9. What to change next

For your own MPI workload, adjust:

  • --nodes for the number of compute nodes.

  • --ntasks-per-node for the number of MPI ranks per node.

  • IMAGE to point to your Apptainer image.

  • SCRIPT to point to your MPI application or Python script.

  • The MPI modules to match the MPI version needed by your application.