Skip to main content

cubecl_runtime/
client.rs

1use crate::{
2    config::{TypeNameFormatLevel, type_name_format},
3    device::HardwareIdentity,
4    id::{GraphId, KernelId},
5    kernel::CubeKernel,
6    logging::ProfileLevel,
7    memory_management::{MemoryAllocationMode, MemoryReport, MemoryScope},
8    server::{
9        BufferBinding, Collective, CommunicationId, ComputeWait, CopyDescriptor, CubeCount, Handle,
10        KernelArguments, KernelResource, MemoryLayout, MemoryLayoutDescriptor,
11        MemoryLayoutStrategy, ProfileError, ProfilingToken, ReduceOperation, Reduction, Server,
12        ServerError, ServerStorage, ServerUtilities,
13    },
14    storage::{ComputeStorage, ManagedResource},
15    throughput::{
16        ThroughputBenchmarker, ThroughputCache, ThroughputError, ThroughputKey, ThroughputValue,
17    },
18};
19use alloc::{boxed::Box, format, string::String, sync::Arc, vec, vec::Vec};
20use core::any::{Any, TypeId};
21
22mod group;
23#[cfg(not(target_family = "wasm"))]
24mod lazy;
25
26use cubecl_common::{
27    bytes::{AllocationProperty, Bytes},
28    device::{DeviceId, ServiceId},
29    device_handle::{CallResultExt, DeviceHandle},
30    profile::ProfileDuration,
31};
32use cubecl_environment::{backtrace::BackTrace, future::DynFut, stream::StreamId, sync::Mutex};
33use cubecl_ir::{DeviceProperties, ElemType, TargetProperties, VectorSize, features::Features};
34use cubecl_zspace::Shape;
35pub use group::*;
36
37#[allow(unused)]
38use cubecl_common::profile::TimingMethod;
39
40/// Work that waits on the host for its peers, a communicator's setup and a transfer's two
41/// halves, queues on every device in the order this hands out.
42static PEER_ORDER: Mutex<()> = Mutex::new(());
43
44fn repeated_device(device_ids: &[DeviceId]) -> Option<DeviceId> {
45    device_ids
46        .iter()
47        .enumerate()
48        .find_map(|(i, id)| device_ids[..i].contains(id).then_some(*id))
49}
50
51/// The `Client` is the entry point to require tasks from the `Server`.
52/// It should be obtained for a specific device via the Compute struct.
53pub struct Client {
54    device: DeviceHandle<dyn Server>,
55    utilities: Arc<ServerUtilities>,
56    stream_id: Option<StreamId>,
57    /// Loads the client of another device of the same runtime.
58    load_peer: fn(DeviceId) -> Client,
59}
60
61/// A captured graph produced by [`Client::stop_capture`]: a recorded
62/// launch sequence that [`replay`](Graph::replay) re-runs against its original
63/// buffers, skipping the launch path it was recorded from. Cheap to clone
64/// (shares one backend graph).
65///
66/// The graph itself lives in the backend server, referenced here only by
67/// [`GraphId`]; this handle holds a reference-counted owner that releases the
68/// backend graph once the last clone drops. The graph replays against the exact
69/// device buffers used during capture. The caller keeps those input/output
70/// [`Handle`]s alive and, each iteration, writes fresh inputs into the input
71/// handles (same device pointers) and reads the output handles after replaying —
72/// see [`Client::stop_capture`].
73///
74/// **Stream ordering.** [`replay`](Graph::replay) always dispatches on the
75/// stream the graph was captured on, but input writes and output reads go on the
76/// *writing client's* current stream. They are ordered against the replay only
77/// when they land on that same stream, so keep the client pinned to the capture
78/// stream (via [`set_stream`](Client::set_stream)) — or issue all writes,
79/// replays, and reads from the same unpinned client — for the whole decode loop.
80/// Refreshing inputs from a client on a different stream races the replay and
81/// silently feeds it stale data.
82pub struct Graph {
83    inner: Arc<GraphHandle>,
84}
85
86impl core::fmt::Debug for Graph {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        f.debug_struct("Graph")
89            .field("id", &self.inner.id)
90            .field("stream_id", &self.inner.stream_id)
91            .finish()
92    }
93}
94
95/// Reference-counted owner of a backend graph. Its [`Drop`] ships the release to
96/// the server actor, so the last [`Graph`] clone frees the backend graph on the
97/// thread that owns it.
98struct GraphHandle {
99    id: GraphId,
100    device: DeviceHandle<dyn Server>,
101    stream_id: StreamId,
102}
103
104impl Graph {
105    /// Replay the captured launch sequence — every recorded kernel re-run
106    /// against the buffers it was captured with, on the stream it was captured
107    /// on. Self-contained (the handle owns its device handle); no client
108    /// needed.
109    ///
110    /// How much of the launch path this skips depends on the backend: a
111    /// hardware graph (CUDA, HIP) replays as one dispatch, while a software
112    /// graph (wgpu) re-encodes the recorded dispatches from prebuilt state.
113    /// Either way pipeline lookup, binding resolution and metadata upload
114    /// happened once, at capture.
115    ///
116    /// Blocking only on the enqueue: [`replay`](Self::replay) waits for the
117    /// device thread to accept the dispatch and hands back what that enqueue
118    /// said — an unknown or destroyed graph, a refusal — then returns without
119    /// waiting for the device. A failure also leaves the graph's write set
120    /// carrying it, so a read of those buffers keeps failing until a replay
121    /// lands.
122    ///
123    /// The wait costs end-to-end throughput nothing: the device-thread work
124    /// happens either way, and blocking here only stops deferring it to the
125    /// next sync. What it does move is the caller-visible latency of this
126    /// call, from the cost of posting to a channel to the real cost of
127    /// enqueuing the pass — so a benchmark reading this column is reading
128    /// latency, not throughput.
129    ///
130    /// # Safety
131    ///
132    /// The dispatch re-runs the recorded kernels against the raw device pointers
133    /// captured with them; nothing validates those buffers still exist or are
134    /// unshared. The caller must guarantee, until the replay's work completes on
135    /// the stream:
136    ///
137    /// - **Liveness** — every [`Handle`] the captured kernels read or wrote is
138    ///   still allocated. Freeing one returns its memory to the pool, and a
139    ///   later replay reads or corrupts whatever the allocator has since placed
140    ///   there.
141    /// - **No concurrent use** — no other stream or thread touches buffers the
142    ///   graph reads or writes while the replay executes; the replay is ordered
143    ///   only against work on its capture stream.
144    /// - **Same-stream refreshes** — input writes and output reads are issued on
145    ///   the capture stream (keep the client pinned to it via
146    ///   [`set_stream`](Client::set_stream), or do everything from the
147    ///   one client), so they order against the replay instead of racing it.
148    pub unsafe fn replay(&self) -> Result<(), ServerError> {
149        let id = self.inner.id;
150        let stream_id = self.inner.stream_id;
151        self.inner
152            .device
153            .submit_blocking(move |server| server.replay(id, stream_id))
154            .unwrap_or_resume()
155    }
156}
157
158impl Clone for Graph {
159    fn clone(&self) -> Self {
160        Self {
161            inner: self.inner.clone(),
162        }
163    }
164}
165
166impl Drop for GraphHandle {
167    fn drop(&mut self) {
168        let id = self.id;
169        let stream_id = self.stream_id;
170        // Destroying the raw executable must happen on the server actor (the
171        // only thread allowed to touch it) and only once in-flight replays have
172        // completed — `replay` returns at enqueue time, not completion. Ship the
173        // release to the actor; the backend syncs the stream before it destroys.
174        self.device
175            .submit(move |server| server.graph_destroy(id, stream_id));
176    }
177}
178
179/// A profiling window opened by [`Client::profile_start`], closed by
180/// [`Client::profile_end`] or dropped by [`Client::profile_abandon`].
181///
182/// It remembers the stream it was opened on, so closing it from another
183/// thread still closes it on that stream. It is a plain value with no
184/// [`Drop`]: a window that is neither ended nor abandoned stays open on the
185/// server.
186#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
187pub struct ProfileWindow {
188    /// The stream the window was opened on.
189    pub stream_id: StreamId,
190    /// The server's token for the window.
191    pub token: ProfilingToken,
192}
193
194/// Where a [`Client::profile`] window opens on its stream.
195#[derive(Debug, Clone, Copy)]
196enum WindowOpens {
197    /// Where the stream is: everything the closure enqueues is in the window.
198    Now,
199    /// At the next kernel launched, for a window around one launch: the host
200    /// preparing that launch stays out of it. See [`Server::start_profile_at_launch`].
201    AtLaunch,
202}
203
204/// The state a `DeviceHandle` reaches, seen as the server it is. A client
205/// keeps this cast, not the server type, so every operation reads the same
206/// whatever backend is underneath.
207fn as_server<S: Server>(state: &mut dyn Any) -> &mut dyn Server {
208    state
209        .downcast_mut::<S>()
210        .expect("State type mismatch in the device registry")
211}
212
213impl Clone for Client {
214    fn clone(&self) -> Self {
215        Self {
216            device: self.device.clone(),
217            utilities: self.utilities.clone(),
218            stream_id: self.stream_id,
219            load_peer: self.load_peer,
220        }
221    }
222}
223
224impl Client {
225    /// The runtime name on this device, as logs and cache keys show it.
226    pub fn name(&self) -> &'static str {
227        self.utilities.name
228    }
229
230    /// Create a new client with a new server.
231    pub fn init<S: ServerStorage>(device_id: DeviceId, server: S) -> Self {
232        Self::try_init(device_id, server)
233            .expect("Can't create a new client on an already registered server")
234    }
235
236    /// Register a server, returning an error if its device is already registered.
237    pub fn try_init<S: ServerStorage>(
238        device_id: DeviceId,
239        server: S,
240    ) -> Result<Self, cubecl_common::device_handle::ServiceCreationError> {
241        let utilities = Server::utilities(&server);
242        let context = DeviceHandle::<S>::insert(device_id, server)?.seen_as(as_server::<S>);
243
244        Ok(Self {
245            device: context,
246            utilities,
247            stream_id: None,
248            load_peer: Self::load::<S>,
249        })
250    }
251
252    /// Load the client for the given device, starting a server of type `S`
253    /// there if none runs yet.
254    pub fn load<S: ServerStorage>(device_id: DeviceId) -> Self {
255        let context = DeviceHandle::<S>::new(device_id).seen_as(as_server::<S>);
256
257        // This is safe because we now know the return type of [`DeviceHandle::utilities()`].
258        let utilities = context
259            .utilities()
260            .downcast::<ServerUtilities>()
261            .expect("Can downcast to `ServerUtilities`");
262
263        Self {
264            device: context,
265            utilities,
266            stream_id: None,
267            load_peer: Self::load::<S>,
268        }
269    }
270
271    fn stream_id(&self) -> StreamId {
272        match self.stream_id {
273            Some(val) => val,
274            None => StreamId::current(),
275        }
276    }
277
278    /// The stream its launches go out on, on its device's service: what a
279    /// [`StreamModeOverride`](crate::execution::StreamModeOverride) sets the
280    /// mode of.
281    pub(crate) fn service_stream(&self) -> crate::execution::ServiceStream {
282        crate::execution::ServiceStream {
283            service: self.service_id(),
284            stream: self.stream_id(),
285        }
286    }
287
288    /// The service this client reaches: what its handles are stamped with.
289    pub fn service_id(&self) -> ServiceId {
290        self.device.service_id()
291    }
292
293    /// Whether the server behind this client is an `S`. The client is erased
294    /// over its server type, so a caller naming one has to be checked here,
295    /// before a downcast on the device thread turns the mismatch into a panic.
296    fn is_service<S: 'static>(&self) -> bool {
297        TypeId::of::<S>() == self.service_id().service
298    }
299
300    /// Whether `binding` addresses this client's device. Memory coordinates
301    /// mean nothing on another device, so a foreign binding is refused here,
302    /// before anything is submitted, rather than read there.
303    fn local(&self, binding: &BufferBinding) -> Result<(), ServerError> {
304        let client = self.service_id();
305        if binding.service == client {
306            return Ok(());
307        }
308        Err(ServerError::ForeignHandle {
309            handle: format!("{}", binding.service),
310            client: format!("{client}"),
311            backtrace: BackTrace::capture(),
312        })
313    }
314
315    /// [`local`](Self::local) for a call that has no error to return: a
316    /// foreign handle is a bug in the caller, and the alternative to stopping
317    /// here is reading another device's memory.
318    #[track_caller]
319    fn expect_local(&self, binding: &BufferBinding) {
320        if let Err(err) = self.local(binding) {
321            panic!("{err}");
322        }
323    }
324
325    /// Set the stream in which the current client is operating on.
326    ///
327    /// # Safety
328    ///
329    /// This is highly unsafe and should probably only be used by the CubeCL/Burn projects for now.
330    pub unsafe fn set_stream(&mut self, stream_id: StreamId) {
331        self.stream_id = Some(stream_id);
332    }
333
334    fn do_read(&self, descriptors: Vec<CopyDescriptor>) -> DynFut<Result<Vec<Bytes>, ServerError>> {
335        if let Some(err) = descriptors
336            .iter()
337            .find_map(|descriptor| self.local(&descriptor.handle).err())
338        {
339            return Box::pin(core::future::ready(Err(err)));
340        }
341        let stream_id = self.stream_id();
342        self.device
343            .submit_blocking(move |server| server.read(descriptors, stream_id))
344            .unwrap_or_resume()
345    }
346
347    /// Given bindings, returns owned resources as bytes.
348    pub fn read_async(
349        &self,
350        handles: Vec<Handle>,
351    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
352        let shapes = handles
353            .iter()
354            .map(|it| [it.size_in_used() as usize].into())
355            .collect::<Vec<Shape>>();
356        let descriptors = handles
357            .into_iter()
358            .zip(shapes)
359            .map(|(handle, shape)| CopyDescriptor::new(handle.binding(), shape, [1].into(), 1))
360            .collect();
361
362        self.do_read(descriptors)
363    }
364
365    /// Given bindings, returns owned resources as bytes.
366    ///
367    /// # Errors
368    ///
369    /// Returns a [`ServerError`] if the read operation fails, or if a an error occurred on the
370    /// compute server leading to the read.
371    pub fn read(&self, handles: Vec<Handle>) -> Result<Vec<Bytes>, ServerError> {
372        cubecl_environment::future::reader::read_sync(self.read_async(handles))
373    }
374
375    /// Given a binding, returns owned resource as bytes.
376    pub fn read_one(&self, handle: Handle) -> Result<Bytes, ServerError> {
377        Ok(cubecl_environment::future::reader::read_sync(self.read_async(vec![handle]))?.remove(0))
378    }
379
380    /// Given a binding, returns owned resource as bytes.
381    ///
382    /// # Remarks
383    ///
384    /// Panics if the read operation fails. Useful for tests.
385    pub fn read_one_unchecked(&self, handle: Handle) -> Bytes {
386        cubecl_environment::future::reader::read_sync(self.read_async(vec![handle]))
387            .unwrap()
388            .remove(0)
389    }
390
391    /// Given bindings, returns owned resources as bytes.
392    pub fn read_tensor_async(
393        &self,
394        descriptors: Vec<CopyDescriptor>,
395    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send {
396        self.do_read(descriptors)
397    }
398
399    /// Given bindings, returns owned resources as bytes.
400    ///
401    /// The tensor must be in the same layout as created by the runtime, or more strict.
402    /// Contiguous tensors are always fine, strided tensors are only ok if the stride is similar to
403    /// the one created by the runtime (i.e. padded on only the last dimension). A way to check
404    /// stride compatibility on the runtime will be added in the future.
405    ///
406    /// Also see [`Client::create_tensor`].
407    ///
408    /// # Errors
409    ///
410    /// Returns a [`ServerError`] if the read operation fails, or if a an error occurred on the
411    /// compute server leading to the read.
412    pub fn read_tensor(&self, descriptors: Vec<CopyDescriptor>) -> Result<Vec<Bytes>, ServerError> {
413        cubecl_environment::future::reader::read_sync(self.read_tensor_async(descriptors))
414    }
415
416    /// Given a binding, returns owned resource as bytes.
417    /// See [`Client::read_tensor`]
418    pub fn read_one_tensor_async(
419        &self,
420        descriptor: CopyDescriptor,
421    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
422        let fut = self.read_tensor_async(vec![descriptor]);
423
424        async { Ok(fut.await?.remove(0)) }
425    }
426
427    /// Given a binding, returns owned resource as bytes.
428    ///
429    /// # Remarks
430    ///
431    /// Panics if the read operation fails.
432    /// See [`Client::read_tensor`]
433    pub fn read_one_unchecked_tensor(&self, descriptor: CopyDescriptor) -> Bytes {
434        self.read_tensor(vec![descriptor])
435            .expect("the read failed, use `read_one_tensor_async` to handle the error")
436            .remove(0)
437    }
438
439    /// Reads the device resource described by `descriptor` lazily.
440    ///
441    /// The returned [`Bytes`] only performs the device-to-host copy on first access (e.g. during
442    /// serialization), keeping the source allocation alive until then. This lets a large number of
443    /// device tensors be serialized without materializing them all in host memory at once: drain
444    /// the [`Bytes`] sequentially rather than holding them all alive.
445    ///
446    /// The data reflects the device state at first access, so the buffer must not be mutated
447    /// between this call and the first read.
448    #[cfg(not(target_family = "wasm"))]
449    pub fn read_lazy(&self, descriptor: CopyDescriptor) -> Bytes {
450        self.expect_local(&descriptor.handle);
451        let len = descriptor.shape.iter().product::<usize>() * descriptor.elem_size;
452        let controller = lazy::LazyDeviceController::new(self.clone(), Arc::new(descriptor));
453        // SAFETY: the controller materializes exactly `len` bytes on first access.
454        unsafe { Bytes::from_controller(alloc::boxed::Box::new(controller), len) }
455    }
456
457    /// Reads the device resource described by `descriptor` lazily, async variant.
458    ///
459    /// On native targets the returned future is immediately ready and yields a lazy [`Bytes`]
460    /// whose device-to-host copy is deferred to first access (see [`read_lazy`](Self::read_lazy)).
461    ///
462    /// # Errors
463    ///
464    /// Returns a [`ServerError`] if a an error occurred on the compute server leading to the read.
465    /// A device fault is not detected here: the copy is deferred, so it surfaces as an error on
466    /// first access to the returned [`Bytes`].
467    #[cfg(not(target_family = "wasm"))]
468    pub fn read_lazy_async(
469        &self,
470        descriptor: CopyDescriptor,
471    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
472        if let Err(err) = self.local(&descriptor.handle) {
473            return core::future::ready(Err(err));
474        }
475        let binding = descriptor.handle.clone();
476        let stream_id = self.stream_id();
477        let checked = self
478            .device
479            .submit_blocking(move |server| server.check(vec![binding], stream_id))
480            .unwrap_or_resume();
481        if let Err(err) = checked {
482            return core::future::ready(Err(err));
483        }
484        let len = descriptor.shape.iter().product::<usize>() * descriptor.elem_size;
485        let controller = lazy::LazyDeviceController::new(self.clone(), Arc::new(descriptor));
486        // SAFETY: the controller materializes exactly `len` bytes on first access.
487        let bytes = unsafe { Bytes::from_controller(alloc::boxed::Box::new(controller), len) };
488        core::future::ready(Ok(bytes))
489    }
490
491    /// Reads the device resource described by `descriptor` lazily, async variant.
492    ///
493    /// On `wasm` the deferred copy cannot run inside the synchronous access path, so awaiting
494    /// performs the read eagerly and yields a materialized [`Bytes`]. Awaiting one tensor at a
495    /// time still bounds peak host memory, which is the point of the lazy API.
496    #[cfg(target_family = "wasm")]
497    pub fn read_lazy_async(
498        &self,
499        descriptor: CopyDescriptor,
500    ) -> impl Future<Output = Result<Bytes, ServerError>> + Send {
501        self.read_one_tensor_async(descriptor)
502    }
503
504    /// Given a resource handle, returns the storage resource.
505    pub fn get_resource<S: ServerStorage>(
506        &self,
507        handle: Handle,
508    ) -> Result<ManagedResource<<S::Storage as ComputeStorage>::Resource>, ServerError> {
509        let stream_id = self.stream_id();
510        let binding = handle.binding();
511        self.local(&binding)?;
512        if !self.is_service::<S>() {
513            return Err(ServerError::ServiceMismatch {
514                client: format!("{}", self.service_id()),
515                requested: String::from(core::any::type_name::<S>()),
516                backtrace: BackTrace::capture(),
517            });
518        }
519
520        self.device
521            .submit_blocking(move |server| {
522                let server = (server as &mut dyn Any)
523                    .downcast_mut::<S>()
524                    .expect("is_service passed, so this is the server's type");
525                server.get_resource(binding, stream_id)
526            })
527            .unwrap_or_resume()
528    }
529
530    fn do_create_from_slices(
531        &self,
532        descriptors: Vec<MemoryLayoutDescriptor>,
533        slices: Vec<Vec<u8>>,
534    ) -> Vec<MemoryLayout> {
535        let stream_id = self.stream_id();
536        let (handle_base, layouts) =
537            self.utilities
538                .layout_policy
539                .apply(self.service_id(), stream_id, &descriptors);
540
541        let descriptors = descriptors
542            .into_iter()
543            .zip(layouts.iter())
544            .zip(slices)
545            .map(|((desc, alloc), data)| {
546                (
547                    CopyDescriptor::new(
548                        alloc.memory.clone().binding(),
549                        desc.shape,
550                        alloc.strides.clone(),
551                        desc.elem_size,
552                    ),
553                    Bytes::from_bytes_vec(data.to_vec()),
554                )
555            })
556            .collect::<Vec<_>>();
557
558        let (size, memory) = (handle_base.size(), handle_base.memory);
559        self.device.submit(move |server| {
560            // Cannot write on memory that wasn't initialized. The error is attached to the
561            // buffer and is reported at the next sync point.
562            if server.initialize_memory(memory, size, stream_id).is_ok() {
563                server.write(descriptors, stream_id);
564            }
565        });
566
567        layouts
568    }
569
570    fn do_create(
571        &self,
572        descriptors: Vec<MemoryLayoutDescriptor>,
573        data: Vec<Bytes>,
574    ) -> Vec<MemoryLayout> {
575        let stream_id = self.stream_id();
576        let (handle_base, layouts) =
577            self.utilities
578                .layout_policy
579                .apply(self.service_id(), stream_id, &descriptors);
580
581        let descriptors = descriptors
582            .into_iter()
583            .zip(layouts.iter())
584            .zip(data)
585            .map(|((desc, layout), data)| {
586                (
587                    CopyDescriptor::new(
588                        layout.memory.clone().binding(),
589                        desc.shape,
590                        layout.strides.clone(),
591                        desc.elem_size,
592                    ),
593                    data,
594                )
595            })
596            .collect::<Vec<_>>();
597
598        let (size, memory) = (handle_base.size(), handle_base.memory);
599        self.device.submit(move |server| {
600            // Cannot write on memory that wasn't initialized. The error is attached to the
601            // buffer and is reported at the next sync point.
602            if server.initialize_memory(memory, size, stream_id).is_ok() {
603                server.write(descriptors, stream_id);
604            }
605        });
606
607        layouts
608    }
609
610    /// Returns a resource handle containing the given data.
611    ///
612    /// # Notes
613    ///
614    /// Prefer using the more efficient [`Self::create`] function.
615    pub fn create_from_slice(&self, slice: &[u8]) -> Handle {
616        let shape: Shape = [slice.len()].into();
617
618        self.do_create_from_slices(
619            vec![MemoryLayoutDescriptor::new(
620                MemoryLayoutStrategy::Contiguous,
621                shape,
622                1,
623            )],
624            vec![slice.to_vec()],
625        )
626        .remove(0)
627        .memory
628    }
629
630    /// Run `task` with this device to itself, so nothing else is scheduled
631    /// against it for the duration.
632    ///
633    /// # Errors
634    ///
635    /// The device could not be taken exclusively — another holder has it, or
636    /// its runner is gone. Nothing ran, so the caller may retry.
637    pub fn exclusive<'a, Re: Send + 'static, F: FnOnce() -> Re + Send + 'a>(
638        &'a self,
639        task: F,
640    ) -> Result<Re, ServerError> {
641        // We then launch the task.
642        self.device
643            .exclusive(task)
644            .map_err(|err| ServerError::Generic {
645                reason: format!("{err:?}"),
646                backtrace: BackTrace::capture(),
647            })
648    }
649
650    /// Run `task` with every allocation it makes routed to the persistent
651    /// pool, then restore the previous mode.
652    ///
653    /// Persistent slices are exact-fit and are not reclaimed by the ordinary
654    /// sweep, which is what weights want: allocated once, alive for the
655    /// process, and stable enough for a graph capture to record against.
656    pub fn memory_persistent_allocation<
657        'a,
658        Re: Send,
659        Input: Send,
660        F: FnOnce(Input) -> Re + Send + 'a,
661    >(
662        &'a self,
663        input: Input,
664        task: F,
665    ) -> Re {
666        self.allocation_window(MemoryAllocationMode::Persistent, input, task)
667    }
668
669    /// Run `task` with every allocation it makes given its own device
670    /// allocation outside every pool, returned to the driver once freed, then
671    /// restore the previous mode.
672    ///
673    /// For buffers that exist for one measurement and nothing after it: they
674    /// stay out of the pools' reservations and out of the statistics an
675    /// adaptive pool sizes its pages from.
676    pub fn memory_dedicated_allocation<
677        'a,
678        Re: Send,
679        Input: Send,
680        F: FnOnce(Input) -> Re + Send + 'a,
681    >(
682        &'a self,
683        input: Input,
684        task: F,
685    ) -> Re {
686        self.allocation_window(MemoryAllocationMode::Dedicated, input, task)
687    }
688
689    /// Open a window of `mode` on the current stream around `task`, and close
690    /// it after. Private because `Auto` is what closes a window: the public
691    /// entry points each name a mode that opens one.
692    fn allocation_window<'a, Re: Send, Input: Send, F: FnOnce(Input) -> Re + Send + 'a>(
693        &'a self,
694        mode: MemoryAllocationMode,
695        input: Input,
696        task: F,
697    ) -> Re {
698        let stream_id = StreamId::current();
699
700        self.device.submit(move |server| {
701            server.allocation_mode(mode, stream_id);
702        });
703
704        // All tasks created on the same stream allocate in this mode.
705        let output = task(input);
706
707        self.device.submit(move |server| {
708            server.allocation_mode(MemoryAllocationMode::Auto, stream_id);
709        });
710
711        output
712    }
713
714    /// Write `data` into an existing allocation, in place (same device pointer).
715    ///
716    /// This is how a captured [`Graph`]'s inputs are refreshed between replays:
717    /// the graph records raw device pointers, so new input bytes must land in
718    /// the very buffer the capture read from. Issue it from the capture stream
719    /// (see the stream-ordering notes on [`Graph`]) so the write orders against
720    /// the replays instead of racing them.
721    ///
722    /// Non-blocking: the write is enqueued on this client's current stream.
723    pub fn write(&self, handle: &Handle, data: Bytes) {
724        let stream_id = self.stream_id();
725        let descriptor =
726            CopyDescriptor::new(handle.clone().binding(), [data.len()].into(), [1].into(), 1);
727        self.expect_local(&descriptor.handle);
728        self.device.submit(move |server| {
729            server.write(vec![(descriptor, data)], stream_id);
730        });
731    }
732
733    /// Returns a resource handle containing the given [Bytes].
734    pub fn create(&self, data: Bytes) -> Handle {
735        let shape = [data.len()].into();
736
737        self.do_create(
738            vec![MemoryLayoutDescriptor::new(
739                MemoryLayoutStrategy::Contiguous,
740                shape,
741                1,
742            )],
743            vec![data],
744        )
745        .remove(0)
746        .memory
747    }
748
749    /// Given a resource and shape, stores it and returns the tensor handle and strides.
750    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
751    /// should be taken when indexing.
752    ///
753    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
754    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
755    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
756    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
757    /// can load as much data as possible in a single instruction. It may be aligned even more to
758    /// also take cache lines into account.
759    ///
760    /// However, the stride must be taken into account when indexing and reading the tensor
761    /// (also see [`Client::read_tensor`]).
762    ///
763    /// # Notes
764    ///
765    /// Prefer using [`Self::create_tensor`] for better performance.
766    pub fn create_tensor_from_slice(
767        &self,
768        slice: &[u8],
769        shape: Shape,
770        elem_size: usize,
771    ) -> MemoryLayout {
772        self.do_create_from_slices(
773            vec![MemoryLayoutDescriptor::new(
774                MemoryLayoutStrategy::Optimized,
775                shape,
776                elem_size,
777            )],
778            vec![slice.to_vec()],
779        )
780        .remove(0)
781    }
782
783    /// Given a resource and shape, stores it and returns the tensor handle and strides.
784    /// This may or may not return contiguous strides. The layout is up to the runtime, and care
785    /// should be taken when indexing.
786    ///
787    /// Currently the tensor may either be contiguous (most runtimes), or "pitched", to use the CUDA
788    /// terminology. This means the last (contiguous) dimension is padded to fit a certain alignment,
789    /// and the strides are adjusted accordingly. This can make memory accesses significantly faster
790    /// since all rows are aligned to at least 16 bytes (the maximum load width), meaning the GPU
791    /// can load as much data as possible in a single instruction. It may be aligned even more to
792    /// also take cache lines into account.
793    ///
794    /// However, the stride must be taken into account when indexing and reading the tensor
795    /// (also see [`Client::read_tensor`]).
796    pub fn create_tensor(&self, bytes: Bytes, shape: Shape, elem_size: usize) -> MemoryLayout {
797        self.do_create(
798            vec![MemoryLayoutDescriptor::new(
799                MemoryLayoutStrategy::Optimized,
800                shape,
801                elem_size,
802            )],
803            vec![bytes],
804        )
805        .remove(0)
806    }
807
808    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
809    /// handle, and returns the handles for them.
810    /// See [`Client::create_tensor`]
811    ///
812    /// # Notes
813    ///
814    /// Prefer using [`Self::create_tensors`] for better performance.
815    pub fn create_tensors_from_slices(
816        &self,
817        descriptors: Vec<(MemoryLayoutDescriptor, &[u8])>,
818    ) -> Vec<MemoryLayout> {
819        let mut data = Vec::with_capacity(descriptors.len());
820        let mut descriptors_ = Vec::with_capacity(descriptors.len());
821        for (a, b) in descriptors {
822            data.push(b.to_vec());
823            descriptors_.push(a);
824        }
825
826        self.do_create_from_slices(descriptors_, data)
827    }
828
829    /// Reserves all `shapes` in a single storage buffer, copies the corresponding `data` into each
830    /// handle, and returns the handles for them.
831    /// See [`Client::create_tensor`]
832    pub fn create_tensors(
833        &self,
834        descriptors: Vec<(MemoryLayoutDescriptor, Bytes)>,
835    ) -> Vec<MemoryLayout> {
836        let (descriptors, data) = descriptors.into_iter().unzip();
837
838        self.do_create(descriptors, data)
839    }
840
841    fn do_empty(&self, descriptors: Vec<MemoryLayoutDescriptor>) -> Vec<MemoryLayout> {
842        let stream_id = self.stream_id();
843        let (handle_base, layouts) =
844            self.utilities
845                .layout_policy
846                .apply(self.service_id(), stream_id, &descriptors);
847
848        let (size, memory) = (handle_base.size(), handle_base.memory);
849        self.device.submit(move |server| {
850            // The error is attached to the buffer and is reported at the next sync point.
851            let _ = server.initialize_memory(memory, size, stream_id);
852        });
853
854        layouts
855    }
856
857    /// Reserves `size` bytes in the storage, and returns a handle over them.
858    pub fn empty(&self, size: usize) -> Handle {
859        let shape: Shape = [size].into();
860        let descriptor = MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, 1);
861        self.do_empty(vec![descriptor]).remove(0).memory
862    }
863
864    /// Reserves `shape` in the storage, and returns a tensor handle for it.
865    /// See [`Client::create_tensor`]
866    pub fn empty_tensor(&self, shape: Shape, elem_size: usize) -> MemoryLayout {
867        let descriptor =
868            MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size);
869        self.do_empty(vec![descriptor]).remove(0)
870    }
871
872    /// Reserves all `shapes` in a single storage buffer, and returns the handles for them.
873    /// See [`Client::create_tensor`]
874    pub fn empty_tensors(&self, descriptors: Vec<MemoryLayoutDescriptor>) -> Vec<MemoryLayout> {
875        self.do_empty(descriptors)
876    }
877
878    /// Marks the given [Bytes] as being a staging buffer, maybe transferring it to pinned memory
879    /// for faster data transfer with compute device.
880    ///
881    /// TODO: This blocks the compute queue, so it will drop the compute utilization.
882    pub fn staging<'a, I>(&self, bytes: I, file_only: bool)
883    where
884        I: Iterator<Item = &'a mut Bytes>,
885    {
886        let has_staging = |b: &Bytes| match b.property() {
887            AllocationProperty::Pinned => false,
888            AllocationProperty::File => true,
889            // A lazily device-backed buffer materializes on access and is staged (if needed)
890            // by the backend write path, so don't force it into a host staging buffer here.
891            AllocationProperty::Device => false,
892            AllocationProperty::Native | AllocationProperty::Other => !file_only,
893        };
894
895        let mut to_be_updated = Vec::new();
896        let sizes = bytes
897            .filter_map(|b| match has_staging(b) {
898                true => {
899                    let len = b.len();
900                    to_be_updated.push(b);
901                    Some(len)
902                }
903                false => None,
904            })
905            .collect::<Vec<usize>>();
906
907        if sizes.is_empty() {
908            return;
909        }
910
911        let stream_id = self.stream_id();
912        let sizes = sizes.to_vec();
913        let stagings = self
914            .device
915            .submit_blocking(move |server| server.staging(&sizes, stream_id))
916            .unwrap_or_resume();
917
918        let stagings = match stagings {
919            Ok(val) => val,
920            Err(_) => return,
921        };
922
923        to_be_updated
924            .into_iter()
925            .zip(stagings)
926            .for_each(|(b, mut staging)| {
927                b.copy_into(&mut staging);
928                core::mem::swap(b, &mut staging);
929            });
930    }
931
932    /// Transfer data from one client to another.
933    ///
934    /// `src` must be this client's. The bytes go device to device when both
935    /// clients are of the same runtime and it has a collective transport;
936    /// otherwise, and always across runtimes, they go through the host.
937    ///
938    /// On CUDA, transfers run on a communicator and stream of their own, so neither waits in
939    /// NCCL's queue behind a collective; a stream [`sync_collective`](Self::sync_collective) made
940    /// wait on one still does.
941    #[cfg_attr(
942        feature = "tracing",
943        tracing::instrument(level = "trace", skip(self, src, dst_server))
944    )]
945    pub fn to_client(&mut self, src: Handle, dst_server: &Self) -> Handle {
946        self.expect_local(&src.clone().binding());
947        let shape = [src.size_in_used() as usize];
948        let src_descriptor = src.copy_descriptor(shape.into(), [1].into(), 1);
949
950        let same_runtime = dst_server.service_id().service == self.service_id().service;
951        if self.has_device_transport() && same_runtime {
952            self.to_client_tensor(src_descriptor, dst_server)
953        } else {
954            let alloc_desc = MemoryLayoutDescriptor::new(
955                MemoryLayoutStrategy::Contiguous,
956                src_descriptor.shape.clone(),
957                src_descriptor.elem_size,
958            );
959            self.change_client_sync(src_descriptor, alloc_desc, dst_server)
960                .memory
961        }
962    }
963
964    /// Sets up the group over `device_ids` on every one of its devices, unless it already is.
965    #[cfg_attr(
966        feature = "tracing",
967        tracing::instrument(level = "trace", skip(self, device_ids))
968    )]
969    pub fn ensure_init_collective(&self, device_ids: Vec<DeviceId>) {
970        self.expect_device_transport(Collective::CommInit);
971        // Two ranks on one device wait on its server thread for each other, forever.
972        if let Some(repeated) = repeated_device(&device_ids) {
973            panic!(
974                "{repeated:?} appears twice among a collective's devices, which need one rank each"
975            );
976        }
977        let comm_id = CommunicationId::from(device_ids.clone());
978        if self.utilities.initialized_comms.read().contains(&comm_id) {
979            return;
980        }
981        // Each member's setup waits for the others', so none may wait in turn on work queued
982        // behind it, such as this caller's next part on its own device.
983        let _order = PEER_ORDER.lock();
984        if self.utilities.initialized_comms.read().contains(&comm_id) {
985            return;
986        }
987        let members: Vec<Self> = device_ids
988            .iter()
989            .map(|device| (self.load_peer)(*device))
990            .collect();
991        for member in &members {
992            let device_ids = device_ids.clone();
993            member
994                .device
995                .submit(move |server| server.comm_init(device_ids).unwrap());
996            member
997                .utilities
998                .initialized_comms
999                .write()
1000                .insert(comm_id.clone());
1001        }
1002        for member in &members {
1003            member.device.flush_queue();
1004        }
1005    }
1006
1007    /// Whether this runtime moves data between its devices itself. Without it, `to_client`
1008    /// copies through the host and the collectives refuse.
1009    pub fn has_device_transport(&self) -> bool {
1010        self.utilities.device_transport.is_some()
1011    }
1012
1013    /// Panics on the caller when the runtime has no device transport.
1014    fn expect_device_transport(&self, operation: Collective) {
1015        // The server refuses too, but on the device thread, where the channel turns the panic into
1016        // a log line and the caller only sees a later read fail.
1017        if !self.has_device_transport() {
1018            let alternative = match operation {
1019                Collective::Send | Collective::Recv => "; `to_client` copies through the host",
1020                _ => "",
1021            };
1022            panic!(
1023                "Can't use `{operation}` on {}, which has no transport between its devices{alternative}",
1024                self.utilities.name
1025            );
1026        }
1027    }
1028
1029    /// Wait on the communication stream.
1030    ///
1031    /// Call it once every device has queued its part of the `all_reduce` calls it waits for: a
1032    /// transfer from this device queued in between would wait on a part not queued yet. A
1033    /// [`ClientGroup`]'s collectives need none: each one's stream waits for it at once.
1034    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
1035    pub fn sync_collective(&self) {
1036        if DeviceHandle::<dyn Server>::is_blocking() {
1037            panic!("Can't use `sync_collective` with a blocking device handle");
1038        }
1039        // Nothing was sent between devices, so there is nothing to wait for.
1040        if !self.has_device_transport() {
1041            return;
1042        }
1043        let stream_id = self.stream_id();
1044
1045        self.device.submit(move |server| {
1046            // Logged rather than unwrapped: a panic on the server thread is
1047            // reduced to a log line by the channel's catch_unwind anyway, so
1048            // report deliberately instead of through a swallowed unwind.
1049            if let Err(err) = server.sync_collective(stream_id) {
1050                log::error!("sync_collective failed: {err}");
1051            }
1052        });
1053
1054        // We don't actually need or want to sync the server here, but we need to make sure any
1055        // task enqueued on the communication channel is done.
1056        self.device.flush_queue();
1057    }
1058
1059    /// Perform an `all_reduce` operation on the given devices.
1060    ///
1061    /// Every device has to queue the `all_reduce` calls over a group in one order. Queue each
1062    /// call on every device before the next: a device waiting in NCCL for its peers stops taking
1063    /// work after about a thousand calls, or as soon as it relocates memory, and the thread
1064    /// queueing them stops with it.
1065    #[cfg_attr(
1066        feature = "tracing",
1067        tracing::instrument(level = "trace", skip(self, src, dst, dtype, device_ids, op))
1068    )]
1069    pub fn all_reduce(
1070        &mut self,
1071        src: Handle,
1072        dst: Handle,
1073        dtype: ElemType,
1074        device_ids: Vec<DeviceId>,
1075        op: ReduceOperation,
1076    ) {
1077        let collective = Collective::AllReduce;
1078        let size = src.size_in_used();
1079        Self::expect_whole_elements(collective, dtype, size);
1080        let held = dst.size_in_used();
1081        assert!(
1082            held == size,
1083            "{collective}'s destination holds {held} bytes where its source holds {size}"
1084        );
1085        self.prepare_collective(collective, device_ids.clone());
1086        self.queue_all_reduce(
1087            src,
1088            dst,
1089            Reduction { dtype, op },
1090            device_ids,
1091            ComputeWait::AtSync,
1092        );
1093    }
1094
1095    fn expect_whole_elements(collective: Collective, dtype: ElemType, size: u64) {
1096        assert!(
1097            size.is_multiple_of(dtype.size() as u64),
1098            "{collective} cannot cut {size} bytes into whole {dtype:?} elements"
1099        );
1100    }
1101
1102    /// Checks `collective` can run from here, and sets up the group over `device_ids`.
1103    fn prepare_collective(&self, collective: Collective, device_ids: Vec<DeviceId>) {
1104        if DeviceHandle::<dyn Server>::is_blocking() {
1105            panic!("Can't use `{collective}` with a blocking device handle");
1106        }
1107        self.expect_device_transport(collective);
1108        self.ensure_init_collective(device_ids);
1109    }
1110
1111    fn queue_all_reduce(
1112        &self,
1113        src: Handle,
1114        dst: Handle,
1115        reduction: Reduction,
1116        device_ids: Vec<DeviceId>,
1117        wait: ComputeWait,
1118    ) {
1119        let (src, dst) = (src.binding(), dst.binding());
1120        self.expect_local(&src);
1121        self.expect_local(&dst);
1122        self.queue_collective(Collective::AllReduce, move |server, stream_id| {
1123            server.all_reduce(src, dst, reduction, stream_id, device_ids, wait)
1124        });
1125    }
1126
1127    fn queue_all_gather(&self, src: Handle, dst: Handle, device_ids: Vec<DeviceId>) {
1128        let (src, dst) = (src.binding(), dst.binding());
1129        self.expect_local(&src);
1130        self.expect_local(&dst);
1131        self.queue_collective(Collective::AllGather, move |server, stream_id| {
1132            server.all_gather(src, dst, stream_id, device_ids)
1133        });
1134    }
1135
1136    fn queue_reduce_scatter(
1137        &self,
1138        src: Handle,
1139        dst: Handle,
1140        reduction: Reduction,
1141        device_ids: Vec<DeviceId>,
1142    ) {
1143        let (src, dst) = (src.binding(), dst.binding());
1144        self.expect_local(&src);
1145        self.expect_local(&dst);
1146        self.queue_collective(Collective::ReduceScatter, move |server, stream_id| {
1147            server.reduce_scatter(src, dst, reduction, stream_id, device_ids)
1148        });
1149    }
1150
1151    fn queue_broadcast(
1152        &self,
1153        src: Option<Handle>,
1154        dst: Handle,
1155        root: DeviceId,
1156        device_ids: Vec<DeviceId>,
1157    ) {
1158        let (src, dst) = (src.map(Handle::binding), dst.binding());
1159        src.iter()
1160            .chain([&dst])
1161            .for_each(|binding| self.expect_local(binding));
1162        self.queue_collective(Collective::Broadcast, move |server, stream_id| {
1163            server.broadcast(src, dst, stream_id, root, device_ids)
1164        });
1165    }
1166
1167    fn queue_all_to_all(&self, src: Handle, dst: Handle, device_ids: Vec<DeviceId>) {
1168        let (src, dst) = (src.binding(), dst.binding());
1169        self.expect_local(&src);
1170        self.expect_local(&dst);
1171        self.queue_collective(Collective::AllToAll, move |server, stream_id| {
1172            server.all_to_all(src, dst, stream_id, device_ids)
1173        });
1174    }
1175
1176    /// Queues this device's part of `collective`.
1177    fn queue_collective(
1178        &self,
1179        collective: Collective,
1180        part: impl FnOnce(&mut dyn Server, StreamId) -> Result<(), ServerError> + Send + 'static,
1181    ) {
1182        let stream_id = self.stream_id();
1183        self.device.submit(move |server| {
1184            // The tainted destination is the report; an unwrap would only become a warning here.
1185            if let Err(err) = part(server, stream_id) {
1186                log::error!(
1187                    "{collective} failed, so the peers that queued their part wait in it, and the \
1188                     destination carries the failure: {err}"
1189                );
1190            }
1191        });
1192    }
1193
1194    /// Transfer data from one client to another
1195    ///
1196    /// Make sure the source description can be read in a contiguous manner.
1197    #[cfg_attr(
1198        feature = "tracing",
1199        tracing::instrument(level = "trace", skip(self, src_descriptor, dst_server))
1200    )]
1201    pub fn to_client_tensor(
1202        &mut self,
1203        src_descriptor: CopyDescriptor,
1204        dst_server: &Self,
1205    ) -> Handle {
1206        self.expect_device_transport(Collective::Send);
1207        self.expect_local(&src_descriptor.handle);
1208        let stream_id_src = self.stream_id();
1209        let stream_id_dst = dst_server.stream_id();
1210
1211        let device_id_src = self.device.device_id();
1212        let device_id_dst = dst_server.device.device_id();
1213
1214        let dst_server = dst_server.clone();
1215        let handle = Handle::new(
1216            dst_server.service_id(),
1217            stream_id_dst,
1218            src_descriptor.handle.size_in_used(),
1219        );
1220        let handle_cloned = handle.clone();
1221
1222        // NCCL pairs sends and recvs in the order each device queues them.
1223        let _order = PEER_ORDER.lock();
1224
1225        self.device.submit(move |server_src| {
1226            // A refused send has no local buffer to answer for, so the log is
1227            // the whole local report. The peer's posted recv is left waiting
1228            // on its communication stream — the recv cannot be recalled from
1229            // here, and cross-device failure propagation needs a design pass
1230            // of its own — so the wedge is named loudly rather than hidden
1231            // behind a swallowed unwrap.
1232            if let Err(err) = server_src.send(src_descriptor, stream_id_src, device_id_dst) {
1233                log::error!(
1234                    "send to {device_id_dst:?} failed; the peer's recv is left waiting: {err}"
1235                );
1236            }
1237        });
1238
1239        dst_server.device.submit(move |server_dst| {
1240            // A failed recv taints the destination handle, so the read that
1241            // consumes this transfer fails on the cause.
1242            if let Err(err) = server_dst.recv(handle_cloned, stream_id_dst, device_id_src) {
1243                log::error!(
1244                    "recv from {device_id_src:?} failed; the destination carries the failure: {err}"
1245                );
1246            }
1247        });
1248
1249        // On a pair's first transfer, the send and the recv each wait in the join for the other, so
1250        // neither may sit in a queue behind work that waits on this thread.
1251        self.device.flush_queue();
1252        dst_server.device.flush_queue();
1253
1254        handle
1255    }
1256
1257    #[track_caller]
1258    #[cfg_attr(feature = "tracing", tracing::instrument(level="trace",
1259        skip(self, kernel, bindings),
1260        fields(
1261            kernel.name = %kernel.name(),
1262            kernel.id = %kernel.id(),
1263        )
1264    ))]
1265    unsafe fn launch_inner(
1266        &self,
1267        kernel: Box<dyn CubeKernel>,
1268        count: CubeCount,
1269        bindings: KernelArguments,
1270        stream_id: StreamId,
1271    ) {
1272        // No work, and some drivers reject a zero grid dim.
1273        if let CubeCount::Static(x, y, z) = &count
1274            && (*x == 0 || *y == 0 || *z == 0)
1275        {
1276            return;
1277        }
1278        if let CubeCount::Dynamic(binding) = &count {
1279            self.expect_local(binding);
1280        }
1281        for resource in &bindings.resources {
1282            self.expect_local(match resource {
1283                KernelResource::Buffer(binding) => binding,
1284                KernelResource::TensorMap(map) => &map.binding,
1285            });
1286        }
1287
1288        crate::launched::note(|| kernel.id());
1289
1290        // Decided here, where the launch is issued, from the stream it goes
1291        // out on: the server receives the verdict, not what decided it.
1292        let launch_mode = crate::execution::LaunchMode::new(self.service_stream());
1293        // So is where its kernels count: the device runs it later, and an
1294        // override closing in between must not change that.
1295        let issued = crate::execution::IssuedRecorder::new();
1296
1297        // A discarded launch runs nothing to time, and a backend timing windows by the
1298        // timestamps its passes write reports a window around one as never measured.
1299        let timed = !launch_mode.discards_launch();
1300        let level = self.utilities.logger.profile_level().filter(|_| timed);
1301
1302        // Before the submit, on the issuing thread: this is the last point at
1303        // which the caller's own context still exists, and attributing a
1304        // launch to what caused it is the whole reason the hook is here rather
1305        // than beside the logger's aggregation.
1306        if crate::logging::is_observing() {
1307            crate::logging::notify_launch(kernel.name());
1308        }
1309
1310        // An observer asking for timing gets the profiled path even with the
1311        // profiling logger off — the two are separate readers of the same
1312        // measurement, and making one depend on the other's configuration
1313        // would mean a caller could not time launches without also logging
1314        // them somewhere it did not choose.
1315        let observed_timing = timed && crate::logging::timing_wanted();
1316
1317        match level {
1318            None | Some(ProfileLevel::ExecutionOnly) if !observed_timing => {
1319                let utilities = self.utilities.clone();
1320                self.device.submit(move |state| {
1321                    let execution_info = if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
1322                        Some(profile_label(kernel.name(), &kernel.id()))
1323                    } else {
1324                        None
1325                    };
1326
1327                    issued.apply(|| unsafe {
1328                        state.launch(kernel, count, bindings, stream_id, launch_mode)
1329                    });
1330
1331                    if let Some(info) = execution_info {
1332                        utilities.logger.register_execution(info);
1333                    }
1334                });
1335            }
1336            level => {
1337                let name = kernel.name();
1338                let kernel_id = kernel.id();
1339                let context = self.device.clone();
1340                // The arguments travel through a slot the profiled closure
1341                // empties, because a profile can be refused — a graph capture
1342                // window refuses one on the spot — and a refusal must hand the
1343                // launch back: dropping a kernel because its measurement could
1344                // not start would turn a missing timing into a missing
1345                // computation.
1346                let slot = Arc::new(cubecl_environment::sync::Mutex::new(Some((
1347                    kernel,
1348                    count.clone(),
1349                    bindings,
1350                ))));
1351                let to_launch = slot.clone();
1352                let issued_profiled = issued.clone();
1353                let profiled = self.profile_opened(
1354                    WindowOpens::AtLaunch,
1355                    move || {
1356                        let (kernel, count, bindings) = to_launch
1357                            .lock()
1358                            .take()
1359                            .expect("filled right above, emptied only here");
1360                        context
1361                            .submit_blocking(move |state| {
1362                                issued_profiled.apply(|| unsafe {
1363                                    state.launch(kernel, count, bindings, stream_id, launch_mode)
1364                                })
1365                            })
1366                            .unwrap_or_resume()
1367                    },
1368                    name,
1369                );
1370                let profile = match profiled {
1371                    Ok(((), profile)) => profile,
1372                    Err(err) => {
1373                        // The logger's timing levels opted into profiling and
1374                        // keep their loud failure. Only the observer's timing
1375                        // degrades: it asked for a measurement, and a refused
1376                        // measurement must not take the launch down with it.
1377                        if !matches!(level, None | Some(ProfileLevel::ExecutionOnly)) {
1378                            panic!("{err:?}");
1379                        }
1380                        match slot.lock().take() {
1381                            // The refusal came before the closure ran, so the
1382                            // kernel was never submitted. Launch it the way an
1383                            // unobserved run would have.
1384                            Some((kernel, count, bindings)) => {
1385                                let utilities = self.utilities.clone();
1386                                let kernel_id = kernel.id();
1387                                self.device.submit(move |state| {
1388                                    issued.apply(|| unsafe {
1389                                        state.launch(
1390                                            kernel,
1391                                            count,
1392                                            bindings,
1393                                            stream_id,
1394                                            launch_mode,
1395                                        )
1396                                    });
1397                                    if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
1398                                        let info = profile_label(name, &kernel_id);
1399                                        utilities.logger.register_execution(info);
1400                                    }
1401                                });
1402                            }
1403                            // The closure ran, so the kernel was submitted;
1404                            // only its measurement was lost.
1405                            None => {
1406                                if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
1407                                    let info = profile_label(name, &kernel_id);
1408                                    self.utilities.logger.register_execution(info);
1409                                }
1410                            }
1411                        }
1412                        log::warn!(
1413                            "Skipped timing a launch of `{name}` for its observer: the profile was refused ({err:?})"
1414                        );
1415                        return;
1416                    }
1417                };
1418                // The observer alone: it takes the measurement unread, so the
1419                // kernels around this one keep running back to back. An observer
1420                // does not change what the logger writes, and `ExecutionOnly` is
1421                // documented as the kernels that ran without their timings, so
1422                // it logs the execution and never the profile.
1423                if observed_timing && matches!(level, None | Some(ProfileLevel::ExecutionOnly)) {
1424                    crate::logging::notify_profiled(name, profile);
1425                    if matches!(level, Some(ProfileLevel::ExecutionOnly)) {
1426                        let info = profile_label(name, &kernel_id);
1427                        self.utilities.logger.register_execution(info);
1428                    }
1429                    return;
1430                }
1431                // Both read this measurement, and a measurement is read once.
1432                // The observer is told first because resolving consumes it: the
1433                // logger's copy is the one that can be deferred, an observer's
1434                // cannot be recovered afterwards.
1435                let profile = if observed_timing {
1436                    // The observer asked to keep its measurements and cannot:
1437                    // the logger reads this one, so the observer is told a
1438                    // duration and its kernels stop overlapping.
1439                    crate::logging::warn_logger_takes_deferred_measurements();
1440                    // Comes back already resolved rather than measured again:
1441                    // the logger and the observer are two readers of one
1442                    // measurement, and a second would not be the same launch.
1443                    crate::logging::read_and_notify_timed(name, profile)
1444                } else {
1445                    profile
1446                };
1447                // Every level left times its launches: the ones that don't
1448                // either never took this path or returned above.
1449                let info = match level {
1450                    Some(ProfileLevel::Full) => {
1451                        format!("{name}: {kernel_id} CubeCount {count:?}")
1452                    }
1453                    _ => profile_label(name, &kernel_id),
1454                };
1455                self.utilities.logger.register_profiled(info, profile);
1456            }
1457        }
1458    }
1459
1460    /// Launches the `kernel` with the given `bindings`.
1461    #[track_caller]
1462    pub fn launch(&self, kernel: Box<dyn CubeKernel>, count: CubeCount, bindings: KernelArguments) {
1463        unsafe { self.launch_inner(kernel, count, bindings, self.stream_id()) }
1464    }
1465
1466    /// Whether the bytes behind `handles` can be trusted, right now and with
1467    /// no barrier: the claim check a read makes, without the read. One lookup
1468    /// per handle, so a fusion layer or an autotuner can recover per tensor
1469    /// instead of tearing down a device.
1470    ///
1471    /// Instant means enqueue-time failures only — a compile or binding
1472    /// failure is visible here immediately, a device fault is not until the
1473    /// queue drains. [`sync_buffers`](Self::sync_buffers) is the complete
1474    /// answer; [`read_one`](Self::read_one) is that plus the copy.
1475    ///
1476    /// # Errors
1477    ///
1478    /// [`ServerError::Several`] naming every failure these buffers carry, each
1479    /// once however many carry it. The bytes are gone, so there is nothing to
1480    /// retry: this is the answer, not a hint.
1481    pub fn check<'a>(
1482        &self,
1483        handles: impl IntoIterator<Item = &'a Handle>,
1484    ) -> Result<(), ServerError> {
1485        let bindings = self.bindings(handles)?;
1486        let stream_id = self.stream_id();
1487        self.device
1488            .submit_blocking(move |server| server.check(bindings, stream_id))
1489            .unwrap_or_resume()
1490    }
1491
1492    /// Flush all outstanding commands.
1493    pub fn flush(&self) -> Result<(), ServerError> {
1494        let stream_id = self.stream_id();
1495
1496        self.device
1497            .submit_blocking(move |server| server.flush(stream_id))
1498            .unwrap_or_resume()
1499    }
1500
1501    /// Compile every kernel queued for compilation (see
1502    /// [`Server::compile_queued`]), before the work submitted after it.
1503    ///
1504    /// Every measurement starts with it — a tune, a throughput probe — so a
1505    /// queue a `CompileOnly` override left is not timed as part of the first
1506    /// launch measured.
1507    pub fn compile_queued(&self) {
1508        self.device.submit(move |server| server.compile_queued());
1509    }
1510
1511    /// Prepare this client's stream for a graph capture (see
1512    /// [`Server::graph_prepare`]). Call this **before** the warmup run, then
1513    /// [`start_capture`](Self::start_capture) around the run to record.
1514    pub fn graph_prepare(&self) -> Result<(), ServerError> {
1515        let stream_id = self.stream_id();
1516        self.device
1517            .submit_blocking(move |server| server.graph_prepare(stream_id))
1518            .unwrap_or_resume()
1519    }
1520
1521    /// Begin recording launches on this client's stream into a graph rather
1522    /// than executing them (see [`Server::begin_capture`]). Pin the
1523    /// client to a dedicated stream with [`set_stream`](Self::set_stream), then
1524    /// [`graph_prepare`](Self::graph_prepare) and warm up first.
1525    ///
1526    /// Between this and [`stop_capture`](Self::stop_capture) the window records
1527    /// launches and nothing else: reading, syncing or profiling the stream is
1528    /// refused, and so is writing to a handle — a recorded graph cannot carry a
1529    /// host copy, so feed fresh inputs by writing *between* replays instead. A
1530    /// refused write is reported late, by failing `stop_capture`, rather than
1531    /// handing back a graph that silently skips it. Nothing is allocated inside
1532    /// the window: a request the pools cannot serve from what the warmup run
1533    /// left fails the capture, which is what the warmup run exists to avoid.
1534    ///
1535    /// Returns an error on backends without graph support.
1536    pub fn start_capture(&self) -> Result<(), ServerError> {
1537        let stream_id = self.stream_id();
1538        self.device
1539            .submit_blocking(move |server| server.begin_capture(stream_id))
1540            .unwrap_or_resume()
1541    }
1542
1543    /// Whether this client's stream is capturing a graph: from
1544    /// [`graph_prepare`](Self::graph_prepare), through the warmup run and the recorded one, until
1545    /// the capture ends. It ends with [`stop_capture`](Self::stop_capture), whether the window
1546    /// opened or the capture was only prepared, and also when another logical stream sharing
1547    /// the same backend stream stops it, or when opening the window fails once
1548    /// [`start_capture`](Self::start_capture) is accepted. A `start_capture` refused up front,
1549    /// e.g. while a capture already records, leaves the capture as it was. Always `false` on a
1550    /// backend without graph support.
1551    ///
1552    /// Answered without reaching the device thread, so code whose buffer decisions have to
1553    /// match between the warmup and the recording can ask before every launch.
1554    pub fn is_capturing(&self) -> bool {
1555        let captures = &self.utilities.captures;
1556        // The stream id costs more than the check, and no capture under way answers already.
1557        captures.any_active() && captures.is_capturing(self.stream_id())
1558    }
1559
1560    /// Stop recording and return the captured graph, ready to
1561    /// [`replay`](Graph::replay).
1562    pub fn stop_capture(&self) -> Result<Graph, ServerError> {
1563        let stream_id = self.stream_id();
1564        let id = self
1565            .device
1566            .submit_blocking(move |server| server.end_capture(stream_id))
1567            .unwrap_or_resume()?;
1568
1569        Ok(Graph {
1570            inner: Arc::new(GraphHandle {
1571                id,
1572                device: self.device.clone(),
1573                stream_id,
1574            }),
1575        })
1576    }
1577
1578    /// Wait for the completion of every task in the server.
1579    ///
1580    /// The barrier alone, which also reports a device fault — the only failure
1581    /// left that no buffer can report. A launch failure is not this sync's to
1582    /// report: it lives on the buffers the launch never wrote and surfaces on
1583    /// any read, [`check`](Self::check) or
1584    /// [`sync_buffers`](Self::sync_buffers) of those.
1585    pub fn sync(&self) -> DynFut<Result<(), ServerError>> {
1586        self.sync_buffers([])
1587    }
1588
1589    /// The barrier, and then an answer for `handles`.
1590    ///
1591    /// [`sync`](Self::sync) first, so a device fault counts, and then the
1592    /// claim check a read would have made — a read without the read, for the
1593    /// caller that needs to know its work produced something trustworthy and
1594    /// does not want to pull it to the host to find out.
1595    ///
1596    /// # Errors
1597    ///
1598    /// The device fault the barrier found, or [`ServerError::Several`] naming
1599    /// every failure these buffers carry.
1600    pub fn sync_buffers<'a>(
1601        &self,
1602        handles: impl IntoIterator<Item = &'a Handle>,
1603    ) -> DynFut<Result<(), ServerError>> {
1604        let stream_id = self.stream_id();
1605        let bindings = match self.bindings(handles) {
1606            Ok(bindings) => bindings,
1607            Err(err) => return Box::pin(core::future::ready(Err(err))),
1608        };
1609
1610        let fut = self
1611            .device
1612            .submit_blocking(move |server| server.sync(bindings, stream_id))
1613            .unwrap_or_resume();
1614
1615        self.utilities.logger.profile_summary();
1616
1617        fut
1618    }
1619
1620    /// The bindings `handles` name, which is what crosses to the device
1621    /// thread: a `Handle` borrows, and the closure that answers for it runs
1622    /// somewhere else.
1623    fn bindings<'a>(
1624        &self,
1625        handles: impl IntoIterator<Item = &'a Handle>,
1626    ) -> Result<Vec<BufferBinding>, ServerError> {
1627        handles
1628            .into_iter()
1629            .map(|handle| {
1630                let binding = handle.clone().binding();
1631                self.local(&binding)?;
1632                Ok(binding)
1633            })
1634            .collect()
1635    }
1636
1637    /// Get the features supported by the compute server.
1638    pub fn properties(&self) -> &DeviceProperties {
1639        &self.utilities.properties
1640    }
1641
1642    /// Get the features supported by the compute server.
1643    pub fn features(&self) -> &Features {
1644        &self.utilities.properties.features
1645    }
1646
1647    /// Which hardware this device is: what a measurement on it holds for.
1648    pub fn hardware_identity(&self) -> HardwareIdentity {
1649        HardwareIdentity::new(self.name(), self.properties())
1650    }
1651
1652    /// The device properties, shared: what a kernel keeps to expand itself
1653    /// on the device thread without holding the client.
1654    pub fn properties_shared(&self) -> Arc<DeviceProperties> {
1655        self.utilities.properties.clone()
1656    }
1657
1658    /// What the target this client compiles for guarantees about its own
1659    /// instructions, resolved once when the device came up.
1660    pub fn target_properties(&self) -> &TargetProperties {
1661        &self.utilities.target_properties
1662    }
1663
1664    /// The target properties, shared: the other half of what a kernel keeps to
1665    /// expand itself on the device thread without naming a runtime.
1666    ///
1667    /// Cloning this is one atomic increment, which is why the generated launch
1668    /// functions can afford to do it per launch where calling
1669    /// [`Runtime::target_properties`] again would not be.
1670    ///
1671    /// [`Runtime::target_properties`]: crate::runtime::Runtime::target_properties
1672    pub fn target_properties_shared(&self) -> Arc<TargetProperties> {
1673        self.utilities.target_properties.clone()
1674    }
1675
1676    /// Everything the memory of `scope` holds, stream by stream: each pool's
1677    /// shape, usage and high-water marks. [`MemoryReport::usage`] sums them.
1678    ///
1679    /// Pools are per stream: a plan measured for a workload reads
1680    /// [`CurrentStream`](MemoryScope::CurrentStream), the stream that runs it,
1681    /// and a caller asking how much the device holds reads
1682    /// [`Device`](MemoryScope::Device).
1683    pub fn memory_report(&self, scope: MemoryScope) -> MemoryReport {
1684        let stream_id = self.stream_id();
1685        self.device
1686            .submit_blocking(move |server| {
1687                let streams = match scope {
1688                    MemoryScope::Device => server.stream_ids(),
1689                    MemoryScope::CurrentStream => Vec::from([stream_id]),
1690                };
1691                MemoryReport {
1692                    streams: streams
1693                        .into_iter()
1694                        .filter_map(|id| server.memory_report(id))
1695                        .collect(),
1696                }
1697            })
1698            .unwrap_or_resume()
1699    }
1700
1701    /// Write a snapshot of the device's [memory report](Self::memory_report),
1702    /// every stream of it, to the environment's records under `label`.
1703    /// Nothing is read when the environment records nothing.
1704    pub fn record_memory(&self, label: &str) {
1705        if !cubecl_environment::records::enabled() {
1706            return;
1707        }
1708        let record = crate::memory_management::MemoryRecord {
1709            label: label.into(),
1710            report: self.memory_report(MemoryScope::Device),
1711        };
1712        cubecl_environment::records::write(
1713            cubecl_environment::records::RecordEffect::Observed,
1714            &record,
1715        );
1716    }
1717
1718    /// Change the memory allocation mode.
1719    ///
1720    /// # Safety
1721    ///
1722    /// This function isn't thread safe and might create memory leaks.
1723    pub unsafe fn allocation_mode(&self, mode: MemoryAllocationMode) {
1724        let stream_id = self.stream_id();
1725        self.device
1726            .submit(move |server| server.allocation_mode(mode, stream_id));
1727    }
1728
1729    /// Ask the client to release memory that it can release.
1730    ///
1731    /// Nb: Results will vary on what the memory allocator deems beneficial,
1732    /// so it's not guaranteed any memory is freed.
1733    ///
1734    /// # Errors
1735    ///
1736    /// Refused while a stream records a graph: releasing memory waits on the
1737    /// device, and a wait on a stream that records aborts its capture.
1738    pub fn memory_cleanup(&self) -> Result<(), ServerError> {
1739        self.device
1740            .submit_blocking(move |server| {
1741                server
1742                    .stream_ids()
1743                    .into_iter()
1744                    .try_for_each(|id| server.memory_cleanup(id))
1745            })
1746            .unwrap_or_resume()
1747    }
1748
1749    /// Open a profiling window at the current position of the calling stream.
1750    ///
1751    /// Prefer the bracketed [`profile`](Self::profile), which also holds the
1752    /// device for the closure. This pair is for a caller that cannot bracket the
1753    /// work in a closure — a lazy queue drained on another thread, say — and
1754    /// only knows *when* on the stream its window opens and closes.
1755    ///
1756    /// The window keeps the stream it was opened on, and
1757    /// [`profile_end`](Self::profile_end) closes it there whichever thread
1758    /// calls it. Nothing keeps other streams' work out of the window.
1759    ///
1760    /// An open window costs something on every backend and stays open until it
1761    /// is ended or [abandoned](Self::profile_abandon), so a caller that bails
1762    /// out between the two calls has to abandon it.
1763    pub fn profile_start(&self) -> Result<ProfileWindow, ProfileError> {
1764        let stream_id = self.stream_id();
1765        let token = self
1766            .device
1767            .submit_blocking(move |server| server.start_profile(stream_id))
1768            .unwrap_or_resume()
1769            .map_err(|err| ProfileError::from(&err))?;
1770        Ok(ProfileWindow { stream_id, token })
1771    }
1772
1773    /// Close `window` at the current position of the stream it was opened on.
1774    pub fn profile_end(&self, window: ProfileWindow) -> Result<ProfileDuration, ProfileError> {
1775        let ProfileWindow { stream_id, token } = window;
1776        self.device
1777            .submit_blocking(move |server| server.end_profile(stream_id, token))
1778            .unwrap_or_resume()
1779    }
1780
1781    /// Drop `window` without measuring it, for a caller that will never reach
1782    /// [`profile_end`](Self::profile_end), such as an error path between the
1783    /// two calls.
1784    ///
1785    /// Does not wait for the server to drop it, but does flush, because this
1786    /// is usually a caller's last word: an abandon left sitting in the queue
1787    /// holds the window open for exactly as long as it is the only thing in
1788    /// there, which is the case it exists for.
1789    pub fn profile_abandon(&self, window: ProfileWindow) {
1790        let ProfileWindow { stream_id, token } = window;
1791        self.device
1792            .submit(move |server| server.abandon_profile(stream_id, token));
1793        self.device.flush_queue();
1794    }
1795
1796    /// Measure the execution time of some inner operations.
1797    #[track_caller]
1798    pub fn profile<O: Send + 'static>(
1799        &self,
1800        func: impl FnOnce() -> O + Send,
1801        func_name: &str,
1802    ) -> Result<(O, ProfileDuration), ProfileError> {
1803        self.profile_opened(WindowOpens::Now, func, func_name)
1804    }
1805
1806    /// [`profile`](Self::profile), its window opened where `opens` says.
1807    #[track_caller]
1808    fn profile_opened<O: Send + 'static>(
1809        &self,
1810        opens: WindowOpens,
1811        func: impl FnOnce() -> O + Send,
1812        #[allow(unused)] func_name: &str,
1813    ) -> Result<(O, ProfileDuration), ProfileError> {
1814        // Get the outer caller. For execute() this points straight to the
1815        // cube kernel. For general profiling it points to whoever calls profile.
1816        #[cfg(feature = "profile-tracy")]
1817        let location = std::panic::Location::caller();
1818
1819        // Make a CPU span. If the server has system profiling this is all you need.
1820        #[cfg(feature = "profile-tracy")]
1821        let _span = tracy_client::Client::running().unwrap().span_alloc(
1822            None,
1823            func_name,
1824            location.file(),
1825            location.line(),
1826            0,
1827        );
1828
1829        let stream_id = self.stream_id();
1830
1831        #[cfg(feature = "profile-tracy")]
1832        let gpu_span = if self.utilities.properties.timing_method == TimingMethod::Device {
1833            let gpu_span = self
1834                .utilities
1835                .gpu_client
1836                .span_alloc(func_name, "profile", location.file(), location.line())
1837                .unwrap();
1838            Some(gpu_span)
1839        } else {
1840            None
1841        };
1842
1843        let device = self.device.clone();
1844        #[allow(unused_mut, reason = "Used in profile-tracy")]
1845        let mut result = self
1846            .device
1847            .exclusive(move || {
1848                // We first get mut access to the server to create a token.
1849                // Then we free to server, since it's going to be accessed in `func()`.
1850                let token = match device.submit_blocking(move |server| match opens {
1851                    WindowOpens::Now => server.start_profile(stream_id),
1852                    WindowOpens::AtLaunch => server.start_profile_at_launch(stream_id),
1853                }) {
1854                    Ok(token) => match token {
1855                        Ok(token) => token,
1856                        Err(err) => return Err(err),
1857                    },
1858                    Err(err) => {
1859                        return Err(ServerError::Generic {
1860                            reason: alloc::format!(
1861                                "Can't start profiling because of a call error: {err:?}"
1862                            ),
1863                            backtrace: BackTrace::capture(),
1864                        });
1865                    }
1866                };
1867
1868                // We execute `func()` which will recursibly access the server.
1869                let out = func();
1870
1871                // Finally we get the result from the token.
1872                let result = device
1873                    .submit_blocking(move |server| {
1874                        let mut result = server.end_profile(stream_id, token);
1875
1876                        match result {
1877                            Ok(result) => Ok((out, result)),
1878                            Err(err) => Err(err),
1879                        }
1880                    })
1881                    .unwrap_or_resume();
1882
1883                Ok(result)
1884            })
1885            .unwrap_or_resume()
1886            .map_err(|err| ProfileError::from(&err))?;
1887
1888        #[cfg(feature = "profile-tracy")]
1889        if let Some(mut gpu_span) = gpu_span {
1890            gpu_span.end_zone();
1891            let epoch = self.utilities.epoch_time;
1892            // Add in the work to upload the timestamp data.
1893            result = result.map(|(o, result)| {
1894                (
1895                    o,
1896                    ProfileDuration::new(
1897                        alloc::boxed::Box::pin(async move {
1898                            let ticks = result.resolve().await;
1899                            // A window that carried no measurement has no span
1900                            // to place: `resolve` answers `None` rather than a
1901                            // zero so nothing reports it as an instant at the
1902                            // epoch.
1903                            if let Some(ticks) = &ticks {
1904                                let start_duration =
1905                                    ticks.start_duration_since(epoch).as_nanos() as i64;
1906                                let end_duration =
1907                                    ticks.end_duration_since(epoch).as_nanos() as i64;
1908                                gpu_span.upload_timestamp_start(start_duration);
1909                                gpu_span.upload_timestamp_end(end_duration);
1910                            }
1911                            ticks
1912                        }),
1913                        TimingMethod::Device,
1914                    ),
1915                )
1916            });
1917        }
1918
1919        result
1920    }
1921
1922    /// Transfer data from one client to another
1923    #[cfg_attr(
1924        feature = "tracing",
1925        tracing::instrument(
1926            level = "trace",
1927            skip(self, src_descriptor, alloc_descriptor, dst_server)
1928        )
1929    )]
1930    fn change_client_sync(
1931        &self,
1932        src_descriptor: CopyDescriptor,
1933        alloc_descriptor: MemoryLayoutDescriptor,
1934        dst_server: &Self,
1935    ) -> MemoryLayout {
1936        let shape = src_descriptor.shape.clone();
1937        let elem_size = src_descriptor.elem_size;
1938        let stream_id_src = self.stream_id();
1939        let stream_id_dst = dst_server.stream_id();
1940
1941        let read = self
1942            .device
1943            .submit_blocking(move |server| server.read(vec![src_descriptor], stream_id_src))
1944            .unwrap_or_resume();
1945
1946        let mut data = cubecl_environment::future::block_on(read).unwrap();
1947
1948        // The allocation belongs to the destination: it is initialized and
1949        // written there, so it takes that device's layout policy, stream and
1950        // `ServiceId`. Stamping it from `self` would hand back a handle the
1951        // destination refuses as foreign.
1952        let (handle_base, mut layouts) = dst_server.utilities.layout_policy.apply(
1953            dst_server.service_id(),
1954            stream_id_dst,
1955            &[alloc_descriptor],
1956        );
1957        let alloc = layouts.remove(0);
1958
1959        let desc_descriptor = CopyDescriptor {
1960            handle: handle_base.clone().binding(),
1961            shape,
1962            strides: alloc.strides.clone(),
1963            elem_size,
1964        };
1965
1966        let (size, memory) = (handle_base.size(), handle_base.memory);
1967        dst_server.device.submit(move |server| {
1968            // Cannot write on memory that wasn't initialized. The error is attached to the
1969            // buffer and is reported at the next sync point.
1970            if server
1971                .initialize_memory(memory, size, stream_id_dst)
1972                .is_ok()
1973            {
1974                server.write(vec![(desc_descriptor, data.remove(0))], stream_id_dst)
1975            }
1976        });
1977
1978        alloc
1979    }
1980
1981    /// Returns all vector sizes that are useful to perform optimal IO operation on the given element.
1982    pub fn io_optimized_vector_sizes(
1983        &self,
1984        size: usize,
1985    ) -> impl Iterator<Item = VectorSize> + Clone {
1986        self.properties().io_optimized_vector_sizes(size)
1987    }
1988
1989    /// Calculates the maximum throughput of the device given the given config (like tensor core with certain sizes and dtypes, or just arithmetic by dtype)
1990    ///
1991    /// `probe` runs on the device runner thread with the device to itself,
1992    /// since one sharing it measures its share rather than the peak. A cached
1993    /// answer takes neither.
1994    ///
1995    /// # Errors
1996    ///
1997    /// Whatever `probe` reports, or [`Launch`](ThroughputError::Launch) where
1998    /// the device could not be taken.
1999    pub fn measure_throughput(
2000        &self,
2001        key: ThroughputKey,
2002        probe: impl FnOnce() -> Result<ThroughputValue, ThroughputError> + Send,
2003    ) -> Result<ThroughputValue, ThroughputError> {
2004        let cache = ThroughputCache::get_for_device(self.name(), self.properties());
2005        let mut throughputs = ThroughputBenchmarker::new(cache);
2006
2007        if let Some(value) = throughputs.cached(key) {
2008            return Ok(value);
2009        }
2010
2011        // Kernels queued for compilation compile now, together, rather than in the probe's
2012        // first launch, inside the time it measures.
2013        self.compile_queued();
2014        // Asked again inside: another thread may have answered while this one queued.
2015        self.exclusive(move || throughputs.measure(key, probe))
2016            .unwrap_or(Err(ThroughputError::Launch))
2017    }
2018}
2019
2020fn profile_label(name: &'static str, kernel_id: &KernelId) -> String {
2021    let base = type_name_format(name, TypeNameFormatLevel::Balanced);
2022    kernel_id.entrypoint_name(&base)
2023}