1use std::fmt;
7
8use nxpu_ir::{
9 AddressSpace, Arena, ArraySize, BinaryOp, Expression, GlobalVariable, Handle, Literal,
10 MathFunction, Module, Scalar, ScalarKind, Statement, StorageAccess, Type, TypeInner,
11 UniqueArena,
12};
13
14pub mod data_type {
16 pub const FLOAT: i32 = 1;
17 pub const UINT8: i32 = 2;
18 pub const INT8: i32 = 3;
19 pub const INT32: i32 = 6;
20 pub const INT64: i32 = 7;
21 pub const BOOL: i32 = 9;
22 pub const FLOAT16: i32 = 10;
23 pub const UINT32: i32 = 12;
24 pub const BFLOAT16: i32 = 16;
25}
26
27#[derive(Debug, thiserror::Error)]
29pub enum AnalysisError {
30 #[error("no entry points in module")]
31 NoEntryPoints,
32 #[error("entry point index {0} out of range")]
33 EntryPointOutOfRange(usize),
34 #[error("unsupported pattern: {0}")]
35 UnsupportedPattern(String),
36 #[error("missing uniform params struct")]
37 MissingParams,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum TensorRole {
43 Input,
44 Output,
45}
46
47impl fmt::Display for TensorRole {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(match self {
50 Self::Input => "Input",
51 Self::Output => "Output",
52 })
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct TensorBinding {
59 pub handle: Handle<GlobalVariable>,
61 pub name: String,
63 pub elem_type: i32,
65 pub role: TensorRole,
67}
68
69impl fmt::Display for TensorBinding {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{} ({})", self.name, self.role)
72 }
73}
74
75#[derive(Debug, Clone)]
83pub struct ScalarBinding {
84 pub name: String,
86 pub elem_type: i32,
88}
89
90impl fmt::Display for ScalarBinding {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{} (Scalar)", self.name)
93 }
94}
95
96#[derive(Debug, Clone)]
98pub enum ChainOperand {
99 Tensor(TensorBinding),
101 Scalar(ScalarBinding),
103}
104
105impl fmt::Display for ChainOperand {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::Tensor(t) => write!(f, "{t}"),
109 Self::Scalar(s) => write!(f, "{s}"),
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
120pub struct ChainStep {
121 pub op: ElementWiseOp,
123 pub operand: ChainOperand,
125}
126
127#[derive(Debug, Clone)]
129pub struct MatMulShape {
130 pub m: String,
132 pub n: String,
134 pub k: String,
136}
137
138impl fmt::Display for MatMulShape {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "MatMul({}x{} * {}x{})", self.m, self.k, self.k, self.n)
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum ElementWiseOp {
147 Add,
148 Sub,
149 Mul,
150 Div,
151}
152
153impl fmt::Display for ElementWiseOp {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str(self.op_name())
156 }
157}
158
159impl ElementWiseOp {
160 pub fn op_name(self) -> &'static str {
162 match self {
163 Self::Add => "Add",
164 Self::Sub => "Sub",
165 Self::Mul => "Mul",
166 Self::Div => "Div",
167 }
168 }
169
170 pub fn is_commutative(self) -> bool {
177 matches!(self, Self::Add | Self::Mul)
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum ActivationOp {
184 Relu,
185 Sigmoid,
186 Tanh,
187 Softmax,
188 Gelu,
189 Silu,
190 Mish,
191}
192
193impl fmt::Display for ActivationOp {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 f.write_str(self.op_name())
196 }
197}
198
199impl ActivationOp {
200 pub fn op_name(self) -> &'static str {
202 match self {
203 Self::Relu => "Relu",
204 Self::Sigmoid => "Sigmoid",
205 Self::Tanh => "Tanh",
206 Self::Softmax => "Softmax",
207 Self::Gelu => "Gelu",
208 Self::Silu => "Silu",
209 Self::Mish => "Mish",
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum ReduceOp {
217 Sum,
218 Mean,
219 Max,
220 Min,
221}
222
223impl fmt::Display for ReduceOp {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(self.op_name())
226 }
227}
228
229impl ReduceOp {
230 pub fn op_name(self) -> &'static str {
232 match self {
233 Self::Sum => "ReduceSum",
234 Self::Mean => "ReduceMean",
235 Self::Max => "ReduceMax",
236 Self::Min => "ReduceMin",
237 }
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum PoolKind {
244 Max,
245 Avg,
246}
247
248impl fmt::Display for PoolKind {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.write_str(self.op_name())
251 }
252}
253
254impl PoolKind {
255 pub fn op_name(self) -> &'static str {
257 match self {
258 Self::Max => "MaxPool",
259 Self::Avg => "AveragePool",
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum NormType {
267 Batch,
268 Layer,
269}
270
271impl fmt::Display for NormType {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.write_str(match self {
274 Self::Batch => "Batch",
275 Self::Layer => "Layer",
276 })
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct Conv2DShape {
283 pub batch: String,
285 pub channels_in: String,
287 pub channels_out: String,
289 pub height: String,
291 pub width: String,
293 pub kernel_h: String,
295 pub kernel_w: String,
297 pub kernel_h_val: i64,
299 pub kernel_w_val: i64,
301 pub stride_h: i64,
303 pub stride_w: i64,
305 pub pad_h: i64,
307 pub pad_w: i64,
309 pub groups: i64,
311 pub dilation_h: i64,
313 pub dilation_w: i64,
315}
316
317impl fmt::Display for Conv2DShape {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 write!(
320 f,
321 "Conv2D({}x{}x{} k{}x{} g{} d{}x{})",
322 self.channels_in,
323 self.height,
324 self.width,
325 self.kernel_h,
326 self.kernel_w,
327 self.groups,
328 self.dilation_h,
329 self.dilation_w
330 )
331 }
332}
333
334#[derive(Debug, Clone)]
336pub struct PoolShape {
337 pub kernel_h: i64,
339 pub kernel_w: i64,
341 pub stride_h: i64,
343 pub stride_w: i64,
345}
346
347impl fmt::Display for PoolShape {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 write!(
350 f,
351 "Pool(k{}x{} s{}x{})",
352 self.kernel_h, self.kernel_w, self.stride_h, self.stride_w
353 )
354 }
355}
356
357#[derive(Debug, Clone)]
359pub enum KernelPattern {
360 MatMul {
362 inputs: [TensorBinding; 2],
363 output: TensorBinding,
364 shape: MatMulShape,
365 },
366 ElementWise {
368 op: ElementWiseOp,
369 inputs: [TensorBinding; 2],
370 output: TensorBinding,
371 dim_name: String,
372 },
373 ElementWiseChain {
394 base: TensorBinding,
396 cast: Option<i32>,
400 steps: Vec<ChainStep>,
402 output: TensorBinding,
403 dim_name: String,
404 },
405 Conv2D {
407 input: TensorBinding,
408 weight: TensorBinding,
409 output: TensorBinding,
410 bias: Option<TensorBinding>,
416 shape: Conv2DShape,
417 activation: Option<ActivationOp>,
426 },
427 Pool {
429 kind: PoolKind,
430 input: TensorBinding,
431 output: TensorBinding,
432 shape: PoolShape,
433 },
434 Activation {
436 op: ActivationOp,
437 input: TensorBinding,
438 output: TensorBinding,
439 dim_name: String,
440 },
441 Reduce {
443 op: ReduceOp,
444 input: TensorBinding,
445 output: TensorBinding,
446 axis: i64,
447 },
448 Transpose {
450 input: TensorBinding,
451 output: TensorBinding,
452 perm: Vec<i64>,
453 },
454 Reshape {
456 input: TensorBinding,
457 output: TensorBinding,
458 },
459 Normalization {
461 input: TensorBinding,
462 scale: TensorBinding,
463 bias: TensorBinding,
464 output: TensorBinding,
465 epsilon: f32,
466 norm_type: NormType,
467 },
468 Concat {
470 inputs: Vec<TensorBinding>,
471 output: TensorBinding,
472 axis: i64,
473 },
474 Split {
476 input: TensorBinding,
477 outputs: Vec<TensorBinding>,
478 axis: i64,
479 },
480 Attention {
482 query: TensorBinding,
483 key: TensorBinding,
484 value: TensorBinding,
485 output: TensorBinding,
486 d_k: String,
487 seq_len: String,
488 num_heads: u32,
490 num_kv_heads: u32,
492 causal: bool,
494 },
495 Gather {
497 data: TensorBinding,
498 indices: TensorBinding,
499 output: TensorBinding,
500 axis: i64,
501 },
502 Scatter {
504 data: TensorBinding,
505 indices: TensorBinding,
506 updates: TensorBinding,
507 output: TensorBinding,
508 axis: i64,
509 },
510 QuantizedMatMul {
534 input: TensorBinding,
536 weight: TensorBinding,
538 scale: TensorBinding,
540 bias: Option<TensorBinding>,
546 output: TensorBinding,
547 shape: MatMulShape,
548 },
549 Unknown { reason: String },
551}
552
553impl fmt::Display for KernelPattern {
554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555 match self {
556 Self::MatMul { shape, .. } => write!(f, "{shape}"),
557 Self::QuantizedMatMul { shape, bias, .. } => {
558 let fused = if bias.is_some() { " + bias" } else { "" };
559 write!(f, "Quantized{shape} (int8, per-channel scale){fused}")
560 }
561 Self::ElementWise { op, .. } => write!(f, "{op}"),
562 Self::ElementWiseChain { cast, steps, .. } => f.write_str(&chain_summary(*cast, steps)),
563 Self::Conv2D { shape, .. } => write!(f, "{shape}"),
564 Self::Pool { kind, shape, .. } => {
565 write!(
566 f,
567 "{}(k{}x{} s{}x{})",
568 kind, shape.kernel_h, shape.kernel_w, shape.stride_h, shape.stride_w
569 )
570 }
571 Self::Activation { op, .. } => write!(f, "{op}"),
572 Self::Reduce { op, axis, .. } => write!(f, "{op}(axis={axis})"),
573 Self::Transpose { perm, .. } => write!(f, "Transpose({perm:?})"),
574 Self::Reshape { .. } => f.write_str("Reshape"),
575 Self::Normalization {
576 epsilon, norm_type, ..
577 } => write!(f, "{norm_type}Normalization(eps={epsilon})"),
578 Self::Concat { axis, .. } => write!(f, "Concat(axis={axis})"),
579 Self::Split { axis, .. } => write!(f, "Split(axis={axis})"),
580 Self::Attention {
581 d_k,
582 seq_len,
583 num_heads,
584 causal,
585 ..
586 } => {
587 let mask = if *causal { ", causal" } else { "" };
588 write!(
589 f,
590 "Attention(d_k={d_k}, seq_len={seq_len}, heads={num_heads}{mask})"
591 )
592 }
593 Self::Gather { axis, .. } => write!(f, "Gather(axis={axis})"),
594 Self::Scatter { axis, .. } => write!(f, "Scatter(axis={axis})"),
595 Self::Unknown { reason } => write!(f, "Unknown({reason})"),
596 }
597 }
598}
599
600pub fn chain_op_names(cast: Option<i32>, steps: &[ChainStep]) -> Vec<&'static str> {
609 let mut parts: Vec<&'static str> = Vec::with_capacity(steps.len() + 1);
610 if cast.is_some() {
611 parts.push("Cast");
612 }
613 parts.extend(steps.iter().map(|s| s.op.op_name()));
614 parts
615}
616
617pub fn chain_summary(cast: Option<i32>, steps: &[ChainStep]) -> String {
624 chain_op_names(cast, steps).join("+")
625}
626
627#[derive(Debug, Clone)]
629pub struct EmbeddedWeight {
630 pub name: String,
632 pub dims: Vec<i64>,
634 pub data: Vec<f32>,
636}
637
638pub fn extract_embedded_weights(module: &Module) -> Vec<EmbeddedWeight> {
643 let mut weights = Vec::new();
644 for (handle, gv) in module.global_variables.iter() {
645 let Some(init_handle) = gv.init else {
646 continue;
647 };
648 let name = gv
649 .name
650 .clone()
651 .unwrap_or_else(|| format!("weight_{}", handle.index()));
652 let dims = type_dims(gv.ty, &module.types);
653 let Some(data) =
654 eval_const_expr_f32(init_handle, &module.global_expressions, &module.types)
655 else {
656 continue;
657 };
658 weights.push(EmbeddedWeight { name, dims, data });
659 }
660 weights
661}
662
663fn eval_const_expr_f32(
665 handle: Handle<Expression>,
666 exprs: &Arena<Expression>,
667 types: &UniqueArena<Type>,
668) -> Option<Vec<f32>> {
669 match exprs.try_get(handle)? {
670 Expression::Literal(Literal::F32(v)) => Some(vec![*v]),
671 Expression::Literal(Literal::I32(v)) => Some(vec![*v as f32]),
672 Expression::Literal(Literal::U32(v)) => Some(vec![*v as f32]),
673 Expression::Compose { components, .. } => {
674 let mut result = Vec::new();
675 for &comp in components {
676 result.extend(eval_const_expr_f32(comp, exprs, types)?);
677 }
678 Some(result)
679 }
680 Expression::ZeroValue(ty) => {
681 let dims = type_dims(*ty, types);
682 let count = dims.iter().product::<i64>().max(1) as usize;
683 Some(vec![0.0; count])
684 }
685 _ => None,
686 }
687}
688
689fn type_dims(ty_handle: Handle<Type>, types: &UniqueArena<Type>) -> Vec<i64> {
691 match &types[ty_handle].inner {
692 TypeInner::Scalar(_) => vec![],
693 TypeInner::Vector { size, .. } => vec![*size as i64],
694 TypeInner::Array {
695 size: ArraySize::Constant(n),
696 ..
697 } => vec![*n as i64],
698 _ => vec![],
699 }
700}
701
702fn is_integer_member(module: &Module, m: &nxpu_ir::StructMember) -> bool {
705 matches!(
706 module.types[m.ty].inner,
707 TypeInner::Scalar(Scalar {
708 kind: ScalarKind::Uint | ScalarKind::Sint,
709 ..
710 })
711 )
712}
713
714fn detect_num_heads(shape_names: &[String], exprs: &Arena<Expression>) -> u32 {
716 for name in shape_names.iter() {
718 let lower = name.to_lowercase();
719 if lower == "num_heads" || lower == "n_head" || lower == "n_heads" || lower == "nhead" {
720 return find_division_literal(exprs).unwrap_or(1);
723 }
724 }
725 if shape_names.iter().any(|n| n == "H") {
727 return find_division_literal(exprs).unwrap_or(1);
728 }
729 1
730}
731
732#[allow(clippy::collapsible_if)] fn find_division_literal(exprs: &Arena<Expression>) -> Option<u32> {
735 for (_, expr) in exprs.iter() {
736 if let Expression::Binary {
737 op: BinaryOp::Divide,
738 right,
739 ..
740 } = expr
741 {
742 if let Some(Expression::Literal(Literal::U32(n))) = exprs.try_get(*right) {
743 if *n > 1 {
744 return Some(*n);
745 }
746 }
747 }
748 }
749 None
750}
751
752fn detect_causal_mask(body: &[Statement], exprs: &Arena<Expression>) -> bool {
762 has_causal_if_in_loop(body, exprs)
763}
764
765#[allow(clippy::collapsible_if)] fn has_causal_if_in_loop(body: &[Statement], exprs: &Arena<Expression>) -> bool {
770 for stmt in body {
771 if let Statement::Loop {
772 body, continuing, ..
773 } = stmt
774 {
775 let mut saw_non_emit = false;
778 for s in body.iter() {
779 match s {
780 Statement::Emit(_) => {}
781 Statement::If {
782 condition,
783 accept,
784 reject,
785 } => {
786 if !saw_non_emit
789 && (matches!(accept.as_slice(), [Statement::Break])
790 || matches!(reject.as_slice(), [Statement::Break]))
791 {
792 saw_non_emit = true;
793 continue;
794 }
795 saw_non_emit = true;
796 if is_causal_guard(*condition, accept, reject, exprs) {
797 return true;
798 }
799 }
800 _ => {
801 saw_non_emit = true;
802 }
803 }
804 }
805 for s in continuing.iter() {
807 if let Statement::If {
808 condition,
809 accept,
810 reject,
811 } = s
812 {
813 if is_causal_guard(*condition, accept, reject, exprs) {
814 return true;
815 }
816 }
817 }
818 if has_causal_if_in_loop(body, exprs) || has_causal_if_in_loop(continuing, exprs) {
820 return true;
821 }
822 }
823 }
824 false
825}
826
827fn is_causal_guard(
830 condition: Handle<Expression>,
831 accept: &[Statement],
832 reject: &[Statement],
833 exprs: &Arena<Expression>,
834) -> bool {
835 let is_comparison = match exprs.try_get(condition) {
836 Some(Expression::Binary { op, .. }) => matches!(
837 op,
838 BinaryOp::Greater | BinaryOp::Less | BinaryOp::GreaterEqual | BinaryOp::LessEqual
839 ),
840 _ => false,
841 };
842 if !is_comparison {
843 return false;
844 }
845 block_stores_large_negative(accept, exprs) || block_stores_large_negative(reject, exprs)
846}
847
848fn block_stores_large_negative(body: &[Statement], exprs: &Arena<Expression>) -> bool {
850 for stmt in body {
851 match stmt {
852 Statement::Store { value, .. } if expr_is_large_negative(*value, exprs) => {
853 return true;
854 }
855 Statement::If { accept, reject, .. }
856 if block_stores_large_negative(accept, exprs)
857 || block_stores_large_negative(reject, exprs) =>
858 {
859 return true;
860 }
861 _ => {}
862 }
863 }
864 false
865}
866
867fn expr_is_large_negative(handle: Handle<Expression>, exprs: &Arena<Expression>) -> bool {
869 match exprs.try_get(handle) {
870 Some(Expression::Literal(Literal::F32(v))) => *v < -1e6,
871 Some(Expression::Unary {
872 op: nxpu_ir::UnaryOp::Negate,
873 expr,
874 }) => {
875 matches!(exprs.try_get(*expr), Some(Expression::Literal(Literal::F32(v))) if *v > 1e6)
877 }
878 _ => false,
879 }
880}
881
882pub fn pattern_op_names(pattern: &KernelPattern) -> Vec<String> {
892 match pattern {
893 KernelPattern::ElementWiseChain { cast, steps, .. } => chain_op_names(*cast, steps)
897 .into_iter()
898 .map(String::from)
899 .collect(),
900 KernelPattern::MatMul { .. } => vec!["MatMul".into()],
901 KernelPattern::QuantizedMatMul { bias, .. } => {
909 let mut names = vec![
910 "Transpose".to_string(),
911 "DequantizeLinear".to_string(),
912 "MatMul".to_string(),
913 ];
914 if bias.is_some() {
915 names.push("Add".to_string());
916 }
917 names
918 }
919 KernelPattern::ElementWise { op, .. } => vec![op.op_name().into()],
920 KernelPattern::Conv2D { .. } => vec!["Conv".into()],
921 KernelPattern::Pool { kind, .. } => vec![kind.op_name().into()],
922 KernelPattern::Activation { op, .. } => vec![op.op_name().into()],
923 KernelPattern::Reduce { op, .. } => vec![op.op_name().into()],
924 KernelPattern::Transpose { .. } => vec!["Transpose".into()],
925 KernelPattern::Reshape { .. } => vec!["Reshape".into()],
926 KernelPattern::Normalization { .. } => vec!["BatchNormalization".into()],
927 KernelPattern::Concat { .. } => vec!["Concat".into()],
928 KernelPattern::Split { .. } => vec!["Split".into()],
929 KernelPattern::Attention { .. } => vec!["Attention".into()],
930 KernelPattern::Gather { .. } => vec!["Gather".into()],
931 KernelPattern::Scatter { .. } => vec!["ScatterND".into()],
932 KernelPattern::Unknown { .. } => vec!["Unknown".into()],
933 }
934}
935
936#[cfg(test)]
937mod pattern_op_names_tests {
938 use super::*;
939
940 fn binding(name: &str) -> TensorBinding {
941 let mut arena: Arena<GlobalVariable> = Arena::new();
942 let ty = {
943 let mut types = UniqueArena::default();
944 types.insert(Type {
945 name: None,
946 inner: TypeInner::Scalar(Scalar::F32),
947 })
948 };
949 let handle = arena.append(GlobalVariable {
950 name: Some(name.into()),
951 space: AddressSpace::Uniform,
952 binding: None,
953 ty,
954 init: None,
955 layout: None,
956 });
957 TensorBinding {
958 handle,
959 name: name.into(),
960 elem_type: data_type::FLOAT,
961 role: TensorRole::Input,
962 }
963 }
964
965 #[test]
969 fn every_pattern_names_something() {
970 let cases: Vec<KernelPattern> = vec![
971 KernelPattern::Transpose {
972 input: binding("a"),
973 output: binding("b"),
974 perm: vec![1, 0],
975 },
976 KernelPattern::Reshape {
977 input: binding("a"),
978 output: binding("b"),
979 },
980 KernelPattern::Reduce {
981 op: ReduceOp::Sum,
982 input: binding("a"),
983 output: binding("b"),
984 axis: 0,
985 },
986 KernelPattern::Unknown {
987 reason: "nothing".into(),
988 },
989 ];
990 for pattern in cases {
991 let names = pattern_op_names(&pattern);
992 assert!(
993 !names.is_empty() && names.iter().all(|n| !n.is_empty()),
994 "{pattern:?} named nothing"
995 );
996 }
997 }
998}
999
1000pub fn classify_entry_point(
1002 module: &Module,
1003 ep_index: usize,
1004) -> Result<KernelPattern, AnalysisError> {
1005 if module.entry_points.is_empty() {
1006 return Err(AnalysisError::NoEntryPoints);
1007 }
1008 let ep = module
1009 .entry_points
1010 .get(ep_index)
1011 .ok_or(AnalysisError::EntryPointOutOfRange(ep_index))?;
1012
1013 let mut inputs: Vec<(Handle<GlobalVariable>, &GlobalVariable)> = Vec::new();
1015 let mut outputs: Vec<(Handle<GlobalVariable>, &GlobalVariable)> = Vec::new();
1016 let mut init_globals: Vec<(Handle<GlobalVariable>, &GlobalVariable)> = Vec::new();
1017 let mut params_members: Option<&[nxpu_ir::StructMember]> = None;
1018 let mut params_global: Option<Handle<GlobalVariable>> = None;
1019
1020 for (handle, gv) in module.global_variables.iter() {
1021 match &gv.space {
1022 AddressSpace::Storage { access } => {
1023 if access.contains(StorageAccess::STORE) {
1024 outputs.push((handle, gv));
1025 } else {
1026 inputs.push((handle, gv));
1027 }
1028 }
1029 AddressSpace::Uniform => {
1030 if let TypeInner::Struct { members, .. } = &module.types[gv.ty].inner {
1031 params_members = Some(members);
1032 params_global = Some(handle);
1033 }
1034 }
1035 _ => {
1036 if gv.init.is_some() {
1037 init_globals.push((handle, gv));
1038 }
1039 }
1040 }
1041 }
1042
1043 inputs.sort_by_key(|(_, gv)| gv.binding.map(|b| b.binding).unwrap_or(u32::MAX));
1045
1046 if outputs.is_empty() {
1047 return Err(AnalysisError::UnsupportedPattern(
1048 "expected at least 1 output storage buffer".into(),
1049 ));
1050 }
1051
1052 let shape_names: Vec<String> = params_members
1057 .map(|members| {
1058 members
1059 .iter()
1060 .filter(|m| is_integer_member(module, m))
1061 .filter_map(|m| m.name.clone())
1062 .collect()
1063 })
1064 .unwrap_or_default();
1065
1066 let param_names: Vec<String> = params_members
1070 .map(|members| {
1071 members
1072 .iter()
1073 .map(|m| m.name.clone().unwrap_or_default())
1074 .collect()
1075 })
1076 .unwrap_or_default();
1077
1078 let has_loop = has_loop(&ep.function.body);
1079 let num_inputs = inputs.len();
1080
1081 if num_inputs == 1 {
1083 let input = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1084
1085 if outputs.len() >= 2 {
1099 let output_handles: Vec<Handle<GlobalVariable>> =
1100 outputs.iter().map(|(h, _)| *h).collect();
1101 let (written, computed) = survey_output_stores(
1102 &ep.function.body,
1103 &ep.function.expressions,
1104 inputs[0].0,
1105 &output_handles,
1106 );
1107 let every_output_is_a_copy =
1108 computed.is_empty() && written.len() == output_handles.len();
1109
1110 if every_output_is_a_copy && has_if_statement(&ep.function.body) {
1111 let out_bindings: Vec<TensorBinding> = outputs
1112 .iter()
1113 .map(|(h, gv)| make_binding(module, *h, gv, TensorRole::Output))
1114 .collect();
1115 let axis =
1116 infer_split_axis(&ep.function.body, &ep.function.expressions, &shape_names);
1117 return Ok(KernelPattern::Split {
1118 input,
1119 outputs: out_bindings,
1120 axis,
1121 });
1122 }
1123
1124 return Ok(KernelPattern::Unknown {
1125 reason: multi_output_refusal(module, &outputs, inputs[0].1, &written, &computed),
1126 });
1127 }
1128
1129 let output = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1130
1131 if !has_loop {
1132 if let Some(act_op) = find_store_activation(&ep.function.body, &ep.function.expressions)
1134 {
1135 let dim_name = shape_names.first().cloned().unwrap_or_else(|| "N".into());
1136 return Ok(KernelPattern::Activation {
1137 op: act_op,
1138 input,
1139 output,
1140 dim_name,
1141 });
1142 }
1143
1144 let ew_op = match find_store_value_op(&ep.function.body, &ep.function.expressions) {
1146 Some(StoreValueOp::Binary(op)) => Some(op),
1147 Some(StoreValueOp::MultiplyAdd) | None => None,
1149 };
1150 if let (Some(ew_op), Some(&(wh, wgv))) = (ew_op, init_globals.first()) {
1151 let weight = make_binding(module, wh, wgv, TensorRole::Input);
1152 let dim_name = shape_names.first().cloned().unwrap_or_else(|| "N".into());
1153 return Ok(KernelPattern::ElementWise {
1154 op: ew_op,
1155 inputs: [input, weight],
1156 output,
1157 dim_name,
1158 });
1159 }
1160
1161 if let Some(perm) = detect_permutation(
1165 &ep.function.body,
1166 &ep.function.expressions,
1167 inputs[0].0,
1168 outputs[0].0,
1169 ) {
1170 return Ok(KernelPattern::Transpose {
1171 input,
1172 output: make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output),
1173 perm,
1174 });
1175 }
1176
1177 if let Some(pattern) = match_elementwise_chain(
1185 module,
1186 &ep.function.body,
1187 &ep.function.expressions,
1188 &outputs,
1189 &shape_names,
1190 ) {
1191 return Ok(pattern);
1192 }
1193
1194 return Ok(KernelPattern::Unknown {
1196 reason: "single input, no loop, no recognized activation function".into(),
1197 });
1198 }
1199
1200 if outputs.len() == 1
1208 && detect_softmax(&ep.function.body, &ep.function.expressions, outputs[0].0)
1209 {
1210 let dim_name = shape_names.last().cloned().unwrap_or_else(|| "N".into());
1211 return Ok(KernelPattern::Activation {
1212 op: ActivationOp::Softmax,
1213 input,
1214 output,
1215 dim_name,
1216 });
1217 }
1218
1219 let pool_bounds = extract_loop_bound_literals(&ep.function.body, &ep.function.expressions);
1226 if shape_names.len() >= 4 && !pool_bounds.is_empty() {
1227 let pool_kind = if find_store_math_fun(
1228 &ep.function.body,
1229 &ep.function.expressions,
1230 MathFunction::Max,
1231 ) {
1232 PoolKind::Max
1233 } else {
1234 PoolKind::Avg
1235 };
1236 let bounds = pool_bounds;
1237 let strides = extract_multiply_literals(&ep.function.expressions);
1238 let kh_u = bounds[0];
1239 let kw_u = bounds.get(1).copied().unwrap_or(kh_u);
1240 let sh_u = strides.first().copied().unwrap_or(kh_u);
1241 let sw_u = strides.get(1).copied().unwrap_or(sh_u);
1242 let kh = kh_u as i64;
1243 let kw = kw_u as i64;
1244 let sh = sh_u as i64;
1245 let sw = sw_u as i64;
1246 let pool_shape = PoolShape {
1247 kernel_h: kh,
1248 kernel_w: kw,
1249 stride_h: sh,
1250 stride_w: sw,
1251 };
1252 return Ok(KernelPattern::Pool {
1253 kind: pool_kind,
1254 input,
1255 output,
1256 shape: pool_shape,
1257 });
1258 }
1259
1260 let reduce_op = detect_reduce_op(&ep.function.body, &ep.function.expressions);
1262 let axis = if shape_names.len() >= 2 { 1 } else { 0 };
1263 return Ok(KernelPattern::Reduce {
1264 op: reduce_op,
1265 input,
1266 output,
1267 axis: axis as i64,
1268 });
1269 }
1270
1271 if num_inputs < 2 {
1273 return Err(AnalysisError::UnsupportedPattern(
1274 "expected at least 1 input storage buffer".into(),
1275 ));
1276 }
1277
1278 if num_inputs >= 3 {
1280 if let Some(pattern) = match_quantized_matmul(
1290 module,
1291 ep,
1292 &inputs,
1293 &outputs,
1294 params_global,
1295 ¶m_names,
1296 &shape_names,
1297 ) {
1298 return Ok(pattern);
1299 }
1300
1301 if has_loop
1307 && has_math_function_in_expressions(&ep.function.expressions, MathFunction::Exp)
1308 && has_math_function_in_expressions(&ep.function.expressions, MathFunction::Sqrt)
1309 {
1310 let query = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1311 let key = make_binding(module, inputs[1].0, inputs[1].1, TensorRole::Input);
1312 let value = make_binding(module, inputs[2].0, inputs[2].1, TensorRole::Input);
1313 let output = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1314 let seq_len = shape_names
1315 .first()
1316 .cloned()
1317 .unwrap_or_else(|| "seq_len".into());
1318 let d_k = shape_names.get(1).cloned().unwrap_or_else(|| "d_k".into());
1319 let num_heads = detect_num_heads(&shape_names, &ep.function.expressions);
1320 let causal = detect_causal_mask(&ep.function.body, &ep.function.expressions);
1321 return Ok(KernelPattern::Attention {
1322 query,
1323 key,
1324 value,
1325 output,
1326 d_k,
1327 seq_len,
1328 num_heads,
1329 num_kv_heads: num_heads, causal,
1331 });
1332 }
1333
1334 if num_inputs == 3
1342 && outputs.len() == 1
1343 && has_loop
1344 && (has_math_function_in_expressions(&ep.function.expressions, MathFunction::Sqrt)
1345 || has_math_function_in_expressions(
1346 &ep.function.expressions,
1347 MathFunction::InverseSqrt,
1348 ))
1349 && !has_math_function_in_expressions(&ep.function.expressions, MathFunction::Exp)
1350 && !has_nested_loop(&ep.function.body)
1351 {
1352 let input = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1353 let scale = make_binding(module, inputs[1].0, inputs[1].1, TensorRole::Input);
1354 let bias = make_binding(module, inputs[2].0, inputs[2].1, TensorRole::Input);
1355 let output = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1356 if shape_names
1364 .iter()
1365 .any(|n| n == "G" || n.eq_ignore_ascii_case("groups"))
1366 {
1367 return Ok(KernelPattern::Unknown {
1368 reason: "group normalization with a group count supplied at \
1369 runtime — the target formats need it as a static \
1370 attribute"
1371 .into(),
1372 });
1373 }
1374 let norm_type = if shape_names.len() <= 2 {
1375 NormType::Layer
1376 } else {
1377 NormType::Batch
1378 };
1379 return Ok(KernelPattern::Normalization {
1380 input,
1381 scale,
1382 bias,
1383 output,
1384 epsilon: 1e-5,
1385 norm_type,
1386 });
1387 }
1388
1389 let names_kernel = |n: &str| {
1405 shape_names.iter().any(|s| {
1406 s.eq_ignore_ascii_case(n) || s.eq_ignore_ascii_case(&format!("kernel_{n}"))
1407 })
1408 };
1409 if num_inputs == 3
1410 && outputs.len() == 1
1411 && has_loop
1412 && names_kernel("kh")
1413 && names_kernel("kw")
1414 && !names_kernel("kd")
1415 {
1416 let input = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1417 let weight = make_binding(module, inputs[1].0, inputs[1].1, TensorRole::Input);
1418 let bias = make_binding(module, inputs[2].0, inputs[2].1, TensorRole::Input);
1419 let output = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1420 let conv_shape = extract_conv2d_shape(
1421 &shape_names,
1422 Some(&ep.function.body),
1423 Some(&ep.function.expressions),
1424 );
1425 return Ok(KernelPattern::Conv2D {
1426 input,
1427 weight,
1428 output,
1429 bias: Some(bias),
1430 shape: conv_shape,
1431 activation: store_value_activation(&ep.function.body, &ep.function.expressions),
1432 });
1433 }
1434
1435 if num_inputs == 3 && outputs.len() == 1 && has_loop {
1439 let names = |n: &str| shape_names.iter().any(|s| s.eq_ignore_ascii_case(n));
1440 if names("kd") {
1441 return Ok(KernelPattern::Unknown {
1442 reason: "3-D convolution — the kernel window has three \
1443 spatial extents and Conv2D has two"
1444 .into(),
1445 });
1446 }
1447 if names("lout") && names("k") {
1448 return Ok(KernelPattern::Unknown {
1449 reason: "1-D or transposed convolution — one spatial \
1450 extent, which Conv2D cannot represent"
1451 .into(),
1452 });
1453 }
1454 }
1455
1456 let input_handles: Vec<Handle<GlobalVariable>> = inputs.iter().map(|(h, _)| *h).collect();
1457 let output_handles_3: Vec<Handle<GlobalVariable>> =
1458 outputs.iter().map(|(h, _)| *h).collect();
1459
1460 if num_inputs == 3
1467 && outputs.len() == 1
1468 && !has_loop
1469 && detect_indexed_write(
1470 &ep.function.body,
1471 &ep.function.expressions,
1472 &input_handles,
1473 &output_handles_3,
1474 )
1475 {
1476 let data = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1477 let indices = make_binding(module, inputs[1].0, inputs[1].1, TensorRole::Input);
1478 let updates = make_binding(module, inputs[2].0, inputs[2].1, TensorRole::Input);
1479 let output = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1480 return Ok(KernelPattern::Scatter {
1481 data,
1482 indices,
1483 updates,
1484 output,
1485 axis: 0,
1486 });
1487 }
1488
1489 if num_inputs == 3 && outputs.len() == 1 && !has_loop {
1495 if let Some(pattern) = match_elementwise_chain(
1496 module,
1497 &ep.function.body,
1498 &ep.function.expressions,
1499 &outputs,
1500 &shape_names,
1501 ) {
1502 return Ok(pattern);
1503 }
1504 return Ok(KernelPattern::Unknown {
1512 reason: "3 inputs combined per element, and not as a chain: a \
1513 chain step takes a whole tensor read at the index \
1514 written, or a scalar the host sets per dispatch, and \
1515 these operands are neither"
1516 .into(),
1517 });
1518 }
1519
1520 return Ok(KernelPattern::Unknown {
1522 reason: "3+ inputs but no recognized pattern (expected Attention, Normalization, or Scatter)".into(),
1523 });
1524 }
1525
1526 let input_a = make_binding(module, inputs[0].0, inputs[0].1, TensorRole::Input);
1528 let input_b = make_binding(module, inputs[1].0, inputs[1].1, TensorRole::Input);
1529 let output_c = make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output);
1530
1531 let input_handles = [inputs[0].0, inputs[1].0];
1539 let output_handles: Vec<Handle<GlobalVariable>> = outputs.iter().map(|(h, _)| *h).collect();
1540
1541 if outputs
1545 .iter()
1546 .any(|(_, gv)| is_atomic_buffer(module, gv.ty))
1547 {
1548 return Ok(KernelPattern::Unknown {
1549 reason: "the output is an array of atomics, so writes to the same slot \
1550 accumulate instead of overwriting — an accumulating scatter. \
1551 Scatter has no reduction mode and takes the tensor it writes \
1552 into as an input; here the destination is the output itself, \
1553 zeroed by the caller"
1554 .into(),
1555 });
1556 }
1557
1558 if detect_indexed_write(
1560 &ep.function.body,
1561 &ep.function.expressions,
1562 &input_handles,
1563 &output_handles,
1564 ) {
1565 let also = if outputs.len() > 1 {
1566 ", and it writes two output tensors where every pattern here writes one"
1567 } else {
1568 ""
1569 };
1570 return Ok(KernelPattern::Unknown {
1571 reason: format!(
1572 "the address written to is loaded from an input buffer — a \
1573 data-dependent placement, where the input data decides where each \
1574 element lands{also}. Scatter takes three inputs, a destination to \
1575 write into as well as the indices and the updates, and there are two"
1576 ),
1577 });
1578 }
1579
1580 let indexed_read = [(0usize, 1usize), (1, 0)].into_iter().find_map(|(d, i)| {
1583 detect_indexed_read(
1584 &ep.function.body,
1585 &ep.function.expressions,
1586 inputs[d].0,
1587 inputs[i].0,
1588 outputs[0].0,
1589 )
1590 .map(|kind| (d, i, kind))
1591 });
1592 if let Some((d, i, kind)) = indexed_read {
1593 let data = make_binding(module, inputs[d].0, inputs[d].1, TensorRole::Input);
1594 let indices = make_binding(module, inputs[i].0, inputs[i].1, TensorRole::Input);
1595 return Ok(match kind {
1596 IndexedRead::Element => KernelPattern::Gather {
1597 data,
1598 indices,
1599 output: output_c,
1600 axis: 0,
1601 },
1602 IndexedRead::Block => KernelPattern::Unknown {
1608 reason: "a row gather: the index is scaled by a row width before it \
1609 becomes an address, so each index names a block rather than \
1610 an element. Gather indexes a flat buffer and carries no row \
1611 width"
1612 .into(),
1613 },
1614 });
1615 }
1616
1617 if !has_loop {
1618 let has_structural_if = has_non_guard_if(&ep.function.body);
1619
1620 let second_is_int = input_b.elem_type == data_type::UINT32
1623 || input_b.elem_type == data_type::INT32
1624 || input_b.elem_type == data_type::INT64;
1625 if second_is_int
1626 && find_store_value_op(&ep.function.body, &ep.function.expressions).is_none()
1627 && !has_structural_if
1628 {
1629 return Ok(KernelPattern::Gather {
1630 data: input_a,
1631 indices: input_b,
1632 output: output_c,
1633 axis: 0,
1634 });
1635 }
1636
1637 if has_structural_if
1639 && find_store_value_op(&ep.function.body, &ep.function.expressions).is_none()
1640 {
1641 let axis = infer_concat_axis(&ep.function.body, &ep.function.expressions, &shape_names);
1642 return Ok(KernelPattern::Concat {
1643 inputs: vec![input_a, input_b],
1644 output: output_c,
1645 axis,
1646 });
1647 }
1648
1649 if !stores_at_the_index_it_read(&ep.function.body, &ep.function.expressions) {
1654 return Ok(KernelPattern::Unknown {
1655 reason: "2 inputs and no loop, but the store index is not the \
1656 index it read from — the values move as well as \
1657 combine, and no operator here does both"
1658 .into(),
1659 });
1660 }
1661
1662 let op = match find_store_value_op(&ep.function.body, &ep.function.expressions) {
1664 Some(StoreValueOp::Binary(op)) => op,
1665 Some(StoreValueOp::MultiplyAdd) => {
1671 if let Some(pattern) = match_elementwise_chain(
1672 module,
1673 &ep.function.body,
1674 &ep.function.expressions,
1675 &outputs,
1676 &shape_names,
1677 ) {
1678 return Ok(pattern);
1679 }
1680 return Ok(KernelPattern::Unknown {
1681 reason: "fused multiply-add (`c + a * b`, or `fma(a, b, c)` after \
1682 optimization) that is not a chain: a chain step takes a whole \
1683 tensor read at the index written, or a scalar the host sets per \
1684 dispatch, and at least one of these operands is neither"
1685 .into(),
1686 });
1687 }
1688 None => {
1689 return Err(AnalysisError::UnsupportedPattern(
1690 "no recognizable binary operation found".into(),
1691 ));
1692 }
1693 };
1694
1695 let dim_name = shape_names.first().cloned().unwrap_or_else(|| "N".into());
1696
1697 return Ok(KernelPattern::ElementWise {
1698 op,
1699 inputs: [input_a, input_b],
1700 output: output_c,
1701 dim_name,
1702 });
1703 }
1704
1705 if (has_math_function_in_expressions(&ep.function.expressions, MathFunction::InverseSqrt)
1712 || has_math_function_in_expressions(&ep.function.expressions, MathFunction::Sqrt))
1713 && !has_math_function_in_expressions(&ep.function.expressions, MathFunction::Exp)
1714 {
1715 return Ok(KernelPattern::Unknown {
1716 reason: "2 inputs, a loop and a reciprocal square root — a \
1717 normalization without a bias, which Normalization cannot \
1718 yet represent"
1719 .into(),
1720 });
1721 }
1722
1723 if has_math_function_in_expressions(&ep.function.expressions, MathFunction::Sin)
1727 || has_math_function_in_expressions(&ep.function.expressions, MathFunction::Cos)
1728 {
1729 return Ok(KernelPattern::Unknown {
1730 reason: "2 inputs, a loop and trigonometry — a transform rather \
1731 than a convolution or a matmul"
1732 .into(),
1733 });
1734 }
1735
1736 let names_kernel_2d = shape_names
1742 .iter()
1743 .any(|s| s.eq_ignore_ascii_case("kh") || s.eq_ignore_ascii_case("kernel_h"))
1744 && shape_names
1745 .iter()
1746 .any(|s| s.eq_ignore_ascii_case("kw") || s.eq_ignore_ascii_case("kernel_w"));
1747 if names_kernel_2d {
1748 let conv_shape = extract_conv2d_shape(
1749 &shape_names,
1750 Some(&ep.function.body),
1751 Some(&ep.function.expressions),
1752 );
1753 return Ok(KernelPattern::Conv2D {
1754 input: input_a,
1755 weight: input_b,
1756 output: output_c,
1757 shape: conv_shape,
1758 bias: None,
1759 activation: store_value_activation(&ep.function.body, &ep.function.expressions),
1760 });
1761 }
1762
1763 if shape_names.len() < 3 {
1770 return Ok(KernelPattern::Unknown {
1771 reason: format!(
1772 "2 inputs and a loop, but {} shape parameters — too few to \
1773 establish a matmul's M, N and K",
1774 shape_names.len()
1775 ),
1776 });
1777 }
1778 let shape = MatMulShape {
1779 m: shape_names[0].clone(),
1780 n: shape_names[1].clone(),
1781 k: shape_names[2].clone(),
1782 };
1783
1784 Ok(KernelPattern::MatMul {
1785 inputs: [input_a, input_b],
1786 output: output_c,
1787 shape,
1788 })
1789}
1790
1791struct PackedCodes {
1815 weight: Handle<GlobalVariable>,
1817 address: Handle<Expression>,
1819 code_bits: u32,
1821 value: Handle<Expression>,
1823}
1824
1825fn packed_code_helpers(module: &Module) -> Vec<(Handle<nxpu_ir::Function>, u32, u32)> {
1832 let mut found = Vec::new();
1833 for (handle, func) in module.functions.iter() {
1834 for (_, expr) in func.expressions.iter() {
1835 let Expression::Math {
1836 fun: MathFunction::ExtractBits,
1837 arg,
1838 arg2: Some(width),
1839 ..
1840 } = expr
1841 else {
1842 continue;
1843 };
1844 let Expression::FunctionArgument(index) =
1848 func.expressions[strip_casts(&func.expressions, *arg)]
1849 else {
1850 continue;
1851 };
1852 if let Some(bits) = literal_u32(&func.expressions, *width) {
1853 found.push((handle, index, bits));
1854 break;
1855 }
1856 }
1857 }
1858 found
1859}
1860
1861fn find_packed_codes(
1863 module: &Module,
1864 ep: &nxpu_ir::EntryPoint,
1865 inputs: &[Handle<GlobalVariable>],
1866) -> Vec<PackedCodes> {
1867 fn walk(
1868 body: &[Statement],
1869 exprs: &Arena<Expression>,
1870 helpers: &[(Handle<nxpu_ir::Function>, u32, u32)],
1871 inputs: &[Handle<GlobalVariable>],
1872 out: &mut Vec<PackedCodes>,
1873 ) {
1874 for stmt in body {
1875 match stmt {
1876 Statement::Call {
1877 function,
1878 arguments,
1879 result: Some(value),
1880 } => {
1881 let Some(&(_, arg_index, code_bits)) =
1882 helpers.iter().find(|(f, ..)| f == function)
1883 else {
1884 continue;
1885 };
1886 let Some(&word) = arguments.get(arg_index as usize) else {
1887 continue;
1888 };
1889 if let Some((weight, address)) = input_load(exprs, word, inputs) {
1890 out.push(PackedCodes {
1891 weight,
1892 address,
1893 code_bits,
1894 value: *value,
1895 });
1896 }
1897 }
1898 Statement::If { accept, reject, .. } => {
1899 walk(accept, exprs, helpers, inputs, out);
1900 walk(reject, exprs, helpers, inputs, out);
1901 }
1902 Statement::Loop {
1903 body, continuing, ..
1904 } => {
1905 walk(body, exprs, helpers, inputs, out);
1906 walk(continuing, exprs, helpers, inputs, out);
1907 }
1908 _ => {}
1909 }
1910 }
1911 }
1912
1913 let exprs = &ep.function.expressions;
1914 let mut out = Vec::new();
1915
1916 for (handle, expr) in exprs.iter() {
1920 let Expression::Math {
1921 fun: MathFunction::ExtractBits,
1922 arg,
1923 arg2: Some(width),
1924 ..
1925 } = expr
1926 else {
1927 continue;
1928 };
1929 let Some(bits) = literal_u32(exprs, *width) else {
1930 continue;
1931 };
1932 if let Some((weight, address)) = input_load(exprs, strip_casts(exprs, *arg), inputs) {
1933 out.push(PackedCodes {
1934 weight,
1935 address,
1936 code_bits: bits,
1937 value: handle,
1938 });
1939 }
1940 }
1941
1942 walk(
1943 &ep.function.body,
1944 exprs,
1945 &packed_code_helpers(module),
1946 inputs,
1947 &mut out,
1948 );
1949 out
1950}
1951
1952fn input_load(
1954 exprs: &Arena<Expression>,
1955 handle: Handle<Expression>,
1956 inputs: &[Handle<GlobalVariable>],
1957) -> Option<(Handle<GlobalVariable>, Handle<Expression>)> {
1958 let Expression::Load { pointer } = exprs.try_get(strip_casts(exprs, handle))? else {
1959 return None;
1960 };
1961 let Expression::Access { base, index } = exprs.try_get(*pointer)? else {
1962 return None;
1963 };
1964 let global = access_base_global(exprs, *base)?;
1965 inputs.contains(&global).then_some((global, *index))
1966}
1967
1968fn literal_u32(exprs: &Arena<Expression>, handle: Handle<Expression>) -> Option<u32> {
1970 match exprs.try_get(handle)? {
1971 Expression::Literal(Literal::U32(n)) => Some(*n),
1972 Expression::Literal(Literal::I32(n)) if *n >= 0 => Some(*n as u32),
1973 Expression::Literal(Literal::AbstractInt(n)) if *n >= 0 => Some(*n as u32),
1974 _ => None,
1975 }
1976}
1977
1978fn multiply_factors(
1987 exprs: &Arena<Expression>,
1988 handle: Handle<Expression>,
1989) -> Option<(Handle<Expression>, Handle<Expression>)> {
1990 match exprs.try_get(handle)? {
1991 Expression::Math {
1992 fun: MathFunction::Fma,
1993 arg,
1994 arg1: Some(b),
1995 ..
1996 } => Some((*arg, *b)),
1997 Expression::Binary {
1998 op: BinaryOp::Multiply,
1999 left,
2000 right,
2001 } => Some((*left, *right)),
2002 Expression::Binary {
2003 op: BinaryOp::Add,
2004 left,
2005 right,
2006 } => multiply_factors(exprs, *left).or_else(|| multiply_factors(exprs, *right)),
2007 _ => None,
2008 }
2009}
2010
2011fn multiply_addend(
2013 exprs: &Arena<Expression>,
2014 handle: Handle<Expression>,
2015) -> Option<Handle<Expression>> {
2016 match exprs.try_get(handle)? {
2017 Expression::Math {
2018 fun: MathFunction::Fma,
2019 arg2: Some(c),
2020 ..
2021 } => Some(*c),
2022 Expression::Binary {
2023 op: BinaryOp::Add,
2024 left,
2025 right,
2026 } => {
2027 if multiply_factors(exprs, *left).is_some() {
2028 Some(*right)
2029 } else if multiply_factors(exprs, *right).is_some() {
2030 Some(*left)
2031 } else {
2032 None
2033 }
2034 }
2035 _ => None,
2036 }
2037}
2038
2039fn same_expression(
2046 exprs: &Arena<Expression>,
2047 a: Handle<Expression>,
2048 b: Handle<Expression>,
2049 depth: u32,
2050) -> bool {
2051 if a == b {
2052 return true;
2053 }
2054 if depth == 0 {
2055 return false;
2056 }
2057 let (Some(ea), Some(eb)) = (exprs.try_get(a), exprs.try_get(b)) else {
2058 return false;
2059 };
2060 let same =
2061 |x: Handle<Expression>, y: Handle<Expression>| same_expression(exprs, x, y, depth - 1);
2062 let same_opt = |x: &Option<Handle<Expression>>, y: &Option<Handle<Expression>>| match (x, y) {
2063 (None, None) => true,
2064 (Some(x), Some(y)) => same_expression(exprs, *x, *y, depth - 1),
2065 _ => false,
2066 };
2067 match (ea, eb) {
2068 (Expression::Literal(x), Expression::Literal(y)) => same_literal(x, y),
2069 (Expression::FunctionArgument(x), Expression::FunctionArgument(y)) => x == y,
2070 (Expression::GlobalVariable(x), Expression::GlobalVariable(y)) => x == y,
2071 (Expression::LocalVariable(x), Expression::LocalVariable(y)) => x == y,
2072 (Expression::CallResult(x), Expression::CallResult(y)) => x == y,
2073 (Expression::Load { pointer: x }, Expression::Load { pointer: y }) => same(*x, *y),
2074 (
2075 Expression::Access {
2076 base: xb,
2077 index: xi,
2078 },
2079 Expression::Access {
2080 base: yb,
2081 index: yi,
2082 },
2083 ) => same(*xb, *yb) && same(*xi, *yi),
2084 (
2085 Expression::AccessIndex {
2086 base: xb,
2087 index: xi,
2088 },
2089 Expression::AccessIndex {
2090 base: yb,
2091 index: yi,
2092 },
2093 ) => xi == yi && same(*xb, *yb),
2094 (Expression::Unary { op: xo, expr: xe }, Expression::Unary { op: yo, expr: ye }) => {
2095 xo == yo && same(*xe, *ye)
2096 }
2097 (
2098 Expression::Binary {
2099 op: xo,
2100 left: xl,
2101 right: xr,
2102 },
2103 Expression::Binary {
2104 op: yo,
2105 left: yl,
2106 right: yr,
2107 },
2108 ) => xo == yo && same(*xl, *yl) && same(*xr, *yr),
2109 (
2110 Expression::As {
2111 expr: xe,
2112 kind: xk,
2113 convert: xc,
2114 },
2115 Expression::As {
2116 expr: ye,
2117 kind: yk,
2118 convert: yc,
2119 },
2120 ) => xk == yk && xc == yc && same(*xe, *ye),
2121 (
2122 Expression::Math {
2123 fun: xf,
2124 arg: xa,
2125 arg1: xa1,
2126 arg2: xa2,
2127 arg3: xa3,
2128 },
2129 Expression::Math {
2130 fun: yf,
2131 arg: ya,
2132 arg1: ya1,
2133 arg2: ya2,
2134 arg3: ya3,
2135 },
2136 ) => {
2137 xf == yf
2138 && same(*xa, *ya)
2139 && same_opt(xa1, ya1)
2140 && same_opt(xa2, ya2)
2141 && same_opt(xa3, ya3)
2142 }
2143 _ => false,
2144 }
2145}
2146
2147fn same_literal(a: &Literal, b: &Literal) -> bool {
2150 match (a, b) {
2151 (Literal::Bool(x), Literal::Bool(y)) => x == y,
2152 (Literal::I32(x), Literal::I32(y)) => x == y,
2153 (Literal::U32(x), Literal::U32(y)) => x == y,
2154 (Literal::F32(x), Literal::F32(y)) => x.to_bits() == y.to_bits(),
2155 (Literal::F64(x), Literal::F64(y)) => x.to_bits() == y.to_bits(),
2156 (Literal::AbstractInt(x), Literal::AbstractInt(y)) => x == y,
2157 (Literal::AbstractFloat(x), Literal::AbstractFloat(y)) => x.to_bits() == y.to_bits(),
2158 _ => false,
2159 }
2160}
2161
2162fn expression_contains(
2169 exprs: &Arena<Expression>,
2170 haystack: Handle<Expression>,
2171 needle: Handle<Expression>,
2172) -> bool {
2173 let mut seen = std::collections::HashSet::new();
2174 let mut stack = vec![haystack];
2175 while let Some(handle) = stack.pop() {
2176 if !seen.insert(handle.index()) {
2177 continue;
2178 }
2179 if same_expression(exprs, handle, needle, EXPR_MATCH_DEPTH) {
2180 return true;
2181 }
2182 if let Some(expr) = exprs.try_get(handle) {
2183 push_operands(expr, &mut stack);
2184 }
2185 }
2186 false
2187}
2188
2189const EXPR_MATCH_DEPTH: u32 = 24;
2195
2196struct InputLoad {
2198 global: Handle<GlobalVariable>,
2199 index: Handle<Expression>,
2200 value: Handle<Expression>,
2201}
2202
2203fn collect_input_loads(
2205 exprs: &Arena<Expression>,
2206 inputs: &[Handle<GlobalVariable>],
2207) -> Vec<InputLoad> {
2208 exprs
2209 .iter()
2210 .filter_map(|(handle, expr)| {
2211 let Expression::Load { pointer } = expr else {
2212 return None;
2213 };
2214 let Expression::Access { base, index } = exprs.try_get(*pointer)? else {
2215 return None;
2216 };
2217 let global = access_base_global(exprs, *base)?;
2218 inputs.contains(&global).then_some(InputLoad {
2219 global,
2220 index: *index,
2221 value: handle,
2222 })
2223 })
2224 .collect()
2225}
2226
2227fn uniform_members_read(
2229 exprs: &Arena<Expression>,
2230 handle: Handle<Expression>,
2231 params: Option<Handle<GlobalVariable>>,
2232) -> Vec<u32> {
2233 let Some(params) = params else {
2234 return Vec::new();
2235 };
2236 let mut seen = std::collections::HashSet::new();
2237 let mut members = Vec::new();
2238 let mut stack = vec![handle];
2239 while let Some(handle) = stack.pop() {
2240 if !seen.insert(handle.index()) {
2241 continue;
2242 }
2243 let Some(expr) = exprs.try_get(handle) else {
2244 continue;
2245 };
2246 #[allow(clippy::collapsible_if)]
2247 if let Expression::AccessIndex { base, index } = expr {
2248 if matches!(exprs.try_get(*base), Some(Expression::GlobalVariable(g)) if *g == params)
2249 && !members.contains(index)
2250 {
2251 members.push(*index);
2252 }
2253 }
2254 push_operands(expr, &mut stack);
2255 }
2256 members
2257}
2258
2259fn find_store_into(
2265 body: &[Statement],
2266 exprs: &Arena<Expression>,
2267 output: Handle<GlobalVariable>,
2268) -> Option<(Handle<Expression>, Handle<Expression>)> {
2269 fn walk(
2270 body: &[Statement],
2271 exprs: &Arena<Expression>,
2272 output: Handle<GlobalVariable>,
2273 found: &mut Vec<(Handle<Expression>, Handle<Expression>)>,
2274 ) {
2275 for stmt in body {
2276 match stmt {
2277 Statement::Store { pointer, value } => {
2278 if access_base_global(exprs, *pointer) == Some(output) {
2279 match exprs.try_get(*pointer) {
2280 Some(Expression::Access { index, .. }) => found.push((*index, *value)),
2281 _ => found.push((*pointer, *value)),
2282 }
2283 }
2284 }
2285 Statement::If { accept, reject, .. } => {
2286 walk(accept, exprs, output, found);
2287 walk(reject, exprs, output, found);
2288 }
2289 Statement::Loop {
2290 body, continuing, ..
2291 } => {
2292 walk(body, exprs, output, found);
2293 walk(continuing, exprs, output, found);
2294 }
2295 _ => {}
2296 }
2297 }
2298 }
2299 let mut found = Vec::new();
2300 walk(body, exprs, output, &mut found);
2301 match found.as_slice() {
2302 [one] => Some(*one),
2303 _ => None,
2304 }
2305}
2306
2307fn match_quantized_matmul(
2314 module: &Module,
2315 ep: &nxpu_ir::EntryPoint,
2316 inputs: &[(Handle<GlobalVariable>, &GlobalVariable)],
2317 outputs: &[(Handle<GlobalVariable>, &GlobalVariable)],
2318 params_global: Option<Handle<GlobalVariable>>,
2319 param_names: &[String],
2324 shape_names: &[String],
2325) -> Option<KernelPattern> {
2326 let exprs = &ep.function.expressions;
2327 let input_handles: Vec<Handle<GlobalVariable>> = inputs.iter().map(|(h, _)| *h).collect();
2328 let codes = find_packed_codes(module, ep, &input_handles);
2329 if codes.is_empty() {
2330 return None;
2331 }
2332 let refuse = |reason: String| Some(KernelPattern::Unknown { reason });
2333
2334 let weights = dedup_globals(codes.iter().map(|c| c.weight));
2339 if weights.len() != 1 {
2340 return refuse(format!(
2341 "{} separate packed weight buffers unpacked in one kernel — several \
2342 quantized projections fused together, which needs a pattern per \
2343 projection and an operator for whatever combines them; this one \
2344 carries a single weight, a single scale and a single contraction",
2345 weights.len()
2346 ));
2347 }
2348 let weight_handle = weights[0];
2349 let code_bits = codes[0].code_bits;
2350 if codes.iter().any(|c| c.code_bits != code_bits) {
2351 return refuse(
2352 "the packed weight is unpacked at two different code widths, so no \
2353 single element type describes it"
2354 .into(),
2355 );
2356 }
2357 if outputs.len() != 1 {
2358 return refuse(format!(
2359 "a quantized matmul writes one result and this kernel writes {}",
2360 outputs.len()
2361 ));
2362 }
2363 if !has_loop(&ep.function.body) {
2364 return refuse(
2365 "integer codes unpacked without a loop — there is no contraction \
2366 here, so this is a dequantization rather than a matmul"
2367 .into(),
2368 );
2369 }
2370
2371 let loads = collect_input_loads(exprs, &input_handles);
2375 let multiplied = exprs.iter().any(|(_, expr)| {
2376 let Some((a, b)) = (match expr {
2377 Expression::Binary {
2378 op: BinaryOp::Multiply,
2379 left,
2380 right,
2381 } => Some((*left, *right)),
2382 Expression::Math {
2383 fun: MathFunction::Fma,
2384 arg,
2385 arg1: Some(b),
2386 ..
2387 } => Some((*arg, *b)),
2388 _ => None,
2389 }) else {
2390 return false;
2391 };
2392 let is_code = |h: Handle<Expression>| {
2393 codes
2394 .iter()
2395 .any(|c| same_expression(exprs, strip_casts(exprs, h), c.value, EXPR_MATCH_DEPTH))
2396 };
2397 let is_input = |h: Handle<Expression>| {
2398 let h = strip_casts(exprs, h);
2399 loads.iter().any(|l| l.value == h)
2400 };
2401 (is_code(a) && is_input(b)) || (is_code(b) && is_input(a))
2402 });
2403 if !multiplied {
2404 return refuse(
2405 "integer codes unpacked but never multiplied by a value from another \
2406 buffer — nothing dequantizes them and nothing contracts them"
2407 .into(),
2408 );
2409 }
2410
2411 let Some((f0, f1)) = multiply_factors(exprs, codes[0].address) else {
2415 return refuse(
2416 "the packed weight is addressed without a row stride, so which axis \
2417 of it is an output channel cannot be established"
2418 .into(),
2419 );
2420 };
2421 let m0 = uniform_members_read(exprs, f0, params_global);
2422 let m1 = uniform_members_read(exprs, f1, params_global);
2423 let (channel, stride_members) = match (m0.is_empty(), m1.is_empty()) {
2424 (true, false) => (f0, m1),
2425 (false, true) => (f1, m0),
2426 _ => {
2427 return refuse(
2428 "the packed weight's row stride cannot be told from its row index \
2429 — both, or neither, are built from the dispatch parameters"
2430 .into(),
2431 );
2432 }
2433 };
2434 let [k_member] = stride_members[..] else {
2435 return refuse(
2436 "the packed weight's row stride is built from more than one dispatch \
2437 parameter, so the contracted extent is not named by any one of them"
2438 .into(),
2439 );
2440 };
2441 let Some(k_name) = param_names.get(k_member as usize).cloned() else {
2442 return refuse(
2443 "the packed weight's row stride reads a dispatch parameter with no name".into(),
2444 );
2445 };
2446
2447 let output_handle = outputs[0].0;
2449 let Some((store_index, store_value)) = find_store_into(&ep.function.body, exprs, output_handle)
2450 else {
2451 return refuse(
2452 "the result is written in more than one place, so what this kernel \
2453 produces is not one matmul"
2454 .into(),
2455 );
2456 };
2457 let bias_load = multiply_addend(exprs, store_value).and_then(|addend| {
2458 let addend = strip_casts(exprs, addend);
2459 loads.iter().find(|l| {
2460 l.value == addend && same_expression(exprs, l.index, store_index, EXPR_MATCH_DEPTH)
2461 })
2462 });
2463 let bias_handle = bias_load.map(|l| l.global);
2464
2465 let per_channel: Vec<Handle<GlobalVariable>> = dedup_globals(loads.iter().filter_map(|l| {
2469 (l.global != weight_handle
2470 && Some(l.global) != bias_handle
2471 && same_expression(exprs, l.index, channel, EXPR_MATCH_DEPTH))
2472 .then_some(l.global)
2473 }));
2474 let [scale_handle] = per_channel[..] else {
2475 let block_scaled: Vec<Handle<GlobalVariable>> =
2476 dedup_globals(loads.iter().filter_map(|l| {
2477 (l.global != weight_handle
2478 && !same_expression(exprs, l.index, channel, EXPR_MATCH_DEPTH)
2479 && expression_contains(exprs, l.index, channel))
2480 .then_some(l.global)
2481 }));
2482 if per_channel.is_empty() && !block_scaled.is_empty() {
2483 return refuse(format!(
2484 "the {code_bits}-bit codes are scaled by a factor read at the \
2485 weight's row *and* the contracted position — one scale per block \
2486 of columns rather than one per output channel. Per-channel \
2487 quantization is what `DequantizeLinear` with an axis and what a \
2488 TFLite per-channel tensor express; a block-wise scale needs a \
2489 blocked dequantization neither backend here emits"
2490 ));
2491 }
2492 return refuse(format!(
2493 "{} buffers are read at exactly the weight's row index, and a \
2494 quantized matmul has one such buffer — the per-channel scale",
2495 per_channel.len()
2496 ));
2497 };
2498
2499 if code_bits != 8 {
2505 return refuse(format!(
2506 "{code_bits}-bit weight codes. The formats this compiler writes carry \
2507 int8 weights; a {code_bits}-bit one needs a sub-byte tensor type — \
2508 ONNX INT4 arrived in opset 21 and this backend emits 13, and the \
2509 TFLite writer here has no sub-byte type"
2510 ));
2511 }
2512
2513 let assigned = [Some(weight_handle), Some(scale_handle), bias_handle];
2517 let rest: Vec<&(Handle<GlobalVariable>, &GlobalVariable)> = inputs
2518 .iter()
2519 .filter(|(h, _)| !assigned.contains(&Some(*h)))
2520 .collect();
2521 let [(activation_handle, activation_gv)] = rest[..] else {
2522 return refuse(format!(
2523 "{} buffers are left over once the weight, its scale and its addend \
2524 are accounted for, and a quantized matmul contracts against one set \
2525 of activations",
2526 rest.len()
2527 ));
2528 };
2529 let input = make_binding(module, *activation_handle, activation_gv, TensorRole::Input);
2530 if input.elem_type != data_type::FLOAT {
2531 return refuse(
2532 "the activations are not f32, so the contraction is integer on both \
2533 sides — an integer matmul with its own accumulator width, which this \
2534 pattern does not describe"
2535 .into(),
2536 );
2537 }
2538
2539 let (n_name, m_name) = if let Some((g0, g1)) = multiply_factors(exprs, store_index) {
2544 let n0 = uniform_members_read(exprs, g0, params_global);
2545 let n1 = uniform_members_read(exprs, g1, params_global);
2546 let members = match (n0.is_empty(), n1.is_empty()) {
2547 (true, false) => n1,
2548 (false, true) => n0,
2549 _ => Vec::new(),
2550 };
2551 let [n_member] = members[..] else {
2552 return refuse(
2553 "the result's row stride is not a single dispatch parameter, so \
2554 the number of output channels is not named"
2555 .into(),
2556 );
2557 };
2558 let Some(n_name) = param_names.get(n_member as usize).cloned() else {
2559 return refuse("the result's row stride reads an unnamed parameter".into());
2560 };
2561 let remaining: Vec<&String> = shape_names
2562 .iter()
2563 .filter(|p| **p != k_name && **p != n_name)
2564 .collect();
2565 let [m_name] = remaining[..] else {
2566 return refuse(format!(
2567 "{} dispatch parameters are left once the contracted extent and \
2568 the output-channel count are named, and the number of rows has \
2569 to be exactly one of them",
2570 remaining.len()
2571 ));
2572 };
2573 (n_name, m_name.clone())
2574 } else {
2575 let remaining: Vec<&String> = shape_names.iter().filter(|p| **p != k_name).collect();
2577 let [n_name] = remaining[..] else {
2578 return refuse(format!(
2579 "the result is one row, so the remaining dispatch parameter is \
2580 the output-channel count — and {} remain",
2581 remaining.len()
2582 ));
2583 };
2584 (n_name.clone(), "1".to_string())
2585 };
2586
2587 let weight_gv = inputs.iter().find(|(h, _)| *h == weight_handle)?.1;
2591 let mut weight = make_binding(module, weight_handle, weight_gv, TensorRole::Input);
2592 weight.elem_type = data_type::INT8;
2593 let scale_gv = inputs.iter().find(|(h, _)| *h == scale_handle)?.1;
2594 let bias = bias_handle.and_then(|h| {
2595 inputs
2596 .iter()
2597 .find(|(g, _)| *g == h)
2598 .map(|(g, gv)| make_binding(module, *g, gv, TensorRole::Input))
2599 });
2600
2601 Some(KernelPattern::QuantizedMatMul {
2602 input,
2603 weight,
2604 scale: make_binding(module, scale_handle, scale_gv, TensorRole::Input),
2605 bias,
2606 output: make_binding(module, outputs[0].0, outputs[0].1, TensorRole::Output),
2607 shape: MatMulShape {
2608 m: m_name,
2609 n: n_name,
2610 k: k_name,
2611 },
2612 })
2613}
2614
2615fn dedup_globals(it: impl Iterator<Item = Handle<GlobalVariable>>) -> Vec<Handle<GlobalVariable>> {
2617 let mut out: Vec<Handle<GlobalVariable>> = Vec::new();
2618 for h in it {
2619 if !out.contains(&h) {
2620 out.push(h);
2621 }
2622 }
2623 out
2624}
2625
2626fn extract_loop_bound_literals(body: &[Statement], exprs: &Arena<Expression>) -> Vec<u32> {
2631 let mut bounds = Vec::new();
2632 for stmt in body {
2633 match stmt {
2634 Statement::Loop {
2635 body,
2636 continuing,
2637 break_if,
2638 } => {
2639 if let Some(n) = break_if.and_then(|bi| {
2642 let Expression::Binary {
2643 op: BinaryOp::GreaterEqual | BinaryOp::Greater,
2644 right,
2645 ..
2646 } = exprs.try_get(bi)?
2647 else {
2648 return None;
2649 };
2650 match exprs.try_get(*right)? {
2651 Expression::Literal(Literal::U32(n)) => Some(*n),
2652 _ => None,
2653 }
2654 }) {
2655 bounds.push(n);
2656 }
2657 if let Some(Statement::If {
2662 condition,
2663 accept,
2664 reject,
2665 }) = body.iter().find(|s| !matches!(s, Statement::Emit(_)))
2666 {
2667 let rhs = match exprs.try_get(*condition) {
2668 Some(&Expression::Binary {
2669 op: BinaryOp::Less | BinaryOp::LessEqual,
2670 right,
2671 ..
2672 }) if accept.is_empty()
2673 && matches!(reject.as_slice(), [Statement::Break]) =>
2674 {
2675 Some(right)
2676 }
2677 Some(&Expression::Binary {
2678 op: BinaryOp::GreaterEqual | BinaryOp::Greater,
2679 right,
2680 ..
2681 }) if reject.is_empty()
2682 && matches!(accept.as_slice(), [Statement::Break]) =>
2683 {
2684 Some(right)
2685 }
2686 _ => None,
2687 };
2688 let bound = rhs.and_then(|h| exprs.try_get(h)).and_then(|e| match *e {
2689 Expression::Literal(Literal::U32(n)) => Some(n),
2690 _ => None,
2691 });
2692 if let Some(n) = bound {
2693 bounds.push(n);
2694 }
2695 }
2696 bounds.extend(extract_loop_bound_literals(body, exprs));
2698 bounds.extend(extract_loop_bound_literals(continuing, exprs));
2699 }
2700 Statement::If { accept, reject, .. } => {
2701 bounds.extend(extract_loop_bound_literals(accept, exprs));
2702 bounds.extend(extract_loop_bound_literals(reject, exprs));
2703 }
2704 _ => {}
2705 }
2706 }
2707 bounds
2708}
2709
2710fn derives_from_invocation_id(exprs: &Arena<Expression>, handle: Handle<Expression>) -> bool {
2722 match exprs.try_get(handle) {
2723 Some(Expression::FunctionArgument(_)) => true,
2725 Some(Expression::AccessIndex { base, .. }) => {
2726 matches!(exprs.try_get(*base), Some(Expression::FunctionArgument(_)))
2727 }
2728 _ => false,
2729 }
2730}
2731
2732fn extract_multiply_literals(exprs: &Arena<Expression>) -> Vec<u32> {
2747 let mut strides = Vec::new();
2748 #[allow(clippy::collapsible_if)]
2751 for (_, expr) in exprs.iter() {
2752 let Expression::Binary {
2753 op: BinaryOp::Multiply,
2754 left,
2755 right,
2756 } = expr
2757 else {
2758 continue;
2759 };
2760 if let Some(&Expression::Literal(Literal::U32(n @ 2..))) = exprs.try_get(*right) {
2761 if derives_from_invocation_id(exprs, *left) {
2762 strides.push(n);
2763 }
2764 } else if let Some(&Expression::Literal(Literal::U32(n @ 2..))) = exprs.try_get(*left) {
2765 if derives_from_invocation_id(exprs, *right) {
2766 strides.push(n);
2767 }
2768 }
2769 }
2770 strides.sort_unstable();
2771 strides.dedup();
2772 strides
2773}
2774
2775fn extract_subtract_literals(exprs: &Arena<Expression>) -> Vec<u32> {
2778 let mut pads = Vec::new();
2779 for (_, expr) in exprs.iter() {
2780 let Expression::Binary {
2781 op: BinaryOp::Subtract,
2782 right,
2783 ..
2784 } = expr
2785 else {
2786 continue;
2787 };
2788 if let Some(&Expression::Literal(Literal::U32(n @ 1..))) = exprs.try_get(*right) {
2789 pads.push(n);
2790 }
2791 }
2792 pads.sort_unstable();
2793 pads.dedup();
2794 pads
2795}
2796
2797fn extract_conv2d_shape(
2804 shape_names: &[String],
2805 body: Option<&[Statement]>,
2806 exprs: Option<&Arena<Expression>>,
2807) -> Conv2DShape {
2808 let get = |i: usize| {
2810 shape_names
2811 .get(i)
2812 .cloned()
2813 .unwrap_or_else(|| format!("d{i}"))
2814 };
2815
2816 let (kernel_h_val, kernel_w_val, stride_h, stride_w, pad_h, pad_w) =
2817 if let (Some(body), Some(exprs)) = (body, exprs) {
2818 let bounds = extract_loop_bound_literals(body, exprs);
2819 let strides = extract_multiply_literals(exprs);
2820 let pads = extract_subtract_literals(exprs);
2821
2822 let kh = bounds.first().copied().unwrap_or(0) as i64;
2824 let kw = bounds.get(1).copied().unwrap_or(0) as i64;
2825 let kw = if kw == 0 && kh > 0 { kh } else { kw };
2827
2828 let sh_u = strides.first().copied().unwrap_or(1);
2830 let sw_u = strides.get(1).copied().unwrap_or(sh_u);
2831 let sh = sh_u as i64;
2832 let sw = sw_u as i64;
2833
2834 let ph_u = pads.first().copied().unwrap_or(0);
2836 let pw_u = pads.get(1).copied().unwrap_or(ph_u);
2837 let ph = ph_u as i64;
2838 let pw = pw_u as i64;
2839
2840 (kh, kw, sh, sw, ph, pw)
2841 } else {
2842 (0, 0, 1, 1, 0, 0)
2843 };
2844
2845 let groups = if shape_names.iter().any(|n| n.eq_ignore_ascii_case("groups")) {
2847 -1 } else {
2850 1
2851 };
2852
2853 Conv2DShape {
2854 batch: get(0),
2855 channels_in: get(1),
2856 height: get(2),
2857 width: get(3),
2858 channels_out: get(4),
2859 kernel_h: get(5),
2860 kernel_w: get(6),
2861 kernel_h_val,
2862 kernel_w_val,
2863 stride_h,
2864 stride_w,
2865 pad_h,
2866 pad_w,
2867 groups,
2868 dilation_h: 1,
2869 dilation_w: 1,
2870 }
2871}
2872
2873fn make_binding(
2874 module: &Module,
2875 handle: Handle<GlobalVariable>,
2876 gv: &GlobalVariable,
2877 role: TensorRole,
2878) -> TensorBinding {
2879 let elem_type = resolve_array_elem_type(module, gv.ty).unwrap_or(data_type::FLOAT);
2880 TensorBinding {
2881 handle,
2882 name: gv
2883 .name
2884 .clone()
2885 .unwrap_or_else(|| format!("tensor_{}", handle.index())),
2886 elem_type,
2887 role,
2888 }
2889}
2890
2891fn resolve_array_elem_type(module: &Module, ty: Handle<Type>) -> Option<i32> {
2893 match &module.types[ty].inner {
2894 TypeInner::Array { base, .. } => match &module.types[*base].inner {
2895 TypeInner::Scalar(s) => Some(scalar_to_onnx_data_type(s)),
2896 _ => None,
2897 },
2898 TypeInner::Tensor { scalar, .. } => Some(scalar_to_onnx_data_type(scalar)),
2899 _ => None,
2900 }
2901}
2902
2903fn scalar_to_onnx_data_type(scalar: &Scalar) -> i32 {
2905 match (scalar.kind, scalar.width) {
2906 (ScalarKind::Float, 4) => data_type::FLOAT,
2907 (ScalarKind::Float, 2) => data_type::FLOAT16,
2908 (ScalarKind::BFloat, 2) => data_type::BFLOAT16,
2909 (ScalarKind::Sint, 4) => data_type::INT32,
2910 (ScalarKind::Sint, 1) => data_type::INT8,
2911 (ScalarKind::Uint, 4) => data_type::UINT32,
2912 (ScalarKind::Uint, 1) => data_type::UINT8,
2913 (ScalarKind::Bool, _) => data_type::BOOL,
2914 _ => data_type::FLOAT,
2915 }
2916}
2917
2918pub fn normalize_axis(axis: i64, ndim: usize) -> i64 {
2922 if axis < 0 {
2923 let ndim = ndim as i64;
2924 ((axis % ndim) + ndim) % ndim
2925 } else {
2926 axis
2927 }
2928}
2929
2930fn infer_concat_axis(body: &[Statement], exprs: &Arena<Expression>, shape_names: &[String]) -> i64 {
2936 find_if_comparison_axis(body, exprs, shape_names).unwrap_or(0)
2937}
2938
2939fn infer_split_axis(body: &[Statement], exprs: &Arena<Expression>, shape_names: &[String]) -> i64 {
2944 find_if_comparison_axis(body, exprs, shape_names).unwrap_or(0)
2945}
2946
2947fn find_if_comparison_axis(
2953 body: &[Statement],
2954 exprs: &Arena<Expression>,
2955 _shape_names: &[String],
2956) -> Option<i64> {
2957 for stmt in body {
2958 match stmt {
2959 Statement::If { condition, .. } => {
2960 #[allow(clippy::collapsible_if)]
2962 if let Some(Expression::Binary {
2963 op, left, right, ..
2964 }) = exprs.try_get(*condition)
2965 {
2966 if matches!(
2967 op,
2968 BinaryOp::Less
2969 | BinaryOp::LessEqual
2970 | BinaryOp::Greater
2971 | BinaryOp::GreaterEqual
2972 ) {
2973 if let Some(idx) = extract_uniform_member_index(exprs, *right) {
2975 return Some(idx as i64);
2976 }
2977 if let Some(idx) = extract_uniform_member_index(exprs, *left) {
2978 return Some(idx as i64);
2979 }
2980 }
2981 }
2982 }
2983 Statement::Loop {
2984 body, continuing, ..
2985 } => {
2986 if let Some(axis) = find_if_comparison_axis(body, exprs, _shape_names) {
2987 return Some(axis);
2988 }
2989 if let Some(axis) = find_if_comparison_axis(continuing, exprs, _shape_names) {
2990 return Some(axis);
2991 }
2992 }
2993 _ => {}
2994 }
2995 }
2996 None
2997}
2998
2999fn extract_uniform_member_index(
3006 exprs: &Arena<Expression>,
3007 handle: Handle<Expression>,
3008) -> Option<u32> {
3009 match exprs.try_get(handle)? {
3010 Expression::AccessIndex { index, .. } => Some(*index),
3011 Expression::Load { pointer } => match exprs.try_get(*pointer)? {
3012 Expression::AccessIndex { index, .. } => Some(*index),
3013 _ => None,
3014 },
3015 _ => None,
3016 }
3017}
3018
3019fn has_math_function_in_expressions(exprs: &Arena<Expression>, target: MathFunction) -> bool {
3021 exprs
3022 .iter()
3023 .any(|(_, expr)| matches!(expr, Expression::Math { fun, .. } if *fun == target))
3024}
3025
3026fn has_if_statement(body: &[Statement]) -> bool {
3028 body.iter().any(|stmt| match stmt {
3029 Statement::If { .. } => true,
3030 Statement::Loop {
3031 body, continuing, ..
3032 } => has_if_statement(body) || has_if_statement(continuing),
3033 _ => false,
3034 })
3035}
3036
3037fn has_non_guard_if(body: &[Statement]) -> bool {
3040 body.iter().any(|stmt| match stmt {
3041 Statement::If { accept, reject, .. } => {
3042 let is_guard = reject.is_empty()
3044 && accept.len() == 1
3045 && matches!(accept[0], Statement::Return { .. });
3046 !is_guard
3047 }
3048 Statement::Loop {
3049 body, continuing, ..
3050 } => has_non_guard_if(body) || has_non_guard_if(continuing),
3051 _ => false,
3052 })
3053}
3054
3055fn has_loop(body: &[Statement]) -> bool {
3057 body.iter().any(|stmt| match stmt {
3058 Statement::Loop { .. } => true,
3059 Statement::If { accept, reject, .. } => has_loop(accept) || has_loop(reject),
3060 _ => false,
3061 })
3062}
3063
3064#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3067enum StoreValueOp {
3068 Binary(ElementWiseOp),
3070 MultiplyAdd,
3081}
3082
3083fn find_store_value_op(body: &[Statement], exprs: &Arena<Expression>) -> Option<StoreValueOp> {
3086 for stmt in body {
3087 match stmt {
3088 Statement::Store { value, .. } => {
3089 if let Some(op) = classify_store_value_op(exprs, *value) {
3090 return Some(op);
3091 }
3092 }
3093 Statement::If { accept, reject, .. } => {
3094 if let Some(op) = find_store_value_op(accept, exprs) {
3095 return Some(op);
3096 }
3097 if let Some(op) = find_store_value_op(reject, exprs) {
3098 return Some(op);
3099 }
3100 }
3101 _ => {}
3102 }
3103 }
3104 None
3105}
3106
3107fn classify_store_value_op(
3109 exprs: &Arena<Expression>,
3110 value: Handle<Expression>,
3111) -> Option<StoreValueOp> {
3112 match exprs.try_get(value)? {
3113 Expression::Math {
3115 fun: MathFunction::Fma,
3116 ..
3117 } => Some(StoreValueOp::MultiplyAdd),
3118 Expression::Binary { op, left, right } => {
3119 let ew = match op {
3120 BinaryOp::Add => {
3125 if expr_is_multiply(exprs, *left) || expr_is_multiply(exprs, *right) {
3126 return Some(StoreValueOp::MultiplyAdd);
3127 }
3128 ElementWiseOp::Add
3129 }
3130 BinaryOp::Subtract => ElementWiseOp::Sub,
3131 BinaryOp::Multiply => ElementWiseOp::Mul,
3132 BinaryOp::Divide => ElementWiseOp::Div,
3133 _ => return None,
3134 };
3135 Some(StoreValueOp::Binary(ew))
3136 }
3137 _ => None,
3138 }
3139}
3140
3141fn expr_is_multiply(exprs: &Arena<Expression>, handle: Handle<Expression>) -> bool {
3144 matches!(
3145 exprs.try_get(handle),
3146 Some(Expression::Binary {
3147 op: BinaryOp::Multiply,
3148 ..
3149 })
3150 )
3151}
3152
3153#[derive(Debug, Clone)]
3155enum ChainLeaf {
3156 Tensor(Handle<GlobalVariable>),
3158 Scalar(ScalarBinding),
3160}
3161
3162struct RawChain {
3164 base: Handle<GlobalVariable>,
3165 cast: Option<i32>,
3166 steps: Vec<(ElementWiseOp, ChainLeaf)>,
3167}
3168
3169fn find_lone_store(
3175 body: &[Statement],
3176 exprs: &Arena<Expression>,
3177) -> Option<(Handle<Expression>, Handle<Expression>)> {
3178 fn walk(
3179 body: &[Statement],
3180 exprs: &Arena<Expression>,
3181 found: &mut Vec<(Handle<Expression>, Handle<Expression>)>,
3182 ) {
3183 for stmt in body {
3184 match stmt {
3185 Statement::Store { pointer, value } => match access_index(exprs, *pointer) {
3186 Some(index) => found.push((index, *value)),
3187 None => found.push((*pointer, *value)),
3191 },
3192 Statement::If { accept, reject, .. } => {
3193 walk(accept, exprs, found);
3194 walk(reject, exprs, found);
3195 }
3196 Statement::Loop {
3197 body, continuing, ..
3198 } => {
3199 walk(body, exprs, found);
3200 walk(continuing, exprs, found);
3201 }
3202 _ => {}
3203 }
3204 }
3205 }
3206 let mut found = Vec::new();
3207 walk(body, exprs, &mut found);
3208 match found.as_slice() {
3209 [one] => Some(*one),
3210 _ => None,
3211 }
3212}
3213
3214fn chain_leaf(
3222 module: &Module,
3223 exprs: &Arena<Expression>,
3224 handle: Handle<Expression>,
3225 store_index: Handle<Expression>,
3226) -> Option<ChainLeaf> {
3227 let Expression::Load { pointer } = exprs.try_get(handle)? else {
3228 return None;
3229 };
3230 match exprs.try_get(*pointer)? {
3231 Expression::Access { base, index } => {
3233 let global = access_base_global(exprs, *base)?;
3234 let gv = module.global_variables.try_get(global)?;
3235 if !matches!(gv.space, AddressSpace::Storage { .. }) {
3236 return None;
3237 }
3238 (*index == store_index).then_some(ChainLeaf::Tensor(global))
3239 }
3240 Expression::GlobalVariable(global) => {
3242 let gv = module.global_variables.try_get(*global)?;
3243 if !matches!(gv.space, AddressSpace::Uniform) {
3244 return None;
3245 }
3246 let TypeInner::Scalar(scalar) = &module.types.try_get(gv.ty)?.inner else {
3247 return None;
3248 };
3249 Some(ChainLeaf::Scalar(ScalarBinding {
3250 name: gv
3251 .name
3252 .clone()
3253 .unwrap_or_else(|| format!("scalar_{}", global.index())),
3254 elem_type: scalar_to_onnx_data_type(scalar),
3255 }))
3256 }
3257 Expression::AccessIndex { base, index } => {
3259 let Expression::GlobalVariable(global) = exprs.try_get(*base)? else {
3260 return None;
3261 };
3262 let gv = module.global_variables.try_get(*global)?;
3263 if !matches!(gv.space, AddressSpace::Uniform) {
3264 return None;
3265 }
3266 let TypeInner::Struct { members, .. } = &module.types.try_get(gv.ty)?.inner else {
3267 return None;
3268 };
3269 let member = members.get(*index as usize)?;
3270 let TypeInner::Scalar(scalar) = &module.types.try_get(member.ty)?.inner else {
3271 return None;
3272 };
3273 Some(ChainLeaf::Scalar(ScalarBinding {
3274 name: member.name.clone()?,
3275 elem_type: scalar_to_onnx_data_type(scalar),
3276 }))
3277 }
3278 _ => None,
3279 }
3280}
3281
3282fn decompose_chain(
3290 module: &Module,
3291 exprs: &Arena<Expression>,
3292 handle: Handle<Expression>,
3293 store_index: Handle<Expression>,
3294 depth: u32,
3295) -> Option<RawChain> {
3296 if depth == 0 {
3297 return None;
3298 }
3299 if let Some(leaf) = chain_leaf(module, exprs, handle, store_index) {
3302 return match leaf {
3303 ChainLeaf::Tensor(global) => Some(RawChain {
3304 base: global,
3305 cast: None,
3306 steps: Vec::new(),
3307 }),
3308 ChainLeaf::Scalar(_) => None,
3309 };
3310 }
3311 match exprs.try_get(handle)? {
3312 Expression::As {
3314 expr,
3315 kind,
3316 convert: Some(width),
3317 } => {
3318 let inner = decompose_chain(module, exprs, *expr, store_index, depth - 1)?;
3319 if inner.cast.is_some() || !inner.steps.is_empty() {
3322 return None;
3323 }
3324 Some(RawChain {
3325 cast: Some(scalar_to_onnx_data_type(&Scalar {
3326 kind: *kind,
3327 width: *width,
3328 })),
3329 ..inner
3330 })
3331 }
3332 Expression::Binary { op, left, right } => {
3333 let ew = match op {
3334 BinaryOp::Add => ElementWiseOp::Add,
3335 BinaryOp::Subtract => ElementWiseOp::Sub,
3336 BinaryOp::Multiply => ElementWiseOp::Mul,
3337 BinaryOp::Divide => ElementWiseOp::Div,
3338 _ => return None,
3339 };
3340 decompose_chain_binary(module, exprs, ew, *left, *right, store_index, depth - 1)
3341 }
3342 Expression::Math {
3347 fun: MathFunction::Fma,
3348 arg,
3349 arg1: Some(factor),
3350 arg2: Some(addend),
3351 ..
3352 } => {
3353 let mut chain = decompose_chain_binary(
3354 module,
3355 exprs,
3356 ElementWiseOp::Mul,
3357 *arg,
3358 *factor,
3359 store_index,
3360 depth - 1,
3361 )?;
3362 let addend = chain_leaf(module, exprs, *addend, store_index)?;
3363 chain.steps.push((ElementWiseOp::Add, addend));
3364 Some(chain)
3365 }
3366 _ => None,
3367 }
3368}
3369
3370#[allow(clippy::collapsible_if)] fn decompose_chain_binary(
3373 module: &Module,
3374 exprs: &Arena<Expression>,
3375 op: ElementWiseOp,
3376 left: Handle<Expression>,
3377 right: Handle<Expression>,
3378 store_index: Handle<Expression>,
3379 depth: u32,
3380) -> Option<RawChain> {
3381 if let Some(operand) = chain_leaf(module, exprs, right, store_index) {
3384 if let Some(mut chain) = decompose_chain(module, exprs, left, store_index, depth) {
3385 chain.steps.push((op, operand));
3386 return Some(chain);
3387 }
3388 }
3389 if op.is_commutative() {
3392 if let Some(operand) = chain_leaf(module, exprs, left, store_index) {
3393 if let Some(mut chain) = decompose_chain(module, exprs, right, store_index, depth) {
3394 chain.steps.push((op, operand));
3395 return Some(chain);
3396 }
3397 }
3398 }
3399 None
3400}
3401
3402fn match_elementwise_chain(
3409 module: &Module,
3410 body: &[Statement],
3411 exprs: &Arena<Expression>,
3412 outputs: &[(Handle<GlobalVariable>, &GlobalVariable)],
3413 shape_names: &[String],
3414) -> Option<KernelPattern> {
3415 let [(out_handle, out_gv)] = outputs else {
3416 return None;
3417 };
3418 if !stores_at_the_index_it_read(body, exprs) {
3421 return None;
3422 }
3423 let (store_index, value) = find_lone_store(body, exprs)?;
3424 let chain = decompose_chain(module, exprs, value, store_index, 32)?;
3425
3426 let has_scalar = chain
3427 .steps
3428 .iter()
3429 .any(|(_, leaf)| matches!(leaf, ChainLeaf::Scalar(_)));
3430 if chain.steps.is_empty() || (chain.cast.is_none() && chain.steps.len() == 1 && !has_scalar) {
3431 return None;
3432 }
3433
3434 let base_gv = module.global_variables.try_get(chain.base)?;
3435 let base = make_binding(module, chain.base, base_gv, TensorRole::Input);
3436 let mut steps = Vec::with_capacity(chain.steps.len());
3437 let mut reads_its_own_output = chain.base == *out_handle;
3438 for (op, leaf) in chain.steps {
3439 let operand = match leaf {
3440 ChainLeaf::Tensor(global) => {
3441 reads_its_own_output |= global == *out_handle;
3442 let gv = module.global_variables.try_get(global)?;
3443 ChainOperand::Tensor(make_binding(module, global, gv, TensorRole::Input))
3444 }
3445 ChainLeaf::Scalar(scalar) => ChainOperand::Scalar(scalar),
3446 };
3447 steps.push(ChainStep { op, operand });
3448 }
3449
3450 let mut output = make_binding(module, *out_handle, out_gv, TensorRole::Output);
3456 if reads_its_own_output {
3457 output.name = format!("{}_out", output.name);
3458 }
3459
3460 Some(KernelPattern::ElementWiseChain {
3461 base,
3462 cast: chain.cast,
3463 steps,
3464 output,
3465 dim_name: shape_names.first().cloned().unwrap_or_else(|| "N".into()),
3466 })
3467}
3468
3469fn find_store_activation(body: &[Statement], exprs: &Arena<Expression>) -> Option<ActivationOp> {
3471 for stmt in body {
3472 match stmt {
3473 Statement::Store { value, .. } => {
3474 if let Some(act) = classify_activation_expr(exprs, *value) {
3475 return Some(act);
3476 }
3477 }
3478 Statement::If { accept, reject, .. } => {
3479 if let Some(op) = find_store_activation(accept, exprs) {
3480 return Some(op);
3481 }
3482 if let Some(op) = find_store_activation(reject, exprs) {
3483 return Some(op);
3484 }
3485 }
3486 _ => {}
3487 }
3488 }
3489 None
3490}
3491
3492fn classify_activation_expr(
3494 exprs: &Arena<Expression>,
3495 handle: Handle<Expression>,
3496) -> Option<ActivationOp> {
3497 match exprs.try_get(handle)? {
3498 Expression::Math {
3500 fun: MathFunction::Max,
3501 ..
3502 } => Some(ActivationOp::Relu),
3503 Expression::Math {
3505 fun: MathFunction::Tanh,
3506 ..
3507 } => Some(ActivationOp::Tanh),
3508 Expression::Binary {
3510 op: BinaryOp::Multiply,
3511 left,
3512 right,
3513 ..
3514 } => {
3515 let has_tanh_left = contains_math_fun(exprs, *left, MathFunction::Tanh);
3516 let has_tanh_right = contains_math_fun(exprs, *right, MathFunction::Tanh);
3517 let has_exp_left = contains_math_fun(exprs, *left, MathFunction::Exp);
3518 let has_exp_right = contains_math_fun(exprs, *right, MathFunction::Exp);
3519
3520 if (has_tanh_left || has_tanh_right) && !has_exp_left && !has_exp_right {
3524 return Some(ActivationOp::Gelu);
3525 }
3526
3527 if (has_tanh_left || has_tanh_right) && (has_exp_left || has_exp_right) {
3530 return Some(ActivationOp::Mish);
3531 }
3532
3533 if has_exp_left || has_exp_right {
3536 let has_div_left = contains_binary_op(exprs, *left, BinaryOp::Divide);
3537 let has_div_right = contains_binary_op(exprs, *right, BinaryOp::Divide);
3538 if has_div_left || has_div_right {
3539 return Some(ActivationOp::Silu);
3540 }
3541 }
3542
3543 classify_activation_expr(exprs, *left)
3545 .or_else(|| classify_activation_expr(exprs, *right))
3546 }
3547 Expression::Binary {
3550 op: BinaryOp::Divide,
3551 left,
3552 right,
3553 ..
3554 } => {
3555 if contains_math_fun(exprs, *left, MathFunction::Exp) {
3556 Some(ActivationOp::Softmax)
3558 } else if contains_math_fun(exprs, *right, MathFunction::Exp) {
3559 Some(ActivationOp::Sigmoid)
3561 } else {
3562 None
3563 }
3564 }
3565 _ => None,
3566 }
3567}
3568
3569fn contains_binary_op(
3571 exprs: &Arena<Expression>,
3572 handle: Handle<Expression>,
3573 target: BinaryOp,
3574) -> bool {
3575 match exprs.try_get(handle) {
3576 Some(Expression::Binary {
3577 op, left, right, ..
3578 }) => {
3579 *op == target
3580 || contains_binary_op(exprs, *left, target)
3581 || contains_binary_op(exprs, *right, target)
3582 }
3583 Some(Expression::Math {
3584 fun,
3585 arg,
3586 arg1,
3587 arg2,
3588 arg3,
3589 }) => {
3590 (*fun == MathFunction::Fma && matches!(target, BinaryOp::Multiply | BinaryOp::Add))
3594 || contains_binary_op(exprs, *arg, target)
3595 || [*arg1, *arg2, *arg3]
3596 .into_iter()
3597 .flatten()
3598 .any(|h| contains_binary_op(exprs, h, target))
3599 }
3600 Some(Expression::Unary { expr, .. }) => contains_binary_op(exprs, *expr, target),
3601 _ => false,
3602 }
3603}
3604
3605fn contains_math_fun(
3607 exprs: &Arena<Expression>,
3608 handle: Handle<Expression>,
3609 target: MathFunction,
3610) -> bool {
3611 match exprs.try_get(handle) {
3612 Some(Expression::Math {
3613 fun,
3614 arg,
3615 arg1,
3616 arg2,
3617 arg3,
3618 }) => {
3619 *fun == target
3622 || contains_math_fun(exprs, *arg, target)
3623 || [*arg1, *arg2, *arg3]
3624 .into_iter()
3625 .flatten()
3626 .any(|h| contains_math_fun(exprs, h, target))
3627 }
3628 Some(Expression::Binary { left, right, .. }) => {
3629 contains_math_fun(exprs, *left, target) || contains_math_fun(exprs, *right, target)
3630 }
3631 Some(Expression::Unary { expr, .. }) => contains_math_fun(exprs, *expr, target),
3632 _ => false,
3633 }
3634}
3635
3636fn id_division_depth(
3649 exprs: &Arena<Expression>,
3650 handle: Handle<Expression>,
3651 depth: u32,
3652) -> Option<u32> {
3653 if depth == 0 {
3654 return None;
3655 }
3656 match exprs.try_get(handle)? {
3657 Expression::AccessIndex { base, index: 0 } => {
3659 matches!(exprs.try_get(*base)?, Expression::FunctionArgument(_)).then_some(0)
3660 }
3661 Expression::Binary { op, left, .. } => match op {
3662 BinaryOp::Modulo => id_division_depth(exprs, *left, depth - 1),
3663 BinaryOp::Divide => id_division_depth(exprs, *left, depth - 1).map(|d| d + 1),
3664 _ => None,
3665 },
3666 _ => None,
3667 }
3668}
3669
3670fn flatten_index_components(
3677 exprs: &Arena<Expression>,
3678 handle: Handle<Expression>,
3679 out: &mut Vec<Handle<Expression>>,
3680 depth: u32,
3681) {
3682 if depth == 0 {
3683 return;
3684 }
3685 match exprs.try_get(handle) {
3686 Some(Expression::Math {
3687 fun: MathFunction::Fma,
3688 arg,
3689 arg2: Some(addend),
3690 ..
3691 }) => {
3692 out.push(*addend);
3693 flatten_index_components(exprs, *arg, out, depth - 1);
3694 }
3695 Some(Expression::Binary {
3696 op: BinaryOp::Add,
3697 left,
3698 right,
3699 }) => {
3700 let (product, addend) = match exprs.try_get(*left) {
3703 Some(Expression::Binary {
3704 op: BinaryOp::Multiply,
3705 ..
3706 }) => (*left, *right),
3707 _ => (*right, *left),
3708 };
3709 out.push(addend);
3710 if let Some(Expression::Binary {
3711 op: BinaryOp::Multiply,
3712 left: inner,
3713 ..
3714 }) = exprs.try_get(product)
3715 {
3716 flatten_index_components(exprs, *inner, out, depth - 1);
3717 }
3718 }
3719 _ => out.push(handle),
3720 }
3721}
3722
3723fn detect_permutation(
3730 body: &[Statement],
3731 exprs: &Arena<Expression>,
3732 input: Handle<GlobalVariable>,
3733 output: Handle<GlobalVariable>,
3734) -> Option<Vec<i64>> {
3735 fn find_store(
3736 body: &[Statement],
3737 exprs: &Arena<Expression>,
3738 input: Handle<GlobalVariable>,
3739 output: Handle<GlobalVariable>,
3740 ) -> Option<(Handle<Expression>, Handle<Expression>)> {
3741 for stmt in body {
3742 match stmt {
3743 Statement::Store { pointer, value } => {
3744 let store_index = match exprs.try_get(*pointer) {
3745 Some(Expression::Access { base, index })
3746 if matches!(
3747 exprs.try_get(*base),
3748 Some(Expression::GlobalVariable(g)) if *g == output
3749 ) =>
3750 {
3751 *index
3752 }
3753 _ => continue,
3754 };
3755 let load_index = match exprs.try_get(*value) {
3756 Some(Expression::Load { pointer }) => match exprs.try_get(*pointer) {
3757 Some(Expression::Access { base, index })
3758 if matches!(
3759 exprs.try_get(*base),
3760 Some(Expression::GlobalVariable(g)) if *g == input
3761 ) =>
3762 {
3763 *index
3764 }
3765 _ => continue,
3766 },
3767 _ => continue,
3768 };
3769 return Some((store_index, load_index));
3770 }
3771 Statement::If { accept, reject, .. } => {
3772 if let Some(found) = find_store(accept, exprs, input, output)
3773 .or_else(|| find_store(reject, exprs, input, output))
3774 {
3775 return Some(found);
3776 }
3777 }
3778 _ => {}
3779 }
3780 }
3781 None
3782 }
3783
3784 let (store_index, load_index) = find_store(body, exprs, input, output)?;
3785
3786 if id_division_depth(exprs, load_index, 32)? != 0 {
3790 return None;
3791 }
3792
3793 let mut components = Vec::new();
3794 flatten_index_components(exprs, store_index, &mut components, 32);
3795 if components.len() < 2 {
3796 return None;
3797 }
3798
3799 let rank = components.len();
3801 let mut perm = Vec::with_capacity(rank);
3802 for handle in components.iter().rev() {
3803 let depth = id_division_depth(exprs, *handle, 32)?;
3804 if depth as usize >= rank {
3805 return None;
3806 }
3807 perm.push((rank - 1 - depth as usize) as i64);
3808 }
3809
3810 let mut seen = perm.clone();
3813 seen.sort_unstable();
3814 if seen != (0..rank as i64).collect::<Vec<_>>() {
3815 return None;
3816 }
3817 if perm.iter().enumerate().all(|(i, p)| i as i64 == *p) {
3818 return None;
3819 }
3820 Some(perm)
3821}
3822
3823fn survey_output_stores(
3837 body: &[Statement],
3838 exprs: &Arena<Expression>,
3839 input: Handle<GlobalVariable>,
3840 outputs: &[Handle<GlobalVariable>],
3841) -> (Vec<Handle<GlobalVariable>>, Vec<Handle<GlobalVariable>>) {
3842 fn walk(
3843 body: &[Statement],
3844 exprs: &Arena<Expression>,
3845 input: Handle<GlobalVariable>,
3846 outputs: &[Handle<GlobalVariable>],
3847 written: &mut Vec<Handle<GlobalVariable>>,
3848 computed: &mut Vec<Handle<GlobalVariable>>,
3849 ) {
3850 for stmt in body {
3851 match stmt {
3852 Statement::Store { pointer, value } => {
3853 let Some(target) = access_base_global(exprs, *pointer) else {
3854 continue;
3855 };
3856 if !outputs.contains(&target) {
3857 continue;
3858 }
3859 if !written.contains(&target) {
3860 written.push(target);
3861 }
3862 let copied = match exprs.try_get(*value) {
3863 Some(Expression::Load { pointer }) => {
3864 access_base_global(exprs, *pointer) == Some(input)
3865 }
3866 _ => false,
3867 };
3868 if !copied && !computed.contains(&target) {
3869 computed.push(target);
3870 }
3871 }
3872 Statement::If { accept, reject, .. } => {
3873 walk(accept, exprs, input, outputs, written, computed);
3874 walk(reject, exprs, input, outputs, written, computed);
3875 }
3876 Statement::Loop {
3877 body, continuing, ..
3878 } => {
3879 walk(body, exprs, input, outputs, written, computed);
3880 walk(continuing, exprs, input, outputs, written, computed);
3881 }
3882 _ => {}
3883 }
3884 }
3885 }
3886
3887 let mut written = Vec::new();
3888 let mut computed = Vec::new();
3889 walk(body, exprs, input, outputs, &mut written, &mut computed);
3890 (written, computed)
3891}
3892
3893fn multi_output_refusal(
3899 module: &Module,
3900 outputs: &[(Handle<GlobalVariable>, &GlobalVariable)],
3901 input: &GlobalVariable,
3902 written: &[Handle<GlobalVariable>],
3903 computed: &[Handle<GlobalVariable>],
3904) -> String {
3905 let name_of = |handle: Handle<GlobalVariable>| -> String {
3906 outputs
3907 .iter()
3908 .find(|(h, _)| *h == handle)
3909 .and_then(|(_, gv)| gv.name.clone())
3910 .unwrap_or_else(|| "an output".into())
3911 };
3912 let quoted = |handles: &[Handle<GlobalVariable>]| -> String {
3913 handles
3914 .iter()
3915 .map(|h| format!("'{}'", name_of(*h)))
3916 .collect::<Vec<_>>()
3917 .join(" and ")
3918 };
3919
3920 let unwritten: Vec<Handle<GlobalVariable>> = outputs
3921 .iter()
3922 .map(|(h, _)| *h)
3923 .filter(|h| !written.contains(h))
3924 .collect();
3925
3926 let mut reason = format!(
3927 "1 input and {} outputs, but this does not slice its input: ",
3928 outputs.len()
3929 );
3930 if !computed.is_empty() {
3931 reason.push_str(&format!(
3932 "{} {} written with a computed value rather than a copy of the input",
3933 quoted(computed),
3934 if computed.len() == 1 { "is" } else { "are" },
3935 ));
3936 if !unwritten.is_empty() {
3937 reason.push_str(", and ");
3938 }
3939 }
3940 if !unwritten.is_empty() {
3941 reason.push_str(&format!(
3942 "{} {} never stored to",
3943 quoted(&unwritten),
3944 if unwritten.len() == 1 { "is" } else { "are" },
3945 ));
3946 }
3947 if computed.is_empty() && unwritten.is_empty() {
3948 reason.push_str("there is no comparison marking where one output ends and the next begins");
3950 }
3951
3952 let input_elem = resolve_array_elem_type(module, input.ty);
3956 if outputs
3957 .iter()
3958 .any(|(_, gv)| resolve_array_elem_type(module, gv.ty) != input_elem)
3959 {
3960 reason.push_str(
3961 ". The outputs do not all share the input's element type either, and a slice \
3962 never changes it",
3963 );
3964 }
3965
3966 reason.push_str(
3967 ". Splitting is the only single-input, multi-output op these backends lower, \
3968 so there is nothing here for one to carry",
3969 );
3970 reason
3971}
3972
3973fn detect_softmax(
3995 body: &[Statement],
3996 exprs: &Arena<Expression>,
3997 output: Handle<GlobalVariable>,
3998) -> bool {
3999 stores_a_normalized_exp(body, exprs, output) && sums_exponentials_in_a_loop(body, exprs, false)
4000}
4001
4002fn stores_a_normalized_exp(
4004 body: &[Statement],
4005 exprs: &Arena<Expression>,
4006 output: Handle<GlobalVariable>,
4007) -> bool {
4008 for stmt in body {
4009 match stmt {
4010 Statement::Store { pointer, value } => {
4011 if access_base_global(exprs, *pointer) == Some(output)
4012 && contains_math_fun(exprs, *value, MathFunction::Exp)
4013 && contains_binary_op(exprs, *value, BinaryOp::Divide)
4014 {
4015 return true;
4016 }
4017 }
4018 Statement::If { accept, reject, .. } => {
4019 if stores_a_normalized_exp(accept, exprs, output)
4020 || stores_a_normalized_exp(reject, exprs, output)
4021 {
4022 return true;
4023 }
4024 }
4025 Statement::Loop {
4026 body, continuing, ..
4027 } if stores_a_normalized_exp(body, exprs, output)
4028 || stores_a_normalized_exp(continuing, exprs, output) =>
4029 {
4030 return true;
4031 }
4032 _ => {}
4033 }
4034 }
4035 false
4036}
4037
4038fn sums_exponentials_in_a_loop(
4042 body: &[Statement],
4043 exprs: &Arena<Expression>,
4044 in_loop: bool,
4045) -> bool {
4046 for stmt in body {
4047 match stmt {
4048 Statement::Store { value, .. } => {
4049 if in_loop
4050 && matches!(
4051 exprs.try_get(*value),
4052 Some(Expression::Binary {
4053 op: BinaryOp::Add,
4054 ..
4055 })
4056 )
4057 && contains_math_fun(exprs, *value, MathFunction::Exp)
4058 {
4059 return true;
4060 }
4061 }
4062 Statement::If { accept, reject, .. } => {
4063 if sums_exponentials_in_a_loop(accept, exprs, in_loop)
4064 || sums_exponentials_in_a_loop(reject, exprs, in_loop)
4065 {
4066 return true;
4067 }
4068 }
4069 Statement::Loop {
4070 body, continuing, ..
4071 } if sums_exponentials_in_a_loop(body, exprs, true)
4072 || sums_exponentials_in_a_loop(continuing, exprs, true) =>
4073 {
4074 return true;
4075 }
4076 _ => {}
4077 }
4078 }
4079 false
4080}
4081
4082fn has_nested_loop(body: &[Statement]) -> bool {
4090 fn inside_loop(body: &[Statement]) -> bool {
4091 body.iter().any(|s| match s {
4092 Statement::Loop { .. } => true,
4093 Statement::If { accept, reject, .. } => inside_loop(accept) || inside_loop(reject),
4094 _ => false,
4095 })
4096 }
4097 body.iter().any(|s| match s {
4098 Statement::Loop {
4099 body, continuing, ..
4100 } => inside_loop(body) || inside_loop(continuing) || has_nested_loop(body),
4101 Statement::If { accept, reject, .. } => has_nested_loop(accept) || has_nested_loop(reject),
4102 _ => false,
4103 })
4104}
4105
4106fn store_value_activation(body: &[Statement], exprs: &Arena<Expression>) -> Option<ActivationOp> {
4122 for stmt in body {
4123 match stmt {
4124 Statement::Store { value, .. } => match exprs.try_get(*value) {
4125 Some(Expression::Math {
4126 fun: MathFunction::Max,
4127 ..
4128 }) => return Some(ActivationOp::Relu),
4129 Some(Expression::Math {
4130 fun: MathFunction::Tanh,
4131 ..
4132 }) => return Some(ActivationOp::Tanh),
4133 _ => {}
4134 },
4135 Statement::If { accept, reject, .. } => {
4136 if let Some(act) = store_value_activation(accept, exprs) {
4137 return Some(act);
4138 }
4139 if let Some(act) = store_value_activation(reject, exprs) {
4140 return Some(act);
4141 }
4142 }
4143 Statement::Loop {
4144 body, continuing, ..
4145 } => {
4146 if let Some(act) = store_value_activation(body, exprs) {
4147 return Some(act);
4148 }
4149 if let Some(act) = store_value_activation(continuing, exprs) {
4150 return Some(act);
4151 }
4152 }
4153 _ => {}
4154 }
4155 }
4156 None
4157}
4158
4159fn find_store_math_fun(
4160 body: &[Statement],
4161 exprs: &Arena<Expression>,
4162 target: MathFunction,
4163) -> bool {
4164 for stmt in body {
4165 match stmt {
4166 Statement::Store { value, .. } if contains_math_fun(exprs, *value, target) => {
4167 return true;
4168 }
4169 Statement::If { accept, reject, .. }
4170 if find_store_math_fun(accept, exprs, target)
4171 || find_store_math_fun(reject, exprs, target) =>
4172 {
4173 return true;
4174 }
4175 Statement::Loop {
4176 body, continuing, ..
4177 } if find_store_math_fun(body, exprs, target)
4178 || find_store_math_fun(continuing, exprs, target) =>
4179 {
4180 return true;
4181 }
4182 _ => {}
4183 }
4184 }
4185 false
4186}
4187
4188fn detect_reduce_op(body: &[Statement], exprs: &Arena<Expression>) -> ReduceOp {
4190 if find_store_math_fun(body, exprs, MathFunction::Max) {
4191 ReduceOp::Max
4192 } else if find_store_math_fun(body, exprs, MathFunction::Min) {
4193 ReduceOp::Min
4194 } else if find_store_binary_divide(body, exprs) {
4195 ReduceOp::Mean
4197 } else {
4198 ReduceOp::Sum
4200 }
4201}
4202
4203fn find_store_binary_divide(body: &[Statement], exprs: &Arena<Expression>) -> bool {
4205 for stmt in body {
4206 match stmt {
4207 Statement::Store { value, .. } => {
4208 if let Some(Expression::Binary {
4209 op: BinaryOp::Divide,
4210 ..
4211 }) = exprs.try_get(*value)
4212 {
4213 return true;
4214 }
4215 }
4216 Statement::If { accept, reject, .. }
4217 if find_store_binary_divide(accept, exprs)
4218 || find_store_binary_divide(reject, exprs) =>
4219 {
4220 return true;
4221 }
4222 Statement::Loop {
4223 body, continuing, ..
4224 } if find_store_binary_divide(body, exprs)
4225 || find_store_binary_divide(continuing, exprs) =>
4226 {
4227 return true;
4228 }
4229 _ => {}
4230 }
4231 }
4232 false
4233}
4234
4235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4248enum IndexedRead {
4249 Element,
4253 Block,
4256}
4257
4258fn stores_at_the_index_it_read(body: &[Statement], exprs: &Arena<Expression>) -> bool {
4271 fn walk(
4272 body: &[Statement],
4273 exprs: &Arena<Expression>,
4274 ok: &mut bool,
4275 seen: &mut Vec<Handle<Expression>>,
4276 ) {
4277 for stmt in body {
4278 match stmt {
4279 Statement::Store { pointer, value } => {
4280 let Some(store_index) = access_index(exprs, *pointer) else {
4281 *ok = false;
4282 return;
4283 };
4284 if seen.iter().all(|i| *i != store_index) {
4287 seen.push(store_index);
4288 }
4289 let mut reads = Vec::new();
4290 collect_load_indices(exprs, *value, &mut reads, 32);
4291 if !reads.contains(&store_index) {
4294 *ok = false;
4295 return;
4296 }
4297 }
4298 Statement::If { accept, reject, .. } => {
4299 walk(accept, exprs, ok, seen);
4300 walk(reject, exprs, ok, seen);
4301 }
4302 Statement::Loop {
4303 body, continuing, ..
4304 } => {
4305 walk(body, exprs, ok, seen);
4306 walk(continuing, exprs, ok, seen);
4307 }
4308 _ => {}
4309 }
4310 }
4311 }
4312 let mut ok = true;
4313 let mut seen = Vec::new();
4314 walk(body, exprs, &mut ok, &mut seen);
4315 ok && seen.len() == 1
4316}
4317
4318fn access_index(
4320 exprs: &Arena<Expression>,
4321 handle: Handle<Expression>,
4322) -> Option<Handle<Expression>> {
4323 match exprs.try_get(handle)? {
4324 Expression::Access { base, index } => {
4325 matches!(exprs.try_get(*base)?, Expression::GlobalVariable(_)).then_some(*index)
4326 }
4327 _ => None,
4328 }
4329}
4330
4331fn collect_load_indices(
4333 exprs: &Arena<Expression>,
4334 handle: Handle<Expression>,
4335 out: &mut Vec<Handle<Expression>>,
4336 depth: u32,
4337) {
4338 if depth == 0 {
4339 return;
4340 }
4341 match exprs.try_get(handle) {
4342 Some(Expression::Load { pointer }) => {
4343 if let Some(index) = access_index(exprs, *pointer) {
4344 out.push(index);
4345 }
4346 }
4347 Some(Expression::Binary { left, right, .. }) => {
4348 collect_load_indices(exprs, *left, out, depth - 1);
4349 collect_load_indices(exprs, *right, out, depth - 1);
4350 }
4351 Some(Expression::Unary { expr, .. }) => collect_load_indices(exprs, *expr, out, depth - 1),
4352 Some(Expression::As { expr, .. }) => collect_load_indices(exprs, *expr, out, depth - 1),
4356 Some(Expression::Math {
4357 arg,
4358 arg1,
4359 arg2,
4360 arg3,
4361 ..
4362 }) => {
4363 collect_load_indices(exprs, *arg, out, depth - 1);
4364 for a in [arg1, arg2, arg3].into_iter().flatten() {
4365 collect_load_indices(exprs, *a, out, depth - 1);
4366 }
4367 }
4368 _ => {}
4369 }
4370}
4371
4372fn access_base_global(
4374 exprs: &Arena<Expression>,
4375 handle: Handle<Expression>,
4376) -> Option<Handle<GlobalVariable>> {
4377 let mut handle = handle;
4378 for _ in 0..32 {
4379 match exprs.try_get(handle)? {
4380 Expression::GlobalVariable(g) => return Some(*g),
4381 Expression::Access { base, .. } | Expression::AccessIndex { base, .. } => {
4382 handle = *base;
4383 }
4384 _ => return None,
4385 }
4386 }
4387 None
4388}
4389
4390fn strip_casts(exprs: &Arena<Expression>, handle: Handle<Expression>) -> Handle<Expression> {
4392 let mut handle = handle;
4393 for _ in 0..8 {
4394 match exprs.try_get(handle) {
4395 Some(Expression::As { expr, .. }) => handle = *expr,
4396 _ => break,
4397 }
4398 }
4399 handle
4400}
4401
4402fn load_address(
4404 exprs: &Arena<Expression>,
4405 handle: Handle<Expression>,
4406 global: Handle<GlobalVariable>,
4407) -> Option<Handle<Expression>> {
4408 let Expression::Load { pointer } = exprs.try_get(handle)? else {
4409 return None;
4410 };
4411 let Expression::Access { base, index } = exprs.try_get(*pointer)? else {
4412 return None;
4413 };
4414 (access_base_global(exprs, *base) == Some(global)).then_some(*index)
4415}
4416
4417fn reads_global(
4423 exprs: &Arena<Expression>,
4424 roots: &[Handle<Expression>],
4425 wanted: &[Handle<GlobalVariable>],
4426) -> bool {
4427 let mut seen = std::collections::HashSet::new();
4428 let mut stack: Vec<Handle<Expression>> = roots.to_vec();
4429 while let Some(handle) = stack.pop() {
4430 if !seen.insert(handle.index()) {
4431 continue;
4432 }
4433 let Some(expr) = exprs.try_get(handle) else {
4434 continue;
4435 };
4436 #[allow(clippy::collapsible_if)]
4439 if let Expression::Load { pointer } = expr {
4440 if access_base_global(exprs, *pointer).is_some_and(|g| wanted.contains(&g)) {
4441 return true;
4442 }
4443 }
4444 push_operands(expr, &mut stack);
4445 }
4446 false
4447}
4448
4449fn push_operands(expr: &Expression, stack: &mut Vec<Handle<Expression>>) {
4451 match expr {
4452 Expression::Load { pointer } => stack.push(*pointer),
4453 Expression::Access { base, index } => {
4454 stack.push(*base);
4455 stack.push(*index);
4456 }
4457 Expression::AccessIndex { base, .. } => stack.push(*base),
4458 Expression::Unary { expr, .. } => stack.push(*expr),
4459 Expression::Binary { left, right, .. } => {
4460 stack.push(*left);
4461 stack.push(*right);
4462 }
4463 Expression::Select {
4464 condition,
4465 accept,
4466 reject,
4467 } => {
4468 stack.push(*condition);
4469 stack.push(*accept);
4470 stack.push(*reject);
4471 }
4472 Expression::Math {
4473 arg,
4474 arg1,
4475 arg2,
4476 arg3,
4477 ..
4478 } => {
4479 stack.push(*arg);
4480 stack.extend([*arg1, *arg2, *arg3].into_iter().flatten());
4481 }
4482 Expression::As { expr, .. } => stack.push(*expr),
4483 Expression::Splat { value, .. } => stack.push(*value),
4484 Expression::Swizzle { vector, .. } => stack.push(*vector),
4485 Expression::Compose { components, .. } => stack.extend(components.iter().copied()),
4486 Expression::ArrayLength(handle) => stack.push(*handle),
4487 _ => {}
4488 }
4489}
4490
4491fn detect_indexed_read(
4498 body: &[Statement],
4499 exprs: &Arena<Expression>,
4500 data: Handle<GlobalVariable>,
4501 indices: Handle<GlobalVariable>,
4502 output: Handle<GlobalVariable>,
4503) -> Option<IndexedRead> {
4504 for stmt in body {
4505 match stmt {
4506 Statement::Store { pointer, value } => {
4507 if access_base_global(exprs, *pointer) != Some(output) {
4508 continue;
4509 }
4510 let Some(address) = load_address(exprs, strip_casts(exprs, *value), data) else {
4511 continue;
4512 };
4513 if !reads_global(exprs, &[address], &[indices]) {
4514 continue;
4515 }
4516 return Some(
4517 if load_address(exprs, strip_casts(exprs, address), indices).is_some() {
4518 IndexedRead::Element
4519 } else {
4520 IndexedRead::Block
4521 },
4522 );
4523 }
4524 Statement::If { accept, reject, .. } => {
4525 if let Some(kind) = detect_indexed_read(accept, exprs, data, indices, output)
4526 .or_else(|| detect_indexed_read(reject, exprs, data, indices, output))
4527 {
4528 return Some(kind);
4529 }
4530 }
4531 Statement::Loop {
4532 body, continuing, ..
4533 } => {
4534 if let Some(kind) = detect_indexed_read(body, exprs, data, indices, output)
4535 .or_else(|| detect_indexed_read(continuing, exprs, data, indices, output))
4536 {
4537 return Some(kind);
4538 }
4539 }
4540 _ => {}
4541 }
4542 }
4543 None
4544}
4545
4546fn detect_indexed_write(
4549 body: &[Statement],
4550 exprs: &Arena<Expression>,
4551 sources: &[Handle<GlobalVariable>],
4552 outputs: &[Handle<GlobalVariable>],
4553) -> bool {
4554 for stmt in body {
4555 match stmt {
4556 Statement::Store { pointer, .. } => {
4557 let Some(Expression::Access { base, index }) = exprs.try_get(*pointer) else {
4558 continue;
4559 };
4560 if !access_base_global(exprs, *base).is_some_and(|g| outputs.contains(&g)) {
4561 continue;
4562 }
4563 if reads_global(exprs, &[*index], sources) {
4564 return true;
4565 }
4566 }
4567 Statement::If { accept, reject, .. }
4568 if detect_indexed_write(accept, exprs, sources, outputs)
4569 || detect_indexed_write(reject, exprs, sources, outputs) =>
4570 {
4571 return true;
4572 }
4573 Statement::Loop {
4574 body, continuing, ..
4575 } if detect_indexed_write(body, exprs, sources, outputs)
4576 || detect_indexed_write(continuing, exprs, sources, outputs) =>
4577 {
4578 return true;
4579 }
4580 _ => {}
4581 }
4582 }
4583 false
4584}
4585
4586fn is_atomic_buffer(module: &Module, ty: Handle<Type>) -> bool {
4591 match &module.types[ty].inner {
4592 TypeInner::Atomic(_) => true,
4593 TypeInner::Array { base, .. } => matches!(&module.types[*base].inner, TypeInner::Atomic(_)),
4594 _ => false,
4595 }
4596}
4597
4598#[cfg(test)]
4599mod tests {
4600 use super::*;
4601 use nxpu_ir::*;
4602
4603 fn make_matmul_module() -> Module {
4604 let mut module = Module::default();
4605
4606 let f32_ty = module.types.insert(Type {
4607 name: None,
4608 inner: TypeInner::Scalar(Scalar::F32),
4609 });
4610 let u32_ty = module.types.insert(Type {
4611 name: None,
4612 inner: TypeInner::Scalar(Scalar::U32),
4613 });
4614 let array_f32 = module.types.insert(Type {
4615 name: None,
4616 inner: TypeInner::Array {
4617 base: f32_ty,
4618 size: ArraySize::Dynamic,
4619 stride: 4,
4620 },
4621 });
4622 let params_ty = module.types.insert(Type {
4623 name: Some("Params".into()),
4624 inner: TypeInner::Struct {
4625 members: vec![
4626 StructMember {
4627 name: Some("M".into()),
4628 ty: u32_ty,
4629 offset: 0,
4630 },
4631 StructMember {
4632 name: Some("N".into()),
4633 ty: u32_ty,
4634 offset: 4,
4635 },
4636 StructMember {
4637 name: Some("K".into()),
4638 ty: u32_ty,
4639 offset: 8,
4640 },
4641 ],
4642 span: 12,
4643 },
4644 });
4645
4646 module.global_variables.append(GlobalVariable {
4647 name: Some("a".into()),
4648 space: AddressSpace::Storage {
4649 access: StorageAccess::LOAD,
4650 },
4651 binding: Some(ResourceBinding {
4652 group: 0,
4653 binding: 0,
4654 }),
4655 ty: array_f32,
4656 init: None,
4657 layout: None,
4658 });
4659 module.global_variables.append(GlobalVariable {
4660 name: Some("b".into()),
4661 space: AddressSpace::Storage {
4662 access: StorageAccess::LOAD,
4663 },
4664 binding: Some(ResourceBinding {
4665 group: 0,
4666 binding: 1,
4667 }),
4668 ty: array_f32,
4669 init: None,
4670 layout: None,
4671 });
4672 module.global_variables.append(GlobalVariable {
4673 name: Some("result".into()),
4674 space: AddressSpace::Storage {
4675 access: StorageAccess::LOAD | StorageAccess::STORE,
4676 },
4677 binding: Some(ResourceBinding {
4678 group: 0,
4679 binding: 2,
4680 }),
4681 ty: array_f32,
4682 init: None,
4683 layout: None,
4684 });
4685 module.global_variables.append(GlobalVariable {
4686 name: Some("params".into()),
4687 space: AddressSpace::Uniform,
4688 binding: Some(ResourceBinding {
4689 group: 0,
4690 binding: 3,
4691 }),
4692 ty: params_ty,
4693 init: None,
4694 layout: None,
4695 });
4696
4697 let mut func = Function::new("main");
4699 func.body.push(Statement::Loop {
4700 body: vec![Statement::Break],
4701 continuing: vec![],
4702 break_if: None,
4703 });
4704
4705 module.entry_points.push(EntryPoint {
4706 name: "main".into(),
4707 workgroup_size: [16, 16, 1],
4708 function: func,
4709 });
4710
4711 module
4712 }
4713
4714 fn make_elementwise_module(op: BinaryOp) -> Module {
4715 let mut module = Module::default();
4716
4717 let f32_ty = module.types.insert(Type {
4718 name: None,
4719 inner: TypeInner::Scalar(Scalar::F32),
4720 });
4721 let u32_ty = module.types.insert(Type {
4722 name: None,
4723 inner: TypeInner::Scalar(Scalar::U32),
4724 });
4725 let array_f32 = module.types.insert(Type {
4726 name: None,
4727 inner: TypeInner::Array {
4728 base: f32_ty,
4729 size: ArraySize::Dynamic,
4730 stride: 4,
4731 },
4732 });
4733 let params_ty = module.types.insert(Type {
4734 name: Some("Params".into()),
4735 inner: TypeInner::Struct {
4736 members: vec![StructMember {
4737 name: Some("N".into()),
4738 ty: u32_ty,
4739 offset: 0,
4740 }],
4741 span: 4,
4742 },
4743 });
4744
4745 let a_gv = module.global_variables.append(GlobalVariable {
4746 name: Some("a".into()),
4747 space: AddressSpace::Storage {
4748 access: StorageAccess::LOAD,
4749 },
4750 binding: Some(ResourceBinding {
4751 group: 0,
4752 binding: 0,
4753 }),
4754 ty: array_f32,
4755 init: None,
4756 layout: None,
4757 });
4758 let b_gv = module.global_variables.append(GlobalVariable {
4759 name: Some("b".into()),
4760 space: AddressSpace::Storage {
4761 access: StorageAccess::LOAD,
4762 },
4763 binding: Some(ResourceBinding {
4764 group: 0,
4765 binding: 1,
4766 }),
4767 ty: array_f32,
4768 init: None,
4769 layout: None,
4770 });
4771 let out_gv = module.global_variables.append(GlobalVariable {
4772 name: Some("c".into()),
4773 space: AddressSpace::Storage {
4774 access: StorageAccess::LOAD | StorageAccess::STORE,
4775 },
4776 binding: Some(ResourceBinding {
4777 group: 0,
4778 binding: 2,
4779 }),
4780 ty: array_f32,
4781 init: None,
4782 layout: None,
4783 });
4784 module.global_variables.append(GlobalVariable {
4785 name: Some("params".into()),
4786 space: AddressSpace::Uniform,
4787 binding: Some(ResourceBinding {
4788 group: 0,
4789 binding: 3,
4790 }),
4791 ty: params_ty,
4792 init: None,
4793 layout: None,
4794 });
4795
4796 let mut func = Function::new("main");
4803 let idx = func
4804 .expressions
4805 .append(Expression::Literal(Literal::U32(0)));
4806 let load_from = |func: &mut Function, gv| {
4807 let base = func.expressions.append(Expression::GlobalVariable(gv));
4808 let access = func
4809 .expressions
4810 .append(Expression::Access { base, index: idx });
4811 func.expressions
4812 .append(Expression::Load { pointer: access })
4813 };
4814 let left = load_from(&mut func, a_gv);
4815 let right = load_from(&mut func, b_gv);
4816 let binary = func
4817 .expressions
4818 .append(Expression::Binary { op, left, right });
4819 let out_base = func.expressions.append(Expression::GlobalVariable(out_gv));
4820 let ptr = func.expressions.append(Expression::Access {
4821 base: out_base,
4822 index: idx,
4823 });
4824 func.body.push(Statement::Store {
4825 pointer: ptr,
4826 value: binary,
4827 });
4828
4829 module.entry_points.push(EntryPoint {
4830 name: "vecadd".into(),
4831 workgroup_size: [256, 1, 1],
4832 function: func,
4833 });
4834
4835 module
4836 }
4837
4838 fn make_activation_module(math_fun: MathFunction) -> Module {
4839 let mut module = Module::default();
4840
4841 let f32_ty = module.types.insert(Type {
4842 name: None,
4843 inner: TypeInner::Scalar(Scalar::F32),
4844 });
4845 let u32_ty = module.types.insert(Type {
4846 name: None,
4847 inner: TypeInner::Scalar(Scalar::U32),
4848 });
4849 let array_f32 = module.types.insert(Type {
4850 name: None,
4851 inner: TypeInner::Array {
4852 base: f32_ty,
4853 size: ArraySize::Dynamic,
4854 stride: 4,
4855 },
4856 });
4857 let params_ty = module.types.insert(Type {
4858 name: Some("Params".into()),
4859 inner: TypeInner::Struct {
4860 members: vec![StructMember {
4861 name: Some("N".into()),
4862 ty: u32_ty,
4863 offset: 0,
4864 }],
4865 span: 4,
4866 },
4867 });
4868
4869 module.global_variables.append(GlobalVariable {
4871 name: Some("a".into()),
4872 space: AddressSpace::Storage {
4873 access: StorageAccess::LOAD,
4874 },
4875 binding: Some(ResourceBinding {
4876 group: 0,
4877 binding: 0,
4878 }),
4879 ty: array_f32,
4880 init: None,
4881 layout: None,
4882 });
4883 module.global_variables.append(GlobalVariable {
4885 name: Some("c".into()),
4886 space: AddressSpace::Storage {
4887 access: StorageAccess::LOAD | StorageAccess::STORE,
4888 },
4889 binding: Some(ResourceBinding {
4890 group: 0,
4891 binding: 1,
4892 }),
4893 ty: array_f32,
4894 init: None,
4895 layout: None,
4896 });
4897 module.global_variables.append(GlobalVariable {
4898 name: Some("params".into()),
4899 space: AddressSpace::Uniform,
4900 binding: Some(ResourceBinding {
4901 group: 0,
4902 binding: 2,
4903 }),
4904 ty: params_ty,
4905 init: None,
4906 layout: None,
4907 });
4908
4909 let mut func = Function::new("main");
4910 let arg = func
4911 .expressions
4912 .append(Expression::Literal(Literal::F32(1.0)));
4913 let arg1 = func
4914 .expressions
4915 .append(Expression::Literal(Literal::F32(0.0)));
4916 let math = func.expressions.append(Expression::Math {
4917 fun: math_fun,
4918 arg,
4919 arg1: Some(arg1),
4920 arg2: None,
4921 arg3: None,
4922 });
4923 let ptr = func
4924 .expressions
4925 .append(Expression::Literal(Literal::F32(0.0)));
4926 func.body.push(Statement::Store {
4927 pointer: ptr,
4928 value: math,
4929 });
4930
4931 module.entry_points.push(EntryPoint {
4932 name: "activation".into(),
4933 workgroup_size: [256, 1, 1],
4934 function: func,
4935 });
4936
4937 module
4938 }
4939
4940 fn make_reduce_module() -> Module {
4941 let mut module = Module::default();
4942
4943 let f32_ty = module.types.insert(Type {
4944 name: None,
4945 inner: TypeInner::Scalar(Scalar::F32),
4946 });
4947 let u32_ty = module.types.insert(Type {
4948 name: None,
4949 inner: TypeInner::Scalar(Scalar::U32),
4950 });
4951 let array_f32 = module.types.insert(Type {
4952 name: None,
4953 inner: TypeInner::Array {
4954 base: f32_ty,
4955 size: ArraySize::Dynamic,
4956 stride: 4,
4957 },
4958 });
4959 let params_ty = module.types.insert(Type {
4960 name: Some("Params".into()),
4961 inner: TypeInner::Struct {
4962 members: vec![StructMember {
4963 name: Some("N".into()),
4964 ty: u32_ty,
4965 offset: 0,
4966 }],
4967 span: 4,
4968 },
4969 });
4970
4971 module.global_variables.append(GlobalVariable {
4972 name: Some("a".into()),
4973 space: AddressSpace::Storage {
4974 access: StorageAccess::LOAD,
4975 },
4976 binding: Some(ResourceBinding {
4977 group: 0,
4978 binding: 0,
4979 }),
4980 ty: array_f32,
4981 init: None,
4982 layout: None,
4983 });
4984 module.global_variables.append(GlobalVariable {
4985 name: Some("c".into()),
4986 space: AddressSpace::Storage {
4987 access: StorageAccess::LOAD | StorageAccess::STORE,
4988 },
4989 binding: Some(ResourceBinding {
4990 group: 0,
4991 binding: 1,
4992 }),
4993 ty: array_f32,
4994 init: None,
4995 layout: None,
4996 });
4997 module.global_variables.append(GlobalVariable {
4998 name: Some("params".into()),
4999 space: AddressSpace::Uniform,
5000 binding: Some(ResourceBinding {
5001 group: 0,
5002 binding: 2,
5003 }),
5004 ty: params_ty,
5005 init: None,
5006 layout: None,
5007 });
5008
5009 let mut func = Function::new("main");
5010 let left = func
5012 .expressions
5013 .append(Expression::Literal(Literal::F32(0.0)));
5014 let right = func
5015 .expressions
5016 .append(Expression::Literal(Literal::F32(1.0)));
5017 let add = func.expressions.append(Expression::Binary {
5018 op: BinaryOp::Add,
5019 left,
5020 right,
5021 });
5022 let ptr = func
5023 .expressions
5024 .append(Expression::Literal(Literal::F32(0.0)));
5025 func.body.push(Statement::Loop {
5026 body: vec![
5027 Statement::Store {
5028 pointer: ptr,
5029 value: add,
5030 },
5031 Statement::Break,
5032 ],
5033 continuing: vec![],
5034 break_if: None,
5035 });
5036
5037 module.entry_points.push(EntryPoint {
5038 name: "reduce".into(),
5039 workgroup_size: [256, 1, 1],
5040 function: func,
5041 });
5042
5043 module
5044 }
5045
5046 #[test]
5047 fn classify_matmul() {
5048 let module = make_matmul_module();
5049 let pattern = classify_entry_point(&module, 0).unwrap();
5050 match pattern {
5051 KernelPattern::MatMul {
5052 inputs,
5053 output,
5054 shape,
5055 } => {
5056 assert_eq!(inputs[0].name, "a");
5057 assert_eq!(inputs[1].name, "b");
5058 assert_eq!(output.name, "result");
5059 assert_eq!(inputs[0].elem_type, data_type::FLOAT);
5060 assert_eq!(shape.m, "M");
5061 assert_eq!(shape.n, "N");
5062 assert_eq!(shape.k, "K");
5063 }
5064 _ => panic!("expected MatMul pattern"),
5065 }
5066 }
5067
5068 #[test]
5069 fn classify_elementwise_add() {
5070 let module = make_elementwise_module(BinaryOp::Add);
5071 let pattern = classify_entry_point(&module, 0).unwrap();
5072 match pattern {
5073 KernelPattern::ElementWise {
5074 op,
5075 inputs,
5076 output,
5077 dim_name,
5078 } => {
5079 assert_eq!(op, ElementWiseOp::Add);
5080 assert_eq!(inputs[0].name, "a");
5081 assert_eq!(inputs[1].name, "b");
5082 assert_eq!(output.name, "c");
5083 assert_eq!(dim_name, "N");
5084 }
5085 _ => panic!("expected ElementWise pattern"),
5086 }
5087 }
5088
5089 #[test]
5090 fn classify_elementwise_div() {
5091 let module = make_elementwise_module(BinaryOp::Divide);
5092 let pattern = classify_entry_point(&module, 0).unwrap();
5093 match &pattern {
5094 KernelPattern::ElementWise { op, .. } => {
5095 assert_eq!(*op, ElementWiseOp::Div);
5096 }
5097 _ => panic!("expected ElementWise pattern"),
5098 }
5099 }
5100
5101 #[test]
5102 fn classify_activation_relu() {
5103 let module = make_activation_module(MathFunction::Max);
5104 let pattern = classify_entry_point(&module, 0).unwrap();
5105 match &pattern {
5106 KernelPattern::Activation { op, .. } => {
5107 assert_eq!(*op, ActivationOp::Relu);
5108 }
5109 _ => panic!("expected Activation pattern, got {pattern:?}"),
5110 }
5111 }
5112
5113 #[test]
5114 fn classify_activation_tanh() {
5115 let module = make_activation_module(MathFunction::Tanh);
5116 let pattern = classify_entry_point(&module, 0).unwrap();
5117 match &pattern {
5118 KernelPattern::Activation { op, .. } => {
5119 assert_eq!(*op, ActivationOp::Tanh);
5120 }
5121 _ => panic!("expected Activation pattern, got {pattern:?}"),
5122 }
5123 }
5124
5125 #[test]
5126 fn classify_reduce_sum() {
5127 let module = make_reduce_module();
5128 let pattern = classify_entry_point(&module, 0).unwrap();
5129 match &pattern {
5130 KernelPattern::Reduce { op, .. } => {
5131 assert_eq!(*op, ReduceOp::Sum);
5132 }
5133 _ => panic!("expected Reduce pattern, got {pattern:?}"),
5134 }
5135 }
5136
5137 #[test]
5138 fn classify_single_input_no_activation_unknown() {
5139 let mut module = Module::default();
5141
5142 let f32_ty = module.types.insert(Type {
5143 name: None,
5144 inner: TypeInner::Scalar(Scalar::F32),
5145 });
5146 let u32_ty = module.types.insert(Type {
5147 name: None,
5148 inner: TypeInner::Scalar(Scalar::U32),
5149 });
5150 let array_f32 = module.types.insert(Type {
5151 name: None,
5152 inner: TypeInner::Array {
5153 base: f32_ty,
5154 size: ArraySize::Dynamic,
5155 stride: 4,
5156 },
5157 });
5158 let params_ty = module.types.insert(Type {
5159 name: Some("Params".into()),
5160 inner: TypeInner::Struct {
5161 members: vec![
5162 StructMember {
5163 name: Some("rows".into()),
5164 ty: u32_ty,
5165 offset: 0,
5166 },
5167 StructMember {
5168 name: Some("cols".into()),
5169 ty: u32_ty,
5170 offset: 4,
5171 },
5172 ],
5173 span: 8,
5174 },
5175 });
5176
5177 module.global_variables.append(GlobalVariable {
5178 name: Some("a".into()),
5179 space: AddressSpace::Storage {
5180 access: StorageAccess::LOAD,
5181 },
5182 binding: Some(ResourceBinding {
5183 group: 0,
5184 binding: 0,
5185 }),
5186 ty: array_f32,
5187 init: None,
5188 layout: None,
5189 });
5190 module.global_variables.append(GlobalVariable {
5191 name: Some("c".into()),
5192 space: AddressSpace::Storage {
5193 access: StorageAccess::LOAD | StorageAccess::STORE,
5194 },
5195 binding: Some(ResourceBinding {
5196 group: 0,
5197 binding: 1,
5198 }),
5199 ty: array_f32,
5200 init: None,
5201 layout: None,
5202 });
5203 module.global_variables.append(GlobalVariable {
5204 name: Some("params".into()),
5205 space: AddressSpace::Uniform,
5206 binding: Some(ResourceBinding {
5207 group: 0,
5208 binding: 2,
5209 }),
5210 ty: params_ty,
5211 init: None,
5212 layout: None,
5213 });
5214
5215 let mut func = Function::new("main");
5217 let val = func
5218 .expressions
5219 .append(Expression::Literal(Literal::F32(42.0)));
5220 let ptr = func
5221 .expressions
5222 .append(Expression::Literal(Literal::F32(0.0)));
5223 func.body.push(Statement::Store {
5224 pointer: ptr,
5225 value: val,
5226 });
5227
5228 module.entry_points.push(EntryPoint {
5229 name: "unknown_kernel".into(),
5230 workgroup_size: [256, 1, 1],
5231 function: func,
5232 });
5233
5234 let pattern = classify_entry_point(&module, 0).unwrap();
5235 assert!(
5236 matches!(&pattern, KernelPattern::Unknown { .. }),
5237 "expected Unknown pattern, got {pattern:?}"
5238 );
5239 }
5240
5241 #[test]
5242 fn classify_elementwise_with_embedded_weight() {
5243 let mut module = Module::default();
5245
5246 let f32_ty = module.types.insert(Type {
5247 name: None,
5248 inner: TypeInner::Scalar(Scalar::F32),
5249 });
5250 let u32_ty = module.types.insert(Type {
5251 name: None,
5252 inner: TypeInner::Scalar(Scalar::U32),
5253 });
5254 let array_f32 = module.types.insert(Type {
5255 name: None,
5256 inner: TypeInner::Array {
5257 base: f32_ty,
5258 size: ArraySize::Dynamic,
5259 stride: 4,
5260 },
5261 });
5262 let array_f32_4 = module.types.insert(Type {
5263 name: None,
5264 inner: TypeInner::Array {
5265 base: f32_ty,
5266 size: ArraySize::Constant(4),
5267 stride: 4,
5268 },
5269 });
5270 let params_ty = module.types.insert(Type {
5271 name: Some("Params".into()),
5272 inner: TypeInner::Struct {
5273 members: vec![StructMember {
5274 name: Some("N".into()),
5275 ty: u32_ty,
5276 offset: 0,
5277 }],
5278 span: 4,
5279 },
5280 });
5281
5282 module.global_variables.append(GlobalVariable {
5284 name: Some("input".into()),
5285 space: AddressSpace::Storage {
5286 access: StorageAccess::LOAD,
5287 },
5288 binding: Some(ResourceBinding {
5289 group: 0,
5290 binding: 0,
5291 }),
5292 ty: array_f32,
5293 init: None,
5294 layout: None,
5295 });
5296 module.global_variables.append(GlobalVariable {
5298 name: Some("output".into()),
5299 space: AddressSpace::Storage {
5300 access: StorageAccess::LOAD | StorageAccess::STORE,
5301 },
5302 binding: Some(ResourceBinding {
5303 group: 0,
5304 binding: 1,
5305 }),
5306 ty: array_f32,
5307 init: None,
5308 layout: None,
5309 });
5310
5311 let lit0 = module
5313 .global_expressions
5314 .append(Expression::Literal(Literal::F32(0.1)));
5315 let lit1 = module
5316 .global_expressions
5317 .append(Expression::Literal(Literal::F32(0.2)));
5318 let lit2 = module
5319 .global_expressions
5320 .append(Expression::Literal(Literal::F32(0.3)));
5321 let lit3 = module
5322 .global_expressions
5323 .append(Expression::Literal(Literal::F32(0.4)));
5324 let compose = module.global_expressions.append(Expression::Compose {
5325 ty: array_f32_4,
5326 components: vec![lit0, lit1, lit2, lit3],
5327 });
5328 module.global_variables.append(GlobalVariable {
5329 name: Some("bias".into()),
5330 space: AddressSpace::Private,
5331 binding: None,
5332 ty: array_f32_4,
5333 init: Some(compose),
5334 layout: None,
5335 });
5336
5337 module.global_variables.append(GlobalVariable {
5339 name: Some("params".into()),
5340 space: AddressSpace::Uniform,
5341 binding: Some(ResourceBinding {
5342 group: 0,
5343 binding: 2,
5344 }),
5345 ty: params_ty,
5346 init: None,
5347 layout: None,
5348 });
5349
5350 let mut func = Function::new("main");
5352 let left = func
5353 .expressions
5354 .append(Expression::Literal(Literal::F32(1.0)));
5355 let right = func
5356 .expressions
5357 .append(Expression::Literal(Literal::F32(2.0)));
5358 let binary = func.expressions.append(Expression::Binary {
5359 op: BinaryOp::Add,
5360 left,
5361 right,
5362 });
5363 let ptr = func
5364 .expressions
5365 .append(Expression::Literal(Literal::F32(0.0)));
5366 func.body.push(Statement::Store {
5367 pointer: ptr,
5368 value: binary,
5369 });
5370
5371 module.entry_points.push(EntryPoint {
5372 name: "main".into(),
5373 workgroup_size: [64, 1, 1],
5374 function: func,
5375 });
5376
5377 let pattern = classify_entry_point(&module, 0).unwrap();
5378 match &pattern {
5379 KernelPattern::ElementWise {
5380 op,
5381 inputs,
5382 output,
5383 dim_name,
5384 } => {
5385 assert_eq!(*op, ElementWiseOp::Add);
5386 assert_eq!(inputs[0].name, "input");
5387 assert_eq!(inputs[1].name, "bias");
5388 assert_eq!(output.name, "output");
5389 assert_eq!(dim_name, "N");
5390 }
5391 _ => panic!("expected ElementWise pattern, got {pattern:?}"),
5392 }
5393 }
5394
5395 #[test]
5396 fn detect_reduce_mean() {
5397 let mut func = Function::new("test");
5399 let left = func
5400 .expressions
5401 .append(Expression::Literal(Literal::F32(0.0)));
5402 let right = func
5403 .expressions
5404 .append(Expression::Literal(Literal::F32(1.0)));
5405 let div = func.expressions.append(Expression::Binary {
5406 op: BinaryOp::Divide,
5407 left,
5408 right,
5409 });
5410 let ptr = func
5411 .expressions
5412 .append(Expression::Literal(Literal::F32(0.0)));
5413 let body = vec![
5414 Statement::Store {
5415 pointer: ptr,
5416 value: div,
5417 },
5418 Statement::Break,
5419 ];
5420 let result = detect_reduce_op(&body, &func.expressions);
5421 assert_eq!(result, ReduceOp::Mean);
5422 }
5423
5424 #[test]
5425 fn classify_out_of_range() {
5426 let module = make_matmul_module();
5427 let err = classify_entry_point(&module, 99).unwrap_err();
5428 assert!(matches!(err, AnalysisError::EntryPointOutOfRange(99)));
5429 }
5430
5431 #[test]
5432 fn classify_empty_module() {
5433 let module = Module::default();
5434 let err = classify_entry_point(&module, 0).unwrap_err();
5435 assert!(matches!(err, AnalysisError::NoEntryPoints));
5436 }
5437
5438 #[test]
5439 fn input_sorted_by_binding() {
5440 let mut module = Module::default();
5443
5444 let f32_ty = module.types.insert(Type {
5445 name: None,
5446 inner: TypeInner::Scalar(Scalar::F32),
5447 });
5448 let u32_ty = module.types.insert(Type {
5449 name: None,
5450 inner: TypeInner::Scalar(Scalar::U32),
5451 });
5452 let array_f32 = module.types.insert(Type {
5453 name: None,
5454 inner: TypeInner::Array {
5455 base: f32_ty,
5456 size: ArraySize::Dynamic,
5457 stride: 4,
5458 },
5459 });
5460 let params_ty = module.types.insert(Type {
5461 name: Some("Params".into()),
5462 inner: TypeInner::Struct {
5463 members: vec![
5464 StructMember {
5465 name: Some("M".into()),
5466 ty: u32_ty,
5467 offset: 0,
5468 },
5469 StructMember {
5470 name: Some("N".into()),
5471 ty: u32_ty,
5472 offset: 4,
5473 },
5474 StructMember {
5475 name: Some("K".into()),
5476 ty: u32_ty,
5477 offset: 8,
5478 },
5479 ],
5480 span: 12,
5481 },
5482 });
5483
5484 module.global_variables.append(GlobalVariable {
5486 name: Some("b".into()),
5487 space: AddressSpace::Storage {
5488 access: StorageAccess::LOAD,
5489 },
5490 binding: Some(ResourceBinding {
5491 group: 0,
5492 binding: 1,
5493 }),
5494 ty: array_f32,
5495 init: None,
5496 layout: None,
5497 });
5498 module.global_variables.append(GlobalVariable {
5499 name: Some("a".into()),
5500 space: AddressSpace::Storage {
5501 access: StorageAccess::LOAD,
5502 },
5503 binding: Some(ResourceBinding {
5504 group: 0,
5505 binding: 0,
5506 }),
5507 ty: array_f32,
5508 init: None,
5509 layout: None,
5510 });
5511 module.global_variables.append(GlobalVariable {
5512 name: Some("result".into()),
5513 space: AddressSpace::Storage {
5514 access: StorageAccess::LOAD | StorageAccess::STORE,
5515 },
5516 binding: Some(ResourceBinding {
5517 group: 0,
5518 binding: 2,
5519 }),
5520 ty: array_f32,
5521 init: None,
5522 layout: None,
5523 });
5524 module.global_variables.append(GlobalVariable {
5525 name: Some("params".into()),
5526 space: AddressSpace::Uniform,
5527 binding: Some(ResourceBinding {
5528 group: 0,
5529 binding: 3,
5530 }),
5531 ty: params_ty,
5532 init: None,
5533 layout: None,
5534 });
5535
5536 let mut func = Function::new("main");
5537 func.body.push(Statement::Loop {
5538 body: vec![Statement::Break],
5539 continuing: vec![],
5540 break_if: None,
5541 });
5542 module.entry_points.push(EntryPoint {
5543 name: "main".into(),
5544 workgroup_size: [16, 16, 1],
5545 function: func,
5546 });
5547
5548 let pattern = classify_entry_point(&module, 0).unwrap();
5549 match pattern {
5550 KernelPattern::MatMul { inputs, .. } => {
5551 assert_eq!(inputs[0].name, "a"); assert_eq!(inputs[1].name, "b"); }
5554 _ => panic!("expected MatMul pattern"),
5555 }
5556 }
5557
5558 #[test]
5559 fn extract_loop_bounds_literal_u32() {
5560 let mut func = Function::new("test");
5561 let local = func.local_variables.append(LocalVariable {
5562 name: Some("kh".into()),
5563 ty: {
5564 let mut types = UniqueArena::new();
5565 types.insert(Type {
5566 name: None,
5567 inner: TypeInner::Scalar(Scalar::U32),
5568 })
5569 },
5570 init: None,
5571 });
5572 let load = func.expressions.append(Expression::LocalVariable(local));
5573 let lit = func
5574 .expressions
5575 .append(Expression::Literal(Literal::U32(5)));
5576 let cmp = func.expressions.append(Expression::Binary {
5577 op: BinaryOp::GreaterEqual,
5578 left: load,
5579 right: lit,
5580 });
5581 let body = vec![Statement::Loop {
5582 body: vec![Statement::Break],
5583 continuing: vec![],
5584 break_if: Some(cmp),
5585 }];
5586 let bounds = extract_loop_bound_literals(&body, &func.expressions);
5587 assert_eq!(bounds, vec![5]);
5588 }
5589
5590 #[test]
5591 fn extract_loop_bounds_nested() {
5592 let mut func = Function::new("test");
5593 let local_kh = func
5595 .expressions
5596 .append(Expression::Literal(Literal::U32(0)));
5597 let lit3 = func
5598 .expressions
5599 .append(Expression::Literal(Literal::U32(3)));
5600 let cmp_inner = func.expressions.append(Expression::Binary {
5601 op: BinaryOp::GreaterEqual,
5602 left: local_kh,
5603 right: lit3,
5604 });
5605 let local_kw = func
5607 .expressions
5608 .append(Expression::Literal(Literal::U32(0)));
5609 let lit5 = func
5610 .expressions
5611 .append(Expression::Literal(Literal::U32(5)));
5612 let cmp_outer = func.expressions.append(Expression::Binary {
5613 op: BinaryOp::GreaterEqual,
5614 left: local_kw,
5615 right: lit5,
5616 });
5617 let body = vec![Statement::Loop {
5618 body: vec![Statement::Loop {
5619 body: vec![Statement::Break],
5620 continuing: vec![],
5621 break_if: Some(cmp_inner),
5622 }],
5623 continuing: vec![],
5624 break_if: Some(cmp_outer),
5625 }];
5626 let bounds = extract_loop_bound_literals(&body, &func.expressions);
5627 assert_eq!(bounds, vec![5, 3]);
5628 }
5629
5630 #[test]
5631 fn extract_loop_bounds_no_literal() {
5632 let func = Function::new("test");
5633 let body = vec![Statement::Loop {
5634 body: vec![Statement::Break],
5635 continuing: vec![],
5636 break_if: None,
5637 }];
5638 let bounds = extract_loop_bound_literals(&body, &func.expressions);
5639 assert_eq!(bounds.len(), 0);
5640 }
5641
5642 #[test]
5645 fn extract_loop_bounds_if_else_break_pattern() {
5646 let mut func = Function::new("test");
5647 let local = func.local_variables.append(LocalVariable {
5648 name: Some("kh".into()),
5649 ty: {
5650 let mut types = UniqueArena::new();
5651 types.insert(Type {
5652 name: None,
5653 inner: TypeInner::Scalar(Scalar::U32),
5654 })
5655 },
5656 init: None,
5657 });
5658 let load = func.expressions.append(Expression::LocalVariable(local));
5659 let lit5 = func
5660 .expressions
5661 .append(Expression::Literal(Literal::U32(5)));
5662 let cmp = func.expressions.append(Expression::Binary {
5663 op: BinaryOp::Less,
5664 left: load,
5665 right: lit5,
5666 });
5667 let body = vec![Statement::Loop {
5669 body: vec![Statement::If {
5670 condition: cmp,
5671 accept: vec![],
5672 reject: vec![Statement::Break],
5673 }],
5674 continuing: vec![],
5675 break_if: None,
5676 }];
5677 let bounds = extract_loop_bound_literals(&body, &func.expressions);
5678 assert_eq!(bounds, vec![5]);
5679 }
5680
5681 fn invocation_id_component(func: &mut Function, index: u32) -> Handle<Expression> {
5687 let gid = func.expressions.append(Expression::FunctionArgument(0));
5688 func.expressions
5689 .append(Expression::AccessIndex { base: gid, index })
5690 }
5691
5692 #[test]
5693 fn extract_multiply_literals_stride() {
5694 let mut func = Function::new("test");
5695 let oh = invocation_id_component(&mut func, 1);
5696 let two = func
5697 .expressions
5698 .append(Expression::Literal(Literal::U32(2)));
5699 func.expressions.append(Expression::Binary {
5700 op: BinaryOp::Multiply,
5701 left: oh,
5702 right: two,
5703 });
5704 let strides = extract_multiply_literals(&func.expressions);
5705 assert_eq!(strides, vec![2]);
5706 }
5707
5708 #[test]
5709 fn a_flattening_factor_is_not_a_stride() {
5710 let mut func = Function::new("test");
5714 let slot = func
5717 .expressions
5718 .append(Expression::Literal(Literal::U32(0)));
5719 let kh = func.expressions.append(Expression::Load { pointer: slot });
5720 let three = func
5721 .expressions
5722 .append(Expression::Literal(Literal::U32(3)));
5723 func.expressions.append(Expression::Binary {
5724 op: BinaryOp::Multiply,
5725 left: kh,
5726 right: three,
5727 });
5728 assert_eq!(
5729 extract_multiply_literals(&func.expressions),
5730 Vec::<u32>::new()
5731 );
5732 }
5733
5734 #[test]
5735 fn extract_multiply_literals_ignores_one() {
5736 let mut func = Function::new("test");
5737 let oh = func
5738 .expressions
5739 .append(Expression::Literal(Literal::U32(0)));
5740 let one = func
5741 .expressions
5742 .append(Expression::Literal(Literal::U32(1)));
5743 func.expressions.append(Expression::Binary {
5744 op: BinaryOp::Multiply,
5745 left: oh,
5746 right: one,
5747 });
5748 let strides = extract_multiply_literals(&func.expressions);
5749 assert_eq!(strides.len(), 0);
5750 }
5751
5752 #[test]
5753 fn extract_subtract_literals_padding() {
5754 let mut func = Function::new("test");
5755 let idx = func
5756 .expressions
5757 .append(Expression::Literal(Literal::U32(10)));
5758 let pad = func
5759 .expressions
5760 .append(Expression::Literal(Literal::U32(1)));
5761 func.expressions.append(Expression::Binary {
5762 op: BinaryOp::Subtract,
5763 left: idx,
5764 right: pad,
5765 });
5766 let pads = extract_subtract_literals(&func.expressions);
5767 assert_eq!(pads, vec![1]);
5768 }
5769
5770 #[test]
5771 fn conv2d_shape_no_body_defaults_to_unknown() {
5772 let names: Vec<String> = ["N", "IC", "IH", "IW", "OC", "KH", "KW"]
5773 .iter()
5774 .map(|s| s.to_string())
5775 .collect();
5776 let shape = extract_conv2d_shape(&names, None, None);
5777 assert_eq!(shape.batch, "N");
5778 assert_eq!(shape.kernel_h, "KH");
5779 assert_eq!(shape.kernel_w, "KW");
5780 assert_eq!(shape.kernel_h_val, 0);
5781 assert_eq!(shape.kernel_w_val, 0);
5782 assert_eq!(shape.stride_h, 1);
5783 assert_eq!(shape.stride_w, 1);
5784 assert_eq!(shape.pad_h, 0);
5785 assert_eq!(shape.pad_w, 0);
5786 }
5787
5788 #[test]
5789 fn conv2d_shape_with_literal_kernel() {
5790 let names: Vec<String> = ["N", "IC", "IH", "IW", "OC", "KH", "KW"]
5791 .iter()
5792 .map(|s| s.to_string())
5793 .collect();
5794 let mut func = Function::new("test");
5796 let kh_var = func
5797 .expressions
5798 .append(Expression::Literal(Literal::U32(0)));
5799 let lit5a = func
5800 .expressions
5801 .append(Expression::Literal(Literal::U32(5)));
5802 let cmp_h = func.expressions.append(Expression::Binary {
5803 op: BinaryOp::GreaterEqual,
5804 left: kh_var,
5805 right: lit5a,
5806 });
5807 let kw_var = func
5808 .expressions
5809 .append(Expression::Literal(Literal::U32(0)));
5810 let lit5b = func
5811 .expressions
5812 .append(Expression::Literal(Literal::U32(5)));
5813 let cmp_w = func.expressions.append(Expression::Binary {
5814 op: BinaryOp::GreaterEqual,
5815 left: kw_var,
5816 right: lit5b,
5817 });
5818 let oh = invocation_id_component(&mut func, 1);
5822 let two = func
5823 .expressions
5824 .append(Expression::Literal(Literal::U32(2)));
5825 func.expressions.append(Expression::Binary {
5826 op: BinaryOp::Multiply,
5827 left: oh,
5828 right: two,
5829 });
5830 let ih = func
5832 .expressions
5833 .append(Expression::Literal(Literal::U32(10)));
5834 let one = func
5835 .expressions
5836 .append(Expression::Literal(Literal::U32(1)));
5837 func.expressions.append(Expression::Binary {
5838 op: BinaryOp::Subtract,
5839 left: ih,
5840 right: one,
5841 });
5842
5843 let body = vec![Statement::Loop {
5844 body: vec![Statement::Loop {
5845 body: vec![Statement::Break],
5846 continuing: vec![],
5847 break_if: Some(cmp_w),
5848 }],
5849 continuing: vec![],
5850 break_if: Some(cmp_h),
5851 }];
5852
5853 let shape = extract_conv2d_shape(&names, Some(&body), Some(&func.expressions));
5854 assert_eq!(shape.kernel_h_val, 5);
5855 assert_eq!(shape.kernel_w_val, 5);
5856 assert_eq!(shape.stride_h, 2);
5857 assert_eq!(shape.stride_w, 2);
5858 assert_eq!(shape.pad_h, 1);
5859 assert_eq!(shape.pad_w, 1);
5860 }
5861
5862 fn make_pool_module(kernel: u32, stride: u32) -> Module {
5864 let mut module = Module::default();
5865
5866 let f32_ty = module.types.insert(Type {
5867 name: None,
5868 inner: TypeInner::Scalar(Scalar::F32),
5869 });
5870 let u32_ty = module.types.insert(Type {
5871 name: None,
5872 inner: TypeInner::Scalar(Scalar::U32),
5873 });
5874 let array_f32 = module.types.insert(Type {
5875 name: None,
5876 inner: TypeInner::Array {
5877 base: f32_ty,
5878 size: ArraySize::Dynamic,
5879 stride: 4,
5880 },
5881 });
5882 let params_ty = module.types.insert(Type {
5883 name: Some("Params".into()),
5884 inner: TypeInner::Struct {
5885 members: vec![
5886 StructMember {
5887 name: Some("C".into()),
5888 ty: u32_ty,
5889 offset: 0,
5890 },
5891 StructMember {
5892 name: Some("IH".into()),
5893 ty: u32_ty,
5894 offset: 4,
5895 },
5896 StructMember {
5897 name: Some("IW".into()),
5898 ty: u32_ty,
5899 offset: 8,
5900 },
5901 StructMember {
5902 name: Some("OH".into()),
5903 ty: u32_ty,
5904 offset: 12,
5905 },
5906 ],
5907 span: 16,
5908 },
5909 });
5910
5911 module.global_variables.append(GlobalVariable {
5912 name: Some("input".into()),
5913 space: AddressSpace::Storage {
5914 access: StorageAccess::LOAD,
5915 },
5916 binding: Some(ResourceBinding {
5917 group: 0,
5918 binding: 0,
5919 }),
5920 ty: array_f32,
5921 init: None,
5922 layout: None,
5923 });
5924 module.global_variables.append(GlobalVariable {
5925 name: Some("output".into()),
5926 space: AddressSpace::Storage {
5927 access: StorageAccess::LOAD | StorageAccess::STORE,
5928 },
5929 binding: Some(ResourceBinding {
5930 group: 0,
5931 binding: 1,
5932 }),
5933 ty: array_f32,
5934 init: None,
5935 layout: None,
5936 });
5937 module.global_variables.append(GlobalVariable {
5938 name: Some("params".into()),
5939 space: AddressSpace::Uniform,
5940 binding: Some(ResourceBinding {
5941 group: 0,
5942 binding: 2,
5943 }),
5944 ty: params_ty,
5945 init: None,
5946 layout: None,
5947 });
5948
5949 let mut func = Function::new("main");
5950
5951 let kh_var = func
5953 .expressions
5954 .append(Expression::Literal(Literal::U32(0)));
5955 let lit_k1 = func
5956 .expressions
5957 .append(Expression::Literal(Literal::U32(kernel)));
5958 let cmp_h = func.expressions.append(Expression::Binary {
5959 op: BinaryOp::GreaterEqual,
5960 left: kh_var,
5961 right: lit_k1,
5962 });
5963 let kw_var = func
5964 .expressions
5965 .append(Expression::Literal(Literal::U32(0)));
5966 let lit_k2 = func
5967 .expressions
5968 .append(Expression::Literal(Literal::U32(kernel)));
5969 let cmp_w = func.expressions.append(Expression::Binary {
5970 op: BinaryOp::GreaterEqual,
5971 left: kw_var,
5972 right: lit_k2,
5973 });
5974
5975 if stride > 1 {
5977 let oh = func
5978 .expressions
5979 .append(Expression::Literal(Literal::U32(0)));
5980 let lit_s = func
5981 .expressions
5982 .append(Expression::Literal(Literal::U32(stride)));
5983 func.expressions.append(Expression::Binary {
5984 op: BinaryOp::Multiply,
5985 left: oh,
5986 right: lit_s,
5987 });
5988 }
5989
5990 let arg = func
5992 .expressions
5993 .append(Expression::Literal(Literal::F32(0.0)));
5994 let arg1 = func
5995 .expressions
5996 .append(Expression::Literal(Literal::F32(0.0)));
5997 let max_expr = func.expressions.append(Expression::Math {
5998 fun: MathFunction::Max,
5999 arg,
6000 arg1: Some(arg1),
6001 arg2: None,
6002 arg3: None,
6003 });
6004 let ptr = func
6005 .expressions
6006 .append(Expression::Literal(Literal::F32(0.0)));
6007 func.body.push(Statement::Loop {
6008 body: vec![
6009 Statement::Loop {
6010 body: vec![
6011 Statement::Store {
6012 pointer: ptr,
6013 value: max_expr,
6014 },
6015 Statement::Break,
6016 ],
6017 continuing: vec![],
6018 break_if: Some(cmp_w),
6019 },
6020 Statement::Break,
6021 ],
6022 continuing: vec![],
6023 break_if: Some(cmp_h),
6024 });
6025
6026 module.entry_points.push(EntryPoint {
6027 name: "main".into(),
6028 workgroup_size: [16, 16, 1],
6029 function: func,
6030 });
6031
6032 module
6033 }
6034
6035 #[test]
6036 fn classify_pool_extracts_2x2_stride2() {
6037 let module = make_pool_module(2, 2);
6038 let pattern = classify_entry_point(&module, 0).unwrap();
6039 match &pattern {
6040 KernelPattern::Pool { kind, shape, .. } => {
6041 assert_eq!(*kind, PoolKind::Max);
6042 assert_eq!(shape.kernel_h, 2);
6043 assert_eq!(shape.kernel_w, 2);
6044 assert_eq!(shape.stride_h, 2);
6045 assert_eq!(shape.stride_w, 2);
6046 }
6047 _ => panic!("expected Pool pattern, got {pattern:?}"),
6048 }
6049 }
6050
6051 #[test]
6052 fn classify_pool_extracts_3x3_stride1() {
6053 let module = make_pool_module(3, 1);
6054 let pattern = classify_entry_point(&module, 0).unwrap();
6055 match &pattern {
6056 KernelPattern::Pool { kind, shape, .. } => {
6057 assert_eq!(*kind, PoolKind::Max);
6058 assert_eq!(shape.kernel_h, 3);
6059 assert_eq!(shape.kernel_w, 3);
6060 assert_eq!(shape.stride_h, 3);
6062 assert_eq!(shape.stride_w, 3);
6063 }
6064 _ => panic!("expected Pool pattern, got {pattern:?}"),
6065 }
6066 }
6067
6068 #[test]
6071 fn eval_const_expr_f32_literal() {
6072 let mut exprs = Arena::new();
6073 let types = UniqueArena::new();
6074 let h = exprs.append(Expression::Literal(Literal::F32(2.78)));
6075 assert_eq!(eval_const_expr_f32(h, &exprs, &types), Some(vec![2.78]));
6076 }
6077
6078 #[test]
6079 fn eval_const_expr_f32_compose() {
6080 let mut exprs = Arena::new();
6081 let types = UniqueArena::new();
6082 let a = exprs.append(Expression::Literal(Literal::F32(0.1)));
6083 let b = exprs.append(Expression::Literal(Literal::F32(0.2)));
6084 let c = exprs.append(Expression::Literal(Literal::F32(0.3)));
6085 let f32_ty = {
6086 let mut t = UniqueArena::new();
6087 t.insert(Type {
6088 name: None,
6089 inner: TypeInner::Scalar(Scalar::F32),
6090 })
6091 };
6092 let compose = exprs.append(Expression::Compose {
6093 ty: f32_ty,
6094 components: vec![a, b, c],
6095 });
6096 let result = eval_const_expr_f32(compose, &exprs, &types).unwrap();
6097 assert_eq!(result.len(), 3);
6098 assert!((result[0] - 0.1).abs() < 1e-6);
6099 assert!((result[1] - 0.2).abs() < 1e-6);
6100 assert!((result[2] - 0.3).abs() < 1e-6);
6101 }
6102
6103 #[test]
6104 fn eval_const_expr_f32_zero_value() {
6105 let mut exprs = Arena::new();
6106 let mut types = UniqueArena::new();
6107 let f32_ty = types.insert(Type {
6108 name: None,
6109 inner: TypeInner::Scalar(Scalar::F32),
6110 });
6111 let arr_ty = types.insert(Type {
6112 name: None,
6113 inner: TypeInner::Array {
6114 base: f32_ty,
6115 size: ArraySize::Constant(3),
6116 stride: 4,
6117 },
6118 });
6119 let h = exprs.append(Expression::ZeroValue(arr_ty));
6120 let result = eval_const_expr_f32(h, &exprs, &types).unwrap();
6121 assert_eq!(result, vec![0.0, 0.0, 0.0]);
6122 }
6123
6124 #[test]
6125 fn extract_embedded_weights_private_global() {
6126 let mut module = Module::default();
6127
6128 let f32_ty = module.types.insert(Type {
6129 name: None,
6130 inner: TypeInner::Scalar(Scalar::F32),
6131 });
6132 let array_f32_4 = module.types.insert(Type {
6133 name: None,
6134 inner: TypeInner::Array {
6135 base: f32_ty,
6136 size: ArraySize::Constant(4),
6137 stride: 4,
6138 },
6139 });
6140
6141 let lit0 = module
6143 .global_expressions
6144 .append(Expression::Literal(Literal::F32(0.1)));
6145 let lit1 = module
6146 .global_expressions
6147 .append(Expression::Literal(Literal::F32(0.2)));
6148 let lit2 = module
6149 .global_expressions
6150 .append(Expression::Literal(Literal::F32(0.3)));
6151 let lit3 = module
6152 .global_expressions
6153 .append(Expression::Literal(Literal::F32(0.4)));
6154 let compose = module.global_expressions.append(Expression::Compose {
6155 ty: array_f32_4,
6156 components: vec![lit0, lit1, lit2, lit3],
6157 });
6158
6159 module.global_variables.append(GlobalVariable {
6160 name: Some("bias".into()),
6161 space: AddressSpace::Private,
6162 binding: None,
6163 ty: array_f32_4,
6164 init: Some(compose),
6165 layout: None,
6166 });
6167
6168 let weights = extract_embedded_weights(&module);
6169 assert_eq!(weights.len(), 1);
6170 assert_eq!(weights[0].name, "bias");
6171 assert_eq!(weights[0].dims, vec![4]);
6172 assert_eq!(weights[0].data.len(), 4);
6173 assert!((weights[0].data[0] - 0.1).abs() < 1e-6);
6174 assert!((weights[0].data[3] - 0.4).abs() < 1e-6);
6175 }
6176
6177 #[test]
6178 fn extract_embedded_weights_no_init() {
6179 let mut module = Module::default();
6180
6181 let f32_ty = module.types.insert(Type {
6182 name: None,
6183 inner: TypeInner::Scalar(Scalar::F32),
6184 });
6185 let array_f32 = module.types.insert(Type {
6186 name: None,
6187 inner: TypeInner::Array {
6188 base: f32_ty,
6189 size: ArraySize::Dynamic,
6190 stride: 4,
6191 },
6192 });
6193
6194 module.global_variables.append(GlobalVariable {
6195 name: Some("a".into()),
6196 space: AddressSpace::Storage {
6197 access: StorageAccess::LOAD,
6198 },
6199 binding: Some(ResourceBinding {
6200 group: 0,
6201 binding: 0,
6202 }),
6203 ty: array_f32,
6204 init: None,
6205 layout: None,
6206 });
6207
6208 let weights = extract_embedded_weights(&module);
6209 assert_eq!(weights.len(), 0);
6210 }
6211
6212 fn make_concat_module(boundary_index: u32, shape_names: &[&str]) -> Module {
6218 let mut module = Module::default();
6219
6220 let f32_ty = module.types.insert(Type {
6221 name: None,
6222 inner: TypeInner::Scalar(Scalar::F32),
6223 });
6224 let u32_ty = module.types.insert(Type {
6225 name: None,
6226 inner: TypeInner::Scalar(Scalar::U32),
6227 });
6228 let array_f32 = module.types.insert(Type {
6229 name: None,
6230 inner: TypeInner::Array {
6231 base: f32_ty,
6232 size: ArraySize::Dynamic,
6233 stride: 4,
6234 },
6235 });
6236 let params_ty = module.types.insert(Type {
6237 name: Some("Params".into()),
6238 inner: TypeInner::Struct {
6239 members: shape_names
6240 .iter()
6241 .enumerate()
6242 .map(|(i, name)| StructMember {
6243 name: Some((*name).into()),
6244 ty: u32_ty,
6245 offset: (i * 4) as u32,
6246 })
6247 .collect(),
6248 span: (shape_names.len() * 4) as u32,
6249 },
6250 });
6251
6252 module.global_variables.append(GlobalVariable {
6254 name: Some("a".into()),
6255 space: AddressSpace::Storage {
6256 access: StorageAccess::LOAD,
6257 },
6258 binding: Some(ResourceBinding {
6259 group: 0,
6260 binding: 0,
6261 }),
6262 ty: array_f32,
6263 init: None,
6264 layout: None,
6265 });
6266 module.global_variables.append(GlobalVariable {
6268 name: Some("b".into()),
6269 space: AddressSpace::Storage {
6270 access: StorageAccess::LOAD,
6271 },
6272 binding: Some(ResourceBinding {
6273 group: 0,
6274 binding: 1,
6275 }),
6276 ty: array_f32,
6277 init: None,
6278 layout: None,
6279 });
6280 module.global_variables.append(GlobalVariable {
6282 name: Some("result".into()),
6283 space: AddressSpace::Storage {
6284 access: StorageAccess::LOAD | StorageAccess::STORE,
6285 },
6286 binding: Some(ResourceBinding {
6287 group: 0,
6288 binding: 2,
6289 }),
6290 ty: array_f32,
6291 init: None,
6292 layout: None,
6293 });
6294 module.global_variables.append(GlobalVariable {
6296 name: Some("params".into()),
6297 space: AddressSpace::Uniform,
6298 binding: Some(ResourceBinding {
6299 group: 0,
6300 binding: 3,
6301 }),
6302 ty: params_ty,
6303 init: None,
6304 layout: None,
6305 });
6306
6307 let mut func = Function::new("main");
6309 let idx = func
6310 .expressions
6311 .append(Expression::Literal(Literal::U32(0)));
6312 let params_gv = {
6314 let mut iter = module.global_variables.iter();
6315 iter.nth(3).unwrap().0
6316 };
6317 let params_expr = func
6318 .expressions
6319 .append(Expression::GlobalVariable(params_gv));
6320 let access = func.expressions.append(Expression::AccessIndex {
6321 base: params_expr,
6322 index: boundary_index,
6323 });
6324 let load = func
6325 .expressions
6326 .append(Expression::Load { pointer: access });
6327 let cmp = func.expressions.append(Expression::Binary {
6328 op: BinaryOp::Less,
6329 left: idx,
6330 right: load,
6331 });
6332
6333 let val = func
6335 .expressions
6336 .append(Expression::Literal(Literal::F32(0.0)));
6337 let ptr = func
6338 .expressions
6339 .append(Expression::Literal(Literal::F32(0.0)));
6340 func.body.push(Statement::If {
6341 condition: cmp,
6342 accept: vec![Statement::Store {
6343 pointer: ptr,
6344 value: val,
6345 }],
6346 reject: vec![Statement::Store {
6347 pointer: ptr,
6348 value: val,
6349 }],
6350 });
6351
6352 module.entry_points.push(EntryPoint {
6353 name: "main".into(),
6354 workgroup_size: [256, 1, 1],
6355 function: func,
6356 });
6357
6358 module
6359 }
6360
6361 fn make_split_module(boundary_index: u32, shape_names: &[&str]) -> Module {
6364 let mut module = Module::default();
6365
6366 let f32_ty = module.types.insert(Type {
6367 name: None,
6368 inner: TypeInner::Scalar(Scalar::F32),
6369 });
6370 let u32_ty = module.types.insert(Type {
6371 name: None,
6372 inner: TypeInner::Scalar(Scalar::U32),
6373 });
6374 let array_f32 = module.types.insert(Type {
6375 name: None,
6376 inner: TypeInner::Array {
6377 base: f32_ty,
6378 size: ArraySize::Dynamic,
6379 stride: 4,
6380 },
6381 });
6382 let params_ty = module.types.insert(Type {
6383 name: Some("Params".into()),
6384 inner: TypeInner::Struct {
6385 members: shape_names
6386 .iter()
6387 .enumerate()
6388 .map(|(i, name)| StructMember {
6389 name: Some((*name).into()),
6390 ty: u32_ty,
6391 offset: (i * 4) as u32,
6392 })
6393 .collect(),
6394 span: (shape_names.len() * 4) as u32,
6395 },
6396 });
6397
6398 module.global_variables.append(GlobalVariable {
6400 name: Some("input".into()),
6401 space: AddressSpace::Storage {
6402 access: StorageAccess::LOAD,
6403 },
6404 binding: Some(ResourceBinding {
6405 group: 0,
6406 binding: 0,
6407 }),
6408 ty: array_f32,
6409 init: None,
6410 layout: None,
6411 });
6412 module.global_variables.append(GlobalVariable {
6414 name: Some("out_a".into()),
6415 space: AddressSpace::Storage {
6416 access: StorageAccess::LOAD | StorageAccess::STORE,
6417 },
6418 binding: Some(ResourceBinding {
6419 group: 0,
6420 binding: 1,
6421 }),
6422 ty: array_f32,
6423 init: None,
6424 layout: None,
6425 });
6426 module.global_variables.append(GlobalVariable {
6428 name: Some("out_b".into()),
6429 space: AddressSpace::Storage {
6430 access: StorageAccess::LOAD | StorageAccess::STORE,
6431 },
6432 binding: Some(ResourceBinding {
6433 group: 0,
6434 binding: 2,
6435 }),
6436 ty: array_f32,
6437 init: None,
6438 layout: None,
6439 });
6440 module.global_variables.append(GlobalVariable {
6442 name: Some("params".into()),
6443 space: AddressSpace::Uniform,
6444 binding: Some(ResourceBinding {
6445 group: 0,
6446 binding: 3,
6447 }),
6448 ty: params_ty,
6449 init: None,
6450 layout: None,
6451 });
6452
6453 let mut func = Function::new("main");
6455 let idx = func
6456 .expressions
6457 .append(Expression::Literal(Literal::U32(0)));
6458 let params_gv = {
6459 let mut iter = module.global_variables.iter();
6460 iter.nth(3).unwrap().0
6461 };
6462 let params_expr = func
6463 .expressions
6464 .append(Expression::GlobalVariable(params_gv));
6465 let access = func.expressions.append(Expression::AccessIndex {
6466 base: params_expr,
6467 index: boundary_index,
6468 });
6469 let load = func
6470 .expressions
6471 .append(Expression::Load { pointer: access });
6472 let cmp = func.expressions.append(Expression::Binary {
6473 op: BinaryOp::Less,
6474 left: idx,
6475 right: load,
6476 });
6477
6478 let (input_gv, out_a_gv, out_b_gv) = {
6484 let mut iter = module.global_variables.iter();
6485 let input = iter.next().unwrap().0;
6486 let out_a = iter.next().unwrap().0;
6487 let out_b = iter.next().unwrap().0;
6488 (input, out_a, out_b)
6489 };
6490 let input_expr = func
6491 .expressions
6492 .append(Expression::GlobalVariable(input_gv));
6493 let read = func.expressions.append(Expression::Access {
6494 base: input_expr,
6495 index: idx,
6496 });
6497 let val = func.expressions.append(Expression::Load { pointer: read });
6498
6499 let mut copy_into = |out: Handle<GlobalVariable>| {
6500 let out_expr = func.expressions.append(Expression::GlobalVariable(out));
6501 let ptr = func.expressions.append(Expression::Access {
6502 base: out_expr,
6503 index: idx,
6504 });
6505 Statement::Store {
6506 pointer: ptr,
6507 value: val,
6508 }
6509 };
6510 let store_a = copy_into(out_a_gv);
6511 let store_b = copy_into(out_b_gv);
6512
6513 func.body.push(Statement::If {
6514 condition: cmp,
6515 accept: vec![store_a],
6516 reject: vec![store_b],
6517 });
6518
6519 module.entry_points.push(EntryPoint {
6520 name: "main".into(),
6521 workgroup_size: [256, 1, 1],
6522 function: func,
6523 });
6524
6525 module
6526 }
6527
6528 #[test]
6529 fn classify_concat_axis_0_default() {
6530 let module = make_concat_module(0, &["N_a", "N_b"]);
6532 let pattern = classify_entry_point(&module, 0).unwrap();
6533 match &pattern {
6534 KernelPattern::Concat { axis, .. } => {
6535 assert_eq!(*axis, 0, "expected axis 0 for boundary at index 0");
6536 }
6537 _ => panic!("expected Concat pattern, got {pattern:?}"),
6538 }
6539 }
6540
6541 #[test]
6542 fn classify_concat_axis_1_inferred() {
6543 let module = make_concat_module(1, &["N", "C1", "C2"]);
6545 let pattern = classify_entry_point(&module, 0).unwrap();
6546 match &pattern {
6547 KernelPattern::Concat {
6548 axis,
6549 inputs,
6550 output,
6551 ..
6552 } => {
6553 assert_eq!(*axis, 1, "expected axis 1 for boundary at index 1");
6554 assert_eq!(inputs[0].name, "a");
6555 assert_eq!(inputs[1].name, "b");
6556 assert_eq!(output.name, "result");
6557 }
6558 _ => panic!("expected Concat pattern, got {pattern:?}"),
6559 }
6560 }
6561
6562 #[test]
6563 fn classify_concat_axis_2_inferred() {
6564 let module = make_concat_module(2, &["N", "C", "W1", "W2"]);
6566 let pattern = classify_entry_point(&module, 0).unwrap();
6567 match &pattern {
6568 KernelPattern::Concat { axis, .. } => {
6569 assert_eq!(*axis, 2, "expected axis 2 for boundary at index 2");
6570 }
6571 _ => panic!("expected Concat pattern, got {pattern:?}"),
6572 }
6573 }
6574
6575 #[test]
6576 fn classify_concat_axis_3_inferred() {
6577 let module = make_concat_module(3, &["N", "C", "H", "W1", "W2"]);
6579 let pattern = classify_entry_point(&module, 0).unwrap();
6580 match &pattern {
6581 KernelPattern::Concat { axis, .. } => {
6582 assert_eq!(*axis, 3, "expected axis 3 for boundary at index 3");
6583 }
6584 _ => panic!("expected Concat pattern, got {pattern:?}"),
6585 }
6586 }
6587
6588 #[test]
6589 fn classify_split_axis_0_default() {
6590 let module = make_split_module(0, &["N_a", "N_b"]);
6592 let pattern = classify_entry_point(&module, 0).unwrap();
6593 match &pattern {
6594 KernelPattern::Split { axis, .. } => {
6595 assert_eq!(*axis, 0, "expected axis 0 for boundary at index 0");
6596 }
6597 _ => panic!("expected Split pattern, got {pattern:?}"),
6598 }
6599 }
6600
6601 #[test]
6602 fn classify_split_axis_1_inferred() {
6603 let module = make_split_module(1, &["N", "C1", "C2"]);
6605 let pattern = classify_entry_point(&module, 0).unwrap();
6606 match &pattern {
6607 KernelPattern::Split {
6608 axis,
6609 input,
6610 outputs,
6611 ..
6612 } => {
6613 assert_eq!(*axis, 1, "expected axis 1 for boundary at index 1");
6614 assert_eq!(input.name, "input");
6615 assert_eq!(outputs.len(), 2);
6616 assert_eq!(outputs[0].name, "out_a");
6617 assert_eq!(outputs[1].name, "out_b");
6618 }
6619 _ => panic!("expected Split pattern, got {pattern:?}"),
6620 }
6621 }
6622
6623 #[test]
6624 fn classify_split_axis_2_inferred() {
6625 let module = make_split_module(2, &["N", "C", "W1", "W2"]);
6627 let pattern = classify_entry_point(&module, 0).unwrap();
6628 match &pattern {
6629 KernelPattern::Split { axis, .. } => {
6630 assert_eq!(*axis, 2, "expected axis 2 for boundary at index 2");
6631 }
6632 _ => panic!("expected Split pattern, got {pattern:?}"),
6633 }
6634 }
6635
6636 #[test]
6637 fn classify_split_axis_3_inferred() {
6638 let module = make_split_module(3, &["N", "C", "H", "W1", "W2"]);
6640 let pattern = classify_entry_point(&module, 0).unwrap();
6641 match &pattern {
6642 KernelPattern::Split { axis, .. } => {
6643 assert_eq!(*axis, 3, "expected axis 3 for boundary at index 3");
6644 }
6645 _ => panic!("expected Split pattern, got {pattern:?}"),
6646 }
6647 }
6648
6649 #[test]
6650 fn normalize_axis_positive() {
6651 assert_eq!(normalize_axis(0, 3), 0);
6652 assert_eq!(normalize_axis(1, 4), 1);
6653 assert_eq!(normalize_axis(2, 4), 2);
6654 }
6655
6656 #[test]
6657 fn normalize_axis_negative() {
6658 assert_eq!(normalize_axis(-1, 4), 3);
6659 assert_eq!(normalize_axis(-2, 4), 2);
6660 assert_eq!(normalize_axis(-3, 4), 1);
6661 assert_eq!(normalize_axis(-4, 4), 0);
6662 }
6663
6664 #[test]
6665 fn normalize_axis_negative_wraps() {
6666 assert_eq!(normalize_axis(-1, 3), 2);
6668 assert_eq!(normalize_axis(-2, 3), 1);
6670 }
6671
6672 #[test]
6673 fn find_if_comparison_axis_direct_access_index() {
6674 let mut func = Function::new("test");
6677 let idx = func
6678 .expressions
6679 .append(Expression::Literal(Literal::U32(0)));
6680 let base = func
6681 .expressions
6682 .append(Expression::Literal(Literal::U32(0)));
6683 let access = func
6684 .expressions
6685 .append(Expression::AccessIndex { base, index: 2 });
6686 let cmp = func.expressions.append(Expression::Binary {
6687 op: BinaryOp::Less,
6688 left: idx,
6689 right: access,
6690 });
6691
6692 let body = vec![Statement::If {
6693 condition: cmp,
6694 accept: vec![],
6695 reject: vec![],
6696 }];
6697 let names: Vec<String> = vec!["N".into(), "C".into(), "W".into()];
6698 let result = find_if_comparison_axis(&body, &func.expressions, &names);
6699 assert_eq!(result, Some(2));
6700 }
6701
6702 #[test]
6703 fn find_if_comparison_axis_in_loop() {
6704 let mut func = Function::new("test");
6706 let idx = func
6707 .expressions
6708 .append(Expression::Literal(Literal::U32(0)));
6709 let base = func
6710 .expressions
6711 .append(Expression::Literal(Literal::U32(0)));
6712 let access = func
6713 .expressions
6714 .append(Expression::AccessIndex { base, index: 1 });
6715 let load = func
6716 .expressions
6717 .append(Expression::Load { pointer: access });
6718 let cmp = func.expressions.append(Expression::Binary {
6719 op: BinaryOp::LessEqual,
6720 left: idx,
6721 right: load,
6722 });
6723
6724 let body = vec![Statement::Loop {
6725 body: vec![Statement::If {
6726 condition: cmp,
6727 accept: vec![],
6728 reject: vec![],
6729 }],
6730 continuing: vec![],
6731 break_if: None,
6732 }];
6733 let names: Vec<String> = vec!["N".into(), "C".into()];
6734 let result = find_if_comparison_axis(&body, &func.expressions, &names);
6735 assert_eq!(result, Some(1));
6736 }
6737
6738 #[test]
6739 fn find_if_comparison_axis_no_if_returns_none() {
6740 let func = Function::new("test");
6741 let body = vec![Statement::Break];
6742 let names: Vec<String> = vec!["N".into()];
6743 let result = find_if_comparison_axis(&body, &func.expressions, &names);
6744 assert_eq!(result, None);
6745 }
6746
6747 #[test]
6751 fn eps_is_not_a_dimension() {
6752 let mut module = Module::default();
6753 let u32_ty = module.types.insert(Type {
6754 name: None,
6755 inner: TypeInner::Scalar(Scalar::U32),
6756 });
6757 let f32_ty = module.types.insert(Type {
6758 name: None,
6759 inner: TypeInner::Scalar(Scalar::F32),
6760 });
6761 let members = [
6762 StructMember {
6763 name: Some("N".into()),
6764 ty: u32_ty,
6765 offset: 0,
6766 },
6767 StructMember {
6768 name: Some("D".into()),
6769 ty: u32_ty,
6770 offset: 4,
6771 },
6772 StructMember {
6773 name: Some("eps".into()),
6774 ty: f32_ty,
6775 offset: 8,
6776 },
6777 ];
6778 let dims: Vec<String> = members
6779 .iter()
6780 .filter(|m| is_integer_member(&module, m))
6781 .filter_map(|m| m.name.clone())
6782 .collect();
6783 assert_eq!(dims, vec!["N".to_string(), "D".to_string()]);
6784 }
6785
6786 fn make_attention_module(param_names: &[&str], divide_by: Option<u32>, causal: bool) -> Module {
6793 let mut module = Module::default();
6794
6795 let f32_ty = module.types.insert(Type {
6796 name: None,
6797 inner: TypeInner::Scalar(Scalar::F32),
6798 });
6799 let u32_ty = module.types.insert(Type {
6800 name: None,
6801 inner: TypeInner::Scalar(Scalar::U32),
6802 });
6803 let array_f32 = module.types.insert(Type {
6804 name: None,
6805 inner: TypeInner::Array {
6806 base: f32_ty,
6807 size: ArraySize::Dynamic,
6808 stride: 4,
6809 },
6810 });
6811
6812 let members: Vec<StructMember> = param_names
6814 .iter()
6815 .enumerate()
6816 .map(|(i, name)| StructMember {
6817 name: Some((*name).into()),
6818 ty: u32_ty,
6819 offset: (i * 4) as u32,
6820 })
6821 .collect();
6822 let params_ty = module.types.insert(Type {
6823 name: Some("Params".into()),
6824 inner: TypeInner::Struct {
6825 members,
6826 span: (param_names.len() * 4) as u32,
6827 },
6828 });
6829
6830 for (i, name) in ["query", "key", "value"].iter().enumerate() {
6832 module.global_variables.append(GlobalVariable {
6833 name: Some((*name).into()),
6834 space: AddressSpace::Storage {
6835 access: StorageAccess::LOAD,
6836 },
6837 binding: Some(ResourceBinding {
6838 group: 0,
6839 binding: i as u32,
6840 }),
6841 ty: array_f32,
6842 init: None,
6843 layout: None,
6844 });
6845 }
6846
6847 module.global_variables.append(GlobalVariable {
6849 name: Some("output".into()),
6850 space: AddressSpace::Storage {
6851 access: StorageAccess::LOAD | StorageAccess::STORE,
6852 },
6853 binding: Some(ResourceBinding {
6854 group: 0,
6855 binding: 3,
6856 }),
6857 ty: array_f32,
6858 init: None,
6859 layout: None,
6860 });
6861
6862 module.global_variables.append(GlobalVariable {
6864 name: Some("params".into()),
6865 space: AddressSpace::Uniform,
6866 binding: Some(ResourceBinding {
6867 group: 0,
6868 binding: 4,
6869 }),
6870 ty: params_ty,
6871 init: None,
6872 layout: None,
6873 });
6874
6875 let mut func = Function::new("main");
6877
6878 let arg_exp = func
6880 .expressions
6881 .append(Expression::Literal(Literal::F32(0.0)));
6882 func.expressions.append(Expression::Math {
6883 fun: MathFunction::Exp,
6884 arg: arg_exp,
6885 arg1: None,
6886 arg2: None,
6887 arg3: None,
6888 });
6889
6890 let arg_sqrt = func
6892 .expressions
6893 .append(Expression::Literal(Literal::F32(1.0)));
6894 func.expressions.append(Expression::Math {
6895 fun: MathFunction::Sqrt,
6896 arg: arg_sqrt,
6897 arg1: None,
6898 arg2: None,
6899 arg3: None,
6900 });
6901
6902 if let Some(n) = divide_by {
6904 let dividend = func
6905 .expressions
6906 .append(Expression::Literal(Literal::U32(64)));
6907 let divisor = func
6908 .expressions
6909 .append(Expression::Literal(Literal::U32(n)));
6910 func.expressions.append(Expression::Binary {
6911 op: BinaryOp::Divide,
6912 left: dividend,
6913 right: divisor,
6914 });
6915 }
6916
6917 let mut loop_inner: Vec<Statement> = Vec::new();
6919
6920 if causal {
6921 let j_expr = func
6924 .expressions
6925 .append(Expression::Literal(Literal::U32(0)));
6926 let i_expr = func
6927 .expressions
6928 .append(Expression::Literal(Literal::U32(0)));
6929 let cmp = func.expressions.append(Expression::Binary {
6930 op: BinaryOp::Greater,
6931 left: j_expr,
6932 right: i_expr,
6933 });
6934
6935 let neg_val = func
6937 .expressions
6938 .append(Expression::Literal(Literal::F32(-1e30)));
6939 let ptr = func
6940 .expressions
6941 .append(Expression::Literal(Literal::F32(0.0)));
6942
6943 let break_cond = func
6945 .expressions
6946 .append(Expression::Literal(Literal::Bool(true)));
6947 loop_inner.push(Statement::If {
6948 condition: break_cond,
6949 accept: vec![],
6950 reject: vec![Statement::Break],
6951 });
6952
6953 loop_inner.push(Statement::If {
6955 condition: cmp,
6956 accept: vec![Statement::Store {
6957 pointer: ptr,
6958 value: neg_val,
6959 }],
6960 reject: vec![],
6961 });
6962 }
6963
6964 loop_inner.push(Statement::Break);
6965
6966 func.body.push(Statement::Loop {
6967 body: loop_inner,
6968 continuing: vec![],
6969 break_if: None,
6970 });
6971
6972 module.entry_points.push(EntryPoint {
6973 name: "main".into(),
6974 workgroup_size: [16, 16, 1],
6975 function: func,
6976 });
6977
6978 module
6979 }
6980
6981 #[test]
6982 fn classify_multihead_4_heads() {
6983 let module = make_attention_module(&["seq_len", "d_model", "num_heads"], Some(4), false);
6984 let pattern = classify_entry_point(&module, 0).unwrap();
6985 match &pattern {
6986 KernelPattern::Attention {
6987 num_heads,
6988 num_kv_heads,
6989 causal,
6990 seq_len,
6991 ..
6992 } => {
6993 assert_eq!(*num_heads, 4, "expected 4 heads from division literal");
6994 assert_eq!(
6995 *num_kv_heads, 4,
6996 "num_kv_heads should equal num_heads for MHA"
6997 );
6998 assert!(!causal, "should not detect causal mask");
6999 assert_eq!(seq_len, "seq_len");
7000 }
7001 _ => panic!("expected Attention pattern, got {pattern:?}"),
7002 }
7003 let display = format!("{pattern}");
7005 assert!(
7006 display.contains("heads=4"),
7007 "display should show heads=4: {display}"
7008 );
7009 assert!(
7010 !display.contains("causal"),
7011 "display should not mention causal: {display}"
7012 );
7013 }
7014
7015 #[test]
7016 fn classify_causal_attention() {
7017 let module = make_attention_module(&["seq_len", "d_k"], None, true);
7018 let pattern = classify_entry_point(&module, 0).unwrap();
7019 match &pattern {
7020 KernelPattern::Attention {
7021 num_heads,
7022 causal,
7023 d_k,
7024 ..
7025 } => {
7026 assert_eq!(*num_heads, 1, "no num_heads param → default 1");
7027 assert!(*causal, "should detect causal mask from If + Store(-1e30)");
7028 assert_eq!(d_k, "d_k");
7029 }
7030 _ => panic!("expected Attention pattern, got {pattern:?}"),
7031 }
7032 let display = format!("{pattern}");
7034 assert!(
7035 display.contains("causal"),
7036 "display should mention causal: {display}"
7037 );
7038 assert!(
7039 display.contains("heads=1"),
7040 "display should show heads=1: {display}"
7041 );
7042 }
7043
7044 #[test]
7045 fn classify_gqa_defaults_to_mha() {
7046 let module = make_attention_module(&["seq_len", "d_model", "num_heads"], Some(4), false);
7048 let pattern = classify_entry_point(&module, 0).unwrap();
7049 match &pattern {
7050 KernelPattern::Attention {
7051 num_heads,
7052 num_kv_heads,
7053 ..
7054 } => {
7055 assert_eq!(*num_heads, 4);
7056 assert_eq!(
7057 *num_kv_heads, *num_heads,
7058 "GQA not implemented: kv_heads defaults to num_heads"
7059 );
7060 }
7061 _ => panic!("expected Attention pattern, got {pattern:?}"),
7062 }
7063 }
7064}