1use std::hash::{Hash, Hasher};
38
39pub mod add_sub_cancel;
40pub mod aos_to_soa_promote;
41mod arithmetic_combine;
42pub mod bank_conflict_pad;
43pub mod bitwise_combine;
44mod body_index;
45pub mod bitwise_idemp {
47 use crate::rewrites::self_binop::rewrite_self_binops;
48 use crate::KernelDescriptor;
49 use vyre_foundation::ir::BinOp;
50
51 #[must_use]
52 pub fn bitwise_idemp(desc: &KernelDescriptor) -> KernelDescriptor {
53 rewrite_self_binops(desc, |bin| matches!(bin, BinOp::BitAnd | BinOp::BitOr))
54 }
55}
56pub mod boolean_simplify;
57pub mod branch_collapse;
58pub mod canonicalize;
59pub mod cmp_normalize;
60pub mod cmp_self_false;
61mod commutative_lit_chain;
62pub mod const_buffer_promote;
63mod dataflow_facts;
64pub mod dead_store;
65pub mod descriptor_const_fold;
66pub mod descriptor_cse;
67pub mod descriptor_dce;
68pub mod div_combine;
69pub mod drop_unused_bindings;
70pub mod drop_unused_child_bodies;
71pub mod drop_unused_literals;
72pub mod egraph_saturation;
73pub mod emit_order;
74pub mod identity_elim;
75pub mod licm;
76mod literal;
77pub mod load_forwarding;
78pub mod loop_fission;
79pub mod loop_fusion;
80pub mod loop_unroll;
81pub mod loop_zero_iter;
82pub mod matmul_promote;
83mod memory_address;
84mod rhs_lit_chain;
85mod self_binop;
86pub mod min_max_idemp {
88 use crate::rewrites::self_binop::rewrite_self_binops;
89 use crate::KernelDescriptor;
90 use vyre_foundation::ir::BinOp;
91
92 #[must_use]
93 pub fn min_max_idemp(desc: &KernelDescriptor) -> KernelDescriptor {
94 rewrite_self_binops(desc, |bin| matches!(bin, BinOp::Min | BinOp::Max))
95 }
96}
97pub mod mod_idemp;
98pub mod mul_add_to_fma;
99pub mod negate_cancel;
100pub mod select_fold;
101pub mod shared_mem_promote;
102pub mod shift_combine;
103pub mod strength_reduce;
104pub mod sub_combine;
105pub mod tail_mask;
106pub mod unary_idemp;
107pub mod xor_self_zero;
108
109pub use add_sub_cancel::add_sub_cancel;
110pub use aos_to_soa_promote::{promote as aos_to_soa_promote, LayoutHint as AosSoaLayoutHint};
111pub use arithmetic_combine::add_combine::add_combine;
112pub use arithmetic_combine::mul_combine::mul_combine;
113pub use arithmetic_combine::{add_combine, mul_combine};
114pub use bank_conflict_pad::bank_conflict_pad;
115pub use bitwise_combine::bitwise_combine;
116pub use bitwise_idemp::bitwise_idemp;
117pub use boolean_simplify::boolean_simplify;
118pub use branch_collapse::branch_collapse;
119pub use canonicalize::canonicalize;
120pub use cmp_normalize::cmp_normalize;
121pub use cmp_self_false::cmp_self_false;
122pub use const_buffer_promote::const_buffer_promote;
123pub use dead_store::{
124 dead_store, dead_store_with_alias_facts, dead_store_with_dataflow_analysis_facts,
125 dead_store_with_dataflow_facts, dead_store_with_weir_alias_facts,
126};
127pub use descriptor_const_fold::descriptor_const_fold;
128pub use descriptor_cse::descriptor_cse;
129pub use descriptor_dce::descriptor_dce;
130pub use div_combine::div_combine;
131pub use drop_unused_bindings::drop_unused_bindings;
132pub use drop_unused_child_bodies::drop_unused_child_bodies;
133pub use drop_unused_literals::drop_unused_literals;
134pub use emit_order::emit_order;
135pub use identity_elim::identity_elim;
136pub use licm::{
137 licm, licm_with_alias_facts, licm_with_dataflow_analysis_facts, licm_with_dataflow_facts,
138 licm_with_weir_alias_facts,
139};
140pub use load_forwarding::{
141 load_forwarding, load_forwarding_with_alias_facts,
142 load_forwarding_with_dataflow_analysis_facts, load_forwarding_with_dataflow_facts,
143 load_forwarding_with_weir_alias_facts,
144};
145pub use loop_fission::{
146 loop_fission, loop_fission_with_alias_facts, loop_fission_with_dataflow_analysis_facts,
147 loop_fission_with_dataflow_facts, loop_fission_with_weir_alias_facts,
148};
149pub use loop_fusion::{
150 loop_fusion, loop_fusion_with_alias_facts, loop_fusion_with_dataflow_analysis_facts,
151 loop_fusion_with_dataflow_facts, loop_fusion_with_weir_alias_facts,
152};
153pub use loop_unroll::loop_unroll;
154pub use loop_zero_iter::loop_zero_iter;
155pub use matmul_promote::{infer_matmul_tile_loops, matmul_promote, MatmulTileLoopPlan};
156pub use min_max_idemp::min_max_idemp;
157pub use mod_idemp::mod_idemp;
158pub use mul_add_to_fma::mul_add_to_fma;
159pub use negate_cancel::negate_cancel;
160pub use select_fold::select_fold;
161pub use shared_mem_promote::shared_mem_promote;
162pub use shift_combine::shift_combine;
163pub use strength_reduce::strength_reduce;
164pub use sub_combine::sub_combine;
165pub use tail_mask::apply_tail_mask;
166pub use unary_idemp::unary_idemp;
167pub use xor_self_zero::xor_self_zero;
168
169#[derive(Debug, Clone, Copy)]
171pub struct DescriptorRewritePass {
172 pub name: &'static str,
174 pub rewrite: fn(&crate::KernelDescriptor) -> crate::KernelDescriptor,
176}
177
178impl DescriptorRewritePass {
179 #[must_use]
180 fn run(self, desc: &crate::KernelDescriptor) -> crate::KernelDescriptor {
181 (self.rewrite)(desc)
182 }
183}
184
185fn egraph_saturation_pass(desc: &crate::KernelDescriptor) -> crate::KernelDescriptor {
186 egraph_saturation::saturate_algebraic_descriptor(desc).0
187}
188
189const CANONICAL_REWRITE_PASSES: &[DescriptorRewritePass] = &[
190 DescriptorRewritePass {
191 name: "strength_reduce",
192 rewrite: strength_reduce,
193 },
194 DescriptorRewritePass {
195 name: "shift_combine",
196 rewrite: shift_combine,
197 },
198 DescriptorRewritePass {
199 name: "shared_mem_promote",
200 rewrite: shared_mem_promote,
201 },
202 DescriptorRewritePass {
203 name: "bank_conflict_pad",
204 rewrite: bank_conflict_pad,
205 },
206 DescriptorRewritePass {
207 name: "const_buffer_promote",
208 rewrite: const_buffer_promote,
209 },
210 DescriptorRewritePass {
211 name: "descriptor_const_fold",
212 rewrite: descriptor_const_fold,
213 },
214 DescriptorRewritePass {
215 name: "add_combine",
216 rewrite: add_combine,
217 },
218 DescriptorRewritePass {
219 name: "sub_combine",
220 rewrite: sub_combine,
221 },
222 DescriptorRewritePass {
223 name: "mul_combine",
224 rewrite: mul_combine,
225 },
226 DescriptorRewritePass {
227 name: "div_combine",
228 rewrite: div_combine,
229 },
230 DescriptorRewritePass {
231 name: "mod_idemp",
232 rewrite: mod_idemp,
233 },
234 DescriptorRewritePass {
235 name: "add_sub_cancel",
236 rewrite: add_sub_cancel,
237 },
238 DescriptorRewritePass {
239 name: "bitwise_combine",
240 rewrite: bitwise_combine,
241 },
242 DescriptorRewritePass {
243 name: "identity_elim",
244 rewrite: identity_elim,
245 },
246 DescriptorRewritePass {
247 name: "boolean_simplify",
248 rewrite: boolean_simplify,
249 },
250 DescriptorRewritePass {
251 name: "negate_cancel",
252 rewrite: negate_cancel,
253 },
254 DescriptorRewritePass {
255 name: "unary_idemp",
256 rewrite: unary_idemp,
257 },
258 DescriptorRewritePass {
259 name: "select_fold",
260 rewrite: select_fold,
261 },
262 DescriptorRewritePass {
263 name: "min_max_idemp",
264 rewrite: min_max_idemp,
265 },
266 DescriptorRewritePass {
267 name: "bitwise_idemp",
268 rewrite: bitwise_idemp,
269 },
270 DescriptorRewritePass {
271 name: "branch_collapse",
272 rewrite: branch_collapse,
273 },
274 DescriptorRewritePass {
275 name: "loop_fusion",
276 rewrite: loop_fusion,
277 },
278 DescriptorRewritePass {
279 name: "loop_unroll",
280 rewrite: loop_unroll,
281 },
282 DescriptorRewritePass {
283 name: "loop_zero_iter",
284 rewrite: loop_zero_iter,
285 },
286 DescriptorRewritePass {
287 name: "licm",
288 rewrite: licm,
289 },
290 DescriptorRewritePass {
291 name: "load_forwarding",
292 rewrite: load_forwarding,
293 },
294 DescriptorRewritePass {
295 name: "mul_add_to_fma",
296 rewrite: mul_add_to_fma,
297 },
298 DescriptorRewritePass {
299 name: "matmul_promote",
300 rewrite: matmul_promote,
301 },
302 DescriptorRewritePass {
303 name: "descriptor_dce_after_forwarding",
304 rewrite: descriptor_dce,
305 },
306 DescriptorRewritePass {
307 name: "dead_store",
308 rewrite: dead_store,
309 },
310 DescriptorRewritePass {
311 name: "descriptor_dce",
312 rewrite: descriptor_dce,
313 },
314 DescriptorRewritePass {
315 name: "cmp_normalize",
316 rewrite: cmp_normalize,
317 },
318 DescriptorRewritePass {
319 name: "cmp_self_false",
320 rewrite: cmp_self_false,
321 },
322 DescriptorRewritePass {
323 name: "xor_self_zero",
324 rewrite: xor_self_zero,
325 },
326 DescriptorRewritePass {
327 name: "canonicalize",
328 rewrite: canonicalize,
329 },
330 DescriptorRewritePass {
331 name: "descriptor_cse",
332 rewrite: descriptor_cse,
333 },
334 DescriptorRewritePass {
335 name: "egraph_saturation",
336 rewrite: egraph_saturation_pass,
337 },
338 DescriptorRewritePass {
339 name: "descriptor_const_fold_post_saturation",
340 rewrite: descriptor_const_fold,
341 },
342 DescriptorRewritePass {
343 name: "identity_elim_post_saturation",
344 rewrite: identity_elim,
345 },
346 DescriptorRewritePass {
347 name: "descriptor_dce_post_saturation",
348 rewrite: descriptor_dce,
349 },
350 DescriptorRewritePass {
351 name: "cmp_normalize_post_saturation",
352 rewrite: cmp_normalize,
353 },
354 DescriptorRewritePass {
355 name: "canonicalize_post_saturation",
356 rewrite: canonicalize,
357 },
358 DescriptorRewritePass {
359 name: "descriptor_cse_post_saturation",
360 rewrite: descriptor_cse,
361 },
362 DescriptorRewritePass {
363 name: "drop_unused_bindings",
364 rewrite: drop_unused_bindings,
365 },
366 DescriptorRewritePass {
367 name: "drop_unused_literals",
368 rewrite: drop_unused_literals,
369 },
370 DescriptorRewritePass {
371 name: "drop_unused_child_bodies",
372 rewrite: drop_unused_child_bodies,
373 },
374 DescriptorRewritePass {
375 name: "emit_order",
376 rewrite: emit_order,
377 },
378];
379
380#[must_use]
382pub const fn canonical_rewrite_passes() -> &'static [DescriptorRewritePass] {
383 CANONICAL_REWRITE_PASSES
384}
385
386fn descriptor_hash(desc: &crate::KernelDescriptor) -> u64 {
392 let mut h = rustc_hash::FxHasher::default();
393 desc.hash(&mut h);
394 h.finish()
395}
396
397fn run_descriptor_passes(
398 desc: &crate::KernelDescriptor,
399 passes: &[DescriptorRewritePass],
400) -> crate::KernelDescriptor {
401 let mut current = desc.clone();
402 for pass in passes {
403 let pre_hash = descriptor_hash(¤t);
404 let next = pass.run(¤t);
405 if descriptor_hash(&next) != pre_hash || next != current {
407 current = next;
408 }
409 #[cfg(debug_assertions)]
410 debug_verify_after_rewrite(¤t, pass.name);
411 }
412 current
413}
414
415#[must_use]
462
463pub fn run_all_once(desc: &crate::KernelDescriptor) -> crate::KernelDescriptor {
464 run_descriptor_passes(desc, canonical_rewrite_passes())
465}
466
467#[cfg(debug_assertions)]
471fn debug_verify_after_rewrite(desc: &crate::KernelDescriptor, pass: &str) {
472 if let Err(errors) = crate::verify::verify(desc) {
473 let violation_count = errors.len();
474 assert!(
475 violation_count == 0,
476 "rewrite pass `{pass}` produced an invalid KernelDescriptor - {} violation(s):\n{errors:#?}",
477 violation_count
478 );
479 }
480}
481
482#[must_use]
485pub fn run_all_once_with_dataflow_facts(
486 desc: &crate::KernelDescriptor,
487 alias_facts: &crate::analyses::alias_facts::AliasFactSet,
488 reaching_defs: &crate::analyses::reaching_def_facts::ReachingDefFactSet,
489) -> crate::KernelDescriptor {
490 let reduced = strength_reduce(desc);
491 let shared_promoted = shared_mem_promote(&reduced);
492 let padded = bank_conflict_pad(&shared_promoted);
493 let const_promoted = const_buffer_promote(&padded);
494 let folded = descriptor_const_fold(&const_promoted);
495 let identified = identity_elim(&folded);
496 let collapsed = branch_collapse(&identified);
497 let fused = loop_fusion_with_dataflow_facts(&collapsed, alias_facts, reaching_defs);
498 let unrolled = loop_unroll(&fused);
499 let hoisted = licm_with_dataflow_facts(&unrolled, alias_facts, reaching_defs);
500 let forwarded = load_forwarding_with_dataflow_facts(&hoisted, alias_facts, reaching_defs);
501 let cleaned = descriptor_dce(&forwarded);
502 let dse_done = dead_store_with_dataflow_facts(&cleaned, alias_facts, reaching_defs);
503 let dced = descriptor_dce(&dse_done);
504 let canon = canonicalize(&dced);
505 let merged = descriptor_cse(&canon);
506 let (saturated, _) = egraph_saturation::saturate_algebraic_descriptor(&merged);
507 let saturated_folded = descriptor_const_fold(&saturated);
508 let saturated_identified = identity_elim(&saturated_folded);
509 let saturated_dced = descriptor_dce(&saturated_identified);
510 let saturated_canon = canonicalize(&saturated_dced);
511 let saturated_merged = descriptor_cse(&saturated_canon);
512 let pruned_bindings = drop_unused_bindings(&saturated_merged);
513 let pruned_literals = drop_unused_literals(&pruned_bindings);
514 let pruned_children = drop_unused_child_bodies(&pruned_literals);
515 emit_order(&pruned_children)
516}
517
518pub const RUN_ALL_MAX_ITERS: usize = 4;
522
523#[must_use]
531pub fn run_all(desc: &crate::KernelDescriptor) -> crate::KernelDescriptor {
532 run_all_with_stats(desc).0
533}
534
535#[must_use]
537pub fn run_all_with_dataflow_facts(
538 desc: &crate::KernelDescriptor,
539 alias_facts: &crate::analyses::alias_facts::AliasFactSet,
540 reaching_defs: &crate::analyses::reaching_def_facts::ReachingDefFactSet,
541) -> crate::KernelDescriptor {
542 run_all_with_dataflow_stats(desc, alias_facts, reaching_defs).0
543}
544
545#[must_use]
547pub fn run_all_with_dataflow_analysis_facts(
548 desc: &crate::KernelDescriptor,
549 alias_facts: &crate::analyses::weir_alias::AliasFactSet,
550 reaching_defs: &crate::analyses::weir_reaching_def::ReachingDefFactSet,
551) -> crate::KernelDescriptor {
552 run_all_with_dataflow_facts(desc, alias_facts, reaching_defs)
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
558pub struct OptimizationStats {
559 pub ops_before: usize,
561 pub ops_after: usize,
563 pub bindings_before: usize,
566 pub bindings_after: usize,
567 pub literals_before: usize,
569 pub literals_after: usize,
570 pub iterations: usize,
575 pub converged: bool,
580}
581
582impl OptimizationStats {
583 pub fn ops_eliminated(&self) -> usize {
587 self.ops_before.saturating_sub(self.ops_after)
588 }
589
590 pub fn bindings_dropped(&self) -> usize {
591 self.bindings_before.saturating_sub(self.bindings_after)
592 }
593
594 pub fn is_no_op(&self) -> bool {
600 self.ops_before == self.ops_after
601 && self.bindings_before == self.bindings_after
602 && self.literals_before == self.literals_after
603 }
604
605 pub fn off_graph_dropped(&self) -> usize {
609 self.bindings_dropped() + self.literals_before.saturating_sub(self.literals_after)
610 }
611
612 pub fn merge(&mut self, other: OptimizationStats) {
618 self.ops_before = self.ops_before.saturating_add(other.ops_before);
619 self.ops_after = self.ops_after.saturating_add(other.ops_after);
620 self.bindings_before = self.bindings_before.saturating_add(other.bindings_before);
621 self.bindings_after = self.bindings_after.saturating_add(other.bindings_after);
622 self.literals_before = self.literals_before.saturating_add(other.literals_before);
623 self.literals_after = self.literals_after.saturating_add(other.literals_after);
624 self.iterations = self.iterations.saturating_add(other.iterations);
625 self.converged = self.converged && other.converged;
626 }
627
628 pub fn zero() -> Self {
631 OptimizationStats {
632 ops_before: 0,
633 ops_after: 0,
634 bindings_before: 0,
635 bindings_after: 0,
636 literals_before: 0,
637 literals_after: 0,
638 iterations: 0,
639 converged: true,
640 }
641 }
642
643 pub fn format_short(&self) -> String {
647 format!(
648 "ops {}→{} (-{}), bindings {}→{} (-{}), iters {} ({})",
649 self.ops_before,
650 self.ops_after,
651 self.ops_eliminated(),
652 self.bindings_before,
653 self.bindings_after,
654 self.bindings_dropped(),
655 self.iterations,
656 if self.converged {
657 "converged"
658 } else {
659 "stopped"
660 },
661 )
662 }
663}
664
665impl std::fmt::Display for OptimizationStats {
666 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667 f.write_str(&self.format_short())
668 }
669}
670
671#[must_use]
674pub fn run_all_with_stats(
675 desc: &crate::KernelDescriptor,
676) -> (crate::KernelDescriptor, OptimizationStats) {
677 let ops_before = desc.body.ops.len();
678 let bindings_before = desc.bindings.slots.len();
679 let literals_before = desc.body.literals.len();
680
681 let mut current = run_all_once(desc);
682 #[cfg(debug_assertions)]
683 debug_verify_after_rewrite(¤t, "run_all_once (iter 1)");
684 let mut iterations = 1usize;
685 let mut current_hash = descriptor_hash(¤t);
686 let mut converged = current_hash == descriptor_hash(desc) && current == *desc;
687 while !converged && iterations < RUN_ALL_MAX_ITERS {
688 let next = run_all_once(¤t);
689 iterations += 1;
690 #[cfg(debug_assertions)]
691 debug_verify_after_rewrite(&next, &format!("run_all_once (iter {iterations})"));
692 let next_hash = descriptor_hash(&next);
693 converged = next_hash == current_hash && next == current;
696 current = next;
697 current_hash = next_hash;
698 }
699
700 let stats = OptimizationStats {
701 ops_before,
702 ops_after: current.body.ops.len(),
703 bindings_before,
704 bindings_after: current.bindings.slots.len(),
705 literals_before,
706 literals_after: current.body.literals.len(),
707 iterations,
708 converged,
709 };
710 (current, stats)
711}
712
713#[must_use]
716pub fn run_all_with_dataflow_stats(
717 desc: &crate::KernelDescriptor,
718 alias_facts: &crate::analyses::alias_facts::AliasFactSet,
719 reaching_defs: &crate::analyses::reaching_def_facts::ReachingDefFactSet,
720) -> (crate::KernelDescriptor, OptimizationStats) {
721 let ops_before = desc.body.ops.len();
722 let bindings_before = desc.bindings.slots.len();
723 let literals_before = desc.body.literals.len();
724
725 let mut current = run_all_once_with_dataflow_facts(desc, alias_facts, reaching_defs);
726 let mut iterations = 1usize;
727 let mut current_hash = descriptor_hash(¤t);
728 let mut converged = current_hash == descriptor_hash(desc) && current == *desc;
729 while !converged && iterations < RUN_ALL_MAX_ITERS {
730 let next = run_all_once_with_dataflow_facts(¤t, alias_facts, reaching_defs);
731 iterations += 1;
732 let next_hash = descriptor_hash(&next);
733 converged = next_hash == current_hash && next == current;
734 current = next;
735 current_hash = next_hash;
736 }
737
738 let stats = OptimizationStats {
739 ops_before,
740 ops_after: current.body.ops.len(),
741 bindings_before,
742 bindings_after: current.bindings.slots.len(),
743 literals_before,
744 literals_after: current.body.literals.len(),
745 iterations,
746 converged,
747 };
748 (current, stats)
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::{
755 BindingLayout, Dispatch, KernelBody, KernelDescriptor, KernelOp, KernelOpKind, LiteralValue,
756 };
757
758 #[test]
759 fn run_all_on_empty_kernel_returns_empty() {
760 let desc = KernelDescriptor {
761 id: "k".into(),
762 bindings: BindingLayout { slots: vec![] },
763 dispatch: Dispatch::new(1, 1, 1),
764 body: KernelBody {
765 ops: vec![],
766 child_bodies: vec![],
767 literals: vec![],
768 },
769 };
770 let out = run_all(&desc);
771 assert!(out.body.ops.is_empty());
772 }
773
774 #[test]
775 fn run_all_is_idempotent() {
776 let desc = KernelDescriptor {
777 id: "k".into(),
778 bindings: BindingLayout { slots: vec![] },
779 dispatch: Dispatch::new(1, 1, 1),
780 body: KernelBody {
781 ops: vec![
782 KernelOp {
783 kind: KernelOpKind::Literal,
784 operands: vec![0],
785 result: Some(0),
786 },
787 KernelOp {
788 kind: KernelOpKind::Literal,
789 operands: vec![0],
790 result: Some(1),
791 }, KernelOp {
793 kind: KernelOpKind::Literal,
794 operands: vec![1],
795 result: Some(2),
796 }, ],
798 child_bodies: vec![],
799 literals: vec![LiteralValue::U32(5), LiteralValue::U32(99)],
800 },
801 };
802 let once = run_all(&desc);
803 let twice = run_all(&once);
804 assert_eq!(once.body.ops.len(), twice.body.ops.len());
805 assert_eq!(once.body.literals, twice.body.literals);
806 }
807
808 #[test]
809 fn run_all_collapses_kitchen_sink_kernel() {
810 let desc = KernelDescriptor {
838 id: "kitchen_sink".into(),
839 bindings: BindingLayout {
840 slots: vec![crate::BindingSlot {
841 slot: 0,
842 element_type: vyre_foundation::ir::DataType::U32,
843 element_count: None,
844 memory_class: crate::MemoryClass::Global,
845 visibility: crate::BindingVisibility::ReadWrite,
846 name: "buf".into(),
847 }],
848 },
849 dispatch: Dispatch::new(1, 1, 1),
850 body: KernelBody {
851 ops: vec![
852 KernelOp {
853 kind: KernelOpKind::Literal,
854 operands: vec![0],
855 result: Some(0),
856 },
857 KernelOp {
858 kind: KernelOpKind::Literal,
859 operands: vec![1],
860 result: Some(1),
861 },
862 KernelOp {
863 kind: KernelOpKind::Literal,
864 operands: vec![2],
865 result: Some(2),
866 },
867 KernelOp {
868 kind: KernelOpKind::Literal,
869 operands: vec![3],
870 result: Some(3),
871 },
872 KernelOp {
873 kind: KernelOpKind::Literal,
874 operands: vec![0],
875 result: Some(4),
876 },
877 KernelOp {
878 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add),
879 operands: vec![3, 0],
880 result: Some(5),
881 },
882 KernelOp {
883 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Mul),
884 operands: vec![3, 2],
885 result: Some(6),
886 },
887 KernelOp {
888 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Mul),
889 operands: vec![3, 1],
890 result: Some(7),
891 },
892 KernelOp {
893 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Mul),
894 operands: vec![3, 0],
895 result: Some(8),
896 },
897 KernelOp {
898 kind: KernelOpKind::StoreGlobal,
899 operands: vec![0, 0, 5],
900 result: None,
901 },
902 KernelOp {
903 kind: KernelOpKind::StoreGlobal,
904 operands: vec![0, 0, 6],
905 result: None,
906 },
907 ],
908 child_bodies: vec![],
909 literals: vec![
910 LiteralValue::U32(0),
911 LiteralValue::U32(1),
912 LiteralValue::U32(8),
913 LiteralValue::U32(7),
914 ],
915 },
916 };
917
918 let before_op_count = desc.body.ops.len();
919 let out = run_all(&desc);
920
921 let store_count = out
927 .body
928 .ops
929 .iter()
930 .filter(|o| matches!(o.kind, KernelOpKind::StoreGlobal))
931 .count();
932 assert_eq!(store_count, 1, "dead_store must drop the overwritten store");
933
934 let mul_count = out
940 .body
941 .ops
942 .iter()
943 .filter(|o| {
944 matches!(
945 o.kind,
946 KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Mul)
947 )
948 })
949 .count();
950 assert_eq!(mul_count, 0, "all 3 Mul ops should be eliminated");
951
952 let add_count = out
953 .body
954 .ops
955 .iter()
956 .filter(|o| {
957 matches!(
958 o.kind,
959 KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add)
960 )
961 })
962 .count();
963 assert_eq!(add_count, 0, "Add(r3, 0) → r3 should be eliminated");
964
965 assert!(
973 out.body.ops.len() <= before_op_count - 5,
974 "expected op count to drop by ≥5 (was {before_op_count}, now {})",
975 out.body.ops.len()
976 );
977
978 let twice = run_all(&out);
980 assert_eq!(out.body.ops.len(), twice.body.ops.len());
981 }
982
983 #[test]
984 fn run_all_forwards_then_drops_redundant_load() {
985 let desc = KernelDescriptor {
990 id: "stl".into(),
991 bindings: BindingLayout {
992 slots: vec![crate::BindingSlot {
993 slot: 0,
994 element_type: vyre_foundation::ir::DataType::U32,
995 element_count: None,
996 memory_class: crate::MemoryClass::Global,
997 visibility: crate::BindingVisibility::ReadWrite,
998 name: "buf".into(),
999 }],
1000 },
1001 dispatch: Dispatch::new(1, 1, 1),
1002 body: KernelBody {
1003 ops: vec![
1004 KernelOp {
1005 kind: KernelOpKind::Literal,
1006 operands: vec![0],
1007 result: Some(0),
1008 }, KernelOp {
1010 kind: KernelOpKind::Literal,
1011 operands: vec![1],
1012 result: Some(1),
1013 }, KernelOp {
1015 kind: KernelOpKind::StoreGlobal,
1016 operands: vec![0, 0, 1],
1017 result: None,
1018 },
1019 KernelOp {
1020 kind: KernelOpKind::LoadGlobal,
1021 operands: vec![0, 0],
1022 result: Some(2),
1023 },
1024 KernelOp {
1025 kind: KernelOpKind::StoreGlobal,
1026 operands: vec![0, 0, 2],
1027 result: None,
1028 },
1029 ],
1030 child_bodies: vec![],
1031 literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
1032 },
1033 };
1034 let out = run_all(&desc);
1035
1036 let store_count = out
1037 .body
1038 .ops
1039 .iter()
1040 .filter(|o| matches!(o.kind, KernelOpKind::StoreGlobal))
1041 .count();
1042 assert_eq!(
1043 store_count, 1,
1044 "dead_store must drop the redundant first store"
1045 );
1046
1047 let load_count = out
1048 .body
1049 .ops
1050 .iter()
1051 .filter(|o| matches!(o.kind, KernelOpKind::LoadGlobal))
1052 .count();
1053 assert_eq!(load_count, 0, "descriptor_dce must drop the redundant load");
1054 }
1055
1056 #[test]
1057 fn run_all_unrolls_then_simplifies() {
1058 let desc = KernelDescriptor {
1063 id: "loop_then_dse".into(),
1064 bindings: BindingLayout {
1065 slots: vec![crate::BindingSlot {
1066 slot: 0,
1067 element_type: vyre_foundation::ir::DataType::U32,
1068 element_count: None,
1069 memory_class: crate::MemoryClass::Global,
1070 visibility: crate::BindingVisibility::ReadWrite,
1071 name: "buf".into(),
1072 }],
1073 },
1074 dispatch: Dispatch::new(1, 1, 1),
1075 body: KernelBody {
1076 ops: vec![
1077 KernelOp {
1079 kind: KernelOpKind::Literal,
1080 operands: vec![0],
1081 result: Some(0),
1082 },
1083 KernelOp {
1084 kind: KernelOpKind::Literal,
1085 operands: vec![1],
1086 result: Some(1),
1087 },
1088 KernelOp {
1089 kind: KernelOpKind::Literal,
1090 operands: vec![0],
1091 result: Some(2),
1092 },
1093 KernelOp {
1094 kind: KernelOpKind::StructuredForLoop {
1095 loop_var: std::sync::Arc::from("i"),
1096 },
1097 operands: vec![0, 1, 0],
1098 result: None,
1099 },
1100 ],
1101 child_bodies: vec![KernelBody {
1102 ops: vec![
1103 KernelOp {
1104 kind: KernelOpKind::Literal,
1105 operands: vec![0],
1106 result: Some(0),
1107 },
1108 KernelOp {
1109 kind: KernelOpKind::Literal,
1110 operands: vec![1],
1111 result: Some(1),
1112 },
1113 KernelOp {
1114 kind: KernelOpKind::StoreGlobal,
1115 operands: vec![0, 0, 1],
1116 result: None,
1117 },
1118 ],
1119 child_bodies: vec![],
1120 literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
1121 }],
1122 literals: vec![LiteralValue::U32(0), LiteralValue::U32(2)],
1123 },
1124 };
1125 let out = run_all(&desc);
1126
1127 let loop_count = out
1129 .body
1130 .ops
1131 .iter()
1132 .filter(|o| matches!(o.kind, KernelOpKind::StructuredForLoop { .. }))
1133 .count();
1134 assert_eq!(loop_count, 0, "loop should be unrolled");
1135
1136 let store_count = out
1144 .body
1145 .ops
1146 .iter()
1147 .filter(|o| matches!(o.kind, KernelOpKind::StoreGlobal))
1148 .count();
1149 assert!(
1150 store_count >= 1,
1151 "at least one store from the unrolled body should survive"
1152 );
1153 }
1154
1155 #[test]
1156 fn run_all_with_stats_reports_op_reduction() {
1157 let desc = KernelDescriptor {
1160 id: "stats".into(),
1161 bindings: BindingLayout {
1162 slots: vec![crate::BindingSlot {
1163 slot: 0,
1164 element_type: vyre_foundation::ir::DataType::U32,
1165 element_count: None,
1166 memory_class: crate::MemoryClass::Global,
1167 visibility: crate::BindingVisibility::ReadWrite,
1168 name: "buf".into(),
1169 }],
1170 },
1171 dispatch: Dispatch::new(1, 1, 1),
1172 body: KernelBody {
1173 ops: vec![
1174 KernelOp {
1175 kind: KernelOpKind::Literal,
1176 operands: vec![0],
1177 result: Some(0),
1178 },
1179 KernelOp {
1180 kind: KernelOpKind::Literal,
1181 operands: vec![1],
1182 result: Some(1),
1183 },
1184 KernelOp {
1185 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add),
1186 operands: vec![1, 0],
1187 result: Some(2),
1188 }, KernelOp {
1190 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Mul),
1191 operands: vec![1, 0],
1192 result: Some(3),
1193 }, KernelOp {
1195 kind: KernelOpKind::StoreGlobal,
1196 operands: vec![0, 0, 1],
1197 result: None,
1198 },
1199 ],
1200 child_bodies: vec![],
1201 literals: vec![LiteralValue::U32(0), LiteralValue::U32(99)],
1202 },
1203 };
1204 let (_out, stats) = run_all_with_stats(&desc);
1205 assert!(
1206 stats.ops_eliminated() >= 2,
1207 "expected ≥2 ops eliminated, got {} ({} → {})",
1208 stats.ops_eliminated(),
1209 stats.ops_before,
1210 stats.ops_after
1211 );
1212 assert!(stats.iterations >= 1);
1213 assert!(stats.iterations <= RUN_ALL_MAX_ITERS);
1214 assert!(stats.converged, "pipeline must converge");
1215 }
1216
1217 #[test]
1218 fn optimization_stats_format_short_includes_all_fields() {
1219 let s = OptimizationStats {
1220 ops_before: 11,
1221 ops_after: 3,
1222 bindings_before: 3,
1223 bindings_after: 1,
1224 literals_before: 4,
1225 literals_after: 2,
1226 iterations: 2,
1227 converged: true,
1228 };
1229 let f = s.format_short();
1230 assert!(f.contains("ops 11→3 (-8)"));
1231 assert!(f.contains("bindings 3→1 (-2)"));
1232 assert!(f.contains("iters 2"));
1233 assert!(f.contains("converged"));
1234 }
1235
1236 #[test]
1237 fn optimization_stats_merge_is_associative() {
1238 let a = OptimizationStats {
1239 ops_before: 10,
1240 ops_after: 3,
1241 bindings_before: 2,
1242 bindings_after: 1,
1243 literals_before: 5,
1244 literals_after: 2,
1245 iterations: 2,
1246 converged: true,
1247 };
1248 let b = OptimizationStats {
1249 ops_before: 7,
1250 ops_after: 4,
1251 bindings_before: 1,
1252 bindings_after: 1,
1253 literals_before: 3,
1254 literals_after: 3,
1255 iterations: 1,
1256 converged: true,
1257 };
1258 let c = OptimizationStats {
1259 ops_before: 5,
1260 ops_after: 2,
1261 bindings_before: 1,
1262 bindings_after: 0,
1263 literals_before: 2,
1264 literals_after: 1,
1265 iterations: 1,
1266 converged: true,
1267 };
1268
1269 let mut left = a;
1270 left.merge(b);
1271 left.merge(c);
1272
1273 let mut bc = b;
1274 bc.merge(c);
1275 let mut right = a;
1276 right.merge(bc);
1277
1278 assert_eq!(left, right);
1279 }
1280
1281 #[test]
1282 fn optimization_stats_merge_aggregates() {
1283 let mut acc = OptimizationStats::zero();
1284 acc.merge(OptimizationStats {
1285 ops_before: 10,
1286 ops_after: 3,
1287 bindings_before: 2,
1288 bindings_after: 1,
1289 literals_before: 5,
1290 literals_after: 2,
1291 iterations: 2,
1292 converged: true,
1293 });
1294 acc.merge(OptimizationStats {
1295 ops_before: 7,
1296 ops_after: 4,
1297 bindings_before: 1,
1298 bindings_after: 1,
1299 literals_before: 3,
1300 literals_after: 3,
1301 iterations: 1,
1302 converged: false,
1303 });
1304 assert_eq!(acc.ops_before, 17);
1305 assert_eq!(acc.ops_after, 7);
1306 assert_eq!(acc.iterations, 3);
1307 assert!(!acc.converged); }
1309
1310 #[test]
1311 fn optimization_stats_zero_is_identity() {
1312 let s = OptimizationStats {
1313 ops_before: 5,
1314 ops_after: 3,
1315 bindings_before: 1,
1316 bindings_after: 1,
1317 literals_before: 2,
1318 literals_after: 1,
1319 iterations: 2,
1320 converged: true,
1321 };
1322 let mut acc = OptimizationStats::zero();
1323 acc.merge(s);
1324 assert_eq!(acc, s);
1325 }
1326
1327 #[test]
1328 fn optimization_stats_format_short_marks_stopped() {
1329 let s = OptimizationStats {
1330 ops_before: 5,
1331 ops_after: 5,
1332 bindings_before: 1,
1333 bindings_after: 1,
1334 literals_before: 1,
1335 literals_after: 1,
1336 iterations: 4,
1337 converged: false,
1338 };
1339 assert!(s.format_short().contains("stopped"));
1340 }
1341
1342 #[test]
1343 fn run_all_with_stats_reports_no_change_when_already_optimal() {
1344 let desc = KernelDescriptor {
1345 id: "minimal".into(),
1346 bindings: BindingLayout {
1347 slots: vec![crate::BindingSlot {
1348 slot: 0,
1349 element_type: vyre_foundation::ir::DataType::U32,
1350 element_count: None,
1351 memory_class: crate::MemoryClass::Global,
1352 visibility: crate::BindingVisibility::ReadWrite,
1353 name: "buf".into(),
1354 }],
1355 },
1356 dispatch: Dispatch::new(1, 1, 1),
1357 body: KernelBody {
1358 ops: vec![
1359 KernelOp {
1360 kind: KernelOpKind::Literal,
1361 operands: vec![0],
1362 result: Some(0),
1363 },
1364 KernelOp {
1365 kind: KernelOpKind::Literal,
1366 operands: vec![1],
1367 result: Some(1),
1368 },
1369 KernelOp {
1370 kind: KernelOpKind::StoreGlobal,
1371 operands: vec![0, 0, 1],
1372 result: None,
1373 },
1374 ],
1375 child_bodies: vec![],
1376 literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
1377 },
1378 };
1379 let (_out, stats) = run_all_with_stats(&desc);
1380 assert_eq!(stats.ops_before, stats.ops_after);
1381 assert_eq!(stats.bindings_before, stats.bindings_after);
1382 assert_eq!(stats.iterations, 1);
1383 assert!(stats.converged);
1384 }
1385
1386 #[test]
1387 fn run_all_with_stats_reports_dropped_bindings() {
1388 let desc = KernelDescriptor {
1389 id: "with_unused".into(),
1390 bindings: BindingLayout {
1391 slots: vec![
1392 crate::BindingSlot {
1393 slot: 0,
1394 element_type: vyre_foundation::ir::DataType::U32,
1395 element_count: None,
1396 memory_class: crate::MemoryClass::Global,
1397 visibility: crate::BindingVisibility::ReadWrite,
1398 name: "used".into(),
1399 },
1400 crate::BindingSlot {
1401 slot: 9,
1402 element_type: vyre_foundation::ir::DataType::U32,
1403 element_count: None,
1404 memory_class: crate::MemoryClass::Global,
1405 visibility: crate::BindingVisibility::ReadOnly,
1410 name: "unused".into(),
1411 },
1412 ],
1413 },
1414 dispatch: Dispatch::new(1, 1, 1),
1415 body: KernelBody {
1416 ops: vec![
1417 KernelOp {
1418 kind: KernelOpKind::Literal,
1419 operands: vec![0],
1420 result: Some(0),
1421 },
1422 KernelOp {
1423 kind: KernelOpKind::Literal,
1424 operands: vec![1],
1425 result: Some(1),
1426 },
1427 KernelOp {
1428 kind: KernelOpKind::StoreGlobal,
1429 operands: vec![0, 0, 1],
1430 result: None,
1431 },
1432 ],
1433 child_bodies: vec![],
1434 literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
1435 },
1436 };
1437 let (_out, stats) = run_all_with_stats(&desc);
1438 assert_eq!(stats.bindings_before, 2);
1444 assert_eq!(stats.bindings_after, 2);
1445 assert_eq!(stats.bindings_dropped(), 0);
1446 }
1447}
1448
1449#[cfg(test)]
1450mod value_differential {
1451 use super::rhs_lit_chain::test_support::{
1469 binop, descriptor_with, empty_body, lit_u32, nonliteral_source,
1470 };
1471 use crate::{KernelDescriptor, KernelOp, KernelOpKind, LiteralValue};
1472 use std::collections::HashMap;
1473 use vyre_foundation::ir::BinOp;
1474
1475 fn apply_u32(op: BinOp, l: u32, r: u32) -> u32 {
1479 match op {
1480 BinOp::Add => l.wrapping_add(r),
1481 BinOp::Sub => l.wrapping_sub(r),
1482 BinOp::Mul => l.wrapping_mul(r),
1483 BinOp::Div => {
1484 if r == 0 {
1485 0
1486 } else {
1487 l / r
1488 }
1489 }
1490 BinOp::Mod => {
1491 if r == 0 {
1492 0
1493 } else {
1494 l % r
1495 }
1496 }
1497 BinOp::BitAnd => l & r,
1498 BinOp::BitOr => l | r,
1499 BinOp::BitXor => l ^ r,
1500 BinOp::Shl => l.wrapping_shl(r & 31),
1501 BinOp::Shr => l.wrapping_shr(r & 31),
1502 other => panic!("value_differential evaluator: unhandled BinOp {other:?}"),
1503 }
1504 }
1505
1506 fn eval_id(
1510 id: u32,
1511 producers: &HashMap<u32, &KernelOp>,
1512 literals: &[LiteralValue],
1513 gid: u32,
1514 memo: &mut HashMap<u32, u32>,
1515 ) -> u32 {
1516 if let Some(v) = memo.get(&id) {
1517 return *v;
1518 }
1519 let op = producers
1520 .get(&id)
1521 .unwrap_or_else(|| panic!("value_differential: result id {id} has no producer"));
1522 let value = match &op.kind {
1523 KernelOpKind::GlobalInvocationId => gid,
1524 KernelOpKind::Literal => {
1525 match literals
1526 .get(op.operands[0] as usize)
1527 .expect("literal pool index in range")
1528 {
1529 LiteralValue::U32(v) => *v,
1530 other => panic!("value_differential: non-u32 literal {other:?}"),
1531 }
1532 }
1533 KernelOpKind::Copy => eval_id(op.operands[0], producers, literals, gid, memo),
1534 KernelOpKind::BinOpKind(b) => {
1535 let l = eval_id(op.operands[0], producers, literals, gid, memo);
1536 let r = eval_id(op.operands[1], producers, literals, gid, memo);
1537 apply_u32(*b, l, r)
1538 }
1539 other => panic!("value_differential evaluator: unhandled op kind {other:?}"),
1540 };
1541 memo.insert(id, value);
1542 value
1543 }
1544
1545 fn eval_scalar(desc: &KernelDescriptor, output_id: u32, gid: u32) -> u32 {
1546 let mut producers: HashMap<u32, &KernelOp> = HashMap::new();
1547 for op in &desc.body.ops {
1548 if let Some(rid) = op.result {
1549 producers.insert(rid, op);
1550 }
1551 }
1552 let mut memo = HashMap::new();
1553 eval_id(output_id, &producers, &desc.body.literals, gid, &mut memo)
1554 }
1555
1556 fn chain(op: BinOp, a: u32, b: u32) -> KernelDescriptor {
1560 let mut body = empty_body();
1561 nonliteral_source(&mut body, 0);
1562 lit_u32(&mut body, a, 1);
1563 binop(&mut body, op, 0, 1, 2);
1564 lit_u32(&mut body, b, 3);
1565 binop(&mut body, op, 2, 3, 4);
1566 descriptor_with("value_differential", body)
1567 }
1568
1569 const PROBES: &[u32] = &[
1570 0,
1571 1,
1572 2,
1573 3,
1574 7,
1575 8,
1576 255,
1577 256,
1578 0x0001_0000,
1579 0x5555_5555,
1580 0xAAAA_AAAA,
1581 0x8000_0000,
1582 0x7FFF_FFFF,
1583 0xFFFF_FFFF,
1584 ];
1585
1586 #[test]
1587 fn evaluator_is_validated_on_known_values() {
1588 assert_eq!(eval_scalar(&chain(BinOp::Add, 3, 5), 4, 10), 18); assert_eq!(eval_scalar(&chain(BinOp::Mul, 3, 5), 4, 4), 60); assert_eq!(eval_scalar(&chain(BinOp::Div, 3, 5), 4, 100), 6); assert_eq!(eval_scalar(&chain(BinOp::Sub, 3, 5), 4, 20), 12); }
1593
1594 #[test]
1595 fn arithmetic_chain_combines_preserve_value_for_every_input() {
1596 type Rewrite = fn(&KernelDescriptor) -> KernelDescriptor;
1597 let cases: &[(&str, Rewrite, BinOp)] = &[
1598 (
1599 "add_combine",
1600 super::arithmetic_combine::add_combine::add_combine,
1601 BinOp::Add,
1602 ),
1603 (
1604 "mul_combine",
1605 super::arithmetic_combine::mul_combine::mul_combine,
1606 BinOp::Mul,
1607 ),
1608 ("sub_combine", super::sub_combine::sub_combine, BinOp::Sub),
1609 ("div_combine", super::div_combine::div_combine, BinOp::Div),
1610 ];
1611 for (name, rewrite, op) in cases {
1615 let original = chain(*op, 3, 5);
1616 let rewritten = rewrite(&original);
1617 for &gid in PROBES {
1618 let before = eval_scalar(&original, 4, gid);
1619 let after = eval_scalar(&rewritten, 4, gid);
1620 assert_eq!(
1621 before, after,
1622 "{name} changed the computed value of (gid {op:?} 3) {op:?} 5 \
1623 at gid={gid:#010x}: {before} -> {after}"
1624 );
1625 }
1626 }
1627 }
1628}