1use 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#[derive(Clone)]
18pub struct ModelContext {
19 runtime: Runtime,
20 compiler: Arc<Mutex<Option<Compiler>>>,
21}
22impl ModelContext {
23 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 pub fn runtime(&self) -> &Runtime {
32 &self.runtime
33 }
34 pub fn compiler(&self) -> Result<Compiler> {
36 crate::cached_init(&self.compiler, || {
37 Compiler::for_target(None, self.runtime.gpu()?.target())
38 })
39 }
40 pub fn allocate(&self, desc: TensorDesc) -> Result<DeviceTensor> {
42 self.allocate_with(desc, MemoryPlacement::GpuLocal)
43 }
44 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 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 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 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 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 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 for dependency in dependencies {
205 dependency.wait()?;
206 }
207 }
208 Ok(output)
209 }
210
211 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 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
244pub struct TensorReadback {
246 buffer: Option<Buffer>,
247 completion: Completion,
248}
249impl TensorReadback {
250 pub fn completion(&self) -> &Completion {
252 &self.completion
253 }
254 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
318pub struct InferenceGraph {
322 pub inputs: Vec<DeviceTensor>,
324 pub outputs: Vec<DeviceTensor>,
326 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#[derive(Clone)]
365pub struct PreparedModel {
366 pool: Arc<Pool>,
367}
368impl PreparedModel {
369 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 pub fn capacity(&self) -> usize {
435 self.pool.slots.len()
436 }
437 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 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 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 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 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 pub fn acquire(&self) -> AcquireSlot<'_> {
521 AcquireSlot { model: self }
522 }
523 pub fn submit(&self, inputs: &[DeviceTensor]) -> Result<Inference> {
525 self.try_acquire()?.submit(inputs)
526 }
527 pub fn submit_host(&self, inputs: &[&[u8]]) -> Result<Inference> {
530 self.try_acquire()?.submit_host(inputs)
531 }
532}
533
534pub 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
568pub struct ModelSlot {
570 lease: Arc<Lease>,
571}
572impl ModelSlot {
573 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 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 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
689pub struct Inference {
692 outputs: Vec<DeviceTensor>,
693 completion: Completion,
694 _lease: Arc<Lease>,
695}
696impl Inference {
697 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 pub fn outputs(&self) -> &[DeviceTensor] {
723 &self.outputs
724 }
725 pub fn completion(&self) -> &Completion {
727 &self.completion
728 }
729 pub fn wait(self) -> Result<Vec<DeviceTensor>> {
731 self.completion.wait()?;
732 Ok(self.outputs)
733 }
734}
735
736pub struct InferenceReadback {
738 completion: Completion,
739 lease: Arc<Lease>,
740 transfer: Arc<Transfer>,
741}
742impl InferenceReadback {
743 pub fn completion(&self) -> &Completion {
745 &self.completion
746 }
747 pub fn wait(self) -> Result<Vec<Vec<u8>>> {
749 self.completion.wait()?;
750 self.bytes()
751 }
752 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}