Skip to main content

oxicuda_levelzero/
memory.rs

1//! Level Zero memory manager — allocates, copies, and frees device memory
2//! buffers using the Level Zero API with host-staging for transfers.
3//!
4//! Device memory is not directly CPU-accessible; all host↔device copies
5//! use a temporary host-side staging allocation and a command list.
6//!
7//! All buffers are tracked by opaque `u64` handles (starting at 1) that
8//! mirror the CUDA device-pointer model used by the rest of OxiCUDA.
9
10use std::{
11    collections::HashMap,
12    sync::{Arc, Mutex},
13};
14
15#[cfg(any(target_os = "linux", target_os = "windows"))]
16use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
17
18use crate::{
19    device::LevelZeroDevice,
20    error::{LevelZeroError, LevelZeroResult},
21};
22
23// ─── Platform-specific imports ───────────────────────────────────────────────
24
25#[cfg(any(target_os = "linux", target_os = "windows"))]
26use std::ffi::c_void;
27
28#[cfg(any(target_os = "linux", target_os = "windows"))]
29use crate::device::{
30    ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC, ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC,
31    ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC, ZeCommandListDesc, ZeCommandListHandle,
32    ZeDeviceMemAllocDesc, ZeHostMemAllocDesc,
33};
34
35// ─── Internal buffer record ──────────────────────────────────────────────────
36
37/// Bookkeeping entry for a single allocated Level Zero device buffer.
38struct L0BufferRecord {
39    /// Raw device pointer (Linux and Windows only).
40    #[cfg(any(target_os = "linux", target_os = "windows"))]
41    device_ptr: *mut c_void,
42    /// Byte size of the allocation.
43    #[cfg(any(target_os = "linux", target_os = "windows"))]
44    size: u64,
45}
46
47// SAFETY: `L0BufferRecord` contains a raw pointer that is logically owned
48// by the `LevelZeroMemoryManager`.  Access is serialized through a `Mutex`.
49#[cfg(any(target_os = "linux", target_os = "windows"))]
50unsafe impl Send for L0BufferRecord {}
51
52// ─── Memory manager ──────────────────────────────────────────────────────────
53
54/// Manages a pool of Level Zero device buffers, returning opaque `u64` handles.
55///
56/// Uses explicit host-staging buffers and command lists for host↔device
57/// data transfers, matching the Level Zero programming model.
58///
59/// All public methods take `&self` so the manager can be shared behind `Arc`.
60pub struct LevelZeroMemoryManager {
61    #[cfg(any(target_os = "linux", target_os = "windows"))]
62    device: Arc<LevelZeroDevice>,
63    buffers: Mutex<HashMap<u64, L0BufferRecord>>,
64    #[cfg(any(target_os = "linux", target_os = "windows"))]
65    next_handle: AtomicU64,
66}
67
68impl LevelZeroMemoryManager {
69    /// Create a new memory manager backed by `device`.
70    #[cfg(any(target_os = "linux", target_os = "windows"))]
71    pub fn new(device: Arc<LevelZeroDevice>) -> Self {
72        Self {
73            device,
74            buffers: Mutex::new(HashMap::new()),
75            next_handle: AtomicU64::new(1),
76        }
77    }
78
79    /// Stub constructor on unsupported platforms.
80    ///
81    /// All methods return [`LevelZeroError::UnsupportedPlatform`].
82    #[cfg(not(any(target_os = "linux", target_os = "windows")))]
83    pub fn new(_device: Arc<LevelZeroDevice>) -> Self {
84        Self {
85            buffers: Mutex::new(HashMap::new()),
86        }
87    }
88
89    /// Get the raw device pointer for a buffer handle.
90    ///
91    /// Returns the `*mut c_void` device pointer that Level Zero allocated for
92    /// the given handle.  This is needed to pass as a kernel argument.
93    #[cfg(any(target_os = "linux", target_os = "windows"))]
94    pub fn device_ptr(&self, handle: u64) -> LevelZeroResult<*mut c_void> {
95        let buffers = self
96            .buffers
97            .lock()
98            .map_err(|_| LevelZeroError::CommandListError("mutex poisoned".into()))?;
99        let rec = buffers
100            .get(&handle)
101            .ok_or_else(|| LevelZeroError::InvalidArgument(format!("unknown handle {handle}")))?;
102        Ok(rec.device_ptr)
103    }
104
105    /// Allocate `bytes` bytes of device memory.
106    ///
107    /// Returns an opaque handle.  The caller must eventually call [`free`](Self::free).
108    pub fn alloc(&self, bytes: usize) -> LevelZeroResult<u64> {
109        #[cfg(any(target_os = "linux", target_os = "windows"))]
110        {
111            let api = &self.device.api;
112            let context = self.device.context;
113            let device_handle = self.device.device;
114
115            let desc = ZeDeviceMemAllocDesc {
116                stype: ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC,
117                p_next: std::ptr::null(),
118                flags: 0,
119                ordinal: 0,
120            };
121
122            let mut ptr: *mut c_void = std::ptr::null_mut();
123            // SAFETY: `context` and `device_handle` are valid Level Zero handles;
124            // `desc` is properly initialized; `ptr` is a valid output pointer.
125            let rc = unsafe {
126                (api.ze_mem_alloc_device)(
127                    context,
128                    &desc,
129                    bytes,
130                    64, // 64-byte alignment
131                    device_handle,
132                    &mut ptr as *mut *mut c_void,
133                )
134            };
135
136            if rc != 0 {
137                return Err(LevelZeroError::ZeError(
138                    rc,
139                    "zeMemAllocDevice failed".into(),
140                ));
141            }
142
143            let handle = self.next_handle.fetch_add(1, Relaxed);
144            self.buffers
145                .lock()
146                .map_err(|_| LevelZeroError::CommandListError("mutex poisoned".into()))?
147                .insert(
148                    handle,
149                    L0BufferRecord {
150                        device_ptr: ptr,
151                        size: bytes as u64,
152                    },
153                );
154
155            Ok(handle)
156        }
157
158        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
159        {
160            let _ = bytes;
161            Err(LevelZeroError::UnsupportedPlatform)
162        }
163    }
164
165    /// Release the device buffer associated with `handle`.
166    ///
167    /// Unknown handles are silently ignored (idempotent free).
168    pub fn free(&self, handle: u64) -> LevelZeroResult<()> {
169        #[cfg(any(target_os = "linux", target_os = "windows"))]
170        {
171            let record = self
172                .buffers
173                .lock()
174                .map_err(|_| LevelZeroError::CommandListError("mutex poisoned".into()))?
175                .remove(&handle);
176
177            if let Some(rec) = record {
178                let api = &self.device.api;
179                let context = self.device.context;
180                // SAFETY: `rec.device_ptr` was allocated by `zeMemAllocDevice`
181                // and has not been freed yet (we just removed it from the map).
182                let rc = unsafe { (api.ze_mem_free)(context, rec.device_ptr) };
183                if rc != 0 {
184                    return Err(LevelZeroError::ZeError(rc, "zeMemFree failed".into()));
185                }
186            }
187            Ok(())
188        }
189
190        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
191        {
192            let _ = handle;
193            Err(LevelZeroError::UnsupportedPlatform)
194        }
195    }
196
197    /// Upload host bytes `src` into the device buffer identified by `handle`.
198    ///
199    /// Allocates a temporary host-side staging buffer, copies the data into it,
200    /// then uses a command list to schedule the device copy and waits for completion.
201    pub fn copy_to_device(&self, handle: u64, src: &[u8]) -> LevelZeroResult<()> {
202        #[cfg(any(target_os = "linux", target_os = "windows"))]
203        {
204            // Hold the `buffers` lock for the whole transfer so a concurrent
205            // `free(handle)` on another thread cannot free `device_ptr` while
206            // the copy is still in flight (TOCTOU use-after-free).
207            let buffers = self
208                .buffers
209                .lock()
210                .map_err(|_| LevelZeroError::CommandListError("mutex poisoned".into()))?;
211            let (device_ptr, alloc_size) = {
212                let rec = buffers.get(&handle).ok_or_else(|| {
213                    LevelZeroError::InvalidArgument(format!("unknown handle {handle}"))
214                })?;
215                (rec.device_ptr, rec.size)
216            };
217
218            // Bound the copy length against the recorded allocation size so a
219            // safe caller cannot overflow the device buffer (OOB device write).
220            if src.len() as u64 > alloc_size {
221                return Err(LevelZeroError::InvalidArgument(format!(
222                    "copy_to_device: src length {} exceeds device allocation size {} (handle {handle})",
223                    src.len(),
224                    alloc_size
225                )));
226            }
227
228            let api = &self.device.api;
229            let context = self.device.context;
230            let device_handle = self.device.device;
231            let queue = self.device.queue;
232            let copy_len = src.len();
233
234            // Allocate a host staging buffer.
235            let host_desc = ZeHostMemAllocDesc {
236                stype: ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
237                p_next: std::ptr::null(),
238                flags: 0,
239            };
240            let mut host_ptr: *mut c_void = std::ptr::null_mut();
241            // SAFETY: `context` is valid; `host_desc` is properly initialized;
242            // `host_ptr` is a valid output pointer.
243            let rc = unsafe {
244                (api.ze_mem_alloc_host)(
245                    context,
246                    &host_desc,
247                    copy_len,
248                    64,
249                    &mut host_ptr as *mut *mut c_void,
250                )
251            };
252            if rc != 0 {
253                return Err(LevelZeroError::ZeError(
254                    rc,
255                    "zeMemAllocHost (staging) failed".into(),
256                ));
257            }
258
259            // Copy host data into the staging buffer.
260            // SAFETY: `host_ptr` is a valid CPU-accessible pointer allocated
261            // by `zeMemAllocHost`; `src` is a valid slice of `copy_len` bytes.
262            unsafe {
263                std::ptr::copy_nonoverlapping(src.as_ptr(), host_ptr as *mut u8, copy_len);
264            }
265
266            // Create a command list for the copy.
267            let list_desc = ZeCommandListDesc {
268                stype: ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
269                p_next: std::ptr::null(),
270                command_queue_group_ordinal: 0,
271                flags: 0,
272            };
273            let mut list: ZeCommandListHandle = std::ptr::null_mut();
274            // SAFETY: `context` and `device_handle` are valid; `list_desc` is
275            // properly initialized; `list` is a valid output pointer.
276            let rc = unsafe {
277                (api.ze_command_list_create)(
278                    context,
279                    device_handle,
280                    &list_desc,
281                    &mut list as *mut ZeCommandListHandle,
282                )
283            };
284            if rc != 0 {
285                // SAFETY: host_ptr was successfully allocated above.
286                unsafe { (api.ze_mem_free)(context, host_ptr) };
287                return Err(LevelZeroError::CommandListError(format!(
288                    "zeCommandListCreate failed: 0x{rc:08x}"
289                )));
290            }
291
292            // Append the host→device memory copy to the command list.
293            // SAFETY: `list`, `device_ptr`, and `host_ptr` are valid;
294            // the copy length matches the data we staged.
295            let rc = unsafe {
296                (api.ze_command_list_append_memory_copy)(
297                    list,
298                    device_ptr,
299                    host_ptr as *const c_void,
300                    copy_len,
301                    0, // no signal event
302                    0, // no wait events
303                    std::ptr::null(),
304                )
305            };
306            if rc != 0 {
307                // SAFETY: list and host_ptr were allocated above.
308                unsafe {
309                    (api.ze_command_list_destroy)(list);
310                    (api.ze_mem_free)(context, host_ptr);
311                }
312                return Err(LevelZeroError::CommandListError(format!(
313                    "zeCommandListAppendMemoryCopy failed: 0x{rc:08x}"
314                )));
315            }
316
317            // Close and execute the command list.
318            // SAFETY: `list` is in the recording state.
319            let rc = unsafe { (api.ze_command_list_close)(list) };
320            if rc != 0 {
321                unsafe {
322                    (api.ze_command_list_destroy)(list);
323                    (api.ze_mem_free)(context, host_ptr);
324                }
325                return Err(LevelZeroError::CommandListError(format!(
326                    "zeCommandListClose failed: 0x{rc:08x}"
327                )));
328            }
329
330            // SAFETY: `queue` is valid; `list` is closed and ready for submission.
331            let rc = unsafe { (api.ze_command_queue_execute_command_lists)(queue, 1, &list, 0) };
332            if rc != 0 {
333                unsafe {
334                    (api.ze_command_list_destroy)(list);
335                    (api.ze_mem_free)(context, host_ptr);
336                }
337                return Err(LevelZeroError::CommandListError(format!(
338                    "zeCommandQueueExecuteCommandLists failed: 0x{rc:08x}"
339                )));
340            }
341
342            // Wait for completion.
343            // SAFETY: `queue` is valid; u64::MAX means "wait indefinitely".
344            let rc = unsafe { (api.ze_command_queue_synchronize)(queue, u64::MAX) };
345            if rc != 0 {
346                unsafe {
347                    (api.ze_command_list_destroy)(list);
348                    (api.ze_mem_free)(context, host_ptr);
349                }
350                return Err(LevelZeroError::CommandListError(format!(
351                    "zeCommandQueueSynchronize failed: 0x{rc:08x}"
352                )));
353            }
354
355            // Clean up.
356            // SAFETY: `list` was created above and is no longer needed.
357            unsafe {
358                (api.ze_command_list_destroy)(list);
359                (api.ze_mem_free)(context, host_ptr);
360            }
361
362            Ok(())
363        }
364
365        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
366        {
367            let _ = (handle, src);
368            Err(LevelZeroError::UnsupportedPlatform)
369        }
370    }
371
372    /// Download device buffer `handle` into `dst`.
373    ///
374    /// Uses a host-staging buffer and a command list for the device→host copy.
375    pub fn copy_from_device(&self, dst: &mut [u8], handle: u64) -> LevelZeroResult<()> {
376        #[cfg(any(target_os = "linux", target_os = "windows"))]
377        {
378            // Hold the `buffers` lock for the whole transfer so a concurrent
379            // `free(handle)` cannot free `device_ptr` mid-copy (TOCTOU
380            // use-after-free).
381            let buffers = self
382                .buffers
383                .lock()
384                .map_err(|_| LevelZeroError::CommandListError("mutex poisoned".into()))?;
385            let (device_ptr, alloc_size) = {
386                let rec = buffers.get(&handle).ok_or_else(|| {
387                    LevelZeroError::InvalidArgument(format!("unknown handle {handle}"))
388                })?;
389                (rec.device_ptr, rec.size)
390            };
391
392            // Bound the read length against the recorded allocation size so a
393            // safe caller cannot read past the device buffer (OOB device read /
394            // adjacent-buffer info leak).
395            if dst.len() as u64 > alloc_size {
396                return Err(LevelZeroError::InvalidArgument(format!(
397                    "copy_from_device: dst length {} exceeds device allocation size {} (handle {handle})",
398                    dst.len(),
399                    alloc_size
400                )));
401            }
402
403            let api = &self.device.api;
404            let context = self.device.context;
405            let device_handle = self.device.device;
406            let queue = self.device.queue;
407            let copy_len = dst.len();
408
409            // Allocate a host staging buffer.
410            let host_desc = ZeHostMemAllocDesc {
411                stype: ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
412                p_next: std::ptr::null(),
413                flags: 0,
414            };
415            let mut host_ptr: *mut c_void = std::ptr::null_mut();
416            // SAFETY: `context` is valid; `host_desc` is properly initialized;
417            // `host_ptr` is a valid output pointer.
418            let rc = unsafe {
419                (api.ze_mem_alloc_host)(
420                    context,
421                    &host_desc,
422                    copy_len,
423                    64,
424                    &mut host_ptr as *mut *mut c_void,
425                )
426            };
427            if rc != 0 {
428                return Err(LevelZeroError::ZeError(
429                    rc,
430                    "zeMemAllocHost (staging) failed".into(),
431                ));
432            }
433
434            // Create a command list for the copy.
435            let list_desc = ZeCommandListDesc {
436                stype: ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
437                p_next: std::ptr::null(),
438                command_queue_group_ordinal: 0,
439                flags: 0,
440            };
441            let mut list: ZeCommandListHandle = std::ptr::null_mut();
442            // SAFETY: `context` and `device_handle` are valid; `list_desc` is
443            // properly initialized; `list` is a valid output pointer.
444            let rc = unsafe {
445                (api.ze_command_list_create)(
446                    context,
447                    device_handle,
448                    &list_desc,
449                    &mut list as *mut ZeCommandListHandle,
450                )
451            };
452            if rc != 0 {
453                unsafe { (api.ze_mem_free)(context, host_ptr) };
454                return Err(LevelZeroError::CommandListError(format!(
455                    "zeCommandListCreate failed: 0x{rc:08x}"
456                )));
457            }
458
459            // Append the device→host memory copy to the command list.
460            // SAFETY: `list`, `host_ptr`, and `device_ptr` are valid;
461            // the copy length matches the destination buffer.
462            let rc = unsafe {
463                (api.ze_command_list_append_memory_copy)(
464                    list,
465                    host_ptr,
466                    device_ptr as *const c_void,
467                    copy_len,
468                    0, // no signal event
469                    0, // no wait events
470                    std::ptr::null(),
471                )
472            };
473            if rc != 0 {
474                unsafe {
475                    (api.ze_command_list_destroy)(list);
476                    (api.ze_mem_free)(context, host_ptr);
477                }
478                return Err(LevelZeroError::CommandListError(format!(
479                    "zeCommandListAppendMemoryCopy failed: 0x{rc:08x}"
480                )));
481            }
482
483            // Close and execute the command list.
484            // SAFETY: `list` is in the recording state.
485            let rc = unsafe { (api.ze_command_list_close)(list) };
486            if rc != 0 {
487                unsafe {
488                    (api.ze_command_list_destroy)(list);
489                    (api.ze_mem_free)(context, host_ptr);
490                }
491                return Err(LevelZeroError::CommandListError(format!(
492                    "zeCommandListClose failed: 0x{rc:08x}"
493                )));
494            }
495
496            // SAFETY: `queue` is valid; `list` is closed and ready for submission.
497            let rc = unsafe { (api.ze_command_queue_execute_command_lists)(queue, 1, &list, 0) };
498            if rc != 0 {
499                unsafe {
500                    (api.ze_command_list_destroy)(list);
501                    (api.ze_mem_free)(context, host_ptr);
502                }
503                return Err(LevelZeroError::CommandListError(format!(
504                    "zeCommandQueueExecuteCommandLists failed: 0x{rc:08x}"
505                )));
506            }
507
508            // Wait for completion.
509            // SAFETY: `queue` is valid; u64::MAX means "wait indefinitely".
510            let rc = unsafe { (api.ze_command_queue_synchronize)(queue, u64::MAX) };
511            if rc != 0 {
512                unsafe {
513                    (api.ze_command_list_destroy)(list);
514                    (api.ze_mem_free)(context, host_ptr);
515                }
516                return Err(LevelZeroError::CommandListError(format!(
517                    "zeCommandQueueSynchronize failed: 0x{rc:08x}"
518                )));
519            }
520
521            // Copy staging buffer to destination.
522            // SAFETY: `host_ptr` is valid and contains `copy_len` bytes of data
523            // transferred from the device; `dst` is a valid mutable slice.
524            unsafe {
525                std::ptr::copy_nonoverlapping(host_ptr as *const u8, dst.as_mut_ptr(), copy_len);
526            }
527
528            // Clean up.
529            // SAFETY: `list` and `host_ptr` were allocated above.
530            unsafe {
531                (api.ze_command_list_destroy)(list);
532                (api.ze_mem_free)(context, host_ptr);
533            }
534
535            Ok(())
536        }
537
538        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
539        {
540            let _ = (dst, handle);
541            Err(LevelZeroError::UnsupportedPlatform)
542        }
543    }
544}
545
546// ─── Drop ────────────────────────────────────────────────────────────────────
547
548impl Drop for LevelZeroMemoryManager {
549    fn drop(&mut self) {
550        #[cfg(any(target_os = "linux", target_os = "windows"))]
551        {
552            let api = &self.device.api;
553            let context = self.device.context;
554
555            if let Ok(mut map) = self.buffers.lock() {
556                for (handle, rec) in map.drain() {
557                    tracing::warn!(
558                        "LevelZeroMemoryManager: leaked buffer handle {handle} ({} bytes)",
559                        rec.size
560                    );
561                    // SAFETY: `rec.device_ptr` is a valid outstanding allocation.
562                    unsafe { (api.ze_mem_free)(context, rec.device_ptr) };
563                }
564            }
565        }
566
567        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
568        {
569            // Nothing to do on unsupported platforms.
570        }
571    }
572}
573
574// ─── Debug ───────────────────────────────────────────────────────────────────
575
576impl std::fmt::Debug for LevelZeroMemoryManager {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        let count = self.buffers.lock().map(|b| b.len()).unwrap_or(0);
579        write!(f, "LevelZeroMemoryManager(buffers={count})")
580    }
581}
582
583// ─── Send + Sync ─────────────────────────────────────────────────────────────
584
585// SAFETY: `LevelZeroMemoryManager` serializes all access through a `Mutex`.
586// The raw pointer inside `L0BufferRecord` is owned and not aliased.
587unsafe impl Send for LevelZeroMemoryManager {}
588// SAFETY: See `Send` impl above.  All mutable operations go through a `Mutex`.
589unsafe impl Sync for LevelZeroMemoryManager {}
590
591// ─── Tests ───────────────────────────────────────────────────────────────────
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    fn try_get_device() -> Option<Arc<LevelZeroDevice>> {
598        LevelZeroDevice::new().ok().map(Arc::new)
599    }
600
601    #[test]
602    fn alloc_and_free_requires_device() {
603        let Some(dev) = try_get_device() else {
604            return;
605        };
606        let mm = LevelZeroMemoryManager::new(dev);
607        let h = mm.alloc(256).expect("alloc 256 bytes");
608        assert!(h > 0);
609        mm.free(h).expect("free");
610        // Double-free: the handle is gone from the map, so it silently ignores.
611        mm.free(h).expect("double-free is a no-op");
612    }
613
614    #[test]
615    fn copy_roundtrip_requires_device() {
616        let Some(dev) = try_get_device() else {
617            return;
618        };
619        let mm = LevelZeroMemoryManager::new(dev);
620
621        let src: Vec<u8> = (0u8..64).collect();
622        let h = mm.alloc(src.len()).expect("alloc");
623        mm.copy_to_device(h, &src).expect("copy_to_device");
624
625        let mut dst = vec![0u8; src.len()];
626        mm.copy_from_device(&mut dst, h).expect("copy_from_device");
627
628        assert_eq!(src, dst);
629        mm.free(h).expect("free");
630    }
631
632    #[test]
633    fn unknown_handle_returns_error() {
634        let Some(dev) = try_get_device() else {
635            return;
636        };
637        let mm = LevelZeroMemoryManager::new(dev);
638        let err = mm.copy_to_device(9999, b"hello").unwrap_err();
639        assert!(matches!(err, LevelZeroError::InvalidArgument(_)));
640    }
641
642    #[test]
643    fn debug_impl_smoke() {
644        let Some(dev) = try_get_device() else {
645            return;
646        };
647        let mm = LevelZeroMemoryManager::new(dev);
648        let s = format!("{mm:?}");
649        assert!(s.contains("LevelZeroMemoryManager"));
650    }
651}