use super::{Access, RuntimeOwner};
use crate::{Error, Result};
use std::{
ops::{Deref, DerefMut, Range},
sync::{Arc, Mutex},
};
#[derive(Clone)]
pub enum MemoryPlacement {
GpuLocal,
HostVisible,
#[cfg(feature = "npu")]
NpuLocal(crate::npu::NpuProgram),
#[cfg(feature = "npu")]
Shared(crate::npu::NpuProgram),
}
#[derive(Default)]
pub(super) struct HostState {
pub readers: usize,
pub writer: bool,
pub external: bool,
pub poison: Option<String>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Engine {
Host,
Gpu,
#[cfg_attr(not(feature = "npu"), allow(dead_code))]
Npu,
}
pub(super) struct Visibility {
pub writer: Engine,
pub visible: [bool; 3],
}
impl Visibility {
pub fn new() -> Self {
Self {
writer: Engine::Host,
visible: [true, false, false],
}
}
#[cfg(any(test, feature = "npu"))]
pub fn sync_to_device(&self, consumer: Engine) -> bool {
self.writer == Engine::Host || consumer == Engine::Npu
}
pub fn wrote(&mut self, engine: Engine) {
self.writer = engine;
self.visible = [false; 3];
self.visible[engine as usize] = true;
}
}
pub(super) struct Storage {
pub accounted: bool,
#[cfg(feature = "npu")]
pub bo: Option<crate::npu::raw::Bo>,
#[cfg(feature = "npu")]
pub descriptor: Option<std::os::fd::OwnedFd>,
pub gpu: Option<crate::gpu::Buffer>,
pub pointer: *mut u8,
pub bytes: usize,
#[cfg(feature = "npu")]
pub npu_device: Option<i32>,
#[cfg(feature = "npu")]
pub group: Option<i32>,
pub shared: bool,
pub host: Mutex<HostState>,
pub visibility: Mutex<Visibility>,
pub runtime: Arc<RuntimeOwner>,
#[cfg(test)]
pub _test_memory: Option<Arc<std::cell::UnsafeCell<[u8; 256]>>>,
pub _reservation: Option<crate::residency::MemoryReservation>,
}
unsafe impl Send for Storage {}
unsafe impl Sync for Storage {}
#[derive(Clone)]
pub struct Buffer {
pub(super) storage: Arc<Storage>,
}
#[derive(Clone)]
pub struct BufferView {
pub(super) buffer: Buffer,
pub(super) range: Range<usize>,
pub(super) retained: Vec<Arc<dyn Send + Sync>>,
}
impl Buffer {
pub fn len(&self) -> usize {
self.storage.bytes
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn view(&self) -> BufferView {
BufferView {
buffer: self.clone(),
range: 0..self.len(),
retained: Vec::new(),
}
}
pub fn slice(&self, range: Range<usize>) -> Result<BufferView> {
self.view().slice(range)
}
pub fn map_read(&self) -> Result<ReadGuard<'_>> {
self.acquire(false, true)?;
Ok(ReadGuard {
buffer: self,
range: 0..self.len(),
})
}
pub fn try_map_read(&self) -> Result<ReadGuard<'_>> {
self.acquire(false, false)?;
Ok(ReadGuard {
buffer: self,
range: 0..self.len(),
})
}
pub fn map_write(&self) -> Result<WriteGuard<'_>> {
self.acquire(true, true)?;
Ok(WriteGuard {
buffer: self,
range: 0..self.len(),
})
}
pub fn try_map_write(&self) -> Result<WriteGuard<'_>> {
self.acquire(true, false)?;
Ok(WriteGuard {
buffer: self,
range: 0..self.len(),
})
}
fn acquire(&self, write: bool, wait: bool) -> Result<()> {
self.acquire_with(write, wait, || self.storage.make_visible(Engine::Host))
}
pub(super) fn acquire_with(
&self,
write: bool,
wait: bool,
make_visible: impl FnOnce() -> Result<()>,
) -> Result<()> {
if self.storage.pointer.is_null() {
return Err(Error::Unsupported(
"GPU-local memory is not host mapped; use an explicit graph copy to shared storage"
.into(),
));
}
let core = &self.storage.runtime.core;
let view = self.view();
let mut scheduler = core.state.lock().unwrap_or_else(|e| e.into_inner());
loop {
{
let host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
if let Some(error) = &host.poison {
return Err(Error::DeviceLost(error.clone()));
}
if host.external || host.writer || (write && host.readers != 0) {
return Err(Error::Busy("conflicting host mapping".into()));
}
}
if !scheduler.conflicts(
&view,
if write {
Access::ReadWrite
} else {
Access::Read
},
) {
break;
}
if !wait {
return Err(Error::Busy("allocation is in device use".into()));
}
scheduler = core
.host_changed
.wait(scheduler)
.unwrap_or_else(|e| e.into_inner());
}
let mut host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
if write {
host.writer = true;
} else {
host.readers += 1;
}
drop(host);
drop(scheduler);
if let Err(error) = make_visible() {
let mut host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
if write {
host.writer = false;
} else {
host.readers -= 1;
}
drop(host);
core.host_changed.notify_all();
return Err(error);
}
Ok(())
}
}
impl BufferView {
pub fn is_host_visible(&self) -> bool {
!self.buffer.storage.pointer.is_null()
}
pub fn map_read(&self) -> Result<ReadGuard<'_>> {
self.buffer.acquire(false, true)?;
Ok(ReadGuard {
buffer: &self.buffer,
range: self.range.clone(),
})
}
pub fn map_write(&self) -> Result<WriteGuard<'_>> {
self.buffer.acquire(true, true)?;
Ok(WriteGuard {
buffer: &self.buffer,
range: self.range.clone(),
})
}
pub fn len(&self) -> usize {
self.range.len()
}
pub fn is_empty(&self) -> bool {
self.range.is_empty()
}
pub fn offset(&self) -> usize {
self.range.start
}
pub fn slice(&self, range: Range<usize>) -> Result<Self> {
if range.start > range.end || range.end > self.len() {
return Err(Error::Message("buffer view is out of bounds".into()));
}
Ok(Self {
buffer: self.buffer.clone(),
range: self.range.start + range.start..self.range.start + range.end,
retained: self.retained.clone(),
})
}
pub(crate) fn retain(mut self, owner: Arc<dyn Send + Sync>) -> Self {
self.retained.push(owner);
self
}
pub(crate) fn overlaps(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage)
&& self.range.start < other.range.end
&& other.range.start < self.range.end
}
pub(crate) fn same_region(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage) && self.range == other.range
}
pub(super) fn conflicts(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage)
&& (self.buffer.storage.shared || self.overlaps(other))
}
pub(super) fn gpu(&self) -> Result<crate::gpu::View<'_>> {
self.buffer
.storage
.gpu
.as_ref()
.ok_or_else(|| Error::Unsupported("NPU-local storage cannot bind to the GPU".into()))?
.try_slice(self.offset(), self.len())
}
}
impl Storage {
pub fn host_conflict(&self, access: Access) -> Result<()> {
let host = self.host.lock().unwrap_or_else(|e| e.into_inner());
if let Some(error) = &host.poison {
return Err(Error::DeviceLost(error.clone()));
}
if host.external || host.writer || (access.writes() && host.readers != 0) {
return Err(Error::Busy(
"device binding conflicts with a live host mapping".into(),
));
}
Ok(())
}
pub fn make_visible(&self, engine: Engine) -> Result<()> {
let mut visibility = self.visibility.lock().unwrap_or_else(|e| e.into_inner());
if visibility.visible[engine as usize] {
return Ok(());
}
#[cfg(feature = "npu")]
if let Some(bo) = &self.bo {
bo.sync(visibility.sync_to_device(engine), self.bytes)
.map_err(Error::Message)?;
self.runtime
.core
.counters
.cache_maintenance_bytes
.fetch_add(self.bytes as u64, std::sync::atomic::Ordering::Relaxed);
}
visibility.visible[engine as usize] = true;
Ok(())
}
}
pub struct ReadGuard<'a> {
buffer: &'a Buffer,
range: Range<usize>,
}
impl Deref for ReadGuard<'_> {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
self.buffer.storage.pointer.add(self.range.start),
self.range.len(),
)
}
}
}
impl Drop for ReadGuard<'_> {
fn drop(&mut self) {
self.buffer
.storage
.host
.lock()
.unwrap_or_else(|e| e.into_inner())
.readers -= 1;
self.buffer.storage.runtime.core.host_changed.notify_all();
}
}
pub struct WriteGuard<'a> {
buffer: &'a Buffer,
range: Range<usize>,
}
impl Deref for WriteGuard<'_> {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
self.buffer.storage.pointer.add(self.range.start),
self.range.len(),
)
}
}
}
impl DerefMut for WriteGuard<'_> {
fn deref_mut(&mut self) -> &mut [u8] {
unsafe {
std::slice::from_raw_parts_mut(
self.buffer.storage.pointer.add(self.range.start),
self.range.len(),
)
}
}
}
impl Drop for WriteGuard<'_> {
fn drop(&mut self) {
self.buffer
.storage
.visibility
.lock()
.unwrap_or_else(|e| e.into_inner())
.wrote(Engine::Host);
self.buffer
.storage
.host
.lock()
.unwrap_or_else(|e| e.into_inner())
.writer = false;
self.buffer.storage.runtime.core.host_changed.notify_all();
}
}
impl Drop for Storage {
fn drop(&mut self) {
#[cfg(feature = "npu")]
if self
.visibility
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.writer
== Engine::Host
&& let Some(bo) = &self.bo
&& bo.sync(true, self.bytes).is_err()
{
std::mem::forget((
self.bo.take(),
self.descriptor.take(),
self.gpu.take(),
self._reservation.take(),
));
return;
}
if self.accounted {
self.runtime
.core
.counters
.live_bytes
.fetch_sub(self.bytes as u64, std::sync::atomic::Ordering::Relaxed);
}
}
}