Skip to content

Commit cb5cc4c

Browse files
committed
feat: support cpu/numa affinity in docker deploy
Signed-off-by: thxCode <thxcode0824@gmail.com>
1 parent 08cd89c commit cb5cc4c

File tree

8 files changed

+161
-30
lines changed

8 files changed

+161
-30
lines changed

gpustack_runtime/deployer/__types__.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515
from .. import envs
1616
from ..detector import (
17-
Devices,
18-
ManufacturerEnum,
17+
Topology,
1918
detect_devices,
19+
get_devices_topologies,
20+
group_devices_by_manufacturer,
2021
manufacturer_to_backend,
2122
)
2223
from .__utils__ import (
@@ -1279,6 +1280,17 @@ class Deployer(ABC):
12791280
"AMD_VISIBLE_DEVICES": ["0", "1"]
12801281
}.
12811282
"""
1283+
_visible_devices_topologies: dict[str, Topology] | None = None
1284+
"""
1285+
Recorded visible devices topologies,
1286+
the key is the runtime visible devices env name,
1287+
the value is the corresponding topology.
1288+
For example:
1289+
{
1290+
"NVIDIA_VISIBLE_DEVICES": Topology(...),
1291+
"AMD_VISIBLE_DEVICES": Topology(...)
1292+
}.
1293+
"""
12821294
_backend_visible_devices_values_alignment: dict[str, dict[str, str]] | None = None
12831295
"""
12841296
Recorded backend visible devices values alignment,
@@ -1326,25 +1338,27 @@ def __enter__(self):
13261338
def __exit__(self, exc_type, exc_value, traceback):
13271339
self.close()
13281340

1329-
def _fetch_visible_devices_env_values(self):
1341+
def _prepare(self):
13301342
"""
1331-
Fetch the visible devices environment variables and values.
1343+
Detect devices once, and construct critical elements for post processing, including:
1344+
- Prepare visible devices environment variables mapping.
1345+
- Prepare visible devices values mapping.
1346+
- Prepare topology.
13321347
"""
13331348
if self._visible_devices_env:
13341349
return
13351350

13361351
self._visible_devices_env = {}
13371352
self._visible_devices_values = {}
1353+
self._visible_devices_topologies = {}
13381354
self._backend_visible_devices_values_alignment = {}
13391355

1340-
devices: dict[ManufacturerEnum, Devices] = {}
1341-
for dev in detect_devices(fast=False):
1342-
if dev.manufacturer not in devices:
1343-
devices[dev.manufacturer] = []
1344-
devices[dev.manufacturer].append(dev)
1356+
group_devices = group_devices_by_manufacturer(
1357+
detect_devices(fast=False),
1358+
)
13451359

1346-
if devices:
1347-
for manu, devs in devices.items():
1360+
if group_devices:
1361+
for manu, devs in group_devices.items():
13481362
backend = manufacturer_to_backend(manu)
13491363
rk = envs.GPUSTACK_RUNTIME_DETECT_BACKEND_MAP_RESOURCE_KEY.get(backend)
13501364
ren = envs.GPUSTACK_RUNTIME_DEPLOY_RESOURCE_KEY_MAP_RUNTIME_VISIBLE_DEVICES.get(
@@ -1377,6 +1391,13 @@ def _fetch_visible_devices_env_values(self):
13771391
self._backend_visible_devices_values_alignment[ben_item] = (
13781392
dev_indexes_alignment
13791393
)
1394+
if (
1395+
envs.GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY
1396+
or envs.GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY
1397+
):
1398+
topos = get_devices_topologies(devices=devs)
1399+
if topos:
1400+
self._visible_devices_topologies[ren] = topos[0]
13801401

13811402
if self._visible_devices_env:
13821403
return
@@ -1385,7 +1406,7 @@ def _fetch_visible_devices_env_values(self):
13851406
self._visible_devices_env["UNKNOWN_RUNTIME_VISIBLE_DEVICES"] = []
13861407
self._visible_devices_values["UNKNOWN_RUNTIME_VISIBLE_DEVICES"] = ["all"]
13871408

1388-
def visible_devices_env_values(
1409+
def get_visible_devices_env_values(
13891410
self,
13901411
) -> (dict[str, list[str]], dict[str, list[str]]):
13911412
"""
@@ -1410,9 +1431,44 @@ def visible_devices_env_values(
14101431
to lists of device indexes or UUIDs.
14111432
14121433
"""
1413-
self._fetch_visible_devices_env_values()
1434+
self._prepare()
14141435
return self._visible_devices_env, self._visible_devices_values
14151436

1437+
def get_visible_devices_affinities(
1438+
self,
1439+
runtime_env: list[str],
1440+
resource_value: str,
1441+
) -> tuple[str, str]:
1442+
"""
1443+
Get the CPU and NUMA affinities for the given runtime environment and resource value.
1444+
1445+
Args:
1446+
runtime_env:
1447+
The list of runtime visible devices environment variable names.
1448+
resource_value:
1449+
The resource value, which can be "all" or a comma-separated list of device indexes
1450+
1451+
Returns:
1452+
A tuple containing:
1453+
- A comma-separated string of CPU affinities.
1454+
- A comma-separated string of NUMA affinities.
1455+
1456+
"""
1457+
dev_indexes = []
1458+
if resource_value != "all":
1459+
dev_indexes = [int(v.strip()) for v in resource_value.split(",")]
1460+
1461+
cpus_set: list[str] = []
1462+
numas_set: list[str] = []
1463+
for re_ in runtime_env:
1464+
topo = self._visible_devices_topologies.get(re_)
1465+
if topo:
1466+
cs, ns = topo.get_affinities(dev_indexes, deduplicate=False)
1467+
cpus_set.extend(cs)
1468+
numas_set.extend(ns)
1469+
1470+
return ",".join(set(cpus_set)), ",".join(set(numas_set))
1471+
14161472
def align_backend_visible_devices_env_values(
14171473
self,
14181474
backend_visible_devices_env: str,
@@ -1440,7 +1496,7 @@ def align_backend_visible_devices_env_values(
14401496
not in envs.GPUSTACK_RUNTIME_DEPLOY_BACKEND_VISIBLE_DEVICES_VALUE_ALIGNMENT
14411497
):
14421498
return resource_key_values
1443-
self._fetch_visible_devices_env_values()
1499+
self._prepare()
14441500
alignments = self._backend_visible_devices_values_alignment.get(
14451501
backend_visible_devices_env,
14461502
)

gpustack_runtime/deployer/docker.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ def _create_containers(
944944
if c.resources:
945945
r_k_runtime_env = workload.resource_key_runtime_env_mapping or {}
946946
r_k_backend_env = workload.resource_key_backend_env_mapping or {}
947-
vd_env, vd_values = self.visible_devices_env_values()
947+
vd_env, vd_values = self.get_visible_devices_env_values()
948948
for r_k, r_v in c.resources.items():
949949
match r_k:
950950
case "cpu":
@@ -1023,6 +1023,20 @@ def _create_containers(
10231023
)
10241024
)
10251025

1026+
# Configure affinity if applicable.
1027+
if (
1028+
envs.GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY
1029+
or envs.GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY
1030+
):
1031+
cpus, numas = self.get_visible_devices_affinities(
1032+
runtime_env,
1033+
r_v,
1034+
)
1035+
if cpus:
1036+
create_options["cpuset_cpus"] = cpus
1037+
if numas and envs.GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY:
1038+
create_options["cpuset_mems"] = numas
1039+
10261040
# Parameterize mounts.
10271041
self._append_container_mounts(
10281042
create_options,

gpustack_runtime/deployer/kuberentes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ def _create_pod(
985985
resources: dict[str, str] = {}
986986
r_k_runtime_env = workload.resource_key_runtime_env_mapping or {}
987987
r_k_backend_env = workload.resource_key_backend_env_mapping or {}
988-
vd_env, vd_values = self.visible_devices_env_values()
988+
vd_env, vd_values = self.get_visible_devices_env_values()
989989
for r_k, r_v in c.resources.items():
990990
if r_k in ("cpu", "memory"):
991991
resources[r_k] = str(r_v)

gpustack_runtime/detector/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,27 @@ def get_devices_topologies(
139139
fast:
140140
If True, return topologies from the first supported detector.
141141
Otherwise, return topologies from all supported detectors.
142+
Only works when `devices` is None.
142143
143144
Returns:
144145
A list of Topology objects for each manufacturer group.
145146
146147
"""
147-
if devices is None:
148+
group = False
149+
if not devices:
148150
devices = detect_devices(fast=fast)
149-
150-
topologies: list[Topology] = []
151+
if not devices:
152+
return []
153+
group = True and not fast
151154

152155
# Group devices by manufacturer.
153-
group_devices = group_devices_by_manufacturer(devices)
154-
if not group_devices:
155-
return topologies
156+
if group:
157+
group_devices = group_devices_by_manufacturer(devices)
158+
else:
159+
group_devices = {devices[0].manufacturer: devices}
156160

157161
# Get topology for each group.
162+
topologies: list[Topology] = []
158163
for manu, devs in group_devices.items():
159164
det = _DETECTORS_MAP.get(manu)
160165
if det is not None:
@@ -163,7 +168,6 @@ def get_devices_topologies(
163168
topologies.append(topo)
164169
if fast and topologies:
165170
return topologies
166-
167171
return topologies
168172

169173

gpustack_runtime/detector/__types__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,46 @@ def stringify(self) -> list[list[str]]:
297297
]
298298
return devices_info
299299

300+
def get_affinities(
301+
self,
302+
device_indexes: list[int] | int,
303+
deduplicate: bool = True,
304+
) -> tuple[list[str], list[str]]:
305+
"""
306+
Get the CPU and NUMA affinities for the given device indexes.
307+
308+
Args:
309+
device_indexes:
310+
A list of device indexes or a single device index.
311+
If an empty list is provided, return all affinities.
312+
deduplicate:
313+
Whether to deduplicate the affinities.
314+
If True, the returned lists will contain unique affinities only.
315+
316+
Returns:
317+
A tuple containing:
318+
- A list contains the CPU affinities for the given device indexes.
319+
- A list contains the NUMA affinities for the given device indexes.
320+
321+
"""
322+
if isinstance(device_indexes, int):
323+
device_indexes = [device_indexes]
324+
325+
cpu_affinities: list[str] = []
326+
numa_affinities: list[str] = []
327+
if not device_indexes:
328+
cpu_affinities.extend(self.devices_cpu_affinities)
329+
numa_affinities.extend(self.devices_numa_affinities)
330+
else:
331+
for index in sorted(set(device_indexes)):
332+
cpu_affinities.append(self.devices_cpu_affinities[index])
333+
numa_affinities.append(self.devices_numa_affinities[index])
334+
335+
if deduplicate:
336+
cpu_affinities = list(set(cpu_affinities))
337+
numa_affinities = list(set(numa_affinities))
338+
return cpu_affinities, numa_affinities
339+
300340

301341
class TopologyDistanceEnum(int, Enum):
302342
"""

gpustack_runtime/detector/amd.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@ def get_topology(self, devices: Devices | None = None) -> Topology | None:
278278

279279
def get_device_handle(dev: Device):
280280
if bdf := dev.appendix.get("bdf", None):
281-
return pyamdsmi.amdsmi_get_processor_handle_from_bdf(bdf)
281+
with contextlib.suppress(pyamdsmi.AmdSmiException):
282+
return pyamdsmi.amdsmi_get_processor_handle_from_bdf(bdf)
282283
nonlocal devs_mapping
283284
if devs_mapping is None:
284285
devs = pyamdsmi.amdsmi_get_processor_handles()

gpustack_runtime/envs.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@
182182
When detected devices are considered to be partially mapped (starting from a non-zero value or not contiguous),
183183
alignment is performed to ensure they are correctly identified.
184184
"""
185+
GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY: bool = False
186+
"""
187+
Enable CPU affinity for deployed workloads.
188+
"""
189+
GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY: bool = False
190+
"""
191+
Enable NUMA affinity for deployed workloads.
192+
When enabled, `GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY` is also implied.
193+
"""
185194

186195
# Deployer
187196

@@ -392,6 +401,12 @@
392401
),
393402
sep=",",
394403
),
404+
"GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY": lambda: to_bool(
405+
getenv("GPUSTACK_RUNTIME_DEPLOY_CPU_AFFINITY", "0"),
406+
),
407+
"GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY": lambda: to_bool(
408+
getenv("GPUSTACK_RUNTIME_DEPLOY_NUMA_AFFINITY", "0"),
409+
),
395410
# Deployer
396411
## Docker
397412
"GPUSTACK_RUNTIME_DOCKER_MIRRORED_NAME_FILTER_LABELS": lambda: to_dict(

tests/gpustack_runtime/detector/samples/detect_output_amd_rx7800xt.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@
99
"runtime_version_original": "7.1.1",
1010
"compute_capability": "gfx1101",
1111
"cores": 60,
12-
"cores_utilization": 0,
12+
"cores_utilization": 19,
1313
"memory": 16368,
14-
"memory_used": 5713,
15-
"memory_utilization": 34.9,
16-
"temperature": 37,
14+
"memory_used": 206,
15+
"memory_utilization": 1.26,
16+
"temperature": 34,
1717
"power": 236,
18-
"power_used": 7,
18+
"power_used": 17,
1919
"appendix": {
2020
"arch_family": "GC 11.0.0",
2121
"vgpu": false,
22+
"bdf": "0000:03:00.0",
2223
"card_id": 1,
23-
"bdf": "0000:03:00.0"
24+
"renderd_id": 128
2425
}
2526
}
2627
]

0 commit comments

Comments
 (0)