Skip to main content

cubecl_server/command/
base.rs

1//! One unit of work against the device.
2//!
3//! Every operation a backend server exposes that touches memory or launches a
4//! kernel goes through a [`Command`]: it pairs the context holding the
5//! compiled kernels with the streams the operation was resolved against, and
6//! resolving is what orders the current stream behind whichever streams own
7//! the buffers it was handed.
8//!
9//! Everything here is the same whichever driver is underneath — the allocation
10//! and reclaim policy, when the drop queue may be flushed, what a copy stages.
11//! The calls that are not are [`Driver`](super::Driver)'s.
12
13use super::stream_copies::StreamCopies;
14use super::{CopyLayout, DeviceResource, DeviceStream, Driver, Staging};
15use crate::id::KernelId;
16use crate::memory_management::drop_queue::Fence;
17use crate::memory_management::relocation::{RelocatingStreams, RelocationNeed, RelocationReason};
18use crate::memory_management::{Cleanup, release_or_leak};
19use crate::memory_management::{
20    ManagedMemoryHandle, MemoryAllocationMode, MemoryHandle, PageGuard, PageUpdate,
21    StreamMemoryReport,
22};
23use crate::server::{BufferBinding, CopyDescriptor, Handle, IoError, LaunchError, ServerError};
24use crate::storage::ManagedResource;
25use crate::stream::ResolvedStreams;
26use crate::stream::TouchedPage;
27use alloc::boxed::Box;
28use alloc::vec;
29use alloc::vec::Vec;
30use cubecl_common::{bytes::Bytes, device::ServiceId};
31use cubecl_environment::backtrace::BackTrace;
32use cubecl_environment::future::DynFut;
33use cubecl_environment::stream::StreamId;
34
35/// One unit of work against the device: the context that holds its compiled
36/// kernels, and the streams it was resolved against.
37///
38/// Built per operation rather than held, because resolving is what orders the
39/// current stream behind whichever streams own the buffers it was given.
40pub struct Command<'a, D: Driver> {
41    ctx: &'a mut D::Context,
42    streams: ResolvedStreams<'a, D::Backend>,
43    /// The service issuing the command: what the handles it allocates are
44    /// stamped with.
45    service: ServiceId,
46}
47
48impl<'a, D: Driver> Command<'a, D> {
49    /// A command against `ctx` over the streams `streams` resolved.
50    pub fn new(
51        ctx: &'a mut D::Context,
52        streams: ResolvedStreams<'a, D::Backend>,
53        service: ServiceId,
54    ) -> Self {
55        Self {
56            ctx,
57            streams,
58            service,
59        }
60    }
61
62    /// The stream this command is issued on.
63    ///
64    /// The one part of the resolution a backend reaches for directly: the
65    /// driver calls take a stream, and this is the one they take.
66    pub fn stream(&mut self) -> &mut D::Stream {
67        self.streams.current()
68    }
69
70    /// The device allocation `binding` names, resolved on the stream that
71    /// created it rather than the current one.
72    ///
73    /// While the current stream records a graph, the page the allocation
74    /// sits on is remembered: the graph replays against the address resolved
75    /// here, so it guards that page once it seals.
76    ///
77    /// # Errors
78    ///
79    /// [`IoError::StorageHandleNotFound`] when the binding names no live allocation.
80    pub fn resource(&mut self, binding: BufferBinding) -> Result<DeviceResource<D>, IoError> {
81        self.streams
82            .current()
83            .capturing()
84            .touch(binding.stream, binding.memory.descriptor().location());
85        self.streams
86            .get(&binding.stream)
87            .device_memory()
88            .get_resource(binding.memory, binding.offset_start, binding.offset_end)
89    }
90
91    /// The device allocation `binding` names, for a caller outside the server
92    /// that keeps its raw address: it holds the allocation and a guard on its
93    /// page for as long as it lives.
94    ///
95    /// # Errors
96    ///
97    /// [`IoError::StorageHandleNotFound`] when the binding names no live allocation.
98    pub fn managed_resource(
99        &mut self,
100        binding: BufferBinding,
101    ) -> Result<ManagedResource<DeviceResource<D>>, IoError> {
102        self.streams
103            .get(&binding.stream)
104            .device_memory()
105            .managed_resource(binding.memory, binding.offset_start, binding.offset_end)
106    }
107
108    /// A guard on every page in `touched`, each looked up in the memory of the
109    /// stream that owns it.
110    pub fn guard_pages(&mut self, touched: &[TouchedPage]) -> Vec<PageGuard> {
111        touched
112            .iter()
113            .filter_map(|page| {
114                self.streams
115                    .get(&page.stream)
116                    .device_memory()
117                    .guard(page.location)
118            })
119            .collect()
120    }
121
122    /// Everything the current stream's device memory holds, pool by pool.
123    pub fn memory_report(&mut self) -> StreamMemoryReport {
124        StreamMemoryReport {
125            stream: self.streams.current,
126            pools: self.streams.current().device_memory().memory_report(),
127            auxiliary: Vec::new(),
128        }
129    }
130
131    /// Release everything the current stream is holding that nothing still
132    /// needs.
133    ///
134    /// # Errors
135    ///
136    /// Refused while any stream records a graph: releasing memory waits on
137    /// the device, and a wait on a stream that records aborts its capture.
138    pub fn memory_cleanup(&mut self) -> Result<(), ServerError> {
139        self.refuse_while_recording()?;
140        let stream = self.streams.current();
141        // Deferred frees sit in the drop queue until a fenced flush, so an
142        // explicit cleanup must drain it first or the pools still see those
143        // slices as live.
144        let signal = stream.signal();
145        stream.drop_queue().drain(|| D::Stream::fence(signal));
146        // The info cache's buffers are live slices in the dynamic pools; an
147        // explicit cleanup exists to leave those pools empty, so every entry
148        // not pinned by a live graph goes too.
149        stream.info_cache().clear_unpinned();
150
151        RelocatingStreams::reclaim(self)?;
152        let (stream, failures) = self.streams.current_and_failures();
153        stream.host_memory().cleanup(Cleanup::Explicit, failures);
154        Ok(())
155    }
156
157    /// Flush the current stream's drop queue, freeing what the device is
158    /// known to be done with.
159    ///
160    /// Deferred while the stream records a graph — the flush records a fence
161    /// on the capturing stream, which corrupts the recording — and the window
162    /// drains the queue itself when it closes. The rule lives here, on the one
163    /// path a server has to the queue, so no call site can rebuild the flush
164    /// without the guard.
165    pub fn flush_drops(&mut self) {
166        let stream = self.streams.current();
167        if stream.capturing().is_recording() {
168            return;
169        }
170        let signal = stream.signal();
171        stream.drop_queue().flush(|| D::Stream::fence(signal));
172    }
173
174    /// Set the [`MemoryAllocationMode`] for the current stream.
175    pub fn allocation_mode(&mut self, mode: MemoryAllocationMode) {
176        self.streams.current().device_memory().mode(mode)
177    }
178
179    /// Allocate `size` bytes of device memory on the current stream.
180    ///
181    /// # Errors
182    ///
183    /// [`IoError::BufferTooBig`] when no device could ever fit it, and
184    /// whatever the allocator reports when a reclaim-and-retry still cannot.
185    pub fn reserve(&mut self, size: u64) -> Result<ManagedMemoryHandle, IoError> {
186        let update = self.streams.current().capturing().page_update();
187        if update == PageUpdate::Allow {
188            self.relocate_when_wanted();
189        }
190
191        let (stream, failures) = self.streams.current_and_failures();
192        match stream.device_memory().reserve(size, update, failures) {
193            Ok(handle) => Ok(handle),
194            // The recording now misses whatever this memory was for.
195            Err(err @ IoError::PageUpdateForbidden { .. }) => {
196                stream.capturing().fail(err.clone().into());
197                Err(err)
198            }
199            Err(err) if update != PageUpdate::Allow || !err.may_succeed_after_reclaim() => Err(err),
200            // Reclaim and retry once; only a failure after that is reported.
201            // Without the retry a transient peak becomes a never-initialized
202            // handle whose every downstream use fails.
203            Err(err) => {
204                log::warn!("device allocation of {size} B failed ({err}); reclaiming and retrying");
205                self.make_room(&err);
206                let (stream, failures) = self.streams.current_and_failures();
207                stream
208                    .device_memory()
209                    .reserve(size, PageUpdate::Allow, failures)
210            }
211        }
212    }
213
214    /// Get back what the reservation that failed with `err` needs: a slot for
215    /// a new page size when the arena is full, memory otherwise.
216    fn make_room(&mut self, err: &IoError) {
217        match err {
218            // Only a relocation that may allocate empties the outdated pools
219            // whatever room the current pages have.
220            IoError::PageSizesExhausted { .. } => {
221                RelocatingStreams::relocate(self, RelocationReason::ArenaFull)
222            }
223            // No stream records (the caller checked), which is the only
224            // refusal a cleanup has.
225            _ => {
226                let _ = self.memory_cleanup();
227            }
228        }
229    }
230
231    /// Give `memory` `size` bytes of device memory on the current stream.
232    ///
233    /// # Errors
234    ///
235    /// Returns an [`IoError`] when the memory cannot be reserved or bound, e.g. when the device
236    /// is out of memory. The caller records it on `memory`, which has no binding to carry it.
237    pub fn initialize_memory(
238        &mut self,
239        memory: ManagedMemoryHandle,
240        size: u64,
241    ) -> Result<(), IoError> {
242        let reserved = match self.reserve(size) {
243            Ok(reserved) => reserved,
244            // The recording already failed on it, and `stop_capture` reports
245            // that: the handle stays unbound, and whatever uses it belongs to
246            // a recording that will not seal.
247            Err(IoError::PageUpdateForbidden { .. }) => return Ok(()),
248            Err(err) => return Err(err),
249        };
250        self.bind(reserved, memory)
251    }
252
253    /// The current stream's cursor.
254    pub fn cursor(&self) -> u64 {
255        self.streams.cursor
256    }
257
258    /// Allocate `size` bytes of device memory and a handle naming it.
259    ///
260    /// # Errors
261    ///
262    /// Whatever the allocation or the bind reports.
263    pub fn empty(&mut self, size: u64) -> Result<Handle, IoError> {
264        let handle = Handle::new(self.service, self.streams.current, size);
265        let reserved = self.reserve(size)?;
266        self.bind(reserved, handle.memory.clone())?;
267
268        Ok(handle)
269    }
270
271    /// Give `reserved`'s storage to `new`, so handles issued against `new`
272    /// resolve to it.
273    ///
274    /// # Errors
275    ///
276    /// [`IoError`] when the reservation has no initialized storage to give.
277    pub fn bind(
278        &mut self,
279        reserved: ManagedMemoryHandle,
280        new: ManagedMemoryHandle,
281    ) -> Result<(), IoError> {
282        let cursor = self.cursor();
283        let (stream, failures) = self.streams.current_and_failures();
284        stream.device_memory().bind(reserved, new, cursor, failures)
285    }
286
287    /// `size` bytes of host memory, pinned when the pool can serve it.
288    ///
289    /// Pinned pages transfer by DMA without a bounce, but they are scarce, so
290    /// an exhausted pool falls back to the heap rather than failing: this
291    /// always answers with a buffer of the size asked for.
292    pub fn reserve_cpu(&mut self, size: usize, origin: Option<StreamId>) -> Bytes {
293        self.reserve_pinned(size, origin)
294            .unwrap_or_else(|| Bytes::from_bytes_vec(vec![0; size]))
295    }
296
297    /// `size` bytes of pinned host memory, or `None` when the pool cannot
298    /// serve it.
299    fn reserve_pinned(&mut self, size: usize, origin: Option<StreamId>) -> Option<Bytes> {
300        let (stream, failures) = match origin {
301            Some(id) => self.streams.get_and_failures(&id),
302            None => self.streams.current_and_failures(),
303        };
304        // A stream recording a graph stages from what the pool holds, and
305        // falls back to the heap rather than allocate.
306        let update = stream.capturing().page_update();
307        let handle = stream
308            .host_memory()
309            .reserve(size as u64, update, failures)
310            .ok()?;
311
312        let binding = MemoryHandle::binding(handle);
313        stream.capturing().prime(&binding);
314        let resource = stream
315            .host_memory()
316            .get_resource(binding.clone(), None, None)
317            .ok()?;
318
319        // SAFETY: the binding has initialized memory for at least `size` bytes,
320        // and `resource` is what the manager just resolved it to.
321        Some(unsafe { D::pinned_bytes(binding, resource, size) })
322    }
323
324    /// Copy each descriptor's device memory back to the host, resolving once
325    /// the copies have landed.
326    ///
327    /// The copies are enqueued before the future is returned; awaiting it
328    /// waits on the fence that follows them.
329    ///
330    /// # Errors
331    ///
332    /// [`IoError::UnsupportedStrides`] for a layout the driver cannot copy,
333    /// and whatever the fence reports when the stream itself failed.
334    pub fn read_async(
335        &mut self,
336        descriptors: Vec<CopyDescriptor>,
337    ) -> impl Future<Output = Result<Vec<Bytes>, ServerError>> + Send + use<D> {
338        let held = descriptors
339            .iter()
340            .map(|descriptor| descriptor.handle.clone())
341            .collect::<Vec<_>>();
342        let result = self.copies_to_bytes(descriptors);
343        let fence = D::Stream::fence(self.streams.current().signal());
344
345        async move {
346            if let Err(err) = fence.wait() {
347                // Both the device sources and host destinations may still be in use.
348                release_or_leak((held, result), &err);
349                return Err(err);
350            }
351            // The bindings kept the source allocations alive across the copies;
352            // the fence above is what says they are done being read.
353            core::mem::drop(held);
354
355            result.map_err(Into::into)
356        }
357    }
358
359    /// Copy each descriptor's device memory into a fresh host buffer.
360    fn copies_to_bytes(&mut self, descriptors: Vec<CopyDescriptor>) -> Result<Vec<Bytes>, IoError> {
361        let mut result = Vec::with_capacity(descriptors.len());
362
363        for descriptor in descriptors {
364            match self.copy_to_bytes(descriptor, None) {
365                Ok(bytes) => result.push(bytes),
366                Err(err) => {
367                    // The buffers collected so far are the destinations of
368                    // copies already enqueued: dropping them hands their
369                    // pinned slices back to a pool whose reuse is gated on
370                    // the refcount alone, while the device is still writing
371                    // them. The fence `read_async` records to cover exactly
372                    // this does not exist yet on the error path, so record
373                    // one here and wait it out before the partial set drops.
374                    if !result.is_empty()
375                        && !D::Stream::fence(self.streams.current().signal()).sync()
376                    {
377                        core::mem::forget(result);
378                    }
379                    return Err(err);
380                }
381            }
382        }
383
384        Ok(result)
385    }
386
387    /// Copy one descriptor's device memory into a fresh host buffer.
388    fn copy_to_bytes(
389        &mut self,
390        descriptor: CopyDescriptor,
391        stream_id: Option<StreamId>,
392    ) -> Result<Bytes, IoError> {
393        let num_bytes = descriptor.shape.iter().product::<usize>() * descriptor.elem_size;
394        let mut bytes = self.reserve_cpu(num_bytes, stream_id);
395        self.write_to_cpu(descriptor, &mut bytes, stream_id)?;
396
397        Ok(bytes)
398    }
399
400    /// Enqueue a copy of `descriptor`'s device memory into `bytes`.
401    ///
402    /// # Errors
403    ///
404    /// [`IoError::UnsupportedStrides`] for a layout that is not pitched
405    /// row-major, [`IoError::StorageHandleNotFound`] for a binding that names no live
406    /// allocation, and the driver's refusal to copy.
407    pub fn write_to_cpu(
408        &mut self,
409        descriptor: CopyDescriptor,
410        bytes: &mut Bytes,
411        stream_id: Option<StreamId>,
412    ) -> Result<(), IoError> {
413        let CopyDescriptor {
414            handle: binding,
415            shape,
416            strides,
417            elem_size,
418        } = descriptor;
419        // Nothing to copy for an empty tensor, and `bytes` has no real backing
420        // for the driver to write into — a dangling zero-size buffer.
421        // Its strides may contain zeros, so skip copy-layout validation too.
422        if bytes.is_empty() {
423            return Ok(());
424        }
425
426        let layout = CopyLayout::of(&shape, &strides, elem_size)?;
427        let resource = self.resource(binding)?;
428        let stream = match stream_id {
429            Some(id) => self.streams.get(&id),
430            None => self.streams.current(),
431        };
432
433        // SAFETY: `resource` is a live device allocation the manager just
434        // resolved, `bytes` was sized for this copy, and the caller awaits the
435        // fence `read_async` records before reading it back.
436        unsafe { D::copy_to_host(&resource, &layout, bytes, stream) }
437    }
438
439    /// Enqueue a copy of `data` into the device memory `descriptor` names.
440    ///
441    /// # Errors
442    ///
443    /// [`IoError::UnsupportedStrides`] for a layout that is not pitched
444    /// row-major, [`IoError::StorageHandleNotFound`] for a binding that names no live
445    /// allocation, and the driver's refusal to copy.
446    pub fn write_to_gpu(&mut self, descriptor: CopyDescriptor, data: Bytes) -> Result<(), IoError> {
447        let CopyDescriptor {
448            handle: binding,
449            shape,
450            strides,
451            elem_size,
452        } = descriptor;
453        let size = data.len();
454
455        // An empty tensor (a zero dim in its shape) has nothing to copy. Bail
456        // before validating its potentially zero strides or staging: the zero-size
457        // staging buffer has no real backing (a dangling pointer), and a 2D copy
458        // would still transfer `width_bytes` from it when only the leading dims are zero.
459        if size == 0 {
460            return Ok(());
461        }
462
463        let layout = CopyLayout::of(&shape, &strides, elem_size)?;
464        let resource = self.resource(binding)?;
465        let staging = Staging::of(size, data.property());
466
467        let data = match staging.through_pinned {
468            true => {
469                // Pinned staging is a DMA optimization, not a requirement, so
470                // an exhausted pinned pool falls back to a plain heap buffer
471                // rather than failing the write — the same answer `reserve_cpu`
472                // gives for the same condition. File-backed data still lands in
473                // real memory before the driver reads it asynchronously, which
474                // is the half of the staging that is mandatory.
475                let mut buffer = self
476                    .reserve_pinned(size, None)
477                    .unwrap_or_else(|| Bytes::from_bytes_vec(vec![0; size]));
478                data.copy_into(&mut buffer);
479                buffer
480            }
481            false => data,
482        };
483
484        let current = self.streams.current();
485
486        // SAFETY: `resource` is a live device allocation, `data` is a valid
487        // host buffer, and either the drop queue or the capture window below
488        // keeps it alive for as long as the device reads it.
489        unsafe { D::copy_to_device(&resource, &layout, &data, current)? };
490
491        if current.capturing().is_recording() {
492            // A copy recorded into a graph is not executed now but re-read on
493            // every replay: the node keeps the raw host pointer, so the bytes
494            // ride the window onto the graph rather than the drop queue —
495            // which frees them when the window closes, exactly when the graph
496            // starts needing them.
497            current.capturing().retain_host(data);
498        } else {
499            current.drop_queue().push(data);
500            if staging.flush_after || current.drop_queue().should_flush() {
501                let signal = current.signal();
502                current.drop_queue().flush(|| D::Stream::fence(signal));
503            }
504        }
505
506        Ok(())
507    }
508
509    /// Allocate device memory for `data` and enqueue the copy into it.
510    ///
511    /// # Errors
512    ///
513    /// Whatever the allocation or the copy reports.
514    pub fn create_with_data(&mut self, data: &[u8]) -> Result<Handle, IoError> {
515        let mut staging =
516            self.reserve_pinned(data.len(), None)
517                .ok_or_else(|| IoError::Unknown {
518                    backtrace: BackTrace::capture(),
519                    description: "Unable to reserve pinned memory".into(),
520                })?;
521
522        staging.copy_from_slice(data);
523
524        let handle = self.empty(staging.len() as u64)?;
525
526        self.write_to_gpu(
527            CopyDescriptor {
528                handle: handle.clone().binding(),
529                shape: [data.len()].into(),
530                strides: [1].into(),
531                elem_size: 1,
532            },
533            staging,
534        )?;
535
536        Ok(handle)
537    }
538
539    /// Wait for everything already enqueued on the current stream to finish.
540    ///
541    /// # Errors
542    ///
543    /// The fault the barrier reveals, when the stream itself failed.
544    pub fn sync(&mut self) -> DynFut<Result<(), ServerError>> {
545        let fence = D::Stream::fence(self.streams.current().signal());
546
547        Box::pin(async move { fence.wait() })
548    }
549
550    /// Enqueue an already-compiled kernel on the current stream.
551    ///
552    /// # Errors
553    ///
554    /// The driver's refusal to enqueue the launch, returned whether or not a
555    /// profile is open. An open profile is not a reason to hold the failure
556    /// here: the caller's write scope is what claims the buffers the launch
557    /// never wrote, and the caller invalidates every open profile on the same
558    /// path, so keeping it would lose the claim and duplicate the report.
559    pub fn kernel(
560        &mut self,
561        kernel: KernelId,
562        count: (u32, u32, u32),
563        args: &mut D::LaunchArgs,
564    ) -> Result<(), LaunchError> {
565        let stream = self.streams.current();
566        let result = D::launch(self.ctx, stream, kernel, count, args);
567
568        // A fenced flush during capture would abort it; defer until the capture
569        // ends, when the deferred staging buffers are reclaimed.
570        if !stream.capturing().is_recording() && stream.drop_queue().should_flush() {
571            let signal = stream.signal();
572            stream.drop_queue().flush(|| D::Stream::fence(signal));
573        }
574
575        result
576    }
577}
578
579impl<D: Driver> RelocatingStreams for Command<'_, D> {
580    fn recording(&mut self) -> bool {
581        self.streams.current().capturing().device().any_recording()
582    }
583
584    fn has_outdated(&mut self) -> bool {
585        self.streams.current().device_memory().has_outdated()
586    }
587
588    fn relocation_need(&mut self) -> RelocationNeed {
589        self.streams.current().device_memory().relocation_need()
590    }
591
592    fn bytes_allocated(&mut self) -> u64 {
593        self.streams
594            .all()
595            .map(|stream| stream.device_memory().bytes_allocated())
596            .sum()
597    }
598
599    /// Nothing here: `StreamCopies` waits on every stream before its first
600    /// copy, so a relocation with nothing to move waits on none.
601    fn finish(&mut self) {}
602
603    fn cleanup_memory(&mut self) {
604        let (stream, failures) = self.streams.current_and_failures();
605        stream.device_memory().cleanup(Cleanup::Explicit, failures);
606    }
607
608    fn relocate_memory(&mut self, reason: RelocationReason) {
609        let current = self.streams.current;
610        self.relocate_stream(current, reason);
611    }
612
613    fn device_has_outdated(&mut self) -> bool {
614        self.streams
615            .all()
616            .any(|stream| stream.device_memory().has_outdated())
617    }
618
619    fn relocate_device_memory(&mut self, reason: RelocationReason) {
620        for stream_id in self.streams.stream_ids() {
621            if self.streams.get(&stream_id).device_memory().has_outdated() {
622                self.relocate_stream(stream_id, reason);
623            }
624        }
625    }
626}
627
628impl<D: Driver> Command<'_, D> {
629    /// Move what `stream_id`'s memory holds on outdated pages, with the copies
630    /// queued on that stream.
631    fn relocate_stream(&mut self, stream_id: StreamId, reason: RelocationReason) {
632        // Rare enough to gather the signals as it goes: it waits on the whole
633        // device anyway.
634        let signals: Vec<_> = self.streams.all().map(|stream| stream.signal()).collect();
635        let queue = self.streams.get(&stream_id).signal();
636        let mut copier = StreamCopies::<D>::new(signals, queue, self.ctx);
637        let (stream, failures) = self.streams.get_and_failures(&stream_id);
638        stream
639            .device_memory()
640            .relocate(&mut copier, reason, failures);
641    }
642}