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}