Skip to main content

oxicuda_levelzero/
usm.rs

1//! Unified Shared Memory (USM) allocation logic for Intel Level Zero.
2//!
3//! Level Zero exposes three flavours of *Unified Shared Memory*:
4//!
5//! - **Device USM** (`zeMemAllocDevice`) — resident in device-local memory,
6//!   not directly CPU-accessible; fastest for kernels.
7//! - **Host USM** (`zeMemAllocHost`) — resident in host memory, accessible by
8//!   both CPU and GPU (the GPU reads it across the bus).
9//! - **Shared USM** (`zeMemAllocShared`) — migratable between host and device;
10//!   the driver moves pages on demand.
11//!
12//! The *device-gated* part is the raw `zeMemAlloc*` call (which lives in
13//! [`crate::memory`]). Everything in this module is **host-side bookkeeping**:
14//! the suballocation arithmetic, alignment rounding, memory-ordinal selection
15//! from a queried property table, and the memory-advise model. All of it is
16//! CPU-testable without a physical Intel GPU.
17//!
18//! A backend allocates a few large `zeMemAlloc*` *blocks* and sub-allocates
19//! individual tensors out of them via [`UsmSuballocator`], returning a byte
20//! *offset* into the block. The caller then pointer-offsets the block base —
21//! that pointer arithmetic is the only step needing a real allocation.
22
23use crate::error::{LevelZeroError, LevelZeroResult};
24
25// ─── USM memory kinds ────────────────────────────────────────
26
27/// The kind of Unified Shared Memory backing an allocation.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum UsmKind {
30    /// Device-local memory (`zeMemAllocDevice`). Not CPU-accessible.
31    Device,
32    /// Host memory (`zeMemAllocHost`). CPU + GPU accessible.
33    Host,
34    /// Migratable shared memory (`zeMemAllocShared`).
35    Shared,
36}
37
38impl UsmKind {
39    /// Returns `true` when this kind is directly readable/writable by the CPU.
40    #[must_use]
41    pub fn is_host_accessible(self) -> bool {
42        matches!(self, UsmKind::Host | UsmKind::Shared)
43    }
44
45    /// The `ze_structure_type_t` value of the corresponding alloc descriptor.
46    ///
47    /// Device → `ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC` (0x15),
48    /// Host → `ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC` (0x16). Shared uses the
49    /// device descriptor with a chained host descriptor in `pNext`.
50    #[must_use]
51    pub fn alloc_desc_stype(self) -> u32 {
52        match self {
53            UsmKind::Device | UsmKind::Shared => 0x15,
54            UsmKind::Host => 0x16,
55        }
56    }
57}
58
59/// Minimum alignment Level Zero guarantees for any USM allocation (bytes).
60///
61/// The L0 spec mandates allocations are at least 8-byte aligned; intel-compute
62/// -runtime returns 64-byte-aligned pointers in practice. We default to 64 so
63/// SIMD16 float loads never straddle a cache line.
64pub const USM_DEFAULT_ALIGNMENT: u64 = 64;
65
66/// Round `value` up to the nearest multiple of `alignment` (a power of two).
67#[must_use]
68pub fn align_up(value: u64, alignment: u64) -> u64 {
69    debug_assert!(alignment.is_power_of_two());
70    (value + alignment - 1) & !(alignment - 1)
71}
72
73// ─── Memory ordinal / property-table selection ───────────────
74
75/// A single entry of `zeDeviceGetMemoryProperties` output.
76///
77/// Each Intel GPU reports one or more memory ordinals (e.g. HBM on Ponte
78/// Vecchio, GDDR6 on Arc, system DRAM on Xe-LP). The backend picks an ordinal
79/// for device allocations based on this table.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct MemoryOrdinalInfo {
82    /// The ordinal index passed to `zeMemAllocDevice`'s descriptor.
83    pub ordinal: u32,
84    /// Human-readable memory name reported by the driver (e.g. `"HBM"`).
85    pub name: String,
86    /// Total bytes of this memory pool.
87    pub total_bytes: u64,
88    /// Peak bandwidth in bytes/sec (0 when the driver does not report it).
89    pub max_bandwidth: u64,
90}
91
92/// A queried memory-property table for one device.
93#[derive(Debug, Clone, Default)]
94pub struct MemoryPropertyTable {
95    ordinals: Vec<MemoryOrdinalInfo>,
96}
97
98impl MemoryPropertyTable {
99    /// Build a table from a list of ordinal records.
100    #[must_use]
101    pub fn new(ordinals: Vec<MemoryOrdinalInfo>) -> Self {
102        Self { ordinals }
103    }
104
105    /// Number of distinct memory ordinals.
106    #[must_use]
107    pub fn len(&self) -> usize {
108        self.ordinals.len()
109    }
110
111    /// Returns `true` when the table has no ordinals.
112    #[must_use]
113    pub fn is_empty(&self) -> bool {
114        self.ordinals.is_empty()
115    }
116
117    /// All ordinals, in driver-reported order.
118    #[must_use]
119    pub fn ordinals(&self) -> &[MemoryOrdinalInfo] {
120        &self.ordinals
121    }
122
123    /// Select the device-memory ordinal best suited for a `bytes`-sized
124    /// allocation.
125    ///
126    /// Strategy: among ordinals with enough capacity, prefer the one with the
127    /// highest reported bandwidth (ties broken by larger capacity, then lower
128    /// ordinal index). This routes large tensors to HBM on Ponte Vecchio while
129    /// falling back to whatever fits on memory-constrained parts.
130    ///
131    /// Returns [`LevelZeroError::OutOfMemory`] when no ordinal can hold `bytes`,
132    /// or [`LevelZeroError::NoSuitableDevice`] when the table is empty.
133    pub fn select_device_ordinal(&self, bytes: u64) -> LevelZeroResult<u32> {
134        if self.ordinals.is_empty() {
135            return Err(LevelZeroError::NoSuitableDevice);
136        }
137        let best = self
138            .ordinals
139            .iter()
140            .filter(|o| o.total_bytes >= bytes)
141            .max_by(|a, b| {
142                a.max_bandwidth
143                    .cmp(&b.max_bandwidth)
144                    .then(a.total_bytes.cmp(&b.total_bytes))
145                    .then(b.ordinal.cmp(&a.ordinal))
146            });
147        match best {
148            Some(info) => Ok(info.ordinal),
149            None => Err(LevelZeroError::OutOfMemory),
150        }
151    }
152
153    /// Total capacity across all reported memory ordinals.
154    #[must_use]
155    pub fn total_capacity(&self) -> u64 {
156        self.ordinals.iter().map(|o| o.total_bytes).sum()
157    }
158}
159
160// ─── Memory advise model ─────────────────────────────────────
161
162/// Memory-advise hints applied to a shared/device USM range.
163///
164/// These mirror `ze_memory_advice_t`. The hints are pure metadata that the
165/// driver uses to steer page migration for shared USM; we model the *intent*
166/// here so a planner can validate and accumulate hints before issuing the
167/// device-gated `zeCommandListAppendMemAdvise`.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum MemoryAdvice {
170    /// `ZE_MEMORY_ADVICE_SET_READ_MOSTLY` — pages are read far more than written.
171    SetReadMostly,
172    /// `ZE_MEMORY_ADVICE_CLEAR_READ_MOSTLY`.
173    ClearReadMostly,
174    /// `ZE_MEMORY_ADVICE_SET_PREFERRED_LOCATION` — pin to the device.
175    SetPreferredLocation,
176    /// `ZE_MEMORY_ADVICE_CLEAR_PREFERRED_LOCATION`.
177    ClearPreferredLocation,
178    /// `ZE_MEMORY_ADVICE_SET_ACCESSED_BY_DEVICE`.
179    SetAccessedByDevice,
180    /// `ZE_MEMORY_ADVICE_CLEAR_ACCESSED_BY_DEVICE`.
181    ClearAccessedByDevice,
182    /// `ZE_MEMORY_ADVICE_BIAS_CACHED`.
183    BiasCached,
184    /// `ZE_MEMORY_ADVICE_BIAS_UNCACHED`.
185    BiasUncached,
186}
187
188impl MemoryAdvice {
189    /// The `ze_memory_advice_t` enum value.
190    #[must_use]
191    pub fn ze_value(self) -> u32 {
192        match self {
193            MemoryAdvice::SetReadMostly => 0,
194            MemoryAdvice::ClearReadMostly => 1,
195            MemoryAdvice::SetPreferredLocation => 2,
196            MemoryAdvice::ClearPreferredLocation => 3,
197            MemoryAdvice::SetAccessedByDevice => 4,
198            MemoryAdvice::ClearAccessedByDevice => 5,
199            MemoryAdvice::BiasCached => 6,
200            MemoryAdvice::BiasUncached => 7,
201        }
202    }
203
204    /// The advice that *clears* this one, if it is a set/clear pair.
205    #[must_use]
206    pub fn clearing_advice(self) -> Option<MemoryAdvice> {
207        match self {
208            MemoryAdvice::SetReadMostly => Some(MemoryAdvice::ClearReadMostly),
209            MemoryAdvice::SetPreferredLocation => Some(MemoryAdvice::ClearPreferredLocation),
210            MemoryAdvice::SetAccessedByDevice => Some(MemoryAdvice::ClearAccessedByDevice),
211            _ => None,
212        }
213    }
214}
215
216/// Accumulates memory-advise hints for a single USM range.
217///
218/// Applying a *set* hint records it; applying the matching *clear* hint removes
219/// it. `BiasCached` / `BiasUncached` are mutually exclusive — the most recent
220/// wins. This lets a planner fold a sequence of advise calls into the minimal
221/// effective set before touching the driver.
222#[derive(Debug, Clone, Default)]
223pub struct MemoryAdviseState {
224    read_mostly: bool,
225    preferred_location: bool,
226    accessed_by_device: bool,
227    /// `Some(true)` = cached bias, `Some(false)` = uncached, `None` = default.
228    bias_cached: Option<bool>,
229}
230
231impl MemoryAdviseState {
232    /// A fresh state with no advice applied.
233    #[must_use]
234    pub fn new() -> Self {
235        Self::default()
236    }
237
238    /// Apply one advice hint, folding it into the effective state.
239    pub fn apply(&mut self, advice: MemoryAdvice) {
240        match advice {
241            MemoryAdvice::SetReadMostly => self.read_mostly = true,
242            MemoryAdvice::ClearReadMostly => self.read_mostly = false,
243            MemoryAdvice::SetPreferredLocation => self.preferred_location = true,
244            MemoryAdvice::ClearPreferredLocation => self.preferred_location = false,
245            MemoryAdvice::SetAccessedByDevice => self.accessed_by_device = true,
246            MemoryAdvice::ClearAccessedByDevice => self.accessed_by_device = false,
247            MemoryAdvice::BiasCached => self.bias_cached = Some(true),
248            MemoryAdvice::BiasUncached => self.bias_cached = Some(false),
249        }
250    }
251
252    /// `true` when `SetReadMostly` is currently in effect.
253    #[must_use]
254    pub fn is_read_mostly(&self) -> bool {
255        self.read_mostly
256    }
257
258    /// `true` when the range is pinned to its preferred device location.
259    #[must_use]
260    pub fn is_preferred_location(&self) -> bool {
261        self.preferred_location
262    }
263
264    /// `true` when the device is marked as an accessor of this range.
265    #[must_use]
266    pub fn is_accessed_by_device(&self) -> bool {
267        self.accessed_by_device
268    }
269
270    /// The cache bias: `Some(true)` cached, `Some(false)` uncached, `None`
271    /// driver-default.
272    #[must_use]
273    pub fn cache_bias(&self) -> Option<bool> {
274        self.bias_cached
275    }
276
277    /// The minimal sequence of advise calls that reproduces this state from a
278    /// freshly-allocated range. Used to replay folded hints to the driver.
279    #[must_use]
280    pub fn effective_advice(&self) -> Vec<MemoryAdvice> {
281        let mut out = Vec::new();
282        if self.read_mostly {
283            out.push(MemoryAdvice::SetReadMostly);
284        }
285        if self.preferred_location {
286            out.push(MemoryAdvice::SetPreferredLocation);
287        }
288        if self.accessed_by_device {
289            out.push(MemoryAdvice::SetAccessedByDevice);
290        }
291        match self.bias_cached {
292            Some(true) => out.push(MemoryAdvice::BiasCached),
293            Some(false) => out.push(MemoryAdvice::BiasUncached),
294            None => {}
295        }
296        out
297    }
298}
299
300// ─── USM sub-allocator ───────────────────────────────────────
301
302/// A sub-allocation carved out of a USM block.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub struct UsmSubAllocation {
305    /// Byte offset of the allocation within the backing block.
306    pub offset: u64,
307    /// Requested usable size (not the alignment-padded size).
308    pub size: u64,
309}
310
311/// One contiguous free span within the backing USM block.
312#[derive(Debug, Clone, Copy)]
313struct FreeSpan {
314    offset: u64,
315    size: u64,
316}
317
318/// First-fit free-list sub-allocator over a single USM block.
319///
320/// Mirrors the device-memory-allocator pattern: tracks free spans, splits on
321/// allocation, coalesces adjacent spans on free, and honours arbitrary
322/// power-of-two alignment (accounting for the introduced leading padding).
323///
324/// The [`UsmKind`] is carried so a backend can keep separate suballocators per
325/// memory flavour and never mix host and device offsets.
326#[derive(Debug)]
327pub struct UsmSuballocator {
328    kind: UsmKind,
329    block_size: u64,
330    free: Vec<FreeSpan>,
331    /// Returned-offset → carved span `(offset, size)` (including padding) so
332    /// `free` restores the original region exactly.
333    live: Vec<(u64, FreeSpan)>,
334}
335
336impl UsmSuballocator {
337    /// Create a suballocator managing `block_size` bytes of `kind` memory.
338    pub fn new(kind: UsmKind, block_size: u64) -> LevelZeroResult<Self> {
339        if block_size == 0 {
340            return Err(LevelZeroError::InvalidArgument(
341                "USM block_size must be > 0".into(),
342            ));
343        }
344        Ok(Self {
345            kind,
346            block_size,
347            free: vec![FreeSpan {
348                offset: 0,
349                size: block_size,
350            }],
351            live: Vec::new(),
352        })
353    }
354
355    /// The USM flavour this suballocator manages.
356    #[must_use]
357    pub fn kind(&self) -> UsmKind {
358        self.kind
359    }
360
361    /// Total managed size in bytes.
362    #[must_use]
363    pub fn block_size(&self) -> u64 {
364        self.block_size
365    }
366
367    /// Sum of all currently-free bytes (possibly fragmented).
368    #[must_use]
369    pub fn free_bytes(&self) -> u64 {
370        self.free.iter().map(|s| s.size).sum()
371    }
372
373    /// The largest single contiguous free span.
374    #[must_use]
375    pub fn largest_free_span(&self) -> u64 {
376        self.free.iter().map(|s| s.size).max().unwrap_or(0)
377    }
378
379    /// Number of live allocations.
380    #[must_use]
381    pub fn live_count(&self) -> usize {
382        self.live.len()
383    }
384
385    /// Allocate `size` bytes aligned to `alignment` (a power of two).
386    pub fn alloc(&mut self, size: u64, alignment: u64) -> LevelZeroResult<UsmSubAllocation> {
387        if size == 0 {
388            return Err(LevelZeroError::InvalidArgument(
389                "USM suballocation size must be > 0".into(),
390            ));
391        }
392        if !alignment.is_power_of_two() {
393            return Err(LevelZeroError::InvalidArgument(format!(
394                "alignment {alignment} must be a power of two"
395            )));
396        }
397
398        let mut chosen: Option<usize> = None;
399        let mut aligned_off = 0u64;
400        for (i, span) in self.free.iter().enumerate() {
401            let a = align_up(span.offset, alignment);
402            let pad = a - span.offset;
403            if pad + size <= span.size {
404                chosen = Some(i);
405                aligned_off = a;
406                break;
407            }
408        }
409        let Some(idx) = chosen else {
410            return Err(LevelZeroError::OutOfMemory);
411        };
412
413        let span = self.free[idx];
414        let pad = aligned_off - span.offset;
415        let carved = FreeSpan {
416            offset: span.offset,
417            size: pad + size,
418        };
419
420        let remaining_off = carved.offset + carved.size;
421        let remaining_size = span.size - carved.size;
422        if remaining_size == 0 {
423            self.free.remove(idx);
424        } else {
425            self.free[idx] = FreeSpan {
426                offset: remaining_off,
427                size: remaining_size,
428            };
429        }
430
431        self.live.push((aligned_off, carved));
432        Ok(UsmSubAllocation {
433            offset: aligned_off,
434            size,
435        })
436    }
437
438    /// Allocate `size` bytes using the default USM alignment.
439    pub fn alloc_default(&mut self, size: u64) -> LevelZeroResult<UsmSubAllocation> {
440        self.alloc(size, USM_DEFAULT_ALIGNMENT)
441    }
442
443    /// Free a previously-returned allocation by its `offset`.
444    pub fn free(&mut self, offset: u64) -> LevelZeroResult<()> {
445        let pos = self
446            .live
447            .iter()
448            .position(|(o, _)| *o == offset)
449            .ok_or_else(|| {
450                LevelZeroError::InvalidArgument(format!("USM free of unknown offset {offset}"))
451            })?;
452        let (_, carved) = self.live.remove(pos);
453        self.insert_and_coalesce(carved);
454        Ok(())
455    }
456
457    /// Insert a freed span and merge with adjacent free spans.
458    fn insert_and_coalesce(&mut self, span: FreeSpan) {
459        self.free.push(span);
460        self.free.sort_by_key(|s| s.offset);
461        let mut merged: Vec<FreeSpan> = Vec::with_capacity(self.free.len());
462        for s in self.free.drain(..) {
463            if let Some(last) = merged.last_mut() {
464                if last.offset + last.size == s.offset {
465                    last.size += s.size;
466                    continue;
467                }
468            }
469            merged.push(s);
470        }
471        self.free = merged;
472    }
473}
474
475// ─── Tests ───────────────────────────────────────────────────
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn usm_kind_host_accessibility() {
483        assert!(!UsmKind::Device.is_host_accessible());
484        assert!(UsmKind::Host.is_host_accessible());
485        assert!(UsmKind::Shared.is_host_accessible());
486    }
487
488    #[test]
489    fn usm_kind_desc_stypes() {
490        assert_eq!(UsmKind::Device.alloc_desc_stype(), 0x15);
491        assert_eq!(UsmKind::Host.alloc_desc_stype(), 0x16);
492        assert_eq!(UsmKind::Shared.alloc_desc_stype(), 0x15);
493    }
494
495    #[test]
496    fn align_up_powers_of_two() {
497        assert_eq!(align_up(0, 64), 0);
498        assert_eq!(align_up(1, 64), 64);
499        assert_eq!(align_up(64, 64), 64);
500        assert_eq!(align_up(65, 64), 128);
501        assert_eq!(align_up(200, 256), 256);
502    }
503
504    #[test]
505    fn memory_table_select_prefers_bandwidth() {
506        let table = MemoryPropertyTable::new(vec![
507            MemoryOrdinalInfo {
508                ordinal: 0,
509                name: "DDR".into(),
510                total_bytes: 64 << 30,
511                max_bandwidth: 50 << 30,
512            },
513            MemoryOrdinalInfo {
514                ordinal: 1,
515                name: "HBM".into(),
516                total_bytes: 48 << 30,
517                max_bandwidth: 1200 << 30,
518            },
519        ]);
520        // HBM (ordinal 1) has much higher bandwidth and fits 1 GiB.
521        assert_eq!(table.select_device_ordinal(1 << 30).unwrap(), 1);
522        assert_eq!(table.len(), 2);
523        assert_eq!(table.total_capacity(), (64 << 30) + (48 << 30));
524    }
525
526    #[test]
527    fn memory_table_select_skips_too_small() {
528        let table = MemoryPropertyTable::new(vec![
529            MemoryOrdinalInfo {
530                ordinal: 0,
531                name: "small-fast".into(),
532                total_bytes: 1 << 20,
533                max_bandwidth: 1000 << 30,
534            },
535            MemoryOrdinalInfo {
536                ordinal: 1,
537                name: "big-slow".into(),
538                total_bytes: 16 << 30,
539                max_bandwidth: 50 << 30,
540            },
541        ]);
542        // 8 GiB only fits in the big-slow pool despite its lower bandwidth.
543        assert_eq!(table.select_device_ordinal(8 << 30).unwrap(), 1);
544    }
545
546    #[test]
547    fn memory_table_errors() {
548        let empty = MemoryPropertyTable::default();
549        assert!(empty.is_empty());
550        assert!(matches!(
551            empty.select_device_ordinal(1),
552            Err(LevelZeroError::NoSuitableDevice)
553        ));
554
555        let tiny = MemoryPropertyTable::new(vec![MemoryOrdinalInfo {
556            ordinal: 0,
557            name: "tiny".into(),
558            total_bytes: 1024,
559            max_bandwidth: 1,
560        }]);
561        assert!(matches!(
562            tiny.select_device_ordinal(1 << 30),
563            Err(LevelZeroError::OutOfMemory)
564        ));
565    }
566
567    #[test]
568    fn memory_advise_set_clear_pairing() {
569        assert_eq!(
570            MemoryAdvice::SetReadMostly.clearing_advice(),
571            Some(MemoryAdvice::ClearReadMostly)
572        );
573        assert_eq!(MemoryAdvice::BiasCached.clearing_advice(), None);
574        assert_eq!(MemoryAdvice::SetReadMostly.ze_value(), 0);
575        assert_eq!(MemoryAdvice::BiasUncached.ze_value(), 7);
576    }
577
578    #[test]
579    fn memory_advise_state_folds() {
580        let mut st = MemoryAdviseState::new();
581        st.apply(MemoryAdvice::SetReadMostly);
582        st.apply(MemoryAdvice::SetPreferredLocation);
583        st.apply(MemoryAdvice::BiasCached);
584        assert!(st.is_read_mostly());
585        assert!(st.is_preferred_location());
586        assert_eq!(st.cache_bias(), Some(true));
587
588        // Clearing read-mostly removes it; later bias overrides earlier.
589        st.apply(MemoryAdvice::ClearReadMostly);
590        st.apply(MemoryAdvice::BiasUncached);
591        assert!(!st.is_read_mostly());
592        assert_eq!(st.cache_bias(), Some(false));
593
594        let eff = st.effective_advice();
595        assert!(eff.contains(&MemoryAdvice::SetPreferredLocation));
596        assert!(eff.contains(&MemoryAdvice::BiasUncached));
597        assert!(!eff.contains(&MemoryAdvice::SetReadMostly));
598    }
599
600    #[test]
601    fn usm_suballoc_basic() {
602        let mut a = UsmSuballocator::new(UsmKind::Device, 1024).unwrap();
603        assert_eq!(a.kind(), UsmKind::Device);
604        assert_eq!(a.block_size(), 1024);
605        assert_eq!(a.free_bytes(), 1024);
606
607        let x = a.alloc(256, 64).unwrap();
608        assert_eq!(x.offset, 0);
609        assert_eq!(x.size, 256);
610        assert_eq!(a.free_bytes(), 768);
611        assert_eq!(a.live_count(), 1);
612
613        let y = a.alloc_default(128).unwrap();
614        assert_eq!(y.offset, 256);
615
616        a.free(x.offset).unwrap();
617        a.free(y.offset).unwrap();
618        assert_eq!(a.free_bytes(), 1024);
619        assert_eq!(a.largest_free_span(), 1024);
620        assert_eq!(a.live_count(), 0);
621    }
622
623    #[test]
624    fn usm_suballoc_alignment_padding() {
625        let mut a = UsmSuballocator::new(UsmKind::Shared, 4096).unwrap();
626        let head = a.alloc(10, 1).unwrap();
627        assert_eq!(head.offset, 0);
628        // Next 256-aligned alloc must skip to offset 256.
629        let aligned = a.alloc(16, 256).unwrap();
630        assert_eq!(aligned.offset, 256);
631        a.free(head.offset).unwrap();
632        a.free(aligned.offset).unwrap();
633        assert_eq!(a.free_bytes(), 4096);
634    }
635
636    #[test]
637    fn usm_suballoc_oom_and_bad_args() {
638        let mut a = UsmSuballocator::new(UsmKind::Host, 128).unwrap();
639        assert!(matches!(a.alloc(256, 1), Err(LevelZeroError::OutOfMemory)));
640        assert!(a.alloc(0, 1).is_err());
641        assert!(a.alloc(8, 3).is_err(), "non-pow2 alignment rejected");
642        assert!(a.free(999).is_err(), "unknown offset rejected");
643        assert!(UsmSuballocator::new(UsmKind::Host, 0).is_err());
644    }
645
646    #[test]
647    fn usm_suballoc_coalesce_middle_hole() {
648        let mut a = UsmSuballocator::new(UsmKind::Device, 300).unwrap();
649        let x = a.alloc(100, 1).unwrap();
650        let y = a.alloc(100, 1).unwrap();
651        let z = a.alloc(100, 1).unwrap();
652        assert_eq!(a.free_bytes(), 0);
653        a.free(x.offset).unwrap();
654        a.free(z.offset).unwrap();
655        // Two disjoint holes — must not merge across the live middle.
656        assert_eq!(a.largest_free_span(), 100);
657        a.free(y.offset).unwrap();
658        assert_eq!(a.largest_free_span(), 300);
659    }
660}