Skip to main content

oxicuda_memory/
virtual_memory.rs

1//! Virtual memory management for fine-grained GPU address space control.
2//!
3//! This module provides abstractions for CUDA's virtual memory management
4//! API (`cuMemAddressReserve`, `cuMemCreate`, `cuMemMap`, etc.), which
5//! allows separating the concepts of virtual address reservation and
6//! physical memory allocation.
7//!
8//! # Concepts
9//!
10//! * **Virtual Address Range** — A reservation of contiguous virtual
11//!   addresses in the GPU address space. No physical memory is committed
12//!   until explicitly mapped.
13//!
14//! * **Physical Allocation** — A chunk of physical GPU memory that can
15//!   be mapped to one or more virtual address ranges.
16//!
17//! * **Mapping** — The association of a physical allocation with a region
18//!   of a virtual address range.
19//!
20//! # Use Cases
21//!
22//! * **Sparse arrays** — Reserve a large virtual range but only commit
23//!   physical memory for the tiles/pages that are actually used.
24//!
25//! * **Resizable buffers** — Reserve a large virtual range up-front and
26//!   map additional physical memory as the buffer grows, without changing
27//!   the base address.
28//!
29//! * **Multi-GPU memory** — Map physical allocations from different devices
30//!   into the same virtual address space.
31//!
32//! # Status
33//!
34//! The virtual memory driver functions are not yet loaded in
35//! `oxicuda-driver`. All operations that would require driver calls
36//! currently return [`CudaError::NotSupported`]. The data structures
37//! are fully functional for planning and validation purposes.
38//!
39//! # Example
40//!
41//! ```rust,no_run
42//! use oxicuda_memory::virtual_memory::{
43//!     VirtualAddressRange, PhysicalAllocation, VirtualMemoryManager, AccessFlags,
44//! };
45//!
46//! // Reserve 1 GiB of virtual address space with 2 MiB alignment.
47//! let va = VirtualMemoryManager::reserve(1 << 30, 1 << 21)?;
48//! assert_eq!(va.size(), 1 << 30);
49//!
50//! // The actual GPU calls are not yet available, so alloc/map/unmap
51//! // return NotSupported.
52//! # Ok::<(), oxicuda_driver::error::CudaError>(())
53//! ```
54
55use std::fmt;
56
57use oxicuda_driver::error::{CudaError, CudaResult};
58
59// ---------------------------------------------------------------------------
60// AccessFlags
61// ---------------------------------------------------------------------------
62
63/// Memory access permission flags for virtual memory mappings.
64///
65/// These flags control how a mapped virtual address range can be accessed
66/// by a given device.
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
68pub enum AccessFlags {
69    /// No access permitted. The mapping exists but cannot be read or written.
70    #[default]
71    None,
72    /// Read-only access. The device can read but not write.
73    Read,
74    /// Full read-write access.
75    ReadWrite,
76}
77
78impl fmt::Display for AccessFlags {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::None => write!(f, "None"),
82            Self::Read => write!(f, "Read"),
83            Self::ReadWrite => write!(f, "ReadWrite"),
84        }
85    }
86}
87
88// ---------------------------------------------------------------------------
89// VirtualAddressRange
90// ---------------------------------------------------------------------------
91
92/// A reserved range of virtual addresses in the GPU address space.
93///
94/// This represents a contiguous block of virtual addresses that has been
95/// reserved but not necessarily backed by physical memory. Physical memory
96/// is associated with the range via [`VirtualMemoryManager::map`].
97///
98/// # Note
99///
100/// On systems without CUDA virtual memory support, the `base` address
101/// is set to 0 and operations on the range will return
102/// [`CudaError::NotSupported`].
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct VirtualAddressRange {
105    base: u64,
106    size: usize,
107    alignment: usize,
108}
109
110impl VirtualAddressRange {
111    /// Returns the base virtual address of the range.
112    #[inline]
113    pub fn base(&self) -> u64 {
114        self.base
115    }
116
117    /// Returns the size of the range in bytes.
118    #[inline]
119    pub fn size(&self) -> usize {
120        self.size
121    }
122
123    /// Returns the alignment of the range in bytes.
124    #[inline]
125    pub fn alignment(&self) -> usize {
126        self.alignment
127    }
128
129    /// Returns whether the range contains the given virtual address.
130    pub fn contains(&self, addr: u64) -> bool {
131        addr >= self.base && addr < self.base.saturating_add(self.size as u64)
132    }
133
134    /// Returns the end address (exclusive) of the range.
135    #[inline]
136    pub fn end(&self) -> u64 {
137        self.base.saturating_add(self.size as u64)
138    }
139}
140
141impl fmt::Display for VirtualAddressRange {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(
144            f,
145            "VA[0x{:016x}..0x{:016x}, {} bytes, align={}]",
146            self.base,
147            self.end(),
148            self.size,
149            self.alignment,
150        )
151    }
152}
153
154// ---------------------------------------------------------------------------
155// PhysicalAllocation
156// ---------------------------------------------------------------------------
157
158/// A physical memory allocation on a specific GPU device.
159///
160/// Physical allocations represent actual GPU VRAM that can be mapped
161/// into virtual address ranges. Multiple virtual ranges can map to
162/// the same physical allocation (aliasing).
163///
164/// # Note
165///
166/// On systems without CUDA virtual memory support, the `handle` is
167/// set to 0 and the allocation is not backed by real memory.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct PhysicalAllocation {
170    handle: u64,
171    size: usize,
172    device_ordinal: i32,
173}
174
175impl PhysicalAllocation {
176    /// Returns the opaque handle for this physical allocation.
177    #[inline]
178    pub fn handle(&self) -> u64 {
179        self.handle
180    }
181
182    /// Returns the size of this allocation in bytes.
183    #[inline]
184    pub fn size(&self) -> usize {
185        self.size
186    }
187
188    /// Returns the device ordinal this allocation belongs to.
189    #[inline]
190    pub fn device_ordinal(&self) -> i32 {
191        self.device_ordinal
192    }
193}
194
195impl fmt::Display for PhysicalAllocation {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(
198            f,
199            "PhysAlloc[handle=0x{:016x}, {} bytes, dev={}]",
200            self.handle, self.size, self.device_ordinal,
201        )
202    }
203}
204
205// ---------------------------------------------------------------------------
206// MappingRecord — tracks virtual-to-physical mappings
207// ---------------------------------------------------------------------------
208
209/// A record of a virtual-to-physical memory mapping.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct MappingRecord {
212    /// Offset within the virtual address range where the mapping starts.
213    pub va_offset: usize,
214    /// Size of the mapped region in bytes.
215    pub size: usize,
216    /// Handle of the physical allocation backing this mapping.
217    pub phys_handle: u64,
218    /// Access permissions for this mapping.
219    pub access: AccessFlags,
220}
221
222// ---------------------------------------------------------------------------
223// VirtualMemoryManager
224// ---------------------------------------------------------------------------
225
226/// Manager for GPU virtual memory operations.
227///
228/// Provides methods for reserving virtual address ranges, allocating
229/// physical memory, mapping/unmapping, and setting access permissions.
230///
231/// # Status
232///
233/// The underlying CUDA virtual memory driver functions
234/// (`cuMemAddressReserve`, `cuMemCreate`, `cuMemMap`, `cuMemUnmap`,
235/// `cuMemSetAccess`) are not yet loaded in `oxicuda-driver`. All
236/// methods that would require driver calls return
237/// [`CudaError::NotSupported`].
238///
239/// The [`reserve`](Self::reserve) method creates a local placeholder
240/// object for planning purposes.
241pub struct VirtualMemoryManager;
242
243impl VirtualMemoryManager {
244    /// Reserves a range of virtual addresses in the GPU address space.
245    ///
246    /// The reserved range is not backed by physical memory until
247    /// [`map`](Self::map) is called.
248    ///
249    /// # Parameters
250    ///
251    /// * `size` - Size of the virtual range to reserve in bytes.
252    ///   Must be a multiple of `alignment`.
253    /// * `alignment` - Alignment requirement in bytes. Must be a power
254    ///   of two and non-zero.
255    ///
256    /// # Errors
257    ///
258    /// * [`CudaError::InvalidValue`] if `size` is zero, `alignment` is
259    ///   zero, `alignment` is not a power of two, or `size` is not a
260    ///   multiple of `alignment`.
261    pub fn reserve(size: usize, alignment: usize) -> CudaResult<VirtualAddressRange> {
262        if size == 0 {
263            return Err(CudaError::InvalidValue);
264        }
265        if alignment == 0 || !alignment.is_power_of_two() {
266            return Err(CudaError::InvalidValue);
267        }
268        if size % alignment != 0 {
269            return Err(CudaError::InvalidValue);
270        }
271
272        // TODO: call cuMemAddressReserve when available in DriverApi.
273        // For now, create a placeholder with a synthetic base address.
274        // We use a deterministic address based on size+alignment for
275        // reproducible testing.
276        let synthetic_base = 0x0000_7F00_0000_0000_u64
277            .wrapping_add(size as u64)
278            .wrapping_add(alignment as u64);
279
280        Ok(VirtualAddressRange {
281            base: synthetic_base,
282            size,
283            alignment,
284        })
285    }
286
287    /// Releases a previously reserved virtual address range.
288    ///
289    /// After this call, the virtual addresses are no longer reserved
290    /// and may be reused by future reservations.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`CudaError::NotSupported`] because the driver function
295    /// `cuMemAddressFree` is not yet loaded.
296    pub fn release(_va: VirtualAddressRange) -> CudaResult<()> {
297        // TODO: call cuMemAddressFree when available
298        Err(CudaError::NotSupported)
299    }
300
301    /// Allocates physical memory on the specified device.
302    ///
303    /// The allocated memory is not accessible until mapped into a
304    /// virtual address range via [`map`](Self::map).
305    ///
306    /// # Parameters
307    ///
308    /// * `size` - Size of the allocation in bytes. Must be non-zero.
309    /// * `device_ordinal` - Ordinal of the device to allocate on.
310    ///
311    /// # Errors
312    ///
313    /// * [`CudaError::InvalidValue`] if `size` is zero.
314    /// * [`CudaError::NotSupported`] because the driver function
315    ///   `cuMemCreate` is not yet loaded.
316    pub fn alloc_physical(size: usize, device_ordinal: i32) -> CudaResult<PhysicalAllocation> {
317        if size == 0 {
318            return Err(CudaError::InvalidValue);
319        }
320        // TODO: call cuMemCreate when available in DriverApi
321        Err(CudaError::NotSupported)?;
322
323        // This code is unreachable today but shows the intended return type.
324        Ok(PhysicalAllocation {
325            handle: 0,
326            size,
327            device_ordinal,
328        })
329    }
330
331    /// Frees a physical memory allocation.
332    ///
333    /// The allocation must not be currently mapped to any virtual range.
334    ///
335    /// # Errors
336    ///
337    /// Returns [`CudaError::NotSupported`] because the driver function
338    /// `cuMemRelease` is not yet loaded.
339    pub fn free_physical(_phys: PhysicalAllocation) -> CudaResult<()> {
340        // TODO: call cuMemRelease when available
341        Err(CudaError::NotSupported)
342    }
343
344    /// Maps a physical allocation to a region of a virtual address range.
345    ///
346    /// After mapping, GPU kernels can access the virtual addresses and
347    /// reads/writes will be routed to the physical memory.
348    ///
349    /// # Parameters
350    ///
351    /// * `va` - The virtual address range to map into.
352    /// * `phys` - The physical allocation to map.
353    /// * `offset` - Byte offset within the virtual range at which to
354    ///   start the mapping. Must be aligned to the VA's alignment.
355    ///
356    /// # Errors
357    ///
358    /// * [`CudaError::InvalidValue`] if `offset` is not aligned, or if
359    ///   the physical allocation would extend past the end of the virtual
360    ///   range.
361    /// * [`CudaError::NotSupported`] because the driver function
362    ///   `cuMemMap` is not yet loaded.
363    pub fn map(
364        va: &VirtualAddressRange,
365        phys: &PhysicalAllocation,
366        offset: usize,
367    ) -> CudaResult<()> {
368        // Validate alignment
369        if va.alignment > 0 && offset % va.alignment != 0 {
370            return Err(CudaError::InvalidValue);
371        }
372        // Validate bounds
373        let end = offset
374            .checked_add(phys.size)
375            .ok_or(CudaError::InvalidValue)?;
376        if end > va.size {
377            return Err(CudaError::InvalidValue);
378        }
379        // TODO: call cuMemMap when available
380        Err(CudaError::NotSupported)
381    }
382
383    /// Unmaps a region of a virtual address range.
384    ///
385    /// After unmapping, accesses to the affected virtual addresses will
386    /// fault. The physical memory is not freed — it can be remapped
387    /// elsewhere.
388    ///
389    /// # Parameters
390    ///
391    /// * `va` - The virtual address range to unmap from.
392    /// * `offset` - Byte offset within the range where unmapping starts.
393    /// * `size` - Number of bytes to unmap.
394    ///
395    /// # Errors
396    ///
397    /// * [`CudaError::InvalidValue`] if the offset+size exceeds the
398    ///   virtual range bounds.
399    /// * [`CudaError::NotSupported`] because the driver function
400    ///   `cuMemUnmap` is not yet loaded.
401    pub fn unmap(va: &VirtualAddressRange, offset: usize, size: usize) -> CudaResult<()> {
402        let end = offset.checked_add(size).ok_or(CudaError::InvalidValue)?;
403        if end > va.size {
404            return Err(CudaError::InvalidValue);
405        }
406        // TODO: call cuMemUnmap when available
407        Err(CudaError::NotSupported)
408    }
409
410    /// Sets access permissions for a virtual address range on a device.
411    ///
412    /// This controls whether the specified device can read and/or write
413    /// to the mapped virtual addresses.
414    ///
415    /// # Parameters
416    ///
417    /// * `va` - The virtual address range to set permissions on.
418    /// * `device_ordinal` - The device to grant/deny access for.
419    /// * `flags` - The access permission flags.
420    ///
421    /// # Errors
422    ///
423    /// Returns [`CudaError::NotSupported`] because the driver function
424    /// `cuMemSetAccess` is not yet loaded.
425    pub fn set_access(
426        _va: &VirtualAddressRange,
427        _device_ordinal: i32,
428        _flags: AccessFlags,
429    ) -> CudaResult<()> {
430        // TODO: call cuMemSetAccess when available
431        Err(CudaError::NotSupported)
432    }
433}
434
435// ---------------------------------------------------------------------------
436// Tests
437// ---------------------------------------------------------------------------
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn reserve_valid_range() {
445        let va = VirtualMemoryManager::reserve(4096, 4096);
446        assert!(va.is_ok());
447        let va = va.ok();
448        assert!(va.is_some());
449        if let Some(va) = va {
450            assert_eq!(va.size(), 4096);
451            assert_eq!(va.alignment(), 4096);
452            assert!(va.base() > 0);
453        }
454    }
455
456    #[test]
457    fn reserve_zero_size_fails() {
458        let result = VirtualMemoryManager::reserve(0, 4096);
459        assert_eq!(result, Err(CudaError::InvalidValue));
460    }
461
462    #[test]
463    fn reserve_zero_alignment_fails() {
464        let result = VirtualMemoryManager::reserve(4096, 0);
465        assert_eq!(result, Err(CudaError::InvalidValue));
466    }
467
468    #[test]
469    fn reserve_non_power_of_two_alignment_fails() {
470        let result = VirtualMemoryManager::reserve(4096, 3);
471        assert_eq!(result, Err(CudaError::InvalidValue));
472    }
473
474    #[test]
475    fn reserve_misaligned_size_fails() {
476        // 4096+1 is not a multiple of 4096
477        let result = VirtualMemoryManager::reserve(4097, 4096);
478        assert_eq!(result, Err(CudaError::InvalidValue));
479    }
480
481    #[test]
482    fn reserve_large_range() {
483        // Reserve 1 GiB with 2 MiB alignment.
484        let gib = 1 << 30;
485        let mib2 = 1 << 21;
486        let va = VirtualMemoryManager::reserve(gib, mib2);
487        assert!(va.is_ok());
488        if let Ok(va) = va {
489            assert_eq!(va.size(), gib);
490            assert_eq!(va.alignment(), mib2);
491        }
492    }
493
494    #[test]
495    fn virtual_address_range_contains() {
496        let va = VirtualMemoryManager::reserve(8192, 4096);
497        assert!(va.is_ok());
498        if let Ok(va) = va {
499            assert!(va.contains(va.base()));
500            assert!(va.contains(va.base() + 1));
501            assert!(va.contains(va.base() + 8191));
502            assert!(!va.contains(va.end()));
503            assert!(!va.contains(va.base().wrapping_sub(1)));
504        }
505    }
506
507    #[test]
508    fn virtual_address_range_end() {
509        let va = VirtualMemoryManager::reserve(4096, 4096);
510        assert!(va.is_ok());
511        if let Ok(va) = va {
512            assert_eq!(va.end(), va.base() + 4096);
513        }
514    }
515
516    #[test]
517    fn virtual_address_range_display() {
518        let va = VirtualMemoryManager::reserve(4096, 4096);
519        assert!(va.is_ok());
520        if let Ok(va) = va {
521            let disp = format!("{va}");
522            assert!(disp.contains("VA["));
523            assert!(disp.contains("4096 bytes"));
524        }
525    }
526
527    #[test]
528    fn alloc_physical_zero_size_fails() {
529        let result = VirtualMemoryManager::alloc_physical(0, 0);
530        assert_eq!(result, Err(CudaError::InvalidValue));
531    }
532
533    #[test]
534    fn alloc_physical_returns_not_supported() {
535        let result = VirtualMemoryManager::alloc_physical(4096, 0);
536        assert_eq!(result, Err(CudaError::NotSupported));
537    }
538
539    #[test]
540    fn release_returns_not_supported() {
541        let va = VirtualMemoryManager::reserve(4096, 4096);
542        assert!(va.is_ok());
543        if let Ok(va) = va {
544            let result = VirtualMemoryManager::release(va);
545            assert_eq!(result, Err(CudaError::NotSupported));
546        }
547    }
548
549    #[test]
550    fn map_validates_alignment() {
551        let va = VirtualMemoryManager::reserve(8192, 4096);
552        assert!(va.is_ok());
553        if let Ok(va) = va {
554            let phys = PhysicalAllocation {
555                handle: 1,
556                size: 4096,
557                device_ordinal: 0,
558            };
559            // Offset 1 is not aligned to 4096
560            let result = VirtualMemoryManager::map(&va, &phys, 1);
561            assert_eq!(result, Err(CudaError::InvalidValue));
562        }
563    }
564
565    #[test]
566    fn map_validates_bounds() {
567        let va = VirtualMemoryManager::reserve(4096, 4096);
568        assert!(va.is_ok());
569        if let Ok(va) = va {
570            let phys = PhysicalAllocation {
571                handle: 1,
572                size: 8192, // larger than VA range
573                device_ordinal: 0,
574            };
575            let result = VirtualMemoryManager::map(&va, &phys, 0);
576            assert_eq!(result, Err(CudaError::InvalidValue));
577        }
578    }
579
580    #[test]
581    fn map_returns_not_supported_when_valid() {
582        let va = VirtualMemoryManager::reserve(8192, 4096);
583        assert!(va.is_ok());
584        if let Ok(va) = va {
585            let phys = PhysicalAllocation {
586                handle: 1,
587                size: 4096,
588                device_ordinal: 0,
589            };
590            let result = VirtualMemoryManager::map(&va, &phys, 0);
591            assert_eq!(result, Err(CudaError::NotSupported));
592        }
593    }
594
595    #[test]
596    fn unmap_validates_bounds() {
597        let va = VirtualMemoryManager::reserve(4096, 4096);
598        assert!(va.is_ok());
599        if let Ok(va) = va {
600            let result = VirtualMemoryManager::unmap(&va, 0, 8192);
601            assert_eq!(result, Err(CudaError::InvalidValue));
602        }
603    }
604
605    #[test]
606    fn unmap_returns_not_supported_when_valid() {
607        let va = VirtualMemoryManager::reserve(4096, 4096);
608        assert!(va.is_ok());
609        if let Ok(va) = va {
610            let result = VirtualMemoryManager::unmap(&va, 0, 4096);
611            assert_eq!(result, Err(CudaError::NotSupported));
612        }
613    }
614
615    #[test]
616    fn set_access_returns_not_supported() {
617        let va = VirtualMemoryManager::reserve(4096, 4096);
618        assert!(va.is_ok());
619        if let Ok(va) = va {
620            let result = VirtualMemoryManager::set_access(&va, 0, AccessFlags::ReadWrite);
621            assert_eq!(result, Err(CudaError::NotSupported));
622        }
623    }
624
625    #[test]
626    fn access_flags_default() {
627        assert_eq!(AccessFlags::default(), AccessFlags::None);
628    }
629
630    #[test]
631    fn access_flags_display() {
632        assert_eq!(format!("{}", AccessFlags::None), "None");
633        assert_eq!(format!("{}", AccessFlags::Read), "Read");
634        assert_eq!(format!("{}", AccessFlags::ReadWrite), "ReadWrite");
635    }
636
637    #[test]
638    fn physical_allocation_display() {
639        let phys = PhysicalAllocation {
640            handle: 0x1234,
641            size: 4096,
642            device_ordinal: 0,
643        };
644        let disp = format!("{phys}");
645        assert!(disp.contains("4096 bytes"));
646        assert!(disp.contains("dev=0"));
647    }
648
649    #[test]
650    fn mapping_record_fields() {
651        let record = MappingRecord {
652            va_offset: 0,
653            size: 4096,
654            phys_handle: 42,
655            access: AccessFlags::ReadWrite,
656        };
657        assert_eq!(record.va_offset, 0);
658        assert_eq!(record.size, 4096);
659        assert_eq!(record.phys_handle, 42);
660        assert_eq!(record.access, AccessFlags::ReadWrite);
661    }
662
663    #[test]
664    fn free_physical_returns_not_supported() {
665        let phys = PhysicalAllocation {
666            handle: 1,
667            size: 4096,
668            device_ordinal: 0,
669        };
670        let result = VirtualMemoryManager::free_physical(phys);
671        assert_eq!(result, Err(CudaError::NotSupported));
672    }
673}