1use crate::DType;
12use smallvec::SmallVec;
13
14#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum Dim {
18 Static(usize),
20 Dynamic(u32),
23}
24
25impl Dim {
26 pub fn unwrap_static(self) -> usize {
27 match self {
28 Self::Static(n) => n,
29 Self::Dynamic(s) => panic!("expected static dim, got dynamic symbol {s}"),
30 }
31 }
32
33 pub fn is_static(self) -> bool {
34 matches!(self, Self::Static(_))
35 }
36}
37
38impl From<usize> for Dim {
39 fn from(n: usize) -> Self {
40 Self::Static(n)
41 }
42}
43
44impl std::fmt::Display for Dim {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Static(n) => write!(f, "{n}"),
48 Self::Dynamic(s) => write!(f, "?{s}"),
49 }
50 }
51}
52
53#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct Shape {
59 dims: SmallVec<[Dim; 4]>,
60 dtype: DType,
61}
62
63impl Shape {
64 pub fn new(dims: &[usize], dtype: DType) -> Self {
66 Self {
67 dims: dims.iter().map(|&d| Dim::Static(d)).collect(),
68 dtype,
69 }
70 }
71
72 pub fn from_dims(dims: &[Dim], dtype: DType) -> Self {
74 Self {
75 dims: dims.into(),
76 dtype,
77 }
78 }
79
80 pub fn scalar(dtype: DType) -> Self {
82 Self {
83 dims: SmallVec::new(),
84 dtype,
85 }
86 }
87
88 pub fn rank(&self) -> usize {
89 self.dims.len()
90 }
91 pub fn dtype(&self) -> DType {
92 self.dtype
93 }
94 pub fn dims(&self) -> &[Dim] {
95 &self.dims
96 }
97 pub fn dim(&self, i: usize) -> Dim {
98 self.dims.get(i).copied().unwrap_or_else(|| {
99 let dims: Vec<_> = self.dims.iter().map(|d| d.unwrap_static()).collect();
100 panic!(
101 "Shape::dim({i}) out of bounds for rank {} dims={dims:?}",
102 self.rank()
103 );
104 })
105 }
106
107 pub fn dynamic_symbols(&self) -> Vec<u32> {
110 let mut syms: Vec<u32> = self
111 .dims
112 .iter()
113 .filter_map(|d| match d {
114 Dim::Dynamic(s) => Some(*s),
115 _ => None,
116 })
117 .collect();
118 syms.sort();
119 syms.dedup();
120 syms
121 }
122
123 pub fn bind(&self, bindings: &DimBinding) -> Self {
128 let dims = self
129 .dims
130 .iter()
131 .map(|d| match d {
132 Dim::Dynamic(s) => match bindings.get(*s) {
133 Some(n) => Dim::Static(n),
134 None => *d,
135 },
136 _ => *d,
137 })
138 .collect();
139 Self {
140 dims,
141 dtype: self.dtype,
142 }
143 }
144
145 pub fn num_elements(&self) -> Option<usize> {
147 let mut total = 1usize;
148 for d in &self.dims {
149 match d {
150 Dim::Static(n) => total = total.checked_mul(*n)?,
151 Dim::Dynamic(_) => return None,
152 }
153 }
154 Some(total)
155 }
156
157 pub fn size_bytes(&self) -> Option<usize> {
159 self.num_elements().map(|n| n * self.dtype.size_bytes())
160 }
161
162 pub fn is_static(&self) -> bool {
164 self.dims.iter().all(|d| d.is_static())
165 }
166
167 pub fn with_dim(mut self, axis: usize, dim: Dim) -> Self {
169 self.dims[axis] = dim;
170 self
171 }
172
173 pub fn with_dtype(mut self, dtype: DType) -> Self {
175 self.dtype = dtype;
176 self
177 }
178
179 pub fn broadcast_with(&self, other: &Shape) -> Result<Shape, String> {
181 broadcast(self, other)
182 }
183}
184
185pub fn ada_modulation_lead_pack(x_dims: &[usize], mod_dims_in: &[usize]) -> [u32; 17] {
193 let rank = x_dims.len();
194 assert!(rank >= 1, "AdaLayerNorm/GatedResidual: rank ≥ 1");
195 let lead = rank - 1;
196 assert!(
197 lead <= 8,
198 "AdaLayerNorm/GatedResidual: at most 8 leading dims"
199 );
200 let pad = rank
201 .checked_sub(mod_dims_in.len())
202 .expect("modulation rank exceeds x");
203 let mut mod_dims = vec![1usize; rank];
204 mod_dims[pad..].copy_from_slice(mod_dims_in);
205 let mut pack = [0u32; 17];
206 pack[0] = lead as u32;
207 for j in 0..lead {
208 pack[1 + j] = x_dims[j] as u32;
209 pack[9 + j] = mod_dims[j] as u32;
210 }
211 pack
212}
213
214pub fn ada_modulation_launch(x_dims: &[usize], mod_dims: &[usize]) -> (u32, u32) {
218 assert!(!x_dims.is_empty() && !mod_dims.is_empty());
219 let xr = x_dims.len() - 1;
220 let mr = mod_dims.len() - 1;
221 let mut seq = 1u32;
222 let mut mods = 1u32;
223 for i in 0..xr {
224 let xd = x_dims[i] as u32;
225 let md = if i + mr >= xr {
226 mod_dims[i - (xr - mr)] as u32
227 } else {
228 1
229 };
230 if md == 1 && xd > 1 {
231 seq = seq.saturating_mul(xd);
232 } else {
233 mods = mods.saturating_mul(xd.max(1));
234 }
235 }
236 (mods.max(1), seq.max(1))
237}
238
239pub fn broadcast(a: &Shape, b: &Shape) -> Result<Shape, String> {
243 let max_rank = a.rank().max(b.rank());
244 let mut dims = SmallVec::new();
245 for i in 0..max_rank {
246 let ad = if i < max_rank - a.rank() {
247 Dim::Static(1)
248 } else {
249 a.dims[i - (max_rank - a.rank())]
250 };
251 let bd = if i < max_rank - b.rank() {
252 Dim::Static(1)
253 } else {
254 b.dims[i - (max_rank - b.rank())]
255 };
256 let d = broadcast_dim(ad, bd)?;
257 dims.push(d);
258 }
259 Ok(Shape {
260 dims,
261 dtype: a.dtype,
262 })
263}
264
265fn broadcast_dim(a: Dim, b: Dim) -> Result<Dim, String> {
266 match (a, b) {
267 (Dim::Static(1), d) | (d, Dim::Static(1)) => Ok(d),
268 (Dim::Static(x), Dim::Static(y)) if x == y => Ok(Dim::Static(x)),
269 (Dim::Static(x), Dim::Static(y)) => Err(format!("cannot broadcast {x} with {y}")),
270 (Dim::Dynamic(s), Dim::Dynamic(t)) if s == t => Ok(Dim::Dynamic(s)),
271 (Dim::Dynamic(_), _) | (_, Dim::Dynamic(_)) => Ok(a), }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub struct GroupedMatMulDims {
279 pub m: usize,
280 pub k: usize,
281 pub n: usize,
282 pub num_experts: usize,
283}
284
285pub fn grouped_matmul_dims(
301 input: &Shape,
302 weight: &Shape,
303 out: Option<&Shape>,
304) -> Result<GroupedMatMulDims, String> {
305 if input.rank() < 2 {
306 return Err(format!(
307 "GroupedMatMul input must be rank >= 2 ([.., M, K]), got {input}"
308 ));
309 }
310 if weight.rank() != 3 {
311 return Err(format!(
312 "GroupedMatMul expert bank must be rank 3 ([E, K, N]), got {weight}"
313 ));
314 }
315 let statics = |d: Dim, what: &str| match d {
316 Dim::Static(v) => Ok(v),
317 Dim::Dynamic(s) => Err(format!(
318 "GroupedMatMul {what} must be static, got dynamic symbol {s}"
319 )),
320 };
321 let m = statics(input.dim(input.rank() - 2), "M")?;
322 let k = statics(input.dim(input.rank() - 1), "K")?;
323 let num_experts = statics(weight.dim(0), "E")?;
324 let k_w = statics(weight.dim(1), "the bank's K axis")?;
325 let n = statics(weight.dim(2), "N")?;
326
327 if k != k_w {
328 return Err(format!(
329 "GroupedMatMul K mismatch: input is {input} (K={k}) but the expert bank \
330 is {weight} (K={k_w}, N={n}). The bank must be [E, K, N]; a checkpoint \
331 tensor stored [E, N, K] ([out, in], the usual `x @ W.T` layout) has to \
332 be transposed before it reaches this op"
333 ));
334 }
335 if let Some(out) = out {
336 let out_n = statics(out.dim(out.rank().saturating_sub(1)), "the output's N")?;
337 let out_elems = out.num_elements().unwrap_or(0);
338 if out_n != n || out_elems != m * n {
339 return Err(format!(
340 "GroupedMatMul output shape mismatch: declared {out} but the operands \
341 give [{m}, {n}] (input {input}, bank {weight})"
342 ));
343 }
344 }
345 Ok(GroupedMatMulDims {
346 m,
347 k,
348 n,
349 num_experts,
350 })
351}
352
353pub fn grouped_matmul_shape(input: &Shape, weight: &Shape) -> Result<Shape, String> {
355 let d = grouped_matmul_dims(input, weight, None)?;
356 Ok(Shape::new(&[d.m, d.n], input.dtype()))
357}
358
359pub fn matmul_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
361 if lhs.rank() < 2 || rhs.rank() < 2 {
362 return Err(format!(
363 "matmul requires rank >= 2, got {} and {}",
364 lhs.rank(),
365 rhs.rank()
366 ));
367 }
368 let m = lhs.dims[lhs.rank() - 2];
369 let k1 = lhs.dims[lhs.rank() - 1];
370 let k2 = rhs.dims[rhs.rank() - 2];
371 let n = rhs.dims[rhs.rank() - 1];
372
373 match (k1, k2) {
375 (Dim::Static(a), Dim::Static(b)) if a != b => {
376 return Err(format!("matmul K mismatch: {a} vs {b}"));
377 }
378 (Dim::Dynamic(s), Dim::Dynamic(t)) if s != t => {
379 return Err(format!("matmul K mismatch: ?{s} vs ?{t}"));
380 }
381 _ => {}
382 }
383
384 let lhs_batch = &lhs.dims[..lhs.rank() - 2];
386 let rhs_batch = &rhs.dims[..rhs.rank() - 2];
387 let batch_a = Shape::from_dims(lhs_batch, lhs.dtype);
388 let batch_b = Shape::from_dims(rhs_batch, rhs.dtype);
389 let batch = if lhs_batch.is_empty() && rhs_batch.is_empty() {
390 SmallVec::new()
391 } else if lhs_batch.is_empty() {
392 rhs_batch.into()
393 } else if rhs_batch.is_empty() {
394 lhs_batch.into()
395 } else {
396 broadcast(&batch_a, &batch_b)?.dims.clone()
397 };
398
399 let mut dims = batch;
400 dims.push(m);
401 dims.push(n);
402 Ok(Shape {
403 dims,
404 dtype: lhs.dtype,
405 })
406}
407
408pub fn expand_shape(input: &Shape, target: &[i64]) -> Result<Shape, String> {
410 if target.iter().any(|&d| d < 0) {
411 return Err("expand target has negative dim".into());
412 }
413 let target_s = Shape::new(
414 &target.iter().map(|&d| d as usize).collect::<Vec<_>>(),
415 input.dtype(),
416 );
417 broadcast(input, &target_s)
418}
419
420pub fn binary_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
422 broadcast(lhs, rhs)
423}
424
425pub fn unary_shape(input: &Shape) -> Shape {
427 input.clone()
428}
429
430pub fn cast_shape(input: &Shape, to: DType) -> Shape {
432 input.clone().with_dtype(to)
433}
434
435pub fn compare_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
437 Ok(broadcast(lhs, rhs)?.with_dtype(DType::Bool))
438}
439
440pub fn reduce_shape(input: &Shape, axes: &[usize], keep_dim: bool) -> Result<Shape, String> {
442 let mut dims = SmallVec::new();
443 for (i, &d) in input.dims.iter().enumerate() {
444 if axes.contains(&i) {
445 if keep_dim {
446 dims.push(Dim::Static(1));
447 }
448 } else {
449 dims.push(d);
450 }
451 }
452 Ok(Shape {
453 dims,
454 dtype: input.dtype,
455 })
456}
457
458pub fn softmax_shape(input: &Shape) -> Shape {
460 input.clone()
461}
462
463pub fn transpose_shape(input: &Shape, perm: &[usize]) -> Result<Shape, String> {
465 if perm.len() != input.rank() {
466 return Err(format!("perm len {} != rank {}", perm.len(), input.rank()));
467 }
468 let dims: SmallVec<[Dim; 4]> = perm.iter().map(|&i| input.dims[i]).collect();
469 Ok(Shape {
470 dims,
471 dtype: input.dtype,
472 })
473}
474
475pub fn narrow_shape(input: &Shape, axis: usize, len: usize) -> Result<Shape, String> {
477 if axis >= input.rank() {
478 return Err(format!("axis {axis} >= rank {}", input.rank()));
479 }
480 Ok(input.clone().with_dim(axis, Dim::Static(len)))
481}
482
483pub fn tile_shape(input: &Shape, reps: &[usize]) -> Result<Shape, String> {
485 if reps.len() != input.rank() {
486 return Err(format!(
487 "tile: reps length {} != rank {}",
488 reps.len(),
489 input.rank()
490 ));
491 }
492 let mut out = input.clone();
493 for (axis, &r) in reps.iter().enumerate() {
494 if r == 1 {
495 continue;
496 }
497 match input.dims[axis] {
498 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n * r)),
499 Dim::Dynamic(_) => return Err(format!("tile: cannot tile dynamic axis {axis}")),
500 }
501 }
502 Ok(out)
503}
504
505pub fn slice_shape(input: &Shape, axis: usize, len: usize) -> Result<Shape, String> {
508 if axis >= input.rank() {
509 return Err(format!("slice: axis {axis} >= rank {}", input.rank()));
510 }
511 Ok(input.clone().with_dim(axis, Dim::Static(len)))
512}
513
514pub fn pad_shape(input: &Shape, pads: &[[usize; 2]]) -> Result<Shape, String> {
518 if pads.len() != input.rank() {
519 return Err(format!(
520 "pad: pads length {} != rank {}",
521 pads.len(),
522 input.rank()
523 ));
524 }
525 let mut out = input.clone();
526 for (axis, &[before, after]) in pads.iter().enumerate() {
527 if before == 0 && after == 0 {
528 continue;
529 }
530 match input.dims[axis] {
531 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n + before + after)),
532 Dim::Dynamic(_) => {
533 return Err(format!(
534 "pad: cannot pad dynamic axis {axis} by ({before},{after})"
535 ));
536 }
537 }
538 }
539 Ok(out)
540}
541
542pub fn concat_shape(inputs: &[&Shape], axis: usize) -> Result<Shape, String> {
544 if inputs.is_empty() {
545 return Err("concat: no inputs".into());
546 }
547 let base = inputs[0];
548 let mut static_sum = 0usize;
549 let mut dyn_sym: Option<u32> = None;
550 for s in inputs {
551 if s.rank() == 0 {
552 return Err("concat: input has rank 0".into());
553 }
554 if s.rank() != base.rank() {
555 return Err(format!(
556 "concat: rank mismatch {} vs {}",
557 s.rank(),
558 base.rank()
559 ));
560 }
561 let ax = axis.min(s.rank().saturating_sub(1));
562 match s.dims[ax] {
563 Dim::Static(n) => static_sum += n,
564 Dim::Dynamic(sym) => {
565 if let Some(prev) = dyn_sym {
566 if prev != sym {
567 return Err(format!(
568 "concat: mismatched dynamic symbols {prev} vs {sym} on axis {axis}"
569 ));
570 }
571 }
572 dyn_sym = Some(sym);
573 }
574 }
575 }
576 let out_dim = match dyn_sym {
577 None => Dim::Static(static_sum),
578 Some(sym) if static_sum == 0 => Dim::Dynamic(sym),
579 Some(sym) => {
580 let _ = static_sum;
583 Dim::Dynamic(sym)
584 }
585 };
586 let out_axis = axis.min(base.rank().saturating_sub(1));
587 Ok(base.clone().with_dim(out_axis, out_dim))
588}
589
590pub fn gather_shape(table: &Shape, indices: &Shape, axis: usize) -> Result<Shape, String> {
592 if axis >= table.rank() {
593 return Err(format!("gather: axis {axis} >= rank {}", table.rank()));
594 }
595 let mut dims: SmallVec<[Dim; 4]> = table.dims[..axis].iter().copied().collect();
601 dims.extend(indices.dims.iter().copied());
602 for i in (axis + 1)..table.rank() {
603 dims.push(table.dims[i]);
604 }
605 Ok(Shape {
606 dims,
607 dtype: table.dtype,
608 })
609}
610
611pub fn reshape_shape(input: &Shape, new_shape: &[i64]) -> Result<Shape, String> {
613 let neg_count = new_shape.iter().filter(|&&d| d == -1).count();
614 if neg_count > 1 {
615 return Err("reshape: at most one -1".into());
616 }
617
618 if input.is_static() {
619 let known_product: i64 = new_shape.iter().filter(|&&d| d != -1).product();
626 let mut dims = SmallVec::new();
627 for &d in new_shape {
628 if d == -1 {
629 let total = input
630 .num_elements()
631 .ok_or_else(|| "reshape: input has dynamic dims".to_string())?;
632 let inferred = total as i64 / known_product.max(1);
633 dims.push(Dim::Static(inferred as usize));
634 } else if d < 0 {
635 return Err(format!("reshape: invalid dim {d}"));
636 } else {
637 dims.push(Dim::Static(d as usize));
638 }
639 }
640 return Ok(Shape {
641 dims,
642 dtype: input.dtype,
643 });
644 }
645
646 let dyn_syms = input.dynamic_symbols();
649 let neg_idx = new_shape.iter().position(|&d| d == -1);
650 let mut out_dims: SmallVec<[Dim; 4]> = SmallVec::new();
651 for (i, &d) in new_shape.iter().enumerate() {
652 if Some(i) == neg_idx {
653 continue;
654 }
655 if d < 0 {
656 return Err(format!("reshape: invalid dim {d}"));
657 }
658 out_dims.push(Dim::Static(d as usize));
659 }
660 if let Some(ni) = neg_idx {
661 let inferred = if dyn_syms.len() == 1 {
662 Dim::Dynamic(dyn_syms[0])
663 } else if dyn_syms.is_empty() {
664 return Err("reshape: cannot infer -1 on static input".into());
665 } else {
666 Dim::Dynamic(crate::dynamic::sym::ROWS)
667 };
668 out_dims.insert(ni, inferred);
669 }
670 Ok(Shape {
671 dims: out_dims,
672 dtype: input.dtype,
673 })
674}
675
676pub fn leading_flatten_fused_shape(input: &Shape) -> Option<Shape> {
678 if input.rank() < 2 {
679 return None;
680 }
681 let Dim::Static(h) = input.dim(input.rank() - 1) else {
682 return None;
683 };
684 let leading = &input.dims()[..input.rank() - 1];
685 let lead_dim = if leading.iter().all(|d| d.is_static()) {
686 Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>())
687 } else {
688 let mut syms: Vec<u32> = leading
689 .iter()
690 .filter_map(|d| match d {
691 Dim::Dynamic(s) => Some(*s),
692 _ => None,
693 })
694 .collect();
695 syms.sort();
696 syms.dedup();
697 match syms.len() {
698 0 => Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>()),
699 1 => Dim::Dynamic(syms[0]),
700 _ => Dim::Dynamic(crate::dynamic::sym::ROWS),
701 }
702 };
703 Some(Shape::from_dims(&[lead_dim, Dim::Static(h)], input.dtype()))
704}
705
706pub fn leading_flatten_shape(input: &Shape, new_shape: &[i64]) -> Option<Shape> {
708 if new_shape.len() != 2 {
709 return None;
710 }
711 let flat = leading_flatten_fused_shape(input)?;
712 let Dim::Static(h) = input.dim(input.rank() - 1) else {
713 return None;
714 };
715 if new_shape[1] as usize != h {
716 return None;
717 }
718 match flat.dim(0) {
719 Dim::Static(lead) if new_shape[0] as usize == lead => Some(flat),
720 Dim::Dynamic(_) if new_shape[0] == -1 => Some(flat),
721 _ => None,
722 }
723}
724
725pub fn attention_shape(q: &Shape) -> Shape {
727 q.clone()
728}
729
730pub fn attention_shape_vdim(
735 q: &Shape,
736 num_heads: usize,
737 head_dim: usize,
738 v_head_dim: usize,
739) -> Shape {
740 if head_dim == v_head_dim || q.rank() == 0 {
741 return q.clone();
742 }
743 let rank = q.rank();
744 let new_last = if rank >= 4 {
745 v_head_dim
746 } else {
747 num_heads * v_head_dim
748 };
749 q.clone().with_dim(rank - 1, Dim::Static(new_last))
750}
751
752impl std::fmt::Display for Shape {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 write!(f, "[")?;
755 for (i, d) in self.dims.iter().enumerate() {
756 if i > 0 {
757 write!(f, ", ")?;
758 }
759 write!(f, "{d}")?;
760 }
761 write!(f, "] {}", self.dtype)
762 }
763}
764
765pub fn conv2d_spatial_output(
767 in_size: usize,
768 kernel: usize,
769 stride: usize,
770 padding: usize,
771 dilation: usize,
772) -> usize {
773 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
774 (in_size + 2 * padding)
775 .saturating_sub(dil_k)
776 .saturating_sub(1)
777 / stride
778 + 1
779}
780
781pub fn conv_transpose2d_spatial_output(
783 in_size: usize,
784 kernel: usize,
785 stride: usize,
786 padding: usize,
787 dilation: usize,
788 output_padding: usize,
789) -> usize {
790 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
791 (in_size - 1) * stride + output_padding + dil_k - 2 * padding + 1
792}
793
794pub fn conv2d_output_shape(
796 input: &Shape,
797 weight: &Shape,
798 kernel_size: [usize; 2],
799 stride: [usize; 2],
800 padding: [usize; 2],
801 dilation: [usize; 2],
802 groups: usize,
803) -> Result<Shape, String> {
804 if input.rank() != 4 || weight.rank() != 4 {
805 return Err("conv2d requires NCHW input and 4-D weight".into());
806 }
807 let n = input.dim(0);
808 let c_in = input.dim(1).unwrap_static();
809 let h = input.dim(2).unwrap_static();
810 let w = input.dim(3).unwrap_static();
811 let c_out = weight.dim(0).unwrap_static();
812 let w_cin = weight.dim(1).unwrap_static();
813 if w_cin * groups != c_in {
814 return Err(format!(
815 "conv2d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
816 ));
817 }
818 let h_out = conv2d_spatial_output(h, kernel_size[0], stride[0], padding[0], dilation[0]);
819 let w_out = conv2d_spatial_output(w, kernel_size[1], stride[1], padding[1], dilation[1]);
820 Ok(Shape::from_dims(
821 &[
822 n,
823 Dim::Static(c_out),
824 Dim::Static(h_out),
825 Dim::Static(w_out),
826 ],
827 input.dtype(),
828 ))
829}
830
831pub fn im2col_output_shape(
834 input: &Shape,
835 kernel_size: [usize; 2],
836 stride: [usize; 2],
837 padding: [usize; 2],
838 dilation: [usize; 2],
839) -> Result<Shape, String> {
840 if input.rank() != 4 {
841 return Err("im2col requires NCHW input".into());
842 }
843 let c_in = input.dim(1).unwrap_static();
844 let h = input.dim(2).unwrap_static();
845 let w = input.dim(3).unwrap_static();
846 let kh = kernel_size[0];
847 let kw = kernel_size[1];
848 let h_out = conv2d_spatial_output(h, kh, stride[0], padding[0], dilation[0]);
849 let w_out = conv2d_spatial_output(w, kw, stride[1], padding[1], dilation[1]);
850 let k = c_in * kh * kw;
851 let spatial = h_out * w_out;
852 let m = match input.dim(0) {
853 Dim::Static(n) => Dim::Static(n * spatial),
854 Dim::Dynamic(crate::dynamic::sym::BATCH) | Dim::Dynamic(crate::dynamic::sym::ROWS) => {
855 Dim::Dynamic(crate::dynamic::sym::ROWS)
856 }
857 Dim::Dynamic(_) => Dim::Dynamic(crate::dynamic::sym::ROWS),
858 };
859 Ok(Shape::from_dims(&[m, Dim::Static(k)], input.dtype()))
860}
861
862pub fn conv_transpose2d_output_shape(
864 input: &Shape,
865 weight: &Shape,
866 kernel_size: [usize; 2],
867 stride: [usize; 2],
868 padding: [usize; 2],
869 dilation: [usize; 2],
870 output_padding: [usize; 2],
871 groups: usize,
872) -> Result<Shape, String> {
873 if input.rank() != 4 || weight.rank() != 4 {
874 return Err("conv_transpose2d requires NCHW input and 4-D weight".into());
875 }
876 let n = input.dim(0);
878 let c_in = input.dim(1).unwrap_static();
879 let h = input.dim(2).unwrap_static();
880 let w = input.dim(3).unwrap_static();
881 let w_cin = weight.dim(0).unwrap_static();
882 let c_out_per_g = weight.dim(1).unwrap_static();
883 if w_cin != c_in {
884 return Err(format!(
885 "conv_transpose2d weight C_in={w_cin} != input C={c_in}"
886 ));
887 }
888 let h_out = conv_transpose2d_spatial_output(
889 h,
890 kernel_size[0],
891 stride[0],
892 padding[0],
893 dilation[0],
894 output_padding[0],
895 );
896 let w_out = conv_transpose2d_spatial_output(
897 w,
898 kernel_size[1],
899 stride[1],
900 padding[1],
901 dilation[1],
902 output_padding[1],
903 );
904 Ok(Shape::from_dims(
905 &[
906 n,
907 Dim::Static(c_out_per_g * groups),
908 Dim::Static(h_out),
909 Dim::Static(w_out),
910 ],
911 input.dtype(),
912 ))
913}
914
915#[allow(clippy::too_many_arguments)]
918pub fn conv3d_output_shape(
919 input: &Shape,
920 weight: &Shape,
921 kernel_size: [usize; 3],
922 stride: [usize; 3],
923 padding: [usize; 3],
924 dilation: [usize; 3],
925 groups: usize,
926) -> Result<Shape, String> {
927 if input.rank() != 5 || weight.rank() != 5 {
928 return Err("conv3d requires NCDHW input and 5-D weight".into());
929 }
930 let n = input.dim(0);
931 let c_in = input.dim(1).unwrap_static();
932 let d = input.dim(2).unwrap_static();
933 let h = input.dim(3).unwrap_static();
934 let w = input.dim(4).unwrap_static();
935 let c_out = weight.dim(0).unwrap_static();
936 let w_cin = weight.dim(1).unwrap_static();
937 if w_cin * groups != c_in {
938 return Err(format!(
939 "conv3d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
940 ));
941 }
942 let d_out = conv2d_spatial_output(d, kernel_size[0], stride[0], padding[0], dilation[0]);
943 let h_out = conv2d_spatial_output(h, kernel_size[1], stride[1], padding[1], dilation[1]);
944 let w_out = conv2d_spatial_output(w, kernel_size[2], stride[2], padding[2], dilation[2]);
945 Ok(Shape::from_dims(
946 &[
947 n,
948 Dim::Static(c_out),
949 Dim::Static(d_out),
950 Dim::Static(h_out),
951 Dim::Static(w_out),
952 ],
953 input.dtype(),
954 ))
955}
956
957#[allow(clippy::too_many_arguments)]
959pub fn conv_transpose3d_output_shape(
960 input: &Shape,
961 weight: &Shape,
962 kernel_size: [usize; 3],
963 stride: [usize; 3],
964 padding: [usize; 3],
965 dilation: [usize; 3],
966 output_padding: [usize; 3],
967 groups: usize,
968) -> Result<Shape, String> {
969 if input.rank() != 5 || weight.rank() != 5 {
970 return Err("conv_transpose3d requires NCDHW input and 5-D weight".into());
971 }
972 let n = input.dim(0);
973 let c_in = input.dim(1).unwrap_static();
974 let d = input.dim(2).unwrap_static();
975 let h = input.dim(3).unwrap_static();
976 let w = input.dim(4).unwrap_static();
977 let w_cin = weight.dim(0).unwrap_static();
978 let c_out_per_g = weight.dim(1).unwrap_static();
979 if w_cin != c_in {
980 return Err(format!(
981 "conv_transpose3d weight C_in={w_cin} != input C={c_in}"
982 ));
983 }
984 let d_out = conv_transpose2d_spatial_output(
985 d,
986 kernel_size[0],
987 stride[0],
988 padding[0],
989 dilation[0],
990 output_padding[0],
991 );
992 let h_out = conv_transpose2d_spatial_output(
993 h,
994 kernel_size[1],
995 stride[1],
996 padding[1],
997 dilation[1],
998 output_padding[1],
999 );
1000 let w_out = conv_transpose2d_spatial_output(
1001 w,
1002 kernel_size[2],
1003 stride[2],
1004 padding[2],
1005 dilation[2],
1006 output_padding[2],
1007 );
1008 Ok(Shape::from_dims(
1009 &[
1010 n,
1011 Dim::Static(c_out_per_g * groups),
1012 Dim::Static(d_out),
1013 Dim::Static(h_out),
1014 Dim::Static(w_out),
1015 ],
1016 input.dtype(),
1017 ))
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn static_shape() {
1026 let s = Shape::new(&[4, 15, 384], DType::F32);
1027 assert_eq!(s.rank(), 3);
1028 assert_eq!(s.num_elements(), Some(4 * 15 * 384));
1029 assert_eq!(s.size_bytes(), Some(4 * 15 * 384 * 4));
1030 assert!(s.is_static());
1031 assert_eq!(format!("{s}"), "[4, 15, 384] f32");
1032 }
1033
1034 #[test]
1037 fn grouped_matmul_derives_m_and_n() {
1038 let input = Shape::new(&[5, 6], DType::F32);
1039 let bank = Shape::new(&[8, 6, 12], DType::F32); let d = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[5, 12], DType::F32)))
1041 .expect("valid");
1042 assert_eq!(
1043 d,
1044 GroupedMatMulDims {
1045 m: 5,
1046 k: 6,
1047 n: 12,
1048 num_experts: 8
1049 }
1050 );
1051 assert_eq!(
1052 grouped_matmul_shape(&input, &bank).unwrap(),
1053 Shape::new(&[5, 12], DType::F32)
1054 );
1055 }
1056
1057 #[test]
1060 fn grouped_matmul_rejects_an_untransposed_bank() {
1061 let input = Shape::new(&[5, 6], DType::F32);
1062 let bank = Shape::new(&[8, 12, 6], DType::F32); let err = grouped_matmul_dims(&input, &bank, None).unwrap_err();
1064 assert!(err.contains("K mismatch"), "unhelpful: {err}");
1065 assert!(err.contains("[E, N, K]"), "should name the cause: {err}");
1066 }
1067
1068 #[test]
1072 fn grouped_matmul_rejects_a_square_bank_by_output_shape() {
1073 let input = Shape::new(&[4, 12, 12], DType::F32);
1074 let bank = Shape::new(&[8, 12, 12], DType::F32);
1075 assert!(grouped_matmul_dims(&input, &bank, None).is_ok());
1077 let err = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[12, 6], DType::F32)))
1079 .unwrap_err();
1080 assert!(err.contains("output shape mismatch"), "unhelpful: {err}");
1081 }
1082
1083 #[test]
1084 fn grouped_matmul_rejects_a_non_bank_weight() {
1085 let input = Shape::new(&[5, 6], DType::F32);
1086 let err = grouped_matmul_dims(&input, &Shape::new(&[6, 12], DType::F32), None).unwrap_err();
1087 assert!(err.contains("rank 3"), "unhelpful: {err}");
1088 }
1089
1090 #[test]
1093 fn broadcast_same() {
1094 let a = Shape::new(&[4, 15, 384], DType::F32);
1095 let r = broadcast(&a, &a).unwrap();
1096 assert_eq!(r.dims(), a.dims());
1097 }
1098
1099 #[test]
1100 fn broadcast_bias() {
1101 let a = Shape::new(&[4, 15, 384], DType::F32);
1102 let b = Shape::new(&[384], DType::F32);
1103 let r = broadcast(&a, &b).unwrap();
1104 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1105 }
1106
1107 #[test]
1108 fn broadcast_scalar() {
1109 let a = Shape::new(&[4, 15, 384], DType::F32);
1110 let b = Shape::scalar(DType::F32);
1111 let r = broadcast(&a, &b).unwrap();
1112 assert_eq!(r, a);
1113 }
1114
1115 #[test]
1116 fn broadcast_mismatch() {
1117 let a = Shape::new(&[4, 15, 384], DType::F32);
1118 let b = Shape::new(&[4, 15, 256], DType::F32);
1119 assert!(broadcast(&a, &b).is_err());
1120 }
1121
1122 #[test]
1123 fn matmul_basic() {
1124 let a = Shape::new(&[4, 15, 384], DType::F32);
1125 let b = Shape::new(&[384, 1536], DType::F32);
1126 let r = matmul_shape(&a, &b).unwrap();
1127 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1128 }
1129
1130 #[test]
1131 fn matmul_batched() {
1132 let a = Shape::new(&[4, 15, 384], DType::F32);
1133 let b = Shape::new(&[4, 384, 1536], DType::F32);
1134 let r = matmul_shape(&a, &b).unwrap();
1135 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1136 }
1137
1138 #[test]
1139 fn matmul_k_mismatch() {
1140 let a = Shape::new(&[4, 15, 384], DType::F32);
1141 let b = Shape::new(&[512, 1536], DType::F32);
1142 assert!(matmul_shape(&a, &b).is_err());
1143 }
1144
1145 #[test]
1146 fn reduce_keepdim() {
1147 let a = Shape::new(&[4, 15, 384], DType::F32);
1148 let r = reduce_shape(&a, &[2], true).unwrap();
1149 assert_eq!(r, Shape::new(&[4, 15, 1], DType::F32));
1150 }
1151
1152 #[test]
1153 fn reduce_no_keepdim() {
1154 let a = Shape::new(&[4, 15, 384], DType::F32);
1155 let r = reduce_shape(&a, &[2], false).unwrap();
1156 assert_eq!(r, Shape::new(&[4, 15], DType::F32));
1157 }
1158
1159 #[test]
1160 fn concat_basic() {
1161 let a = Shape::new(&[4, 15, 384], DType::F32);
1162 let b = Shape::new(&[4, 15, 384], DType::F32);
1163 let r = concat_shape(&[&a, &b], 2).unwrap();
1164 assert_eq!(r, Shape::new(&[4, 15, 768], DType::F32));
1165 }
1166
1167 #[test]
1168 fn gather_embedding() {
1169 let table = Shape::new(&[30522, 384], DType::F32);
1170 let indices = Shape::new(&[4, 15], DType::I64);
1171 let r = gather_shape(&table, &indices, 0).unwrap();
1172 assert_eq!(
1173 r,
1174 Shape::from_dims(
1175 &[Dim::Static(4), Dim::Static(15), Dim::Static(384)],
1176 DType::F32
1177 )
1178 );
1179 }
1180
1181 #[test]
1182 fn gather_nonzero_axis_keeps_prefix() {
1183 let table = Shape::new(&[4, 3], DType::F32);
1187 let indices = Shape::new(&[6], DType::I64);
1188 let r = gather_shape(&table, &indices, 1).unwrap();
1189 assert_eq!(r, Shape::new(&[4, 6], DType::F32));
1190 let table = Shape::new(&[2, 5, 7], DType::F32);
1192 let indices = Shape::new(&[3, 4], DType::I64);
1193 let r = gather_shape(&table, &indices, 1).unwrap();
1194 assert_eq!(r, Shape::new(&[2, 3, 4, 7], DType::F32));
1195 }
1196
1197 #[test]
1198 fn reshape_with_neg1() {
1199 let a = Shape::new(&[4, 15, 384], DType::F32);
1200 let r = reshape_shape(&a, &[60, -1]).unwrap();
1201 assert_eq!(r, Shape::new(&[60, 384], DType::F32));
1202 }
1203
1204 #[test]
1205 fn transpose_basic() {
1206 let a = Shape::new(&[4, 15, 384], DType::F32);
1207 let r = transpose_shape(&a, &[0, 2, 1]).unwrap();
1208 assert_eq!(r, Shape::new(&[4, 384, 15], DType::F32));
1209 }
1210
1211 #[test]
1212 fn narrow_basic() {
1213 let a = Shape::new(&[4, 15, 1152], DType::F32);
1214 let r = narrow_shape(&a, 2, 384).unwrap();
1215 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1216 }
1217
1218 #[test]
1219 fn compare_bool_output() {
1220 let a = Shape::new(&[4, 15], DType::F32);
1221 let b = Shape::new(&[4, 15], DType::F32);
1222 let r = compare_shape(&a, &b).unwrap();
1223 assert_eq!(r.dtype(), DType::Bool);
1224 assert_eq!(r.rank(), 2);
1225 }
1226
1227 #[test]
1230 fn dynamic_shape() {
1231 let s = Shape::from_dims(
1232 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1233 DType::F32,
1234 );
1235 assert_eq!(s.rank(), 3);
1236 assert_eq!(s.num_elements(), None);
1237 assert!(!s.is_static());
1238 assert_eq!(format!("{s}"), "[?0, ?1, 384] f32");
1239 }
1240
1241 #[test]
1242 fn dynamic_symbols_lists_distinct_dims() {
1243 let s = Shape::from_dims(
1244 &[
1245 Dim::Dynamic(1),
1246 Dim::Static(384),
1247 Dim::Dynamic(0),
1248 Dim::Dynamic(1),
1249 ],
1250 DType::F32,
1251 );
1252 assert_eq!(s.dynamic_symbols(), vec![0, 1]);
1253 }
1254
1255 #[test]
1256 fn bind_specializes_known_symbols() {
1257 let s = Shape::from_dims(
1258 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1259 DType::F32,
1260 );
1261 let mut b = DimBinding::new();
1262 b.set(0, 8);
1263 b.set(1, 64);
1264 let s2 = s.bind(&b);
1265 assert!(s2.is_static());
1266 assert_eq!(s2.num_elements(), Some(8 * 64 * 384));
1267 }
1268
1269 #[test]
1270 fn bind_leaves_unknown_symbols_alone() {
1271 let s = Shape::from_dims(&[Dim::Dynamic(0), Dim::Dynamic(99)], DType::F32);
1272 let mut b = DimBinding::new();
1273 b.set(0, 4);
1274 let s2 = s.bind(&b);
1275 assert!(!s2.is_static()); assert_eq!(s2.dynamic_symbols(), vec![99]);
1277 }
1278}
1279
1280#[derive(Debug, Clone, Default)]
1283pub struct DimBinding {
1284 map: std::collections::HashMap<u32, usize>,
1285}
1286
1287impl DimBinding {
1288 pub fn new() -> Self {
1289 Self::default()
1290 }
1291 pub fn set(&mut self, symbol: u32, size: usize) -> Option<usize> {
1292 self.map.insert(symbol, size)
1293 }
1294 pub fn get(&self, symbol: u32) -> Option<usize> {
1295 self.map.get(&symbol).copied()
1296 }
1297 pub fn is_empty(&self) -> bool {
1298 self.map.is_empty()
1299 }
1300 pub fn len(&self) -> usize {
1301 self.map.len()
1302 }
1303 pub fn iter(&self) -> impl Iterator<Item = (u32, usize)> + '_ {
1304 self.map.iter().map(|(&s, &n)| (s, n))
1305 }
1306}