alpaka
Abstraction Library for Parallel Kernel Acceleration
Loading...
Searching...
No Matches
utility.hpp
Go to the documentation of this file.
1/* Copyright 2026 René Widera
2 * SPDX-License-Identifier: MPL-2.0
3 */
4
5#pragma once
6
10#include "alpaka/unused.hpp"
11
12#include <cerrno>
13#include <cstring>
14#include <fstream>
15#include <limits>
16#include <optional>
17#include <sstream>
18#include <stdexcept>
19#include <string>
20#include <thread>
21#include <vector>
22
23/** Implement functions required to set thread affinity and pin memory.
24 *
25 * There is always a fallback implementation to be able to run without hwloc.
26 * In this case domain selection is not possible and all cores will be taken into account.
27 *
28 * Domain model:
29 * - A CPU domain is the object used as an alpaka host device.
30 * - If hwloc Group objects with CPU sets are available, CPU domains are Groups.
31 * - Otherwise, CPU domains fall back to NUMA nodes with CPU sets.
32 * - NUMA nodes are memory targets attached to a CPU domain, not necessarily CPU devices themselves.
33 *
34 * This avoids duplicating the same CPU cores as multiple alpaka devices on systems where DDR and HBM are modeled as
35 * separate NUMA nodes below the same Group.
36 */
37namespace alpaka::onHost::internal::hwloc
38{
39 /** Constant to select all CPU domains. */
40 constexpr uint32_t allDomains = std::numeric_limits<uint32_t>::max();
41
42#if ALPAKA_HAS_HWLOC
43 /** Helper singleton to cache the hwloc topology.
44 *
45 * Caching is required to reduce the overhead for repeating operations.
46 * Building the topology can be expensive.
47 */
48 class TopologyCache
49 {
50 public:
51 static TopologyCache& instance()
52 {
53 static TopologyCache topology;
54 return topology;
55 }
56
57 hwloc_topology_t get() const noexcept
58 {
59 return m_topology;
60 }
61
62 private:
63 TopologyCache()
64 {
65 if(hwloc_topology_init(&m_topology) != 0)
66 {
67 throw std::runtime_error("hwloc_topology_init failed");
68 }
69 if(hwloc_topology_load(m_topology) != 0)
70 {
71 hwloc_topology_destroy(m_topology);
72 throw std::runtime_error("hwloc_topology_load failed");
73 }
74 }
75
76 ~TopologyCache()
77 {
78 if(m_topology != nullptr)
79 {
80 hwloc_topology_destroy(m_topology);
81 }
82 }
83
84 TopologyCache(TopologyCache const&) = delete;
85 TopologyCache& operator=(TopologyCache const&) = delete;
86 TopologyCache(TopologyCache&&) = delete;
87 TopologyCache& operator=(TopologyCache&&) = delete;
88
89 private:
90 hwloc_topology_t m_topology{};
91 };
92
93 [[noreturn]] inline void throwErrno(char const* what)
94 {
95 throw std::runtime_error(std::string(what) + ": " + std::strerror(errno));
96 }
97
98 /** Shorthand to get the cached hwloc topology cache */
99 inline hwloc_topology_t getTopology()
100 {
101 return TopologyCache::instance().get();
102 }
103
104 /** Check if there are CPUs under the object
105 *
106 * It is possible to create numa domains which does not contain CPUs, those we do not want to use.
107 *
108 * @return true if the object has CPUs, else false.
109 */
110 inline bool hasNonEmptyCpuSet(hwloc_obj_t obj)
111 {
112 return obj != nullptr && obj->cpuset != nullptr && !hwloc_bitmap_iszero(obj->cpuset);
113 }
114
115 /** Check if a hwloc group exists
116 *
117 * With a hwloc group it is possible to combine more than one numa domain together with cores.
118 *
119 * @return true if 'subtreeRoot' contains a group, else false
120 */
121 inline bool isObjectInSubtree(hwloc_obj_t obj, hwloc_obj_t subtreeRoot)
122 {
123 for(hwloc_obj_t current = obj; current != nullptr; current = current->parent)
124 {
125 if(current == subtreeRoot)
126 return true;
127 }
128 return false;
129 }
130
131 /** Search for all hwloc groups.
132 *
133 * @return A list with hwloc objects where each object is a group. Each group guarantees to contain numa domains
134 * and CPUs.
135 */
136 inline std::vector<hwloc_obj_t> getGroupCpuDomains()
137 {
138 std::vector<hwloc_obj_t> domains;
139 hwloc_topology_t const topology = getTopology();
140
141 hwloc_obj_t group = nullptr;
142 while((group = hwloc_get_next_obj_by_type(topology, HWLOC_OBJ_GROUP, group)) != nullptr)
143 {
144 // skip if no CPUs are present in the group
145 if(!hasNonEmptyCpuSet(group))
146 continue;
147
148 // Use only groups that actually contain a NUMA node. This avoids selecting artificial grouping levels that
149 // do not describe a CPU/memory locality domain.
150 bool containsNumaNode = false;
151 hwloc_obj_t node = nullptr;
152 while((node = hwloc_get_next_obj_by_type(topology, HWLOC_OBJ_NUMANODE, node)) != nullptr)
153 {
154 if(isObjectInSubtree(node, group))
155 {
156 containsNumaNode = true;
157 break;
158 }
159 }
160
161 if(containsNumaNode)
162 domains.push_back(group);
163 }
164
165 return domains;
166 }
167
168 /** Search for all hwloc numa domains.
169 *
170 * @return A list with hwloc objects where each object is a numa domain. Each domain guarantees to contain CPUs.
171 */
172 inline std::vector<hwloc_obj_t> getNumaCpuDomains()
173 {
174 std::vector<hwloc_obj_t> domains;
175 hwloc_topology_t const topology = getTopology();
176
177 hwloc_obj_t node = nullptr;
178 while((node = hwloc_get_next_obj_by_type(topology, HWLOC_OBJ_NUMANODE, node)) != nullptr)
179 {
180 if(hasNonEmptyCpuSet(node))
181 domains.push_back(node);
182 }
183
184 return domains;
185 }
186
187 /** Return the host CPU domains
188 *
189 @return List with hwloc Groups with CPU sets and NUMA children, otherwise fall back to NUMA nodes with CPU sets.
190 */
191 inline std::vector<hwloc_obj_t> getCpuDomains()
192 {
193 std::vector<hwloc_obj_t> domains = getGroupCpuDomains();
194 if(!domains.empty())
195 return domains;
196
197 return getNumaCpuDomains();
198 }
199
200 /** Return the hwloc object for the domain index. */
201 inline hwloc_obj_t getCpuDomainObj(uint32_t domainIdx)
202 {
203 std::vector<hwloc_obj_t> const domains = getCpuDomains();
204 if(domainIdx >= domains.size())
205 {
206 throw std::out_of_range("CPU domain index out of range: " + std::to_string(domainIdx));
207 }
208 return domains[domainIdx];
209 }
210
211 /** Return all hwloc numa domains of an object.
212 *
213 * @return List of all numa domains under an object. It is guaranteed that each object has at least one numa
214 * domain.
215 */
216 inline std::vector<hwloc_obj_t> getMemoryNodes(hwloc_obj_t domainObj)
217 {
218 std::vector<hwloc_obj_t> nodes;
219 hwloc_topology_t const topology = getTopology();
220
221 if(domainObj == nullptr)
222 return nodes;
223
224 if(domainObj->type == HWLOC_OBJ_NUMANODE)
225 {
226 nodes.push_back(domainObj);
227 return nodes;
228 }
229
230 hwloc_obj_t node = nullptr;
231 while((node = hwloc_get_next_obj_by_type(topology, HWLOC_OBJ_NUMANODE, node)) != nullptr)
232 {
233 if(isObjectInSubtree(node, domainObj))
234 nodes.push_back(node);
235 }
236
237 return nodes;
238 }
239
240#endif
241
242 /** Get the number of alpaka host CPU domains. */
243 inline uint32_t getNumCpuDomains()
244 {
245#if ALPAKA_HAS_HWLOC
246 std::vector<hwloc_obj_t> const domains = getCpuDomains();
247 return static_cast<uint32_t>(domains.size());
248#else
249 return 1u;
250#endif
251 }
252
253 /** Parse the OS NUMA information.
254 *
255 * hwloc is not providing the available free memory in a NUMA domain.
256 * Therefore we fall back to check the NUMA node information in the OS directly.
257 *
258 * @param osNodeIndex The index of the NUMA node in the OS.
259 * @param key The key value you want to read out e.g. 'MemFree:' or 'HugePages_Total:'.
260 */
261 inline std::optional<size_t> parseNodeMemInfoValueBytes(unsigned osNodeIndex, std::string_view key)
262 {
263 std::ifstream in("/sys/devices/system/node/node" + std::to_string(osNodeIndex) + "/meminfo");
264 if(!in)
265 {
266 return std::nullopt;
267 }
268
269 std::string line;
270 while(std::getline(in, line))
271 {
272 if(line.find(std::string(key)) == std::string::npos)
273 {
274 continue;
275 }
276
277 // Example line:
278 // Node 0 MemFree: 123456 kB
279 std::istringstream iss(line);
280 std::string nodeWord;
281 unsigned nodeNumber = 0;
282 std::string field;
283 size_t valueKB = 0;
284 std::string unit;
285 if(iss >> nodeWord >> nodeNumber >> field >> valueKB >> unit)
286 {
287 if(field == key && unit == "kB")
288 {
289 return valueKB * 1024ULL;
290 }
291 }
292 }
293
294 return std::nullopt;
295 }
296
297 /** Set the affinity of the current thread to all cores of a CPU domain.
298 *
299 * @param cpuDomainIdx Legacy name: CPU-domain index starting with zero, or 'allDomains' to use all cores.
300 */
301 inline void setThreadAffinity(uint32_t cpuDomainIdx)
302 {
303#if ALPAKA_HAS_HWLOC
304 hwloc_cpuset_t cpuset = nullptr;
305
306 if(cpuDomainIdx == allDomains)
307 {
308 hwloc_const_cpuset_t const fullSet = hwloc_topology_get_complete_cpuset(getTopology());
309 if(fullSet == nullptr)
310 {
311 throw std::runtime_error("Topology has no complete cpuset");
312 }
313
314 cpuset = hwloc_bitmap_dup(fullSet);
315 }
316 else
317 {
318 hwloc_obj_t const domain = getCpuDomainObj(cpuDomainIdx);
319 if(domain->cpuset == nullptr || hwloc_bitmap_iszero(domain->cpuset))
320 {
321 throw std::runtime_error("CPU domain has no cpuset");
322 }
323
324 cpuset = hwloc_bitmap_dup(domain->cpuset);
325 }
326
327 if(cpuset == nullptr)
328 {
329 throw std::bad_alloc();
330 }
331
332 int const rc = hwloc_set_cpubind(getTopology(), cpuset, HWLOC_CPUBIND_THREAD | HWLOC_CPUBIND_STRICT);
333
334 hwloc_bitmap_free(cpuset);
335
336 if(rc != 0)
337 {
338 throwErrno("hwloc_set_cpubind failed");
339 }
340#else
341 alpaka::unused(cpuDomainIdx);
342 return;
343#endif
344 }
345
346#if ALPAKA_HAS_HWLOC
347 /** Set the NUMA memory target for the memory range described by ptr and bytes. */
348 template<typename T>
349 inline void pinPointerToNumaNode(T* const ptr, size_t bytes, hwloc_obj_t const node)
350 {
351 if(ptr == nullptr || bytes == 0u)
352 return;
353
354 if(node == nullptr)
355 throw std::runtime_error("NUMA node is null");
356
357 if(node->type != HWLOC_OBJ_NUMANODE)
358 throw std::runtime_error("Memory binding target is not a NUMA node");
359
360 if(node->nodeset == nullptr)
361 {
362 throw std::runtime_error("NUMA node has no nodeset");
363 }
364
365 hwloc_nodeset_t nodeset = hwloc_bitmap_dup(node->nodeset);
366 if(nodeset == nullptr)
367 {
368 throw std::bad_alloc();
369 }
370
371 int const rc = hwloc_set_area_membind(
372 getTopology(),
374 bytes,
375 nodeset,
376 HWLOC_MEMBIND_BIND,
377 HWLOC_MEMBIND_BYNODESET | HWLOC_MEMBIND_STRICT);
378
379 hwloc_bitmap_free(nodeset);
380
381 if(rc != 0)
382 {
383# ifdef ALPAKA_HOST_MEM_PINNING_CAN_FAIL
384 // Missing privileges, e.g. within a container.
385 bool const operationNotSupported = errno == EPERM;
386 // Unsupported platform.
387 bool const functionNotImplemented = errno == ENOSYS;
388 // NUMA node is not allowed by cpuset/cgroup.
389 bool const operationNotAllowed = errno == EXDEV;
390 if(operationNotSupported || functionNotImplemented || operationNotAllowed)
391 {
392 return;
393 }
394# endif
395 throwErrno("hwloc_set_area_membind failed");
396 }
397 }
398#endif
399
400 /** Set the default NUMA memory node for a CPU-domain memory range.
401 *
402 * @attention This method should be called before the memory is touched, else it may have no effect.
403 * If a cpuDomainIdx contains more than one numa domain, the first numa domain will be used.
404 *
405 * @param ptr pointer address to pin, nullptr is valid input.
406 * @param bytes the number of bytes to pin starting from the ptr address.
407 * @param cpuDomainIdx Index of the cpu group.
408 */
409 template<typename T>
410 inline void pinPointer(T* const ptr, size_t bytes, uint32_t cpuDomainIdx)
411 {
412#if ALPAKA_HAS_HWLOC
413 if(cpuDomainIdx == allDomains)
414 return;
415
416 if(ptr == nullptr || bytes == 0u)
417 return;
418
419 std::vector<hwloc_obj_t> const nodes = getMemoryNodes(getCpuDomainObj(cpuDomainIdx));
420 if(nodes.empty())
421 {
422 throw std::runtime_error("CPU domain has no associated NUMA memory node");
423 }
424
425 /** take the first numa domain
426 *
427 * @todo: this should be slectable during onHost::alloc()
428 */
429 pinPointerToNumaNode(ptr, bytes, nodes.front());
430#else
431 alpaka::unused(ptr, bytes, cpuDomainIdx);
432 return;
433#endif
434 }
435
436 /** Return the number of cores in a CPU domain.
437 *
438 * Here "cores" means logical CPUs / processing units, so SMT siblings are counted too.
439 *
440 * @param cpuDomainIdx Index of the cpu group.
441 */
442 inline uint32_t getNumCores(uint32_t cpuDomainIdx)
443 {
444#if ALPAKA_HAS_HWLOC
445 if(cpuDomainIdx == allDomains)
446 return std::thread::hardware_concurrency();
447
448 hwloc_obj_t const domain = getCpuDomainObj(cpuDomainIdx);
449 if(domain->cpuset == nullptr || hwloc_bitmap_iszero(domain->cpuset))
450 {
451 throw std::runtime_error("CPU domain has no cpuset");
452 }
453
454 int const numPUs = hwloc_bitmap_weight(domain->cpuset);
455 if(numPUs < 0)
456 {
457 throw std::runtime_error("hwloc_bitmap_weight failed");
458 }
459
460 return static_cast<uint32_t>(numPUs);
461#else
462 alpaka::unused(cpuDomainIdx);
463 return std::thread::hardware_concurrency();
464#endif
465 }
466
467 /** Return the memory capacity of a CPU domain.
468 *
469 * If the CPU domain has multiple attached NUMA memory nodes, e.g. DDR and HBM, the returned value is the sum of
470 * all attached memory nodes.
471 *
472 * @param cpuDomainIdx Index of the cpu group.
473 */
474 inline size_t getMemCapacityBytes(uint32_t cpuDomainIdx)
475 {
476#if ALPAKA_HAS_HWLOC
477 if(cpuDomainIdx == allDomains)
479
480 size_t bytes = 0u;
481 for(hwloc_obj_t const node : getMemoryNodes(getCpuDomainObj(cpuDomainIdx)))
482 {
483 if(node->attr == nullptr)
484 {
485 throw std::runtime_error("NUMA node has no attributes");
486 }
487 bytes += static_cast<size_t>(node->attr->numanode.local_memory);
488 }
489
490 return bytes;
491#else
492 alpaka::unused(cpuDomainIdx);
494#endif
495 }
496
497 /** Return the number of free bytes in a CPU domain.
498 *
499 * Linux-only implementation via /sys/devices/system/node/nodeX/meminfo. If the CPU domain has multiple attached
500 * NUMA memory nodes, e.g. DDR and HBM, the returned value is the sum of all attached memory nodes.
501 *
502 * @param cpuDomainIdx Index of the cpu group.
503 */
504 inline size_t getFreeGlobalMemBytes(uint32_t cpuDomainIdx)
505 {
506#if ALPAKA_HAS_HWLOC
507 if(cpuDomainIdx == allDomains)
509
510 size_t bytes = 0u;
511 for(hwloc_obj_t const node : getMemoryNodes(getCpuDomainObj(cpuDomainIdx)))
512 {
513 auto const freeBytes = parseNodeMemInfoValueBytes(node->os_index, "MemFree:");
514 if(!freeBytes.has_value())
515 {
516 throw std::runtime_error(
517 "Could not read per-node MemFree from /sys/devices/system/node/node"
518 + std::to_string(node->os_index) + "/meminfo");
519 }
520 bytes += *freeBytes;
521 }
522
523 return bytes;
524#else
525 alpaka::unused(cpuDomainIdx);
527#endif
528 }
529} // namespace alpaka::onHost::internal::hwloc
auto getFreeGlobalMemBytes() -> std::size_t
Definition sysInfo.hpp:210
auto getGlobalMemCapacityBytes() -> std::size_t
Definition sysInfo.hpp:147
auto * toVoidPtr(T inPtr)
Cast a pointer that may or may not point to volatile memory to a (void*) or (void const*).
Definition util.hpp:34
constexpr decltype(auto) get(concepts::SpecializationOf< Dict > auto &t) noexcept
Definition Dict.hpp:156