Files
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

545 lines
16 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from collections.abc import Iterator
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional, Union
from haystack.lazy_imports import LazyImport
with LazyImport(
message="PyTorch must be installed to use torch.device or use GPU support in HuggingFace transformers. "
"Run 'pip install \"transformers[torch]\"'"
) as torch_import:
import torch
class DeviceType(Enum):
"""
Represents device types supported by Haystack.
This also includes devices that are not directly used by models - for example, the disk device is exclusively used
in device maps for frameworks that support offloading model weights to disk.
"""
CPU = "cpu"
GPU = "cuda"
DISK = "disk"
MPS = "mps"
XPU = "xpu"
def __str__(self) -> str:
return self.value
@staticmethod
def from_str(string: str) -> "DeviceType":
"""
Create a device type from a string.
:param string:
The string to convert.
:returns:
The device type.
"""
mapping = {e.value: e for e in DeviceType}
_type = mapping.get(string)
if _type is None:
raise ValueError(f"Unknown device type string '{string}'")
return _type
@dataclass
class Device:
"""
A generic representation of a device.
:param type:
The device type.
:param id:
The optional device id.
"""
type: DeviceType
id: int | None = field(default=None)
def __init__(self, type: DeviceType, id: int | None = None) -> None: # noqa:A002
"""
Create a generic device.
:param type:
The device type.
:param id:
The device id.
"""
if id is not None and id < 0:
raise ValueError(f"Device id must be >= 0, got {id}")
self.type = type
self.id = id
def __str__(self) -> str:
if self.id is None:
return str(self.type)
return f"{self.type}:{self.id}"
@staticmethod
def cpu() -> "Device":
"""
Create a generic CPU device.
:returns:
The CPU device.
"""
return Device(DeviceType.CPU)
@staticmethod
def gpu(id: int = 0) -> "Device": # noqa:A002
"""
Create a generic GPU device.
:param id:
The GPU id.
:returns:
The GPU device.
"""
return Device(DeviceType.GPU, id)
@staticmethod
def disk() -> "Device":
"""
Create a generic disk device.
:returns:
The disk device.
"""
return Device(DeviceType.DISK)
@staticmethod
def mps() -> "Device":
"""
Create a generic Apple Metal Performance Shader device.
:returns:
The MPS device.
"""
return Device(DeviceType.MPS)
@staticmethod
def xpu() -> "Device":
"""
Create a generic Intel GPU Optimization device.
:returns:
The XPU device.
"""
return Device(DeviceType.XPU)
@staticmethod
def from_str(string: str) -> "Device":
"""
Create a generic device from a string.
:returns:
The device.
"""
device_type_str, device_id = _split_device_string(string)
return Device(DeviceType.from_str(device_type_str), device_id)
@dataclass
class DeviceMap:
"""
A generic mapping from strings to devices.
The semantics of the strings are dependent on target framework. Primarily used to deploy HuggingFace models to
multiple devices.
:param mapping:
Dictionary mapping strings to devices.
"""
mapping: dict[str, Device] = field(default_factory=dict, hash=False)
def __getitem__(self, key: str) -> Device:
return self.mapping[key]
def __setitem__(self, key: str, value: Device) -> None:
self.mapping[key] = value
def __contains__(self, key: str) -> bool:
return key in self.mapping
def __len__(self) -> int:
return len(self.mapping)
def __iter__(self) -> Iterator[tuple[str, Device]]:
return iter(self.mapping.items())
def to_dict(self) -> dict[str, str]:
"""
Serialize the mapping to a JSON-serializable dictionary.
:returns:
The serialized mapping.
"""
return {key: str(device) for key, device in self.mapping.items()}
@property
def first_device(self) -> Device | None:
"""
Return the first device in the mapping, if any.
:returns:
The first device.
"""
if not self.mapping:
return None
return next(iter(self.mapping.values()))
@staticmethod
def from_dict(dict: dict[str, str]) -> "DeviceMap": # noqa:A002
"""
Create a generic device map from a JSON-serialized dictionary.
:param dict:
The serialized mapping.
:returns:
The generic device map.
"""
mapping = {}
for key, device_str in dict.items():
mapping[key] = Device.from_str(device_str)
return DeviceMap(mapping)
@staticmethod
def from_hf(hf_device_map: dict[str, Union[int, str, "torch.device"]]) -> "DeviceMap":
"""
Create a generic device map from a HuggingFace device map.
:param hf_device_map:
The HuggingFace device map.
:returns:
The deserialized device map.
:raises TypeError: If a device value in the map is not an int, str, or torch.device.
"""
mapping = {}
for key, device in hf_device_map.items():
if isinstance(device, int):
mapping[key] = Device(DeviceType.GPU, device)
elif isinstance(device, str):
device_type, device_id = _split_device_string(device)
mapping[key] = Device(DeviceType.from_str(device_type), device_id)
elif isinstance(device, torch.device):
device_type = device.type
device_id = device.index
mapping[key] = Device(DeviceType.from_str(device_type), device_id)
else:
raise TypeError(
f"Couldn't convert HuggingFace device map - unexpected device '{str(device)}' for '{key}'"
)
return DeviceMap(mapping)
@dataclass(frozen=True)
class ComponentDevice:
"""
A representation of a device for a component.
This can be either a single device or a device map.
"""
_single_device: Device | None = field(default=None)
_multiple_devices: DeviceMap | None = field(default=None)
@classmethod
def from_str(cls, device_str: str) -> "ComponentDevice":
"""
Create a component device representation from a device string.
The device string can only represent a single device.
:param device_str:
The device string.
:returns:
The component device representation.
"""
device = Device.from_str(device_str)
return cls.from_single(device)
@classmethod
def from_single(cls, device: Device) -> "ComponentDevice":
"""
Create a component device representation from a single device.
Disks cannot be used as single devices.
:param device:
The device.
:returns:
The component device representation.
"""
if device.type == DeviceType.DISK:
raise ValueError("The disk device can only be used as a part of device maps")
return cls(_single_device=device)
@classmethod
def from_multiple(cls, device_map: DeviceMap) -> "ComponentDevice":
"""
Create a component device representation from a device map.
:param device_map:
The device map.
:returns:
The component device representation.
"""
return cls(_multiple_devices=device_map)
def _validate(self) -> None:
"""
Validate the component device representation.
"""
if not (self._single_device is not None) ^ (self._multiple_devices is not None):
raise ValueError(
"The component device can neither be empty nor contain both a single device and a device map"
)
def to_torch(self) -> "torch.device":
"""
Convert the component device representation to PyTorch format.
Device maps are not supported.
:returns:
The PyTorch device representation.
"""
self._validate()
if self._single_device is None:
raise ValueError("Only single devices can be converted to PyTorch format")
torch_import.check()
assert self._single_device is not None
return torch.device(str(self._single_device))
def to_torch_str(self) -> str:
"""
Convert the component device representation to PyTorch string format.
Device maps are not supported.
:returns:
The PyTorch device string representation.
"""
self._validate()
if self._single_device is None:
raise ValueError("Only single devices can be converted to PyTorch format")
assert self._single_device is not None
return str(self._single_device)
def to_spacy(self) -> int:
"""
Convert the component device representation to spaCy format.
Device maps are not supported.
:returns:
The spaCy device representation.
"""
self._validate()
if self._single_device is None:
raise ValueError("Only single devices can be converted to spaCy format")
assert self._single_device is not None
if self._single_device.type == DeviceType.GPU:
assert self._single_device.id is not None
return self._single_device.id
return -1
def to_hf(self) -> int | str | dict[str, int | str]:
"""
Convert the component device representation to HuggingFace format.
:returns:
The HuggingFace device representation.
"""
self._validate()
def convert_device(device: Device, *, gpu_id_only: bool = False) -> int | str:
if gpu_id_only and device.type == DeviceType.GPU:
assert device.id is not None
return device.id
return str(device)
if self._single_device is not None:
return convert_device(self._single_device)
assert self._multiple_devices is not None
return {key: convert_device(device, gpu_id_only=True) for key, device in self._multiple_devices.mapping.items()}
def update_hf_kwargs(self, hf_kwargs: dict[str, Any], *, overwrite: bool) -> dict[str, Any]:
"""
Convert the component device representation to HuggingFace format.
Add them as canonical keyword arguments to the keyword arguments dictionary.
:param hf_kwargs:
The HuggingFace keyword arguments dictionary.
:param overwrite:
Whether to overwrite existing device arguments.
:returns:
The HuggingFace keyword arguments dictionary.
"""
self._validate()
if not overwrite and any(x in hf_kwargs for x in ("device", "device_map")):
return hf_kwargs
converted = self.to_hf()
key = "device_map" if self.has_multiple_devices else "device"
hf_kwargs[key] = converted
return hf_kwargs
@property
def has_multiple_devices(self) -> bool:
"""
Whether this component device representation contains multiple devices.
"""
self._validate()
return self._multiple_devices is not None
@property
def first_device(self) -> Optional["ComponentDevice"]:
"""
Return either the single device or the first device in the device map, if any.
:returns:
The first device.
"""
self._validate()
if self._single_device is not None:
return self.from_single(self._single_device)
assert self._multiple_devices is not None
assert self._multiple_devices.first_device is not None
return self.from_single(self._multiple_devices.first_device)
@staticmethod
def resolve_device(device: Optional["ComponentDevice"] = None) -> "ComponentDevice":
"""
Select a device for a component. If a device is specified, it's used. Otherwise, the default device is used.
:param device:
The provided device, if any.
:returns:
The resolved device.
"""
if not isinstance(device, ComponentDevice) and device is not None:
raise ValueError(
f"Invalid component device type '{type(device).__name__}'. Must either be None or ComponentDevice."
)
if device is None:
device = ComponentDevice.from_single(_get_default_device())
return device
def to_dict(self) -> dict[str, Any]:
"""
Convert the component device representation to a JSON-serializable dictionary.
:returns:
The dictionary representation.
"""
if self._single_device is not None:
return {"type": "single", "device": str(self._single_device)}
if self._multiple_devices is not None:
return {"type": "multiple", "device_map": self._multiple_devices.to_dict()}
# Unreachable
raise AssertionError()
@classmethod
def from_dict(cls, dict: dict[str, Any]) -> "ComponentDevice": # noqa:A002
"""
Create a component device representation from a JSON-serialized dictionary.
:param dict:
The serialized representation.
:returns:
The deserialized component device.
"""
if dict["type"] == "single":
return cls.from_str(dict["device"])
if dict["type"] == "multiple":
return cls.from_multiple(DeviceMap.from_dict(dict["device_map"]))
raise ValueError(f"Unknown component device type '{dict['type']}' in serialized data")
def _get_default_device() -> Device:
"""
Return the default device for Haystack.
Precedence:
GPU > XPU > MPS > CPU. If PyTorch is not installed, only CPU is available.
:returns:
The default device.
"""
try:
torch_import.check()
has_mps = (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_available()
and os.getenv("HAYSTACK_MPS_ENABLED", "true") != "false"
)
has_cuda = torch.cuda.is_available()
has_xpu = (
hasattr(torch, "xpu")
and hasattr(torch.xpu, "is_available")
and torch.xpu.is_available()
and os.getenv("HAYSTACK_XPU_ENABLED", "true") != "false"
)
except ImportError:
has_mps = False
has_cuda = False
has_xpu = False
if has_cuda:
return Device.gpu()
if has_xpu:
return Device.xpu()
if has_mps:
return Device.mps()
return Device.cpu()
def _split_device_string(string: str) -> tuple[str, int | None]:
"""
Split a device string into device type and device id.
:param string:
The device string to split.
:returns:
The device type and device id, if any.
"""
if ":" in string:
device_type, device_id_str = string.split(":")
try:
device_id = int(device_id_str)
except ValueError as e:
raise ValueError(f"Device id must be an integer, got {device_id_str}") from e
else:
device_type = string
device_id = None
return device_type, device_id