Skip to main content

nxpu_opt/
shape.rs

1//! Shape inference pass.
2//!
3//! Propagates tensor shapes through the IR module. For each global variable,
4//! infers the concrete or symbolic shape and stores it in a `ShapeMap`.
5
6use std::collections::HashMap;
7
8use nxpu_ir::{AddressSpace, Dimension, GlobalVariable, Handle, Module, StorageAccess, TypeInner};
9
10use crate::Pass;
11
12/// A dimension: either a concrete size, a symbolic name, or fully dynamic.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum Dim {
15    /// Known concrete size.
16    Known(u64),
17    /// Symbolic (named, constrained) dimension.
18    Symbolic(String),
19    /// Fully dynamic (unknown, unnamed) dimension.
20    Dynamic,
21}
22
23/// Inferred shape of a tensor.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct Shape {
26    pub dims: Vec<Dim>,
27}
28
29impl Shape {
30    /// Number of dimensions.
31    pub fn rank(&self) -> usize {
32        self.dims.len()
33    }
34}
35
36/// Unify two dimensions according to shape inference rules:
37/// - `Known(n) + Known(n)` -> `Known(n)` (match)
38/// - `Known(n) + Dynamic` -> `Known(n)` (concrete wins)
39/// - `Dynamic + Known(n)` -> `Known(n)` (concrete wins)
40/// - `Dynamic + Dynamic` -> `Dynamic`
41/// - `Symbolic(s) + Symbolic(s)` -> `Symbolic(s)` (same name matches)
42/// - `Symbolic(s) + Dynamic` -> `Symbolic(s)` (named wins)
43/// - `Known(1) + any` -> broadcast (returns the other)
44///
45/// Returns `None` if the dimensions are incompatible (e.g. `Known(3)` vs `Known(5)`).
46pub fn unify_dim(a: &Dim, b: &Dim) -> Option<Dim> {
47    match (a, b) {
48        // Identical known dimensions
49        (Dim::Known(x), Dim::Known(y)) if x == y => Some(Dim::Known(*x)),
50        // Broadcast: Known(1) with any -> the other
51        (Dim::Known(1), other) | (other, Dim::Known(1)) => Some(other.clone()),
52        // Mismatched known dimensions (non-broadcastable)
53        (Dim::Known(_), Dim::Known(_)) => None,
54        // Concrete wins over dynamic/symbolic
55        (Dim::Known(n), Dim::Dynamic) | (Dim::Dynamic, Dim::Known(n)) => Some(Dim::Known(*n)),
56        (Dim::Known(n), Dim::Symbolic(_)) | (Dim::Symbolic(_), Dim::Known(n)) => {
57            Some(Dim::Known(*n))
58        }
59        // Same symbolic name
60        (Dim::Symbolic(a_name), Dim::Symbolic(b_name)) if a_name == b_name => {
61            Some(Dim::Symbolic(a_name.clone()))
62        }
63        // Different symbolic names — cannot unify
64        (Dim::Symbolic(_), Dim::Symbolic(_)) => None,
65        // Symbolic wins over dynamic
66        (Dim::Symbolic(s), Dim::Dynamic) | (Dim::Dynamic, Dim::Symbolic(s)) => {
67            Some(Dim::Symbolic(s.clone()))
68        }
69        // Both dynamic
70        (Dim::Dynamic, Dim::Dynamic) => Some(Dim::Dynamic),
71    }
72}
73
74/// Unify two shapes element-wise. Returns `None` if ranks differ or
75/// any dimension pair is incompatible.
76pub fn unify_shapes(a: &Shape, b: &Shape) -> Option<Shape> {
77    if a.rank() != b.rank() {
78        return None;
79    }
80    let dims: Option<Vec<Dim>> = a
81        .dims
82        .iter()
83        .zip(b.dims.iter())
84        .map(|(da, db)| unify_dim(da, db))
85        .collect();
86    dims.map(|d| Shape { dims: d })
87}
88
89/// Convert an IR `Dimension` to a shape inference `Dim`.
90pub fn ir_dim_to_dim(d: &Dimension) -> Dim {
91    match d {
92        Dimension::Fixed(n) => Dim::Known(*n as u64),
93        Dimension::Symbolic(name) => Dim::Symbolic(name.clone()),
94        Dimension::Dynamic(Some(name)) => Dim::Symbolic(name.clone()),
95        Dimension::Dynamic(None) => Dim::Dynamic,
96    }
97}
98
99/// Map from global variable handles to their inferred shapes.
100pub type ShapeMap = HashMap<Handle<GlobalVariable>, Shape>;
101
102/// Shape inference pass.
103///
104/// Analyzes the module's global variables and uniform params to infer
105/// tensor shapes. Shapes are inferred from:
106/// - Array types (runtime-sized → dynamic dim)
107/// - Uniform struct member names (convention: N, M, K, C, H, W, etc.)
108/// - Workgroup size hints
109///
110/// This pass does not modify the module; it only produces a `ShapeMap`
111/// stored in module metadata (currently as a side-channel).
112#[derive(Debug)]
113pub struct ShapeInference;
114
115impl Pass for ShapeInference {
116    fn name(&self) -> &str {
117        "ShapeInference"
118    }
119
120    fn run(&self, module: &mut Module) -> bool {
121        let _shape_map = infer_shapes(module);
122        // Shape inference is a pure analysis pass — it does not modify the IR.
123        // Consumers should call `infer_shapes()` directly to obtain the map.
124        false
125    }
126}
127
128/// Infer shapes for all storage global variables in the module.
129pub fn infer_shapes(module: &Module) -> ShapeMap {
130    let mut map = ShapeMap::new();
131
132    // 1. Extract param names from Uniform structs.
133    let param_names = extract_param_names(module);
134
135    // 2. For each storage buffer, infer shape from type and params.
136    for (handle, gv) in module.global_variables.iter() {
137        if let AddressSpace::Storage { access } = &gv.space {
138            let shape = infer_global_shape(module, gv, &param_names, *access);
139            map.insert(handle, shape);
140        }
141    }
142
143    map
144}
145
146/// Extract parameter names from uniform struct members.
147#[allow(clippy::collapsible_if)] // nested if-let for MSRV 1.87 compat (no let chains)
148fn extract_param_names(module: &Module) -> Vec<String> {
149    for (_handle, gv) in module.global_variables.iter() {
150        if gv.space == AddressSpace::Uniform {
151            if let TypeInner::Struct { members, .. } = &module.types[gv.ty].inner {
152                return members.iter().filter_map(|m| m.name.clone()).collect();
153            }
154        }
155    }
156    vec![]
157}
158
159/// Infer shape for a single storage global variable.
160fn infer_global_shape(
161    module: &Module,
162    gv: &GlobalVariable,
163    param_names: &[String],
164    access: StorageAccess,
165) -> Shape {
166    match &module.types[gv.ty].inner {
167        TypeInner::Array { size, .. } => {
168            match size {
169                nxpu_ir::ArraySize::Constant(n) => {
170                    // Fixed-size array → single known dimension.
171                    Shape {
172                        dims: vec![Dim::Known(*n as u64)],
173                    }
174                }
175                nxpu_ir::ArraySize::Dynamic => {
176                    // Dynamic array → infer shape from params.
177                    infer_dynamic_shape(param_names, access)
178                }
179            }
180        }
181        TypeInner::Tensor { shape, .. } => {
182            // Tensor type already carries shape information;
183            // convert IR Dimensions to inference Dims.
184            Shape {
185                dims: shape.dims.iter().map(ir_dim_to_dim).collect(),
186            }
187        }
188        _ => {
189            // Non-array type → scalar, rank 0.
190            Shape { dims: vec![] }
191        }
192    }
193}
194
195/// Infer shape for a dynamically-sized storage buffer from param names.
196fn infer_dynamic_shape(param_names: &[String], access: StorageAccess) -> Shape {
197    let is_output = access.contains(StorageAccess::STORE);
198
199    match param_names.len() {
200        // MatMul convention: M, N, K
201        3 => {
202            if is_output {
203                // Output: [M, N]
204                Shape {
205                    dims: vec![
206                        Dim::Symbolic(param_names[0].clone()),
207                        Dim::Symbolic(param_names[1].clone()),
208                    ],
209                }
210            } else {
211                // Inputs are [M,K] or [K,N] — we'd need binding info
212                // to distinguish, so use symbolic dims.
213                Shape {
214                    dims: vec![Dim::Symbolic("?".into()), Dim::Symbolic("?".into())],
215                }
216            }
217        }
218        // ElementWise or Activation: N
219        1 => Shape {
220            dims: vec![Dim::Symbolic(param_names[0].clone())],
221        },
222        // Conv2D or complex: multi-dimensional
223        n if n > 3 => {
224            // Assume NCHW layout
225            let dims = param_names
226                .iter()
227                .take(4.min(n))
228                .map(|name| Dim::Symbolic(name.clone()))
229                .collect();
230            Shape { dims }
231        }
232        // 2 params (e.g., transpose: rows, cols)
233        2 => Shape {
234            dims: vec![
235                Dim::Symbolic(param_names[0].clone()),
236                Dim::Symbolic(param_names[1].clone()),
237            ],
238        },
239        // Unknown
240        _ => Shape {
241            dims: vec![Dim::Symbolic("?".into())],
242        },
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use nxpu_ir::*;
250
251    fn make_simple_module() -> Module {
252        let mut module = Module::default();
253
254        let f32_ty = module.types.insert(Type {
255            name: None,
256            inner: TypeInner::Scalar(Scalar::F32),
257        });
258        let u32_ty = module.types.insert(Type {
259            name: None,
260            inner: TypeInner::Scalar(Scalar::U32),
261        });
262        let array_f32 = module.types.insert(Type {
263            name: None,
264            inner: TypeInner::Array {
265                base: f32_ty,
266                size: ArraySize::Dynamic,
267                stride: 4,
268            },
269        });
270        let params_ty = module.types.insert(Type {
271            name: Some("Params".into()),
272            inner: TypeInner::Struct {
273                members: vec![StructMember {
274                    name: Some("N".into()),
275                    ty: u32_ty,
276                    offset: 0,
277                }],
278                span: 4,
279            },
280        });
281
282        module.global_variables.append(GlobalVariable {
283            name: Some("a".into()),
284            space: AddressSpace::Storage {
285                access: StorageAccess::LOAD,
286            },
287            binding: Some(ResourceBinding {
288                group: 0,
289                binding: 0,
290            }),
291            ty: array_f32,
292            init: None,
293            layout: None,
294        });
295        module.global_variables.append(GlobalVariable {
296            name: Some("c".into()),
297            space: AddressSpace::Storage {
298                access: StorageAccess::LOAD | StorageAccess::STORE,
299            },
300            binding: Some(ResourceBinding {
301                group: 0,
302                binding: 1,
303            }),
304            ty: array_f32,
305            init: None,
306            layout: None,
307        });
308        module.global_variables.append(GlobalVariable {
309            name: Some("params".into()),
310            space: AddressSpace::Uniform,
311            binding: Some(ResourceBinding {
312                group: 0,
313                binding: 2,
314            }),
315            ty: params_ty,
316            init: None,
317            layout: None,
318        });
319
320        module
321    }
322
323    #[test]
324    fn infer_elementwise_shapes() {
325        let module = make_simple_module();
326        let shapes = infer_shapes(&module);
327        assert_eq!(shapes.len(), 2); // only storage buffers
328
329        for shape in shapes.values() {
330            assert_eq!(shape.rank(), 1);
331            assert_eq!(shape.dims[0], Dim::Symbolic("N".into()));
332        }
333    }
334
335    #[test]
336    fn infer_matmul_shapes() {
337        let mut module = Module::default();
338
339        let f32_ty = module.types.insert(Type {
340            name: None,
341            inner: TypeInner::Scalar(Scalar::F32),
342        });
343        let u32_ty = module.types.insert(Type {
344            name: None,
345            inner: TypeInner::Scalar(Scalar::U32),
346        });
347        let array_f32 = module.types.insert(Type {
348            name: None,
349            inner: TypeInner::Array {
350                base: f32_ty,
351                size: ArraySize::Dynamic,
352                stride: 4,
353            },
354        });
355        let params_ty = module.types.insert(Type {
356            name: Some("Params".into()),
357            inner: TypeInner::Struct {
358                members: vec![
359                    StructMember {
360                        name: Some("M".into()),
361                        ty: u32_ty,
362                        offset: 0,
363                    },
364                    StructMember {
365                        name: Some("N".into()),
366                        ty: u32_ty,
367                        offset: 4,
368                    },
369                    StructMember {
370                        name: Some("K".into()),
371                        ty: u32_ty,
372                        offset: 8,
373                    },
374                ],
375                span: 12,
376            },
377        });
378
379        module.global_variables.append(GlobalVariable {
380            name: Some("a".into()),
381            space: AddressSpace::Storage {
382                access: StorageAccess::LOAD,
383            },
384            binding: Some(ResourceBinding {
385                group: 0,
386                binding: 0,
387            }),
388            ty: array_f32,
389            init: None,
390            layout: None,
391        });
392        module.global_variables.append(GlobalVariable {
393            name: Some("result".into()),
394            space: AddressSpace::Storage {
395                access: StorageAccess::LOAD | StorageAccess::STORE,
396            },
397            binding: Some(ResourceBinding {
398                group: 0,
399                binding: 2,
400            }),
401            ty: array_f32,
402            init: None,
403            layout: None,
404        });
405        module.global_variables.append(GlobalVariable {
406            name: Some("params".into()),
407            space: AddressSpace::Uniform,
408            binding: Some(ResourceBinding {
409                group: 0,
410                binding: 3,
411            }),
412            ty: params_ty,
413            init: None,
414            layout: None,
415        });
416
417        let shapes = infer_shapes(&module);
418        // Output should be [M, N]
419        let result_handle = module
420            .global_variables
421            .iter()
422            .find(|(_, gv)| gv.name.as_deref() == Some("result"))
423            .unwrap()
424            .0;
425        let result_shape = &shapes[&result_handle];
426        assert_eq!(result_shape.rank(), 2);
427        assert_eq!(result_shape.dims[0], Dim::Symbolic("M".into()));
428        assert_eq!(result_shape.dims[1], Dim::Symbolic("N".into()));
429    }
430
431    #[test]
432    fn shape_inference_pass_runs() {
433        let mut module = make_simple_module();
434        let pass = ShapeInference;
435        let changed = pass.run(&mut module);
436        // Analysis pass never reports changes.
437        assert!(!changed);
438    }
439
440    #[test]
441    fn empty_module_no_shapes() {
442        let module = Module::default();
443        let shapes = infer_shapes(&module);
444        assert_eq!(shapes.len(), 0);
445    }
446
447    #[test]
448    fn unify_known_known_match() {
449        assert_eq!(
450            unify_dim(&Dim::Known(10), &Dim::Known(10)),
451            Some(Dim::Known(10))
452        );
453    }
454
455    #[test]
456    fn unify_known_known_mismatch() {
457        assert_eq!(unify_dim(&Dim::Known(3), &Dim::Known(5)), None);
458    }
459
460    #[test]
461    fn unify_known_dynamic() {
462        assert_eq!(
463            unify_dim(&Dim::Known(10), &Dim::Dynamic),
464            Some(Dim::Known(10))
465        );
466        assert_eq!(
467            unify_dim(&Dim::Dynamic, &Dim::Known(10)),
468            Some(Dim::Known(10))
469        );
470    }
471
472    #[test]
473    fn unify_dynamic_dynamic() {
474        assert_eq!(unify_dim(&Dim::Dynamic, &Dim::Dynamic), Some(Dim::Dynamic));
475    }
476
477    #[test]
478    fn unify_symbolic_same_name() {
479        assert_eq!(
480            unify_dim(
481                &Dim::Symbolic("batch".into()),
482                &Dim::Symbolic("batch".into())
483            ),
484            Some(Dim::Symbolic("batch".into()))
485        );
486    }
487
488    #[test]
489    fn unify_symbolic_different_names() {
490        assert_eq!(
491            unify_dim(&Dim::Symbolic("batch".into()), &Dim::Symbolic("seq".into())),
492            None
493        );
494    }
495
496    #[test]
497    fn unify_symbolic_dynamic() {
498        assert_eq!(
499            unify_dim(&Dim::Symbolic("batch".into()), &Dim::Dynamic),
500            Some(Dim::Symbolic("batch".into()))
501        );
502        assert_eq!(
503            unify_dim(&Dim::Dynamic, &Dim::Symbolic("batch".into())),
504            Some(Dim::Symbolic("batch".into()))
505        );
506    }
507
508    #[test]
509    fn unify_broadcast_known_one() {
510        assert_eq!(
511            unify_dim(&Dim::Known(1), &Dim::Known(10)),
512            Some(Dim::Known(10))
513        );
514        assert_eq!(
515            unify_dim(&Dim::Known(10), &Dim::Known(1)),
516            Some(Dim::Known(10))
517        );
518        assert_eq!(unify_dim(&Dim::Known(1), &Dim::Dynamic), Some(Dim::Dynamic));
519        assert_eq!(
520            unify_dim(&Dim::Known(1), &Dim::Symbolic("batch".into())),
521            Some(Dim::Symbolic("batch".into()))
522        );
523    }
524
525    #[test]
526    fn unify_shapes_match() {
527        let a = Shape {
528            dims: vec![Dim::Known(10), Dim::Symbolic("K".into())],
529        };
530        let b = Shape {
531            dims: vec![Dim::Known(10), Dim::Symbolic("K".into())],
532        };
533        let result = unify_shapes(&a, &b).unwrap();
534        assert_eq!(result.dims, vec![Dim::Known(10), Dim::Symbolic("K".into())]);
535    }
536
537    #[test]
538    fn unify_shapes_rank_mismatch() {
539        let a = Shape {
540            dims: vec![Dim::Known(10)],
541        };
542        let b = Shape {
543            dims: vec![Dim::Known(10), Dim::Known(5)],
544        };
545        assert!(unify_shapes(&a, &b).is_none());
546    }
547
548    #[test]
549    fn unify_shapes_concrete_wins_over_dynamic() {
550        let a = Shape {
551            dims: vec![Dim::Known(10), Dim::Dynamic],
552        };
553        let b = Shape {
554            dims: vec![Dim::Dynamic, Dim::Known(20)],
555        };
556        let result = unify_shapes(&a, &b).unwrap();
557        assert_eq!(result.dims, vec![Dim::Known(10), Dim::Known(20)]);
558    }
559
560    #[test]
561    fn ir_dim_conversion() {
562        use nxpu_ir::Dimension;
563        assert_eq!(ir_dim_to_dim(&Dimension::Fixed(42)), Dim::Known(42));
564        assert_eq!(ir_dim_to_dim(&Dimension::Dynamic(None)), Dim::Dynamic);
565        assert_eq!(
566            ir_dim_to_dim(&Dimension::Dynamic(Some("batch".into()))),
567            Dim::Symbolic("batch".into())
568        );
569        assert_eq!(
570            ir_dim_to_dim(&Dimension::Symbolic("seq_len".into())),
571            Dim::Symbolic("seq_len".into())
572        );
573    }
574
575    #[test]
576    fn infer_tensor_type_shape() {
577        let mut module = Module::default();
578
579        let tensor_ty = module.types.insert(Type {
580            name: None,
581            inner: TypeInner::Tensor {
582                scalar: Scalar::F32,
583                shape: TensorShape {
584                    dims: vec![
585                        Dimension::Symbolic("batch".into()),
586                        Dimension::Fixed(224),
587                        Dimension::Fixed(224),
588                        Dimension::Fixed(3),
589                    ],
590                },
591            },
592        });
593
594        module.global_variables.append(GlobalVariable {
595            name: Some("image".into()),
596            space: AddressSpace::Storage {
597                access: StorageAccess::LOAD,
598            },
599            binding: None,
600            ty: tensor_ty,
601            init: None,
602            layout: None,
603        });
604
605        let shapes = infer_shapes(&module);
606        assert_eq!(shapes.len(), 1);
607        let shape = shapes.values().next().unwrap();
608        assert_eq!(shape.rank(), 4);
609        assert_eq!(shape.dims[0], Dim::Symbolic("batch".into()));
610        assert_eq!(shape.dims[1], Dim::Known(224));
611        assert_eq!(shape.dims[2], Dim::Known(224));
612        assert_eq!(shape.dims[3], Dim::Known(3));
613    }
614
615    #[test]
616    fn fixed_size_array() {
617        let mut module = Module::default();
618
619        let f32_ty = module.types.insert(Type {
620            name: None,
621            inner: TypeInner::Scalar(Scalar::F32),
622        });
623        let array_fixed = module.types.insert(Type {
624            name: None,
625            inner: TypeInner::Array {
626                base: f32_ty,
627                size: ArraySize::Constant(128),
628                stride: 4,
629            },
630        });
631
632        module.global_variables.append(GlobalVariable {
633            name: Some("buf".into()),
634            space: AddressSpace::Storage {
635                access: StorageAccess::LOAD,
636            },
637            binding: None,
638            ty: array_fixed,
639            init: None,
640            layout: None,
641        });
642
643        let shapes = infer_shapes(&module);
644        assert_eq!(shapes.len(), 1);
645        let shape = shapes.values().next().unwrap();
646        assert_eq!(shape.dims[0], Dim::Known(128));
647    }
648}