main
py 298 lines 12.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """OMP Aware Multiprocessing manager for running multiprocessing.Process()
4 Copyright (c) 2026 Red Hat Inc
5 Copyright (c) 2026 Cambridge Greys Ltd
6 """
7
8 import os
9 from collections.abc import Callable
10 from contextlib import contextmanager
11 from typing import TYPE_CHECKING
12
13 import vllm.utils.cpu_resource_utils as cr_utils
14 from vllm import envs
15 from vllm.logger import init_logger
16 from vllm.platforms import CpuArchEnum, current_platform
17 from vllm.utils.cpu_resource_utils import LogicalCPUInfo
18
19 if TYPE_CHECKING:
20 from vllm.config import VllmConfig
21
22 logger = init_logger(__name__)
23
24
25 class OMPProcessManager:
26 def __init__(self, config: "VllmConfig"):
27 if not current_platform.is_cpu():
28 return
29
30 self.local_world_size = config.parallel_config.local_world_size
31 self.local_dp_rank = config.parallel_config.data_parallel_rank_local
32 # This is a bit tricky because the internal DP size
33 # is always 1 for non-MoE models
34 self.internal_dp_size = config.parallel_config._api_process_count
35
36 self.simulate_multi_node = os.environ.get("VLLM_CPU_SIM_MULTI_NUMA", "0") != "0"
37 ld_preload_str = os.getenv("LD_PRELOAD", "")
38 self.use_iomp = "libiomp" in ld_preload_str or "libomp" in ld_preload_str
39 self.use_gomp = "libgomp" in ld_preload_str
40
41 assert not (self.use_iomp and self.use_gomp)
42
43 # at least reserve 1/local_world_size(for ARM/RISC-V) core for scheduler
44 # proc as always use MP executor
45 # TODO: make scheduler proc sleep when idle
46 self.reserve_cpu_num = (
47 self.local_world_size
48 if current_platform.get_cpu_architecture()
49 in (CpuArchEnum.X86, CpuArchEnum.ARM, CpuArchEnum.RISCV)
50 else 1
51 )
52 # reserve at one more core for nixl_connector under p/d case
53 if config.kv_transfer_config:
54 self.reserve_cpu_num += self.local_world_size
55
56 if envs.VLLM_CPU_NUM_OF_RESERVED_CPU is not None:
57 if self.reserve_cpu_num > envs.VLLM_CPU_NUM_OF_RESERVED_CPU:
58 msg = (
59 f"VLLM_CPU_NUM_OF_RESERVED_CPU is less than "
60 "the minimum requirement"
61 f": {self.reserve_cpu_num} cores"
62 )
63 logger.warning(msg=msg)
64 self.reserve_cpu_num = envs.VLLM_CPU_NUM_OF_RESERVED_CPU
65
66 self._parse_omp_threads_bind_env()
67
68 assert not self.simulate_multi_node or self.auto_setup
69
70 @contextmanager
71 def configure_omp_envs(self, rank: int, local_rank: int):
72 if not current_platform.is_cpu() or self.skip_setup:
73 yield
74 return
75
76 envs_dict = {}
77 cpu_list = [str(i) for i in self.cpu_lists[local_rank]]
78 envs_dict["OMP_NUM_THREADS"] = str(len(cpu_list))
79 if self.use_iomp:
80 # set IOMP envs
81 cpu_list_str = ",".join(cpu_list)
82 envs_dict["KMP_AFFINITY"] = (
83 f"granularity=fine,explicit,proclist=[{cpu_list_str}]"
84 )
85 # The time(milliseconds) that a thread should wait after
86 # completing the execution of a parallel region, before sleeping.
87 # A value of 5 masks thread underutilization.
88 # Set to 1 when debugging thread utilization issues.
89 envs_dict["KMP_BLOCKTIME"] = "5"
90 # Prevents the CPU to run into low performance state
91 envs_dict["KMP_TPAUSE"] = "0"
92 elif self.use_gomp:
93 # set GOMP envs
94 # likes '0 1 2 ...'
95 cpu_list_str = " ".join(cpu_list)
96 envs_dict["GOMP_CPU_AFFINITY"] = cpu_list_str
97 else:
98 # set OMP envs
99 # likes '{0,1,2,...}'
100 cpu_list_str = ",".join(cpu_list)
101 envs_dict["OMP_PLACES"] = f"{{{cpu_list_str}}}"
102 envs_dict["OMP_PROC_BIND"] = "true"
103
104 # backup envs
105 old_envs_dict = {}
106 for k in envs_dict:
107 old_envs_dict[k] = os.environ.get(k)
108
109 try:
110 # set envs
111 for k, v in envs_dict.items():
112 os.environ[k] = v
113 yield
114 finally:
115 # restore old envs
116 for k, v in old_envs_dict.items(): # type: ignore
117 if v is None:
118 os.environ.pop(k, None)
119 else:
120 os.environ[k] = v
121
122 def _parse_omp_threads_bind_env(self):
123 vllm_mask = envs.VLLM_CPU_OMP_THREADS_BIND
124 self.skip_setup = vllm_mask == "nobind"
125 self.auto_setup = vllm_mask == "auto"
126 self.reserved_cpu_list = []
127 self.cpu_lists = []
128
129 if self.auto_setup:
130 # auto generate CPU lists
131 cpu_arch = current_platform.get_cpu_architecture()
132 if cpu_arch == CpuArchEnum.POWERPC:
133 # For POWERPC SMT-8/4/2
134 cpu_list, reserve_list = self._get_autobind_cpu_ids(
135 lambda cpus: [cpu for cpu in cpus if cpu.id % 8 < 4]
136 )
137 elif cpu_arch in (CpuArchEnum.X86, CpuArchEnum.S390X):
138 # For x86/S390X SMT-2, use 1 logical CPU per physical core
139 cpu_list, reserve_list = self._get_autobind_cpu_ids(
140 lambda cpus: cpus[-1:]
141 )
142 elif cpu_arch in (CpuArchEnum.ARM, CpuArchEnum.RISCV):
143 # For AArch64 / RISC-V, no SMT, use all logical CPUs
144 cpu_list, reserve_list = self._get_autobind_cpu_ids(lambda cpus: cpus)
145 else:
146 cpu_list, reserve_list = [], []
147 raise RuntimeError(f"{cpu_arch} doesn't support auto CPU binding.")
148
149 for item in cpu_list:
150 self.cpu_lists.append([x.id for x in item])
151 self.reserved_cpu_list = [x.id for x in reserve_list]
152 elif not self.skip_setup:
153 # user defined CPU lists
154 omp_cpuids_list = vllm_mask.split("|")
155 if self.local_dp_rank is not None:
156 local_dp_rank = self.local_dp_rank
157 world_size = self.local_world_size
158 # Rank mapping [DP, PP, TP]
159 omp_cpuids_list = omp_cpuids_list[
160 local_dp_rank * world_size : (local_dp_rank + 1) * world_size
161 ]
162
163 assert len(omp_cpuids_list) == self.local_world_size, (
164 "Given "
165 f"number of CPU id list {omp_cpuids_list} doesn't match "
166 f"local world size {self.local_world_size}."
167 )
168
169 # parse CPU list strings like "5,2-4" to [5, 2, 3, 4]
170 self.cpu_lists = [cr_utils.parse_id_list(s) for s in omp_cpuids_list]
171 else:
172 # skip
173 self.cpu_lists = []
174
175 msg = (
176 "OpenMP thread binding info: \n"
177 f"\tVLLM_CPU_OMP_THREADS_BIND={vllm_mask!r}, "
178 f"auto_setup={self.auto_setup}, skip_setup={self.skip_setup}\n"
179 f"\tlocal_world_size={self.local_world_size}, "
180 f"reserve_cpu_num={self.reserve_cpu_num}\n"
181 )
182 for i, cpus in enumerate(self.cpu_lists):
183 msg += f"\tlocal_rank={i}, core ids={cpus}\n"
184 msg += f"\treserved_cpus={self.reserved_cpu_list}"
185 logger.info(msg)
186
187 def _get_autobind_cpu_ids(
188 self, cpu_selector: Callable[[list[LogicalCPUInfo]], list[LogicalCPUInfo]]
189 ) -> tuple[list[list[LogicalCPUInfo]], list[LogicalCPUInfo]]:
190 """Return CPU ids to bind based on NUMA nodes, and CPU ids reserved for
191 other processes.
192 Currently for rank N, only CPU ids on the N-th node in available NUMA
193 node list will be selected.
194
195 Args:
196 cpu_selector: a callable object to select CPUs from a CPU list
197 of a physical core. The input is a LogicalCPUInfo list contains
198 logical CPUs of a physical CPU, sorted by the
199 LogicalCPUInfo.id. A selected LogicalCPUInfo list should be
200 returned.
201
202 """
203 # this memory node list has been sliced for DP offset
204 allowed_numa_nodes = cr_utils.get_visible_memory_node()
205 logical_cpu_list = cr_utils.get_allowed_cpu_list()
206
207 local_world_size = self.local_world_size
208
209 # On s390x, numa_node is remapped to book IDs (the best CPU
210 # partitioning level). Derive topology domains from the CPU list
211 # since they no longer correspond to physical memory nodes.
212 cpu_arch = current_platform.get_cpu_architecture()
213 if cpu_arch == CpuArchEnum.S390X:
214 allowed_numa_nodes = sorted(set(cpu.numa_node for cpu in logical_cpu_list))
215
216 assert (
217 len(allowed_numa_nodes) >= local_world_size or self.simulate_multi_node
218 ), (
219 f"Not enough allowed NUMA nodes to bind threads of "
220 f"{local_world_size} local CPUWorkers. "
221 f"Allowed NUMA nodes are {allowed_numa_nodes}. "
222 "Please try to bind threads manually or decrease DP/TP/PP."
223 )
224
225 # Generate OMP CPU list for each rank
226 cpu_lists_of_ranks = []
227 reserved_cpu_list = []
228 total_cpu_num = 0
229 for local_rank in range(self.local_world_size):
230 if not self.simulate_multi_node:
231 selected_numa_node = allowed_numa_nodes[local_rank]
232 selected_logical_cpu_list = [
233 x for x in logical_cpu_list if x.numa_node == selected_numa_node
234 ]
235 else:
236 world_size_across_dp = self.local_world_size * self.internal_dp_size
237 assert len(logical_cpu_list) >= world_size_across_dp
238 selected_logical_cpu_list = sorted(
239 logical_cpu_list, key=lambda x: x.numa_node
240 )
241 sim_cpu_num_per_node = (
242 len(selected_logical_cpu_list) // world_size_across_dp
243 )
244 assert self.local_dp_rank is not None
245 start_idx = (
246 local_rank + self.local_world_size * self.local_dp_rank
247 ) * sim_cpu_num_per_node
248 selected_logical_cpu_list = selected_logical_cpu_list[
249 start_idx : (start_idx + sim_cpu_num_per_node)
250 ]
251
252 # Select logical CPUs on same physical cores via cpu_selector
253 core_to_cpus: dict[int, list[LogicalCPUInfo]] = {}
254 for cpu_info in selected_logical_cpu_list:
255 if cpu_info.physical_core not in core_to_cpus:
256 core_to_cpus[cpu_info.physical_core] = []
257 core_to_cpus[cpu_info.physical_core].append(cpu_info)
258 selected_logical_cpu_list = []
259 for cpu_list in core_to_cpus.values():
260 cpu_list = sorted(cpu_list, key=lambda x: x.id)
261 selected_logical_cpu_list.extend(cpu_selector(cpu_list))
262
263 # sort selected cores based on core id
264 selected_logical_cpu_list = sorted(
265 selected_logical_cpu_list, key=lambda x: x.id
266 )
267
268 cpu_lists_of_ranks.append(selected_logical_cpu_list)
269 total_cpu_num += len(selected_logical_cpu_list)
270
271 # Reserve CPUs for other processes
272 if total_cpu_num <= self.reserve_cpu_num:
273 logger.warning(
274 "Selected CPU core number (%s) "
275 "should be greater than reserved CPU core "
276 "number (%s).",
277 total_cpu_num,
278 self.reserve_cpu_num,
279 )
280 return cpu_lists_of_ranks, []
281
282 reserve_num_per_rank = [
283 self.reserve_cpu_num // self.local_world_size
284 ] * self.local_world_size
285 # last rank first
286 for i in range(
287 self.local_world_size - 1,
288 self.local_world_size - 1 - self.reserve_cpu_num % self.local_world_size,
289 -1,
290 ):
291 reserve_num_per_rank[i] += 1
292 for i in range(self.local_world_size):
293 num = reserve_num_per_rank[i]
294 if num > 0:
295 reserved_cpu_list.extend(cpu_lists_of_ranks[i][-num:])
296 cpu_lists_of_ranks[i] = cpu_lists_of_ranks[i][:-num]
297
298 return cpu_lists_of_ranks, reserved_cpu_list