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