Install EBFM with the performance features:
pip install ebfm[performance]This additionally installs numba to run with multiple CPU-threads.
For GPU offloading, install the GPU dependencies instead:
pip install ebfm[gpu]Note: on a cluster you usually have to load a CUDA/ROCm toolkit and point CUDA_HOME at it, otherwise Numba
cannot compile the kernels even though the GPU itself is visible. See
CUDA runtime not found below.
It is recommended to verify your cuda setup by running:
python -c "from numba import cuda; print(cuda.is_available())"
# expected output: TrueEBFM offers several options for tuning performance on a given system and for testing and benchmarking it.
The option --with-numba runs the LOOP_SNOW.py kernels (compaction, heat conduction, percolation) as compiled,
multi-threaded Numba kernels instead of NumPy array operations. Use it on a multi-core CPU when no GPU is available.
It generally outperforms the NumPy path, the more clearly the larger the mesh and the longer the run.
It is not on by default because numba is an optional dependency and the kernels compile on first use. A very short run can therefore spend more time compiling than it saves.
Control the number of threads with --numba-threads N (replace N with the desired thread count).
Example: Run EBFM on the MATLAB example mesh with the Numba kernels on two threads:
ebfm --matlab-mesh examples/dem_and_mask.mat --with-numba --numba-threads 2Note: If you use more than one thread, you must specify --numba-threads. In practice, 2 threads have shown the
best performance so far, but optimal settings depend on your hardware and problem size. Feel free to experiment.
--numba-threads is per MPI rank and cannot exceed the number of CPUs the rank may run on. Under MPI and Slurm this
is usually less than the number of CPUs of the node: mpirun or srun may bind each rank to a few cores, and Slurm
only grants the CPUs of the allocation. EBFM logs Numba's limit and the usable CPUs at startup, and stops with an error
if --numba-threads exceeds them (see Troubleshooting).
You can use the option --with-gpu when the node you run on has a GPU: it offloads the same kernels to the device as for the option --with-numba. How much that
gains depends on the mesh size and the GPU, and for small meshes it is not necessarily faster than --with-numba.
The options --with-gpu and --with-numba tell EBFM to select alternative backends for those kernels and are therefore mutually exclusive.
Example: Run EBFM on the MATLAB example mesh with the GPU kernels:
ebfm --matlab-mesh examples/dem_and_mask.mat --with-gpuThe same kernels run on NVIDIA and AMD GPUs, see --gpu-vendor in ebfm --help.
When --with-gpu is set, EBFM will report at startup which vendor backend (nvidia or amd) and which device have been found, so you can check it went to the GPU you expected:
[GPU] backend enabled (nvidia). Device: NVIDIA A100-SXM4-80GB free=78.83 GiB total=79.15 GiB
To measure the total runtime prepend your command with
time:
time ebfm --matlab-mesh examples/dem_and_mask.mat --with-numba --numba-threads 2The shell then prints the wall-clock (real) and CPU times, for example:
real 1m12.771s
user 2m18.492s
sys 0m2.113s
For benchmarking, compare real between runs: user exceeding it only means several threads ran in parallel.
The script tools/compare_snapshots.py allows to compare two output files (e.g., from different runs or configurations).
To create a reference file for comparison, use the --dump-reference option at the end of your run:
ebfm --matlab-mesh examples/dem_and_mask.mat --dump-reference reference_run.npzThen compare with:
python tools/compare_snapshots.py reference_run.npz new_run.npzNote: uncoupled runs (such as the example testcase) generate their climate forcing randomly, so make sure to additionally set the option --random-seed, as explained below.
-
The option
--random-seedallows reproducible runs (especially if the random forcing in the example testcase is used). Set a fixed seed to ensure identical results for repeated runs (important for benchmarking and debugging):ebfm --matlab-mesh examples/dem_and_mask.mat --random-seed 42
-
Use
--print-diagnosticsto print some diagnostics for a quick overview for every timestep:ebfm --matlab-mesh examples/dem_and_mask.mat --print-diagnostics
Problem: --with-gpu fails, or python -c "from numba import cuda; print(cuda.is_available())" prints False, even though the GPU is visible. Note that cuda.detect() and nvidia-smi can still report the device correctly: they only need the CUDA driver, while Numba additionally needs the CUDA runtime (libnvvm, libcudart) to compile kernels.
Solution: Point CUDA_HOME at a CUDA toolkit installation and add its libraries to LD_LIBRARY_PATH. On Levante, for example:
export CUDA_HOME=/sw/spack-levante/nvhpc-22.5-v4oky3/Linux_x86_64/22.5/cuda/11.7
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATHAfterwards the check above should succeed:
python -c "from numba import cuda; print(cuda.is_available())"
# expected output: TrueProblem: EBFM stops at startup with
error: --numba-threads=8 exceeds Numba's thread limit of 2: this process may only run on 2 CPUs. ...
Numba can run at most as many threads as the CPUs the process is allowed to run on (its CPU affinity), or as many as
the environment variable NUMBA_NUM_THREADS allows if it is set. Under MPI the affinity is often much smaller than the
node: OpenMPI binds each rank to a single core by default when it starts 2 or fewer processes, e.g. one EBFM rank plus
one coupled component. With srun, each task may only run on the CPUs it requested with --cpus-per-task. On Levante
a core has 2 hardware threads and Slurm allocates whole cores, hence a limit of 2 for a single core or
--cpus-per-task=1.
To check the limit Numba sees, launch Python exactly like EBFM, e.g. with mpirun or srun:
mpirun -n 1 python -c "import os, numba; print(len(os.sched_getaffinity(0)), numba.config.NUMBA_NUM_THREADS)"
srun -n 1 python -c "import os, numba; print(len(os.sched_getaffinity(0)), numba.config.NUMBA_NUM_THREADS)"Solution: Use one of
mpirun --bind-to none ...: each rank may run on all CPUs of the Slurm allocation. Simple, and suitable for interactive runs with few ranks per node; ranks on the same node then share these CPUs.mpirun --map-by slot:PE=N ...with an allocation that has enough CPUs per task (e.g.salloc -c ...): each rank is bound to its own N cores. Preferable for production runs with many ranks per node.- with
srun:--cpus-per-task=Nfor the job and forsrunitself, e.g.srun --cpus-per-task="$SLURM_CPUS_PER_TASK" ...in a batch script. On Levante N counts hardware threads. - a lower
--numba-threads.
If the error names NUMBA_NUM_THREADS, that environment variable is set below --numba-threads: raise or unset it.