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}