query_runtime/runtime/gpu/
inventory.rs1#[cfg(feature = "nvml")]
4use std::sync::OnceLock;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use query_planning::error::Result;
8use serde::Serialize;
9
10use crate::native_error::cudf_query_error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[non_exhaustive]
15pub enum DeviceTelemetryStatus {
16 Available,
18 FeatureDisabled,
20 Unavailable,
22}
23
24impl DeviceTelemetryStatus {
25 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Available => "available",
29 Self::FeatureDisabled => "feature_disabled",
30 Self::Unavailable => "unavailable",
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41pub struct DeviceTelemetrySnapshot {
42 pub(crate) sampled_at_unix_ms: u64,
44 pub device_ordinal: usize,
46 pub cuda_status: DeviceTelemetryStatus,
48 pub(crate) cuda_total_bytes: Option<u64>,
49 pub cuda_free_bytes: Option<u64>,
51 pub nvml_status: DeviceTelemetryStatus,
53 pub(crate) nvml_total_bytes: Option<u64>,
54 pub(crate) nvml_reserved_bytes: Option<u64>,
55 pub nvml_used_bytes: Option<u64>,
57 pub(crate) nvml_free_bytes: Option<u64>,
58 pub gpu_utilization_pct: Option<u32>,
60 pub memory_utilization_pct: Option<u32>,
62}
63
64pub(crate) trait GpuTelemetry: Send + Sync {
69 fn snapshot(&self, device_ordinal: usize) -> DeviceTelemetrySnapshot;
76}
77
78pub(crate) fn device_telemetry_snapshots_for_ordinals(
79 telemetry: &dyn GpuTelemetry,
80 device_ordinals: &[usize],
81) -> Vec<DeviceTelemetrySnapshot> {
82 device_ordinals.iter().map(|ordinal| telemetry.snapshot(*ordinal)).collect()
83}
84
85#[derive(Debug, Default, Clone, Copy)]
87pub(crate) struct AlgeonNvmlTelemetry;
88
89impl GpuTelemetry for AlgeonNvmlTelemetry {
90 fn snapshot(&self, device_ordinal: usize) -> DeviceTelemetrySnapshot {
91 let mut snapshot = DeviceTelemetrySnapshot {
92 sampled_at_unix_ms: sampled_at_unix_ms(),
93 device_ordinal,
94 cuda_status: DeviceTelemetryStatus::Unavailable,
95 cuda_total_bytes: None,
96 cuda_free_bytes: None,
97 nvml_status: nvml_status_without_sample(),
98 nvml_total_bytes: None,
99 nvml_reserved_bytes: None,
100 nvml_used_bytes: None,
101 nvml_free_bytes: None,
102 gpu_utilization_pct: None,
103 memory_utilization_pct: None,
104 };
105 let Ok(device) = cudf::DeviceId::new(device_ordinal) else {
106 return snapshot;
107 };
108 if let Ok(memory) = device.available_memory() {
109 snapshot.cuda_status = DeviceTelemetryStatus::Available;
110 snapshot.cuda_total_bytes = Some(memory.total_bytes);
111 snapshot.cuda_free_bytes = Some(memory.free_bytes);
112 }
113 sample_nvml(device, &mut snapshot);
114 snapshot
115 }
116}
117
118fn sampled_at_unix_ms() -> u64 {
119 SystemTime::now()
120 .duration_since(UNIX_EPOCH)
121 .ok()
122 .and_then(|duration| u64::try_from(duration.as_millis()).ok())
123 .unwrap_or(0)
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct DeviceFacts {
129 pub device: usize,
131 pub model_name: Option<String>,
133 pub uuid: Option<String>,
135 pub physical_total_bytes: Option<u64>,
137 pub total_bytes: u64,
139 pub(crate) free_bytes: u64,
143 pub(crate) sm_count: Option<u32>,
144}
145
146pub(crate) trait GpuInventory: Send + Sync {
148 fn facts(&self, device_ordinal: usize) -> Result<DeviceFacts>;
154}
155
156#[derive(Debug, Default, Clone, Copy)]
158pub(crate) struct AlgeonGpuInventory;
159
160impl GpuInventory for AlgeonGpuInventory {
161 fn facts(&self, device_ordinal: usize) -> Result<DeviceFacts> {
162 let device = cudf::DeviceId::new(device_ordinal).map_err(|error| {
163 cudf_query_error(
164 "query_engine.gpu_inventory.device_id",
165 "failed to resolve CUDA device id while collecting inventory facts",
166 error,
167 )
168 .with_fact("device_ordinal", device_ordinal)
169 })?;
170 let memory = device.available_memory().map_err(|error| {
171 cudf_query_error("query_engine.gpu_inventory.memory", "failed to query CUDA device memory inventory", error)
172 .with_fact("device_ordinal", device.ordinal())
173 })?;
174 let sm_count = device.multiprocessor_count().ok().filter(|count| *count > 0);
175 let (model_name, uuid, physical_total_bytes) = nvml_device_identity(device);
176 Ok(DeviceFacts {
177 device: device_ordinal,
178 model_name,
179 uuid,
180 physical_total_bytes,
181 total_bytes: memory.total_bytes,
182 free_bytes: memory.free_bytes,
183 sm_count,
184 })
185 }
186}
187
188#[cfg(feature = "nvml")]
189fn nvml_device_identity(device: cudf::DeviceId) -> (Option<String>, Option<String>, Option<u64>) {
190 let Some(nvml) = shared_nvml() else {
191 return (None, None, None);
192 };
193 let Ok(pci_bus_id) = device.pci_bus_id() else {
194 return (None, None, None);
195 };
196 let Ok(device) = nvml.device_by_pci_bus_id(pci_bus_id) else {
197 return (None, None, None);
198 };
199 (device.name().ok(), device.uuid().ok(), device.memory_info().ok().map(|memory| memory.total))
200}
201
202#[cfg(not(feature = "nvml"))]
203fn nvml_device_identity(_device: cudf::DeviceId) -> (Option<String>, Option<String>, Option<u64>) {
204 (None, None, None)
205}
206
207#[cfg(test)]
209pub(crate) fn device_budget_bytes_from_facts(facts: &DeviceFacts, headroom_bytes: u64, fallback_bytes: u64) -> u64 {
210 facts.total_bytes.checked_sub(headroom_bytes).filter(|value| *value > 0).unwrap_or(fallback_bytes)
211}
212
213#[cfg(feature = "nvml")]
214fn shared_nvml() -> Option<&'static nvml_wrapper::Nvml> {
215 static NVML: OnceLock<Option<nvml_wrapper::Nvml>> = OnceLock::new();
216 NVML.get_or_init(|| nvml_wrapper::Nvml::init().ok()).as_ref()
217}
218
219#[cfg(feature = "nvml")]
220fn nvml_status_without_sample() -> DeviceTelemetryStatus {
221 DeviceTelemetryStatus::Unavailable
222}
223
224#[cfg(not(feature = "nvml"))]
225fn nvml_status_without_sample() -> DeviceTelemetryStatus {
226 DeviceTelemetryStatus::FeatureDisabled
227}
228
229#[cfg(feature = "nvml")]
230fn sample_nvml(device: cudf::DeviceId, snapshot: &mut DeviceTelemetrySnapshot) {
231 let Some(nvml) = shared_nvml() else {
232 return;
233 };
234 let Ok(pci_bus_id) = device.pci_bus_id() else {
235 return;
236 };
237 let Ok(device) = nvml.device_by_pci_bus_id(pci_bus_id) else {
238 return;
239 };
240 let mut sampled = false;
241 if let Ok(memory) = device.memory_info() {
242 sampled = true;
243 snapshot.nvml_total_bytes = Some(memory.total);
244 snapshot.nvml_reserved_bytes = Some(memory.reserved);
245 snapshot.nvml_used_bytes = Some(memory.used);
246 snapshot.nvml_free_bytes = Some(memory.free);
247 }
248 if let Ok(utilization) = device.utilization_rates() {
249 sampled = true;
250 snapshot.gpu_utilization_pct = Some(utilization.gpu);
251 snapshot.memory_utilization_pct = Some(utilization.memory);
252 }
253 if sampled {
254 snapshot.nvml_status = DeviceTelemetryStatus::Available;
255 }
256}
257
258#[cfg(not(feature = "nvml"))]
259fn sample_nvml(_device: cudf::DeviceId, _snapshot: &mut DeviceTelemetrySnapshot) {}
260
261#[cfg(test)]
262mod tests {
263 use super::{
264 DeviceTelemetrySnapshot, DeviceTelemetryStatus, GpuTelemetry, device_telemetry_snapshots_for_ordinals,
265 };
266
267 struct FakeTelemetry;
268
269 impl GpuTelemetry for FakeTelemetry {
270 fn snapshot(&self, device_ordinal: usize) -> DeviceTelemetrySnapshot {
271 let nvml_available = device_ordinal == 3;
272 DeviceTelemetrySnapshot {
273 sampled_at_unix_ms: 42,
274 device_ordinal,
275 cuda_status: DeviceTelemetryStatus::Available,
276 cuda_total_bytes: Some(1_000 + device_ordinal as u64),
277 cuda_free_bytes: Some(500),
278 nvml_status: if nvml_available {
279 DeviceTelemetryStatus::Available
280 } else {
281 DeviceTelemetryStatus::Unavailable
282 },
283 nvml_total_bytes: nvml_available.then_some(1_000),
284 nvml_reserved_bytes: None,
285 nvml_used_bytes: nvml_available.then_some(250),
286 nvml_free_bytes: nvml_available.then_some(750),
287 gpu_utilization_pct: nvml_available.then_some(12),
288 memory_utilization_pct: nvml_available.then_some(34),
289 }
290 }
291 }
292
293 #[test]
294 fn telemetry_provider_samples_every_configured_ordinal_in_order() {
295 let snapshots = device_telemetry_snapshots_for_ordinals(&FakeTelemetry, &[3, 1]);
296
297 assert_eq!(snapshots.len(), 2);
298 assert_eq!(snapshots[0].device_ordinal, 3);
299 assert_eq!(snapshots[0].cuda_total_bytes, Some(1_003));
300 assert_eq!(snapshots[0].nvml_status, DeviceTelemetryStatus::Available);
301 assert_eq!(snapshots[0].gpu_utilization_pct, Some(12));
302 assert_eq!(snapshots[1].device_ordinal, 1);
303 assert_eq!(snapshots[1].nvml_status, DeviceTelemetryStatus::Unavailable);
304 }
305
306 #[cfg(feature = "nvml")]
307 #[test]
308 fn live_telemetry_covers_every_cuda_device_with_valid_ranges() {
309 use super::AlgeonNvmlTelemetry;
310
311 let device_count = cudf::DeviceId::count().expect("CUDA device count");
312 assert!(device_count > 0, "GPU test lane requires at least one CUDA device");
313 let ordinals = (0..device_count).collect::<Vec<_>>();
314
315 let snapshots = device_telemetry_snapshots_for_ordinals(&AlgeonNvmlTelemetry, &ordinals);
316
317 assert_eq!(snapshots.len(), device_count);
318 for (ordinal, snapshot) in snapshots.iter().enumerate() {
319 assert_eq!(snapshot.device_ordinal, ordinal);
320 assert_eq!(snapshot.cuda_status, DeviceTelemetryStatus::Available);
321 assert!(snapshot.cuda_free_bytes.unwrap() <= snapshot.cuda_total_bytes.unwrap());
322 assert_eq!(snapshot.nvml_status, DeviceTelemetryStatus::Available);
323 assert!(snapshot.nvml_free_bytes.unwrap() <= snapshot.nvml_total_bytes.unwrap());
324 assert!(snapshot.gpu_utilization_pct.unwrap() <= 100);
325 assert!(snapshot.memory_utilization_pct.unwrap() <= 100);
326 }
327 }
328}