"""Global configuration for the computational backend used by dynasor's
compute functions.
Keeping this outside the compute functions' own signatures means
``compute_dynamic_structure_factors``/``compute_static_structure_factors``
don't need ``backend``/``precision``/``batch_size`` kwargs of their own --
set the backend once, and every subsequent call uses it::
from dynasor import set_compute_config, compute_dynamic_structure_factors
set_compute_config(backend='torch', precision='float32')
sample = compute_dynamic_structure_factors(traj, q_points, dt=25.0, window_size=2000)
See :ref:`the backends reference page <backends>` for the full picture,
including which parts of a calculation this actually affects.
"""
import importlib
from dataclasses import dataclass
from typing import Optional
_VALID_BACKENDS = ('numba', 'torch', 'cupy')
_VALID_PRECISIONS = ('float64', 'float32')
_DEFAULT_BATCH_SIZE = 32
@dataclass(frozen=True)
class ComputeConfig:
"""Immutable snapshot of the current compute configuration.
Attributes
----------
backend
One of ``'numba'``, ``'torch'``, or ``'cupy'``.
precision
One of ``'float64'`` or ``'float32'``. Only affects the GPU backends;
ignored for ``backend='numba'``.
batch_size
Number of trajectory frames stacked into a single GPU kernel call.
Only affects the GPU backends; ignored for ``backend='numba'``. Larger
values make better use of GPU memory bandwidth for small systems, but
scale up GPU memory usage roughly linearly (``batch_size * N_atoms *
N_qpoints``) -- reduce it for large systems/q-point grids to avoid an
out-of-memory error. See :ref:`the backends reference page <backends>`.
"""
backend: str
precision: str
batch_size: int
_config = ComputeConfig(backend='numba', precision='float64', batch_size=_DEFAULT_BATCH_SIZE)
[docs]
def set_compute_config(
backend: Optional[str] = None,
precision: Optional[str] = None,
batch_size: Optional[int] = None,
) -> None:
"""Set the computational backend, precision, and/or GPU batch size used
by all subsequent calls to
``compute_dynamic_structure_factors``/``compute_static_structure_factors``.
Parameters
----------
backend
One of ``'numba'`` (CPU, default), ``'torch'``, or ``'cupy'`` (both CUDA
GPU, optional dependencies). Leave unset to keep the current value.
precision
One of ``'float64'`` (default) or ``'float32'``. Only affects the GPU
backends. Leave unset to keep the current value.
batch_size
Number of trajectory frames stacked into a single GPU kernel call
(default 32). Only affects the GPU backends. Leave unset to keep the
current value; see :attr:`ComputeConfig.batch_size` for tuning guidance.
Examples
--------
>>> from dynasor import set_compute_config
>>> set_compute_config(backend='torch', precision='float32')
"""
global _config
new_backend = _config.backend if backend is None else backend
new_precision = _config.precision if precision is None else precision
new_batch_size = _config.batch_size if batch_size is None else batch_size
if new_backend not in _VALID_BACKENDS:
raise ValueError(f'backend must be one of {_VALID_BACKENDS}, got {new_backend!r}')
if new_precision not in _VALID_PRECISIONS:
raise ValueError(f'precision must be one of {_VALID_PRECISIONS}, got {new_precision!r}')
if not isinstance(new_batch_size, int) or isinstance(new_batch_size, bool) \
or new_batch_size < 1:
raise ValueError(f'batch_size must be a positive integer, got {new_batch_size!r}')
_config = ComputeConfig(backend=new_backend, precision=new_precision,
batch_size=new_batch_size)
_GPU_BACKEND_PACKAGES = {'torch': ('torch', 'gpu-torch'), 'cupy': ('cupy', 'gpu-cupy')}
def check_backend_is_available(backend: str) -> None:
"""Check that *backend* can run on this machine, and raise a message
naming what is missing if it cannot.
Selecting a backend only records a name, so the optional dependency and
the CUDA device it needs are looked at here, where the calculation is
about to start. Without this the first thing the user sees is an error
from torch or cupy about a missing device, raised from inside dynasor
after the trajectory has already been opened.
Parameters
----------
backend
One of the backends accepted by :func:`set_compute_config`.
``'numba'`` needs nothing and always passes.
Raises
------
RuntimeError
If the package the backend needs is not installed, or if it is
installed but reports no CUDA device.
Examples
--------
>>> from dynasor.compute_config import check_backend_is_available
>>> check_backend_is_available('numba')
"""
if backend not in _GPU_BACKEND_PACKAGES:
return
package, extra = _GPU_BACKEND_PACKAGES[backend]
try:
module = importlib.import_module(package)
except ImportError as error:
raise RuntimeError(
f'backend {backend!r} requires the {package} package, which is not installed; '
f'install it with "pip install dynasor[{extra}]", or use backend="numba" to run '
'on the CPU') from error
is_available = module.cuda.is_available if backend == 'torch' else module.is_available
if not is_available():
raise RuntimeError(
f'backend {backend!r} requires a CUDA device, but {package} reports none; use '
'backend="numba" to run on the CPU. See the backends page of the dynasor '
'documentation for how to select a device.')
[docs]
def get_compute_config() -> ComputeConfig:
"""Return the current compute configuration.
Returns
-------
ComputeConfig
The current ``backend``/``precision``/``batch_size`` setting;
``ComputeConfig(backend='numba', precision='float64', batch_size=32)``
if :func:`set_compute_config` has never been called.
"""
return _config