Skip to main content

cubecl_runtime/
client.rs

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