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