metrale_telemetry/export/
prometheus.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The telemetry section of `/metrics` in the Prometheus text
4//! format, which the server appends to its registry output.
5//!
6//! Names carry their unit (`_seconds`, `_joules`, `_watts`, `_bytes`,
7//! `_celsius`); the NVML-sourced energy counters, clocks and PCIe throughput
8//! keep NVML's units (`_millijoules`, `_mhz`, `_kbps`). TTFT is not exported
9//! here: the server's `metrale_time_to_first_token_seconds` carries it,
10//! labelled by model.
11//!
12//! Owner: telemetry.
13//! Invariants: the section is empty at level `Off`.
14
15use std::fmt::Write;
16
17use crate::device::CLOCK_EVENT_REASONS;
18use crate::hub::Telemetry;
19use crate::level::Level;
20use crate::snapshot::TelemetrySnapshot;
21
22use super::prometheus_layers::{render_kernel, render_sched, render_spec_and_requests};
23use super::prometheus_text::{head, histogram, one, opt};
24
25/// 2026-09-26: Render the telemetry section. `kernel_name` names a kernel
26/// handle; a handle it cannot name is exported by address.
27pub fn render(t: &Telemetry, kernel_name: &dyn Fn(u64) -> Option<String>) -> String {
28    let mut out = String::new();
29    if t.level() == Level::Off {
30        return out;
31    }
32    let snap = t.snapshot();
33    render_level_and_device(&mut out, &snap);
34    render_energy(&mut out, t, &snap);
35    render_sched(&mut out, t, &snap);
36    render_spec_and_requests(&mut out, t, &snap);
37    if t.level() == Level::Kernel {
38        render_kernel(&mut out, t, kernel_name);
39    }
40    out
41}
42fn render_level_and_device(out: &mut String, snap: &TelemetrySnapshot) {
43    head(
44        out,
45        "metrale_telemetry_level",
46        "gauge",
47        "Telemetry level in force (1 on the active label)",
48    );
49    for l in Level::NAMES {
50        let _ = writeln!(
51            out,
52            "metrale_telemetry_level{{level=\"{l}\"}} {}",
53            u8::from(l == snap.level)
54        );
55    }
56    one(
57        out,
58        "metrale_gpu_device_available",
59        "gauge",
60        "1 while the NVML device layer is reading",
61        u8::from(snap.device_state == "available"),
62    );
63    let Some(d) = &snap.device else { return };
64    let r = &d.reading;
65    one(
66        out,
67        "metrale_gpu_samples_total",
68        "counter",
69        "Device samples taken",
70        d.samples,
71    );
72    one(
73        out,
74        "metrale_gpu_sample_age_seconds",
75        "gauge",
76        "Age of the latest device sample",
77        d.age_ns as f64 / 1e9,
78    );
79    one(
80        out,
81        "metrale_gpu_read_errors_total",
82        "counter",
83        "NVML field reads that failed",
84        d.read_errors,
85    );
86    opt(
87        out,
88        "metrale_gpu_power_watts",
89        "GPU-rail power draw (nvmlDeviceGetPowerUsage)",
90        r.power_mw.map(|v| v as f64 / 1e3),
91    );
92    opt(
93        out,
94        "metrale_gpu_temperature_celsius",
95        "GPU temperature",
96        r.temperature_c.map(|v| v as f64),
97    );
98    head(
99        out,
100        "metrale_gpu_clock_mhz",
101        "gauge",
102        "Current clocks by domain",
103    );
104    for (dom, v) in [
105        ("graphics", r.graphics_clock_mhz),
106        ("sm", r.sm_clock_mhz),
107        ("mem", r.mem_clock_mhz),
108    ] {
109        if let Some(v) = v {
110            let _ = writeln!(out, "metrale_gpu_clock_mhz{{clock=\"{dom}\"}} {v}");
111        }
112    }
113    if let Some(mask) = r.clocks_event_reasons {
114        one(
115            out,
116            "metrale_gpu_clocks_event_reasons",
117            "gauge",
118            "NVML clocks-event-reasons bitmask",
119            mask,
120        );
121        head(
122            out,
123            "metrale_gpu_clocks_event_reason_active",
124            "gauge",
125            "1 while the named clock-event reason is asserted",
126        );
127        for (name, bit) in CLOCK_EVENT_REASONS {
128            let _ = writeln!(
129                out,
130                "metrale_gpu_clocks_event_reason_active{{reason=\"{name}\"}} {}",
131                u8::from(mask & bit != 0)
132            );
133        }
134    }
135    opt(
136        out,
137        "metrale_gpu_memory_used_bytes",
138        "Device memory in use (NVML)",
139        r.mem_used_bytes.map(|v| v as f64),
140    );
141    opt(
142        out,
143        "metrale_gpu_memory_total_bytes",
144        "Device memory total (NVML)",
145        r.mem_total_bytes.map(|v| v as f64),
146    );
147    if r.pcie_tx_kbps.is_some() || r.pcie_rx_kbps.is_some() {
148        head(
149            out,
150            "metrale_gpu_pcie_throughput_kbps",
151            "gauge",
152            "PCIe throughput by direction",
153        );
154        for (dir, v) in [("tx", r.pcie_tx_kbps), ("rx", r.pcie_rx_kbps)] {
155            if let Some(v) = v {
156                let _ = writeln!(
157                    out,
158                    "metrale_gpu_pcie_throughput_kbps{{direction=\"{dir}\"}} {v}"
159                );
160            }
161        }
162    }
163    opt(
164        out,
165        "metrale_gpu_nvlink_active_links",
166        "Active NVLink links",
167        r.nvlink_active_links.map(|v| v as f64),
168    );
169}
170fn render_energy(out: &mut String, t: &Telemetry, snap: &TelemetrySnapshot) {
171    let e = &snap.energy;
172    one(
173        out,
174        "metrale_gpu_energy_counter_millijoules",
175        "gauge",
176        "Raw NVML total-energy counter at the latest sample (GPU rail, mJ since driver load)",
177        e.gpu_counter_mj,
178    );
179    one(
180        out,
181        "metrale_gpu_energy_millijoules_total",
182        "counter",
183        "GPU-rail energy since the first sample, reset- and wrap-corrected",
184        e.gpu_mj_total,
185    );
186    one(
187        out,
188        "metrale_gpu_energy_counter_resets_total",
189        "counter",
190        "Times the NVML energy counter restarted",
191        e.counter_resets,
192    );
193    one(
194        out,
195        "metrale_gpu_energy_counter_wraps_total",
196        "counter",
197        "Times the NVML energy counter wrapped",
198        e.counter_wraps,
199    );
200    one(
201        out,
202        "metrale_energy_tokens_total",
203        "counter",
204        "Tokens emitted since the first energy sample (the J/token denominator)",
205        e.tokens_total,
206    );
207    opt(
208        out,
209        "metrale_energy_joules_per_token",
210        "GPU-rail J/token over the last second of samples",
211        e.joules_per_token_live,
212    );
213    opt(
214        out,
215        "metrale_energy_joules_per_token_cumulative",
216        "GPU-rail J/token since the first energy sample",
217        e.joules_per_token_cumulative,
218    );
219    histogram(
220        out,
221        "metrale_request_energy_joules",
222        "GPU-rail joules attributed to each finished request",
223        &t.requests.energy.snapshot(),
224        1e3,
225    );
226    one(
227        out,
228        "metrale_request_energy_unattributed_total",
229        "counter",
230        "Finished requests with no energy window to attribute from",
231        snap.requests.energy_unattributed,
232    );
233}
234#[cfg(test)]
235#[path = "prometheus_tests.rs"]
236mod tests;