1use crate::inference_context::InferenceContext;
43use crate::{DType, Device, Shape};
44use fuel_ir::shape::{Dim, Dims};
45use std::sync::Arc;
46
47#[derive(Clone, Debug)]
98pub struct Tensor {
99 pub(crate) inner: fuel_graph::NodeHandle,
104}
105
106impl Tensor {
107 pub fn from_f32(
129 data: impl Into<Arc<[f32]>>,
130 shape: impl Into<Shape>,
131 device: &crate::Device,
132 ) -> fuel_ir::Result<Self> {
133 Ok(Self {
134 inner: fuel_graph::NodeHandle::from_f32(data, shape, device.as_dyn())?,
135 })
136 }
137
138 pub fn graph_id(&self) -> fuel_graph::GraphId {
141 self.inner.graph_id()
142 }
143
144 pub fn graph(&self) -> &fuel_graph::SharedGraph {
147 self.inner.graph()
148 }
149
150 pub fn from_f32_on(
164 graph: &fuel_graph::SharedGraph,
165 data: impl Into<Arc<[f32]>>,
166 shape: impl Into<Shape>,
167 device: &crate::Device,
168 ) -> fuel_ir::Result<Self> {
169 Ok(Self {
170 inner: fuel_graph::NodeHandle::from_f32_on(graph, data, shape, device.as_dyn())?,
171 })
172 }
173
174 pub fn from_f64_on(
176 graph: &fuel_graph::SharedGraph,
177 data: impl Into<Arc<[f64]>>,
178 shape: impl Into<Shape>,
179 device: &crate::Device,
180 ) -> fuel_ir::Result<Self> {
181 Ok(Self {
182 inner: fuel_graph::NodeHandle::from_f64_on(graph, data, shape, device.as_dyn())?,
183 })
184 }
185
186 pub fn from_bf16_on(
190 graph: &fuel_graph::SharedGraph,
191 data: impl Into<Arc<[half::bf16]>>,
192 shape: impl Into<Shape>,
193 device: &crate::Device,
194 ) -> fuel_ir::Result<Self> {
195 Ok(Self {
196 inner: fuel_graph::NodeHandle::from_bf16_on(graph, data, shape, device.as_dyn())?,
197 })
198 }
199
200 pub fn from_f16_on(
202 graph: &fuel_graph::SharedGraph,
203 data: impl Into<Arc<[half::f16]>>,
204 shape: impl Into<Shape>,
205 device: &crate::Device,
206 ) -> fuel_ir::Result<Self> {
207 Ok(Self {
208 inner: fuel_graph::NodeHandle::from_f16_on(graph, data, shape, device.as_dyn())?,
209 })
210 }
211
212 pub fn from_u32_on(
215 graph: &fuel_graph::SharedGraph,
216 data: impl Into<Arc<[u32]>>,
217 shape: impl Into<Shape>,
218 device: &crate::Device,
219 ) -> fuel_ir::Result<Self> {
220 Ok(Self {
221 inner: fuel_graph::NodeHandle::from_u32_on(graph, data, shape, device.as_dyn())?,
222 })
223 }
224
225 pub fn from_f64(
232 data: impl Into<Arc<[f64]>>,
233 shape: impl Into<Shape>,
234 device: &crate::Device,
235 ) -> fuel_ir::Result<Self> {
236 Ok(Self {
237 inner: fuel_graph::NodeHandle::from_f64(data, shape, device.as_dyn())?,
238 })
239 }
240
241 pub fn from_bf16(
250 data: impl Into<Arc<[half::bf16]>>,
251 shape: impl Into<Shape>,
252 device: &crate::Device,
253 ) -> fuel_ir::Result<Self> {
254 Ok(Self {
255 inner: fuel_graph::NodeHandle::from_bf16(data, shape, device.as_dyn())?,
256 })
257 }
258
259 pub fn from_f16(
262 data: impl Into<Arc<[half::f16]>>,
263 shape: impl Into<Shape>,
264 device: &crate::Device,
265 ) -> fuel_ir::Result<Self> {
266 Ok(Self {
267 inner: fuel_graph::NodeHandle::from_f16(data, shape, device.as_dyn())?,
268 })
269 }
270
271 pub fn from_u32(
275 data: impl Into<Arc<[u32]>>,
276 shape: impl Into<Shape>,
277 device: &crate::Device,
278 ) -> fuel_ir::Result<Self> {
279 Ok(Self {
280 inner: fuel_graph::NodeHandle::from_u32(data, shape, device.as_dyn())?,
281 })
282 }
283
284 pub fn const_f32_like(
295 &self,
296 data: impl Into<Arc<[f32]>>,
297 shape: impl Into<Shape>,
298 ) -> std::result::Result<Self, fuel_ir::Error> {
299 Ok(Self {
300 inner: self.inner.const_f32_like(data, shape)?,
301 })
302 }
303
304 pub fn const_f16_like(
306 &self,
307 data: impl Into<Arc<[half::f16]>>,
308 shape: impl Into<Shape>,
309 ) -> std::result::Result<Self, fuel_ir::Error> {
310 Ok(Self {
311 inner: self.inner.const_f16_like(data, shape)?,
312 })
313 }
314
315 pub fn const_bf16_like(
319 &self,
320 data: impl Into<Arc<[half::bf16]>>,
321 shape: impl Into<Shape>,
322 ) -> std::result::Result<Self, fuel_ir::Error> {
323 Ok(Self {
324 inner: self.inner.const_bf16_like(data, shape)?,
325 })
326 }
327
328 pub fn const_like_dtype(
340 &self,
341 data: &[f32],
342 shape: impl Into<Shape>,
343 dtype: fuel_ir::DType,
344 ) -> std::result::Result<Self, fuel_ir::Error> {
345 match dtype {
346 fuel_ir::DType::F32 => self.const_f32_like(data.to_vec(), shape),
347 fuel_ir::DType::BF16 => {
348 let converted: Vec<half::bf16> =
349 data.iter().map(|&v| half::bf16::from_f32(v)).collect();
350 self.const_bf16_like(converted, shape)
351 }
352 other => Err(fuel_ir::Error::Msg(format!(
353 "const_like_dtype: unsupported activation dtype {other:?} \
354 (expected F32 or BF16)",
355 ))
356 .bt()),
357 }
358 }
359
360 pub fn into_graph_tensor(self) -> fuel_graph::NodeHandle {
364 self.inner
365 }
366
367 pub fn graph_tensor(&self) -> &fuel_graph::NodeHandle {
369 &self.inner
370 }
371
372 pub fn from_graph_tensor(t: fuel_graph::NodeHandle) -> Self {
376 Self { inner: t }
377 }
378
379 pub fn shape(&self) -> Shape {
383 self.inner.shape()
384 }
385
386 pub fn dtype(&self) -> DType {
388 self.inner.dtype()
389 }
390
391 pub fn graph_handle(&self) -> &fuel_graph::SharedGraph {
399 self.inner.graph()
400 }
401
402 pub fn node_id(&self) -> fuel_graph::NodeId {
404 self.inner.id()
405 }
406
407 pub fn rank(&self) -> usize {
409 self.inner.shape().dims().len()
410 }
411
412 pub fn elem_count(&self) -> usize {
414 self.inner.shape().elem_count()
415 }
416
417 pub fn numel(&self) -> usize {
419 self.elem_count()
420 }
421
422 pub fn dim<D: Dim>(&self, dim: D) -> std::result::Result<usize, fuel_ir::Error> {
426 let shape = self.inner.shape();
427 let dim = dim.to_index(&shape, "dim")?;
428 Ok(shape.dims()[dim])
429 }
430
431 pub fn add(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
436 self.check_strict_binary("add", other)?;
437 Ok(Self {
438 inner: self.inner.add(&other.inner),
439 })
440 }
441
442 pub fn sub(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
444 self.check_strict_binary("sub", other)?;
445 Ok(Self {
446 inner: self.inner.sub(&other.inner),
447 })
448 }
449
450 pub fn mul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
452 self.check_strict_binary("mul", other)?;
453 Ok(Self {
454 inner: self.inner.mul(&other.inner),
455 })
456 }
457
458 pub fn div(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
460 self.check_strict_binary("div", other)?;
461 Ok(Self {
462 inner: self.inner.div(&other.inner),
463 })
464 }
465
466 pub fn maximum(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
468 self.check_strict_binary("maximum", other)?;
469 Ok(Self {
470 inner: self.inner.maximum(&other.inner),
471 })
472 }
473
474 pub fn minimum(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
476 self.check_strict_binary("minimum", other)?;
477 Ok(Self {
478 inner: self.inner.minimum(&other.inner),
479 })
480 }
481
482 pub fn fmax_ieee(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
486 self.check_strict_binary("fmax_ieee", other)?;
487 Ok(Self {
488 inner: self.inner.fmax_ieee(&other.inner),
489 })
490 }
491
492 pub fn fmin_ieee(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
495 self.check_strict_binary("fmin_ieee", other)?;
496 Ok(Self {
497 inner: self.inner.fmin_ieee(&other.inner),
498 })
499 }
500
501 pub fn rem_trunc(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
505 self.check_strict_binary("rem_trunc", other)?;
506 Ok(Self {
507 inner: self.inner.rem_trunc(&other.inner),
508 })
509 }
510
511 pub fn trunc(&self) -> Self {
515 Self {
516 inner: self.inner.trunc(),
517 }
518 }
519
520 pub fn eq(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
525 self.check_strict_binary("eq", other)?;
526 Ok(Self {
527 inner: self.inner.eq(&other.inner),
528 })
529 }
530
531 pub fn ne(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
535 self.check_strict_binary("ne", other)?;
536 Ok(Self {
537 inner: self.inner.ne(&other.inner),
538 })
539 }
540
541 pub fn lt(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
545 self.check_strict_binary("lt", other)?;
546 Ok(Self {
547 inner: self.inner.lt(&other.inner),
548 })
549 }
550
551 pub fn le(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
554 self.check_strict_binary("le", other)?;
555 Ok(Self {
556 inner: self.inner.le(&other.inner),
557 })
558 }
559
560 pub fn gt(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
563 self.check_strict_binary("gt", other)?;
564 Ok(Self {
565 inner: self.inner.gt(&other.inner),
566 })
567 }
568
569 pub fn ge(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
574 self.check_strict_binary("ge", other)?;
575 Ok(Self {
576 inner: self.inner.ge(&other.inner),
577 })
578 }
579
580 pub fn where_cond(&self, a: &Self, b: &Self) -> std::result::Result<Self, fuel_ir::Error> {
588 if self.inner.dtype() != fuel_ir::DType::Bool {
595 return Err(fuel_ir::Error::Msg(format!(
596 "where_cond: cond mask must be Bool, got {:?} — cast a numeric \
597 mask to Bool explicitly (GAP-168(c))",
598 self.inner.dtype(),
599 ))
600 .bt());
601 }
602 if a.inner.dtype() != b.inner.dtype() {
603 return Err(fuel_ir::Error::Msg(format!(
604 "where_cond: branches must share dtype, got a={:?} b={:?}",
605 a.inner.dtype(),
606 b.inner.dtype(),
607 ))
608 .bt());
609 }
610 let cond_dims = self.inner.shape();
611 let a_dims = a.inner.shape();
612 let b_dims = b.inner.shape();
613 if a_dims.dims() != cond_dims.dims() || b_dims.dims() != cond_dims.dims() {
614 return Err(fuel_ir::Error::Msg(format!(
615 "where_cond: shapes must match cond, got cond={:?} a={:?} b={:?}",
616 cond_dims.dims(),
617 a_dims.dims(),
618 b_dims.dims(),
619 ))
620 .bt());
621 }
622 Ok(Self {
623 inner: self.inner.where_cond(&a.inner, &b.inner),
624 })
625 }
626
627 pub fn broadcast_add(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
631 self.check_broadcast_binary("broadcast_add", other)?;
632 Ok(Self {
633 inner: self.inner.broadcast_add(&other.inner),
634 })
635 }
636
637 pub fn broadcast_sub(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
639 self.check_broadcast_binary("broadcast_sub", other)?;
640 Ok(Self {
641 inner: self.inner.broadcast_sub(&other.inner),
642 })
643 }
644
645 pub fn broadcast_mul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
647 self.check_broadcast_binary("broadcast_mul", other)?;
648 Ok(Self {
649 inner: self.inner.broadcast_mul(&other.inner),
650 })
651 }
652
653 pub fn broadcast_div(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
655 self.check_broadcast_binary("broadcast_div", other)?;
656 Ok(Self {
657 inner: self.inner.broadcast_div(&other.inner),
658 })
659 }
660
661 fn check_strict_binary(
662 &self,
663 name: &'static str,
664 other: &Self,
665 ) -> std::result::Result<(), fuel_ir::Error> {
666 if self.inner.dtype() != other.inner.dtype() {
667 return Err(fuel_ir::Error::Msg(format!(
668 "{name}: dtype mismatch lhs={:?} rhs={:?}",
669 self.inner.dtype(),
670 other.inner.dtype(),
671 ))
672 .bt());
673 }
674 let a_shape = self.inner.shape();
675 let b_shape = other.inner.shape();
676 if a_shape.dims() != b_shape.dims() {
677 return Err(fuel_ir::Error::Msg(format!(
678 "{name}: shape mismatch lhs={:?} rhs={:?}",
679 a_shape.dims(),
680 b_shape.dims(),
681 ))
682 .bt());
683 }
684 Ok(())
685 }
686
687 fn check_broadcast_binary(
688 &self,
689 name: &'static str,
690 other: &Self,
691 ) -> std::result::Result<(), fuel_ir::Error> {
692 if self.inner.dtype() != other.inner.dtype() {
693 return Err(fuel_ir::Error::Msg(format!(
694 "{name}: dtype mismatch lhs={:?} rhs={:?}",
695 self.inner.dtype(),
696 other.inner.dtype(),
697 ))
698 .bt());
699 }
700 let a_shape = self.inner.shape();
701 let b_shape = other.inner.shape();
702 let a_dims = a_shape.dims();
703 let b_dims = b_shape.dims();
704 let rank = a_dims.len().max(b_dims.len());
707 for i in 0..rank {
708 let ad = a_dims
709 .get(a_dims.len().wrapping_sub(1 + i))
710 .copied()
711 .unwrap_or(1);
712 let bd = b_dims
713 .get(b_dims.len().wrapping_sub(1 + i))
714 .copied()
715 .unwrap_or(1);
716 if ad != bd && ad != 1 && bd != 1 {
717 return Err(fuel_ir::Error::Msg(format!(
718 "{name}: shapes {:?} and {:?} are not broadcast-compatible",
719 a_dims, b_dims,
720 ))
721 .bt());
722 }
723 }
724 Ok(())
725 }
726
727 pub fn neg(&self) -> Self {
731 Self {
732 inner: self.inner.neg(),
733 }
734 }
735
736 pub fn sqr(&self) -> Self {
738 Self {
739 inner: self.inner.sqr(),
740 }
741 }
742
743 pub fn sqrt(&self) -> Self {
745 Self {
746 inner: self.inner.sqrt(),
747 }
748 }
749
750 pub fn exp(&self) -> Self {
752 Self {
753 inner: self.inner.exp(),
754 }
755 }
756
757 pub fn log(&self) -> Self {
759 Self {
760 inner: self.inner.log(),
761 }
762 }
763
764 pub fn relu(&self) -> Self {
766 Self {
767 inner: self.inner.relu(),
768 }
769 }
770
771 pub fn silu(&self) -> Self {
773 Self {
774 inner: self.inner.silu(),
775 }
776 }
777
778 pub fn gelu_tanh(&self) -> Self {
780 Self {
781 inner: self.inner.gelu_tanh(),
782 }
783 }
784
785 pub fn sigmoid(&self) -> Self {
787 Self {
788 inner: self.inner.sigmoid(),
789 }
790 }
791
792 pub fn tanh(&self) -> Self {
794 Self {
795 inner: self.inner.tanh(),
796 }
797 }
798
799 pub fn sin(&self) -> Self {
801 Self {
802 inner: self.inner.sin(),
803 }
804 }
805
806 pub fn cos(&self) -> Self {
808 Self {
809 inner: self.inner.cos(),
810 }
811 }
812
813 pub fn step(&self) -> Self {
816 Self {
817 inner: self.inner.step(),
818 }
819 }
820
821 pub fn recip(&self) -> Self {
823 Self {
824 inner: self.inner.recip(),
825 }
826 }
827
828 pub fn abs(&self) -> Self {
830 Self {
831 inner: self.inner.abs(),
832 }
833 }
834
835 pub fn floor(&self) -> Self {
838 Self {
839 inner: self.inner.floor(),
840 }
841 }
842
843 pub fn ceil(&self) -> Self {
846 Self {
847 inner: self.inner.ceil(),
848 }
849 }
850
851 pub fn round(&self) -> Self {
856 Self {
857 inner: self.inner.round(),
858 }
859 }
860
861 pub fn sign(&self) -> Self {
865 Self {
866 inner: self.inner.sign(),
867 }
868 }
869
870 pub fn erf(&self) -> Self {
873 Self {
874 inner: self.inner.erf(),
875 }
876 }
877
878 pub fn gelu(&self) -> Self {
882 Self {
883 inner: self.inner.gelu(),
884 }
885 }
886
887 pub fn pow(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
893 Ok(Self {
894 inner: self.inner.pow(&other.inner)?,
895 })
896 }
897
898 pub fn rsqrt(&self) -> Self {
902 Self {
903 inner: self.inner.rsqrt(),
904 }
905 }
906
907 pub fn rem(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
913 Ok(Self {
914 inner: self.inner.rem(&other.inner)?,
915 })
916 }
917
918 pub fn flip<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
922 let shape = self.inner.shape();
923 let dim = dim.to_index(&shape, "flip")?;
924 Ok(Self {
925 inner: self.inner.flip(dim)?,
926 })
927 }
928
929 pub fn roll<D: Dim>(&self, dim: D, shift: i64) -> std::result::Result<Self, fuel_ir::Error> {
933 let shape = self.inner.shape();
934 let dim = dim.to_index(&shape, "roll")?;
935 Ok(Self {
936 inner: self.inner.roll(dim, shift)?,
937 })
938 }
939
940 pub fn cumsum<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
944 let shape = self.inner.shape();
945 let dim = dim.to_index(&shape, "cumsum")?;
946 Ok(Self {
947 inner: self.inner.cumsum(dim)?,
948 })
949 }
950
951 pub fn pad(
958 &self,
959 padding: Vec<(usize, usize)>,
960 mode: fuel_graph::PadMode,
961 value: f64,
962 ) -> std::result::Result<Self, fuel_ir::Error> {
963 Ok(Self {
964 inner: self.inner.pad(padding, mode, value)?,
965 })
966 }
967
968 pub fn powi(&self, n: i32) -> Self {
970 Self {
971 inner: self.inner.powi(n),
972 }
973 }
974
975 pub fn matmul(&self, other: &Self) -> std::result::Result<Self, fuel_ir::Error> {
981 let a_dims = self.inner.shape().dims().to_vec();
982 let b_dims = other.inner.shape().dims().to_vec();
983 if a_dims.len() < 2 || b_dims.len() < 2 {
984 return Err(fuel_ir::Error::Msg(format!(
985 "matmul: both operands must be rank >= 2, got lhs={a_dims:?} rhs={b_dims:?}",
986 ))
987 .bt());
988 }
989 let a_k = a_dims[a_dims.len() - 1];
990 let b_k = b_dims[b_dims.len() - 2];
991 if a_k != b_k {
992 return Err(fuel_ir::Error::Msg(format!(
993 "matmul: contracting dim mismatch lhs[..., M, {a_k}] vs rhs[..., {b_k}, N]",
994 ))
995 .bt());
996 }
997 Ok(Self {
998 inner: self.inner.matmul(&other.inner),
999 })
1000 }
1001
1002 pub fn matmul_dyn_m(
1009 &self,
1010 other: &Self,
1011 row_count: fuel_ir::DynScalar,
1012 ) -> std::result::Result<Self, fuel_ir::Error> {
1013 let a_dims = self.inner.shape().dims().to_vec();
1014 let b_dims = other.inner.shape().dims().to_vec();
1015 if a_dims.len() < 2 || b_dims.len() < 2 {
1016 return Err(fuel_ir::Error::Msg(format!(
1017 "matmul_dyn_m: both operands must be rank >= 2, got lhs={a_dims:?} rhs={b_dims:?}",
1018 ))
1019 .bt());
1020 }
1021 let a_k = a_dims[a_dims.len() - 1];
1022 let b_k = b_dims[b_dims.len() - 2];
1023 if a_k != b_k {
1024 return Err(fuel_ir::Error::Msg(format!(
1025 "matmul_dyn_m: contracting dim mismatch lhs[..., M, {a_k}] vs rhs[..., {b_k}, N]",
1026 ))
1027 .bt());
1028 }
1029 Ok(Self {
1030 inner: self.inner.matmul_dyn_m(&other.inner, row_count),
1031 })
1032 }
1033
1034 pub fn qmatmul(
1043 &self,
1044 weight_bytes: &Self,
1045 quant_type: fuel_graph::QuantType,
1046 k: usize,
1047 n: usize,
1048 ) -> std::result::Result<Self, fuel_ir::Error> {
1049 if self.inner.dtype() != fuel_ir::DType::F32 {
1050 return Err(fuel_ir::Error::Msg(format!(
1051 "qmatmul: activations must be F32, got {:?}",
1052 self.inner.dtype(),
1053 ))
1054 .bt());
1055 }
1056 if weight_bytes.inner.dtype() != fuel_ir::DType::U32 {
1057 return Err(fuel_ir::Error::Msg(format!(
1058 "qmatmul: weight_bytes must be U32 (raw block bytes reinterpreted), got {:?}",
1059 weight_bytes.inner.dtype(),
1060 ))
1061 .bt());
1062 }
1063 let a_shape = self.inner.shape();
1064 let a_dims = a_shape.dims();
1065 if a_dims.len() < 2 {
1066 return Err(fuel_ir::Error::Msg(format!(
1067 "qmatmul: activations must be rank >= 2, got {a_dims:?}",
1068 ))
1069 .bt());
1070 }
1071 if a_dims[a_dims.len() - 1] != k {
1072 return Err(fuel_ir::Error::Msg(format!(
1073 "qmatmul: last dim of activations ({}) must equal k ({k})",
1074 a_dims[a_dims.len() - 1],
1075 ))
1076 .bt());
1077 }
1078 let block_size = quant_type.elements_per_block();
1079 if !k.is_multiple_of(block_size) {
1080 return Err(fuel_ir::Error::Msg(format!(
1081 "qmatmul: k={k} must be a multiple of {quant_type:?}'s block size ({block_size})",
1082 ))
1083 .bt());
1084 }
1085 let expected_bytes = n * (k / block_size) * quant_type.bytes_per_block();
1086 let expected_u32_elems = expected_bytes / 4;
1087 let actual_elems = weight_bytes.inner.shape().elem_count();
1088 if actual_elems != expected_u32_elems {
1089 return Err(fuel_ir::Error::Msg(format!(
1090 "qmatmul: weight_bytes has {actual_elems} u32 elements, expected {expected_u32_elems} for N={n}, K={k}, {quant_type:?}",
1091 )).bt());
1092 }
1093 Ok(Self {
1094 inner: self.inner.qmatmul(&weight_bytes.inner, quant_type, k, n),
1095 })
1096 }
1097
1098 pub fn transpose(&self) -> std::result::Result<Self, fuel_ir::Error> {
1102 Ok(Self {
1103 inner: self.inner.try_transpose()?,
1104 })
1105 }
1106
1107 pub fn permute<D: Dims>(&self, axes: D) -> std::result::Result<Self, fuel_ir::Error> {
1111 let shape = self.inner.shape();
1112 let axes = axes.to_indexes(&shape, "permute")?;
1113 Ok(Self {
1114 inner: self.inner.try_permute(&axes)?,
1115 })
1116 }
1117
1118 pub fn reshape(&self, shape: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
1121 Ok(Self {
1122 inner: self.inner.try_reshape(shape)?,
1123 })
1124 }
1125
1126 pub fn squeeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1134 let shape = self.inner.shape();
1135 let dim = dim.to_index(&shape, "squeeze")?;
1136 Ok(Self {
1137 inner: self.inner.squeeze(dim)?,
1138 })
1139 }
1140
1141 pub fn broadcast_to(
1144 &self,
1145 shape: impl Into<Shape>,
1146 ) -> std::result::Result<Self, fuel_ir::Error> {
1147 Ok(Self {
1148 inner: self.inner.try_broadcast_to(shape)?,
1149 })
1150 }
1151
1152 pub fn layer_norm_affine(
1162 &self,
1163 gain: std::sync::Arc<[f32]>,
1164 bias: std::sync::Arc<[f32]>,
1165 eps: f64,
1166 ) -> std::result::Result<Self, fuel_ir::Error> {
1167 let hidden = gain.len();
1168 debug_assert_eq!(
1169 bias.len(),
1170 hidden,
1171 "layer_norm_affine: gain ({}) and bias ({}) must have the same length",
1172 gain.len(),
1173 bias.len()
1174 );
1175 let normed = self.layer_norm_last_dim(eps)?;
1176 let dims_v: Vec<usize> = self.inner.shape().dims().to_vec();
1177 let mut affine_shape = vec![1_usize; dims_v.len()];
1178 affine_shape[dims_v.len() - 1] = hidden;
1179 let bc_shape = Shape::from_dims(&dims_v);
1180 let g = normed
1181 .const_f32_like(gain, Shape::from_dims(&[hidden]))?
1182 .reshape(Shape::from_dims(&affine_shape))?
1183 .broadcast_to(bc_shape.clone())?;
1184 let b = normed
1185 .const_f32_like(bias, Shape::from_dims(&[hidden]))?
1186 .reshape(Shape::from_dims(&affine_shape))?
1187 .broadcast_to(bc_shape)?;
1188 normed.mul(&g)?.add(&b)
1189 }
1190
1191 pub fn l2_normalize<D: Dim>(
1199 &self,
1200 dim: D,
1201 eps: f64,
1202 ) -> std::result::Result<Self, fuel_ir::Error> {
1203 let sq = self.sqr();
1204 let summed = sq.sum_keepdim(dim)?;
1205 let with_eps = if eps == 0.0 {
1206 summed
1207 } else {
1208 summed.add_scalar(eps)
1209 };
1210 let l2 = with_eps.sqrt();
1211 let dims_v: Vec<usize> = self.inner.shape().dims().to_vec();
1212 let l2_bc = l2.broadcast_to(Shape::from_dims(&dims_v))?;
1213 self.div(&l2_bc)
1214 }
1215
1216 pub fn repeat_interleave<D: Dim>(
1224 &self,
1225 dim: D,
1226 repeats: usize,
1227 ) -> std::result::Result<Self, fuel_ir::Error> {
1228 let shape = self.inner.shape();
1229 let dim = dim.to_index(&shape, "repeat_interleave")?;
1230 if repeats == 0 {
1231 return Err(fuel_ir::Error::Msg("repeat_interleave: repeats must be ≥ 1".into()).bt());
1232 }
1233 if repeats == 1 {
1234 return Ok(self.clone());
1235 }
1236 let dims_v: Vec<usize> = shape.dims().to_vec();
1237 let mut unsq_shape = dims_v.clone();
1238 unsq_shape.insert(dim + 1, 1);
1239 let mut bc_shape = unsq_shape.clone();
1240 bc_shape[dim + 1] = repeats;
1241 let unsq = self.reshape(Shape::from_dims(&unsq_shape))?;
1242 let bc = unsq.broadcast_to(Shape::from_dims(&bc_shape))?;
1243 let mut out_shape = dims_v.clone();
1244 out_shape[dim] *= repeats;
1245 bc.reshape(Shape::from_dims(&out_shape))
1246 }
1247
1248 pub fn slice<D: Dim>(
1252 &self,
1253 dim: D,
1254 start: usize,
1255 len: usize,
1256 ) -> std::result::Result<Self, fuel_ir::Error> {
1257 let shape = self.inner.shape();
1258 let dim = dim.to_index(&shape, "slice")?;
1259 let dim_size = shape.dims()[dim];
1260 if start.saturating_add(len) > dim_size {
1261 return Err(fuel_ir::Error::Msg(format!(
1262 "slice: start={start} + len={len} exceeds dim {dim} size {dim_size}",
1263 ))
1264 .bt());
1265 }
1266 Ok(Self {
1267 inner: self.inner.slice(dim, start, len),
1268 })
1269 }
1270
1271 pub fn concat<D: Dim>(
1274 &self,
1275 other: &Self,
1276 dim: D,
1277 ) -> std::result::Result<Self, fuel_ir::Error> {
1278 let shape = self.inner.shape();
1279 let dim = dim.to_index(&shape, "concat")?;
1280 let self_dims = shape.dims().to_vec();
1281 let other_dims = other.inner.shape().dims().to_vec();
1282 if self_dims.len() != other_dims.len() {
1283 return Err(fuel_ir::Error::Msg(format!(
1284 "concat: rank mismatch lhs={self_dims:?} rhs={other_dims:?}",
1285 ))
1286 .bt());
1287 }
1288 for (i, (&a, &b)) in self_dims.iter().zip(other_dims.iter()).enumerate() {
1289 if i != dim && a != b {
1290 return Err(fuel_ir::Error::Msg(format!(
1291 "concat: dim {i} mismatch lhs={a} rhs={b} (concat dim is {dim})",
1292 ))
1293 .bt());
1294 }
1295 }
1296 Ok(Self {
1297 inner: self.inner.concat(&other.inner, dim),
1298 })
1299 }
1300
1301 pub fn add_scalar(&self, c: f64) -> Self {
1303 Self {
1304 inner: self.inner.add_scalar(c),
1305 }
1306 }
1307
1308 pub fn mul_scalar(&self, c: f64) -> Self {
1310 Self {
1311 inner: self.inner.mul_scalar(c),
1312 }
1313 }
1314
1315 pub fn argmax_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1319 let shape = self.inner.shape();
1320 let dim = dim.to_index(&shape, "argmax_dim")?;
1321 Ok(Self {
1322 inner: self.inner.argmax_dim(dim),
1323 })
1324 }
1325
1326 pub fn realize_u32(&self) -> Vec<u32> {
1332 let graph = self.inner.graph().clone();
1333 let target = self.inner.id();
1334 let device = crate::Device::cpu();
1335 crate::pipelined_bridge::realize_one_as::<u32>(&graph, target, &device)
1336 .expect("realize_u32 via PipelinedExecutor")
1337 }
1338
1339 pub fn realize_u8(&self) -> Vec<u8> {
1348 let graph = self.inner.graph().clone();
1349 let target = self.inner.id();
1350 let device = crate::Device::cpu();
1351 crate::pipelined_bridge::realize_one_bytes(&graph, target, &device)
1352 .expect("realize_u8 via PipelinedExecutor")
1353 }
1354
1355 pub fn sum_all(&self) -> Self {
1359 Self {
1360 inner: self.inner.sum_all(),
1361 }
1362 }
1363
1364 pub fn mean_all(&self) -> Self {
1366 Self {
1367 inner: self.inner.mean_all(),
1368 }
1369 }
1370
1371 pub fn max_all(&self) -> Self {
1373 Self {
1374 inner: self.inner.max_all(),
1375 }
1376 }
1377
1378 pub fn min_all(&self) -> Self {
1380 Self {
1381 inner: self.inner.min_all(),
1382 }
1383 }
1384
1385 pub fn sum_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1388 let shape = self.inner.shape();
1389 let dim = dim.to_index(&shape, "sum_dim")?;
1390 Ok(Self {
1391 inner: self.inner.sum_dim(dim),
1392 })
1393 }
1394
1395 pub fn max_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1397 let shape = self.inner.shape();
1398 let dim = dim.to_index(&shape, "max_dim")?;
1399 Ok(Self {
1400 inner: self.inner.max_dim(dim),
1401 })
1402 }
1403
1404 pub fn min_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1406 let shape = self.inner.shape();
1407 let dim = dim.to_index(&shape, "min_dim")?;
1408 Ok(Self {
1409 inner: self.inner.min_dim(dim),
1410 })
1411 }
1412
1413 pub fn clamp(&self, min: f64, max: f64) -> Self {
1415 Self {
1416 inner: self.inner.clamp(min, max),
1417 }
1418 }
1419
1420 pub fn mean_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
1422 let shape = self.inner.shape();
1423 let dim = dim.to_index(&shape, "mean_dim")?;
1424 Ok(Self {
1425 inner: self.inner.mean_dim(dim),
1426 })
1427 }
1428
1429 pub fn reduce_sum_to(&self, target: impl Into<Shape>) -> Self {
1433 Self {
1434 inner: self.inner.reduce_sum_to(target),
1435 }
1436 }
1437
1438 pub fn reduce_max_to(&self, target: impl Into<Shape>) -> Self {
1441 Self {
1442 inner: self.inner.reduce_max_to(target),
1443 }
1444 }
1445
1446 pub fn softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
1451 let shape = self.inner.shape();
1452 let dims = shape.dims();
1453 if dims.is_empty() {
1454 return Err(fuel_ir::Error::Msg(
1455 "softmax_last_dim: input must be rank >= 1, got scalar".into(),
1456 )
1457 .bt());
1458 }
1459 Ok(Self {
1460 inner: self.inner.softmax_last_dim(),
1461 })
1462 }
1463
1464 pub fn nf4_matmul(&self, w_packed: &Self, absmax: &Self, block_size: usize) -> Self {
1468 Self {
1469 inner: self
1470 .inner
1471 .nf4_matmul(&w_packed.inner, &absmax.inner, block_size),
1472 }
1473 }
1474
1475 pub fn ssd_chunk_scan(
1480 &self,
1481 dt: &Self,
1482 a: &Self,
1483 b: &Self,
1484 c: &Self,
1485 chunk_size: usize,
1486 ) -> Self {
1487 Self {
1488 inner: self
1489 .inner
1490 .ssd_chunk_scan(&dt.inner, &a.inner, &b.inner, &c.inner, chunk_size),
1491 }
1492 }
1493
1494 pub fn selective_scan(
1500 &self,
1501 delta: &Self,
1502 a: &Self,
1503 b: &Self,
1504 c: &Self,
1505 delta_softplus: bool,
1506 ) -> Self {
1507 Self {
1508 inner: self.inner.selective_scan(
1509 &delta.inner,
1510 &a.inner,
1511 &b.inner,
1512 &c.inner,
1513 delta_softplus,
1514 ),
1515 }
1516 }
1517
1518 pub fn selective_scan_bundled(
1526 &self,
1527 delta: &Self,
1528 a: &Self,
1529 b: &Self,
1530 c: &Self,
1531 delta_softplus: bool,
1532 ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1533 let (y, last_state) = self.inner.selective_scan_bundled(
1534 &delta.inner,
1535 &a.inner,
1536 &b.inner,
1537 &c.inner,
1538 delta_softplus,
1539 )?;
1540 Ok((Self { inner: y }, Self { inner: last_state }))
1541 }
1542
1543 pub fn ssd_chunk_scan_bundled(
1547 &self,
1548 dt: &Self,
1549 a: &Self,
1550 b: &Self,
1551 c: &Self,
1552 chunk_size: usize,
1553 ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1554 let (y, last_state) = self
1555 .inner
1556 .ssd_chunk_scan_bundled(&dt.inner, &a.inner, &b.inner, &c.inner, chunk_size)?;
1557 Ok((Self { inner: y }, Self { inner: last_state }))
1558 }
1559
1560 pub fn nonzero_indices_bundled(
1572 &self,
1573 count_sym: fuel_ir::SymId,
1574 ) -> std::result::Result<(Self, Self), fuel_ir::Error> {
1575 let (indices, count) = self.inner.nonzero_indices_bundled(count_sym)?;
1576 Ok((Self { inner: indices }, Self { inner: count }))
1577 }
1578
1579 pub fn fresh_data_determined_sym(&self) -> fuel_ir::SymId {
1587 let graph = self.inner.graph();
1588 let g = graph.read().unwrap();
1589 g.next_data_determined_sym()
1590 }
1591
1592 pub fn causal_conv1d(&self, weight: &Self, bias: &Self, use_silu: bool) -> Self {
1597 Self {
1598 inner: self
1599 .inner
1600 .causal_conv1d(&weight.inner, &bias.inner, use_silu),
1601 }
1602 }
1603
1604 pub fn fused_softmax_cross_entropy(
1609 &self,
1610 targets: &Self,
1611 reduction: fuel_graph::registry::Reduction,
1612 ignore_index: i64,
1613 ) -> Self {
1614 Self {
1615 inner: self
1616 .inner
1617 .fused_softmax_cross_entropy(&targets.inner, reduction, ignore_index),
1618 }
1619 }
1620
1621 pub fn layer_norm_last_dim(&self, eps: f64) -> std::result::Result<Self, fuel_ir::Error> {
1624 let shape = self.inner.shape();
1625 let dims = shape.dims();
1626 if dims.last().copied().unwrap_or(0) == 0 {
1627 return Err(fuel_ir::Error::Msg(format!(
1628 "layer_norm_last_dim: input must have non-zero last dim, got {dims:?}",
1629 ))
1630 .bt());
1631 }
1632 Ok(Self {
1633 inner: self.inner.layer_norm_last_dim(eps),
1634 })
1635 }
1636
1637 pub fn rms_norm_last_dim(&self, eps: f64) -> std::result::Result<Self, fuel_ir::Error> {
1640 let shape = self.inner.shape();
1641 let dims = shape.dims();
1642 if dims.last().copied().unwrap_or(0) == 0 {
1643 return Err(fuel_ir::Error::Msg(format!(
1644 "rms_norm_last_dim: input must have non-zero last dim, got {dims:?}",
1645 ))
1646 .bt());
1647 }
1648 Ok(Self {
1649 inner: self.inner.rms_norm_last_dim(eps),
1650 })
1651 }
1652
1653 pub fn rope(&self, base: f64, start_pos: usize) -> std::result::Result<Self, fuel_ir::Error> {
1656 let shape = self.inner.shape();
1657 let dims = shape.dims();
1658 if dims.len() < 2 {
1659 return Err(fuel_ir::Error::Msg(format!(
1660 "rope: input must have rank >= 2, got {dims:?}",
1661 ))
1662 .bt());
1663 }
1664 Ok(Self {
1665 inner: self.inner.rope(base, start_pos),
1666 })
1667 }
1668
1669 pub fn rope_with_tables(
1676 &self,
1677 cos: &Self,
1678 sin: &Self,
1679 ) -> std::result::Result<Self, fuel_ir::Error> {
1680 if self.inner.dtype() != fuel_ir::DType::F32 {
1681 return Err(fuel_ir::Error::Msg(format!(
1682 "rope: only f32 is supported today, got {:?} (cast explicitly for other dtypes)",
1683 self.inner.dtype(),
1684 ))
1685 .bt());
1686 }
1687 let in_shape = self.inner.shape();
1688 let dims = in_shape.dims();
1689 let rank = dims.len();
1690 if rank < 2 {
1691 return Err(fuel_ir::Error::Msg(format!(
1692 "rope: input must have rank >= 2, got {dims:?}",
1693 ))
1694 .bt());
1695 }
1696 let seq = dims[rank - 2];
1697 let d = dims[rank - 1];
1698 if !d.is_multiple_of(2) {
1699 return Err(fuel_ir::Error::Msg(format!("rope: feature dim {d} must be even",)).bt());
1700 }
1701 let cos_shape = cos.inner.shape();
1702 let cos_dims = cos_shape.dims();
1703 if cos_dims != [seq, d] {
1704 return Err(fuel_ir::Error::Msg(format!(
1705 "rope_with_tables: cos shape {cos_dims:?} does not match [seq, d] = [{seq}, {d}]",
1706 ))
1707 .bt());
1708 }
1709 let sin_shape = sin.inner.shape();
1710 let sin_dims = sin_shape.dims();
1711 if sin_dims != [seq, d] {
1712 return Err(fuel_ir::Error::Msg(format!(
1713 "rope_with_tables: sin shape {sin_dims:?} does not match [seq, d] = [{seq}, {d}]",
1714 ))
1715 .bt());
1716 }
1717 Ok(Self {
1718 inner: self.inner.rope_with_tables(&cos.inner, &sin.inner),
1719 })
1720 }
1721
1722 pub fn rope_with_tables_decomposed(
1739 &self,
1740 cos: &Self,
1741 sin: &Self,
1742 ) -> std::result::Result<Self, fuel_ir::Error> {
1743 if self.inner.dtype() != fuel_ir::DType::F32 {
1744 return Err(fuel_ir::Error::Msg(format!(
1745 "rope: only f32 is supported today, got {:?} (cast explicitly for other dtypes)",
1746 self.inner.dtype(),
1747 ))
1748 .bt());
1749 }
1750 let in_shape = self.inner.shape();
1751 let dims = in_shape.dims();
1752 let rank = dims.len();
1753 if rank < 2 {
1754 return Err(fuel_ir::Error::Msg(format!(
1755 "rope: input must have rank >= 2, got {dims:?}",
1756 ))
1757 .bt());
1758 }
1759 let seq = dims[rank - 2];
1760 let d = dims[rank - 1];
1761 if !d.is_multiple_of(2) {
1762 return Err(fuel_ir::Error::Msg(format!("rope: feature dim {d} must be even",)).bt());
1763 }
1764 let cos_dims = cos.inner.shape();
1765 if cos_dims.dims() != [seq, d] {
1766 return Err(fuel_ir::Error::Msg(format!(
1767 "rope_with_tables_decomposed: cos shape {:?} does not match [seq, d] = [{seq}, {d}]",
1768 cos_dims.dims(),
1769 )).bt());
1770 }
1771 let sin_dims = sin.inner.shape();
1772 if sin_dims.dims() != [seq, d] {
1773 return Err(fuel_ir::Error::Msg(format!(
1774 "rope_with_tables_decomposed: sin shape {:?} does not match [seq, d] = [{seq}, {d}]",
1775 sin_dims.dims(),
1776 )).bt());
1777 }
1778 Ok(Self {
1779 inner: self
1780 .inner
1781 .rope_with_tables_decomposed(&cos.inner, &sin.inner),
1782 })
1783 }
1784
1785 pub fn index_select<D: Dim>(
1791 &self,
1792 dim: D,
1793 indices: &Self,
1794 ) -> std::result::Result<Self, fuel_ir::Error> {
1795 let shape = self.inner.shape();
1796 let dim = dim.to_index(&shape, "index_select")?;
1797 if indices.inner.dtype() != fuel_ir::DType::U32 {
1798 return Err(fuel_ir::Error::Msg(format!(
1799 "index_select: index tensor must be U32, got {:?}",
1800 indices.inner.dtype(),
1801 ))
1802 .bt());
1803 }
1804 let idx_shape = indices.inner.shape();
1805 let idx_dims = idx_shape.dims();
1806 if idx_dims.len() != 1 {
1807 return Err(fuel_ir::Error::Msg(format!(
1808 "index_select: index tensor must be rank 1, got {idx_dims:?}",
1809 ))
1810 .bt());
1811 }
1812 Ok(Self {
1813 inner: self.inner.index_select(dim, &indices.inner),
1814 })
1815 }
1816
1817 pub fn gather<D: Dim>(
1822 &self,
1823 dim: D,
1824 indices: &Self,
1825 ) -> std::result::Result<Self, fuel_ir::Error> {
1826 let shape = self.inner.shape();
1827 let dim = dim.to_index(&shape, "gather")?;
1828 if indices.inner.dtype() != fuel_ir::DType::U32 {
1829 return Err(fuel_ir::Error::Msg(format!(
1830 "gather: index tensor must be U32, got {:?}",
1831 indices.inner.dtype(),
1832 ))
1833 .bt());
1834 }
1835 let data_rank = shape.dims().len();
1836 let idx_shape = indices.inner.shape();
1837 let idx_rank = idx_shape.dims().len();
1838 if data_rank != idx_rank {
1839 return Err(fuel_ir::Error::Msg(format!(
1840 "gather: data and index must have the same rank, got {data_rank} vs {idx_rank}",
1841 ))
1842 .bt());
1843 }
1844 Ok(Self {
1845 inner: self.inner.gather(dim, &indices.inner),
1846 })
1847 }
1848
1849 pub fn to_dtype(&self, dtype: DType) -> std::result::Result<Self, fuel_ir::Error> {
1861 if self.inner.dtype() == dtype {
1862 return Ok(self.clone());
1863 }
1864 Ok(Self {
1865 inner: self.inner.cast(dtype),
1866 })
1867 }
1868
1869 pub fn detach(&self) -> Self {
1876 self.clone()
1877 }
1878
1879 pub fn track_op(&self) -> bool {
1883 true
1884 }
1885
1886 pub fn realize_f32(&self) -> Vec<f32> {
1930 let graph = self.inner.graph().clone();
1931 let target = self.inner.id();
1932 let device = crate::Device::cpu();
1933 crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device)
1934 .expect("realize_f32 via PipelinedExecutor")
1935 }
1936
1937 pub fn realize_f32_reference(&self) -> Vec<f32> {
1953 let graph = self.inner.graph().clone();
1954 let target = self.inner.id();
1955 let device = crate::Device::cpu();
1956 crate::pipelined_bridge::realize_one_reference_as::<f32>(&graph, target, &device)
1957 .expect("realize_f32_reference via PipelinedExecutor")
1958 }
1959
1960 pub fn realize_f64(&self) -> Vec<f64> {
1973 let graph = self.inner.graph().clone();
1974 let target = self.inner.id();
1975 let device = crate::Device::cpu();
1976 crate::pipelined_bridge::realize_one_as::<f64>(&graph, target, &device)
1977 .expect("realize_f64 via PipelinedExecutor")
1978 }
1979
1980 pub fn realize_bf16(&self) -> Vec<half::bf16> {
1983 let graph = self.inner.graph().clone();
1984 let target = self.inner.id();
1985 let device = crate::Device::cpu();
1986 crate::pipelined_bridge::realize_one_as::<half::bf16>(&graph, target, &device)
1987 .expect("realize_bf16 via PipelinedExecutor")
1988 }
1989
1990 pub fn realize_f16(&self) -> Vec<half::f16> {
1993 let graph = self.inner.graph().clone();
1994 let target = self.inner.id();
1995 let device = crate::Device::cpu();
1996 crate::pipelined_bridge::realize_one_as::<half::f16>(&graph, target, &device)
1997 .expect("realize_f16 via PipelinedExecutor")
1998 }
1999
2000 #[cfg(feature = "cuda")]
2009 pub fn realize_f32_cuda(&self, device: &fuel_cuda_backend::CudaDevice) -> Vec<f32> {
2010 let graph = self.inner.graph().clone();
2011 let target = self.inner.id();
2012 let fc_device: crate::Device = device.clone().into();
2013 crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &fc_device)
2014 .expect("realize_f32_cuda via PipelinedExecutor")
2015 }
2016
2017 #[cfg(feature = "vulkan")]
2025 pub fn realize_f32_vulkan(
2026 &self,
2027 backend: &std::sync::Arc<fuel_vulkan_backend::VulkanBackend>,
2028 ) -> Vec<f32> {
2029 let graph = self.inner.graph().clone();
2030 let target = self.inner.id();
2031 let fc_device: crate::Device = backend.clone().into();
2032 crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &fc_device)
2033 .expect("realize_f32_vulkan via PipelinedExecutor")
2034 }
2035}
2036
2037pub fn realize_many_f32(tensors: &[&Tensor]) -> Vec<Vec<f32>> {
2039 if tensors.is_empty() {
2040 return Vec::new();
2041 }
2042 let graph = tensors[0].inner.graph().clone();
2043 let targets: Vec<fuel_graph::NodeId> = tensors.iter().map(|t| t.inner.id()).collect();
2044 let device = crate::Device::cpu();
2045 crate::pipelined_bridge::realize_many_as::<f32>(&graph, &targets, &device)
2046 .expect("realize_many_f32 via PipelinedExecutor")
2047}
2048
2049#[cfg(feature = "cuda")]
2052pub fn realize_many_f32_cuda(
2053 tensors: &[&Tensor],
2054 device: &fuel_cuda_backend::CudaDevice,
2055) -> Vec<Vec<f32>> {
2056 if tensors.is_empty() {
2057 return Vec::new();
2058 }
2059 let graph = tensors[0].inner.graph().clone();
2060 let targets: Vec<fuel_graph::NodeId> = tensors.iter().map(|t| t.inner.id()).collect();
2061 let fc_device: crate::Device = device.clone().into();
2062 crate::pipelined_bridge::realize_many_as::<f32>(&graph, &targets, &fc_device)
2063 .expect("realize_many_f32_cuda via PipelinedExecutor")
2064}
2065
2066#[cfg(test)]
2067mod tests {
2068 use super::*;
2069 use crate::Shape;
2070
2071 #[test]
2072 fn constructors_wrap_graph_tensor_correctly() {
2073 let t =
2074 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2075 assert_eq!(t.dtype(), DType::F32);
2076 assert_eq!(t.shape().dims(), &[3]);
2077 assert_eq!(t.rank(), 1);
2078 assert_eq!(t.elem_count(), 3);
2079 }
2080
2081 #[test]
2082 fn add_builds_add_node_in_underlying_graph() {
2083 let a =
2084 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2085 let b = a
2086 .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
2087 .unwrap();
2088 let c = a.add(&b).unwrap();
2089 assert_eq!(c.shape().dims(), &[3]);
2090 assert!(std::sync::Arc::ptr_eq(
2093 c.graph_tensor().graph(),
2094 a.graph_tensor().graph(),
2095 ));
2096 }
2097
2098 #[test]
2099 fn chained_lazy_method_call_builds_sensible_graph() {
2100 let x = Tensor::from_f32(
2105 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
2106 Shape::from_dims(&[2, 3]),
2107 &Device::cpu(),
2108 )
2109 .unwrap();
2110 let w = x
2111 .const_f32_like(
2112 vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0],
2113 Shape::from_dims(&[3, 3]),
2114 )
2115 .unwrap();
2116 let y = x
2117 .rms_norm_last_dim(1e-6)
2118 .unwrap()
2119 .matmul(&w)
2120 .unwrap()
2121 .relu();
2122 assert_eq!(y.shape().dims(), &[2, 3]);
2123 assert_eq!(y.dtype(), DType::F32);
2124 }
2125
2126 #[test]
2127 fn rope_through_lazy_wrapper() {
2128 let x = Tensor::from_f32(
2130 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
2131 Shape::from_dims(&[2, 4]),
2132 &Device::cpu(),
2133 )
2134 .unwrap();
2135 let y = x.rope(10000.0, 0).unwrap();
2136 assert_eq!(y.shape().dims(), &[2, 4]);
2137 assert_eq!(y.dtype(), DType::F32);
2138 }
2139
2140 #[test]
2141 fn rope_delta_rotate_equals_direct_shift() {
2142 let dev = Device::cpu();
2148 let (n_kv_heads, head_dim, bs) = (2usize, 4usize, 4usize);
2149 let theta = 10000.0;
2150 let (p0, m) = (0usize, 8usize); let raw: Vec<f32> = (0..bs * n_kv_heads * head_dim)
2152 .map(|i| (i as f32 * 0.017).sin())
2153 .collect();
2154 let rope_pool_at = |data: &[f32], start_pos: usize| -> Vec<f32> {
2158 let k = Tensor::from_f32(
2159 data.to_vec(),
2160 Shape::from_dims(&[bs, n_kv_heads, head_dim]),
2161 &dev,
2162 )
2163 .unwrap();
2164 let k4 = k
2165 .permute([1, 0, 2])
2166 .unwrap()
2167 .reshape(Shape::from_dims(&[1, n_kv_heads, bs, head_dim]))
2168 .unwrap();
2169 let (c, s) = k4.rope_tables_const(theta, start_pos, bs, head_dim);
2170 k4.rope_with_tables_decomposed(&c, &s)
2171 .unwrap()
2172 .reshape(Shape::from_dims(&[n_kv_heads, bs, head_dim]))
2173 .unwrap()
2174 .permute([1, 0, 2])
2175 .unwrap()
2176 .realize_f32()
2177 };
2178 let direct = rope_pool_at(&raw, p0 + m); let at_p0 = rope_pool_at(&raw, p0); let shifted =
2181 Tensor::rope_delta_rotate_block_f32(&dev, &at_p0, theta, m, bs, n_kv_heads, head_dim);
2182 let maxdiff = direct
2183 .iter()
2184 .zip(&shifted)
2185 .map(|(a, b)| (a - b).abs())
2186 .fold(0.0f32, f32::max);
2187 assert!(
2188 maxdiff < 1e-5,
2189 "delta-rotate == direct shift (maxdiff {maxdiff})"
2190 );
2191 }
2192
2193 #[test]
2194 fn cast_switches_dtype_through_wrapper() {
2195 let x =
2196 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2197 let y = x.to_dtype(DType::F64).unwrap();
2198 assert_eq!(y.dtype(), DType::F64);
2199 assert_eq!(y.shape().dims(), &[3]);
2200 }
2201
2202 #[test]
2203 fn indexing_builds_correct_output_shape() {
2204 let data =
2205 Tensor::from_f32(vec![1.0; 12], Shape::from_dims(&[3, 4]), &Device::cpu()).unwrap();
2206 let idx = data
2207 .const_u32_like(vec![0, 2, 1], Shape::from_dims(&[3]))
2208 .unwrap();
2209 let out = data.index_select(0, &idx).unwrap();
2210 assert_eq!(out.shape().dims(), &[3, 4]);
2211 }
2212
2213 #[test]
2216 fn realize_f32_executes_the_underlying_graph() {
2217 let a =
2221 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
2222 let b = a
2223 .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
2224 .unwrap();
2225 let c = a.add(&b).unwrap().mul(&a).unwrap();
2226 let result = c.realize_f32();
2227 assert_eq!(result, vec![5.0, 14.0, 27.0]);
2228 }
2229
2230 #[test]
2231 fn realize_f32_relu_and_maximum_propagate_nan_end_to_end() {
2232 let a = Tensor::from_f32(
2238 vec![f32::NAN, -2.0, 3.0],
2239 Shape::from_dims(&[3]),
2240 &Device::cpu(),
2241 )
2242 .unwrap();
2243 let relu_result = a.relu().realize_f32();
2244 assert!(
2245 relu_result[0].is_nan(),
2246 "relu(NaN) must propagate NaN through realize, got {}",
2247 relu_result[0]
2248 );
2249 assert_eq!(relu_result[1], 0.0, "relu(-2) must clip to 0");
2250 assert_eq!(relu_result[2], 3.0, "relu(3) must pass through");
2251
2252 let b = a
2253 .const_f32_like(vec![1.0, f32::NAN, 2.0], Shape::from_dims(&[3]))
2254 .unwrap();
2255 let max_result = a.maximum(&b).unwrap().realize_f32();
2256 assert!(
2257 max_result[0].is_nan(),
2258 "maximum(NaN, 1) must propagate NaN through realize, got {}",
2259 max_result[0]
2260 );
2261 assert!(
2262 max_result[1].is_nan(),
2263 "maximum(-2, NaN) must propagate NaN through realize, got {}",
2264 max_result[1]
2265 );
2266 assert_eq!(max_result[2], 3.0, "maximum(3, 2) non-NaN sanity");
2267 }
2268
2269 #[test]
2275 fn fmax_fmin_ieee_suppress_nan_where_prop_propagates() {
2276 let a = Tensor::from_f32(
2280 vec![f32::NAN, 2.0, 5.0],
2281 Shape::from_dims(&[3]),
2282 &Device::cpu(),
2283 )
2284 .unwrap();
2285 let b = a
2286 .const_f32_like(vec![3.0, f32::NAN, 4.0], Shape::from_dims(&[3]))
2287 .unwrap();
2288
2289 let fmax = a.fmax_ieee(&b).unwrap().realize_f32();
2290 assert_eq!(fmax[0], 3.0, "fmax_ieee(NaN,3) suppresses NaN → 3");
2291 assert_eq!(fmax[1], 2.0, "fmax_ieee(2,NaN) suppresses NaN → 2");
2292 assert_eq!(fmax[2], 5.0, "fmax_ieee(5,4) → 5");
2293 let max = a.maximum(&b).unwrap().realize_f32();
2294 assert!(
2295 max[0].is_nan() && max[1].is_nan(),
2296 "maximum PROPAGATES NaN — the divergence that makes fmax_ieee distinct"
2297 );
2298
2299 let fmin = a.fmin_ieee(&b).unwrap().realize_f32();
2300 assert_eq!(fmin[0], 3.0, "fmin_ieee(NaN,3) suppresses NaN → 3");
2301 assert_eq!(fmin[1], 2.0, "fmin_ieee(2,NaN) suppresses NaN → 2");
2302 assert_eq!(fmin[2], 4.0, "fmin_ieee(5,4) → 4");
2303 let min = a.minimum(&b).unwrap().realize_f32();
2304 assert!(
2305 min[0].is_nan() && min[1].is_nan(),
2306 "minimum PROPAGATES NaN — the divergence that makes fmin_ieee distinct"
2307 );
2308 }
2309
2310 #[test]
2311 fn fmax_fmin_ieee_signed_zero_tie_operand_a_wins_bitwise() {
2312 let a = Tensor::from_f32(vec![-0.0, 0.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
2330 let b = a
2331 .const_f32_like(vec![0.0, -0.0], Shape::from_dims(&[2]))
2332 .unwrap();
2333
2334 let fmax = a.fmax_ieee(&b).unwrap().realize_f32();
2335 assert_eq!(
2336 fmax[0].to_bits(),
2337 (-0.0f32).to_bits(),
2338 "fmax_ieee(-0.0,+0.0): operand a wins the tie → -0.0 (bitwise)"
2339 );
2340 assert_eq!(
2341 fmax[1].to_bits(),
2342 (0.0f32).to_bits(),
2343 "fmax_ieee(+0.0,-0.0): operand a wins the tie → +0.0 (bitwise)"
2344 );
2345
2346 let fmin = a.fmin_ieee(&b).unwrap().realize_f32();
2347 assert_eq!(
2348 fmin[0].to_bits(),
2349 (-0.0f32).to_bits(),
2350 "fmin_ieee(-0.0,+0.0): operand a wins the tie → -0.0 (bitwise)"
2351 );
2352 assert_eq!(
2353 fmin[1].to_bits(),
2354 (0.0f32).to_bits(),
2355 "fmin_ieee(+0.0,-0.0): operand a wins the tie → +0.0 (bitwise)"
2356 );
2357 }
2358
2359 #[test]
2360 fn rem_trunc_diverges_from_floored_rem_on_opposite_signs() {
2361 let a = Tensor::from_f32(
2365 vec![-7.0, 7.0, -7.0],
2366 Shape::from_dims(&[3]),
2367 &Device::cpu(),
2368 )
2369 .unwrap();
2370 let b = a
2371 .const_f32_like(vec![3.0, -3.0, -3.0], Shape::from_dims(&[3]))
2372 .unwrap();
2373
2374 let rt = a.rem_trunc(&b).unwrap().realize_f32();
2375 assert_eq!(rt[0], -1.0, "-7 rem_trunc 3 = -1 (sign of dividend)");
2376 assert_eq!(rt[1], 1.0, "7 rem_trunc -3 = 1 (sign of dividend)");
2377 assert_eq!(rt[2], -1.0, "-7 rem_trunc -3 = -1");
2378 let rf = a.rem(&b).unwrap().realize_f32();
2379 assert_eq!(
2380 rf[0], 2.0,
2381 "-7 rem 3 = 2 (floored, sign of divisor) — the divergence from rem_trunc"
2382 );
2383 }
2384
2385 #[test]
2386 fn trunc_edges_signed_zero_nan_inf_bitwise() {
2387 let q = Tensor::from_f32(
2391 vec![
2392 2.7,
2393 -2.7,
2394 -0.0,
2395 0.0,
2396 f32::NAN,
2397 f32::INFINITY,
2398 f32::NEG_INFINITY,
2399 ],
2400 Shape::from_dims(&[7]),
2401 &Device::cpu(),
2402 )
2403 .unwrap();
2404 let t = q.trunc().realize_f32();
2405 assert_eq!(t[0], 2.0, "trunc(2.7) = 2 (toward zero)");
2406 assert_eq!(t[1], -2.0, "trunc(-2.7) = -2 (toward zero, NOT -3)");
2407 assert_eq!(
2408 t[2].to_bits(),
2409 (-0.0f32).to_bits(),
2410 "trunc(-0.0) must PRESERVE -0.0 (bitwise, not +0.0)"
2411 );
2412 assert_eq!(
2413 t[3].to_bits(),
2414 (0.0f32).to_bits(),
2415 "trunc(+0.0) = +0.0 (bitwise)"
2416 );
2417 assert!(t[4].is_nan(), "trunc(NaN) = NaN");
2418 assert_eq!(t[5], f32::INFINITY, "trunc(+inf) = +inf");
2419 assert_eq!(t[6], f32::NEG_INFINITY, "trunc(-inf) = -inf");
2420 }
2421
2422 #[test]
2423 fn realize_f32_matmul_hand_computed() {
2424 let a = Tensor::from_f32(
2426 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
2427 Shape::from_dims(&[2, 3]),
2428 &Device::cpu(),
2429 )
2430 .unwrap();
2431 let b = a
2432 .const_f32_like(
2433 vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
2434 Shape::from_dims(&[3, 2]),
2435 )
2436 .unwrap();
2437 let c = a.matmul(&b).unwrap();
2438 assert_eq!(c.shape().dims(), &[2, 2]);
2439 assert_eq!(c.realize_f32(), vec![58.0, 64.0, 139.0, 154.0]);
2440 }
2441
2442 #[test]
2443 fn fast_and_reference_agree_on_medium_matmul() {
2444 let m = 64;
2449 let k = 96;
2450 let n = 32;
2451 let a_data: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.01).sin()).collect();
2452 let b_data: Vec<f32> = (0..k * n).map(|i| (i as f32 * 0.013).cos()).collect();
2453 let a = Tensor::from_f32(a_data, Shape::from_dims(&[m, k]), &Device::cpu()).unwrap();
2454 let b = a.const_f32_like(b_data, Shape::from_dims(&[k, n])).unwrap();
2455 let c = a.matmul(&b).unwrap();
2456 let fast = c.realize_f32();
2457 let reference = c.realize_f32();
2458 assert_eq!(fast.len(), reference.len());
2459 for (i, (&f, &r)) in fast.iter().zip(&reference).enumerate() {
2460 let diff = (f - r).abs();
2466 let rel = if r.abs() > 1e-6 { diff / r.abs() } else { 0.0 };
2467 assert!(
2468 diff < 1e-4 || rel < 1e-3,
2469 "at index {i}: fast={f}, reference={r}, diff={diff}, rel={rel}",
2470 );
2471 }
2472 }
2473
2474 fn ref_attention(
2479 q: &[f32],
2480 k: &[f32],
2481 v: &[f32],
2482 hq: usize,
2483 hkv: usize,
2484 sq: usize,
2485 sk: usize,
2486 d: usize,
2487 scale: f32,
2488 causal: bool,
2489 softcap: Option<f32>,
2490 alibi: Option<&[f32]>,
2491 ) -> Vec<f32> {
2492 let g = hq / hkv;
2493 let mut out = vec![0.0f32; hq * sq * d];
2494 for h in 0..hq {
2495 let hk = h / g;
2496 let qh = &q[h * sq * d..(h + 1) * sq * d];
2497 let kh = &k[hk * sk * d..(hk + 1) * sk * d];
2498 let vh = &v[hk * sk * d..(hk + 1) * sk * d];
2499 for i in 0..sq {
2500 let mut scores = vec![0.0f32; sk];
2501 for j in 0..sk {
2502 let mut s = 0.0f32;
2503 for l in 0..d {
2504 s += qh[i * d + l] * kh[j * d + l];
2505 }
2506 let mut sc = scale * s;
2507 if let Some(cap) = softcap {
2508 sc = cap * (sc / cap).tanh();
2509 }
2510 if let Some(slopes) = alibi {
2511 sc += slopes[h] * (j as f32 - i as f32);
2513 }
2514 scores[j] = if causal && j > i {
2515 f32::NEG_INFINITY
2516 } else {
2517 sc
2518 };
2519 }
2520 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2521 let mut sum = 0.0f32;
2522 let exps: Vec<f32> = scores
2523 .iter()
2524 .map(|&s| {
2525 let e = (s - m).exp();
2526 sum += e;
2527 e
2528 })
2529 .collect();
2530 for l in 0..d {
2531 let mut o = 0.0f32;
2532 for j in 0..sk {
2533 o += (exps[j] / sum) * vh[j * d + l];
2534 }
2535 out[(h * sq + i) * d + l] = o;
2536 }
2537 }
2538 }
2539 out
2540 }
2541
2542 fn flash_decompose_vs_reference(
2548 hq: usize,
2549 hkv: usize,
2550 causal: bool,
2551 softcap: Option<f32>,
2552 alibi: bool,
2553 ) {
2554 let dev = Device::cpu();
2555 let (sq, sk, d) = (2usize, 2usize, 2usize);
2556 let q_data: Vec<f32> = (0..hq * sq * d).map(|i| (i as f32 * 0.1).sin()).collect();
2558 let k_data: Vec<f32> = (0..hkv * sk * d).map(|i| (i as f32 * 0.13).cos()).collect();
2559 let v_data: Vec<f32> = (0..hkv * sk * d).map(|i| i as f32 * 0.07 + 1.0).collect();
2560 #[allow(clippy::approx_constant)]
2563 let scale = 0.7071f32;
2564 let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, sq, d]), &dev).unwrap();
2565 let k = q
2566 .const_f32_like(k_data.clone(), Shape::from_dims(&[1, hkv, sk, d]))
2567 .unwrap();
2568 let v = q
2569 .const_f32_like(v_data.clone(), Shape::from_dims(&[1, hkv, sk, d]))
2570 .unwrap();
2571 let alibi_slopes: Option<Vec<f32>> = if alibi {
2573 Some((0..hq).map(|h| 0.5f32.powi(h as i32 + 1)).collect())
2574 } else {
2575 None
2576 };
2577 let alibi_t = alibi_slopes.as_ref().map(|s| {
2578 q.const_f32_like(s.clone(), Shape::from_dims(&[hq]))
2579 .unwrap()
2580 });
2581 let attn = q
2582 .flash_attn(&k, &v, alibi_t.as_ref(), scale, causal, None, None, softcap)
2583 .unwrap();
2584
2585 let graph = attn.inner.graph().clone();
2587 let id = attn.inner.id();
2588 let roots =
2589 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2590 assert_eq!(roots.len(), 1, "lowering should keep a single root");
2591 let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2592 .expect("realize decomposed FlashAttn on CPU");
2593
2594 let expected = ref_attention(
2595 &q_data,
2596 &k_data,
2597 &v_data,
2598 hq,
2599 hkv,
2600 sq,
2601 sk,
2602 d,
2603 scale,
2604 causal,
2605 softcap,
2606 alibi_slopes.as_deref(),
2607 );
2608 assert_eq!(got.len(), expected.len());
2609 for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
2610 assert!(
2611 (g - e).abs() < 1e-4,
2612 "FlashAttn decompose mismatch (hq={hq} hkv={hkv} causal={causal} \
2613 softcap={softcap:?} alibi={alibi}) at {i}: got {g}, expected {e}",
2614 );
2615 }
2616 }
2617
2618 #[test]
2619 fn flash_attn_decompose_vanilla() {
2620 flash_decompose_vs_reference(1, 1, false, None, false);
2621 }
2622
2623 #[test]
2624 fn flash_attn_decompose_causal() {
2625 flash_decompose_vs_reference(1, 1, true, None, false);
2626 }
2627
2628 #[test]
2629 fn flash_attn_decompose_gqa_causal() {
2630 flash_decompose_vs_reference(2, 1, true, None, false); }
2632
2633 #[test]
2634 fn flash_attn_decompose_softcap() {
2635 flash_decompose_vs_reference(1, 1, false, Some(0.5), false); }
2637
2638 #[test]
2639 fn flash_attn_decompose_alibi() {
2640 flash_decompose_vs_reference(2, 2, false, None, true);
2643 }
2644
2645 #[test]
2646 fn flash_attn_decompose_alibi_causal() {
2647 flash_decompose_vs_reference(2, 2, true, None, true);
2648 }
2649
2650 fn ref_decode_attn(
2656 q: &[f32],
2657 k: &[f32],
2658 v: &[f32],
2659 hq: usize,
2660 hkv: usize,
2661 sq: usize,
2662 cap: usize,
2663 kl: usize,
2664 d: usize,
2665 scale: f32,
2666 causal: bool,
2667 ) -> Vec<f32> {
2668 let g = hq / hkv;
2669 let offset = kl - sq;
2670 let mut out = vec![0f32; hq * sq * d];
2671 for h in 0..hq {
2672 let hk = h / g;
2673 for i in 0..sq {
2674 let qh = &q[((h * sq) + i) * d..][..d];
2675 let mut scores = vec![f32::NEG_INFINITY; kl];
2676 for j in 0..kl {
2677 if causal && j > offset + i {
2678 continue;
2679 }
2680 let kh = &k[((hk * cap) + j) * d..][..d];
2681 let s: f32 = (0..d).map(|l| qh[l] * kh[l]).sum();
2682 scores[j] = scale * s;
2683 }
2684 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2685 let mut sum = 0f32;
2686 let exps: Vec<f32> = scores
2687 .iter()
2688 .map(|&s| {
2689 let e = (s - m).exp();
2690 sum += e;
2691 e
2692 })
2693 .collect();
2694 for l in 0..d {
2695 let mut o = 0f32;
2696 for j in 0..kl {
2697 o += (exps[j] / sum) * v[((hk * cap) + j) * d + l];
2698 }
2699 out[(h * sq + i) * d + l] = o;
2700 }
2701 }
2702 }
2703 out
2704 }
2705
2706 #[test]
2714 fn flash_attn_decompose_concrete_klen() {
2715 use fuel_graph::Op;
2716 use fuel_graph::registry::FusedOps;
2717 use fuel_ir::DynScalar;
2718 let dev = Device::cpu();
2719 let (hq, hkv, sq, cap, kl, d) = (2usize, 1usize, 2usize, 4usize, 3usize, 2usize);
2720 #[allow(clippy::approx_constant)]
2723 let scale = 0.7071f32;
2724 let causal = true;
2725 let q_data: Vec<f32> = (0..hq * sq * d).map(|i| (i as f32 * 0.1).sin()).collect();
2726 let k_data: Vec<f32> = (0..hkv * cap * d)
2727 .map(|i| (i as f32 * 0.13).cos())
2728 .collect();
2729 let v_data: Vec<f32> = (0..hkv * cap * d).map(|i| i as f32 * 0.07 + 1.0).collect();
2730 let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, sq, d]), &dev).unwrap();
2731 let k = q
2732 .inner
2733 .const_f32_like(k_data.clone(), Shape::from_dims(&[1, hkv, cap, d]))
2734 .unwrap();
2735 let v = q
2736 .inner
2737 .const_f32_like(v_data.clone(), Shape::from_dims(&[1, hkv, cap, d]))
2738 .unwrap();
2739 let attn = q.inner.flash_attn_dyn(
2741 &k,
2742 &v,
2743 None,
2744 scale,
2745 causal,
2746 None,
2747 None,
2748 None,
2749 DynScalar::Concrete(kl),
2750 );
2751
2752 let graph = attn.graph().clone();
2753 let id = attn.id();
2754 let roots =
2755 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2756 assert_eq!(roots.len(), 1, "lowering should keep a single root");
2757
2758 {
2760 let g = graph.read().unwrap();
2761 let mut stack = vec![roots[0]];
2762 let mut seen = std::collections::HashSet::new();
2763 while let Some(nid) = stack.pop() {
2764 if !seen.insert(nid) {
2765 continue;
2766 }
2767 let node = g.node(nid);
2768 assert!(
2769 !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::FLASH_ATTN),
2770 "concrete-k_len FlashAttn still fused after lowering (self-return)",
2771 );
2772 for &inp in &node.inputs {
2773 stack.push(inp);
2774 }
2775 }
2776 }
2777
2778 let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2779 .expect("realize decomposed concrete-k_len FlashAttn on CPU");
2780 let expected = ref_decode_attn(
2781 &q_data, &k_data, &v_data, hq, hkv, sq, cap, kl, d, scale, causal,
2782 );
2783 assert_eq!(got.len(), expected.len());
2784 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
2785 assert!(
2786 (gv - ev).abs() < 1e-4,
2787 "concrete-k_len decode mismatch at {i}: got {gv}, expected {ev}",
2788 );
2789 }
2790 }
2791
2792 fn assert_f32_bits_eq(got: &[f32], want: &[f32], ctx: &str) {
2795 assert_eq!(got.len(), want.len(), "{ctx}: length mismatch");
2796 for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
2797 assert_eq!(
2798 g.to_bits(),
2799 w.to_bits(),
2800 "{ctx}: bit mismatch at {i}: got {g} (0x{:08x}), want {w} (0x{:08x})",
2801 g.to_bits(),
2802 w.to_bits(),
2803 );
2804 }
2805 }
2806
2807 fn assert_f32_within_ulp(got: &[f32], want: &[f32], max_ulp: i64, ctx: &str) {
2811 assert_eq!(got.len(), want.len(), "{ctx}: length mismatch");
2812 for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
2813 let d = (g.to_bits() as i64 - w.to_bits() as i64).abs();
2814 assert!(
2815 d <= max_ulp,
2816 "{ctx}: {d} ULP at {i} (> {max_ulp}): got {g} (0x{:08x}), want {w} (0x{:08x})",
2817 g.to_bits(),
2818 w.to_bits(),
2819 );
2820 }
2821 }
2822
2823 #[test]
2848 fn reduce_swap_sumdim_vs_reducesumto_bit_exact_on_cpu_backend() {
2849 let dev = Device::cpu();
2850 let (rows, cols) = (2usize, 4usize);
2851 let data: Vec<f32> = vec![1e8, 1.0, -1e8, 1.0, 3.0e7, 2.0, -3.0e7, -1.0];
2856
2857 let mut left_fold = vec![0.0f32; rows];
2860 let mut right_fold = vec![0.0f32; rows];
2861 for r in 0..rows {
2862 let row = &data[r * cols..(r + 1) * cols];
2863 let mut acc = 0.0f32;
2864 for &v in row.iter() {
2865 acc += v;
2866 }
2867 left_fold[r] = acc;
2868 let mut acc_r = 0.0f32;
2869 for &v in row.iter().rev() {
2870 acc_r += v;
2871 }
2872 right_fold[r] = acc_r;
2873 }
2874 assert_ne!(
2875 left_fold, right_fold,
2876 "test data must be accumulation-order-sensitive so the parity has teeth",
2877 );
2878
2879 let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[rows, cols]), &dev).unwrap();
2880 let sumdim = x.inner.sum_dim(1);
2882 let got_sumdim = crate::pipelined_bridge::realize_one_as::<f32>(
2883 &sumdim.graph().clone(),
2884 sumdim.id(),
2885 &dev,
2886 )
2887 .expect("realize SumDim(last) on CPU");
2888 let reducesumto = x.inner.reduce_sum_to(Shape::from_dims(&[rows, 1]));
2890 let got_reducesumto = crate::pipelined_bridge::realize_one_as::<f32>(
2891 &reducesumto.graph().clone(),
2892 reducesumto.id(),
2893 &dev,
2894 )
2895 .expect("realize ReduceSumTo(keepdim) on CPU");
2896
2897 assert_f32_bits_eq(&got_sumdim, &got_reducesumto, "SumDim vs ReduceSumTo");
2899 assert_f32_bits_eq(&got_sumdim, &left_fold, "SumDim vs left-fold");
2902
2903 let maxdim = x.inner.max_dim(1);
2908 let got_maxdim = crate::pipelined_bridge::realize_one_as::<f32>(
2909 &maxdim.graph().clone(),
2910 maxdim.id(),
2911 &dev,
2912 )
2913 .expect("realize MaxDim(last) on CPU");
2914 let reducemaxto = x.inner.reduce_max_to(Shape::from_dims(&[rows, 1]));
2915 let got_reducemaxto = crate::pipelined_bridge::realize_one_as::<f32>(
2916 &reducemaxto.graph().clone(),
2917 reducemaxto.id(),
2918 &dev,
2919 )
2920 .expect("realize ReduceMaxTo(keepdim) on CPU");
2921 let row_max: Vec<f32> = (0..rows)
2922 .map(|r| {
2923 data[r * cols..(r + 1) * cols]
2924 .iter()
2925 .cloned()
2926 .fold(f32::NEG_INFINITY, f32::max)
2927 })
2928 .collect();
2929 assert_f32_bits_eq(&got_maxdim, &got_reducemaxto, "MaxDim vs ReduceMaxTo");
2930 assert_f32_bits_eq(&got_maxdim, &row_max, "MaxDim vs per-row max");
2931 }
2932
2933 #[test]
2953 fn softmax_last_dim_recipe_decompose_matches_legacy_on_cpu() {
2954 let dev = Device::cpu();
2955 let (rows, cols) = (3usize, 5usize);
2956 let data: Vec<f32> = (0..rows * cols)
2957 .map(|i| ((i as f32) * 0.37).sin() * 4.0 - (i as f32) * 0.11)
2958 .collect();
2959 let shape = || Shape::from_dims(&[rows, cols]);
2960 let keepdim = || Shape::from_dims(&[rows, 1]);
2961
2962 let recipe = Tensor::from_f32(data.clone(), shape(), &dev)
2964 .unwrap()
2965 .softmax_last_dim()
2966 .expect("softmax recipe build");
2967 let graph = recipe.inner.graph().clone();
2968 let id = recipe.inner.id();
2969 let roots =
2970 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
2971 assert_eq!(roots.len(), 1, "lowering keeps a single root");
2972 let recipe_out = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
2973 .expect("realize recipe-decomposed softmax on CPU");
2974
2975 let x = Tensor::from_f32(data.clone(), shape(), &dev).unwrap().inner;
2978 let m = x.reduce_max_to(keepdim());
2979 let mb = m.broadcast_to(shape());
2980 let s = x.sub(&mb);
2981 let e = s.exp();
2982 let d = e.reduce_sum_to(keepdim());
2983 let db = d.broadcast_to(shape());
2984 let legacy = e.div(&db);
2985 let legacy_out = crate::pipelined_bridge::realize_one_as::<f32>(
2986 &legacy.graph().clone(),
2987 legacy.id(),
2988 &dev,
2989 )
2990 .expect("realize legacy softmax on CPU");
2991
2992 assert_f32_bits_eq(&recipe_out, &legacy_out, "softmax recipe vs legacy");
2994 for r in 0..rows {
2996 let sum: f32 = recipe_out[r * cols..(r + 1) * cols].iter().sum();
2997 assert!((sum - 1.0).abs() < 1e-5, "row {r} softmax sum = {sum}");
2998 }
2999
3000 let fused_out = Tensor::from_f32(data.clone(), shape(), &dev)
3004 .unwrap()
3005 .softmax_last_dim()
3006 .expect("softmax fused build")
3007 .realize_f32();
3008 assert_f32_within_ulp(&recipe_out, &fused_out, 1, "softmax recipe vs fused kernel");
3009 }
3010
3011 #[test]
3019 fn rms_norm_last_dim_recipe_decompose_matches_legacy_on_cpu() {
3020 let dev = Device::cpu();
3021 let (rows, cols) = (3usize, 6usize);
3022 let eps = 1e-6f64;
3023 let data: Vec<f32> = (0..rows * cols)
3024 .map(|i| ((i as f32) * 0.29).cos() * 2.5 + 0.5)
3025 .collect();
3026 let shape = || Shape::from_dims(&[rows, cols]);
3027 let keepdim = || Shape::from_dims(&[rows, 1]);
3028
3029 let recipe = Tensor::from_f32(data.clone(), shape(), &dev)
3031 .unwrap()
3032 .rms_norm_last_dim(eps)
3033 .expect("rms_norm recipe build");
3034 let graph = recipe.inner.graph().clone();
3035 let id = recipe.inner.id();
3036 let roots =
3037 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3038 assert_eq!(roots.len(), 1, "lowering keeps a single root");
3039 let recipe_out = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
3040 .expect("realize recipe-decomposed rms_norm on CPU");
3041
3042 let x = Tensor::from_f32(data.clone(), shape(), &dev).unwrap().inner;
3045 let sq = x.sqr();
3046 let mean = sq.mean_dim(1);
3047 let mean_kd = mean.reshape(keepdim());
3048 let denom_sq = mean_kd.add_scalar(eps);
3049 let denom = denom_sq.sqrt();
3050 let denom_b = denom.broadcast_to(shape());
3051 let legacy = x.div(&denom_b);
3052 let legacy_out = crate::pipelined_bridge::realize_one_as::<f32>(
3053 &legacy.graph().clone(),
3054 legacy.id(),
3055 &dev,
3056 )
3057 .expect("realize legacy rms_norm on CPU");
3058
3059 assert_f32_bits_eq(&recipe_out, &legacy_out, "rms_norm recipe vs legacy");
3061
3062 let fused_out = Tensor::from_f32(data.clone(), shape(), &dev)
3065 .unwrap()
3066 .rms_norm_last_dim(eps)
3067 .expect("rms_norm fused build")
3068 .realize_f32();
3069 assert_f32_within_ulp(
3070 &recipe_out,
3071 &fused_out,
3072 1,
3073 "rms_norm recipe vs fused kernel",
3074 );
3075 }
3076
3077 #[test]
3088 fn selective_scan_decompose_lowers_to_scan_and_matches() {
3089 use fuel_graph::Op;
3090 use fuel_graph::registry::FusedOps;
3091 let dev = Device::cpu();
3092 let u = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3093 let delta = u
3094 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3095 .unwrap();
3096 let a = u
3097 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3098 .unwrap();
3099 let b = u
3100 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3101 .unwrap();
3102 let c = u
3103 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3104 .unwrap();
3105 let y = u.selective_scan(&delta, &a, &b, &c, false);
3106
3107 let got = y.realize_f32();
3109 assert_eq!(got.len(), 1);
3110 assert!((got[0] - 12.0).abs() < 1e-4, "fused kernel y: {}", got[0]);
3111
3112 let graph = y.inner.graph().clone();
3115 let id = y.inner.id();
3116 let roots =
3117 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3118 assert_eq!(roots.len(), 1);
3119 let scan_id = {
3120 let g = graph.read().unwrap();
3121 let mut stack = vec![roots[0]];
3122 let mut seen = std::collections::HashSet::new();
3123 let mut scan_id = None;
3124 while let Some(nid) = stack.pop() {
3125 if !seen.insert(nid) {
3126 continue;
3127 }
3128 let node = g.node(nid);
3129 assert!(
3130 !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::SELECTIVE_SCAN),
3131 "SelectiveScan must lower to Op::Scan, not remain fused"
3132 );
3133 if matches!(node.op, Op::Scan { .. }) {
3134 scan_id = Some(nid);
3135 }
3136 for &inp in &node.inputs {
3137 stack.push(inp);
3138 }
3139 }
3140 scan_id.expect("an Op::Scan terminal must be present after lowering")
3141 };
3142 let ys = {
3144 let mut g = graph.write().unwrap();
3145 fuel_graph::scan::unroll_scan(&mut g, scan_id, 1)
3146 .expect("unroll")
3147 .0[0] };
3149 let oracle = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3150 .expect("realize unrolled selective_scan oracle on CPU");
3151 assert!(
3152 oracle.iter().any(|&v| (v - 12.0).abs() < 1e-4),
3153 "unroll oracle must contain y = 12, got {oracle:?}"
3154 );
3155 }
3156
3157 #[test]
3170 fn ssd_chunk_scan_decompose_lowers_to_scan_and_matches() {
3171 use fuel_graph::Op;
3172 use fuel_graph::registry::FusedOps;
3173 let dev = Device::cpu();
3174 let x = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3177 let dt = x
3178 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3179 .unwrap();
3180 let a = x
3181 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3182 .unwrap();
3183 let b = x
3184 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3185 .unwrap();
3186 let c = x
3187 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3188 .unwrap();
3189 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 1);
3190
3191 let got = y.realize_f32();
3193 assert!(
3194 (got[0] - 12.0).abs() < 1e-4,
3195 "fused ssd kernel y: {}",
3196 got[0]
3197 );
3198
3199 let graph = y.inner.graph().clone();
3202 let id = y.inner.id();
3203 let roots =
3204 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
3205 let scan_id = {
3206 let g = graph.read().unwrap();
3207 let mut stack = vec![roots[0]];
3208 let mut seen = std::collections::HashSet::new();
3209 let mut scan_id = None;
3210 while let Some(nid) = stack.pop() {
3211 if !seen.insert(nid) {
3212 continue;
3213 }
3214 let node = g.node(nid);
3215 assert!(
3216 !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::SSD_CHUNK_SCAN),
3217 "SsdChunkScan must lower to Op::Scan, not remain fused"
3218 );
3219 if matches!(node.op, Op::Scan { .. }) {
3220 scan_id = Some(nid);
3221 }
3222 for &inp in &node.inputs {
3223 stack.push(inp);
3224 }
3225 }
3226 scan_id.expect("an Op::Scan terminal must be present after lowering")
3227 };
3228 let ys = {
3229 let mut g = graph.write().unwrap();
3230 fuel_graph::scan::unroll_scan(&mut g, scan_id, 1)
3231 .expect("unroll")
3232 .0[0] };
3234 let oracle = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3235 .expect("realize unrolled ssd_chunk_scan oracle on CPU");
3236 assert!(
3237 oracle.iter().any(|&v| (v - 12.0).abs() < 1e-4),
3238 "oracle y=12, got {oracle:?}"
3239 );
3240 }
3241
3242 #[test]
3249 fn affine_scan_bptt_matches_finite_difference() {
3250 use fuel_graph::{Node, NodeHandle, Op, ScanEmit, ScanRole};
3251 use fuel_ir::{DType, Shape};
3252 let dev = Device::cpu();
3253 let build = |init_v: f32,
3256 a_v: f32,
3257 b_v: f32|
3258 -> (
3259 std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3260 fuel_graph::NodeId,
3261 fuel_graph::NodeHandle,
3262 fuel_graph::NodeHandle,
3263 fuel_graph::NodeHandle,
3264 ) {
3265 let init =
3266 NodeHandle::from_f32(vec![init_v], Shape::from_dims(&[1]), dev.as_dyn()).unwrap();
3267 let g = init.graph().clone();
3268 let a = NodeHandle::from_existing(g.clone(), init.id())
3269 .const_f32_like(vec![a_v], Shape::from_dims(&[1]))
3270 .unwrap();
3271 let b = NodeHandle::from_existing(g.clone(), init.id())
3272 .const_f32_like(vec![b_v], Shape::from_dims(&[1]))
3273 .unwrap();
3274 let nc = {
3275 let mut gw = g.write().unwrap();
3276 let s = Shape::from_dims(&[1]);
3277 let hole = gw.push(Node {
3278 op: Op::ScanPlaceholder {
3279 role: ScanRole::Carry,
3280 index: 0,
3281 },
3282 inputs: vec![],
3283 shape: s.clone(),
3284 dtype: DType::F32,
3285 });
3286 let ac = gw.push(Node {
3287 op: Op::Mul,
3288 inputs: vec![a.id(), hole],
3289 shape: s.clone(),
3290 dtype: DType::F32,
3291 });
3292 gw.push(Node {
3293 op: Op::Add,
3294 inputs: vec![ac, b.id()],
3295 shape: s.clone(),
3296 dtype: DType::F32,
3297 })
3298 };
3299 let nc_t = NodeHandle::from_existing(g.clone(), nc);
3300 let out = init
3301 .scan(
3302 &[],
3303 &[a.clone(), b.clone()],
3304 &nc_t,
3305 &nc_t,
3306 3,
3307 ScanEmit::Final,
3308 )
3309 .expect("scan");
3310 (g, out.id(), init, a, b)
3311 };
3312 let fwd = |init_v: f32, a_v: f32, b_v: f32| -> f32 {
3314 let (g, out_id, _i, _a, _b) = build(init_v, a_v, b_v);
3315 let scan_id = {
3316 let gr = g.read().unwrap();
3317 gr.node(out_id).inputs[0]
3318 };
3319 let carry = {
3320 let mut gw = g.write().unwrap();
3321 fuel_graph::scan::unroll_scan(&mut gw, scan_id, 3)
3322 .expect("unroll")
3323 .0[0] }; crate::pipelined_bridge::realize_one_as::<f32>(&g, carry, &dev).expect("realize")[0]
3326 };
3327 let (g, out_id, init, a, b) = build(1.0, 0.5, 0.1);
3329 let out = fuel_graph::NodeHandle::from_existing(g.clone(), out_id);
3330 let grads = out.backward();
3331 let realize_grad = |t: &fuel_graph::NodeHandle| {
3332 crate::pipelined_bridge::realize_one_as::<f32>(
3333 &g,
3334 grads.get(t).expect("grad").id(),
3335 &dev,
3336 )
3337 .expect("realize grad")[0]
3338 };
3339 let g_init = realize_grad(&init);
3340 let g_a = realize_grad(&a);
3341 let g_b = realize_grad(&b);
3342 let h = 1e-3f32;
3344 let fd = |dinit: f32, da: f32, db: f32| {
3345 (fwd(1.0 + dinit, 0.5 + da, 0.1 + db) - fwd(1.0 - dinit, 0.5 - da, 0.1 - db))
3346 / (2.0 * h)
3347 };
3348 assert!(
3349 (g_init - fd(h, 0.0, 0.0)).abs() < 2e-2,
3350 "d/dinit: autograd {g_init} vs FD {}",
3351 fd(h, 0.0, 0.0)
3352 );
3353 assert!(
3354 (g_a - fd(0.0, h, 0.0)).abs() < 2e-2,
3355 "d/da: autograd {g_a} vs FD {}",
3356 fd(0.0, h, 0.0)
3357 );
3358 assert!(
3359 (g_b - fd(0.0, 0.0, h)).abs() < 2e-2,
3360 "d/db: autograd {g_b} vs FD {}",
3361 fd(0.0, 0.0, h)
3362 );
3363 }
3364
3365 #[test]
3366 fn selective_scan_is_differentiable_backward_matches_fd() {
3367 let dev = Device::cpu();
3368 let fwd = |u_v: f32| -> f32 {
3369 let u = Tensor::from_f32(vec![u_v], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3370 let delta = u
3371 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3372 .unwrap();
3373 let a = u
3374 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3375 .unwrap();
3376 let b = u
3377 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3378 .unwrap();
3379 let c = u
3380 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3381 .unwrap();
3382 u.selective_scan(&delta, &a, &b, &c, false).realize_f32()[0]
3383 };
3384 let u = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1]), &dev).unwrap();
3386 let delta = u
3387 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3388 .unwrap();
3389 let a = u
3390 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1, 1]))
3391 .unwrap();
3392 let b = u
3393 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1]))
3394 .unwrap();
3395 let c = u
3396 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1]))
3397 .unwrap();
3398 let y = u.selective_scan(&delta, &a, &b, &c, false);
3399 let grads = y.inner.backward();
3400 let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3401 let g_u =
3402 crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3403 .expect("realize")[0];
3404 let h = 1e-3;
3405 let fd = (fwd(2.0 + h) - fwd(2.0 - h)) / (2.0 * h);
3406 assert!(
3407 (g_u - fd).abs() < 5e-2,
3408 "selective_scan d/du: autograd {g_u} vs FD {fd}"
3409 );
3410 }
3411
3412 #[test]
3413 fn ssd_chunk_scan_is_differentiable_backward_matches_fd() {
3414 let dev = Device::cpu();
3415 let fwd = |x_v: f32| -> f32 {
3418 let x = Tensor::from_f32(vec![x_v], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3419 let dt = x
3420 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3421 .unwrap();
3422 let a = x
3423 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3424 .unwrap();
3425 let b = x
3426 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3427 .unwrap();
3428 let c = x
3429 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3430 .unwrap();
3431 x.ssd_chunk_scan(&dt, &a, &b, &c, 1).realize_f32()[0]
3432 };
3433 let x = Tensor::from_f32(vec![2.0f32], Shape::from_dims(&[1, 1, 1, 1]), &dev).unwrap();
3435 let dt = x
3436 .const_f32_like(vec![0.5f32], Shape::from_dims(&[1, 1, 1]))
3437 .unwrap();
3438 let a = x
3439 .const_f32_like(vec![-1.0f32], Shape::from_dims(&[1]))
3440 .unwrap();
3441 let b = x
3442 .const_f32_like(vec![3.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3443 .unwrap();
3444 let c = x
3445 .const_f32_like(vec![4.0f32], Shape::from_dims(&[1, 1, 1, 1]))
3446 .unwrap();
3447 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 1);
3448 let grads = y.inner.backward();
3449 let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3450 let g_x =
3451 crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3452 .expect("realize")[0];
3453 let h = 1e-3;
3454 let fd = (fwd(2.0 + h) - fwd(2.0 - h)) / (2.0 * h);
3455 assert!(
3456 (g_x - fd).abs() < 5e-2,
3457 "ssd_chunk_scan d/dx: autograd {g_x} vs FD {fd}"
3458 );
3459 }
3460
3461 fn act_grad_and_fd(
3464 x_v: f32,
3465 act: impl Fn(&Tensor) -> Tensor,
3466 fd_act: impl Fn(&Tensor) -> Tensor,
3467 ) -> (f32, f32) {
3468 let dev = Device::cpu();
3469 let x = Tensor::from_f32(vec![x_v], Shape::from_dims(&[1]), &dev).unwrap();
3470 let y = act(&x);
3471 let grads = y.inner.backward();
3472 let g_id = grads.get(x.graph_tensor()).expect("grad x").id();
3473 let g =
3474 crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_id, &dev)
3475 .expect("realize")[0];
3476 let fwd = |v: f32| -> f32 {
3477 let t = Tensor::from_f32(vec![v], Shape::from_dims(&[1]), &dev).unwrap();
3478 fd_act(&t).realize_f32()[0]
3479 };
3480 let h = 1e-2f32;
3481 (g, (fwd(x_v + h) - fwd(x_v - h)) / (2.0 * h))
3482 }
3483
3484 #[test]
3488 fn gelu_tanh_backward_matches_finite_difference() {
3489 for x_v in [-3.0f32, -1.5, -0.5, 0.0, 0.3, 1.0, 2.5] {
3490 let (g, fd) = act_grad_and_fd(x_v, |x| x.gelu_tanh(), |x| x.gelu_tanh());
3491 assert!(
3492 (g - fd).abs() < 2e-3,
3493 "gelu_tanh d/dx at {x_v}: autograd {g} vs FD {fd}"
3494 );
3495 }
3496 }
3497
3498 #[test]
3500 fn gelu_tanh_inplace_backward_matches_finite_difference() {
3501 for x_v in [-3.0f32, -1.5, -0.5, 0.0, 0.3, 1.0, 2.5] {
3502 let (g, fd) = act_grad_and_fd(x_v, |x| x.gelu_tanh_inplace(), |x| x.gelu_tanh());
3503 assert!(
3504 (g - fd).abs() < 2e-3,
3505 "gelu_tanh_inplace d/dx at {x_v}: autograd {g} vs FD {fd}"
3506 );
3507 }
3508 }
3509
3510 #[test]
3516 fn finite_difference_harness_rejects_a_wrong_activation_gradient() {
3517 let (g_silu, fd_gelu) = act_grad_and_fd(-1.5, |x| x.silu(), |x| x.gelu_tanh());
3518 assert!(
3519 (g_silu - fd_gelu).abs() > 2e-2,
3520 "silu grad {g_silu} too close to gelu_tanh FD {fd_gelu}: harness cannot discriminate"
3521 );
3522 }
3523
3524 fn grad_vs_fd(
3528 x_vals: &[f32],
3529 x_shape: &[usize],
3530 build: impl Fn(&Tensor) -> Tensor,
3531 ) -> (Vec<f32>, Vec<f32>) {
3532 grad_vs_fd_impl(x_vals, x_shape, build, false)
3533 }
3534
3535 fn grad_vs_fd_lowered(
3538 x_vals: &[f32],
3539 x_shape: &[usize],
3540 build: impl Fn(&Tensor) -> Tensor,
3541 ) -> (Vec<f32>, Vec<f32>) {
3542 grad_vs_fd_impl(x_vals, x_shape, build, true)
3543 }
3544
3545 fn grad_vs_fd_impl(
3546 x_vals: &[f32],
3547 x_shape: &[usize],
3548 build: impl Fn(&Tensor) -> Tensor,
3549 lower_forward: bool,
3550 ) -> (Vec<f32>, Vec<f32>) {
3551 let dev = Device::cpu();
3552 let x = Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(x_shape), &dev).unwrap();
3553 let y = build(&x);
3554 let grads = y.inner.backward();
3555 let g_id = grads.get(x.graph_tensor()).expect("grad x").id();
3556 let g =
3557 crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_id, &dev)
3558 .expect("realize");
3559 let fwd_sum = |vals: &[f32]| -> f32 {
3560 let t = Tensor::from_f32(vals.to_vec(), Shape::from_dims(x_shape), &dev).unwrap();
3561 let y = build(&t);
3562 if lower_forward {
3563 let graph = y.inner.graph().clone();
3564 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3565 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3566 crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
3567 .expect("realize lowered forward")
3568 .iter()
3569 .sum()
3570 } else {
3571 y.realize_f32().iter().sum()
3572 }
3573 };
3574 let h = 1e-2f32;
3575 let fd = (0..x_vals.len())
3576 .map(|i| {
3577 let (mut p, mut m) = (x_vals.to_vec(), x_vals.to_vec());
3578 p[i] += h;
3579 m[i] -= h;
3580 (fwd_sum(&p) - fwd_sum(&m)) / (2.0 * h)
3581 })
3582 .collect();
3583 (g, fd)
3584 }
3585
3586 fn assert_grad_close(label: &str, g: &[f32], fd: &[f32]) {
3587 assert_eq!(g.len(), fd.len(), "{label}: length");
3588 for (i, (a, b)) in g.iter().zip(fd).enumerate() {
3589 assert!(
3590 (a - b).abs() < 5e-3 + 5e-3 * b.abs(),
3591 "{label}[{i}]: autograd {a} vs FD {b}"
3592 );
3593 }
3594 }
3595
3596 #[test]
3600 fn nf4_matmul_activation_gradient_matches_finite_difference() {
3601 let (g, fd) = grad_vs_fd(&[1.0, 2.0, 2.0, 4.0], &[1, 4], |act| {
3602 let w_packed = Tensor::from_graph_tensor(
3603 act.graph_tensor()
3604 .const_u8_like(vec![247_u8, 247, 127, 127], Shape::from_dims(&[2, 2]))
3605 .unwrap(),
3606 );
3607 let absmax = act
3608 .const_f32_like(vec![1.0_f32, 2.0, 10.0, 20.0], Shape::from_dims(&[2, 2]))
3609 .unwrap();
3610 act.nf4_matmul(&w_packed, &absmax, 2)
3611 });
3612 assert_grad_close("nf4_matmul d/dact", &g, &fd);
3613 }
3614
3615 #[test]
3620 fn awq_matmul_activation_gradient_matches_finite_difference() {
3621 let (g, fd) = grad_vs_fd_lowered(
3622 &[0.5, -1.0, 2.0, 0.25, -0.75, 1.5, 0.1, -0.3],
3623 &[1, 8],
3624 |act| {
3625 let qweight = act
3626 .graph_tensor()
3627 .const_u32_like(
3628 vec![
3629 0x1234_5678,
3630 0x9ABC_DEF0,
3631 0x0FED_CBA9,
3632 0x8765_4321,
3633 0x1357_9BDF,
3634 0x2468_ACE0,
3635 0xFEDC_BA98,
3636 0x0123_4567,
3637 ],
3638 Shape::from_dims(&[8, 1]),
3639 )
3640 .unwrap();
3641 let qzeros = act
3642 .graph_tensor()
3643 .const_u32_like(vec![0x7777_7777], Shape::from_dims(&[1, 1]))
3644 .unwrap();
3645 let scales = act
3646 .const_f32_like(
3647 vec![0.1, 0.2, 0.15, 0.05, 0.3, 0.25, 0.12, 0.08],
3648 Shape::from_dims(&[1, 8]),
3649 )
3650 .unwrap();
3651 Tensor::from_graph_tensor(act.graph_tensor().awq_matmul(
3652 &qweight,
3653 &qzeros,
3654 scales.graph_tensor(),
3655 8,
3656 8,
3657 ))
3658 },
3659 );
3660 assert_grad_close("awq_matmul d/dact", &g, &fd);
3661 }
3662
3663 #[test]
3664 fn causal_conv1d_weight_and_bias_gradients_match_finite_difference() {
3665 let x_data = vec![0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7];
3666 for use_silu in [false, true] {
3667 let (g, fd) = grad_vs_fd(&[0.5, -0.25, 0.75, 0.1, 0.9, -0.4], &[2, 1, 3], |w| {
3669 let x = w
3670 .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3671 .unwrap();
3672 let b = w
3673 .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3674 .unwrap();
3675 x.causal_conv1d(w, &b, use_silu)
3676 });
3677 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dw"), &g, &fd);
3678 let (g, fd) = grad_vs_fd(&[0.05, -0.05], &[2], |b| {
3680 let x = b
3681 .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3682 .unwrap();
3683 let w = b
3684 .const_f32_like(
3685 vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3686 Shape::from_dims(&[2, 1, 3]),
3687 )
3688 .unwrap();
3689 x.causal_conv1d(&w, b, use_silu)
3690 });
3691 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dbias"), &g, &fd);
3692 }
3693 }
3694
3695 #[test]
3696 fn causal_conv1d_input_gradient_matches_finite_difference() {
3697 for use_silu in [false, true] {
3699 let (g, fd) = grad_vs_fd(
3700 &[0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7],
3701 &[1, 2, 4],
3702 |x| {
3703 let w = x
3704 .const_f32_like(
3705 vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3706 Shape::from_dims(&[2, 1, 3]),
3707 )
3708 .unwrap();
3709 let b = x
3710 .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3711 .unwrap();
3712 x.causal_conv1d(&w, &b, use_silu)
3713 },
3714 );
3715 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dx"), &g, &fd);
3716 }
3717 }
3718
3719 #[test]
3726 fn selective_scan_seqlen2_bptt_matches_finite_difference() {
3727 let dev = Device::cpu();
3728 let fwd = |u_vals: &[f32]| -> f32 {
3731 let u = Tensor::from_f32(u_vals.to_vec(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3732 let delta = u
3733 .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3734 .unwrap();
3735 let a = u
3736 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3737 .unwrap();
3738 let b = u
3739 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3740 .unwrap();
3741 let c = u
3742 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3743 .unwrap();
3744 u.selective_scan(&delta, &a, &b, &c, false)
3745 .realize_f32()
3746 .iter()
3747 .sum()
3748 };
3749 let u0 = vec![1.0f32, 2.0];
3750 let u = Tensor::from_f32(u0.clone(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3751 let delta = u
3752 .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3753 .unwrap();
3754 let a = u
3755 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3756 .unwrap();
3757 let b = u
3758 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3759 .unwrap();
3760 let c = u
3761 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3762 .unwrap();
3763 let y = u.selective_scan(&delta, &a, &b, &c, false);
3764 let grads = y.inner.backward(); let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3766 let g_u =
3767 crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3768 .expect("realize");
3769 let h = 1e-3f32;
3772 let mut up = u0.clone();
3773 up[0] += h;
3774 let mut um = u0.clone();
3775 um[0] -= h;
3776 let fd0 = (fwd(&up) - fwd(&um)) / (2.0 * h);
3777 assert!(
3778 (g_u[0] - fd0).abs() < 5e-2,
3779 "selective_scan seqlen2 dL/du_1: autograd {} vs FD {fd0}",
3780 g_u[0]
3781 );
3782 assert!(
3785 g_u[0].abs() > 0.5,
3786 "recurrent-gate grad must be non-trivial, got {g_u:?}"
3787 );
3788 }
3789
3790 #[test]
3791 fn ssd_chunk_scan_seqlen4_chunk2_bptt_matches_finite_difference() {
3792 let dev = Device::cpu();
3793 let fwd = |x_vals: &[f32]| -> f32 {
3797 let x =
3798 Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3799 let dt = x
3800 .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3801 .unwrap();
3802 let a = x
3803 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3804 .unwrap();
3805 let b = x
3806 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3807 .unwrap();
3808 let c = x
3809 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3810 .unwrap();
3811 x.ssd_chunk_scan(&dt, &a, &b, &c, 2)
3812 .realize_f32()
3813 .iter()
3814 .sum()
3815 };
3816 let x0 = vec![1.0f32, 2.0, 3.0, 4.0];
3817 let x = Tensor::from_f32(x0.clone(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3818 let dt = x
3819 .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3820 .unwrap();
3821 let a = x
3822 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3823 .unwrap();
3824 let b = x
3825 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3826 .unwrap();
3827 let c = x
3828 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3829 .unwrap();
3830 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 2);
3831 let grads = y.inner.backward();
3832 let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3833 let g_x =
3834 crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3835 .expect("realize");
3836 let h = 1e-3f32;
3839 let mut xp = x0.clone();
3840 xp[0] += h;
3841 let mut xm = x0.clone();
3842 xm[0] -= h;
3843 let fd0 = (fwd(&xp) - fwd(&xm)) / (2.0 * h);
3844 assert!(
3845 (g_x[0] - fd0).abs() < 5e-2,
3846 "ssd_chunk_scan seqlen4 dL/dx_0: autograd {} vs FD {fd0}",
3847 g_x[0]
3848 );
3849 assert!(
3850 g_x[0].abs() > 0.5,
3851 "cross-chunk-gate grad must be non-trivial, got {g_x:?}"
3852 );
3853 }
3854
3855 fn find_scan_terminal(
3869 graph: &std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3870 root: fuel_graph::NodeId,
3871 fused_id: fuel_graph::registry::FusedOpId,
3872 ) -> fuel_graph::NodeId {
3873 use fuel_graph::Op;
3874 let g = graph.read().unwrap();
3875 let mut stack = vec![root];
3876 let mut seen = std::collections::HashSet::new();
3877 let mut scan_id = None;
3878 while let Some(nid) = stack.pop() {
3879 if !seen.insert(nid) {
3880 continue;
3881 }
3882 let node = g.node(nid);
3883 assert!(
3884 !matches!(node.op, Op::Fused(fid, _) if fid == fused_id),
3885 "SSM op must lower to Op::Scan, not remain fused",
3886 );
3887 if matches!(node.op, Op::Scan { .. }) {
3888 scan_id = Some(nid);
3889 }
3890 for &inp in &node.inputs {
3891 stack.push(inp);
3892 }
3893 }
3894 scan_id.expect("an Op::Scan terminal must be present after lowering")
3895 }
3896
3897 fn scan_ys_to_fused_layout(ys: &[f32], seqlen: usize, batch: usize, tail: usize) -> Vec<f32> {
3910 assert_eq!(ys.len(), seqlen * batch * tail, "ys layout mismatch");
3911 let mut out = vec![0f32; ys.len()];
3912 for s in 0..seqlen {
3913 for bb in 0..batch {
3914 for t in 0..tail {
3915 out[(bb * seqlen + s) * tail + t] = ys[(s * batch + bb) * tail + t];
3916 }
3917 }
3918 }
3919 out
3920 }
3921
3922 #[test]
3938 fn selective_scan_multidim_unroll_matches_fused_kernel() {
3939 use fuel_graph::registry::FusedOps;
3940 let dev = Device::cpu();
3941 let (batch, seqlen, dim, dstate) = (2usize, 3usize, 2usize, 2usize);
3942 let u = Tensor::from_f32(
3944 vec![
3945 0.5, -0.3, 1.0, 0.2, -0.5, 0.7, 0.25, 0.9, -0.8, 0.4, 0.6, -0.1,
3946 ],
3947 Shape::from_dims(&[batch, seqlen, dim]),
3948 &dev,
3949 )
3950 .unwrap();
3951 let delta = u
3953 .const_f32_like(
3954 vec![
3955 0.1, 0.2, 0.3, 0.15, 0.25, 0.4, 0.35, 0.05, 0.2, 0.3, 0.12, 0.28,
3956 ],
3957 Shape::from_dims(&[batch, seqlen, dim]),
3958 )
3959 .unwrap();
3960 let a = u
3962 .const_f32_like(
3963 vec![-0.7, -0.3, -0.5, -0.9],
3964 Shape::from_dims(&[dim, dstate]),
3965 )
3966 .unwrap();
3967 let b = u
3969 .const_f32_like(
3970 vec![
3971 1.0, 0.5, 0.25, 0.75, 0.6, 0.2, 0.8, 0.4, 0.3, 0.9, 0.55, 0.15,
3972 ],
3973 Shape::from_dims(&[batch, seqlen, dstate]),
3974 )
3975 .unwrap();
3976 let c = u
3978 .const_f32_like(
3979 vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3, 1.2, 0.6, 0.9, 0.4, 0.7, 1.1],
3980 Shape::from_dims(&[batch, seqlen, dstate]),
3981 )
3982 .unwrap();
3983 let y = u.selective_scan(&delta, &a, &b, &c, false);
3984
3985 let fused = y.realize_f32();
3987 assert_eq!(fused.len(), batch * seqlen * dim);
3988
3989 let graph = y.inner.graph().clone();
3991 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3992 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3993 assert_eq!(roots.len(), 1);
3994 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
3995 let ys = {
3996 let mut g = graph.write().unwrap();
3997 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
3998 .expect("unroll")
3999 .0[0] };
4001 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4002 .expect("realize selective_scan unroll oracle on CPU");
4003 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4005 assert_eq!(oracle.len(), fused.len());
4006 let mut max_drift = 0f32;
4007 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4008 let d = (o - f).abs();
4009 max_drift = max_drift.max(d);
4010 assert!(
4014 d < 1e-3,
4015 "selective_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4016 );
4017 }
4018 eprintln!("selective_scan multidim: max honest drift = {max_drift:e}");
4019 }
4020
4021 #[test]
4027 fn selective_scan_softplus_multitoken_unroll_matches_fused_kernel() {
4028 use fuel_graph::registry::FusedOps;
4029 let dev = Device::cpu();
4030 let (batch, seqlen, dim, dstate) = (1usize, 3usize, 2usize, 2usize);
4031 let u = Tensor::from_f32(
4032 vec![0.5, -0.3, 1.0, 0.2, -0.5, 0.7],
4033 Shape::from_dims(&[batch, seqlen, dim]),
4034 &dev,
4035 )
4036 .unwrap();
4037 let delta = u
4039 .const_f32_like(
4040 vec![-0.5, 0.2, 0.8, -1.0, 0.3, -0.2],
4041 Shape::from_dims(&[batch, seqlen, dim]),
4042 )
4043 .unwrap();
4044 let a = u
4045 .const_f32_like(
4046 vec![-0.7, -0.3, -0.5, -0.9],
4047 Shape::from_dims(&[dim, dstate]),
4048 )
4049 .unwrap();
4050 let b = u
4051 .const_f32_like(
4052 vec![1.0, 0.5, 0.25, 0.75, 0.6, 0.2],
4053 Shape::from_dims(&[batch, seqlen, dstate]),
4054 )
4055 .unwrap();
4056 let c = u
4057 .const_f32_like(
4058 vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3],
4059 Shape::from_dims(&[batch, seqlen, dstate]),
4060 )
4061 .unwrap();
4062 let y = u.selective_scan(&delta, &a, &b, &c, true);
4063
4064 let fused = y.realize_f32();
4065 assert_eq!(fused.len(), batch * seqlen * dim);
4066
4067 let graph = y.inner.graph().clone();
4068 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4069 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4070 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
4071 let ys = {
4072 let mut g = graph.write().unwrap();
4073 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4074 .expect("unroll")
4075 .0[0] };
4077 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4078 .expect("realize selective_scan softplus oracle on CPU");
4079 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4080 let mut max_drift = 0f32;
4081 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4082 let d = (o - f).abs();
4083 max_drift = max_drift.max(d);
4084 assert!(
4085 d < 1e-3,
4086 "selective_scan softplus mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4087 );
4088 }
4089 eprintln!("selective_scan softplus: max honest drift = {max_drift:e}");
4090 }
4091
4092 #[test]
4106 fn ssd_chunk_scan_multidim_unroll_matches_fused_kernel() {
4107 use fuel_graph::registry::FusedOps;
4108 let dev = Device::cpu();
4109 let (batch, seqlen, heads, head_dim, state_dim, chunk) =
4110 (2usize, 4usize, 2usize, 2usize, 2usize, 2usize);
4111 let x_data: Vec<f32> = (0..batch * seqlen * heads * head_dim)
4114 .map(|i| 0.20 + 0.05 * i as f32)
4115 .collect();
4116 let dt_data: Vec<f32> = (0..batch * seqlen * heads)
4117 .map(|i| 0.10 + 0.04 * i as f32)
4118 .collect();
4119 let b_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4120 .map(|i| 0.15 + 0.03 * i as f32)
4121 .collect();
4122 let c_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4123 .map(|i| 0.25 + 0.035 * i as f32)
4124 .collect();
4125 let x = Tensor::from_f32(
4126 x_data,
4127 Shape::from_dims(&[batch, seqlen, heads, head_dim]),
4128 &dev,
4129 )
4130 .unwrap();
4131 let dt = x
4132 .const_f32_like(dt_data, Shape::from_dims(&[batch, seqlen, heads]))
4133 .unwrap();
4134 let a = x
4135 .const_f32_like(vec![-0.6f32, -1.0], Shape::from_dims(&[heads]))
4136 .unwrap();
4137 let b = x
4138 .const_f32_like(b_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4139 .unwrap();
4140 let c = x
4141 .const_f32_like(c_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4142 .unwrap();
4143 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, chunk);
4144
4145 let fused = y.realize_f32();
4147 assert_eq!(fused.len(), batch * seqlen * heads * head_dim);
4148
4149 let graph = y.inner.graph().clone();
4151 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4152 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4153 assert_eq!(roots.len(), 1);
4154 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SSD_CHUNK_SCAN);
4155 let ys = {
4156 let mut g = graph.write().unwrap();
4157 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4158 .expect("unroll")
4159 .0[0] };
4161 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4162 .expect("realize ssd_chunk_scan unroll oracle on CPU");
4163 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, heads * head_dim);
4166 assert_eq!(oracle.len(), fused.len());
4167 let mut max_drift = 0f32;
4168 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4169 let d = (o - f).abs();
4170 max_drift = max_drift.max(d);
4171 assert!(
4172 d < 1e-3,
4173 "ssd_chunk_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4174 );
4175 }
4176 eprintln!("ssd_chunk_scan multidim: max honest drift = {max_drift:e}");
4177 }
4178
4179 #[test]
4188 fn nf4_matmul_decompose_matches_kernel() {
4189 use fuel_graph::Op;
4190 use fuel_graph::registry::FusedOps;
4191 let dev = Device::cpu();
4192 let weight = crate::nf4::nf4_from_bytes(
4194 vec![247_u8, 247, 127, 127],
4195 vec![1.0_f32, 2.0, 10.0, 20.0],
4196 2,
4197 4,
4198 2,
4199 &dev,
4200 )
4201 .expect("nf4_from_bytes");
4202 let act = Tensor::from_graph_tensor(
4203 weight
4204 .w_packed
4205 .graph_tensor()
4206 .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
4207 .unwrap(),
4208 );
4209 let y = weight.matmul(&act);
4210
4211 let graph = y.inner.graph().clone();
4213 let id = y.inner.id();
4214 let roots =
4215 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
4216 assert_eq!(roots.len(), 1, "lowering should keep a single root");
4217
4218 {
4221 let g = graph.read().unwrap();
4222 let mut stack = vec![roots[0]];
4223 let mut seen = std::collections::HashSet::new();
4224 while let Some(nid) = stack.pop() {
4225 if !seen.insert(nid) {
4226 continue;
4227 }
4228 let node = g.node(nid);
4229 assert!(
4230 !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::NF4_MATMUL),
4231 "decomposed graph still contains an Op::Fused(NF4_MATMUL) island",
4232 );
4233 for &inp in &node.inputs {
4234 stack.push(inp);
4235 }
4236 }
4237 }
4238
4239 let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
4240 .expect("realize decomposed Nf4Matmul on CPU");
4241 assert_eq!(got.len(), 2);
4242 assert!((got[0] - 10.0).abs() < 1e-4, "out 0: {}", got[0]);
4243 assert!((got[1] - 50.0).abs() < 1e-4, "out 1: {}", got[1]);
4244 }
4245
4246 fn lower_realize_fused(
4252 anchor: &Tensor,
4253 op: fuel_graph::Op,
4254 inputs: Vec<fuel_graph::NodeId>,
4255 shape: Shape,
4256 ) -> Vec<f32> {
4257 let graph = anchor.inner.graph().clone();
4258 let fused_id = {
4259 let mut g = graph.write().unwrap();
4260 g.push(fuel_graph::Node {
4261 op,
4262 inputs,
4263 shape,
4264 dtype: DType::F32,
4265 })
4266 };
4267 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4268 .optimize_to_fixpoint(&graph, &[fused_id]);
4269 assert_eq!(roots.len(), 1, "lowering keeps a single root");
4270 crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &Device::cpu())
4271 .expect("realize decomposed fused op on CPU")
4272 }
4273
4274 #[test]
4275 fn powi_backward_decompose_matches_reference() {
4276 use fuel_graph::registry::{FusedOpParams, FusedOps};
4277 let dev = Device::cpu();
4278 let exp = 3i32;
4279 let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4280 let up_data = vec![1.0f32, 0.5, 2.0, -1.0];
4281 let shape = Shape::from_dims(&[4]);
4282 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4283 let up = x.const_f32_like(up_data.clone(), shape.clone()).unwrap();
4284 let got = lower_realize_fused(
4285 &x,
4286 fuel_graph::Op::Fused(FusedOps::POWI_BACKWARD, FusedOpParams::PowIBackward { exp }),
4287 vec![x.inner.id(), up.inner.id()],
4288 shape,
4289 );
4290 let expected: Vec<f32> = x_data
4292 .iter()
4293 .zip(&up_data)
4294 .map(|(&x, &u)| exp as f32 * x.powi(exp - 1) * u)
4295 .collect();
4296 assert_eq!(got.len(), expected.len());
4297 for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4298 assert!(
4299 (g - e).abs() < 1e-4,
4300 "powi_backward at {i}: got {g}, expected {e}"
4301 );
4302 }
4303 }
4304
4305 #[test]
4313 fn inplace_affine_decompose_matches_affine_reference() {
4314 use fuel_graph::registry::{FusedOpParams, FusedOps};
4315 let dev = Device::cpu();
4316 let (mul, add) = (2.5f64, -1.0f64);
4317 let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4318 let shape = Shape::from_dims(&[4]);
4319 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4320 let got = lower_realize_fused(
4321 &x,
4322 fuel_graph::Op::Fused(
4323 FusedOps::INPLACE_AFFINE,
4324 FusedOpParams::InplaceAffine { mul, add },
4325 ),
4326 vec![x.inner.id()],
4327 shape,
4328 );
4329 let expected: Vec<f32> = x_data
4331 .iter()
4332 .map(|&v| (mul as f32) * v + add as f32)
4333 .collect();
4334 assert_eq!(got.len(), expected.len());
4335 for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4336 assert_eq!(g, e, "inplace_affine at {i}: got {g}, expected {e}");
4337 }
4338 }
4339
4340 #[test]
4341 fn softmax_last_dim_backward_decompose_matches_reference() {
4342 use fuel_graph::registry::{FusedOpParams, FusedOps};
4343 let dev = Device::cpu();
4344 let (rows, cols) = (2usize, 3usize);
4345 let logits = [[0.5f32, -1.0, 2.0], [1.0, 0.0, -0.5]];
4347 let mut s_data = Vec::new();
4348 for row in &logits {
4349 let m = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4350 let exps: Vec<f32> = row.iter().map(|&v| (v - m).exp()).collect();
4351 let sum: f32 = exps.iter().sum();
4352 s_data.extend(exps.iter().map(|&e| e / sum));
4353 }
4354 let g_data = vec![1.0f32, -0.5, 0.3, 0.2, 1.5, -1.0];
4355 let shape = Shape::from_dims(&[rows, cols]);
4356 let s = Tensor::from_f32(s_data.clone(), shape.clone(), &dev).unwrap();
4357 let g = s.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4358 let got = lower_realize_fused(
4359 &s,
4360 fuel_graph::Op::Fused(
4361 FusedOps::SOFTMAX_LAST_DIM_BACKWARD,
4362 FusedOpParams::SoftmaxLastDimBackward,
4363 ),
4364 vec![s.inner.id(), g.inner.id()],
4365 shape,
4366 );
4367 let mut expected = vec![0.0f32; rows * cols];
4369 for r in 0..rows {
4370 let dot: f32 = (0..cols)
4371 .map(|c| g_data[r * cols + c] * s_data[r * cols + c])
4372 .sum();
4373 for c in 0..cols {
4374 expected[r * cols + c] = s_data[r * cols + c] * (g_data[r * cols + c] - dot);
4375 }
4376 }
4377 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4378 assert!(
4379 (gv - ev).abs() < 1e-5,
4380 "softmax_bwd at {i}: got {gv}, expected {ev}"
4381 );
4382 }
4383 }
4384
4385 #[test]
4386 fn rms_norm_last_dim_backward_decompose_matches_reference() {
4387 use fuel_graph::registry::{FusedOpParams, FusedOps};
4388 let dev = Device::cpu();
4389 let (rows, cols) = (2usize, 3usize);
4390 let eps = 1e-5f64;
4391 let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4392 let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4393 let shape = Shape::from_dims(&[rows, cols]);
4394 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4395 let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4396 let got = lower_realize_fused(
4397 &x,
4398 fuel_graph::Op::Fused(
4399 FusedOps::RMS_NORM_LAST_DIM_BACKWARD,
4400 FusedOpParams::RmsNormLastDimBackward { eps },
4401 ),
4402 vec![x.inner.id(), g.inner.id()],
4403 shape,
4404 );
4405 let n = cols as f32;
4407 let mut expected = vec![0.0f32; rows * cols];
4408 for r in 0..rows {
4409 let meansq: f32 = (0..cols).map(|c| x_data[r * cols + c].powi(2)).sum::<f32>() / n;
4410 let denom = meansq + eps as f32;
4411 let rrms = 1.0 / denom.sqrt();
4412 let s: f32 = (0..cols)
4413 .map(|c| g_data[r * cols + c] * x_data[r * cols + c])
4414 .sum();
4415 for c in 0..cols {
4416 let term = x_data[r * cols + c] * s / (n * denom);
4417 expected[r * cols + c] = rrms * (g_data[r * cols + c] - term);
4418 }
4419 }
4420 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4421 assert!(
4422 (gv - ev).abs() < 1e-4,
4423 "rms_norm_bwd at {i}: got {gv}, expected {ev}"
4424 );
4425 }
4426 }
4427
4428 #[test]
4429 fn layer_norm_last_dim_backward_decompose_matches_reference() {
4430 use fuel_graph::registry::{FusedOpParams, FusedOps};
4431 let dev = Device::cpu();
4432 let (rows, cols) = (2usize, 3usize);
4433 let eps = 1e-5f64;
4434 let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4435 let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4436 let shape = Shape::from_dims(&[rows, cols]);
4437 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4438 let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4439 let got = lower_realize_fused(
4440 &x,
4441 fuel_graph::Op::Fused(
4442 FusedOps::LAYER_NORM_LAST_DIM_BACKWARD,
4443 FusedOpParams::LayerNormLastDimBackward { eps },
4444 ),
4445 vec![x.inner.id(), g.inner.id()],
4446 shape,
4447 );
4448 let n = cols as f32;
4450 let mut expected = vec![0.0f32; rows * cols];
4451 for r in 0..rows {
4452 let mean_x: f32 = (0..cols).map(|c| x_data[r * cols + c]).sum::<f32>() / n;
4453 let var: f32 = (0..cols)
4454 .map(|c| (x_data[r * cols + c] - mean_x).powi(2))
4455 .sum::<f32>()
4456 / n;
4457 let istd = 1.0 / (var + eps as f32).sqrt();
4458 let xhat: Vec<f32> = (0..cols)
4459 .map(|c| (x_data[r * cols + c] - mean_x) * istd)
4460 .collect();
4461 let mean_g: f32 = (0..cols).map(|c| g_data[r * cols + c]).sum::<f32>() / n;
4462 let mean_gxh: f32 = (0..cols)
4463 .map(|c| g_data[r * cols + c] * xhat[c])
4464 .sum::<f32>()
4465 / n;
4466 for c in 0..cols {
4467 expected[r * cols + c] =
4468 istd * (g_data[r * cols + c] - mean_g - xhat[c] * mean_gxh);
4469 }
4470 }
4471 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4472 assert!(
4473 (gv - ev).abs() < 1e-4,
4474 "layer_norm_bwd at {i}: got {gv}, expected {ev}"
4475 );
4476 }
4477 }
4478
4479 #[test]
4480 fn reduce_max_to_backward_decompose_matches_reference() {
4481 use fuel_graph::registry::{FusedOpParams, FusedOps};
4482 let dev = Device::cpu();
4483 let x_data = vec![1.0f32, 3.0, 3.0, 2.0, 0.0, 1.0]; let up_data = vec![10.0f32, 5.0]; let x_shape = Shape::from_dims(&[2, 3]);
4487 let up_shape = Shape::from_dims(&[2, 1]);
4488 let x = Tensor::from_f32(x_data.clone(), x_shape.clone(), &dev).unwrap();
4489 let up = x.const_f32_like(up_data.clone(), up_shape).unwrap();
4490 let got = lower_realize_fused(
4491 &x,
4492 fuel_graph::Op::Fused(
4493 FusedOps::REDUCE_MAX_TO_BACKWARD,
4494 FusedOpParams::ReduceMaxToBackward,
4495 ),
4496 vec![x.inner.id(), up.inner.id()],
4497 x_shape,
4498 );
4499 let expected = vec![0.0f32, 5.0, 5.0, 5.0, 0.0, 0.0];
4501 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4502 assert!(
4503 (gv - ev).abs() < 1e-5,
4504 "reduce_max_bwd at {i}: got {gv}, expected {ev}"
4505 );
4506 }
4507 }
4508
4509 fn causal_conv1d_check(use_silu: bool) {
4510 use fuel_graph::registry::{FusedOpParams, FusedOps};
4511 let dev = Device::cpu();
4512 let (b, c, k, seq) = (1usize, 2usize, 3usize, 2usize);
4513 let x_seq = seq + (k - 1); let x_data = vec![0.0f32, 1.0, 2.0, 3.0, 1.0, 0.0, -1.0, -2.0];
4516 let w_data = vec![0.5f32, 1.0, -0.5, 1.0, 0.0, 2.0];
4517 let bias_data = vec![0.1f32, -0.2];
4518 let x = Tensor::from_f32(x_data.clone(), Shape::from_dims(&[b, c, x_seq]), &dev).unwrap();
4519 let w = x
4520 .const_f32_like(w_data.clone(), Shape::from_dims(&[c, 1, k]))
4521 .unwrap();
4522 let bias = x
4523 .const_f32_like(bias_data.clone(), Shape::from_dims(&[c]))
4524 .unwrap();
4525 let got = lower_realize_fused(
4526 &x,
4527 fuel_graph::Op::Fused(
4528 FusedOps::CAUSAL_CONV1D,
4529 FusedOpParams::CausalConv1d { use_silu },
4530 ),
4531 vec![x.inner.id(), w.inner.id(), bias.inner.id()],
4532 Shape::from_dims(&[b, c, seq]),
4533 );
4534 let mut expected = vec![0.0f32; b * c * seq];
4536 for ch in 0..c {
4537 for t in 0..seq {
4538 let mut acc = 0.0f32;
4539 for kk in 0..k {
4540 acc += w_data[ch * k + kk] * x_data[ch * x_seq + t + kk];
4541 }
4542 acc += bias_data[ch];
4543 if use_silu {
4544 acc *= 1.0 / (1.0 + (-acc).exp());
4545 }
4546 expected[ch * seq + t] = acc;
4547 }
4548 }
4549 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4550 assert!(
4551 (gv - ev).abs() < 1e-4,
4552 "causal_conv1d (silu={use_silu}) at {i}: got {gv}, expected {ev}",
4553 );
4554 }
4555 }
4556
4557 #[test]
4558 fn causal_conv1d_decompose_matches_reference() {
4559 causal_conv1d_check(false);
4560 causal_conv1d_check(true);
4561 }
4562
4563 #[test]
4564 fn flash_attn_backward_decompose_matches_reference() {
4565 use fuel_graph::registry::{FusedOpParams, FusedOps};
4566 let dev = Device::cpu();
4567 let (sq, sk, d) = (2usize, 2usize, 2usize);
4568 #[allow(clippy::approx_constant)]
4571 let scale = 0.7071f32;
4572 let q_data = vec![0.1f32, -0.2, 0.3, 0.5]; let k_data = vec![0.4f32, 0.1, -0.3, 0.2]; let v_data = vec![1.0f32, 2.0, -1.0, 0.5]; let do_data = vec![0.5f32, -1.0, 0.2, 0.3]; let qshape = Shape::from_dims(&[1, 1, sq, d]);
4577 let kshape = Shape::from_dims(&[1, 1, sk, d]);
4578 let q = Tensor::from_f32(q_data.clone(), qshape.clone(), &dev).unwrap();
4579 let k = q.const_f32_like(k_data.clone(), kshape.clone()).unwrap();
4580 let v = q.const_f32_like(v_data.clone(), kshape.clone()).unwrap();
4581 let dout = q.const_f32_like(do_data.clone(), qshape.clone()).unwrap();
4582 let params = FusedOpParams::FlashAttnBackward {
4583 softmax_scale: scale,
4584 causal: false,
4585 window_size_left: None,
4586 window_size_right: None,
4587 softcap: None,
4588 };
4589 let inputs = vec![q.inner.id(), k.inner.id(), v.inner.id(), dout.inner.id()];
4590 let dq = lower_realize_fused(
4591 &q,
4592 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_Q, params.clone()),
4593 inputs.clone(),
4594 qshape.clone(),
4595 );
4596 let dk = lower_realize_fused(
4597 &q,
4598 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_K, params.clone()),
4599 inputs.clone(),
4600 kshape.clone(),
4601 );
4602 let dv = lower_realize_fused(
4603 &q,
4604 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_V, params),
4605 inputs,
4606 kshape,
4607 );
4608
4609 let mut p = vec![0.0f32; sq * sk];
4611 for i in 0..sq {
4612 let mut scores = vec![0.0f32; sk];
4613 for j in 0..sk {
4614 let mut s = 0.0f32;
4615 for l in 0..d {
4616 s += q_data[i * d + l] * k_data[j * d + l];
4617 }
4618 scores[j] = scale * s;
4619 }
4620 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4621 let mut sum = 0.0f32;
4622 let e: Vec<f32> = scores
4623 .iter()
4624 .map(|&s| {
4625 let x = (s - m).exp();
4626 sum += x;
4627 x
4628 })
4629 .collect();
4630 for j in 0..sk {
4631 p[i * sk + j] = e[j] / sum;
4632 }
4633 }
4634 let mut ref_dv = vec![0.0f32; sk * d];
4636 for j in 0..sk {
4637 for l in 0..d {
4638 ref_dv[j * d + l] = (0..sq).map(|i| p[i * sk + j] * do_data[i * d + l]).sum();
4639 }
4640 }
4641 let mut dp = vec![0.0f32; sq * sk];
4643 for i in 0..sq {
4644 for j in 0..sk {
4645 dp[i * sk + j] = (0..d).map(|l| do_data[i * d + l] * v_data[j * d + l]).sum();
4646 }
4647 }
4648 let mut dsr = vec![0.0f32; sq * sk];
4650 for i in 0..sq {
4651 let rowdot: f32 = (0..sk).map(|j| dp[i * sk + j] * p[i * sk + j]).sum();
4652 for j in 0..sk {
4653 dsr[i * sk + j] = scale * p[i * sk + j] * (dp[i * sk + j] - rowdot);
4654 }
4655 }
4656 let mut ref_dq = vec![0.0f32; sq * d];
4658 for i in 0..sq {
4659 for l in 0..d {
4660 ref_dq[i * d + l] = (0..sk).map(|j| dsr[i * sk + j] * k_data[j * d + l]).sum();
4661 }
4662 }
4663 let mut ref_dk = vec![0.0f32; sk * d];
4664 for j in 0..sk {
4665 for l in 0..d {
4666 ref_dk[j * d + l] = (0..sq).map(|i| dsr[i * sk + j] * q_data[i * d + l]).sum();
4667 }
4668 }
4669 let check = |name: &str, got: &[f32], exp: &[f32]| {
4670 assert_eq!(got.len(), exp.len(), "{name} length");
4671 for (i, (&gv, &ev)) in got.iter().zip(exp).enumerate() {
4672 assert!(
4673 (gv - ev).abs() < 1e-4,
4674 "{name} at {i}: got {gv}, expected {ev}"
4675 );
4676 }
4677 };
4678 check("dQ", &dq, &ref_dq);
4679 check("dK", &dk, &ref_dk);
4680 check("dV", &dv, &ref_dv);
4681 }
4682
4683 #[test]
4684 fn paged_attn_decompose_matches_reference() {
4685 use fuel_graph::registry::{FusedOpParams, FusedOps};
4686 let dev = Device::cpu();
4687 let block_size = 2usize;
4689 let scale = 1.0f32;
4690 let kc = vec![
4693 1.0f32, 0.0, 0.0, 1.0, 9.0, 9.0, 9.0, 9.0, 1.0, 1.0, 2.0, 0.0,
4694 ];
4695 let vc = vec![
4696 1.0f32, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0, 6.0, 7.0, 8.0,
4697 ];
4698 let q_data = vec![0.5f32, 0.5]; let bt = vec![0u32, 2u32]; let cl = vec![3u32]; let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, 1, 1, 2]), &dev).unwrap();
4703 let kcache = q
4704 .const_f32_like(kc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4705 .unwrap();
4706 let vcache = q
4707 .const_f32_like(vc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4708 .unwrap();
4709 let block_table = q.const_u32_like(bt, Shape::from_dims(&[1, 2])).unwrap();
4710 let context_lens = q.const_u32_like(cl, Shape::from_dims(&[1])).unwrap();
4711 let params = FusedOpParams::PagedAttn {
4712 softmax_scale: scale,
4713 block_size,
4714 softcap: None,
4715 };
4716 let inputs = vec![
4717 q.inner.id(),
4718 kcache.inner.id(),
4719 vcache.inner.id(),
4720 block_table.inner.id(),
4721 context_lens.inner.id(),
4722 ];
4723 let got = lower_realize_fused(
4724 &q,
4725 fuel_graph::Op::Fused(FusedOps::PAGED_ATTN, params),
4726 inputs,
4727 Shape::from_dims(&[1, 1, 1, 2]),
4728 );
4729
4730 let (kv_len, ctx) = (4usize, 3usize);
4733 let mut k_seq = vec![];
4734 let mut v_seq = vec![];
4735 for &blk in &[0usize, 2usize] {
4736 for p in 0..block_size {
4737 let base = (blk * block_size + p) * 2; k_seq.push([kc[base], kc[base + 1]]);
4739 v_seq.push([vc[base], vc[base + 1]]);
4740 }
4741 }
4742 let mut scores = vec![0.0f32; kv_len];
4743 for j in 0..kv_len {
4744 let s = q_data[0] * k_seq[j][0] + q_data[1] * k_seq[j][1];
4745 scores[j] = if j >= ctx {
4746 f32::NEG_INFINITY
4747 } else {
4748 scale * s
4749 };
4750 }
4751 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4752 let mut sum = 0.0f32;
4753 let e: Vec<f32> = scores
4754 .iter()
4755 .map(|&s| {
4756 let x = (s - m).exp();
4757 sum += x;
4758 x
4759 })
4760 .collect();
4761 let mut expected = vec![0.0f32; 2];
4762 for j in 0..kv_len {
4763 for l in 0..2 {
4764 expected[l] += (e[j] / sum) * v_seq[j][l];
4765 }
4766 }
4767 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4768 assert!(
4769 (gv - ev).abs() < 1e-4,
4770 "paged_attn at {i}: got {gv}, expected {ev}"
4771 );
4772 }
4773 }
4774
4775 #[test]
4776 #[cfg(feature = "cuda")]
4777 fn cuda_executor_matches_cpu_on_add_mul() {
4778 let a =
4779 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
4780 let b = a
4781 .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
4782 .unwrap();
4783 let c = a.add(&b).unwrap().mul(&a).unwrap();
4784 let cpu_result = c.realize_f32();
4785 let executor = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4786 let cuda_result = c.realize_f32_cuda(&executor);
4787 assert_eq!(cpu_result, cuda_result);
4788 }
4789
4790 #[test]
4808 #[cfg(feature = "cuda")]
4809 #[ignore = "requires a live CUDA device"]
4810 fn maximum_minimum_nan_convention_lazy_realize_smoke() {
4811 let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4812 Ok(d) => d,
4813 Err(e) => {
4814 return fuel_test_support::hardware::skip(
4815 fuel_test_support::hardware::Hardware::Cuda,
4816 fuel_test_support::hardware::Missing::device(format!(
4817 "CudaDevice::new(0): {e:?}"
4818 )),
4819 );
4820 }
4821 };
4822 let a = Tensor::from_f32(
4823 vec![f32::NAN, -2.0, 3.0, f32::NAN],
4824 Shape::from_dims(&[4]),
4825 &Device::cpu(),
4826 )
4827 .unwrap();
4828 let b = a
4829 .const_f32_like(vec![1.0, f32::NAN, 2.0, f32::NAN], Shape::from_dims(&[4]))
4830 .unwrap();
4831
4832 let max_lazy = a.maximum(&b).unwrap();
4833 let cpu_max = max_lazy.realize_f32();
4834 let cuda_max = max_lazy.realize_f32_cuda(&cuda);
4835 for i in 0..4 {
4836 assert_eq!(
4837 cpu_max[i].is_nan(),
4838 cuda_max[i].is_nan(),
4839 "maximum[{i}] NaN-ness must match: cpu={}, cuda={}",
4840 cpu_max[i],
4841 cuda_max[i],
4842 );
4843 if !cpu_max[i].is_nan() {
4844 assert_eq!(cpu_max[i], cuda_max[i], "maximum[{i}]");
4845 }
4846 }
4847
4848 let min_lazy = a.minimum(&b).unwrap();
4849 let cpu_min = min_lazy.realize_f32();
4850 let cuda_min = min_lazy.realize_f32_cuda(&cuda);
4851 for i in 0..4 {
4852 assert_eq!(
4853 cpu_min[i].is_nan(),
4854 cuda_min[i].is_nan(),
4855 "minimum[{i}] NaN-ness must match: cpu={}, cuda={}",
4856 cpu_min[i],
4857 cuda_min[i],
4858 );
4859 if !cpu_min[i].is_nan() {
4860 assert_eq!(cpu_min[i], cuda_min[i], "minimum[{i}]");
4861 }
4862 }
4863 }
4864
4865 #[test]
4888 #[cfg(feature = "cuda")]
4889 #[ignore = "requires a live CUDA device"]
4890 fn relu_nan_convention_lazy_realize_smoke() {
4891 let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4892 Ok(d) => d,
4893 Err(e) => {
4894 return fuel_test_support::hardware::skip(
4895 fuel_test_support::hardware::Hardware::Cuda,
4896 fuel_test_support::hardware::Missing::device(format!(
4897 "CudaDevice::new(0): {e:?}"
4898 )),
4899 );
4900 }
4901 };
4902 let a = Tensor::from_f32(
4903 vec![f32::NAN, -2.0, 3.0],
4904 Shape::from_dims(&[3]),
4905 &Device::cpu(),
4906 )
4907 .unwrap();
4908 let relu_lazy = a.relu();
4909 let cpu_result = relu_lazy.realize_f32();
4910 let cuda_result = relu_lazy.realize_f32_cuda(&cuda);
4911
4912 for i in 0..3 {
4913 assert_eq!(
4914 cpu_result[i].is_nan(),
4915 cuda_result[i].is_nan(),
4916 "relu[{i}] NaN-ness must match: cpu={}, cuda={}",
4917 cpu_result[i],
4918 cuda_result[i],
4919 );
4920 if !cpu_result[i].is_nan() {
4921 assert_eq!(cpu_result[i], cuda_result[i], "relu[{i}]");
4922 }
4923 }
4924 }
4925
4926 #[test]
4927 #[cfg(feature = "cuda")]
4928 fn cuda_executor_matches_cpu_on_matmul() {
4929 let a = Tensor::from_f32(
4930 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4931 Shape::from_dims(&[2, 3]),
4932 &Device::cpu(),
4933 )
4934 .unwrap();
4935 let b = a
4936 .const_f32_like(
4937 vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
4938 Shape::from_dims(&[3, 2]),
4939 )
4940 .unwrap();
4941 let c = a.matmul(&b).unwrap();
4942 let cpu = c.realize_f32();
4943 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4944 let cuda = c.realize_f32_cuda(&exe);
4945 assert_eq!(cpu.len(), cuda.len());
4946 for (i, (a, b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4947 assert!((a - b).abs() < 1e-3, "matmul[{i}]: cpu={a}, cuda={b}",);
4948 }
4949 }
4950
4951 #[test]
4952 #[cfg(feature = "cuda")]
4953 fn cuda_executor_matches_cpu_on_broadcast_matmul() {
4954 let x = Tensor::from_f32(
4957 (0..12).map(|i| i as f32 * 0.1).collect::<Vec<_>>(),
4958 Shape::from_dims(&[1, 3, 4]),
4959 &Device::cpu(),
4960 )
4961 .unwrap();
4962 let w = x
4963 .const_f32_like(
4964 (0..8).map(|i| i as f32 * 0.2).collect::<Vec<_>>(),
4965 Shape::from_dims(&[4, 2]),
4966 )
4967 .unwrap();
4968 let y = x.matmul(&w).unwrap();
4969 let cpu = y.realize_f32();
4970 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4971 let cuda = y.realize_f32_cuda(&exe);
4972 assert_eq!(cpu.len(), cuda.len());
4973 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4974 assert!((a - b).abs() < 1e-3, "bcast_mm[{i}]: cpu={a}, cuda={b}");
4975 }
4976 }
4977
4978 #[test]
4979 #[cfg(feature = "cuda")]
4980 fn cuda_executor_matches_cpu_on_permute() {
4981 let x = Tensor::from_f32(
4982 (0..24).map(|i| i as f32).collect::<Vec<_>>(),
4983 Shape::from_dims(&[1, 2, 3, 4]),
4984 &Device::cpu(),
4985 )
4986 .unwrap();
4987 let y = x.permute([0, 2, 1, 3_usize]).unwrap();
4988 let cpu = y.realize_f32();
4989 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4990 let cuda = y.realize_f32_cuda(&exe);
4991 assert_eq!(cpu, cuda, "permute mismatch");
4992 }
4993
4994 #[test]
4995 #[cfg(feature = "cuda")]
4996 fn cuda_executor_matches_cpu_on_softmax() {
4997 let x = Tensor::from_f32(
4998 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4999 Shape::from_dims(&[2, 3]),
5000 &Device::cpu(),
5001 )
5002 .unwrap();
5003 let y = x.softmax_last_dim().unwrap();
5004 let cpu = y.realize_f32();
5005 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5006 let cuda = y.realize_f32_cuda(&exe);
5007 assert_eq!(cpu.len(), cuda.len());
5008 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5009 assert!((a - b).abs() < 1e-4, "softmax[{i}]: cpu={a}, cuda={b}");
5010 }
5011 }
5012
5013 #[test]
5028 #[cfg(feature = "cuda")]
5029 fn cuda_executor_matches_cpu_on_softmax_via_lowering() {
5030 let n = 24;
5033 let last = 5;
5034 let data: Vec<f32> = (0..n * last)
5035 .map(|i| ((i as f32) * 0.13).sin() * 2.0 - 0.7)
5036 .collect();
5037 let x = Tensor::from_f32(data, Shape::from_dims(&[n, last]), &Device::cpu()).unwrap();
5038 let y = x.softmax_last_dim().unwrap();
5039
5040 let cpu = y.realize_f32();
5043
5044 let graph = y.inner.graph().clone();
5050 let registry = fuel_graph::opt::RuleRegistry::lowering_only();
5051 let remapped = registry.optimize_to_fixpoint(&graph, &[y.inner.id()]);
5052 let dev = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5053 let fc_device: crate::Device = dev.clone().into();
5054 let cuda = crate::pipelined_bridge::realize_one_as::<f32>(&graph, remapped[0], &fc_device)
5055 .expect("realize lowered softmax on CUDA");
5056
5057 assert_eq!(cpu.len(), cuda.len());
5058 let mut max_abs_err = 0.0_f32;
5059 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5060 let err = (a - b).abs();
5061 if err > max_abs_err {
5062 max_abs_err = err;
5063 }
5064 assert!(
5065 err < 1e-5,
5066 "lowered softmax[{i}]: cpu={a} (fused), cuda={b} (composed), err={err}",
5067 );
5068 }
5069 eprintln!("max_abs_err over lowered-vs-fused softmax: {max_abs_err:.3e}");
5070 }
5071
5072 #[test]
5073 #[cfg(feature = "cuda")]
5074 fn cuda_executor_matches_cpu_on_concat_slice() {
5075 let a = Tensor::from_f32(
5076 vec![1.0, 2.0, 3.0, 4.0],
5077 Shape::from_dims(&[2, 2]),
5078 &Device::cpu(),
5079 )
5080 .unwrap();
5081 let b = a
5082 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], Shape::from_dims(&[2, 2]))
5083 .unwrap();
5084 let cat = a.concat(&b, 1).unwrap(); let sliced = cat.slice(1, 1, 2).unwrap(); let cpu = sliced.realize_f32();
5087 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5088 let cuda = sliced.realize_f32_cuda(&exe);
5089 assert_eq!(cpu, cuda, "concat+slice mismatch");
5090 }
5091
5092 #[test]
5093 #[cfg(feature = "cuda")]
5094 fn cuda_executor_matches_cpu_on_rms_norm() {
5095 let x = Tensor::from_f32(
5096 (0..8).map(|i| i as f32 * 0.5 - 1.5).collect::<Vec<_>>(),
5097 Shape::from_dims(&[2, 4]),
5098 &Device::cpu(),
5099 )
5100 .unwrap();
5101 let y = x.rms_norm_last_dim(1e-5).unwrap();
5102 let cpu = y.realize_f32();
5103 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5104 let cuda = y.realize_f32_cuda(&exe);
5105 assert_eq!(cpu.len(), cuda.len());
5106 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5107 assert!((a - b).abs() < 1e-3, "rms_norm[{i}]: cpu={a}, cuda={b}");
5108 }
5109 }
5110
5111 #[test]
5112 fn realize_f64_through_bridge() {
5113 let a =
5114 Tensor::from_f64(vec![1.5, 2.5, 3.5], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
5115 let b = a.mul(&a).unwrap();
5116 assert_eq!(b.realize_f64(), vec![2.25, 6.25, 12.25]);
5117 }
5118
5119 #[test]
5130 fn gap327_realize_f32_on_f64_root_must_reject_not_reinterpret() {
5131 let t =
5132 Tensor::from_f64(vec![1.0_f64, 2.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
5133 let graph = t.inner.graph().clone();
5134 let target = t.inner.id();
5135 let device = Device::cpu();
5136
5137 let prev_hook = std::panic::take_hook();
5140 std::panic::set_hook(Box::new(|_| {}));
5141 let ctl_f64 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f64()));
5143 let ctl_f16 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f16()));
5146 let subj_method =
5148 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f32()));
5149 std::panic::set_hook(prev_hook);
5150
5151 let subj_raw = crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device);
5153 let bytes_view = crate::pipelined_bridge::realize_one_bytes(&graph, target, &device);
5156
5157 assert!(
5158 ctl_f64.is_ok(),
5159 "control: realize_f64 on an F64 root must succeed; got {ctl_f64:?}"
5160 );
5161 assert!(
5162 ctl_f16.is_err(),
5163 "positive control: realize_f16() on an F64 root must panic (guard fires); if it \
5164 does not, the harness is not observing panics and the subject asserts are vacuous"
5165 );
5166 assert!(
5167 subj_method.is_err(),
5168 "realize_f32() on an F64 root must REJECT (panic), not silently reinterpret bytes"
5169 );
5170 assert!(
5171 matches!(subj_raw, Err(fuel_ir::Error::UnexpectedDType { .. })),
5172 "realize_one_as::<f32> on an F64 root must return Err(UnexpectedDType), not \
5173 Ok(reinterpreted bytes); got {subj_raw:?}"
5174 );
5175 assert!(
5176 matches!(&bytes_view, Ok(b) if b.len() == 16),
5177 "byte view realize_one_bytes on an F64 [1.0, 2.0] root must return 16 raw bytes \
5178 (unguarded), not an error; got {bytes_view:?}"
5179 );
5180 }
5181
5182 #[test]
5183 fn lazy_tensor_mini_llama_block_forward() {
5184 let seq = 3;
5193 let d_head = 4; let num_heads = 2;
5195 let d_model = num_heads * d_head; let x_data: Vec<f32> = (0..seq * d_model).map(|i| i as f32 * 0.01).collect();
5199 let x =
5200 Tensor::from_f32(x_data, Shape::from_dims(&[1, seq, d_model]), &Device::cpu()).unwrap();
5201
5202 let w_q = x
5206 .const_f32_like(
5207 identity_matrix(d_model),
5208 Shape::from_dims(&[d_model, d_model]),
5209 )
5210 .unwrap();
5211 let w_k = x
5212 .const_f32_like(
5213 identity_matrix(d_model),
5214 Shape::from_dims(&[d_model, d_model]),
5215 )
5216 .unwrap();
5217 let w_v = x
5218 .const_f32_like(
5219 identity_matrix(d_model),
5220 Shape::from_dims(&[d_model, d_model]),
5221 )
5222 .unwrap();
5223 let w_o = x
5224 .const_f32_like(
5225 identity_matrix(d_model),
5226 Shape::from_dims(&[d_model, d_model]),
5227 )
5228 .unwrap();
5229
5230 let x_norm = x.rms_norm_last_dim(1e-6).unwrap();
5232 let q = x_norm.matmul(&w_q).unwrap();
5233 let k = x_norm.matmul(&w_k).unwrap();
5234 let v = x_norm.matmul(&w_v).unwrap();
5235
5236 let q_h = q
5238 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5239 .unwrap()
5240 .permute([0, 2, 1, 3_usize])
5241 .unwrap();
5242 let k_h = k
5243 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5244 .unwrap()
5245 .permute([0, 2, 1, 3_usize])
5246 .unwrap();
5247 let v_h = v
5248 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5249 .unwrap()
5250 .permute([0, 2, 1, 3_usize])
5251 .unwrap();
5252
5253 let q_r = q_h.rope(10000.0, 0).unwrap();
5255 let k_r = k_h.rope(10000.0, 0).unwrap();
5256
5257 let k_t = k_r.transpose().unwrap();
5259 let scores = q_r.matmul(&k_t).unwrap();
5260 let attn = scores.softmax_last_dim().unwrap();
5261 let attn_v = attn.matmul(&v_h).unwrap();
5262
5263 let merged = attn_v
5265 .permute([0, 2, 1, 3_usize])
5266 .unwrap()
5267 .reshape(Shape::from_dims(&[1, seq, d_model]))
5268 .unwrap();
5269 let attn_out = merged.matmul(&w_o).unwrap();
5270 let h = x.add(&attn_out).unwrap();
5271
5272 let result = h.realize_f32();
5274 assert_eq!(result.len(), seq * d_model);
5275 for &v in &result {
5276 assert!(
5277 v.is_finite(),
5278 "bridge-based LLaMA block output non-finite: {v}"
5279 );
5280 }
5281 }
5282
5283 fn identity_matrix(n: usize) -> Vec<f32> {
5285 let mut out = vec![0.0_f32; n * n];
5286 for i in 0..n {
5287 out[i * n + i] = 1.0;
5288 }
5289 out
5290 }
5291}
5292
5293impl Tensor {
5297 pub fn const_u32_like(
5299 &self,
5300 data: impl Into<Arc<[u32]>>,
5301 shape: impl Into<Shape>,
5302 ) -> std::result::Result<Self, fuel_ir::Error> {
5303 Ok(Self {
5304 inner: self.inner.const_u32_like(data, shape)?,
5305 })
5306 }
5307
5308 pub fn const_placeholder_like(&self, shape: impl Into<Shape>, dtype: fuel_ir::DType) -> Self {
5319 Self {
5320 inner: self.inner.const_placeholder_like(shape, dtype),
5321 }
5322 }
5323
5324 pub fn write_slice(&self, source: &Self, ranges: Vec<(usize, usize)>) -> crate::Result<Self> {
5336 let inner = self.inner.write_slice(&source.inner, ranges)?;
5337 Ok(Self { inner })
5338 }
5339
5340 pub fn write_slice_dyn(
5348 &self,
5349 source: &Self,
5350 ranges: Vec<(usize, usize)>,
5351 dyn_axis: usize,
5352 offset: fuel_ir::DynScalar,
5353 ) -> crate::Result<Self> {
5354 let inner = self
5355 .inner
5356 .write_slice_dyn(&source.inner, ranges, dyn_axis, offset)?;
5357 Ok(Self { inner })
5358 }
5359
5360 pub fn write_slice_rotating(
5375 &self,
5376 source: &Self,
5377 position: &Self,
5378 axis: usize,
5379 modulus: usize,
5380 ranges: Vec<(usize, usize)>,
5381 ) -> crate::Result<Self> {
5382 let inner = self.inner.write_slice_rotating(
5383 &source.inner,
5384 &position.inner,
5385 axis,
5386 modulus,
5387 ranges,
5388 )?;
5389 Ok(Self { inner })
5390 }
5391
5392 pub fn write_slice_doff(
5405 &self,
5406 source: &Self,
5407 offset: &Self,
5408 axis: usize,
5409 ranges: Vec<(usize, usize)>,
5410 ) -> crate::Result<Self> {
5411 let inner = self
5412 .inner
5413 .write_slice_doff(&source.inner, &offset.inner, axis, ranges)?;
5414 Ok(Self { inner })
5415 }
5416
5417 pub fn conv2d(
5428 &self,
5429 weight: &Self,
5430 bias: Option<&Self>,
5431 stride: (usize, usize),
5432 padding: (usize, usize),
5433 groups: usize,
5434 ) -> std::result::Result<Self, fuel_ir::Error> {
5435 if groups < 1 {
5436 return Err(
5437 fuel_ir::Error::Msg(format!("conv2d: groups must be >= 1, got {groups}",)).bt(),
5438 );
5439 }
5440 let x_shape = self.inner.shape();
5441 let x_dims = x_shape.dims();
5442 let w_shape = weight.inner.shape();
5443 let w_dims = w_shape.dims();
5444 if x_dims.len() != 4 {
5445 return Err(fuel_ir::Error::Msg(format!(
5446 "conv2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5447 ))
5448 .bt());
5449 }
5450 if w_dims.len() != 4 {
5451 return Err(fuel_ir::Error::Msg(format!(
5452 "conv2d: weight must be rank 4 [Cout, Cin/groups, Kh, Kw], got {w_dims:?}",
5453 ))
5454 .bt());
5455 }
5456 let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5457 let (cout, cin_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5458 if cin != cin_per_g * groups {
5459 return Err(fuel_ir::Error::Msg(format!(
5460 "conv2d: x has {cin} in-channels but weight expects {} ({cin_per_g}*{groups})",
5461 cin_per_g * groups,
5462 ))
5463 .bt());
5464 }
5465 if cout % groups != 0 {
5466 return Err(fuel_ir::Error::Msg(format!(
5467 "conv2d: Cout={cout} must be divisible by groups={groups}",
5468 ))
5469 .bt());
5470 }
5471 if let Some(b) = bias {
5472 let b_shape = b.inner.shape();
5473 let b_dims = b_shape.dims();
5474 if b_dims != [cout] {
5475 return Err(fuel_ir::Error::Msg(format!(
5476 "conv2d: bias shape {b_dims:?} must match [Cout={cout}]",
5477 ))
5478 .bt());
5479 }
5480 }
5481 let (stride_h, stride_w) = stride;
5482 let (pad_h, pad_w) = padding;
5483 if stride_h < 1 || stride_w < 1 {
5484 return Err(fuel_ir::Error::Msg(format!(
5485 "conv2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5486 ))
5487 .bt());
5488 }
5489 let h_padded = h_in + 2 * pad_h;
5490 let w_padded = w_in + 2 * pad_w;
5491 if h_padded < kh || w_padded < kw {
5492 return Err(fuel_ir::Error::Msg(format!(
5493 "conv2d: padded input ({h_padded}x{w_padded}) smaller than kernel ({kh}x{kw})",
5494 ))
5495 .bt());
5496 }
5497 Ok(Self {
5498 inner: self.inner.conv2d(
5499 &weight.inner,
5500 bias.map(|b| &b.inner),
5501 stride,
5502 padding,
5503 groups,
5504 ),
5505 })
5506 }
5507
5508 pub fn flash_attn(
5518 &self,
5519 k: &Self,
5520 v: &Self,
5521 alibi_slopes: Option<&Self>,
5522 softmax_scale: f32,
5523 causal: bool,
5524 window_size_left: Option<usize>,
5525 window_size_right: Option<usize>,
5526 softcap: Option<f32>,
5527 ) -> std::result::Result<Self, fuel_ir::Error> {
5528 let q_shape = self.inner.shape();
5529 let q_dims = q_shape.dims();
5530 let k_shape = k.inner.shape();
5531 let k_dims = k_shape.dims();
5532 let v_shape = v.inner.shape();
5533 let v_dims = v_shape.dims();
5534 if q_dims.len() != 4 {
5535 return Err(fuel_ir::Error::Msg(format!(
5536 "flash_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5537 ))
5538 .bt());
5539 }
5540 if k_dims.len() != 4 {
5541 return Err(fuel_ir::Error::Msg(format!(
5542 "flash_attn: k must be rank 4 [B, Hkv, Sk, D], got {k_dims:?}",
5543 ))
5544 .bt());
5545 }
5546 if v_dims.len() != 4 {
5547 return Err(fuel_ir::Error::Msg(format!(
5548 "flash_attn: v must be rank 4 [B, Hkv, Sk, D], got {v_dims:?}",
5549 ))
5550 .bt());
5551 }
5552 let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5553 let (bk, hkv, sk, dk) = (k_dims[0], k_dims[1], k_dims[2], k_dims[3]);
5554 let (bv, hkv_v, sk_v, dv) = (v_dims[0], v_dims[1], v_dims[2], v_dims[3]);
5555 if b != bk || b != bv {
5556 return Err(fuel_ir::Error::Msg(
5557 format!("flash_attn: B mismatch q={b} k={bk} v={bv}",),
5558 )
5559 .bt());
5560 }
5561 if hkv != hkv_v {
5562 return Err(fuel_ir::Error::Msg(format!(
5563 "flash_attn: Hkv mismatch k={hkv} vs v={hkv_v}",
5564 ))
5565 .bt());
5566 }
5567 if sk != sk_v {
5568 return Err(fuel_ir::Error::Msg(
5569 format!("flash_attn: Sk mismatch k={sk} vs v={sk_v}",),
5570 )
5571 .bt());
5572 }
5573 if d != dk || d != dv {
5574 return Err(fuel_ir::Error::Msg(format!(
5575 "flash_attn: head_dim mismatch q={d} k={dk} v={dv}",
5576 ))
5577 .bt());
5578 }
5579 if hkv == 0 || hq % hkv != 0 {
5580 return Err(fuel_ir::Error::Msg(format!(
5581 "flash_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5582 ))
5583 .bt());
5584 }
5585 if let Some(a) = alibi_slopes {
5586 let a_shape = a.inner.shape();
5587 let a_dims = a_shape.dims();
5588 if a_dims != [hq] {
5589 return Err(fuel_ir::Error::Msg(format!(
5590 "flash_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5591 ))
5592 .bt());
5593 }
5594 }
5595 Ok(Self {
5596 inner: self.inner.flash_attn(
5597 &k.inner,
5598 &v.inner,
5599 alibi_slopes.map(|t| &t.inner),
5600 softmax_scale,
5601 causal,
5602 window_size_left,
5603 window_size_right,
5604 softcap,
5605 ),
5606 })
5607 }
5608
5609 pub fn paged_attn(
5619 &self,
5620 k_cache: &Self,
5621 v_cache: &Self,
5622 block_table: &Self,
5623 context_lens: &Self,
5624 alibi_slopes: Option<&Self>,
5625 softmax_scale: f32,
5626 block_size: usize,
5627 softcap: Option<f32>,
5628 ) -> std::result::Result<Self, fuel_ir::Error> {
5629 if block_size < 1 {
5630 return Err(fuel_ir::Error::Msg("paged_attn: block_size must be >= 1".into()).bt());
5631 }
5632 let q_shape = self.inner.shape();
5633 let q_dims = q_shape.dims();
5634 let kc_shape = k_cache.inner.shape();
5635 let kc_dims = kc_shape.dims();
5636 let vc_shape = v_cache.inner.shape();
5637 let vc_dims = vc_shape.dims();
5638 let bt_shape = block_table.inner.shape();
5639 let bt_dims = bt_shape.dims();
5640 let cl_shape = context_lens.inner.shape();
5641 let cl_dims = cl_shape.dims();
5642 if q_dims.len() != 4 {
5643 return Err(fuel_ir::Error::Msg(format!(
5644 "paged_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5645 ))
5646 .bt());
5647 }
5648 if kc_dims.len() != 4 {
5649 return Err(fuel_ir::Error::Msg(format!(
5650 "paged_attn: k_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {kc_dims:?}",
5651 )).bt());
5652 }
5653 if vc_dims.len() != 4 {
5654 return Err(fuel_ir::Error::Msg(format!(
5655 "paged_attn: v_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {vc_dims:?}",
5656 )).bt());
5657 }
5658 if bt_dims.len() != 2 {
5659 return Err(fuel_ir::Error::Msg(format!(
5660 "paged_attn: block_table must be rank 2 [B, max_blocks], got {bt_dims:?}",
5661 ))
5662 .bt());
5663 }
5664 if cl_dims.len() != 1 {
5665 return Err(fuel_ir::Error::Msg(format!(
5666 "paged_attn: context_lens must be rank 1 [B], got {cl_dims:?}",
5667 ))
5668 .bt());
5669 }
5670 let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5671 if kc_dims[1] != block_size {
5672 return Err(fuel_ir::Error::Msg(format!(
5673 "paged_attn: k_cache block dim {} != block_size {block_size}",
5674 kc_dims[1],
5675 ))
5676 .bt());
5677 }
5678 if vc_dims[1] != block_size {
5679 return Err(fuel_ir::Error::Msg(format!(
5680 "paged_attn: v_cache block dim {} != block_size {block_size}",
5681 vc_dims[1],
5682 ))
5683 .bt());
5684 }
5685 let hkv = kc_dims[2];
5686 if vc_dims[2] != hkv {
5687 return Err(fuel_ir::Error::Msg(format!(
5688 "paged_attn: Hkv mismatch k_cache={hkv} vs v_cache={}",
5689 vc_dims[2],
5690 ))
5691 .bt());
5692 }
5693 if kc_dims[3] != d || vc_dims[3] != d {
5694 return Err(fuel_ir::Error::Msg(format!(
5695 "paged_attn: D mismatch q={d} k={} v={}",
5696 kc_dims[3], vc_dims[3],
5697 ))
5698 .bt());
5699 }
5700 if hkv == 0 || hq % hkv != 0 {
5701 return Err(fuel_ir::Error::Msg(format!(
5702 "paged_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5703 ))
5704 .bt());
5705 }
5706 if bt_dims[0] != b {
5707 return Err(fuel_ir::Error::Msg(format!(
5708 "paged_attn: block_table batch dim {} != B={b}",
5709 bt_dims[0],
5710 ))
5711 .bt());
5712 }
5713 if cl_dims[0] != b {
5714 return Err(fuel_ir::Error::Msg(format!(
5715 "paged_attn: context_lens len {} != B={b}",
5716 cl_dims[0],
5717 ))
5718 .bt());
5719 }
5720 if block_table.inner.dtype() != fuel_ir::DType::U32 {
5721 return Err(fuel_ir::Error::Msg(format!(
5722 "paged_attn: block_table must be U32, got {:?}",
5723 block_table.inner.dtype(),
5724 ))
5725 .bt());
5726 }
5727 if context_lens.inner.dtype() != fuel_ir::DType::U32 {
5728 return Err(fuel_ir::Error::Msg(format!(
5729 "paged_attn: context_lens must be U32, got {:?}",
5730 context_lens.inner.dtype(),
5731 ))
5732 .bt());
5733 }
5734 if let Some(a) = alibi_slopes {
5735 let a_shape = a.inner.shape();
5736 let a_dims = a_shape.dims();
5737 if a_dims != [hq] {
5738 return Err(fuel_ir::Error::Msg(format!(
5739 "paged_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5740 ))
5741 .bt());
5742 }
5743 }
5744 Ok(Self {
5745 inner: self.inner.paged_attn(
5746 &k_cache.inner,
5747 &v_cache.inner,
5748 &block_table.inner,
5749 &context_lens.inner,
5750 alibi_slopes.map(|t| &t.inner),
5751 softmax_scale,
5752 block_size,
5753 softcap,
5754 ),
5755 })
5756 }
5757
5758 pub fn conv_transpose2d(
5768 &self,
5769 weight: &Self,
5770 stride: (usize, usize),
5771 padding: (usize, usize),
5772 output_padding: (usize, usize),
5773 dilation: (usize, usize),
5774 groups: usize,
5775 ) -> std::result::Result<Self, fuel_ir::Error> {
5776 if groups < 1 {
5777 return Err(fuel_ir::Error::Msg(format!(
5778 "conv_transpose2d: groups must be >= 1, got {groups}",
5779 ))
5780 .bt());
5781 }
5782 let x_shape = self.inner.shape();
5783 let x_dims = x_shape.dims();
5784 let w_shape = weight.inner.shape();
5785 let w_dims = w_shape.dims();
5786 if x_dims.len() != 4 {
5787 return Err(fuel_ir::Error::Msg(format!(
5788 "conv_transpose2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5789 ))
5790 .bt());
5791 }
5792 if w_dims.len() != 4 {
5793 return Err(fuel_ir::Error::Msg(format!(
5794 "conv_transpose2d: weight must be rank 4 [Cin, Cout/groups, Kh, Kw], got {w_dims:?}",
5795 )).bt());
5796 }
5797 let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5798 let (cin_w, cout_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5799 if cin != cin_w {
5800 return Err(fuel_ir::Error::Msg(format!(
5801 "conv_transpose2d: x has {cin} in-channels but weight has {cin_w}",
5802 ))
5803 .bt());
5804 }
5805 if cin % groups != 0 {
5806 return Err(fuel_ir::Error::Msg(format!(
5807 "conv_transpose2d: Cin={cin} must be divisible by groups={groups}",
5808 ))
5809 .bt());
5810 }
5811 let (stride_h, stride_w) = stride;
5812 let (pad_h, pad_w) = padding;
5813 let (out_pad_h, out_pad_w) = output_padding;
5814 let (dil_h, dil_w) = dilation;
5815 if stride_h < 1 || stride_w < 1 {
5816 return Err(fuel_ir::Error::Msg(format!(
5817 "conv_transpose2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5818 ))
5819 .bt());
5820 }
5821 if dil_h < 1 || dil_w < 1 {
5822 return Err(fuel_ir::Error::Msg(format!(
5823 "conv_transpose2d: dilation must be >= 1, got ({dil_h}, {dil_w})",
5824 ))
5825 .bt());
5826 }
5827 let h_out = h_in.saturating_sub(1) * stride_h + dil_h * (kh - 1) + out_pad_h + 1;
5828 let w_out = w_in.saturating_sub(1) * stride_w + dil_w * (kw - 1) + out_pad_w + 1;
5829 if h_out <= 2 * pad_h || w_out <= 2 * pad_w {
5830 return Err(fuel_ir::Error::Msg(format!(
5831 "conv_transpose2d: padding ({pad_h}x{pad_w}) is larger than the produced output dims ({h_out}x{w_out})",
5832 )).bt());
5833 }
5834 let _ = cout_per_g;
5835 Ok(Self {
5836 inner: self.inner.conv_transpose2d(
5837 &weight.inner,
5838 stride,
5839 padding,
5840 output_padding,
5841 dilation,
5842 groups,
5843 ),
5844 })
5845 }
5846
5847 pub fn conv_transpose1d(
5861 &self,
5862 weight: &Self,
5863 stride: usize,
5864 padding: usize,
5865 output_padding: usize,
5866 dilation: usize,
5867 groups: usize,
5868 ) -> std::result::Result<Self, fuel_ir::Error> {
5869 if groups < 1 {
5870 return Err(fuel_ir::Error::Msg(format!(
5871 "conv_transpose1d: groups must be >= 1, got {groups}",
5872 ))
5873 .bt());
5874 }
5875 let x_shape = self.inner.shape();
5876 let x_dims = x_shape.dims();
5877 let w_shape = weight.inner.shape();
5878 let w_dims = w_shape.dims();
5879 if x_dims.len() != 3 {
5880 return Err(fuel_ir::Error::Msg(format!(
5881 "conv_transpose1d: x must be rank 3 [N, Cin, Lin], got {x_dims:?}",
5882 ))
5883 .bt());
5884 }
5885 if w_dims.len() != 3 {
5886 return Err(fuel_ir::Error::Msg(format!(
5887 "conv_transpose1d: weight must be rank 3 [Cin, Cout/groups, K], got {w_dims:?}",
5888 ))
5889 .bt());
5890 }
5891 if stride < 1 {
5892 return Err(fuel_ir::Error::Msg(format!(
5893 "conv_transpose1d: stride must be >= 1, got {stride}",
5894 ))
5895 .bt());
5896 }
5897 if dilation < 1 {
5898 return Err(fuel_ir::Error::Msg(format!(
5899 "conv_transpose1d: dilation must be >= 1, got {dilation}",
5900 ))
5901 .bt());
5902 }
5903 let cin = x_dims[1];
5904 let cin_w = w_dims[0];
5905 if cin != cin_w {
5906 return Err(fuel_ir::Error::Msg(format!(
5907 "conv_transpose1d: x has {cin} in-channels but weight has {cin_w}",
5908 ))
5909 .bt());
5910 }
5911 if !cin.is_multiple_of(groups) {
5912 return Err(fuel_ir::Error::Msg(format!(
5913 "conv_transpose1d: Cin={cin} must be divisible by groups={groups}",
5914 ))
5915 .bt());
5916 }
5917 Ok(Self {
5918 inner: self.inner.conv_transpose1d(
5919 &weight.inner,
5920 stride,
5921 padding,
5922 output_padding,
5923 dilation,
5924 groups,
5925 ),
5926 })
5927 }
5928}
5929
5930impl Tensor {
5939 pub fn unsqueeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5945 let shape = self.inner.shape();
5946 let dim = dim.to_index_plus_one(&shape, "unsqueeze")?;
5947 Ok(Self {
5948 inner: self.inner.try_unsqueeze(dim)?,
5949 })
5950 }
5951
5952 pub fn triu(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5957 Ok(Self {
5958 inner: self.inner.triu(diagonal)?,
5959 })
5960 }
5961
5962 pub fn tril(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5965 Ok(Self {
5966 inner: self.inner.tril(diagonal)?,
5967 })
5968 }
5969
5970 pub fn log_softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
5974 Ok(Self {
5975 inner: self.inner.log_softmax_last_dim()?,
5976 })
5977 }
5978
5979 pub fn softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5988 let shape = self.inner.shape();
5989 let axis: usize = dim.to_index(&shape, "softmax")?;
5993 let m = self.max_keepdim(axis)?;
5994 let shifted = self.broadcast_sub(&m)?;
5995 let e = shifted.exp();
5996 let s = e.sum_keepdim(axis)?;
5997 e.broadcast_div(&s)
5998 }
5999
6000 pub fn log_softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6010 let shape = self.inner.shape();
6011 let axis: usize = dim.to_index(&shape, "log_softmax")?;
6012 let m = self.max_keepdim(axis)?;
6013 let shifted = self.broadcast_sub(&m)?;
6014 let lse = shifted.exp().sum_keepdim(axis)?.log();
6015 shifted.broadcast_sub(&lse)
6016 }
6017
6018 pub fn argmin_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6022 let shape = self.inner.shape();
6023 let dim = dim.to_index(&shape, "argmin_dim")?;
6024 Ok(Self {
6025 inner: self.inner.argmin_dim(dim),
6026 })
6027 }
6028
6029 pub fn masked_fill(
6035 &self,
6036 mask: &Self,
6037 value: fuel_ir::Scalar,
6038 ) -> std::result::Result<Self, fuel_ir::Error> {
6039 Ok(Self {
6040 inner: self.inner.masked_fill(&mask.inner, value)?,
6041 })
6042 }
6043
6044 pub fn index_add<D: Dim>(
6050 &self,
6051 dim: D,
6052 indices: &Self,
6053 src: &Self,
6054 ) -> std::result::Result<Self, fuel_ir::Error> {
6055 let shape = self.inner.shape();
6056 let dim = dim.to_index(&shape, "index_add")?;
6057 if indices.inner.dtype() != fuel_ir::DType::U32 {
6058 return Err(fuel_ir::Error::Msg(format!(
6059 "index_add: index must be U32, got {:?}",
6060 indices.inner.dtype(),
6061 ))
6062 .bt());
6063 }
6064 if self.inner.dtype() != src.inner.dtype() {
6065 return Err(fuel_ir::Error::Msg(format!(
6066 "index_add: base and src dtypes must match, got {:?} vs {:?}",
6067 self.inner.dtype(),
6068 src.inner.dtype(),
6069 ))
6070 .bt());
6071 }
6072 let base_dims = shape.dims();
6073 let src_shape = src.inner.shape();
6074 let src_dims = src_shape.dims();
6075 if base_dims.len() != src_dims.len() {
6076 return Err(fuel_ir::Error::Msg(format!(
6077 "index_add: base and src must have the same rank, got {} vs {}",
6078 base_dims.len(),
6079 src_dims.len(),
6080 ))
6081 .bt());
6082 }
6083 let idx_shape = indices.inner.shape();
6084 let idx_dims = idx_shape.dims();
6085 if idx_dims.len() != 1 {
6086 return Err(fuel_ir::Error::Msg(format!(
6087 "index_add: index must be rank 1, got {idx_dims:?}",
6088 ))
6089 .bt());
6090 }
6091 if src_dims[dim] != idx_dims[0] {
6092 return Err(fuel_ir::Error::Msg(format!(
6093 "index_add: src dim {dim} ({}) must match index length ({})",
6094 src_dims[dim], idx_dims[0],
6095 ))
6096 .bt());
6097 }
6098 Ok(Self {
6099 inner: self.inner.index_add(dim, &indices.inner, &src.inner),
6100 })
6101 }
6102
6103 pub fn scatter_add<D: Dim>(
6108 &self,
6109 dim: D,
6110 indices: &Self,
6111 src: &Self,
6112 ) -> std::result::Result<Self, fuel_ir::Error> {
6113 let shape = self.inner.shape();
6114 let dim = dim.to_index(&shape, "scatter_add")?;
6115 if indices.inner.dtype() != fuel_ir::DType::U32 {
6116 return Err(fuel_ir::Error::Msg(format!(
6117 "scatter_add: index must be U32, got {:?}",
6118 indices.inner.dtype(),
6119 ))
6120 .bt());
6121 }
6122 if self.inner.dtype() != src.inner.dtype() {
6123 return Err(fuel_ir::Error::Msg(format!(
6124 "scatter_add: base and src dtypes must match, got {:?} vs {:?}",
6125 self.inner.dtype(),
6126 src.inner.dtype(),
6127 ))
6128 .bt());
6129 }
6130 let idx_shape = indices.inner.shape();
6131 let src_shape = src.inner.shape();
6132 if idx_shape.dims() != src_shape.dims() {
6133 return Err(fuel_ir::Error::Msg(format!(
6134 "scatter_add: index and src must have the same shape, got {:?} vs {:?}",
6135 idx_shape.dims(),
6136 src_shape.dims(),
6137 ))
6138 .bt());
6139 }
6140 Ok(Self {
6141 inner: self.inner.scatter_add(dim, &indices.inner, &src.inner),
6142 })
6143 }
6144
6145 pub fn relu_inplace(&self) -> Self {
6155 Self {
6156 inner: self.inner.relu_inplace(),
6157 }
6158 }
6159
6160 pub fn silu_inplace(&self) -> Self {
6163 Self {
6164 inner: self.inner.silu_inplace(),
6165 }
6166 }
6167
6168 pub fn gelu_tanh_inplace(&self) -> Self {
6171 Self {
6172 inner: self.inner.gelu_tanh_inplace(),
6173 }
6174 }
6175
6176 pub fn tanh_inplace(&self) -> Self {
6179 Self {
6180 inner: self.inner.tanh_inplace(),
6181 }
6182 }
6183
6184 pub fn sigmoid_inplace(&self) -> Self {
6187 Self {
6188 inner: self.inner.sigmoid_inplace(),
6189 }
6190 }
6191
6192 pub fn affine_inplace(&self, mul: f64, add: f64) -> Self {
6195 Self {
6196 inner: self.inner.affine_inplace(mul, add),
6197 }
6198 }
6199
6200 pub fn const_f64_like(
6204 &self,
6205 data: impl Into<Arc<[f64]>>,
6206 shape: impl Into<Shape>,
6207 ) -> std::result::Result<Self, fuel_ir::Error> {
6208 Ok(Self {
6209 inner: self.inner.const_f64_like(data, shape)?,
6210 })
6211 }
6212
6213 pub fn const_i64_like(
6217 &self,
6218 data: impl Into<Arc<[i64]>>,
6219 shape: impl Into<Shape>,
6220 ) -> std::result::Result<Self, fuel_ir::Error> {
6221 Ok(Self {
6222 inner: self.inner.const_i64_like(data, shape)?,
6223 })
6224 }
6225
6226 pub fn on_device(self, device: &Device) -> Self {
6232 Self {
6233 inner: self.inner.on_device(device.location()),
6234 }
6235 }
6236
6237 pub fn release(&self) -> Self {
6241 Self {
6242 inner: self.inner.release(),
6243 }
6244 }
6245
6246 pub fn move_to_device(&self, device: &Device) -> Self {
6249 Self {
6250 inner: self.inner.move_to_device(device.location()),
6251 }
6252 }
6253
6254 pub fn copy_to_device(&self, device: &Device) -> Self {
6257 Self {
6258 inner: self.inner.copy_to_device(device.location()),
6259 }
6260 }
6261
6262 pub fn backward(&self) -> fuel_graph::GradMap {
6269 self.inner.backward()
6270 }
6271}
6272
6273impl Tensor {
6281 pub fn transpose_last_two(&self) -> std::result::Result<Self, fuel_ir::Error> {
6286 self.transpose()
6287 }
6288
6289 pub fn t(&self) -> std::result::Result<Self, fuel_ir::Error> {
6292 self.transpose()
6293 }
6294
6295 pub fn transpose_dims<D1: Dim, D2: Dim>(
6300 &self,
6301 dim1: D1,
6302 dim2: D2,
6303 ) -> std::result::Result<Self, fuel_ir::Error> {
6304 let shape = self.shape();
6305 let dim1 = dim1.to_index(&shape, "transpose_dims")?;
6306 let dim2 = dim2.to_index(&shape, "transpose_dims")?;
6307 if dim1 == dim2 {
6308 return Ok(self.clone());
6309 }
6310 let rank = shape.dims().len();
6311 let mut axes: Vec<usize> = (0..rank).collect();
6312 axes.swap(dim1, dim2);
6313 self.permute(axes.as_slice())
6314 }
6315
6316 pub fn flatten<D1: Dim, D2: Dim>(
6321 &self,
6322 start_dim: D1,
6323 end_dim: D2,
6324 ) -> std::result::Result<Self, fuel_ir::Error> {
6325 let shape = self.shape();
6326 let rank = shape.dims().len();
6327 if rank == 0 {
6328 return Ok(self.clone());
6329 }
6330 let start_dim = start_dim.to_index(&shape, "flatten")?;
6331 let end_dim = end_dim.to_index(&shape, "flatten")?;
6332 if start_dim > end_dim {
6333 return Err(fuel_ir::Error::Msg(format!(
6334 "flatten: start_dim={start_dim} > end_dim={end_dim}",
6335 ))
6336 .bt());
6337 }
6338 let dims = shape.dims();
6339 let merged: usize = dims[start_dim..=end_dim].iter().product();
6340 let mut new_dims: Vec<usize> = Vec::with_capacity(rank - (end_dim - start_dim));
6341 new_dims.extend_from_slice(&dims[..start_dim]);
6342 new_dims.push(merged);
6343 new_dims.extend_from_slice(&dims[end_dim + 1..]);
6344 self.reshape(new_dims)
6345 }
6346
6347 pub fn flatten_to<D: Dim>(&self, end_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6349 self.flatten(0_usize, end_dim)
6350 }
6351
6352 pub fn flatten_from<D: Dim>(&self, start_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6354 let rank = self.shape().dims().len();
6355 if rank == 0 {
6356 return Ok(self.clone());
6357 }
6358 self.flatten(start_dim, rank - 1)
6359 }
6360
6361 pub fn flatten_all(&self) -> std::result::Result<Self, fuel_ir::Error> {
6363 let rank = self.shape().dims().len();
6364 if rank == 0 {
6365 return Ok(self.clone());
6366 }
6367 self.flatten(0, rank - 1)
6368 }
6369
6370 pub fn stack<D: Dim>(args: &[&Self], dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6375 if args.is_empty() {
6376 return Err(fuel_ir::Error::Msg("stack: requires at least one tensor".into()).bt());
6377 }
6378 let reference_shape = args[0].shape();
6379 let reference_dims = reference_shape.dims().to_vec();
6380 let dim = dim.to_index_plus_one(&reference_shape, "stack")?;
6381 for (idx, t) in args.iter().enumerate().skip(1) {
6382 if t.shape().dims() != reference_dims.as_slice() {
6383 return Err(fuel_ir::Error::Msg(format!(
6384 "stack: tensor {idx} shape {:?} != reference shape {:?}",
6385 t.shape().dims(),
6386 reference_dims,
6387 ))
6388 .bt());
6389 }
6390 }
6391 let mut iter = args.iter();
6393 let first = iter.next().unwrap().unsqueeze(dim)?;
6394 let mut acc = first;
6395 for t in iter {
6396 let u = t.unsqueeze(dim)?;
6397 acc = acc.concat(&u, dim)?;
6398 }
6399 Ok(acc)
6400 }
6401
6402 pub fn sum_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6412 let shape = self.shape();
6413 let dim = dim.to_index(&shape, "sum_keepdim")?;
6414 self.sum_dim(dim).unwrap().unsqueeze(dim)
6415 }
6416
6417 pub fn mean_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6419 let shape = self.shape();
6420 let dim = dim.to_index(&shape, "mean_keepdim")?;
6421 self.mean_dim(dim).unwrap().unsqueeze(dim)
6422 }
6423
6424 pub fn max_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6426 let shape = self.shape();
6427 let dim = dim.to_index(&shape, "max_keepdim")?;
6428 self.max_dim(dim).unwrap().unsqueeze(dim)
6429 }
6430
6431 pub fn min_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6433 let shape = self.shape();
6434 let dim = dim.to_index(&shape, "min_keepdim")?;
6435 self.min_dim(dim).unwrap().unsqueeze(dim)
6439 }
6440
6441 pub fn var_keepdim<D: Dim>(
6455 &self,
6456 dim: D,
6457 correction: f64,
6458 ) -> std::result::Result<Self, fuel_ir::Error> {
6459 let shape = self.shape();
6460 let dim = dim.to_index(&shape, "var_keepdim")?;
6461 let dims = shape.dims();
6462 let n = dims[dim];
6463 let mean = self.mean_keepdim(dim)?;
6464 let deviation = self.broadcast_sub(&mean).unwrap();
6465 let squares = deviation.sqr();
6466 let summed = squares.sum_keepdim(dim)?;
6468 let divisor = n as f64 - correction;
6469 Ok(summed.mul_scalar(1.0 / divisor))
6470 }
6471
6472 pub fn var<D: Dim>(
6475 &self,
6476 dim: D,
6477 correction: f64,
6478 ) -> std::result::Result<Self, fuel_ir::Error> {
6479 let shape = self.shape();
6480 let dim = dim.to_index(&shape, "var")?;
6481 self.var_keepdim(dim, correction)?.squeeze(dim)
6482 }
6483
6484 pub fn affine(&self, mul: f64, add: f64) -> Self {
6491 self.mul_scalar(mul).add_scalar(add)
6492 }
6493
6494 pub fn scale_and_shift(&self, scale: f64, shift: f64) -> Self {
6498 self.affine(scale, shift)
6499 }
6500
6501 pub fn elu(&self, alpha: f64) -> Self {
6505 let neg_branch = self.exp().affine(alpha, -alpha);
6507 let zero = self
6512 .const_f32_like(vec![0.0; self.elem_count()], self.shape())
6513 .expect("elu: vec![_; self.elem_count()] against self.shape() -- same source");
6514 let mask = self.gt(&zero).unwrap();
6515 mask.where_cond(self, &neg_branch).unwrap()
6516 }
6517
6518 pub fn dot(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6521 let a = self.shape().dims().to_vec();
6522 let b = rhs.shape().dims().to_vec();
6523 if a.len() != 1 || b.len() != 1 {
6524 return Err(fuel_ir::Error::Msg(format!(
6525 "dot: requires rank-1 inputs, got lhs={a:?} rhs={b:?}",
6526 ))
6527 .bt());
6528 }
6529 if a[0] != b[0] {
6530 return Err(fuel_ir::Error::Msg(format!(
6531 "dot: length mismatch lhs={} rhs={}",
6532 a[0], b[0],
6533 ))
6534 .bt());
6535 }
6536 Ok(self.mul(rhs).unwrap().sum_all())
6537 }
6538
6539 pub fn mv(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6543 let a = self.shape().dims().to_vec();
6544 let b = rhs.shape().dims().to_vec();
6545 if a.len() != 2 || b.len() != 1 || a[1] != b[0] {
6546 return Err(fuel_ir::Error::Msg(format!(
6547 "mv: shape mismatch lhs={a:?} rhs={b:?} (need [m,n] · [n])",
6548 ))
6549 .bt());
6550 }
6551 let rhs_col = rhs.unsqueeze(1_usize)?;
6553 let prod = self.matmul(&rhs_col).unwrap();
6554 prod.squeeze(1_usize)
6555 }
6556
6557 pub fn matvec(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6560 self.mv(rhs)
6561 }
6562
6563 pub fn broadcast_matmul(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6567 self.matmul(rhs)
6568 }
6569
6570 pub fn ones_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6580 let n = self.elem_count();
6581 let shape = self.shape();
6582 match self.dtype() {
6583 DType::F32 => self.const_f32_like(vec![1.0_f32; n], shape),
6584 DType::F64 => self.const_f64_like(vec![1.0_f64; n], shape),
6585 DType::BF16 => self.const_bf16_like(vec![half::bf16::ONE; n], shape),
6586 DType::F16 => self.const_f16_like(vec![half::f16::ONE; n], shape),
6587 DType::U32 => self.const_u32_like(vec![1_u32; n], shape),
6588 DType::I64 => self.const_i64_like(vec![1_i64; n], shape),
6589 other => {
6590 Err(fuel_ir::Error::Msg(format!("ones_like: unsupported dtype {other:?}",)).bt())
6591 }
6592 }
6593 }
6594
6595 pub fn zeros_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6599 let n = self.elem_count();
6600 let shape = self.shape();
6601 match self.dtype() {
6602 DType::F32 => self.const_f32_like(vec![0.0_f32; n], shape),
6603 DType::F64 => self.const_f64_like(vec![0.0_f64; n], shape),
6604 DType::BF16 => self.const_bf16_like(vec![half::bf16::ZERO; n], shape),
6605 DType::F16 => self.const_f16_like(vec![half::f16::ZERO; n], shape),
6606 DType::U32 => self.const_u32_like(vec![0_u32; n], shape),
6607 DType::I64 => self.const_i64_like(vec![0_i64; n], shape),
6608 other => {
6609 Err(fuel_ir::Error::Msg(format!("zeros_like: unsupported dtype {other:?}",)).bt())
6610 }
6611 }
6612 }
6613
6614 pub fn ones(
6618 shape: impl Into<Shape>,
6619 dtype: DType,
6620 device: &Device,
6621 ) -> std::result::Result<Self, fuel_ir::Error> {
6622 let shape = shape.into();
6623 let n = shape.elem_count();
6624 match dtype {
6625 DType::F32 => Self::from_f32(vec![1.0_f32; n], shape, device),
6626 DType::F64 => Self::from_f64(vec![1.0_f64; n], shape, device),
6627 DType::BF16 => Self::from_bf16(vec![half::bf16::ONE; n], shape, device),
6628 DType::F16 => Self::from_f16(vec![half::f16::ONE; n], shape, device),
6629 DType::U32 => Self::from_u32(vec![1_u32; n], shape, device),
6630 other => Err(fuel_ir::Error::Msg(format!("ones: unsupported dtype {other:?}",)).bt()),
6631 }
6632 }
6633
6634 pub fn zeros(
6644 shape: impl Into<Shape>,
6645 dtype: DType,
6646 device: &Device,
6647 ) -> std::result::Result<Self, fuel_ir::Error> {
6648 let shape = shape.into();
6649 let n = shape.elem_count();
6650 match dtype {
6651 DType::F32 => Self::from_f32(vec![0.0_f32; n], shape, device),
6652 DType::F64 => Self::from_f64(vec![0.0_f64; n], shape, device),
6653 DType::BF16 => Self::from_bf16(vec![half::bf16::ZERO; n], shape, device),
6654 DType::F16 => Self::from_f16(vec![half::f16::ZERO; n], shape, device),
6655 DType::U32 => Self::from_u32(vec![0_u32; n], shape, device),
6656 other => Err(fuel_ir::Error::Msg(format!("zeros: unsupported dtype {other:?}",)).bt()),
6657 }
6658 }
6659
6660 pub fn full(
6669 shape: impl Into<Shape>,
6670 value: fuel_ir::Scalar,
6671 device: &Device,
6672 ) -> std::result::Result<Self, fuel_ir::Error> {
6673 let shape = shape.into();
6674 let n = shape.elem_count();
6675 match value {
6676 fuel_ir::Scalar::F32(v) => Self::from_f32(vec![v; n], shape, device),
6677 fuel_ir::Scalar::F64(v) => Self::from_f64(vec![v; n], shape, device),
6678 fuel_ir::Scalar::BF16(v) => Self::from_bf16(vec![v; n], shape, device),
6679 fuel_ir::Scalar::F16(v) => Self::from_f16(vec![v; n], shape, device),
6680 fuel_ir::Scalar::U32(v) => Self::from_u32(vec![v; n], shape, device),
6681 other => Err(fuel_ir::Error::Msg(format!(
6682 "full: unsupported scalar dtype {:?}",
6683 other.dtype(),
6684 ))
6685 .bt()),
6686 }
6687 }
6688
6689 pub fn eye(n: usize, dtype: DType, device: &Device) -> Self {
6692 let mut data = vec![0.0_f32; n * n];
6693 for i in 0..n {
6694 data[i * n + i] = 1.0;
6695 }
6696 let base = Self::from_f32(data, vec![n, n], device).expect(
6705 "eye: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n both computed from `n` in this function -- lengths cannot disagree",
6706 );
6707 if dtype == DType::F32 {
6708 base
6709 } else {
6710 base.to_dtype(dtype).unwrap()
6711 }
6712 }
6713
6714 pub fn split_heads(
6720 &self,
6721 num_heads: usize,
6722 head_dim: usize,
6723 ) -> std::result::Result<Self, fuel_ir::Error> {
6724 let dims = self.inner.shape().dims().to_vec();
6725 debug_assert_eq!(
6726 dims.len(),
6727 3,
6728 "split_heads: input must be rank 3 (B, N, embed), got {dims:?}"
6729 );
6730 debug_assert_eq!(
6731 dims[2],
6732 num_heads * head_dim,
6733 "split_heads: trailing dim ({}) != num_heads * head_dim ({} * {} = {})",
6734 dims[2],
6735 num_heads,
6736 head_dim,
6737 num_heads * head_dim
6738 );
6739 let b = dims[0];
6740 let n = dims[1];
6741 self.reshape(Shape::from_dims(&[b, n, num_heads, head_dim]))?
6742 .permute([0, 2, 1, 3_usize])
6743 }
6744
6745 pub fn merge_heads(&self) -> std::result::Result<Self, fuel_ir::Error> {
6749 let dims = self.inner.shape().dims().to_vec();
6750 debug_assert_eq!(
6751 dims.len(),
6752 4,
6753 "merge_heads: input must be rank 4 (B, heads, N, head_dim), got {dims:?}"
6754 );
6755 let b = dims[0];
6756 let num_heads = dims[1];
6757 let n = dims[2];
6758 let head_dim = dims[3];
6759 self.permute([0, 2, 1, 3_usize])?
6760 .reshape(Shape::from_dims(&[b, n, num_heads * head_dim]))
6761 }
6762
6763 pub fn add_trailing_bias(
6774 &self,
6775 bias: std::sync::Arc<[f32]>,
6776 ) -> std::result::Result<Self, fuel_ir::Error> {
6777 let n = bias.len();
6778 let bias_t = self.const_like_dtype(&bias, Shape::from_dims(&[n]), self.dtype())?;
6784 self.broadcast_add(&bias_t)
6785 }
6786
6787 pub fn embed_tokens(
6799 embed_table: std::sync::Arc<[f32]>,
6800 vocab_size: usize,
6801 hidden: usize,
6802 tokens: &[u32],
6803 device: &crate::Device,
6804 ) -> std::result::Result<Self, fuel_ir::Error> {
6805 let seq = tokens.len();
6806 if seq == 0 {
6807 return Err(fuel_ir::Error::Msg("embed_tokens: tokens must be non-empty".into()).bt());
6808 }
6809 let embed = Self::from_f32(embed_table, Shape::from_dims(&[vocab_size, hidden]), device)?;
6810 let token_ids = embed.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6811 embed
6812 .index_select(0_usize, &token_ids)?
6813 .reshape(Shape::from_dims(&[1, seq, hidden]))
6814 }
6815
6816 pub fn embed_tokens_anchored(
6824 &self,
6825 embed_table: std::sync::Arc<[f32]>,
6826 vocab_size: usize,
6827 hidden: usize,
6828 tokens: &[u32],
6829 ) -> std::result::Result<Self, fuel_ir::Error> {
6830 let seq = tokens.len();
6831 if seq == 0 {
6832 return Err(fuel_ir::Error::Msg(
6833 "embed_tokens_anchored: tokens must be non-empty".into(),
6834 )
6835 .bt());
6836 }
6837 let embed = self.const_f32_like(embed_table, Shape::from_dims(&[vocab_size, hidden]))?;
6838 let token_ids = self.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6839 embed
6840 .index_select(0_usize, &token_ids)?
6841 .reshape(Shape::from_dims(&[1, seq, hidden]))
6842 }
6843
6844 pub fn rope_tables_const(
6855 &self,
6856 theta: f64,
6857 start_pos: usize,
6858 seq: usize,
6859 head_dim: usize,
6860 ) -> (Self, Self) {
6861 let (cos_data, sin_data) = fuel_graph::build_rope_tables(theta, start_pos, seq, head_dim);
6862 let rope_shape = Shape::from_dims(&[seq, head_dim]);
6863 let rope_cos = self
6869 .const_f32_like(cos_data, rope_shape.clone())
6870 .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6871 let rope_sin = self
6872 .const_f32_like(sin_data, rope_shape)
6873 .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6874 (rope_cos, rope_sin)
6875 }
6876
6877 pub fn rope_delta_tables_const(
6889 &self,
6890 theta: f64,
6891 delta: usize,
6892 rows: usize,
6893 head_dim: usize,
6894 ) -> (Self, Self) {
6895 let (c1, s1) = fuel_graph::build_rope_tables(theta, delta, 1, head_dim);
6896 let mut cos = Vec::with_capacity(rows * head_dim);
6897 let mut sin = Vec::with_capacity(rows * head_dim);
6898 for _ in 0..rows {
6899 cos.extend_from_slice(&c1);
6900 sin.extend_from_slice(&s1);
6901 }
6902 let shape = Shape::from_dims(&[rows, head_dim]);
6903 (
6907 self.const_f32_like(cos, shape.clone()).expect(
6908 "rope tables: rows appends of c1 (local); c1 has head_dim elements (not local)",
6909 ),
6910 self.const_f32_like(sin, shape).expect(
6911 "rope tables: rows appends of s1 (local); s1 has head_dim elements (not local)",
6912 ),
6913 )
6914 }
6915
6916 pub fn rope_delta_rotate_block_f32(
6931 dev: &crate::Device,
6932 k_block: &[f32],
6933 theta: f64,
6934 delta: usize,
6935 block_size: usize,
6936 n_kv_heads: usize,
6937 head_dim: usize,
6938 ) -> Vec<f32> {
6939 let pool_shape = Shape::from_dims(&[block_size, n_kv_heads, head_dim]);
6940 let k = Tensor::from_f32(k_block.to_vec(), pool_shape, dev).expect(
6951 "rope_delta_rotate_block_f32: k_block length must equal block_size*n_kv_heads*head_dim -- both caller-supplied, so this function cannot prove it; the -> Vec<f32> signature has no error channel (see the sibling permute/reshape expects)",
6952 );
6953 let k4 = k
6954 .permute([1, 0, 2])
6955 .expect("rope_delta_rotate_block_f32: permute [bs,Hkv,D]->[Hkv,bs,D]")
6956 .reshape(Shape::from_dims(&[1, n_kv_heads, block_size, head_dim]))
6957 .expect("rope_delta_rotate_block_f32: reshape to [1,Hkv,bs,D]");
6958 let (cos, sin) = k4.rope_delta_tables_const(theta, delta, block_size, head_dim);
6959 k4.rope_with_tables_decomposed(&cos, &sin)
6960 .expect("rope_delta_rotate_block_f32: rope_with_tables_decomposed")
6961 .reshape(Shape::from_dims(&[n_kv_heads, block_size, head_dim]))
6962 .expect("rope_delta_rotate_block_f32: reshape back to [Hkv,bs,D]")
6963 .permute([1, 0, 2])
6964 .expect("rope_delta_rotate_block_f32: permute [Hkv,bs,D]->[bs,Hkv,D]")
6965 .realize_f32()
6966 }
6967
6968 pub fn rope_tables_const_batched(
6981 &self,
6982 theta: f64,
6983 positions: &[usize],
6984 head_dim: usize,
6985 ) -> (Self, Self) {
6986 let b = positions.len();
6987 let mut cos_data = Vec::with_capacity(b * head_dim);
6988 let mut sin_data = Vec::with_capacity(b * head_dim);
6989 for &pos in positions {
6990 let (c, s) = fuel_graph::build_rope_tables(theta, pos, 1, head_dim);
6991 cos_data.extend_from_slice(&c);
6992 sin_data.extend_from_slice(&s);
6993 }
6994 let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
6995 let rope_cos = self
7002 .const_f32_like(cos_data, shape.clone())
7003 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
7004 let rope_sin = self
7005 .const_f32_like(sin_data, shape)
7006 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
7007 (rope_cos, rope_sin)
7008 }
7009
7010 pub fn rope_batched(
7030 &self,
7031 cos: &Self,
7032 sin: &Self,
7033 ) -> std::result::Result<Self, fuel_ir::Error> {
7034 let dims: Vec<usize> = self.inner.shape().dims().to_vec();
7035 let rank = dims.len();
7036 if rank < 2 {
7037 return Err(fuel_ir::Error::Msg(format!(
7038 "rope_batched: expected rank >= 2, got {dims:?}",
7039 ))
7040 .bt());
7041 }
7042 let d = dims[rank - 1];
7043 if !d.is_multiple_of(2) {
7044 return Err(
7045 fuel_ir::Error::Msg(format!("rope_batched: head_dim {d} must be even",)).bt(),
7046 );
7047 }
7048 let half = d / 2;
7049 let target = Shape::from_dims(&dims);
7050
7051 let cos_b = cos.broadcast_to(target.clone())?;
7052 let sin_b = sin.broadcast_to(target)?;
7053
7054 let first = self.slice(rank - 1, 0, half)?;
7055 let second = self.slice(rank - 1, half, half)?;
7056 let rotated = second.neg().concat(&first, rank - 1)?;
7057
7058 let left = self.mul(&cos_b)?;
7059 let right = rotated.mul(&sin_b)?;
7060 left.add(&right)
7061 }
7062
7063 pub fn rope_partial(
7074 &self,
7075 rope_cos: &Self,
7076 rope_sin: &Self,
7077 rope_dim: usize,
7078 ) -> std::result::Result<Self, fuel_ir::Error> {
7079 let dims = self.inner.shape();
7080 let dims = dims.dims();
7081 let head_dim = *dims.last().ok_or_else(|| {
7082 fuel_ir::Error::Msg("rope_partial: receiver must have at least one dimension".into())
7083 .bt()
7084 })?;
7085 if rope_dim == head_dim {
7086 return self.rope_with_tables(rope_cos, rope_sin);
7087 }
7088 if rope_dim > head_dim {
7089 return Err(fuel_ir::Error::Msg(format!(
7090 "rope_partial: rope_dim={rope_dim} exceeds head_dim={head_dim}",
7091 ))
7092 .bt());
7093 }
7094 let last = dims.len() - 1;
7095 let pass_dim = head_dim - rope_dim;
7096 let rot = self.slice(last, 0, rope_dim)?;
7097 let pass = self.slice(last, rope_dim, pass_dim)?;
7098 let rot_rotated = rot.rope_with_tables(rope_cos, rope_sin)?;
7099 rot_rotated.concat(&pass, last)
7100 }
7101
7102 pub fn add_optional_trailing_bias(
7107 &self,
7108 bias: Option<&std::sync::Arc<[f32]>>,
7109 ) -> std::result::Result<Self, fuel_ir::Error> {
7110 match bias {
7111 None => Ok(self.clone()),
7112 Some(b) => self.add_trailing_bias(std::sync::Arc::clone(b)),
7113 }
7114 }
7115
7116 pub fn softcap(&self, cap: f64) -> Self {
7122 self.mul_scalar(1.0 / cap).tanh().mul_scalar(cap)
7123 }
7124
7125 pub fn softcap_optional(&self, cap: Option<f64>) -> Self {
7129 match cap {
7130 Some(c) if c > 0.0 => self.softcap(c),
7131 _ => self.clone(),
7132 }
7133 }
7134
7135 pub fn rms_norm_affine_with_offset(
7144 &self,
7145 gain: &[f32],
7146 offset: f32,
7147 eps: f64,
7148 ) -> std::result::Result<Self, fuel_ir::Error> {
7149 let shifted: std::sync::Arc<[f32]> =
7150 std::sync::Arc::from(gain.iter().map(|g| *g + offset).collect::<Vec<_>>());
7151 self.rms_norm_affine(shifted, eps)
7152 }
7153
7154 pub fn rms_norm_affine(
7159 &self,
7160 gain: std::sync::Arc<[f32]>,
7161 eps: f64,
7162 ) -> std::result::Result<Self, fuel_ir::Error> {
7163 let hidden = gain.len();
7164 let normed = self.rms_norm_last_dim(eps)?;
7165 let gain_t = self.const_f32_like(gain, Shape::from_dims(&[hidden]))?;
7166 normed.broadcast_mul(&gain_t)
7167 }
7168
7169 pub fn global_avg_pool_2d(&self) -> std::result::Result<Self, fuel_ir::Error> {
7179 let dims = self.inner.shape().dims().to_vec();
7180 debug_assert_eq!(
7181 dims.len(),
7182 4,
7183 "global_avg_pool_2d: input must be rank 4 (B, C, H, W), got {dims:?}"
7184 );
7185 self.mean_dim(3_usize)?.mean_dim(2_usize)
7187 }
7188
7189 pub fn channel_affine_4d(
7201 &self,
7202 gain: std::sync::Arc<[f32]>,
7203 bias: std::sync::Arc<[f32]>,
7204 ) -> std::result::Result<Self, fuel_ir::Error> {
7205 let dims = self.inner.shape().dims().to_vec();
7206 debug_assert_eq!(
7207 dims.len(),
7208 4,
7209 "channel_affine_4d: input must be rank 4 (B, C, H, W), got {dims:?}"
7210 );
7211 let channels = dims[1];
7212 debug_assert_eq!(
7213 gain.len(),
7214 channels,
7215 "channel_affine_4d: gain len ({}) != C ({})",
7216 gain.len(),
7217 channels
7218 );
7219 debug_assert_eq!(
7220 bias.len(),
7221 channels,
7222 "channel_affine_4d: bias len ({}) != C ({})",
7223 bias.len(),
7224 channels
7225 );
7226 let w_t = self
7227 .const_f32_like(gain, Shape::from_dims(&[channels]))?
7228 .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7229 let b_t = self
7230 .const_f32_like(bias, Shape::from_dims(&[channels]))?
7231 .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7232 self.broadcast_mul(&w_t)?.broadcast_add(&b_t)
7233 }
7234
7235 pub fn additive_causal_mask_like(anchor: &Tensor, seq_len: usize) -> Self {
7244 let mut data = vec![0.0_f32; seq_len * seq_len];
7245 for i in 0..seq_len {
7246 for j in (i + 1)..seq_len {
7247 data[i * seq_len + j] = f32::NEG_INFINITY;
7248 }
7249 }
7250 anchor
7254 .const_f32_like(
7255 std::sync::Arc::from(data),
7256 Shape::from_dims(&[seq_len, seq_len]),
7257 )
7258 .expect(
7259 "additive_causal_mask_like: vec![_; seq_len*seq_len] against [seq_len, seq_len]",
7260 )
7261 }
7262
7263 pub fn tril2(n: usize, dtype: DType, device: &Device) -> Self {
7266 let mut data = vec![0.0_f32; n * n];
7267 for i in 0..n {
7268 for j in 0..=i {
7269 data[i * n + j] = 1.0;
7270 }
7271 }
7272 let base = Self::from_f32(data, vec![n, n], device).expect(
7281 "tril2: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n both computed from `n` in this function -- lengths cannot disagree",
7282 );
7283 if dtype == DType::F32 {
7284 base
7285 } else {
7286 base.to_dtype(dtype).unwrap()
7287 }
7288 }
7289
7290 pub fn triu2(n: usize, dtype: DType, device: &Device) -> Self {
7292 let mut data = vec![0.0_f32; n * n];
7293 for i in 0..n {
7294 for j in i..n {
7295 data[i * n + j] = 1.0;
7296 }
7297 }
7298 let base = Self::from_f32(data, vec![n, n], device).expect(
7307 "triu2: data is vec![_; n*n] and shape is [n, n] (elem_count n*n), \n both computed from `n` in this function -- lengths cannot disagree",
7308 );
7309 if dtype == DType::F32 {
7310 base
7311 } else {
7312 base.to_dtype(dtype).unwrap()
7313 }
7314 }
7315
7316 pub fn narrow<D: Dim>(
7323 &self,
7324 dim: D,
7325 start: usize,
7326 len: usize,
7327 ) -> std::result::Result<Self, fuel_ir::Error> {
7328 self.slice(dim, start, len)
7329 }
7330
7331 pub fn chunk<D: Dim>(
7337 &self,
7338 chunks: usize,
7339 dim: D,
7340 ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
7341 let shape = self.shape();
7342 let dim = dim.to_index(&shape, "chunk")?;
7343 if chunks == 0 {
7344 return Err(fuel_ir::Error::Msg("chunk: chunks must be > 0".into()).bt());
7345 }
7346 let dims = shape.dims();
7347 let size = dims[dim];
7348 if size < chunks {
7349 return Ok((0..size).map(|i| self.slice(dim, i, 1).unwrap()).collect());
7350 }
7351 let base = size / chunks;
7352 let extra = size % chunks;
7353 let mut out = Vec::with_capacity(chunks);
7354 let mut start = 0;
7355 for i in 0..chunks {
7356 let len = if i < extra { base + 1 } else { base };
7357 out.push(self.slice(dim, start, len).unwrap());
7358 start += len;
7359 }
7360 Ok(out)
7361 }
7362
7363 pub fn get(&self, i: usize) -> std::result::Result<Self, fuel_ir::Error> {
7366 let dims = self.shape().dims().to_vec();
7367 if dims.is_empty() {
7368 return Ok(self.clone());
7369 }
7370 self.slice(0, i, 1).unwrap().squeeze(0)
7371 }
7372
7373 pub fn get_on_dim<D: Dim>(
7377 &self,
7378 dim: D,
7379 index: usize,
7380 ) -> std::result::Result<Self, fuel_ir::Error> {
7381 let shape = self.shape();
7382 let dim = dim.to_index(&shape, "get_on_dim")?;
7383 self.slice(dim, index, 1).unwrap().squeeze(dim)
7384 }
7385
7386 pub fn sum_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7390 let shape = self.shape();
7391 let mut sorted = dims.to_indexes(&shape, "sum_dims")?;
7392 sorted.sort_by(|a, b| b.cmp(a));
7393 sorted.dedup();
7394 let mut acc = self.clone();
7395 for d in sorted {
7396 acc = acc.sum_dim(d)?;
7397 }
7398 Ok(acc)
7399 }
7400
7401 pub fn mean_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7404 let shape = self.shape();
7405 let mut sorted = dims.to_indexes(&shape, "mean_dims")?;
7406 sorted.sort_by(|a, b| b.cmp(a));
7407 sorted.dedup();
7408 let mut acc = self.clone();
7409 for d in sorted {
7410 acc = acc.mean_dim(d)?;
7411 }
7412 Ok(acc)
7413 }
7414
7415 pub fn sum_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7420 let mut sorted: Vec<usize> = dims.to_vec();
7421 sorted.sort();
7422 sorted.dedup();
7423 let mut acc = self.clone();
7424 for d in sorted {
7425 acc = acc.sum_keepdim(d)?;
7426 }
7427 Ok(acc)
7428 }
7429
7430 pub fn mean_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7432 let mut sorted: Vec<usize> = dims.to_vec();
7433 sorted.sort();
7434 sorted.dedup();
7435 let mut acc = self.clone();
7436 for d in sorted {
7437 acc = acc.mean_keepdim(d)?;
7438 }
7439 Ok(acc)
7440 }
7441
7442 pub fn rand_like(&self, lo: f64, up: f64) -> std::result::Result<Self, fuel_ir::Error> {
7445 Self::rand(self.shape(), lo, up, self.dtype(), &Device::cpu())
7446 }
7447
7448 pub fn randn_like(&self, mean: f64, stdev: f64) -> std::result::Result<Self, fuel_ir::Error> {
7451 Self::randn(self.shape(), mean, stdev, self.dtype(), &Device::cpu())
7452 }
7453
7454 pub fn rand(
7458 shape: impl Into<Shape>,
7459 lo: f64,
7460 up: f64,
7461 dtype: DType,
7462 device: &Device,
7463 ) -> std::result::Result<Self, fuel_ir::Error> {
7464 use rand::RngExt;
7465 let shape = shape.into();
7466 let n = shape.elem_count();
7467 let mut rng = rand::rng();
7468 match dtype {
7469 DType::F32 => {
7470 let data: Vec<f32> = (0..n).map(|_| rng.random_range(lo..up) as f32).collect();
7471 Self::from_f32(data, shape, device)
7472 }
7473 DType::F64 => {
7474 let data: Vec<f64> = (0..n).map(|_| rng.random_range(lo..up)).collect();
7475 Self::from_f64(data, shape, device)
7476 }
7477 DType::BF16 => {
7478 let data: Vec<half::bf16> = (0..n)
7479 .map(|_| half::bf16::from_f64(rng.random_range(lo..up)))
7480 .collect();
7481 Self::from_bf16(data, shape, device)
7482 }
7483 DType::F16 => {
7484 let data: Vec<half::f16> = (0..n)
7485 .map(|_| half::f16::from_f64(rng.random_range(lo..up)))
7486 .collect();
7487 Self::from_f16(data, shape, device)
7488 }
7489 other => {
7490 Err(fuel_ir::Error::Msg(format!("Tensor::rand: unsupported dtype {other:?}",)).bt())
7491 }
7492 }
7493 }
7494
7495 pub fn randn(
7499 shape: impl Into<Shape>,
7500 mean: f64,
7501 stdev: f64,
7502 dtype: DType,
7503 device: &Device,
7504 ) -> std::result::Result<Self, fuel_ir::Error> {
7505 use rand_distr::{Distribution, Normal};
7506 let shape = shape.into();
7507 let n = shape.elem_count();
7508 let normal = Normal::new(mean, stdev).map_err(|e| {
7509 fuel_ir::Error::Msg(format!("Tensor::randn: invalid stdev={stdev}: {e}",)).bt()
7510 })?;
7511 let mut rng = rand::rng();
7512 match dtype {
7513 DType::F32 => {
7514 let data: Vec<f32> = (0..n).map(|_| normal.sample(&mut rng) as f32).collect();
7515 Self::from_f32(data, shape, device)
7516 }
7517 DType::F64 => {
7518 let data: Vec<f64> = (0..n).map(|_| normal.sample(&mut rng)).collect();
7519 Self::from_f64(data, shape, device)
7520 }
7521 DType::BF16 => {
7522 let data: Vec<half::bf16> = (0..n)
7523 .map(|_| half::bf16::from_f64(normal.sample(&mut rng)))
7524 .collect();
7525 Self::from_bf16(data, shape, device)
7526 }
7527 DType::F16 => {
7528 let data: Vec<half::f16> = (0..n)
7529 .map(|_| half::f16::from_f64(normal.sample(&mut rng)))
7530 .collect();
7531 Self::from_f16(data, shape, device)
7532 }
7533 other => Err(fuel_ir::Error::Msg(format!(
7534 "Tensor::randn: unsupported dtype {other:?}",
7535 ))
7536 .bt()),
7537 }
7538 }
7539
7540 pub fn arange(start: f32, end: f32, device: &Device) -> Self {
7543 Self::arange_step(start, end, 1.0, device)
7544 }
7545
7546 pub fn arange_step(start: f32, end: f32, step: f32, device: &Device) -> Self {
7551 assert!(step != 0.0, "arange_step: step must be non-zero");
7552 let mut data = Vec::new();
7553 let mut current = start;
7554 if step > 0.0 {
7555 while current < end {
7556 data.push(current);
7557 current += step;
7558 }
7559 } else {
7560 while current > end {
7561 data.push(current);
7562 current += step;
7563 }
7564 }
7565 let n = data.len();
7566 Self::from_f32(data, vec![n], device).expect(
7573 "arange_step: shape is [n] where n IS data.len(), computed one line above from that same buffer -- the lengths cannot disagree",
7574 )
7575 }
7576
7577 pub fn linspace(start: f32, end: f32, n: usize, device: &Device) -> Self {
7580 assert!(n >= 1, "linspace: n must be >= 1");
7581 if n == 1 {
7582 return Self::from_f32(vec![start], vec![1], device)
7584 .expect("linspace: vec![start] has exactly 1 element and the shape is [1]");
7585 }
7586 let step = (end - start) / ((n - 1) as f32);
7587 let data: Vec<f32> = (0..n).map(|i| start + step * (i as f32)).collect();
7588 Self::from_f32(data, vec![n], device).expect(
7591 "linspace: data is (0..n).map(..).collect() so it has exactly n elements, and the shape is [n] -- both from the same `n`",
7592 )
7593 }
7594
7595 pub fn norm(&self) -> Self {
7597 self.sqr().sum_all().sqrt()
7598 }
7599
7600 pub fn conv1d(
7615 &self,
7616 weight: &Self,
7617 bias: Option<&Self>,
7618 stride: usize,
7619 padding: usize,
7620 groups: usize,
7621 ) -> std::result::Result<Self, fuel_ir::Error> {
7622 let x_dims = self.shape().dims().to_vec();
7623 let w_dims = weight.shape().dims().to_vec();
7624 if x_dims.len() != 3 {
7625 return Err(fuel_ir::Error::Msg(format!(
7626 "conv1d: x must be rank 3 [N, Cin, T], got {x_dims:?}",
7627 ))
7628 .bt());
7629 }
7630 if w_dims.len() != 3 {
7631 return Err(fuel_ir::Error::Msg(format!(
7632 "conv1d: weight must be rank 3 [Cout, Cin/groups, K], got {w_dims:?}",
7633 ))
7634 .bt());
7635 }
7636 if groups < 1 {
7637 return Err(fuel_ir::Error::Msg("conv1d: groups must be ≥ 1".into()).bt());
7638 }
7639 if stride < 1 {
7640 return Err(fuel_ir::Error::Msg("conv1d: stride must be ≥ 1".into()).bt());
7641 }
7642 let x_4d = self.unsqueeze(2_usize)?;
7644 let w_4d = weight.unsqueeze(2_usize)?;
7645 let out_4d = x_4d.conv2d(&w_4d, bias, (1, stride), (0, padding), groups)?;
7646 out_4d.squeeze(2)
7647 }
7648
7649 pub fn conv1d_with_algo<A>(
7654 &self,
7655 weight: &Self,
7656 bias: Option<&Self>,
7657 stride: usize,
7658 padding: usize,
7659 groups: usize,
7660 _algo: A,
7661 ) -> std::result::Result<Self, fuel_ir::Error> {
7662 self.conv1d(weight, bias, stride, padding, groups)
7663 }
7664
7665 pub fn avg_pool2d(
7675 &self,
7676 kernel: (usize, usize),
7677 stride: (usize, usize),
7678 padding: (usize, usize),
7679 count_include_pad: bool,
7680 ) -> std::result::Result<Self, fuel_ir::Error> {
7681 let dims = self.shape().dims().to_vec();
7682 if dims.len() != 4 {
7683 return Err(fuel_ir::Error::Msg(format!(
7684 "avg_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7685 ))
7686 .bt());
7687 }
7688 let (c, h, w) = (dims[1], dims[2], dims[3]);
7689 let (kh, kw) = kernel;
7690 if kh == 0 || kw == 0 {
7691 return Err(
7692 fuel_ir::Error::Msg("avg_pool2d: kernel sizes must be positive".into()).bt(),
7693 );
7694 }
7695
7696 if count_include_pad || padding == (0, 0) {
7701 let inv = 1.0_f32 / ((kh * kw) as f32);
7702 let weight =
7706 self.const_f32_like(vec![inv; c * kh * kw], Shape::from_dims(&[c, 1, kh, kw]))?;
7707 return self.conv2d(&weight, None, stride, padding, c);
7708 }
7709
7710 if let Some((axis, out_idx)) = first_zero_tap_output_index(h, kh, stride.0, padding.0)
7725 .map(|i| ("H", i))
7726 .or_else(|| first_zero_tap_output_index(w, kw, stride.1, padding.1).map(|i| ("W", i)))
7727 {
7728 return Err(fuel_ir::Error::Msg(format!(
7729 "avg_pool2d: count_include_pad=false has no valid (non-padded) \
7730 taps for output position {out_idx} on the {axis} axis \
7731 (kernel={kernel:?}, stride={stride:?}, padding={padding:?}, \
7732 input H×W={h}×{w}) — the average would divide by zero",
7733 ))
7734 .bt());
7735 }
7736
7737 let sum_weight = self.const_f32_like(
7738 vec![1.0_f32; c * kh * kw],
7739 Shape::from_dims(&[c, 1, kh, kw]),
7740 )?;
7741 let summed = self.conv2d(&sum_weight, None, stride, padding, c)?;
7742
7743 let ones_hw = self.const_f32_like(vec![1.0_f32; h * w], Shape::from_dims(&[1, 1, h, w]))?;
7747 let count_weight =
7748 self.const_f32_like(vec![1.0_f32; kh * kw], Shape::from_dims(&[1, 1, kh, kw]))?;
7749 let count = ones_hw.conv2d(&count_weight, None, stride, padding, 1)?;
7750
7751 summed.broadcast_div(&count)
7752 }
7753
7754 pub fn avg_pool2d_with_stride(
7758 &self,
7759 kernel: (usize, usize),
7760 stride: (usize, usize),
7761 ) -> std::result::Result<Self, fuel_ir::Error> {
7762 self.avg_pool2d(kernel, stride, (0, 0), true)
7765 }
7766
7767 pub fn max_pool2d(
7782 &self,
7783 kernel: (usize, usize),
7784 stride: (usize, usize),
7785 padding: (usize, usize),
7786 ) -> std::result::Result<Self, fuel_ir::Error> {
7787 self.max_pool2d_with_pad_value(kernel, stride, padding, f32::NEG_INFINITY)
7795 }
7796
7797 pub fn max_pool2d_with_pad_value(
7803 &self,
7804 kernel: (usize, usize),
7805 stride: (usize, usize),
7806 padding: (usize, usize),
7807 pad_value: f32,
7808 ) -> std::result::Result<Self, fuel_ir::Error> {
7809 let dims = self.shape().dims().to_vec();
7810 if dims.len() != 4 {
7811 return Err(fuel_ir::Error::Msg(format!(
7812 "max_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7813 ))
7814 .bt());
7815 }
7816 let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7817 let (kh, kw) = kernel;
7818 let (sh, sw) = stride;
7819 let (ph, pw) = padding;
7820 if kh == 0 || kw == 0 {
7821 return Err(
7822 fuel_ir::Error::Msg("max_pool2d: kernel sizes must be positive".into()).bt(),
7823 );
7824 }
7825 if sh == 0 || sw == 0 {
7826 return Err(fuel_ir::Error::Msg("max_pool2d: strides must be positive".into()).bt());
7827 }
7828 let h_padded_min = h + 2 * ph;
7829 let w_padded_min = w + 2 * pw;
7830 if h_padded_min < kh || w_padded_min < kw {
7831 return Err(fuel_ir::Error::Msg(format!(
7832 "max_pool2d: padded input ({h_padded_min}×{w_padded_min}) smaller than kernel ({kh}×{kw})",
7833 )).bt());
7834 }
7835 let h_out = (h_padded_min - kh) / sh + 1;
7836 let w_out = (w_padded_min - kw) / sw + 1;
7837 let h_total = h_out * sh + (kh - 1);
7840 let w_total = w_out * sw + (kw - 1);
7841 let extra_h = h_total.saturating_sub(h_padded_min);
7842 let extra_w = w_total.saturating_sub(w_padded_min);
7843 let padded = self
7844 .pad_with_value(2, ph, ph + extra_h, pad_value)?
7845 .pad_with_value(3, pw, pw + extra_w, pad_value)?;
7846 let mut acc: Option<Tensor> = None;
7848 for ky in 0..kh {
7849 let row_slice = padded.slice(2, ky, h_out * sh).unwrap();
7853 let row_reshaped = row_slice.reshape(vec![n, c, h_out, sh, w_total])?;
7856 let row_tap = row_reshaped.slice(3, 0, 1).unwrap().squeeze(3)?;
7857 for kx in 0..kw {
7858 let col_slice = row_tap.slice(3, kx, w_out * sw).unwrap();
7859 let col_reshaped = col_slice.reshape(vec![n, c, h_out, w_out, sw])?;
7860 let win = col_reshaped.slice(4, 0, 1).unwrap().squeeze(4)?;
7861 acc = Some(match acc {
7862 None => win,
7863 Some(a) => a.maximum(&win).unwrap(),
7864 });
7865 }
7866 }
7867 acc.ok_or_else(|| fuel_ir::Error::Msg("max_pool2d: empty kernel".into()).bt())
7868 }
7869
7870 pub fn max_pool2d_with_stride(
7872 &self,
7873 kernel: (usize, usize),
7874 stride: (usize, usize),
7875 ) -> std::result::Result<Self, fuel_ir::Error> {
7876 self.max_pool2d(kernel, stride, (0, 0))
7877 }
7878
7879 pub fn upsample_nearest2d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7889 if scale == 0 {
7890 return Err(
7891 fuel_ir::Error::Msg("upsample_nearest2d: scale must be positive".into()).bt(),
7892 );
7893 }
7894 let dims = self.shape().dims().to_vec();
7895 if dims.len() != 4 {
7896 return Err(fuel_ir::Error::Msg(format!(
7897 "upsample_nearest2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7898 ))
7899 .bt());
7900 }
7901 if scale == 1 {
7902 return Ok(self.clone());
7903 }
7904 let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7905 let expanded = self.reshape(vec![n, c, h, 1, w, 1])?;
7907 let h_expanded = (0..scale)
7909 .fold(None, |acc: Option<Tensor>, _| {
7910 Some(match acc {
7911 None => expanded.clone(),
7912 Some(a) => a.concat(&expanded, 3).unwrap(),
7913 })
7914 })
7915 .unwrap();
7916 let h_then_w = (0..scale)
7917 .fold(None, |acc: Option<Tensor>, _| {
7918 Some(match acc {
7919 None => h_expanded.clone(),
7920 Some(a) => a.concat(&h_expanded, 5).unwrap(),
7921 })
7922 })
7923 .unwrap();
7924 h_then_w.reshape(vec![n, c, h * scale, w * scale])
7925 }
7926
7927 pub fn upsample_nearest1d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7931 if scale == 0 {
7932 return Err(
7933 fuel_ir::Error::Msg("upsample_nearest1d: scale must be positive".into()).bt(),
7934 );
7935 }
7936 let dims = self.shape().dims().to_vec();
7937 if dims.len() != 3 {
7938 return Err(fuel_ir::Error::Msg(format!(
7939 "upsample_nearest1d: input must be rank 3 [N, C, T], got {dims:?}",
7940 ))
7941 .bt());
7942 }
7943 if scale == 1 {
7944 return Ok(self.clone());
7945 }
7946 let (n, c, t) = (dims[0], dims[1], dims[2]);
7947 let expanded = self.reshape(vec![n, c, t, 1])?;
7948 let replicated = (0..scale)
7949 .fold(None, |acc: Option<Tensor>, _| {
7950 Some(match acc {
7951 None => expanded.clone(),
7952 Some(a) => a.concat(&expanded, 3).unwrap(),
7953 })
7954 })
7955 .unwrap();
7956 replicated.reshape(vec![n, c, t * scale])
7957 }
7958
7959 pub fn interpolate2d(
7969 &self,
7970 target_h: usize,
7971 target_w: usize,
7972 ) -> std::result::Result<Self, fuel_ir::Error> {
7973 let dims = self.shape().dims().to_vec();
7974 if dims.len() != 4 {
7975 return Err(fuel_ir::Error::Msg(format!(
7976 "interpolate2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7977 ))
7978 .bt());
7979 }
7980 let h = dims[2];
7981 let w = dims[3];
7982 if h == 0 || w == 0 || target_h == 0 || target_w == 0 {
7983 return Err(fuel_ir::Error::Msg(
7984 "interpolate2d: input + target spatial dims must be positive".into(),
7985 )
7986 .bt());
7987 }
7988 if target_h == h && target_w == w {
7990 return Ok(self.clone());
7991 }
7992 if target_h.is_multiple_of(h) && target_w.is_multiple_of(w) && target_h / h == target_w / w
7996 {
7997 return self.upsample_nearest2d(target_h / h);
7998 }
7999 let h_idx: Vec<u32> = (0..target_h)
8003 .map(|oi| ((oi * h) / target_h).min(h - 1) as u32)
8004 .collect();
8005 let w_idx: Vec<u32> = (0..target_w)
8006 .map(|oj| ((oj * w) / target_w).min(w - 1) as u32)
8007 .collect();
8008 let h_idx_tensor = self.const_u32_like(h_idx, fuel_ir::Shape::from_dims(&[target_h]))?;
8009 let w_idx_tensor = self.const_u32_like(w_idx, fuel_ir::Shape::from_dims(&[target_w]))?;
8010 let after_h = self.index_select(2_usize, &h_idx_tensor)?;
8011 after_h.index_select(3_usize, &w_idx_tensor)
8012 }
8013
8014 pub fn interpolate1d(&self, target_t: usize) -> std::result::Result<Self, fuel_ir::Error> {
8018 let dims = self.shape().dims().to_vec();
8019 if dims.len() != 3 {
8020 return Err(fuel_ir::Error::Msg(format!(
8021 "interpolate1d: input must be rank 3 [N, C, T], got {dims:?}",
8022 ))
8023 .bt());
8024 }
8025 let t = dims[2];
8026 if t == 0 {
8027 return Err(
8028 fuel_ir::Error::Msg("interpolate1d: input length must be positive".into()).bt(),
8029 );
8030 }
8031 if !target_t.is_multiple_of(t) {
8032 return Err(fuel_ir::Error::Msg(format!(
8033 "interpolate1d: target {target_t} must be integer multiple of input {t}; non-integer ratios are future work",
8034 )).bt());
8035 }
8036 self.upsample_nearest1d(target_t / t)
8037 }
8038
8039 pub fn pad_with_zeros<D: Dim>(
8043 &self,
8044 dim: D,
8045 left: usize,
8046 right: usize,
8047 ) -> std::result::Result<Self, fuel_ir::Error> {
8048 self.pad_with_value(dim, left, right, 0.0)
8049 }
8050
8051 pub fn pad_with_value<D: Dim>(
8058 &self,
8059 dim: D,
8060 before: usize,
8061 after: usize,
8062 value: f32,
8063 ) -> std::result::Result<Self, fuel_ir::Error> {
8064 let shape = self.shape();
8065 let dim = dim.to_index(&shape, "pad_with_value")?;
8066 let rank = shape.dims().len();
8067 if before == 0 && after == 0 {
8068 return Ok(self.clone());
8069 }
8070 let mut padding: Vec<(usize, usize)> = vec![(0, 0); rank];
8071 padding[dim] = (before, after);
8072 self.pad(padding, fuel_graph::PadMode::Constant, value as f64)
8073 }
8074
8075 pub fn meshgrid(
8088 args: &[&Self],
8089 xy_indexing: bool,
8090 ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
8091 if args.len() < 2 {
8092 return Err(fuel_ir::Error::Msg(
8093 "meshgrid: requires at least two rank-1 tensors".into(),
8094 )
8095 .bt());
8096 }
8097 let ordered: Vec<&Self> = if xy_indexing {
8098 args.iter().rev().copied().collect()
8099 } else {
8100 args.to_vec()
8101 };
8102 let mut lens = Vec::with_capacity(ordered.len());
8103 for (i, t) in ordered.iter().enumerate() {
8104 let dims = t.shape().dims().to_vec();
8105 if dims.len() != 1 {
8106 return Err(fuel_ir::Error::Msg(format!(
8107 "meshgrid: input {i} must be rank 1, got shape {dims:?}",
8108 ))
8109 .bt());
8110 }
8111 lens.push(dims[0]);
8112 }
8113 let mut grids = Vec::with_capacity(ordered.len());
8114 for (idx, t) in ordered.iter().enumerate() {
8115 let mut shape = vec![1_usize; ordered.len()];
8116 shape[idx] = lens[idx];
8117 let placed = t.reshape(shape)?;
8118 let mut repeats = lens.clone();
8119 repeats[idx] = 1;
8120 let grid = placed.repeat(repeats)?;
8121 grids.push(grid);
8122 }
8123 if xy_indexing {
8124 grids.reverse();
8125 }
8126 Ok(grids)
8127 }
8128
8129 pub fn repeat(&self, repeats: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
8133 let repeats = repeats.into();
8134 let repeats: Vec<usize> = repeats.dims().to_vec();
8135 let self_rank = self.shape().dims().len();
8136 let target_rank = repeats.len();
8137 let mut work = if self_rank < target_rank {
8138 let pad_count = target_rank - self_rank;
8139 let mut new_shape: Vec<usize> = vec![1; pad_count];
8140 new_shape.extend_from_slice(self.shape().dims());
8141 self.reshape(new_shape)?
8142 } else if self_rank > target_rank {
8143 return Err(fuel_ir::Error::Msg(format!(
8144 "repeat: repeats rank {target_rank} smaller than tensor rank {self_rank}",
8145 ))
8146 .bt());
8147 } else {
8148 self.clone()
8149 };
8150 for (axis, &n) in repeats.iter().enumerate() {
8151 if n == 0 {
8152 return Err(fuel_ir::Error::Msg(format!(
8153 "repeat: zero repeat count at axis {axis} not supported",
8154 ))
8155 .bt());
8156 }
8157 if n == 1 {
8158 continue;
8159 }
8160 let base = work.clone();
8162 for _ in 1..n {
8163 work = work.concat(&base, axis)?;
8164 }
8165 }
8166 Ok(work)
8167 }
8168}
8169
8170fn first_zero_tap_output_index(
8179 dim: usize,
8180 kernel: usize,
8181 stride: usize,
8182 pad: usize,
8183) -> Option<usize> {
8184 let out_dim = (dim + 2 * pad).saturating_sub(kernel) / stride + 1;
8185 (0..out_dim).find(|&o| {
8186 let start = o as isize * stride as isize - pad as isize;
8187 let end = start + kernel as isize;
8188 let valid_start = start.max(0);
8189 let valid_end = end.min(dim as isize);
8190 valid_end <= valid_start
8191 })
8192}
8193
8194impl Tensor {
8197 pub fn from_safetensors_bytes(
8209 bytes: &[u8],
8210 dtype: safetensors::Dtype,
8211 shape: &[usize],
8212 device: &Device,
8213 ) -> crate::Result<Self> {
8214 use safetensors::Dtype;
8215 let shape_obj = Shape::from_dims(shape);
8216 let elem_count = shape_obj.elem_count();
8217
8218 let check_len = |expected: usize| -> crate::Result<()> {
8219 if bytes.len() != expected {
8220 crate::bail!(
8221 "from_safetensors_bytes: expected {expected} bytes for dtype {dtype:?} \
8222 and shape {shape:?}, got {}",
8223 bytes.len(),
8224 );
8225 }
8226 Ok(())
8227 };
8228
8229 match dtype {
8230 Dtype::F32 => {
8231 check_len(elem_count * 4)?;
8232 let mut data = Vec::with_capacity(elem_count);
8233 for chunk in bytes.as_chunks::<4>().0.iter() {
8234 data.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8235 }
8236 Self::from_f32(data, shape_obj, device)
8237 }
8238 Dtype::F64 => {
8239 check_len(elem_count * 8)?;
8240 let mut data = Vec::with_capacity(elem_count);
8241 for chunk in bytes.as_chunks::<8>().0.iter() {
8242 let arr: [u8; 8] = *chunk;
8243 data.push(f64::from_le_bytes(arr));
8244 }
8245 Self::from_f64(data, shape_obj, device)
8246 }
8247 Dtype::BF16 => {
8248 check_len(elem_count * 2)?;
8249 let mut data = Vec::with_capacity(elem_count);
8250 for chunk in bytes.as_chunks::<2>().0.iter() {
8251 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8252 data.push(half::bf16::from_bits(raw));
8253 }
8254 Self::from_bf16(data, shape_obj, device)
8255 }
8256 Dtype::F16 => {
8257 check_len(elem_count * 2)?;
8258 let mut data = Vec::with_capacity(elem_count);
8259 for chunk in bytes.as_chunks::<2>().0.iter() {
8260 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8261 data.push(half::f16::from_bits(raw));
8262 }
8263 Self::from_f16(data, shape_obj, device)
8264 }
8265 Dtype::U32 => {
8266 check_len(elem_count * 4)?;
8267 let mut data = Vec::with_capacity(elem_count);
8268 for chunk in bytes.as_chunks::<4>().0.iter() {
8269 data.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8270 }
8271 Self::from_u32(data, shape_obj, device)
8272 }
8273 other => crate::bail!(
8274 "from_safetensors_bytes: unsupported dtype {other:?} — extend Tensor's \
8275 safetensors loader to handle it",
8276 ),
8277 }
8278 }
8279
8280 pub fn from_safetensors_view(
8284 view: &safetensors::tensor::TensorView<'_>,
8285 device: &Device,
8286 ) -> crate::Result<Self> {
8287 Self::from_safetensors_bytes(view.data(), view.dtype(), view.shape(), device)
8288 }
8289}
8290
8291pub type LayerNormPair = (Arc<[f32]>, Arc<[f32]>);
8295
8296pub type ConvWeightBias = (Arc<[f32]>, Arc<[f32]>);
8300
8301#[derive(Debug, Clone)]
8324pub enum WeightStorage {
8325 F32(Arc<[f32]>),
8326 BF16(Arc<[half::bf16]>),
8327 Q4_0 {
8337 words: Arc<[u32]>,
8338 bytes_len: usize,
8339 in_features: usize,
8340 out_features: usize,
8341 },
8342 WithLoRA {
8353 base: Box<WeightStorage>,
8354 lora_a: Arc<[f32]>,
8356 lora_b: Arc<[f32]>,
8358 rank: usize,
8359 alpha: f32,
8361 in_features: usize,
8362 out_features: usize,
8363 },
8364}
8365
8366impl WeightStorage {
8367 pub fn elem_count(&self) -> usize {
8368 match self {
8369 Self::F32(a) => a.len(),
8370 Self::BF16(a) => a.len(),
8371 Self::Q4_0 {
8373 in_features,
8374 out_features,
8375 ..
8376 } => *in_features * *out_features,
8377 Self::WithLoRA {
8378 in_features,
8379 out_features,
8380 ..
8381 } => *in_features * *out_features,
8382 }
8383 }
8384
8385 pub fn dtype(&self) -> fuel_ir::DType {
8386 match self {
8387 Self::F32(_) => fuel_ir::DType::F32,
8388 Self::BF16(_) => fuel_ir::DType::BF16,
8389 Self::Q4_0 { .. } => fuel_ir::DType::U32,
8393 Self::WithLoRA { base, .. } => base.dtype(),
8396 }
8397 }
8398
8399 pub fn const_like(
8410 &self,
8411 anchor: &Tensor,
8412 shape: Shape,
8413 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8414 match self {
8415 Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8416 Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8417 Self::Q4_0 { words, .. } => {
8418 let _ = shape; anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8421 }
8422 Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8423 "WeightStorage::WithLoRA::const_like is not supported \
8424 — the base + LoRA update must be applied via \
8425 apply_linear to produce the right graph structure."
8426 .into(),
8427 )
8428 .bt()),
8429 }
8430 }
8431
8432 pub fn apply_linear_with_bias(
8441 &self,
8442 x: &Tensor,
8443 in_features: usize,
8444 out_features: usize,
8445 bias: std::sync::Arc<[f32]>,
8446 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8447 if bias.len() != out_features {
8448 return Err(fuel_ir::Error::Msg(format!(
8453 "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8454 bias.len(),
8455 ))
8456 .bt());
8457 }
8458 let projected = self.apply_linear(x, in_features, out_features)?;
8459 let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8460 projected.broadcast_add(&bias_t)
8461 }
8462
8463 pub fn apply_linear(
8481 &self,
8482 x: &Tensor,
8483 in_features: usize,
8484 out_features: usize,
8485 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8486 let x_dims = x.shape().dims().to_vec();
8492 match x_dims.last() {
8493 Some(&k) if k == in_features => {}
8494 _ => {
8495 return Err(fuel_ir::Error::Msg(format!(
8496 "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8497 must have trailing dim {in_features}",
8498 ))
8499 .bt());
8500 }
8501 }
8502 match self {
8503 Self::F32(_) | Self::BF16(_) => {
8504 let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8505 x.matmul(&w)
8506 }
8507 Self::Q4_0 {
8508 in_features: expected_in,
8509 out_features: expected_out,
8510 ..
8511 } => {
8512 if *expected_in != in_features || *expected_out != out_features {
8513 return Err(fuel_ir::Error::Msg(format!(
8514 "WeightStorage::Q4_0 shape mismatch: stored \
8515 [{expected_in}, {expected_out}], requested \
8516 [{in_features}, {out_features}]",
8517 ))
8518 .bt());
8519 }
8520 let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8521 x.qmatmul(
8522 &w_bytes,
8523 fuel_graph::QuantType::Q4_0,
8524 in_features,
8525 out_features,
8526 )
8527 }
8528 Self::WithLoRA {
8529 base,
8530 lora_a,
8531 lora_b,
8532 rank,
8533 alpha,
8534 in_features: expected_in,
8535 out_features: expected_out,
8536 } => {
8537 if *expected_in != in_features || *expected_out != out_features {
8538 return Err(fuel_ir::Error::Msg(format!(
8539 "WeightStorage::WithLoRA shape mismatch: stored \
8540 [{expected_in}, {expected_out}], requested \
8541 [{in_features}, {out_features}]",
8542 ))
8543 .bt());
8544 }
8545 let base_out = base.apply_linear(x, in_features, out_features)?;
8547 let a_t =
8549 x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8550 let b_t =
8551 x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8552 let scale = *alpha as f64 / *rank as f64;
8553 let lora_path = Tensor {
8555 inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8556 };
8557 base_out.add(&lora_path)
8558 }
8559 }
8560 }
8561
8562 pub fn apply_linear_dyn_m(
8575 &self,
8576 x: &Tensor,
8577 in_features: usize,
8578 out_features: usize,
8579 row_count: fuel_ir::DynScalar,
8580 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8581 match self {
8582 Self::F32(_) => {
8583 let w = self
8585 .const_like(x, Shape::from_dims(&[in_features, out_features]))
8586 .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8587 x.matmul_dyn_m(&w, row_count)
8588 }
8589 other => Err(fuel_ir::Error::Msg(format!(
8590 "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8591 weight (cast the expert weights to F32)",
8592 other.dtype(),
8593 ))
8594 .bt()),
8595 }
8596 }
8597
8598 pub fn with_lora(
8603 self,
8604 lora_a: Arc<[f32]>,
8605 lora_b: Arc<[f32]>,
8606 rank: usize,
8607 alpha: f32,
8608 in_features: usize,
8609 out_features: usize,
8610 ) -> Self {
8611 assert_eq!(
8612 lora_a.len(),
8613 in_features * rank,
8614 "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8615 lora_a.len(),
8616 in_features * rank,
8617 );
8618 assert_eq!(
8619 lora_b.len(),
8620 rank * out_features,
8621 "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8622 lora_b.len(),
8623 rank * out_features,
8624 );
8625 assert!(
8626 !matches!(self, Self::WithLoRA { .. }),
8627 "with_lora: base is already WithLoRA (nested adapters unsupported)",
8628 );
8629 Self::WithLoRA {
8630 base: Box::new(self),
8631 lora_a,
8632 lora_b,
8633 rank,
8634 alpha,
8635 in_features,
8636 out_features,
8637 }
8638 }
8639}
8640
8641impl From<Arc<[f32]>> for WeightStorage {
8645 fn from(a: Arc<[f32]>) -> Self {
8646 Self::F32(a)
8647 }
8648}
8649impl From<Vec<f32>> for WeightStorage {
8650 fn from(v: Vec<f32>) -> Self {
8651 Self::F32(Arc::from(v))
8652 }
8653}
8654impl From<Arc<[half::bf16]>> for WeightStorage {
8655 fn from(a: Arc<[half::bf16]>) -> Self {
8656 Self::BF16(a)
8657 }
8658}
8659impl From<Vec<half::bf16>> for WeightStorage {
8660 fn from(v: Vec<half::bf16>) -> Self {
8661 Self::BF16(Arc::from(v))
8662 }
8663}
8664
8665#[derive(Debug, Clone)]
8677pub struct LayerWeights {
8678 pub attn_q: WeightStorage,
8680 pub attn_q_bias: Option<Arc<[f32]>>,
8682 pub attn_k: WeightStorage,
8684 pub attn_k_bias: Option<Arc<[f32]>>,
8686 pub attn_v: WeightStorage,
8688 pub attn_v_bias: Option<Arc<[f32]>>,
8690 pub attn_o: WeightStorage,
8692 pub ffn_gate: WeightStorage,
8694 pub ffn_up: WeightStorage,
8696 pub ffn_down: WeightStorage,
8698 pub attn_norm_gain: Arc<[f32]>,
8701 pub ffn_norm_gain: Arc<[f32]>,
8703}
8704
8705pub fn invalidate_decode_pair_if_stale<C>(
8706 session: &mut Option<crate::inference_context::DecodeSession>,
8707 captured: &mut Option<C>,
8708 ctx: &mut InferenceContext,
8709 seq: usize,
8710 max_seq_len: Option<usize>,
8711 cache_dtype: DType,
8712 n_layers: usize,
8713 shape_key: u64,
8714 kv: &dyn crate::inference_context::KvRebindSource,
8715 drop_session: impl FnOnce(
8716 &mut Option<crate::inference_context::DecodeSession>,
8717 &mut InferenceContext,
8718 ),
8719) -> SessionDisposition {
8720 refresh_decode_session(
8721 session,
8722 ctx,
8723 seq,
8724 max_seq_len,
8725 cache_dtype,
8726 n_layers,
8727 shape_key,
8728 kv,
8729 || *captured = None,
8733 drop_session,
8734 )
8735}
8736
8737#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8739pub enum SessionDisposition {
8740 Absent,
8742 Kept,
8744 Rebound,
8747 Dropped(Option<crate::inference_context::RebindRefusal>),
8751}
8752
8753pub fn refresh_decode_session(
8778 session: &mut Option<crate::inference_context::DecodeSession>,
8779 ctx: &mut InferenceContext,
8780 seq: usize,
8781 max_seq_len: Option<usize>,
8782 cache_dtype: DType,
8783 n_layers: usize,
8784 shape_key: u64,
8785 kv: &dyn crate::inference_context::KvRebindSource,
8786 on_invalidate: impl FnOnce(),
8787 drop_session: impl FnOnce(
8788 &mut Option<crate::inference_context::DecodeSession>,
8789 &mut InferenceContext,
8790 ),
8791) -> SessionDisposition {
8792 use crate::inference_context::PlanValidity;
8793
8794 let verdict = match (session.as_ref(), max_seq_len) {
8795 (None, _) => return SessionDisposition::Absent,
8796 (Some(s), Some(msl)) => {
8797 s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8798 }
8799 (Some(_), None) => PlanValidity::Stale,
8802 };
8803 if verdict == PlanValidity::Valid {
8804 return SessionDisposition::Kept;
8805 }
8806
8807 on_invalidate();
8808
8809 let refusal = if verdict == PlanValidity::AllocationChanged {
8810 match session.as_mut().map(|s| s.rebind_kv(kv)) {
8811 Some(Ok(())) => return SessionDisposition::Rebound,
8812 Some(Err(r)) => Some(r),
8813 None => None,
8817 }
8818 } else {
8819 None
8820 };
8821
8822 drop_session(session, ctx);
8823 SessionDisposition::Dropped(refusal)
8824}
8825
8826pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8827 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8828 for q_idx in 0..seq {
8829 let abs_q = cached_len + q_idx;
8830 for k_idx in (abs_q + 1)..max_seq_len {
8831 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8832 }
8833 }
8834 mask_data
8835}
8836
8837pub fn build_decode_causal_mask_windowed(
8877 cached_len: usize,
8878 seq: usize,
8879 max_seq_len: usize,
8880 window: usize,
8881) -> Vec<f32> {
8882 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8883 for q_idx in 0..seq {
8884 let abs_q = cached_len + q_idx;
8885 for k_idx in 0..max_seq_len {
8886 if k_idx > abs_q || k_idx + window <= abs_q {
8888 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8889 }
8890 }
8891 }
8892 mask_data
8893}
8894
8895#[cfg(test)]
8896mod decode_mask_tests {
8897 use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8898
8899 fn attendable(row: &[f32]) -> Vec<u8> {
8902 row.iter().map(|&v| u8::from(v == 0.0)).collect()
8903 }
8904
8905 #[test]
8912 fn windowed_mask_excludes_positions_older_than_the_window() {
8913 let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8914 assert_eq!(
8915 attendable(&m),
8916 vec![0, 0, 1, 1, 0, 0],
8917 "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8918 );
8919 }
8920
8921 #[test]
8926 fn windowed_mask_offsets_each_row_by_cached_len() {
8927 let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8928 assert_eq!(
8929 attendable(&m[0..5]),
8930 vec![1, 1, 0, 0, 0],
8931 "row 0 = abs pos 1"
8932 );
8933 assert_eq!(
8934 attendable(&m[5..10]),
8935 vec![0, 1, 1, 0, 0],
8936 "row 1 = abs pos 2"
8937 );
8938 }
8939
8940 #[test]
8949 fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
8950 for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
8951 let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
8952 let windowed =
8953 build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
8954 assert_eq!(
8955 dense, windowed,
8956 "window == max_seq_len must equal dense at \
8957 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
8958 );
8959 }
8960 }
8961
8962 #[test]
8971 fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
8972 let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
8973
8974 let mut prefill = vec![0.0_f32; n * n];
8976 for i in 0..n {
8977 for j in 0..n {
8978 if j > i || j + window <= i {
8979 prefill[i * n + j] = f32::NEG_INFINITY;
8980 }
8981 }
8982 }
8983
8984 for p in 0..n {
8985 let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
8986 assert_eq!(
8987 attendable(&decode[0..n]),
8988 attendable(&prefill[p * n..(p + 1) * n]),
8989 "decode at cached_len={p} must match prefill row {p}",
8990 );
8991 assert!(
8992 decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
8993 "the unwritten tail beyond position {n} must stay masked",
8994 );
8995 }
8996 }
8997
8998 }
9039
9040pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9078 match window {
9079 None => (None, None),
9080 Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9081 }
9082}
9083
9084pub fn offer_flash_decode_arm_for_region(
9089 graph: &fuel_graph::SharedGraph,
9090 q: fuel_graph::NodeId,
9091 k: fuel_graph::NodeId,
9092 v: fuel_graph::NodeId,
9093 decomposed_out: fuel_graph::NodeId,
9094 reconverge: fuel_graph::NodeId,
9095 softmax_scale: f32,
9096 attended_len_sym: fuel_ir::SymId,
9097 attn_window: Option<usize>,
9098 softcap: Option<f32>,
9099 cap: fuel_dispatch::decode_flash::FlashArmCapability,
9100) -> crate::Result<Option<fuel_graph::NodeId>> {
9101 use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9102 let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9103 let spec = DecodeFlashSpec {
9104 q,
9105 k,
9106 v,
9107 alibi: None,
9108 softmax_scale,
9109 causal: true,
9110 window_size_left,
9117 window_size_right,
9118 softcap,
9119 k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9120 decomposed_out,
9121 reconverge,
9122 };
9123 let mut g = graph.write().map_err(|_| {
9124 fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9125 })?;
9126 offer_decode_flash_arm(&mut g, &spec, cap)
9127}
9128
9129pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9130 assert_eq!(
9131 gain.len(),
9132 dim,
9133 "apply_affine_rms_norm: gain length must equal dim"
9134 );
9135 let normalized = x.rms_norm_last_dim(eps).unwrap();
9136 let gain_t = x
9142 .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9143 .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9144 normalized.broadcast_mul(&gain_t).unwrap()
9145}
9146
9147pub fn load_tensor_as_f32(
9154 st: &fuel_loaders::safetensors::MmapedSafetensors,
9155 name: &str,
9156) -> crate::Result<Vec<f32>> {
9157 use safetensors::Dtype;
9158 let view = st.get(name)?;
9159 let bytes = view.data();
9160 match view.dtype() {
9161 Dtype::F32 => {
9162 let mut out = Vec::with_capacity(bytes.len() / 4);
9163 for chunk in bytes.as_chunks::<4>().0.iter() {
9164 out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9165 }
9166 Ok(out)
9167 }
9168 Dtype::F64 => {
9169 let mut out = Vec::with_capacity(bytes.len() / 8);
9170 for chunk in bytes.as_chunks::<8>().0.iter() {
9171 let arr: [u8; 8] = *chunk;
9172 out.push(f64::from_le_bytes(arr) as f32);
9173 }
9174 Ok(out)
9175 }
9176 Dtype::BF16 => {
9177 let mut out = Vec::with_capacity(bytes.len() / 2);
9178 for chunk in bytes.as_chunks::<2>().0.iter() {
9179 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9180 out.push(half::bf16::from_bits(raw).to_f32());
9181 }
9182 Ok(out)
9183 }
9184 Dtype::F16 => {
9185 let mut out = Vec::with_capacity(bytes.len() / 2);
9186 for chunk in bytes.as_chunks::<2>().0.iter() {
9187 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9188 out.push(half::f16::from_bits(raw).to_f32());
9189 }
9190 Ok(out)
9191 }
9192 other => {
9193 crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9194 }
9195 }
9196}
9197
9198pub fn load_transposed_matrix(
9205 st: &fuel_loaders::safetensors::MmapedSafetensors,
9206 name: &str,
9207 out_features: usize,
9208 in_features: usize,
9209) -> crate::Result<Vec<f32>> {
9210 let flat = load_tensor_as_f32(st, name)?;
9211 if flat.len() != out_features * in_features {
9212 crate::bail!(
9213 "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9214 flat.len(),
9215 out_features * in_features,
9216 );
9217 }
9218 let mut out = vec![0.0_f32; out_features * in_features];
9222 for i in 0..out_features {
9223 for j in 0..in_features {
9224 out[j * out_features + i] = flat[i * in_features + j];
9225 }
9226 }
9227 Ok(out)
9228}
9229
9230pub fn load_transposed_matrix_preserve_dtype(
9242 st: &fuel_loaders::safetensors::MmapedSafetensors,
9243 name: &str,
9244 out_features: usize,
9245 in_features: usize,
9246) -> crate::Result<WeightStorage> {
9247 use safetensors::Dtype;
9248 let view = st.get(name)?;
9249 let bytes = view.data();
9250 let expected = out_features * in_features;
9251 match view.dtype() {
9252 Dtype::BF16 => {
9253 if bytes.len() != expected * 2 {
9254 crate::bail!(
9255 "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9256 bytes.len(),
9257 expected * 2,
9258 );
9259 }
9260 let mut out = vec![half::bf16::ZERO; expected];
9263 for i in 0..out_features {
9264 for j in 0..in_features {
9265 let src_off = (i * in_features + j) * 2;
9266 let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9267 out[j * out_features + i] = half::bf16::from_bits(bits);
9268 }
9269 }
9270 Ok(WeightStorage::BF16(Arc::from(out)))
9271 }
9272 _ => {
9273 let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9277 Ok(WeightStorage::F32(Arc::from(flat)))
9278 }
9279 }
9280}
9281
9282#[derive(Debug, Clone, Copy, Default)]
9284pub enum SamplingStrategy {
9285 #[default]
9287 Greedy,
9288 Temperature { temp: f32, seed: u64 },
9293}
9294
9295pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9299 match strategy {
9300 SamplingStrategy::Greedy => {
9301 let (i, _) = logits
9302 .iter()
9303 .enumerate()
9304 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9305 .expect("sample_logits: empty logits");
9306 i as u32
9307 }
9308 SamplingStrategy::Temperature { temp, .. } => {
9309 let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9312 let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9313 let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9314 let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9315 let sum: f32 = exp.iter().sum();
9316 let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9317 sample_multinomial(&probs, rng_state)
9318 }
9319 }
9320}
9321
9322pub fn spec_argmax(logits: &[f32]) -> u32 {
9330 let mut best = 0;
9331 let mut best_v = logits[0];
9332 for (i, &v) in logits.iter().enumerate().skip(1) {
9333 if v > best_v {
9334 best_v = v;
9335 best = i;
9336 }
9337 }
9338 best as u32
9339}
9340
9341pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9343 let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9344 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9345 let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9346 let sum: f32 = exp.iter().sum();
9347 exp.iter().map(|&x| x / sum).collect()
9348}
9349
9350pub fn spec_next_u01(state: &mut u64) -> f32 {
9352 *state = state
9353 .wrapping_mul(6364136223846793005)
9354 .wrapping_add(1442695040888963407);
9355 (*state >> 32) as f32 / u32::MAX as f32
9356}
9357
9358pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9360 let u = spec_next_u01(state);
9361 let mut cum = 0.0_f32;
9362 for (i, &p) in probs.iter().enumerate() {
9363 cum += p;
9364 if u <= cum {
9365 return i as u32;
9366 }
9367 }
9368 (probs.len() - 1) as u32
9369}
9370
9371pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9375 *rng_state = rng_state
9377 .wrapping_mul(6364136223846793005)
9378 .wrapping_add(1442695040888963407);
9379 let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9380 let mut cumulative = 0.0_f32;
9381 for (i, &p) in probs.iter().enumerate() {
9382 cumulative += p;
9383 if u <= cumulative {
9384 return i as u32;
9385 }
9386 }
9387 (probs.len() - 1) as u32
9389}
9390
9391#[cfg(test)]
9392mod lora_tests {
9393 use super::*;
9394 use crate::Shape;
9395
9396 #[test]
9397 fn with_lora_matches_manual_base_plus_lora() {
9398 let in_f = 4;
9400 let out_f = 3;
9401 let rank = 2;
9402 let alpha = 8.0_f32;
9403
9404 let anchor =
9405 Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9406 let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9408 let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9409 let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9410
9411 let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9412 Arc::from(lora_a_vec.clone()),
9413 Arc::from(lora_b_vec.clone()),
9414 rank,
9415 alpha,
9416 in_f,
9417 out_f,
9418 );
9419
9420 let batch = 2;
9422 let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9423 let x = anchor
9424 .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9425 .unwrap();
9426 let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9427 let got = y.realize_f32().to_vec();
9428
9429 let mut expected = vec![0.0_f32; batch * out_f];
9431 for b in 0..batch {
9432 for j in 0..out_f {
9433 let mut acc = 0.0_f32;
9434 for k in 0..in_f {
9436 acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9437 }
9438 let scale = alpha as f64 / rank as f64;
9440 for r in 0..rank {
9441 let mut xar = 0.0_f32;
9442 for k in 0..in_f {
9443 xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9444 }
9445 acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9446 }
9447 expected[b * out_f + j] = acc;
9448 }
9449 }
9450
9451 for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9452 let diff = (e - g).abs();
9453 assert!(
9454 diff <= 1e-4,
9455 "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9456 );
9457 }
9458 }
9459
9460 #[test]
9461 #[should_panic(expected = "lora_a length")]
9462 fn with_lora_rejects_mismatched_a_shape() {
9463 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let bad_a = Arc::from(vec![0.0_f32; 3]); let b = Arc::from(vec![0.0_f32; 6]); let _ = ws.with_lora(bad_a, b, 2, 8.0, 4, 3);
9467 }
9468
9469 #[test]
9481 fn apply_linear_errors_on_trailing_dim_mismatch() {
9482 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x = Tensor::from_f32(
9484 vec![0.0_f32; 10],
9485 Shape::from_dims(&[2, 5]), &Device::cpu(),
9487 )
9488 .unwrap();
9489 let err = ws
9490 .apply_linear(&x, 4, 3)
9491 .expect_err("must not panic, must Err");
9492 let msg = err.to_string();
9493 assert!(
9494 msg.contains("trailing dim 4"),
9495 "error should name the expected trailing dim, got: {msg}",
9496 );
9497 }
9498
9499 #[test]
9500 fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9501 let ws = WeightStorage::Q4_0 {
9504 words: Arc::from(vec![0_u32; 8]),
9505 bytes_len: 18, in_features: 4,
9507 out_features: 4,
9508 };
9509 let x =
9510 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9511 let err = ws
9514 .apply_linear(&x, 4, 7)
9515 .expect_err("must not panic, must Err");
9516 let msg = err.to_string();
9517 assert!(
9518 msg.contains("Q4_0 shape mismatch"),
9519 "error should name the stored/requested disagreement, got: {msg}",
9520 );
9521 }
9522
9523 #[test]
9524 fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9525 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x =
9527 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9528 let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); let err = ws
9530 .apply_linear_with_bias(&x, 4, 3, bias)
9531 .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9532 assert!(
9533 err.to_string().contains("bias len"),
9534 "error should name the bias length, got: {err}",
9535 );
9536 }
9537}
9538
9539#[cfg(test)]
9540mod safetensors_bridge_tests {
9541 use super::*;
9542
9543 #[test]
9544 fn from_safetensors_bytes_round_trip_f32() {
9545 let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9549 let mut bytes = Vec::with_capacity(original.len() * 4);
9550 for &v in &original {
9551 bytes.extend_from_slice(&v.to_le_bytes());
9552 }
9553 let t =
9554 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9555 .unwrap();
9556 assert_eq!(t.shape().dims(), &[4]);
9557 assert_eq!(t.dtype(), DType::F32);
9558 assert_eq!(t.realize_f32(), original);
9559 }
9560
9561 #[test]
9562 fn from_safetensors_bytes_round_trip_bf16() {
9563 let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9564 let bf16_vec: Vec<half::bf16> = original_f32
9565 .iter()
9566 .map(|&v| half::bf16::from_f32(v))
9567 .collect();
9568 let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9569 for b in &bf16_vec {
9570 bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9571 }
9572 let t =
9573 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9574 .unwrap();
9575 assert_eq!(t.dtype(), DType::BF16);
9576 let realized = t.realize_bf16();
9579 assert_eq!(realized, bf16_vec);
9580 }
9581
9582 #[test]
9583 fn from_safetensors_bytes_rejects_wrong_byte_count() {
9584 let bad_bytes = vec![0_u8; 4];
9587 let result = Tensor::from_safetensors_bytes(
9588 &bad_bytes,
9589 safetensors::Dtype::F32,
9590 &[3],
9591 &Device::cpu(),
9592 );
9593 assert!(result.is_err());
9594 }
9595}
9596
9597#[cfg(test)]
9606mod phase_a1_wrapper_tests {
9607 use super::*;
9608
9609 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9610 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9611 }
9612
9613 #[test]
9614 fn unsqueeze_adds_size_one_dim() {
9615 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9616 let out = t.unsqueeze(0_usize).unwrap();
9617 assert_eq!(out.shape().dims(), &[1, 2, 2]);
9618 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9619 }
9620
9621 #[test]
9622 fn unsqueeze_errors_out_of_bounds() {
9623 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9624 assert!(t.unsqueeze(0_usize).is_ok());
9626 assert!(t.unsqueeze(1_usize).is_ok());
9627 assert!(t.unsqueeze(2_usize).is_err());
9628 }
9629
9630 #[test]
9631 fn try_reshape_errors_on_size_mismatch() {
9632 let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9633 assert!(t.reshape(vec![3, 2]).is_ok());
9634 assert!(t.reshape(vec![2, 2]).is_err());
9635 }
9636
9637 #[test]
9638 fn try_permute_validates_axes() {
9639 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9640 assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9642 assert!(t.permute([2_usize, 0, 1]).is_ok());
9643 assert!(t.permute([0_usize, 1]).is_err()); assert!(t.permute([0_usize, 0, 1]).is_err()); }
9646
9647 #[test]
9648 fn try_transpose_requires_rank_two_plus() {
9649 let scalar = cpu_f32(vec![1.0], &[1]);
9650 let _ = scalar.transpose();
9652 }
9653
9654 #[test]
9655 fn triu_tril_shape_preserved() {
9656 let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9657 let upper = t.triu(0).unwrap();
9658 let lower = t.tril(0).unwrap();
9659 assert_eq!(upper.shape().dims(), &[3, 3]);
9660 assert_eq!(lower.shape().dims(), &[3, 3]);
9661 assert_eq!(
9663 lower.realize_f32(),
9664 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9665 );
9666 }
9667
9668 #[test]
9669 fn triu_rejects_rank_one() {
9670 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9671 assert!(t.triu(0).is_err());
9672 }
9673
9674 #[test]
9675 fn log_softmax_last_dim_shape_preserved() {
9676 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9677 let out = t.log_softmax_last_dim().unwrap();
9678 assert_eq!(out.shape().dims(), &[2, 2]);
9679 for v in out.realize_f32() {
9681 assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9682 }
9683 }
9684
9685 #[test]
9686 fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9687 let data: Vec<f32> = vec![
9696 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0, ];
9705 let t = cpu_f32(data, &[2, 3, 2]);
9706 let out = t.softmax(1_usize).unwrap();
9707 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9708 let v = out.realize_f32();
9709
9710 let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9712 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9713 let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9714 let s = exps[0] + exps[1] + exps[2];
9715 [exps[0] / s, exps[1] / s, exps[2] / s]
9716 };
9717 let lanes: [[f32; 3]; 4] = [
9719 [-1.0, 0.0, -2.0], [0.0, -3.0, -1.0], [-2.0, -1.0, 0.0], [-1.0, -1.0, 0.0], ];
9724 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9725
9726 for b in 0..2 {
9728 for c in 0..2 {
9729 let lane_ix = b * 2 + c;
9730 for r in 0..3 {
9731 let got = v[b * 6 + r * 2 + c];
9732 let want = refs[lane_ix][r];
9733 assert!(
9734 (got - want).abs() < 1e-6,
9735 "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9736 );
9737 }
9738 let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9740 assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9741 }
9742 }
9743 }
9744
9745 #[test]
9746 fn softmax_last_axis_matches_softmax_last_dim() {
9747 let data: Vec<f32> = vec![
9748 1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, 4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9750 ];
9751 let t = cpu_f32(data, &[2, 2, 4]);
9752 let via_general = t.softmax(2_usize).unwrap();
9753 let via_fused = t.softmax_last_dim().unwrap();
9754 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9755 let g = via_general.realize_f32();
9756 let f = via_fused.realize_f32();
9757 assert_eq!(g.len(), f.len());
9758 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9759 assert!(
9760 (a - b).abs() < 1e-6,
9761 "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9762 );
9763 }
9764 }
9765
9766 #[test]
9767 fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9768 let data: Vec<f32> = vec![
9771 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9772 ];
9773 let t = cpu_f32(data, &[2, 3, 2]);
9774 let out = t.log_softmax(1_usize).unwrap();
9775 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9776 let v = out.realize_f32();
9777
9778 let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9779 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9780 let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9781 let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9782 [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9783 };
9784 let lanes: [[f32; 3]; 4] = [
9785 [-1.0, 0.0, -2.0],
9786 [0.0, -3.0, -1.0],
9787 [-2.0, -1.0, 0.0],
9788 [-1.0, -1.0, 0.0],
9789 ];
9790 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9791
9792 for b in 0..2 {
9793 for c in 0..2 {
9794 let lane_ix = b * 2 + c;
9795 for r in 0..3 {
9796 let got = v[b * 6 + r * 2 + c];
9797 let want = refs[lane_ix][r];
9798 assert!(
9799 (got - want).abs() < 1e-6,
9800 "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9801 );
9802 assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9804 }
9805 }
9806 }
9807 }
9808
9809 #[test]
9810 fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9811 let data: Vec<f32> = vec![
9812 1.0, 2.0, -1.0, 0.5, 0.0, -2.0, 3.0, 1.5, 4.0, -1.0, 2.0, 0.0, -3.0, 0.25, 0.75, 1.0,
9813 ];
9814 let t = cpu_f32(data, &[2, 2, 4]);
9815 let via_general = t.log_softmax(2_usize).unwrap();
9816 let via_fused = t.log_softmax_last_dim().unwrap();
9817 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9818 let g = via_general.realize_f32();
9819 let f = via_fused.realize_f32();
9820 assert_eq!(g.len(), f.len());
9821 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9822 assert!(
9823 (a - b).abs() < 1e-6,
9824 "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9825 );
9826 }
9827 }
9828
9829 #[test]
9830 fn argmin_dim_drops_reduced_axis() {
9831 let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9832 let out = t.argmin_dim(1_usize).unwrap();
9833 assert_eq!(out.shape().dims(), &[2]);
9834 assert_eq!(out.dtype(), DType::U32);
9835 assert_eq!(out.realize_u32(), vec![1, 0]);
9836 }
9837
9838 #[test]
9839 fn masked_fill_smoke() {
9840 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9841 let probe = t
9844 .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9845 .unwrap();
9846 let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9847 let mask = probe.gt(&threshold).unwrap(); let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9849 assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9850 }
9851
9852 #[test]
9858 fn bool_is_distinguishable_from_u8_end_to_end() {
9859 let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9864 let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9865 let mask = t.gt(&thr).unwrap();
9866 assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9867 assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9868
9869 let u8_vals = t
9876 .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9877 .unwrap()
9878 .to_dtype(DType::U8)
9879 .unwrap();
9880 assert_eq!(u8_vals.dtype(), DType::U8);
9881 assert_eq!(
9882 u8_vals.realize_u8(),
9883 vec![0, 5, 3, 0],
9884 "negative control: the U8 source must really hold 5 and 3, else the \
9885 conversion assertion below cannot discriminate"
9886 );
9887
9888 let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9892 assert_eq!(as_bool.dtype(), DType::Bool);
9893 assert_eq!(
9894 as_bool.realize_u8(),
9895 vec![0, 1, 1, 0],
9896 "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9897 );
9898
9899 assert_eq!(
9901 as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9902 vec![0.0, 1.0, 1.0, 0.0],
9903 "Bool->F32 yields 0.0/1.0"
9904 );
9905 assert_eq!(
9908 mask.to_dtype(DType::U8).unwrap().realize_u8(),
9909 vec![0, 1, 1, 0]
9910 );
9911
9912 let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9917 assert!(
9918 err.is_err(),
9919 "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9920 byte-identical, so accepting it would 'work' and erase the distinction"
9921 );
9922 }
9923
9924 #[test]
9938 fn float_to_bool_lowering_preserves_ieee_semantics() {
9939 let t = cpu_f32(
9940 vec![
9941 0.0,
9942 -0.0,
9943 5.0,
9944 -3.0,
9945 f32::NAN,
9946 f32::INFINITY,
9947 f32::NEG_INFINITY,
9948 ],
9949 &[7],
9950 );
9951 let b = t.to_dtype(DType::Bool).unwrap();
9952 assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
9953 assert_eq!(
9954 b.realize_u8(),
9955 vec![0, 0, 1, 1, 1, 1, 1],
9956 "0 and -0.0 are false; every other value including NaN and both \
9957 infinities is true",
9958 );
9959 }
9960
9961 #[test]
9962 fn index_add_smoke() {
9963 let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
9964 let src = base
9965 .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
9966 .unwrap();
9967 let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
9968 let out = base.index_add(0, &indices, &src).unwrap();
9969 assert_eq!(out.shape().dims(), &[2, 2]);
9970 let v = out.realize_f32();
9972 assert_eq!(v[0], 41.0); assert_eq!(v[1], 61.0); assert_eq!(v[2], 1.0);
9975 assert_eq!(v[3], 1.0);
9976 }
9977
9978 #[test]
9979 fn scatter_add_smoke() {
9980 let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
9981 let src = base
9982 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
9983 .unwrap();
9984 let indices = base
9985 .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
9986 .unwrap();
9987 let out = base.scatter_add(0, &indices, &src).unwrap();
9988 assert_eq!(out.shape().dims(), &[2, 2]);
9989 }
9990
9991 #[test]
9992 fn inplace_activations_compile_and_run() {
9993 let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
9994 let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
9996 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
9997 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
9998 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
9999 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10000 let _ = t.affine_inplace(2.0, 1.0);
10001 let v = r.realize_f32();
10003 assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10004 }
10005
10006 #[test]
10007 fn const_f64_like_round_trips() {
10008 let anchor = cpu_f32(vec![0.0], &[1]);
10009 let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10010 assert_eq!(t.shape().dims(), &[3]);
10011 assert_eq!(t.dtype(), DType::F64);
10012 assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10013 }
10014
10015 #[test]
10016 fn const_i64_like_round_trips() {
10017 let anchor = cpu_f32(vec![0.0], &[1]);
10018 let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10019 assert_eq!(t.shape().dims(), &[3]);
10020 assert_eq!(t.dtype(), DType::I64);
10021 }
10022
10023 #[test]
10024 fn on_device_smoke_cpu() {
10025 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10026 let pinned = t.on_device(&Device::cpu());
10027 assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10028 }
10029
10030 #[test]
10031 fn copy_to_device_same_device_round_trips() {
10032 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10033 let copied = t.copy_to_device(&Device::cpu());
10034 assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10035 }
10036}
10037
10038#[cfg(test)]
10042mod phase_a2_composite_tests {
10043 use super::*;
10044
10045 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10046 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10047 }
10048
10049 #[test]
10050 fn transpose_last_two_swaps_last_two_dims() {
10051 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10052 let out = t.transpose_last_two().unwrap();
10053 assert_eq!(out.shape().dims(), &[3, 2]);
10054 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10055 }
10056
10057 #[test]
10058 fn t_is_alias_of_transpose_last_two() {
10059 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10060 assert_eq!(
10061 t.t().unwrap().realize_f32(),
10062 t.transpose_last_two().unwrap().realize_f32()
10063 );
10064 }
10065
10066 #[test]
10067 fn transpose_dims_swaps_arbitrary_axes() {
10068 let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10069 let out = t.transpose_dims(0, 2).unwrap();
10070 assert_eq!(out.shape().dims(), &[4, 3, 2]);
10071 }
10072
10073 #[test]
10074 fn transpose_dims_identity_when_same_axis() {
10075 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10076 let out = t.transpose_dims(0, 0).unwrap();
10077 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10078 }
10079
10080 #[test]
10081 fn transpose_dims_errors_out_of_bounds() {
10082 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10083 assert!(t.transpose_dims(0, 2).is_err());
10084 assert!(t.transpose_dims(5, 0).is_err());
10085 }
10086
10087 #[test]
10088 fn flatten_merges_middle_dims() {
10089 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10090 let out = t.flatten(0, 1).unwrap();
10091 assert_eq!(out.shape().dims(), &[6, 4]);
10092 }
10093
10094 #[test]
10095 fn flatten_to_merges_leading_dims() {
10096 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10097 let out = t.flatten_to(1).unwrap();
10098 assert_eq!(out.shape().dims(), &[6, 4]);
10099 }
10100
10101 #[test]
10102 fn flatten_from_merges_trailing_dims() {
10103 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10104 let out = t.flatten_from(1).unwrap();
10105 assert_eq!(out.shape().dims(), &[2, 12]);
10106 }
10107
10108 #[test]
10109 fn flatten_all_produces_rank_one() {
10110 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10111 let out = t.flatten_all().unwrap();
10112 assert_eq!(out.shape().dims(), &[6]);
10113 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10114 }
10115
10116 #[test]
10126 fn rope_batched_matches_single_position_rope_row_by_row() {
10127 let lcg = |n: usize, seed: u32| -> Vec<f32> {
10128 let mut s = seed;
10129 (0..n)
10130 .map(|_| {
10131 s = s.wrapping_mul(1103515245).wrapping_add(12345);
10132 ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10133 })
10134 .collect()
10135 };
10136 let (b, heads, hd) = (3usize, 4usize, 8usize);
10137 let base = 10000.0f64;
10138 let positions = [0usize, 7, 3];
10139
10140 let x_data = lcg(b * heads * hd, 11);
10141 let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10142 let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10143 let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10144
10145 let per_row = heads * hd;
10146 for (row, &pos) in positions.iter().enumerate() {
10147 let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10148 let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10149 let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10150 let want = xr
10151 .rope_with_tables_decomposed(&c, &s)
10152 .unwrap()
10153 .realize_f32();
10154 assert_eq!(
10155 &got[row * per_row..(row + 1) * per_row],
10156 &want[..],
10157 "row {row} (position {pos}) != decomposed RoPE at that position",
10158 );
10159 }
10160
10161 let shared = [positions[0]; 3];
10166 let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10167 let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10168 for (row, &pos) in positions.iter().enumerate().skip(1) {
10169 assert_ne!(
10170 &wrong[row * per_row..(row + 1) * per_row],
10171 &got[row * per_row..(row + 1) * per_row],
10172 "CONTROL: row {row} at shared position 0 matched its own position \
10173 {pos} — per-row RoPE not applied, or comparison blind to it",
10174 );
10175 }
10176 }
10177
10178 #[test]
10179 fn flatten_errors_on_bad_range() {
10180 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10181 assert!(t.flatten(0, 5).is_err());
10182 assert!(t.flatten(2, 1).is_err()); }
10184
10185 #[test]
10186 fn split_heads_then_merge_heads_round_trip() {
10187 let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10189 let split = x.split_heads(2, 3).unwrap();
10190 assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10191 let merged = split.merge_heads().unwrap();
10192 assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10193 let m = merged.realize_f32();
10194 let original = x.realize_f32();
10195 for (a, b) in m.iter().zip(original.iter()) {
10196 assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10197 }
10198 }
10199
10200 #[test]
10201 fn rope_tables_const_shapes_and_anchoring() {
10202 let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10203 let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10204 assert_eq!(cos.shape().dims(), &[4, 8]);
10205 assert_eq!(sin.shape().dims(), &[4, 8]);
10206 let cv = cos.realize_f32();
10208 let sv = sin.realize_f32();
10209 for i in 0..8 {
10211 assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10212 assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10213 }
10214 }
10215
10216 #[test]
10217 fn embed_tokens_shape_and_lookup() {
10218 let vocab_size = 5;
10221 let hidden = 3;
10222 let table: Vec<f32> = (0..vocab_size)
10223 .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10224 .collect();
10225 let tokens = vec![1_u32, 3, 0];
10226 let out = Tensor::embed_tokens(
10227 std::sync::Arc::from(table),
10228 vocab_size,
10229 hidden,
10230 &tokens,
10231 &crate::Device::cpu(),
10232 )
10233 .unwrap();
10234 assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10235 let v = out.realize_f32();
10236 let want = [
10237 1.0_f32, 1.5, 2.0, 3.0, 3.5, 4.0, 0.0, 0.5, 1.0, ];
10241 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10242 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10243 }
10244 }
10245
10246 #[test]
10247 fn embed_tokens_anchored_lives_on_receiver_graph() {
10248 let vocab_size = 4;
10252 let hidden = 2;
10253 let table: Vec<f32> = (0..vocab_size)
10254 .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10255 .collect();
10256 let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10257 let tokens = vec![2_u32, 1];
10258
10259 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10260 let embedded = anchor
10261 .embed_tokens_anchored(
10262 std::sync::Arc::clone(&table_arc),
10263 vocab_size,
10264 hidden,
10265 &tokens,
10266 )
10267 .unwrap();
10268 assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10269
10270 let one_scaled = anchor
10272 .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10273 .unwrap();
10274 let _ = embedded
10275 .add(
10276 &one_scaled
10277 .reshape(Shape::from_dims(&[1, 1, 1]))
10278 .unwrap()
10279 .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10280 .unwrap(),
10281 )
10282 .unwrap();
10283 let v = embedded.realize_f32();
10284 let want = [
10285 2.0_f32, 4.0, 1.0, 2.0, ];
10288 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10289 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10290 }
10291 }
10292
10293 #[test]
10294 fn embed_tokens_empty_returns_error() {
10295 let r = Tensor::embed_tokens(
10296 std::sync::Arc::from(vec![0.0_f32]),
10297 1,
10298 1,
10299 &[],
10300 &crate::Device::cpu(),
10301 );
10302 assert!(r.is_err());
10303 }
10304
10305 #[test]
10306 fn softcap_matches_tanh_form() {
10307 let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10309 let capped = x.softcap(10.0).realize_f32();
10310 let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10312 for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10313 assert!(
10314 (got - want).abs() < 1e-3,
10315 "softcap[{i}] got={got} want={want}"
10316 );
10317 }
10318 }
10319
10320 #[test]
10321 fn softcap_optional_none_returns_input_unchanged() {
10322 let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10323 let out = x.softcap_optional(None).realize_f32();
10324 let expect = [1.0_f32, -2.0, 30.0];
10325 for (g, w) in out.iter().zip(expect.iter()) {
10326 assert!((g - w).abs() < 1e-6);
10327 }
10328 let out = x.softcap_optional(Some(0.0)).realize_f32();
10330 for (g, w) in out.iter().zip(expect.iter()) {
10331 assert!((g - w).abs() < 1e-6);
10332 }
10333 }
10334
10335 #[test]
10336 fn rope_partial_full_dim_matches_rope_with_tables() {
10337 let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10339 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10340 let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10341 let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10342 let a = via_partial.realize_f32();
10343 let b = via_full.realize_f32();
10344 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10345 assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10346 }
10347 }
10348
10349 #[test]
10350 fn rope_partial_pass_through_suffix_unchanged() {
10351 let qk = cpu_f32(
10355 vec![
10356 1.0_f32, 2.0, 3.0, 4.0,
10358 ],
10359 &[1, 1, 1, 4],
10360 );
10361 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10362 let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10363 for (i, &v) in out.iter().enumerate() {
10364 let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10365 assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10366 }
10367 }
10368
10369 #[test]
10370 fn add_optional_trailing_bias_none_returns_input_unchanged() {
10371 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10372 let original = a.realize_f32();
10373 let out = a.add_optional_trailing_bias(None).unwrap();
10374 assert_eq!(out.realize_f32(), original);
10375 }
10376
10377 #[test]
10378 fn add_optional_trailing_bias_some_applies_add() {
10379 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10380 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10381 let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10382 assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10383 }
10384
10385 #[test]
10386 fn add_trailing_bias_broadcasts_across_leading_dims() {
10387 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10389 let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10390 let out = x.add_trailing_bias(bias).unwrap();
10391 assert_eq!(out.shape().dims(), &[2, 3]);
10392 let v = out.realize_f32();
10393 assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10394 }
10395
10396 #[test]
10397 fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10398 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10399 let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10400 let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10401 let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10402 let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10403 let a = via_offset.realize_f32();
10404 let b = via_plain.realize_f32();
10405 for (x, y) in a.iter().zip(b.iter()) {
10406 assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10407 }
10408 }
10409
10410 #[test]
10411 fn global_avg_pool_2d_averages_spatial_dims() {
10412 let x = cpu_f32(
10416 vec![
10417 1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0,
10418 ],
10419 &[1, 2, 2, 3],
10420 );
10421 let out = x.global_avg_pool_2d().unwrap();
10422 assert_eq!(out.shape().dims(), &[1, 2]);
10423 let v = out.realize_f32();
10424 assert!((v[0] - 3.5).abs() < 1e-5);
10425 assert!((v[1] - 35.0).abs() < 1e-4);
10426 }
10427
10428 #[test]
10429 fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10430 let x = cpu_f32(
10432 vec![
10433 1.0_f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0, ],
10436 &[1, 2, 2, 2],
10437 );
10438 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10439 let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10440 let out = x.channel_affine_4d(gain, bias).unwrap();
10441 let v = out.realize_f32();
10442 assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10444 assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10446 }
10447
10448 #[test]
10449 fn additive_causal_mask_has_strict_lower_triangle() {
10450 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10451 let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10452 assert_eq!(mask.shape().dims(), &[4, 4]);
10453 let v = mask.realize_f32();
10454 for i in 0..4 {
10460 for j in 0..4 {
10461 let got = v[i * 4 + j];
10462 if j > i {
10463 assert!(
10464 got.is_infinite() && got.is_sign_negative(),
10465 "above-diag (i={i}, j={j}) should be -inf, got {got}"
10466 );
10467 } else {
10468 assert_eq!(
10469 got, 0.0,
10470 "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10471 );
10472 }
10473 }
10474 }
10475 }
10476
10477 #[test]
10478 fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10479 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10480 let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10481 let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10482 let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10483 let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10484 let va = out_affine.realize_f32();
10485 let vp = out_plain.realize_f32();
10486 for (a, b) in va.iter().zip(vp.iter()) {
10487 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10488 }
10489 }
10490
10491 #[test]
10492 fn layer_norm_affine_applies_gain_and_bias() {
10493 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10494 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10495 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10496 let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10497 let v = out.realize_f32();
10498 let mean = 2.0_f32;
10500 let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10501 let den = (var + 1e-5_f32).sqrt();
10502 let expected = [
10503 ((1.0 - mean) / den) * 2.0 + 10.0,
10504 ((2.0 - mean) / den) * 0.5 + (-5.0),
10505 ((3.0 - mean) / den) * 1.5 + 0.0,
10506 ];
10507 for (got, want) in v.iter().zip(expected.iter()) {
10508 assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10509 }
10510 }
10511
10512 #[test]
10513 fn l2_normalize_last_dim_unit_norm_per_row() {
10514 let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10516 let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10517 assert_eq!(out.shape().dims(), &[2, 3]);
10518 let v = out.realize_f32();
10519 let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10520 let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10521 assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10522 assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10523 assert!((v[0] - 0.6).abs() < 1e-5);
10525 assert!((v[1] - 0.8).abs() < 1e-5);
10526 assert!(v[2].abs() < 1e-5);
10527 }
10528
10529 #[test]
10530 fn l2_normalize_eps_zero_works_when_nonzero() {
10531 let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10532 let out = a.l2_normalize(0_usize, 0.0).unwrap();
10533 let v = out.realize_f32();
10534 assert!((v[0] - 1.0).abs() < 1e-6);
10535 assert!(v[1].abs() < 1e-6);
10536 }
10537
10538 #[test]
10539 fn repeat_interleave_last_dim_matches_torch_semantics() {
10540 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10544 let out = a.repeat_interleave(1_usize, 2).unwrap();
10545 assert_eq!(out.shape().dims(), &[2, 6]);
10546 assert_eq!(
10547 out.realize_f32(),
10548 vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0]
10549 );
10550 }
10551
10552 #[test]
10553 fn repeat_interleave_middle_dim() {
10554 let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10556 let out = a.repeat_interleave(1_usize, 3).unwrap();
10557 assert_eq!(out.shape().dims(), &[2, 6, 2]);
10558 let v = out.realize_f32();
10560 assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10561 assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10562 }
10563
10564 #[test]
10565 fn repeat_interleave_repeats_one_is_noop() {
10566 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10567 let out = a.repeat_interleave(0_usize, 1).unwrap();
10568 assert_eq!(out.shape().dims(), &[3]);
10569 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10570 }
10571
10572 #[test]
10573 fn repeat_interleave_repeats_zero_errors() {
10574 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10575 assert!(a.repeat_interleave(0_usize, 0).is_err());
10576 }
10577
10578 #[test]
10579 fn stack_adds_leading_dim() {
10580 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10581 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10582 let out = Tensor::stack(&[&a, &b], 0).unwrap();
10583 assert_eq!(out.shape().dims(), &[2, 3]);
10584 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10585 }
10586
10587 #[test]
10588 fn stack_adds_trailing_dim() {
10589 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10590 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10591 let out = Tensor::stack(&[&a, &b], 1).unwrap();
10592 assert_eq!(out.shape().dims(), &[3, 2]);
10593 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10594 }
10595
10596 #[test]
10597 fn stack_rejects_mismatched_shapes() {
10598 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10599 let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10600 assert!(Tensor::stack(&[&a, &b], 0).is_err());
10601 }
10602
10603 #[test]
10604 fn stack_rejects_empty_input() {
10605 let result = Tensor::stack(&[], 0);
10606 assert!(result.is_err());
10607 }
10608
10609 #[test]
10610 fn repeat_tiles_along_each_dim() {
10611 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10612 let out = t.repeat(vec![3]).unwrap();
10613 assert_eq!(out.shape().dims(), &[6]);
10614 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10615 }
10616
10617 #[test]
10618 fn repeat_extends_rank_when_needed() {
10619 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10620 let out = t.repeat(vec![3, 2]).unwrap();
10622 assert_eq!(out.shape().dims(), &[3, 4]);
10623 }
10624
10625 #[test]
10626 fn repeat_identity_with_all_ones() {
10627 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10628 let out = t.repeat(vec![1]).unwrap();
10629 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10630 }
10631}
10632
10633#[cfg(test)]
10637mod phase_a3_keepdim_tests {
10638 use super::*;
10639
10640 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10641 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10642 }
10643
10644 #[test]
10645 fn sum_keepdim_preserves_dim_as_one() {
10646 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10647 let out = t.sum_keepdim(1_usize).unwrap();
10648 assert_eq!(out.shape().dims(), &[2, 1]);
10649 assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10650 }
10651
10652 #[test]
10653 fn mean_keepdim_preserves_dim_as_one() {
10654 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10655 let out = t.mean_keepdim(0_usize).unwrap();
10656 assert_eq!(out.shape().dims(), &[1, 2]);
10657 assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10658 }
10659
10660 #[test]
10661 fn max_keepdim_preserves_dim_as_one() {
10662 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10663 let out = t.max_keepdim(1_usize).unwrap();
10664 assert_eq!(out.shape().dims(), &[2, 1]);
10665 assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10666 }
10667
10668 #[test]
10669 fn min_keepdim_preserves_dim_as_one() {
10670 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10671 let out = t.min_keepdim(1_usize).unwrap();
10672 assert_eq!(out.shape().dims(), &[2, 1]);
10673 assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10674 }
10675
10676 #[test]
10677 fn var_matches_unbiased_formula_with_correction_1() {
10678 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10680 let out = t.var(1, 1.0).unwrap();
10681 assert_eq!(out.shape().dims(), &[2]);
10682 let v = out.realize_f32();
10683 assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10684 assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10685 }
10686
10687 #[test]
10692 fn var_population_with_correction_0() {
10693 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10695 let out = t.var(1, 0.0).unwrap();
10696 let v = out.realize_f32();
10697 let want = 2.0 / 3.0;
10698 assert!(
10699 (v[0] - want).abs() < 1e-5,
10700 "population var row 0 = {} != {want}",
10701 v[0]
10702 );
10703 assert!(
10704 (v[1] - want).abs() < 1e-5,
10705 "population var row 1 = {} != {want}",
10706 v[1]
10707 );
10708 }
10709
10710 #[test]
10711 fn var_keepdim_preserves_dim() {
10712 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10713 let out = t.var_keepdim(1, 1.0).unwrap();
10714 assert_eq!(out.shape().dims(), &[2, 1]);
10715 }
10716
10717 #[test]
10718 fn var_errors_out_of_bounds() {
10719 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10720 assert!(t.var(3, 1.0).is_err());
10721 assert!(t.var_keepdim(3, 1.0).is_err());
10722 }
10723}
10724
10725#[cfg(test)]
10729mod phase_a4_composite_tests {
10730 use super::*;
10731
10732 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10733 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10734 }
10735
10736 #[test]
10737 fn affine_applies_mul_then_add() {
10738 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10739 let out = t.affine(2.0, 10.0);
10740 assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10741 }
10742
10743 #[test]
10744 fn scale_and_shift_alias_of_affine() {
10745 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10746 assert_eq!(
10747 t.scale_and_shift(2.0, 10.0).realize_f32(),
10748 t.affine(2.0, 10.0).realize_f32(),
10749 );
10750 }
10751
10752 #[test]
10753 fn elu_matches_reference_values() {
10754 let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10755 let out = t.elu(1.0);
10756 let v = out.realize_f32();
10757 assert!((v[0] - 1.0).abs() < 1e-5);
10760 assert!(v[1].abs() < 1e-5);
10761 assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10762 assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10763 }
10764
10765 #[test]
10766 fn dot_of_rank_one_vectors() {
10767 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10768 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10769 let out = a.dot(&b).unwrap();
10770 assert_eq!(out.shape().elem_count(), 1);
10771 let v = out.realize_f32();
10772 assert_eq!(v[0], 32.0); }
10774
10775 #[test]
10776 fn dot_rejects_non_rank_one() {
10777 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10778 let b = a
10779 .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10780 .unwrap();
10781 assert!(a.dot(&b).is_err());
10782 }
10783
10784 #[test]
10785 fn dot_rejects_length_mismatch() {
10786 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10787 let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10788 assert!(a.dot(&b).is_err());
10789 }
10790
10791 #[test]
10792 fn mv_matrix_times_vector() {
10793 let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10794 let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10795 let out = m.mv(&v).unwrap();
10796 assert_eq!(out.shape().dims(), &[2]);
10797 assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10798 }
10799
10800 #[test]
10801 fn matvec_is_mv_alias() {
10802 let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10803 let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10804 let a = m.mv(&v).unwrap().realize_f32();
10805 let b = m.matvec(&v).unwrap().realize_f32();
10806 assert_eq!(a, b);
10807 }
10808
10809 #[test]
10810 fn mv_rejects_shape_mismatch() {
10811 let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10812 let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10813 assert!(m.mv(&v).is_err());
10814 }
10815
10816 #[test]
10817 fn broadcast_matmul_passes_through_to_matmul() {
10818 let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10819 let b = a
10820 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10821 .unwrap();
10822 let out = a.broadcast_matmul(&b).unwrap();
10823 assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10824 }
10825}
10826
10827#[cfg(test)]
10831mod phase_a5_factory_tests {
10832 use super::*;
10833
10834 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10835 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10836 }
10837
10838 #[test]
10839 fn ones_like_matches_shape_dtype_graph() {
10840 let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10841 let ones = t.ones_like().unwrap();
10842 assert_eq!(ones.shape().dims(), t.shape().dims());
10843 assert_eq!(ones.dtype(), t.dtype());
10844 assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10845 }
10846
10847 #[test]
10848 fn zeros_like_matches_shape_dtype_graph() {
10849 let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10850 let zeros = t.zeros_like().unwrap();
10851 assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10852 }
10853
10854 #[test]
10855 fn static_ones_f32() {
10856 let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10857 assert_eq!(t.shape().dims(), &[2, 3]);
10858 assert_eq!(t.realize_f32(), vec![1.0; 6]);
10859 }
10860
10861 #[test]
10862 fn static_zeros_f64() {
10863 let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10864 assert_eq!(t.dtype(), DType::F64);
10865 assert_eq!(t.realize_f64(), vec![0.0; 4]);
10866 }
10867
10868 #[test]
10869 fn full_with_f32_scalar() {
10870 let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10871 assert_eq!(t.realize_f32(), vec![2.5; 5]);
10872 }
10873
10874 #[test]
10875 fn eye_identity_matrix() {
10876 let t = Tensor::eye(3, DType::F32, &Device::cpu());
10877 assert_eq!(t.shape().dims(), &[3, 3]);
10878 assert_eq!(
10879 t.realize_f32(),
10880 vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10881 );
10882 }
10883
10884 #[test]
10885 fn tril2_lower_triangular_ones() {
10886 let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10887 assert_eq!(
10888 t.realize_f32(),
10889 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10890 );
10891 }
10892
10893 #[test]
10894 fn triu2_upper_triangular_ones() {
10895 let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10896 assert_eq!(
10897 t.realize_f32(),
10898 vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10899 );
10900 }
10901
10902 #[test]
10903 fn meshgrid_ij_indexing_two_inputs() {
10904 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10905 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10906 let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10907 assert_eq!(grids.len(), 2);
10908 assert_eq!(grids[0].shape().dims(), &[3, 2]);
10910 assert_eq!(grids[1].shape().dims(), &[3, 2]);
10911 assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10913 assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10915 }
10916
10917 #[test]
10918 fn meshgrid_xy_indexing_swaps_first_two() {
10919 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10920 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10921 let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10922 assert_eq!(grids[0].shape().dims(), &[2, 3]);
10924 assert_eq!(grids[1].shape().dims(), &[2, 3]);
10925 assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10927 assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10929 }
10930
10931 #[test]
10932 fn meshgrid_rejects_single_input() {
10933 let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
10934 assert!(Tensor::meshgrid(&[&x], false).is_err());
10935 }
10936
10937 #[test]
10938 fn meshgrid_rejects_non_rank_one() {
10939 let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
10940 let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
10941 assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
10942 }
10943
10944 #[test]
10947 fn narrow_is_slice_alias() {
10948 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
10949 let a = t.narrow(0, 1, 3).unwrap().realize_f32();
10950 let b = t.slice(0, 1, 3).unwrap().realize_f32();
10951 assert_eq!(a, b);
10952 assert_eq!(a, vec![2.0, 3.0, 4.0]);
10953 }
10954
10955 #[test]
10956 fn chunk_splits_evenly() {
10957 let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
10958 let parts = t.chunk(3, 0).unwrap();
10959 assert_eq!(parts.len(), 3);
10960 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
10961 assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
10962 assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
10963 }
10964
10965 #[test]
10966 fn chunk_distributes_remainder_to_leading() {
10967 let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
10969 let parts = t.chunk(3, 0).unwrap();
10970 assert_eq!(parts.len(), 3);
10971 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
10972 assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
10973 assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
10974 }
10975
10976 #[test]
10977 fn chunk_returns_singletons_when_size_less_than_chunks() {
10978 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10979 let parts = t.chunk(5, 0).unwrap();
10980 assert_eq!(parts.len(), 2);
10981 assert_eq!(parts[0].realize_f32(), vec![1.0]);
10982 assert_eq!(parts[1].realize_f32(), vec![2.0]);
10983 }
10984
10985 #[test]
10986 fn get_at_first_dim() {
10987 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
10988 let row1 = t.get(1).unwrap();
10989 assert_eq!(row1.shape().dims(), &[2]);
10990 assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
10991 }
10992
10993 #[test]
10994 fn get_on_dim_arbitrary_axis() {
10995 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
10996 let col0 = t.get_on_dim(1, 0).unwrap();
10997 assert_eq!(col0.shape().dims(), &[3]);
10998 assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
10999 }
11000
11001 #[test]
11002 fn sum_dims_multi_dim_reduces_to_smaller() {
11003 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11005 let s = t.sum_dims([0, 2_usize]).unwrap();
11006 assert_eq!(s.shape().dims(), &[3]);
11007 assert_eq!(s.realize_f32(), vec![8.0; 3]);
11009 }
11010
11011 #[test]
11012 fn mean_dims_multi_dim() {
11013 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11014 let m = t.mean_dims([0, 1_usize]).unwrap();
11015 assert_eq!(m.shape().dims(), &[] as &[usize]);
11016 assert_eq!(m.realize_f32(), vec![2.5]);
11017 }
11018
11019 #[test]
11020 fn sum_dims_keepdim_preserves_rank() {
11021 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11022 let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11023 assert_eq!(s.shape().dims(), &[1, 3, 1]);
11024 }
11025
11026 #[test]
11027 fn mean_dims_keepdim_preserves_rank() {
11028 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11029 let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11030 assert_eq!(m.shape().dims(), &[1, 1]);
11031 assert_eq!(m.realize_f32(), vec![2.5]);
11032 }
11033
11034 #[test]
11035 fn rand_like_matches_shape_dtype() {
11036 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11037 let r = t.rand_like(-1.0, 1.0).unwrap();
11038 assert_eq!(r.shape().dims(), t.shape().dims());
11039 assert_eq!(r.dtype(), t.dtype());
11040 for v in r.realize_f32() {
11042 assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11043 }
11044 }
11045
11046 #[test]
11047 fn randn_like_matches_shape_dtype() {
11048 let t = cpu_f32(vec![0.0; 4], &[4]);
11049 let r = t.randn_like(0.0, 1.0).unwrap();
11050 assert_eq!(r.shape().dims(), &[4]);
11051 assert_eq!(r.dtype(), DType::F32);
11052 for v in r.realize_f32() {
11054 assert!(v.is_finite());
11055 }
11056 }
11057
11058 #[test]
11059 fn static_rand_f32() {
11060 let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11061 let v = t.realize_f32();
11062 let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11064 assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11065 }
11066
11067 #[test]
11068 fn static_randn_f64() {
11069 let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11070 let v = t.realize_f64();
11071 let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11072 assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11074 }
11075
11076 #[test]
11077 fn arange_int_step() {
11078 let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11079 assert_eq!(t.shape().dims(), &[5]);
11080 assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11081 }
11082
11083 #[test]
11084 fn arange_step_fractional() {
11085 let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11086 assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11087 }
11088
11089 #[test]
11090 fn arange_step_negative_descends() {
11091 let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11092 assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11093 }
11094
11095 #[test]
11096 fn linspace_includes_endpoints() {
11097 let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11098 assert_eq!(t.shape().dims(), &[5]);
11099 let v = t.realize_f32();
11100 assert!((v[0] - 0.0).abs() < 1e-6);
11101 assert!((v[4] - 1.0).abs() < 1e-6);
11102 assert!((v[2] - 0.5).abs() < 1e-6);
11103 }
11104
11105 #[test]
11106 fn linspace_n_one_returns_start() {
11107 let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11108 assert_eq!(t.realize_f32(), vec![7.0]);
11109 }
11110
11111 #[test]
11112 fn norm_is_sqrt_sum_sq() {
11113 let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11114 let n = t.norm();
11115 assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11116 }
11117
11118 #[test]
11119 fn pad_with_zeros_left_and_right() {
11120 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11121 let p = t.pad_with_zeros(0, 2, 1).unwrap();
11122 assert_eq!(p.shape().dims(), &[6]);
11123 assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11124 }
11125
11126 #[test]
11127 fn pad_with_zeros_identity_when_both_zero() {
11128 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11129 let p = t.pad_with_zeros(0, 0, 0).unwrap();
11130 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11131 }
11132
11133 #[test]
11134 fn pad_with_zeros_rejects_bad_dim() {
11135 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11136 assert!(t.pad_with_zeros(3, 1, 1).is_err());
11137 }
11138
11139 #[test]
11140 fn pad_with_value_zero_matches_pad_with_zeros() {
11141 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11144 let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11145 let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11146 assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11147 assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11148 assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11149 assert_eq!(
11150 via_value.realize_f32(),
11151 vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11152 );
11153 }
11154
11155 #[test]
11156 fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11157 let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11161 let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11162 assert_eq!(padded.shape().dims(), &[7]);
11163 let v = padded.realize_f32();
11164 assert!(v[0].is_infinite() && v[0].is_sign_negative());
11166 assert!(v[1].is_infinite() && v[1].is_sign_negative());
11167 assert_eq!(v[2], -1.0);
11168 assert_eq!(v[3], -2.0);
11169 assert_eq!(v[4], -3.0);
11170 assert!(v[5].is_infinite() && v[5].is_sign_negative());
11171 assert!(v[6].is_infinite() && v[6].is_sign_negative());
11172 let m = padded.max_all().realize_f32();
11174 assert_eq!(m, vec![-1.0]);
11175 }
11176
11177 #[test]
11178 fn pad_with_value_identity_when_both_zero() {
11179 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11182 let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11183 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11184 }
11185
11186 #[test]
11187 fn pad_with_value_rejects_bad_dim() {
11188 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11189 assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11190 }
11191
11192 #[test]
11193 fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11194 let x = cpu_f32(
11205 vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11206 &[1, 1, 3, 3],
11207 );
11208 let out = x
11209 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11210 .unwrap();
11211 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11212 let v = out.realize_f32();
11213 assert_eq!(v[0], -1.0);
11216 assert_eq!(v[4], -1.0);
11218 assert_eq!(v[8], -5.0);
11221
11222 let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11232 let vd = default_pad.realize_f32();
11233 assert_eq!(
11234 vd[0], -1.0,
11235 "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11236 );
11237 assert_eq!(vd[8], -5.0);
11238
11239 let explicit_zero_pad = x
11240 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11241 .unwrap();
11242 let vz = explicit_zero_pad.realize_f32();
11243 assert_eq!(
11244 vz[0], 0.0,
11245 "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11246 );
11247 assert_eq!(vz[8], 0.0);
11248 }
11249
11250 #[test]
11251 fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11252 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11255 let x = cpu_f32(data, &[1, 1, 4, 4]);
11256 let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11257 let b = x
11258 .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11259 .unwrap();
11260 assert_eq!(a.shape().dims(), b.shape().dims());
11261 assert_eq!(a.realize_f32(), b.realize_f32());
11262 }
11263
11264 #[test]
11267 fn conv1d_identity_kernel_passes_input_through() {
11268 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11270 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11271 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11272 assert_eq!(out.shape().dims(), &[1, 1, 5]);
11273 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11274 }
11275
11276 #[test]
11277 fn conv1d_sum_kernel_two_wide() {
11278 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11280 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11281 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11282 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11283 assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11284 }
11285
11286 #[test]
11287 fn conv1d_with_bias_applies_correctly() {
11288 let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11289 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11290 let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11291 let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11292 assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11293 }
11294
11295 #[test]
11296 fn conv1d_stride_two_halves_output() {
11297 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11299 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11300 let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11301 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11302 assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11303 }
11304
11305 #[test]
11306 fn conv1d_padding_one_preserves_length() {
11307 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11309 let w = x
11310 .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11311 .unwrap();
11312 let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11313 assert_eq!(out.shape().dims(), &[1, 1, 4]);
11314 assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11317 }
11318
11319 #[test]
11320 fn conv1d_multi_channel_output() {
11321 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11323 let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11325 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11326 assert_eq!(out.shape().dims(), &[1, 2, 3]);
11327 assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11329 }
11330
11331 #[test]
11332 fn conv1d_rejects_rank_two_input() {
11333 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11334 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11335 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11336 }
11337
11338 #[test]
11339 fn conv1d_rejects_rank_two_weight() {
11340 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11341 let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11342 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11343 }
11344
11345 #[test]
11346 fn conv1d_rejects_zero_groups_or_stride() {
11347 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11348 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11349 assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11350 assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11351 }
11352
11353 #[test]
11354 fn conv1d_with_algo_ignores_algo_param() {
11355 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11356 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11357 let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11359 let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11360 assert_eq!(a.realize_f32(), b.realize_f32());
11361 }
11362
11363 #[test]
11366 fn avg_pool2d_2x2_stride2() {
11367 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11369 let x = cpu_f32(data, &[1, 1, 4, 4]);
11370 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11371 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11372 let v = out.realize_f32();
11376 assert!((v[0] - 2.5).abs() < 1e-5);
11377 assert!((v[1] - 4.5).abs() < 1e-5);
11378 assert!((v[2] - 10.5).abs() < 1e-5);
11379 assert!((v[3] - 12.5).abs() < 1e-5);
11380 }
11381
11382 #[test]
11383 fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11384 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11385 let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11386 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11387 }
11388
11389 #[test]
11407 fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11408 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11409 let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11410 let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11411 assert_eq!(included.shape().dims(), excluded.shape().dims());
11412
11413 let ve = excluded.realize_f32();
11414 for (i, &v) in ve.iter().enumerate() {
11415 assert!(
11416 (v - 1.0).abs() < 1e-5,
11417 "count_include_pad=false must be exactly 1.0 at every position \
11418 (all real taps are 1.0) — position {i} got {v}",
11419 );
11420 }
11421
11422 let vi = included.realize_f32();
11423 assert!(
11424 (vi[0] - 1.0).abs() > 0.1,
11425 "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11426 real → 4/9, far from 1.0) — got {}, modes should disagree here",
11427 vi[0]
11428 );
11429 }
11430
11431 #[test]
11440 fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11441 let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11442 let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11443 let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11444 let vi = included.realize_f32();
11445 let ve = excluded.realize_f32();
11446 assert!(
11447 (vi[0] - 2.5).abs() < 1e-5,
11448 "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11449 vi[0]
11450 );
11451 assert!(
11452 (ve[0] - 10.0).abs() < 1e-5,
11453 "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11454 ve[0]
11455 );
11456 }
11457
11458 #[test]
11463 fn avg_pool2d_rejects_zero_valid_tap_window() {
11464 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11465 let err = x
11466 .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11467 .expect_err("a window with zero real taps must be rejected, not silently NaN");
11468 let msg = err.to_string();
11469 assert!(
11470 msg.contains("divide by zero") || msg.contains("no valid"),
11471 "error should explain the zero-valid-tap cause, got: {msg}"
11472 );
11473
11474 let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11479 assert!(
11480 ok.is_ok(),
11481 "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11482 );
11483 }
11484
11485 #[test]
11486 fn avg_pool2d_multi_channel() {
11487 let x = cpu_f32(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], &[1, 2, 2, 2]);
11489 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11490 assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11491 assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11492 }
11493
11494 #[test]
11495 fn max_pool2d_2x2_stride2() {
11496 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11497 let x = cpu_f32(data, &[1, 1, 4, 4]);
11498 let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11499 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11500 assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11502 }
11503
11504 #[test]
11505 fn max_pool2d_3x3_stride1_padding1() {
11506 let x = cpu_f32(
11507 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11508 &[1, 1, 3, 3],
11509 );
11510 let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11511 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11512 let v = out.realize_f32();
11514 assert!((v[4] - 9.0).abs() < 1e-5);
11516 }
11517
11518 #[test]
11519 fn upsample_nearest2d_2x() {
11520 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11521 let out = x.upsample_nearest2d(2).unwrap();
11522 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11523 assert_eq!(
11525 out.realize_f32(),
11526 vec![
11527 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 3.0, 3.0, 4.0, 4.0,
11528 ],
11529 );
11530 }
11531
11532 #[test]
11533 fn upsample_nearest2d_3x() {
11534 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11535 let out = x.upsample_nearest2d(3).unwrap();
11536 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11537 assert_eq!(out.realize_f32(), vec![5.0; 9]);
11538 }
11539
11540 #[test]
11541 fn upsample_nearest2d_identity_scale_one() {
11542 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11543 let out = x.upsample_nearest2d(1).unwrap();
11544 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11545 }
11546
11547 #[test]
11548 fn upsample_nearest1d_2x() {
11549 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11550 let out = x.upsample_nearest1d(2).unwrap();
11551 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11552 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11553 }
11554
11555 #[test]
11556 fn interpolate2d_integer_multiple() {
11557 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11558 let out = x.interpolate2d(4, 4).unwrap();
11559 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11560 }
11561
11562 #[test]
11563 fn interpolate2d_accepts_non_integer_ratio() {
11564 let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11569 let out = x.interpolate2d(3, 4).unwrap();
11570 assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11571 }
11572
11573 #[test]
11574 fn interpolate1d_integer_multiple() {
11575 let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11576 let out = x.interpolate1d(6).unwrap();
11577 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11578 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11579 }
11580
11581 #[test]
11582 fn pool_rejects_bad_rank() {
11583 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11584 assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11585 assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11586 }
11587
11588 #[test]
11589 fn pool_rejects_zero_kernel() {
11590 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11591 assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11592 assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11593 }
11594
11595 #[test]
11598 fn numel_matches_elem_count() {
11599 let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11600 assert_eq!(t.numel(), t.elem_count());
11601 assert_eq!(t.numel(), 12);
11602 }
11603
11604 #[test]
11605 fn dim_returns_specific_axis_size() {
11606 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11607 assert_eq!(t.dim(0).unwrap(), 2);
11608 assert_eq!(t.dim(1).unwrap(), 3);
11609 assert_eq!(t.dim(2).unwrap(), 4);
11610 assert!(t.dim(3).is_err());
11611 }
11612
11613 #[test]
11614 fn to_dtype_switches_dtype() {
11615 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11616 let b = t.to_dtype(DType::F64).unwrap();
11617 assert_eq!(b.dtype(), DType::F64);
11618 assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11619 }
11620
11621 #[test]
11622 fn to_dtype_same_dtype_is_noop() {
11623 let t = cpu_f32(vec![1.0_f32], &[1]);
11624 let b = t.to_dtype(DType::F32).unwrap();
11625 assert_eq!(b.dtype(), DType::F32);
11626 }
11627
11628 #[test]
11629 fn detach_is_identity_on_lazy() {
11630 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11631 let d = t.detach();
11632 assert_eq!(d.realize_f32(), t.realize_f32());
11633 }
11634
11635 #[test]
11636 fn track_op_is_true_on_lazy() {
11637 let t = cpu_f32(vec![0.0], &[1]);
11638 assert!(t.track_op());
11639 }
11640
11641 #[test]
11644 fn try_permute_accepts_tuple_syntax() {
11645 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11646 let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11648 assert_eq!(out.shape().dims(), &[4, 2, 3]);
11649 }
11650
11651 #[test]
11652 fn dim_arg_methods_accept_negative_indexing() {
11653 use fuel_ir::D;
11654 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11655 let sum_last = t.shape().dims().to_vec(); assert_eq!(sum_last, vec![2, 3]);
11658 let cumsum_last = t.cumsum(D::Minus1).unwrap();
11661 assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11662 }
11663
11664 #[test]
11665 fn unsqueeze_accepts_dim_trait() {
11666 use fuel_ir::D;
11667 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11668 let out = t.unsqueeze(D::Minus1).unwrap();
11670 assert_eq!(out.shape().dims().len(), 2);
11673 }
11674}
11675pub struct TokenDataHost {
11676 pub token_ids: fuel_ir::HostBuffer,
11677 pub rope_cos: fuel_ir::HostBuffer,
11678 pub rope_sin: fuel_ir::HostBuffer,
11679 pub mask: fuel_ir::HostBuffer,
11680 pub offset: Option<fuel_ir::HostBuffer>,
11681}
11682
11683#[cfg(feature = "cuda")]
11688pub struct TokenDataBytes {
11689 pub token_ids: Vec<u8>,
11690 pub rope_cos: Vec<u8>,
11691 pub rope_sin: Vec<u8>,
11692 pub mask: Vec<u8>,
11693 pub offset: Option<Vec<u8>>,
11694}
11695
11696#[cfg(feature = "cuda")]
11709pub fn captured_output_to_f32(
11710 output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11711) -> fuel_ir::error::Result<Vec<f32>> {
11712 use fuel_memory::BackendStorage;
11713 let guard = output.read().map_err(|_| {
11714 fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11715 })?;
11716 let bytes: Vec<u8> = match &guard.inner {
11717 BackendStorage::Cpu(c) => c.bytes().to_vec(),
11718 BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11719 #[allow(unreachable_patterns)]
11720 other => {
11721 return Err(fuel_ir::Error::Msg(format!(
11722 "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11723 std::mem::discriminant(other),
11724 ))
11725 .bt());
11726 }
11727 };
11728 Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11729}