Skip to main content

COULOMB optimization through CPU parallelism

Goal

Walk you through the usage of Codee to optimize Coulomb, a code that computes electrical currents in 2D planes, by parallelizing computations on CPU.

info

This guide is part of the NERSC + Codee Training Series 2024. Code available for download at the previous link.

Getting started

First, navigate to the source code for COULOMB:

cd codee-demos/C/COULOMB

Next, load the latest Codee version available on Perlmutter:

module load codee/2024.3.1

Walkthrough

1. Explore the source code

The computation is handled by a triple-nested for loop within coulomb.c:

for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
double mat_ij = 0;
for(int k = 0; k < size; k+=4) {
double dx = vec[k+0] - (scaleX * j + x0);
double dy = vec[k+1] - (scaleY * i + y0);
double dz = vec[k+2] - (z0);
double charge = 1e-9 * vec[k+3];
double dist = sqrt(dx*dx + dy*dy + dz*dz);
mat_ij += charge / dist;
}
mat[j + i * cols] = mat_ij / (4 * PI * e0);
}
}

2. Run the checks report

To explore how Codee can help speed up this loop by offloading it to a CPU, use --target-arch to include CPU-related checks in the analysis:

Codee command
codee checks --verbose --target-arch cpu coulomb.c:coulomb -- gcc coulomb.c -Ofast -lm
Codee output
Date: 2024-09-05 Codee version: 2024.3.1 License type: Full
Compiler invocation: gcc coulomb.c -Ofast -lm

[1/1] coulomb.c ... Done

CHECKS REPORT

<...>

coulomb.c:26:2 [PWR050] (level: L2): Consider applying multithreading parallelism to forall loop
Suggestion: Use 'rewrite' to automatically optimize the code
Documentation: https://github.com/codee-com/open-catalog/tree/main/Checks/PWR050
AutoFix (choose one option):
* Using OpenMP 'for' (recommended):
codee rewrite --multi omp-for --in-place coulomb.c:26:2 -- gcc coulomb.c -Ofast -lm
* Using OpenMP 'taskwait':
codee rewrite --multi omp-taskwait --in-place coulomb.c:26:2 -- gcc coulomb.c -Ofast -lm
* Using OpenMP 'taskloop':
codee rewrite --multi omp-taskloop --in-place coulomb.c:26:2 -- gcc coulomb.c -Ofast -lm

<...>

SUGGESTIONS

Use --check-id to focus on specific subsets of checkers, e.g.:
codee checks --check-id PWR045 --verbose --target-arch cpu coulomb.c:coulomb -- gcc coulomb.c -Ofast -lm

1 file, 1 function, 3 loops successfully analyzed (6 checkers) and 0 non-analyzed files in 54 ms

Codee suggests various options to optimize the loop using OpenMP parallelization.

3. Autofix

OpenMP

Let's use Codee's autofix capabilities to automatically optimize the code, following the recomended option.

We can copy and paste the suggested Codee invocation to perform the parallelization; just replace the --in-place argument with -o to create a new file with the modified code:

Codee command
codee rewrite --multi omp-for -o coulomb_omp.c coulomb.c:26:2 -- gcc coulomb.c -Ofast -lm
Codee output
Date: 2024-09-05 Codee version: 2024.3.1 License type: Full
Compiler invocation: gcc coulomb.c -Ofast -lm

Results for file '/global/homes/u/user/codee-demos/C/COULOMB/coulomb.c':
Successfully applied AutoFix to the loop at 'coulomb.c:coulomb:26:2' [using multi-threading]:
[INFO] coulomb.c:26:2 Parallel forall: variable 'mat'
[INFO] coulomb.c:26:2 Loop parallelized with multithreading using OpenMP directive 'for'
[INFO] coulomb.c:26:2 Parallel region defined by OpenMP directive 'parallel'
[INFO] coulomb.c:26:2 Make sure there is no aliasing among variables: vec, mat

Successfully created coulomb_omp.c

Minimum software stack requirements: OpenMP version 3.0 with multithreading capabilities
diff -C 2 coulomb.c coulomb_omp.c
        double scaleY = (y1 - y0) / rows;

+ // Codee: Loop modified by Codee (2024-09-05 04:27:26)
+ // Codee: Technique applied: multithreading with 'omp-for' pragmas
+ #pragma omp parallel default(none) shared(PI, cols, e0, mat, rows, scaleX, scaleY, size, vec, x0, y0, z0)
+ {
+ #pragma omp for schedule(auto)
for(int i=0; i<rows; i++) {
for(int j=0; j<cols; j++) {
***************
*** 38,41 ****
--- 43,47 ----
}
}
+ } // end parallel
}

4. Execution

Finally, compile and run both the original and the optimized code to assess the speed improvements. The following SLURM scripts can be used as reference; create launch.sh and COULOMB.sh, and add execution permissions to the latter:

chmod u+x COULOMB.sh
launch.sh
#!/bin/bash

#SBATCH --account=ntrain6
#SBATCH --job-name=codee_c_coulomb

#SBATCH --constraint=cpu
#SBATCH --qos=regular
#SBATCH --reservation=codee_day1
#SBATCH --time=0:05:00

#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=32
#SBATCH --gpus-per-task=1

export SLURM_CPU_BIND="cores"
srun COULOMB.sh
COULOMB.sh
#!/bin/bash

rm -f coulomb coulomb_omp

gcc Vector.c Matrix2D.c coulomb.c -Ofast -lm -o coulomb
./coulomb 600

gcc Vector.c Matrix2D.c coulomb_omp.c -Ofast -fopenmp -lm -o coulomb_omp
./coulomb_omp 600

The optimized version ran on 1.90 seconds, while the original took 27.68 seconds, which represents an speedup of 14.57x.