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