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