Skip to main content

cubecl_runtime/
client.rs

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