cubecl_server/command/staging.rs
1//! What a host-to-device copy should do about its source buffer.
2//!
3//! The decision is plain data with a constructor, which is the point: it is
4//! the only part of a copy that can be checked without a device, and inline in
5//! the copy it never was.
6
7use cubecl_common::bytes::AllocationProperty;
8
9/// A megabyte, for the thresholds below.
10const MB: usize = 1024 * 1024;
11
12/// Transfers up to this size go through a pinned staging buffer, which the
13/// driver can DMA from without a bounce. Above it the copy is long enough that
14/// the bounce costs less than pinning would.
15const STAGE_MAX: usize = 100 * MB;
16
17/// Above this size the drop queue is flushed after the copy, so the source is
18/// released promptly rather than waiting for the next batch to fill.
19const FLUSH_MIN: usize = 10 * MB;
20
21/// What a host-to-device copy does about its source.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Staging {
24 /// Copy the source into pinned host memory before handing it to the
25 /// driver.
26 pub through_pinned: bool,
27 /// Flush the drop queue once the copy is enqueued, rather than leaving the
28 /// source held until the next batch fills.
29 pub flush_after: bool,
30}
31
32impl Staging {
33 /// What a copy of `size` bytes of memory allocated as `property` should do.
34 pub fn of(size: usize, property: AllocationProperty) -> Self {
35 let file_backed = matches!(property, AllocationProperty::File);
36 Self {
37 // File-backed data is staged whatever its size: the driver reads
38 // the source asynchronously, and it has to be real memory by then.
39 // Otherwise stage only what is small enough to be worth pinning,
40 // and never what is pinned already — that would be a redundant
41 // pinned-to-pinned copy.
42 through_pinned: file_backed
43 || (size < STAGE_MAX && !matches!(property, AllocationProperty::Pinned)),
44 // A large source, or one mapped from a file, is worth releasing
45 // now rather than holding until the batch fills.
46 flush_after: file_backed || size > FLUSH_MIN,
47 }
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 /// File-backed data is staged whatever its size.
56 ///
57 /// The driver reads the source after the copy is enqueued, so a mapping
58 /// that may still fault has to become real memory first. Size is why
59 /// staging is *worth* it for everything else; for a file it is why the
60 /// copy is correct.
61 #[test]
62 fn file_backed_data_is_always_staged() {
63 for size in [1, STAGE_MAX, STAGE_MAX * 4] {
64 assert!(Staging::of(size, AllocationProperty::File).through_pinned);
65 }
66 }
67
68 /// Already-pinned memory is handed to the driver as it is.
69 ///
70 /// Staging it would copy pinned memory into pinned memory, paying for a
71 /// bounce that exists to avoid one.
72 #[test]
73 fn pinned_data_is_never_restaged() {
74 for size in [1, STAGE_MAX / 2, STAGE_MAX * 4] {
75 assert!(!Staging::of(size, AllocationProperty::Pinned).through_pinned);
76 }
77 }
78
79 /// Ordinary host memory is staged only while it is small enough that
80 /// pinning costs less than the bounce it saves.
81 #[test]
82 fn plain_data_is_staged_up_to_the_threshold() {
83 assert!(Staging::of(STAGE_MAX - 1, AllocationProperty::Native).through_pinned);
84 assert!(!Staging::of(STAGE_MAX, AllocationProperty::Native).through_pinned);
85 }
86
87 /// A copy big enough to be worth releasing promptly flushes the queue,
88 /// whether or not it was staged.
89 #[test]
90 fn a_large_source_is_released_without_waiting_for_the_batch() {
91 assert!(!Staging::of(FLUSH_MIN, AllocationProperty::Native).flush_after);
92 assert!(Staging::of(FLUSH_MIN + 1, AllocationProperty::Native).flush_after);
93 assert!(Staging::of(1, AllocationProperty::File).flush_after);
94 }
95}