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