metrale_telemetry/export/
prometheus.rs1use 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
25pub 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;