1use std::collections::HashMap;
27use std::fmt;
28use std::time::Instant;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum SmVersion {
41 Sm75,
43 Sm80,
45 Sm86,
47 Sm89,
49 Sm90,
51 Sm100,
53 Sm120,
55}
56
57impl SmVersion {
58 #[must_use]
65 pub const fn max_warps_per_sm(self) -> u32 {
66 match self {
67 Self::Sm75 => 32,
68 Self::Sm86 | Self::Sm89 | Self::Sm120 => 48,
69 Self::Sm80 | Self::Sm90 | Self::Sm100 => 64,
70 }
71 }
72
73 #[must_use]
75 pub const fn max_blocks_per_sm(self) -> u32 {
76 match self {
77 Self::Sm75 | Self::Sm86 => 16,
78 Self::Sm89 | Self::Sm120 => 24,
79 Self::Sm80 | Self::Sm90 | Self::Sm100 => 32,
80 }
81 }
82
83 #[must_use]
85 pub const fn registers_per_sm(self) -> u32 {
86 65536
87 }
88
89 #[must_use]
91 pub const fn max_registers_per_thread(self) -> u32 {
92 255
93 }
94
95 #[must_use]
101 pub const fn max_shared_mem_per_sm(self) -> u32 {
102 match self {
103 Self::Sm75 => 65_536,
104 Self::Sm80 => 167_936,
105 Self::Sm86 | Self::Sm89 => 102_400,
106 Self::Sm90 | Self::Sm100 | Self::Sm120 => 232_448,
107 }
108 }
109
110 #[must_use]
112 pub const fn warp_size(self) -> u32 {
113 32
114 }
115
116 #[must_use]
121 pub const fn register_alloc_granularity(self) -> u32 {
122 8
126 }
127
128 #[must_use]
130 pub const fn shared_mem_alloc_granularity(self) -> u32 {
131 match self {
132 Self::Sm75 | Self::Sm80 | Self::Sm86 | Self::Sm89 => 256,
133 Self::Sm90 | Self::Sm100 | Self::Sm120 => 128,
134 }
135 }
136}
137
138impl fmt::Display for SmVersion {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 let s = match self {
141 Self::Sm75 => "sm_75",
142 Self::Sm80 => "sm_80",
143 Self::Sm86 => "sm_86",
144 Self::Sm89 => "sm_89",
145 Self::Sm90 => "sm_90",
146 Self::Sm100 => "sm_100",
147 Self::Sm120 => "sm_120",
148 };
149 f.write_str(s)
150 }
151}
152
153#[derive(Debug, Clone)]
163pub struct LaunchTelemetry {
164 pub kernel_name: String,
166 pub grid_dim: (u32, u32, u32),
168 pub block_dim: (u32, u32, u32),
170 pub shared_memory_bytes: u32,
172 pub register_count: Option<u32>,
174 pub elapsed_ms: Option<f64>,
176 pub achieved_occupancy: Option<f64>,
178 pub theoretical_occupancy: Option<f64>,
180 pub timestamp: Instant,
182}
183
184impl LaunchTelemetry {
185 #[must_use]
190 pub fn new(kernel_name: &str, grid_dim: (u32, u32, u32), block_dim: (u32, u32, u32)) -> Self {
191 Self {
192 kernel_name: kernel_name.to_owned(),
193 grid_dim,
194 block_dim,
195 shared_memory_bytes: 0,
196 register_count: None,
197 elapsed_ms: None,
198 achieved_occupancy: None,
199 theoretical_occupancy: None,
200 timestamp: Instant::now(),
201 }
202 }
203
204 #[must_use]
206 pub fn with_shared_memory(mut self, bytes: u32) -> Self {
207 self.shared_memory_bytes = bytes;
208 self
209 }
210
211 #[must_use]
213 pub fn with_register_count(mut self, count: u32) -> Self {
214 self.register_count = Some(count);
215 self
216 }
217
218 #[must_use]
220 pub fn with_elapsed_ms(mut self, ms: f64) -> Self {
221 self.elapsed_ms = Some(ms);
222 self
223 }
224
225 #[must_use]
227 pub fn with_achieved_occupancy(mut self, occ: f64) -> Self {
228 self.achieved_occupancy = Some(occ);
229 self
230 }
231
232 #[must_use]
234 pub fn with_theoretical_occupancy(mut self, occ: f64) -> Self {
235 self.theoretical_occupancy = Some(occ);
236 self
237 }
238
239 #[must_use]
241 pub fn total_threads(&self) -> u64 {
242 let grid_total = self.grid_dim.0 as u64 * self.grid_dim.1 as u64 * self.grid_dim.2 as u64;
243 let block_total =
244 self.block_dim.0 as u64 * self.block_dim.1 as u64 * self.block_dim.2 as u64;
245 grid_total * block_total
246 }
247}
248
249impl fmt::Display for LaunchTelemetry {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 write!(
252 f,
253 "Kernel '{}': grid=({},{},{}), block=({},{},{}), smem={}B",
254 self.kernel_name,
255 self.grid_dim.0,
256 self.grid_dim.1,
257 self.grid_dim.2,
258 self.block_dim.0,
259 self.block_dim.1,
260 self.block_dim.2,
261 self.shared_memory_bytes,
262 )?;
263 if let Some(regs) = self.register_count {
264 write!(f, ", regs={regs}")?;
265 }
266 if let Some(ms) = self.elapsed_ms {
267 write!(f, ", time={ms:.3}ms")?;
268 }
269 if let Some(occ) = self.achieved_occupancy {
270 write!(f, ", occupancy={:.1}%", occ * 100.0)?;
271 }
272 Ok(())
273 }
274}
275
276#[derive(Debug, Clone)]
282pub struct KernelStats {
283 pub kernel_name: String,
285 pub launch_count: u32,
287 pub total_time_ms: f64,
289 pub avg_time_ms: f64,
291 pub min_time_ms: f64,
293 pub max_time_ms: f64,
295 pub avg_occupancy: f64,
297}
298
299impl fmt::Display for KernelStats {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(
302 f,
303 "{}: {} launches, total={:.3}ms, avg={:.3}ms, min={:.3}ms, max={:.3}ms, occ={:.1}%",
304 self.kernel_name,
305 self.launch_count,
306 self.total_time_ms,
307 self.avg_time_ms,
308 self.min_time_ms,
309 self.max_time_ms,
310 self.avg_occupancy * 100.0,
311 )
312 }
313}
314
315#[derive(Debug, Clone)]
324pub struct TelemetrySummary {
325 pub total_launches: usize,
327 pub total_gpu_time_ms: f64,
329 pub avg_gpu_time_ms: f64,
331 pub min_gpu_time_ms: f64,
333 pub max_gpu_time_ms: f64,
335 pub avg_occupancy: f64,
337 pub hottest_kernel: Option<String>,
339 pub per_kernel_stats: Vec<KernelStats>,
341}
342
343impl fmt::Display for TelemetrySummary {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 writeln!(f, "=== Telemetry Summary ===")?;
346 writeln!(f, "Total launches: {}", self.total_launches)?;
347 writeln!(f, "Total GPU time: {:.3} ms", self.total_gpu_time_ms)?;
348 writeln!(f, "Avg GPU time: {:.3} ms", self.avg_gpu_time_ms)?;
349 writeln!(f, "Min GPU time: {:.3} ms", self.min_gpu_time_ms)?;
350 writeln!(f, "Max GPU time: {:.3} ms", self.max_gpu_time_ms)?;
351 writeln!(f, "Avg occupancy: {:.1}%", self.avg_occupancy * 100.0)?;
352 if let Some(ref hot) = self.hottest_kernel {
353 writeln!(f, "Hottest kernel: {hot}")?;
354 }
355 if !self.per_kernel_stats.is_empty() {
356 writeln!(f, "--- Per-kernel ---")?;
357 for ks in &self.per_kernel_stats {
358 writeln!(f, " {ks}")?;
359 }
360 }
361 Ok(())
362 }
363}
364
365#[derive(Debug)]
374pub struct TelemetryCollector {
375 entries: Vec<LaunchTelemetry>,
376 enabled: bool,
377 max_entries: usize,
378}
379
380impl TelemetryCollector {
381 #[must_use]
383 pub fn new(max_entries: usize) -> Self {
384 Self {
385 entries: Vec::new(),
386 enabled: true,
387 max_entries,
388 }
389 }
390
391 pub fn record(&mut self, telemetry: LaunchTelemetry) {
396 if !self.enabled {
397 return;
398 }
399 if self.entries.len() >= self.max_entries {
400 return;
401 }
402 self.entries.push(telemetry);
403 }
404
405 pub fn enable(&mut self) {
407 self.enabled = true;
408 }
409
410 pub fn disable(&mut self) {
412 self.enabled = false;
413 }
414
415 #[must_use]
417 pub fn is_enabled(&self) -> bool {
418 self.enabled
419 }
420
421 pub fn clear(&mut self) {
423 self.entries.clear();
424 }
425
426 #[must_use]
428 pub fn entries(&self) -> &[LaunchTelemetry] {
429 &self.entries
430 }
431
432 #[must_use]
434 pub fn len(&self) -> usize {
435 self.entries.len()
436 }
437
438 #[must_use]
440 pub fn is_empty(&self) -> bool {
441 self.entries.is_empty()
442 }
443
444 #[must_use]
448 pub fn summary(&self) -> TelemetrySummary {
449 compute_summary(&self.entries)
450 }
451}
452
453fn compute_summary(entries: &[LaunchTelemetry]) -> TelemetrySummary {
455 if entries.is_empty() {
456 return TelemetrySummary {
457 total_launches: 0,
458 total_gpu_time_ms: 0.0,
459 avg_gpu_time_ms: 0.0,
460 min_gpu_time_ms: 0.0,
461 max_gpu_time_ms: 0.0,
462 avg_occupancy: 0.0,
463 hottest_kernel: None,
464 per_kernel_stats: Vec::new(),
465 };
466 }
467
468 let mut total_time = 0.0_f64;
469 let mut min_time = f64::MAX;
470 let mut max_time = f64::MIN;
471 let mut time_count = 0usize;
472 let mut total_occ = 0.0_f64;
473 let mut occ_count = 0usize;
474
475 struct KernelAccum {
477 count: u32,
478 total_time: f64,
479 min_time: f64,
480 max_time: f64,
481 total_occ: f64,
482 occ_count: u32,
483 }
484
485 let mut per_kernel: HashMap<String, KernelAccum> = HashMap::new();
486
487 for entry in entries {
488 if let Some(ms) = entry.elapsed_ms {
489 total_time += ms;
490 if ms < min_time {
491 min_time = ms;
492 }
493 if ms > max_time {
494 max_time = ms;
495 }
496 time_count += 1;
497 }
498 if let Some(occ) = entry.achieved_occupancy {
499 total_occ += occ;
500 occ_count += 1;
501 }
502
503 let acc = per_kernel
504 .entry(entry.kernel_name.clone())
505 .or_insert(KernelAccum {
506 count: 0,
507 total_time: 0.0,
508 min_time: f64::MAX,
509 max_time: f64::MIN,
510 total_occ: 0.0,
511 occ_count: 0,
512 });
513 acc.count += 1;
514 if let Some(ms) = entry.elapsed_ms {
515 acc.total_time += ms;
516 if ms < acc.min_time {
517 acc.min_time = ms;
518 }
519 if ms > acc.max_time {
520 acc.max_time = ms;
521 }
522 }
523 if let Some(occ) = entry.achieved_occupancy {
524 acc.total_occ += occ;
525 acc.occ_count += 1;
526 }
527 }
528
529 if time_count == 0 {
531 min_time = 0.0;
532 max_time = 0.0;
533 }
534
535 let mut per_kernel_stats: Vec<KernelStats> = per_kernel
537 .into_iter()
538 .map(|(name, acc)| {
539 let min_t = if acc.min_time == f64::MAX {
540 0.0
541 } else {
542 acc.min_time
543 };
544 let max_t = if acc.max_time == f64::MIN {
545 0.0
546 } else {
547 acc.max_time
548 };
549 let avg_t = if acc.count > 0 {
550 acc.total_time / f64::from(acc.count)
551 } else {
552 0.0
553 };
554 let avg_o = if acc.occ_count > 0 {
555 acc.total_occ / f64::from(acc.occ_count)
556 } else {
557 0.0
558 };
559 KernelStats {
560 kernel_name: name,
561 launch_count: acc.count,
562 total_time_ms: acc.total_time,
563 avg_time_ms: avg_t,
564 min_time_ms: min_t,
565 max_time_ms: max_t,
566 avg_occupancy: avg_o,
567 }
568 })
569 .collect();
570
571 per_kernel_stats.sort_by(|a, b| {
573 b.total_time_ms
574 .partial_cmp(&a.total_time_ms)
575 .unwrap_or(std::cmp::Ordering::Equal)
576 });
577
578 let hottest_kernel = per_kernel_stats.first().map(|ks| ks.kernel_name.clone());
579
580 let avg_gpu_time = if time_count > 0 {
581 total_time / time_count as f64
582 } else {
583 0.0
584 };
585 let avg_occ = if occ_count > 0 {
586 total_occ / occ_count as f64
587 } else {
588 0.0
589 };
590
591 TelemetrySummary {
592 total_launches: entries.len(),
593 total_gpu_time_ms: total_time,
594 avg_gpu_time_ms: avg_gpu_time,
595 min_gpu_time_ms: min_time,
596 max_gpu_time_ms: max_time,
597 avg_occupancy: avg_occ,
598 hottest_kernel,
599 per_kernel_stats,
600 }
601}
602
603pub struct TelemetryExporter;
611
612impl TelemetryExporter {
613 #[must_use]
618 pub fn to_json(entries: &[LaunchTelemetry]) -> String {
619 let mut out = String::from("[\n");
620 for (i, e) in entries.iter().enumerate() {
621 out.push_str(" {\n");
622 json_field_str(&mut out, "kernel_name", &e.kernel_name);
623 out.push_str(&format!(
624 " \"grid_dim\": [{}, {}, {}],\n",
625 e.grid_dim.0, e.grid_dim.1, e.grid_dim.2
626 ));
627 out.push_str(&format!(
628 " \"block_dim\": [{}, {}, {}],\n",
629 e.block_dim.0, e.block_dim.1, e.block_dim.2
630 ));
631 out.push_str(&format!(
632 " \"shared_memory_bytes\": {},\n",
633 e.shared_memory_bytes
634 ));
635 json_field_opt_u32(&mut out, "register_count", e.register_count);
636 json_field_opt_f64(&mut out, "elapsed_ms", e.elapsed_ms);
637 json_field_opt_f64(&mut out, "achieved_occupancy", e.achieved_occupancy);
638 json_field_opt_f64_last(&mut out, "theoretical_occupancy", e.theoretical_occupancy);
639 out.push_str(" }");
640 if i + 1 < entries.len() {
641 out.push(',');
642 }
643 out.push('\n');
644 }
645 out.push(']');
646 out
647 }
648
649 #[must_use]
653 pub fn to_csv(entries: &[LaunchTelemetry]) -> String {
654 let mut out = String::from(
655 "kernel_name,grid_x,grid_y,grid_z,block_x,block_y,block_z,\
656 shared_memory_bytes,register_count,elapsed_ms,\
657 achieved_occupancy,theoretical_occupancy\n",
658 );
659 for e in entries {
660 out.push_str(&csv_escape(&e.kernel_name));
661 out.push(',');
662 out.push_str(&format!(
663 "{},{},{},{},{},{},{},",
664 e.grid_dim.0,
665 e.grid_dim.1,
666 e.grid_dim.2,
667 e.block_dim.0,
668 e.block_dim.1,
669 e.block_dim.2,
670 e.shared_memory_bytes,
671 ));
672 csv_opt_u32(&mut out, e.register_count);
673 out.push(',');
674 csv_opt_f64(&mut out, e.elapsed_ms);
675 out.push(',');
676 csv_opt_f64(&mut out, e.achieved_occupancy);
677 out.push(',');
678 csv_opt_f64(&mut out, e.theoretical_occupancy);
679 out.push('\n');
680 }
681 out
682 }
683
684 #[must_use]
689 pub fn to_chrome_trace(entries: &[LaunchTelemetry]) -> String {
690 let mut out = String::from("{\"traceEvents\":[\n");
691 let mut ts_us = 0.0_f64; for (i, e) in entries.iter().enumerate() {
693 let dur_us = e.elapsed_ms.unwrap_or(0.0) * 1000.0;
694 out.push_str(&format!(
695 " {{\"name\":\"{}\",\"cat\":\"gpu\",\"ph\":\"X\",\
696 \"ts\":{:.3},\"dur\":{:.3},\"pid\":1,\"tid\":1,\
697 \"args\":{{\"grid\":\"{},{},{}\",\"block\":\"{},{},{}\",\
698 \"smem\":{}",
699 json_escape_str(&e.kernel_name),
700 ts_us,
701 dur_us,
702 e.grid_dim.0,
703 e.grid_dim.1,
704 e.grid_dim.2,
705 e.block_dim.0,
706 e.block_dim.1,
707 e.block_dim.2,
708 e.shared_memory_bytes,
709 ));
710 if let Some(regs) = e.register_count {
711 out.push_str(&format!(",\"regs\":{regs}"));
712 }
713 if let Some(occ) = e.achieved_occupancy {
714 out.push_str(&format!(",\"occupancy\":{occ:.4}"));
715 }
716 out.push_str("}}");
717 if i + 1 < entries.len() {
718 out.push(',');
719 }
720 out.push('\n');
721 ts_us += dur_us;
722 }
723 out.push_str("]}\n");
724 out
725 }
726}
727
728fn json_escape_str(s: &str) -> String {
733 s.replace('\\', "\\\\")
734 .replace('"', "\\\"")
735 .replace('\n', "\\n")
736 .replace('\r', "\\r")
737 .replace('\t', "\\t")
738}
739
740fn json_field_str(out: &mut String, key: &str, val: &str) {
741 out.push_str(&format!(" \"{key}\": \"{}\",\n", json_escape_str(val)));
742}
743
744fn json_field_opt_u32(out: &mut String, key: &str, val: Option<u32>) {
745 match val {
746 Some(v) => out.push_str(&format!(" \"{key}\": {v},\n")),
747 None => out.push_str(&format!(" \"{key}\": null,\n")),
748 }
749}
750
751fn json_field_opt_f64(out: &mut String, key: &str, val: Option<f64>) {
752 match val {
753 Some(v) => out.push_str(&format!(" \"{key}\": {v},\n")),
754 None => out.push_str(&format!(" \"{key}\": null,\n")),
755 }
756}
757
758fn json_field_opt_f64_last(out: &mut String, key: &str, val: Option<f64>) {
759 match val {
760 Some(v) => out.push_str(&format!(" \"{key}\": {v}\n")),
761 None => out.push_str(&format!(" \"{key}\": null\n")),
762 }
763}
764
765fn csv_escape(s: &str) -> String {
766 if s.contains(',') || s.contains('"') || s.contains('\n') {
767 format!("\"{}\"", s.replace('"', "\"\""))
768 } else {
769 s.to_owned()
770 }
771}
772
773fn csv_opt_u32(out: &mut String, val: Option<u32>) {
774 if let Some(v) = val {
775 out.push_str(&v.to_string());
776 }
777}
778
779fn csv_opt_f64(out: &mut String, val: Option<f64>) {
780 if let Some(v) = val {
781 out.push_str(&format!("{v}"));
782 }
783}
784
785#[must_use]
810pub fn estimate_occupancy(
811 block_size: u32,
812 registers_per_thread: u32,
813 shared_mem: u32,
814 sm_version: SmVersion,
815) -> f64 {
816 if block_size == 0 {
817 return 0.0;
818 }
819
820 let warp_size = sm_version.warp_size();
821 let max_warps = sm_version.max_warps_per_sm();
822 let max_blocks = sm_version.max_blocks_per_sm();
823 let regs_per_sm = sm_version.registers_per_sm();
824 let max_smem = sm_version.max_shared_mem_per_sm();
825 let reg_granularity = sm_version.register_alloc_granularity();
826 let smem_granularity = sm_version.shared_mem_alloc_granularity();
827
828 let warps_per_block = block_size.div_ceil(warp_size);
830
831 let regs_per_thread = if registers_per_thread == 0 {
833 1 } else {
835 registers_per_thread
836 };
837 let regs_alloc = regs_per_thread.div_ceil(reg_granularity) * reg_granularity;
839 let regs_per_warp = regs_alloc * warp_size;
840 let warps_limited_by_regs = regs_per_sm
848 .checked_div(regs_per_warp)
849 .unwrap_or(max_warps)
850 .min(max_warps);
851
852 let smem_per_block = if shared_mem == 0 {
854 0
855 } else {
856 shared_mem.div_ceil(smem_granularity) * smem_granularity
857 };
858 let blocks_limited_by_smem = max_smem.checked_div(smem_per_block).unwrap_or(max_blocks);
859
860 let blocks_by_warps = warps_limited_by_regs
862 .checked_div(warps_per_block)
863 .unwrap_or(max_blocks);
864
865 let active_blocks = max_blocks.min(blocks_by_warps).min(blocks_limited_by_smem);
866
867 let active_warps = active_blocks * warps_per_block;
868 let occupancy = active_warps as f64 / max_warps as f64;
869
870 occupancy.clamp(0.0, 1.0)
871}
872
873#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
884 fn telemetry_new_defaults() {
885 let t = LaunchTelemetry::new("kern", (4, 1, 1), (256, 1, 1));
886 assert_eq!(t.kernel_name, "kern");
887 assert_eq!(t.grid_dim, (4, 1, 1));
888 assert_eq!(t.block_dim, (256, 1, 1));
889 assert_eq!(t.shared_memory_bytes, 0);
890 assert!(t.register_count.is_none());
891 assert!(t.elapsed_ms.is_none());
892 assert!(t.achieved_occupancy.is_none());
893 assert!(t.theoretical_occupancy.is_none());
894 }
895
896 #[test]
897 fn telemetry_builder_methods() {
898 let t = LaunchTelemetry::new("kern", (1, 1, 1), (128, 1, 1))
899 .with_shared_memory(4096)
900 .with_register_count(32)
901 .with_elapsed_ms(1.5)
902 .with_achieved_occupancy(0.75)
903 .with_theoretical_occupancy(0.80);
904
905 assert_eq!(t.shared_memory_bytes, 4096);
906 assert_eq!(t.register_count, Some(32));
907 assert!((t.elapsed_ms.unwrap_or(0.0) - 1.5).abs() < f64::EPSILON);
908 assert!((t.achieved_occupancy.unwrap_or(0.0) - 0.75).abs() < f64::EPSILON);
909 assert!((t.theoretical_occupancy.unwrap_or(0.0) - 0.80).abs() < f64::EPSILON);
910 }
911
912 #[test]
913 fn telemetry_total_threads() {
914 let t = LaunchTelemetry::new("k", (4, 2, 1), (16, 16, 1));
915 assert_eq!(t.total_threads(), 4 * 2 * 16 * 16);
916 }
917
918 #[test]
919 fn telemetry_display() {
920 let t = LaunchTelemetry::new("add", (4, 1, 1), (256, 1, 1))
921 .with_elapsed_ms(0.5)
922 .with_register_count(24)
923 .with_achieved_occupancy(0.85);
924 let s = format!("{t}");
925 assert!(s.contains("add"));
926 assert!(s.contains("0.500ms"));
927 assert!(s.contains("regs=24"));
928 assert!(s.contains("85.0%"));
929 }
930
931 #[test]
934 fn collector_record_and_len() {
935 let mut c = TelemetryCollector::new(100);
936 assert!(c.is_empty());
937 c.record(LaunchTelemetry::new("k", (1, 1, 1), (64, 1, 1)));
938 assert_eq!(c.len(), 1);
939 assert!(!c.is_empty());
940 }
941
942 #[test]
943 fn collector_enable_disable() {
944 let mut c = TelemetryCollector::new(100);
945 assert!(c.is_enabled());
946
947 c.disable();
948 assert!(!c.is_enabled());
949 c.record(LaunchTelemetry::new("k", (1, 1, 1), (64, 1, 1)));
950 assert_eq!(c.len(), 0); c.enable();
953 c.record(LaunchTelemetry::new("k", (1, 1, 1), (64, 1, 1)));
954 assert_eq!(c.len(), 1);
955 }
956
957 #[test]
958 fn collector_max_entries_cap() {
959 let mut c = TelemetryCollector::new(3);
960 for _ in 0..10 {
961 c.record(LaunchTelemetry::new("k", (1, 1, 1), (64, 1, 1)));
962 }
963 assert_eq!(c.len(), 3);
964 }
965
966 #[test]
967 fn collector_clear() {
968 let mut c = TelemetryCollector::new(100);
969 c.record(LaunchTelemetry::new("k", (1, 1, 1), (64, 1, 1)));
970 c.clear();
971 assert!(c.is_empty());
972 }
973
974 #[test]
977 fn summary_empty() {
978 let c = TelemetryCollector::new(100);
979 let s = c.summary();
980 assert_eq!(s.total_launches, 0);
981 assert!((s.total_gpu_time_ms).abs() < f64::EPSILON);
982 assert!(s.hottest_kernel.is_none());
983 assert!(s.per_kernel_stats.is_empty());
984 }
985
986 #[test]
987 fn summary_single_kernel() {
988 let mut c = TelemetryCollector::new(100);
989 c.record(
990 LaunchTelemetry::new("add", (1, 1, 1), (256, 1, 1))
991 .with_elapsed_ms(1.0)
992 .with_achieved_occupancy(0.8),
993 );
994 c.record(
995 LaunchTelemetry::new("add", (1, 1, 1), (256, 1, 1))
996 .with_elapsed_ms(3.0)
997 .with_achieved_occupancy(0.9),
998 );
999 let s = c.summary();
1000 assert_eq!(s.total_launches, 2);
1001 assert!((s.total_gpu_time_ms - 4.0).abs() < f64::EPSILON);
1002 assert!((s.avg_gpu_time_ms - 2.0).abs() < f64::EPSILON);
1003 assert!((s.min_gpu_time_ms - 1.0).abs() < f64::EPSILON);
1004 assert!((s.max_gpu_time_ms - 3.0).abs() < f64::EPSILON);
1005 assert!((s.avg_occupancy - 0.85).abs() < 1e-9);
1006 assert_eq!(s.hottest_kernel.as_deref(), Some("add"));
1007 assert_eq!(s.per_kernel_stats.len(), 1);
1008 assert_eq!(s.per_kernel_stats[0].launch_count, 2);
1009 }
1010
1011 #[test]
1012 fn summary_per_kernel_aggregation() {
1013 let mut c = TelemetryCollector::new(100);
1014 c.record(LaunchTelemetry::new("matmul", (1, 1, 1), (256, 1, 1)).with_elapsed_ms(10.0));
1015 c.record(LaunchTelemetry::new("add", (1, 1, 1), (128, 1, 1)).with_elapsed_ms(1.0));
1016 c.record(LaunchTelemetry::new("matmul", (1, 1, 1), (256, 1, 1)).with_elapsed_ms(12.0));
1017
1018 let s = c.summary();
1019 assert_eq!(s.total_launches, 3);
1020 assert_eq!(s.hottest_kernel.as_deref(), Some("matmul"));
1022 assert_eq!(s.per_kernel_stats.len(), 2);
1023 assert_eq!(s.per_kernel_stats[0].kernel_name, "matmul");
1025 assert_eq!(s.per_kernel_stats[0].launch_count, 2);
1026 assert!((s.per_kernel_stats[0].total_time_ms - 22.0).abs() < f64::EPSILON);
1027 }
1028
1029 #[test]
1030 fn summary_display() {
1031 let mut c = TelemetryCollector::new(100);
1032 c.record(
1033 LaunchTelemetry::new("k", (1, 1, 1), (256, 1, 1))
1034 .with_elapsed_ms(2.0)
1035 .with_achieved_occupancy(0.5),
1036 );
1037 let s = c.summary();
1038 let text = format!("{s}");
1039 assert!(text.contains("Telemetry Summary"));
1040 assert!(text.contains("Total launches: 1"));
1041 assert!(text.contains("50.0%"));
1042 }
1043
1044 #[test]
1047 fn export_json() {
1048 let entries = vec![
1049 LaunchTelemetry::new("kern", (4, 1, 1), (256, 1, 1))
1050 .with_elapsed_ms(0.5)
1051 .with_register_count(32),
1052 ];
1053 let json = TelemetryExporter::to_json(&entries);
1054 assert!(json.starts_with('['));
1055 assert!(json.contains("\"kernel_name\": \"kern\""));
1056 assert!(json.contains("\"grid_dim\": [4, 1, 1]"));
1057 assert!(json.contains("\"elapsed_ms\": 0.5"));
1058 assert!(json.contains("\"register_count\": 32"));
1059 assert!(json.contains("\"achieved_occupancy\": null"));
1060 }
1061
1062 #[test]
1065 fn export_csv() {
1066 let entries =
1067 vec![LaunchTelemetry::new("kern", (2, 1, 1), (128, 1, 1)).with_elapsed_ms(1.0)];
1068 let csv = TelemetryExporter::to_csv(&entries);
1069 let lines: Vec<&str> = csv.lines().collect();
1070 assert_eq!(lines.len(), 2); assert!(lines[0].starts_with("kernel_name,"));
1072 assert!(lines[1].starts_with("kern,"));
1073 assert!(lines[1].contains("128"));
1074 }
1075
1076 #[test]
1079 fn export_chrome_trace() {
1080 let entries = vec![
1081 LaunchTelemetry::new("k1", (1, 1, 1), (256, 1, 1)).with_elapsed_ms(1.0),
1082 LaunchTelemetry::new("k2", (2, 1, 1), (128, 1, 1)).with_elapsed_ms(2.0),
1083 ];
1084 let trace = TelemetryExporter::to_chrome_trace(&entries);
1085 assert!(trace.contains("\"traceEvents\""));
1086 assert!(trace.contains("\"name\":\"k1\""));
1087 assert!(trace.contains("\"name\":\"k2\""));
1088 assert!(trace.contains("\"ph\":\"X\""));
1089 assert!(trace.contains("\"cat\":\"gpu\""));
1090 }
1091
1092 #[test]
1095 fn occupancy_basic() {
1096 let occ = estimate_occupancy(256, 32, 0, SmVersion::Sm80);
1097 assert!(occ > 0.0);
1098 assert!(occ <= 1.0);
1099 }
1100
1101 #[test]
1102 fn occupancy_zero_block() {
1103 let occ = estimate_occupancy(0, 32, 0, SmVersion::Sm80);
1104 assert!((occ).abs() < f64::EPSILON);
1105 }
1106
1107 #[test]
1108 fn occupancy_high_registers_lowers_occupancy() {
1109 let high_reg = estimate_occupancy(256, 128, 0, SmVersion::Sm80);
1110 let low_reg = estimate_occupancy(256, 16, 0, SmVersion::Sm80);
1111 assert!(high_reg < low_reg);
1112 }
1113
1114 #[test]
1115 fn occupancy_large_shared_mem_lowers_occupancy() {
1116 let large_smem = estimate_occupancy(256, 32, 100_000, SmVersion::Sm80);
1117 let small_smem = estimate_occupancy(256, 32, 0, SmVersion::Sm80);
1118 assert!(large_smem <= small_smem);
1119 }
1120
1121 #[test]
1122 fn occupancy_sm_versions() {
1123 for sm in [
1124 SmVersion::Sm75,
1125 SmVersion::Sm80,
1126 SmVersion::Sm86,
1127 SmVersion::Sm89,
1128 SmVersion::Sm90,
1129 SmVersion::Sm100,
1130 SmVersion::Sm120,
1131 ] {
1132 let occ = estimate_occupancy(128, 32, 0, sm);
1133 assert!(occ > 0.0, "occupancy should be positive for {sm}");
1134 assert!(occ <= 1.0, "occupancy should be <= 1.0 for {sm}");
1135 }
1136 }
1137
1138 #[test]
1141 fn sm_version_display() {
1142 assert_eq!(format!("{}", SmVersion::Sm80), "sm_80");
1143 assert_eq!(format!("{}", SmVersion::Sm90), "sm_90");
1144 }
1145
1146 #[test]
1151 fn sm_version_max_warps_matches_real_hardware() {
1152 assert_eq!(SmVersion::Sm75.max_warps_per_sm(), 32);
1157 assert_eq!(SmVersion::Sm80.max_warps_per_sm(), 64);
1158 assert_eq!(SmVersion::Sm86.max_warps_per_sm(), 48);
1159 assert_eq!(SmVersion::Sm89.max_warps_per_sm(), 48);
1160 assert_eq!(SmVersion::Sm90.max_warps_per_sm(), 64);
1161 assert_eq!(SmVersion::Sm100.max_warps_per_sm(), 64);
1162 assert_eq!(SmVersion::Sm120.max_warps_per_sm(), 48);
1163 }
1164
1165 #[test]
1166 fn sm_version_max_blocks_matches_real_hardware() {
1167 assert_eq!(SmVersion::Sm75.max_blocks_per_sm(), 16);
1168 assert_eq!(SmVersion::Sm80.max_blocks_per_sm(), 32);
1169 assert_eq!(SmVersion::Sm86.max_blocks_per_sm(), 16);
1170 assert_eq!(SmVersion::Sm89.max_blocks_per_sm(), 24);
1171 assert_eq!(SmVersion::Sm90.max_blocks_per_sm(), 32);
1172 assert_eq!(SmVersion::Sm100.max_blocks_per_sm(), 32);
1173 assert_eq!(SmVersion::Sm120.max_blocks_per_sm(), 24);
1174 }
1175
1176 #[test]
1177 fn sm_version_max_shared_mem_matches_real_hardware() {
1178 assert_eq!(SmVersion::Sm75.max_shared_mem_per_sm(), 65_536);
1182 assert_eq!(SmVersion::Sm80.max_shared_mem_per_sm(), 167_936);
1183 assert_eq!(SmVersion::Sm86.max_shared_mem_per_sm(), 102_400);
1184 assert_eq!(SmVersion::Sm89.max_shared_mem_per_sm(), 102_400);
1185 assert_eq!(SmVersion::Sm90.max_shared_mem_per_sm(), 232_448);
1186 }
1187
1188 #[test]
1189 fn estimate_occupancy_caps_register_derived_warps_at_hardware_max() {
1190 let occ = estimate_occupancy(224, 1, 0, SmVersion::Sm86);
1201 assert!(
1202 occ < 1.0,
1203 "occupancy must reflect the true SM warp-residency ceiling \
1204 instead of a register-derived overcount masked by clamping, got {occ}"
1205 );
1206 assert!(
1207 (occ - 0.875).abs() < 1e-9,
1208 "expected 42/48 = 0.875 (6 blocks * 7 warps / 48 max warps), got {occ}"
1209 );
1210 }
1211
1212 #[test]
1215 fn kernel_stats_display() {
1216 let ks = KernelStats {
1217 kernel_name: "matmul".to_owned(),
1218 launch_count: 5,
1219 total_time_ms: 10.0,
1220 avg_time_ms: 2.0,
1221 min_time_ms: 1.0,
1222 max_time_ms: 4.0,
1223 avg_occupancy: 0.75,
1224 };
1225 let s = format!("{ks}");
1226 assert!(s.contains("matmul"));
1227 assert!(s.contains("5 launches"));
1228 assert!(s.contains("75.0%"));
1229 }
1230}