1use crate::error::{LevelZeroError, LevelZeroResult};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum UsmKind {
30 Device,
32 Host,
34 Shared,
36}
37
38impl UsmKind {
39 #[must_use]
41 pub fn is_host_accessible(self) -> bool {
42 matches!(self, UsmKind::Host | UsmKind::Shared)
43 }
44
45 #[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
59pub const USM_DEFAULT_ALIGNMENT: u64 = 64;
65
66#[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#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct MemoryOrdinalInfo {
82 pub ordinal: u32,
84 pub name: String,
86 pub total_bytes: u64,
88 pub max_bandwidth: u64,
90}
91
92#[derive(Debug, Clone, Default)]
94pub struct MemoryPropertyTable {
95 ordinals: Vec<MemoryOrdinalInfo>,
96}
97
98impl MemoryPropertyTable {
99 #[must_use]
101 pub fn new(ordinals: Vec<MemoryOrdinalInfo>) -> Self {
102 Self { ordinals }
103 }
104
105 #[must_use]
107 pub fn len(&self) -> usize {
108 self.ordinals.len()
109 }
110
111 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.ordinals.is_empty()
115 }
116
117 #[must_use]
119 pub fn ordinals(&self) -> &[MemoryOrdinalInfo] {
120 &self.ordinals
121 }
122
123 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 #[must_use]
155 pub fn total_capacity(&self) -> u64 {
156 self.ordinals.iter().map(|o| o.total_bytes).sum()
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum MemoryAdvice {
170 SetReadMostly,
172 ClearReadMostly,
174 SetPreferredLocation,
176 ClearPreferredLocation,
178 SetAccessedByDevice,
180 ClearAccessedByDevice,
182 BiasCached,
184 BiasUncached,
186}
187
188impl MemoryAdvice {
189 #[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 #[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#[derive(Debug, Clone, Default)]
223pub struct MemoryAdviseState {
224 read_mostly: bool,
225 preferred_location: bool,
226 accessed_by_device: bool,
227 bias_cached: Option<bool>,
229}
230
231impl MemoryAdviseState {
232 #[must_use]
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 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 #[must_use]
254 pub fn is_read_mostly(&self) -> bool {
255 self.read_mostly
256 }
257
258 #[must_use]
260 pub fn is_preferred_location(&self) -> bool {
261 self.preferred_location
262 }
263
264 #[must_use]
266 pub fn is_accessed_by_device(&self) -> bool {
267 self.accessed_by_device
268 }
269
270 #[must_use]
273 pub fn cache_bias(&self) -> Option<bool> {
274 self.bias_cached
275 }
276
277 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub struct UsmSubAllocation {
305 pub offset: u64,
307 pub size: u64,
309}
310
311#[derive(Debug, Clone, Copy)]
313struct FreeSpan {
314 offset: u64,
315 size: u64,
316}
317
318#[derive(Debug)]
327pub struct UsmSuballocator {
328 kind: UsmKind,
329 block_size: u64,
330 free: Vec<FreeSpan>,
331 live: Vec<(u64, FreeSpan)>,
334}
335
336impl UsmSuballocator {
337 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 #[must_use]
357 pub fn kind(&self) -> UsmKind {
358 self.kind
359 }
360
361 #[must_use]
363 pub fn block_size(&self) -> u64 {
364 self.block_size
365 }
366
367 #[must_use]
369 pub fn free_bytes(&self) -> u64 {
370 self.free.iter().map(|s| s.size).sum()
371 }
372
373 #[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 #[must_use]
381 pub fn live_count(&self) -> usize {
382 self.live.len()
383 }
384
385 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 pub fn alloc_default(&mut self, size: u64) -> LevelZeroResult<UsmSubAllocation> {
440 self.alloc(size, USM_DEFAULT_ALIGNMENT)
441 }
442
443 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 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#[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 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 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 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 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 assert_eq!(a.largest_free_span(), 100);
657 a.free(y.offset).unwrap();
658 assert_eq!(a.largest_free_span(), 300);
659 }
660}