Skip to main content

oxicuda_memory/
aligned.rs

1//! Aligned GPU memory allocation for optimal access patterns.
2//!
3//! This module provides [`AlignedBuffer<T>`], a device memory buffer that
4//! guarantees a specific alignment for the starting address.  Proper alignment
5//! is critical for coalesced memory accesses on GPUs — misaligned loads and
6//! stores can incur extra memory transactions, significantly hurting
7//! throughput.
8//!
9//! # Alignment options
10//!
11//! | Variant          | Bytes   | Use case                                    |
12//! |------------------|---------|---------------------------------------------|
13//! | `Default`        | 256     | CUDA's natural allocation alignment          |
14//! | `Align256`       | 256     | Explicit 256-byte alignment                  |
15//! | `Align512`       | 512     | Optimal for many GPU texture/surface ops     |
16//! | `Align1024`      | 1024    | Large-stride access patterns                 |
17//! | `Align4096`      | 4096    | Page-aligned for unified/mapped memory       |
18//! | `Custom(n)`      | n       | User-specified (must be a power of two)      |
19//!
20//! # Platform note (macOS)
21//!
22//! There is no CUDA driver on macOS, so [`AlignedBuffer::alloc`] returns
23//! [`CudaError::NotInitialized`] there — it never fabricates a synthetic
24//! device pointer. This matches `NativeMemoryPool::new` (`pool.rs`) and
25//! `VirtualMemoryReservation::reserve` (`virtual_memory.rs`) elsewhere in
26//! this crate. This is a *different* contract from `host_registered.rs`'s
27//! pinned-host-memory wrappers, which deliberately return a synthetic `Ok`
28//! handle on macOS because pinning already-valid host memory has a sensible
29//! host-only meaning even without a device; see that module's own docs.
30//!
31//! # Example
32//!
33//! ```rust,no_run
34//! # use oxicuda_memory::aligned::{Alignment, AlignedBuffer};
35//! let buf = AlignedBuffer::<f32>::alloc(1024, Alignment::Align512)?;
36//! assert!(buf.is_aligned());
37//! assert_eq!(buf.as_device_ptr() % 512, 0);
38//! # Ok::<(), oxicuda_driver::error::CudaError>(())
39//! ```
40
41use std::marker::PhantomData;
42
43use oxicuda_driver::error::{CudaError, CudaResult};
44use oxicuda_driver::ffi::CUdeviceptr;
45use oxicuda_driver::loader::try_driver;
46
47// ---------------------------------------------------------------------------
48// Alignment enum
49// ---------------------------------------------------------------------------
50
51/// Specifies the byte alignment for a device memory allocation.
52///
53/// All variants represent alignments that are powers of two.  The `Custom`
54/// variant is validated at allocation time.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum Alignment {
57    /// CUDA's default allocation alignment (typically 256 bytes).
58    Default,
59    /// 256-byte alignment.
60    Align256,
61    /// 512-byte alignment.
62    Align512,
63    /// 1024-byte alignment.
64    Align1024,
65    /// 4096-byte (page) alignment.
66    Align4096,
67    /// User-specified alignment in bytes (must be a power of two).
68    Custom(usize),
69}
70
71impl Alignment {
72    /// Returns the alignment in bytes.
73    ///
74    /// For [`Default`](Alignment::Default), this returns 256 (the typical CUDA
75    /// allocation alignment).
76    #[inline]
77    pub fn bytes(&self) -> usize {
78        match self {
79            Self::Default => 256,
80            Self::Align256 => 256,
81            Self::Align512 => 512,
82            Self::Align1024 => 1024,
83            Self::Align4096 => 4096,
84            Self::Custom(n) => *n,
85        }
86    }
87
88    /// Returns `true` if the alignment value is a power of two.
89    ///
90    /// This is always `true` for the named variants and may be `false` for
91    /// [`Custom`](Alignment::Custom) with an invalid value.
92    #[inline]
93    pub fn is_power_of_two(&self) -> bool {
94        let b = self.bytes();
95        b > 0 && (b & (b - 1)) == 0
96    }
97
98    /// Returns `true` if the given device pointer satisfies this alignment.
99    #[inline]
100    pub fn is_aligned(&self, ptr: u64) -> bool {
101        let b = self.bytes() as u64;
102        if b == 0 {
103            return false;
104        }
105        (ptr % b) == 0
106    }
107}
108
109impl std::fmt::Display for Alignment {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::Default => write!(f, "Default(256)"),
113            Self::Align256 => write!(f, "256"),
114            Self::Align512 => write!(f, "512"),
115            Self::Align1024 => write!(f, "1024"),
116            Self::Align4096 => write!(f, "4096"),
117            Self::Custom(n) => write!(f, "Custom({n})"),
118        }
119    }
120}
121
122// ---------------------------------------------------------------------------
123// Validation helpers
124// ---------------------------------------------------------------------------
125
126/// Maximum alignment we allow (256 MiB).  Anything beyond this is almost
127/// certainly a programming error.
128const MAX_ALIGNMENT: usize = 256 * 1024 * 1024;
129
130/// Validates that an [`Alignment`] is a power of two and within a reasonable
131/// range.
132///
133/// # Errors
134///
135/// Returns [`CudaError::InvalidValue`] if:
136/// - The alignment is zero.
137/// - The alignment is not a power of two.
138/// - The alignment exceeds 256 MiB.
139pub fn validate_alignment(alignment: &Alignment) -> CudaResult<()> {
140    let b = alignment.bytes();
141    if b == 0 {
142        return Err(CudaError::InvalidValue);
143    }
144    if !alignment.is_power_of_two() {
145        return Err(CudaError::InvalidValue);
146    }
147    if b > MAX_ALIGNMENT {
148        return Err(CudaError::InvalidValue);
149    }
150    Ok(())
151}
152
153/// Rounds `bytes` up to the next multiple of `alignment`.
154///
155/// `alignment` must be a power of two; otherwise the result is unspecified.
156///
157/// # Examples
158///
159/// ```
160/// # use oxicuda_memory::aligned::round_up_to_alignment;
161/// assert_eq!(round_up_to_alignment(100, 256), 256);
162/// assert_eq!(round_up_to_alignment(256, 256), 256);
163/// assert_eq!(round_up_to_alignment(257, 256), 512);
164/// assert_eq!(round_up_to_alignment(0, 256), 0);
165/// ```
166#[inline]
167pub fn round_up_to_alignment(bytes: usize, alignment: usize) -> usize {
168    if alignment == 0 {
169        return bytes;
170    }
171    let mask = alignment - 1;
172    (bytes + mask) & !mask
173}
174
175/// Recommends an optimal [`Alignment`] for a type based on its size.
176///
177/// The heuristic prefers alignments that enable coalesced memory accesses:
178///
179/// - Types of 16 bytes or more benefit from 512-byte alignment because a
180///   32-thread warp issuing 16-byte loads touches exactly 512 bytes.
181/// - Types of 8 bytes benefit from 256-byte alignment (warp touches 256 bytes).
182/// - Smaller types use the CUDA default (256 bytes).
183pub fn optimal_alignment_for_type<T>() -> Alignment {
184    let size = std::mem::size_of::<T>();
185    if size >= 16 {
186        Alignment::Align512
187    } else if size >= 8 {
188        Alignment::Align256
189    } else {
190        Alignment::Default
191    }
192}
193
194/// Computes the smallest alignment that ensures coalesced memory access for a
195/// given `access_width` (in bytes) across a warp of `warp_size` threads.
196///
197/// The coalesced access pattern requires that `warp_size * access_width` bytes
198/// are naturally aligned to the segment boundary used by the memory controller.
199/// This function returns the smallest power-of-two alignment that is at least
200/// `warp_size * access_width` bytes, capped at 4096 (page alignment).
201///
202/// # Examples
203///
204/// ```
205/// # use oxicuda_memory::aligned::coalesce_alignment;
206/// // 32 threads × 4 bytes = 128 → rounded up to 128
207/// assert_eq!(coalesce_alignment(4, 32), 128);
208/// // 32 threads × 16 bytes = 512
209/// assert_eq!(coalesce_alignment(16, 32), 512);
210/// ```
211pub fn coalesce_alignment(access_width: usize, warp_size: u32) -> usize {
212    let total = (warp_size as usize).saturating_mul(access_width);
213    if total == 0 {
214        return 1;
215    }
216    // Round up to the next power of two.
217    let pot = total.next_power_of_two();
218    // Cap at page alignment.
219    pot.min(4096)
220}
221
222// ---------------------------------------------------------------------------
223// AlignmentInfo
224// ---------------------------------------------------------------------------
225
226/// Information about the alignment of an existing device pointer.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct AlignmentInfo {
229    /// The device pointer that was inspected.
230    pub ptr: CUdeviceptr,
231    /// The largest power-of-two alignment that the pointer satisfies.
232    pub natural_alignment: usize,
233    /// Whether the pointer is 256-byte aligned.
234    pub is_256_aligned: bool,
235    /// Whether the pointer is 512-byte aligned.
236    pub is_512_aligned: bool,
237    /// Whether the pointer is page-aligned (4096 bytes).
238    pub is_page_aligned: bool,
239}
240
241/// Inspects a device pointer and reports its alignment characteristics.
242///
243/// For a null (zero) pointer the natural alignment is reported as `usize::MAX`
244/// because zero is trivially aligned to every power of two.
245pub fn check_alignment(ptr: CUdeviceptr) -> AlignmentInfo {
246    let natural = if ptr == 0 {
247        // Zero is aligned to every power of two; report maximum.
248        usize::MAX
249    } else {
250        // The largest power-of-two factor is 2^(trailing_zeros).
251        1_usize << (ptr.trailing_zeros().min(63))
252    };
253    AlignmentInfo {
254        ptr,
255        natural_alignment: natural,
256        is_256_aligned: (ptr % 256) == 0,
257        is_512_aligned: (ptr % 512) == 0,
258        is_page_aligned: (ptr % 4096) == 0,
259    }
260}
261
262// ---------------------------------------------------------------------------
263// AlignedBuffer<T>
264// ---------------------------------------------------------------------------
265
266/// A device memory buffer whose starting address is guaranteed to meet the
267/// requested [`Alignment`].
268///
269/// Internally this may over-allocate by up to `alignment - 1` extra bytes and
270/// offset the user-visible pointer so that it lands on an aligned boundary.
271/// The extra bytes (if any) are reported by [`wasted_bytes`](Self::wasted_bytes).
272///
273/// The buffer frees the *original* (unaligned) allocation on [`Drop`].
274pub struct AlignedBuffer<T: Copy> {
275    /// The aligned device pointer presented to the user.
276    ptr: CUdeviceptr,
277    /// Number of `T` elements.
278    len: usize,
279    /// Total bytes allocated (may be larger than `len * size_of::<T>()`).
280    allocated_bytes: usize,
281    /// The alignment that was requested.
282    alignment: Alignment,
283    /// Byte offset from the raw allocation base to `ptr`.
284    offset: usize,
285    /// The raw allocation base pointer (what we pass to `cuMemFree`).
286    #[cfg_attr(target_os = "macos", allow(dead_code))]
287    raw_ptr: CUdeviceptr,
288    /// Phantom marker for `T`.
289    _phantom: PhantomData<T>,
290}
291
292// SAFETY: Same reasoning as `DeviceBuffer<T>` — the `u64` device pointer
293// handle is managed by the thread-safe CUDA driver.
294unsafe impl<T: Copy + Send> Send for AlignedBuffer<T> {}
295unsafe impl<T: Copy + Sync> Sync for AlignedBuffer<T> {}
296
297impl<T: Copy> AlignedBuffer<T> {
298    /// Allocates an aligned device buffer capable of holding `n` elements of
299    /// type `T`.
300    ///
301    /// The returned buffer's device pointer is guaranteed to be aligned to
302    /// `alignment.bytes()`.  The allocation may be slightly larger than
303    /// `n * size_of::<T>()` to accommodate the alignment offset.
304    ///
305    /// # Errors
306    ///
307    /// * [`CudaError::InvalidValue`] if `n` is zero, alignment is invalid, or
308    ///   the byte-size computation overflows.
309    /// * [`CudaError::OutOfMemory`] if the GPU cannot satisfy the allocation.
310    pub fn alloc(n: usize, alignment: Alignment) -> CudaResult<Self> {
311        if n == 0 {
312            return Err(CudaError::InvalidValue);
313        }
314        validate_alignment(&alignment)?;
315
316        let elem_bytes = n
317            .checked_mul(std::mem::size_of::<T>())
318            .ok_or(CudaError::InvalidValue)?;
319
320        let align_bytes = alignment.bytes();
321
322        // Over-allocate by (alignment - 1) so we can always find an aligned
323        // address within the allocation.
324        let extra = align_bytes.saturating_sub(1);
325        let total_bytes = elem_bytes
326            .checked_add(extra)
327            .ok_or(CudaError::InvalidValue)?;
328
329        // `try_driver()` returns `Err(CudaError::NotInitialized)` on macOS
330        // (there is no CUDA driver to load there), so this call fails
331        // naturally on that platform — the same contract already followed
332        // by `NativeMemoryPool::new` (pool.rs) and
333        // `VirtualMemoryReservation::reserve` (virtual_memory.rs) elsewhere
334        // in this crate. No synthetic, unbacked device pointer is
335        // fabricated and returned as a fake success.
336        let (raw_ptr, aligned_ptr, offset) = {
337            let api = try_driver()?;
338            let mut base: CUdeviceptr = 0;
339            let rc = unsafe { (api.cu_mem_alloc_v2)(&mut base, total_bytes) };
340            oxicuda_driver::check(rc)?;
341            let aligned = round_up_to_alignment(base as usize, align_bytes) as CUdeviceptr;
342            let off = (aligned - base) as usize;
343            (base, aligned, off)
344        };
345
346        Ok(Self {
347            ptr: aligned_ptr,
348            len: n,
349            allocated_bytes: total_bytes,
350            alignment,
351            offset,
352            raw_ptr,
353            _phantom: PhantomData,
354        })
355    }
356
357    /// Returns the aligned device pointer.
358    #[inline]
359    pub fn as_device_ptr(&self) -> CUdeviceptr {
360        self.ptr
361    }
362
363    /// Returns the number of `T` elements in this buffer.
364    #[inline]
365    pub fn len(&self) -> usize {
366        self.len
367    }
368
369    /// Returns `true` if the buffer contains zero elements.
370    ///
371    /// In practice this is always `false` because [`alloc`](Self::alloc)
372    /// rejects zero-length allocations.
373    #[inline]
374    pub fn is_empty(&self) -> bool {
375        self.len == 0
376    }
377
378    /// Returns a reference to the alignment that was requested.
379    #[inline]
380    pub fn alignment(&self) -> &Alignment {
381        &self.alignment
382    }
383
384    /// Returns the number of bytes wasted for alignment padding.
385    ///
386    /// This is the difference between the total allocation size and the
387    /// minimum required (`len * size_of::<T>()`).
388    #[inline]
389    pub fn wasted_bytes(&self) -> usize {
390        let needed = self.len * std::mem::size_of::<T>();
391        self.allocated_bytes.saturating_sub(needed)
392    }
393
394    /// Returns `true` if the buffer's device pointer satisfies the requested
395    /// alignment.
396    #[inline]
397    pub fn is_aligned(&self) -> bool {
398        self.alignment.is_aligned(self.ptr)
399    }
400
401    /// Returns the total number of bytes that were allocated (including
402    /// alignment padding).
403    #[inline]
404    pub fn allocated_bytes(&self) -> usize {
405        self.allocated_bytes
406    }
407
408    /// Returns the byte offset from the raw allocation base to the aligned
409    /// pointer.
410    #[inline]
411    pub fn offset(&self) -> usize {
412        self.offset
413    }
414}
415
416impl<T: Copy> Drop for AlignedBuffer<T> {
417    fn drop(&mut self) {
418        // Free the *raw* (unaligned) allocation, not the offset pointer.
419        #[cfg(not(target_os = "macos"))]
420        {
421            if let Ok(api) = try_driver() {
422                let rc = unsafe { (api.cu_mem_free_v2)(self.raw_ptr) };
423                if rc != 0 {
424                    tracing::warn!(
425                        cuda_error = rc,
426                        ptr = self.raw_ptr,
427                        aligned_ptr = self.ptr,
428                        len = self.len,
429                        "cuMemFree_v2 failed during AlignedBuffer drop"
430                    );
431                }
432            }
433        }
434    }
435}
436
437// ---------------------------------------------------------------------------
438// Tests
439// ---------------------------------------------------------------------------
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    // -- Alignment enum tests -----------------------------------------------
446
447    #[test]
448    fn alignment_bytes_named_variants() {
449        assert_eq!(Alignment::Default.bytes(), 256);
450        assert_eq!(Alignment::Align256.bytes(), 256);
451        assert_eq!(Alignment::Align512.bytes(), 512);
452        assert_eq!(Alignment::Align1024.bytes(), 1024);
453        assert_eq!(Alignment::Align4096.bytes(), 4096);
454    }
455
456    #[test]
457    fn alignment_bytes_custom() {
458        assert_eq!(Alignment::Custom(64).bytes(), 64);
459        assert_eq!(Alignment::Custom(2048).bytes(), 2048);
460    }
461
462    #[test]
463    fn alignment_is_power_of_two() {
464        assert!(Alignment::Default.is_power_of_two());
465        assert!(Alignment::Align256.is_power_of_two());
466        assert!(Alignment::Align512.is_power_of_two());
467        assert!(Alignment::Align1024.is_power_of_two());
468        assert!(Alignment::Align4096.is_power_of_two());
469        assert!(Alignment::Custom(128).is_power_of_two());
470        assert!(!Alignment::Custom(0).is_power_of_two());
471        assert!(!Alignment::Custom(3).is_power_of_two());
472        assert!(!Alignment::Custom(100).is_power_of_two());
473    }
474
475    #[test]
476    fn alignment_is_aligned() {
477        let a256 = Alignment::Align256;
478        assert!(a256.is_aligned(0));
479        assert!(a256.is_aligned(256));
480        assert!(a256.is_aligned(512));
481        assert!(!a256.is_aligned(1));
482        assert!(!a256.is_aligned(128));
483        assert!(!a256.is_aligned(255));
484
485        let a512 = Alignment::Align512;
486        assert!(a512.is_aligned(0));
487        assert!(a512.is_aligned(512));
488        assert!(!a512.is_aligned(256));
489    }
490
491    // -- round_up_to_alignment tests ----------------------------------------
492
493    #[test]
494    fn round_up_basic() {
495        assert_eq!(round_up_to_alignment(0, 256), 0);
496        assert_eq!(round_up_to_alignment(1, 256), 256);
497        assert_eq!(round_up_to_alignment(100, 256), 256);
498        assert_eq!(round_up_to_alignment(256, 256), 256);
499        assert_eq!(round_up_to_alignment(257, 256), 512);
500        assert_eq!(round_up_to_alignment(511, 512), 512);
501        assert_eq!(round_up_to_alignment(512, 512), 512);
502        assert_eq!(round_up_to_alignment(513, 512), 1024);
503    }
504
505    #[test]
506    fn round_up_zero_alignment() {
507        // Zero alignment should not modify the value.
508        assert_eq!(round_up_to_alignment(42, 0), 42);
509    }
510
511    // -- validate_alignment tests -------------------------------------------
512
513    #[test]
514    fn validate_named_variants_ok() {
515        assert!(validate_alignment(&Alignment::Default).is_ok());
516        assert!(validate_alignment(&Alignment::Align256).is_ok());
517        assert!(validate_alignment(&Alignment::Align512).is_ok());
518        assert!(validate_alignment(&Alignment::Align1024).is_ok());
519        assert!(validate_alignment(&Alignment::Align4096).is_ok());
520    }
521
522    #[test]
523    fn validate_custom_ok() {
524        assert!(validate_alignment(&Alignment::Custom(64)).is_ok());
525        assert!(validate_alignment(&Alignment::Custom(128)).is_ok());
526        assert!(validate_alignment(&Alignment::Custom(8192)).is_ok());
527    }
528
529    #[test]
530    fn validate_custom_bad() {
531        // Zero
532        assert!(validate_alignment(&Alignment::Custom(0)).is_err());
533        // Not power of two
534        assert!(validate_alignment(&Alignment::Custom(3)).is_err());
535        assert!(validate_alignment(&Alignment::Custom(100)).is_err());
536        // Too large (> 256 MiB)
537        assert!(validate_alignment(&Alignment::Custom(512 * 1024 * 1024)).is_err());
538    }
539
540    // -- optimal_alignment_for_type tests -----------------------------------
541
542    #[test]
543    fn optimal_alignment_small_types() {
544        // f32 = 4 bytes → Default
545        assert_eq!(optimal_alignment_for_type::<f32>(), Alignment::Default);
546        // u8 = 1 byte → Default
547        assert_eq!(optimal_alignment_for_type::<u8>(), Alignment::Default);
548    }
549
550    #[test]
551    fn optimal_alignment_medium_types() {
552        // f64 = 8 bytes → Align256
553        assert_eq!(optimal_alignment_for_type::<f64>(), Alignment::Align256);
554        // u64 = 8 bytes → Align256
555        assert_eq!(optimal_alignment_for_type::<u64>(), Alignment::Align256);
556    }
557
558    #[test]
559    fn optimal_alignment_large_types() {
560        // [f32; 4] = 16 bytes → Align512
561        assert_eq!(
562            optimal_alignment_for_type::<[f32; 4]>(),
563            Alignment::Align512
564        );
565        // [f64; 4] = 32 bytes → Align512
566        assert_eq!(
567            optimal_alignment_for_type::<[f64; 4]>(),
568            Alignment::Align512
569        );
570    }
571
572    // -- coalesce_alignment tests -------------------------------------------
573
574    #[test]
575    fn coalesce_basic() {
576        // 32 threads × 4 bytes = 128
577        assert_eq!(coalesce_alignment(4, 32), 128);
578        // 32 threads × 8 bytes = 256
579        assert_eq!(coalesce_alignment(8, 32), 256);
580        // 32 threads × 16 bytes = 512
581        assert_eq!(coalesce_alignment(16, 32), 512);
582        // 32 threads × 32 bytes = 1024
583        assert_eq!(coalesce_alignment(32, 32), 1024);
584    }
585
586    #[test]
587    fn coalesce_caps_at_page() {
588        // 64 threads × 128 bytes = 8192 → capped at 4096
589        assert_eq!(coalesce_alignment(128, 64), 4096);
590    }
591
592    #[test]
593    fn coalesce_zero_inputs() {
594        assert_eq!(coalesce_alignment(0, 32), 1);
595        assert_eq!(coalesce_alignment(4, 0), 1);
596        assert_eq!(coalesce_alignment(0, 0), 1);
597    }
598
599    // -- check_alignment tests ----------------------------------------------
600
601    #[test]
602    fn check_alignment_page_aligned() {
603        let info = check_alignment(4096);
604        assert!(info.is_256_aligned);
605        assert!(info.is_512_aligned);
606        assert!(info.is_page_aligned);
607        assert!(info.natural_alignment >= 4096);
608    }
609
610    #[test]
611    fn check_alignment_512_not_page() {
612        let info = check_alignment(512);
613        assert!(info.is_256_aligned);
614        assert!(info.is_512_aligned);
615        assert!(!info.is_page_aligned);
616        assert_eq!(info.natural_alignment, 512);
617    }
618
619    #[test]
620    fn check_alignment_odd_ptr() {
621        let info = check_alignment(0x0001_0001);
622        assert!(!info.is_256_aligned);
623        assert!(!info.is_512_aligned);
624        assert!(!info.is_page_aligned);
625        assert_eq!(info.natural_alignment, 1);
626    }
627
628    #[test]
629    fn check_alignment_null() {
630        let info = check_alignment(0);
631        assert_eq!(info.natural_alignment, usize::MAX);
632        assert!(info.is_256_aligned);
633        assert!(info.is_512_aligned);
634        assert!(info.is_page_aligned);
635    }
636
637    // -- AlignedBuffer tests (macOS: no CUDA driver) ------------------------
638    //
639    // There is no CUDA driver on macOS, so `AlignedBuffer::alloc` must fail
640    // the same honest way `NativeMemoryPool::new` (pool.rs) and
641    // `VirtualMemoryReservation::reserve` (virtual_memory.rs) do —
642    // `Err(CudaError::NotInitialized)` — rather than fabricating an
643    // unbacked device pointer and returning `Ok`. The pure alignment-
644    // arithmetic helpers (`round_up_to_alignment`, `validate_alignment`,
645    // `coalesce_alignment`, `check_alignment`) are covered by the
646    // platform-independent tests above; the real allocation path (which
647    // needs an actual driver) is covered by `gpu_tests` below.
648
649    #[cfg(target_os = "macos")]
650    mod buffer_tests {
651        use super::super::*;
652
653        /// Asserts that `result` is `Err(CudaError::NotInitialized)`.
654        ///
655        /// Takes the success value by type only (no `Debug` bound) since
656        /// `AlignedBuffer<T>` does not implement `Debug`.
657        fn assert_not_initialized<T>(result: CudaResult<T>) {
658            let err = match result {
659                Err(e) => e,
660                Ok(_) => panic!("expected Err(NotInitialized) on macOS, got Ok(_)"),
661            };
662            assert!(
663                matches!(err, CudaError::NotInitialized),
664                "expected NotInitialized, got {err:?}"
665            );
666        }
667
668        #[test]
669        fn alloc_default_alignment_fails_without_driver() {
670            assert_not_initialized(AlignedBuffer::<f32>::alloc(128, Alignment::Default));
671        }
672
673        #[test]
674        fn alloc_512_alignment_fails_without_driver() {
675            assert_not_initialized(AlignedBuffer::<f32>::alloc(256, Alignment::Align512));
676        }
677
678        #[test]
679        fn alloc_4096_alignment_fails_without_driver() {
680            assert_not_initialized(AlignedBuffer::<f64>::alloc(64, Alignment::Align4096));
681        }
682
683        /// `n == 0` is rejected before the driver is ever consulted, so this
684        /// stays `InvalidValue` regardless of platform.
685        #[test]
686        fn alloc_zero_elements_fails_with_invalid_value() {
687            let result = AlignedBuffer::<f32>::alloc(0, Alignment::Default);
688            let err = match result {
689                Err(e) => e,
690                Ok(_) => panic!("expected an error for zero elements"),
691            };
692            assert!(matches!(err, CudaError::InvalidValue));
693        }
694
695        /// An invalid alignment is likewise rejected before the driver is
696        /// consulted, so this stays `InvalidValue` regardless of platform.
697        #[test]
698        fn alloc_invalid_alignment_fails_with_invalid_value() {
699            let result = AlignedBuffer::<f32>::alloc(64, Alignment::Custom(3));
700            let err = match result {
701                Err(e) => e,
702                Ok(_) => panic!("expected an error for invalid alignment"),
703            };
704            assert!(matches!(err, CudaError::InvalidValue));
705        }
706    }
707
708    // -- AlignedBuffer tests (real driver, Linux/Windows + NVIDIA) ----------
709
710    #[cfg(feature = "gpu-tests")]
711    mod gpu_tests {
712        use super::super::*;
713
714        // Each test skips gracefully (rather than failing) when no driver
715        // is available, so this same module is safe to compile under
716        // `gpu-tests` on any platform (matching the pattern used by
717        // `pool.rs`'s and `virtual_memory.rs`'s own `gpu_tests` modules)
718        // while providing real coverage on Linux/Windows + NVIDIA CI.
719
720        #[test]
721        fn alloc_default_alignment_round_trips() {
722            let Ok(buf) = AlignedBuffer::<f32>::alloc(128, Alignment::Default) else {
723                return;
724            };
725            assert_eq!(buf.len(), 128);
726            assert!(!buf.is_empty());
727            assert!(buf.is_aligned());
728        }
729
730        #[test]
731        fn alloc_512_alignment_round_trips() {
732            let Ok(buf) = AlignedBuffer::<f32>::alloc(256, Alignment::Align512) else {
733                return;
734            };
735            assert!(buf.is_aligned());
736            assert_eq!(buf.as_device_ptr() % 512, 0);
737        }
738
739        #[test]
740        fn alloc_4096_alignment_round_trips() {
741            let Ok(buf) = AlignedBuffer::<f64>::alloc(64, Alignment::Align4096) else {
742                return;
743            };
744            assert!(buf.is_aligned());
745            assert_eq!(buf.as_device_ptr() % 4096, 0);
746        }
747
748        #[test]
749        fn wasted_bytes_bounded_by_alignment() {
750            let Ok(buf) = AlignedBuffer::<f32>::alloc(128, Alignment::Align512) else {
751                return;
752            };
753            // Wasted bytes = allocated_bytes - (128 * 4)
754            // allocated_bytes = 128*4 + (512 - 1) = 1023
755            // wasted = 1023 - 512 = 511
756            assert!(buf.wasted_bytes() <= buf.alignment().bytes());
757        }
758
759        #[test]
760        fn alignment_accessor_round_trips() {
761            let Ok(buf) = AlignedBuffer::<u8>::alloc(64, Alignment::Align1024) else {
762                return;
763            };
764            assert_eq!(*buf.alignment(), Alignment::Align1024);
765        }
766    }
767
768    // -- Display -----------------------------------------------------------
769
770    #[test]
771    fn alignment_display() {
772        assert_eq!(format!("{}", Alignment::Default), "Default(256)");
773        assert_eq!(format!("{}", Alignment::Align256), "256");
774        assert_eq!(format!("{}", Alignment::Align512), "512");
775        assert_eq!(format!("{}", Alignment::Custom(128)), "Custom(128)");
776    }
777}