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 /// A kernel loaded on the device, as the server's
135 /// [`KernelLoader`](crate::compiler::KernelLoader) hands it out: launching
136 /// it takes no lookup, so nothing that empties the loader in between —
137 /// an environment switch — can leave a launch without its kernel.
138 type Loaded;
139
140 /// Hand out `size` bytes of the pinned host allocation `binding` names,
141 /// released back to the pool when the [`Bytes`] drop.
142 ///
143 /// # Safety
144 ///
145 /// `binding` names initialized host memory of at least `size` bytes, and
146 /// `resource` resolves it.
147 unsafe fn pinned_bytes(
148 binding: ManagedMemoryBinding,
149 resource: HostResource<Self>,
150 size: usize,
151 ) -> Bytes;
152
153 /// Enqueue a copy from device memory into `bytes` on `stream`.
154 ///
155 /// # Safety
156 ///
157 /// `resource` is a live device allocation of at least `bytes.len()`
158 /// readable bytes, `bytes` has room for the copy, and the caller
159 /// synchronizes `stream` before reading it back.
160 ///
161 /// # Errors
162 ///
163 /// The driver's refusal to copy.
164 unsafe fn copy_to_host(
165 resource: &DeviceResource<Self>,
166 layout: &CopyLayout<'_>,
167 bytes: &mut Bytes,
168 stream: &Self::Stream,
169 ) -> Result<(), IoError>;
170
171 /// Enqueue a copy of `data` into device memory on `stream`.
172 ///
173 /// # Safety
174 ///
175 /// `resource` is a live device allocation big enough for `data`, and
176 /// `data` stays alive until `stream` is synchronized — which is what the
177 /// caller's drop queue guarantees.
178 ///
179 /// # Errors
180 ///
181 /// The driver's refusal to copy.
182 unsafe fn copy_to_device(
183 resource: &DeviceResource<Self>,
184 layout: &CopyLayout<'_>,
185 data: &[u8],
186 stream: &Self::Stream,
187 ) -> Result<(), IoError>;
188
189 /// Enqueue a copy of `source`'s bytes into `target` on `stream`, both
190 /// device memory of the same size — how a live allocation is relocated
191 /// off an outdated page.
192 ///
193 /// # Safety
194 ///
195 /// Both resources are live device allocations of the same size that do
196 /// not overlap, and nothing reads `target` or writes `source` until the
197 /// caller synchronizes `stream`.
198 ///
199 /// # Errors
200 ///
201 /// The driver's refusal to copy.
202 unsafe fn copy_on_device(
203 source: &DeviceResource<Self>,
204 target: &DeviceResource<Self>,
205 queue: <Self::Stream as DeviceStream>::Signal,
206 ) -> Result<(), IoError>;
207
208 /// Wait for device work enqueued outside the command's streams, such as
209 /// CUDA's collectives and transfers on streams of their own, before a
210 /// relocation moves the allocations that work may still read or write.
211 ///
212 /// Nothing by default: a backend whose every device operation runs on its
213 /// command streams has nothing else to wait on.
214 ///
215 /// # Errors
216 ///
217 /// The driver's refusal to synchronize.
218 fn wait_outside_streams(_ctx: &mut Self::Context) -> Result<(), ServerError> {
219 Ok(())
220 }
221
222 /// Enqueue `kernel`, already compiled and loaded under `id`, on `stream`.
223 ///
224 /// Always a compiled kernel: the server compiles before entering its write
225 /// scope, and a skipped launch stops there, before any resource is
226 /// resolved.
227 ///
228 /// # Errors
229 ///
230 /// The driver's refusal to enqueue the launch.
231 fn launch(
232 ctx: &mut Self::Context,
233 stream: &mut Self::Stream,
234 id: &KernelId,
235 kernel: &Self::Loaded,
236 count: (u32, u32, u32),
237 args: &mut Self::LaunchArgs,
238 ) -> Result<(), LaunchError>;
239}
240
241/// The device allocation a driver's buffers resolve to.
242pub type DeviceResource<D> =
243 <<<D as Driver>::Stream as DeviceStream>::DeviceStorage as ComputeStorage>::Resource;
244
245/// The host allocation a driver's staging buffers resolve to.
246pub type HostResource<D> =
247 <<<D as Driver>::Stream as DeviceStream>::HostStorage as ComputeStorage>::Resource;