Skip to main content

cubecl_runtime/
client.rs

1use crate::{
2    config::memory::MemoryPoolsConfig,
3    config::{TypeNameFormatLevel, type_name_format},
4    id::GraphId,
5    kernel::KernelMetadata,
6    logging::ProfileLevel,
7    memory_management::{
8        InstallMemoryPoolsError, MemoryAllocationMode, MemoryConfiguration, MemoryReport,
9        MemoryUsage,
10    },
11    runtime::Runtime,
12    server::{
13        CommunicationId, ComputeServer, CopyDescriptor, CubeCount, Handle, IoError,
14        KernelArguments, MemoryLayout, MemoryLayoutDescriptor, MemoryLayoutPolicy,
15        MemoryLayoutStrategy, ProfileError, ReduceOperation, ServerCommunication, ServerError,
16        ServerUtilities,
17    },
18    storage::{ComputeStorage, ManagedResource},
19    throughput::{
20        KernelConfig, ThroughputBenchmarker, ThroughputCache, ThroughputKey, ThroughputValue,
21    },
22};
23use alloc::{format, string::String, sync::Arc, vec, vec::Vec};
24
25#[cfg(not(target_family = "wasm"))]
26mod lazy;
27use cubecl_common::{
28    bytes::{AllocationProperty, Bytes},
29    device::{Device, DeviceId},
30    device_handle::{CallResultExt, DeviceHandle},
31    profile::ProfileDuration,
32};
33use cubecl_environment::backtrace::BackTrace;
34use cubecl_environment::future::DynFut;
35use cubecl_ir::{DeviceProperties, ElemType, VectorSize, features::Features};
36use cubecl_zspace::Shape;
37
38#[allow(unused)]
39use cubecl_common::profile::TimingMethod;
40use cubecl_environment::stream::StreamId;
41
42/// The `ComputeClient` is the entry point to require tasks from the `ComputeServer`.
43/// It should be obtained for a specific device via the Compute struct.
44pub struct ComputeClient<R: Runtime> {
45    device: DeviceHandle<R::Server>,
46    utilities: Arc<ServerUtilities<R::Server>>,
47    stream_id: Option<StreamId>,
48}
49
50/// A captured graph produced by [`ComputeClient::stop_capture`]: a recorded
51/// launch sequence that [`replay`](Graph::replay) re-runs against its original
52/// buffers, skipping the launch path it was recorded from. Cheap to clone
53/// (shares one backend graph).
54///
55/// The graph itself lives in the backend server, referenced here only by
56/// [`GraphId`]; this handle holds a reference-counted owner that releases the
57/// backend graph once the last clone drops. The graph replays against the exact
58/// device buffers used during capture. The caller keeps those input/output
59/// [`Handle`]s alive and, each iteration, writes fresh inputs into the input
60/// handles (same device pointers) and reads the output handles after replaying —
61/// see [`ComputeClient::stop_capture`].
62///
63/// **Stream ordering.** [`replay`](Graph::replay) always dispatches on the
64/// stream the graph was captured on, but input writes and output reads go on the
65/// *writing client's* current stream. They are ordered against the replay only
66/// when they land on that same stream, so keep the client pinned to the capture
67/// stream (via [`set_stream`](ComputeClient::set_stream)) — or issue all writes,
68/// replays, and reads from the same unpinned client — for the whole decode loop.
69/// Refreshing inputs from a client on a different stream races the replay and
70/// silently feeds it stale data.
71pub struct Graph<R: Runtime> {
72    inner: Arc<GraphHandle<R>>,
73}
74
75impl<R: Runtime> core::fmt::Debug for Graph<R> {
76    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77        f.debug_struct("Graph")
78            .field("id", &self.inner.id)
79            .field("stream_id", &self.inner.stream_id)
80            .finish()
81    }
82}
83
84/// Reference-counted owner of a backend graph. Its [`Drop`] ships the release to
85/// the server actor, so the last [`Graph`] clone frees the backend graph on the
86/// thread that owns it.
87struct GraphHandle<R: Runtime> {
88    id: GraphId,
89    device: DeviceHandle<R::Server>,
90    stream_id: StreamId,
91}
92
93impl<R: Runtime> Graph<R> {
94    /// Replay the captured launch sequence — every recorded kernel re-run
95    /// against the buffers it was captured with, on the stream it was captured
96    /// on. Self-contained (the handle owns its device handle); no client
97    /// needed.
98    ///
99    /// How much of the launch path this skips depends on the backend: a
100    /// hardware graph (CUDA, HIP) replays as one dispatch, while a software
101    /// graph (wgpu) re-encodes the recorded dispatches from prebuilt state.
102    /// Either way pipeline lookup, binding resolution and metadata upload
103    /// happened once, at capture.
104    ///
105    /// Non-blocking, like a kernel launch: this enqueues the dispatch and returns
106    /// immediately. A replay failure is not reported here — it lands in the
107    /// stream's error queue and surfaces on the next
108    /// [`sync`](ComputeClient::sync)/[`flush`](ComputeClient::flush) (e.g. when
109    /// reading the output back).
110    ///
111    /// # Safety
112    ///
113    /// The dispatch re-runs the recorded kernels against the raw device pointers
114    /// captured with them; nothing validates those buffers still exist or are
115    /// unshared. The caller must guarantee, until the replay's work completes on
116    /// the stream:
117    ///
118    /// - **Liveness** — every [`Handle`] the captured kernels read or wrote is
119    ///   still allocated. Freeing one returns its memory to the pool, and a
120    ///   later replay reads or corrupts whatever the allocator has since placed
121    ///   there.
122    /// - **No concurrent use** — no other stream or thread touches buffers the
123    ///   graph reads or writes while the replay executes; the replay is ordered
124    ///   only against work on its capture stream.
125    /// - **Same-stream refreshes** — input writes and output reads are issued on
126    ///   the capture stream (keep the client pinned to it via
127    ///   [`set_stream`](ComputeClient::set_stream), or do everything from the
128    ///   one client), so they order against the replay instead of racing it.
129    pub unsafe fn replay(&self) {
130        let id = self.inner.id;
131        let stream_id = self.inner.stream_id;
132        self.inner
133            .device
134            .submit(move |server| server.replay(id, stream_id));
135    }
136}
137
138impl<R: Runtime> Clone for Graph<R> {
139    fn clone(&self) -> Self {
140        Self {
141            inner: self.inner.clone(),
142        }
143    }
144}
145
146impl<R: Runtime> Drop for GraphHandle<R> {
147    fn drop(&mut self) {
148        let id = self.id;
149        let stream_id = self.stream_id;
150        // Destroying the raw executable must happen on the server actor (the
151        // only thread allowed to touch it) and only once in-flight replays have
152        // completed — `replay` returns at enqueue time, not completion. Ship the
153        // release to the actor; the backend syncs the stream before it destroys.
154        self.device
155            .submit(move |server| server.graph_destroy(id, stream_id));
156    }
157}
158
159impl<R: Runtime> Clone for ComputeClient<R> {
160    fn clone(&self) -> Self {
161        Self {
162            device: self.device.clone(),
163            utilities: self.utilities.clone(),
164            stream_id: self.stream_id,
165        }
166    }
167}
168
169impl<R: Runtime> ComputeClient<R> {
170    /// Get the info of the current backend.
171    pub fn info(&self) -> &<R::Server as ComputeServer>::Info {
172        &self.utilities.info
173    }
174
175    /// Create a new client with a new server.
176    pub fn init<D: Device>(device: &D, server: R::Server) -> Self {
177        let utilities = server.utilities();
178        let context = DeviceHandle::<R::Server>::insert(device.to_id(), server)
179            .expect("Can't create a new client on an already registered server");
180
181        Self {
182            device: context,
183            utilities,
184            stream_id: None,
185        }
186    }
187
188    /// Load the client for the given device.
189    pub fn load<D: Device>(device: &D) -> Self {
190        let context = DeviceHandle::<R::Server>::new(device.to_id());
191
192        // This is safe because we now know the return type of [`DeviceHandle::utilities()`].
193        let utilities = context
194            .utilities()
195            .downcast::<ServerUtilities<R::Server>>()
196            .expect("Can downcast to `ServerUtilities`");
197
198        Self {
199            device: context,
200            utilities,
201            stream_id: None,
202        }
203    }
204
205    fn stream_id(&self) -> StreamId {
206        match self.stream_id {
207            Some(val) => val,
208            None => StreamId::current(),
209        }
210    }
211
212    /// Set the stream in which the current client is operating on.
213    ///
214    /// # Safety
215    ///
216    /// This is highly unsafe and should probably only be used by the CubeCL/Burn projects for now.
217    pub unsafe fn set_stream(&mut self, stream_id: StreamId) {
218        self.stream_id = Some(stream_id);
219    }
220
221    fn do_read(&self, descriptors: Vec<CopyDescriptor>) -> DynFut<Result<Vec<Bytes>, ServerError>> {
222        let stream_id = self.stream_id();
223        self.device
224            .submit_blocking(move |server| server.read(descriptors, stream_id))
225            .unwrap_or_resume()
226    }
227
228    /// Given bindings, returns owned resources as bytes.
229    pub fn read_async(
230        &self,
231        handles: Vec<Handle>,
232    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
233        let shapes = handles
234            .iter()
235            .map(|it| [it.size_in_used() as usize].into())
236            .collect::<Vec<Shape>>();
237        let descriptors = handles
238            .into_iter()
239            .zip(shapes)
240            .map(|(handle, shape)| CopyDescriptor::new(handle.binding(), shape, [1].into(), 1))
241            .collect();
242
243        self.do_read(descriptors)
244    }
245
246    /// Given bindings, returns owned resources as bytes.
247    ///
248    /// # Remarks
249    ///
250    /// Panics if the read operation fails.
251    pub fn read(&self, handles: Vec<Handle>) -> Vec<Bytes> {
252        cubecl_environment::future::reader::read_sync(self.read_async(handles)).expect("TODO")
253    }
254
255    /// Given a binding, returns owned resource as bytes.
256    pub fn read_one(&self, handle: Handle) -> Result<Bytes, ServerError> {
257        Ok(cubecl_environment::future::reader::read_sync(self.read_async(vec![handle]))?.remove(0))
258    }
259
260    /// Given a binding, returns owned resource as bytes.
261    ///
262    /// # Remarks
263    ///
264    /// Panics if the read operation fails. Useful for tests.
265    pub fn read_one_unchecked(&self, handle: Handle) -> Bytes {
266        cubecl_environment::future::reader::read_sync(self.read_async(vec![handle]))
267            .unwrap()
268            .remove(0)
269    }
270
271    /// Given bindings, returns owned resources as bytes.
272    pub fn read_tensor_async(
273        &self,
274        descriptors: Vec<CopyDescriptor>,
275    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
276        self.do_read(descriptors)
277    }
278
279    /// Given bindings, returns owned resources as bytes.
280    ///
281    /// # Remarks
282    ///
283    /// Panics if the read operation fails.
284    ///
285    /// The tensor must be in the same layout as created by the runtime, or more strict.
286    /// Contiguous tensors are always fine, strided tensors are only ok if the stride is similar to
287    /// the one created by the runtime (i.e. padded on only the last dimension). A way to check
288    /// stride compatibility on the runtime will be added in the future.
289    ///
290    /// Also see [`ComputeClient::create_tensor`].
291    pub fn read_tensor(&self, descriptors: Vec<CopyDescriptor>) -> Vec<Bytes> {
292        cubecl_environment::future::reader::read_sync(self.read_tensor_async(descriptors))
293            .expect("TODO")
294    }
295
296    /// Given a binding, returns owned resource as bytes.
297    /// See [`ComputeClient::read_tensor`]
298    pub fn read_one_tensor_async(
299        &self,
300        descriptor: CopyDescriptor,
301    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
302        let fut = self.read_tensor_async(vec![descriptor]);
303
304        async { Ok(fut.await?.remove(0)) }
305    }
306
307    /// Given a binding, returns owned resource as bytes.
308    ///
309    /// # Remarks
310    ///
311    /// Panics if the read operation fails.
312    /// See [`ComputeClient::read_tensor`]
313    pub fn read_one_unchecked_tensor(&self, descriptor: CopyDescriptor) -> Bytes {
314        self.read_tensor(vec![descriptor]).remove(0)
315    }
316
317    /// Reads the device resource described by `descriptor` lazily.
318    ///
319    /// The returned [`Bytes`] only performs the device-to-host copy on first access (e.g. during
320    /// serialization), keeping the source allocation alive until then. This lets a large number of
321    /// device tensors be serialized without materializing them all in host memory at once: drain
322    /// the [`Bytes`] sequentially rather than holding them all alive.
323    ///
324    /// The data reflects the device state at first access, so the buffer must not be mutated
325    /// between this call and the first read.
326    #[cfg(not(target_family = "wasm"))]
327    pub fn read_lazy(&self, descriptor: CopyDescriptor) -> Bytes {
328        let len = descriptor.shape.iter().product::<usize>() * descriptor.elem_size;
329        let controller = lazy::LazyDeviceController::new(self.clone(), Arc::new(descriptor));
330        // SAFETY: the controller materializes exactly `len` bytes on first access.
331        unsafe { Bytes::from_controller(alloc::boxed::Box::new(controller), len) }
332    }
333
334    /// Reads the device resource described by `descriptor` lazily, async variant.
335    ///
336    /// On native targets the returned future is immediately ready and yields a lazy [`Bytes`]
337    /// whose device-to-host copy is deferred to first access (see [`read_lazy`](Self::read_lazy)).
338    #[cfg(not(target_family = "wasm"))]
339    pub fn read_lazy_async(
340        &self,
341        descriptor: CopyDescriptor,
342    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
343        let len = descriptor.shape.iter().product::<usize>() * descriptor.elem_size;
344        let controller = lazy::LazyDeviceController::new(self.clone(), Arc::new(descriptor));
345        // SAFETY: the controller materializes exactly `len` bytes on first access.
346        let bytes = unsafe { Bytes::from_controller(alloc::boxed::Box::new(controller), len) };
347        core::future::ready(Ok(bytes))
348    }
349
350    /// Reads the device resource described by `descriptor` lazily, async variant.
351    ///
352    /// On `wasm` the deferred copy cannot run inside the synchronous access path, so awaiting
353    /// performs the read eagerly and yields a materialized [`Bytes`]. Awaiting one tensor at a
354    /// time still bounds peak host memory, which is the point of the lazy API.
355    #[cfg(target_family = "wasm")]
356    pub fn read_lazy_async(
357        &self,
358        descriptor: CopyDescriptor,
359    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
360        self.read_one_tensor_async(descriptor)
361    }
362
363    /// Given a resource handle, returns the storage resource.
364    pub fn get_resource(
365        &self,
366        handle: Handle,
367    ) -> Result<
368        ManagedResource<<<R::Server as ComputeServer>::Storage as ComputeStorage>::Resource>,
369        ServerError,
370    > {
371        let stream_id = self.stream_id();
372        let binding = handle.binding();
373
374        self.device
375            .submit_blocking(move |state| state.get_resource(binding, stream_id))
376            .unwrap_or_resume()
377    }
378
379    fn do_create_from_slices(
380        &self,
381        descriptors: Vec<MemoryLayoutDescriptor>,
382        slices: Vec<Vec<u8>>,
383    ) -> Result<Vec<MemoryLayout>, IoError> {
384        let stream_id = self.stream_id();
385        let (handle_base, layouts) = self.utilities.layout_policy.apply(stream_id, &descriptors);
386
387        let descriptors = descriptors
388            .into_iter()
389            .zip(layouts.iter())
390            .zip(slices)
391            .map(|((desc, alloc), data)| {
392                (
393                    CopyDescriptor::new(
394                        alloc.memory.clone().binding(),
395                        desc.shape,
396                        alloc.strides.clone(),
397                        desc.elem_size,
398                    ),
399                    Bytes::from_bytes_vec(data.to_vec()),
400                )
401            })
402            .collect::<Vec<_>>();
403
404        let (size, memory) = (handle_base.size(), handle_base.memory);
405        self.device.submit(move |server| {
406            server.initialize_memory(memory, size, stream_id);
407            server.write(descriptors, stream_id);
408        });
409
410        Ok(layouts)
411    }
412
413    fn do_create(
414        &self,
415        descriptors: Vec<MemoryLayoutDescriptor>,
416        data: Vec<Bytes>,
417    ) -> Result<Vec<MemoryLayout>, IoError> {
418        let stream_id = self.stream_id();
419        let (handle_base, layouts) = self.utilities.layout_policy.apply(stream_id, &descriptors);
420
421        let descriptors = descriptors
422            .into_iter()
423            .zip(layouts.iter())
424            .zip(data)
425            .map(|((desc, layout), data)| {
426                (
427                    CopyDescriptor::new(
428                        layout.memory.clone().binding(),
429                        desc.shape,
430                        layout.strides.clone(),
431                        desc.elem_size,
432                    ),
433                    data,
434                )
435            })
436            .collect::<Vec<_>>();
437
438        let (size, memory) = (handle_base.size(), handle_base.memory);
439        self.device.submit(move |server| {
440            server.initialize_memory(memory, size, stream_id);
441            server.write(descriptors, stream_id);
442        });
443
444        Ok(layouts)
445    }
446
447    /// Returns a resource handle containing the given data.
448    ///
449    /// # Notes
450    ///
451    /// Prefer using the more efficient [`Self::create`] function.
452    pub fn create_from_slice(&self, slice: &[u8]) -> Handle {
453        let shape: Shape = [slice.len()].into();
454
455        self.do_create_from_slices(
456            vec![MemoryLayoutDescriptor::new(
457                MemoryLayoutStrategy::Contiguous,
458                shape,
459                1,
460            )],
461            vec![slice.to_vec()],
462        )
463        .unwrap()
464        .remove(0)
465        .memory
466    }
467
468    /// todo: docs
469    pub fn exclusive<'a, Re: Send + 'static, F: FnOnce() -> Re + Send + 'a>(
470        &'a self,
471        task: F,
472    ) -> Result<Re, ServerError> {
473        // We then launch the task.
474        self.device
475            .exclusive(task)
476            .map_err(|err| ServerError::Generic {
477                reason: format!("{err:?}"),
478                backtrace: BackTrace::capture(),
479            })
480    }
481
482    /// dodo: Docs
483    pub fn memory_persistent_allocation<
484        'a,
485        Re: Send,
486        Input: Send,
487        F: FnOnce(Input) -> Re + Send + 'a,
488    >(
489        &'a self,
490        input: Input,
491        task: F,
492    ) -> Result<Re, ServerError> {
493        let stream_id = StreamId::current();
494
495        self.device.submit(move |server| {
496            server.allocation_mode(MemoryAllocationMode::Persistent, stream_id);
497        });
498
499        // All tasks created on the same stream will have persistent memory.
500        let output = task(input);
501
502        self.device.submit(move |server| {
503            server.allocation_mode(MemoryAllocationMode::Auto, stream_id);
504        });
505
506        Ok(output)
507    }
508
509    /// Write `data` into an existing allocation, in place (same device pointer).
510    ///
511    /// This is how a captured [`Graph`]'s inputs are refreshed between replays:
512    /// the graph records raw device pointers, so new input bytes must land in
513    /// the very buffer the capture read from. Issue it from the capture stream
514    /// (see the stream-ordering notes on [`Graph`]) so the write orders against
515    /// the replays instead of racing them.
516    ///
517    /// Non-blocking: the write is enqueued on this client's current stream.
518    pub fn write(&self, handle: &Handle, data: Bytes) {
519        let stream_id = self.stream_id();
520        let descriptor =
521            CopyDescriptor::new(handle.clone().binding(), [data.len()].into(), [1].into(), 1);
522        self.device.submit(move |server| {
523            server.write(vec![(descriptor, data)], stream_id);
524        });
525    }
526
527    /// Returns a resource handle containing the given [Bytes].
528    pub fn create(&self, data: Bytes) -> Handle {
529        let shape = [data.len()].into();
530
531        self.do_create(
532            vec![MemoryLayoutDescriptor::new(
533                MemoryLayoutStrategy::Contiguous,
534                shape,
535                1,
536            )],
537            vec![data],
538        )
539        .unwrap()
540        .remove(0)
541        .memory
542    }
543
544    /// Given a resource and shape, stores it and returns the tensor handle and strides.
545    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
546    /// should be taken when indexing.
547    ///
548    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
549    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
550    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
551    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
552    /// can load as much data as possible in a single instruction. It may be aligned even more to
553    /// also take cache lines into account.
554    ///
555    /// However, the stride must be taken into account when indexing and reading the tensor
556    /// (also see [`ComputeClient::read_tensor`]).
557    ///
558    /// # Notes
559    ///
560    /// Prefer using [`Self::create_tensor`] for better performance.
561    pub fn create_tensor_from_slice(
562        &self,
563        slice: &[u8],
564        shape: Shape,
565        elem_size: usize,
566    ) -> MemoryLayout {
567        self.do_create_from_slices(
568            vec![MemoryLayoutDescriptor::new(
569                MemoryLayoutStrategy::Optimized,
570                shape,
571                elem_size,
572            )],
573            vec![slice.to_vec()],
574        )
575        .unwrap()
576        .remove(0)
577    }
578
579    /// Given a resource and shape, stores it and returns the tensor handle and strides.
580    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
581    /// should be taken when indexing.
582    ///
583    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
584    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
585    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
586    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
587    /// can load as much data as possible in a single instruction. It may be aligned even more to
588    /// also take cache lines into account.
589    ///
590    /// However, the stride must be taken into account when indexing and reading the tensor
591    /// (also see [`ComputeClient::read_tensor`]).
592    pub fn create_tensor(&self, bytes: Bytes, shape: Shape, elem_size: usize) -> MemoryLayout {
593        self.do_create(
594            vec![MemoryLayoutDescriptor::new(
595                MemoryLayoutStrategy::Optimized,
596                shape,
597                elem_size,
598            )],
599            vec![bytes],
600        )
601        .unwrap()
602        .remove(0)
603    }
604
605    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
606    /// handle, and returns the handles for them.
607    /// See [`ComputeClient::create_tensor`]
608    ///
609    /// # Notes
610    ///
611    /// Prefer using [`Self::create_tensors`] for better performance.
612    pub fn create_tensors_from_slices(
613        &self,
614        descriptors: Vec<(MemoryLayoutDescriptor, &[u8])>,
615    ) -> Vec<MemoryLayout> {
616        let mut data = Vec::with_capacity(descriptors.len());
617        let mut descriptors_ = Vec::with_capacity(descriptors.len());
618        for (a, b) in descriptors {
619            data.push(b.to_vec());
620            descriptors_.push(a);
621        }
622
623        self.do_create_from_slices(descriptors_, data).unwrap()
624    }
625
626    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
627    /// handle, and returns the handles for them.
628    /// See [`ComputeClient::create_tensor`]
629    pub fn create_tensors(
630        &self,
631        descriptors: Vec<(MemoryLayoutDescriptor, Bytes)>,
632    ) -> Vec<MemoryLayout> {
633        let (descriptors, data) = descriptors.into_iter().unzip();
634
635        self.do_create(descriptors, data).unwrap()
636    }
637
638    fn do_empty(
639        &self,
640        descriptors: Vec<MemoryLayoutDescriptor>,
641    ) -> Result<Vec<MemoryLayout>, IoError> {
642        let stream_id = self.stream_id();
643        let (handle_base, layouts) = self.utilities.layout_policy.apply(stream_id, &descriptors);
644
645        let (size, memory) = (handle_base.size(), handle_base.memory);
646        self.device.submit(move |server| {
647            server.initialize_memory(memory, size, stream_id);
648        });
649
650        Ok(layouts)
651    }
652
653    /// Reserves `size` bytes in the storage, and returns a handle over them.
654    pub fn empty(&self, size: usize) -> Handle {
655        let shape: Shape = [size].into();
656        let descriptor = MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, 1);
657        self.do_empty(vec![descriptor]).unwrap().remove(0).memory
658    }
659
660    /// Reserves `shape` in the storage, and returns a tensor handle for it.
661    /// See [`ComputeClient::create_tensor`]
662    pub fn empty_tensor(&self, shape: Shape, elem_size: usize) -> MemoryLayout {
663        let descriptor =
664            MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size);
665        self.do_empty(vec![descriptor]).unwrap().remove(0)
666    }
667
668    /// Reserves all `shapes` in a single storage buffer, and returns the handles for them.
669    /// See [`ComputeClient::create_tensor`]
670    pub fn empty_tensors(&self, descriptors: Vec<MemoryLayoutDescriptor>) -> Vec<MemoryLayout> {
671        self.do_empty(descriptors).unwrap()
672    }
673
674    /// Marks the given [Bytes] as being a staging buffer, maybe transferring it to pinned memory
675    /// for faster data transfer with compute device.
676    ///
677    /// TODO: This blocks the compute queue, so it will drop the compute utilization.
678    pub fn staging<'a, I>(&self, bytes: I, file_only: bool)
679    where
680        I: Iterator<Item = &'a mut Bytes>,
681    {
682        let has_staging = |b: &Bytes| match b.property() {
683            AllocationProperty::Pinned => false,
684            AllocationProperty::File => true,
685            // A lazily device-backed buffer materializes on access and is staged (if needed)
686            // by the backend write path, so don't force it into a host staging buffer here.
687            AllocationProperty::Device => false,
688            AllocationProperty::Native | AllocationProperty::Other => !file_only,
689        };
690
691        let mut to_be_updated = Vec::new();
692        let sizes = bytes
693            .filter_map(|b| match has_staging(b) {
694                true => {
695                    let len = b.len();
696                    to_be_updated.push(b);
697                    Some(len)
698                }
699                false => None,
700            })
701            .collect::<Vec<usize>>();
702
703        if sizes.is_empty() {
704            return;
705        }
706
707        let stream_id = self.stream_id();
708        let sizes = sizes.to_vec();
709        let stagings = self
710            .device
711            .submit_blocking(move |server| server.staging(&sizes, stream_id))
712            .unwrap_or_resume();
713
714        let stagings = match stagings {
715            Ok(val) => val,
716            Err(_) => return,
717        };
718
719        to_be_updated
720            .into_iter()
721            .zip(stagings)
722            .for_each(|(b, mut staging)| {
723                b.copy_into(&mut staging);
724                core::mem::swap(b, &mut staging);
725            });
726    }
727
728    /// Transfer data from one client to another
729    #[cfg_attr(
730        feature = "tracing",
731        tracing::instrument(level = "trace", skip(self, src, dst_server))
732    )]
733    pub fn to_client(&mut self, src: Handle, dst_server: &Self, dtype: ElemType) -> Handle {
734        let shape = [src.size_in_used() as usize];
735        let src_descriptor = src.copy_descriptor(shape.into(), [1].into(), 1);
736
737        if R::Server::SERVER_COMM_ENABLED {
738            self.to_client_tensor(src_descriptor, dst_server, dtype)
739        } else {
740            let alloc_desc = MemoryLayoutDescriptor::new(
741                MemoryLayoutStrategy::Contiguous,
742                src_descriptor.shape.clone(),
743                src_descriptor.elem_size,
744            );
745            self.change_client_sync(src_descriptor, alloc_desc, dst_server)
746                .memory
747        }
748    }
749
750    /// Perform an `all_reduce` operation on the given devices.
751    #[cfg_attr(
752        feature = "tracing",
753        tracing::instrument(level = "trace", skip(self, device_ids))
754    )]
755    pub fn ensure_init_collective(&mut self, device_ids: Vec<DeviceId>) {
756        let comm_id = CommunicationId::from(device_ids.clone());
757        let is_comms_init = self.utilities.initialized_comms.read().contains(&comm_id);
758        if !is_comms_init {
759            self.device
760                .submit(move |server| server.comm_init(device_ids).unwrap());
761            let mut initialized_comms = self.utilities.initialized_comms.write();
762            initialized_comms.insert(comm_id);
763            // Flush immediately so other devices aren't blocked waiting on this initialization.
764            self.device.flush_queue();
765        }
766    }
767
768    /// Wait on the communication stream.
769    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
770    pub fn sync_collective(&self) {
771        if DeviceHandle::<R::Server>::is_blocking() {
772            panic!("Can't use `sync_collective` with a blocking device handle");
773        }
774        let stream_id = self.stream_id();
775
776        self.device.submit(move |server| {
777            server.sync_collective(stream_id).unwrap();
778        });
779
780        // We don't actually need or want to sync the server here, but we need to make sure any
781        // task enqueued on the communication channel is done.
782        self.device.flush_queue();
783    }
784
785    /// Perform an `all_reduce` operation on the given devices.
786    #[cfg_attr(
787        feature = "tracing",
788        tracing::instrument(level = "trace", skip(self, src, dst, dtype, device_ids, op))
789    )]
790    pub fn all_reduce(
791        &mut self,
792        src: Handle,
793        dst: Handle,
794        dtype: ElemType,
795        device_ids: Vec<DeviceId>,
796        op: ReduceOperation,
797    ) {
798        if DeviceHandle::<R::Server>::is_blocking() {
799            panic!("Can't use `all_reduce` with a blocking device handle");
800        }
801
802        let stream_id = self.stream_id();
803        let src = src.binding();
804        let dst = dst.binding();
805
806        self.ensure_init_collective(device_ids.clone());
807
808        self.device.submit(move |server| {
809            server
810                .all_reduce(src, dst, dtype, stream_id, op, device_ids)
811                .unwrap();
812        });
813    }
814
815    /// Transfer data from one client to another
816    ///
817    /// Make sure the source description can be read in a contiguous manner.
818    #[cfg_attr(
819        feature = "tracing",
820        tracing::instrument(level = "trace", skip(self, src_descriptor, dst_server))
821    )]
822    pub fn to_client_tensor(
823        &mut self,
824        src_descriptor: CopyDescriptor,
825        dst_server: &Self,
826        dtype: ElemType,
827    ) -> Handle {
828        let stream_id_src = self.stream_id();
829        let stream_id_dst = dst_server.stream_id();
830
831        let device_id_src = self.device.device_id();
832        let device_id_dst = dst_server.device.device_id();
833
834        let mut dst_server = dst_server.clone();
835        let handle = Handle::new(stream_id_dst, src_descriptor.handle.size_in_used());
836        let handle_cloned = handle.clone();
837
838        let device_ids = vec![device_id_src, device_id_dst];
839        self.ensure_init_collective(device_ids.clone());
840        dst_server.ensure_init_collective(device_ids);
841
842        self.device.submit(move |server_src| {
843            server_src
844                .send(src_descriptor, dtype, stream_id_src, device_id_dst)
845                .unwrap()
846        });
847
848        dst_server.device.submit(move |server_dst| {
849            server_dst
850                .recv(handle_cloned, dtype, stream_id_dst, device_id_src)
851                .unwrap();
852            server_dst.sync_collective(stream_id_dst).unwrap();
853        });
854
855        // `ServerCommunication::send` and`ServerCommunication::recv` are blocking: they each wait for the corresponding recv/send
856        // call to be made. We flush the operations right away so that the neither server ends up in a deadlock.
857        // The actual data transfer is still executed asynchronously on the communication stream.
858        self.device.flush_queue();
859        dst_server.device.flush_queue();
860
861        handle
862    }
863
864    #[track_caller]
865    #[cfg_attr(feature = "tracing", tracing::instrument(level="trace",
866        skip(self, kernel, bindings),
867        fields(
868            kernel.name = %kernel.name(),
869            kernel.id = %kernel.id(),
870        )
871    ))]
872    unsafe fn launch_inner(
873        &self,
874        kernel: <R::Server as ComputeServer>::Kernel,
875        count: CubeCount,
876        bindings: KernelArguments,
877        stream_id: StreamId,
878    ) {
879        // No work, and some drivers reject a zero grid dim.
880        if let CubeCount::Static(x, y, z) = &count
881            && (*x == 0 || *y == 0 || *z == 0)
882        {
883            return;
884        }
885
886        // Decided here, on the issuing thread, because that is the only place
887        // that still knows whether this launch is an autotune measurement — by
888        // the time it reaches the server thread, that context is gone.
889        let launch_mode = crate::dry_run::launch_mode();
890
891        let level = self.utilities.logger.profile_level();
892
893        match level {
894            None | Some(ProfileLevel::ExecutionOnly) => {
895                let utilities = self.utilities.clone();
896                self.device.submit(move |state| {
897                    let name = kernel.name();
898                    unsafe { state.launch(kernel, count, bindings, stream_id, launch_mode) };
899
900                    if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
901                        let info = type_name_format(name, TypeNameFormatLevel::Balanced);
902                        utilities.logger.register_execution(info);
903                    }
904                });
905            }
906            Some(level) => {
907                let name = kernel.name();
908                let kernel_id = kernel.id();
909                let context = self.device.clone();
910                let count_moved = count.clone();
911                let (result, profile) = self
912                    .profile(
913                        move || {
914                            context
915                                .submit_blocking(move |state| unsafe {
916                                    state.launch(
917                                        kernel,
918                                        count_moved,
919                                        bindings,
920                                        stream_id,
921                                        launch_mode,
922                                    )
923                                })
924                                .unwrap_or_resume()
925                        },
926                        name,
927                    )
928                    .unwrap();
929                let info = match level {
930                    ProfileLevel::Full => {
931                        format!("{name}: {kernel_id} CubeCount {count:?}")
932                    }
933                    _ => type_name_format(name, TypeNameFormatLevel::Balanced),
934                };
935                self.utilities.logger.register_profiled(info, profile);
936                result
937            }
938        }
939    }
940
941    /// Launches the `kernel` with the given `bindings`.
942    #[track_caller]
943    pub fn launch(
944        &self,
945        kernel: <R::Server as ComputeServer>::Kernel,
946        count: CubeCount,
947        bindings: KernelArguments,
948    ) {
949        unsafe { self.launch_inner(kernel, count, bindings, self.stream_id()) }
950    }
951
952    /// Flush all outstanding commands.
953    pub fn flush(&self) -> Result<(), ServerError> {
954        let stream_id = self.stream_id();
955
956        self.device
957            .submit_blocking(move |server| server.flush(stream_id))
958            .unwrap_or_resume()
959    }
960
961    /// Prepare this client's stream for a graph capture (see
962    /// [`ComputeServer::graph_prepare`]) — enable the persistent pool + capture
963    /// recording. Call this **before** the warmup run, then
964    /// [`start_capture`](Self::start_capture) around the run to record.
965    pub fn graph_prepare(&self) -> Result<(), ServerError> {
966        let stream_id = self.stream_id();
967        self.device
968            .submit_blocking(move |server| server.graph_prepare(stream_id))
969            .unwrap_or_resume()
970    }
971
972    /// Begin recording launches on this client's stream into a graph rather
973    /// than executing them (see [`ComputeServer::begin_capture`]). Pin the
974    /// client to a dedicated stream with [`set_stream`](Self::set_stream), then
975    /// [`graph_prepare`](Self::graph_prepare) and warm up first.
976    ///
977    /// Between this and [`stop_capture`](Self::stop_capture) the window records
978    /// launches and nothing else: reading, syncing or profiling the stream is
979    /// refused, and so is writing to a handle — a recorded graph cannot carry a
980    /// host copy, so feed fresh inputs by writing *between* replays instead. A
981    /// refused write is reported late, by failing `stop_capture`, rather than
982    /// handing back a graph that silently skips it. Fresh allocation inside the
983    /// window is fatal on a hardware-graph backend and merely wasteful on a
984    /// software-graph one, which is what the warmup run exists to avoid.
985    ///
986    /// Returns an error on backends without graph support.
987    pub fn start_capture(&self) -> Result<(), ServerError> {
988        let stream_id = self.stream_id();
989        self.device
990            .submit_blocking(move |server| server.begin_capture(stream_id))
991            .unwrap_or_resume()
992    }
993
994    /// Stop recording and return the captured graph, ready to
995    /// [`replay`](Graph::replay).
996    pub fn stop_capture(&self) -> Result<Graph<R>, ServerError> {
997        let stream_id = self.stream_id();
998        let id = self
999            .device
1000            .submit_blocking(move |server| server.end_capture(stream_id))
1001            .unwrap_or_resume()?;
1002
1003        Ok(Graph {
1004            inner: Arc::new(GraphHandle {
1005                id,
1006                device: self.device.clone(),
1007                stream_id,
1008            }),
1009        })
1010    }
1011
1012    /// Wait for the completion of every task in the server.
1013    pub fn sync(&self) -> DynFut<Result<(), ServerError>> {
1014        let stream_id = self.stream_id();
1015
1016        let fut = self
1017            .device
1018            .submit_blocking(move |server| server.sync(stream_id))
1019            .unwrap_or_resume();
1020
1021        self.utilities.logger.profile_summary();
1022
1023        fut
1024    }
1025
1026    /// Get the features supported by the compute server.
1027    pub fn properties(&self) -> &DeviceProperties {
1028        &self.utilities.properties
1029    }
1030
1031    /// Get the features supported by the compute server.
1032    pub fn features(&self) -> &Features {
1033        &self.utilities.properties.features
1034    }
1035
1036    /// # Warning
1037    ///
1038    /// For private use only.
1039    pub fn properties_mut(&mut self) -> Option<&mut DeviceProperties> {
1040        Arc::get_mut(&mut self.utilities).map(|state| &mut state.properties)
1041    }
1042
1043    /// Total memory usage across all streams on this client's device.
1044    ///
1045    /// The closure iterates the server's `stream_ids()` and folds each
1046    /// per-stream `memory_usage(id)` with `MemoryUsage::combine`, so the
1047    /// result is correct regardless of which thread queries it.
1048    pub fn memory_usage(&self) -> Result<MemoryUsage, ServerError> {
1049        self.device
1050            .submit_blocking(move |server| {
1051                server
1052                    .stream_ids()
1053                    .into_iter()
1054                    .try_fold(MemoryUsage::default(), |acc, id| {
1055                        Ok(acc.combine(server.memory_usage(id)?))
1056                    })
1057            })
1058            .unwrap_or_resume()
1059    }
1060
1061    /// Structured per-pool report of the **calling stream's** main GPU memory:
1062    /// each pool's shape, usage, and high-water marks, in allocation-routing
1063    /// order.
1064    ///
1065    /// The read side of a measured memory plan — install a layout with
1066    /// [`install_memory_pools`](Self::install_memory_pools), measure under a
1067    /// [`DryRun`](crate::dry_run::DryRun), cap at the observed peaks; the full
1068    /// cycle is on [`MemoryReport`].
1069    ///
1070    /// Unlike [`memory_usage`](Self::memory_usage), which aggregates across
1071    /// streams, this reads one stream: pools are per stream, and a plan is
1072    /// measured and installed on the stream that runs the workload.
1073    pub fn memory_report(&self) -> Result<MemoryReport, ServerError> {
1074        let stream_id = self.stream_id();
1075        self.device
1076            .submit_blocking(move |server| server.memory_report(stream_id))
1077            .unwrap_or_resume()
1078    }
1079
1080    /// Get all devices of a specific type available to this runtime
1081    pub fn enumerate_devices(&self, type_id: u16) -> Vec<DeviceId> {
1082        R::enumerate_devices(type_id, self.info())
1083    }
1084
1085    /// Get all devices available to this runtime
1086    pub fn enumerate_all_devices(&self) -> Vec<DeviceId> {
1087        R::enumerate_all_devices(self.info())
1088    }
1089
1090    /// Get the number of devices of a specific type available to this runtime
1091    pub fn device_count(&self, type_id: u16) -> usize {
1092        self.enumerate_devices(type_id).len()
1093    }
1094
1095    /// Get the number of devices of a specific type available to this runtime
1096    pub fn device_count_total(&self) -> usize {
1097        self.enumerate_all_devices().len()
1098    }
1099
1100    /// Change the memory allocation mode.
1101    ///
1102    /// # Safety
1103    ///
1104    /// This function isn't thread safe and might create memory leaks.
1105    pub unsafe fn allocation_mode(&self, mode: MemoryAllocationMode) {
1106        let stream_id = self.stream_id();
1107        self.device
1108            .submit(move |server| server.allocation_mode(mode, stream_id));
1109    }
1110
1111    /// Ask the client to release memory that it can release.
1112    ///
1113    /// Nb: Results will vary on what the memory allocator deems beneficial,
1114    /// so it's not guaranteed any memory is freed.
1115    pub fn memory_cleanup(&self) {
1116        self.device.submit(move |server| {
1117            for id in server.stream_ids() {
1118                server.memory_cleanup(id);
1119            }
1120        });
1121    }
1122
1123    /// Install a new dynamic-pool layout for the device's main GPU memory.
1124    ///
1125    /// This replaces the pools themselves, not just a setting they read. It
1126    /// lands in two places:
1127    ///
1128    /// - **The calling stream's pools are rebuilt in place**, discarding the
1129    ///   old ones — which is why it only happens when nothing is live in them,
1130    ///   and why the high-water marks in
1131    ///   [`memory_report`](Self::memory_report) start over.
1132    /// - **The layout becomes the one every stream created afterwards is
1133    ///   built with.** Other streams that already exist keep theirs; memory is
1134    ///   per stream, and rebuilding a stream this call is not synchronized
1135    ///   with would swap pools under its live slices.
1136    ///
1137    /// Pool layouts are a purely programmatic, runtime setting — there is no
1138    /// config-file pathway — sized per workload (e.g. per model, just before
1139    /// loading it), so install at a quiescent point such as right after
1140    /// unloading a model. Auxiliary pools (pinned CPU, staging, uniforms) and
1141    /// the persistent pool are never affected.
1142    ///
1143    /// # Errors
1144    ///
1145    /// [`PoolsInUse`](InstallMemoryPoolsError::PoolsInUse) when the current
1146    /// stream kept its old layout because something was still live in its
1147    /// pools — e.g. a garbage-collection task that has not released its
1148    /// cross-stream pins yet, which can lag behind an explicit
1149    /// [`memory_cleanup`](Self::memory_cleanup). Nothing is disturbed, the
1150    /// layout still applies to streams created afterwards, and retrying after
1151    /// the remaining work drains rebuilds the current stream too.
1152    ///
1153    /// [`StreamUnavailable`](InstallMemoryPoolsError::StreamUnavailable) when
1154    /// the current stream is already in an error state; that failure surfaces
1155    /// at the next flush or sync, as usual.
1156    ///
1157    /// [`Unsupported`](InstallMemoryPoolsError::Unsupported) from a runtime
1158    /// with no configurable pools, where retrying will never succeed.
1159    ///
1160    /// # Panics
1161    ///
1162    /// Panics if the layout is invalid (empty list, too many pools, zero page
1163    /// size, slice larger than page, cap smaller than page, unavailable
1164    /// preset) — that is a bad layout literal rather than a runtime condition,
1165    /// and an explicit layout that cannot be honored must not be silently
1166    /// replaced.
1167    pub fn install_memory_pools(
1168        &self,
1169        pools: &MemoryPoolsConfig,
1170    ) -> Result<(), InstallMemoryPoolsError> {
1171        let config =
1172            match MemoryConfiguration::default().resolve(Some(pools), &self.properties().memory) {
1173                Ok(config) => config,
1174                Err(err) => panic!("Invalid memory pools configuration: {err}"),
1175            };
1176        let stream_id = self.stream_id();
1177        self.device
1178            .submit_blocking(move |server| server.install_memory_pools(config, stream_id))
1179            .unwrap_or_resume()
1180    }
1181
1182    /// Measure the execution time of some inner operations.
1183    #[track_caller]
1184    pub fn profile<O: Send + 'static>(
1185        &self,
1186        func: impl FnOnce() -> O + Send,
1187        #[allow(unused)] func_name: &str,
1188    ) -> Result<(O, ProfileDuration), ProfileError> {
1189        // Get the outer caller. For execute() this points straight to the
1190        // cube kernel. For general profiling it points to whoever calls profile.
1191        #[cfg(feature = "profile-tracy")]
1192        let location = std::panic::Location::caller();
1193
1194        // Make a CPU span. If the server has system profiling this is all you need.
1195        #[cfg(feature = "profile-tracy")]
1196        let _span = tracy_client::Client::running().unwrap().span_alloc(
1197            None,
1198            func_name,
1199            location.file(),
1200            location.line(),
1201            0,
1202        );
1203
1204        let stream_id = self.stream_id();
1205
1206        #[cfg(feature = "profile-tracy")]
1207        let gpu_span = if self.utilities.properties.timing_method == TimingMethod::Device {
1208            let gpu_span = self
1209                .utilities
1210                .gpu_client
1211                .span_alloc(func_name, "profile", location.file(), location.line())
1212                .unwrap();
1213            Some(gpu_span)
1214        } else {
1215            None
1216        };
1217
1218        let device = self.device.clone();
1219        #[allow(unused_mut, reason = "Used in profile-tracy")]
1220        let mut result = self
1221            .device
1222            .exclusive(move || {
1223                // We first get mut access to the server to create a token.
1224                // Then we free to server, since it's going to be accessed in `func()`.
1225                let token =
1226                    match device.submit_blocking(move |server| server.start_profile(stream_id)) {
1227                        Ok(token) => match token {
1228                            Ok(token) => token,
1229                            Err(err) => return Err(err),
1230                        },
1231                        Err(err) => {
1232                            return Err(ServerError::Generic {
1233                                reason: alloc::format!(
1234                                    "Can't start profiling because of a call error: {err:?}"
1235                                ),
1236                                backtrace: BackTrace::capture(),
1237                            });
1238                        }
1239                    };
1240
1241                // We execute `func()` which will recursibly access the server.
1242                let out = func();
1243
1244                // Finally we get the result from the token.
1245                let result = device
1246                    .submit_blocking(move |server| {
1247                        let mut result = server.end_profile(stream_id, token);
1248
1249                        match result {
1250                            Ok(result) => Ok((out, result)),
1251                            Err(err) => Err(err),
1252                        }
1253                    })
1254                    .unwrap_or_resume();
1255
1256                Ok(result)
1257            })
1258            .unwrap_or_resume()
1259            .map_err(|err| ProfileError::Unknown {
1260                reason: alloc::format!("{err}"),
1261                backtrace: BackTrace::capture(),
1262            })?;
1263
1264        #[cfg(feature = "profile-tracy")]
1265        if let Some(mut gpu_span) = gpu_span {
1266            gpu_span.end_zone();
1267            let epoch = self.utilities.epoch_time;
1268            // Add in the work to upload the timestamp data.
1269            result = result.map(|(o, result)| {
1270                (
1271                    o,
1272                    ProfileDuration::new(
1273                        alloc::boxed::Box::pin(async move {
1274                            let ticks = result.resolve().await;
1275                            let start_duration =
1276                                ticks.start_duration_since(epoch).as_nanos() as i64;
1277                            let end_duration = ticks.end_duration_since(epoch).as_nanos() as i64;
1278                            gpu_span.upload_timestamp_start(start_duration);
1279                            gpu_span.upload_timestamp_end(end_duration);
1280                            ticks
1281                        }),
1282                        TimingMethod::Device,
1283                    ),
1284                )
1285            });
1286        }
1287
1288        result
1289    }
1290
1291    /// Transfer data from one client to another
1292    #[cfg_attr(
1293        feature = "tracing",
1294        tracing::instrument(
1295            level = "trace",
1296            skip(self, src_descriptor, alloc_descriptor, dst_server)
1297        )
1298    )]
1299    fn change_client_sync(
1300        &self,
1301        src_descriptor: CopyDescriptor,
1302        alloc_descriptor: MemoryLayoutDescriptor,
1303        dst_server: &Self,
1304    ) -> MemoryLayout {
1305        let shape = src_descriptor.shape.clone();
1306        let elem_size = src_descriptor.elem_size;
1307        let stream_id = self.stream_id();
1308
1309        let read = self
1310            .device
1311            .submit_blocking(move |server| server.read(vec![src_descriptor], stream_id))
1312            .unwrap_or_resume();
1313
1314        let mut data = cubecl_environment::future::block_on(read).unwrap();
1315
1316        let (handle_base, mut layouts) = self
1317            .utilities
1318            .layout_policy
1319            .apply(stream_id, &[alloc_descriptor]);
1320        let alloc = layouts.remove(0);
1321
1322        let desc_descriptor = CopyDescriptor {
1323            handle: handle_base.clone().binding(),
1324            shape,
1325            strides: alloc.strides.clone(),
1326            elem_size,
1327        };
1328
1329        let (size, memory) = (handle_base.size(), handle_base.memory);
1330        dst_server.device.submit(move |server| {
1331            server.initialize_memory(memory, size, stream_id);
1332            server.write(vec![(desc_descriptor, data.remove(0))], stream_id)
1333        });
1334
1335        alloc
1336    }
1337
1338    /// Returns all vector sizes that are useful to perform optimal IO operation on the given element.
1339    pub fn io_optimized_vector_sizes(
1340        &self,
1341        size: usize,
1342    ) -> impl Iterator<Item = VectorSize> + Clone {
1343        let load_width = self.properties().hardware.load_width as usize;
1344        let size_bits = size * 8;
1345        let max = load_width / size_bits;
1346        let max = usize::min(self.properties().hardware.max_vector_size, max);
1347
1348        // If the max is 8, we want to test 1, 2, 4, 8 which is log2(8) + 1.
1349        let num_candidates = max.trailing_zeros() + 1;
1350
1351        (0..num_candidates).map(|i| 2usize.pow(i)).rev()
1352    }
1353
1354    /// Stable per-device identity, used to key device-level measurement caches.
1355    fn device_key(&self) -> String {
1356        format!("{}_dev{}", R::name(self), self.device.device_id().index_id)
1357    }
1358
1359    /// Calculates the maximum throughput of the device given the given config (like tensor core with certain sizes and dtypes, or just arithmetic by dtype)
1360    pub fn measure_throughput(
1361        &self,
1362        key: ThroughputKey,
1363        kernel_config: KernelConfig,
1364    ) -> ThroughputValue {
1365        let cache = ThroughputCache::get_for_device(&self.device_key());
1366        let mut throughputs = ThroughputBenchmarker::new(cache);
1367        throughputs.measure(key, kernel_config)
1368    }
1369}