1use crate::ops::TensorOp;
2use lift_core::attributes::Attributes;
3use lift_core::types::{Dimension, TensorTypeInfo};
4
5#[derive(Debug)]
6pub struct ShapeInference;
7
8fn spatial_param(attrs: Option<&Attributes>, key: &str, default: i64) -> i64 {
13 attrs.and_then(|a| a.get_integer(key)).unwrap_or(default)
14}
15
16fn conv_output_dim(input: i64, kernel: i64, stride: i64, padding: i64, dilation: i64) -> i64 {
19 let numerator = input + 2 * padding - dilation * (kernel - 1) - 1;
20 (numerator.max(0) / stride.max(1)) + 1
21}
22
23impl ShapeInference {
24 pub fn infer_output_shape(
25 op: &TensorOp,
26 inputs: &[&TensorTypeInfo],
27 attrs: Option<&Attributes>,
28 ) -> Result<Vec<TensorTypeInfo>, String> {
29 match op {
30 TensorOp::Add | TensorOp::Sub | TensorOp::Mul | TensorOp::Div => {
32 if inputs.len() != 2 {
33 return Err(format!("{} requires 2 inputs", op.name()));
34 }
35 let result = broadcast_shapes(&inputs[0].shape, &inputs[1].shape)?;
36 Ok(vec![TensorTypeInfo {
37 shape: result,
38 dtype: inputs[0].dtype,
39 layout: inputs[0].layout,
40 }])
41 }
42
43 TensorOp::Neg
45 | TensorOp::ReLU
46 | TensorOp::GeLU
47 | TensorOp::SiLU
48 | TensorOp::Sigmoid
49 | TensorOp::Tanh
50 | TensorOp::LeakyReLU
51 | TensorOp::ELU
52 | TensorOp::Mish
53 | TensorOp::HardSwish
54 | TensorOp::HardSigmoid
55 | TensorOp::Softmax
56 | TensorOp::Cumsum
57 | TensorOp::Quantize
58 | TensorOp::Dequantize
59 | TensorOp::QuantizeInt4
60 | TensorOp::DequantizeInt4
61 | TensorOp::QuantizeFp8
62 | TensorOp::DequantizeFp8
63 | TensorOp::Checkpoint
64 | TensorOp::Offload
65 | TensorOp::GradReLU
66 | TensorOp::GradGeLU
67 | TensorOp::GradSoftmax => {
68 if inputs.is_empty() {
69 return Err(format!("{} requires at least 1 input", op.name()));
70 }
71 Ok(vec![inputs[0].clone()])
72 }
73
74 TensorOp::LayerNorm
76 | TensorOp::RMSNorm
77 | TensorOp::BatchNorm
78 | TensorOp::GroupNorm
79 | TensorOp::InstanceNorm
80 | TensorOp::GradLayerNorm => {
81 if inputs.is_empty() {
82 return Err(format!("{} requires at least 1 input", op.name()));
83 }
84 Ok(vec![inputs[0].clone()])
85 }
86
87 TensorOp::MatMul | TensorOp::SparseMatMul => {
89 if inputs.len() != 2 {
90 return Err("matmul requires 2 inputs".into());
91 }
92 let a = &inputs[0].shape;
93 let b = &inputs[1].shape;
94 if a.len() < 2 || b.len() < 2 {
95 return Err("matmul inputs must be at least 2D".into());
96 }
97 let m = a[a.len() - 2].clone();
98 let n = b[b.len() - 1].clone();
99
100 let k_a = &a[a.len() - 1];
101 let k_b = &b[b.len() - 2];
102 if let (Some(ka), Some(kb)) = (k_a.static_value(), k_b.static_value()) {
103 if ka != kb {
104 return Err(format!("matmul inner dimension mismatch: {} vs {}", ka, kb));
105 }
106 }
107
108 let mut result_shape = Vec::new();
109 let batch_a = &a[..a.len() - 2];
110 let batch_b = &b[..b.len() - 2];
111 let batch = broadcast_shapes(batch_a, batch_b)?;
112 result_shape.extend(batch);
113 result_shape.push(m);
114 result_shape.push(n);
115
116 Ok(vec![TensorTypeInfo {
117 shape: result_shape,
118 dtype: inputs[0].dtype,
119 layout: inputs[0].layout,
120 }])
121 }
122
123 TensorOp::Linear => {
125 if inputs.len() < 2 {
126 return Err("linear requires at least 2 inputs (x, W)".into());
127 }
128 let x = &inputs[0].shape;
129 let w = &inputs[1].shape;
130 if x.is_empty() || w.len() != 2 {
131 return Err("linear: x must be at least 1D, W must be 2D".into());
132 }
133 let mut result_shape = x[..x.len() - 1].to_vec();
134 result_shape.push(w[1].clone());
135
136 Ok(vec![TensorTypeInfo {
137 shape: result_shape,
138 dtype: inputs[0].dtype,
139 layout: inputs[0].layout,
140 }])
141 }
142
143 TensorOp::Conv2D | TensorOp::DepthwiseConv2D | TensorOp::DilatedConv2D => {
145 if inputs.len() < 2 {
146 return Err("conv2d requires at least 2 inputs (input, kernel)".into());
147 }
148 let input = &inputs[0].shape;
149 let kernel = &inputs[1].shape;
150 if input.len() != 4 || kernel.len() != 4 {
151 return Err("conv2d: input and kernel must be 4D (NCHW)".into());
152 }
153
154 let stride = spatial_param(attrs, "stride", 1);
159 let padding = spatial_param(attrs, "padding", 0);
160 let dilation = spatial_param(
161 attrs,
162 "dilation",
163 if matches!(op, TensorOp::DilatedConv2D) {
164 2
165 } else {
166 1
167 },
168 );
169
170 let n = input[0].clone();
171 let cout = kernel[0].clone();
172 let h_out = match (&input[2], &kernel[2]) {
173 (Dimension::Constant(ih), Dimension::Constant(kh)) => Dimension::Constant(
174 conv_output_dim(*ih as i64, *kh as i64, stride, padding, dilation) as usize,
175 ),
176 _ => Dimension::Symbolic("H_out".into()),
177 };
178 let w_out = match (&input[3], &kernel[3]) {
179 (Dimension::Constant(iw), Dimension::Constant(kw)) => Dimension::Constant(
180 conv_output_dim(*iw as i64, *kw as i64, stride, padding, dilation) as usize,
181 ),
182 _ => Dimension::Symbolic("W_out".into()),
183 };
184
185 Ok(vec![TensorTypeInfo {
186 shape: vec![n, cout, h_out, w_out],
187 dtype: inputs[0].dtype,
188 layout: inputs[0].layout,
189 }])
190 }
191
192 TensorOp::Conv1D => {
194 if inputs.len() < 2 {
195 return Err("conv1d requires at least 2 inputs".into());
196 }
197 let input = &inputs[0].shape;
198 let kernel = &inputs[1].shape;
199 if input.len() != 3 || kernel.len() != 3 {
200 return Err("conv1d: input [N,C,L] and kernel [Cout,Cin,K]".into());
201 }
202 let stride = spatial_param(attrs, "stride", 1);
203 let padding = spatial_param(attrs, "padding", 0);
204 let dilation = spatial_param(attrs, "dilation", 1);
205 let n = input[0].clone();
206 let cout = kernel[0].clone();
207 let l_out = match (&input[2], &kernel[2]) {
208 (Dimension::Constant(il), Dimension::Constant(kl)) => Dimension::Constant(
209 conv_output_dim(*il as i64, *kl as i64, stride, padding, dilation) as usize,
210 ),
211 _ => Dimension::Symbolic("L_out".into()),
212 };
213 Ok(vec![TensorTypeInfo {
214 shape: vec![n, cout, l_out],
215 dtype: inputs[0].dtype,
216 layout: inputs[0].layout,
217 }])
218 }
219
220 TensorOp::Conv3D => {
222 if inputs.len() < 2 {
223 return Err("conv3d requires at least 2 inputs".into());
224 }
225 let input = &inputs[0].shape;
226 let kernel = &inputs[1].shape;
227 if input.len() != 5 || kernel.len() != 5 {
228 return Err("conv3d: input [N,C,D,H,W] and kernel [Cout,Cin,Kd,Kh,Kw]".into());
229 }
230 let stride = spatial_param(attrs, "stride", 1);
231 let padding = spatial_param(attrs, "padding", 0);
232 let dilation = spatial_param(attrs, "dilation", 1);
233 let n = input[0].clone();
234 let cout = kernel[0].clone();
235 let dims: Vec<Dimension> = (2..5)
236 .map(|i| match (&input[i], &kernel[i]) {
237 (Dimension::Constant(iv), Dimension::Constant(kv)) => Dimension::Constant(
238 conv_output_dim(*iv as i64, *kv as i64, stride, padding, dilation)
239 as usize,
240 ),
241 _ => Dimension::Symbolic(format!("dim{}_out", i)),
242 })
243 .collect();
244 Ok(vec![TensorTypeInfo {
245 shape: vec![n, cout, dims[0].clone(), dims[1].clone(), dims[2].clone()],
246 dtype: inputs[0].dtype,
247 layout: inputs[0].layout,
248 }])
249 }
250
251 TensorOp::MaxPool2D | TensorOp::AvgPool2D => {
261 if inputs.is_empty() {
262 return Err(format!("{} requires at least 1 input", op.name()));
263 }
264 let Some(kernel) = inputs.get(1) else {
265 return Ok(vec![inputs[0].clone()]);
266 };
267 let input = &inputs[0].shape;
268 let kh = kernel.shape.first().and_then(|d| d.static_value());
269 let kw = kernel.shape.get(1).and_then(|d| d.static_value());
270 if input.len() < 4 {
271 return Err(format!("{}: input must be 4D [N,C,H,W]", op.name()));
272 }
273 let padding = spatial_param(attrs, "padding", 0);
274 let mut out = input.clone();
275 if let (Some(kh), Dimension::Constant(ih)) = (kh, &input[2]) {
276 let stride = spatial_param(attrs, "stride", kh as i64);
277 out[2] = Dimension::Constant(conv_output_dim(
278 *ih as i64, kh as i64, stride, padding, 1,
279 ) as usize);
280 }
281 if let (Some(kw), Dimension::Constant(iw)) = (kw, &input[3]) {
282 let stride = spatial_param(attrs, "stride", kw as i64);
283 out[3] = Dimension::Constant(conv_output_dim(
284 *iw as i64, kw as i64, stride, padding, 1,
285 ) as usize);
286 }
287 Ok(vec![TensorTypeInfo {
288 shape: out,
289 dtype: inputs[0].dtype,
290 layout: inputs[0].layout,
291 }])
292 }
293
294 TensorOp::AdaptiveAvgPool2D => {
300 if inputs.is_empty() {
301 return Err("adaptive_avgpool2d requires 1 input".into());
302 }
303 Ok(vec![inputs[0].clone()])
304 }
305
306 TensorOp::GlobalAvgPool => {
307 if inputs.is_empty() {
308 return Err("global_avgpool requires 1 input".into());
309 }
310 let shape = &inputs[0].shape;
311 if shape.len() < 3 {
312 return Err("global_avgpool: input must be at least 3D [N,C,...]".into());
313 }
314 let mut out = vec![shape[0].clone(), shape[1].clone()];
316 for _ in 2..shape.len() {
317 out.push(Dimension::Constant(1));
318 }
319 Ok(vec![TensorTypeInfo {
320 shape: out,
321 dtype: inputs[0].dtype,
322 layout: inputs[0].layout,
323 }])
324 }
325
326 TensorOp::Attention
328 | TensorOp::MultiHeadAttention
329 | TensorOp::MultiQueryAttention
330 | TensorOp::GroupedQueryAttention
331 | TensorOp::FlashAttention
332 | TensorOp::SlidingWindowAttention
333 | TensorOp::CrossAttention
334 | TensorOp::PagedAttention
335 | TensorOp::GradAttention => {
336 if inputs.len() < 3 {
337 return Err("attention requires at least 3 inputs (Q, K, V)".into());
338 }
339 Ok(vec![inputs[0].clone()])
340 }
341
342 TensorOp::LSTMCell => {
344 if inputs.len() < 2 {
345 return Err("lstm_cell requires input and hidden state".into());
346 }
347 Ok(vec![inputs[1].clone(), inputs[1].clone()])
349 }
350
351 TensorOp::GRUCell | TensorOp::RNNCell => {
352 if inputs.len() < 2 {
353 return Err(format!("{} requires input and hidden state", op.name()));
354 }
355 Ok(vec![inputs[1].clone()])
356 }
357
358 TensorOp::Reshape
360 | TensorOp::Transpose
361 | TensorOp::Squeeze
362 | TensorOp::Unsqueeze
363 | TensorOp::Permute
364 | TensorOp::Expand
365 | TensorOp::Slice
366 | TensorOp::Pad
367 | TensorOp::Tile => {
368 if inputs.is_empty() {
370 return Err(format!("{} requires at least 1 input", op.name()));
371 }
372 Ok(vec![inputs[0].clone()])
373 }
374
375 TensorOp::Concat => {
377 if inputs.is_empty() {
378 return Err("concat requires at least 1 input".into());
379 }
380 Ok(vec![inputs[0].clone()])
381 }
382
383 TensorOp::TopK | TensorOp::Sort => {
385 if inputs.is_empty() {
386 return Err(format!("{} requires 1 input", op.name()));
387 }
388 Ok(vec![inputs[0].clone()])
389 }
390
391 TensorOp::FFT | TensorOp::IFFT => {
393 if inputs.is_empty() {
394 return Err(format!("{} requires 1 input", op.name()));
395 }
396 Ok(vec![inputs[0].clone()])
397 }
398
399 TensorOp::SVD => {
401 if inputs.is_empty() {
402 return Err("svd requires 1 input".into());
403 }
404 Ok(vec![inputs[0].clone()])
405 }
406
407 TensorOp::Where | TensorOp::Clamp => {
409 if inputs.len() < 2 {
410 return Err(format!("{} requires at least 2 inputs", op.name()));
411 }
412 Ok(vec![inputs[0].clone()])
413 }
414
415 _ => {
416 if !inputs.is_empty() {
418 Ok(vec![inputs[0].clone()])
419 } else {
420 Ok(Vec::new())
421 }
422 }
423 }
424 }
425
426 pub fn compute_flops(
427 op: &TensorOp,
428 inputs: &[&TensorTypeInfo],
429 attrs: Option<&Attributes>,
430 ) -> Option<u64> {
431 match op {
432 TensorOp::MatMul | TensorOp::SparseMatMul => {
433 if inputs.len() != 2 {
434 return None;
435 }
436 let a = &inputs[0].shape;
437 let b = &inputs[1].shape;
438 let m = a.get(a.len().checked_sub(2)?)?.static_value()? as u64;
439 let k = a.last()?.static_value()? as u64;
440 let n = b.last()?.static_value()? as u64;
441 let batch: u64 = a[..a.len() - 2]
442 .iter()
443 .filter_map(|d| d.static_value())
444 .map(|v| v as u64)
445 .product::<u64>()
446 .max(1);
447 Some(2 * batch * m * n * k)
448 }
449
450 TensorOp::Add | TensorOp::Sub | TensorOp::Mul | TensorOp::Div => {
451 if inputs.is_empty() {
452 return None;
453 }
454 Some(element_count(&inputs[0].shape)? as u64)
455 }
456
457 TensorOp::ReLU
458 | TensorOp::Sigmoid
459 | TensorOp::Tanh
460 | TensorOp::LeakyReLU
461 | TensorOp::ELU
462 | TensorOp::HardSigmoid => {
463 if inputs.is_empty() {
464 return None;
465 }
466 Some(element_count(&inputs[0].shape)? as u64)
467 }
468
469 TensorOp::GeLU | TensorOp::SiLU | TensorOp::Mish | TensorOp::HardSwish => {
470 if inputs.is_empty() {
471 return None;
472 }
473 let n = element_count(&inputs[0].shape)? as u64;
474 Some(8 * n)
475 }
476
477 TensorOp::Softmax => {
478 if inputs.is_empty() {
479 return None;
480 }
481 let n = element_count(&inputs[0].shape)? as u64;
482 Some(5 * n)
483 }
484
485 TensorOp::LayerNorm
486 | TensorOp::RMSNorm
487 | TensorOp::GroupNorm
488 | TensorOp::InstanceNorm => {
489 if inputs.is_empty() {
490 return None;
491 }
492 let n = element_count(&inputs[0].shape)? as u64;
493 Some(7 * n)
494 }
495
496 TensorOp::BatchNorm => {
497 if inputs.is_empty() {
498 return None;
499 }
500 let n = element_count(&inputs[0].shape)? as u64;
501 Some(5 * n)
502 }
503
504 TensorOp::Linear => {
505 if inputs.len() < 2 {
506 return None;
507 }
508 let x = &inputs[0].shape;
509 let w = &inputs[1].shape;
510 let m: u64 = x[..x.len() - 1]
511 .iter()
512 .filter_map(|d| d.static_value())
513 .map(|v| v as u64)
514 .product::<u64>()
515 .max(1);
516 let k = x.last()?.static_value()? as u64;
517 let n = w.last()?.static_value()? as u64;
518 Some(2 * m * n * k + n)
519 }
520
521 TensorOp::Conv2D | TensorOp::DepthwiseConv2D | TensorOp::DilatedConv2D => {
522 if inputs.len() < 2 {
523 return None;
524 }
525 let kernel = &inputs[1].shape;
526 let cout = kernel[0].static_value()? as u64;
527 let cin = kernel[1].static_value()? as u64;
528 let kh = kernel[2].static_value()? as u64;
529 let kw = kernel[3].static_value()? as u64;
530 let input = &inputs[0].shape;
531 let n = input[0].static_value()? as u64;
532 let ih = input[2].static_value()? as u64;
533 let iw = input[3].static_value()? as u64;
534 let stride = spatial_param(attrs, "stride", 1);
535 let padding = spatial_param(attrs, "padding", 0);
536 let dilation = spatial_param(
537 attrs,
538 "dilation",
539 if matches!(op, TensorOp::DilatedConv2D) {
540 2
541 } else {
542 1
543 },
544 );
545 let oh = conv_output_dim(ih as i64, kh as i64, stride, padding, dilation) as u64;
546 let ow = conv_output_dim(iw as i64, kw as i64, stride, padding, dilation) as u64;
547 Some(2 * n * cout * cin * kh * kw * oh * ow)
548 }
549
550 TensorOp::Conv1D => {
551 if inputs.len() < 2 {
552 return None;
553 }
554 let kernel = &inputs[1].shape;
555 let cout = kernel[0].static_value()? as u64;
556 let cin = kernel[1].static_value()? as u64;
557 let k = kernel[2].static_value()? as u64;
558 let input = &inputs[0].shape;
559 let n = input[0].static_value()? as u64;
560 let il = input[2].static_value()? as u64;
561 let stride = spatial_param(attrs, "stride", 1);
562 let padding = spatial_param(attrs, "padding", 0);
563 let dilation = spatial_param(attrs, "dilation", 1);
564 let ol = conv_output_dim(il as i64, k as i64, stride, padding, dilation) as u64;
565 Some(2 * n * cout * cin * k * ol)
566 }
567
568 TensorOp::Conv3D => {
569 if inputs.len() < 2 {
570 return None;
571 }
572 let kernel = &inputs[1].shape;
573 let cout = kernel.first()?.static_value()? as u64;
574 let cin = kernel.get(1)?.static_value()? as u64;
575 let kd = kernel.get(2)?.static_value()? as u64;
576 let kh = kernel.get(3)?.static_value()? as u64;
577 let kw = kernel.get(4)?.static_value()? as u64;
578 let input = &inputs[0].shape;
579 let n = input.first()?.static_value()? as u64;
580 let id = input.get(2)?.static_value()? as u64;
581 let ih = input.get(3)?.static_value()? as u64;
582 let iw = input.get(4)?.static_value()? as u64;
583 let stride = spatial_param(attrs, "stride", 1);
584 let padding = spatial_param(attrs, "padding", 0);
585 let dilation = spatial_param(attrs, "dilation", 1);
586 let od = conv_output_dim(id as i64, kd as i64, stride, padding, dilation) as u64;
587 let oh = conv_output_dim(ih as i64, kh as i64, stride, padding, dilation) as u64;
588 let ow = conv_output_dim(iw as i64, kw as i64, stride, padding, dilation) as u64;
589 Some(2 * n * cout * cin * kd * kh * kw * od * oh * ow)
590 }
591
592 TensorOp::Attention
594 | TensorOp::MultiHeadAttention
595 | TensorOp::MultiQueryAttention
596 | TensorOp::GroupedQueryAttention
597 | TensorOp::FlashAttention
598 | TensorOp::SlidingWindowAttention
599 | TensorOp::CrossAttention => {
600 if inputs.is_empty() {
601 return None;
602 }
603 let shape = &inputs[0].shape;
604 if shape.len() < 3 {
605 return None;
606 }
607 let b = shape[0].static_value().unwrap_or(1) as u64;
608 let s = shape[shape.len() - 2].static_value()? as u64;
609 let d = shape.last()?.static_value()? as u64;
610 let h = if shape.len() >= 4 {
611 shape[1].static_value().unwrap_or(1) as u64
612 } else {
613 1
614 };
615 Some(4 * b * h * s * s * d)
616 }
617
618 TensorOp::LSTMCell => {
620 if inputs.len() < 2 {
622 return None;
623 }
624 let input_size = inputs[0].shape.last()?.static_value()? as u64;
625 let hidden_size = inputs[1].shape.last()?.static_value()? as u64;
626 Some(8 * (input_size + hidden_size) * hidden_size)
627 }
628
629 TensorOp::GRUCell => {
630 if inputs.len() < 2 {
631 return None;
632 }
633 let input_size = inputs[0].shape.last()?.static_value()? as u64;
634 let hidden_size = inputs[1].shape.last()?.static_value()? as u64;
635 Some(6 * (input_size + hidden_size) * hidden_size)
636 }
637
638 TensorOp::RNNCell => {
639 if inputs.len() < 2 {
640 return None;
641 }
642 let input_size = inputs[0].shape.last()?.static_value()? as u64;
643 let hidden_size = inputs[1].shape.last()?.static_value()? as u64;
644 Some(2 * (input_size + hidden_size) * hidden_size)
645 }
646
647 TensorOp::FFT | TensorOp::IFFT => {
649 if inputs.is_empty() {
650 return None;
651 }
652 let n = element_count(&inputs[0].shape)? as u64;
653 if n == 0 {
654 return Some(0);
655 }
656 let log2n = (n as f64).log2().ceil() as u64;
657 Some(5 * n * log2n)
658 }
659
660 TensorOp::MaxPool2D
662 | TensorOp::AvgPool2D
663 | TensorOp::AdaptiveAvgPool2D
664 | TensorOp::GlobalAvgPool => {
665 if inputs.is_empty() {
666 return None;
667 }
668 Some(element_count(&inputs[0].shape)? as u64)
669 }
670
671 _ if op.is_zero_flop() => Some(0),
673
674 _ => None,
675 }
676 }
677
678 pub fn compute_memory_bytes(
685 op: &TensorOp,
686 inputs: &[&TensorTypeInfo],
687 attrs: Option<&Attributes>,
688 ) -> Option<u64> {
689 let input_bytes: u64 = inputs
690 .iter()
691 .filter_map(|i| tensor_bytes(i).map(|b| b as u64))
692 .sum();
693 let output_bytes: u64 = Self::infer_output_shape(op, inputs, attrs)
694 .ok()
695 .map(|outs| {
696 outs.iter()
697 .filter_map(|o| tensor_info_bytes(o).map(|b| b as u64))
698 .sum()
699 })
700 .unwrap_or(0);
701 Some(input_bytes + output_bytes)
702 }
703}
704
705fn broadcast_shapes(a: &[Dimension], b: &[Dimension]) -> Result<Vec<Dimension>, String> {
706 let max_rank = a.len().max(b.len());
707 let mut result = Vec::with_capacity(max_rank);
708
709 for i in 0..max_rank {
710 let da = if i < a.len() {
711 Some(&a[a.len() - 1 - i])
712 } else {
713 None
714 };
715 let db = if i < b.len() {
716 Some(&b[b.len() - 1 - i])
717 } else {
718 None
719 };
720
721 let dim = match (da, db) {
722 (Some(a_dim), Some(b_dim)) => match (a_dim.static_value(), b_dim.static_value()) {
723 (Some(a_val), Some(b_val)) => {
724 if a_val == b_val {
725 Dimension::Constant(a_val)
726 } else if a_val == 1 {
727 Dimension::Constant(b_val)
728 } else if b_val == 1 {
729 Dimension::Constant(a_val)
730 } else {
731 return Err(format!("Shape broadcast error: {} vs {}", a_val, b_val));
732 }
733 }
734 _ => Dimension::Symbolic("broadcast".into()),
735 },
736 (Some(d), None) | (None, Some(d)) => d.clone(),
737 (None, None) => unreachable!(),
738 };
739 result.push(dim);
740 }
741
742 result.reverse();
743 Ok(result)
744}
745
746fn element_count(shape: &[Dimension]) -> Option<usize> {
747 let mut count = 1usize;
748 for dim in shape {
749 count = count.checked_mul(dim.static_value()?)?;
750 }
751 Some(count)
752}
753
754fn tensor_bytes(info: &TensorTypeInfo) -> Option<usize> {
755 Some(element_count(&info.shape)? * info.dtype.byte_size())
756}
757
758fn tensor_info_bytes(info: &TensorTypeInfo) -> Option<usize> {
759 tensor_bytes(info)
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use lift_core::attributes::Attribute;
766 use lift_core::types::{DataType, MemoryLayout};
767
768 fn make_tensor(shape: Vec<usize>, dtype: DataType) -> TensorTypeInfo {
769 TensorTypeInfo {
770 shape: shape.into_iter().map(Dimension::Constant).collect(),
771 dtype,
772 layout: MemoryLayout::Contiguous,
773 }
774 }
775
776 #[test]
777 fn test_matmul_shape() {
778 let a = make_tensor(vec![2, 3, 4], DataType::FP32);
779 let b = make_tensor(vec![2, 4, 5], DataType::FP32);
780 let result =
781 ShapeInference::infer_output_shape(&TensorOp::MatMul, &[&a, &b], None).unwrap();
782 assert_eq!(result.len(), 1);
783 let shape = &result[0].shape;
784 assert_eq!(shape.len(), 3);
785 assert_eq!(shape[0].static_value(), Some(2));
786 assert_eq!(shape[1].static_value(), Some(3));
787 assert_eq!(shape[2].static_value(), Some(5));
788 }
789
790 #[test]
791 fn test_matmul_dimension_mismatch() {
792 let a = make_tensor(vec![3, 4], DataType::FP32);
793 let b = make_tensor(vec![5, 6], DataType::FP32);
794 let result = ShapeInference::infer_output_shape(&TensorOp::MatMul, &[&a, &b], None);
795 assert!(result.is_err());
796 }
797
798 #[test]
799 fn test_matmul_flops() {
800 let a = make_tensor(vec![2, 3], DataType::FP32);
801 let b = make_tensor(vec![3, 4], DataType::FP32);
802 let flops = ShapeInference::compute_flops(&TensorOp::MatMul, &[&a, &b], None);
803 assert_eq!(flops, Some(2 * 2 * 4 * 3)); }
805
806 #[test]
807 fn test_relu_shape() {
808 let a = make_tensor(vec![2, 3, 4], DataType::FP32);
809 let result = ShapeInference::infer_output_shape(&TensorOp::ReLU, &[&a], None).unwrap();
810 assert_eq!(result[0].shape, a.shape);
811 }
812
813 #[test]
814 fn test_linear_shape() {
815 let x = make_tensor(vec![1, 784], DataType::FP32);
816 let w = make_tensor(vec![784, 64], DataType::FP32);
817 let b = make_tensor(vec![64], DataType::FP32);
818 let result =
819 ShapeInference::infer_output_shape(&TensorOp::Linear, &[&x, &w, &b], None).unwrap();
820 assert_eq!(result[0].shape[0].static_value(), Some(1));
821 assert_eq!(result[0].shape[1].static_value(), Some(64));
822 }
823
824 #[test]
825 fn test_conv2d_shape() {
826 let input = make_tensor(vec![1, 3, 28, 28], DataType::FP32);
827 let kernel = make_tensor(vec![16, 3, 5, 5], DataType::FP32);
828 let result =
829 ShapeInference::infer_output_shape(&TensorOp::Conv2D, &[&input, &kernel], None)
830 .unwrap();
831 assert_eq!(result[0].shape[0].static_value(), Some(1));
832 assert_eq!(result[0].shape[1].static_value(), Some(16));
833 assert_eq!(result[0].shape[2].static_value(), Some(24)); assert_eq!(result[0].shape[3].static_value(), Some(24));
835 }
836
837 #[test]
841 fn test_conv2d_shape_honours_stride_padding_dilation() {
842 let input = make_tensor(vec![1, 3, 28, 28], DataType::FP32);
843 let kernel = make_tensor(vec![16, 3, 3, 3], DataType::FP32);
844
845 let mut attrs = Attributes::new();
847 attrs.set("stride", Attribute::Integer(2));
848 attrs.set("padding", Attribute::Integer(1));
849 let result =
850 ShapeInference::infer_output_shape(&TensorOp::Conv2D, &[&input, &kernel], Some(&attrs))
851 .unwrap();
852 assert_eq!(result[0].shape[2].static_value(), Some(14));
853 assert_eq!(result[0].shape[3].static_value(), Some(14));
854
855 let flops =
856 ShapeInference::compute_flops(&TensorOp::Conv2D, &[&input, &kernel], Some(&attrs))
857 .unwrap();
858 assert_eq!(flops, 2 * 16 * 3 * 3 * 3 * 14 * 14);
859 }
860
861 #[test]
866 fn test_dilated_conv2d_differs_from_plain_conv2d_by_default() {
867 let input = make_tensor(vec![1, 3, 28, 28], DataType::FP32);
868 let kernel = make_tensor(vec![16, 3, 3, 3], DataType::FP32);
869
870 let plain = ShapeInference::infer_output_shape(&TensorOp::Conv2D, &[&input, &kernel], None)
871 .unwrap();
872 let dilated =
873 ShapeInference::infer_output_shape(&TensorOp::DilatedConv2D, &[&input, &kernel], None)
874 .unwrap();
875
876 assert_eq!(plain[0].shape[2].static_value(), Some(26)); assert_eq!(dilated[0].shape[2].static_value(), Some(24));
879 assert_ne!(
880 plain[0].shape[2], dilated[0].shape[2],
881 "DilatedConv2D must not silently behave like Conv2D"
882 );
883 }
884
885 #[test]
888 fn test_maxpool2d_reduces_spatial_dims() {
889 let input = make_tensor(vec![1, 64, 32, 32], DataType::FP32);
890 let kernel = make_tensor(vec![2, 2], DataType::FP32);
891 let result =
892 ShapeInference::infer_output_shape(&TensorOp::MaxPool2D, &[&input, &kernel], None)
893 .unwrap();
894 assert_eq!(result[0].shape[0].static_value(), Some(1));
895 assert_eq!(result[0].shape[1].static_value(), Some(64));
896 assert_eq!(result[0].shape[2].static_value(), Some(16));
898 assert_eq!(result[0].shape[3].static_value(), Some(16));
899 }
900
901 #[test]
905 fn test_compute_memory_bytes_includes_output_for_every_op() {
906 let input = make_tensor(vec![1, 3, 28, 28], DataType::FP32); let kernel = make_tensor(vec![16, 3, 5, 5], DataType::FP32); let mem = ShapeInference::compute_memory_bytes(&TensorOp::Conv2D, &[&input, &kernel], None)
909 .unwrap();
910 let expected = (2352 + 1200 + 16 * 24 * 24) * 4;
912 assert_eq!(mem, expected as u64);
913 }
914}