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 if axis >= input.rank() {
558 return Err(format!("axis {axis} >= rank {}", input.rank()));
559 }
560 Ok(input.clone().with_dim(axis, Dim::Static(len)))
561}
562
563pub fn tile_shape(input: &Shape, reps: &[usize]) -> Result<Shape, String> {
565 if reps.len() != input.rank() {
566 return Err(format!(
567 "tile: reps length {} != rank {}",
568 reps.len(),
569 input.rank()
570 ));
571 }
572 let mut out = input.clone();
573 for (axis, &r) in reps.iter().enumerate() {
574 if r == 1 {
575 continue;
576 }
577 match input.dims[axis] {
578 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n * r)),
579 Dim::Dynamic(_) => return Err(format!("tile: cannot tile dynamic axis {axis}")),
580 }
581 }
582 Ok(out)
583}
584
585pub fn slice_shape(input: &Shape, axis: usize, len: usize) -> Result<Shape, String> {
588 if axis >= input.rank() {
589 return Err(format!("slice: axis {axis} >= rank {}", input.rank()));
590 }
591 Ok(input.clone().with_dim(axis, Dim::Static(len)))
592}
593
594pub fn pad_shape(input: &Shape, pads: &[[usize; 2]]) -> Result<Shape, String> {
598 if pads.len() != input.rank() {
599 return Err(format!(
600 "pad: pads length {} != rank {}",
601 pads.len(),
602 input.rank()
603 ));
604 }
605 let mut out = input.clone();
606 for (axis, &[before, after]) in pads.iter().enumerate() {
607 if before == 0 && after == 0 {
608 continue;
609 }
610 match input.dims[axis] {
611 Dim::Static(n) => out = out.with_dim(axis, Dim::Static(n + before + after)),
612 Dim::Dynamic(_) => {
613 return Err(format!(
614 "pad: cannot pad dynamic axis {axis} by ({before},{after})"
615 ));
616 }
617 }
618 }
619 Ok(out)
620}
621
622pub fn concat_shape(inputs: &[&Shape], axis: usize) -> Result<Shape, String> {
624 if inputs.is_empty() {
625 return Err("concat: no inputs".into());
626 }
627 let base = inputs[0];
628 let mut static_sum = 0usize;
629 let mut dyn_sym: Option<u32> = None;
630 for s in inputs {
631 if s.rank() == 0 {
632 return Err("concat: input has rank 0".into());
633 }
634 if s.rank() != base.rank() {
635 return Err(format!(
636 "concat: rank mismatch {} vs {}",
637 s.rank(),
638 base.rank()
639 ));
640 }
641 let ax = axis.min(s.rank().saturating_sub(1));
642 for d in 0..s.rank() {
651 if d == ax {
652 continue;
653 }
654 if let (Dim::Static(a), Dim::Static(b)) = (s.dims[d], base.dims[d])
655 && a != b
656 {
657 return Err(format!(
658 "concat: axis {d} mismatch {a} vs {b} (concat axis is {axis}); all axes but the concat axis must match"
659 ));
660 }
661 }
662 match s.dims[ax] {
663 Dim::Static(n) => static_sum += n,
664 Dim::Dynamic(sym) => {
665 if let Some(prev) = dyn_sym {
666 if prev != sym {
667 return Err(format!(
668 "concat: mismatched dynamic symbols {prev} vs {sym} on axis {axis}"
669 ));
670 }
671 }
672 dyn_sym = Some(sym);
673 }
674 }
675 }
676 let out_dim = match dyn_sym {
677 None => Dim::Static(static_sum),
678 Some(sym) if static_sum == 0 => Dim::Dynamic(sym),
679 Some(sym) => {
680 let _ = static_sum;
683 Dim::Dynamic(sym)
684 }
685 };
686 let out_axis = axis.min(base.rank().saturating_sub(1));
687 Ok(base.clone().with_dim(out_axis, out_dim))
688}
689
690pub fn gather_shape(table: &Shape, indices: &Shape, axis: usize) -> Result<Shape, String> {
692 if axis >= table.rank() {
693 return Err(format!("gather: axis {axis} >= rank {}", table.rank()));
694 }
695 let mut dims: SmallVec<[Dim; 4]> = table.dims[..axis].iter().copied().collect();
701 dims.extend(indices.dims.iter().copied());
702 for i in (axis + 1)..table.rank() {
703 dims.push(table.dims[i]);
704 }
705 Ok(Shape {
706 dims,
707 dtype: table.dtype,
708 })
709}
710
711pub fn reshape_shape(input: &Shape, new_shape: &[i64]) -> Result<Shape, String> {
713 let neg_count = new_shape.iter().filter(|&&d| d == -1).count();
714 if neg_count > 1 {
715 return Err("reshape: at most one -1".into());
716 }
717
718 if input.is_static() {
719 let known_product: i64 = new_shape.iter().filter(|&&d| d != -1).product();
726 let mut dims = SmallVec::new();
727 for &d in new_shape {
728 if d == -1 {
729 let total = input
730 .num_elements()
731 .ok_or_else(|| "reshape: input has dynamic dims".to_string())?;
732 let inferred = total as i64 / known_product.max(1);
733 dims.push(Dim::Static(inferred as usize));
734 } else if d < 0 {
735 return Err(format!("reshape: invalid dim {d}"));
736 } else {
737 dims.push(Dim::Static(d as usize));
738 }
739 }
740 return Ok(Shape {
741 dims,
742 dtype: input.dtype,
743 });
744 }
745
746 let dyn_syms = input.dynamic_symbols();
749 let neg_idx = new_shape.iter().position(|&d| d == -1);
750 let mut out_dims: SmallVec<[Dim; 4]> = SmallVec::new();
751 for (i, &d) in new_shape.iter().enumerate() {
752 if Some(i) == neg_idx {
753 continue;
754 }
755 if d < 0 {
756 return Err(format!("reshape: invalid dim {d}"));
757 }
758 out_dims.push(Dim::Static(d as usize));
759 }
760 if let Some(ni) = neg_idx {
761 let inferred = if dyn_syms.len() == 1 {
762 Dim::Dynamic(dyn_syms[0])
763 } else if dyn_syms.is_empty() {
764 return Err("reshape: cannot infer -1 on static input".into());
765 } else {
766 Dim::Dynamic(crate::dynamic::sym::ROWS)
767 };
768 out_dims.insert(ni, inferred);
769 }
770 Ok(Shape {
771 dims: out_dims,
772 dtype: input.dtype,
773 })
774}
775
776pub fn leading_flatten_fused_shape(input: &Shape) -> Option<Shape> {
778 if input.rank() < 2 {
779 return None;
780 }
781 let Dim::Static(h) = input.dim(input.rank() - 1) else {
782 return None;
783 };
784 let leading = &input.dims()[..input.rank() - 1];
785 let lead_dim = if leading.iter().all(|d| d.is_static()) {
786 Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>())
787 } else {
788 let mut syms: Vec<u32> = leading
789 .iter()
790 .filter_map(|d| match d {
791 Dim::Dynamic(s) => Some(*s),
792 _ => None,
793 })
794 .collect();
795 syms.sort();
796 syms.dedup();
797 match syms.len() {
798 0 => Dim::Static(leading.iter().map(|d| d.unwrap_static()).product::<usize>()),
799 1 => Dim::Dynamic(syms[0]),
800 _ => Dim::Dynamic(crate::dynamic::sym::ROWS),
801 }
802 };
803 Some(Shape::from_dims(&[lead_dim, Dim::Static(h)], input.dtype()))
804}
805
806pub fn leading_flatten_shape(input: &Shape, new_shape: &[i64]) -> Option<Shape> {
808 if new_shape.len() != 2 {
809 return None;
810 }
811 let flat = leading_flatten_fused_shape(input)?;
812 let Dim::Static(h) = input.dim(input.rank() - 1) else {
813 return None;
814 };
815 if new_shape[1] as usize != h {
816 return None;
817 }
818 match flat.dim(0) {
819 Dim::Static(lead) if new_shape[0] as usize == lead => Some(flat),
820 Dim::Dynamic(_) if new_shape[0] == -1 => Some(flat),
821 _ => None,
822 }
823}
824
825pub fn attention_shape(q: &Shape) -> Shape {
827 q.clone()
828}
829
830pub fn attention_shape_vdim(
835 q: &Shape,
836 num_heads: usize,
837 head_dim: usize,
838 v_head_dim: usize,
839) -> Shape {
840 if head_dim == v_head_dim || q.rank() == 0 {
841 return q.clone();
842 }
843 let rank = q.rank();
844 let new_last = if rank >= 4 {
845 v_head_dim
846 } else {
847 num_heads * v_head_dim
848 };
849 q.clone().with_dim(rank - 1, Dim::Static(new_last))
850}
851
852impl std::fmt::Display for Shape {
853 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854 write!(f, "[")?;
855 for (i, d) in self.dims.iter().enumerate() {
856 if i > 0 {
857 write!(f, ", ")?;
858 }
859 write!(f, "{d}")?;
860 }
861 write!(f, "] {}", self.dtype)
862 }
863}
864
865pub fn conv2d_spatial_output(
867 in_size: usize,
868 kernel: usize,
869 stride: usize,
870 padding: usize,
871 dilation: usize,
872) -> usize {
873 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
874 (in_size + 2 * padding)
875 .saturating_sub(dil_k)
876 .saturating_sub(1)
877 / stride
878 + 1
879}
880
881pub fn conv_transpose2d_spatial_output(
883 in_size: usize,
884 kernel: usize,
885 stride: usize,
886 padding: usize,
887 dilation: usize,
888 output_padding: usize,
889) -> usize {
890 let dil_k = dilation.saturating_mul(kernel.saturating_sub(1));
891 (in_size - 1) * stride + output_padding + dil_k - 2 * padding + 1
892}
893
894pub fn conv2d_output_shape(
896 input: &Shape,
897 weight: &Shape,
898 kernel_size: [usize; 2],
899 stride: [usize; 2],
900 padding: [usize; 2],
901 dilation: [usize; 2],
902 groups: usize,
903) -> Result<Shape, String> {
904 if input.rank() != 4 || weight.rank() != 4 {
905 return Err("conv2d requires NCHW input and 4-D weight".into());
906 }
907 let n = input.dim(0);
908 let c_in = input.dim(1).unwrap_static();
909 let h = input.dim(2).unwrap_static();
910 let w = input.dim(3).unwrap_static();
911 let c_out = weight.dim(0).unwrap_static();
912 let w_cin = weight.dim(1).unwrap_static();
913 if w_cin * groups != c_in {
914 return Err(format!(
915 "conv2d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
916 ));
917 }
918 let h_out = conv2d_spatial_output(h, kernel_size[0], stride[0], padding[0], dilation[0]);
919 let w_out = conv2d_spatial_output(w, kernel_size[1], stride[1], padding[1], dilation[1]);
920 Ok(Shape::from_dims(
921 &[
922 n,
923 Dim::Static(c_out),
924 Dim::Static(h_out),
925 Dim::Static(w_out),
926 ],
927 input.dtype(),
928 ))
929}
930
931pub fn rope_table_stride(cos: &Shape, n_rot: usize) -> usize {
947 if cos.rank() >= 2 {
948 cos.dims()
949 .last()
950 .map_or_else(|| n_rot / 2, |d| d.unwrap_static())
951 .max(1)
952 } else {
953 (n_rot / 2).max(1)
954 }
955}
956
957pub fn pool2d_output_shape(
962 input: &Shape,
963 kernel_size: [usize; 2],
964 stride: [usize; 2],
965 padding: [usize; 2],
966) -> Result<Shape, String> {
967 if input.rank() != 4 {
968 return Err("pool2d requires an NCHW input".into());
969 }
970 if stride[0] == 0 || stride[1] == 0 {
971 return Err("pool2d stride must be non-zero".into());
972 }
973 let n = input.dim(0);
974 let c = input.dim(1);
975 let h = input.dim(2).unwrap_static();
976 let w = input.dim(3).unwrap_static();
977 let h_out = conv2d_spatial_output(h, kernel_size[0], stride[0], padding[0], 1);
978 let w_out = conv2d_spatial_output(w, kernel_size[1], stride[1], padding[1], 1);
979 if h_out == 0 || w_out == 0 {
980 return Err(format!(
981 "pool2d window {kernel_size:?} with stride {stride:?} and padding {padding:?} \
982 leaves no output for a {h}x{w} input"
983 ));
984 }
985 Ok(Shape::from_dims(
986 &[n, c, Dim::Static(h_out), Dim::Static(w_out)],
987 input.dtype(),
988 ))
989}
990
991pub fn im2col_output_shape(
994 input: &Shape,
995 kernel_size: [usize; 2],
996 stride: [usize; 2],
997 padding: [usize; 2],
998 dilation: [usize; 2],
999) -> Result<Shape, String> {
1000 if input.rank() != 4 {
1001 return Err("im2col requires NCHW input".into());
1002 }
1003 let c_in = input.dim(1).unwrap_static();
1004 let h = input.dim(2).unwrap_static();
1005 let w = input.dim(3).unwrap_static();
1006 let kh = kernel_size[0];
1007 let kw = kernel_size[1];
1008 let h_out = conv2d_spatial_output(h, kh, stride[0], padding[0], dilation[0]);
1009 let w_out = conv2d_spatial_output(w, kw, stride[1], padding[1], dilation[1]);
1010 let k = c_in * kh * kw;
1011 let spatial = h_out * w_out;
1012 let m = match input.dim(0) {
1013 Dim::Static(n) => Dim::Static(n * spatial),
1014 Dim::Dynamic(crate::dynamic::sym::BATCH) | Dim::Dynamic(crate::dynamic::sym::ROWS) => {
1015 Dim::Dynamic(crate::dynamic::sym::ROWS)
1016 }
1017 Dim::Dynamic(_) => Dim::Dynamic(crate::dynamic::sym::ROWS),
1018 };
1019 Ok(Shape::from_dims(&[m, Dim::Static(k)], input.dtype()))
1020}
1021
1022pub fn conv_transpose2d_output_shape(
1024 input: &Shape,
1025 weight: &Shape,
1026 kernel_size: [usize; 2],
1027 stride: [usize; 2],
1028 padding: [usize; 2],
1029 dilation: [usize; 2],
1030 output_padding: [usize; 2],
1031 groups: usize,
1032) -> Result<Shape, String> {
1033 if input.rank() != 4 || weight.rank() != 4 {
1034 return Err("conv_transpose2d requires NCHW input and 4-D weight".into());
1035 }
1036 let n = input.dim(0);
1038 let c_in = input.dim(1).unwrap_static();
1039 let h = input.dim(2).unwrap_static();
1040 let w = input.dim(3).unwrap_static();
1041 let w_cin = weight.dim(0).unwrap_static();
1042 let c_out_per_g = weight.dim(1).unwrap_static();
1043 if w_cin != c_in {
1044 return Err(format!(
1045 "conv_transpose2d weight C_in={w_cin} != input C={c_in}"
1046 ));
1047 }
1048 let h_out = conv_transpose2d_spatial_output(
1049 h,
1050 kernel_size[0],
1051 stride[0],
1052 padding[0],
1053 dilation[0],
1054 output_padding[0],
1055 );
1056 let w_out = conv_transpose2d_spatial_output(
1057 w,
1058 kernel_size[1],
1059 stride[1],
1060 padding[1],
1061 dilation[1],
1062 output_padding[1],
1063 );
1064 Ok(Shape::from_dims(
1065 &[
1066 n,
1067 Dim::Static(c_out_per_g * groups),
1068 Dim::Static(h_out),
1069 Dim::Static(w_out),
1070 ],
1071 input.dtype(),
1072 ))
1073}
1074
1075#[allow(clippy::too_many_arguments)]
1078pub fn conv3d_output_shape(
1079 input: &Shape,
1080 weight: &Shape,
1081 kernel_size: [usize; 3],
1082 stride: [usize; 3],
1083 padding: [usize; 3],
1084 dilation: [usize; 3],
1085 groups: usize,
1086) -> Result<Shape, String> {
1087 if input.rank() != 5 || weight.rank() != 5 {
1088 return Err("conv3d requires NCDHW input and 5-D weight".into());
1089 }
1090 let n = input.dim(0);
1091 let c_in = input.dim(1).unwrap_static();
1092 let d = input.dim(2).unwrap_static();
1093 let h = input.dim(3).unwrap_static();
1094 let w = input.dim(4).unwrap_static();
1095 let c_out = weight.dim(0).unwrap_static();
1096 let w_cin = weight.dim(1).unwrap_static();
1097 if w_cin * groups != c_in {
1098 return Err(format!(
1099 "conv3d weight C_in/g={w_cin} * groups={groups} != input C={c_in}"
1100 ));
1101 }
1102 let d_out = conv2d_spatial_output(d, kernel_size[0], stride[0], padding[0], dilation[0]);
1103 let h_out = conv2d_spatial_output(h, kernel_size[1], stride[1], padding[1], dilation[1]);
1104 let w_out = conv2d_spatial_output(w, kernel_size[2], stride[2], padding[2], dilation[2]);
1105 Ok(Shape::from_dims(
1106 &[
1107 n,
1108 Dim::Static(c_out),
1109 Dim::Static(d_out),
1110 Dim::Static(h_out),
1111 Dim::Static(w_out),
1112 ],
1113 input.dtype(),
1114 ))
1115}
1116
1117#[allow(clippy::too_many_arguments)]
1119pub fn conv_transpose3d_output_shape(
1120 input: &Shape,
1121 weight: &Shape,
1122 kernel_size: [usize; 3],
1123 stride: [usize; 3],
1124 padding: [usize; 3],
1125 dilation: [usize; 3],
1126 output_padding: [usize; 3],
1127 groups: usize,
1128) -> Result<Shape, String> {
1129 if input.rank() != 5 || weight.rank() != 5 {
1130 return Err("conv_transpose3d requires NCDHW input and 5-D weight".into());
1131 }
1132 let n = input.dim(0);
1133 let c_in = input.dim(1).unwrap_static();
1134 let d = input.dim(2).unwrap_static();
1135 let h = input.dim(3).unwrap_static();
1136 let w = input.dim(4).unwrap_static();
1137 let w_cin = weight.dim(0).unwrap_static();
1138 let c_out_per_g = weight.dim(1).unwrap_static();
1139 if w_cin != c_in {
1140 return Err(format!(
1141 "conv_transpose3d weight C_in={w_cin} != input C={c_in}"
1142 ));
1143 }
1144 let d_out = conv_transpose2d_spatial_output(
1145 d,
1146 kernel_size[0],
1147 stride[0],
1148 padding[0],
1149 dilation[0],
1150 output_padding[0],
1151 );
1152 let h_out = conv_transpose2d_spatial_output(
1153 h,
1154 kernel_size[1],
1155 stride[1],
1156 padding[1],
1157 dilation[1],
1158 output_padding[1],
1159 );
1160 let w_out = conv_transpose2d_spatial_output(
1161 w,
1162 kernel_size[2],
1163 stride[2],
1164 padding[2],
1165 dilation[2],
1166 output_padding[2],
1167 );
1168 Ok(Shape::from_dims(
1169 &[
1170 n,
1171 Dim::Static(c_out_per_g * groups),
1172 Dim::Static(d_out),
1173 Dim::Static(h_out),
1174 Dim::Static(w_out),
1175 ],
1176 input.dtype(),
1177 ))
1178}
1179
1180pub fn kv_append_shape(
1197 cache: &Shape,
1198 row: &Shape,
1199 axis: usize,
1200 pos: usize,
1201) -> Result<Shape, String> {
1202 if axis >= cache.rank() {
1203 return Err(format!(
1204 "kv_append: axis {axis} is out of range for a rank-{} cache",
1205 cache.rank()
1206 ));
1207 }
1208 if row.rank() != cache.rank() {
1209 return Err(format!(
1210 "kv_append: row rank {} does not match cache rank {} — the row is a \
1211 one-step slice of the cache, not a squeezed vector",
1212 row.rank(),
1213 cache.rank()
1214 ));
1215 }
1216 if row.dtype() != cache.dtype() {
1217 return Err(format!(
1218 "kv_append: row dtype {:?} does not match cache dtype {:?} — the write \
1219 is a raw copy, so a mismatch reinterprets the row's bits",
1220 row.dtype(),
1221 cache.dtype()
1222 ));
1223 }
1224 if let Dim::Static(cap) = cache.dim(axis)
1225 && pos >= cap
1226 {
1227 return Err(format!(
1228 "kv_append: pos {pos} is past the cache's axis-{axis} capacity {cap} — \
1229 the cache must be sized to the CAPACITY it will grow to, not to the \
1230 number of rows it currently holds"
1231 ));
1232 }
1233 if let Dim::Static(n) = row.dim(axis)
1234 && n != 1
1235 {
1236 return Err(format!(
1237 "kv_append: row axis-{axis} extent is {n}, must be 1 (one step)"
1238 ));
1239 }
1240 for i in 0..cache.rank() {
1241 if i == axis {
1242 continue;
1243 }
1244 if let (Dim::Static(c), Dim::Static(r)) = (cache.dim(i), row.dim(i))
1245 && c != r
1246 {
1247 return Err(format!(
1248 "kv_append: row dim {i} is {r}, cache dim {i} is {c} — every axis but \
1249 {axis} must match or the copy walks off the row"
1250 ));
1251 }
1252 }
1253 Ok(cache.clone().with_dim(axis, Dim::Static(pos + 1)))
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258
1259 #[test]
1268 fn concat_rejects_non_concat_axis_mismatch() {
1269 let pad = Shape::new(&[3, 1, 1, 31], DType::F32);
1270 let act = Shape::new(&[3, 1, 8, 1000], DType::F32);
1271 let err = concat_shape(&[&pad, &act], 3).unwrap_err();
1272 assert!(
1273 err.contains("axis 2 mismatch"),
1274 "expected the channel-axis mismatch to be named, got: {err}"
1275 );
1276
1277 let pad_ok = Shape::new(&[3, 1, 8, 31], DType::F32);
1279 let out = concat_shape(&[&pad_ok, &act], 3).expect("matching dims must concat");
1280 assert_eq!(out.dims()[3].unwrap_static(), 1031);
1281 assert_eq!(out.dims()[2].unwrap_static(), 8);
1282 }
1283
1284 #[test]
1286 fn concat_axis_itself_may_differ() {
1287 let a = Shape::new(&[2, 3], DType::F32);
1288 let b = Shape::new(&[5, 3], DType::F32);
1289 let out = concat_shape(&[&a, &b], 0).expect("concat axis may differ");
1290 assert_eq!(out.dims()[0].unwrap_static(), 7);
1291 }
1292
1293 use super::*;
1294
1295 #[test]
1298 fn dequant_matmul_contracts_the_weights_last_axis() {
1299 let (m, k, n) = (2usize, 64usize, 3usize);
1300 let x = Shape::new(&[m, k], DType::F32);
1301 let w_nk = Shape::new(&[n, k], DType::F32);
1302
1303 assert_eq!(
1304 dequant_matmul_shape(&x, &w_nk).expect("[n,k] weight is the GGUF layout"),
1305 Shape::new(&[m, n], DType::F32),
1306 );
1307
1308 assert!(matmul_shape(&x, &w_nk).unwrap_err().contains("K mismatch"));
1311
1312 assert_eq!(
1314 dequant_matmul_shape(&Shape::new(&[5, m, k], DType::F32), &w_nk).unwrap(),
1315 Shape::new(&[5, m, n], DType::F32),
1316 );
1317
1318 let err = dequant_matmul_shape(&x, &Shape::new(&[n, k + 1], DType::F32)).unwrap_err();
1320 assert!(err.contains("K mismatch"), "{err}");
1321 assert!(err.contains("last axis"), "{err}");
1322
1323 assert!(dequant_matmul_shape(&x, &Shape::new(&[1024], DType::U8)).is_err());
1326 }
1327
1328 #[test]
1330 fn gather_index_count_handles_both_index_forms() {
1331 let batched = Shape::new(&[2, 11], DType::F32);
1333 assert_eq!(batched.gather_index_count(1), 11);
1334 assert_eq!(batched.gather_index_count(0), 2);
1335
1336 let flat = Shape::new(&[11], DType::F32);
1339 assert_eq!(flat.gather_index_count(1), 11);
1340 assert_eq!(flat.gather_index_count(0), 11);
1341 assert_eq!(flat.gather_index_count(3), 11, "any axis beyond rank");
1342
1343 assert_eq!(Shape::new(&[], DType::F32).gather_index_count(0), 1);
1345 }
1346
1347 #[test]
1348 fn static_shape() {
1349 let s = Shape::new(&[4, 15, 384], DType::F32);
1350 assert_eq!(s.rank(), 3);
1351 assert_eq!(s.num_elements(), Some(4 * 15 * 384));
1352 assert_eq!(s.size_bytes(), Some(4 * 15 * 384 * 4));
1353 assert!(s.is_static());
1354 assert_eq!(format!("{s}"), "[4, 15, 384] f32");
1355 }
1356
1357 #[test]
1360 fn grouped_matmul_derives_m_and_n() {
1361 let input = Shape::new(&[5, 6], DType::F32);
1362 let bank = Shape::new(&[8, 6, 12], DType::F32); let d = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[5, 12], DType::F32)))
1364 .expect("valid");
1365 assert_eq!(
1366 d,
1367 GroupedMatMulDims {
1368 m: 5,
1369 k: 6,
1370 n: 12,
1371 num_experts: 8
1372 }
1373 );
1374 assert_eq!(
1375 grouped_matmul_shape(&input, &bank).unwrap(),
1376 Shape::new(&[5, 12], DType::F32)
1377 );
1378 }
1379
1380 #[test]
1383 fn grouped_matmul_rejects_an_untransposed_bank() {
1384 let input = Shape::new(&[5, 6], DType::F32);
1385 let bank = Shape::new(&[8, 12, 6], DType::F32); let err = grouped_matmul_dims(&input, &bank, None).unwrap_err();
1387 assert!(err.contains("K mismatch"), "unhelpful: {err}");
1388 assert!(err.contains("[E, N, K]"), "should name the cause: {err}");
1389 }
1390
1391 #[test]
1395 fn grouped_matmul_rejects_a_square_bank_by_output_shape() {
1396 let input = Shape::new(&[4, 12, 12], DType::F32);
1397 let bank = Shape::new(&[8, 12, 12], DType::F32);
1398 assert!(grouped_matmul_dims(&input, &bank, None).is_ok());
1400 let err = grouped_matmul_dims(&input, &bank, Some(&Shape::new(&[12, 6], DType::F32)))
1402 .unwrap_err();
1403 assert!(err.contains("output shape mismatch"), "unhelpful: {err}");
1404 }
1405
1406 #[test]
1407 fn grouped_matmul_rejects_a_non_bank_weight() {
1408 let input = Shape::new(&[5, 6], DType::F32);
1409 let err = grouped_matmul_dims(&input, &Shape::new(&[6, 12], DType::F32), None).unwrap_err();
1410 assert!(err.contains("rank 3"), "unhelpful: {err}");
1411 }
1412
1413 #[test]
1416 fn broadcast_same() {
1417 let a = Shape::new(&[4, 15, 384], DType::F32);
1418 let r = broadcast(&a, &a).unwrap();
1419 assert_eq!(r.dims(), a.dims());
1420 }
1421
1422 #[test]
1423 fn broadcast_bias() {
1424 let a = Shape::new(&[4, 15, 384], DType::F32);
1425 let b = Shape::new(&[384], DType::F32);
1426 let r = broadcast(&a, &b).unwrap();
1427 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1428 }
1429
1430 #[test]
1431 fn broadcast_scalar() {
1432 let a = Shape::new(&[4, 15, 384], DType::F32);
1433 let b = Shape::scalar(DType::F32);
1434 let r = broadcast(&a, &b).unwrap();
1435 assert_eq!(r, a);
1436 }
1437
1438 #[test]
1439 fn broadcast_mismatch() {
1440 let a = Shape::new(&[4, 15, 384], DType::F32);
1441 let b = Shape::new(&[4, 15, 256], DType::F32);
1442 assert!(broadcast(&a, &b).is_err());
1443 }
1444
1445 #[test]
1446 fn matmul_basic() {
1447 let a = Shape::new(&[4, 15, 384], DType::F32);
1448 let b = Shape::new(&[384, 1536], DType::F32);
1449 let r = matmul_shape(&a, &b).unwrap();
1450 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1451 }
1452
1453 #[test]
1454 fn matmul_batched() {
1455 let a = Shape::new(&[4, 15, 384], DType::F32);
1456 let b = Shape::new(&[4, 384, 1536], DType::F32);
1457 let r = matmul_shape(&a, &b).unwrap();
1458 assert_eq!(r, Shape::new(&[4, 15, 1536], DType::F32));
1459 }
1460
1461 #[test]
1462 fn matmul_k_mismatch() {
1463 let a = Shape::new(&[4, 15, 384], DType::F32);
1464 let b = Shape::new(&[512, 1536], DType::F32);
1465 assert!(matmul_shape(&a, &b).is_err());
1466 }
1467
1468 #[test]
1469 fn reduce_keepdim() {
1470 let a = Shape::new(&[4, 15, 384], DType::F32);
1471 let r = reduce_shape(&a, &[2], true).unwrap();
1472 assert_eq!(r, Shape::new(&[4, 15, 1], DType::F32));
1473 }
1474
1475 #[test]
1476 fn reduce_no_keepdim() {
1477 let a = Shape::new(&[4, 15, 384], DType::F32);
1478 let r = reduce_shape(&a, &[2], false).unwrap();
1479 assert_eq!(r, Shape::new(&[4, 15], DType::F32));
1480 }
1481
1482 #[test]
1483 fn concat_basic() {
1484 let a = Shape::new(&[4, 15, 384], DType::F32);
1485 let b = Shape::new(&[4, 15, 384], DType::F32);
1486 let r = concat_shape(&[&a, &b], 2).unwrap();
1487 assert_eq!(r, Shape::new(&[4, 15, 768], DType::F32));
1488 }
1489
1490 #[test]
1491 fn gather_embedding() {
1492 let table = Shape::new(&[30522, 384], DType::F32);
1493 let indices = Shape::new(&[4, 15], DType::I64);
1494 let r = gather_shape(&table, &indices, 0).unwrap();
1495 assert_eq!(
1496 r,
1497 Shape::from_dims(
1498 &[Dim::Static(4), Dim::Static(15), Dim::Static(384)],
1499 DType::F32
1500 )
1501 );
1502 }
1503
1504 #[test]
1505 fn gather_nonzero_axis_keeps_prefix() {
1506 let table = Shape::new(&[4, 3], DType::F32);
1510 let indices = Shape::new(&[6], DType::I64);
1511 let r = gather_shape(&table, &indices, 1).unwrap();
1512 assert_eq!(r, Shape::new(&[4, 6], DType::F32));
1513 let table = Shape::new(&[2, 5, 7], DType::F32);
1515 let indices = Shape::new(&[3, 4], DType::I64);
1516 let r = gather_shape(&table, &indices, 1).unwrap();
1517 assert_eq!(r, Shape::new(&[2, 3, 4, 7], DType::F32));
1518 }
1519
1520 #[test]
1521 fn reshape_with_neg1() {
1522 let a = Shape::new(&[4, 15, 384], DType::F32);
1523 let r = reshape_shape(&a, &[60, -1]).unwrap();
1524 assert_eq!(r, Shape::new(&[60, 384], DType::F32));
1525 }
1526
1527 #[test]
1528 fn transpose_basic() {
1529 let a = Shape::new(&[4, 15, 384], DType::F32);
1530 let r = transpose_shape(&a, &[0, 2, 1]).unwrap();
1531 assert_eq!(r, Shape::new(&[4, 384, 15], DType::F32));
1532 }
1533
1534 #[test]
1535 fn narrow_basic() {
1536 let a = Shape::new(&[4, 15, 1152], DType::F32);
1537 let r = narrow_shape(&a, 2, 384).unwrap();
1538 assert_eq!(r, Shape::new(&[4, 15, 384], DType::F32));
1539 }
1540
1541 #[test]
1542 fn compare_bool_output() {
1543 let a = Shape::new(&[4, 15], DType::F32);
1544 let b = Shape::new(&[4, 15], DType::F32);
1545 let r = compare_shape(&a, &b).unwrap();
1546 assert_eq!(r.dtype(), DType::Bool);
1547 assert_eq!(r.rank(), 2);
1548 }
1549
1550 #[test]
1553 fn dynamic_shape() {
1554 let s = Shape::from_dims(
1555 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1556 DType::F32,
1557 );
1558 assert_eq!(s.rank(), 3);
1559 assert_eq!(s.num_elements(), None);
1560 assert!(!s.is_static());
1561 assert_eq!(format!("{s}"), "[?0, ?1, 384] f32");
1562 }
1563
1564 #[test]
1565 fn dynamic_symbols_lists_distinct_dims() {
1566 let s = Shape::from_dims(
1567 &[
1568 Dim::Dynamic(1),
1569 Dim::Static(384),
1570 Dim::Dynamic(0),
1571 Dim::Dynamic(1),
1572 ],
1573 DType::F32,
1574 );
1575 assert_eq!(s.dynamic_symbols(), vec![0, 1]);
1576 }
1577
1578 #[test]
1579 fn bind_specializes_known_symbols() {
1580 let s = Shape::from_dims(
1581 &[Dim::Dynamic(0), Dim::Dynamic(1), Dim::Static(384)],
1582 DType::F32,
1583 );
1584 let mut b = DimBinding::new();
1585 b.set(0, 8);
1586 b.set(1, 64);
1587 let s2 = s.bind(&b);
1588 assert!(s2.is_static());
1589 assert_eq!(s2.num_elements(), Some(8 * 64 * 384));
1590 }
1591
1592 #[test]
1593 fn bind_leaves_unknown_symbols_alone() {
1594 let s = Shape::from_dims(&[Dim::Dynamic(0), Dim::Dynamic(99)], DType::F32);
1595 let mut b = DimBinding::new();
1596 b.set(0, 4);
1597 let s2 = s.bind(&b);
1598 assert!(!s2.is_static()); assert_eq!(s2.dynamic_symbols(), vec![99]);
1600 }
1601}
1602
1603#[derive(Debug, Clone, Default)]
1606pub struct DimBinding {
1607 map: std::collections::HashMap<u32, usize>,
1608}
1609
1610impl DimBinding {
1611 pub fn new() -> Self {
1612 Self::default()
1613 }
1614 pub fn set(&mut self, symbol: u32, size: usize) -> Option<usize> {
1615 self.map.insert(symbol, size)
1616 }
1617 pub fn get(&self, symbol: u32) -> Option<usize> {
1618 self.map.get(&symbol).copied()
1619 }
1620 pub fn is_empty(&self) -> bool {
1621 self.map.is_empty()
1622 }
1623 pub fn len(&self) -> usize {
1624 self.map.len()
1625 }
1626 pub fn iter(&self) -> impl Iterator<Item = (u32, usize)> + '_ {
1627 self.map.iter().map(|(&s, &n)| (s, n))
1628 }
1629}