1use std::collections::BTreeMap;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Mutex, OnceLock};
20use std::time::Duration;
21
22#[derive(Default, Clone)]
23struct Row {
24 calls: u64,
25 gpu_ns: u128,
26 cpu_ns: u128,
27 refused: u64,
28 worst: f32,
29}
30
31fn table() -> &'static Mutex<BTreeMap<(usize, usize, usize), Row>> {
32 static T: OnceLock<Mutex<BTreeMap<(usize, usize, usize), Row>>> = OnceLock::new();
33 T.get_or_init(|| Mutex::new(BTreeMap::new()))
34}
35
36pub fn on() -> bool {
37 static ON: OnceLock<bool> = OnceLock::new();
38 *ON.get_or_init(|| std::env::var("CMF_MM_AB").as_deref() == Ok("1"))
39}
40
41static SEEN: AtomicU64 = AtomicU64::new(0);
44
45#[allow(clippy::too_many_arguments)]
46pub fn record(
47 b: usize,
48 rows: usize,
49 cols: usize,
50 gpu_took_it: bool,
51 gpu: Duration,
52 cpu: Duration,
53 g: &[f32],
54 c: &[f32],
55) {
56 SEEN.fetch_add(1, Ordering::Relaxed);
57 let mut t = table().lock().unwrap();
58 let e = t.entry((b, rows, cols)).or_default();
59 e.calls += 1;
60 e.cpu_ns += cpu.as_nanos();
61 if gpu_took_it {
62 e.gpu_ns += gpu.as_nanos();
63 let scale = c.iter().fold(0f32, |m, &v| m.max(v.abs())).max(1e-6);
66 let d = g
67 .iter()
68 .zip(c)
69 .fold(0f32, |m, (&a, &b)| m.max((a - b).abs()))
70 / scale;
71 e.worst = e.worst.max(d);
72 if d > 1e-2 {
73 eprintln!(
74 "mm-ab: b={b} {rows}x{cols}: GPU vs CPU max rel diff {d:.3e} (call {})",
75 e.calls
76 );
77 }
78 } else {
79 e.refused += 1;
80 }
81}
82
83pub fn report() -> String {
86 if SEEN.load(Ordering::Relaxed) == 0 {
87 return "no q4tp matmat calls were eligible for the device arm".into();
88 }
89 let t = table().lock().unwrap();
90 let mut rows: Vec<_> = t.iter().collect();
91 rows.sort_by_key(|(_, r)| std::cmp::Reverse(r.cpu_ns));
92 let mut s = String::from(
93 "\n q4tp matmat, both arms per call (CMF_MM_AB=1)\n\
94 \x20 b rows cols calls gpu ms cpu ms ratio worst\n",
95 );
96 let (mut tg, mut tc) = (0u128, 0u128);
97 for ((b, r, c), e) in rows {
98 let took = e.calls - e.refused;
99 let g = e.gpu_ns as f64 / 1e6;
100 let cp = e.cpu_ns as f64 / 1e6;
101 tg += e.gpu_ns;
102 tc += e.cpu_ns;
103 let ratio = if took > 0 && g > 0.0 {
104 format!("{:.2}x", cp / g)
105 } else {
106 "refused".into()
107 };
108 s.push_str(&format!(
109 " {b:>5} {r:>8} {c:>7} {:>6} {g:>9.1} {cp:>9.1} {ratio:>7} {:>6.4}\n",
110 e.calls, e.worst
111 ));
112 }
113 s.push_str(&format!(
114 " total gpu {:.1} ms, cpu {:.1} ms — the device arm is {:.2}x the host's\n",
115 tg as f64 / 1e6,
116 tc as f64 / 1e6,
117 tc as f64 / (tg as f64).max(1.0),
118 ));
119 s
120}
121
122pub fn split_on() -> bool {
128 static ON: OnceLock<bool> = OnceLock::new();
129 *ON.get_or_init(|| std::env::var("CMF_MM_SPLIT").as_deref() == Ok("1"))
130}
131
132#[derive(Default, Clone)]
133struct Split {
134 calls: u64,
135 kernel_ns: u128,
136 rb_ns: u128,
137}
138
139fn splits() -> &'static Mutex<BTreeMap<(usize, usize, usize), Split>> {
140 static T: OnceLock<Mutex<BTreeMap<(usize, usize, usize), Split>>> = OnceLock::new();
141 T.get_or_init(|| Mutex::new(BTreeMap::new()))
142}
143
144pub fn split_note(b: usize, rows: usize, cols: usize, kernel: Duration, rb: Duration) {
145 let mut t = splits().lock().unwrap();
146 let e = t.entry((b, rows, cols)).or_default();
147 e.calls += 1;
148 e.kernel_ns += kernel.as_nanos();
149 e.rb_ns += rb.as_nanos();
150}
151
152pub fn split_report() -> String {
153 let t = splits().lock().unwrap();
154 if t.is_empty() {
155 return "no split-timed calls".into();
156 }
157 let mut s = String::from(
158 "\n device arm, kernel vs readback (CMF_MM_SPLIT=1)\n\
159 \x20 b rows cols calls kernel ms readbk ms rb MB/call\n",
160 );
161 for ((b, r, c), e) in t.iter() {
162 s.push_str(&format!(
163 " {b:>5} {r:>8} {c:>7} {:>6} {:>10.1} {:>10.1} {:>10.1}\n",
164 e.calls,
165 e.kernel_ns as f64 / 1e6,
166 e.rb_ns as f64 / 1e6,
167 (b * r * 4) as f64 / 1e6,
168 ));
169 }
170 s
171}