metrale_gpu_runtime/timing.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: L1 GPU spans: CUDA event pairs around each serve lane on every
4//! step, and around each eager kernel launch on the steps the telemetry hub arms
5//! (one in `kernel_span_every`), at telemetry level `Kernel` only.
6//!
7//! The bookkeeping is `metrale_telemetry::kernel::SpanRing`; this module is the
8//! CUDA half: the event pool (`cuda_events`) and the process-wide recorder that
9//! `MetraleCudaBackend::new` installs.
10//!
11//! Lane spans are recorded on the registry's stream (`MetraleRegistry::raw_stream`),
12//! which cudarc creates non-blocking, so a lane pair times the work queued on that
13//! stream only.
14//!
15//! Owner: gpu-runtime (telemetry).
16//! Invariants:
17//! - No span synchronises: a completed pair is harvested with a non-blocking
18//! `cuEventQuery`, and a pair still in flight is left for a later harvest.
19//! - Below level `Kernel` no entry point reaches CUDA: each first reads the hub's
20//! level or armed flag (a relaxed atomic load), and `kernel_end` acts only on a
21//! span `kernel_begin` opened.
22//! - No kernel span is opened on a stream that is being captured.
23
24#[cfg(feature = "cuda")]
25mod cuda_events;
26
27#[cfg(feature = "cuda")]
28pub use imp::*;
29
30/// 2026-09-25: Event pairs in the pool. The ring hands out slots in turn; a span
31/// whose slot is still in flight is dropped and counted (`spans_dropped`).
32pub const SPAN_SLOTS: usize = 4096;
33
34#[cfg(feature = "cuda")]
35mod imp {
36 use std::sync::atomic::{AtomicUsize, Ordering};
37 use std::sync::{Arc, RwLock};
38
39 use metrale_telemetry::kernel::{LANES, SpanKey, SpanRing, SpanToken};
40 use metrale_telemetry::{Level, global};
41
42 use super::SPAN_SLOTS;
43 use super::cuda_events::CudaEvents;
44
45 /// 2026-09-25: The recorder `install_for_stream` installs; a later install replaces it.
46 pub struct GpuSpans {
47 ring: SpanRing<CudaEvents>,
48 stream: u64,
49 open_lanes: [AtomicUsize; LANES.len()],
50 }
51
52 const NONE: usize = usize::MAX;
53
54 static INSTALLED: RwLock<Option<Arc<GpuSpans>>> = RwLock::new(None);
55
56 fn installed() -> Option<Arc<GpuSpans>> {
57 INSTALLED.read().ok()?.clone()
58 }
59
60 /// 2026-09-25: Build and install the recorder for a backend whose stream is
61 /// `stream`, when telemetry is at level `Kernel`. Called from
62 /// `MetraleCudaBackend::new` after the registry loads. A pool that cannot be
63 /// created leaves L1 off and logs why.
64 pub fn install_for_stream(stream: u64) {
65 if global().level() != Level::Kernel {
66 return;
67 }
68 match CudaEvents::create(SPAN_SLOTS) {
69 Ok(events) => {
70 let spans = GpuSpans {
71 ring: SpanRing::new(events, SPAN_SLOTS),
72 stream,
73 open_lanes: [const { AtomicUsize::new(NONE) }; LANES.len()],
74 };
75 if let Ok(mut slot) = INSTALLED.write() {
76 *slot = Some(Arc::new(spans));
77 }
78 }
79 Err(e) => tracing::warn!("telemetry: GPU spans disabled — {e}"),
80 }
81 }
82
83 /// 2026-09-25: Open lane `lane`'s span; an index outside `LANES` is ignored.
84 pub fn lane_begin(lane: usize) {
85 if !global().lane_spans() {
86 return;
87 }
88 let Some(s) = installed() else { return };
89 let Some(slot) = s.open_lanes.get(lane) else {
90 return;
91 };
92 if let Some(tok) = s
93 .ring
94 .begin(SpanKey::Lane(lane), s.stream, &global().kernel)
95 {
96 slot.store(tok.slot(), Ordering::Relaxed);
97 }
98 }
99
100 /// 2026-09-25: Close lane `lane`'s span, if one is open.
101 pub fn lane_end(lane: usize) {
102 if !global().lane_spans() {
103 return;
104 }
105 let Some(s) = installed() else { return };
106 let Some(slot) = s.open_lanes.get(lane) else {
107 return;
108 };
109 let i = slot.swap(NONE, Ordering::Relaxed);
110 if i != NONE {
111 s.ring
112 .end(SpanToken::from_slot(i), s.stream, &global().kernel);
113 }
114 }
115
116 /// 2026-09-25: Land every completed span. The scheduler calls it at each step begin.
117 pub fn harvest() {
118 if global().level() != Level::Kernel {
119 return;
120 }
121 if let Some(s) = installed() {
122 s.ring.harvest(&global().kernel);
123 }
124 }
125
126 /// 2026-09-25: A kernel span, when this step is armed for kernel spans and
127 /// `stream` is not being captured (an event recorded into a capture would be a
128 /// graph node, not a timestamp).
129 pub fn kernel_begin(
130 gpu: &dyn crate::gpu::GpuBackend,
131 func: u64,
132 stream: u64,
133 ) -> Option<KernelSpan> {
134 if !global().kernel_spans_armed() || gpu.stream_is_capturing(stream) {
135 return None;
136 }
137 let s = installed()?;
138 let tok = s
139 .ring
140 .begin(SpanKey::Kernel(func), stream, &global().kernel)?;
141 Some(KernelSpan { spans: s, tok })
142 }
143
144 /// 2026-09-25: Close a kernel span opened by [`kernel_begin`].
145 pub fn kernel_end(span: Option<KernelSpan>, stream: u64) {
146 if let Some(k) = span {
147 k.spans.ring.end(k.tok, stream, &global().kernel);
148 }
149 }
150
151 /// 2026-09-25: An open kernel span.
152 pub struct KernelSpan {
153 spans: Arc<GpuSpans>,
154 tok: SpanToken,
155 }
156}
157
158/// 2026-09-25: Non-CUDA builds carry no GPU spans. The entry points the scheduler
159/// calls stay, so callers need no cfg of their own.
160#[cfg(not(feature = "cuda"))]
161mod imp_absent {
162 pub fn lane_begin(_lane: usize) {}
163 pub fn lane_end(_lane: usize) {}
164 pub fn harvest() {}
165}
166#[cfg(not(feature = "cuda"))]
167pub use imp_absent::*;