main
py 196 lines 6.45 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from __future__ import annotations
5
6 import contextlib
7 import ctypes
8 import importlib.util
9 import os
10 from collections.abc import Iterator, Mapping
11
12 import torch
13
14 import vllm.envs as envs
15 from vllm.logger import init_logger
16
17 logger = init_logger(__name__)
18
19
20 def find_nccl_library() -> str:
21 """Return NCCL/RCCL shared library name to load.
22
23 Uses `VLLM_NCCL_SO_PATH` if set; otherwise chooses by torch backend.
24 """
25 so_file = envs.VLLM_NCCL_SO_PATH
26 if so_file:
27 logger.info(
28 "Found nccl from environment variable VLLM_NCCL_SO_PATH=%s", so_file
29 )
30 else:
31 if torch.version.cuda is not None:
32 so_file = "libnccl.so.2"
33 elif torch.version.hip is not None:
34 so_file = "librccl.so.1"
35 else:
36 raise ValueError("NCCL only supports CUDA and ROCm backends.")
37 logger.debug_once("Found nccl from library %s", so_file)
38 return so_file
39
40
41 def find_nccl_include_paths() -> list[str] | None:
42 """Return possible include paths containing `nccl.h`.
43
44 Considers `VLLM_NCCL_INCLUDE_PATH` and the `nvidia-nccl-cuXX` package.
45 """
46 paths: list[str] = []
47 inc = envs.VLLM_NCCL_INCLUDE_PATH
48 if inc and os.path.isdir(inc):
49 paths.append(inc)
50
51 try:
52 spec = importlib.util.find_spec("nvidia.nccl")
53 if spec and (locs := getattr(spec, "submodule_search_locations", None)):
54 for loc in locs:
55 inc_dir = os.path.join(loc, "include")
56 if os.path.exists(os.path.join(inc_dir, "nccl.h")):
57 paths.append(inc_dir)
58 except Exception as e:
59 logger.debug("Failed to find nccl include path from nvidia.nccl package: %s", e)
60
61 seen: set[str] = set()
62 out: list[str] = []
63 for p in paths:
64 if p and p not in seen:
65 out.append(p)
66 seen.add(p)
67 return out or None
68
69
70 def find_nccl_library_paths() -> list[str] | None:
71 """Return possible library paths containing `libnccl.so`.
72
73 Looks inside the `nvidia-nccl-cuXX` pip package.
74 """
75 paths: list[str] = []
76 try:
77 spec = importlib.util.find_spec("nvidia.nccl")
78 if spec and (locs := getattr(spec, "submodule_search_locations", None)):
79 for loc in locs:
80 lib_dir = os.path.join(loc, "lib")
81 if os.path.isdir(lib_dir):
82 paths.append(lib_dir)
83 except Exception as e:
84 logger.debug("Failed to find nccl library path from nvidia.nccl package: %s", e)
85 return paths or None
86
87
88 def query_nccl_gin_type(
89 group: torch.distributed.ProcessGroup, *, railed: bool = False
90 ) -> int | None:
91 """Return the full or railed GIN type, or ``None`` on query failure."""
92 from vllm.distributed.device_communicators.pynccl_wrapper import (
93 NCCL_COMM_PROPERTIES_LAYOUT_VERSION,
94 NCCLLibrary,
95 ncclCommProperties,
96 )
97
98 try:
99 backend = group._get_backend(torch.device("cuda"))
100 # GIN is a property of this initialized communicator, not just the
101 # NCCL version. ncclCommQueryProperties requires its ncclComm_t.
102 comm_ptr = backend._comm_ptr()
103 if comm_ptr == 0:
104 return None
105 except Exception:
106 logger.warning(
107 "Failed to extract NCCL comm pointer from process group",
108 exc_info=True,
109 )
110 return None
111
112 try:
113 nccl = NCCLLibrary()
114 query_fn = nccl._funcs.get("ncclCommQueryProperties")
115 if query_fn is None:
116 return None
117
118 props = ncclCommProperties()
119 ctypes.memset(ctypes.addressof(props), 0, ctypes.sizeof(props))
120 props.size = ctypes.sizeof(props)
121 props.magic = 0xCAFEBEEF
122 props.version = min(
123 nccl.ncclGetRawVersion(), NCCL_COMM_PROPERTIES_LAYOUT_VERSION
124 )
125 result = query_fn(ctypes.c_void_p(comm_ptr), ctypes.byref(props))
126 except Exception:
127 logger.warning("Failed to query NCCL communicator properties", exc_info=True)
128 return None
129
130 if result != 0:
131 logger.warning("ncclCommQueryProperties returned error %d", result)
132 return None
133 return props.railedGinType if railed else props.ginType
134
135
136 # Values the variables set by `pin_nccl_env` had before it: None until something
137 # is pinned, empty if NCCL cannot re-read them.
138 _unpinned_env: dict[str, str | None] | None = None
139
140
141 def pin_nccl_env(pins: dict[str, str]) -> None:
142 """Set NCCL variables for this process's own communicators.
143
144 NCCL never reconciles these across ranks, so communicators shared with a
145 process that does not set them are created without them, see
146 `unpinned_nccl_env`. NCCL caches most variables on first read; listing the
147 pins in NCCL_NO_CACHE makes it re-read them per communicator. NCCL parses
148 that list once, so this must run before the process's first NCCL call.
149 """
150 global _unpinned_env
151 launch_env = _swap_env(pins)
152 if _unpinned_env is not None:
153 return
154 _unpinned_env = launch_env if _nccl_has_no_cache() else {}
155 if _unpinned_env:
156 no_cache = os.environ.get("NCCL_NO_CACHE")
157 os.environ["NCCL_NO_CACHE"] = ",".join(filter(None, [no_cache, *pins]))
158
159
160 @contextlib.contextmanager
161 def unpinned_nccl_env() -> Iterator[None]:
162 """Create communicators shared with other processes without the pins."""
163 if _unpinned_env == {}:
164 logger.warning_once(
165 "NCCL_NO_CACHE needs CUDA NCCL >= 2.29.7, so the NCCL settings vLLM "
166 "pinned also apply here; a process sharing this communicator must "
167 "set the same NCCL_* variables or it will hang."
168 )
169 pinned = _swap_env(_unpinned_env or {})
170 try:
171 yield
172 finally:
173 _swap_env(pinned)
174
175
176 def _nccl_has_no_cache() -> bool:
177 # NCCL_NO_CACHE arrived in NCCL 2.29.7; ask the library vLLM loads.
178 if torch.version.cuda is None:
179 return False
180 version = ctypes.c_int()
181 try:
182 ctypes.CDLL(find_nccl_library()).ncclGetVersion(ctypes.byref(version))
183 except Exception:
184 return False
185 return version.value >= 22907
186
187
188 def _swap_env(values: Mapping[str, str | None]) -> dict[str, str | None]:
189 """Set (None: unset) the given variables and return their previous values."""
190 old = {name: os.environ.get(name) for name in values}
191 for name, value in values.items():
192 if value is None:
193 os.environ.pop(name, None)
194 else:
195 os.environ[name] = value
196 return old