1use std::collections::HashMap;
7
8use nxpu_ir::{AddressSpace, Dimension, GlobalVariable, Handle, Module, StorageAccess, TypeInner};
9
10use crate::Pass;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum Dim {
15 Known(u64),
17 Symbolic(String),
19 Dynamic,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct Shape {
26 pub dims: Vec<Dim>,
27}
28
29impl Shape {
30 pub fn rank(&self) -> usize {
32 self.dims.len()
33 }
34}
35
36pub fn unify_dim(a: &Dim, b: &Dim) -> Option<Dim> {
47 match (a, b) {
48 (Dim::Known(x), Dim::Known(y)) if x == y => Some(Dim::Known(*x)),
50 (Dim::Known(1), other) | (other, Dim::Known(1)) => Some(other.clone()),
52 (Dim::Known(_), Dim::Known(_)) => None,
54 (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 (Dim::Symbolic(a_name), Dim::Symbolic(b_name)) if a_name == b_name => {
61 Some(Dim::Symbolic(a_name.clone()))
62 }
63 (Dim::Symbolic(_), Dim::Symbolic(_)) => None,
65 (Dim::Symbolic(s), Dim::Dynamic) | (Dim::Dynamic, Dim::Symbolic(s)) => {
67 Some(Dim::Symbolic(s.clone()))
68 }
69 (Dim::Dynamic, Dim::Dynamic) => Some(Dim::Dynamic),
71 }
72}
73
74pub 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
89pub 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
99pub type ShapeMap = HashMap<Handle<GlobalVariable>, Shape>;
101
102#[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 false
125 }
126}
127
128pub fn infer_shapes(module: &Module) -> ShapeMap {
130 let mut map = ShapeMap::new();
131
132 let param_names = extract_param_names(module);
134
135 for (handle, gv) in module.global_variables.iter() {
137 if let AddressSpace::Storage { access } = &gv.space {
138 let shape = infer_global_shape(module, gv, ¶m_names, *access);
139 map.insert(handle, shape);
140 }
141 }
142
143 map
144}
145
146#[allow(clippy::collapsible_if)] fn 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
159fn 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 Shape {
172 dims: vec![Dim::Known(*n as u64)],
173 }
174 }
175 nxpu_ir::ArraySize::Dynamic => {
176 infer_dynamic_shape(param_names, access)
178 }
179 }
180 }
181 TypeInner::Tensor { shape, .. } => {
182 Shape {
185 dims: shape.dims.iter().map(ir_dim_to_dim).collect(),
186 }
187 }
188 _ => {
189 Shape { dims: vec![] }
191 }
192 }
193}
194
195fn infer_dynamic_shape(param_names: &[String], access: StorageAccess) -> Shape {
197 let is_output = access.contains(StorageAccess::STORE);
198
199 match param_names.len() {
200 3 => {
202 if is_output {
203 Shape {
205 dims: vec![
206 Dim::Symbolic(param_names[0].clone()),
207 Dim::Symbolic(param_names[1].clone()),
208 ],
209 }
210 } else {
211 Shape {
214 dims: vec![Dim::Symbolic("?".into()), Dim::Symbolic("?".into())],
215 }
216 }
217 }
218 1 => Shape {
220 dims: vec![Dim::Symbolic(param_names[0].clone())],
221 },
222 n if n > 3 => {
224 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 => Shape {
234 dims: vec![
235 Dim::Symbolic(param_names[0].clone()),
236 Dim::Symbolic(param_names[1].clone()),
237 ],
238 },
239 _ => 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); 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 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 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}