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, ServerError};
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/// Only these, because everything else a command does — deciding what to
116/// stage, when to reclaim, whether a layout needs a 2D copy, when the drop
117/// queue may be 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 a copy of `source`'s bytes into `target` on `stream`, both
185    /// device memory of the same size — how a live allocation is relocated
186    /// off an outdated page.
187    ///
188    /// # Safety
189    ///
190    /// Both resources are live device allocations of the same size that do
191    /// not overlap, and nothing reads `target` or writes `source` until the
192    /// caller synchronizes `stream`.
193    ///
194    /// # Errors
195    ///
196    /// The driver's refusal to copy.
197    unsafe fn copy_on_device(
198        source: &DeviceResource<Self>,
199        target: &DeviceResource<Self>,
200        queue: <Self::Stream as DeviceStream>::Signal,
201    ) -> Result<(), IoError>;
202
203    /// Wait for device work enqueued outside the command's streams — CUDA's
204    /// collectives, on a stream of their own that the compute streams only
205    /// wait on at a collective sync — before a relocation moves the
206    /// allocations that work may still read or write.
207    ///
208    /// Nothing by default: a backend whose every device operation runs on its
209    /// command streams has nothing else to wait on.
210    ///
211    /// # Errors
212    ///
213    /// The driver's refusal to synchronize.
214    fn wait_outside_streams(_ctx: &mut Self::Context) -> Result<(), ServerError> {
215        Ok(())
216    }
217
218    /// Enqueue an already-compiled kernel on `stream`.
219    ///
220    /// Always a compiled kernel: the server compiles before entering its write
221    /// scope, and a skipped launch stops there, before any resource is
222    /// resolved.
223    ///
224    /// # Errors
225    ///
226    /// The driver's refusal to enqueue the launch.
227    fn launch(
228        ctx: &mut Self::Context,
229        stream: &mut Self::Stream,
230        kernel: KernelId,
231        count: (u32, u32, u32),
232        args: &mut Self::LaunchArgs,
233    ) -> Result<(), LaunchError>;
234}
235
236/// The device allocation a driver's buffers resolve to.
237pub type DeviceResource<D> =
238    <<<D as Driver>::Stream as DeviceStream>::DeviceStorage as ComputeStorage>::Resource;
239
240/// The host allocation a driver's staging buffers resolve to.
241pub type HostResource<D> =
242    <<<D as Driver>::Stream as DeviceStream>::HostStorage as ComputeStorage>::Resource;