Step 7 — Run the Solver on IBM Polus

Dec 15, 2025·
Hailin Liu
Hailin Liu
· 11 min read
projects

By this point, the solver has several execution modes:

sequential
OpenMP
MPI
MPI + OpenMP
MPI + CUDA

Running them on a workstation is useful for development, but the coursework also asks us to execute and evaluate the implementations on IBM Polus.

Polus uses IBM Spectrum LSF as its batch scheduler. The practical workflow is therefore different from a local shell session:

connect to Polus
prepare the software environment
compile the executable
describe the requested resources
submit the job to LSF
wait in the queue
run on an allocated compute node
collect logs and solution files

The SM25 repository captures this workflow in run_polus.sh and a collection of .lsf job files under config/.

This step explains how those pieces fit together.

1. Understand the target system

The official Polus documentation describes a five-node IBM POWER8 system, with one node also serving frontend functions.

The compute-node hardware includes

2 × 10-core IBM POWER8 CPUs
8 hardware threads per CPU core
2 × NVIDIA Tesla P100 GPUs
16 GB memory per GPU
NVLink

The documented software environment includes GNU and IBM compilers, OpenMP, IBM Spectrum MPI, Open MPI, IBM Spectrum LSF, and CUDA.

For our solver, this hardware naturally maps to the execution models developed in the previous steps:

POWER8 cores and hardware threads
Sequential / OpenMP

multiple processes and nodes
MPI / MPI+OpenMP

Tesla P100 GPUs
MPI+CUDA

The cluster therefore gives us one environment in which all implementations can be tested under the same scheduler and filesystem.

2. Connect to Polus and work from the frontend

The Polus documentation provides SSH access through

polus.hpc.cs.msu.ru

or

polus.cs.msu.ru

and access is based on SSH keys.

A typical login command is

ssh <username>@polus.hpc.cs.msu.ru

The frontend is the place to prepare source code, inspect files, compile programs, and submit jobs.

The actual numerical experiments should be sent to the scheduler so that LSF can allocate CPU cores, GPUs, wall time, and compute nodes.

For the SM25 repository, the working directory should contain at least

Makefile
run_polus.sh
task.cpp
task_mpi.cpp
task_mpi_cuda.cpp

include/
src/
config/
solution/
log/

The run script creates solution/ and log/ when needed, so the main requirement is that the source tree and configuration files remain together.

3. Load the MPI environment used by the repository

The official Polus compilation guide documents both Spectrum MPI and Open MPI. It recommends Spectrum MPI for general use, while also providing Open MPI as a supported option.

The SM25 Polus script makes an explicit reproducibility choice and uses Open MPI:

module purge
module load OpenMPI/4.0.0
ulimit -s 10240

module purge clears previously loaded modules so that an older compiler or MPI environment does not silently remain active.

The repository then loads the exact Open MPI module expected by its Polus workflow.

This matters because the MPI compiler wrappers and runtime launcher should come from the same MPI environment:

mpicxx
mpiexec

Using one MPI implementation for compilation and another for execution can produce avoidable runtime problems.

For this project, the simplest rule is to reproduce the environment encoded in run_polus.sh when reproducing the repository experiments.

4. Compile on Polus, execute through LSF

Compilation happens before the job is submitted.

For example, the MPI implementation is built with

mpicxx -O3 -o task_mpi \
    task_mpi.cpp \
    src/conjugate_gradient.cpp \
    src/mpi_conjugate_gradient.cpp \
    -Iinclude \
    -lm \
    -std=c++11

The hybrid version adds OpenMP support:

mpicxx -O3 -o task_mpi_omp \
    task_mpi.cpp \
    src/conjugate_gradient.cpp \
    src/mpi_conjugate_gradient.cpp \
    -Iinclude \
    -lm \
    -std=c++11 \
    -fopenmp

The Polus GPU build uses

make ARCH=sm_60

which produces

task_mpi_cuda

through the repository Makefile.

After compilation, the executable is launched through LSF rather than being used as a long-running frontend process.

The central scheduler command is

bsub

with the general form

bsub [options] command [arguments]

5. Describe resources with bsub

The Polus LSF documentation defines several options that appear throughout the repository:

OptionPurpose
-n Nrequest CPU cores / slots
-W timeset the wall-time limit (a bare number means minutes, e.g. -W 1; HH:MM is also accepted)
-J nameassign a job name
-q queueselect a queue
-R "..."add resource or placement requirements
-gpu "..."request GPUs
-o, -ewrite stdout/stderr, appending to existing files
-oo, -eowrite stdout/stderr, replacing existing files

For example, the sequential jobs in run_polus.sh are submitted in the form

bsub \
    -J "seq_40x40" \
    -n 1 \
    -W 1 \
    -oo "log/stage_1/seq_40x40_cout.log" \
    -eo "log/stage_1/seq_40x40_cerr.log" \
    ./task_seq 40 40

The solver therefore runs only after LSF has allocated the requested resource.

The Polus documentation describes three queues:

short
normal
fullcluster

short is the default queue for short runs with a documented limit of 30 minutes, while normal is intended for jobs of up to three hours. fullcluster is used for full-cluster work and requires prior access.

For our experiments, wall-time requests should reflect the expected scale of the run instead of reserving unnecessarily large windows.

6. Use .lsf files for structured jobs

A long bsub command quickly becomes difficult to maintain, especially for hybrid and GPU runs.

LSF supports command files whose scheduler directives begin with

#BSUB

For example, a minimal MPI+OpenMP job can look like

#BSUB -n 2
#BSUB -W 1
#BSUB -J "mpi_omp_40_40_2_4"
#BSUB -o "log/stage_7/mpi_omp_40_40_2_4_cout.log"
#BSUB -e "log/stage_7/mpi_omp_40_40_2_4_cerr.log"

export OMP_NUM_THREADS=4
export OMP_PLACES=cores
export OMP_PROC_BIND=close

mpiexec ./task_mpi_omp 40 40

The repository contains exactly this configuration for the two-process, four-thread correctness run.

Submit a command file with

bsub < config/stage_7/mpi_omp_40_40_2_4.lsf

This separates two concerns cleanly:

solver executable
numerical computation

.lsf file
cluster resource configuration

The same binary can therefore be reused with several resource layouts.

7. Submit the OpenMP runs with explicit placement

OpenMP jobs need both a thread count and a processor-placement strategy.

The Polus documentation supports affinity requests such as

-R "affinity[core(N)]"

when OpenMP threads should be associated with CPU cores.

The repository uses this form for its smaller OpenMP tests:

bsub \
    -J "omp_40_40_4" \
    -W 1 \
    -R "affinity[core(4)]" \
    OMP_NUM_THREADS=4 \
    ./task_omp 40 40

For a larger single-node OpenMP experiment, the Polus documentation also provides the helper

/polusfs/lsf/openmp/launchOpenMP.py

for affinity-aware launching.

The repository uses it for the 32-thread \(800\times1200\) case:

bsub \
    -R "affinity[core(16)]" \
    OMP_NUM_THREADS=32 \
    /polusfs/lsf/openmp/launchOpenMP.py \
    ./task_omp 800 1200

This is a useful reminder that OMP_NUM_THREADS describes the OpenMP team, while LSF resource requests describe where that team is allowed to run.

8. Submit MPI jobs through the scheduler

The MPI executable is launched inside the LSF allocation with

mpiexec

For the \(40\times40\) correctness cases, run_polus.sh submits

1 MPI process
2 MPI processes
4 MPI processes

using jobs equivalent to

bsub -n 1 -W 1 mpiexec ./task_mpi 40 40
bsub -n 2 -W 1 mpiexec ./task_mpi 40 40
bsub -n 4 -W 1 mpiexec ./task_mpi 40 40

The larger MPI experiments extend the same pattern to the \(400\times600\) and \(800\times1200\) grids.

For example, the repository contains a Polus configuration for a 20-process \(800\times1200\) run:

#BSUB -n 20
#BSUB -W 30
#BSUB -J mpi_800_1200_20
#BSUB -oo "log/stage_9/mpi_800_1200_20_cout.log"
#BSUB -eo "log/stage_9/mpi_800_1200_20_cerr.log"
#BSUB -R "span[hosts=1]"

mpiexec ./task_mpi 800 1200

The resource expression

span[hosts=1]

asks LSF to place all requested CPU slots on one compute node.

Other LSF placement expressions, such as

span[ptile=N]

can limit the number of allocated slots placed on each host.

9. Submit the hybrid MPI+OpenMP runs

The Polus documentation treats hybrid execution as two parameters:

\[ P=\text{MPI processes}, \]\[ T=\text{OpenMP threads per MPI process}. \]

At the shell level, the pattern is

OMP_NUM_THREADS=T mpiexec ./task_mpi_omp ...

with LSF allocating the resources for the MPI processes.

The SM25 correctness configurations use four OpenMP threads per rank:

1 MPI process × 4 OpenMP threads
2 MPI processes × 4 OpenMP threads

The first of these is exactly the mpi_omp_40_40_2_4.lsf file shown in Section 6; the one-process configuration is the same file with -n 1.

The repository also contains a larger Polus configuration that deliberately uses the POWER8 hardware-thread structure:

#BSUB -n 20
#BSUB -W 30
#BSUB -R "span[hosts=1]"

export OMP_NUM_THREADS=8
export OMP_PROC_BIND=true
export OMP_PLACES=threads

mpiexec ./task_mpi_omp 800 1200

Here the job requests 20 MPI slots on one host and creates eight OpenMP threads inside each process.

This configuration is useful for exploring the POWER8 simultaneous multithreading capacity described by the Polus documentation.

The important point for the experiment report is to record both dimensions:

MPI process count
OpenMP threads per process

A label such as

20 MPI × 8 OpenMP

is much more informative than recording only a total thread count.

10. Request GPUs explicitly for MPI+CUDA

GPU jobs add the LSF

-gpu

resource request.

The Polus documentation describes the general form as

-gpu "num=...:mode=...:..."

and notes that each Polus node contains two GPUs.

The repository requests exclusive GPU access for the CUDA measurements.

A one-GPU job is configured as

#BSUB -n 1
#BSUB -W 30
#BSUB -J "mpi_cuda_800_1200_g1"
#BSUB -oo "log/stage_9/mpi_cuda_800_1200_g1_cout.log"
#BSUB -eo "log/stage_9/mpi_cuda_800_1200_g1_cerr.log"
#BSUB -gpu "num=1:mode=exclusive_process"

mpiexec ./task_mpi_cuda 800 1200

The two-GPU version requests two CPU slots and two exclusive GPUs:

#BSUB -n 2
#BSUB -W 30
#BSUB -J "mpi_cuda_800_1200_g2"
#BSUB -oo "log/stage_9/mpi_cuda_800_1200_g2_cout.log"
#BSUB -eo "log/stage_9/mpi_cuda_800_1200_g2_cerr.log"
#BSUB -R "span[ptile=2]"
#BSUB -gpu "num=2:mode=exclusive_process"

mpiexec ./task_mpi_cuda 800 1200

Inside the executable, the rank-to-device logic from Step 6 maps each node-local MPI rank to a visible CUDA device.

The allocation and execution layers therefore work together:

LSF
    reserves the GPUs
MPI
    creates the processes
local MPI rank
    selects a CUDA device
CUDA kernels
    execute the local solver

Keeping GPU allocation in the .lsf file and GPU selection in the executable makes the resource request explicit and reproducible.

11. Monitor jobs and keep stdout and stderr

After submission, LSF assigns a numeric job identifier.

The standard command for inspecting the queue is

bjobs

and detailed information for one job can be requested with

bjobs -l <JOBID>

The Polus documentation lists common states including

PEND    waiting for resources
RUN     executing
DONE    completed successfully
EXIT    completed with a nonzero status

run_polus.sh captures the ID returned by bsub. For jobs submitted from a .lsf file, the form is

JOB_ID=$(bsub < "${LSF_FILE}" | awk '{print $2}' | tr -d '<>')

where ${LSF_FILE} is the job script path; the inline form used for the earlier stages (bsub ... ./task_seq 40 40) is parsed the same way.

It then waits for that specific job:

bwait -w "ended(${JOB_ID})"

This makes the experiment driver sequential at the job level:

submit one configuration
wait for completion
submit the next configuration

Each experiment writes separate stdout and stderr logs under directories such as

log/stage_1/
log/stage_2/
log/stage_5/
log/stage_7/
log/stage_9/

Keeping the error stream is important even when the program appears to have finished, because MPI, CUDA, or scheduler errors may be reported there.

12. Understand run_polus.sh as the experiment driver

The repository’s run_polus.sh combines environment setup, compilation, submission, waiting, and log organization.

It accepts

--stage
--stop_stage

so a subset of the experiment pipeline can be executed.

For example:

bash run_polus.sh --stage 5 --stop_stage 7

runs the internal script stages from 5 through 7.

These internal stage numbers belong to the automation script and are separate from the Step 1–8 structure of this walkthrough.

The script currently covers:

internal stage 1
    sequential correctness grids

internal stage 2
    OpenMP 1 / 4 / 16 thread correctness runs

internal stage 3
    larger OpenMP measurements

internal stage 5
    MPI 1 / 2 / 4 process correctness runs

internal stage 6
    larger MPI measurements

internal stage 7
    MPI+OpenMP correctness runs

internal stage 8
    larger MPI+OpenMP measurements

internal stage 9
    matched MPI, MPI+OpenMP, and MPI+CUDA batch runs

The last stage is especially useful for final comparisons because it runs pre-written .lsf configurations over several grid sizes.

The current size list is

800 × 1200
1600 × 2400
3200 × 4800

and for each size the script submits configurations for

MPI
MPI + OpenMP
MPI + CUDA with one GPU
MPI + CUDA with two GPUs

Before those GPU jobs, it builds the CUDA target with

make ARCH=sm_60

which is the architecture setting used by the repository for Polus.

13. Direct LSF submission and mpisubmit.pl

The official Polus documentation provides a convenience utility named

mpisubmit.pl

for MPI, OpenMP, MPI+OpenMP, and MPI+CUDA jobs.

Its interface exposes parameters such as

-p    MPI processes
-t    threads per process
-w    wall time
-g    GPU execution

For example, the documented style is conceptually

mpisubmit.pl -p 10 -t 8 -w 00:12 executable

The documentation recommends this helper for common parallel jobs and direct LSF usage for more specialized resource requests.

SM25 uses direct bsub commands and explicit .lsf files.

That choice makes several experiment details visible in version control:

job name
wall time
CPU-slot request
host placement
OpenMP placement variables
GPU count and sharing mode
stdout path
stderr path
executable and grid size

For a reproducibility-oriented project, these job files become part of the experiment configuration rather than temporary shell commands.

14. Preserve the execution configuration with the results

When a cluster run finishes, the numerical output alone is not enough to reconstruct the experiment.

For every timing result, keep the associated configuration:

grid size
executable
MPI process count
OpenMP thread count
GPU count
LSF placement request
wall-time request
compiler/build configuration
stdout log
stderr log

The SM25 repository already separates configuration and output into

config/
log/
solution/

which gives us a useful structure for this record.

At this stage, the purpose is to execute every implementation under controlled and documented resource settings.

The next step uses those outputs to answer the final questions of the coursework: whether the implementations agree numerically, how runtime changes with parallelism, where communication and data-transfer costs appear, and how to present speedup and efficiency in the final report.

Sources

The Polus-specific details in this step follow the official MSU HPC documentation (checked August 2026):

Hailin Liu
Authors
PhD Researcher in Agentic AI and Multi-Agent Systems
Hailin Liu is a PhD researcher in Artificial Intelligence and Machine Learning, focusing on Agentic AI, Multi-Agent Systems, and AI Security. His research explores runtime governance mechanisms for autonomous intelligent systems, including agent safety, long-horizon reasoning, and adaptive control.