chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Note: This import is to avoid circular import errors
import ray.train.torch # noqa: F401
+213
View File
@@ -0,0 +1,213 @@
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union
from ray.train import Checkpoint, DataConfig
from ray.train.trainer import GenDataset
from ray.train.v2._internal.execution.local_mode.torch import LocalTorchController
from ray.train.v2.api.config import RunConfig, ScalingConfig
from ray.train.v2.api.data_parallel_trainer import DataParallelTrainer
from ray.train.v2.api.validation_config import ValidationConfig
from ray.util import PublicAPI
if TYPE_CHECKING:
# NOTE: `ray.train.torch` module imports in this file will break
# with a circular import error if the TorchTrainer class is captured
# in the scope of a Ray task.
from ray.train.torch.config import TorchConfig
@PublicAPI(stability="stable")
class TorchTrainer(DataParallelTrainer):
"""A Trainer for data parallel PyTorch training.
At a high level, this Trainer does the following:
1. Launches multiple workers as defined by the ``scaling_config``.
2. Sets up a distributed PyTorch environment
on these workers as defined by the ``torch_config``.
3. Ingests the input ``datasets`` based on the ``dataset_config``.
4. Runs the input ``train_loop_per_worker(train_loop_config)``
on all workers.
For more details, see:
* :ref:`PyTorch Guide <train-pytorch>`
* :ref:`PyTorch Lightning Guide <train-pytorch-lightning>`
* :ref:`Hugging Face Transformers Guide <train-pytorch-transformers>`
Example:
.. testcode::
import os
import tempfile
import torch
from torch import nn
from torch.nn.parallel import DistributedDataParallel
import ray.train
from ray.train.torch import TorchTrainer
# If using GPUs, set this to True.
use_gpu = False
# Number of processes to run training on.
num_workers = 2
# Define your network structure.
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.layer1 = nn.Linear(1, 32)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(32, 1)
def forward(self, input):
return self.layer2(self.relu(self.layer1(input)))
# Training loop.
def train_fn_per_worker(config):
# Read configurations.
lr = config["lr"]
batch_size = config["batch_size"]
num_epochs = config["num_epochs"]
# Fetch training dataset.
train_dataset_shard = ray.train.get_dataset_shard("train")
# Instantiate and prepare model for training.
model = NeuralNetwork()
model = ray.train.torch.prepare_model(model)
# Define loss and optimizer.
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
# Create data loader.
dataloader = train_dataset_shard.iter_torch_batches(
batch_size=batch_size, dtypes=torch.float
)
# Train multiple epochs.
for epoch in range(num_epochs):
# Train epoch.
for batch in dataloader:
output = model(batch["input"])
loss = loss_fn(output, batch["label"])
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Create checkpoint.
base_model = (
model.module
if isinstance(model, DistributedDataParallel)
else model
)
with tempfile.TemporaryDirectory() as temp_checkpoint_dir:
torch.save(
{"model_state_dict": base_model.state_dict()},
os.path.join(temp_checkpoint_dir, "model.pt"),
)
checkpoint = ray.train.Checkpoint.from_directory(temp_checkpoint_dir)
# Report metrics and checkpoint.
ray.train.report({"loss": loss.item()}, checkpoint=checkpoint)
# Define datasets.
train_dataset = ray.data.from_items(
[{"input": [x], "label": [2 * x + 1]} for x in range(128)]
)
# Initialize the Trainer.
trainer = TorchTrainer(
train_fn_per_worker,
train_loop_config={"num_epochs": 1, "lr": 0.01, "batch_size": 32},
scaling_config=ray.train.ScalingConfig(num_workers=num_workers, use_gpu=use_gpu),
datasets={"train": train_dataset},
)
# Train the model.
result = trainer.fit()
# Inspect the results.
final_loss = result.metrics["loss"]
Args:
train_loop_per_worker: The training function to execute on each worker.
This function can either take in zero arguments or a single ``Dict``
argument which is set by defining ``train_loop_config``.
Within this function you can use any of the
:ref:`Ray Train Loop utilities <train-loop-api>`.
train_loop_config: A configuration ``Dict`` to pass in as an argument to
``train_loop_per_worker``.
This is typically used for specifying hyperparameters. Passing large
datasets via `train_loop_config` is not recommended and may introduce
large overhead and unknown issues with serialization and deserialization.
torch_config: The configuration for setting up the PyTorch Distributed backend.
If set to None, a default configuration will be used in which
GPU training uses NCCL and CPU training uses Gloo.
scaling_config: The configuration for how to scale data parallel training.
``num_workers`` determines how many Python processes are used for training,
and ``use_gpu`` determines whether or not each process should use GPUs.
See :class:`~ray.train.ScalingConfig` for more info.
run_config: The configuration for the execution of the training run.
See :class:`~ray.train.RunConfig` for more info.
datasets: The Ray Datasets to ingest for training.
Datasets are keyed by name (``{name: dataset}``).
Each dataset can be accessed from within the ``train_loop_per_worker``
by calling ``ray.train.get_dataset_shard(name)``.
Sharding and additional configuration can be done by
passing in a ``dataset_config``.
dataset_config: The configuration for ingesting the input ``datasets``.
By default, all the Ray Dataset are split equally across workers.
See :class:`~ray.train.DataConfig` for more details.
validation_config: [Alpha] Configuration for checkpoint validation.
If provided and ``ray.train.report`` is called with the ``validation``
argument, Ray Train will validate the reported checkpoint using
the validation function specified in this config.
metadata: [Deprecated]
resume_from_checkpoint: [Deprecated]
"""
def __init__(
self,
train_loop_per_worker: Union[Callable[[], Any], Callable[[Dict], Any]],
*,
train_loop_config: Optional[Dict] = None,
torch_config: Optional["TorchConfig"] = None,
scaling_config: Optional[ScalingConfig] = None,
run_config: Optional[RunConfig] = None,
datasets: Optional[Dict[str, GenDataset]] = None,
dataset_config: Optional[DataConfig] = None,
validation_config: Optional[ValidationConfig] = None,
# TODO: [Deprecated]
metadata: Optional[Dict[str, Any]] = None,
resume_from_checkpoint: Optional[Checkpoint] = None,
):
from ray.train.torch.config import TorchConfig
torch_config = torch_config or TorchConfig()
if not torch_config.backend:
is_gpu_training = scaling_config and scaling_config.use_gpu
torch_config.backend = "nccl" if is_gpu_training else "gloo"
super(TorchTrainer, self).__init__(
train_loop_per_worker=train_loop_per_worker,
train_loop_config=train_loop_config,
backend_config=torch_config,
scaling_config=scaling_config,
run_config=run_config,
dataset_config=dataset_config,
datasets=datasets,
resume_from_checkpoint=resume_from_checkpoint,
metadata=metadata,
validation_config=validation_config,
)
def _get_local_controller(self) -> LocalTorchController:
return LocalTorchController(
experiment_name=self.run_config.name,
datasets=self.datasets,
)
+117
View File
@@ -0,0 +1,117 @@
import logging
import os
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict
import ray
from ray.train.torch.config import (
TorchConfig,
_TorchBackend,
)
from ray.train.v2._internal.constants import TORCHFT_LIGHTHOUSE_ADDR_ENV_VAR
from ray.train.v2._internal.execution.worker_group import ReplicaGroup, WorkerGroup
from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy
logger = logging.getLogger(__name__)
class TorchftConfig(TorchConfig):
"""Configuration for torchft-based fault tolerant training.
See https://github.com/meta-pytorch/torchft for more info.
Args:
lighthouse_kwargs: Keyword arguments to pass to the torchft.Lighthouse constructor.
**kwargs: Additional keyword arguments to pass to the TorchConfig constructor.
"""
def __init__(self, lighthouse_kwargs: Dict[str, Any], **kwargs):
self.lighthouse_kwargs = lighthouse_kwargs
super().__init__(**kwargs)
@property
def backend_cls(self):
return _TorchftBackend
@ray.remote
class LighthouseServerActor:
"""Actor that runs the torchft.Lighthouse server.
ray.remote(LighthouseServer) does not work because it is a PyO3 type.
"""
def __init__(self, lighthouse_kwargs: Dict[str, Any]):
from torchft.coordination import LighthouseServer
self.lighthouse = LighthouseServer(**lighthouse_kwargs)
def address(self) -> str:
return self.lighthouse.address()
class _TorchftBackend(_TorchBackend):
"""Backend for torchft-based fault-tolerant training with replica groups.
Creates a separate process group per replica group by calling
the parent _TorchBackend.on_start() once per replica group.
"""
has_replica_groups: bool = True
def __init__(self):
super().__init__()
self.lighthouse_actor = None
def _maybe_create_lighthouse_actor(self, backend_config: TorchftConfig) -> str:
"""Create lighthouse actor if it doesn't exist and return its address."""
if self.lighthouse_actor is not None:
# Intentionally read address from actor in case it was restarted.
return ray.get(self.lighthouse_actor.address.remote())
# Let the OS pick a free port by default
if "bind" in backend_config.lighthouse_kwargs:
lighthouse_kwargs = backend_config.lighthouse_kwargs
else:
lighthouse_kwargs = {"bind": "[::]:0"} | backend_config.lighthouse_kwargs
# Store reference so the actor lives as long as the backend/controller.
self.lighthouse_actor = LighthouseServerActor.options(
# Schedule lightweight lighthouse actor on head node
scheduling_strategy=NodeAffinitySchedulingStrategy(
node_id=ray.get_runtime_context().get_node_id(),
soft=False,
)
).remote(lighthouse_kwargs=lighthouse_kwargs)
lighthouse_address = ray.get(self.lighthouse_actor.address.remote())
logger.info(f"Created torchft lighthouse at {lighthouse_address}")
return lighthouse_address
def on_start(self, worker_group, backend_config: TorchftConfig):
lighthouse_address = self._maybe_create_lighthouse_actor(backend_config)
# Push the lighthouse address to all workers in this group.
# Necessary because workers were already started before on_start runs.
def _set_lighthouse_address(addr: str):
os.environ[TORCHFT_LIGHTHOUSE_ADDR_ENV_VAR] = addr
worker_group.execute(_set_lighthouse_address, lighthouse_address)
# Bind super() eagerly — the zero-arg form relies on a __class__ cell
# that doesn't transfer correctly into ThreadPoolExecutor submissions.
parent_on_start = super().on_start
if isinstance(worker_group, ReplicaGroup):
# Single replica group replacement — just start this one.
parent_on_start(worker_group, backend_config)
else:
# Full worker group startup — start all replica groups in parallel.
assert isinstance(worker_group, WorkerGroup)
replica_groups = worker_group.get_replica_groups()
with ThreadPoolExecutor() as executor:
futures = [
executor.submit(parent_on_start, rg, backend_config)
for rg in replica_groups
]
for f in futures:
f.result()
@@ -0,0 +1,435 @@
import logging
import os
import random
from typing import Any, Callable, Dict, List, Optional, Union
import numpy as np
import torch
from packaging.version import Version
from torch.nn.parallel import DistributedDataParallel
from torch.utils.data import (
DataLoader,
DistributedSampler,
IterableDataset,
RandomSampler,
SequentialSampler,
)
import ray.train.torch
from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag
from ray.train.torch.train_loop_utils import (
_WrappedDataLoader,
get_devices as get_devices_distributed,
)
from ray.train.v2._internal.execution.train_fn_utils import get_train_fn_utils
from ray.train.v2._internal.util import requires_train_worker
from ray.util.annotations import Deprecated, PublicAPI
logger = logging.getLogger(__name__)
_TORCH_AMP_DEPRECATION_MESSAGE = (
"The `accelerate`, `backward`, and `prepare_optimizer` utility methods "
"in the `ray.train.torch` module are deprecated and will be removed in a "
"future release. "
"Please use the native PyTorch mixed precision API directly, or "
"a library such as Lightning or HuggingFace Transformers/Accelerate. "
"See this issue for more context: "
"https://github.com/ray-project/ray/issues/49454"
)
@PublicAPI(stability="stable")
@requires_train_worker()
def get_device() -> torch.device:
"""Gets the correct torch device configured for the current worker.
Returns the torch device for the current worker. If more than 1 GPU is
requested per worker, returns the device with the lowest device index.
.. note::
If you requested multiple GPUs per worker, and want to get
the full list of torch devices, please use
:meth:`~ray.train.torch.get_devices`.
Assumes that `CUDA_VISIBLE_DEVICES` is set and is a
superset of the `ray.get_gpu_ids()`.
Returns:
The torch device assigned to the current worker.
Examples:
Example: Launched 2 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:0")
Example: Launched 4 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:2")
Example: Launched 2 workers on the current node, each with 2 GPUs
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
ray.get_gpu_ids() == [2,3]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:2")
You can move a model to device by:
.. testcode::
:skipif: True
model.to(ray.train.torch.get_device())
Instead of manually checking the device type:
.. testcode::
:skipif: True
model.to("cuda" if torch.cuda.is_available() else "cpu")
"""
return get_devices()[0]
@PublicAPI(stability="beta")
@requires_train_worker()
def get_devices() -> List[torch.device]:
"""Gets the list of torch devices configured for the current worker.
Assumes that `CUDA_VISIBLE_DEVICES` is set and is a
superset of the `ray.get_gpu_ids()`.
Returns:
The list of torch devices assigned to the current worker.
Examples:
Example: Launched 2 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:0")]
Example: Launched 4 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "0,1,2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:2")]
Example: Launched 2 workers on the current node, each with 2 GPUs
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "0,1,2,3"
ray.get_gpu_ids() == [2,3]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:2"), torch.device("cuda:3")]
"""
if get_train_fn_utils().is_distributed():
return get_devices_distributed()
else:
# Local mode, we defer to torch.cuda
# TODO(xgui): Use `ScalingConfig.use_gpu` instead
if torch.cuda.is_available():
return [torch.device(f"cuda:{torch.cuda.current_device()}")]
else:
return [torch.device("cpu")]
def prepare_model(
model: torch.nn.Module,
move_to_device: Union[bool, torch.device] = True,
parallel_strategy: Optional[str] = "ddp",
parallel_strategy_kwargs: Optional[Dict[str, Any]] = None,
) -> torch.nn.Module:
"""Prepares the model for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
Args:
model: A torch model to prepare.
move_to_device: Either a boolean indiciating whether to move
the model to the correct device or an actual device to
move the model to. If set to False, the model needs
to manually be moved to the correct device.
parallel_strategy: Whether to wrap models in
``DistributedDataParallel``, ``FullyShardedDataParallel``,
or neither. Must be one of ``"ddp"``, ``"fsdp"``, or ``None``.
parallel_strategy_kwargs: Args to pass into
``DistributedDataParallel`` or ``FullyShardedDataParallel``
initialization if ``parallel_strategy`` is set to "ddp"
or "fsdp", respectively.
Returns:
The prepared model, wrapped according to ``parallel_strategy``.
"""
if parallel_strategy == "fsdp" and Version(torch.__version__) < Version("1.11.0"):
raise ImportError(
"FullyShardedDataParallel requires torch>=1.11.0. "
"Run `pip install 'torch>=1.11.0'` to use FullyShardedDataParallel."
)
record_extra_usage_tag(TagKey.TRAIN_TORCH_PREPARE_MODEL, "1")
parallel_strategy_kwargs = parallel_strategy_kwargs or {}
rank = ray.train.get_context().get_local_rank()
if isinstance(move_to_device, torch.device):
device = move_to_device
else:
device = ray.train.torch.get_device()
if isinstance(device, list):
device = device[0]
if torch.cuda.is_available() and device.type == "cuda":
torch.cuda.set_device(device)
if move_to_device:
if rank == 0:
logger.info(f"Moving model to device: {device}")
else:
logger.debug(f"Moving model to device: {device}")
model = model.to(device)
world_size = ray.train.get_context().get_world_size()
if parallel_strategy and world_size > 1:
if parallel_strategy == "ddp":
DataParallel = DistributedDataParallel
if torch.cuda.is_available() and device.type != "cpu":
parallel_strategy_kwargs = {
"device_ids": [device],
"output_device": device,
**parallel_strategy_kwargs,
}
else:
if not torch.cuda.is_available():
raise RuntimeError(
"FSDP is only available with GPU-enabled "
"training. Set "
"`use_gpu=True` in your Trainer to train with "
"GPUs."
)
from torch.distributed.fsdp import FullyShardedDataParallel
DataParallel = FullyShardedDataParallel
if rank == 0:
logger.info(f"Wrapping provided model in {DataParallel.__name__}.")
else:
logger.debug(f"Wrapping provided model in {DataParallel.__name__}.")
model = DataParallel(model, **parallel_strategy_kwargs)
return model
@PublicAPI(stability="stable")
def prepare_data_loader(
data_loader: torch.utils.data.DataLoader,
add_dist_sampler: bool = True,
move_to_device: bool = True,
auto_transfer: bool = True,
) -> torch.utils.data.DataLoader:
"""Prepares :class:`~torch.utils.data.DataLoader` for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
.. note::
This method adds a `DistributedSampler` to the `DataLoader` if the
number of training workers is greater than 1. If shuffling is
enabled on the original `DataLoader`, then `shuffle=True` will also
be passed into the `DistributedSampler` constructor. `shuffle=False`
on the original `DataLoader` also means that shuffling is disabled
on the sampler.
With more than 1 worker, calling the `DistributedSampler.set_epoch` method
at the beginning of each epoch before creating the DataLoader iterator
is necessary to make shuffling work properly across multiple epochs.
Otherwise, the same ordering will be always used.
See: https://pytorch.org/docs/stable/data.html#torch.utils.data.distributed.DistributedSampler # noqa: E501
Example:
.. testcode:
:skipif: True
import torch
import ray.train.torch
train_dataloader = torch.utils.data.DataLoader(
..., batch_size=..., shuffle=True
)
train_dataloader = ray.train.torch.prepare_data_loader(train_loader)
for epoch in range(10):
if ray.train.get_context().get_world_size() > 1:
# Required for the distributed sampler to shuffle properly across epochs
train_dataloader.sampler.set_epoch(epoch)
for X, y in train_loader:
# No need to move data to GPU, this is done by `prepare_data_loader`!
# X, y = X.to("cuda"), y.to("cuda")
...
Args:
data_loader: The DataLoader to prepare.
add_dist_sampler: Whether to add a DistributedSampler to
the provided DataLoader.
move_to_device: If set, automatically move the data
returned by the data loader to the correct device.
auto_transfer: If set and device is GPU, another CUDA stream
is created to automatically copy data from host (CPU) memory
to device (GPU) memory (the default CUDA stream still runs the
training procedure). If device is CPU, it will be disabled
regardless of the setting. This configuration will be ignored
if ``move_to_device`` is False.
Returns:
The prepared DataLoader.
"""
record_extra_usage_tag(TagKey.TRAIN_TORCH_PREPARE_DATALOADER, "1")
world_size = ray.train.get_context().get_world_size()
world_rank = ray.train.get_context().get_world_rank()
# Only add Distributed Sampler if the following conditions hold:
# 1. More than one training worker is being used.
# 2. A DistributedSampler has not already been added by the user.
# 3. The dataset is not an IterableDataset. Samplers do not worker with
# IterableDatasets.
if (
world_size > 1
and not isinstance(data_loader.sampler, DistributedSampler)
and not (
hasattr(data_loader, "dataset")
and isinstance(data_loader.dataset, IterableDataset)
)
and add_dist_sampler
):
def with_sampler(loader):
# Automatically set the DistributedSampler
# If you're using a sampler, the DataLoader shuffle flag must be set to
# False. Shuffling is instead determined by the shuffle argument passed
# to the DistributedSampler constructor.
# If no sampler is passed to the DataLoader constructor, Torch
# constructs a default sampler. The default sampler is a RandomSampler
# if shuffling is enabled and a SequentialSampler otherwise. DataLoader
# does not have a shuffle attribute, so we instead identify whether
# shuffling is enabled by checking the default sampler type.
shuffle = not isinstance(loader.sampler, SequentialSampler)
worker_init_fn: Optional[Callable[[int], None]] = loader.worker_init_fn
generator: Optional[torch.Generator] = loader.generator
using_default_sampler = isinstance(
loader.sampler, (SequentialSampler, RandomSampler)
)
if not using_default_sampler and world_rank == 0:
logger.warning(
f"The {loader.sampler.__class__.__name__} will be overwritten "
"with a DistributedSampler. You can disable this by setting "
"`with_sampler` to False in `prepare_data_loader`."
)
data_loader_args = {
"dataset": loader.dataset,
"batch_size": loader.batch_size,
"shuffle": False,
"num_workers": loader.num_workers,
"collate_fn": loader.collate_fn,
"pin_memory": loader.pin_memory,
"drop_last": loader.drop_last,
"timeout": loader.timeout,
"worker_init_fn": worker_init_fn,
"generator": generator,
"sampler": DistributedSampler(loader.dataset, shuffle=shuffle),
}
return DataLoader(**data_loader_args)
data_loader = with_sampler(data_loader)
if move_to_device:
device = ray.train.torch.get_device()
data_loader = _WrappedDataLoader(data_loader, device, auto_transfer)
return data_loader
@Deprecated
def accelerate(amp: bool = False) -> None:
raise DeprecationWarning(_TORCH_AMP_DEPRECATION_MESSAGE)
@Deprecated
def prepare_optimizer(optimizer: torch.optim.Optimizer) -> torch.optim.Optimizer:
raise DeprecationWarning(_TORCH_AMP_DEPRECATION_MESSAGE)
@Deprecated
def backward(tensor: torch.Tensor) -> None:
raise DeprecationWarning(_TORCH_AMP_DEPRECATION_MESSAGE)
@PublicAPI(stability="stable")
def enable_reproducibility(seed: int = 0) -> None:
"""Limits sources of nondeterministic behavior.
This function:
* Seeds PyTorch, Python, and NumPy.
* Disables CUDA convolution benchmarking.
* Configures PyTorch to use determinstic algorithms.
* Seeds workers spawned for multi-process data loading.
Args:
seed: The number to seed libraries and data workers with.
.. warning:: ``train.torch.enable_reproducibility()`` can't guarantee
completely reproducible results across executions. To learn more, read
the `PyTorch notes on randomness
<https://pytorch.org/docs/stable/notes/randomness.html>`_.
"""
torch.manual_seed(seed)
random.seed(seed)
np.random.seed(seed)
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False
# If you want to use deterministic algorithms with CUDA, then you need to set
# the CUBLAS_WORKSPACE_CONFIG environment variable; otherwise, Torch errors.
# See https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility.
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"