A.9 Numerical reproducibility errors
In this section we describe cases where calculations fail to reproduce the same numerical results on different platforms, or indeed after multiple runs on the same platform. We discuss two common error messages that result, as well as their possible cause and remedy, which typically is due to some underlying software-hardware interconnectivity problem.
The first, and more common error we have observed is a so called “charge integration error” where the sum of the atomic charges computed from the density differs from the expected number of electrons in the system by a larger than prescribed amount. This occurs after an SCF step, and for problematic systems may occur at a random SCF step, or not at all. We deal with possible causes and solutions for this in Section A.9.1.
The second, and less commonly observed error is a non-positive definite overlap matrix. This occurs in the SCF initialization step, before a single SCF iteration is performed, during the diagonalization of the overlap matrix. Typically negative, or other non-sensical eigenvalues of the overlap matrix will result. Note that this is a more severe error than the so-called “ill-conditioning” of the overlap matrix, where eigenvalues are smaller than a prescribed threshold, which is typically due to overcompleteness of the basis set. Rather, a non-positive definite matrix may be an indication of an underlying software or hardware problem, and we deal with possible causes and solutions in Section A.9.2. Similar to charge integration errors, these may appear randomly, with one calculation succeeding, and then another run on the same software/hardware stack with the same inputs failing.
Please note also that charge integration and non-positive overlap matrix errors may co-occur on the same system, for the same input, with e.g. one run producing a charge integration error and one producing a non-positive definite overlap matrix error. In this case, one should try the debugging steps in both sections below to address the problem. Furthermore, if you are only observing charge integration errors but none of the remedies work, then please also try the approaches to remedy the non-positive definite overlap matrix in case these can help.
A.9.1 Charge integration errors
On some platforms, we have observed reproducible erroneous calculations for single- and multi-node submissions, including reproducible charge integration errors, SCF instabilities, and wrong energies.
Single-node runs are fine, only multi-node runs are affected
The main culprit appears to be the MPI interconnect setup, and the choice of libfabric / tuning file that is used during the job. A workaround is manually setting an alternative Intel MPI tuning binary[1] in the submission script via the variable I_MPI_TUNING_BIN.
If you are not sure where the MPI tuning binaries are stored on your system, then you can add the following MPI debug option into your job submission script (for bash scripts; in csh use setenv rather than export):
export I_MPI_DEBUG=10
At the top of your subsequent aims.out file, you will see a line, e.g.
[0] MPI startup(): Load tuning file:
"/intel/mpi/2021.12/opt/mpi/etc/tuning_generic_shm.dat"
This may not correspond to the correct file, or the file itself may not exist. The solution to this problem is to manually select the tuning file. Some indication about the correct one could come from the interconnect that is used on your system — for example, Mellanox, Elastic Fabric Adapter, or Ethernet — and the corresponding libfabric, e.g. mlx, efa or tcp. If information about the interconnect does not exist in the documentation for your cluster, the command lspci (if available) should give some information about the interconnects, and the command fi_info -l (if available) should also give some information about the fabric providers available. The following pages from Intel should give some more background information about these topics[2][3][4][5].
A tuning file corresponding, for example, to a Mellanox interconnect with mlx libfabric could be:
tuning_generic_shm-ofi_mlx_hcoll.dat
and this can be set in a job submission script via
export I_MPI_TUNING_BIN=<absolute_path>/<tuning_file>.dat
We have observed that the generic files are the ones that work with AMD systems, as opposed to ones with e.g. skx in the name (which stands for Intel SkyLake CPU). However, this can be a process of trial and error. Intel documents tuning files as performance configurations for particular hardware, so you should select only a file that matches the CPU, provider, and Intel MPI release. Validate the tuning file by checking whether the error disappeared, and record the file that gave you the most correct results for your particular hardware.
In case this does not help, some other things to test are to change the following environment variables:
export I_MPI_OFI_PROVIDER=<PROVIDER> export I_MPI_FABRICS=<FABRIC>
Some information about I_MPI_OFI_PROVIDER is provided by Intel[2][3], as well as for I_MPI_FABRICS[4][5].
If you are still experiencing problems, a software stack that we have observed to work with manually selecting the tuning file is to use Intel oneAPI 2024, in combination with an externally installed ELPA 2024. For further information on installing and linking external ELPA to FHI-aims, please see here[6].
Finally, in order to test that everything is working correctly, we recommend running the paracetamol test case included in the benchmarks folder on 1 and 2 nodes, and ensuring that the energies you obtain match those in the benchmark result.
Both single- and multi-node runs are affected
This behaviour was observed on AMD CPU clusters in combination with the Intel MPI library, often with shared calculation nodes — nodes on which more than one job (potentially belonging to different users) can run at the same time. On these systems the erroneous results were found to be correlated with topology aware collective MPI algorithms, and/or MPI process pinning. Below, we describe first the topology aware collective MPI algorithms and approaches to solve this issue, as we have the most direct evidence for this being the issue, and so should be attempted first before addressing MPI process pinning.
1. What are topology aware collective MPI algorithms?
Collective MPI algorithms are communication routines that coordinate data movement or global computation across all processes within a specified communicator, such as MPI_Bcast (which copies data from one "root" process to all other processes) and MPI_Allreduce (which applies a reduction operation, e.g. a sum, to data across all processes and distributes the result back to all processes). Such routines are used extensively throughout FHI-aims, for example in the eigensolver ELPA. By default Intel MPI chooses the collective communication algorithms at runtime from message size, communicator size, and node topology, from the multiple possible algorithms that are available [7]. On AMD CPUs, for certain task counts and matrix sizes, the topology-aware, shared-memory variants of these algorithms appear to be problematic, returning different numerical results for a fixed input. One of the ELPA developers, Petr Karpov, has produced a minimal reproducer illustrating this problem with MPI_Bcast + MPI_Allreduce[8].
How to resolve issues with topology aware collective MPI algorithms
One method to disable these topology-aware shared memory collective algorithms is to set the environment variable:
export I_MPI_COLL_INTRANODE = pt2pt
which uses only point-to-point communication based collectives, and does not use shared memory ones.
If this does not resolve the problem, then one can instead completely disable topology aware collectives via the environment variable
export I_MPI_CBWR=1
This enforces Conditional Numerical Reproducibility (CNR), which provides controls for obtaining reproducible floating-point results on collective operations. With this feature, Intel MPI collective operations are designed to return the same floating-point results from run to run in the case of the same number of MPI ranks. The value 1 disables topology-aware collectives, so that the result of a collective operation does not depend on the rank placement. The mode guarantees reproducibility of results across different runs on the same cluster (independent of the rank placement). Our tests proved it to be sufficient to avoid charge integration errors popping up randomly for a given system and a given number of cores. This can incur some performance penalty, which based on limited testing is greater than that imposed by I_MPI_COLL_INTRANODE, and is why we recommend attempting that approach first.
CNR is also possible to enforce with the Intel MKL library, and will be discussed in Section A.9.2, as it has been observed to help the non-positive definite overlap matrix error. If setting I_MPI_CBWR=1 does not remedy the charge integration error on its own, we would recommend combining this with the Intel MKL CNR discussed in Section A.9.2 to have CNR across the whole software stack.
If none of these approaches work, then next we would recommend to control MPI process pinning.
2. What is process pinning?
Process pinning (also called process affinity or CPU binding) is the binding of each MPI rank to a fixed set of logical CPUs, so that the operating-system scheduler is not free to move (“migrate”) the rank onto other cores while it runs.[9]
Where a rank runs matters on a modern multi-socket compute node for several reasons:
-
•
Non-uniform memory access (NUMA) locality. Memory that a rank allocates is placed physically close to the core that first writes to it (the Linux “first-touch” policy). If the rank later migrates to a core on a different socket / NUMA domain, every access to that memory has to cross the inter-socket link, which is substantially slower.
-
•
Caches. Migration cold-starts the L1 / L2 caches the rank was using.
On a dedicated node, poor pinning normally only costs performance. On a shared node the failure modes are more severe: ranks can be squeezed onto a handful of cores, migrated unpredictably as other jobs start and finish, or massively oversubscribe the cores allotted to the job. On the affected platforms this coincided with the errors described above, and enforcing correct one-rank-per-physical-core pinning resolved them.
Step 1. Control the number of threads first.
Before touching pinning, make sure each rank is not silently launching extra threads. FHI-aims can link a threaded BLAS / LAPACK (typically Intel MKL); left at its defaults, each rank may start one MKL thread per core, so ranks threads badly oversubscribe the node. For the standard pure-MPI FHI-aims setup, force single-threaded math:
export OMP_NUM_THREADS=1 export MKL_NUM_THREADS=1
Step 2. Re-run with a different number of cores.
MPI can use different internal algorithms for carrying out operations based on the number of cores, matrix sizes and types involved, inner topology of the MPI process map, etc (as discussed above in relation to the topology aware MPI collectives). This can legally reorder floating-point operations and be the reason for large floating-point errors resulting in the charge integration error. If you are free to choose, just changing the number of cores can be sufficient to change the sizes of the matrices involved in the computation in one MPI process, and thus shift the algorithm choice from a faulty one to one that works.
Step 3. Enforce one rank per physical core.
When fewer ranks are launched than there are cores on the node, Intel MPI does not pin each rank to a single core by default; instead it gives every rank a domain spanning several logical CPUs, shown in a pinning table (which is printed to stdout if I_MPI_DEBUG is more than 4) as a range in curly braces:
[0] MPI startup(): Rank Pid Node name Pin cpu
[0] MPI startup(): 0 30126 ip-172-31-23-142 {0-2}
[0] MPI startup(): 1 30127 ip-172-31-23-142 {3-5}
[0] MPI startup(): 2 30128 ip-172-31-23-142 {6-8}
...
Here 32 ranks on a 96-core node each receive a 3-core domain. A rank is then free to migrate within its domain, and — more importantly — Intel MPI’s algorithms make assumptions about where each rank sits that a loose, multi-core domain can violate. We also observed situations where the pinning table showed 1-to-1 pinning on logical cores on a system with hyper-threading; this too was sufficient to get the charge integration error.
To force strict one-rank-per-physical-core (1-to-1) pinning with mpirun, set the domain size to a single physical core:
export I_MPI_PIN_DOMAIN=1 export I_MPI_PIN_CELL=core
Then confirm from the debug output that every rank shows a single-CPU {n} mask.
If you launch through Slurm, do not rely on a bare srun. Bind the tasks to cores explicitly:
srun --cpu-bind=cores ...
A.9.2 Non-positive definite overlap matrix errors
As mentioned in the introduction to this Appendix section, we have observed non-positive definite overlap matrix errors more rarely than charge integration errors. However, unlike charge integration errors, we have more commonly seen this on Intel, rather than AMD CPUs, in combination with an Intel software stack (compiler, MPI library and MKL). A potential solution to this problem is to enforce conditional numerical reproducibility for the Intel MKL library, via the MKL_CBWR flag [10]. This should be set equal to the instruction set architecture that your CPU supports (e.g. AVX, AVX2, AVX512) plus the STRICT keyword. For example
export MKL_CBWR="AVX2,STRICT"
If this, on it’s own, does not fix the issue, it is recommended to pair with CNR on the Intel MPI library, as documented in Section A.9.1 via
export I_MPI_CBWR=1
References
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.