Skip to main content

hrx/
inference.rs

1//! Shared model context, owned tensor transfers, and bounded inference slots.
2
3use crate::{
4    Completion, Error, Result, Runtime,
5    execution::{Buffer, BufferView, ExecutableGraph, MemoryPlacement, RuntimeOptions},
6    loom::Compiler,
7    tensor::{DeviceTensor, TensorDesc},
8};
9use std::{
10    future::Future,
11    pin::Pin,
12    sync::{Arc, Condvar, Mutex},
13    task::{Context, Poll, Waker},
14};
15
16/// Explicitly shared allocation, compilation, and scheduling domain for models.
17#[derive(Clone)]
18pub struct ModelContext {
19    runtime: Runtime,
20    compiler: Arc<Mutex<Option<Compiler>>>,
21}
22impl ModelContext {
23    /// Construct a context; hardware and compiler initialization remain lazy.
24    pub fn new(options: RuntimeOptions) -> Result<Self> {
25        Ok(Self {
26            runtime: Runtime::with_options(options)?,
27            compiler: Arc::new(Mutex::new(None)),
28        })
29    }
30    /// Coordinated runtime shared by every model and tensor in this context.
31    pub fn runtime(&self) -> &Runtime {
32        &self.runtime
33    }
34    /// Lazily obtain the compiler for the selected device's actual target.
35    pub fn compiler(&self) -> Result<Compiler> {
36        crate::cached_init(&self.compiler, || {
37            Compiler::for_target(None, self.runtime.gpu()?.target())
38        })
39    }
40    /// Allocate initialized device storage. Empty tensors allocate nothing.
41    pub fn allocate(&self, desc: TensorDesc) -> Result<DeviceTensor> {
42        self.allocate_with(desc, MemoryPlacement::GpuLocal)
43    }
44    /// Allocate a tensor with explicit storage placement. Host-visible IO can
45    /// be submitted and read back directly without transfer staging or copies.
46    pub fn allocate_with(
47        &self,
48        desc: TensorDesc,
49        placement: MemoryPlacement,
50    ) -> Result<DeviceTensor> {
51        let view = if desc.is_empty() {
52            None
53        } else {
54            Some(self.runtime.allocate(desc.bytes(), placement)?.view())
55        };
56        Ok(DeviceTensor {
57            runtime: self.runtime.clone(),
58            desc,
59            view,
60            producer: Completion::ready(),
61            lease: None,
62        })
63    }
64    /// Wrap existing tracked storage without copying. The producer must belong
65    /// to this runtime or represent already completed initialization.
66    pub fn tensor(
67        &self,
68        desc: TensorDesc,
69        view: crate::execution::BufferView,
70        producer: Completion,
71    ) -> Result<DeviceTensor> {
72        producer.validate_runtime(&self.runtime)?;
73        if !self.runtime.owns(&view)
74            || view.len() < desc.bytes()
75            || !view.offset().is_multiple_of(desc.dtype().bytes())
76        {
77            return Err(Error::Message(
78                "tensor storage has the wrong context, extent, or alignment".into(),
79            ));
80        }
81        let view = if desc.is_empty() {
82            None
83        } else {
84            Some(view.slice(0..desc.bytes())?)
85        };
86        Ok(DeviceTensor {
87            runtime: self.runtime.clone(),
88            desc,
89            view,
90            producer,
91            lease: None,
92        })
93    }
94    /// Copy host bytes into owned staging and enqueue upload. The caller may
95    /// release or mutate its source as soon as this method returns.
96    pub fn upload(&self, desc: TensorDesc, bytes: &[u8]) -> Result<DeviceTensor> {
97        if bytes.len() != desc.bytes() {
98            return Err(Error::Message(
99                "upload byte count does not match tensor".into(),
100            ));
101        }
102        let mut tensor = self.allocate(desc)?;
103        if let Some(destination) = tensor.binding() {
104            let staging = self
105                .runtime
106                .allocate(bytes.len(), MemoryPlacement::HostVisible)?;
107            staging.map_write()?.copy_from_slice(bytes);
108            let mut graph = self.runtime.graph();
109            graph.copy(destination, staging.view())?;
110            tensor.producer = graph.prepare()?.submit()?;
111        }
112        Ok(tensor)
113    }
114    /// Copy a contiguous tensor into a byte-aligned subrange of another tensor.
115    /// Both tensors must be contiguous and have the same dtype. This updates the
116    /// destination's producer dependency, preserving failures from earlier writes.
117    /// Slot owners survive until the copy drains, even if the source is dropped.
118    pub fn copy_into(
119        &self,
120        destination: &mut DeviceTensor,
121        byte_offset: usize,
122        source: &DeviceTensor,
123    ) -> Result<()> {
124        self.validate(destination)?;
125        self.validate(source)?;
126        let end = byte_offset
127            .checked_add(source.desc.bytes())
128            .filter(|&v| v <= destination.desc.bytes())
129            .ok_or_else(|| Error::Message("tensor copy exceeds destination".into()))?;
130        if destination.desc.dtype() != source.desc.dtype()
131            || !destination.desc.is_contiguous()
132            || !source.desc.is_contiguous()
133            || !byte_offset.is_multiple_of(source.desc.dtype().bytes())
134        {
135            return Err(Error::Message(
136                "invalid tensor copy dtype, strides or alignment".into(),
137            ));
138        }
139        if let Some(from) = source.binding() {
140            let to = destination
141                .binding()
142                .expect("checked nonempty destination")
143                .slice(byte_offset..end)?;
144            let mut graph = self.runtime.graph();
145            graph.copy(to, from)?;
146            destination.producer = graph
147                .prepare()?
148                .submit_after(&[destination.producer.clone(), source.producer.clone()])?;
149        } else {
150            source.producer.wait()?;
151        }
152        Ok(())
153    }
154
155    /// Concatenate contiguous tensors along their first axis on the device.
156    /// Inputs must have identical trailing dimensions, dtype and layout. Empty
157    /// inputs are allowed; scalars and an empty input list are not.
158    pub fn concatenate(&self, inputs: &[DeviceTensor]) -> Result<DeviceTensor> {
159        let first = inputs
160            .first()
161            .ok_or_else(|| Error::Message("concatenate needs inputs".into()))?;
162        let prototype = first.desc();
163        if prototype.shape().is_empty() || !prototype.is_contiguous() {
164            return Err(Error::Message(
165                "concatenate requires contiguous non-scalar tensors".into(),
166            ));
167        }
168        let mut shape = prototype.shape().to_vec();
169        shape[0] = 0;
170        for input in inputs {
171            self.validate(input)?;
172            let desc = input.desc();
173            if desc.dtype() != prototype.dtype()
174                || desc.layout() != prototype.layout()
175                || desc.shape().len() != prototype.shape().len()
176                || desc.shape()[1..] != prototype.shape()[1..]
177                || !desc.is_contiguous()
178            {
179                return Err(Error::Message("incompatible concatenate input".into()));
180            }
181            shape[0] = shape[0]
182                .checked_add(desc.shape()[0])
183                .ok_or_else(|| Error::Message("concatenate dimension overflow".into()))?;
184        }
185        let mut output = self.allocate(
186            TensorDesc::new(prototype.dtype(), shape)?.with_layout(prototype.layout())?,
187        )?;
188        // Even zero-byte inputs may carry failed producers: do not silently
189        // discard their dependencies by treating concatenation as a no-op.
190        let dependencies: Vec<_> = inputs.iter().map(|t| t.producer.clone()).collect();
191        if let Some(destination) = output.binding() {
192            let mut graph = self.runtime.graph();
193            let mut offset = 0;
194            for input in inputs {
195                if let Some(source) = input.binding() {
196                    graph.copy(destination.slice(offset..offset + source.len())?, source)?;
197                    offset += input.desc().bytes();
198                }
199            }
200            output.producer = graph.prepare()?.submit_after(&dependencies)?;
201        } else {
202            // No native operation is needed. A caller cannot observe a successful
203            // empty tensor until every producer has completed successfully.
204            for dependency in dependencies {
205                dependency.wait()?;
206            }
207        }
208        Ok(output)
209    }
210
211    /// Queue readback after a tensor's producer, without a host wait.
212    pub fn download(&self, tensor: &DeviceTensor) -> Result<TensorReadback> {
213        self.validate(tensor)?;
214        let Some(source) = tensor.binding() else {
215            return Ok(TensorReadback {
216                buffer: None,
217                completion: tensor.producer.clone(),
218            });
219        };
220        let staging = self
221            .runtime
222            .allocate(tensor.desc.bytes(), MemoryPlacement::HostVisible)?;
223        let mut graph = self.runtime.graph();
224        graph.copy(staging.view(), source)?;
225        let completion = graph
226            .prepare()?
227            .submit_after(std::slice::from_ref(&tensor.producer))?;
228        Ok(TensorReadback {
229            buffer: Some(staging),
230            completion,
231        })
232    }
233    /// Reject a tensor from another allocation domain before any work is queued.
234    pub fn validate(&self, tensor: &DeviceTensor) -> Result<()> {
235        if !self.runtime.same_domain(&tensor.runtime) {
236            return Err(Error::Message(
237                "tensor belongs to another model context".into(),
238            ));
239        }
240        Ok(())
241    }
242}
243
244/// Owned asynchronous readback. Dropping it never invalidates queued work.
245pub struct TensorReadback {
246    buffer: Option<Buffer>,
247    completion: Completion,
248}
249impl TensorReadback {
250    /// Readback completion, suitable for polling or awaiting.
251    pub fn completion(&self) -> &Completion {
252        &self.completion
253    }
254    /// Wait and return the tensor's storage bytes, including declared stride gaps.
255    pub fn wait(self) -> Result<Vec<u8>> {
256        self.completion.wait()?;
257        self.bytes()
258    }
259    fn bytes(&self) -> Result<Vec<u8>> {
260        self.buffer
261            .as_ref()
262            .map_or_else(|| Ok(Vec::new()), |buffer| Ok(buffer.map_read()?.to_vec()))
263    }
264}
265impl Future for TensorReadback {
266    type Output = Result<Vec<u8>>;
267    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
268        match Pin::new(&mut self.completion).poll(cx) {
269            Poll::Pending => Poll::Pending,
270            Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
271            Poll::Ready(Ok(())) => Poll::Ready(self.bytes()),
272        }
273    }
274}
275
276struct Slot {
277    inputs: Vec<DeviceTensor>,
278    outputs: Vec<DeviceTensor>,
279    upload: Mutex<Option<Arc<Transfer>>>,
280    download: Mutex<Option<Arc<Transfer>>>,
281    graph: ExecutableGraph,
282}
283
284struct Transfer {
285    buffers: Vec<Option<BufferView>>,
286    graph: Option<ExecutableGraph>,
287}
288impl Transfer {
289    fn prepare(context: &ModelContext, tensors: &[DeviceTensor], upload: bool) -> Result<Self> {
290        let mut graph = context.runtime.graph();
291        let mut copied = false;
292        let buffers = tensors
293            .iter()
294            .map(|tensor| {
295                let Some(device) = tensor.binding() else {
296                    return Ok(None);
297                };
298                if device.is_host_visible() {
299                    return Ok(Some(device));
300                }
301                let host = context
302                    .runtime
303                    .allocate(tensor.desc.bytes(), MemoryPlacement::HostVisible)?;
304                if upload {
305                    graph.copy(device, host.view())?;
306                } else {
307                    graph.copy(host.view(), device)?;
308                }
309                copied = true;
310                Ok(Some(host.view()))
311            })
312            .collect::<Result<Vec<_>>>()?;
313        let graph = if copied { Some(graph.prepare()?) } else { None };
314        Ok(Self { buffers, graph })
315    }
316}
317
318/// One executable slot and its actual tensor bindings. Bindings may be slices
319/// of shared scratch or alias each other for in-place execution. No extra model
320/// input/output buffers are introduced by the inference pool.
321pub struct InferenceGraph {
322    /// Fixed-address inputs populated before execution.
323    pub inputs: Vec<DeviceTensor>,
324    /// Fixed-address outputs leased to the caller after submission.
325    pub outputs: Vec<DeviceTensor>,
326    /// The graph which reads inputs and produces outputs.
327    pub graph: ExecutableGraph,
328}
329#[derive(Default)]
330struct SlotState {
331    leased: bool,
332    completion: Option<Completion>,
333}
334struct PoolState {
335    slots: Vec<SlotState>,
336    wakers: Vec<Waker>,
337}
338struct Pool {
339    context: ModelContext,
340    slots: Vec<Slot>,
341    state: Mutex<PoolState>,
342    changed: Condvar,
343}
344struct Lease {
345    pool: Arc<Pool>,
346    index: usize,
347}
348impl Drop for Lease {
349    fn drop(&mut self) {
350        let wakers = {
351            let mut state = self.pool.state.lock().unwrap_or_else(|e| e.into_inner());
352            state.slots[self.index].leased = false;
353            std::mem::take(&mut state.wakers)
354        };
355        self.pool.changed.notify_all();
356        for waker in wakers {
357            waker.wake();
358        }
359    }
360}
361
362/// A prepared model with shared immutable resources and fixed-address private
363/// input, output, scratch, and graph storage for each bounded inference slot.
364#[derive(Clone)]
365pub struct PreparedModel {
366    pool: Arc<Pool>,
367}
368impl PreparedModel {
369    /// Build each slot once, including its actual input/output bindings. The
370    /// builder may share immutable weights, but must allocate independent
371    /// writable storage for each slot. Host staging is created on first use.
372    pub fn prepare(
373        context: &ModelContext,
374        capacity: usize,
375        mut build: impl FnMut(&ModelContext) -> Result<InferenceGraph>,
376    ) -> Result<Self> {
377        if capacity == 0 {
378            return Err(Error::Message("inference capacity must be nonzero".into()));
379        }
380        let mut slots: Vec<Slot> = Vec::with_capacity(capacity);
381        for _ in 0..capacity {
382            let InferenceGraph {
383                inputs,
384                outputs,
385                graph,
386            } = build(context)?;
387            graph.validate_runtime(&context.runtime)?;
388            for tensor in inputs.iter().chain(&outputs) {
389                context.validate(tensor)?;
390                tensor.producer.wait()?;
391                if let Some(view) = tensor.binding()
392                    && slots
393                        .iter()
394                        .flat_map(|slot| slot.inputs.iter().chain(&slot.outputs))
395                        .filter_map(DeviceTensor::binding)
396                        .any(|old| view.overlaps(&old))
397                {
398                    return Err(Error::Message("inference slots share writable IO".into()));
399                }
400            }
401            if let Some(first) = slots.first()
402                && (inputs
403                    .iter()
404                    .map(DeviceTensor::desc)
405                    .ne(first.inputs.iter().map(DeviceTensor::desc))
406                    || outputs
407                        .iter()
408                        .map(DeviceTensor::desc)
409                        .ne(first.outputs.iter().map(DeviceTensor::desc)))
410            {
411                return Err(Error::Message("inference slot descriptors differ".into()));
412            }
413            slots.push(Slot {
414                inputs,
415                outputs,
416                upload: Mutex::new(None),
417                download: Mutex::new(None),
418                graph,
419            });
420        }
421        Ok(Self {
422            pool: Arc::new(Pool {
423                context: context.clone(),
424                slots,
425                state: Mutex::new(PoolState {
426                    slots: (0..capacity).map(|_| SlotState::default()).collect(),
427                    wakers: Vec::new(),
428                }),
429                changed: Condvar::new(),
430            }),
431        })
432    }
433    /// Number of resident inference slots.
434    pub fn capacity(&self) -> usize {
435        self.pool.slots.len()
436    }
437    /// Whether every slot is unleased and drained, suitable for idle cache eviction.
438    pub fn is_idle(&self) -> bool {
439        self.pool
440            .state
441            .lock()
442            .unwrap_or_else(|e| e.into_inner())
443            .slots
444            .iter()
445            .all(|slot| {
446                !slot.leased && slot.completion.as_ref().is_none_or(Completion::is_complete)
447            })
448    }
449    /// Reserve an idle slot without waiting. Retained outputs apply backpressure.
450    pub fn try_acquire(&self) -> Result<ModelSlot> {
451        let mut state = self.pool.state.lock().unwrap_or_else(|e| e.into_inner());
452        self.acquire_locked(&mut state)
453    }
454    fn acquire_locked(&self, state: &mut PoolState) -> Result<ModelSlot> {
455        let mut failure = None;
456        for (index, slot) in state.slots.iter_mut().enumerate() {
457            if slot.leased {
458                continue;
459            }
460            if let Some(completion) = &slot.completion
461                && !completion.is_complete()
462            {
463                continue;
464            }
465            // Cancellation and failed producers do not poison untouched storage.
466            // Native failures do: leave that slot disabled, but try healthy peers.
467            if let Err(error) = self.pool.slots[index].graph.usable() {
468                failure = Some(error);
469                continue;
470            }
471            slot.completion = None;
472            slot.leased = true;
473            return Ok(ModelSlot {
474                lease: Arc::new(Lease {
475                    pool: self.pool.clone(),
476                    index,
477                }),
478            });
479        }
480        if state.slots.iter().all(|slot| {
481            !slot.leased && slot.completion.as_ref().is_none_or(Completion::is_complete)
482        }) && let Some(error) = failure
483        {
484            return Err(error);
485        }
486        Err(Error::Busy(
487            "all inference slots are retained or running".into(),
488        ))
489    }
490    /// Wait for capacity without growing the queue or duplicating weights.
491    pub fn acquire_blocking(&self) -> Result<ModelSlot> {
492        loop {
493            let mut state = self.pool.state.lock().unwrap_or_else(|e| e.into_inner());
494            match self.acquire_locked(&mut state) {
495                Ok(slot) => return Ok(slot),
496                Err(Error::Busy(_)) => {}
497                Err(error) => return Err(error),
498            }
499            if let Some(completion) = state
500                .slots
501                .iter()
502                .filter(|slot| !slot.leased)
503                .filter_map(|slot| slot.completion.clone())
504                .find(|completion| !completion.is_complete())
505            {
506                drop(state);
507                // Recheck storage after draining, including safe cancellation.
508                let _ = completion.wait();
509            } else {
510                drop(
511                    self.pool
512                        .changed
513                        .wait(state)
514                        .unwrap_or_else(|e| e.into_inner()),
515                );
516            }
517        }
518    }
519    /// Wait asynchronously for a slot; polling never waits for a device.
520    pub fn acquire(&self) -> AcquireSlot<'_> {
521        AcquireSlot { model: self }
522    }
523    /// Reserve a slot and submit immediately, or return `Busy`.
524    pub fn submit(&self, inputs: &[DeviceTensor]) -> Result<Inference> {
525        self.try_acquire()?.submit(inputs)
526    }
527    /// Submit host bytes through preallocated upload storage, without a warm
528    /// path allocation or graph preparation.
529    pub fn submit_host(&self, inputs: &[&[u8]]) -> Result<Inference> {
530        self.try_acquire()?.submit_host(inputs)
531    }
532}
533
534/// Future yielding an exclusively reserved inference slot.
535pub struct AcquireSlot<'a> {
536    model: &'a PreparedModel,
537}
538impl Future for AcquireSlot<'_> {
539    type Output = Result<ModelSlot>;
540    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
541        let mut state = self
542            .model
543            .pool
544            .state
545            .lock()
546            .unwrap_or_else(|e| e.into_inner());
547        match self.model.acquire_locked(&mut state) {
548            Ok(slot) => return Poll::Ready(Ok(slot)),
549            Err(Error::Busy(_)) => {}
550            Err(error) => return Poll::Ready(Err(error)),
551        }
552        let mut ready = false;
553        for slot in state.slots.iter_mut().filter(|slot| !slot.leased) {
554            if let Some(completion) = &mut slot.completion {
555                ready |= Pin::new(completion).poll(cx).is_ready();
556            }
557        }
558        if ready {
559            cx.waker().wake_by_ref();
560        }
561        if !state.wakers.iter().any(|old| old.will_wake(cx.waker())) {
562            state.wakers.push(cx.waker().clone());
563        }
564        Poll::Pending
565    }
566}
567
568/// An exclusively reserved slot. Dropping without submission releases capacity.
569pub struct ModelSlot {
570    lease: Arc<Lease>,
571}
572impl ModelSlot {
573    /// Stage host inputs into this reserved slot and enqueue inference.
574    pub fn submit_host(self, inputs: &[&[u8]]) -> Result<Inference> {
575        let pool = &self.lease.pool;
576        let slot = &pool.slots[self.lease.index];
577        if inputs.len() != slot.inputs.len()
578            || inputs
579                .iter()
580                .zip(&slot.inputs)
581                .any(|(bytes, tensor)| bytes.len() != tensor.desc.bytes())
582        {
583            return Err(Error::Message(
584                "host model input byte count mismatch".into(),
585            ));
586        }
587        self.submit_host_with(|index, destination| {
588            destination.copy_from_slice(inputs[index]);
589            Ok(())
590        })
591    }
592
593    /// Publish directly into the exclusively leased slot's host staging.
594    /// The callback runs once per input, in order, before any GPU work is
595    /// submitted. An error releases the slot without executing the graph.
596    /// Unwritten bytes retain their previous contents: initialize every byte
597    /// the graph can read. This supports packed variable-length inputs without
598    /// building or clearing a capacity-sized temporary host buffer.
599    pub fn submit_host_with(
600        self,
601        mut write: impl FnMut(usize, &mut [u8]) -> Result<()>,
602    ) -> Result<Inference> {
603        let pool = &self.lease.pool;
604        let slot = &pool.slots[self.lease.index];
605        let upload = crate::cached_init(&slot.upload, || {
606            Ok(Arc::new(Transfer::prepare(
607                &pool.context,
608                &slot.inputs,
609                true,
610            )?))
611        })?;
612        for (index, host) in upload.buffers.iter().enumerate() {
613            if let Some(host) = host {
614                write(index, &mut host.map_write()?)?;
615            } else {
616                write(index, &mut [])?;
617            }
618        }
619        let dependencies = if let Some(upload) = &upload.graph {
620            let completion = upload.submit()?;
621            pool.state.lock().unwrap_or_else(|e| e.into_inner()).slots[self.lease.index]
622                .completion = Some(completion.clone());
623            vec![completion]
624        } else {
625            vec![]
626        };
627        self.execute(&dependencies)
628    }
629    /// Copy device inputs into fixed slot addresses and enqueue the model.
630    /// Input producers are ordered and failures propagate without host waits.
631    pub fn submit(self, inputs: &[DeviceTensor]) -> Result<Inference> {
632        let pool = &self.lease.pool;
633        let slot = &pool.slots[self.lease.index];
634        if inputs.len() != slot.inputs.len() {
635            return Err(Error::Message("model input count mismatch".into()));
636        }
637        for (input, expected) in inputs.iter().zip(&slot.inputs) {
638            pool.context.validate(input)?;
639            if input.desc != expected.desc {
640                return Err(Error::Message("model input descriptor mismatch".into()));
641            }
642        }
643        let producers = inputs
644            .iter()
645            .map(|input| input.producer.clone())
646            .collect::<Vec<_>>();
647        let mut copy = pool.context.runtime.graph();
648        let mut copied = false;
649        for (source, destination) in inputs.iter().zip(&slot.inputs) {
650            if let (Some(source), Some(destination)) = (source.binding(), destination.binding()) {
651                copy.copy(destination, source)?;
652                copied = true;
653            }
654        }
655        let dependencies = if copied {
656            let completion = copy.prepare()?.submit_after(&producers)?;
657            pool.state.lock().unwrap_or_else(|e| e.into_inner()).slots[self.lease.index]
658                .completion = Some(completion.clone());
659            vec![completion]
660        } else {
661            producers
662        };
663        self.execute(&dependencies)
664    }
665    fn execute(self, dependencies: &[Completion]) -> Result<Inference> {
666        let pool = &self.lease.pool;
667        let slot = &pool.slots[self.lease.index];
668        let completion = slot.graph.submit_after(dependencies)?;
669        pool.state.lock().unwrap_or_else(|e| e.into_inner()).slots[self.lease.index].completion =
670            Some(completion.clone());
671        let outputs = slot
672            .outputs
673            .iter()
674            .cloned()
675            .map(|mut output| {
676                output.producer = completion.clone();
677                output.lease = Some(self.lease.clone());
678                output
679            })
680            .collect();
681        Ok(Inference {
682            outputs,
683            completion,
684            _lease: self.lease,
685        })
686    }
687}
688
689/// Asynchronous inference outputs and their completion. Dropping the handle
690/// does not cancel execution; exported outputs keep the slot reserved.
691pub struct Inference {
692    outputs: Vec<DeviceTensor>,
693    completion: Completion,
694    _lease: Arc<Lease>,
695}
696impl Inference {
697    /// Queue all outputs into reusable host storage, allocated on first readback.
698    pub fn download(self) -> Result<InferenceReadback> {
699        let pool = &self._lease.pool;
700        let slot = &pool.slots[self._lease.index];
701        let transfer = crate::cached_init(&slot.download, || {
702            Ok(Arc::new(Transfer::prepare(
703                &pool.context,
704                &slot.outputs,
705                false,
706            )?))
707        })?;
708        let completion = if let Some(download) = &transfer.graph {
709            download.submit_after(std::slice::from_ref(&self.completion))?
710        } else {
711            self.completion.clone()
712        };
713        pool.state.lock().unwrap_or_else(|e| e.into_inner()).slots[self._lease.index].completion =
714            Some(completion.clone());
715        Ok(InferenceReadback {
716            completion,
717            lease: self._lease,
718            transfer,
719        })
720    }
721    /// Device outputs, available for dependent submission before completion.
722    pub fn outputs(&self) -> &[DeviceTensor] {
723        &self.outputs
724    }
725    /// Completion observer; cancellation drains submitted work safely.
726    pub fn completion(&self) -> &Completion {
727        &self.completion
728    }
729    /// Wait for successful execution and return owned device outputs.
730    pub fn wait(self) -> Result<Vec<DeviceTensor>> {
731        self.completion.wait()?;
732        Ok(self.outputs)
733    }
734}
735
736/// Readbacks sharing their inference slot's preallocated host storage.
737pub struct InferenceReadback {
738    completion: Completion,
739    lease: Arc<Lease>,
740    transfer: Arc<Transfer>,
741}
742impl InferenceReadback {
743    /// Completion of the final device-to-host copy.
744    pub fn completion(&self) -> &Completion {
745        &self.completion
746    }
747    /// Wait and copy outputs into owned host byte vectors.
748    pub fn wait(self) -> Result<Vec<Vec<u8>>> {
749        self.completion.wait()?;
750        self.bytes()
751    }
752    /// Wait and copy into caller-owned output buffers without allocating.
753    pub fn read_into(self, outputs: &mut [&mut [u8]]) -> Result<()> {
754        let slot = &self.lease.pool.slots[self.lease.index];
755        if outputs.len() != slot.outputs.len()
756            || outputs
757                .iter()
758                .zip(&slot.outputs)
759                .any(|(bytes, tensor)| bytes.len() != tensor.desc.bytes())
760        {
761            return Err(Error::Message("model output byte count mismatch".into()));
762        }
763        self.completion.wait()?;
764        for (out, host) in outputs.iter_mut().zip(&self.transfer.buffers) {
765            if let Some(host) = host {
766                out.copy_from_slice(&host.map_read()?);
767            }
768        }
769        Ok(())
770    }
771    fn bytes(&self) -> Result<Vec<Vec<u8>>> {
772        self.transfer
773            .buffers
774            .iter()
775            .map(|host| {
776                host.as_ref()
777                    .map_or_else(|| Ok(Vec::new()), |host| Ok(host.map_read()?.to_vec()))
778            })
779            .collect()
780    }
781}
782impl Future for InferenceReadback {
783    type Output = Result<Vec<Vec<u8>>>;
784    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
785        match Pin::new(&mut self.completion).poll(cx) {
786            Poll::Pending => Poll::Pending,
787            Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
788            Poll::Ready(Ok(())) => Poll::Ready(self.bytes()),
789        }
790    }
791}
792impl Future for Inference {
793    type Output = Result<Vec<DeviceTensor>>;
794    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
795        match Pin::new(&mut self.completion).poll(cx) {
796            Poll::Pending => Poll::Pending,
797            Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
798            Poll::Ready(Ok(())) => Poll::Ready(Ok(std::mem::take(&mut self.outputs))),
799        }
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806    use crate::tensor::DType;
807    #[test]
808    fn empty_tensors_need_no_device_and_keep_context_identity() {
809        let a = ModelContext::new(RuntimeOptions::default()).unwrap();
810        let b = ModelContext::new(RuntimeOptions::default()).unwrap();
811        let tensor = a
812            .upload(TensorDesc::new(DType::F32, vec![0]).unwrap(), &[])
813            .unwrap();
814        assert!(tensor.binding().is_none());
815        assert!(a.download(&tensor).unwrap().wait().unwrap().is_empty());
816        assert!(b.download(&tensor).is_err());
817        assert!(
818            tensor
819                .view(0, TensorDesc::new(DType::F32, vec![]).unwrap())
820                .is_err()
821        );
822    }
823}