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 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}