Skip to main content

hrx/execution/
buffer.rs

1use super::{Access, RuntimeOwner};
2use crate::{Error, Result};
3use std::{
4    ops::{Deref, DerefMut, Range},
5    sync::{Arc, Mutex},
6};
7
8/// Explicit storage placement. Shared storage requires a resident NPU program
9/// to create its device mapping once, before execution.
10#[derive(Clone)]
11pub enum MemoryPlacement {
12    /// GPU pool memory for GPU-only work.
13    GpuLocal,
14    /// Coherent host-local memory mapped for guarded CPU access and GPU copies.
15    /// Does not require an NPU or its runtime.
16    HostVisible,
17    /// NPU host-only memory, bound to the program's host memory group.
18    #[cfg(feature = "npu")]
19    NpuLocal(crate::npu::NpuProgram),
20    /// One exportable GPU allocation, imported into the NPU without copying.
21    #[cfg(feature = "npu")]
22    Shared(crate::npu::NpuProgram),
23}
24#[derive(Default)]
25pub(super) struct HostState {
26    pub readers: usize,
27    pub writer: bool,
28    pub external: bool,
29    pub poison: Option<String>,
30}
31#[derive(Clone, Copy, PartialEq, Eq)]
32pub(super) enum Engine {
33    Host,
34    Gpu,
35    #[cfg_attr(not(feature = "npu"), allow(dead_code))]
36    Npu,
37}
38pub(super) struct Visibility {
39    pub writer: Engine,
40    pub visible: [bool; 3],
41}
42impl Visibility {
43    pub fn new() -> Self {
44        Self {
45            writer: Engine::Host,
46            visible: [true, false, false],
47        }
48    }
49    #[cfg(any(test, feature = "npu"))]
50    pub fn sync_to_device(&self, consumer: Engine) -> bool {
51        self.writer == Engine::Host || consumer == Engine::Npu
52    }
53    pub fn wrote(&mut self, engine: Engine) {
54        self.writer = engine;
55        self.visible = [false; 3];
56        self.visible[engine as usize] = true;
57    }
58}
59pub(super) struct Storage {
60    pub accounted: bool,
61    // Imported mappings drop before descriptor and backing GPU allocation.
62    #[cfg(feature = "npu")]
63    pub bo: Option<crate::npu::raw::Bo>,
64    #[cfg(feature = "npu")]
65    pub descriptor: Option<std::os::fd::OwnedFd>,
66    pub gpu: Option<crate::gpu::Buffer>,
67    pub pointer: *mut u8,
68    pub bytes: usize,
69    #[cfg(feature = "npu")]
70    pub npu_device: Option<i32>,
71    #[cfg(feature = "npu")]
72    pub group: Option<i32>,
73    pub shared: bool,
74    pub host: Mutex<HostState>,
75    pub visibility: Mutex<Visibility>,
76    pub runtime: Arc<RuntimeOwner>,
77    #[cfg(test)]
78    pub _test_memory: Option<Arc<std::cell::UnsafeCell<[u8; 256]>>>,
79    // Last: keep native storage charged through destruction and quarantine.
80    pub _reservation: Option<crate::residency::MemoryReservation>,
81}
82// All host dereferences require a host lease; device access is reserved under
83// the scheduler lock. Native handles never escape this tracked allocation.
84unsafe impl Send for Storage {}
85unsafe impl Sync for Storage {}
86/// A cloneable, tracked allocation. Every alias shares lifetime and access state.
87#[derive(Clone)]
88pub struct Buffer {
89    pub(super) storage: Arc<Storage>,
90}
91/// An owned checked view. Keeping a view keeps its root allocation alive.
92#[derive(Clone)]
93pub struct BufferView {
94    pub(super) buffer: Buffer,
95    pub(super) range: Range<usize>,
96    pub(super) retained: Vec<Arc<dyn Send + Sync>>,
97}
98impl Buffer {
99    /// Allocation size in bytes.
100    pub fn len(&self) -> usize {
101        self.storage.bytes
102    }
103    /// Whether the allocation is empty. Allocations reject zero lengths.
104    pub fn is_empty(&self) -> bool {
105        self.len() == 0
106    }
107    /// Borrow all bytes as an owned device binding.
108    pub fn view(&self) -> BufferView {
109        BufferView {
110            buffer: self.clone(),
111            range: 0..self.len(),
112            retained: Vec::new(),
113        }
114    }
115    /// Create a checked subregion.
116    pub fn slice(&self, range: Range<usize>) -> Result<BufferView> {
117        self.view().slice(range)
118    }
119    /// Wait for device writers and map initialized bytes for host reading.
120    /// Live conflicting host guards return `Busy` instead of waiting.
121    pub fn map_read(&self) -> Result<ReadGuard<'_>> {
122        self.acquire(false, true)?;
123        Ok(ReadGuard {
124            buffer: self,
125            range: 0..self.len(),
126        })
127    }
128    /// Map for reading without waiting for device work.
129    pub fn try_map_read(&self) -> Result<ReadGuard<'_>> {
130        self.acquire(false, false)?;
131        Ok(ReadGuard {
132            buffer: self,
133            range: 0..self.len(),
134        })
135    }
136    /// Wait for device access and map initialized bytes for host mutation.
137    pub fn map_write(&self) -> Result<WriteGuard<'_>> {
138        self.acquire(true, true)?;
139        Ok(WriteGuard {
140            buffer: self,
141            range: 0..self.len(),
142        })
143    }
144    /// Map for mutation without waiting for device work.
145    pub fn try_map_write(&self) -> Result<WriteGuard<'_>> {
146        self.acquire(true, false)?;
147        Ok(WriteGuard {
148            buffer: self,
149            range: 0..self.len(),
150        })
151    }
152    fn acquire(&self, write: bool, wait: bool) -> Result<()> {
153        self.acquire_with(write, wait, || self.storage.make_visible(Engine::Host))
154    }
155    // Fault-injection seam for testing lease rollback when cache maintenance fails.
156    pub(super) fn acquire_with(
157        &self,
158        write: bool,
159        wait: bool,
160        make_visible: impl FnOnce() -> Result<()>,
161    ) -> Result<()> {
162        if self.storage.pointer.is_null() {
163            return Err(Error::Unsupported(
164                "GPU-local memory is not host mapped; use an explicit graph copy to shared storage"
165                    .into(),
166            ));
167        }
168        let core = &self.storage.runtime.core;
169        let view = self.view();
170        let mut scheduler = core.state.lock().unwrap_or_else(|e| e.into_inner());
171        loop {
172            {
173                let host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
174                if let Some(error) = &host.poison {
175                    return Err(Error::DeviceLost(error.clone()));
176                }
177                if host.external || host.writer || (write && host.readers != 0) {
178                    return Err(Error::Busy("conflicting host mapping".into()));
179                }
180            }
181            if !scheduler.conflicts(
182                &view,
183                if write {
184                    Access::ReadWrite
185                } else {
186                    Access::Read
187                },
188            ) {
189                break;
190            }
191            if !wait {
192                return Err(Error::Busy("allocation is in device use".into()));
193            }
194            scheduler = core
195                .host_changed
196                .wait(scheduler)
197                .unwrap_or_else(|e| e.into_inner());
198        }
199        let mut host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
200        if write {
201            host.writer = true;
202        } else {
203            host.readers += 1;
204        }
205        // Reserve the lease while submissions are excluded, then let unrelated
206        // allocations proceed during potentially expensive cache maintenance.
207        drop(host);
208        drop(scheduler);
209        if let Err(error) = make_visible() {
210            let mut host = self.storage.host.lock().unwrap_or_else(|e| e.into_inner());
211            if write {
212                host.writer = false;
213            } else {
214                host.readers -= 1;
215            }
216            drop(host);
217            core.host_changed.notify_all();
218            return Err(error);
219        }
220        Ok(())
221    }
222}
223impl BufferView {
224    /// Whether this allocation supports guarded host access without a copy.
225    pub fn is_host_visible(&self) -> bool {
226        !self.buffer.storage.pointer.is_null()
227    }
228    /// Map this subrange for reading, waiting for device writers. Host access
229    /// reserves the entire allocation, including other views of it.
230    pub fn map_read(&self) -> Result<ReadGuard<'_>> {
231        self.buffer.acquire(false, true)?;
232        Ok(ReadGuard {
233            buffer: &self.buffer,
234            range: self.range.clone(),
235        })
236    }
237    /// Map this subrange for mutation, waiting for device accesses. Host access
238    /// reserves the entire allocation, including other views of it.
239    pub fn map_write(&self) -> Result<WriteGuard<'_>> {
240        self.buffer.acquire(true, true)?;
241        Ok(WriteGuard {
242            buffer: &self.buffer,
243            range: self.range.clone(),
244        })
245    }
246    /// Size of this view.
247    pub fn len(&self) -> usize {
248        self.range.len()
249    }
250    /// Whether this view is empty.
251    pub fn is_empty(&self) -> bool {
252        self.range.is_empty()
253    }
254    /// Byte offset within the root allocation.
255    pub fn offset(&self) -> usize {
256        self.range.start
257    }
258    /// Create a subregion relative to this view.
259    pub fn slice(&self, range: Range<usize>) -> Result<Self> {
260        if range.start > range.end || range.end > self.len() {
261            return Err(Error::Message("buffer view is out of bounds".into()));
262        }
263        Ok(Self {
264            buffer: self.buffer.clone(),
265            range: self.range.start + range.start..self.range.start + range.end,
266            retained: self.retained.clone(),
267        })
268    }
269    pub(crate) fn retain(mut self, owner: Arc<dyn Send + Sync>) -> Self {
270        self.retained.push(owner);
271        self
272    }
273    pub(crate) fn overlaps(&self, other: &Self) -> bool {
274        Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage)
275            && self.range.start < other.range.end
276            && other.range.start < self.range.end
277    }
278    pub(crate) fn same_region(&self, other: &Self) -> bool {
279        Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage) && self.range == other.range
280    }
281    pub(super) fn conflicts(&self, other: &Self) -> bool {
282        // Cache maintenance is allocation-wide in the initial shared-memory ABI.
283        Arc::ptr_eq(&self.buffer.storage, &other.buffer.storage)
284            && (self.buffer.storage.shared || self.overlaps(other))
285    }
286    pub(super) fn gpu(&self) -> Result<crate::gpu::View<'_>> {
287        self.buffer
288            .storage
289            .gpu
290            .as_ref()
291            .ok_or_else(|| Error::Unsupported("NPU-local storage cannot bind to the GPU".into()))?
292            .try_slice(self.offset(), self.len())
293    }
294}
295impl Storage {
296    pub fn host_conflict(&self, access: Access) -> Result<()> {
297        let host = self.host.lock().unwrap_or_else(|e| e.into_inner());
298        if let Some(error) = &host.poison {
299            return Err(Error::DeviceLost(error.clone()));
300        }
301        if host.external || host.writer || (access.writes() && host.readers != 0) {
302            return Err(Error::Busy(
303                "device binding conflicts with a live host mapping".into(),
304            ));
305        }
306        Ok(())
307    }
308    pub fn make_visible(&self, engine: Engine) -> Result<()> {
309        let mut visibility = self.visibility.lock().unwrap_or_else(|e| e.into_inner());
310        if visibility.visible[engine as usize] {
311            return Ok(());
312        }
313        #[cfg(feature = "npu")]
314        if let Some(bo) = &self.bo {
315            // Only cross-engine transitions require maintenance. Producer work
316            // has already completed before this method can be reached. Host-dirty
317            // lines must be flushed even when the consumer is the GPU.
318            bo.sync(visibility.sync_to_device(engine), self.bytes)
319                .map_err(Error::Message)?;
320            self.runtime
321                .core
322                .counters
323                .cache_maintenance_bytes
324                .fetch_add(self.bytes as u64, std::sync::atomic::Ordering::Relaxed);
325        }
326        visibility.visible[engine as usize] = true;
327        Ok(())
328    }
329}
330/// A host read lease. The slice cannot outlive the guard.
331///
332/// ```compile_fail
333/// # use hrx::execution::Buffer;
334/// fn escape(buffer: &Buffer) -> &[u8] {
335///     let guard = buffer.map_read().unwrap();
336///     &guard
337/// }
338/// ```
339pub struct ReadGuard<'a> {
340    buffer: &'a Buffer,
341    range: Range<usize>,
342}
343impl Deref for ReadGuard<'_> {
344    type Target = [u8];
345    fn deref(&self) -> &[u8] {
346        unsafe {
347            std::slice::from_raw_parts(
348                self.buffer.storage.pointer.add(self.range.start),
349                self.range.len(),
350            )
351        }
352    }
353}
354impl Drop for ReadGuard<'_> {
355    fn drop(&mut self) {
356        self.buffer
357            .storage
358            .host
359            .lock()
360            .unwrap_or_else(|e| e.into_inner())
361            .readers -= 1;
362        self.buffer.storage.runtime.core.host_changed.notify_all();
363    }
364}
365/// An exclusive host write lease. Releasing it marks host writes authoritative.
366pub struct WriteGuard<'a> {
367    buffer: &'a Buffer,
368    range: Range<usize>,
369}
370impl Deref for WriteGuard<'_> {
371    type Target = [u8];
372    fn deref(&self) -> &[u8] {
373        unsafe {
374            std::slice::from_raw_parts(
375                self.buffer.storage.pointer.add(self.range.start),
376                self.range.len(),
377            )
378        }
379    }
380}
381impl DerefMut for WriteGuard<'_> {
382    fn deref_mut(&mut self) -> &mut [u8] {
383        unsafe {
384            std::slice::from_raw_parts_mut(
385                self.buffer.storage.pointer.add(self.range.start),
386                self.range.len(),
387            )
388        }
389    }
390}
391impl Drop for WriteGuard<'_> {
392    fn drop(&mut self) {
393        // Publish dirty state before releasing the host lease.
394        self.buffer
395            .storage
396            .visibility
397            .lock()
398            .unwrap_or_else(|e| e.into_inner())
399            .wrote(Engine::Host);
400        self.buffer
401            .storage
402            .host
403            .lock()
404            .unwrap_or_else(|e| e.into_inner())
405            .writer = false;
406        self.buffer.storage.runtime.core.host_changed.notify_all();
407    }
408}
409
410impl Drop for Storage {
411    fn drop(&mut self) {
412        #[cfg(feature = "npu")]
413        if self
414            .visibility
415            .get_mut()
416            .unwrap_or_else(|e| e.into_inner())
417            .writer
418            == Engine::Host
419            && let Some(bo) = &self.bo
420            && bo.sync(true, self.bytes).is_err()
421        {
422            // Do not release pages while failed cache maintenance could leave
423            // CPU writes able to reach a later allocation of those pages.
424            std::mem::forget((
425                self.bo.take(),
426                self.descriptor.take(),
427                self.gpu.take(),
428                self._reservation.take(),
429            ));
430            return;
431        }
432
433        if self.accounted {
434            self.runtime
435                .core
436                .counters
437                .live_bytes
438                .fetch_sub(self.bytes as u64, std::sync::atomic::Ordering::Relaxed);
439        }
440    }
441}