main
py 115 lines 3.42 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """CPU hardware discovery for optional vLLM Recipes runtime tuning.
5
6 Keep this module focused on facts exposed to the current process/container.
7 Tuning decisions belong in runtime_tuning.py.
8 """
9
10 from __future__ import annotations
11
12 import json
13 import platform
14 from dataclasses import asdict, dataclass
15 from pathlib import Path
16
17 from vllm.utils.cpu_resource_utils import (
18 get_allowed_cpu_list,
19 get_memory_affinity,
20 get_memory_node_info,
21 )
22
23
24 @dataclass(frozen=True)
25 class NumaNodeInfo:
26 node_id: int
27 logical_cpu_ids: tuple[int, ...]
28 physical_core_count: int
29 total_memory_bytes: int
30 available_memory_bytes: int
31
32
33 @dataclass(frozen=True)
34 class HardwareInfo:
35 architecture: str
36 socket_count: int | None
37 numa_nodes: tuple[NumaNodeInfo, ...]
38 allowed_logical_cpu_count: int
39 physical_core_count: int
40
41 @property
42 def numa_node_count(self) -> int:
43 return len(self.numa_nodes)
44
45 def to_dict(self) -> dict[str, object]:
46 return asdict(self)
47
48
49 def _read_socket_id(cpu_id: int) -> int | None:
50 path = Path(f"/sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id")
51 try:
52 return int(path.read_text().strip())
53 except (OSError, ValueError):
54 return None
55
56
57 def detect_hardware() -> HardwareInfo:
58 """Return CPU resources effectively available to this process.
59
60 vLLM's CPU helpers already honor process CPU affinity and cgroup-aware
61 memory limits. We intentionally use get_memory_affinity() rather than
62 get_visible_memory_node() so CPU_VISIBLE_MEMORY_NODES does not become an
63 input to the detector; auto binding remains owned by the CPU backend.
64 """
65 cpus = get_allowed_cpu_list()
66 if not cpus:
67 raise RuntimeError("vLLM reported no CPUs available to this process.")
68
69 cpu_numa_nodes = {cpu.numa_node for cpu in cpus if cpu.numa_node >= 0}
70 memory_numa_nodes = set(get_memory_affinity())
71 effective_numa_nodes = sorted(cpu_numa_nodes & memory_numa_nodes)
72 if not effective_numa_nodes:
73 effective_numa_nodes = sorted(cpu_numa_nodes)
74
75 physical_cores = {
76 (cpu.numa_node, cpu.physical_core) for cpu in cpus if cpu.physical_core >= 0
77 }
78
79 numa_nodes = []
80 for node_id in effective_numa_nodes:
81 node_cpus = [cpu for cpu in cpus if cpu.numa_node == node_id]
82 node_physical_cores = {
83 cpu.physical_core for cpu in node_cpus if cpu.physical_core >= 0
84 }
85 memory = get_memory_node_info(node_id)
86 numa_nodes.append(
87 NumaNodeInfo(
88 node_id=node_id,
89 logical_cpu_ids=tuple(sorted(cpu.id for cpu in node_cpus)),
90 physical_core_count=len(node_physical_cores),
91 total_memory_bytes=memory.total_memory,
92 available_memory_bytes=memory.available_memory,
93 )
94 )
95
96 socket_ids = {
97 socket_id for cpu in cpus if (socket_id := _read_socket_id(cpu.id)) is not None
98 }
99
100 return HardwareInfo(
101 architecture=platform.machine(),
102 socket_count=len(socket_ids) if socket_ids else None,
103 numa_nodes=tuple(numa_nodes),
104 allowed_logical_cpu_count=len(cpus),
105 physical_core_count=len(physical_cores),
106 )
107
108
109 def main() -> int:
110 print(json.dumps(detect_hardware().to_dict(), indent=2))
111 return 0
112
113
114 if __name__ == "__main__":
115 raise SystemExit(main())