1use super::get_current_cpu_numa_node;
16use cudarc::driver::result::malloc_host;
17use cudarc::driver::sys::CU_MEMHOSTALLOC_DEVICEMAP;
18use nix::libc;
19use std::sync::mpsc::{Receiver, Sender, channel};
20use std::sync::{Arc, Mutex, OnceLock};
21use std::thread::{self, JoinHandle};
22use std::time::Duration;
23
24use super::{NumaNode, get_device_numa_node};
25
26struct SendPtr(*mut u8);
36
37unsafe impl Send for SendPtr {}
40
41struct AllocRequest {
43 size: usize,
45 node: NumaNode,
47 gpu_id: u32,
49 response: Sender<AllocResult>,
51}
52
53type AllocResult = Result<SendPtr, String>;
55
56struct NumaWorker {
58 node: NumaNode,
59 request_tx: Option<Sender<AllocRequest>>,
60 handle: Option<JoinHandle<()>>,
61}
62
63impl NumaWorker {
64 fn spawn(node: NumaNode) -> Result<Self, String> {
66 let (request_tx, request_rx) = channel();
67
68 let handle = thread::Builder::new()
69 .name(format!("numa-worker-{}", node.0))
70 .spawn(move || {
71 Self::worker_loop(node, request_rx);
72 })
73 .map_err(|e| format!("Failed to spawn worker thread: {}", e))?;
74
75 Ok(Self {
76 node,
77 request_tx: Some(request_tx),
78 handle: Some(handle),
79 })
80 }
81
82 fn worker_loop(node: NumaNode, requests: Receiver<AllocRequest>) {
88 tracing::trace!("Pinning worker thread to node {}", node.0);
90 if let Err(e) = super::pin_thread_to_numa_node(node) {
91 tracing::error!("Failed to pin worker thread to node {}: {}", node.0, e);
92 tracing::error!("Worker will continue but allocations may be suboptimal");
93 } else {
94 tracing::trace!("Successfully pinned worker thread to node {}", node.0);
95
96 thread::yield_now();
103 thread::sleep(Duration::from_millis(1));
104
105 let current_node = super::get_current_cpu_numa_node();
107 tracing::trace!("Current node after pinning: {}", current_node.0);
108 if current_node != node {
109 tracing::warn!(
110 "Worker thread on node {} after pinning (expected {})",
111 current_node.0,
112 node.0
113 );
114 } else {
115 tracing::trace!("NUMA worker thread for node {} started and pinned", node.0);
116 }
117 }
118
119 loop {
121 tracing::trace!("Worker waiting for request on node {}", node.0);
122 match requests.recv() {
123 Ok(req) => {
124 tracing::trace!(
125 "Worker received CUDA pinned allocation request on node {}",
126 node.0
127 );
128 let result = Self::do_cuda_pinned_allocation(req.size, req.node, req.gpu_id);
129 match result {
130 Ok(SendPtr(ptr)) => {
131 if let Err(_e) = req.response.send(Ok(SendPtr(ptr))) {
132 tracing::warn!(
134 "Receiver dropped before receiving allocation, freeing {} bytes at {:p}",
135 req.size,
136 ptr
137 );
138 unsafe {
139 let _ = cudarc::driver::result::free_host(
140 ptr as *mut std::ffi::c_void,
141 );
142 }
143 }
144 }
145 Err(err) => {
146 let _ = req.response.send(Err(err));
147 }
148 }
149 }
150 Err(_) => {
151 tracing::trace!(
153 "NUMA worker for node {} shutting down (channel closed)",
154 node.0
155 );
156 break;
157 }
158 }
159 }
160 }
161
162 fn do_cuda_pinned_allocation(size: usize, node: NumaNode, gpu_id: u32) -> AllocResult {
164 if size == 0 {
165 return Err("Cannot allocate zero bytes".to_string());
166 }
167
168 let node_before = get_current_cpu_numa_node();
170 if node_before != node {
171 tracing::warn!(
172 "Worker thread moved! Expected NUMA node {}, currently on node {}",
173 node.0,
174 node_before.0
175 );
176 }
177
178 let ctx = crate::device::cuda_context(gpu_id)
180 .map_err(|e| format!("Failed to create CUDA context for device {}: {}", gpu_id, e))?;
181
182 unsafe {
183 ctx.bind_to_thread()
186 .map_err(|e| format!("Failed to bind CUDA context to worker thread: {:?}", e))?;
187
188 let node_after_ctx = get_current_cpu_numa_node();
190 if node_after_ctx != node {
191 tracing::warn!(
192 "Thread moved after CUDA context bind! Expected node {}, now on node {}",
193 node.0,
194 node_after_ctx.0
195 );
196 }
197
198 let ptr = malloc_host(size, CU_MEMHOSTALLOC_DEVICEMAP)
202 .map_err(|e| format!("malloc_host failed: {:?}", e))?;
203
204 let ptr = ptr as *mut u8;
205
206 if ptr.is_null() {
207 return Err("malloc_host returned null".to_string());
208 }
209
210 let node_before_touch = get_current_cpu_numa_node();
212 if node_before_touch != node {
213 tracing::error!(
214 "Thread on wrong node before first-touch! Expected {}, on node {} - memory will be misplaced!",
215 node.0,
216 node_before_touch.0
217 );
218 }
219
220 let page_size = match libc::sysconf(libc::_SC_PAGESIZE) {
223 n if n > 0 => n as usize,
224 _ => 4096,
225 };
226 let mut offset = 0usize;
227 while offset < size {
228 std::ptr::write_volatile(ptr.add(offset), 0);
229 offset = offset.saturating_add(page_size);
230 }
231 if size > 0 && !size.is_multiple_of(page_size) {
233 std::ptr::write_volatile(ptr.add(size - 1), 0);
234 }
235
236 let node_after_touch = get_current_cpu_numa_node();
238
239 tracing::trace!(
240 "Worker allocated {} bytes (CUDA pinned) on GPU {} (target NUMA node {}) at {:p} - thread nodes: before={} after_ctx={} before_touch={} after_touch={}",
241 size,
242 gpu_id,
243 node.0,
244 ptr,
245 node_before.0,
246 node_after_ctx.0,
247 node_before_touch.0,
248 node_after_touch.0
249 );
250
251 Ok(SendPtr(ptr))
252 }
253 }
254
255 fn allocate(&self, size: usize, gpu_id: u32) -> AllocResult {
257 let (response_tx, response_rx) = channel();
258
259 let request = AllocRequest {
260 size,
261 node: self.node,
262 gpu_id,
263 response: response_tx,
264 };
265
266 self.request_tx
267 .as_ref()
268 .ok_or_else(|| "Worker has been shut down".to_string())?
269 .send(request)
270 .map_err(|_| "Worker thread has died".to_string())?;
271
272 let timeout_secs = 10u64 + (size as u64 / (1024 * 1024 * 1024));
276 let timeout = Duration::from_secs(timeout_secs.clamp(10, 300)); tracing::trace!(
279 "Worker pool waiting for allocation of {} MB with timeout of {} seconds",
280 size / (1024 * 1024),
281 timeout.as_secs()
282 );
283
284 response_rx
285 .recv_timeout(timeout)
286 .map_err(|e| format!("Worker timeout after {} seconds: {}", timeout.as_secs(), e))?
287 }
288}
289
290impl Drop for NumaWorker {
291 fn drop(&mut self) {
292 tracing::trace!("Dropping NUMA worker for node {}", self.node.0);
293
294 self.request_tx.take();
297 tracing::trace!("Channel closed for worker node {}", self.node.0);
298
299 if let Some(handle) = self.handle.take() {
301 tracing::trace!("Waiting for worker thread {} to join", self.node.0);
302 let _ = handle.join();
303 tracing::trace!("Worker thread {} joined", self.node.0);
304 }
305 }
306}
307
308pub struct NumaWorkerPool {
314 workers: Mutex<std::collections::HashMap<u32, Arc<NumaWorker>>>,
315}
316
317impl NumaWorkerPool {
318 fn new() -> Self {
319 Self {
320 workers: Mutex::new(std::collections::HashMap::new()),
321 }
322 }
323
324 pub fn global() -> &'static Self {
328 static POOL: OnceLock<NumaWorkerPool> = OnceLock::new();
329 POOL.get_or_init(NumaWorkerPool::new)
330 }
331
332 fn get_or_spawn_worker(&self, node: NumaNode) -> Result<Arc<NumaWorker>, String> {
334 let mut workers = self.workers.lock().unwrap();
335
336 if let Some(worker) = workers.get(&node.0) {
337 return Ok(worker.clone());
338 }
339
340 let worker = NumaWorker::spawn(node)?;
342 let worker = Arc::new(worker);
343 workers.insert(node.0, worker.clone());
344
345 tracing::trace!("Spawned NUMA worker for node {}", node.0);
346
347 Ok(worker)
348 }
349
350 pub fn allocate_pinned_for_gpu(
368 &self,
369 size: usize,
370 gpu_id: u32,
371 ) -> Result<Option<*mut u8>, String> {
372 let node = match get_device_numa_node(gpu_id) {
373 Some(node) => node,
374 None => {
375 tracing::debug!(
376 "NUMA node unknown for GPU {}, skipping NUMA-aware allocation",
377 gpu_id
378 );
379 return Ok(None);
380 }
381 };
382
383 tracing::debug!(
384 "Allocating {} bytes pinned memory for GPU {} (NUMA node {})",
385 size,
386 gpu_id,
387 node.0
388 );
389
390 let worker = self.get_or_spawn_worker(node)?;
391 worker
392 .allocate(size, gpu_id)
393 .map(|send_ptr| Some(send_ptr.0))
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::numa::get_current_cpu_numa_node;
401
402 #[test]
403 fn test_worker_spawn() {
404 let node = NumaNode(0);
405 let worker = NumaWorker::spawn(node);
406 assert!(worker.is_ok());
407 }
408
409 #[test]
410 fn test_worker_pool_singleton() {
411 let pool1 = NumaWorkerPool::global();
412 let pool2 = NumaWorkerPool::global();
413 assert!(std::ptr::eq(pool1, pool2));
414 }
415
416 #[test]
417 fn test_get_current_cpu_numa_node() {
418 let node = get_current_cpu_numa_node();
419 if !node.is_unknown() {
420 println!("Current CPU on NUMA node: {}", node.0);
421 } else {
422 println!("NUMA node detection unavailable (single-node or fake NUMA)");
423 }
424 }
425
426 #[test]
427 fn test_numa_node_display() {
428 let node = NumaNode(0);
429 assert_eq!(format!("{}", node), "NumaNode(0)");
430
431 let unknown = NumaNode::UNKNOWN;
432 assert_eq!(format!("{}", unknown), "UNKNOWN");
433 }
434
435 #[test]
436 fn test_numa_node_is_unknown() {
437 let valid = NumaNode(0);
438 assert!(!valid.is_unknown());
439
440 let unknown = NumaNode::UNKNOWN;
441 assert!(unknown.is_unknown());
442 }
443}
444
445#[cfg(all(test, feature = "testing-cuda"))]
446mod cuda_tests {
447 use super::*;
448 use crate::numa::get_device_numa_node;
449
450 #[test]
451 fn test_worker_allocate_pinned() {
452 let node = NumaNode(0);
453 let worker = NumaWorker::spawn(node).unwrap();
454
455 let send_ptr = worker.allocate(4096, 0).unwrap();
456 let ptr = send_ptr.0;
457 assert!(!ptr.is_null());
458
459 unsafe {
460 cudarc::driver::result::free_host(ptr as *mut std::ffi::c_void).unwrap();
461 }
462 }
463
464 #[test]
465 fn test_worker_pool() {
466 let pool = NumaWorkerPool::new();
467
468 match pool.allocate_pinned_for_gpu(8192, 0).unwrap() {
469 Some(ptr) => unsafe {
470 assert!(!ptr.is_null());
471 cudarc::driver::result::free_host(ptr as *mut std::ffi::c_void).unwrap();
472 },
473 None => {
474 println!(
475 "NUMA node unknown for GPU 0, allocation skipped (expected on single-socket)"
476 );
477 }
478 }
479 }
480
481 #[test]
482 fn test_worker_reuse() {
483 let pool = NumaWorkerPool::new();
484
485 let r1 = pool.allocate_pinned_for_gpu(1024, 0).unwrap();
487 let r2 = pool.allocate_pinned_for_gpu(1024, 0).unwrap();
488
489 match (r1, r2) {
490 (Some(ptr1), Some(ptr2)) => unsafe {
491 assert!(!ptr1.is_null());
492 assert!(!ptr2.is_null());
493 assert_ne!(ptr1, ptr2);
494 cudarc::driver::result::free_host(ptr1 as *mut std::ffi::c_void).unwrap();
495 cudarc::driver::result::free_host(ptr2 as *mut std::ffi::c_void).unwrap();
496 },
497 (None, None) => {
498 println!("NUMA node unknown, both allocations skipped");
499 }
500 _ => panic!("inconsistent NUMA detection between two calls for same GPU"),
501 }
502 }
503
504 #[test]
505 fn test_zero_size_allocation_with_known_node() {
506 let pool = NumaWorkerPool::new();
510 let result = pool.allocate_pinned_for_gpu(0, 0);
511 match result {
512 Ok(None) => {
513 println!("NUMA node unknown, zero-size check not reached");
514 }
515 Err(e) => {
516 assert!(e.contains("zero"));
517 }
518 Ok(Some(_)) => panic!("zero-size allocation should not succeed"),
519 }
520 }
521
522 #[test]
523 fn test_get_device_numa_node() {
524 let node = get_device_numa_node(0);
525 match node {
526 Some(n) => {
527 assert!(n.0 < 16, "NUMA node {} seems unreasonably high", n.0);
528 println!("GPU 0 on NUMA node: {}", n.0);
529 }
530 None => {
531 println!("GPU 0 has no determinable NUMA node");
532 }
533 }
534 }
535
536 #[test]
537 fn test_pinned_allocation_api() {
538 let pool = NumaWorkerPool::new();
539
540 if let Some(ptr) = pool.allocate_pinned_for_gpu(1024, 0).unwrap() {
541 assert!(!ptr.is_null());
542 unsafe {
543 cudarc::driver::result::free_host(ptr as *mut std::ffi::c_void).unwrap();
544 }
545 }
546 }
547
548 #[test]
549 fn test_worker_channel_communication() {
550 let node = NumaNode(0);
551 let worker = NumaWorker::spawn(node).unwrap();
552
553 let send_ptr = worker.allocate(1024, 0).unwrap();
554 let ptr = send_ptr.0;
555 assert!(!ptr.is_null());
556
557 unsafe {
558 cudarc::driver::result::free_host(ptr as *mut std::ffi::c_void).unwrap();
559 }
560 }
561}