Skip to main content

hermes_simd_core/numa/
locality.rs

1/// Returns the index of the NUMA node the current thread is executing on.
2///
3/// Delegates to the themis topology SSOT. `None` means the platform did not
4/// report a node — never a fabricated node 0.
5pub fn current_numa_node() -> Option<u32> {
6    themis::try_current_numa_node().map(|node| node.get())
7}
8
9/// Refreshes and returns the current NUMA node index for the executing thread.
10pub fn refresh_numa_node() -> Option<u32> {
11    current_numa_node()
12}
13
14#[cfg(feature = "std")]
15#[derive(Copy, Clone)]
16struct CacheEntry {
17    ptr_start: usize,
18    ptr_end: usize,
19    node: u32,
20    generation: u64,
21    local: bool,
22}
23
24#[cfg(feature = "std")]
25struct LocalityCache {
26    entries: [Option<CacheEntry>; 16],
27    next_idx: usize,
28}
29
30#[cfg(feature = "std")]
31impl LocalityCache {
32    const fn new() -> Self {
33        Self {
34            entries: [None; 16],
35            next_idx: 0,
36        }
37    }
38}
39
40#[cfg(feature = "std")]
41thread_local! {
42    // clippy 1.97.0 FP: already const. ATLAS-MNEMOSYNE-CI-1.
43    #[allow(clippy::missing_const_for_thread_local)]
44    static LOCALITY_CACHE: core::cell::RefCell<LocalityCache> = const {
45        core::cell::RefCell::new(LocalityCache::new())
46    };
47}
48
49#[cfg(feature = "std")]
50#[repr(align(64))]
51struct CacheAlignedAtomicU64(core::sync::atomic::AtomicU64);
52
53#[cfg(feature = "std")]
54static ALLOC_GENERATION: CacheAlignedAtomicU64 =
55    CacheAlignedAtomicU64(core::sync::atomic::AtomicU64::new(0));
56
57/// Bump the global allocation generation counter.
58///
59/// This invalidates thread-local locality cache entries, preventing stale cache hits
60/// when virtual memory addresses are deallocated and subsequently reallocated.
61///
62/// `Release` publishes the dealloc that precedes the bump; the paired `Acquire`
63/// load in [`get_alloc_generation`] establishes the happens-before required for a
64/// reader to never trust a cache entry tagged with a superseded generation.
65#[inline]
66pub fn bump_alloc_generation() {
67    #[cfg(feature = "std")]
68    ALLOC_GENERATION
69        .0
70        .fetch_add(1, core::sync::atomic::Ordering::Release);
71}
72
73/// Returns the current global allocation generation counter.
74#[inline]
75pub fn get_alloc_generation() -> u64 {
76    #[cfg(feature = "std")]
77    {
78        ALLOC_GENERATION
79            .0
80            .load(core::sync::atomic::Ordering::Acquire)
81    }
82    #[cfg(not(feature = "std"))]
83    {
84        0
85    }
86}
87
88/// Verify if the physical memory backing a pointer range is resident on a specific node.
89pub fn verify_numa_locality(ptr: *const u8, size: usize, expected_node: u32) -> bool {
90    #[cfg(feature = "std")]
91    {
92        let ptr_val = ptr as usize;
93        let gen = get_alloc_generation();
94
95        let cached = LOCALITY_CACHE.with(|cache| {
96            let cache_ref = cache.borrow();
97            let end_val = ptr_val.saturating_add(size);
98            for entry in cache_ref.entries.iter().flatten() {
99                if entry.node == expected_node
100                    && entry.generation == gen
101                    && ptr_val >= entry.ptr_start
102                    && end_val <= entry.ptr_end
103                {
104                    return Some(entry.local);
105                }
106            }
107            None
108        });
109
110        if let Some(local) = cached {
111            return local;
112        }
113
114        let local = verify_numa_locality_os(ptr, size, expected_node);
115
116        // Tag the entry with the generation captured *before* the OS probe. A
117        // concurrent `bump_alloc_generation` during the probe then leaves this
118        // entry mismatched (its `gen` is already stale), so the next lookup
119        // re-probes instead of trusting probe data gathered under a superseded
120        // generation. Re-reading the counter here would (incorrectly) stamp the
121        // pre-bump data with the post-bump generation.
122        LOCALITY_CACHE.with(|cache| {
123            let mut cache_mut = cache.borrow_mut();
124            let idx = cache_mut.next_idx;
125            let end_val = ptr_val.saturating_add(size);
126            cache_mut.entries[idx] = Some(CacheEntry {
127                ptr_start: ptr_val,
128                ptr_end: end_val,
129                node: expected_node,
130                generation: gen,
131                local,
132            });
133            cache_mut.next_idx = (idx + 1) % 16;
134        });
135
136        local
137    }
138
139    #[cfg(not(feature = "std"))]
140    {
141        verify_numa_locality_os(ptr, size, expected_node)
142    }
143}
144
145fn verify_numa_locality_os(ptr: *const u8, size: usize, expected_node: u32) -> bool {
146    #[cfg(target_os = "windows")]
147    unsafe {
148        use core::ffi::c_void;
149        #[repr(C)]
150        #[derive(Copy, Clone)]
151        struct PsapiWorkingSetExInformation {
152            virtual_address: *mut c_void,
153            virtual_attributes: usize,
154        }
155        extern "system" {
156            fn GetCurrentProcess() -> *mut c_void;
157            fn K32QueryWorkingSetEx(hProcess: *mut c_void, pv: *mut c_void, cb: u32) -> i32;
158        }
159        let page_size = 4096;
160        let start_page = (ptr as usize) & !(page_size - 1);
161        let end_page = ((ptr as usize) + size + page_size - 1) & !(page_size - 1);
162        let pages_count = (end_page - start_page) / page_size;
163        if pages_count == 0 {
164            return true;
165        }
166
167        const CHUNK_SIZE: usize = 64;
168        let mut info_arr = [PsapiWorkingSetExInformation {
169            virtual_address: core::ptr::null_mut(),
170            virtual_attributes: 0,
171        }; CHUNK_SIZE];
172
173        let mut checked = 0;
174        while checked < pages_count {
175            let chunk_len = core::cmp::min(pages_count - checked, CHUNK_SIZE);
176            for i in 0..chunk_len {
177                info_arr[i].virtual_address =
178                    (start_page + (checked + i) * page_size) as *mut c_void;
179                info_arr[i].virtual_attributes = 0;
180            }
181            let cb = (chunk_len * core::mem::size_of::<PsapiWorkingSetExInformation>()) as u32;
182            let res = K32QueryWorkingSetEx(
183                GetCurrentProcess(),
184                info_arr.as_mut_ptr() as *mut c_void,
185                cb,
186            );
187            if res != 0 {
188                for i in 0..chunk_len {
189                    let flags = info_arr[i].virtual_attributes;
190                    let valid = (flags & 1) != 0;
191                    if valid {
192                        let node = (flags >> 16) & 0x3F;
193                        if node as u32 != expected_node {
194                            return false;
195                        }
196                    }
197                }
198            } else {
199                return false;
200            }
201            checked += chunk_len;
202        }
203        true
204    }
205
206    #[cfg(all(target_os = "linux", feature = "libnuma"))]
207    unsafe {
208        #[link(name = "numa")]
209        extern "C" {
210            fn move_pages(
211                pid: i32,
212                count: usize,
213                pages: *const *mut core::ffi::c_void,
214                nodes: *const i32,
215                status: *mut i32,
216                flags: i32,
217            ) -> i32;
218        }
219        let page_size = 4096;
220        let start_page = (ptr as usize) & !(page_size - 1);
221        let end_page = ((ptr as usize) + size + page_size - 1) & !(page_size - 1);
222        let pages_count = (end_page - start_page) / page_size;
223        if pages_count == 0 {
224            return true;
225        }
226
227        const CHUNK_SIZE: usize = 64;
228        let mut pages_arr = [core::ptr::null_mut(); CHUNK_SIZE];
229        let mut status_arr = [0i32; CHUNK_SIZE];
230
231        let mut checked = 0;
232        while checked < pages_count {
233            let chunk_len = core::cmp::min(pages_count - checked, CHUNK_SIZE);
234            for i in 0..chunk_len {
235                pages_arr[i] = (start_page + (checked + i) * page_size) as *mut core::ffi::c_void;
236            }
237            let res = move_pages(
238                0,
239                chunk_len,
240                pages_arr.as_ptr(),
241                core::ptr::null(),
242                status_arr.as_mut_ptr(),
243                0,
244            );
245            if res >= 0 {
246                for i in 0..chunk_len {
247                    let node = status_arr[i];
248                    if node >= 0 && node as u32 != expected_node {
249                        return false;
250                    }
251                }
252            } else {
253                return false;
254            }
255            checked += chunk_len;
256        }
257        return true;
258    }
259
260    #[cfg(all(target_os = "linux", not(feature = "libnuma")))]
261    {
262        let _ = ptr;
263        let _ = size;
264        let _ = expected_node;
265        true
266    }
267
268    #[cfg(not(any(target_os = "windows", target_os = "linux")))]
269    {
270        let _ = ptr;
271        let _ = size;
272        let _ = expected_node;
273        true
274    }
275}