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 gather_index_count(&self, axis: usize) -> usize {
124 if self.rank() > axis {
125 self.dim(axis).unwrap_static()
126 } else {
127 self.dims
128 .iter()
129 .map(|d| d.unwrap_static())
130 .product::<usize>()
131 .max(1)
132 }
133 }
134
135 pub fn dynamic_symbols(&self) -> Vec<u32> {
138 let mut syms: Vec<u32> = self
139 .dims
140 .iter()
141 .filter_map(|d| match d {
142 Dim::Dynamic(s) => Some(*s),
143 _ => None,
144 })
145 .collect();
146 syms.sort();
147 syms.dedup();
148 syms
149 }
150
151 pub fn bind(&self, bindings: &DimBinding) -> Self {
156 let dims = self
157 .dims
158 .iter()
159 .map(|d| match d {
160 Dim::Dynamic(s) => match bindings.get(*s) {
161 Some(n) => Dim::Static(n),
162 None => *d,
163 },
164 _ => *d,
165 })
166 .collect();
167 Self {
168 dims,
169 dtype: self.dtype,
170 }
171 }
172
173 pub fn num_elements(&self) -> Option<usize> {
175 let mut total = 1usize;
176 for d in &self.dims {
177 match d {
178 Dim::Static(n) => total = total.checked_mul(*n)?,
179 Dim::Dynamic(_) => return None,
180 }
181 }
182 Some(total)
183 }
184
185 pub fn size_bytes(&self) -> Option<usize> {
187 self.num_elements().map(|n| n * self.dtype.size_bytes())
188 }
189
190 pub fn is_static(&self) -> bool {
192 self.dims.iter().all(|d| d.is_static())
193 }
194
195 pub fn with_dim(mut self, axis: usize, dim: Dim) -> Self {
197 self.dims[axis] = dim;
198 self
199 }
200
201 pub fn with_dtype(mut self, dtype: DType) -> Self {
203 self.dtype = dtype;
204 self
205 }
206
207 pub fn broadcast_with(&self, other: &Shape) -> Result<Shape, String> {
209 broadcast(self, other)
210 }
211}
212
213pub fn ada_modulation_lead_pack(x_dims: &[usize], mod_dims_in: &[usize]) -> [u32; 17] {
221 let rank = x_dims.len();
222 assert!(rank >= 1, "AdaLayerNorm/GatedResidual: rank ≥ 1");
223 let lead = rank - 1;
224 assert!(
225 lead <= 8,
226 "AdaLayerNorm/GatedResidual: at most 8 leading dims"
227 );
228 let pad = rank
229 .checked_sub(mod_dims_in.len())
230 .expect("modulation rank exceeds x");
231 let mut mod_dims = vec![1usize; rank];
232 mod_dims[pad..].copy_from_slice(mod_dims_in);
233 let mut pack = [0u32; 17];
234 pack[0] = lead as u32;
235 for j in 0..lead {
236 pack[1 + j] = x_dims[j] as u32;
237 pack[9 + j] = mod_dims[j] as u32;
238 }
239 pack
240}
241
242pub fn ada_modulation_launch(x_dims: &[usize], mod_dims: &[usize]) -> (u32, u32) {
246 assert!(!x_dims.is_empty() && !mod_dims.is_empty());
247 let xr = x_dims.len() - 1;
248 let mr = mod_dims.len() - 1;
249 let mut seq = 1u32;
250 let mut mods = 1u32;
251 for i in 0..xr {
252 let xd = x_dims[i] as u32;
253 let md = if i + mr >= xr {
254 mod_dims[i - (xr - mr)] as u32
255 } else {
256 1
257 };
258 if md == 1 && xd > 1 {
259 seq = seq.saturating_mul(xd);
260 } else {
261 mods = mods.saturating_mul(xd.max(1));
262 }
263 }
264 (mods.max(1), seq.max(1))
265}
266
267pub fn broadcast(a: &Shape, b: &Shape) -> Result<Shape, String> {
271 let max_rank = a.rank().max(b.rank());
272 let mut dims = SmallVec::new();
273 for i in 0..max_rank {
274 let ad = if i < max_rank - a.rank() {
275 Dim::Static(1)
276 } else {
277 a.dims[i - (max_rank - a.rank())]
278 };
279 let bd = if i < max_rank - b.rank() {
280 Dim::Static(1)
281 } else {
282 b.dims[i - (max_rank - b.rank())]
283 };
284 let d = broadcast_dim(ad, bd)?;
285 dims.push(d);
286 }
287 Ok(Shape {
288 dims,
289 dtype: a.dtype,
290 })
291}
292
293fn broadcast_dim(a: Dim, b: Dim) -> Result<Dim, String> {
294 match (a, b) {
295 (Dim::Static(1), d) | (d, Dim::Static(1)) => Ok(d),
296 (Dim::Static(x), Dim::Static(y)) if x == y => Ok(Dim::Static(x)),
297 (Dim::Static(x), Dim::Static(y)) => Err(format!("cannot broadcast {x} with {y}")),
298 (Dim::Dynamic(s), Dim::Dynamic(t)) if s == t => Ok(Dim::Dynamic(s)),
299 (Dim::Dynamic(_), _) | (_, Dim::Dynamic(_)) => Ok(a), }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct GroupedMatMulDims {
307 pub m: usize,
308 pub k: usize,
309 pub n: usize,
310 pub num_experts: usize,
311}
312
313pub fn grouped_matmul_dims(
329 input: &Shape,
330 weight: &Shape,
331 out: Option<&Shape>,
332) -> Result<GroupedMatMulDims, String> {
333 if input.rank() < 2 {
334 return Err(format!(
335 "GroupedMatMul input must be rank >= 2 ([.., M, K]), got {input}"
336 ));
337 }
338 if weight.rank() != 3 {
339 return Err(format!(
340 "GroupedMatMul expert bank must be rank 3 ([E, K, N]), got {weight}"
341 ));
342 }
343 let statics = |d: Dim, what: &str| match d {
344 Dim::Static(v) => Ok(v),
345 Dim::Dynamic(s) => Err(format!(
346 "GroupedMatMul {what} must be static, got dynamic symbol {s}"
347 )),
348 };
349 let m = statics(input.dim(input.rank() - 2), "M")?;
350 let k = statics(input.dim(input.rank() - 1), "K")?;
351 let num_experts = statics(weight.dim(0), "E")?;
352 let k_w = statics(weight.dim(1), "the bank's K axis")?;
353 let n = statics(weight.dim(2), "N")?;
354
355 if k != k_w {
356 return Err(format!(
357 "GroupedMatMul K mismatch: input is {input} (K={k}) but the expert bank \
358 is {weight} (K={k_w}, N={n}). The bank must be [E, K, N]; a checkpoint \
359 tensor stored [E, N, K] ([out, in], the usual `x @ W.T` layout) has to \
360 be transposed before it reaches this op"
361 ));
362 }
363 if let Some(out) = out {
364 let out_n = statics(out.dim(out.rank().saturating_sub(1)), "the output's N")?;
365 let out_elems = out.num_elements().unwrap_or(0);
366 if out_n != n || out_elems != m * n {
367 return Err(format!(
368 "GroupedMatMul output shape mismatch: declared {out} but the operands \
369 give [{m}, {n}] (input {input}, bank {weight})"
370 ));
371 }
372 }
373 Ok(GroupedMatMulDims {
374 m,
375 k,
376 n,
377 num_experts,
378 })
379}
380
381pub fn grouped_matmul_shape(input: &Shape, weight: &Shape) -> Result<Shape, String> {
383 let d = grouped_matmul_dims(input, weight, None)?;
384 Ok(Shape::new(&[d.m, d.n], input.dtype()))
385}
386
387pub fn matmul_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
389 if lhs.rank() < 2 || rhs.rank() < 2 {
390 return Err(format!(
391 "matmul requires rank >= 2, got {} and {}",
392 lhs.rank(),
393 rhs.rank()
394 ));
395 }
396 let m = lhs.dims[lhs.rank() - 2];
397 let k1 = lhs.dims[lhs.rank() - 1];
398 let k2 = rhs.dims[rhs.rank() - 2];
399 let n = rhs.dims[rhs.rank() - 1];
400
401 match (k1, k2) {
403 (Dim::Static(a), Dim::Static(b)) if a != b => {
404 return Err(format!("matmul K mismatch: {a} vs {b}"));
405 }
406 (Dim::Dynamic(s), Dim::Dynamic(t)) if s != t => {
407 return Err(format!("matmul K mismatch: ?{s} vs ?{t}"));
408 }
409 _ => {}
410 }
411
412 let lhs_batch = &lhs.dims[..lhs.rank() - 2];
414 let rhs_batch = &rhs.dims[..rhs.rank() - 2];
415 let batch_a = Shape::from_dims(lhs_batch, lhs.dtype);
416 let batch_b = Shape::from_dims(rhs_batch, rhs.dtype);
417 let batch = if lhs_batch.is_empty() && rhs_batch.is_empty() {
418 SmallVec::new()
419 } else if lhs_batch.is_empty() {
420 rhs_batch.into()
421 } else if rhs_batch.is_empty() {
422 lhs_batch.into()
423 } else {
424 broadcast(&batch_a, &batch_b)?.dims.clone()
425 };
426
427 let mut dims = batch;
428 dims.push(m);
429 dims.push(n);
430 Ok(Shape {
431 dims,
432 dtype: lhs.dtype,
433 })
434}
435
436pub fn dequant_matmul_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
449 if lhs.rank() < 2 {
450 return Err(format!(
451 "dequant matmul activations require rank >= 2, got {}",
452 lhs.rank()
453 ));
454 }
455 if rhs.rank() != 2 {
456 return Err(format!(
457 "dequant matmul weight must be rank 2 `[n, k]`, got rank {}",
458 rhs.rank()
459 ));
460 }
461 let m = lhs.dims[lhs.rank() - 2];
462 let k_lhs = lhs.dims[lhs.rank() - 1];
463 let n = rhs.dims[0];
464 let k_rhs = rhs.dims[1];
465
466 match (k_lhs, k_rhs) {
467 (Dim::Static(a), Dim::Static(b)) if a != b => {
468 return Err(format!(
469 "dequant matmul K mismatch: {a} vs {b} — the GGUF weight is \
470 `[n, k]`, so K is its last axis, not its first"
471 ));
472 }
473 (Dim::Dynamic(s), Dim::Dynamic(t)) if s != t => {
474 return Err(format!("dequant matmul K mismatch: ?{s} vs ?{t}"));
475 }
476 _ => {}
477 }
478
479 let mut dims: SmallVec<[Dim; 4]> = lhs.dims[..lhs.rank() - 2].into();
480 dims.push(m);
481 dims.push(n);
482 Ok(Shape {
483 dims,
484 dtype: lhs.dtype,
485 })
486}
487
488pub fn expand_shape(input: &Shape, target: &[i64]) -> Result<Shape, String> {
490 if target.iter().any(|&d| d < 0) {
491 return Err("expand target has negative dim".into());
492 }
493 let target_s = Shape::new(
494 &target.iter().map(|&d| d as usize).collect::<Vec<_>>(),
495 input.dtype(),
496 );
497 broadcast(input, &target_s)
498}
499
500pub fn binary_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
502 broadcast(lhs, rhs)
503}
504
505pub fn unary_shape(input: &Shape) -> Shape {
507 input.clone()
508}
509
510pub fn cast_shape(input: &Shape, to: DType) -> Shape {
512 input.clone().with_dtype(to)
513}
514
515pub fn compare_shape(lhs: &Shape, rhs: &Shape) -> Result<Shape, String> {
517 Ok(broadcast(lhs, rhs)?.with_dtype(DType::Bool))
518}
519
520pub fn reduce_shape(input: &Shape, axes: &[usize], keep_dim: bool) -> Result<Shape, String> {
522 let mut dims = SmallVec::new();
523 for (i, &d) in input.dims.iter().enumerate() {
524 if axes.contains(&i) {
525 if keep_dim {
526 dims.push(Dim::Static(1));
527 }
528 } else {
529 dims.push(d);
530 }
531 }
532 Ok(Shape {
533 dims,
534 dtype: input.dtype,
535 })
536}
537
538pub fn softmax_shape(input: &Shape) -> Shape {
540 input.clone()
541}
542
543pub fn transpose_shape(input: &Shape, perm: &[usize]) -> Result<Shape, String> {
545 if perm.len() != input.rank() {
546 return Err(format!("perm len {} != rank {}", perm.len(), input.rank()));
547 }
548 let dims: SmallVec<[Dim; 4]> = perm.iter().map(|&i| input.dims[i]).collect();
549 Ok(Shape {
550 dims,
551 dtype: input.dtype,
552 })
553}
554
555pub fn narrow_shape(input: &Shape, axis: usize, len: usize) -> Result<Shape, String> {
557 narrow_shape_at(input, axis, 0, len)
558}
559
560pub fn narrow_shape_at(
567 input: &Shape,
568 axis: usize,
569 start: usize,
570 len: usize,
571) -> Result<Shape, String> {
572 if axis >= input.rank() {
573 return Err(format!("axis {axis} >= rank {}", input.rank()));
574 }
575 if let Dim::Static(extent) = input.dim(axis) {
577 if start + len > extent {
578 return Err(format!(
579 "narrow: window start {start} + len {len} exceeds axis {axis} extent {extent}"
580 ));
581 }
582 }
583 Ok(input.clone().with_dim(axis, Dim::Static(len)))
584}
585
586pub fn tile_shape(input: &Shape, reps: &[usize]) -> Result<Shape, String> {
588 if reps.len() != input.rank() {
589 return Err(format!(
590 "tile: reps length {} != rank {}",
591 reps.len(),
592 input.rank()
593 ));
594 }
595 let mut out = input.clone();
596 for (axis, &r) in reps.iter().enumerate() {
597 if r == 1 {
598 continue;
599 }
600 match input.dims[axis] {
601 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n * r)),
602 Dim::Dynamic(_) => return Err(format!("tile: cannot tile dynamic axis {axis}")),
603 }
604 }
605 Ok(out)
606}
607
608pub fn slice_shape(input: &Shape, axis: usize, len: usize) -> Result<Shape, String> {
611 if axis >= input.rank() {
612 return Err(format!("slice: axis {axis} >= rank {}", input.rank()));
613 }
614 Ok(input.clone().with_dim(axis, Dim::Static(len)))
615}
616
617pub fn pad_shape(input: &Shape, pads: &[[usize; 2]]) -> Result<Shape, String> {
621 if pads.len() != input.rank() {
622 return Err(format!(
623 "pad: pads length {} != rank {}",
624 pads.len(),
625 input.rank()
626 ));
627 }
628 let mut out = input.clone();
629 for (axis, &[before, after]) in pads.iter().enumerate() {
630 if before == 0 && after == 0 {
631 continue;
632 }
633 match input.dims[axis] {
634 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n + before + after)),
635 Dim::Dynamic(_) => {
636 return Err(format!(
637 "pad: cannot pad dynamic axis {axis} by ({before},{after})"
638 ));
639 }
640 }
641 }
642 Ok(out)
643}
644
645pub fn concat_shape(inputs: &[&Shape], axis: usize) -> Result<Shape, String> {
647 if inputs.is_empty() {
648 return Err("concat: no inputs".into());
649 }
650 let base = inputs[0];
651 let mut static_sum = 0usize;
652 let mut dyn_sym: Option<u32> = None;
653 for s in inputs {
654 if s.rank() == 0 {
655 return Err("concat: input has rank 0".into());
656 }
657 if s.rank() != base.rank() {
658 return Err(format!(
659 "concat: rank mismatch {} vs {}",
660 s.rank(),
661 base.rank()
662 ));
663 }
664 let ax = axis.min(s.rank().saturating_sub(1));
665 for d in 0..s.rank() {
674 if d == ax {
675 continue;
676 }
677 if let (Dim::Static(a), Dim::Static(b)) = (s.dims[d], base.dims[d])
678 && a != b
679 {
680 return Err(format!(
681 "concat: axis {d} mismatch {a} vs {b} (concat axis is {axis}); all axes but the concat axis must match"
682 ));
683 }
684 }
685 match s.dims[ax] {
686 Dim::Static(n) => static_sum += n,
687 Dim::Dynamic(sym) => {
688 if let Some(prev) = dyn_sym {
689 if prev != sym {
690 return Err(format!(
691 "concat: mismatched dynamic symbols {prev} vs {sym} on axis {axis}"
692 ));
693 }
694 }
695 dyn_sym = Some(sym);
696 }
697 }
698 }
699 let out_dim = match dyn_sym {
700 None => Dim::Static(static_sum),
701 Some(sym) if static_sum == 0 => Dim::Dynamic(sym),
702 Some(sym) => {
703 let _ = static_sum;
706 Dim::Dynamic(sym)
707 }
708 };
709 let out_axis = axis.min(base.rank().saturating_sub(1));
710 Ok(base.clone().with_dim(out_axis, out_dim))
711}
712
713pub fn gather_shape(table: &Shape, indices: &Shape, axis: usize) -> Result<Shape, String> {
715 if axis >= table.rank() {
716 return Err(format!("gather: axis {axis} >= rank {}", table.rank()));
717 }
718 let mut dims: SmallVec<[Dim; 4]> = table.dims[..axis].iter().copied().collect();
724 dims.extend(indices.dims.iter().copied());
725 for i in (axis + 1)..table.rank() {
726 dims.push(table.dims[i]);
727 }
728 Ok(Shape {
729 dims,
730 dtype: table.dtype,
731 })
732}
733
734pub fn reshape_shape(input: &Shape, new_shape: &[i64]) -> Result<Shape, String> {
736 let neg_count = new_shape.iter().filter(|&&d| d == -1).count();
737 if neg_count > 1 {
738 return Err("reshape: at most one -1".into());
739 }
740
741 if input.is_static() {
742 let known_product: i64 = new_shape.iter().filter(|&&d| d != -1).product();
749 let mut dims = SmallVec::new();
750 for &d in new_shape {
751 if d == -1 {
752 let total = input
753 .num_elements()
754 .ok_or_else(|| "reshape: input has dynamic dims".to_string())?;
755 let inferred = total as i64 / known_product.max(1);
756 dims.push(Dim::Static(inferred as usize));
757 } else if d < 0 {
758 return Err(format!("reshape: invalid dim {d}"));
759 } else {
760 dims.push(Dim::Static(d as usize));
761 }
762 }
763 if let Some(total) = input.num_elements() {
775 let out_total: usize = dims
776 .iter()
777 .map(|d: &Dim| d.unwrap_static())
778 .try_fold(1usize, |a, d| a.checked_mul(d))
779 .unwrap_or(usize::MAX);
780 if out_total != total {
781 return Err(format!(
782 "reshape: {:?} ({total} elements) -> {new_shape:?} ({out_total} elements); \
783 a reshape must preserve the element count",
784 input.dims()
785 ));
786 }
787 }
788
789 return Ok(Shape {
790 dims,
791 dtype: input.dtype,
792 });
793 }
794
795 let dyn_syms = input.dynamic_symbols();
798 let neg_idx = new_shape.iter().position(|&d| d == -1);
799 let mut out_dims: SmallVec<[Dim; 4]> = SmallVec::new();
800 for (i, &d) in new_shape.iter().enumerate() {
801 if Some(i) == neg_idx {
802 continue;
803 }
804 if d < 0 {
805 return Err(format!("reshape: invalid dim {d}"));
806 }
807 out_dims.push(Dim::Static(d as usize));
808 }
809 if let Some(ni) = neg_idx {
810 let inferred = if dyn_syms.len() == 1 {
811 Dim::Dynamic(dyn_syms[0])
812 } else if dyn_syms.is_empty() {
813 return Err("reshape: cannot infer -1 on static input".into());
814 } else {
815 Dim::Dynamic(crate::dynamic::sym::ROWS)
816 };
817 out_dims.insert(ni, inferred);
818 }
819 Ok(Shape {
820 dims: out_dims,
821 dtype: input.dtype,
822 })
823}
824
825pub fn leading_flatten_fused_shape(input: &Shape) -> Option<Shape> {
827 if input.rank() < 2 {
828 return None;
829 }
830 let Dim::Static(h) = input.dim(input.rank() - 1) else {
831 return None;
832 };
833 let leading = &input.dims()[..input.rank() - 1];
834 let lead_dim = if leading.iter().all(|d| d.is_static()) {
835 Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>())
836 } else {
837 let mut syms: Vec<u32> = leading
838 .iter()
839 .filter_map(|d| match d {
840 Dim::Dynamic(s) => Some(*s),
841 _ => None,
842 })
843 .collect();
844 syms.sort();
845 syms.dedup();
846 match syms.len() {
847 0 => Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>()),
848 1 => Dim::Dynamic(syms[0]),
849 _ => Dim::Dynamic(crate::dynamic::sym::ROWS),
850 }
851 };
852 Some(Shape::from_dims(&[lead_dim, Dim::Static(h)], input.dtype()))
853}
854
855pub fn leading_flatten_shape(input: &Shape, new_shape: &[i64]) -> Option<Shape> {
857 if new_shape.len() != 2 {
858 return None;
859 }
860 let flat = leading_flatten_fused_shape(input)?;
861 let Dim::Static(h) = input.dim(input.rank() - 1) else {
862 return None;
863 };
864 if new_shape[1] as usize != h {
865 return None;
866 }
867 match flat.dim(0) {
868 Dim::Static(lead) if new_shape[0] as usize == lead => Some(flat),
869 Dim::Dynamic(_) if new_shape[0] == -1 => Some(flat),
870 _ => None,
871 }
872}
873
874pub fn attention_shape(q: &Shape) -> Shape {
876 q.clone()
877}
878
879pub fn attention_shape_vdim(
884 q: &Shape,
885 num_heads: usize,
886 head_dim: usize,
887 v_head_dim: usize,
888) -> Shape {
889 if head_dim == v_head_dim || q.rank() == 0 {
890 return q.clone();
891 }
892 let rank = q.rank();
893 let new_last = if rank >= 4 {
894 v_head_dim
895 } else {
896 num_heads * v_head_dim
897 };
898 q.clone().with_dim(rank - 1, Dim::Static(new_last))
899}
900
901impl std::fmt::Display for Shape {
902 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
903 write!(f, "[")?;
904 for (i, d) in self.dims.iter().enumerate() {
905 if i > 0 {
906 write!(f, ", ")?;
907 }
908 write!(f, "{d}")?;
909 }
910 write!(f, "] {}", self.dtype)
911 }
912}
913
914pub fn conv2d_spatial_output(
916 in_size: usize,
917 kernel: usize,
918 stride: usize,
919 padding: usize,
920 dilation: usize,
921) -> usize {
922 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
923 (in_size + 2 * padding)
924 .saturating_sub(dil_k)
925 .saturating_sub(1)
926 / stride
927 + 1
928}
929
930pub fn conv_transpose2d_spatial_output(
932 in_size: usize,
933 kernel: usize,
934 stride: usize,
935 padding: usize,
936 dilation: usize,
937 output_padding: usize,
938) -> usize {
939 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
940 (in_size - 1) * stride + output_padding + dil_k - 2 * padding + 1
941}
942
943pub fn conv2d_output_shape(
945 input: &Shape,
946 weight: &Shape,
947 kernel_size: [usize; 2],
948 stride: [usize; 2],
949 padding: [usize; 2],
950 dilation: [usize; 2],
951 groups: usize,
952) -> Result<Shape, String> {
953 if input.rank() != 4 || weight.rank() != 4 {
954 return Err("conv2d requires NCHW input and 4-D weight".into());
955 }
956 let n = input.dim(0);
957 let c_in = input.dim(1).unwrap_static();
958 let h = input.dim(2).unwrap_static();
959 let w = input.dim(3).unwrap_static();
960 let c_out = weight.dim(0).unwrap_static();
961 let w_cin = weight.dim(1).unwrap_static();
962 if w_cin * groups != c_in {
963 return Err(format!(
964 "conv2d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
965 ));
966 }
967 let h_out = conv2d_spatial_output(h, kernel_size[0], stride[0], padding[0], dilation[0]);
968 let w_out = conv2d_spatial_output(w, kernel_size[1], stride[1], padding[1], dilation[1]);
969 Ok(Shape::from_dims(
970 &[
971 n,
972 Dim::Static(c_out),
973 Dim::Static(h_out),
974 Dim::Static(w_out),
975 ],
976 input.dtype(),
977 ))
978}
979
980pub fn rope_table_stride(cos: &Shape, n_rot: usize) -> usize {
996 if cos.rank() >= 2 {
997 cos.dims()
998 .last()
999 .map_or_else(|| n_rot / 2, |d| d.unwrap_static())
1000 .max(1)
1001 } else {
1002 (n_rot / 2).max(1)
1003 }
1004}
1005
1006pub fn pool2d_output_shape(
1011 input: &Shape,
1012 kernel_size: [usize; 2],
1013 stride: [usize; 2],
1014 padding: [usize; 2],
1015) -> Result<Shape, String> {
1016 if input.rank() != 4 {
1017 return Err("pool2d requires an NCHW input".into());
1018 }
1019 if stride[0] == 0 || stride[1] == 0 {
1020 return Err("pool2d stride must be non-zero".into());
1021 }
1022 let n = input.dim(0);
1023 let c = input.dim(1);
1024 let h = input.dim(2).unwrap_static();
1025 let w = input.dim(3).unwrap_static();
1026 let h_out = conv2d_spatial_output(h, kernel_size[0], stride[0], padding[0], 1);
1027 let w_out = conv2d_spatial_output(w, kernel_size[1], stride[1], padding[1], 1);
1028 if h_out == 0 || w_out == 0 {
1029 return Err(format!(
1030 "pool2d window {kernel_size:?} with stride {stride:?} and padding {padding:?} \
1031 leaves no output for a {h}x{w} input"
1032 ));
1033 }
1034 Ok(Shape::from_dims(
1035 &[n, c, Dim::Static(h_out), Dim::Static(w_out)],
1036 input.dtype(),
1037 ))
1038}
1039
1040pub fn im2col_output_shape(
1043 input: &Shape,
1044 kernel_size: [usize; 2],
1045 stride: [usize; 2],
1046 padding: [usize; 2],
1047 dilation: [usize; 2],
1048) -> Result<Shape, String> {
1049 if input.rank() != 4 {
1050 return Err("im2col requires NCHW input".into());
1051 }
1052 let c_in = input.dim(1).unwrap_static();
1053 let h = input.dim(2).unwrap_static();
1054 let w = input.dim(3).unwrap_static();
1055 let kh = kernel_size[0];
1056 let kw = kernel_size[1];
1057 let h_out = conv2d_spatial_output(h, kh, stride[0], padding[0], dilation[0]);
1058 let w_out = conv2d_spatial_output(w, kw, stride[1], padding[1], dilation[1]);
1059 let k = c_in * kh * kw;
1060 let spatial = h_out * w_out;
1061 let m = match input.dim(0) {
1062 Dim::Static(n) => Dim::Static(n * spatial),
1063 Dim::Dynamic(crate::dynamic::sym::BATCH) | Dim::Dynamic(crate::dynamic::sym::ROWS) => {
1064 Dim::Dynamic(crate::dynamic::sym::ROWS)
1065 }
1066 Dim::Dynamic(_) => Dim::Dynamic(crate::dynamic::sym::ROWS),
1067 };
1068 Ok(Shape::from_dims(&[m, Dim::Static(k)], input.dtype()))
1069}
1070
1071pub fn conv_transpose2d_output_shape(
1073 input: &Shape,
1074 weight: &Shape,
1075 kernel_size: [usize; 2],
1076 stride: [usize; 2],
1077 padding: [usize; 2],
1078 dilation: [usize; 2],
1079 output_padding: [usize; 2],
1080 groups: usize,
1081) -> Result<Shape, String> {
1082 if input.rank() != 4 || weight.rank() != 4 {
1083 return Err("conv_transpose2d requires NCHW input and 4-D weight".into());
1084 }
1085 let n = input.dim(0);
1087 let c_in = input.dim(1).unwrap_static();
1088 let h = input.dim(2).unwrap_static();
1089 let w = input.dim(3).unwrap_static();
1090 let w_cin = weight.dim(0).unwrap_static();
1091 let c_out_per_g = weight.dim(1).unwrap_static();
1092 if w_cin != c_in {
1093 return Err(format!(
1094 "conv_transpose2d weight C_in={w_cin} != input C={c_in}"
1095 ));
1096 }
1097 let h_out = conv_transpose2d_spatial_output(
1098 h,
1099 kernel_size[0],
1100 stride[0],
1101 padding[0],
1102 dilation[0],
1103 output_padding[0],
1104 );
1105 let w_out = conv_transpose2d_spatial_output(
1106 w,
1107 kernel_size[1],
1108 stride[1],
1109 padding[1],
1110 dilation[1],
1111 output_padding[1],
1112 );
1113 Ok(Shape::from_dims(
1114 &[
1115 n,
1116 Dim::Static(c_out_per_g * groups),
1117 Dim::Static(h_out),
1118 Dim::Static(w_out),
1119 ],
1120 input.dtype(),
1121 ))
1122}
1123
1124#[allow(clippy::too_many_arguments)]
1127pub fn conv3d_output_shape(
1128 input: &Shape,
1129 weight: &Shape,
1130 kernel_size: [usize; 3],
1131 stride: [usize; 3],
1132 padding: [usize; 3],
1133 dilation: [usize; 3],
1134 groups: usize,
1135) -> Result<Shape, String> {
1136 if input.rank() != 5 || weight.rank() != 5 {
1137 return Err("conv3d requires NCDHW input and 5-D weight".into());
1138 }
1139 let n = input.dim(0);
1140 let c_in = input.dim(1).unwrap_static();
1141 let d = input.dim(2).unwrap_static();
1142 let h = input.dim(3).unwrap_static();
1143 let w = input.dim(4).unwrap_static();
1144 let c_out = weight.dim(0).unwrap_static();
1145 let w_cin = weight.dim(1).unwrap_static();
1146 if w_cin * groups != c_in {
1147 return Err(format!(
1148 "conv3d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
1149 ));
1150 }
1151 let d_out = conv2d_spatial_output(d, kernel_size[0], stride[0], padding[0], dilation[0]);
1152 let h_out = conv2d_spatial_output(h, kernel_size[1], stride[1], padding[1], dilation[1]);
1153 let w_out = conv2d_spatial_output(w, kernel_size[2], stride[2], padding[2], dilation[2]);
1154 Ok(Shape::from_dims(
1155 &[
1156 n,
1157 Dim::Static(c_out),
1158 Dim::Static(d_out),
1159 Dim::Static(h_out),
1160 Dim::Static(w_out),
1161 ],
1162 input.dtype(),
1163 ))
1164}
1165
1166#[allow(clippy::too_many_arguments)]
1168pub fn conv_transpose3d_output_shape(
1169 input: &Shape,
1170 weight: &Shape,
1171 kernel_size: [usize; 3],
1172 stride: [usize; 3],
1173 padding: [usize; 3],
1174 dilation: [usize; 3],
1175 output_padding: [usize; 3],
1176 groups: usize,
1177) -> Result<Shape, String> {
1178 if input.rank() != 5 || weight.rank() != 5 {
1179 return Err("conv_transpose3d requires NCDHW input and 5-D weight".into());
1180 }
1181 let n = input.dim(0);
1182 let c_in = input.dim(1).unwrap_static();
1183 let d = input.dim(2).unwrap_static();
1184 let h = input.dim(3).unwrap_static();
1185 let w = input.dim(4).unwrap_static();
1186 let w_cin = weight.dim(0).unwrap_static();
1187 let c_out_per_g = weight.dim(1).unwrap_static();
1188 if w_cin != c_in {
1189 return Err(format!(
1190 "conv_transpose3d weight C_in={w_cin} != input C={c_in}"
1191 ));
1192 }
1193 let d_out = conv_transpose2d_spatial_output(
1194 d,
1195 kernel_size[0],
1196 stride[0],
1197 padding[0],
1198 dilation[0],
1199 output_padding[0],
1200 );
1201 let h_out = conv_transpose2d_spatial_output(
1202 h,
1203 kernel_size[1],
1204 stride[1],
1205 padding[1],
1206 dilation[1],
1207 output_padding[1],
1208 );
1209 let w_out = conv_transpose2d_spatial_output(
1210 w,
1211 kernel_size[2],
1212 stride[2],
1213 padding[2],
1214 dilation[2],
1215 output_padding[2],
1216 );
1217 Ok(Shape::from_dims(
1218 &[
1219 n,
1220 Dim::Static(c_out_per_g * groups),
1221 Dim::Static(d_out),
1222 Dim::Static(h_out),
1223 Dim::Static(w_out),
1224 ],
1225 input.dtype(),
1226 ))
1227}
1228
1229pub fn kv_append_shape(
1246 cache: &Shape,
1247 row: &Shape,
1248 axis: usize,
1249 pos: usize,
1250) -> Result<Shape, String> {
1251 if axis >= cache.rank() {
1252 return Err(format!(
1253 "kv_append: axis {axis} is out of range for a rank-{} cache",
1254 cache.rank()
1255 ));
1256 }
1257 if row.rank() != cache.rank() {
1258 return Err(format!(
1259 "kv_append: row rank {} does not match cache rank {} — the row is a \
1260 one-step slice of the cache, not a squeezed vector",
1261 row.rank(),
1262 cache.rank()
1263 ));
1264 }
1265 if row.dtype() != cache.dtype() {
1266 return Err(format!(
1267 "kv_append: row dtype {:?} does not match cache dtype {:?} — the write \
1268 is a raw copy, so a mismatch reinterprets the row's bits",
1269 row.dtype(),
1270 cache.dtype()
1271 ));
1272 }
1273 if let Dim::Static(cap) = cache.dim(axis)
1274 && pos >= cap
1275 {
1276 return Err(format!(
1277 "kv_append: pos {pos} is past the cache's axis-{axis} capacity {cap} — \
1278 the cache must be sized to the CAPACITY it will grow to, not to the \
1279 number of rows it currently holds"
1280 ));
1281 }
1282 if let Dim::Static(n) = row.dim(axis)
1283 && n != 1
1284 {
1285 return Err(format!(
1286 "kv_append: row axis-{axis} extent is {n}, must be 1 (one step)"
1287 ));
1288 }
1289 for i in 0..cache.rank() {
1290 if i == axis {
1291 continue;
1292 }
1293 if let (Dim::Static(c), Dim::Static(r)) = (cache.dim(i), row.dim(i))
1294 && c != r
1295 {
1296 return Err(format!(
1297 "kv_append: row dim {i} is {r}, cache dim {i} is {c} — every axis but \
1298 {axis} must match or the copy walks off the row"
1299 ));
1300 }
1301 }
1302 Ok(cache.clone().with_dim(axis, Dim::Static(pos + 1)))
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307
1308 #[test]
1317 fn concat_rejects_non_concat_axis_mismatch() {
1318 let pad = Shape::new(&[3, 1, 1, 31], DType::F32);
1319 let act = Shape::new(&[3, 1, 8, 1000], DType::F32);
1320 let err = concat_shape(&[&pad, &act], 3).unwrap_err();
1321 assert!(
1322 err.contains("axis 2 mismatch"),
1323 "expected the channel-axis mismatch to be named, got: {err}"
1324 );
1325
1326 let pad_ok = Shape::new(&[3, 1, 8, 31], DType::F32);
1328 let out = concat_shape(&[&pad_ok, &act], 3).expect("matching dims must concat");
1329 assert_eq!(out.dims()[3].unwrap_static(), 1031);
1330 assert_eq!(out.dims()[2].unwrap_static(), 8);
1331 }
1332
1333 #[test]
1335 fn concat_axis_itself_may_differ() {
1336 let a = Shape::new(&[2, 3], DType::F32);
1337 let b = Shape::new(&[5, 3], DType::F32);
1338 let out = concat_shape(&[&a, &b], 0).expect("concat axis may differ");
1339 assert_eq!(out.dims()[0].unwrap_static(), 7);
1340 }
1341
1342 use super::*;
1343
1344 #[test]
1347 fn dequant_matmul_contracts_the_weights_last_axis() {
1348 let (m, k, n) = (2usize, 64usize, 3usize);
1349 let x = Shape::new(&[m, k], DType::F32);
1350 let w_nk = Shape::new(&[n, k], DType::F32);
1351
1352 assert_eq!(
1353 dequant_matmul_shape(&x, &w_nk).expect("[n,k] weight is the GGUF layout"),
1354 Shape::new(&[m, n], DType::F32),
1355 );
1356
1357 assert!(matmul_shape(&x, &w_nk).unwrap_err().contains("K mismatch"));
1360
1361 assert_eq!(
1363 dequant_matmul_shape(&Shape::new(&[5, m, k], DType::F32), &w_nk).unwrap(),
1364 Shape::new(&[5, m, n], DType::F32),
1365 );
1366
1367 let err = dequant_matmul_shape(&x, &Shape::new(&[n, k + 1], DType::F32)).unwrap_err();
1369 assert!(err.contains("K mismatch"), "{err}");
1370 assert!(err.contains("last axis"), "{err}");
1371
1372 assert!(dequant_matmul_shape(&x, &Shape::new(&[1024], DType::U8)).is_err());
1375 }
1376
1377 #[test]
1379 fn gather_index_count_handles_both_index_forms() {
1380 let batched = Shape::new(&[2, 11], DType::F32);
1382 assert_eq!(batched.gather_index_count(1), 11);
1383 assert_eq!(batched.gather_index_count(0), 2);
1384
1385 let flat = Shape::new(&[11], DType::F32);
1388 assert_eq!(flat.gather_index_count(1), 11);
1389 assert_eq!(flat.gather_index_count(0), 11);
1390 assert_eq!(flat.gather_index_count(3), 11, "any axis beyond rank");
1391
1392 assert_eq!(Shape::new(&[], DType::F32).gather_index_count(0), 1);
1394 }
1395
1396 #[test]
1397 fn static_shape() {
1398 let s = Shape::new(&[4, 15, 384], DType::F32);
1399 assert_eq!(s.rank(), 3);
1400 assert_eq!(s.num_elements(), Some(4 * 15 * 384));
1401 assert_eq!(s.size_bytes(), Some(4 * 15 * 384 * 4));
1402 assert!(s.is_static());
1403 assert_eq!(format!("{s}"), "[4, 15, 384] f32");
1404 }
1405
1406 #[test]
1409 fn grouped_matmul_derives_m_and_n() {
1410 let input = Shape::new(&[5, 6], DType::F32);
1411 let bank = Shape::new(&[8, 6, 12], DType::F32); let d = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[5, 12], DType::F32)))
1413 .expect("valid");
1414 assert_eq!(
1415 d,
1416 GroupedMatMulDims {
1417 m: 5,
1418 k: 6,
1419 n: 12,
1420 num_experts: 8
1421 }
1422 );
1423 assert_eq!(
1424 grouped_matmul_shape(&input, &bank).unwrap(),
1425 Shape::new(&[5, 12], DType::F32)
1426 );
1427 }
1428
1429 #[test]
1432 fn grouped_matmul_rejects_an_untransposed_bank() {
1433 let input = Shape::new(&[5, 6], DType::F32);
1434 let bank = Shape::new(&[8, 12, 6], DType::F32); let err = grouped_matmul_dims(&input, &bank, None).unwrap_err();
1436 assert!(err.contains("K mismatch"), "unhelpful: {err}");
1437 assert!(err.contains("[E, N, K]"), "should name the cause: {err}");
1438 }
1439
1440 #[test]
1444 fn grouped_matmul_rejects_a_square_bank_by_output_shape() {
1445 let input = Shape::new(&[4, 12, 12], DType::F32);
1446 let bank = Shape::new(&[8, 12, 12], DType::F32);
1447 assert!(grouped_matmul_dims(&input, &bank, None).is_ok());
1449 let err = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[12, 6], DType::F32)))
1451 .unwrap_err();
1452 assert!(err.contains("output shape mismatch"), "unhelpful: {err}");
1453 }
1454
1455 #[test]
1456 fn grouped_matmul_rejects_a_non_bank_weight() {
1457 let input = Shape::new(&[5, 6], DType::F32);
1458 let err = grouped_matmul_dims(&input, &Shape::new(&[6, 12], DType::F32), None).unwrap_err();
1459 assert!(err.contains("rank 3"), "unhelpful: {err}");
1460 }
1461
1462 #[test]
1465 fn broadcast_same() {
1466 let a = Shape::new(&[4, 15, 384], DType::F32);
1467 let r = broadcast(&a, &a).unwrap();
1468 assert_eq!(r.dims(), a.dims());
1469 }
1470
1471 #[test]
1472 fn broadcast_bias() {
1473 let a = Shape::new(&[4, 15, 384], DType::F32);
1474 let b = Shape::new(&[384], DType::F32);
1475 let r = broadcast(&a, &b).unwrap();
1476 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1477 }
1478
1479 #[test]
1480 fn broadcast_scalar() {
1481 let a = Shape::new(&[4, 15, 384], DType::F32);
1482 let b = Shape::scalar(DType::F32);
1483 let r = broadcast(&a, &b).unwrap();
1484 assert_eq!(r, a);
1485 }
1486
1487 #[test]
1488 fn broadcast_mismatch() {
1489 let a = Shape::new(&[4, 15, 384], DType::F32);
1490 let b = Shape::new(&[4, 15, 256], DType::F32);
1491 assert!(broadcast(&a, &b).is_err());
1492 }
1493
1494 #[test]
1495 fn matmul_basic() {
1496 let a = Shape::new(&[4, 15, 384], DType::F32);
1497 let b = Shape::new(&[384, 1536], DType::F32);
1498 let r = matmul_shape(&a, &b).unwrap();
1499 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1500 }
1501
1502 #[test]
1503 fn matmul_batched() {
1504 let a = Shape::new(&[4, 15, 384], DType::F32);
1505 let b = Shape::new(&[4, 384, 1536], DType::F32);
1506 let r = matmul_shape(&a, &b).unwrap();
1507 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1508 }
1509
1510 #[test]
1511 fn matmul_k_mismatch() {
1512 let a = Shape::new(&[4, 15, 384], DType::F32);
1513 let b = Shape::new(&[512, 1536], DType::F32);
1514 assert!(matmul_shape(&a, &b).is_err());
1515 }
1516
1517 #[test]
1518 fn reduce_keepdim() {
1519 let a = Shape::new(&[4, 15, 384], DType::F32);
1520 let r = reduce_shape(&a, &[2], true).unwrap();
1521 assert_eq!(r, Shape::new(&[4, 15, 1], DType::F32));
1522 }
1523
1524 #[test]
1525 fn reduce_no_keepdim() {
1526 let a = Shape::new(&[4, 15, 384], DType::F32);
1527 let r = reduce_shape(&a, &[2], false).unwrap();
1528 assert_eq!(r, Shape::new(&[4, 15], DType::F32));
1529 }
1530
1531 #[test]
1532 fn concat_basic() {
1533 let a = Shape::new(&[4, 15, 384], DType::F32);
1534 let b = Shape::new(&[4, 15, 384], DType::F32);
1535 let r = concat_shape(&[&a, &b], 2).unwrap();
1536 assert_eq!(r, Shape::new(&[4, 15, 768], DType::F32));
1537 }
1538
1539 #[test]
1540 fn gather_embedding() {
1541 let table = Shape::new(&[30522, 384], DType::F32);
1542 let indices = Shape::new(&[4, 15], DType::I64);
1543 let r = gather_shape(&table, &indices, 0).unwrap();
1544 assert_eq!(
1545 r,
1546 Shape::from_dims(
1547 &[Dim::Static(4), Dim::Static(15), Dim::Static(384)],
1548 DType::F32
1549 )
1550 );
1551 }
1552
1553 #[test]
1554 fn gather_nonzero_axis_keeps_prefix() {
1555 let table = Shape::new(&[4, 3], DType::F32);
1559 let indices = Shape::new(&[6], DType::I64);
1560 let r = gather_shape(&table, &indices, 1).unwrap();
1561 assert_eq!(r, Shape::new(&[4, 6], DType::F32));
1562 let table = Shape::new(&[2, 5, 7], DType::F32);
1564 let indices = Shape::new(&[3, 4], DType::I64);
1565 let r = gather_shape(&table, &indices, 1).unwrap();
1566 assert_eq!(r, Shape::new(&[2, 3, 4, 7], DType::F32));
1567 }
1568
1569 #[test]
1570 fn reshape_with_neg1() {
1571 let a = Shape::new(&[4, 15, 384], DType::F32);
1572 let r = reshape_shape(&a, &[60, -1]).unwrap();
1573 assert_eq!(r, Shape::new(&[60, 384], DType::F32));
1574 }
1575
1576 #[test]
1577 fn transpose_basic() {
1578 let a = Shape::new(&[4, 15, 384], DType::F32);
1579 let r = transpose_shape(&a, &[0, 2, 1]).unwrap();
1580 assert_eq!(r, Shape::new(&[4, 384, 15], DType::F32));
1581 }
1582
1583 #[test]
1584 fn narrow_basic() {
1585 let a = Shape::new(&[4, 15, 1152], DType::F32);
1586 let r = narrow_shape(&a, 2, 384).unwrap();
1587 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1588 }
1589
1590 #[test]
1591 fn compare_bool_output() {
1592 let a = Shape::new(&[4, 15], DType::F32);
1593 let b = Shape::new(&[4, 15], DType::F32);
1594 let r = compare_shape(&a, &b).unwrap();
1595 assert_eq!(r.dtype(), DType::Bool);
1596 assert_eq!(r.rank(), 2);
1597 }
1598
1599 #[test]
1602 fn dynamic_shape() {
1603 let s = Shape::from_dims(
1604 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1605 DType::F32,
1606 );
1607 assert_eq!(s.rank(), 3);
1608 assert_eq!(s.num_elements(), None);
1609 assert!(!s.is_static());
1610 assert_eq!(format!("{s}"), "[?0, ?1, 384] f32");
1611 }
1612
1613 #[test]
1614 fn dynamic_symbols_lists_distinct_dims() {
1615 let s = Shape::from_dims(
1616 &[
1617 Dim::Dynamic(1),
1618 Dim::Static(384),
1619 Dim::Dynamic(0),
1620 Dim::Dynamic(1),
1621 ],
1622 DType::F32,
1623 );
1624 assert_eq!(s.dynamic_symbols(), vec![0, 1]);
1625 }
1626
1627 #[test]
1628 fn bind_specializes_known_symbols() {
1629 let s = Shape::from_dims(
1630 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1631 DType::F32,
1632 );
1633 let mut b = DimBinding::new();
1634 b.set(0, 8);
1635 b.set(1, 64);
1636 let s2 = s.bind(&b);
1637 assert!(s2.is_static());
1638 assert_eq!(s2.num_elements(), Some(8 * 64 * 384));
1639 }
1640
1641 #[test]
1642 fn bind_leaves_unknown_symbols_alone() {
1643 let s = Shape::from_dims(&[Dim::Dynamic(0), Dim::Dynamic(99)], DType::F32);
1644 let mut b = DimBinding::new();
1645 b.set(0, 4);
1646 let s2 = s.bind(&b);
1647 assert!(!s2.is_static()); assert_eq!(s2.dynamic_symbols(), vec![99]);
1649 }
1650}
1651
1652#[derive(Debug, Clone, Default)]
1655pub struct DimBinding {
1656 map: std::collections::HashMap<u32, usize>,
1657}
1658
1659impl DimBinding {
1660 pub fn new() -> Self {
1661 Self::default()
1662 }
1663 pub fn set(&mut self, symbol: u32, size: usize) -> Option<usize> {
1664 self.map.insert(symbol, size)
1665 }
1666 pub fn get(&self, symbol: u32) -> Option<usize> {
1667 self.map.get(&symbol).copied()
1668 }
1669 pub fn is_empty(&self) -> bool {
1670 self.map.is_empty()
1671 }
1672 pub fn len(&self) -> usize {
1673 self.map.len()
1674 }
1675 pub fn iter(&self) -> impl Iterator<Item = (u32, usize)> + '_ {
1676 self.map.iter().map(|(&s, &n)| (s, n))
1677 }
1678}