environment.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # Copyright 2022 The HuggingFace Team. All rights reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. import math
  16. import os
  17. import platform
  18. import subprocess
  19. import sys
  20. from contextlib import contextmanager
  21. from dataclasses import dataclass, field
  22. from functools import lru_cache, wraps
  23. from shutil import which
  24. from typing import Optional, Union
  25. import torch
  26. from packaging.version import parse
  27. logger = logging.getLogger(__name__)
  28. def convert_dict_to_env_variables(current_env: dict):
  29. """
  30. Verifies that all keys and values in `current_env` do not contain illegal keys or values, and returns a list of
  31. strings as the result.
  32. Example:
  33. ```python
  34. >>> from accelerate.utils.environment import verify_env
  35. >>> env = {"ACCELERATE_DEBUG_MODE": "1", "BAD_ENV_NAME": "<mything", "OTHER_ENV": "2"}
  36. >>> valid_env_items = verify_env(env)
  37. >>> print(valid_env_items)
  38. ["ACCELERATE_DEBUG_MODE=1\n", "OTHER_ENV=2\n"]
  39. ```
  40. """
  41. forbidden_chars = [";", "\n", "<", ">", " "]
  42. valid_env_items = []
  43. for key, value in current_env.items():
  44. if all(char not in (key + value) for char in forbidden_chars) and len(key) >= 1 and len(value) >= 1:
  45. valid_env_items.append(f"{key}={value}\n")
  46. else:
  47. logger.warning(f"WARNING: Skipping {key}={value} as it contains forbidden characters or missing values.")
  48. return valid_env_items
  49. def str_to_bool(value, to_bool: bool = False) -> Union[int, bool]:
  50. """
  51. Converts a string representation of truth to `True` (1) or `False` (0).
  52. True values are `y`, `yes`, `t`, `true`, `on`, and `1`; False value are `n`, `no`, `f`, `false`, `off`, and `0`;
  53. """
  54. value = value.lower()
  55. if value in ("y", "yes", "t", "true", "on", "1"):
  56. return 1 if not to_bool else True
  57. elif value in ("n", "no", "f", "false", "off", "0"):
  58. return 0 if not to_bool else False
  59. else:
  60. raise ValueError(f"invalid truth value {value}")
  61. def get_int_from_env(env_keys, default):
  62. """Returns the first positive env value found in the `env_keys` list or the default."""
  63. for e in env_keys:
  64. val = int(os.environ.get(e, -1))
  65. if val >= 0:
  66. return val
  67. return default
  68. def parse_flag_from_env(key, default=False):
  69. """Returns truthy value for `key` from the env if available else the default."""
  70. value = os.environ.get(key, str(default))
  71. return str_to_bool(value) == 1 # As its name indicates `str_to_bool` actually returns an int...
  72. def parse_choice_from_env(key, default="no"):
  73. value = os.environ.get(key, str(default))
  74. return value
  75. def are_libraries_initialized(*library_names: str) -> list[str]:
  76. """
  77. Checks if any of `library_names` are imported in the environment. Will return any names that are.
  78. """
  79. return [lib_name for lib_name in library_names if lib_name in sys.modules.keys()]
  80. def get_current_device_type() -> tuple[str, str]:
  81. """
  82. Determines the current device type and distributed type without initializing any device.
  83. This is particularly important when using fork-based multiprocessing, as device initialization
  84. before forking can cause errors.
  85. The device detection order follows the same priority as state.py:_prepare_backend():
  86. MLU -> SDAA -> MUSA -> NPU -> HPU -> CUDA -> XPU
  87. Returns:
  88. tuple[str, str]: A tuple of (device_type, distributed_type)
  89. - device_type: The device string (e.g., "cuda", "npu", "xpu")
  90. - distributed_type: The distributed type string (e.g., "MULTI_GPU", "MULTI_NPU")
  91. Example:
  92. ```python
  93. >>> device_type, distributed_type = get_current_device_type()
  94. >>> print(device_type) # "cuda"
  95. >>> print(distributed_type) # "MULTI_GPU"
  96. ```
  97. """
  98. from .imports import (
  99. is_hpu_available,
  100. is_mlu_available,
  101. is_musa_available,
  102. is_npu_available,
  103. is_sdaa_available,
  104. is_xpu_available,
  105. )
  106. if is_mlu_available():
  107. return "mlu", "MULTI_MLU"
  108. elif is_sdaa_available():
  109. return "sdaa", "MULTI_SDAA"
  110. elif is_musa_available():
  111. return "musa", "MULTI_MUSA"
  112. elif is_npu_available():
  113. return "npu", "MULTI_NPU"
  114. elif is_hpu_available():
  115. return "hpu", "MULTI_HPU"
  116. elif torch.cuda.is_available():
  117. return "cuda", "MULTI_GPU"
  118. elif is_xpu_available():
  119. return "xpu", "MULTI_XPU"
  120. else:
  121. # Default to CUDA even if not available (for CPU-only scenarios where CUDA code paths are still used)
  122. return "cuda", "MULTI_GPU"
  123. def _nvidia_smi():
  124. """
  125. Returns the right nvidia-smi command based on the system.
  126. """
  127. if platform.system() == "Windows":
  128. # If platform is Windows and nvidia-smi can't be found in path
  129. # try from systemd drive with default installation path
  130. command = which("nvidia-smi")
  131. if command is None:
  132. command = f"{os.environ['systemdrive']}\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe"
  133. else:
  134. command = "nvidia-smi"
  135. return command
  136. def get_gpu_info():
  137. """
  138. Gets GPU count and names using `nvidia-smi` instead of torch to not initialize CUDA.
  139. Largely based on the `gputil` library.
  140. """
  141. # Returns as list of `n` GPUs and their names
  142. output = subprocess.check_output(
  143. [_nvidia_smi(), "--query-gpu=count,name", "--format=csv,noheader"], universal_newlines=True
  144. )
  145. output = output.strip()
  146. gpus = output.split(os.linesep)
  147. # Get names from output
  148. gpu_count = len(gpus)
  149. gpu_names = [gpu.split(",")[1].strip() for gpu in gpus]
  150. return gpu_names, gpu_count
  151. def get_driver_version():
  152. """
  153. Returns the driver version
  154. In the case of multiple GPUs, will return the first.
  155. """
  156. output = subprocess.check_output(
  157. [_nvidia_smi(), "--query-gpu=driver_version", "--format=csv,noheader"], universal_newlines=True
  158. )
  159. output = output.strip()
  160. return output.split(os.linesep)[0]
  161. def check_cuda_p2p_ib_support():
  162. """
  163. Checks if the devices being used have issues with P2P and IB communications, namely any consumer GPU hardware after
  164. the 3090.
  165. Notably uses `nvidia-smi` instead of torch to not initialize CUDA.
  166. """
  167. try:
  168. device_names, device_count = get_gpu_info()
  169. # As new consumer GPUs get released, add them to `unsupported_devices``
  170. unsupported_devices = {"RTX 40"}
  171. if device_count > 1:
  172. if any(
  173. unsupported_device in device_name
  174. for device_name in device_names
  175. for unsupported_device in unsupported_devices
  176. ):
  177. # Check if they have the right driver version
  178. acceptable_driver_version = "550.40.07"
  179. current_driver_version = get_driver_version()
  180. if parse(current_driver_version) < parse(acceptable_driver_version):
  181. return False
  182. return True
  183. except Exception:
  184. pass
  185. return True
  186. @lru_cache
  187. def check_cuda_fp8_capability():
  188. """
  189. Checks if the current GPU available supports FP8.
  190. Notably might initialize `torch.cuda` to check.
  191. """
  192. try:
  193. # try to get the compute capability from nvidia-smi
  194. output = subprocess.check_output(
  195. [_nvidia_smi(), "--query-gpu=compute_capability", "--format=csv,noheader"], universal_newlines=True
  196. )
  197. output = output.strip()
  198. # we take the first GPU's compute capability
  199. compute_capability = tuple(map(int, output.split(os.linesep)[0].split(".")))
  200. except Exception:
  201. compute_capability = torch.cuda.get_device_capability()
  202. return compute_capability >= (8, 9)
  203. @dataclass
  204. class CPUInformation:
  205. """
  206. Stores information about the CPU in a distributed environment. It contains the following attributes:
  207. - rank: The rank of the current process.
  208. - world_size: The total number of processes in the world.
  209. - local_rank: The rank of the current process on the local node.
  210. - local_world_size: The total number of processes on the local node.
  211. """
  212. rank: int = field(default=0, metadata={"help": "The rank of the current process."})
  213. world_size: int = field(default=1, metadata={"help": "The total number of processes in the world."})
  214. local_rank: int = field(default=0, metadata={"help": "The rank of the current process on the local node."})
  215. local_world_size: int = field(default=1, metadata={"help": "The total number of processes on the local node."})
  216. def get_cpu_distributed_information() -> CPUInformation:
  217. """
  218. Returns various information about the environment in relation to CPU distributed training as a `CPUInformation`
  219. dataclass.
  220. """
  221. information = {}
  222. information["rank"] = get_int_from_env(["RANK", "PMI_RANK", "OMPI_COMM_WORLD_RANK", "MV2_COMM_WORLD_RANK"], 0)
  223. information["world_size"] = get_int_from_env(
  224. ["WORLD_SIZE", "PMI_SIZE", "OMPI_COMM_WORLD_SIZE", "MV2_COMM_WORLD_SIZE"], 1
  225. )
  226. information["local_rank"] = get_int_from_env(
  227. ["LOCAL_RANK", "MPI_LOCALRANKID", "OMPI_COMM_WORLD_LOCAL_RANK", "MV2_COMM_WORLD_LOCAL_RANK"], 0
  228. )
  229. information["local_world_size"] = get_int_from_env(
  230. ["LOCAL_WORLD_SIZE", "MPI_LOCALNRANKS", "OMPI_COMM_WORLD_LOCAL_SIZE", "MV2_COMM_WORLD_LOCAL_SIZE"],
  231. 1,
  232. )
  233. return CPUInformation(**information)
  234. def override_numa_affinity(local_process_index: int, verbose: Optional[bool] = None) -> None:
  235. """
  236. Overrides whatever NUMA affinity is set for the current process. This is very taxing and requires recalculating the
  237. affinity to set, ideally you should use `utils.environment.set_numa_affinity` instead.
  238. Args:
  239. local_process_index (int):
  240. The index of the current process on the current server.
  241. verbose (bool, *optional*):
  242. Whether to log out the assignment of each CPU. If `ACCELERATE_DEBUG_MODE` is enabled, will default to True.
  243. """
  244. if verbose is None:
  245. verbose = parse_flag_from_env("ACCELERATE_DEBUG_MODE", False)
  246. if torch.cuda.is_available():
  247. from accelerate.utils import is_pynvml_available
  248. if not is_pynvml_available():
  249. raise ImportError(
  250. "To set CPU affinity on CUDA GPUs the `nvidia-ml-py` package must be available. (`pip install nvidia-ml-py`)"
  251. )
  252. import pynvml as nvml
  253. # The below code is based on https://github.com/NVIDIA/DeepLearningExamples/blob/master/TensorFlow2/LanguageModeling/BERT/gpu_affinity.py
  254. nvml.nvmlInit()
  255. num_elements = math.ceil(os.cpu_count() / 64)
  256. handle = nvml.nvmlDeviceGetHandleByIndex(local_process_index)
  257. affinity_string = ""
  258. for j in nvml.nvmlDeviceGetCpuAffinity(handle, num_elements):
  259. # assume nvml returns list of 64 bit ints
  260. affinity_string = f"{j:064b}{affinity_string}"
  261. affinity_list = [int(x) for x in affinity_string]
  262. affinity_list.reverse() # so core 0 is the 0th element
  263. affinity_to_set = [i for i, e in enumerate(affinity_list) if e != 0]
  264. os.sched_setaffinity(0, affinity_to_set)
  265. if verbose:
  266. cpu_cores = os.sched_getaffinity(0)
  267. logger.info(f"Assigning {len(cpu_cores)} cpu cores to process {local_process_index}: {cpu_cores}")
  268. @lru_cache
  269. def set_numa_affinity(local_process_index: int, verbose: Optional[bool] = None) -> None:
  270. """
  271. Assigns the current process to a specific NUMA node. Ideally most efficient when having at least 2 cpus per node.
  272. This result is cached between calls. If you want to override it, please use
  273. `accelerate.utils.environment.override_numa_afifnity`.
  274. Args:
  275. local_process_index (int):
  276. The index of the current process on the current server.
  277. verbose (bool, *optional*):
  278. Whether to print the new cpu cores assignment for each process. If `ACCELERATE_DEBUG_MODE` is enabled, will
  279. default to True.
  280. """
  281. override_numa_affinity(local_process_index=local_process_index, verbose=verbose)
  282. @contextmanager
  283. def clear_environment():
  284. """
  285. A context manager that will temporarily clear environment variables.
  286. When this context exits, the previous environment variables will be back.
  287. Example:
  288. ```python
  289. >>> import os
  290. >>> from accelerate.utils import clear_environment
  291. >>> os.environ["FOO"] = "bar"
  292. >>> with clear_environment():
  293. ... print(os.environ)
  294. ... os.environ["FOO"] = "new_bar"
  295. ... print(os.environ["FOO"])
  296. {}
  297. new_bar
  298. >>> print(os.environ["FOO"])
  299. bar
  300. ```
  301. """
  302. _old_os_environ = os.environ.copy()
  303. os.environ.clear()
  304. try:
  305. yield
  306. finally:
  307. os.environ.clear() # clear any added keys,
  308. os.environ.update(_old_os_environ) # then restore previous environment
  309. @contextmanager
  310. def patch_environment(**kwargs):
  311. """
  312. A context manager that will add each keyword argument passed to `os.environ` and remove them when exiting.
  313. Will convert the values in `kwargs` to strings and upper-case all the keys.
  314. Example:
  315. ```python
  316. >>> import os
  317. >>> from accelerate.utils import patch_environment
  318. >>> with patch_environment(FOO="bar"):
  319. ... print(os.environ["FOO"]) # prints "bar"
  320. >>> print(os.environ["FOO"]) # raises KeyError
  321. ```
  322. """
  323. existing_vars = {}
  324. for key, value in kwargs.items():
  325. key = key.upper()
  326. if key in os.environ:
  327. existing_vars[key] = os.environ[key]
  328. os.environ[key] = str(value)
  329. try:
  330. yield
  331. finally:
  332. for key in kwargs:
  333. key = key.upper()
  334. if key in existing_vars:
  335. # restore previous value
  336. os.environ[key] = existing_vars[key]
  337. else:
  338. os.environ.pop(key, None)
  339. def purge_accelerate_environment(func_or_cls):
  340. """Decorator to clean up accelerate environment variables set by the decorated class or function.
  341. In some circumstances, calling certain classes or functions can result in accelerate env vars being set and not
  342. being cleaned up afterwards. As an example, when calling:
  343. TrainingArguments(fp16=True, ...)
  344. The following env var will be set:
  345. ACCELERATE_MIXED_PRECISION=fp16
  346. This can affect subsequent code, since the env var takes precedence over TrainingArguments(fp16=False). This is
  347. especially relevant for unit testing, where we want to avoid the individual tests to have side effects on one
  348. another. Decorate the unit test function or whole class with this decorator to ensure that after each test, the env
  349. vars are cleaned up. This works for both unittest.TestCase and normal classes (pytest); it also works when
  350. decorating the parent class.
  351. """
  352. prefix = "ACCELERATE_"
  353. @contextmanager
  354. def env_var_context():
  355. # Store existing accelerate env vars
  356. existing_vars = {k: v for k, v in os.environ.items() if k.startswith(prefix)}
  357. try:
  358. yield
  359. finally:
  360. # Restore original env vars or remove new ones
  361. for key in [k for k in os.environ if k.startswith(prefix)]:
  362. if key in existing_vars:
  363. os.environ[key] = existing_vars[key]
  364. else:
  365. os.environ.pop(key, None)
  366. def wrap_function(func):
  367. @wraps(func)
  368. def wrapper(*args, **kwargs):
  369. with env_var_context():
  370. return func(*args, **kwargs)
  371. wrapper._accelerate_is_purged_environment_wrapped = True
  372. return wrapper
  373. if not isinstance(func_or_cls, type):
  374. return wrap_function(func_or_cls)
  375. # Handle classes by wrapping test methods
  376. def wrap_test_methods(test_class_instance):
  377. for name in dir(test_class_instance):
  378. if name.startswith("test"):
  379. method = getattr(test_class_instance, name)
  380. if callable(method) and not hasattr(method, "_accelerate_is_purged_environment_wrapped"):
  381. setattr(test_class_instance, name, wrap_function(method))
  382. return test_class_instance
  383. # Handle inheritance
  384. wrap_test_methods(func_or_cls)
  385. func_or_cls.__init_subclass__ = classmethod(lambda cls, **kw: wrap_test_methods(cls))
  386. return func_or_cls