Skip to main content

cubecl_server/compiler/
record.rs

1//! What the environment records of a kernel's compilation.
2
3use crate::id::KernelId;
4use cubecl_environment::records::{Record, RecordEffect, RecordLevel, Span};
5
6use super::{KernelCacheKey, build_id_hash};
7
8/// One kernel's trip through a backend's compilation path, as the environment
9/// records it: compiled fresh, loaded from the compilation store, or rekeyed
10/// from an artifact the store held for the same source.
11///
12/// A kernel the [`KernelLoader`](super::KernelLoader) already holds is not a
13/// trip and is not recorded:
14/// nothing here runs per launch. Neither is a trip that fails: the launch
15/// error carries that account, and the environment holds nothing of it.
16#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub struct CompilationRecord {
18    /// The kernel's type.
19    pub kernel: alloc::string::String,
20    /// The store entry naming the artifact: what tells two instances of one
21    /// kernel type apart.
22    pub key: KernelCacheKey,
23    /// The kernel as cubecl defined it, before the backend's compiler — the
24    /// IR's textual form, for a reader to render — at [`RecordLevel::Full`].
25    /// Only a trip that misses the store defines the kernel, so a
26    /// [`Loaded`](CompilationOutcome::Loaded) one carries none.
27    pub ir: Option<alloc::string::String>,
28    /// How the artifact was obtained.
29    pub outcome: CompilationOutcome,
30    /// The work obtaining it took: its own lowering, finalizing and loading,
31    /// which is what it would have cost alone. A kernel compiled beside others
32    /// counts none of the time it waited for them, so the work of a batch adds
33    /// up to more than the time it took when it ran on several threads: that
34    /// time is its [`CompilationBatchRecord::wall`].
35    ///
36    /// Also read from `duration`, the name older records carry: each of those
37    /// kernels compiled alone, so the time it took is its work.
38    #[serde(alias = "duration")]
39    pub work: core::time::Duration,
40    /// The source the backend compiled, at [`RecordLevel::Full`].
41    pub source: Option<alloc::string::String>,
42}
43
44/// How a backend obtained a kernel's artifact.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46pub enum CompilationOutcome {
47    /// Compiled from its definition: expanded, compiled by the backend's
48    /// compiler, loaded on the device.
49    Compiled,
50    /// Read from the compilation store and loaded on the device.
51    Loaded,
52    /// Expanded to a source the store already held an artifact for, under
53    /// another key: the artifact was kept under this one too — moved, when
54    /// the other key is of an earlier build — and loaded, and the backend's
55    /// compiler never ran.
56    Rekeyed,
57}
58
59impl Record for CompilationRecord {
60    const KIND: &'static str = "compilation";
61}
62
63/// One batch of kernels a server obtained together — compiled, or read from
64/// the compilation store, and loaded — as the environment records it. A
65/// kernel a launch compiles alone is a batch of one.
66///
67/// Each kernel the batch obtained has its [`CompilationRecord`], stamped when
68/// the batch started, as this one is. What the batch cost the process is its
69/// [`wall`](Self::wall): adding up its kernels' [`work`](CompilationRecord::work)
70/// counts every thread's time as if they ran one after another.
71#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
72pub struct CompilationBatchRecord {
73    /// How many kernels the batch was asked for, failures included.
74    pub kernels: u32,
75    /// How many threads compiled at once: one when at most one kernel had to
76    /// be compiled, the rest read from the store.
77    pub threads: u32,
78    /// The time from the batch's start to its last kernel loaded.
79    pub wall: core::time::Duration,
80}
81
82impl Record for CompilationBatchRecord {
83    const KIND: &'static str = "compilation_batch";
84}
85
86/// How a batch went, for its [`CompilationBatchRecording`] to record.
87#[derive(Debug)]
88pub(crate) struct BatchOutcome {
89    /// How many kernels it was asked for, failures included.
90    pub(crate) kernels: usize,
91    /// How many threads compiled at once.
92    pub(crate) threads: usize,
93    /// Whether the store took any of its kernels, as their records say.
94    pub(crate) stored: bool,
95}
96
97/// A batch being recorded: opened where a server starts obtaining kernels,
98/// closed when it has them. A no-op when the environment records nothing.
99#[derive(Debug)]
100pub(crate) struct CompilationBatchRecording {
101    span: Option<Span>,
102}
103
104impl CompilationBatchRecording {
105    pub(crate) fn new() -> Self {
106        Self { span: Span::new() }
107    }
108
109    /// The batch obtained what it was asked for.
110    pub(crate) fn close(self, batch: BatchOutcome) {
111        let Some(span) = self.span else {
112            return;
113        };
114        // A batch for a session that is gone is dropped, as its kernels are.
115        let Some(wall) = span.elapsed() else {
116            return;
117        };
118        let record = CompilationBatchRecord {
119            kernels: batch.kernels.try_into().unwrap_or(u32::MAX),
120            threads: batch.threads.try_into().unwrap_or(u32::MAX),
121            wall,
122        };
123        span.close(effect(batch.stored), &record);
124    }
125}
126
127/// A compilation being recorded: the [`KernelLoader`](super::KernelLoader)
128/// opens one for each kernel it does not hold, the backend tells it what the
129/// trip goes through as it lowers the kernel, and the loader closes it with how
130/// the artifact was obtained. Every call is a
131/// no-op when the environment records nothing, and one dropped unclosed, by a
132/// trip that failed, records nothing.
133#[derive(Debug)]
134pub struct CompilationRecording {
135    open: Option<OpenRecording>,
136}
137
138/// What a [`CompilationRecording`] holds while the environment records.
139#[derive(Debug)]
140struct OpenRecording {
141    span: Span,
142    /// The work done so far, as [`CompilationRecording::worked`] reports it.
143    work: core::time::Duration,
144    kernel: &'static str,
145    key: KernelCacheKey,
146    ir: Option<alloc::string::String>,
147    source: Option<alloc::string::String>,
148}
149
150impl CompilationRecording {
151    /// Start recording `kernel_id`'s trip.
152    pub fn new(kernel_id: &KernelId) -> Self {
153        let open = Span::new().map(|span| OpenRecording {
154            span,
155            work: core::time::Duration::ZERO,
156            kernel: kernel_id.type_name(),
157            key: KernelCacheKey::new(kernel_id, build_id_hash()),
158            ir: None,
159            source: None,
160        });
161        Self { open }
162    }
163
164    /// The kernel was defined: keep its IR, at [`RecordLevel::Full`] only.
165    /// The textual IR runs to hundreds of KB per kernel, where the compiled
166    /// artifact is tens.
167    pub fn defined(&mut self, definition: &crate::kernel::KernelDefinition) {
168        if let Some(open) = self.open.as_mut().filter(|_| keeps_code()) {
169            open.ir = Some(alloc::format!("{}", definition.body));
170        }
171    }
172
173    /// The backend's compiler produced `source`: keep it, at
174    /// [`RecordLevel::Full`] only.
175    pub fn source(&mut self, source: &str) {
176        if let Some(open) = self.open.as_mut().filter(|_| keeps_code()) {
177            open.source = Some(source.into());
178        }
179    }
180
181    /// The trip did `duration` more of its work: what the record's
182    /// [`work`](CompilationRecord::work) adds up.
183    pub fn worked(&mut self, duration: core::time::Duration) {
184        if let Some(open) = self.open.as_mut() {
185            open.work += duration;
186        }
187    }
188
189    /// The trip obtained its artifact by `outcome`. `stored` is whether the
190    /// store took it, as [`store_compiled`](super::store_compiled) answers:
191    /// a trip whose artifact the store did not take — read from it, or with
192    /// no store to take it — changed nothing.
193    pub fn close(self, outcome: CompilationOutcome, stored: bool) {
194        let Some(open) = self.open else {
195            return;
196        };
197        // The span still says whether the session the trip started in is
198        // the one recording: a record for a session that is gone is dropped.
199        if open.span.elapsed().is_none() {
200            return;
201        }
202        let record = CompilationRecord {
203            kernel: open.kernel.into(),
204            key: open.key,
205            ir: open.ir,
206            outcome,
207            work: open.work,
208            source: open.source,
209        };
210        open.span.close(effect(stored), &record);
211    }
212}
213
214/// Whether a record keeps the kernel's code, its IR and its source: code is
215/// the heaviest thing a record can carry.
216fn keeps_code() -> bool {
217    cubecl_environment::records::level() == RecordLevel::Full
218}
219
220/// An artifact the store took is the environment changing.
221fn effect(stored: bool) -> RecordEffect {
222    if stored {
223        RecordEffect::Changed
224    } else {
225        RecordEffect::Observed
226    }
227}