Skip to main content

tract_gpu/tensor/
lazy.rs

1use std::fmt::{self, Display};
2use std::sync::OnceLock;
3
4use tract_core::internal::*;
5
6use super::DeviceTensor;
7use crate::device::get_context;
8
9/// Host storage for a tensor whose bytes are still on a device.
10///
11/// A device tensor crosses the model boundary wrapped in one of these. The
12/// result is a host tensor as far as facts, datum type and shape go, but
13/// nothing is copied back until something reads the bytes: `as_bytes`,
14/// `as_slice`, `try_as_plain_ram` and friends materialize it once and keep the
15/// result, and a caller that only hands it to the next `run()` never pays a
16/// readback at all, because `DeviceSync` takes the device tensor back out of
17/// it.
18///
19/// Only owned device tensors belong here. A view borrows a buffer whose
20/// contents are only good for the turn, so it is copied out at the boundary
21/// rather than held.
22#[derive(Debug)]
23pub struct LazyHostStorage {
24    /// Dropped once the host side is written to: a mutated tensor is a plain
25    /// host tensor and must not alias a device buffer anyone else holds.
26    device: Option<DeviceTensor>,
27    host: OnceLock<Arc<Tensor>>,
28}
29
30impl LazyHostStorage {
31    /// Wrap a device tensor whose bytes are to be read later.
32    ///
33    /// Waits for the work producing those bytes before wrapping: the wait
34    /// belongs to the thread that queued the work, and a reader is free to be
35    /// another one -- a Metal stream is thread-local, so a wait deferred to the
36    /// reader would be a wait on an empty queue and the bytes would be read
37    /// before the device wrote them.
38    pub fn new(device: DeviceTensor) -> TractResult<Self> {
39        ensure!(
40            !device.is_exotic(),
41            "A lazy host tensor stands in for plain host bytes, and {device:?} would come back as its own storage"
42        );
43        get_context()?.synchronize()?;
44        Ok(LazyHostStorage { device: Some(device), host: OnceLock::new() })
45    }
46
47    /// The device tensor still backing this storage, if it has not been
48    /// detached by a host-side mutation.
49    pub fn device(&self) -> Option<&DeviceTensor> {
50        self.device.as_ref()
51    }
52
53    /// True once the bytes have been brought back to host.
54    pub fn is_materialized(&self) -> bool {
55        self.host.get().is_some()
56    }
57
58    pub fn datum_type(&self) -> DatumType {
59        match (&self.device, self.host.get()) {
60            (Some(d), _) => d.datum_type(),
61            (None, Some(h)) => h.datum_type(),
62            (None, None) => DatumType::U8,
63        }
64    }
65
66    pub fn shape(&self) -> TVec<usize> {
67        match (&self.device, self.host.get()) {
68            (Some(d), _) => d.shape().into(),
69            (None, Some(h)) => h.shape().into(),
70            (None, None) => tvec![],
71        }
72    }
73
74    /// Wrap into a `Tensor` carrying the device tensor's datum type and shape.
75    pub fn into_tensor(self) -> Tensor {
76        let dt = self.datum_type();
77        let shape = self.shape();
78        Tensor::from_storage(dt, &shape, self)
79    }
80
81    /// The host tensor, brought back from the device on the first call.
82    ///
83    /// Kept as the `Arc` the device tensor handed out rather than a tensor of
84    /// our own: on a unified-memory backend the device buffer wraps that very
85    /// allocation and the backend keeps a share of it, so taking ownership is
86    /// never possible and copying would spend a full readback to own bytes we
87    /// can already read. Sharing them is free, and safe because everything
88    /// handed out from here is immutable -- a write goes through `as_plain_ram_mut`,
89    /// which copies out of the `Arc` first.
90    fn materialize(&self) -> TractResult<&Arc<Tensor>> {
91        if let Some(host) = self.host.get() {
92            return Ok(host);
93        }
94        let device = self
95            .device
96            .as_ref()
97            .context("Lazy host storage has neither a device tensor nor host bytes")?;
98        let host = device.to_host().context("While materializing a lazy host tensor")?;
99        Ok(self.host.get_or_init(|| host))
100    }
101}
102
103impl PartialEq for LazyHostStorage {
104    fn eq(&self, other: &Self) -> bool {
105        match (&self.device, &other.device) {
106            (Some(a), Some(b)) => a == b,
107            (None, None) => self.host.get() == other.host.get(),
108            _ => false,
109        }
110    }
111}
112
113impl Eq for LazyHostStorage {}
114
115impl Display for LazyHostStorage {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self.host.get() {
118            Some(host) => write!(f, "LazyHost(materialized): {{ {host:?} }}"),
119            None => {
120                write!(f, "LazyHost(on device): {{ {:?} {:?} }}", self.datum_type(), self.shape())
121            }
122        }
123    }
124}
125
126impl TensorStorage for LazyHostStorage {
127    fn byte_len(&self) -> usize {
128        match (&self.device, self.host.get()) {
129            (Some(d), _) => d.len() * d.datum_type().size_of(),
130            (None, Some(h)) => h.len() * h.datum_type().size_of(),
131            (None, None) => 0,
132        }
133    }
134
135    fn is_empty(&self) -> bool {
136        self.byte_len() == 0
137    }
138
139    fn deep_clone(&self) -> Box<dyn TensorStorage> {
140        // Shares whatever has been materialized; a write on either side copies
141        // it out first, so the two stay independent as the contract requires.
142        let host = OnceLock::new();
143        if let Some(h) = self.host.get() {
144            let _ = host.set(Arc::clone(h));
145        }
146        Box::new(LazyHostStorage { device: self.device.clone(), host })
147    }
148
149    fn as_plain_ram(&self) -> Option<&PlainStorage> {
150        // Cheap predicate: answers what is here now, never triggers a readback.
151        self.host.get().and_then(|h| h.as_plain_ram_storage())
152    }
153
154    fn as_plain_ram_mut(&mut self) -> Option<&mut PlainStorage> {
155        self.materialize().ok()?;
156        // Writing to the host side detaches the device tensor: from here on
157        // this is an ordinary host tensor, and nothing aliases device memory.
158        // make_mut copies the bytes out only if the backend is still sharing
159        // them, which is exactly when writing through would reach the device.
160        self.device = None;
161        Arc::make_mut(self.host.get_mut()?).as_plain_ram_storage_mut()
162    }
163
164    fn into_plain_ram(self: Box<Self>) -> Option<PlainStorage> {
165        let me = *self;
166        me.materialize().ok()?;
167        let host = me.host.into_inner()?;
168        let host = Arc::try_unwrap(host).unwrap_or_else(|shared| (*shared).clone());
169        host.into_blob().ok().map(PlainStorage::from)
170    }
171
172    fn dyn_hash(&self, _state: &mut dyn std::hash::Hasher) {
173        // no meaningful hash for device memory, as for DeviceTensor
174    }
175
176    fn exotic_fact(&self, _shape: &[usize]) -> TractResult<Option<Box<dyn ExoticFact>>> {
177        // A lazily-materialized host tensor is a plain host tensor: it carries
178        // its real datum type and shape, and no exotic fact.
179        Ok(None)
180    }
181
182    fn is_exotic(&self) -> bool {
183        false
184    }
185
186    fn in_ram(&self) -> bool {
187        self.is_materialized()
188    }
189
190    fn materialize_plain_ram(&self) -> TractResult<&PlainStorage> {
191        self.materialize()?
192            .as_plain_ram_storage()
193            .context("Device readback did not produce plain storage")
194    }
195
196    /// Bytes still on the device are sliced there, and the slice stays lazy: an
197    /// application truncating state it hands straight back to the next `run()`
198    /// never brings either side across. Once they are here the generic copy is
199    /// the cheaper path, and a host-side write has detached the device tensor,
200    /// leaving nothing to ask.
201    fn slice(
202        &self,
203        dt: DatumType,
204        shape: &[usize],
205        axis: usize,
206        start: usize,
207        end: usize,
208    ) -> TractResult<Option<Tensor>> {
209        if self.is_materialized() {
210            return Ok(None);
211        }
212        let Some(device) = self.device.as_ref() else { return Ok(None) };
213        let Some(sliced) = device.slice_on_device(dt, shape, axis, start, end)? else {
214            return Ok(None);
215        };
216        Ok(Some(Self::new(sliced)?.into_tensor()))
217    }
218}