1use std::sync::OnceLock;
20use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
21use std::time::Instant;
22
23use crate::clock::{Clock, MonotonicClock};
24use crate::device::{DeviceReading, SAMPLE_WORDS};
25use crate::energy::{
26 Advance, CounterTracker, EnergyPoint, EnergyRing, RING_SLOTS, joules_per_token,
27 request_millijoules,
28};
29use crate::instrument::{Counter, Gauge, GaugeF64};
30use crate::kernel::{KernelInstruments, LANES};
31use crate::level::{Level, TelemetryConfig};
32use crate::request::{RequestInstruments, RequestTiming};
33use crate::sched::{MAX_PHASES, SchedInstruments, SchedShape, SpecMatrix};
34use crate::seqcell::SeqCell;
35
36pub const LIVE_WINDOW: u64 = 10;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[repr(u8)]
42pub enum DeviceState {
43 NotStarted = 0,
45 Available = 1,
46 Unavailable = 2,
50}
51
52pub struct Telemetry {
53 level: AtomicU8,
54 clock: &'static dyn Clock,
55 kernel_every: AtomicU32,
56 phase_names: OnceLock<&'static [&'static str]>,
57 pub(crate) device: SeqCell<SAMPLE_WORDS>,
58 device_state: AtomicU8,
59 device_reason: OnceLock<String>,
60 pub device_read_errors: Gauge,
61 pub energy_mj: Counter,
62 pub energy_counter_raw: Gauge,
63 pub energy_resets: Counter,
64 pub energy_wraps: Counter,
65 pub tokens: Counter,
66 energy_token_base: Gauge,
68 pub joules_per_token_live: GaugeF64,
69 ring: OnceLock<EnergyRing>,
70 pub kernel: KernelInstruments,
71 pub sched: SchedInstruments,
72 pub spec: SpecMatrix,
73 pub spec_confidence: crate::spec_confidence::ConfidenceOutcomes,
74 pub requests: RequestInstruments,
75}
76
77impl std::fmt::Debug for Telemetry {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("Telemetry")
80 .field("level", &self.level())
81 .finish_non_exhaustive()
82 }
83}
84
85static GLOBAL: Telemetry = Telemetry::new(&MonotonicClock);
86
87pub fn global() -> &'static Telemetry {
89 &GLOBAL
90}
91
92impl Telemetry {
93 pub const fn new(clock: &'static dyn Clock) -> Self {
95 Self {
96 level: AtomicU8::new(Level::Off as u8),
97 clock,
98 kernel_every: AtomicU32::new(u32::MAX),
99 phase_names: OnceLock::new(),
100 device: SeqCell::new(),
101 device_state: AtomicU8::new(DeviceState::NotStarted as u8),
102 device_reason: OnceLock::new(),
103 device_read_errors: Gauge::new(),
104 energy_mj: Counter::new(),
105 energy_counter_raw: Gauge::new(),
106 energy_resets: Counter::new(),
107 energy_wraps: Counter::new(),
108 tokens: Counter::new(),
109 energy_token_base: Gauge::new(),
110 joules_per_token_live: GaugeF64::new(),
111 ring: OnceLock::new(),
112 kernel: KernelInstruments::new(),
113 sched: SchedInstruments::new(),
114 spec: SpecMatrix::new(),
115 spec_confidence: crate::spec_confidence::ConfidenceOutcomes::new(),
116 requests: RequestInstruments::new(),
117 }
118 }
119
120 pub fn configure(&self, cfg: &TelemetryConfig) {
124 if cfg.level() >= Level::Basic {
125 self.ring
126 .get_or_init(|| EnergyRing::with_capacity(RING_SLOTS));
127 }
128 self.kernel_every
129 .store(cfg.kernel_span_every(), Ordering::Relaxed);
130 self.kernel.armed.store(false, Ordering::Relaxed);
131 self.level.store(cfg.level() as u8, Ordering::Release);
132 }
133
134 #[inline]
135 pub fn level(&self) -> Level {
136 Level::from_u8(self.level.load(Ordering::Relaxed))
137 }
138
139 #[inline]
140 fn on(&self) -> bool {
141 self.level.load(Ordering::Relaxed) != Level::Off as u8
142 }
143
144 pub fn set_phase_names(&self, names: &'static [&'static str]) {
147 let _ = self.phase_names.set(&names[..names.len().min(MAX_PHASES)]);
148 }
149
150 pub fn phase_names(&self) -> &'static [&'static str] {
151 self.phase_names.get().copied().unwrap_or(&[])
152 }
153
154 #[inline]
156 pub fn tokens(&self, n: u64) {
157 if !self.on() {
158 return;
159 }
160 self.tokens.add(n);
161 }
162
163 #[inline]
166 pub fn phase(&self, idx: usize, since: Instant) {
167 if !self.on() {
168 return;
169 }
170 let ns = self.clock.elapsed_ns(since);
171 self.sched.phase[idx.min(MAX_PHASES - 1)].observe(ns);
172 }
173
174 #[inline]
176 pub fn sched_tick(&self, shape: &SchedShape) {
177 if !self.on() {
178 return;
179 }
180 self.sched.record(shape);
181 }
182
183 #[inline]
184 pub fn stream_sync(&self) {
185 if self.on() {
186 self.sched.stream_syncs.add(1);
187 }
188 }
189
190 #[inline]
191 pub fn blocking_d2h(&self) {
192 if self.on() {
193 self.sched.blocking_d2h.add(1);
194 }
195 }
196
197 #[inline]
198 pub fn graph_replay(&self) {
199 if self.on() {
200 self.sched.graph_replays.add(1);
201 }
202 }
203
204 #[inline]
205 pub fn graph_capture(&self) {
206 if self.on() {
207 self.sched.graph_captures.add(1);
208 }
209 }
210
211 #[inline]
213 pub fn spec_verified(&self, drafts: usize, accepted: usize) {
214 if !self.on() {
215 return;
216 }
217 self.spec.record(drafts, accepted);
218 }
219
220 #[inline]
223 pub fn spec_draft_confidence(&self, lp: f32, accepted: bool) {
224 if !self.on() {
225 return;
226 }
227 self.spec_confidence.record(lp, accepted);
228 }
229
230 pub fn request_finished(&self, r: &RequestTiming) {
233 if !self.on() {
234 return;
235 }
236 let end = self.clock.now_ns();
237 let start = end.saturating_sub(r.e2e_ns());
238 let energy = self
239 .ring
240 .get()
241 .filter(|_| self.device_state() == DeviceState::Available)
242 .and_then(|ring| request_millijoules(ring, start, end, r.output_tokens));
243 self.requests.record(r, energy);
244 }
245
246 #[inline]
250 pub fn step_begin(&self) -> bool {
251 if self.level() != Level::Kernel {
252 return false;
253 }
254 let n = self.kernel.step.fetch_add(1, Ordering::Relaxed);
255 let every = u64::from(self.kernel_every.load(Ordering::Relaxed).max(1));
256 let arm = n.is_multiple_of(every);
257 self.kernel.armed.store(arm, Ordering::Relaxed);
258 arm
259 }
260
261 #[inline]
264 pub fn kernel_spans_armed(&self) -> bool {
265 self.kernel.armed.load(Ordering::Relaxed)
266 }
267
268 #[inline]
270 pub fn lane_spans(&self) -> bool {
271 self.level() == Level::Kernel
272 }
273
274 pub fn lane_names(&self) -> &'static [&'static str] {
275 &LANES
276 }
277
278 pub fn device_state(&self) -> DeviceState {
279 match self.device_state.load(Ordering::Acquire) {
280 1 => DeviceState::Available,
281 2 => DeviceState::Unavailable,
282 _ => DeviceState::NotStarted,
283 }
284 }
285
286 pub fn device_unavailable_reason(&self) -> Option<&str> {
287 self.device_reason.get().map(String::as_str)
288 }
289
290 pub fn mark_device_unavailable(&self, why: String) {
293 let _ = self.device_reason.set(why);
294 self.device_state
295 .store(DeviceState::Unavailable as u8, Ordering::Release);
296 }
297
298 pub fn absorb_device(
303 &self,
304 tracker: &mut CounterTracker,
305 reading: &DeviceReading,
306 read_errors: u64,
307 ) {
308 let at_ns = self.clock.now_ns();
309 self.device.store(reading.encode(at_ns));
310 self.device_read_errors.set(read_errors);
311 self.device_state
312 .store(DeviceState::Available as u8, Ordering::Release);
313 let Some(raw) = reading.energy_counter_mj else {
314 return;
315 };
316 self.energy_counter_raw.set(raw);
317 let step = tracker.advance(raw);
318 match step {
319 Advance::Wrapped(_) => self.energy_wraps.add(1),
320 Advance::Reset(_) => self.energy_resets.add(1),
321 Advance::First => self.energy_token_base.set(self.tokens.get()),
322 Advance::Delta(_) => {}
323 }
324 self.energy_mj.add(step.millijoules());
325 let Some(ring) = self.ring.get() else { return };
326 let now = EnergyPoint {
327 at_ns,
328 energy_mj: self.energy_mj.get(),
329 tokens: self.tokens.get(),
330 };
331 ring.push(now);
332 let live = (1..=LIVE_WINDOW)
333 .rev()
334 .find_map(|k| ring.back(k))
335 .and_then(|then| {
336 joules_per_token(now.energy_mj - then.energy_mj, now.tokens - then.tokens)
337 });
338 self.joules_per_token_live.set(live);
339 }
340
341 pub fn device_reading(&self) -> Option<(DeviceReading, u64, u64)> {
344 let (stores, words) = self.device.load();
345 (stores > 0).then(|| {
346 let (r, at) = DeviceReading::decode(&words);
347 (r, at, stores)
348 })
349 }
350
351 pub fn joules_per_token_cumulative(&self) -> Option<f64> {
353 joules_per_token(self.energy_mj.get(), self.tokens_since_first_sample())
354 }
355
356 pub fn tokens_since_first_sample(&self) -> u64 {
359 self.tokens
360 .get()
361 .saturating_sub(self.energy_token_base.get())
362 }
363
364 pub fn now_ns(&self) -> u64 {
365 self.clock.now_ns()
366 }
367}
368
369#[cfg(test)]
370#[path = "hub_tests.rs"]
371mod tests;