cubecl_runtime/server/handle.rs
1use alloc::sync::Arc;
2use cubecl_common::{device::ServiceId, device_handle::DeviceHandle};
3use cubecl_environment::{stream::StreamId, sync::Mutex};
4use cubecl_zspace::{Shape, Strides};
5
6use crate::{
7 memory_management::{
8 ManagedMemoryBinding, ManagedMemoryHandle, ManagedMemoryId, WeakMemoryBinding,
9 },
10 server::{CopyDescriptor, Server, ServerError, TensorMapBinding},
11};
12
13/// How two handles' bytes relate.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Overlap {
16 /// No byte in common.
17 Disjoint,
18 /// The same bytes.
19 Same,
20 /// Some bytes in common, but not all.
21 Partial,
22}
23
24/// Server handle containing the [memory handle](crate::server::Handle).
25pub struct Handle {
26 /// Memory handle.
27 pub memory: ManagedMemoryHandle,
28 /// The service whose memory this handle addresses. A client checks it
29 /// before handing the handle to its device: memory coordinates mean
30 /// nothing on another device, and reading them there is not an error
31 /// but garbage.
32 pub service: ServiceId,
33 /// Memory offset in bytes.
34 pub offset_start: Option<u64>,
35 /// Memory offset in bytes.
36 pub offset_end: Option<u64>,
37 /// The stream where the data was created.
38 pub stream: StreamId,
39 /// Length of the underlying buffer ignoring offsets
40 pub(crate) size: u64,
41 /// The address kernels on other devices bind this memory at, once the
42 /// owning client [shared](crate::client::Client::share_with_peers) it.
43 pub peer: Option<PeerMemory>,
44}
45
46/// Memory one device lets kernels on the other devices of its runtime bind.
47///
48/// It holds the address the owning device resolved for the allocation, and a
49/// binding that keeps the allocation in place there: a bound allocation is
50/// never freed, handed out again or relocated, so the address stays valid for
51/// as long as any clone lives.
52///
53/// Nothing orders the accesses. A kernel on another device reads whatever the
54/// memory holds when it runs, and its writes land whenever they land. Callers
55/// order them by synchronizing the devices, or by handing off inside the
56/// kernels through system-scope atomics.
57///
58/// Failures cross over through it. The owner marks it when work that should
59/// have written the memory failed, so a peer kernel reading it does not run on
60/// stale bytes, and a peer kernel that failed to write it marks it and taints
61/// the buffer on its owner, so a read there fails on the cause.
62#[derive(Clone)]
63pub struct PeerMemory {
64 inner: Arc<PeerMemoryInner>,
65}
66
67struct PeerMemoryInner {
68 /// Keeps the allocation in place on its device.
69 pin: ManagedMemoryBinding,
70 /// The device address of the allocation's first byte.
71 address: u64,
72 /// The device that owns the memory, which a peer's failure is reported to.
73 owner: DeviceHandle<dyn Server>,
74 /// The failure the memory carries, as far as peers are concerned.
75 failure: Mutex<Option<ServerError>>,
76}
77
78impl PeerMemory {
79 /// Memory at `address` on `owner`, held in place by `pin`.
80 ///
81 /// Only the owning client builds one, from the address its device
82 /// resolved: a kernel dereferences the address as it is.
83 pub(crate) fn new(
84 pin: ManagedMemoryBinding,
85 address: u64,
86 owner: DeviceHandle<dyn Server>,
87 ) -> Self {
88 Self {
89 inner: Arc::new(PeerMemoryInner {
90 pin,
91 address,
92 owner,
93 failure: Mutex::new(None),
94 }),
95 }
96 }
97
98 /// The device address of the allocation's first byte.
99 pub fn address(&self) -> u64 {
100 self.inner.address
101 }
102
103 /// The device that owns the memory.
104 pub fn owner(&self) -> &DeviceHandle<dyn Server> {
105 &self.inner.owner
106 }
107
108 /// The failure of the work that should have written the memory last, if it failed.
109 pub fn failure(&self) -> Option<ServerError> {
110 self.inner.failure.lock().clone()
111 }
112
113 /// The work that should have written the memory failed with `error`.
114 pub fn fail(&self, error: ServerError) {
115 *self.inner.failure.lock() = Some(error);
116 }
117
118 /// Work writing the memory was enqueued. Whether it carried a failure until now.
119 pub fn repair(&self) -> bool {
120 self.inner.failure.lock().take().is_some()
121 }
122
123 /// Whether this is the memory `binding` names.
124 fn names(&self, binding: &BufferBinding) -> bool {
125 self.inner.pin.id() == binding.memory.id()
126 }
127}
128
129impl core::fmt::Debug for PeerMemory {
130 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131 f.debug_struct("PeerMemory")
132 .field("address", &format_args!("{:#x}", self.inner.address))
133 .finish()
134 }
135}
136
137impl core::fmt::Debug for Handle {
138 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139 f.debug_struct("Handle")
140 .field("id", &self.memory)
141 .field("service", &self.service)
142 .field("offset_start", &self.offset_start)
143 .field("offset_end", &self.offset_end)
144 .field("stream", &self.stream)
145 .field("size", &self.size)
146 .field("peer", &self.peer)
147 .finish()
148 }
149}
150
151impl Clone for Handle {
152 fn clone(&self) -> Self {
153 Self {
154 memory: self.memory.clone(),
155 service: self.service,
156 offset_start: self.offset_start,
157 offset_end: self.offset_end,
158 stream: self.stream,
159 size: self.size,
160 peer: self.peer.clone(),
161 }
162 }
163}
164
165impl Handle {
166 /// Creates a new handle of the given size.
167 pub fn from_memory(
168 id: ManagedMemoryHandle,
169 service: ServiceId,
170 stream: StreamId,
171 size: u64,
172 ) -> Self {
173 Self {
174 memory: id,
175 service,
176 offset_start: None,
177 offset_end: None,
178 stream,
179 size,
180 peer: None,
181 }
182 }
183 /// Creates a new handle of the given size.
184 pub fn new(service: ServiceId, stream: StreamId, size: u64) -> Self {
185 Self {
186 memory: ManagedMemoryHandle::new(),
187 service,
188 offset_start: None,
189 offset_end: None,
190 stream,
191 size,
192 peer: None,
193 }
194 }
195 /// Checks whether the handle can be mutated in-place without affecting other computation.
196 pub fn can_mut(&self) -> bool {
197 self.memory.can_mut()
198 }
199
200 /// Returns the [`BufferBinding`] corresponding to the current handle.
201 pub fn binding(self) -> BufferBinding {
202 BufferBinding {
203 memory: self.memory.binding(),
204 service: self.service,
205 offset_start: self.offset_start,
206 offset_end: self.offset_end,
207 stream: self.stream,
208 size: self.size,
209 peer: self.peer,
210 }
211 }
212
213 /// Add to the current offset in bytes.
214 pub fn offset_start(mut self, offset: u64) -> Self {
215 if let Some(val) = &mut self.offset_start {
216 *val += offset;
217 } else {
218 self.offset_start = Some(offset);
219 }
220
221 self
222 }
223 /// Add to the current offset in bytes.
224 pub fn offset_end(mut self, offset: u64) -> Self {
225 if let Some(val) = &mut self.offset_end {
226 *val += offset;
227 } else {
228 self.offset_end = Some(offset);
229 }
230
231 self
232 }
233
234 /// Convert the [handle](Handle) into a [binding](Binding) with shape and stride metadata.
235 pub fn copy_descriptor(
236 self,
237 shape: Shape,
238 strides: Strides,
239 elem_size: usize,
240 ) -> CopyDescriptor {
241 CopyDescriptor {
242 shape,
243 strides,
244 elem_size,
245 handle: self.binding(),
246 }
247 }
248 /// How this handle's bytes relate to `other`'s.
249 pub fn overlap(&self, other: &Self) -> Overlap {
250 let bytes = |handle: &Self| {
251 handle.offset_start.unwrap_or(0)..handle.size - handle.offset_end.unwrap_or(0)
252 };
253 let mine = bytes(self);
254 let theirs = bytes(other);
255 if self.memory.descriptor().id != other.memory.descriptor().id
256 || mine.end <= theirs.start
257 || theirs.end <= mine.start
258 {
259 Overlap::Disjoint
260 } else if mine == theirs {
261 Overlap::Same
262 } else {
263 Overlap::Partial
264 }
265 }
266 /// Get the size of the handle, in bytes, accounting for offsets
267 pub fn size_in_used(&self) -> u64 {
268 self.size - self.offset_start.unwrap_or(0) - self.offset_end.unwrap_or(0)
269 }
270 /// Get the total size of the handle, in bytes.
271 pub fn size(&self) -> u64 {
272 self.size
273 }
274}
275
276/// A resource passed to a kernel function
277#[allow(clippy::large_enum_variant)]
278#[derive(Clone, Debug)]
279pub enum KernelResource {
280 /// Buffer resource
281 Buffer(BufferBinding),
282 /// Tensor map resource for CUDA
283 TensorMap(TensorMapBinding),
284 /// Memory shared with the other devices of the runtime, passed to a `Peer`
285 /// parameter. The client binds it as a [`Buffer`](Self::Buffer) on the
286 /// device it lives on, so a server only ever sees memory of another device
287 /// here, bound at its address there.
288 Peer(PeerBinding),
289}
290
291impl KernelResource {
292 /// The buffer of this device the resource binds, or `None` for memory
293 /// of another device.
294 pub fn local_binding(&self) -> Option<&BufferBinding> {
295 match self {
296 KernelResource::Buffer(binding) => Some(binding),
297 KernelResource::TensorMap(map) => Some(&map.binding),
298 KernelResource::Peer(_) => None,
299 }
300 }
301}
302
303/// A buffer of another device, as a kernel binds it.
304#[derive(Clone, Debug)]
305pub struct PeerBinding {
306 /// The buffer on its own device. Holding it keeps the memory in place
307 /// until the server drops it, once the kernel binding it completed.
308 binding: BufferBinding,
309 /// The device address of the bound bytes, offsets applied.
310 address: u64,
311}
312
313impl PeerBinding {
314 /// `binding` at the address its owner shared, or `None` when it was
315 /// never shared, or the shared memory is not the memory it names.
316 pub fn new(binding: BufferBinding) -> Option<Self> {
317 let peer = binding.peer.as_ref()?;
318 if !peer.names(&binding) {
319 return None;
320 }
321 let address = peer.address() + binding.offset_start.unwrap_or(0);
322 Some(Self { binding, address })
323 }
324
325 /// The buffer on its own device.
326 pub fn binding(&self) -> &BufferBinding {
327 &self.binding
328 }
329
330 /// The memory shared with peers the buffer lives in.
331 pub fn memory(&self) -> &PeerMemory {
332 self.binding
333 .peer
334 .as_ref()
335 .expect("a peer binding is built from shared memory")
336 }
337
338 /// The buffer on its own device, which keeps the memory in place.
339 pub fn into_binding(self) -> BufferBinding {
340 self.binding
341 }
342
343 /// The device address of the bound bytes, offsets applied.
344 pub fn address(&self) -> u64 {
345 self.address
346 }
347
348 /// The bytes bound.
349 pub fn size(&self) -> u64 {
350 self.binding.size_in_used()
351 }
352}
353
354/// A buffer binding represents a [Handle] that is bound to managed memory.
355///
356/// The memory used is known by the compute server.
357/// A buffer binding is only valid after being initlized with [`super::Server::initialize_bindings`]
358///
359/// # Notes
360///
361/// A buffer binding is detached from a [`Handle`], meaning that is won't affect [`Handle::can_mut`].
362#[derive(Clone, Debug)]
363pub struct BufferBinding {
364 /// The id of the handle the binding is bound to.
365 pub memory: ManagedMemoryBinding,
366 /// The service whose memory this binding addresses; see [`Handle::service`].
367 pub service: ServiceId,
368 /// Memory offset in bytes.
369 pub offset_start: Option<u64>,
370 /// Memory offset in bytes.
371 pub offset_end: Option<u64>,
372 /// The stream where the data was created.
373 pub stream: StreamId,
374 /// Length of the underlying buffer ignoring offsets
375 pub size: u64,
376 /// See [`Handle::peer`].
377 pub peer: Option<PeerMemory>,
378}
379
380/// A [`BufferBinding`] that does not keep its memory reserved: what a record
381/// of a buffer holds when holding the buffer would change what its pool may
382/// reuse.
383#[derive(Clone, Debug)]
384pub struct WeakBufferBinding {
385 memory: WeakMemoryBinding,
386 service: ServiceId,
387 offset_start: Option<u64>,
388 offset_end: Option<u64>,
389 stream: StreamId,
390 size: u64,
391}
392
393impl WeakBufferBinding {
394 /// The binding back, while its memory is allocated (see
395 /// [`WeakMemoryBinding::upgrade`]).
396 pub fn upgrade(&self) -> Option<BufferBinding> {
397 Some(BufferBinding {
398 memory: self.memory.upgrade()?,
399 service: self.service,
400 offset_start: self.offset_start,
401 offset_end: self.offset_end,
402 stream: self.stream,
403 size: self.size,
404 peer: None,
405 })
406 }
407}
408
409impl BufferBinding {
410 /// This binding, without keeping its memory reserved.
411 pub fn downgrade(&self) -> WeakBufferBinding {
412 WeakBufferBinding {
413 memory: self.memory.downgrade(),
414 service: self.service,
415 offset_start: self.offset_start,
416 offset_end: self.offset_end,
417 stream: self.stream,
418 size: self.size,
419 }
420 }
421
422 /// Get the size of the handle, in bytes, accounting for offsets
423 pub fn size_in_used(&self) -> u64 {
424 self.size - self.offset_start.unwrap_or(0) - self.offset_end.unwrap_or(0)
425 }
426
427 /// The byte range of the allocation this binding names: what the offsets
428 /// leave of the buffer. This is the region the taint bookkeeping claims
429 /// when work writing through this binding fails, and releases when work
430 /// writing through it lands.
431 pub fn range(&self) -> core::ops::Range<u64> {
432 self.offset_start.unwrap_or(0)..self.size - self.offset_end.unwrap_or(0)
433 }
434
435 /// The identity a claim on this binding is stored under: the allocation
436 /// and the byte range of it, exactly as [`range`](Self::range) computes
437 /// it.
438 ///
439 /// Anything that deduplicates or compares claims — a capture's write set,
440 /// above all — must key by this and nothing coarser. Two tensors carved
441 /// from one batched allocation share a memory id and nothing else; keyed
442 /// by the id alone, one sibling's claim swallows the others'.
443 pub fn claim_key(&self) -> (ManagedMemoryId, u64, u64) {
444 let range = self.range();
445 (self.memory.id(), range.start, range.end)
446 }
447 /// Get the total size of the handle, in bytes.
448 pub fn size(&self) -> u64 {
449 self.size
450 }
451}