Source code for mcframework.backends.torch_cpu

r"""
Torch CPU execution backend for Monte Carlo simulations.

This module provides:

Classes
    :class:`TorchCPUBackend` — Torch-based batch execution on CPU

The CPU backend enables vectorized execution using PyTorch on CPU,
providing a good balance of speed and compatibility.

Notes
-----
**When to use CPU backend:**

- Baseline testing before GPU deployment
- Systems without GPU acceleration
- Debugging and validation
- Small to medium simulation sizes

**RNG discipline.** Uses explicit :class:`torch.Generator` objects seeded from
:class:`numpy.random.SeedSequence`. Fully deterministic with same seed.
"""

from __future__ import annotations

import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

import numpy as np

from .torch_base import ensure_torch_batch_overridden, import_torch, make_torch_generator

if TYPE_CHECKING:
    from ..simulation import MonteCarloSimulation

logger = logging.getLogger(__name__)

__all__ = ["TorchCPUBackend"]


[docs] class TorchCPUBackend: r""" Torch CPU batch execution backend. Uses PyTorch for vectorized execution on CPU. Requires simulations to implement :meth:`~mcframework.core.MonteCarloSimulation.torch_batch` and set :attr:`~mcframework.simulation.MonteCarloSimulation.supports_batch` to ``True``. Notes ----- **RNG architecture.** Uses explicit :class:`torch.Generator` objects seeded from :meth:`numpy.random.SeedSequence.spawn`. This preserves: - Deterministic parallel streams - Counter-based RNG (Philox) semantics - Identical statistical structure across backends **Never uses** :func:`torch.manual_seed` (global state). Examples -------- >>> backend = TorchCPUBackend() >>> results = backend.run(sim, n_simulations=100000, seed_seq=seed_seq) # doctest: +SKIP """ device_type: str = "cpu" _MAX_BATCH: int = 10_000_000
[docs] def __init__(self, max_batch_size: int | None = None): """ Initialize Torch CPU backend. Parameters ---------- max_batch_size : int or None, default None Maximum number of simulations per batch. If *None* the class default ``_MAX_BATCH`` (10 M) is used. Workloads larger than this are split into batches to keep memory bounded. Raises ------ ImportError If PyTorch is not installed. """ th = import_torch() self.device = th.device("cpu") if max_batch_size is None: self._max_batch = self._MAX_BATCH elif max_batch_size <= 0: raise ValueError("max_batch_size must be a positive integer") else: self._max_batch = max_batch_size
[docs] def run( self, sim: MonteCarloSimulation, n_simulations: int, seed_seq: np.random.SeedSequence | None, progress_callback: Callable[[int, int], None] | None = None, **_simulation_kwargs: Any, ) -> np.ndarray: r""" Run simulations using Torch CPU batch execution. Parameters ---------- sim : MonteCarloSimulation The simulation instance to run. Must have :attr:`~mcframework.simulation.MonteCarloSimulation.supports_batch` = ``True`` and implement :meth:`~mcframework.core.MonteCarloSimulation.torch_batch`. n_simulations : int Number of simulation draws to perform. seed_seq : SeedSequence or None Seed sequence for reproducible random streams. progress_callback : callable or None Optional callback ``f(completed, total)`` for progress reporting. **_simulation_kwargs : Any Ignored for Torch backend (batch method handles all parameters). Returns ------- np.ndarray Array of simulation results with shape ``(n_simulations, ...)``. Raises ------ ValueError If the simulation does not support batch execution. NotImplementedError If the simulation does not implement :meth:`~mcframework.core.MonteCarloSimulation.torch_batch`. """ th = import_torch() if not getattr(sim, "supports_batch", False): raise ValueError( f"Simulation '{sim.name}' does not support Torch batch execution. " "Set supports_batch = True and implement torch_batch()." ) ensure_torch_batch_overridden(sim) logger.info( "Computing %d simulations using Torch CPU batch...", n_simulations, ) if n_simulations <= self._max_batch: generator = make_torch_generator(self.device, seed_seq) samples = sim.torch_batch(n_simulations, device=self.device, generator=generator) samples = samples.detach().to(th.float64) if progress_callback: progress_callback(n_simulations, n_simulations) return samples.numpy() n_batches = (n_simulations + self._max_batch - 1) // self._max_batch batch_seeds = seed_seq.spawn(n_batches) if seed_seq else [None] * n_batches parts: list[np.ndarray] = [] completed = 0 for bs in batch_seeds: batch_n = min(self._max_batch, n_simulations - completed) generator = make_torch_generator(self.device, bs) samples = sim.torch_batch(batch_n, device=self.device, generator=generator) parts.append(samples.detach().to(th.float64).numpy()) completed += batch_n if progress_callback: progress_callback(completed, n_simulations) return np.concatenate(parts)