Building Parallel Code¶
Learning Objectives
Explain why parallel builds require different compilers and flags than serial builds
Configure CMake and Autotools for MPI, OpenMP, and CUDA builds
Select appropriate compiler flags for optimization and vectorization
Reserve compute resources for builds instead of using login nodes
Verify parallel executables with
lddand test runsAvoid common pitfalls: wrong compilers, missing modules, architecture mismatches
Building parallel applications requires the same build systems as serial code (CMake, Autotools), but with special compiler wrappers, flags, and library dependencies to enable parallelism.
Why parallel builds differ:
MPI: Code runs across multiple processes on potentially different nodes; requires special compilers (
mpicc,mpicxx) that know how to link MPI libraries and set up the runtimeOpenMP: Multi-threading within a single process; requires compiler flags (
-fopenmp) that generate thread-creation code and link thread librariesCUDA/GPU: Code runs on a different processor entirely; requires NVCC or HIP compilers that compile both host (CPU) and device (GPU) code, then link CUDA runtime libraries
Where to Build¶
Never build on login nodes. Build operations are CPU- and memory-intensive, and can degrade the experience for all users. Reserve a compute or devel node instead.
Why not login nodes?
Login nodes are shared by all users for editing files and submitting jobs
Heavy compilation can consume CPU/memory, slowing down the entire cluster
Build failures or infinite loops can crash the login node
Compute nodes have more resources and are designed for heavy workloads
Interactive Build Session¶
Reserve a node for an interactive build where you can see compiler output in real-time:
# Interactive build session (4 cores, 1 hour)
# You get a shell on a compute node to run make/cmake interactively
srun -c4 --time=1:00:00 --pty bash
How this works:
srunrequests resources from the SLURM scheduler-c4requests 4 CPU cores (adjust based on your build’s parallelism)--time=1:00:00sets a 1-hour limit (adjust for larger builds)--ptycreates a pseudo-terminal so you get an interactive shellOnce the scheduler allocates a node, you’re on a compute node, not the login node
Batch Build Job¶
For large builds or long compilation, submit a batch job instead:
# Create build-job.sh
#!/bin/bash
#SBATCH --job-name=build
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=8
#SBATCH --time=2:00:00
#SBATCH --mem=16G
module purge
module load gcc openmpi
cd $PROJECT_DIR
make -j8
# Submit the build job
sbatch build-job.sh
# Check job status
squeue -u $USER
# View output when complete
tail build-job.sh.out
Why batch for large builds?
Jobs queue properly and don’t block if you disconnect
Can request more resources (memory, GPUs, many cores)
Output is saved to a file for later review
Scheduler optimizes cluster utilization
Common Build Patterns¶
CMake with MPI¶
CMake is the modern standard for C/C++ builds. MPI requires special compiler wrappers that automatically include the right flags and library paths.
mkdir build; cd build
cmake .. \
-DCMAKE_C_COMPILER=mpicc \
-DCMAKE_CXX_COMPILER=mpicxx \
-DCMAKE_INSTALL_PREFIX=$HOME/.local
make -j$(nproc)
make install
Why use mpicc/mpicxx instead of gcc/g++?
These are compiler wrappers, not different compilers
They call
gcc/g++internally but add MPI-specific flags automaticallyThey set include paths (
-I/usr/include/mpi) so headers are foundThey set library paths (
-L/usr/lib/mpi) so linking worksThey link against the MPI library (
-lmpi) automaticallyUsing
gccdirectly would result in “undefined reference to MPI_Init” errors
CMake flags explained:
-DCMAKE_C_COMPILER=mpicc: Tell CMake to use the MPI wrapper for C-DCMAKE_CXX_COMPILER=mpicxx: Tell CMake to use the MPI wrapper for C++-DCMAKE_INSTALL_PREFIX=$HOME/.local: Install to user-writable location (no root needed)
Why make -j$(nproc)?
-jenables parallel compilation (multiple files at once)$(nproc)uses all available cores (faster builds)For MPI builds, this compiles each source file in parallel, then links once
CMake with CUDA¶
CUDA builds require the NVCC compiler and GPU architecture specifications.
cmake .. \
-DCMAKE_CXX_COMPILER=nvcc \
-DCMAKE_CUDA_ARCHITECTURES=80 \
-DCMAKE_INSTALL_PREFIX=$HOME/.local
Why nvcc?
NVCC is NVIDIA’s compiler that handles both host (CPU) and device (GPU) code
It separates
.cufiles into host code (compiled with g++) and device code (compiled with nvcc’s PTX assembler)It links against CUDA runtime libraries automatically
Why CMAKE_CUDA_ARCHITECTURES?
GPUs have specific compute capabilities (e.g., 80 = Ampere, 90 = Hopper)
This flag tells NVCC which GPU architectures to generate code for
80targets NVIDIA A100/Ampere GPUs (common on HPC clusters)Multiple architectures:
-DCMAKE_CUDA_ARCHITECTURES=70;80;90Using
nativedetects the current GPU:-DCMAKE_CUDA_ARCHITECTURES=native
CMake with OpenMP¶
OpenMP requires only a compiler flag, no special compiler wrapper:
cmake .. \
-DCMAKE_CXX_COMPILER=g++ \
-DCMAKE_CXX_FLAGS="-fopenmp -O3" \
-DCMAKE_INSTALL_PREFIX=$HOME/.local
How OpenMP differs from MPI:
OpenMP uses compiler flags, not wrapper compilers
-fopenmptells the compiler to recognize#pragma ompdirectivesThe compiler generates thread-creation code automatically
Links against
libgomp(GNU OpenMP runtime) automatically
Autotools with MPI¶
Legacy projects often use Autotools (./configure && make). MPI requires setting environment variables for the configure script.
./configure \
--prefix=$HOME/.local \
CC=mpicc CXX=mpicxx
make -j$(nproc)
make install
How Autotools works:
./configurechecks for compilers, libraries, and system featuresIt generates a
Makefiletailored to your systemCC=mpiccsets the C compiler to the MPI wrapperCXX=mpicxxsets the C++ compiler to the MPI wrapper--prefixsets the installation directory
Why environment variables instead of flags?
Autotools expects compiler names in
CC,CXX,FCvariablesThese are checked during configuration to find headers and libraries
Setting them before
./configureensures the right compilers are detected
Compiler Flags for Parallelism¶
Different compilers use different flags for the same features. Here’s a reference for common parallelism targets:
Target |
GCC/Clang |
NVHPC |
Intel |
Why This Flag? |
|---|---|---|---|---|
OpenMP |
|
|
|
Enable |
Optimization |
|
|
|
Aggressive optimization + CPU-specific instructions |
Vectorization |
|
|
|
Allow unsafe math for SIMD vectorization |
MPI |
|
|
|
Use MPI wrapper compilers (not direct flags) |
CUDA |
N/A |
|
N/A |
Specify host compiler for NVCC (NVHPC only) |
Understanding the Flags¶
Optimization flags (-O3, -O2, -O0):
-O0: No optimization (fastest compilation, slowest code) - use for debugging-O2: Moderate optimization (good balance) - default for many projects-O3: Aggressive optimization (fastest code, longer compile) - use for productionHigher optimization enables loop unrolling, inlining, vectorization
Architecture-specific flags:
-march=native(GCC/Clang): Generate code optimized for the current CPU-tp=native(NVHPC): Same, for NVIDIA compilers-xHost(Intel): Same, for Intel compilersWhy use these? Enables CPU-specific instructions (AVX-512, AVX2, etc.)
Warning: Code may not run on older CPUs; only use if targeting a specific machine
Vectorization flags:
-ffast-math: Relax IEEE math compliance for speed (allows reordering, assumes no NaNs)-ffinite-math-only: Assume no NaN/Inf, enables more optimizationsWhy needed? Compilers can’t vectorize code that must handle special floating-point cases
Trade-off: Faster code, but may produce slightly different results on edge cases
Common Pitfalls¶
1. Missing Libraries¶
Problem: Compiler can’t find MPI headers or CUDA libraries.
Why it happens: Parallel libraries aren’t in the default search paths. You must load them with module load.
Solution:
# Load required modules BEFORE building
module load gcc openmpi cuda
# Now build
mkdir build; cd build
cmake .. -DCMAKE_C_COMPILER=mpicc
make
How to find available modules:
module avail mpi
module avail cuda
module avail openmp
2. Wrong Compiler Wrappers¶
Problem: Using gcc instead of mpicc for MPI code.
Why it fails:
gcc -o my-mpi-program main.c # ERROR: undefined reference to `MPI_Init'
Solution:
mpicc -o my-mpi-program main.c # WORKS: links MPI library automatically
Why mpicc works:
mpiccis a wrapper that callsgccwith hidden flagsIt adds
-I/usr/include/mpi(header path)It adds
-L/usr/lib/mpi -lmpi(library path and link flag)You don’t need to know these paths manually
3. Architecture Mismatches¶
Problem: Build on one machine, run on another with different CPU.
Why it fails:
If you compile with
-march=nativeon a login node (older CPU)Then run on a compute node (newer CPU), it usually works
But if you compile on a newer CPU and run on an older one, it crashes
AVX-512 instructions don’t exist on older CPUs
Solution:
# Option 1: Don't use -march=native (most portable)
gcc -O3 -o program main.c
# Option 2: Target the oldest CPU you'll run on
gcc -O3 -march=haswell -o program main.c
# Option 3: Build on the actual compute node
srun --pty bash # Get on a compute node
# Then build there with -march=native
4. Link Errors¶
Problem: “undefined reference to” errors for libraries.
Why it happens:
Library not installed
Library not in
LD_LIBRARY_PATHWrong link order (libraries after object files)
Solution:
# Check if library exists
ldconfig -p | grep mpi
# Add library path
export LD_LIBRARY_PATH=/usr/lib/mpi:$LD_LIBRARY_PATH
# Correct link order: objects first, then libraries
gcc -o program main.o utils.o -lmpi -lm # Libraries at the end
Why library order matters:
The linker processes files left-to-right
It resolves symbols as it goes
If
main.oreferencesMPI_Initbut-lmpicomes beforemain.o, the symbol isn’t found yet
Build Verification¶
After building, verify your executable is correctly linked and functional.
Check Linked Libraries¶
# Check for linked libraries
ldd my-program | grep -E 'mpi|cuda|openmp'
What ldd shows:
Lists all shared libraries your program depends on
Shows where each library is loaded from
If a library shows “not found”, you have a runtime problem
Expected output for MPI program:
libmpi.so.40 => /usr/lib64/libmpi.so.40
libopen-rte.so.40 => /usr/lib64/libopen-rte.so.40
libopen-pal.so.40 => /usr/lib64/libopen-pal.so.40
Run a Quick Test¶
# Run a quick test on a compute node
srun -n4 ./my-program --test
Why test with srun?
MPI programs need to be launched with
mpiexecorsrunRunning
./my-programdirectly won’t work for MPI codesrun -n4launches 4 MPI processesTest mode (
--test) verifies correctness without full workload
Verify OpenMP Threading¶
# Check if OpenMP is working
export OMP_NUM_THREADS=4
./my-program
# Add this to your program to verify:
#pragma omp parallel
printf("Running on thread %d of %d\n", omp_get_thread_num(), omp_get_num_threads());
See Also¶
MPI - Message Passing Interface for distributed memory
OpenMP - Shared memory parallelism with threads
CUDA - GPU programming with NVIDIA CUDA
Auto-Vectorization - Compiler SIMD optimization