# Copyright (c) Meta Platforms, Inc. and affiliates.# All rights reserved.## This source code is licensed under the BSD-style license found in the# LICENSE file in the root directory of this source tree."""This module provides abstractions for managing PyTorch devices and handling CUDAcontexts."""from__future__importannotationsfromabcimportABC,abstractmethodfromcollections.abcimportIteratorfromcontextlibimportcontextmanagerfromtypingimportAny,Final,TypeAlias,finalimporttorchfromtyping_extensionsimportoverridefromfairseq2.errorimportInternalErrorfromfairseq2.runtime.dependencyimportget_dependency_resolverfromfairseq2.typingimportContextManagerfromfairseq2.utils.envimportEnvironment,EnvironmentVariableError,get_local_rankfromfairseq2.utils.versionimporttorch_greater_or_equalfromfairseq2.utils.warnimport_warn_deprecatedDevice:TypeAlias=torch.deviceCPU:Final=Device("cpu")META_DEVICE:Final=Device("meta")
[docs]classSupportsDeviceTransfer(ABC):"""Represents an object that can be transferred between devices."""
[docs]@abstractmethoddefto(self,device:Device,*,non_blocking:bool=False)->None:""" Transfers this object to the specified device. If ``non_blocking`` is ``True``, the transfer will be performed asynchronously if possible. """
[docs]defset_device(device:Device)->ContextManager[None]:""" Changes the device of the calling thread to the specified device. This function acts as a context manager, ensuring that within its scope, any operation that constructs tensors uses the specified device - unless an explicit ``device`` argument is provided. .. note:: This function is equivalent to using a ``torch.device`` as a context manager. .. code:: python import torch from fairseq2.device import set_device cuda0_device = torch.device("cuda:0") with set_device(cuda0_device): t = torch.ones((4,4)) assert t.device == cuda0_device cuda1_device = torch.device("cuda:1") with set_device(cuda1_device): t = torch.ones((4, 4)) assert t.device == cuda1_device t = torch.ones((4, 4)) assert t.device == torch.device("cpu") """resolver=get_dependency_resolver()returnresolver.resolve(DeviceContext).set_device(device)
[docs]defget_current_device()->Device:""" Returns the current device of the calling thread. .. warning:: This function might impose a slight performance cost. Avoid calling it in hot code paths. """resolver=get_dependency_resolver()returnresolver.resolve(DeviceContext).get_current_device()
[docs]classDeviceContext(ABC):""" Provides methods to get and set the current device of the calling thread. This interface can be used as an alternative to the corresponding standalone functions in object-oriented code. """
@finalclass_StandardDeviceContext(DeviceContext):@overridedefget_current_device(self)->Device:iftorch_greater_or_equal(2,8):returntorch.get_default_device()# In PyTorch versions earlier than 2.8 the only way to determine the# closest contextual device is to create a dummy tensor.returntorch.empty(()).device@override@contextmanagerdefset_device(self,device:Device)->Iterator[None]:withdevice:yield
[docs]defdetect_default_device()->Device:""" Detects the default device of this process from environment variables. The default device is determined by the following precedence: 1) If ``FAIRSEQ2_DEVICE`` environment variable is set, the specified device will be used. 2) If CUDA is enabled and ``CUDA_VISIBLE_DEVICES`` environment variable contains a single device, the specified device will be used. 3) If CUDA is enabled and ``LOCAL_RANK`` environment variable is set, the CUDA device at the specified index will be used. 4) CPU will be used. :raises EnvironmentVariableError: ``FAIRSEQ2_DEVICE`` environment variable does not represent a device. :raises EnvironmentVariableError: ``LOCAL_RANK`` environment variable is not a positive integer. :raises LocalRankOutOfRangeError: ``LOCAL_RANK`` environment variable exceeds the number of available devices. """resolver=get_dependency_resolver()returnresolver.resolve(_DefaultDeviceDetector).detect()
defget_default_device()->Device:_warn_deprecated("`get_default_device()` is deprecated and will be removed in v0.14. Use `detect_default_device()` instead.")returndetect_default_device()@finalclass_DefaultDeviceDetector:def__init__(self,env:Environment,cuda_context:CudaContext)->None:self._env=envself._cuda_context=cuda_contextdefdetect(self)->Device:"""See :func:`detect_default_device`."""device=self._maybe_get_device_from_env("FAIRSEQ2_DEVICE")ifdeviceisNone:device=self._get_default_cuda_device()ifdeviceisNone:device=CPUreturndevicedef_maybe_get_device_from_env(self,var_name:str)->Device|None:s=self._env.maybe_get(var_name)ifsisNone:returnNonetry:returnDevice(s)except(RuntimeError,ValueError)asex:message=f"`{var_name}` environment variable does not represent a PyTorch device."raiseEnvironmentVariableError(var_name,message)fromexdef_get_default_cuda_device(self)->Device|None:ifnotself._cuda_context.is_available():returnNonenum_devices=self._cuda_context.device_count()ifnum_devices==0:returnNonevisible_devices=self._env.maybe_get("CUDA_VISIBLE_DEVICES")ifvisible_devicesisnotNone:try:int(visible_devices)exceptValueError:# If here, this means CUDA_VISIBLE_DEVICES is a list instead of# a single device index.device=Noneelse:device=Device("cuda",index=0)else:device=NoneifdeviceisNone:idx=self._get_device_index(num_devices,device_type="cuda")device=Device("cuda",index=idx)returndevicedef_get_device_index(self,num_devices:int,device_type:str)->int:ifnum_devices<=0:raiseInternalError(f"`num_devices` is {num_devices}.")rank=get_local_rank(self._env)ifrankisNone:return0ifrank>=num_devices:raiseLocalRankOutOfRangeError(rank,num_devices,device_type)returnrank
[docs]classLocalRankOutOfRangeError(Exception):def__init__(self,rank:int,num_devices:int,device_type:str)->None:super().__init__(f"Host has {num_devices} `{device_type}` device(s), but the local rank of the process is {rank}.")self.rank=rankself.num_devices=num_devicesself.device_type=device_type
[docs]classCudaContext(ABC):""" Represents an interface for interacting with CUDA runtime and device information. """