Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 45 additions & 19 deletions docs/topology-and-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,35 @@ if (discovery.discover()) {
topo.hostname; // System hostname
topo.num_gpus; // Total GPU count
topo.num_numa_nodes; // Total NUMA node count
topo.numa_nodes; // Per-NUMA-node capacity and memory kind
topo.gpus; // Per-GPU topology info
topo.network_devices; // NICs with NUMA affinity
topo.storage_devices; // NVMe/SATA drives with NUMA affinity

// Lookups return nullopt for an unknown node rather than a silent 0.
topo.get_numa_memory_capacity(0); // std::optional<std::size_t>
topo.get_numa_free_memory(0); // std::optional<std::size_t>
topo.get_total_numa_memory_capacity(); // host memory only
topo.find_numa_node(0); // numa_topology_info const*
}
```

**Per-NUMA-node information** (`numa_topology_info`), read from
`/sys/devices/system/node/node<id>/`:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `int` | NUMA node ID |
| `memory_capacity` | `std::size_t` | `MemTotal` in bytes (0 if unknown) |
| `free_memory` | `std::size_t` | `MemFree` in bytes (0 if unknown) |
| `has_cpus` | `bool` | Whether `cpulist` names any CPU |
| `is_device_memory` | `bool` | Node has memory but no CPUs -- device memory, not host memory |

A node with memory and no CPUs is not host memory. DGX Station and Grace-Hopper expose GPU
HBM as its own CPU-less node, and CXL memory expanders do the same. Such nodes are excluded
from `get_total_numa_memory_capacity()`, and `set_usage_limit_ratio_per_numa_region()`
throws rather than sizing a host space from device memory.

**Per-GPU information** (`gpu_topology_info`):

| Field | Type | Description |
Expand Down Expand Up @@ -151,8 +174,8 @@ configurator
.set_number_of_gpus(2)
.set_gpu_usage_limit(4ULL << 30) // 4 GB per GPU
.set_reservation_fraction_per_gpu(0.85) // 85% reservable
.set_per_host_capacity(16ULL << 30) // 16 GB per host space
.use_host_per_numa(); // One host space per NUMA node
.set_per_numa_region_capacity(16ULL << 30) // 16 GB per host space
.use_numa_id_as_host_id(); // One host space per NUMA node

// With topology
topology_discovery discovery;
Expand Down Expand Up @@ -186,19 +209,22 @@ Note: `set_gpu_usage_limit()` and `set_usage_limit_ratio_per_gpu()` are mutually

| Method | Description | Default |
|--------|-------------|---------|
| `use_host_per_gpu()` | One host space per GPU (testing) | N/A |
| `use_host_per_numa()` | One host space per NUMA node (production) | `use_host_per_numa()` |
| `use_gpu_id_as_host_id()` | One host space per GPU (testing) | N/A |
| `use_numa_id_as_host_id()` | One host space per NUMA node (production) | `use_numa_id_as_host_id()` |
| `set_total_host_capacity(bytes)` | Total host memory across all spaces | 4 GB |
| `set_per_host_capacity(bytes)` | Memory per host space | 4 GB |
| `set_reservation_fraction_per_host(0.85)` | Fraction reservable | 0.85 |
| `set_reservation_limit_per_host(bytes)` | Absolute reservation limit | N/A |
| `set_downgrade_fractions_per_host(0.85, 0.65)` | (trigger, stop) fractions | (0.85, 0.65) |
| `set_per_numa_region_capacity(bytes)` | Memory per host space | 4 GB |
| `set_usage_limit_ratio_per_numa_region(0.25)` | Memory per host space as a fraction of its NUMA region's capacity | N/A |
| `set_reservation_fraction_per_numa_region(0.85)` | Fraction reservable | 0.85 |
| `set_reservation_limit_per_numa_region(bytes)` | Absolute reservation limit | N/A |
| `set_downgrade_fractions_per_numa_region(0.85, 0.65)` | (trigger, stop) fractions | (0.85, 0.65) |
| `set_host_pool_features(chunk, block, count)` | Block allocator settings | (1MB, 128, 4) |
| `set_host_memory_resource_factory(fn)` | Custom host allocator factory | NUMA-pinned |

Host creation policies:
- **`use_host_per_gpu()`** -- creates one host space per GPU, regardless of NUMA topology. Useful for testing.
- **`use_host_per_numa()`** -- creates one host space per NUMA node, shared by GPUs on that node. Optimal for production.
- **`use_gpu_id_as_host_id()`** -- creates one host space per GPU and identifies it by the GPU id. The space is still bound to that GPU's NUMA node for allocation; only the space id differs. Useful for testing.
- **`use_numa_id_as_host_id()`** -- creates one host space per NUMA node, identified by the NUMA node id and shared by GPUs on that node. Default, and optimal for production.

`set_total_host_capacity()` is the only host-wide setting: it is divided evenly across the host spaces. Every other capacity setting applies per space, i.e. per NUMA region.

### Disk Configuration

Expand Down Expand Up @@ -233,8 +259,8 @@ reservation_manager_configurator configurator;
configurator
.set_gpu_usage_limit(4ULL << 30) // 4 GB GPU
.set_reservation_fraction_per_gpu(0.8) // 80% reservable
.set_per_host_capacity(8ULL << 30) // 8 GB host
.use_host_per_gpu();
.set_per_numa_region_capacity(8ULL << 30) // 8 GB host
.use_gpu_id_as_host_id();

auto configs = configurator.build();
memory_reservation_manager manager(std::move(configs));
Expand All @@ -252,8 +278,8 @@ configurator
.set_usage_limit_ratio_per_gpu(0.8) // 80% of each GPU
.set_reservation_fraction_per_gpu(0.85)
.set_downgrade_fractions_per_gpu(0.85, 0.65)
.set_per_host_capacity(32ULL << 30) // 32 GB per NUMA node
.use_host_per_numa()
.set_per_numa_region_capacity(32ULL << 30) // 32 GB per NUMA node
.use_numa_id_as_host_id()
.set_host_pool_features(
2ULL << 20, // 2 MB blocks
64, // 64 blocks per pool
Expand Down Expand Up @@ -302,10 +328,10 @@ configurator

// Host: NUMA-aware, 64 GB per node, 2 MB blocks
configurator
.use_host_per_numa()
.set_per_host_capacity(64ULL << 30)
.set_reservation_fraction_per_host(0.85)
.set_downgrade_fractions_per_host(0.85, 0.65)
.use_numa_id_as_host_id()
.set_per_numa_region_capacity(64ULL << 30)
.set_reservation_fraction_per_numa_region(0.85)
.set_downgrade_fractions_per_numa_region(0.85, 0.65)
.set_host_pool_features(2ULL << 20, 128, 8);

// Disk: 1 TB NVMe
Expand Down Expand Up @@ -334,7 +360,7 @@ memory_reservation_manager manager(std::move(configs));
| Host block size | 1 MB (`1 << 20`) |
| Host pool size | 128 blocks per pool |
| Host initial pools | 4 |
| Host creation policy | `use_host_per_numa()` |
| Host creation policy | `use_numa_id_as_host_id()` |
| GPU allocator | `rmm::cuda_async_memory_resource` |
| Host allocator | `numa_region_pinned_host_memory_resource` |
| Stream pool size | 16 streams |
Expand Down
89 changes: 58 additions & 31 deletions include/cucascade/memory/reservation_manager_configurator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ namespace memory {
* builder.set_number_of_gpus(2)
* .set_gpu_usage_limit(2UL << 30)
* .set_reservation_limit_ratio_per_gpu(0.8)
* .set_numa_ids({0, 1})
* .set_capacity_per_numa_node(8UL << 30)
* .use_numa_id_as_host_id()
* .set_per_numa_region_capacity(8UL << 30)
* .set_gpu_memory_resource_factory(custom_gpu_factory)
* .set_cpu_memory_resource_factory(custom_cpu_factory);
* auto configs = builder.build(system_topology);
Expand Down Expand Up @@ -102,35 +102,50 @@ class reservation_manager_configurator {
/// @param mr_fn Function to create GPU memory resource.
builder_reference& set_gpu_memory_resource_factory(DeviceMemoryResourceFactoryFn mr_fn);

// --- cpu / host settings ---
// --- cpu / numa region settings ---
//
// A host memory space covers a single NUMA region, not the whole host, so the per-space
// settings are named after the region they size. Only `set_total_host_capacity()` is
// host-wide: it is divided across the spaces.

/// @brief set host ids
/// @param host_ids Vector of host ids.
/// @note this is meant to be used for testing purpose only, host ids will be mapped to numa ids
builder_reference& use_host_per_gpu();
/// @brief identify each host space by the id of the GPU it backs
/// @note One host space is created per GPU. The space is still bound to that GPU's NUMA
/// node for allocation; only the space id differs. Meant for testing.
builder_reference& use_gpu_id_as_host_id();

/// @brief automatically bind cpu tiers to gpus based on topology
builder_reference& use_host_per_numa();
/// @brief identify each host space by its NUMA node id (default)
/// @note One host space is created per NUMA node, shared by every GPU on that node.
builder_reference& use_numa_id_as_host_id();

/// set capacity per host tier
/// @param bytes Memory capacity per NUMA node in bytes.
/// @brief set the memory capacity shared by all host spaces
/// @param bytes Total host memory capacity in bytes, split evenly across the host spaces.
builder_reference& set_total_host_capacity(std::size_t bytes);

/// set capacity per host tier
/// @param bytes Memory capacity per NUMA node in bytes.
builder_reference& set_per_host_capacity(std::size_t bytes);
/// @brief set the capacity of each NUMA region space
/// @param bytes Memory capacity per NUMA region in bytes.
builder_reference& set_per_numa_region_capacity(std::size_t bytes);

/// \brief set reservation limit ratio per GPU
/// @param fraction Fraction of GPU memory capacity to reserve.
builder_reference& set_downgrade_fractions_per_host(double start, double end);
/// @brief set the capacity of each NUMA region space as a fraction of that region's capacity
/// @param fraction Fraction of the NUMA region memory capacity to use, in (0.0, 1.0].
/// @note Requires NUMA capacities in the topology passed to `build()`; `build()` throws
/// if the capacity of a NUMA region backing a space is unknown, or if that region is
/// device memory rather than host memory.
/// @note The fraction applies per space. With `use_gpu_id_as_host_id()` and several GPUs
/// on the same NUMA node, each space gets that fraction of the shared region.
builder_reference& set_usage_limit_ratio_per_numa_region(double fraction);

/// \brief set the downgrade trigger and stop fractions per NUMA region
/// @param start Fraction of region capacity at which downgrading starts.
/// @param end Fraction of region capacity at which downgrading stops.
builder_reference& set_downgrade_fractions_per_numa_region(double start, double end);

/// \brief set ratio of space capacity used for reservation in cpus
/// @param fraction Fraction of NUMA node memory capacity to reserve.
builder_reference& set_reservation_limit_per_host(size_t bytes);
/// \brief set absolute reservation limit per NUMA region
/// @param bytes Reservable bytes per NUMA region.
builder_reference& set_reservation_limit_per_numa_region(size_t bytes);

/// \brief set ratio of space capacity used for reservation in cpus
/// @param fraction Fraction of NUMA node memory capacity to reserve.
builder_reference& set_reservation_fraction_per_host(double fraction);
/// \brief set ratio of space capacity used for reservation per NUMA region
/// @param fraction Fraction of NUMA region memory capacity to reserve.
builder_reference& set_reservation_fraction_per_numa_region(double fraction);

/// \brief set the function that takes in the numa node id and create cpu memory resource
/// @param mr_fn Function to create CPU memory resource.
Expand Down Expand Up @@ -168,15 +183,17 @@ class reservation_manager_configurator {
int numa_id{-1};
};

struct host_info {
struct numa_region_info {
int space_id{-1};
int numa_id{-1};
std::optional<std::size_t> numa_capacity{}; ///< Nullopt when the region capacity is unknown.
bool is_device_memory{false}; ///< True when the region is GPU/device memory.
};

std::vector<gpu_info> extract_gpu_ids(const system_topology_info& topology) const;

std::vector<host_info> extract_host_ids(const std::vector<gpu_info>& gpus,
const system_topology_info& topology) const;
std::vector<numa_region_info> extract_numa_region_ids(const std::vector<gpu_info>& gpus,
const system_topology_info& topology) const;

struct fraction_or_size {
fraction_or_size(double fraction) : _fraction_or_size_value(fraction) {}
Expand All @@ -193,6 +210,16 @@ class reservation_manager_configurator {
}
}

[[nodiscard]] bool holds_fraction() const
{
return std::holds_alternative<double>(_fraction_or_size_value);
}

[[nodiscard]] std::size_t get_size() const
{
return std::get<std::size_t>(_fraction_or_size_value);
}

[[nodiscard]] std::size_t get_capacity(std::size_t total_size) const
{
if (std::holds_alternative<double>(_fraction_or_size_value)) {
Expand All @@ -214,15 +241,15 @@ class reservation_manager_configurator {
std::pair<double, double> downgrade_fractions_per_gpu_{0.85, 0.65};
mutable DeviceMemoryResourceFactoryFn _gpu_mr_fn = make_default_gpu_memory_resource;

std::size_t _host_capacity{static_cast<std::size_t>(4UL << 30)}; // 4GB
fraction_or_size _numa_region_capacity{static_cast<std::size_t>(4UL << 30)}; // 4GB
bool _is_capacity_per_space{true};
struct bind_host_to_gpu_id {};
struct bind_cpu_to_gpu_numa {};
std::variant<bind_cpu_to_gpu_numa, bind_host_to_gpu_id> _host_creation_policy{};
struct bind_host_id_to_gpu_id {};
struct bind_host_id_to_numa_id {};
std::variant<bind_host_id_to_numa_id, bind_host_id_to_gpu_id> _host_id_policy{};
std::optional<std::size_t> chunk_size;
std::optional<std::size_t> block_size;
std::optional<std::size_t> initial_block_count;
std::pair<double, double> downgrade_fractions_per_host_{0.85, 0.65};
std::pair<double, double> downgrade_fractions_per_numa_region_{0.85, 0.65};
fraction_or_size _cpu_reservation{0.85}; // 75% limit per NUMA node by default
mutable DeviceMemoryResourceFactoryFn _cpu_mr_fn{};
std::optional<bool> _host_memory_portability;
Expand Down
82 changes: 82 additions & 0 deletions include/cucascade/memory/topology_discovery.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#pragma once

#include <cstddef>
#include <optional>
#include <string>
#include <vector>
Expand Down Expand Up @@ -50,6 +51,26 @@ struct storage_device_info {
std::string pci_bus_id; ///< PCI bus ID.
};

/**
* @brief NUMA node memory information.
*
* Capacities are reported in bytes and are read from
* `/sys/devices/system/node/node<id>/meminfo`. They are 0 when the kernel does not
* expose the corresponding entry.
*
* @note Not every NUMA node backs host memory. Systems such as DGX Station and
* Grace-Hopper expose device memory (GPU HBM) as its own CPU-less NUMA node, and
* CXL memory expanders do the same. Those nodes are flagged with
* `is_device_memory` and must not be used to size a host memory space.
*/
struct numa_topology_info {
int id{-1}; ///< NUMA node ID.
std::size_t memory_capacity{0}; ///< Total memory of the node in bytes (0 if unknown).
std::size_t free_memory{0}; ///< Currently free memory of the node in bytes (0 if unknown).
bool has_cpus{false}; ///< Whether any CPU is assigned to this node.
bool is_device_memory{false}; ///< Whether this node is device memory rather than host memory.
};

/**
* @brief System topology information.
*/
Expand All @@ -61,6 +82,67 @@ struct system_topology_info {
std::vector<gpu_topology_info> gpus; ///< GPU topology information.
std::vector<network_device_info> network_devices; ///< Network device information.
std::vector<storage_device_info> storage_devices; ///< Storage device information.
std::vector<numa_topology_info> numa_nodes; ///< NUMA node information, sorted by id.

/**
* @brief Find a NUMA node by ID.
*
* @param numa_id NUMA node ID to look up.
* @return Pointer to the node, or `nullptr` if no such node was discovered.
*/
[[nodiscard]] numa_topology_info const* find_numa_node(int numa_id) const
{
for (auto const& node : numa_nodes) {
if (node.id == numa_id) { return &node; }
}
return nullptr;
}

/**
* @brief Get the memory capacity of a NUMA node.
*
* @param numa_id NUMA node ID to look up.
* @return Capacity of the node in bytes, or `std::nullopt` if the node was not
* discovered or the kernel did not report its capacity. The two cases are
* distinguishable via `find_numa_node()`.
*/
[[nodiscard]] std::optional<std::size_t> get_numa_memory_capacity(int numa_id) const
{
auto const* node = find_numa_node(numa_id);
if (node == nullptr || node->memory_capacity == 0) { return std::nullopt; }
return node->memory_capacity;
}

/**
* @brief Get the free memory of a NUMA node.
*
* @param numa_id NUMA node ID to look up.
* @return Free memory of the node in bytes, or `std::nullopt` if the node was not
* discovered or the kernel did not report its free memory.
*/
[[nodiscard]] std::optional<std::size_t> get_numa_free_memory(int numa_id) const
{
auto const* node = find_numa_node(numa_id);
if (node == nullptr || node->free_memory == 0) { return std::nullopt; }
return node->free_memory;
}

/**
* @brief Get the summed memory capacity of all host-backing NUMA nodes.
*
* Nodes flagged as device memory are excluded, so the result is usable host memory.
*
* @return Total host memory capacity in bytes.
*/
[[nodiscard]] std::size_t get_total_numa_memory_capacity() const
{
std::size_t total = 0;
for (auto const& node : numa_nodes) {
if (node.is_device_memory) { continue; }
total += node.memory_capacity;
}
return total;
}
};

/**
Expand Down
Loading
Loading