Skip to content

Parallelism

MDAPY runs its compiled kernels on a thread pool of its own. This page shows how to choose the number of threads, how to follow and stop long computations, and how MDAPY behaves next to PyTorch and LAMMPS.

import mdapy as mp

mp.get_num_threads()  # default: the logical CPUs this process may use
mp.set_num_threads(8)  # for the rest of the process

with mp.num_threads(2):  # only inside this block
    ...

Choosing the number of threads

Name Type Default Meaning
set_num_threads(n) function Use n threads from now on.
num_threads(n) context manager Use n threads inside a with block, then restore the previous value.
get_num_threads() function The current number of threads.
MDAPY_NUM_THREADS environment variable logical CPUs this process may use Initial number of threads, read once when MDAPY is imported.

n must be a positive integer; anything else raises InvalidParameter. An invalid MDAPY_NUM_THREADS makes import mdapy fail with the same error, rather than silently falling back to a default.

The setting is global to the process, not to the calling Python thread.

Progress and cancellation

Computations that can take long, such as NeighborList.build, accept two keyword-only arguments.

Name Type Default Meaning
progress callable or None None Called as progress(done, total) with the number of finished and total work items.
cancel CancelToken or None None Stops the computation once token.cancel() is called.

progress is called on the thread that started the computation, which also does a share of the work, between its pieces of work and at most every 50 ms, and once more with done == total when it finishes. done never decreases. If progress raises an exception, the computation stops and that exception propagates to the caller.

A cancelled computation raises Cancelled. The token can be cancelled from any thread, including from inside progress, and can be reused after token.reset().

token = mp.CancelToken()
# in a GUI: connect a "Stop" button to token.cancel
try:
    result = some_analysis.compute(system, progress=report, cancel=token)
except mp.errors.Cancelled:
    ...

Pressing Ctrl-C stops a running kernel and raises KeyboardInterrupt as usual.

Reproducible results

Results do not depend on the number of threads: the same computation with 1 or 64 threads gives bit-for-bit identical numbers.

Working with PyTorch and LAMMPS

MDAPY does not use OpenMP and loads no OpenMP runtime. Two copies of an OpenMP runtime in one process (for example mdapy's and PyTorch's) is a common cause of crashes such as OMP: Error #15 on macOS; this cannot happen with MDAPY, so no workaround such as KMP_DUPLICATE_LIB_OK is needed.

Other libraries keep their own thread settings:

  • PyTorch: torch.set_num_threads.
  • NumPy (BLAS): the OMP_NUM_THREADS or OPENBLAS_NUM_THREADS environment variables.
  • polars: the POLARS_MAX_THREADS environment variable, which must be set before polars is first imported.

When several libraries compute at the same time, keep the total number of threads close to the number of CPU cores. After each piece of parallel work, mdapy's idle threads keep checking for more for 0.2 ms before they sleep, so that the steps of one computation follow each other without waking them.

Calling MDAPY from several threads

Compiled kernels release the Python global interpreter lock (GIL), so other Python threads, for example a GUI event loop, keep running while MDAPY computes.

The pool serves one computation at a time. If a second Python thread starts a computation while the pool is busy, that computation runs on its own thread instead of waiting. It gives the same result, only without parallel speed-up.

Many processes

Every process starts as many threads as there are CPUs, so when many processes compute at once, give each of them one thread: ten processes without a limit were six to seven times slower than ten with one thread each. See Screening alloys in many processes.

Start processes with spawn, as multiprocessing.get_context("spawn") gives it, never with fork. MDAPY's own pool works with fork, a forked child creating its own on first use, but this is a limit of polars: once a parent has used its threads, as the functions of MDAPY do, a forked child that needs them hangs without a message. Linux uses fork by default before Python 3.14, so ask for spawn there. MDAPY gives a RuntimeWarning in every process that was forked.