1use std::{
6 collections::HashMap,
7 sync::{Arc, Mutex},
8 time::Duration,
9};
10
11use bitflags::bitflags;
12use futures::{StreamExt, TryFutureExt, future::try_join_all, try_join};
13use joule_profiler_core::{
14 sensor::{Sensor, Sensors},
15 source::MetricReader,
16 types::{Metric, Metrics},
17 unit::{MetricUnit, Unit, UnitPrefix},
18};
19use log::{debug, trace};
20use tokio::task::{JoinHandle, spawn_blocking};
21use tokio_timerfd::Interval;
22use tokio_util::sync::CancellationToken;
23
24use crate::{
25 config::NvmlConfig,
26 counters::{Counter, EnergyCounter, PowerCounter, UtilizationCounter, VramCounter},
27 error::NvmlError,
28 hardware::{NvmlHardware, NvmlWrapperHardware},
29};
30
31pub mod config;
32pub mod counters;
33mod error;
34mod hardware;
35
36const NVML_SOURCE_NAME: &str = "NVML";
37
38const MILLI_JOULE_UNIT: MetricUnit = MetricUnit {
39 prefix: UnitPrefix::Milli,
40 unit: Unit::Joule,
41};
42
43const BYTE_UNIT: MetricUnit = MetricUnit {
44 prefix: UnitPrefix::None,
45 unit: Unit::Byte,
46};
47
48const PERCENT_UNIT: MetricUnit = MetricUnit {
49 prefix: UnitPrefix::None,
50 unit: Unit::Percent,
51};
52
53bitflags! {
54 #[derive(Debug, Clone, Copy)]
56 struct DeviceSupport: u8 {
57 const Energy = 1;
58 const Power = 1 << 1;
59 const Vram = 1 << 2;
60 const Utilization = 1 << 3;
61 }
62}
63
64type Result<T> = std::result::Result<T, NvmlError>;
66
67type WorkerHandle = (CancellationToken, JoinHandle<Result<()>>);
69
70#[derive(Debug, Clone, Copy)]
71pub struct Device {
72 index: u32,
74
75 support: DeviceSupport,
77}
78
79#[allow(private_interfaces, private_bounds)]
85pub struct Nvml<H: NvmlHardware = NvmlWrapperHardware> {
86 config: NvmlConfig,
88
89 hardware: Arc<H>,
91
92 devices: Arc<Vec<Device>>,
94
95 handle: Option<WorkerHandle>,
97
98 energy_counters: HashMap<u32, EnergyCounter>,
100
101 vram_counters: Arc<Mutex<HashMap<u32, VramCounter>>>,
103
104 utilization_counters: Arc<Mutex<HashMap<u32, UtilizationCounter>>>,
106
107 power_counters: Arc<Mutex<HashMap<u32, PowerCounter>>>,
109}
110
111impl<H: NvmlHardware> Nvml<H> {
112 pub fn create_worker(
114 hardware: Arc<H>,
115 devices: Arc<Vec<Device>>,
116 power_counters: Arc<Mutex<HashMap<u32, PowerCounter>>>,
117 vram_counters: Arc<Mutex<HashMap<u32, VramCounter>>>,
118 utilization_counters: Arc<Mutex<HashMap<u32, UtilizationCounter>>>,
119 poll_interval: Duration,
120 ) -> Result<WorkerHandle> {
121 let mut ticker = Interval::new_interval(poll_interval)?;
122
123 let cancellation_token = CancellationToken::new();
124 let cancellation_token_clone = cancellation_token.clone();
125
126 let handle = tokio::spawn(async move {
127 debug!("Starting NVML source polling.");
128
129 loop {
130 tokio::select! {
131 _ = ticker.next() => {
132 trace!("Polled NVML source.");
133 Self::read_polled_counters(&hardware, &devices, &power_counters, &vram_counters, &utilization_counters).await?;
134 }
135
136 () = cancellation_token.cancelled() => {
137 debug!("NVML worker stopped.");
138 break;
139 }
140 }
141 }
142
143 Ok(())
144 });
145
146 Ok((cancellation_token_clone, handle))
147 }
148
149 async fn read_polled_counters(
155 hardware: &Arc<H>,
156 processors: &Arc<Vec<Device>>,
157 power_counters: &Arc<Mutex<HashMap<u32, PowerCounter>>>,
158 vram_counters: &Arc<Mutex<HashMap<u32, VramCounter>>>,
159 utilization_counters: &Arc<Mutex<HashMap<u32, UtilizationCounter>>>,
160 ) -> Result<()> {
161 let tasks = processors.iter().copied().map(|device| {
162 let hardware = hardware.clone();
163 spawn_blocking(move || {
164 let vram = device
165 .support
166 .contains(DeviceSupport::Vram)
167 .then(|| hardware.get_vram_usage(device));
168 let utilization = device
169 .support
170 .contains(DeviceSupport::Utilization)
171 .then(|| hardware.get_utilization(device));
172 let power = device
173 .support
174 .contains(DeviceSupport::Power)
175 .then(|| hardware.get_power(device));
176 (device.index, vram, utilization, power)
177 })
178 });
179
180 for (index, vram, utilization, power) in try_join_all(tasks).await? {
181 if let Some(vram) = vram {
182 let mut lock = vram_counters.lock().map_err(|_| NvmlError::MutexPoisoned)?;
183 lock.entry(index).or_default().update(vram?);
184 }
185
186 if let Some(utilization) = utilization {
187 let mut lock = utilization_counters
188 .lock()
189 .map_err(|_| NvmlError::MutexPoisoned)?;
190 lock.entry(index).or_default().update(utilization?);
191 }
192
193 if let Some(power) = power {
194 let mut lock = power_counters
195 .lock()
196 .map_err(|_| NvmlError::MutexPoisoned)?;
197 lock.entry(index).or_default().push(power?);
198 }
199 }
200
201 Ok(())
202 }
203}
204
205impl<H: NvmlHardware> MetricReader for Nvml<H> {
206 type Type = HashMap<u32, Counter>;
207
208 type Error = NvmlError;
209
210 type Config = NvmlConfig;
211
212 fn from_config(config: NvmlConfig) -> Result<Self> {
215 let mut hardware = H::new()?;
216 let devices = hardware.init_devices(config.gpus_spec.as_ref())?;
217
218 Ok(Self {
219 config,
220 hardware: Arc::new(hardware),
221 devices: Arc::new(devices),
222 handle: None,
223 energy_counters: HashMap::default(),
224 power_counters: Arc::default(),
225 utilization_counters: Arc::default(),
226 vram_counters: Arc::default(),
227 })
228 }
229
230 async fn measure(&mut self) -> Result<()> {
231 debug!("NVML measure triggered.");
232
233 let energy_future = try_join_all(
234 self.devices
235 .iter()
236 .filter(|device| device.support.contains(DeviceSupport::Energy))
237 .copied()
238 .map(|device| {
239 let hardware = self.hardware.clone();
240 spawn_blocking(move || {
241 let energy = hardware.get_energy(device);
242 (device.index, energy)
243 })
244 }),
245 )
246 .map_err(NvmlError::JoinError);
247
248 let polled_future = Self::read_polled_counters(
249 &self.hardware,
250 &self.devices,
251 &self.power_counters,
252 &self.vram_counters,
253 &self.utilization_counters,
254 );
255
256 let (energy_results, ()) = try_join!(energy_future, polled_future)?;
257
258 for (index, energy) in energy_results {
259 self.energy_counters
260 .entry(index)
261 .or_default()
262 .update(energy?);
263 }
264 Ok(())
265 }
266
267 async fn retrieve(&mut self) -> Result<Self::Type> {
269 debug!("Retrieving NVML counters.");
270 let mut energy_counters = self.energy_counters.clone();
271 for counter in self.energy_counters.values_mut() {
272 counter.reset();
273 }
274
275 let mut lock = self
276 .vram_counters
277 .lock()
278 .map_err(|_| NvmlError::MutexPoisoned)?;
279 let mut vram_counters = lock.clone();
280 for counter in lock.values_mut() {
281 counter.reset();
282 }
283
284 let mut lock = self
285 .utilization_counters
286 .lock()
287 .map_err(|_| NvmlError::MutexPoisoned)?;
288 let mut utilization_counters = lock.clone();
289 for counter in lock.values_mut() {
290 counter.reset();
291 }
292
293 let mut lock = self
294 .power_counters
295 .lock()
296 .map_err(|_| NvmlError::MutexPoisoned)?;
297 let mut power_counters = lock.clone();
298 for counter in lock.values_mut() {
299 counter.reset();
300 }
301
302 let map = self
303 .devices
304 .iter()
305 .map(|device| {
306 let energy = energy_counters.remove(&device.index);
307 let vram = vram_counters.remove(&device.index);
308 let utilization = utilization_counters.remove(&device.index);
309 let power = power_counters.remove(&device.index);
310 let counter = Counter {
311 energy,
312 vram,
313 utilization,
314 power,
315 };
316 (device.index, counter)
317 })
318 .collect();
319
320 Ok(map)
321 }
322
323 fn get_sensors(&self) -> Result<Sensors> {
324 Ok(self
325 .devices
326 .iter()
327 .flat_map(|device| {
328 vec![
329 Sensor::new(
330 format!("GPU-{}-energy", device.index),
331 MILLI_JOULE_UNIT,
332 Self::get_name(),
333 ),
334 Sensor::new(
335 format!("GPU-{}-vram_min", device.index),
336 BYTE_UNIT,
337 Self::get_name(),
338 ),
339 Sensor::new(
340 format!("GPU-{}-vram_max", device.index),
341 BYTE_UNIT,
342 Self::get_name(),
343 ),
344 Sensor::new(
345 format!("GPU-{}-utilization_min", device.index),
346 PERCENT_UNIT,
347 Self::get_name(),
348 ),
349 Sensor::new(
350 format!("GPU-{}-utilization_max", device.index),
351 PERCENT_UNIT,
352 Self::get_name(),
353 ),
354 ]
355 })
356 .collect())
357 }
358
359 fn to_metrics(&self, result: Self::Type) -> Result<Metrics> {
360 let metrics = result
361 .into_iter()
362 .flat_map(|(index, counter)| {
363 let mut processor_metrics = Vec::new();
364
365 let energy = counter
366 .energy
367 .map_or_else(|| counter.power.map(|c| c.compute_energy()), |c| c.diff());
368
369 if let Some(energy) = energy {
370 processor_metrics.push(Metric::new(
371 format!("GPU-{index}-energy"),
372 energy,
373 MILLI_JOULE_UNIT,
374 Self::get_name(),
375 ));
376 }
377
378 if let Some(vram) = counter.vram
379 && let Some(min) = vram.min
380 && let Some(max) = vram.max
381 {
382 processor_metrics.push(Metric::new(
383 format!("GPU-{index}-vram_min"),
384 min,
385 BYTE_UNIT,
386 Self::get_name(),
387 ));
388
389 processor_metrics.push(Metric::new(
390 format!("GPU-{index}-vram_max"),
391 max,
392 BYTE_UNIT,
393 Self::get_name(),
394 ));
395 }
396
397 if let Some(utilization) = counter.utilization
398 && let Some(min) = utilization.min
399 && let Some(max) = utilization.max
400 {
401 processor_metrics.push(Metric::new(
402 format!("GPU-{index}-utilization_min"),
403 u64::from(min),
404 PERCENT_UNIT,
405 Self::get_name(),
406 ));
407
408 processor_metrics.push(Metric::new(
409 format!("GPU-{index}-utilization_max"),
410 u64::from(max),
411 PERCENT_UNIT,
412 Self::get_name(),
413 ));
414 }
415
416 Ok::<Metrics, NvmlError>(processor_metrics)
417 })
418 .flatten()
419 .collect();
420 Ok(metrics)
421 }
422
423 async fn init(&mut self, _pid: i32) -> Result<()> {
425 self.handle = Some(Self::create_worker(
426 self.hardware.clone(),
427 self.devices.clone(),
428 self.power_counters.clone(),
429 self.vram_counters.clone(),
430 self.utilization_counters.clone(),
431 self.config.poll_interval,
432 )?);
433 debug!("NVML source initialized.");
434 Ok(())
435 }
436
437 async fn join(&mut self) -> Result<()> {
439 if let Some((cancellation_token, handle)) = self.handle.take() {
440 debug!("Joining NVML source polling task.");
441 cancellation_token.cancel();
442 handle.await??;
443 }
444 Ok(())
445 }
446
447 fn get_name() -> &'static str {
448 NVML_SOURCE_NAME
449 }
450
451 fn get_id() -> &'static str {
452 "nvml"
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use std::{collections::HashMap, sync::Arc, time::Duration};
459
460 use joule_profiler_core::source::MetricReader;
461 use mockall::predicate;
462 use tokio::time::sleep;
463
464 use crate::{
465 Device, DeviceSupport, Nvml,
466 config::NvmlConfig,
467 counters::{Counter, PowerMeasurement},
468 error::NvmlError,
469 hardware::MockNvmlHardware,
470 };
471
472 fn make_device(index: u32, support: DeviceSupport) -> Device {
473 Device { index, support }
474 }
475
476 fn build_nvml(
477 hardware: MockNvmlHardware,
478 devices: Vec<Device>,
479 poll_interval: Duration,
480 ) -> Nvml<MockNvmlHardware> {
481 Nvml {
482 config: NvmlConfig {
483 poll_interval,
484 gpus_spec: None,
485 },
486 hardware: Arc::new(hardware),
487 devices: Arc::new(devices),
488 handle: None,
489 energy_counters: HashMap::default(),
490 power_counters: Arc::default(),
491 utilization_counters: Arc::default(),
492 vram_counters: Arc::default(),
493 }
494 }
495
496 fn power_measurement(power: u32) -> PowerMeasurement {
497 PowerMeasurement {
498 timestamp: 0,
499 power,
500 }
501 }
502
503 #[tokio::test]
504 async fn measure_reads_energy_for_energy_capable_device() {
505 let device = make_device(0, DeviceSupport::Energy);
506
507 let mut hw = MockNvmlHardware::default();
508 hw.expect_get_energy()
509 .with(predicate::function(move |d: &Device| {
510 d.index == device.index
511 }))
512 .once()
513 .returning(|_| Ok(42_000));
514
515 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
516 nvml.measure().await.unwrap();
517
518 assert!(nvml.energy_counters.contains_key(&0));
519 }
520
521 #[tokio::test]
522 async fn measure_reads_power_for_power_only_device() {
523 let device = make_device(0, DeviceSupport::Power);
524
525 let mut hw = MockNvmlHardware::default();
526 hw.expect_get_power()
527 .with(predicate::function(move |d: &Device| {
528 d.index == device.index
529 }))
530 .once()
531 .returning(|_| Ok(power_measurement(150_000)));
532
533 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
534 nvml.measure().await.unwrap();
535
536 assert!(nvml.power_counters.lock().unwrap().contains_key(&0));
537 }
538
539 #[tokio::test]
540 async fn measure_reads_vram_for_vram_capable_device() {
541 let device = make_device(0, DeviceSupport::Vram);
542
543 let mut hw = MockNvmlHardware::default();
544 hw.expect_get_vram_usage()
545 .with(predicate::function(move |d: &Device| {
546 d.index == device.index
547 }))
548 .once()
549 .returning(|_| Ok(8_000_000_000));
550
551 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
552 nvml.measure().await.unwrap();
553
554 assert!(nvml.vram_counters.lock().unwrap().contains_key(&0));
555 }
556
557 #[tokio::test]
558 async fn measure_reads_utilization_for_utilization_capable_device() {
559 let device = make_device(0, DeviceSupport::Utilization);
560
561 let mut hw = MockNvmlHardware::default();
562 hw.expect_get_utilization()
563 .with(predicate::function(|d: &Device| d.index == 0))
564 .once()
565 .returning(|_| Ok(42));
566
567 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
568 nvml.measure().await.unwrap();
569
570 assert!(nvml.utilization_counters.lock().unwrap().contains_key(&0));
571 }
572
573 #[tokio::test]
574 async fn measure_skips_energy_and_power_for_vram_only_device() {
575 let device = make_device(0, DeviceSupport::Vram);
576
577 let mut hw = MockNvmlHardware::default();
578 hw.expect_get_energy().never();
579 hw.expect_get_power().never();
580 hw.expect_get_vram_usage()
581 .once()
582 .returning(|_| Ok(1_000_000));
583
584 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
585 nvml.measure().await.unwrap();
586
587 assert!(nvml.energy_counters.is_empty());
588 assert!(nvml.power_counters.lock().unwrap().is_empty());
589 }
590
591 #[tokio::test]
592 async fn measure_propagates_energy_error() {
593 let device = make_device(0, DeviceSupport::Energy);
594
595 let mut hw = MockNvmlHardware::default();
596 hw.expect_get_energy()
597 .once()
598 .returning(|_| Err(NvmlError::NoPermission));
599
600 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
601 assert!(nvml.measure().await.is_err());
602 }
603
604 #[tokio::test]
605 async fn measure_propagates_power_error() {
606 let device = make_device(0, DeviceSupport::Power);
607
608 let mut hw = MockNvmlHardware::default();
609 hw.expect_get_power()
610 .once()
611 .returning(|_| Err(NvmlError::NoPermission));
612
613 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
614 assert!(nvml.measure().await.is_err());
615 }
616
617 #[tokio::test]
618 async fn retrieve_returns_counters_and_resets_them() {
619 let device = make_device(0, DeviceSupport::Energy | DeviceSupport::Vram);
620
621 let mut hw = MockNvmlHardware::default();
622 hw.expect_get_energy().returning(|_| Ok(10_000));
623 hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
624
625 let mut nvml = build_nvml(hw, vec![device], Duration::from_secs(1));
626 nvml.measure().await.unwrap();
627 nvml.measure().await.unwrap();
628
629 let result = nvml.retrieve().await.unwrap();
630 assert!(result.contains_key(&0));
631
632 let result2 = nvml.retrieve().await.unwrap();
633 let counter: &Counter = result2.get(&0).unwrap();
634 assert!(
635 counter
636 .energy
637 .as_ref()
638 .and_then(super::counters::EnergyCounter::diff)
639 .is_none_or(|v| v == 0)
640 );
641 }
642
643 #[tokio::test]
644 async fn retrieve_includes_entry_for_every_device() {
645 let devices = vec![
646 make_device(0, DeviceSupport::Energy),
647 make_device(1, DeviceSupport::Power),
648 make_device(2, DeviceSupport::Vram),
649 ];
650
651 let mut hw = MockNvmlHardware::default();
652 hw.expect_get_energy().returning(|_| Ok(1_000));
653 hw.expect_get_power()
654 .returning(|_| Ok(power_measurement(5_000)));
655 hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
656
657 let mut nvml = build_nvml(hw, devices, Duration::from_secs(1));
658 nvml.measure().await.unwrap();
659
660 let result = nvml.retrieve().await.unwrap();
661 assert_eq!(result.len(), 3);
662 for index in [0, 1, 2] {
663 assert!(result.contains_key(&index));
664 }
665 }
666
667 #[tokio::test]
668 async fn worker_polls_counters_and_can_be_cancelled() {
669 let device = make_device(
670 0,
671 DeviceSupport::Power | DeviceSupport::Vram | DeviceSupport::Utilization,
672 );
673
674 let mut hw = MockNvmlHardware::default();
675 hw.expect_get_power()
676 .returning(|_| Ok(power_measurement(5_000)));
677 hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
678 hw.expect_get_utilization().returning(|_| Ok(50));
679
680 let mut nvml = build_nvml(hw, vec![device], Duration::from_millis(20));
681 nvml.init(0).await.unwrap();
682
683 sleep(Duration::from_millis(80)).await;
684
685 nvml.join().await.unwrap();
686
687 assert!(nvml.power_counters.lock().unwrap().contains_key(&0));
688 }
689}