Parallel Chemistry with OpenMP
Overview
In reactive transport simulations, geochemical speciation calculations performed by PHREEQC typically consume >95% of the total simulation time. Because these calculations are embarrassingly parallel — each element’s chemistry can be computed independently — OGS can distribute them across multiple OpenMP threads for a significant speedup.
Two usage modes are available:
| OpenMP-only | Hybrid MPI+OpenMP | |
|---|---|---|
| Build preset | release / debug |
release-petsc / debug-petsc |
| Transport | single process | distributed across MPI ranks |
| Chemistry | multi-threaded | multi-threaded within each rank |
| Use case | desktop / workstation | clusters, large problems |
Mode 1: OpenMP-only (no MPI)
This is the simplest way to speed up reactive transport. Transport runs on a single CPU while chemistry is parallelized across OpenMP threads. No MPI installation is required.
Build
See the developer guide — build configuration .
Run
Set the number of chemistry threads via the environment variable or the project file (see Configuration below), then run OGS normally:
OGS_CHEM_THREADS=8 ogs project.prjOr with the project-file option:
<chemical_system chemical_solver="Phreeqc">
<mesh>ReactiveDomain</mesh>
<database>phreeqc.dat</database>
<chemistry_threads>8</chemistry_threads>
<!-- ... other settings ... -->
</chemical_system>ogs project.prjMode 2: Hybrid MPI+OpenMP
For large problems or cluster environments, combine MPI domain decomposition for transport with OpenMP threads for chemistry within each rank:
- MPI handles transport with optimal communication patterns (typically 2-4 ranks)
- OpenMP threads parallelize chemistry within each rank (8-16+ threads)
- Total chemistry parallelism = MPI ranks x threads per rank
Build
See the developer guide — build configuration for MPI and PETSc .
Run
OGS_CHEM_THREADS=8 mpirun -np 2 ogs project.prjTotal cores used: 2 ranks x 8 threads = 16 cores during the chemistry phase.
Or equivalently, with <chemistry_threads>8</chemistry_threads> in the
project file:
mpirun -np 2 ogs project.prjRecommended settings
Note: OpenMP threads use shared memory and therefore cannot span multiple cluster nodes. Each MPI rank and all its chemistry threads must fit within a single node.
Single-node examples (all ranks on one node):
| Cores per node | MPI ranks | Chemistry threads/rank | Total cores used |
|---|---|---|---|
| 16 | 2 | 8 | 16 |
| 32 | 2 | 16 | 32 |
| 64 | 4 | 16 | 64 |
Multi-node jobs: Use one (or a small number of) MPI rank(s) per node and fill the remaining cores on each node with chemistry threads. The key constraint is:
ranks_per_node × threads_per_rank ≤ cores_per_nodeFor example, on a cluster with 32-core nodes and a 4-node job:
| Nodes | Ranks/node | Threads/rank | Total MPI ranks | Total chemistry parallelism |
|---|---|---|---|---|
| 4 | 2 | 16 | 8 | 8 × 16 = 128 |
| 4 | 4 | 8 | 16 | 16 × 8 = 128 |
Chemistry throughput scales with the total parallelism (ranks × threads), so both knobs contribute equally to the chemistry phase. The two rows above therefore reach the same chemistry throughput. The trade-off is on the transport side and on resource usage: more MPI ranks improve transport scalability but increase communication overhead and per-node memory (each rank loads its own PHREEQC database), while more threads per rank reduce both at fixed total parallelism but cannot span nodes. The optimal balance depends on the ratio of transport to chemistry cost for your specific problem.
General guidelines:
- Keep
ranks_per_node × threads_per_rank = cores_per_nodeto fully utilise each node - Use your MPI launcher’s binding options (e.g.
--ntasks-per-node,--cpus-per-taskin SLURM) to ensure each rank’s threads stay on the same node - Benchmark with a few time steps to find the optimal rank/thread ratio
Example SLURM job scripts
Single node (2 ranks × 8 threads = 16 cores):
#!/bin/bash
#SBATCH --job-name=reactive_transport
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=2 # 2 MPI ranks on this node
#SBATCH --cpus-per-task=8 # 8 OpenMP threads per rank
#SBATCH --mem=16G
#SBATCH --time=24:00:00
export OGS_CHEM_THREADS=8
export OMP_PROC_BIND=close
export OMP_PLACES=cores
srun ogs your_project.prj -o outputMulti-node (4 nodes × 2 ranks/node × 16 threads/rank = 128 chemistry threads):
#!/bin/bash
#SBATCH --job-name=reactive_transport
#SBATCH --nodes=4
#SBATCH --ntasks-per-node=2 # 2 MPI ranks per node
#SBATCH --cpus-per-task=16 # 16 OpenMP threads per rank
#SBATCH --mem=64G
#SBATCH --time=24:00:00
export OGS_CHEM_THREADS=16
export OMP_PROC_BIND=close
export OMP_PLACES=cores
srun ogs your_project.prj -o outputUsing --ntasks-per-node and --cpus-per-task together tells SLURM to
pin each rank to a fixed set of cores on one node, so OpenMP threads
never migrate across node boundaries.
Configuration
There are two ways to set the number of chemistry threads. Both work identically in OpenMP-only and hybrid MPI+OpenMP mode.
Environment variable
Set OGS_CHEM_THREADS before running OGS:
export OGS_CHEM_THREADS=8If OGS_CHEM_THREADS is not set, the number of assembly threads
(OGS_ASM_THREADS) is used as the default. If neither variable is set,
chemistry runs sequentially (single-threaded).
Project file parameter
Add <chemistry_threads> inside <chemical_system>:
<chemical_system chemical_solver="Phreeqc">
<chemistry_threads>8</chemistry_threads>
<!-- ... -->
</chemical_system>Priority: The project file setting overrides the environment variable.
Memory Considerations
Each chemistry thread creates an independent PHREEQC instance, which loads the thermodynamic database into memory. Typical memory usage:
- ~10-50 MB per PHREEQC instance (depends on database size)
- 8 threads = 80-400 MB additional memory per OGS process (in hybrid MPI+OpenMP mode, this cost is incurred on each MPI rank)
For memory-constrained systems, balance thread count against available RAM.
Performance Tips
- Profile first: Run with different thread counts to find the optimal configuration for your specific problem
- Avoid over-committing CPUs: Ensure
threads <= physical cores(OpenMP-only) orranks x threads <= physical cores(hybrid) - Consider problem size: Small problems may not benefit from many threads due to overhead
- Monitor memory: Use
htopor similar to check memory usage when increasing threads
Troubleshooting
Chemistry threads not working
- Verify OpenMP is enabled: check CMake output for
OpenMP: YES - Check environment variable:
echo $OGS_CHEM_THREADS - Ensure
use_stream_for_data_exchangeistrue(default) in your project file
Slower than expected
- Reduce threads if memory-bound (check swap usage with
free -h) - Ensure no CPU over-commitment
- Check if problem is too small for parallel overhead
Different results with threads
Results should be identical within floating-point precision (typically < 1e-10 relative difference). If results differ significantly, please report this as a bug.
Debugging parallel issues
Run with a single thread first to verify the sequential case works:
# OpenMP-only
OGS_CHEM_THREADS=1 ogs project.prj
# Hybrid
OGS_CHEM_THREADS=1 mpirun -np 2 ogs project.prjThen gradually increase threads to identify where issues occur.
Technical Details
The parallel chemistry implementation uses:
- Multiple PHREEQC instances: Each thread has its own isolated PHREEQC instance
- Dynamic scheduling:
schedule(dynamic)handles load imbalance between chemical systems - Thread-safe design: Each chemical system is processed by exactly one thread
- Exception handling: Failed chemical systems are collected into a
mutex-guarded list inside the parallel region; after it ends, any failure is
raised collectively across MPI ranks via
BaseLib::MPI::allRanksThrowOrNone
The parallel execution follows this pattern:
- Generate input strings for all chemical systems (sequential)
- Execute PHREEQC calculations in parallel with OpenMP
- Parse output strings and update shared state (sequential)
This article was written by OpenGeoSys Community. If you are missing something or you find an error please reach out
to us on our forum.
Generated with Hugo 0.164.0
in CI job 825193
|
Last revision: April 20, 2026
Commit: refactor(MeshLib): remove PropertyVector::push_back 870422dde
| Edit this page on