oxicuda_memory/managed_hints.rs
1//! Ergonomic managed memory hints API.
2//!
3//! This module builds on the raw [`crate::memory_info::mem_advise`]
4//! and [`crate::memory_info::mem_prefetch`] functions to provide
5//! a higher-level, builder-style API for controlling unified memory migration
6//! behaviour.
7//!
8//! # Key types
9//!
10//! - [`MigrationPolicy`] — declarative policy for common migration patterns.
11//! - [`ManagedMemoryHints`] — builder for applying hints to a memory region.
12//! - [`PrefetchPlan`] — batch multiple prefetch operations into one plan.
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! # use oxicuda_memory::managed_hints::*;
18//! # use oxicuda_driver::device::Device;
19//! # use oxicuda_driver::stream::Stream;
20//! // Assume `buf` is a UnifiedBuffer<f32> and `dev`/`stream` are valid.
21//! // let hints = ManagedMemoryHints::from_unified(&buf);
22//! // hints.set_read_mostly(&dev)?;
23//! // hints.prefetch_to(&dev, &stream)?;
24//! # Ok::<(), oxicuda_driver::error::CudaError>(())
25//! ```
26
27use oxicuda_driver::device::Device;
28use oxicuda_driver::error::{CudaError, CudaResult};
29use oxicuda_driver::stream::Stream;
30
31use crate::memory_info::{MemAdvice, mem_advise, mem_advise_host, mem_prefetch};
32use crate::unified::UnifiedBuffer;
33
34// ---------------------------------------------------------------------------
35// MigrationPolicy
36// ---------------------------------------------------------------------------
37
38/// Declarative migration policy for unified memory regions.
39///
40/// Each variant encodes a common access pattern that can be translated into
41/// one or more [`MemAdvice`] hints via [`to_advice_pairs`](MigrationPolicy::to_advice_pairs).
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum MigrationPolicy {
44 /// No special migration policy. Uses CUDA defaults.
45 Default,
46 /// Mark the region as read-mostly, enabling read-replica creation on
47 /// accessing devices to reduce migration overhead.
48 ReadMostly,
49 /// Prefer that the data resides on the device with the given ordinal.
50 PreferDevice(i32),
51 /// Prefer that the data resides in host (CPU) memory.
52 PreferHost,
53}
54
55impl MigrationPolicy {
56 /// Converts this policy into the corresponding [`MemAdvice`] values
57 /// that should be applied.
58 ///
59 /// For [`Default`](MigrationPolicy::Default) the returned vec is empty
60 /// (no advice to set). For compound policies the vec contains all advice
61 /// hints that need to be issued.
62 pub fn to_advice_pairs(&self) -> Vec<MemAdvice> {
63 match self {
64 Self::Default => Vec::new(),
65 Self::ReadMostly => vec![MemAdvice::SetReadMostly],
66 Self::PreferDevice(_) => vec![MemAdvice::SetPreferredLocation],
67 Self::PreferHost => vec![MemAdvice::SetPreferredLocation],
68 }
69 }
70
71 /// Returns whether this is the [`Default`](MigrationPolicy::Default) variant.
72 #[inline]
73 pub fn is_default(&self) -> bool {
74 matches!(self, Self::Default)
75 }
76}
77
78impl std::fmt::Display for MigrationPolicy {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 Self::Default => write!(f, "MigrationPolicy::Default"),
82 Self::ReadMostly => write!(f, "MigrationPolicy::ReadMostly"),
83 Self::PreferDevice(ord) => write!(f, "MigrationPolicy::PreferDevice({ord})"),
84 Self::PreferHost => write!(f, "MigrationPolicy::PreferHost"),
85 }
86 }
87}
88
89// ---------------------------------------------------------------------------
90// ManagedMemoryHints
91// ---------------------------------------------------------------------------
92
93/// Builder-style API for applying memory hints to a unified memory region.
94///
95/// Wraps a raw pointer + byte size and exposes methods that issue the
96/// appropriate [`mem_advise`] / [`mem_prefetch`] driver calls.
97///
98/// # Construction
99///
100/// Use [`for_buffer`](Self::for_buffer) for raw pointers or
101/// [`from_unified`](Self::from_unified) for [`UnifiedBuffer`] references.
102#[derive(Debug, Clone)]
103pub struct ManagedMemoryHints {
104 /// Device pointer to the start of the managed region.
105 ptr: u64,
106 /// Total size of the region in bytes.
107 byte_size: usize,
108}
109
110impl ManagedMemoryHints {
111 /// Creates a `ManagedMemoryHints` from a raw device pointer and byte size.
112 ///
113 /// # Errors
114 ///
115 /// Returns [`CudaError::InvalidValue`] if `byte_size` is zero.
116 pub fn for_buffer(ptr: u64, byte_size: usize) -> CudaResult<Self> {
117 if byte_size == 0 {
118 return Err(CudaError::InvalidValue);
119 }
120 Ok(Self { ptr, byte_size })
121 }
122
123 /// Creates a `ManagedMemoryHints` from a [`UnifiedBuffer`] reference.
124 ///
125 /// The pointer and byte size are extracted from the buffer.
126 ///
127 /// # Errors
128 ///
129 /// Returns [`CudaError::InvalidValue`] if the buffer reports zero bytes
130 /// (should not happen for a validly constructed buffer).
131 pub fn from_unified<T: Copy>(buf: &UnifiedBuffer<T>) -> CudaResult<Self> {
132 Self::for_buffer(buf.as_device_ptr(), buf.byte_size())
133 }
134
135 /// Returns the device pointer this hint set targets.
136 #[inline]
137 pub fn ptr(&self) -> u64 {
138 self.ptr
139 }
140
141 /// Returns the byte size of the targeted region.
142 #[inline]
143 pub fn byte_size(&self) -> usize {
144 self.byte_size
145 }
146
147 // -- Individual advice methods ------------------------------------------
148
149 /// Marks the region as read-mostly on `device`, enabling read replicas.
150 pub fn set_read_mostly(&self, device: &Device) -> CudaResult<()> {
151 mem_advise(self.ptr, self.byte_size, MemAdvice::SetReadMostly, device)
152 }
153
154 /// Removes the read-mostly hint for `device`.
155 pub fn unset_read_mostly(&self, device: &Device) -> CudaResult<()> {
156 mem_advise(self.ptr, self.byte_size, MemAdvice::UnsetReadMostly, device)
157 }
158
159 /// Sets the preferred location to `device` for this region.
160 pub fn set_preferred_location(&self, device: &Device) -> CudaResult<()> {
161 mem_advise(
162 self.ptr,
163 self.byte_size,
164 MemAdvice::SetPreferredLocation,
165 device,
166 )
167 }
168
169 /// Removes the preferred-location hint for `device`.
170 pub fn unset_preferred_location(&self, device: &Device) -> CudaResult<()> {
171 mem_advise(
172 self.ptr,
173 self.byte_size,
174 MemAdvice::UnsetPreferredLocation,
175 device,
176 )
177 }
178
179 /// Indicates that `device` will access this memory region.
180 pub fn set_accessed_by(&self, device: &Device) -> CudaResult<()> {
181 mem_advise(self.ptr, self.byte_size, MemAdvice::SetAccessedBy, device)
182 }
183
184 /// Removes the accessed-by hint for `device`.
185 pub fn unset_accessed_by(&self, device: &Device) -> CudaResult<()> {
186 mem_advise(self.ptr, self.byte_size, MemAdvice::UnsetAccessedBy, device)
187 }
188
189 // -- Prefetch methods ---------------------------------------------------
190
191 /// Prefetches the entire region to `device` on `stream`.
192 pub fn prefetch_to(&self, device: &Device, stream: &Stream) -> CudaResult<()> {
193 mem_prefetch(self.ptr, self.byte_size, device, stream)
194 }
195
196 /// Prefetches a sub-range of the region to `device`.
197 ///
198 /// # Parameters
199 ///
200 /// * `offset_bytes` — byte offset from the start of the region.
201 /// * `count_bytes` — number of bytes to prefetch.
202 ///
203 /// # Errors
204 ///
205 /// Returns [`CudaError::InvalidValue`] if the range
206 /// `[offset_bytes, offset_bytes + count_bytes)` exceeds the buffer, or
207 /// if `count_bytes` is zero.
208 pub fn prefetch_range(
209 &self,
210 offset_bytes: usize,
211 count_bytes: usize,
212 device: &Device,
213 stream: &Stream,
214 ) -> CudaResult<()> {
215 if count_bytes == 0 {
216 return Err(CudaError::InvalidValue);
217 }
218 let end = offset_bytes
219 .checked_add(count_bytes)
220 .ok_or(CudaError::InvalidValue)?;
221 if end > self.byte_size {
222 return Err(CudaError::InvalidValue);
223 }
224 let range_ptr = self
225 .ptr
226 .checked_add(offset_bytes as u64)
227 .ok_or(CudaError::InvalidValue)?;
228 mem_prefetch(range_ptr, count_bytes, device, stream)
229 }
230
231 // -- Policy convenience -------------------------------------------------
232
233 /// Applies a [`MigrationPolicy`] to this memory region.
234 ///
235 /// For [`MigrationPolicy::Default`] this is a no-op.
236 /// For other variants the corresponding advice hint(s) are issued.
237 pub fn apply_policy(&self, policy: &MigrationPolicy, device: &Device) -> CudaResult<()> {
238 apply_migration_policy(self.ptr, self.byte_size, policy, device)
239 }
240}
241
242// ---------------------------------------------------------------------------
243// PrefetchPlan
244// ---------------------------------------------------------------------------
245
246/// An entry in a [`PrefetchPlan`] recording a single prefetch operation.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct PrefetchEntry {
249 /// Device pointer to the start of the region.
250 pub ptr: u64,
251 /// Size of the region in bytes.
252 pub byte_size: usize,
253 /// Target device ordinal.
254 pub device_ordinal: i32,
255}
256
257/// Batch multiple prefetch operations into a single plan.
258///
259/// Operations are recorded first, then executed together on a single stream
260/// via [`execute`](PrefetchPlan::execute).
261///
262/// # Example
263///
264/// ```rust,no_run
265/// # use oxicuda_memory::managed_hints::PrefetchPlan;
266/// # use oxicuda_driver::stream::Stream;
267/// let mut plan = PrefetchPlan::new();
268/// plan.add(0x1000, 4096, 0)
269/// .add(0x2000, 8192, 0);
270/// assert_eq!(plan.len(), 2);
271/// // plan.execute(&stream)?;
272/// # Ok::<(), oxicuda_driver::error::CudaError>(())
273/// ```
274#[derive(Debug, Clone)]
275pub struct PrefetchPlan {
276 entries: Vec<PrefetchEntry>,
277}
278
279impl PrefetchPlan {
280 /// Creates an empty prefetch plan.
281 pub fn new() -> Self {
282 Self {
283 entries: Vec::new(),
284 }
285 }
286
287 /// Records a prefetch operation.
288 ///
289 /// The actual prefetch is deferred until [`execute`](Self::execute).
290 pub fn add(&mut self, ptr: u64, byte_size: usize, device_ordinal: i32) -> &mut Self {
291 self.entries.push(PrefetchEntry {
292 ptr,
293 byte_size,
294 device_ordinal,
295 });
296 self
297 }
298
299 /// Returns the number of recorded prefetch operations.
300 #[inline]
301 pub fn len(&self) -> usize {
302 self.entries.len()
303 }
304
305 /// Returns `true` if no operations have been recorded.
306 #[inline]
307 pub fn is_empty(&self) -> bool {
308 self.entries.is_empty()
309 }
310
311 /// Returns a slice of all recorded entries.
312 #[inline]
313 pub fn entries(&self) -> &[PrefetchEntry] {
314 &self.entries
315 }
316
317 /// Executes all recorded prefetch operations on `stream`.
318 ///
319 /// Each entry is issued as a separate `mem_prefetch` call targeting the
320 /// device identified by the entry's `device_ordinal`. Operations are
321 /// enqueued in the order they were added.
322 ///
323 /// # Errors
324 ///
325 /// Returns the first error encountered. Entries before the failing one
326 /// will already have been enqueued.
327 pub fn execute(&self, stream: &Stream) -> CudaResult<()> {
328 for entry in &self.entries {
329 let device = Device::get(entry.device_ordinal)?;
330 mem_prefetch(entry.ptr, entry.byte_size, &device, stream)?;
331 }
332 Ok(())
333 }
334}
335
336impl Default for PrefetchPlan {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342// ---------------------------------------------------------------------------
343// Free function convenience
344// ---------------------------------------------------------------------------
345
346/// Applies a [`MigrationPolicy`] to a raw unified memory region.
347///
348/// This is a convenience function that translates the high-level policy into
349/// the appropriate [`mem_advise`] calls.
350///
351/// # Parameters
352///
353/// * `ptr` — device pointer to the managed allocation.
354/// * `byte_size` — size of the region in bytes.
355/// * `policy` — the migration policy to apply.
356/// * `device` — the device to which hints are directed for
357/// [`MigrationPolicy::ReadMostly`]. Ignored by
358/// [`MigrationPolicy::PreferHost`] (which always targets the CPU) and by
359/// [`MigrationPolicy::PreferDevice`] (which resolves its own ordinal via
360/// [`Device::get`] instead, so the advice always targets the ordinal the
361/// caller declared in the policy rather than whatever `device` happens to
362/// be).
363///
364/// # Errors
365///
366/// Forwards any error from the underlying driver call, including
367/// [`CudaError::InvalidDevice`] if [`MigrationPolicy::PreferDevice`]'s
368/// ordinal does not name a valid device.
369/// Returns [`CudaError::InvalidValue`] if `byte_size` is zero (when
370/// policy is not `Default`).
371pub fn apply_migration_policy(
372 ptr: u64,
373 byte_size: usize,
374 policy: &MigrationPolicy,
375 device: &Device,
376) -> CudaResult<()> {
377 match policy {
378 MigrationPolicy::Default => Ok(()),
379 MigrationPolicy::ReadMostly => mem_advise(ptr, byte_size, MemAdvice::SetReadMostly, device),
380 MigrationPolicy::PreferDevice(ordinal) => {
381 // Honour the ordinal declared in the policy itself rather than
382 // whatever `device` the caller happened to pass in, so the
383 // advice always targets the documented device.
384 let target = Device::get(*ordinal)?;
385 mem_advise(ptr, byte_size, MemAdvice::SetPreferredLocation, &target)
386 }
387 MigrationPolicy::PreferHost => {
388 // Host-preferred data has no `Device` handle to target — issue
389 // the advice against the driver's `CU_DEVICE_CPU` sentinel
390 // directly instead of silently redirecting it at `device` (a
391 // GPU).
392 mem_advise_host(ptr, byte_size, MemAdvice::SetPreferredLocation)
393 }
394 }
395}
396
397// ---------------------------------------------------------------------------
398// Tests
399// ---------------------------------------------------------------------------
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 // -- MigrationPolicy tests ----------------------------------------------
406
407 #[test]
408 fn migration_policy_default_produces_empty_advice() {
409 let pairs = MigrationPolicy::Default.to_advice_pairs();
410 assert!(pairs.is_empty());
411 }
412
413 #[test]
414 fn migration_policy_read_mostly_advice() {
415 let pairs = MigrationPolicy::ReadMostly.to_advice_pairs();
416 assert_eq!(pairs.len(), 1);
417 assert_eq!(pairs[0], MemAdvice::SetReadMostly);
418 }
419
420 #[test]
421 fn migration_policy_prefer_device_advice() {
422 let pairs = MigrationPolicy::PreferDevice(0).to_advice_pairs();
423 assert_eq!(pairs.len(), 1);
424 assert_eq!(pairs[0], MemAdvice::SetPreferredLocation);
425 }
426
427 #[test]
428 fn migration_policy_prefer_host_advice() {
429 let pairs = MigrationPolicy::PreferHost.to_advice_pairs();
430 assert_eq!(pairs.len(), 1);
431 assert_eq!(pairs[0], MemAdvice::SetPreferredLocation);
432 }
433
434 #[test]
435 fn migration_policy_is_default() {
436 assert!(MigrationPolicy::Default.is_default());
437 assert!(!MigrationPolicy::ReadMostly.is_default());
438 assert!(!MigrationPolicy::PreferDevice(0).is_default());
439 assert!(!MigrationPolicy::PreferHost.is_default());
440 }
441
442 #[test]
443 fn migration_policy_display() {
444 let s = format!("{}", MigrationPolicy::PreferDevice(2));
445 assert!(s.contains("PreferDevice(2)"));
446
447 let s2 = format!("{}", MigrationPolicy::Default);
448 assert!(s2.contains("Default"));
449 }
450
451 // -- ManagedMemoryHints construction tests ------------------------------
452
453 #[test]
454 fn hints_for_buffer_rejects_zero_size() {
455 let result = ManagedMemoryHints::for_buffer(0x1000, 0);
456 assert!(result.is_err());
457 }
458
459 #[test]
460 fn hints_for_buffer_valid() {
461 let hints = ManagedMemoryHints::for_buffer(0x1000, 4096);
462 assert!(hints.is_ok());
463 let hints = hints.ok();
464 assert!(hints.is_some());
465 let hints = hints.map(|h| {
466 assert_eq!(h.ptr(), 0x1000);
467 assert_eq!(h.byte_size(), 4096);
468 });
469 let _ = hints;
470 }
471
472 #[test]
473 fn hints_accessors() {
474 let hints = ManagedMemoryHints::for_buffer(0xDEAD, 512);
475 if let Ok(h) = hints {
476 assert_eq!(h.ptr(), 0xDEAD);
477 assert_eq!(h.byte_size(), 512);
478 }
479 }
480
481 // -- PrefetchPlan tests -------------------------------------------------
482
483 #[test]
484 fn prefetch_plan_new_is_empty() {
485 let plan = PrefetchPlan::new();
486 assert!(plan.is_empty());
487 assert_eq!(plan.len(), 0);
488 }
489
490 #[test]
491 fn prefetch_plan_default_is_empty() {
492 let plan = PrefetchPlan::default();
493 assert!(plan.is_empty());
494 }
495
496 #[test]
497 fn prefetch_plan_add_and_len() {
498 let mut plan = PrefetchPlan::new();
499 plan.add(0x1000, 4096, 0).add(0x2000, 8192, 1);
500 assert_eq!(plan.len(), 2);
501 assert!(!plan.is_empty());
502
503 let entries = plan.entries();
504 assert_eq!(entries[0].ptr, 0x1000);
505 assert_eq!(entries[0].byte_size, 4096);
506 assert_eq!(entries[0].device_ordinal, 0);
507 assert_eq!(entries[1].ptr, 0x2000);
508 assert_eq!(entries[1].byte_size, 8192);
509 assert_eq!(entries[1].device_ordinal, 1);
510 }
511
512 #[test]
513 fn prefetch_plan_chaining() {
514 let mut plan = PrefetchPlan::new();
515 plan.add(0x100, 100, 0)
516 .add(0x200, 200, 0)
517 .add(0x300, 300, 0);
518 assert_eq!(plan.len(), 3);
519 }
520
521 // -- prefetch_range validation tests ------------------------------------
522
523 #[test]
524 fn prefetch_range_rejects_zero_count() {
525 // We need a Device and Stream for prefetch_range, but we can test
526 // the zero-count path only if Device::get succeeds.
527 if let Ok(dev) = Device::get(0) {
528 // We cannot construct a Stream without a context, so just verify
529 // the hints struct validates before calling the driver.
530 let hints = ManagedMemoryHints::for_buffer(0x1000, 4096);
531 // The zero-count check happens in prefetch_range before the
532 // driver call, so we test the function signature compiles.
533 let _ = (hints, dev);
534 }
535 // Compile-time signature check
536 let _: fn(&ManagedMemoryHints, usize, usize, &Device, &Stream) -> CudaResult<()> =
537 ManagedMemoryHints::prefetch_range;
538 }
539
540 #[test]
541 fn prefetch_range_out_of_bounds_detected() {
542 // Verify the bounds checking logic without needing a GPU.
543 // We replicate the internal check manually.
544 let byte_size: usize = 4096;
545 let offset: usize = 4000;
546 let count: usize = 200;
547 let end = offset.checked_add(count);
548 assert!(end.is_some());
549 let end = end.map(|e| e > byte_size);
550 // 4000 + 200 = 4200 > 4096
551 assert_eq!(end, Some(true));
552 }
553
554 // -- apply_migration_policy tests ---------------------------------------
555
556 #[test]
557 fn apply_policy_default_is_noop() {
558 // Default policy should return Ok without calling the driver.
559 let fake_dev: Device = unsafe { std::mem::zeroed() };
560 let result = apply_migration_policy(0x1000, 4096, &MigrationPolicy::Default, &fake_dev);
561 assert!(result.is_ok());
562 }
563
564 // -- Compile-time signature checks for GPU-requiring functions ----------
565
566 #[test]
567 fn signature_set_read_mostly() {
568 let _: fn(&ManagedMemoryHints, &Device) -> CudaResult<()> =
569 ManagedMemoryHints::set_read_mostly;
570 }
571
572 #[test]
573 fn signature_unset_read_mostly() {
574 let _: fn(&ManagedMemoryHints, &Device) -> CudaResult<()> =
575 ManagedMemoryHints::unset_read_mostly;
576 }
577
578 #[test]
579 fn signature_prefetch_to() {
580 let _: fn(&ManagedMemoryHints, &Device, &Stream) -> CudaResult<()> =
581 ManagedMemoryHints::prefetch_to;
582 }
583
584 #[test]
585 fn signature_apply_policy() {
586 let _: fn(&ManagedMemoryHints, &MigrationPolicy, &Device) -> CudaResult<()> =
587 ManagedMemoryHints::apply_policy;
588 }
589
590 #[test]
591 fn signature_execute_plan() {
592 let _: fn(&PrefetchPlan, &Stream) -> CudaResult<()> = PrefetchPlan::execute;
593 }
594
595 // -- Regression tests for F072 -------------------------------------------
596
597 #[cfg(feature = "gpu-tests")]
598 mod gpu_tests {
599 use super::*;
600 use crate::unified::UnifiedBuffer;
601
602 /// Establishes a real CUDA context on device 0. Returns `None` if no
603 /// driver/GPU is available so tests can skip gracefully.
604 fn real_context() -> Option<oxicuda_driver::context::Context> {
605 if oxicuda_driver::init().is_err() || Device::count().unwrap_or(0) == 0 {
606 return None;
607 }
608 let dev = Device::get(0).ok()?;
609 oxicuda_driver::context::Context::new(&dev).ok()
610 }
611
612 /// `MigrationPolicy::PreferHost` must direct the advice at the
613 /// driver's `CU_DEVICE_CPU` sentinel, not at whatever `Device` the
614 /// caller happens to pass in — so this must succeed on a real
615 /// managed allocation even though the passed-in device is
616 /// unrelated to "host memory".
617 #[test]
618 fn prefer_host_advises_cpu_not_passed_device() {
619 let Some(_ctx) = real_context() else {
620 eprintln!("skipping: no CUDA driver/device");
621 return;
622 };
623 let Ok(buf) = UnifiedBuffer::<f32>::alloc(256) else {
624 eprintln!("skipping: managed alloc failed");
625 return;
626 };
627 let Ok(dev) = Device::get(0) else {
628 eprintln!("skipping: no device 0");
629 return;
630 };
631 let result = apply_migration_policy(
632 buf.as_device_ptr(),
633 buf.byte_size(),
634 &MigrationPolicy::PreferHost,
635 &dev,
636 );
637 assert!(result.is_ok(), "PreferHost advice failed: {result:?}");
638 }
639
640 /// `MigrationPolicy::PreferDevice(ordinal)` must resolve and honour
641 /// its own declared ordinal rather than silently using whichever
642 /// `Device` the caller passed in: requesting an out-of-range
643 /// ordinal must fail even when a valid `device` argument is
644 /// supplied.
645 #[test]
646 fn prefer_device_honors_its_own_ordinal_not_passed_device() {
647 let Some(_ctx) = real_context() else {
648 eprintln!("skipping: no CUDA driver/device");
649 return;
650 };
651 let Ok(buf) = UnifiedBuffer::<f32>::alloc(256) else {
652 eprintln!("skipping: managed alloc failed");
653 return;
654 };
655 let Ok(dev0) = Device::get(0) else {
656 eprintln!("skipping: no device 0");
657 return;
658 };
659
660 // A valid ordinal (0) must succeed even though `dev0` is passed
661 // as the (now-ignored for this variant) `device` argument.
662 let ok_result = apply_migration_policy(
663 buf.as_device_ptr(),
664 buf.byte_size(),
665 &MigrationPolicy::PreferDevice(0),
666 &dev0,
667 );
668 assert!(ok_result.is_ok(), "PreferDevice(0) failed: {ok_result:?}");
669
670 // An out-of-range ordinal must fail — proving the policy's own
671 // ordinal is actually being resolved and used, not silently
672 // ignored in favour of `dev0`.
673 let bad_result = apply_migration_policy(
674 buf.as_device_ptr(),
675 buf.byte_size(),
676 &MigrationPolicy::PreferDevice(9999),
677 &dev0,
678 );
679 assert!(
680 bad_result.is_err(),
681 "PreferDevice(9999) unexpectedly succeeded; ordinal was not honored"
682 );
683 }
684 }
685}