Skip to main content

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 stream whose next launch records `start`, until one does.
64    waits_for_launch: Option<A::Stream>,
65    /// The anchor this window opened under, kept so that its start and its end
66    /// are placed against the same one however many refreshes happen in
67    /// between.
68    anchor: Anchor<A>,
69}
70
71impl<A: EventApi> EventProfiler<A> {
72    /// Open a window at the current position of `stream`.
73    ///
74    /// # Errors
75    ///
76    /// [`ServerError`] when the device refuses an event or the stream will not
77    /// record one. No window is opened and no token is issued, so the caller
78    /// owes nothing; the events involved return to the pool.
79    pub fn start(&mut self, stream: A::Stream) -> Result<ProfilingToken, ServerError> {
80        let anchor = self.anchor()?;
81        let start = self.pool.acquire()?;
82        start.record(stream)?;
83
84        Ok(self.opened(Open {
85            start,
86            waits_for_launch: None,
87            anchor,
88        }))
89    }
90
91    /// Open a window at the next kernel launched on `stream`, rather than at
92    /// where the stream is now.
93    ///
94    /// What lies between the two is the host preparing that launch. A device
95    /// with nothing queued stamps an event the moment it is recorded, so a
96    /// window opened before the preparation charges it to the kernel: a launch
97    /// timed alone on an idle device read its own host latency as device time.
98    /// The backend records the start right before it launches
99    /// ([`launching`](Self::launching)); a window closed with no launch in it
100    /// records its start as it closes, and reads empty.
101    ///
102    /// # Errors
103    ///
104    /// [`ServerError`] when the device refuses an event. No window is opened.
105    pub fn start_at_launch(&mut self, stream: A::Stream) -> Result<ProfilingToken, ServerError> {
106        let anchor = self.anchor()?;
107        let start = self.pool.acquire()?;
108
109        Ok(self.opened(Open {
110            start,
111            waits_for_launch: Some(stream),
112            anchor,
113        }))
114    }
115
116    /// A kernel is about to be launched on `stream`: every window waiting for
117    /// that launch starts here. Free when no window is open.
118    ///
119    /// A start that cannot be recorded fails its window, not the launch.
120    pub fn launching(&mut self, stream: A::Stream) {
121        for state in self.open.values_mut() {
122            let Ok(open) = state else { continue };
123            if open.waits_for_launch != Some(stream) {
124                continue;
125            }
126            open.waits_for_launch = None;
127            if let Err(err) = open.start.record(stream) {
128                *state = Err(profile_error(err));
129            }
130        }
131    }
132
133    fn opened(&mut self, open: Open<A>) -> ProfilingToken {
134        let token = ProfilingToken { id: self.counter };
135        self.counter += 1;
136        self.open.insert(token, Ok(open));
137        token
138    }
139
140    /// Close the window `token` opened at the current position of `stream`.
141    ///
142    /// Returns at enqueue time. The duration is read from the device by the
143    /// returned future, which is where the wait for the window's work lives.
144    ///
145    /// That future blocks the thread that polls it: the device APIs offer no
146    /// way to be woken when an event is reached, only a synchronize that parks
147    /// the caller, so the first poll returns once the device has reached both
148    /// events. Await it from a thread that can afford to wait — the same place
149    /// a synchronize would have gone.
150    ///
151    /// # Errors
152    ///
153    /// [`ProfileError::NotRegistered`] for a token this profiler never issued
154    /// or has already closed, the error registered against the window by
155    /// [`failure`](Self::failure) when device work failed inside it, and
156    /// [`ProfileError::Server`] when the closing event cannot be recorded.
157    /// There is nothing to measure in any of those cases and the window is
158    /// gone either way.
159    pub fn stop(
160        &mut self,
161        stream: A::Stream,
162        token: ProfilingToken,
163    ) -> Result<ProfileDuration, ProfileError> {
164        let Open {
165            start,
166            waits_for_launch,
167            anchor,
168        } = match self.open.remove(&token) {
169            Some(state) => state?,
170            None => {
171                return Err(ProfileError::NotRegistered {
172                    backtrace: BackTrace::capture(),
173                });
174            }
175        };
176        if waits_for_launch.is_some() {
177            // No launch came: the window is empty, and starts where it ends.
178            start.record(stream).map_err(profile_error)?;
179        }
180
181        let end = self.pool.acquire().map_err(profile_error)?;
182        end.record(stream).map_err(profile_error)?;
183
184        Ok(ProfileDuration::new_device_time(async move {
185            read::<A>(&start, &end, &anchor).unwrap_or_else(|err| {
186                log::error!(
187                    "Could not read back a {} profiling window ({err}); reporting {UNREADABLE:?} \
188                     so nothing mistakes it for a fast one",
189                    A::BACKEND
190                );
191                let now = Instant::now();
192                ProfileTicks::from_start_end(now, now + UNREADABLE)
193            })
194            // `start` and `end` are dropped here, which is what returns them
195            // to the pool.
196        }))
197    }
198
199    /// Drop the window `token` opened without measuring it, for a caller that
200    /// has no way to record its end.
201    pub fn abandon(&mut self, token: ProfilingToken) {
202        self.open.remove(&token);
203    }
204
205    /// Mark every open window invalid because device work failed.
206    ///
207    /// This is what keeps a tuning candidate that failed from benchmarking at
208    /// close to zero and winning the tune. A no-op with no window open, so a
209    /// failure path calls it unconditionally and pays nothing for the common
210    /// case of no measurement in flight.
211    pub fn failure(&mut self, error: &ServerError) {
212        if self.open.is_empty() {
213            return;
214        }
215
216        let error = ProfileError::from(error);
217        self.open
218            .values_mut()
219            .for_each(|state| *state = Err(error.clone()));
220    }
221
222    /// The anchor a window opening now measures against, refreshed when the
223    /// current one has aged past [`ANCHOR_MAX_AGE`].
224    fn anchor(&mut self) -> Result<Anchor<A>, DriverError> {
225        if self.anchoring.is_none() {
226            self.anchoring = Some(Anchoring::new(&self.pool)?);
227        }
228        let anchoring = self.anchoring.as_mut().expect("filled right above");
229
230        if anchoring.current.instant.elapsed() > ANCHOR_MAX_AGE {
231            anchoring.current = Anchor::take(anchoring.stream, &self.pool)?;
232        }
233
234        Ok(anchoring.current.clone())
235    }
236}
237
238impl<A: EventApi> Default for EventProfiler<A> {
239    fn default() -> Self {
240        Self {
241            open: HashMap::default(),
242            counter: 0,
243            pool: EventPool::default(),
244            anchoring: None,
245        }
246    }
247}
248
249impl<A: EventApi> core::fmt::Debug for EventProfiler<A> {
250    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251        f.debug_struct("EventProfiler")
252            .field("backend", &A::BACKEND)
253            .field("open", &self.open.len())
254            .field("anchored", &self.anchoring.is_some())
255            .finish()
256    }
257}
258
259/// Read a closed window back from the device.
260fn read<A: EventApi>(
261    start: &Event<A>,
262    end: &Event<A>,
263    anchor: &Anchor<A>,
264) -> Result<ProfileTicks, DriverError> {
265    // Both, rather than the end alone. The end implies the start only because
266    // the two are recorded on the same stream, and waiting on an event the
267    // device has already passed returns immediately — a free way to stop
268    // depending on that.
269    start.wait()?;
270    end.wait()?;
271
272    // The span is measured between the window's own two events rather than as
273    // the difference of two anchor offsets. A second out from the anchor, two
274    // `f32` millisecond readings cancel down to some 60 ns of noise — a percent
275    // of a small kernel; between the two events themselves the reading is as
276    // exact as the device clock.
277    let offset = anchor.event.elapsed(start)?;
278    let span = start.elapsed(end)?;
279
280    let start_instant = anchor.instant + offset;
281    Ok(ProfileTicks::from_start_end(
282        start_instant,
283        start_instant + span,
284    ))
285}
286
287/// A point where the device clock and the host clock were read together.
288///
289/// An elapsed-time call measures from one event to another and knows nothing of
290/// the host clock, so placing a window on the host timeline takes a third event
291/// whose host time is known.
292struct Anchor<A: EventApi> {
293    /// Shared: a window that has not been read back yet still measures against
294    /// the anchor it opened under, which the profiler may have replaced since.
295    event: Arc<Pooled<A>>,
296    instant: Instant,
297}
298
299impl<A: EventApi> Anchor<A> {
300    /// Record an event on `stream` and wait for it, so the host time taken
301    /// right afterwards is the time the device stamped it.
302    fn take(stream: A::Stream, pool: &EventPool<A>) -> Result<Self, DriverError> {
303        let event = pool.acquire()?;
304        event.record(stream)?;
305        event.wait()?;
306
307        Ok(Self {
308            event: Arc::new(event),
309            instant: Instant::now(),
310        })
311    }
312}
313
314impl<A: EventApi> Clone for Anchor<A> {
315    fn clone(&self) -> Self {
316        Self {
317            event: self.event.clone(),
318            instant: self.instant,
319        }
320    }
321}
322
323/// The current [`Anchor`] and the stream it is recorded on.
324struct Anchoring<A: EventApi> {
325    /// A stream of its own, used for nothing but anchoring. Anchoring waits for
326    /// its event, and an anchor recorded into a working stream would wait for
327    /// everything queued ahead of it — exactly the drain this profiler exists
328    /// to avoid.
329    stream: A::Stream,
330    current: Anchor<A>,
331}
332
333impl<A: EventApi> Anchoring<A> {
334    fn new(pool: &EventPool<A>) -> Result<Self, DriverError> {
335        let stream = A::stream_create_non_blocking()?;
336        let current = Anchor::take(stream, pool)?;
337
338        Ok(Self { stream, current })
339    }
340}
341
342impl<A: EventApi> Drop for Anchoring<A> {
343    fn drop(&mut self) {
344        if let Err(err) = A::stream_destroy(self.stream) {
345            log::warn!(
346                "Failed to release the {} profiling anchor stream: {err}",
347                A::BACKEND
348            );
349        }
350    }
351}
352
353/// A driver failure while setting a window up, as the error a profile reports.
354fn profile_error(error: DriverError) -> ProfileError {
355    ProfileError::Server(Box::new(ServerError::from(error)))
356}