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