Skip to main content

cubecl_runtime/
client.rs

1use crate::{
2    config::{TypeNameFormatLevel, type_name_format},
3    kernel::KernelMetadata,
4    logging::ProfileLevel,
5    memory_management::{MemoryAllocationMode, MemoryUsage},
6    runtime::Runtime,
7    server::{
8        CommunicationId, ComputeServer, CopyDescriptor, CubeCount, ExecutionMode, Handle, IoError,
9        KernelArguments, MemoryLayout, MemoryLayoutDescriptor, MemoryLayoutPolicy,
10        MemoryLayoutStrategy, ProfileError, ReduceOperation, ServerCommunication, ServerError,
11        ServerUtilities,
12    },
13    storage::{ComputeStorage, ManagedResource},
14};
15use alloc::{format, sync::Arc, vec, vec::Vec};
16use cubecl_common::{
17    backtrace::BackTrace,
18    bytes::{AllocationProperty, Bytes},
19    device::{Device, DeviceId},
20    device_handle::DeviceHandle,
21    future::DynFut,
22    profile::ProfileDuration,
23};
24use cubecl_ir::{DeviceProperties, ElemType, VectorSize, features::Features};
25use cubecl_zspace::Shape;
26
27#[allow(unused)]
28use cubecl_common::profile::TimingMethod;
29use cubecl_common::stream_id::StreamId;
30
31/// The `ComputeClient` is the entry point to require tasks from the `ComputeServer`.
32/// It should be obtained for a specific device via the Compute struct.
33pub struct ComputeClient<R: Runtime> {
34    device: DeviceHandle<R::Server>,
35    utilities: Arc<ServerUtilities<R::Server>>,
36    stream_id: Option<StreamId>,
37}
38
39impl<R: Runtime> Clone for ComputeClient<R> {
40    fn clone(&self) -> Self {
41        Self {
42            device: self.device.clone(),
43            utilities: self.utilities.clone(),
44            stream_id: self.stream_id,
45        }
46    }
47}
48
49impl<R: Runtime> ComputeClient<R> {
50    /// Get the info of the current backend.
51    pub fn info(&self) -> &<R::Server as ComputeServer>::Info {
52        &self.utilities.info
53    }
54
55    /// Create a new client with a new server.
56    pub fn init<D: Device>(device: &D, server: R::Server) -> Self {
57        let utilities = server.utilities();
58        let context = DeviceHandle::<R::Server>::insert(device.to_id(), server)
59            .expect("Can't create a new client on an already registered server");
60
61        Self {
62            device: context,
63            utilities,
64            stream_id: None,
65        }
66    }
67
68    /// Load the client for the given device.
69    pub fn load<D: Device>(device: &D) -> Self {
70        let context = DeviceHandle::<R::Server>::new(device.to_id());
71
72        // This is safe because we now know the return type of [`DeviceHandle::utilities()`].
73        let utilities = context
74            .utilities()
75            .downcast::<ServerUtilities<R::Server>>()
76            .expect("Can downcast to `ServerUtilities`");
77
78        Self {
79            device: context,
80            utilities,
81            stream_id: None,
82        }
83    }
84
85    fn stream_id(&self) -> StreamId {
86        match self.stream_id {
87            Some(val) => val,
88            None => StreamId::current(),
89        }
90    }
91
92    /// Set the stream in which the current client is operating on.
93    ///
94    /// # Safety
95    ///
96    /// This is highly unsafe and should probably only be used by the CubeCL/Burn projects for now.
97    pub unsafe fn set_stream(&mut self, stream_id: StreamId) {
98        self.stream_id = Some(stream_id);
99    }
100
101    fn do_read(&self, descriptors: Vec<CopyDescriptor>) -> DynFut<Result<Vec<Bytes>, ServerError>> {
102        let stream_id = self.stream_id();
103        self.device
104            .submit_blocking(move |server| server.read(descriptors, stream_id))
105            .unwrap()
106    }
107
108    /// Given bindings, returns owned resources as bytes.
109    pub fn read_async(
110        &self,
111        handles: Vec<Handle>,
112    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
113        let shapes = handles
114            .iter()
115            .map(|it| [it.size_in_used() as usize].into())
116            .collect::<Vec<Shape>>();
117        let descriptors = handles
118            .into_iter()
119            .zip(shapes)
120            .map(|(handle, shape)| CopyDescriptor::new(handle.binding(), shape, [1].into(), 1))
121            .collect();
122
123        self.do_read(descriptors)
124    }
125
126    /// Given bindings, returns owned resources as bytes.
127    ///
128    /// # Remarks
129    ///
130    /// Panics if the read operation fails.
131    pub fn read(&self, handles: Vec<Handle>) -> Vec<Bytes> {
132        cubecl_common::reader::read_sync(self.read_async(handles)).expect("TODO")
133    }
134
135    /// Given a binding, returns owned resource as bytes.
136    pub fn read_one(&self, handle: Handle) -> Result<Bytes, ServerError> {
137        Ok(cubecl_common::reader::read_sync(self.read_async(vec![handle]))?.remove(0))
138    }
139
140    /// Given a binding, returns owned resource as bytes.
141    ///
142    /// # Remarks
143    ///
144    /// Panics if the read operation fails. Useful for tests.
145    pub fn read_one_unchecked(&self, handle: Handle) -> Bytes {
146        cubecl_common::reader::read_sync(self.read_async(vec![handle]))
147            .unwrap()
148            .remove(0)
149    }
150
151    /// Given bindings, returns owned resources as bytes.
152    pub fn read_tensor_async(
153        &self,
154        descriptors: Vec<CopyDescriptor>,
155    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
156        self.do_read(descriptors)
157    }
158
159    /// Given bindings, returns owned resources as bytes.
160    ///
161    /// # Remarks
162    ///
163    /// Panics if the read operation fails.
164    ///
165    /// The tensor must be in the same layout as created by the runtime, or more strict.
166    /// Contiguous tensors are always fine, strided tensors are only ok if the stride is similar to
167    /// the one created by the runtime (i.e. padded on only the last dimension). A way to check
168    /// stride compatibility on the runtime will be added in the future.
169    ///
170    /// Also see [`ComputeClient::create_tensor`].
171    pub fn read_tensor(&self, descriptors: Vec<CopyDescriptor>) -> Vec<Bytes> {
172        cubecl_common::reader::read_sync(self.read_tensor_async(descriptors)).expect("TODO")
173    }
174
175    /// Given a binding, returns owned resource as bytes.
176    /// See [`ComputeClient::read_tensor`]
177    pub fn read_one_tensor_async(
178        &self,
179        descriptor: CopyDescriptor,
180    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
181        let fut = self.read_tensor_async(vec![descriptor]);
182
183        async { Ok(fut.await?.remove(0)) }
184    }
185
186    /// Given a binding, returns owned resource as bytes.
187    ///
188    /// # Remarks
189    ///
190    /// Panics if the read operation fails.
191    /// See [`ComputeClient::read_tensor`]
192    pub fn read_one_unchecked_tensor(&self, descriptor: CopyDescriptor) -> Bytes {
193        self.read_tensor(vec![descriptor]).remove(0)
194    }
195
196    /// Given a resource handle, returns the storage resource.
197    pub fn get_resource(
198        &self,
199        handle: Handle,
200    ) -> Result<
201        ManagedResource<<<R::Server as ComputeServer>::Storage as ComputeStorage>::Resource>,
202        ServerError,
203    > {
204        let stream_id = self.stream_id();
205        let binding = handle.binding();
206
207        self.device
208            .submit_blocking(move |state| state.get_resource(binding, stream_id))
209            .unwrap()
210    }
211
212    fn do_create(
213        &self,
214        descriptors: Vec<MemoryLayoutDescriptor>,
215        mut data: Vec<Bytes>,
216    ) -> Result<Vec<MemoryLayout>, IoError> {
217        self.staging(data.iter_mut(), true);
218
219        let stream_id = self.stream_id();
220        let (handle_base, layouts) = self.utilities.layout_policy.apply(stream_id, &descriptors);
221
222        let descriptors = descriptors
223            .into_iter()
224            .zip(layouts.iter())
225            .zip(data)
226            .map(|((desc, layout), data)| {
227                (
228                    CopyDescriptor::new(
229                        layout.memory.clone().binding(),
230                        desc.shape,
231                        layout.strides.clone(),
232                        desc.elem_size,
233                    ),
234                    // `data` is already owned: hand it to the server as is instead of
235                    // cloning it (and re-aligning the clone) on every upload.
236                    data,
237                )
238            })
239            .collect::<Vec<_>>();
240
241        let (size, memory) = (handle_base.size(), handle_base.memory);
242        self.device.submit(move |server| {
243            server.initialize_memory(memory, size, stream_id);
244            server.write(descriptors, stream_id);
245        });
246
247        Ok(layouts)
248    }
249
250    /// Returns a resource handle containing the given data.
251    ///
252    /// # Notes
253    ///
254    /// Prefer using the more efficient [`Self::create`] function.
255    pub fn create_from_slice(&self, slice: &[u8]) -> Handle {
256        let shape: Shape = [slice.len()].into();
257
258        self.do_create(
259            vec![MemoryLayoutDescriptor::new(
260                MemoryLayoutStrategy::Contiguous,
261                shape,
262                1,
263            )],
264            vec![staged_bytes(slice)],
265        )
266        .unwrap()
267        .remove(0)
268        .memory
269    }
270
271    /// todo: docs
272    pub fn exclusive<'a, Re: Send + 'static, F: FnOnce() -> Re + Send + 'a>(
273        &'a self,
274        task: F,
275    ) -> Result<Re, ServerError> {
276        // We then launch the task.
277        self.device
278            .exclusive(task)
279            .map_err(|err| ServerError::Generic {
280                reason: format!("Communication channel with the server is down: {err:?}"),
281                backtrace: BackTrace::capture(),
282            })
283    }
284
285    /// dodo: Docs
286    pub fn memory_persistent_allocation<
287        'a,
288        Re: Send,
289        Input: Send,
290        F: FnOnce(Input) -> Re + Send + 'a,
291    >(
292        &'a self,
293        input: Input,
294        task: F,
295    ) -> Result<Re, ServerError> {
296        let stream_id = StreamId::current();
297
298        self.device.submit(move |server| {
299            server.allocation_mode(MemoryAllocationMode::Persistent, stream_id);
300        });
301
302        // All tasks created on the same stream will have persistent memory.
303        let output = task(input);
304
305        self.device.submit(move |server| {
306            server.allocation_mode(MemoryAllocationMode::Auto, stream_id);
307        });
308
309        Ok(output)
310    }
311
312    /// Returns a resource handle containing the given [Bytes].
313    pub fn create(&self, data: Bytes) -> Handle {
314        let shape = [data.len()].into();
315
316        self.do_create(
317            vec![MemoryLayoutDescriptor::new(
318                MemoryLayoutStrategy::Contiguous,
319                shape,
320                1,
321            )],
322            vec![data],
323        )
324        .unwrap()
325        .remove(0)
326        .memory
327    }
328
329    /// Given a resource and shape, stores it and returns the tensor handle and strides.
330    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
331    /// should be taken when indexing.
332    ///
333    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
334    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
335    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
336    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
337    /// can load as much data as possible in a single instruction. It may be aligned even more to
338    /// also take cache lines into account.
339    ///
340    /// However, the stride must be taken into account when indexing and reading the tensor
341    /// (also see [`ComputeClient::read_tensor`]).
342    ///
343    /// # Notes
344    ///
345    /// Prefer using [`Self::create_tensor`] for better performance.
346    pub fn create_tensor_from_slice(
347        &self,
348        slice: &[u8],
349        shape: Shape,
350        elem_size: usize,
351    ) -> MemoryLayout {
352        self.do_create(
353            vec![MemoryLayoutDescriptor::new(
354                MemoryLayoutStrategy::Optimized,
355                shape,
356                elem_size,
357            )],
358            vec![staged_bytes(slice)],
359        )
360        .unwrap()
361        .remove(0)
362    }
363
364    /// Given a resource and shape, stores it and returns the tensor handle and strides.
365    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
366    /// should be taken when indexing.
367    ///
368    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
369    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
370    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
371    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
372    /// can load as much data as possible in a single instruction. It may be aligned even more to
373    /// also take cache lines into account.
374    ///
375    /// However, the stride must be taken into account when indexing and reading the tensor
376    /// (also see [`ComputeClient::read_tensor`]).
377    pub fn create_tensor(&self, bytes: Bytes, shape: Shape, elem_size: usize) -> MemoryLayout {
378        self.do_create(
379            vec![MemoryLayoutDescriptor::new(
380                MemoryLayoutStrategy::Optimized,
381                shape,
382                elem_size,
383            )],
384            vec![bytes],
385        )
386        .unwrap()
387        .remove(0)
388    }
389
390    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
391    /// handle, and returns the handles for them.
392    /// See [`ComputeClient::create_tensor`]
393    ///
394    /// # Notes
395    ///
396    /// Prefer using [`Self::create_tensors`] for better performance.
397    pub fn create_tensors_from_slices(
398        &self,
399        descriptors: Vec<(MemoryLayoutDescriptor, &[u8])>,
400    ) -> Vec<MemoryLayout> {
401        let mut data = Vec::with_capacity(descriptors.len());
402        let mut descriptors_ = Vec::with_capacity(descriptors.len());
403        for (a, b) in descriptors {
404            data.push(staged_bytes(b));
405            descriptors_.push(a);
406        }
407
408        self.do_create(descriptors_, data).unwrap()
409    }
410
411    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
412    /// handle, and returns the handles for them.
413    /// See [`ComputeClient::create_tensor`]
414    pub fn create_tensors(
415        &self,
416        descriptors: Vec<(MemoryLayoutDescriptor, Bytes)>,
417    ) -> Vec<MemoryLayout> {
418        let (descriptors, data) = descriptors.into_iter().unzip();
419
420        self.do_create(descriptors, data).unwrap()
421    }
422
423    fn do_empty(
424        &self,
425        descriptors: Vec<MemoryLayoutDescriptor>,
426    ) -> Result<Vec<MemoryLayout>, IoError> {
427        let stream_id = self.stream_id();
428        let (handle_base, layouts) = self.utilities.layout_policy.apply(stream_id, &descriptors);
429
430        let (size, memory) = (handle_base.size(), handle_base.memory);
431        self.device.submit(move |server| {
432            server.initialize_memory(memory, size, stream_id);
433        });
434
435        Ok(layouts)
436    }
437
438    /// Reserves `size` bytes in the storage, and returns a handle over them.
439    pub fn empty(&self, size: usize) -> Handle {
440        let shape: Shape = [size].into();
441        let descriptor = MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, 1);
442        self.do_empty(vec![descriptor]).unwrap().remove(0).memory
443    }
444
445    /// Reserves `shape` in the storage, and returns a tensor handle for it.
446    /// See [`ComputeClient::create_tensor`]
447    pub fn empty_tensor(&self, shape: Shape, elem_size: usize) -> MemoryLayout {
448        let descriptor =
449            MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size);
450        self.do_empty(vec![descriptor]).unwrap().remove(0)
451    }
452
453    /// Reserves all `shapes` in a single storage buffer, and returns the handles for them.
454    /// See [`ComputeClient::create_tensor`]
455    pub fn empty_tensors(&self, descriptors: Vec<MemoryLayoutDescriptor>) -> Vec<MemoryLayout> {
456        self.do_empty(descriptors).unwrap()
457    }
458
459    /// Marks the given [Bytes] as being a staging buffer, maybe transferring it to pinned memory
460    /// for faster data transfer with compute device.
461    ///
462    /// TODO: This blocks the compute queue, so it will drop the compute utilization.
463    pub fn staging<'a, I>(&self, bytes: I, file_only: bool)
464    where
465        I: Iterator<Item = &'a mut Bytes>,
466    {
467        let has_staging = |b: &Bytes| match b.property() {
468            AllocationProperty::Pinned => false,
469            AllocationProperty::File => true,
470            AllocationProperty::Native | AllocationProperty::Other => !file_only,
471        };
472
473        let mut to_be_updated = Vec::new();
474        let sizes = bytes
475            .filter_map(|b| match has_staging(b) {
476                true => {
477                    let len = b.len();
478                    to_be_updated.push(b);
479                    Some(len)
480                }
481                false => None,
482            })
483            .collect::<Vec<usize>>();
484
485        if sizes.is_empty() {
486            return;
487        }
488
489        let stream_id = self.stream_id();
490        let sizes = sizes.to_vec();
491        let stagings = self
492            .device
493            .submit_blocking(move |server| server.staging(&sizes, stream_id))
494            .unwrap();
495
496        let stagings = match stagings {
497            Ok(val) => val,
498            Err(_) => return,
499        };
500
501        to_be_updated
502            .into_iter()
503            .zip(stagings)
504            .for_each(|(b, mut staging)| {
505                b.copy_into(&mut staging);
506                core::mem::swap(b, &mut staging);
507            });
508    }
509
510    /// Transfer data from one client to another
511    #[cfg_attr(
512        feature = "tracing",
513        tracing::instrument(level = "trace", skip(self, src, dst_server))
514    )]
515    pub fn to_client(&mut self, src: Handle, dst_server: &Self, dtype: ElemType) -> Handle {
516        let shape = [src.size_in_used() as usize];
517        let src_descriptor = src.copy_descriptor(shape.into(), [1].into(), 1);
518
519        if R::Server::SERVER_COMM_ENABLED {
520            self.to_client_tensor(src_descriptor, dst_server, dtype)
521        } else {
522            let alloc_desc = MemoryLayoutDescriptor::new(
523                MemoryLayoutStrategy::Contiguous,
524                src_descriptor.shape.clone(),
525                src_descriptor.elem_size,
526            );
527            self.change_client_sync(src_descriptor, alloc_desc, dst_server)
528                .memory
529        }
530    }
531
532    /// Perform an `all_reduce` operation on the given devices.
533    #[cfg_attr(
534        feature = "tracing",
535        tracing::instrument(level = "trace", skip(self, device_ids))
536    )]
537    pub fn ensure_init_collective(&mut self, device_ids: Vec<DeviceId>) {
538        let comm_id = CommunicationId::from(device_ids.clone());
539        let is_comms_init = self
540            .utilities
541            .initialized_comms
542            .read()
543            .unwrap()
544            .contains(&comm_id);
545        if !is_comms_init {
546            self.device
547                .submit(move |server| server.comm_init(device_ids).unwrap());
548            let mut initialized_comms = self.utilities.initialized_comms.write().unwrap();
549            initialized_comms.insert(comm_id);
550            // Flush immediately so other devices aren't blocked waiting on this initialization.
551            self.device.flush_queue();
552        }
553    }
554
555    /// Wait on the communication stream.
556    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
557    pub fn sync_collective(&self) {
558        if DeviceHandle::<R::Server>::is_blocking() {
559            panic!("Can't use `sync_collective` with a blocking device handle");
560        }
561        let stream_id = self.stream_id();
562
563        self.device.submit(move |server| {
564            server.sync_collective(stream_id).unwrap();
565        });
566
567        // We don't actually need or want to sync the server here, but we need to make sure any
568        // task enqueued on the communication channel is done.
569        self.device.flush_queue();
570    }
571
572    /// Perform an `all_reduce` operation on the given devices.
573    #[cfg_attr(
574        feature = "tracing",
575        tracing::instrument(level = "trace", skip(self, src, dst, dtype, device_ids, op))
576    )]
577    pub fn all_reduce(
578        &mut self,
579        src: Handle,
580        dst: Handle,
581        dtype: ElemType,
582        device_ids: Vec<DeviceId>,
583        op: ReduceOperation,
584    ) {
585        if DeviceHandle::<R::Server>::is_blocking() {
586            panic!("Can't use `all_reduce` with a blocking device handle");
587        }
588
589        let stream_id = self.stream_id();
590        let src = src.binding();
591        let dst = dst.binding();
592
593        self.ensure_init_collective(device_ids.clone());
594
595        self.device.submit(move |server| {
596            server
597                .all_reduce(src, dst, dtype, stream_id, op, device_ids)
598                .unwrap();
599        });
600    }
601
602    /// Transfer data from one client to another
603    ///
604    /// Make sure the source description can be read in a contiguous manner.
605    #[cfg_attr(
606        feature = "tracing",
607        tracing::instrument(level = "trace", skip(self, src_descriptor, dst_server))
608    )]
609    pub fn to_client_tensor(
610        &mut self,
611        src_descriptor: CopyDescriptor,
612        dst_server: &Self,
613        dtype: ElemType,
614    ) -> Handle {
615        let stream_id_src = self.stream_id();
616        let stream_id_dst = dst_server.stream_id();
617
618        let device_id_src = self.device.device_id();
619        let device_id_dst = dst_server.device.device_id();
620
621        let mut dst_server = dst_server.clone();
622        let handle = Handle::new(stream_id_dst, src_descriptor.handle.size_in_used());
623        let handle_cloned = handle.clone();
624
625        let device_ids = vec![device_id_src, device_id_dst];
626        self.ensure_init_collective(device_ids.clone());
627        dst_server.ensure_init_collective(device_ids);
628
629        self.device.submit(move |server_src| {
630            server_src
631                .send(src_descriptor, dtype, stream_id_src, device_id_dst)
632                .unwrap()
633        });
634
635        dst_server.device.submit(move |server_dst| {
636            server_dst
637                .recv(handle_cloned, dtype, stream_id_dst, device_id_src)
638                .unwrap();
639            server_dst.sync_collective(stream_id_dst).unwrap();
640        });
641
642        // `ServerCommunication::send` and`ServerCommunication::recv` are blocking: they each wait for the corresponding recv/send
643        // call to be made. We flush the operations right away so that the neither server ends up in a deadlock.
644        // The actual data transfer is still executed asynchronously on the communication stream.
645        self.device.flush_queue();
646        dst_server.device.flush_queue();
647
648        handle
649    }
650
651    #[track_caller]
652    #[cfg_attr(feature = "tracing", tracing::instrument(level="trace",
653        skip(self, kernel, bindings),
654        fields(
655            kernel.name = %kernel.name(),
656            kernel.id = %kernel.id(),
657        )
658    ))]
659    unsafe fn launch_inner(
660        &self,
661        kernel: <R::Server as ComputeServer>::Kernel,
662        count: CubeCount,
663        bindings: KernelArguments,
664        mode: ExecutionMode,
665        stream_id: StreamId,
666    ) {
667        let level = self.utilities.logger.profile_level();
668
669        match level {
670            None | Some(ProfileLevel::ExecutionOnly) => {
671                let utilities = self.utilities.clone();
672                self.device.submit(move |state| {
673                    let name = kernel.name();
674                    unsafe { state.launch(kernel, count, bindings, mode, stream_id) };
675
676                    if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
677                        let info = type_name_format(name, TypeNameFormatLevel::Balanced);
678                        utilities.logger.register_execution(info);
679                    }
680                });
681            }
682            Some(level) => {
683                let name = kernel.name();
684                let kernel_id = kernel.id();
685                let context = self.device.clone();
686                let count_moved = count.clone();
687                let (result, profile) = self
688                    .profile(
689                        move || {
690                            context
691                                .submit_blocking(move |state| unsafe {
692                                    state.launch(kernel, count_moved, bindings, mode, stream_id)
693                                })
694                                .unwrap()
695                        },
696                        name,
697                    )
698                    .unwrap();
699                let info = match level {
700                    ProfileLevel::Full => {
701                        format!("{name}: {kernel_id} CubeCount {count:?}")
702                    }
703                    _ => type_name_format(name, TypeNameFormatLevel::Balanced),
704                };
705                self.utilities.logger.register_profiled(info, profile);
706                result
707            }
708        }
709    }
710
711    /// Launches the `kernel` with the given `bindings`.
712    #[track_caller]
713    pub fn launch(
714        &self,
715        kernel: <R::Server as ComputeServer>::Kernel,
716        count: CubeCount,
717        bindings: KernelArguments,
718    ) {
719        // SAFETY: Using checked execution mode.
720        unsafe {
721            self.launch_inner(
722                kernel,
723                count,
724                bindings,
725                ExecutionMode::Checked,
726                self.stream_id(),
727            )
728        }
729    }
730
731    /// Launches the `kernel` with the given `bindings` without performing any bound checks.
732    ///
733    /// # Safety
734    ///
735    /// To ensure this is safe, you must verify your kernel:
736    /// - Has no out-of-bound reads and writes that can happen.
737    /// - Has no infinite loops that might never terminate.
738    #[track_caller]
739    pub unsafe fn launch_unchecked(
740        &self,
741        kernel: <R::Server as ComputeServer>::Kernel,
742        count: CubeCount,
743        bindings: KernelArguments,
744    ) {
745        // SAFETY: Caller has to uphold kernel being safe.
746        unsafe {
747            self.launch_inner(
748                kernel,
749                count,
750                bindings,
751                match self.utilities.check_mode {
752                    crate::config::compilation::BoundsCheckMode::Enforce => ExecutionMode::Checked,
753                    crate::config::compilation::BoundsCheckMode::Validate => {
754                        ExecutionMode::Validate
755                    }
756                    crate::config::compilation::BoundsCheckMode::Auto => ExecutionMode::Unchecked,
757                },
758                self.stream_id(),
759            )
760        }
761    }
762
763    /// Flush all outstanding commands.
764    pub fn flush(&self) -> Result<(), ServerError> {
765        let stream_id = self.stream_id();
766
767        self.device
768            .submit_blocking(move |server| server.flush(stream_id))
769            .unwrap()
770    }
771
772    /// Wait for the completion of every task in the server.
773    pub fn sync(&self) -> DynFut<Result<(), ServerError>> {
774        let stream_id = self.stream_id();
775
776        let fut = self
777            .device
778            .submit_blocking(move |server| server.sync(stream_id))
779            .unwrap();
780
781        self.utilities.logger.profile_summary();
782
783        fut
784    }
785
786    /// Get the features supported by the compute server.
787    pub fn properties(&self) -> &DeviceProperties {
788        &self.utilities.properties
789    }
790
791    /// Get the features supported by the compute server.
792    pub fn features(&self) -> &Features {
793        &self.utilities.properties.features
794    }
795
796    /// # Warning
797    ///
798    /// For private use only.
799    pub fn properties_mut(&mut self) -> Option<&mut DeviceProperties> {
800        Arc::get_mut(&mut self.utilities).map(|state| &mut state.properties)
801    }
802
803    /// Get the current memory usage of this client.
804    pub fn memory_usage(&self) -> Result<MemoryUsage, ServerError> {
805        let stream_id = self.stream_id();
806        self.device
807            .submit_blocking(move |server| server.memory_usage(stream_id))
808            .unwrap()
809    }
810
811    /// Execute a closure with mutable access to the underlying compute server.
812    ///
813    /// This is the general-purpose escape hatch for backend-specific operations
814    /// that are not covered by the `ComputeClient` API (e.g., extracting a raw
815    /// CUDA stream for FFI interop).
816    ///
817    /// Returns `None` if the device handle call fails.
818    pub fn with_server<R2: Send + 'static>(
819        &self,
820        f: impl FnOnce(&mut R::Server) -> R2 + Send + 'static,
821    ) -> Option<R2> {
822        self.device.submit_blocking(f).ok()
823    }
824
825    /// Get all devices of a specific type available to this runtime
826    pub fn enumerate_devices(&self, type_id: u16) -> Vec<DeviceId> {
827        R::enumerate_devices(type_id, self.info())
828    }
829
830    /// Get all devices available to this runtime
831    pub fn enumerate_all_devices(&self) -> Vec<DeviceId> {
832        R::enumerate_all_devices(self.info())
833    }
834
835    /// Get the number of devices of a specific type available to this runtime
836    pub fn device_count(&self, type_id: u16) -> usize {
837        self.enumerate_devices(type_id).len()
838    }
839
840    /// Get the number of devices of a specific type available to this runtime
841    pub fn device_count_total(&self) -> usize {
842        self.enumerate_all_devices().len()
843    }
844
845    /// Change the memory allocation mode.
846    ///
847    /// # Safety
848    ///
849    /// This function isn't thread safe and might create memory leaks.
850    pub unsafe fn allocation_mode(&self, mode: MemoryAllocationMode) {
851        let stream_id = self.stream_id();
852        self.device
853            .submit(move |server| server.allocation_mode(mode, stream_id));
854    }
855
856    /// Ask the client to release memory that it can release.
857    ///
858    /// Nb: Results will vary on what the memory allocator deems beneficial,
859    /// so it's not guaranteed any memory is freed.
860    pub fn memory_cleanup(&self) {
861        let stream_id = self.stream_id();
862        self.device
863            .submit(move |server| server.memory_cleanup(stream_id));
864    }
865
866    /// Measure the execution time of some inner operations.
867    #[track_caller]
868    pub fn profile<O: Send + 'static>(
869        &self,
870        func: impl FnOnce() -> O + Send,
871        #[allow(unused)] func_name: &str,
872    ) -> Result<(O, ProfileDuration), ProfileError> {
873        // Get the outer caller. For execute() this points straight to the
874        // cube kernel. For general profiling it points to whoever calls profile.
875        #[cfg(feature = "profile-tracy")]
876        let location = std::panic::Location::caller();
877
878        // Make a CPU span. If the server has system profiling this is all you need.
879        #[cfg(feature = "profile-tracy")]
880        let _span = tracy_client::Client::running().unwrap().span_alloc(
881            None,
882            func_name,
883            location.file(),
884            location.line(),
885            0,
886        );
887
888        let stream_id = self.stream_id();
889
890        #[cfg(feature = "profile-tracy")]
891        let gpu_span = if self.utilities.properties.timing_method == TimingMethod::Device {
892            let gpu_span = self
893                .utilities
894                .gpu_client
895                .span_alloc(func_name, "profile", location.file(), location.line())
896                .unwrap();
897            Some(gpu_span)
898        } else {
899            None
900        };
901
902        let device = self.device.clone();
903        #[allow(unused_mut, reason = "Used in profile-tracy")]
904        let mut result = self
905            .device
906            .exclusive(move || {
907                // We first get mut access to the server to create a token.
908                // Then we free to server, since it's going to be accessed in `func()`.
909                let token =
910                    match device.submit_blocking(move |server| server.start_profile(stream_id)) {
911                        Ok(token) => match token {
912                            Ok(token) => token,
913                            Err(err) => return Err(err),
914                        },
915                        Err(err) => {
916                            return Err(ServerError::Generic {
917                                reason: alloc::format!(
918                                    "Can't start profiling because of a call error: {err:?}"
919                                ),
920                                backtrace: BackTrace::capture(),
921                            });
922                        }
923                    };
924
925                // We execute `func()` which will recursibly access the server.
926                let out = func();
927
928                // Finally we get the result from the token.
929                let result = device
930                    .submit_blocking(move |server| {
931                        let mut result = server.end_profile(stream_id, token);
932
933                        match result {
934                            Ok(result) => Ok((out, result)),
935                            Err(err) => Err(err),
936                        }
937                    })
938                    .unwrap();
939
940                Ok(result)
941            })
942            .unwrap()
943            .map_err(|err| ProfileError::Unknown {
944                reason: alloc::format!("{err:?}"),
945                backtrace: BackTrace::capture(),
946            })?;
947
948        #[cfg(feature = "profile-tracy")]
949        if let Some(mut gpu_span) = gpu_span {
950            gpu_span.end_zone();
951            let epoch = self.utilities.epoch_time;
952            // Add in the work to upload the timestamp data.
953            result = result.map(|(o, result)| {
954                (
955                    o,
956                    ProfileDuration::new(
957                        alloc::boxed::Box::pin(async move {
958                            let ticks = result.resolve().await;
959                            let start_duration =
960                                ticks.start_duration_since(epoch).as_nanos() as i64;
961                            let end_duration = ticks.end_duration_since(epoch).as_nanos() as i64;
962                            gpu_span.upload_timestamp_start(start_duration);
963                            gpu_span.upload_timestamp_end(end_duration);
964                            ticks
965                        }),
966                        TimingMethod::Device,
967                    ),
968                )
969            });
970        }
971
972        result
973    }
974
975    /// Transfer data from one client to another
976    #[cfg_attr(
977        feature = "tracing",
978        tracing::instrument(
979            level = "trace",
980            skip(self, src_descriptor, alloc_descriptor, dst_server)
981        )
982    )]
983    fn change_client_sync(
984        &self,
985        src_descriptor: CopyDescriptor,
986        alloc_descriptor: MemoryLayoutDescriptor,
987        dst_server: &Self,
988    ) -> MemoryLayout {
989        let shape = src_descriptor.shape.clone();
990        let elem_size = src_descriptor.elem_size;
991        let stream_id = self.stream_id();
992
993        let read = self
994            .device
995            .submit_blocking(move |server| server.read(vec![src_descriptor], stream_id))
996            .unwrap();
997
998        let mut data = cubecl_common::future::block_on(read).unwrap();
999
1000        let (handle_base, mut layouts) = self
1001            .utilities
1002            .layout_policy
1003            .apply(stream_id, &[alloc_descriptor]);
1004        let alloc = layouts.remove(0);
1005
1006        let desc_descriptor = CopyDescriptor {
1007            handle: handle_base.clone().binding(),
1008            shape,
1009            strides: alloc.strides.clone(),
1010            elem_size,
1011        };
1012
1013        let (size, memory) = (handle_base.size(), handle_base.memory);
1014        dst_server.device.submit(move |server| {
1015            server.initialize_memory(memory, size, stream_id);
1016            server.write(vec![(desc_descriptor, data.remove(0))], stream_id)
1017        });
1018
1019        alloc
1020    }
1021
1022    /// Returns all vector sizes that are useful to perform optimal IO operation on the given element.
1023    pub fn io_optimized_vector_sizes(
1024        &self,
1025        size: usize,
1026    ) -> impl Iterator<Item = VectorSize> + Clone {
1027        let load_width = self.properties().hardware.load_width as usize;
1028        let size_bits = size * 8;
1029        let max = load_width / size_bits;
1030        let max = usize::min(self.properties().hardware.max_vector_size, max);
1031
1032        // If the max is 8, we want to test 1, 2, 4, 8 which is log2(8) + 1.
1033        let num_candidates = max.trailing_zeros() + 1;
1034
1035        (0..num_candidates).map(|i| 2usize.pow(i)).rev()
1036    }
1037}
1038
1039/// Copy a borrowed host slice exactly once into owned, `MAX_ALIGN`-aligned [`Bytes`].
1040///
1041/// The `*_from_slice` entry points need this staging copy: the server reads the
1042/// data asynchronously after the call returns, so it cannot keep borrowing the
1043/// caller's buffer. Writing straight into an aligned allocation avoids the extra
1044/// `Vec<u8>` round trip that [`Bytes::from_bytes_vec`] would realign (and copy)
1045/// again. Callers that already own their data should use
1046/// [`ComputeClient::create`] with [`Bytes::from_elems`], which copies nothing.
1047fn staged_bytes(slice: &[u8]) -> Bytes {
1048    // An empty `u128` vector carries the maximum alignment, so the single
1049    // `extend_from_byte_slice` below allocates directly at that alignment.
1050    let mut bytes = Bytes::from_elems(Vec::<u128>::new());
1051    bytes.extend_from_byte_slice(slice);
1052    bytes
1053}
1054
1055#[cfg(test)]
1056mod staged_bytes_tests {
1057    use super::staged_bytes;
1058    use cubecl_common::bytes::Bytes;
1059
1060    #[test]
1061    fn staged_bytes_copies_once_into_max_aligned_storage() {
1062        for len in [0usize, 1, 7, 16, 17, 4096, 65_537] {
1063            let src: alloc::vec::Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
1064            let staged = staged_bytes(&src);
1065            assert_eq!(&staged[..], &src[..], "len {len}");
1066            assert_eq!(staged.len(), len);
1067            assert!(staged.align() >= core::mem::align_of::<u128>(), "len {len}");
1068            if len > 0 {
1069                assert_ne!(staged.as_ptr(), src.as_ptr(), "staging must own its copy");
1070            }
1071        }
1072    }
1073
1074    #[test]
1075    fn owned_bytes_keep_their_allocation() {
1076        // `create(Bytes::from_elems(vec))` is the zero-copy upload path: the
1077        // `Bytes` keeps the vector's allocation.
1078        let data = alloc::vec![1.0f64, 2.0, 3.0];
1079        let ptr = data.as_ptr() as *const u8;
1080        let bytes = Bytes::from_elems(data);
1081        assert_eq!(bytes.as_ptr(), ptr);
1082    }
1083}