1use serde::{Deserialize, Serialize};
21
22use crate::{KernelBody, KernelDescriptor, KernelOpKind};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25pub struct OpHistogram {
26 pub literal: u32,
27 pub arithmetic: u32,
28 pub memory: u32,
29 pub control_flow: u32,
30 pub subgroup: u32,
31 pub builtin: u32,
32 pub other: u32,
33}
34
35impl OpHistogram {
36 pub fn total(&self) -> u32 {
37 self.literal
38 + self.arithmetic
39 + self.memory
40 + self.control_flow
41 + self.subgroup
42 + self.builtin
43 + self.other
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.total() == 0
49 }
50
51 pub fn dominant(&self) -> Option<(&'static str, u32)> {
55 let mut entries = [
56 ("literal", self.literal),
57 ("arithmetic", self.arithmetic),
58 ("memory", self.memory),
59 ("control_flow", self.control_flow),
60 ("subgroup", self.subgroup),
61 ("builtin", self.builtin),
62 ("other", self.other),
63 ];
64 entries.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
65 let (name, count) = entries[0];
66 if count == 0 {
67 None
68 } else {
69 Some((name, count))
70 }
71 }
72
73 pub fn is_memory_bound(&self) -> bool {
77 let non_memory = self.total() - self.memory;
78 self.memory > non_memory
79 }
80
81 pub fn is_arithmetic_bound(&self) -> bool {
84 let non_arith = self.total() - self.arithmetic;
85 self.arithmetic > non_arith
86 }
87
88 pub fn merge(&mut self, other: OpHistogram) {
91 self.literal = self.literal.saturating_add(other.literal);
92 self.arithmetic = self.arithmetic.saturating_add(other.arithmetic);
93 self.memory = self.memory.saturating_add(other.memory);
94 self.control_flow = self.control_flow.saturating_add(other.control_flow);
95 self.subgroup = self.subgroup.saturating_add(other.subgroup);
96 self.builtin = self.builtin.saturating_add(other.builtin);
97 self.other = self.other.saturating_add(other.other);
98 }
99
100 pub fn zero() -> Self {
103 Self::default()
104 }
105
106 pub fn format_short(&self) -> String {
109 format!(
110 "{} ops: lit={} arith={} mem={} cf={} sg={} bi={} other={}",
111 self.total(),
112 self.literal,
113 self.arithmetic,
114 self.memory,
115 self.control_flow,
116 self.subgroup,
117 self.builtin,
118 self.other,
119 )
120 }
121}
122
123impl std::fmt::Display for OpHistogram {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.write_str(&self.format_short())
126 }
127}
128
129#[must_use]
130pub fn analyze(desc: &KernelDescriptor) -> OpHistogram {
131 let mut h = OpHistogram::default();
132 walk(&desc.body, &mut h);
133 h
134}
135
136fn walk(body: &KernelBody, h: &mut OpHistogram) {
137 for op in &body.ops {
138 bump(&op.kind, h);
139 }
140 for child in &body.child_bodies {
141 walk(child, h);
142 }
143}
144
145fn bump(kind: &KernelOpKind, h: &mut OpHistogram) {
146 use KernelOpKind::*;
147 match kind {
148 Literal => h.literal += 1,
149 Copy | BinOpKind(_) | UnOpKind(_) | Fma | MatrixMma { .. } | Select | Cast { .. } => {
150 h.arithmetic += 1
151 }
152 LoadGlobal
153 | LoadShared
154 | LoadConstant
155 | StoreGlobal
156 | StoreShared
157 | LoopCarrierInit { .. }
158 | LoopCarrierEnd { .. }
159 | Atomic { .. }
160 | AsyncLoad { .. }
161 | AsyncStore { .. } => h.memory += 1,
162 StructuredIfThen
163 | StructuredIfThenElse
164 | StructuredForLoop { .. }
165 | StructuredBlock
166 | Region { .. }
167 | Barrier { .. }
168 | Return
169 | Trap { .. }
170 | Resume { .. } => h.control_flow += 1,
171 SubgroupBallot | SubgroupShuffle | SubgroupBroadcast | SubgroupReduce { .. } => h.subgroup += 1,
172 LocalInvocationId
173 | GlobalInvocationId
174 | WorkgroupId
175 | SubgroupLocalId
176 | SubgroupSize
177 | LoopIndex { .. }
178 | LoopCarrier { .. }
179 | BufferLength => h.builtin += 1,
180 Call { .. }
181 | OpaqueExpr(..)
182 | OpaqueNode(..)
183 | AsyncWait { .. }
184 | IndirectDispatch { .. } => h.other += 1,
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::{
192 BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
193 KernelOp, KernelOpKind, LiteralValue, MemoryClass,
194 };
195 use vyre_foundation::ir::{BinOp, DataType};
196
197 fn build(ops: Vec<KernelOp>, child_bodies: Vec<KernelBody>) -> KernelDescriptor {
198 KernelDescriptor {
199 id: "k".into(),
200 bindings: BindingLayout {
201 slots: vec![BindingSlot {
202 slot: 0,
203 element_type: DataType::U32,
204 element_count: None,
205 memory_class: MemoryClass::Global,
206 visibility: BindingVisibility::ReadWrite,
207 name: "buf".into(),
208 }],
209 },
210 dispatch: Dispatch::new(64, 1, 1),
211 body: KernelBody {
212 ops,
213 child_bodies,
214 literals: vec![LiteralValue::U32(7)],
215 },
216 }
217 }
218
219 #[test]
220 fn histogram_merge_is_associative() {
221 let a = OpHistogram {
223 literal: 2,
224 arithmetic: 3,
225 memory: 1,
226 control_flow: 0,
227 subgroup: 0,
228 builtin: 0,
229 other: 0,
230 };
231 let b = OpHistogram {
232 literal: 5,
233 arithmetic: 0,
234 memory: 4,
235 control_flow: 1,
236 subgroup: 0,
237 builtin: 2,
238 other: 0,
239 };
240 let c = OpHistogram {
241 literal: 1,
242 arithmetic: 1,
243 memory: 1,
244 control_flow: 1,
245 subgroup: 1,
246 builtin: 1,
247 other: 1,
248 };
249
250 let mut left = a;
251 left.merge(b);
252 left.merge(c);
253
254 let mut bc = b;
255 bc.merge(c);
256 let mut right = a;
257 right.merge(bc);
258
259 assert_eq!(left, right);
260 }
261
262 #[test]
263 fn histogram_zero_is_identity() {
264 let h = OpHistogram {
265 literal: 7,
266 arithmetic: 3,
267 memory: 2,
268 control_flow: 1,
269 subgroup: 0,
270 builtin: 0,
271 other: 0,
272 };
273 let mut acc = OpHistogram::zero();
274 acc.merge(h);
275 assert_eq!(acc, h);
276
277 let mut acc2 = h;
278 acc2.merge(OpHistogram::zero());
279 assert_eq!(acc2, h);
280 }
281
282 #[test]
283 fn histogram_merge_aggregates() {
284 let mut acc = OpHistogram::zero();
285 acc.merge(OpHistogram {
286 literal: 2,
287 arithmetic: 3,
288 memory: 1,
289 control_flow: 0,
290 subgroup: 0,
291 builtin: 0,
292 other: 0,
293 });
294 acc.merge(OpHistogram {
295 literal: 5,
296 arithmetic: 0,
297 memory: 4,
298 control_flow: 1,
299 subgroup: 0,
300 builtin: 2,
301 other: 0,
302 });
303 assert_eq!(acc.literal, 7);
304 assert_eq!(acc.arithmetic, 3);
305 assert_eq!(acc.memory, 5);
306 assert_eq!(acc.control_flow, 1);
307 assert_eq!(acc.builtin, 2);
308 assert_eq!(acc.total(), 18);
309 }
310
311 #[test]
312 fn dominant_picks_largest_category() {
313 let h = OpHistogram {
314 literal: 2,
315 arithmetic: 7,
316 memory: 5,
317 control_flow: 1,
318 subgroup: 0,
319 builtin: 0,
320 other: 0,
321 };
322 let (name, count) = h.dominant().unwrap();
323 assert_eq!(name, "arithmetic");
324 assert_eq!(count, 7);
325 }
326
327 #[test]
328 fn dominant_none_when_empty() {
329 let h = OpHistogram::default();
330 assert!(h.dominant().is_none());
331 assert!(h.is_empty());
332 }
333
334 #[test]
335 fn empty_kernel_has_all_zeros() {
336 let h = analyze(&build(vec![], vec![]));
337 assert_eq!(h.total(), 0);
338 assert!(!h.is_memory_bound());
339 assert!(!h.is_arithmetic_bound());
340 }
341
342 #[test]
343 fn literal_and_binop_counted_separately() {
344 let h = analyze(&build(
345 vec![
346 KernelOp {
347 kind: KernelOpKind::Literal,
348 operands: vec![0],
349 result: Some(0),
350 },
351 KernelOp {
352 kind: KernelOpKind::Literal,
353 operands: vec![0],
354 result: Some(1),
355 },
356 KernelOp {
357 kind: KernelOpKind::BinOpKind(BinOp::Add),
358 operands: vec![0, 1],
359 result: Some(2),
360 },
361 ],
362 vec![],
363 ));
364 assert_eq!(h.literal, 2);
365 assert_eq!(h.arithmetic, 1);
366 assert_eq!(h.total(), 3);
367 }
368
369 #[test]
370 fn memory_bound_when_loads_dominate() {
371 let mut ops = vec![KernelOp {
373 kind: KernelOpKind::Literal,
374 operands: vec![0],
375 result: Some(0),
376 }];
377 for i in 0..5 {
378 ops.push(KernelOp {
379 kind: KernelOpKind::LoadGlobal,
380 operands: vec![0, 0],
381 result: Some(1 + i),
382 });
383 }
384 let h = analyze(&build(ops, vec![]));
385 assert_eq!(h.memory, 5);
386 assert_eq!(h.literal, 1);
387 assert!(h.is_memory_bound());
388 assert!(!h.is_arithmetic_bound());
389 }
390
391 #[test]
392 fn arithmetic_bound_when_binops_dominate() {
393 let mut ops = vec![
394 KernelOp {
395 kind: KernelOpKind::Literal,
396 operands: vec![0],
397 result: Some(0),
398 },
399 KernelOp {
400 kind: KernelOpKind::Literal,
401 operands: vec![0],
402 result: Some(1),
403 },
404 ];
405 for i in 0..5 {
406 ops.push(KernelOp {
407 kind: KernelOpKind::BinOpKind(BinOp::Add),
408 operands: vec![0, 1],
409 result: Some(2 + i),
410 });
411 }
412 let h = analyze(&build(ops, vec![]));
413 assert_eq!(h.arithmetic, 5);
414 assert!(h.is_arithmetic_bound());
415 assert!(!h.is_memory_bound());
416 }
417
418 #[test]
419 fn control_flow_includes_barrier() {
420 let h = analyze(&build(
421 vec![KernelOp {
422 kind: KernelOpKind::Barrier {
423 ordering: vyre_foundation::runtime::memory_model::MemoryOrdering::SeqCst,
424 },
425 operands: vec![],
426 result: None,
427 }],
428 vec![],
429 ));
430 assert_eq!(h.control_flow, 1);
431 }
432
433 #[test]
434 fn subgroup_ops_categorized() {
435 let h = analyze(&build(
436 vec![KernelOp {
437 kind: KernelOpKind::SubgroupBallot,
438 operands: vec![0],
439 result: Some(0),
440 }],
441 vec![],
442 ));
443 assert_eq!(h.subgroup, 1);
444 }
445
446 #[test]
447 fn builtin_ops_categorized() {
448 let h = analyze(&build(
449 vec![
450 KernelOp {
451 kind: KernelOpKind::LocalInvocationId,
452 operands: vec![0],
453 result: Some(0),
454 },
455 KernelOp {
456 kind: KernelOpKind::WorkgroupId,
457 operands: vec![0],
458 result: Some(1),
459 },
460 ],
461 vec![],
462 ));
463 assert_eq!(h.builtin, 2);
464 }
465
466 #[test]
467 fn child_body_ops_recursively_counted() {
468 let child = KernelBody {
469 ops: vec![
470 KernelOp {
471 kind: KernelOpKind::Literal,
472 operands: vec![0],
473 result: Some(0),
474 },
475 KernelOp {
476 kind: KernelOpKind::BinOpKind(BinOp::Add),
477 operands: vec![0, 0],
478 result: Some(1),
479 },
480 ],
481 child_bodies: vec![],
482 literals: vec![LiteralValue::U32(3)],
483 };
484 let h = analyze(&build(
485 vec![KernelOp {
486 kind: KernelOpKind::Literal,
487 operands: vec![0],
488 result: Some(0),
489 }],
490 vec![child],
491 ));
492 assert_eq!(h.literal, 2);
494 assert_eq!(h.arithmetic, 1);
495 assert_eq!(h.total(), 3);
496 }
497
498 #[test]
499 fn format_short_includes_all_categories() {
500 let h = OpHistogram {
501 literal: 2,
502 arithmetic: 5,
503 memory: 3,
504 control_flow: 1,
505 subgroup: 0,
506 builtin: 1,
507 other: 0,
508 };
509 let s = h.format_short();
510 assert!(s.contains("12 ops"));
511 assert!(s.contains("lit=2"));
512 assert!(s.contains("arith=5"));
513 assert!(s.contains("mem=3"));
514 assert_eq!(format!("{h}"), s);
516 }
517
518 #[test]
519 fn total_sums_all_categories() {
520 let h = OpHistogram {
521 literal: 3,
522 arithmetic: 5,
523 memory: 7,
524 control_flow: 2,
525 subgroup: 1,
526 builtin: 4,
527 other: 0,
528 };
529 assert_eq!(h.total(), 22);
530 }
531}