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}