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}