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