Skip to main content

dgen_data/
numa.rs

1// src/numa.rs
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! NUMA topology detection and CPU pinning
6//!
7//! Uses hwlocality for cross-platform NUMA topology detection
8
9use anyhow::Result;
10use hwlocality::{object::types::ObjectType, Topology};
11use std::collections::HashSet;
12
13/// NUMA node information
14#[derive(Debug, Clone)]
15pub struct NumaNode {
16    /// Node ID
17    pub node_id: usize,
18    /// CPU IDs in this NUMA node
19    pub cpus: Vec<usize>,
20    /// Memory in GB local to this node
21    pub memory_gb: f64,
22}
23
24/// System NUMA topology
25#[derive(Debug, Clone)]
26pub struct NumaTopology {
27    /// Number of NUMA nodes
28    pub num_nodes: usize,
29    /// Total physical cores
30    pub physical_cores: usize,
31    /// Total logical CPUs
32    pub logical_cpus: usize,
33    /// Per-NUMA node details
34    pub nodes: Vec<NumaNode>,
35    /// Is this a UMA system (single NUMA node)
36    pub is_uma: bool,
37}
38
39impl NumaTopology {
40    /// Detect NUMA topology from system using hwlocality
41    pub fn detect() -> Result<Self> {
42        tracing::debug!("Detecting NUMA topology via hwlocality...");
43
44        let topology = Topology::new()?;
45
46        // Get all NUMA nodes
47        let numa_nodes: Vec<_> = topology.objects_with_type(ObjectType::NUMANode).collect();
48
49        let num_nodes = numa_nodes.len().max(1); // At least 1 node
50        let is_uma = num_nodes == 1;
51
52        tracing::info!("Detected {} NUMA node(s)", num_nodes);
53
54        // Build node details
55        let nodes: Vec<NumaNode> = if numa_nodes.is_empty() {
56            // No NUMA nodes detected - treat as single UMA node
57            vec![NumaNode {
58                node_id: 0,
59                cpus: (0..num_cpus::get()).collect(),
60                memory_gb: 0.0,
61            }]
62        } else {
63            numa_nodes
64                .iter()
65                .filter_map(|node| {
66                    let node_id = node.os_index()?;
67
68                    // Get CPUs in this NUMA node's cpuset
69                    let cpuset = node.cpuset()?;
70                    let cpus: Vec<usize> = (0..topology.objects_with_type(ObjectType::PU).count())
71                        .filter(|&cpu_id| cpuset.is_set(cpu_id))
72                        .collect();
73
74                    Some(NumaNode {
75                        node_id,
76                        cpus,
77                        memory_gb: 0.0, // hwlocality can provide this if needed
78                    })
79                })
80                .collect()
81        };
82
83        let physical_cores = num_cpus::get_physical();
84        let logical_cpus = num_cpus::get();
85
86        Ok(Self {
87            num_nodes,
88            physical_cores,
89            logical_cpus,
90            nodes,
91            is_uma,
92        })
93    }
94
95    /// Check if NUMA-aware optimizations should be enabled
96    pub fn should_enable_numa_pinning(&self) -> bool {
97        self.num_nodes > 1
98    }
99
100    /// Get deployment type description
101    pub fn deployment_type(&self) -> &str {
102        if self.is_uma {
103            "UMA (single NUMA node - cloud VM or workstation)"
104        } else {
105            "NUMA (multi-socket system or large cloud VM)"
106        }
107    }
108
109    /// Get CPUs for a specific NUMA node
110    pub fn cpus_for_node(&self, node_id: usize) -> Option<&[usize]> {
111        self.nodes
112            .iter()
113            .find(|n| n.node_id == node_id)
114            .map(|n| n.cpus.as_slice())
115    }
116}
117
118/// Detect number of NUMA nodes
119///
120/// Cloud VMs typically present as single NUMA node.
121/// Bare metal multi-socket shows 2+ nodes.
122#[allow(dead_code)] // May be used in future for additional validation
123fn detect_numa_nodes() -> Result<usize> {
124    tracing::trace!("detect_numa_nodes called");
125
126    #[cfg(target_os = "linux")]
127    {
128        // Method 1: Check /sys/devices/system/node/
129        let node_path = std::path::Path::new("/sys/devices/system/node");
130        if node_path.exists() {
131            let mut numa_nodes = Vec::new();
132
133            for entry in std::fs::read_dir(node_path)? {
134                let entry = entry?;
135                let name = entry.file_name();
136                let name_str = name.to_string_lossy();
137
138                if name_str.starts_with("node") && name_str[4..].chars().all(|c| c.is_ascii_digit())
139                {
140                    if let Ok(node_id) = name_str[4..].parse::<usize>() {
141                        numa_nodes.push(node_id);
142                    }
143                }
144            }
145
146            if !numa_nodes.is_empty() {
147                return Ok(numa_nodes.len());
148            }
149        }
150
151        // Method 2: Check /proc/cpuinfo for physical id
152        if let Ok(cpuinfo) = std::fs::read_to_string("/proc/cpuinfo") {
153            let mut physical_ids = HashSet::new();
154
155            for line in cpuinfo.lines() {
156                if line.starts_with("physical id") {
157                    if let Some(id_str) = line.split(':').nth(1) {
158                        if let Ok(id) = id_str.trim().parse::<usize>() {
159                            physical_ids.insert(id);
160                        }
161                    }
162                }
163            }
164
165            if !physical_ids.is_empty() {
166                return Ok(physical_ids.len());
167            }
168        }
169    }
170
171    // Fallback: Assume UMA
172    tracing::debug!("Could not detect NUMA topology, assuming UMA");
173    Ok(1)
174}
175
176/// Detect detailed NUMA topology using /sys interface
177#[allow(dead_code)] // May be used in future for detailed topology analysis
178fn detect_numa_topology_details() -> Result<Vec<NumaNode>> {
179    #[cfg(target_os = "linux")]
180    {
181        let node_path = std::path::Path::new("/sys/devices/system/node");
182        if !node_path.exists() {
183            anyhow::bail!("NUMA topology not available");
184        }
185
186        let mut numa_nodes = Vec::new();
187
188        for entry in std::fs::read_dir(node_path)? {
189            let entry = entry?;
190            let name = entry.file_name();
191            let name_str = name.to_string_lossy();
192
193            if name_str.starts_with("node") && name_str[4..].chars().all(|c| c.is_ascii_digit()) {
194                if let Ok(node_id) = name_str[4..].parse::<usize>() {
195                    let node_dir = entry.path();
196
197                    // Read CPUs from cpulist
198                    let mut cpus = Vec::new();
199                    let cpulist_path = node_dir.join("cpulist");
200                    if let Ok(cpulist) = std::fs::read_to_string(&cpulist_path) {
201                        for range in cpulist.trim().split(',') {
202                            if range.contains('-') {
203                                let parts: Vec<&str> = range.split('-').collect();
204                                if parts.len() == 2 {
205                                    if let (Ok(start), Ok(end)) =
206                                        (parts[0].parse::<usize>(), parts[1].parse::<usize>())
207                                    {
208                                        for cpu in start..=end {
209                                            cpus.push(cpu);
210                                        }
211                                    }
212                                }
213                            } else if let Ok(cpu) = range.parse::<usize>() {
214                                cpus.push(cpu);
215                            }
216                        }
217                    }
218
219                    // Read memory from meminfo
220                    let mut memory_gb = 0.0;
221                    let meminfo_path = node_dir.join("meminfo");
222                    if let Ok(meminfo) = std::fs::read_to_string(&meminfo_path) {
223                        for line in meminfo.lines() {
224                            if line.contains("MemTotal:") {
225                                let parts: Vec<&str> = line.split_whitespace().collect();
226                                if parts.len() >= 4 {
227                                    if let Ok(kb) = parts[3].parse::<f64>() {
228                                        memory_gb = kb / 1024.0 / 1024.0;
229                                    }
230                                }
231                                break;
232                            }
233                        }
234                    }
235
236                    numa_nodes.push(NumaNode {
237                        node_id,
238                        cpus,
239                        memory_gb,
240                    });
241                }
242            }
243        }
244
245        if numa_nodes.is_empty() {
246            anyhow::bail!("No NUMA nodes detected");
247        }
248
249        numa_nodes.sort_by_key(|n| n.node_id);
250        Ok(numa_nodes)
251    }
252
253    #[cfg(not(target_os = "linux"))]
254    {
255        anyhow::bail!("NUMA detection only supported on Linux");
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn init_tracing() {
264        use tracing_subscriber::{fmt, EnvFilter};
265        let _ = fmt()
266            .with_env_filter(EnvFilter::from_default_env())
267            .try_init();
268    }
269
270    #[test]
271    fn test_detect_topology() {
272        init_tracing();
273        if let Ok(topology) = NumaTopology::detect() {
274            println!("NUMA topology: {:?}", topology);
275            assert!(topology.num_nodes >= 1);
276            assert!(topology.physical_cores >= 1);
277            assert!(topology.logical_cpus >= topology.physical_cores);
278        }
279    }
280}