cubecl_server/device_events/profiler.rs
1use alloc::boxed::Box;
2
3use cubecl_common::profile::{Duration, Instant, ProfileDuration, ProfileTicks};
4use cubecl_environment::backtrace::BackTrace;
5use cubecl_environment::collections::HashMap;
6use cubecl_environment::sync::Arc;
7
8use crate::device_events::{Event, EventApi, EventPool, Pooled};
9use crate::driver::DriverError;
10use crate::server::{ProfileError, ProfilingToken, ServerError};
11
12/// How long an [anchor](Anchor) is trusted before the next window takes a fresh
13/// one.
14///
15/// The device APIs answer an elapsed time in `f32` milliseconds, so the further
16/// an event sits from the anchor the coarser its placement on the host clock: a
17/// second out, the representable step is about 60 ns; an hour out, a quarter of
18/// a millisecond. A window's own duration is measured between its own two
19/// events and is unaffected — this only bounds the error on *where* the window
20/// lands, which is what a tracing profiler lines its spans up with.
21const ANCHOR_MAX_AGE: Duration = Duration::from_secs(1);
22
23/// What a window reports when its events could not be read back.
24///
25/// The read only fails when the device is already in trouble, and a resolved
26/// profile is a duration rather than a result — there is no error to hand back
27/// at that point. Reporting zero would give an autotune sweep a candidate that
28/// won by failing, so an unreadable window reports a time no real one reaches
29/// and says why in the log.
30const UNREADABLE: Duration = Duration::from_secs(3600);
31
32/// Device profiling on the GPU's own clock.
33///
34/// A profiling window is two events recorded into the stream where the window
35/// opened and closed. Nothing on the host waits for them: the device stamps
36/// each one as the queue reaches it, and the span between the two is read back
37/// later, by whoever awaits the measurement.
38///
39/// That deferral is what makes a measurement mean something when profiles nest.
40/// The [system-time profiler](crate::timestamp_profiler::TimestampProfiler) has
41/// to drain the stream at both ends of every window to have anything to time,
42/// so an inner window's two drains happen *inside* the outer one and are
43/// charged to it: the outer measurement grows with the number of inner ones,
44/// and every window reads back the launch latency the drain exposed rather than
45/// the work. Events are recorded in-queue and cost the host nothing, so an
46/// inner window measures its own kernels and the outer one measures the work
47/// rather than the profiling.
48///
49/// The state is the windows currently open, and the anchor that places them on
50/// the host clock.
51pub struct EventProfiler<A: EventApi> {
52 open: HashMap<ProfilingToken, Result<Open<A>, ProfileError>>,
53 counter: u64,
54 pool: EventPool<A>,
55 /// Created with the first window rather than with the profiler: a process
56 /// that never profiles should not own a stream and an event for it.
57 anchoring: Option<Anchoring<A>>,
58}
59
60/// A window that has opened and not yet closed.
61struct Open<A: EventApi> {
62 start: Pooled<A>,
63 /// The anchor this window opened under, kept so that its start and its end
64 /// are placed against the same one however many refreshes happen in
65 /// between.
66 anchor: Anchor<A>,
67}
68
69impl<A: EventApi> EventProfiler<A> {
70 /// Open a window at the current position of `stream`.
71 ///
72 /// # Errors
73 ///
74 /// [`ServerError`] when the device refuses an event or the stream will not
75 /// record one. No window is opened and no token is issued, so the caller
76 /// owes nothing; the events involved return to the pool.
77 pub fn start(&mut self, stream: A::Stream) -> Result<ProfilingToken, ServerError> {
78 let anchor = self.anchor()?;
79 let start = self.pool.acquire()?;
80 start.record(stream)?;
81
82 let token = ProfilingToken { id: self.counter };
83 self.counter += 1;
84 self.open.insert(token, Ok(Open { start, anchor }));
85
86 Ok(token)
87 }
88
89 /// Close the window `token` opened at the current position of `stream`.
90 ///
91 /// Returns at enqueue time. The duration is read from the device by the
92 /// returned future, which is where the wait for the window's work lives.
93 ///
94 /// That future blocks the thread that polls it: the device APIs offer no
95 /// way to be woken when an event is reached, only a synchronize that parks
96 /// the caller, so the first poll returns once the device has reached both
97 /// events. Await it from a thread that can afford to wait — the same place
98 /// a synchronize would have gone.
99 ///
100 /// # Errors
101 ///
102 /// [`ProfileError::NotRegistered`] for a token this profiler never issued
103 /// or has already closed, the error registered against the window by
104 /// [`failure`](Self::failure) when device work failed inside it, and
105 /// [`ProfileError::Server`] when the closing event cannot be recorded.
106 /// There is nothing to measure in any of those cases and the window is
107 /// gone either way.
108 pub fn stop(
109 &mut self,
110 stream: A::Stream,
111 token: ProfilingToken,
112 ) -> Result<ProfileDuration, ProfileError> {
113 let Open { start, anchor } = match self.open.remove(&token) {
114 Some(state) => state?,
115 None => {
116 return Err(ProfileError::NotRegistered {
117 backtrace: BackTrace::capture(),
118 });
119 }
120 };
121
122 let end = self.pool.acquire().map_err(profile_error)?;
123 end.record(stream).map_err(profile_error)?;
124
125 Ok(ProfileDuration::new_device_time(async move {
126 read::<A>(&start, &end, &anchor).unwrap_or_else(|err| {
127 log::error!(
128 "Could not read back a {} profiling window ({err}); reporting {UNREADABLE:?} \
129 so nothing mistakes it for a fast one",
130 A::BACKEND
131 );
132 let now = Instant::now();
133 ProfileTicks::from_start_end(now, now + UNREADABLE)
134 })
135 // `start` and `end` are dropped here, which is what returns them
136 // to the pool.
137 }))
138 }
139
140 /// Drop the window `token` opened without measuring it, for a caller that
141 /// has no way to record its end.
142 pub fn abandon(&mut self, token: ProfilingToken) {
143 self.open.remove(&token);
144 }
145
146 /// Mark every open window invalid because device work failed.
147 ///
148 /// This is what keeps a tuning candidate that failed from benchmarking at
149 /// close to zero and winning the tune. A no-op with no window open, so a
150 /// failure path calls it unconditionally and pays nothing for the common
151 /// case of no measurement in flight.
152 pub fn failure(&mut self, error: &ServerError) {
153 if self.open.is_empty() {
154 return;
155 }
156
157 let error = ProfileError::from(error);
158 self.open
159 .values_mut()
160 .for_each(|state| *state = Err(error.clone()));
161 }
162
163 /// The anchor a window opening now measures against, refreshed when the
164 /// current one has aged past [`ANCHOR_MAX_AGE`].
165 fn anchor(&mut self) -> Result<Anchor<A>, DriverError> {
166 if self.anchoring.is_none() {
167 self.anchoring = Some(Anchoring::new(&self.pool)?);
168 }
169 let anchoring = self.anchoring.as_mut().expect("filled right above");
170
171 if anchoring.current.instant.elapsed() > ANCHOR_MAX_AGE {
172 anchoring.current = Anchor::take(anchoring.stream, &self.pool)?;
173 }
174
175 Ok(anchoring.current.clone())
176 }
177}
178
179impl<A: EventApi> Default for EventProfiler<A> {
180 fn default() -> Self {
181 Self {
182 open: HashMap::default(),
183 counter: 0,
184 pool: EventPool::default(),
185 anchoring: None,
186 }
187 }
188}
189
190impl<A: EventApi> core::fmt::Debug for EventProfiler<A> {
191 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192 f.debug_struct("EventProfiler")
193 .field("backend", &A::BACKEND)
194 .field("open", &self.open.len())
195 .field("anchored", &self.anchoring.is_some())
196 .finish()
197 }
198}
199
200/// Read a closed window back from the device.
201fn read<A: EventApi>(
202 start: &Event<A>,
203 end: &Event<A>,
204 anchor: &Anchor<A>,
205) -> Result<ProfileTicks, DriverError> {
206 // Both, rather than the end alone. The end implies the start only because
207 // the two are recorded on the same stream, and waiting on an event the
208 // device has already passed returns immediately — a free way to stop
209 // depending on that.
210 start.wait()?;
211 end.wait()?;
212
213 // The span is measured between the window's own two events rather than as
214 // the difference of two anchor offsets. A second out from the anchor, two
215 // `f32` millisecond readings cancel down to some 60 ns of noise — a percent
216 // of a small kernel; between the two events themselves the reading is as
217 // exact as the device clock.
218 let offset = anchor.event.elapsed(start)?;
219 let span = start.elapsed(end)?;
220
221 let start_instant = anchor.instant + offset;
222 Ok(ProfileTicks::from_start_end(
223 start_instant,
224 start_instant + span,
225 ))
226}
227
228/// A point where the device clock and the host clock were read together.
229///
230/// An elapsed-time call measures from one event to another and knows nothing of
231/// the host clock, so placing a window on the host timeline takes a third event
232/// whose host time is known.
233struct Anchor<A: EventApi> {
234 /// Shared: a window that has not been read back yet still measures against
235 /// the anchor it opened under, which the profiler may have replaced since.
236 event: Arc<Pooled<A>>,
237 instant: Instant,
238}
239
240impl<A: EventApi> Anchor<A> {
241 /// Record an event on `stream` and wait for it, so the host time taken
242 /// right afterwards is the time the device stamped it.
243 fn take(stream: A::Stream, pool: &EventPool<A>) -> Result<Self, DriverError> {
244 let event = pool.acquire()?;
245 event.record(stream)?;
246 event.wait()?;
247
248 Ok(Self {
249 event: Arc::new(event),
250 instant: Instant::now(),
251 })
252 }
253}
254
255impl<A: EventApi> Clone for Anchor<A> {
256 fn clone(&self) -> Self {
257 Self {
258 event: self.event.clone(),
259 instant: self.instant,
260 }
261 }
262}
263
264/// The current [`Anchor`] and the stream it is recorded on.
265struct Anchoring<A: EventApi> {
266 /// A stream of its own, used for nothing but anchoring. Anchoring waits for
267 /// its event, and an anchor recorded into a working stream would wait for
268 /// everything queued ahead of it — exactly the drain this profiler exists
269 /// to avoid.
270 stream: A::Stream,
271 current: Anchor<A>,
272}
273
274impl<A: EventApi> Anchoring<A> {
275 fn new(pool: &EventPool<A>) -> Result<Self, DriverError> {
276 let stream = A::stream_create_non_blocking()?;
277 let current = Anchor::take(stream, pool)?;
278
279 Ok(Self { stream, current })
280 }
281}
282
283impl<A: EventApi> Drop for Anchoring<A> {
284 fn drop(&mut self) {
285 if let Err(err) = A::stream_destroy(self.stream) {
286 log::warn!(
287 "Failed to release the {} profiling anchor stream: {err}",
288 A::BACKEND
289 );
290 }
291 }
292}
293
294/// A driver failure while setting a window up, as the error a profile reports.
295fn profile_error(error: DriverError) -> ProfileError {
296 ProfileError::Server(Box::new(ServerError::from(error)))
297}