Skip to main content

vyre_lower/analyses/op_histogram/
mod.rs

1//! Per-kind op histogram.
2//!
3//! Counts each `KernelOpKind` variant occurrence in the descriptor
4//! (parent body + recursive child bodies). Useful for telemetry  -
5//! answers "what does this kernel mostly do?" with a single struct.
6//!
7//! Group categories collapse related variants:
8//! - `arithmetic` = BinOpKind + UnOpKind + Fma + Cast + Select
9//! - `memory` = Load* + Store* + Atomic + AsyncLoad + AsyncStore
10//! - `control_flow` = StructuredIfThen + StructuredIfThenElse +
11//!   StructuredForLoop + StructuredBlock + Region + Barrier + Return +
12//!   Trap + Resume
13//! - `subgroup` = SubgroupBallot + SubgroupShuffle + SubgroupAdd
14//! - `builtin` = LocalInvocationId + GlobalInvocationId + WorkgroupId +
15//!   SubgroupLocalId + SubgroupSize + BufferLength
16//! - `literal` = Literal
17//! - `other` = Call + OpaqueExpr + OpaqueNode + AsyncWait +
18//!   IndirectDispatch
19
20use 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    /// True iff every category is zero. The kernel has no ops at all.
47    pub fn is_empty(&self) -> bool {
48        self.total() == 0
49    }
50
51    /// The dominant category and its count. Returns `None` if the
52    /// histogram is empty. Useful for one-line "this kernel is mostly
53    /// X" reporting.
54    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    /// True iff the kernel is dominated by memory ops (memory > all
74    /// other categories combined). Memory-bound kernels typically
75    /// benefit most from coalesce / vec_pack / shared-mem promotion.
76    pub fn is_memory_bound(&self) -> bool {
77        let non_memory = self.total() - self.memory;
78        self.memory > non_memory
79    }
80
81    /// True iff the kernel is dominated by arithmetic ops. ALU-bound
82    /// kernels benefit from strength_reduce / fma fusion / tensor cores.
83    pub fn is_arithmetic_bound(&self) -> bool {
84        let non_arith = self.total() - self.arithmetic;
85        self.arithmetic > non_arith
86    }
87
88    /// Merge another histogram into self (saturating-adds every
89    /// category). Useful for corpus-level rollup.
90    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    /// Identity element for [`Self::merge`]  -  all zeros. `Default` produces
101    /// the same value but `zero()` reads more naturally as a fold seed.
102    pub fn zero() -> Self {
103        Self::default()
104    }
105
106    /// One-line human-readable summary suitable for log lines.
107    /// Format: `"N ops: lit=X arith=Y mem=Z cf=W sg=V bi=U other=T"`.
108    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        // (a + b) + c == a + (b + c)
222        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        // 5 loads, 1 lit  -  memory > everything else (1 lit).
372        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        // 2 literals (1 parent + 1 child), 1 arithmetic (child Add).
493        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        // Display delegates to format_short.
515        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}