Skip to main content

ruda/runtime/
client.rs

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