Source code for mcframework.backends.torch_base
r"""
Base Torch utilities for GPU-accelerated Monte Carlo simulations.
This module provides shared utilities for all Torch-based backends:
Functions
:func:`make_torch_generator` — Create explicit Torch RNG generators
:func:`validate_torch_available` — Check if PyTorch is installed
Constants
:data:`VALID_TORCH_DEVICES` — Supported Torch device types
Notes
-----
**RNG discipline.** All random sampling uses explicit ``torch.Generator``
objects seeded from :class:`numpy.random.SeedSequence`. Never uses global
Torch RNG (``torch.manual_seed``).
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
import torch
logger = logging.getLogger(__name__)
__all__ = [
"VALID_TORCH_DEVICES",
"make_torch_generator",
"make_curand_generator",
"validate_torch_available",
"import_torch",
]
# Valid Torch device types
VALID_TORCH_DEVICES = ("cpu", "mps", "cuda")
def import_torch():
"""
Import and return the torch module.
Returns
-------
module
The torch module.
Raises
------
ImportError
If PyTorch is not installed.
"""
try:
import torch as th # pylint: disable=import-outside-toplevel
return th
except ImportError as e:
raise ImportError(
"Torch backend requires PyTorch. Install with: pip install mcframework[gpu]"
) from e
def validate_torch_available() -> None:
"""
Check if PyTorch is installed and available.
Raises
------
ImportError
If PyTorch is not installed.
"""
import_torch()
[docs]
def make_torch_generator(
device: "torch.device",
seed_seq: np.random.SeedSequence | None,
) -> "torch.Generator":
r"""
Create an explicit Torch generator seeded from a SeedSequence.
This function spawns a child seed from the provided SeedSequence and
uses it to initialize a Torch Generator. This preserves the hierarchical
spawning model used by the NumPy backend.
Parameters
----------
device : torch.device
Device for the generator (``"cpu"``, ``"mps"``, or ``"cuda"``).
seed_seq : SeedSequence or None
NumPy seed sequence to derive the Torch seed from.
Returns
-------
torch.Generator
Explicitly seeded generator for reproducible sampling.
Notes
-----
**Why explicit generators?**
- ``torch.manual_seed()`` is global state that breaks parallel composition
- Explicit generators enable deterministic multi-stream MC
- This mirrors NumPy's ``SeedSequence.spawn()`` semantics
**Seed derivation:**
.. code-block:: python
child_seed = seed_seq.spawn(1)[0]
seed_int = child_seed.generate_state(1, dtype="uint64")[0]
generator.manual_seed(seed_int)
This ensures each call with the same ``seed_seq`` produces identical results.
Examples
--------
>>> import numpy as np
>>> import torch
>>> seed_seq = np.random.SeedSequence(42)
>>> gen = make_torch_generator(torch.device("cpu"), seed_seq) # doctest: +SKIP
"""
th = import_torch()
generator = th.Generator(device=device)
if seed_seq is not None:
# Spawn a child seed to preserve hierarchical RNG structure
child_seed = seed_seq.spawn(1)[0]
# Convert to 64-bit integer for Torch's Philox counter
seed_int = int(child_seed.generate_state(1, dtype=np.uint64)[0])
generator.manual_seed(seed_int)
else:
logger.warning(
"No seed set for Torch backend; results will not be reproducible. "
"Call set_seed() before run() for deterministic simulations."
)
return generator
def make_curand_generator(
device_id: int,
seed_seq: np.random.SeedSequence | None,
):
r"""
Create an explicit cuRAND generator seeded from a SeedSequence.
This function spawns a child seed from the provided SeedSequence and
uses it to initialize a CuPy RandomState. This preserves the hierarchical
spawning model used by the NumPy backend.
Parameters
----------
device_id : int
CUDA device index for the generator.
seed_seq : SeedSequence or None
NumPy seed sequence to derive the cuRAND seed from.
Returns
-------
cupy.random.RandomState
Explicitly seeded cuRAND generator for reproducible sampling.
Raises
------
ImportError
If CuPy is not installed.
Notes
-----
**Why explicit generators?**
- ``cupy.random.seed()`` is global state that breaks parallel composition
- Explicit generators enable deterministic multi-stream MC
- This mirrors NumPy's ``SeedSequence.spawn()`` semantics
**Seed derivation:**
.. code-block:: python
child_seed = seed_seq.spawn(1)[0]
seed_int = child_seed.generate_state(1, dtype="uint64")[0]
rng = cupy.random.RandomState(seed=seed_int)
This ensures each call with the same ``seed_seq`` produces identical results.
Examples
--------
>>> import numpy as np
>>> seed_seq = np.random.SeedSequence(42)
>>> # Requires CuPy installation
>>> # gen = make_curand_generator(0, seed_seq) # doctest: +SKIP
"""
try:
import cupy as cp # pylint: disable=import-outside-toplevel
except ImportError as e:
raise ImportError(
"cuRAND backend requires CuPy. Install with: pip install mcframework[cuda]"
) from e
# Set device context
cp.cuda.Device(device_id).use()
if seed_seq is not None:
# Spawn a child seed to preserve hierarchical RNG structure
child_seed = seed_seq.spawn(1)[0]
# Convert to 64-bit integer for cuRAND
seed_int = int(child_seed.generate_state(1, dtype=np.uint64)[0])
rng = cp.random.RandomState(seed=seed_int)
else:
logger.warning(
"No seed set for cuRAND backend; results will not be reproducible. "
"Call set_seed() before run() for deterministic simulations."
)
rng = cp.random.RandomState()
return rng