Skip to main content

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}