Skip to main content

cubecl_server/command/
backend.rs

1//! What a backend supplies so the shared [`Command`](super::Command) can run.
2//!
3//! Two traits, because they answer different questions. [`Driver`] is the four
4//! device calls the runtime cannot make itself. [`DeviceStream`] is where a
5//! backend's stream keeps the state those calls move in step with — a command
6//! reaches all of it, and none of it is the driver's to reach.
7
8use crate::allocator::Pitch;
9use crate::id::KernelId;
10use crate::memory_management::drop_queue::{Fence, PendingDropQueue};
11use crate::memory_management::{ManagedMemoryBinding, MemoryManagement};
12use crate::metadata_cache::MetadataInfoCache;
13use crate::server::{Handle, IoError, LaunchError};
14use crate::storage::ComputeStorage;
15use crate::stream::{EventStreamBackend, StreamCapture};
16use cubecl_common::bytes::Bytes;
17use cubecl_environment::backtrace::BackTrace;
18use cubecl_zspace::{Shape, Strides, striding::has_pitched_row_major_strides};
19
20/// The state a backend's stream keeps beside its driver handle.
21///
22/// All of it moves in step with the work queued on that stream, which is why
23/// it is the stream's and not the device's: the deferred frees a fenced flush
24/// releases, the capture window that forbids allocating, the per-launch info
25/// buffers a capture may not evict, and the memory the stream's kernels see.
26pub trait DeviceStream {
27    /// The fence this backend records on a stream. Fencing is how the drop
28    /// queue knows the device is done with what it is holding.
29    type Fence: Fence + Send + 'static;
30    /// The storage backing the memory this stream's kernels address.
31    type DeviceStorage: ComputeStorage;
32    /// The storage backing the host buffers that stage transfers to it.
33    type HostStorage: ComputeStorage;
34
35    /// The memory this stream's kernels see. Allocations are per stream, so a
36    /// buffer resolves to the stream that created it and nowhere else.
37    fn device_memory(&mut self) -> &mut MemoryManagement<Self::DeviceStorage>;
38
39    /// The pinned host memory this stream stages transfers through.
40    fn host_memory(&mut self) -> &mut MemoryManagement<Self::HostStorage>;
41
42    /// Frees deferred until the device is known to be done with them.
43    fn drop_queue(&mut self) -> &mut PendingDropQueue<Self::Fence>;
44
45    /// Where this stream sits in the graph-capture lifecycle.
46    fn capturing(&mut self) -> &mut StreamCapture;
47
48    /// The per-launch metadata buffers this stream reuses.
49    fn info_cache(&mut self) -> &mut MetadataInfoCache<Handle>;
50
51    /// A cheap, copyable identifier for this stream.
52    ///
53    /// It exists so a fence can be recorded while the stream's own fields are
54    /// borrowed: draining the drop queue needs a fresh fence per rotation, and
55    /// the queue is reached through `&mut self`.
56    type Signal: Copy;
57
58    /// This stream's signal.
59    fn signal(&self) -> Self::Signal;
60
61    /// Record a fence on the stream `signal` names, signalled once everything
62    /// already enqueued on it has run.
63    fn fence(signal: Self::Signal) -> Self::Fence;
64}
65
66/// The layout of the device side of a copy.
67///
68/// The pitch is computed once, by [`of`](Self::of), because whether a buffer's
69/// rows are padded is the same question whichever driver performs the copy —
70/// and getting it wrong scrambles the rows rather than failing.
71///
72/// Which is why a driver cannot build one. The fields are readable, since a
73/// driver needs all four, but only `of` puts them together: a hand-built
74/// layout could carry a pitch its strides do not agree with, and nothing
75/// downstream would notice.
76#[non_exhaustive]
77pub struct CopyLayout<'a> {
78    /// The extent of each dimension.
79    pub shape: &'a Shape,
80    /// The stride of each dimension, in elements.
81    pub strides: &'a Strides,
82    /// The size of one element, in bytes.
83    pub elem_size: usize,
84    /// The 2D geometry when the rows are padded; `None` when the whole buffer
85    /// is one contiguous span and a linear copy is both correct and faster.
86    pub pitch: Option<Pitch>,
87}
88
89impl<'a> CopyLayout<'a> {
90    /// The layout of a copy over this shape, refusing anything the drivers
91    /// cannot express.
92    ///
93    /// # Errors
94    ///
95    /// [`IoError::UnsupportedStrides`] for a layout that is not pitched
96    /// row-major. A driver copies either one contiguous span or a stack of
97    /// evenly-spaced rows; nothing else has a call to make.
98    pub fn of(shape: &'a Shape, strides: &'a Strides, elem_size: usize) -> Result<Self, IoError> {
99        if !has_pitched_row_major_strides(shape, strides) {
100            return Err(IoError::UnsupportedStrides {
101                backtrace: BackTrace::capture(),
102            });
103        }
104        Ok(Self {
105            shape,
106            strides,
107            elem_size,
108            pitch: Pitch::of(shape, strides, elem_size),
109        })
110    }
111}
112
113/// The device calls a [`Command`](super::Command) cannot make itself.
114///
115/// Four, because everything else a command does — deciding what to stage, when
116/// to reclaim, whether a layout needs a 2D copy, when the drop queue may be
117/// flushed — is the same whichever driver is underneath.
118pub trait Driver: Sized {
119    /// The multi-stream backend whose streams this driver drives.
120    type Backend: EventStreamBackend<Stream = Self::Stream>;
121    /// The backend's stream.
122    type Stream: DeviceStream;
123    /// Whatever the backend needs to launch a compiled kernel — its loaded
124    /// modules, its profiling clocks.
125    type Context;
126    /// What this backend hands a kernel at launch.
127    ///
128    /// Not simply the buffers: CUDA passes tensor-map descriptors alongside
129    /// them and HIP has none, so what a launch is given is the backend's to
130    /// say. The command only carries it from the server to [`launch`].
131    ///
132    /// [`launch`]: Self::launch
133    type LaunchArgs: ?Sized;
134
135    /// Hand out `size` bytes of the pinned host allocation `binding` names,
136    /// released back to the pool when the [`Bytes`] drop.
137    ///
138    /// # Safety
139    ///
140    /// `binding` names initialized host memory of at least `size` bytes, and
141    /// `resource` resolves it.
142    unsafe fn pinned_bytes(
143        binding: ManagedMemoryBinding,
144        resource: HostResource<Self>,
145        size: usize,
146    ) -> Bytes;
147
148    /// Enqueue a copy from device memory into `bytes` on `stream`.
149    ///
150    /// # Safety
151    ///
152    /// `resource` is a live device allocation of at least `bytes.len()`
153    /// readable bytes, `bytes` has room for the copy, and the caller
154    /// synchronizes `stream` before reading it back.
155    ///
156    /// # Errors
157    ///
158    /// The driver's refusal to copy.
159    unsafe fn copy_to_host(
160        resource: &DeviceResource<Self>,
161        layout: &CopyLayout<'_>,
162        bytes: &mut Bytes,
163        stream: &Self::Stream,
164    ) -> Result<(), IoError>;
165
166    /// Enqueue a copy of `data` into device memory on `stream`.
167    ///
168    /// # Safety
169    ///
170    /// `resource` is a live device allocation big enough for `data`, and
171    /// `data` stays alive until `stream` is synchronized — which is what the
172    /// caller's drop queue guarantees.
173    ///
174    /// # Errors
175    ///
176    /// The driver's refusal to copy.
177    unsafe fn copy_to_device(
178        resource: &DeviceResource<Self>,
179        layout: &CopyLayout<'_>,
180        data: &[u8],
181        stream: &Self::Stream,
182    ) -> Result<(), IoError>;
183
184    /// Enqueue an already-compiled kernel on `stream`.
185    ///
186    /// Always a compiled kernel: the server compiles before entering its write
187    /// scope, and a skipped launch stops there, before any resource is
188    /// resolved.
189    ///
190    /// # Errors
191    ///
192    /// The driver's refusal to enqueue the launch.
193    fn launch(
194        ctx: &mut Self::Context,
195        stream: &mut Self::Stream,
196        kernel: KernelId,
197        count: (u32, u32, u32),
198        args: &mut Self::LaunchArgs,
199    ) -> Result<(), LaunchError>;
200}
201
202/// The device allocation a driver's buffers resolve to.
203pub type DeviceResource<D> =
204    <<<D as Driver>::Stream as DeviceStream>::DeviceStorage as ComputeStorage>::Resource;
205
206/// The host allocation a driver's staging buffers resolve to.
207pub type HostResource<D> =
208    <<<D as Driver>::Stream as DeviceStream>::HostStorage as ComputeStorage>::Resource;