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 x_vals: Vec<f32> = (0..64).map(|k| ((k * 7) % 13) as f32 * 0.1 - 0.6).collect();
3622 let (g, fd) = grad_vs_fd_lowered(&x_vals, &[1, 64], |act| {
3623 let words: Vec<u32> = (0..64u32)
3624 .map(|i| i.wrapping_mul(0x9E37_79B1) ^ 0x1234_5678)
3625 .collect();
3626 let weight = act
3627 .graph_tensor()
3628 .const_u32_like(words, Shape::from_dims(&[8, 8]))
3629 .unwrap();
3630 let zeros = act
3631 .graph_tensor()
3632 .const_u32_like(vec![0x7777_7777; 16], Shape::from_dims(&[8, 2]))
3633 .unwrap();
3634 let scales: Vec<half::f16> = (0..128)
3635 .map(|i| half::f16::from_f32(0.05 + 0.01 * (i % 7) as f32))
3636 .collect();
3637 let scales = act
3638 .const_f16_like(scales, Shape::from_dims(&[8, 16]))
3639 .unwrap();
3640 Tensor::from_graph_tensor(act.graph_tensor().awq_matmul(
3641 &weight,
3642 &zeros,
3643 scales.graph_tensor(),
3644 64,
3645 8,
3646 ))
3647 });
3648 assert_grad_close("awq_matmul d/dact", &g, &fd);
3649 }
3650
3651 #[test]
3652 fn causal_conv1d_weight_and_bias_gradients_match_finite_difference() {
3653 let x_data = vec![0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7];
3654 for use_silu in [false, true] {
3655 let (g, fd) = grad_vs_fd(&[0.5, -0.25, 0.75, 0.1, 0.9, -0.4], &[2, 1, 3], |w| {
3657 let x = w
3658 .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3659 .unwrap();
3660 let b = w
3661 .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3662 .unwrap();
3663 x.causal_conv1d(w, &b, use_silu)
3664 });
3665 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dw"), &g, &fd);
3666 let (g, fd) = grad_vs_fd(&[0.05, -0.05], &[2], |b| {
3668 let x = b
3669 .const_f32_like(x_data.clone(), Shape::from_dims(&[1, 2, 4]))
3670 .unwrap();
3671 let w = b
3672 .const_f32_like(
3673 vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3674 Shape::from_dims(&[2, 1, 3]),
3675 )
3676 .unwrap();
3677 x.causal_conv1d(&w, b, use_silu)
3678 });
3679 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dbias"), &g, &fd);
3680 }
3681 }
3682
3683 #[test]
3684 fn causal_conv1d_input_gradient_matches_finite_difference() {
3685 for use_silu in [false, true] {
3687 let (g, fd) = grad_vs_fd(
3688 &[0.3, -0.5, 0.8, 0.1, -0.2, 0.6, 0.4, -0.7],
3689 &[1, 2, 4],
3690 |x| {
3691 let w = x
3692 .const_f32_like(
3693 vec![0.5, -0.25, 0.75, 0.1, 0.9, -0.4],
3694 Shape::from_dims(&[2, 1, 3]),
3695 )
3696 .unwrap();
3697 let b = x
3698 .const_f32_like(vec![0.05, -0.05], Shape::from_dims(&[2]))
3699 .unwrap();
3700 x.causal_conv1d(&w, &b, use_silu)
3701 },
3702 );
3703 assert_grad_close(&format!("causal_conv1d(silu={use_silu}) d/dx"), &g, &fd);
3704 }
3705 }
3706
3707 #[test]
3714 fn selective_scan_seqlen2_bptt_matches_finite_difference() {
3715 let dev = Device::cpu();
3716 let fwd = |u_vals: &[f32]| -> f32 {
3719 let u = Tensor::from_f32(u_vals.to_vec(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3720 let delta = u
3721 .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3722 .unwrap();
3723 let a = u
3724 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3725 .unwrap();
3726 let b = u
3727 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3728 .unwrap();
3729 let c = u
3730 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3731 .unwrap();
3732 u.selective_scan(&delta, &a, &b, &c, false)
3733 .realize_f32()
3734 .iter()
3735 .sum()
3736 };
3737 let u0 = vec![1.0f32, 2.0];
3738 let u = Tensor::from_f32(u0.clone(), Shape::from_dims(&[1, 2, 1]), &dev).unwrap();
3739 let delta = u
3740 .const_f32_like(vec![0.7f32, 0.7], Shape::from_dims(&[1, 2, 1]))
3741 .unwrap();
3742 let a = u
3743 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1, 1]))
3744 .unwrap();
3745 let b = u
3746 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3747 .unwrap();
3748 let c = u
3749 .const_f32_like(vec![1.0f32, 1.0], Shape::from_dims(&[1, 2, 1]))
3750 .unwrap();
3751 let y = u.selective_scan(&delta, &a, &b, &c, false);
3752 let grads = y.inner.backward(); let g_u_id = grads.get(u.graph_tensor()).expect("grad u").id();
3754 let g_u =
3755 crate::pipelined_bridge::realize_one_as::<f32>(&u.inner.graph().clone(), g_u_id, &dev)
3756 .expect("realize");
3757 let h = 1e-3f32;
3760 let mut up = u0.clone();
3761 up[0] += h;
3762 let mut um = u0.clone();
3763 um[0] -= h;
3764 let fd0 = (fwd(&up) - fwd(&um)) / (2.0 * h);
3765 assert!(
3766 (g_u[0] - fd0).abs() < 5e-2,
3767 "selective_scan seqlen2 dL/du_1: autograd {} vs FD {fd0}",
3768 g_u[0]
3769 );
3770 assert!(
3773 g_u[0].abs() > 0.5,
3774 "recurrent-gate grad must be non-trivial, got {g_u:?}"
3775 );
3776 }
3777
3778 #[test]
3779 fn ssd_chunk_scan_seqlen4_chunk2_bptt_matches_finite_difference() {
3780 let dev = Device::cpu();
3781 let fwd = |x_vals: &[f32]| -> f32 {
3785 let x =
3786 Tensor::from_f32(x_vals.to_vec(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3787 let dt = x
3788 .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3789 .unwrap();
3790 let a = x
3791 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3792 .unwrap();
3793 let b = x
3794 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3795 .unwrap();
3796 let c = x
3797 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3798 .unwrap();
3799 x.ssd_chunk_scan(&dt, &a, &b, &c, 2)
3800 .realize_f32()
3801 .iter()
3802 .sum()
3803 };
3804 let x0 = vec![1.0f32, 2.0, 3.0, 4.0];
3805 let x = Tensor::from_f32(x0.clone(), Shape::from_dims(&[1, 4, 1, 1]), &dev).unwrap();
3806 let dt = x
3807 .const_f32_like(vec![0.6f32, 0.6, 0.6, 0.6], Shape::from_dims(&[1, 4, 1]))
3808 .unwrap();
3809 let a = x
3810 .const_f32_like(vec![-0.5f32], Shape::from_dims(&[1]))
3811 .unwrap();
3812 let b = x
3813 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3814 .unwrap();
3815 let c = x
3816 .const_f32_like(vec![1.0f32, 1.0, 1.0, 1.0], Shape::from_dims(&[1, 4, 1, 1]))
3817 .unwrap();
3818 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, 2);
3819 let grads = y.inner.backward();
3820 let g_x_id = grads.get(x.graph_tensor()).expect("grad x").id();
3821 let g_x =
3822 crate::pipelined_bridge::realize_one_as::<f32>(&x.inner.graph().clone(), g_x_id, &dev)
3823 .expect("realize");
3824 let h = 1e-3f32;
3827 let mut xp = x0.clone();
3828 xp[0] += h;
3829 let mut xm = x0.clone();
3830 xm[0] -= h;
3831 let fd0 = (fwd(&xp) - fwd(&xm)) / (2.0 * h);
3832 assert!(
3833 (g_x[0] - fd0).abs() < 5e-2,
3834 "ssd_chunk_scan seqlen4 dL/dx_0: autograd {} vs FD {fd0}",
3835 g_x[0]
3836 );
3837 assert!(
3838 g_x[0].abs() > 0.5,
3839 "cross-chunk-gate grad must be non-trivial, got {g_x:?}"
3840 );
3841 }
3842
3843 fn find_scan_terminal(
3857 graph: &std::sync::Arc<std::sync::RwLock<fuel_graph::Graph>>,
3858 root: fuel_graph::NodeId,
3859 fused_id: fuel_graph::registry::FusedOpId,
3860 ) -> fuel_graph::NodeId {
3861 use fuel_graph::Op;
3862 let g = graph.read().unwrap();
3863 let mut stack = vec![root];
3864 let mut seen = std::collections::HashSet::new();
3865 let mut scan_id = None;
3866 while let Some(nid) = stack.pop() {
3867 if !seen.insert(nid) {
3868 continue;
3869 }
3870 let node = g.node(nid);
3871 assert!(
3872 !matches!(node.op, Op::Fused(fid, _) if fid == fused_id),
3873 "SSM op must lower to Op::Scan, not remain fused",
3874 );
3875 if matches!(node.op, Op::Scan { .. }) {
3876 scan_id = Some(nid);
3877 }
3878 for &inp in &node.inputs {
3879 stack.push(inp);
3880 }
3881 }
3882 scan_id.expect("an Op::Scan terminal must be present after lowering")
3883 }
3884
3885 fn scan_ys_to_fused_layout(ys: &[f32], seqlen: usize, batch: usize, tail: usize) -> Vec<f32> {
3898 assert_eq!(ys.len(), seqlen * batch * tail, "ys layout mismatch");
3899 let mut out = vec![0f32; ys.len()];
3900 for s in 0..seqlen {
3901 for bb in 0..batch {
3902 for t in 0..tail {
3903 out[(bb * seqlen + s) * tail + t] = ys[(s * batch + bb) * tail + t];
3904 }
3905 }
3906 }
3907 out
3908 }
3909
3910 #[test]
3926 fn selective_scan_multidim_unroll_matches_fused_kernel() {
3927 use fuel_graph::registry::FusedOps;
3928 let dev = Device::cpu();
3929 let (batch, seqlen, dim, dstate) = (2usize, 3usize, 2usize, 2usize);
3930 let u = Tensor::from_f32(
3932 vec![
3933 0.5, -0.3, 1.0, 0.2, -0.5, 0.7, 0.25, 0.9, -0.8, 0.4, 0.6, -0.1,
3934 ],
3935 Shape::from_dims(&[batch, seqlen, dim]),
3936 &dev,
3937 )
3938 .unwrap();
3939 let delta = u
3941 .const_f32_like(
3942 vec![
3943 0.1, 0.2, 0.3, 0.15, 0.25, 0.4, 0.35, 0.05, 0.2, 0.3, 0.12, 0.28,
3944 ],
3945 Shape::from_dims(&[batch, seqlen, dim]),
3946 )
3947 .unwrap();
3948 let a = u
3950 .const_f32_like(
3951 vec![-0.7, -0.3, -0.5, -0.9],
3952 Shape::from_dims(&[dim, dstate]),
3953 )
3954 .unwrap();
3955 let b = u
3957 .const_f32_like(
3958 vec![
3959 1.0, 0.5, 0.25, 0.75, 0.6, 0.2, 0.8, 0.4, 0.3, 0.9, 0.55, 0.15,
3960 ],
3961 Shape::from_dims(&[batch, seqlen, dstate]),
3962 )
3963 .unwrap();
3964 let c = u
3966 .const_f32_like(
3967 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],
3968 Shape::from_dims(&[batch, seqlen, dstate]),
3969 )
3970 .unwrap();
3971 let y = u.selective_scan(&delta, &a, &b, &c, false);
3972
3973 let fused = y.realize_f32();
3975 assert_eq!(fused.len(), batch * seqlen * dim);
3976
3977 let graph = y.inner.graph().clone();
3979 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
3980 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
3981 assert_eq!(roots.len(), 1);
3982 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
3983 let ys = {
3984 let mut g = graph.write().unwrap();
3985 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
3986 .expect("unroll")
3987 .0[0] };
3989 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
3990 .expect("realize selective_scan unroll oracle on CPU");
3991 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
3993 assert_eq!(oracle.len(), fused.len());
3994 let mut max_drift = 0f32;
3995 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
3996 let d = (o - f).abs();
3997 max_drift = max_drift.max(d);
3998 assert!(
4002 d < 1e-3,
4003 "selective_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4004 );
4005 }
4006 eprintln!("selective_scan multidim: max honest drift = {max_drift:e}");
4007 }
4008
4009 #[test]
4015 fn selective_scan_softplus_multitoken_unroll_matches_fused_kernel() {
4016 use fuel_graph::registry::FusedOps;
4017 let dev = Device::cpu();
4018 let (batch, seqlen, dim, dstate) = (1usize, 3usize, 2usize, 2usize);
4019 let u = Tensor::from_f32(
4020 vec![0.5, -0.3, 1.0, 0.2, -0.5, 0.7],
4021 Shape::from_dims(&[batch, seqlen, dim]),
4022 &dev,
4023 )
4024 .unwrap();
4025 let delta = u
4027 .const_f32_like(
4028 vec![-0.5, 0.2, 0.8, -1.0, 0.3, -0.2],
4029 Shape::from_dims(&[batch, seqlen, dim]),
4030 )
4031 .unwrap();
4032 let a = u
4033 .const_f32_like(
4034 vec![-0.7, -0.3, -0.5, -0.9],
4035 Shape::from_dims(&[dim, dstate]),
4036 )
4037 .unwrap();
4038 let b = u
4039 .const_f32_like(
4040 vec![1.0, 0.5, 0.25, 0.75, 0.6, 0.2],
4041 Shape::from_dims(&[batch, seqlen, dstate]),
4042 )
4043 .unwrap();
4044 let c = u
4045 .const_f32_like(
4046 vec![2.0, 1.0, 0.5, 1.5, 0.8, 0.3],
4047 Shape::from_dims(&[batch, seqlen, dstate]),
4048 )
4049 .unwrap();
4050 let y = u.selective_scan(&delta, &a, &b, &c, true);
4051
4052 let fused = y.realize_f32();
4053 assert_eq!(fused.len(), batch * seqlen * dim);
4054
4055 let graph = y.inner.graph().clone();
4056 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4057 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4058 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SELECTIVE_SCAN);
4059 let ys = {
4060 let mut g = graph.write().unwrap();
4061 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4062 .expect("unroll")
4063 .0[0] };
4065 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4066 .expect("realize selective_scan softplus oracle on CPU");
4067 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, dim);
4068 let mut max_drift = 0f32;
4069 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4070 let d = (o - f).abs();
4071 max_drift = max_drift.max(d);
4072 assert!(
4073 d < 1e-3,
4074 "selective_scan softplus mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4075 );
4076 }
4077 eprintln!("selective_scan softplus: max honest drift = {max_drift:e}");
4078 }
4079
4080 #[test]
4094 fn ssd_chunk_scan_multidim_unroll_matches_fused_kernel() {
4095 use fuel_graph::registry::FusedOps;
4096 let dev = Device::cpu();
4097 let (batch, seqlen, heads, head_dim, state_dim, chunk) =
4098 (2usize, 4usize, 2usize, 2usize, 2usize, 2usize);
4099 let x_data: Vec<f32> = (0..batch * seqlen * heads * head_dim)
4102 .map(|i| 0.20 + 0.05 * i as f32)
4103 .collect();
4104 let dt_data: Vec<f32> = (0..batch * seqlen * heads)
4105 .map(|i| 0.10 + 0.04 * i as f32)
4106 .collect();
4107 let b_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4108 .map(|i| 0.15 + 0.03 * i as f32)
4109 .collect();
4110 let c_data: Vec<f32> = (0..batch * seqlen * heads * state_dim)
4111 .map(|i| 0.25 + 0.035 * i as f32)
4112 .collect();
4113 let x = Tensor::from_f32(
4114 x_data,
4115 Shape::from_dims(&[batch, seqlen, heads, head_dim]),
4116 &dev,
4117 )
4118 .unwrap();
4119 let dt = x
4120 .const_f32_like(dt_data, Shape::from_dims(&[batch, seqlen, heads]))
4121 .unwrap();
4122 let a = x
4123 .const_f32_like(vec![-0.6f32, -1.0], Shape::from_dims(&[heads]))
4124 .unwrap();
4125 let b = x
4126 .const_f32_like(b_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4127 .unwrap();
4128 let c = x
4129 .const_f32_like(c_data, Shape::from_dims(&[batch, seqlen, heads, state_dim]))
4130 .unwrap();
4131 let y = x.ssd_chunk_scan(&dt, &a, &b, &c, chunk);
4132
4133 let fused = y.realize_f32();
4135 assert_eq!(fused.len(), batch * seqlen * heads * head_dim);
4136
4137 let graph = y.inner.graph().clone();
4139 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4140 .optimize_to_fixpoint(&graph, &[y.inner.id()]);
4141 assert_eq!(roots.len(), 1);
4142 let scan_id = find_scan_terminal(&graph, roots[0], FusedOps::SSD_CHUNK_SCAN);
4143 let ys = {
4144 let mut g = graph.write().unwrap();
4145 fuel_graph::scan::unroll_scan(&mut g, scan_id, seqlen)
4146 .expect("unroll")
4147 .0[0] };
4149 let ys_flat = crate::pipelined_bridge::realize_one_as::<f32>(&graph, ys, &dev)
4150 .expect("realize ssd_chunk_scan unroll oracle on CPU");
4151 let oracle = scan_ys_to_fused_layout(&ys_flat, seqlen, batch, heads * head_dim);
4154 assert_eq!(oracle.len(), fused.len());
4155 let mut max_drift = 0f32;
4156 for (i, (o, f)) in oracle.iter().zip(fused.iter()).enumerate() {
4157 let d = (o - f).abs();
4158 max_drift = max_drift.max(d);
4159 assert!(
4160 d < 1e-3,
4161 "ssd_chunk_scan multidim mismatch at {i}: oracle {o} vs fused {f} (drift {d})",
4162 );
4163 }
4164 eprintln!("ssd_chunk_scan multidim: max honest drift = {max_drift:e}");
4165 }
4166
4167 #[test]
4176 fn nf4_matmul_decompose_matches_kernel() {
4177 use fuel_graph::Op;
4178 use fuel_graph::registry::FusedOps;
4179 let dev = Device::cpu();
4180 let weight = crate::nf4::nf4_from_bytes(
4182 vec![247_u8, 247, 127, 127],
4183 vec![1.0_f32, 2.0, 10.0, 20.0],
4184 2,
4185 4,
4186 2,
4187 &dev,
4188 )
4189 .expect("nf4_from_bytes");
4190 let act = Tensor::from_graph_tensor(
4191 weight
4192 .w_packed
4193 .graph_tensor()
4194 .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
4195 .unwrap(),
4196 );
4197 let y = weight.matmul(&act);
4198
4199 let graph = y.inner.graph().clone();
4201 let id = y.inner.id();
4202 let roots =
4203 fuel_graph::opt::RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[id]);
4204 assert_eq!(roots.len(), 1, "lowering should keep a single root");
4205
4206 {
4209 let g = graph.read().unwrap();
4210 let mut stack = vec![roots[0]];
4211 let mut seen = std::collections::HashSet::new();
4212 while let Some(nid) = stack.pop() {
4213 if !seen.insert(nid) {
4214 continue;
4215 }
4216 let node = g.node(nid);
4217 assert!(
4218 !matches!(node.op, Op::Fused(fid, _) if fid == FusedOps::NF4_MATMUL),
4219 "decomposed graph still contains an Op::Fused(NF4_MATMUL) island",
4220 );
4221 for &inp in &node.inputs {
4222 stack.push(inp);
4223 }
4224 }
4225 }
4226
4227 let got = crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &dev)
4228 .expect("realize decomposed Nf4Matmul on CPU");
4229 assert_eq!(got.len(), 2);
4230 assert!((got[0] - 10.0).abs() < 1e-4, "out 0: {}", got[0]);
4231 assert!((got[1] - 50.0).abs() < 1e-4, "out 1: {}", got[1]);
4232 }
4233
4234 fn lower_realize_fused(
4240 anchor: &Tensor,
4241 op: fuel_graph::Op,
4242 inputs: Vec<fuel_graph::NodeId>,
4243 shape: Shape,
4244 ) -> Vec<f32> {
4245 let graph = anchor.inner.graph().clone();
4246 let fused_id = {
4247 let mut g = graph.write().unwrap();
4248 g.push(fuel_graph::Node {
4249 op,
4250 inputs,
4251 shape,
4252 dtype: DType::F32,
4253 })
4254 };
4255 let roots = fuel_graph::opt::RuleRegistry::lowering_only()
4256 .optimize_to_fixpoint(&graph, &[fused_id]);
4257 assert_eq!(roots.len(), 1, "lowering keeps a single root");
4258 crate::pipelined_bridge::realize_one_as::<f32>(&graph, roots[0], &Device::cpu())
4259 .expect("realize decomposed fused op on CPU")
4260 }
4261
4262 #[test]
4263 fn powi_backward_decompose_matches_reference() {
4264 use fuel_graph::registry::{FusedOpParams, FusedOps};
4265 let dev = Device::cpu();
4266 let exp = 3i32;
4267 let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4268 let up_data = vec![1.0f32, 0.5, 2.0, -1.0];
4269 let shape = Shape::from_dims(&[4]);
4270 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4271 let up = x.const_f32_like(up_data.clone(), shape.clone()).unwrap();
4272 let got = lower_realize_fused(
4273 &x,
4274 fuel_graph::Op::Fused(FusedOps::POWI_BACKWARD, FusedOpParams::PowIBackward { exp }),
4275 vec![x.inner.id(), up.inner.id()],
4276 shape,
4277 );
4278 let expected: Vec<f32> = x_data
4280 .iter()
4281 .zip(&up_data)
4282 .map(|(&x, &u)| exp as f32 * x.powi(exp - 1) * u)
4283 .collect();
4284 assert_eq!(got.len(), expected.len());
4285 for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4286 assert!(
4287 (g - e).abs() < 1e-4,
4288 "powi_backward at {i}: got {g}, expected {e}"
4289 );
4290 }
4291 }
4292
4293 #[test]
4301 fn inplace_affine_decompose_matches_affine_reference() {
4302 use fuel_graph::registry::{FusedOpParams, FusedOps};
4303 let dev = Device::cpu();
4304 let (mul, add) = (2.5f64, -1.0f64);
4305 let x_data = vec![1.5f32, -2.0, 0.5, 3.0];
4306 let shape = Shape::from_dims(&[4]);
4307 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4308 let got = lower_realize_fused(
4309 &x,
4310 fuel_graph::Op::Fused(
4311 FusedOps::INPLACE_AFFINE,
4312 FusedOpParams::InplaceAffine { mul, add },
4313 ),
4314 vec![x.inner.id()],
4315 shape,
4316 );
4317 let expected: Vec<f32> = x_data
4319 .iter()
4320 .map(|&v| (mul as f32) * v + add as f32)
4321 .collect();
4322 assert_eq!(got.len(), expected.len());
4323 for (i, (&g, &e)) in got.iter().zip(&expected).enumerate() {
4324 assert_eq!(g, e, "inplace_affine at {i}: got {g}, expected {e}");
4325 }
4326 }
4327
4328 #[test]
4329 fn softmax_last_dim_backward_decompose_matches_reference() {
4330 use fuel_graph::registry::{FusedOpParams, FusedOps};
4331 let dev = Device::cpu();
4332 let (rows, cols) = (2usize, 3usize);
4333 let logits = [[0.5f32, -1.0, 2.0], [1.0, 0.0, -0.5]];
4335 let mut s_data = Vec::new();
4336 for row in &logits {
4337 let m = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4338 let exps: Vec<f32> = row.iter().map(|&v| (v - m).exp()).collect();
4339 let sum: f32 = exps.iter().sum();
4340 s_data.extend(exps.iter().map(|&e| e / sum));
4341 }
4342 let g_data = vec![1.0f32, -0.5, 0.3, 0.2, 1.5, -1.0];
4343 let shape = Shape::from_dims(&[rows, cols]);
4344 let s = Tensor::from_f32(s_data.clone(), shape.clone(), &dev).unwrap();
4345 let g = s.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4346 let got = lower_realize_fused(
4347 &s,
4348 fuel_graph::Op::Fused(
4349 FusedOps::SOFTMAX_LAST_DIM_BACKWARD,
4350 FusedOpParams::SoftmaxLastDimBackward,
4351 ),
4352 vec![s.inner.id(), g.inner.id()],
4353 shape,
4354 );
4355 let mut expected = vec![0.0f32; rows * cols];
4357 for r in 0..rows {
4358 let dot: f32 = (0..cols)
4359 .map(|c| g_data[r * cols + c] * s_data[r * cols + c])
4360 .sum();
4361 for c in 0..cols {
4362 expected[r * cols + c] = s_data[r * cols + c] * (g_data[r * cols + c] - dot);
4363 }
4364 }
4365 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4366 assert!(
4367 (gv - ev).abs() < 1e-5,
4368 "softmax_bwd at {i}: got {gv}, expected {ev}"
4369 );
4370 }
4371 }
4372
4373 #[test]
4374 fn rms_norm_last_dim_backward_decompose_matches_reference() {
4375 use fuel_graph::registry::{FusedOpParams, FusedOps};
4376 let dev = Device::cpu();
4377 let (rows, cols) = (2usize, 3usize);
4378 let eps = 1e-5f64;
4379 let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4380 let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4381 let shape = Shape::from_dims(&[rows, cols]);
4382 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4383 let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4384 let got = lower_realize_fused(
4385 &x,
4386 fuel_graph::Op::Fused(
4387 FusedOps::RMS_NORM_LAST_DIM_BACKWARD,
4388 FusedOpParams::RmsNormLastDimBackward { eps },
4389 ),
4390 vec![x.inner.id(), g.inner.id()],
4391 shape,
4392 );
4393 let n = cols as f32;
4395 let mut expected = vec![0.0f32; rows * cols];
4396 for r in 0..rows {
4397 let meansq: f32 = (0..cols).map(|c| x_data[r * cols + c].powi(2)).sum::<f32>() / n;
4398 let denom = meansq + eps as f32;
4399 let rrms = 1.0 / denom.sqrt();
4400 let s: f32 = (0..cols)
4401 .map(|c| g_data[r * cols + c] * x_data[r * cols + c])
4402 .sum();
4403 for c in 0..cols {
4404 let term = x_data[r * cols + c] * s / (n * denom);
4405 expected[r * cols + c] = rrms * (g_data[r * cols + c] - term);
4406 }
4407 }
4408 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4409 assert!(
4410 (gv - ev).abs() < 1e-4,
4411 "rms_norm_bwd at {i}: got {gv}, expected {ev}"
4412 );
4413 }
4414 }
4415
4416 #[test]
4417 fn layer_norm_last_dim_backward_decompose_matches_reference() {
4418 use fuel_graph::registry::{FusedOpParams, FusedOps};
4419 let dev = Device::cpu();
4420 let (rows, cols) = (2usize, 3usize);
4421 let eps = 1e-5f64;
4422 let x_data = vec![1.0f32, -2.0, 0.5, 3.0, 0.25, -1.5];
4423 let g_data = vec![0.5f32, 1.0, -0.3, 0.2, -1.0, 0.7];
4424 let shape = Shape::from_dims(&[rows, cols]);
4425 let x = Tensor::from_f32(x_data.clone(), shape.clone(), &dev).unwrap();
4426 let g = x.const_f32_like(g_data.clone(), shape.clone()).unwrap();
4427 let got = lower_realize_fused(
4428 &x,
4429 fuel_graph::Op::Fused(
4430 FusedOps::LAYER_NORM_LAST_DIM_BACKWARD,
4431 FusedOpParams::LayerNormLastDimBackward { eps },
4432 ),
4433 vec![x.inner.id(), g.inner.id()],
4434 shape,
4435 );
4436 let n = cols as f32;
4438 let mut expected = vec![0.0f32; rows * cols];
4439 for r in 0..rows {
4440 let mean_x: f32 = (0..cols).map(|c| x_data[r * cols + c]).sum::<f32>() / n;
4441 let var: f32 = (0..cols)
4442 .map(|c| (x_data[r * cols + c] - mean_x).powi(2))
4443 .sum::<f32>()
4444 / n;
4445 let istd = 1.0 / (var + eps as f32).sqrt();
4446 let xhat: Vec<f32> = (0..cols)
4447 .map(|c| (x_data[r * cols + c] - mean_x) * istd)
4448 .collect();
4449 let mean_g: f32 = (0..cols).map(|c| g_data[r * cols + c]).sum::<f32>() / n;
4450 let mean_gxh: f32 = (0..cols)
4451 .map(|c| g_data[r * cols + c] * xhat[c])
4452 .sum::<f32>()
4453 / n;
4454 for c in 0..cols {
4455 expected[r * cols + c] =
4456 istd * (g_data[r * cols + c] - mean_g - xhat[c] * mean_gxh);
4457 }
4458 }
4459 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4460 assert!(
4461 (gv - ev).abs() < 1e-4,
4462 "layer_norm_bwd at {i}: got {gv}, expected {ev}"
4463 );
4464 }
4465 }
4466
4467 #[test]
4468 fn reduce_max_to_backward_decompose_matches_reference() {
4469 use fuel_graph::registry::{FusedOpParams, FusedOps};
4470 let dev = Device::cpu();
4471 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]);
4475 let up_shape = Shape::from_dims(&[2, 1]);
4476 let x = Tensor::from_f32(x_data.clone(), x_shape.clone(), &dev).unwrap();
4477 let up = x.const_f32_like(up_data.clone(), up_shape).unwrap();
4478 let got = lower_realize_fused(
4479 &x,
4480 fuel_graph::Op::Fused(
4481 FusedOps::REDUCE_MAX_TO_BACKWARD,
4482 FusedOpParams::ReduceMaxToBackward,
4483 ),
4484 vec![x.inner.id(), up.inner.id()],
4485 x_shape,
4486 );
4487 let expected = vec![0.0f32, 5.0, 5.0, 5.0, 0.0, 0.0];
4489 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4490 assert!(
4491 (gv - ev).abs() < 1e-5,
4492 "reduce_max_bwd at {i}: got {gv}, expected {ev}"
4493 );
4494 }
4495 }
4496
4497 fn causal_conv1d_check(use_silu: bool) {
4498 use fuel_graph::registry::{FusedOpParams, FusedOps};
4499 let dev = Device::cpu();
4500 let (b, c, k, seq) = (1usize, 2usize, 3usize, 2usize);
4501 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];
4504 let w_data = vec![0.5f32, 1.0, -0.5, 1.0, 0.0, 2.0];
4505 let bias_data = vec![0.1f32, -0.2];
4506 let x = Tensor::from_f32(x_data.clone(), Shape::from_dims(&[b, c, x_seq]), &dev).unwrap();
4507 let w = x
4508 .const_f32_like(w_data.clone(), Shape::from_dims(&[c, 1, k]))
4509 .unwrap();
4510 let bias = x
4511 .const_f32_like(bias_data.clone(), Shape::from_dims(&[c]))
4512 .unwrap();
4513 let got = lower_realize_fused(
4514 &x,
4515 fuel_graph::Op::Fused(
4516 FusedOps::CAUSAL_CONV1D,
4517 FusedOpParams::CausalConv1d { use_silu },
4518 ),
4519 vec![x.inner.id(), w.inner.id(), bias.inner.id()],
4520 Shape::from_dims(&[b, c, seq]),
4521 );
4522 let mut expected = vec![0.0f32; b * c * seq];
4524 for ch in 0..c {
4525 for t in 0..seq {
4526 let mut acc = 0.0f32;
4527 for kk in 0..k {
4528 acc += w_data[ch * k + kk] * x_data[ch * x_seq + t + kk];
4529 }
4530 acc += bias_data[ch];
4531 if use_silu {
4532 acc *= 1.0 / (1.0 + (-acc).exp());
4533 }
4534 expected[ch * seq + t] = acc;
4535 }
4536 }
4537 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4538 assert!(
4539 (gv - ev).abs() < 1e-4,
4540 "causal_conv1d (silu={use_silu}) at {i}: got {gv}, expected {ev}",
4541 );
4542 }
4543 }
4544
4545 #[test]
4546 fn causal_conv1d_decompose_matches_reference() {
4547 causal_conv1d_check(false);
4548 causal_conv1d_check(true);
4549 }
4550
4551 #[test]
4552 fn flash_attn_backward_decompose_matches_reference() {
4553 use fuel_graph::registry::{FusedOpParams, FusedOps};
4554 let dev = Device::cpu();
4555 let (sq, sk, d) = (2usize, 2usize, 2usize);
4556 #[allow(clippy::approx_constant)]
4559 let scale = 0.7071f32;
4560 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]);
4565 let kshape = Shape::from_dims(&[1, 1, sk, d]);
4566 let q = Tensor::from_f32(q_data.clone(), qshape.clone(), &dev).unwrap();
4567 let k = q.const_f32_like(k_data.clone(), kshape.clone()).unwrap();
4568 let v = q.const_f32_like(v_data.clone(), kshape.clone()).unwrap();
4569 let dout = q.const_f32_like(do_data.clone(), qshape.clone()).unwrap();
4570 let params = FusedOpParams::FlashAttnBackward {
4571 softmax_scale: scale,
4572 causal: false,
4573 window_size_left: None,
4574 window_size_right: None,
4575 softcap: None,
4576 };
4577 let inputs = vec![q.inner.id(), k.inner.id(), v.inner.id(), dout.inner.id()];
4578 let dq = lower_realize_fused(
4579 &q,
4580 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_Q, params.clone()),
4581 inputs.clone(),
4582 qshape.clone(),
4583 );
4584 let dk = lower_realize_fused(
4585 &q,
4586 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_K, params.clone()),
4587 inputs.clone(),
4588 kshape.clone(),
4589 );
4590 let dv = lower_realize_fused(
4591 &q,
4592 fuel_graph::Op::Fused(FusedOps::FLASH_ATTN_BACKWARD_V, params),
4593 inputs,
4594 kshape,
4595 );
4596
4597 let mut p = vec![0.0f32; sq * sk];
4599 for i in 0..sq {
4600 let mut scores = vec![0.0f32; sk];
4601 for j in 0..sk {
4602 let mut s = 0.0f32;
4603 for l in 0..d {
4604 s += q_data[i * d + l] * k_data[j * d + l];
4605 }
4606 scores[j] = scale * s;
4607 }
4608 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4609 let mut sum = 0.0f32;
4610 let e: Vec<f32> = scores
4611 .iter()
4612 .map(|&s| {
4613 let x = (s - m).exp();
4614 sum += x;
4615 x
4616 })
4617 .collect();
4618 for j in 0..sk {
4619 p[i * sk + j] = e[j] / sum;
4620 }
4621 }
4622 let mut ref_dv = vec![0.0f32; sk * d];
4624 for j in 0..sk {
4625 for l in 0..d {
4626 ref_dv[j * d + l] = (0..sq).map(|i| p[i * sk + j] * do_data[i * d + l]).sum();
4627 }
4628 }
4629 let mut dp = vec![0.0f32; sq * sk];
4631 for i in 0..sq {
4632 for j in 0..sk {
4633 dp[i * sk + j] = (0..d).map(|l| do_data[i * d + l] * v_data[j * d + l]).sum();
4634 }
4635 }
4636 let mut dsr = vec![0.0f32; sq * sk];
4638 for i in 0..sq {
4639 let rowdot: f32 = (0..sk).map(|j| dp[i * sk + j] * p[i * sk + j]).sum();
4640 for j in 0..sk {
4641 dsr[i * sk + j] = scale * p[i * sk + j] * (dp[i * sk + j] - rowdot);
4642 }
4643 }
4644 let mut ref_dq = vec![0.0f32; sq * d];
4646 for i in 0..sq {
4647 for l in 0..d {
4648 ref_dq[i * d + l] = (0..sk).map(|j| dsr[i * sk + j] * k_data[j * d + l]).sum();
4649 }
4650 }
4651 let mut ref_dk = vec![0.0f32; sk * d];
4652 for j in 0..sk {
4653 for l in 0..d {
4654 ref_dk[j * d + l] = (0..sq).map(|i| dsr[i * sk + j] * q_data[i * d + l]).sum();
4655 }
4656 }
4657 let check = |name: &str, got: &[f32], exp: &[f32]| {
4658 assert_eq!(got.len(), exp.len(), "{name} length");
4659 for (i, (&gv, &ev)) in got.iter().zip(exp).enumerate() {
4660 assert!(
4661 (gv - ev).abs() < 1e-4,
4662 "{name} at {i}: got {gv}, expected {ev}"
4663 );
4664 }
4665 };
4666 check("dQ", &dq, &ref_dq);
4667 check("dK", &dk, &ref_dk);
4668 check("dV", &dv, &ref_dv);
4669 }
4670
4671 #[test]
4672 fn paged_attn_decompose_matches_reference() {
4673 use fuel_graph::registry::{FusedOpParams, FusedOps};
4674 let dev = Device::cpu();
4675 let block_size = 2usize;
4677 let scale = 1.0f32;
4678 let kc = vec![
4681 1.0f32, 0.0, 0.0, 1.0, 9.0, 9.0, 9.0, 9.0, 1.0, 1.0, 2.0, 0.0,
4682 ];
4683 let vc = vec![
4684 1.0f32, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0, 6.0, 7.0, 8.0,
4685 ];
4686 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();
4691 let kcache = q
4692 .const_f32_like(kc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4693 .unwrap();
4694 let vcache = q
4695 .const_f32_like(vc.clone(), Shape::from_dims(&[3, 2, 1, 2]))
4696 .unwrap();
4697 let block_table = q.const_u32_like(bt, Shape::from_dims(&[1, 2])).unwrap();
4698 let context_lens = q.const_u32_like(cl, Shape::from_dims(&[1])).unwrap();
4699 let params = FusedOpParams::PagedAttn {
4700 softmax_scale: scale,
4701 block_size,
4702 softcap: None,
4703 };
4704 let inputs = vec![
4705 q.inner.id(),
4706 kcache.inner.id(),
4707 vcache.inner.id(),
4708 block_table.inner.id(),
4709 context_lens.inner.id(),
4710 ];
4711 let got = lower_realize_fused(
4712 &q,
4713 fuel_graph::Op::Fused(FusedOps::PAGED_ATTN, params),
4714 inputs,
4715 Shape::from_dims(&[1, 1, 1, 2]),
4716 );
4717
4718 let (kv_len, ctx) = (4usize, 3usize);
4721 let mut k_seq = vec![];
4722 let mut v_seq = vec![];
4723 for &blk in &[0usize, 2usize] {
4724 for p in 0..block_size {
4725 let base = (blk * block_size + p) * 2; k_seq.push([kc[base], kc[base + 1]]);
4727 v_seq.push([vc[base], vc[base + 1]]);
4728 }
4729 }
4730 let mut scores = vec![0.0f32; kv_len];
4731 for j in 0..kv_len {
4732 let s = q_data[0] * k_seq[j][0] + q_data[1] * k_seq[j][1];
4733 scores[j] = if j >= ctx {
4734 f32::NEG_INFINITY
4735 } else {
4736 scale * s
4737 };
4738 }
4739 let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4740 let mut sum = 0.0f32;
4741 let e: Vec<f32> = scores
4742 .iter()
4743 .map(|&s| {
4744 let x = (s - m).exp();
4745 sum += x;
4746 x
4747 })
4748 .collect();
4749 let mut expected = vec![0.0f32; 2];
4750 for j in 0..kv_len {
4751 for l in 0..2 {
4752 expected[l] += (e[j] / sum) * v_seq[j][l];
4753 }
4754 }
4755 for (i, (&gv, &ev)) in got.iter().zip(&expected).enumerate() {
4756 assert!(
4757 (gv - ev).abs() < 1e-4,
4758 "paged_attn at {i}: got {gv}, expected {ev}"
4759 );
4760 }
4761 }
4762
4763 #[test]
4764 #[cfg(feature = "cuda")]
4765 fn cuda_executor_matches_cpu_on_add_mul() {
4766 let a =
4767 Tensor::from_f32(vec![1.0, 2.0, 3.0], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
4768 let b = a
4769 .const_f32_like(vec![4.0, 5.0, 6.0], Shape::from_dims(&[3]))
4770 .unwrap();
4771 let c = a.add(&b).unwrap().mul(&a).unwrap();
4772 let cpu_result = c.realize_f32();
4773 let executor = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4774 let cuda_result = c.realize_f32_cuda(&executor);
4775 assert_eq!(cpu_result, cuda_result);
4776 }
4777
4778 #[test]
4796 #[cfg(feature = "cuda")]
4797 #[ignore = "requires a live CUDA device"]
4798 fn maximum_minimum_nan_convention_lazy_realize_smoke() {
4799 let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4800 Ok(d) => d,
4801 Err(e) => {
4802 return fuel_test_support::hardware::skip(
4803 fuel_test_support::hardware::Hardware::Cuda,
4804 fuel_test_support::hardware::Missing::device(format!(
4805 "CudaDevice::new(0): {e:?}"
4806 )),
4807 );
4808 }
4809 };
4810 let a = Tensor::from_f32(
4811 vec![f32::NAN, -2.0, 3.0, f32::NAN],
4812 Shape::from_dims(&[4]),
4813 &Device::cpu(),
4814 )
4815 .unwrap();
4816 let b = a
4817 .const_f32_like(vec![1.0, f32::NAN, 2.0, f32::NAN], Shape::from_dims(&[4]))
4818 .unwrap();
4819
4820 let max_lazy = a.maximum(&b).unwrap();
4821 let cpu_max = max_lazy.realize_f32();
4822 let cuda_max = max_lazy.realize_f32_cuda(&cuda);
4823 for i in 0..4 {
4824 assert_eq!(
4825 cpu_max[i].is_nan(),
4826 cuda_max[i].is_nan(),
4827 "maximum[{i}] NaN-ness must match: cpu={}, cuda={}",
4828 cpu_max[i],
4829 cuda_max[i],
4830 );
4831 if !cpu_max[i].is_nan() {
4832 assert_eq!(cpu_max[i], cuda_max[i], "maximum[{i}]");
4833 }
4834 }
4835
4836 let min_lazy = a.minimum(&b).unwrap();
4837 let cpu_min = min_lazy.realize_f32();
4838 let cuda_min = min_lazy.realize_f32_cuda(&cuda);
4839 for i in 0..4 {
4840 assert_eq!(
4841 cpu_min[i].is_nan(),
4842 cuda_min[i].is_nan(),
4843 "minimum[{i}] NaN-ness must match: cpu={}, cuda={}",
4844 cpu_min[i],
4845 cuda_min[i],
4846 );
4847 if !cpu_min[i].is_nan() {
4848 assert_eq!(cpu_min[i], cuda_min[i], "minimum[{i}]");
4849 }
4850 }
4851 }
4852
4853 #[test]
4876 #[cfg(feature = "cuda")]
4877 #[ignore = "requires a live CUDA device"]
4878 fn relu_nan_convention_lazy_realize_smoke() {
4879 let cuda = match fuel_cuda_backend::CudaDevice::new(0) {
4880 Ok(d) => d,
4881 Err(e) => {
4882 return fuel_test_support::hardware::skip(
4883 fuel_test_support::hardware::Hardware::Cuda,
4884 fuel_test_support::hardware::Missing::device(format!(
4885 "CudaDevice::new(0): {e:?}"
4886 )),
4887 );
4888 }
4889 };
4890 let a = Tensor::from_f32(
4891 vec![f32::NAN, -2.0, 3.0],
4892 Shape::from_dims(&[3]),
4893 &Device::cpu(),
4894 )
4895 .unwrap();
4896 let relu_lazy = a.relu();
4897 let cpu_result = relu_lazy.realize_f32();
4898 let cuda_result = relu_lazy.realize_f32_cuda(&cuda);
4899
4900 for i in 0..3 {
4901 assert_eq!(
4902 cpu_result[i].is_nan(),
4903 cuda_result[i].is_nan(),
4904 "relu[{i}] NaN-ness must match: cpu={}, cuda={}",
4905 cpu_result[i],
4906 cuda_result[i],
4907 );
4908 if !cpu_result[i].is_nan() {
4909 assert_eq!(cpu_result[i], cuda_result[i], "relu[{i}]");
4910 }
4911 }
4912 }
4913
4914 #[test]
4915 #[cfg(feature = "cuda")]
4916 fn cuda_executor_matches_cpu_on_matmul() {
4917 let a = Tensor::from_f32(
4918 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4919 Shape::from_dims(&[2, 3]),
4920 &Device::cpu(),
4921 )
4922 .unwrap();
4923 let b = a
4924 .const_f32_like(
4925 vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0],
4926 Shape::from_dims(&[3, 2]),
4927 )
4928 .unwrap();
4929 let c = a.matmul(&b).unwrap();
4930 let cpu = c.realize_f32();
4931 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4932 let cuda = c.realize_f32_cuda(&exe);
4933 assert_eq!(cpu.len(), cuda.len());
4934 for (i, (a, b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4935 assert!((a - b).abs() < 1e-3, "matmul[{i}]: cpu={a}, cuda={b}",);
4936 }
4937 }
4938
4939 #[test]
4940 #[cfg(feature = "cuda")]
4941 fn cuda_executor_matches_cpu_on_broadcast_matmul() {
4942 let x = Tensor::from_f32(
4945 (0..12).map(|i| i as f32 * 0.1).collect::<Vec<_>>(),
4946 Shape::from_dims(&[1, 3, 4]),
4947 &Device::cpu(),
4948 )
4949 .unwrap();
4950 let w = x
4951 .const_f32_like(
4952 (0..8).map(|i| i as f32 * 0.2).collect::<Vec<_>>(),
4953 Shape::from_dims(&[4, 2]),
4954 )
4955 .unwrap();
4956 let y = x.matmul(&w).unwrap();
4957 let cpu = y.realize_f32();
4958 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4959 let cuda = y.realize_f32_cuda(&exe);
4960 assert_eq!(cpu.len(), cuda.len());
4961 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4962 assert!((a - b).abs() < 1e-3, "bcast_mm[{i}]: cpu={a}, cuda={b}");
4963 }
4964 }
4965
4966 #[test]
4967 #[cfg(feature = "cuda")]
4968 fn cuda_executor_matches_cpu_on_permute() {
4969 let x = Tensor::from_f32(
4970 (0..24).map(|i| i as f32).collect::<Vec<_>>(),
4971 Shape::from_dims(&[1, 2, 3, 4]),
4972 &Device::cpu(),
4973 )
4974 .unwrap();
4975 let y = x.permute([0, 2, 1, 3_usize]).unwrap();
4976 let cpu = y.realize_f32();
4977 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4978 let cuda = y.realize_f32_cuda(&exe);
4979 assert_eq!(cpu, cuda, "permute mismatch");
4980 }
4981
4982 #[test]
4983 #[cfg(feature = "cuda")]
4984 fn cuda_executor_matches_cpu_on_softmax() {
4985 let x = Tensor::from_f32(
4986 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
4987 Shape::from_dims(&[2, 3]),
4988 &Device::cpu(),
4989 )
4990 .unwrap();
4991 let y = x.softmax_last_dim().unwrap();
4992 let cpu = y.realize_f32();
4993 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
4994 let cuda = y.realize_f32_cuda(&exe);
4995 assert_eq!(cpu.len(), cuda.len());
4996 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
4997 assert!((a - b).abs() < 1e-4, "softmax[{i}]: cpu={a}, cuda={b}");
4998 }
4999 }
5000
5001 #[test]
5016 #[cfg(feature = "cuda")]
5017 fn cuda_executor_matches_cpu_on_softmax_via_lowering() {
5018 let n = 24;
5021 let last = 5;
5022 let data: Vec<f32> = (0..n * last)
5023 .map(|i| ((i as f32) * 0.13).sin() * 2.0 - 0.7)
5024 .collect();
5025 let x = Tensor::from_f32(data, Shape::from_dims(&[n, last]), &Device::cpu()).unwrap();
5026 let y = x.softmax_last_dim().unwrap();
5027
5028 let cpu = y.realize_f32();
5031
5032 let graph = y.inner.graph().clone();
5038 let registry = fuel_graph::opt::RuleRegistry::lowering_only();
5039 let remapped = registry.optimize_to_fixpoint(&graph, &[y.inner.id()]);
5040 let dev = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5041 let fc_device: crate::Device = dev.clone().into();
5042 let cuda = crate::pipelined_bridge::realize_one_as::<f32>(&graph, remapped[0], &fc_device)
5043 .expect("realize lowered softmax on CUDA");
5044
5045 assert_eq!(cpu.len(), cuda.len());
5046 let mut max_abs_err = 0.0_f32;
5047 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5048 let err = (a - b).abs();
5049 if err > max_abs_err {
5050 max_abs_err = err;
5051 }
5052 assert!(
5053 err < 1e-5,
5054 "lowered softmax[{i}]: cpu={a} (fused), cuda={b} (composed), err={err}",
5055 );
5056 }
5057 eprintln!("max_abs_err over lowered-vs-fused softmax: {max_abs_err:.3e}");
5058 }
5059
5060 #[test]
5061 #[cfg(feature = "cuda")]
5062 fn cuda_executor_matches_cpu_on_concat_slice() {
5063 let a = Tensor::from_f32(
5064 vec![1.0, 2.0, 3.0, 4.0],
5065 Shape::from_dims(&[2, 2]),
5066 &Device::cpu(),
5067 )
5068 .unwrap();
5069 let b = a
5070 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], Shape::from_dims(&[2, 2]))
5071 .unwrap();
5072 let cat = a.concat(&b, 1).unwrap(); let sliced = cat.slice(1, 1, 2).unwrap(); let cpu = sliced.realize_f32();
5075 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5076 let cuda = sliced.realize_f32_cuda(&exe);
5077 assert_eq!(cpu, cuda, "concat+slice mismatch");
5078 }
5079
5080 #[test]
5081 #[cfg(feature = "cuda")]
5082 fn cuda_executor_matches_cpu_on_rms_norm() {
5083 let x = Tensor::from_f32(
5084 (0..8).map(|i| i as f32 * 0.5 - 1.5).collect::<Vec<_>>(),
5085 Shape::from_dims(&[2, 4]),
5086 &Device::cpu(),
5087 )
5088 .unwrap();
5089 let y = x.rms_norm_last_dim(1e-5).unwrap();
5090 let cpu = y.realize_f32();
5091 let exe = fuel_cuda_backend::CudaDevice::new(0).unwrap();
5092 let cuda = y.realize_f32_cuda(&exe);
5093 assert_eq!(cpu.len(), cuda.len());
5094 for (i, (&a, &b)) in cpu.iter().zip(cuda.iter()).enumerate() {
5095 assert!((a - b).abs() < 1e-3, "rms_norm[{i}]: cpu={a}, cuda={b}");
5096 }
5097 }
5098
5099 #[test]
5100 fn realize_f64_through_bridge() {
5101 let a =
5102 Tensor::from_f64(vec![1.5, 2.5, 3.5], Shape::from_dims(&[3]), &Device::cpu()).unwrap();
5103 let b = a.mul(&a).unwrap();
5104 assert_eq!(b.realize_f64(), vec![2.25, 6.25, 12.25]);
5105 }
5106
5107 #[test]
5118 fn gap327_realize_f32_on_f64_root_must_reject_not_reinterpret() {
5119 let t =
5120 Tensor::from_f64(vec![1.0_f64, 2.0], Shape::from_dims(&[2]), &Device::cpu()).unwrap();
5121 let graph = t.inner.graph().clone();
5122 let target = t.inner.id();
5123 let device = Device::cpu();
5124
5125 let prev_hook = std::panic::take_hook();
5128 std::panic::set_hook(Box::new(|_| {}));
5129 let ctl_f64 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f64()));
5131 let ctl_f16 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f16()));
5134 let subj_method =
5136 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| t.realize_f32()));
5137 std::panic::set_hook(prev_hook);
5138
5139 let subj_raw = crate::pipelined_bridge::realize_one_as::<f32>(&graph, target, &device);
5141 let bytes_view = crate::pipelined_bridge::realize_one_bytes(&graph, target, &device);
5144
5145 assert!(
5146 ctl_f64.is_ok(),
5147 "control: realize_f64 on an F64 root must succeed; got {ctl_f64:?}"
5148 );
5149 assert!(
5150 ctl_f16.is_err(),
5151 "positive control: realize_f16() on an F64 root must panic (guard fires); if it \
5152 does not, the harness is not observing panics and the subject asserts are vacuous"
5153 );
5154 assert!(
5155 subj_method.is_err(),
5156 "realize_f32() on an F64 root must REJECT (panic), not silently reinterpret bytes"
5157 );
5158 assert!(
5159 matches!(subj_raw, Err(fuel_ir::Error::UnexpectedDType { .. })),
5160 "realize_one_as::<f32> on an F64 root must return Err(UnexpectedDType), not \
5161 Ok(reinterpreted bytes); got {subj_raw:?}"
5162 );
5163 assert!(
5164 matches!(&bytes_view, Ok(b) if b.len() == 16),
5165 "byte view realize_one_bytes on an F64 [1.0, 2.0] root must return 16 raw bytes \
5166 (unguarded), not an error; got {bytes_view:?}"
5167 );
5168 }
5169
5170 #[test]
5171 fn lazy_tensor_mini_llama_block_forward() {
5172 let seq = 3;
5181 let d_head = 4; let num_heads = 2;
5183 let d_model = num_heads * d_head; let x_data: Vec<f32> = (0..seq * d_model).map(|i| i as f32 * 0.01).collect();
5187 let x =
5188 Tensor::from_f32(x_data, Shape::from_dims(&[1, seq, d_model]), &Device::cpu()).unwrap();
5189
5190 let w_q = x
5194 .const_f32_like(
5195 identity_matrix(d_model),
5196 Shape::from_dims(&[d_model, d_model]),
5197 )
5198 .unwrap();
5199 let w_k = x
5200 .const_f32_like(
5201 identity_matrix(d_model),
5202 Shape::from_dims(&[d_model, d_model]),
5203 )
5204 .unwrap();
5205 let w_v = x
5206 .const_f32_like(
5207 identity_matrix(d_model),
5208 Shape::from_dims(&[d_model, d_model]),
5209 )
5210 .unwrap();
5211 let w_o = x
5212 .const_f32_like(
5213 identity_matrix(d_model),
5214 Shape::from_dims(&[d_model, d_model]),
5215 )
5216 .unwrap();
5217
5218 let x_norm = x.rms_norm_last_dim(1e-6).unwrap();
5220 let q = x_norm.matmul(&w_q).unwrap();
5221 let k = x_norm.matmul(&w_k).unwrap();
5222 let v = x_norm.matmul(&w_v).unwrap();
5223
5224 let q_h = q
5226 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5227 .unwrap()
5228 .permute([0, 2, 1, 3_usize])
5229 .unwrap();
5230 let k_h = k
5231 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5232 .unwrap()
5233 .permute([0, 2, 1, 3_usize])
5234 .unwrap();
5235 let v_h = v
5236 .reshape(Shape::from_dims(&[1, seq, num_heads, d_head]))
5237 .unwrap()
5238 .permute([0, 2, 1, 3_usize])
5239 .unwrap();
5240
5241 let q_r = q_h.rope(10000.0, 0).unwrap();
5243 let k_r = k_h.rope(10000.0, 0).unwrap();
5244
5245 let k_t = k_r.transpose().unwrap();
5247 let scores = q_r.matmul(&k_t).unwrap();
5248 let attn = scores.softmax_last_dim().unwrap();
5249 let attn_v = attn.matmul(&v_h).unwrap();
5250
5251 let merged = attn_v
5253 .permute([0, 2, 1, 3_usize])
5254 .unwrap()
5255 .reshape(Shape::from_dims(&[1, seq, d_model]))
5256 .unwrap();
5257 let attn_out = merged.matmul(&w_o).unwrap();
5258 let h = x.add(&attn_out).unwrap();
5259
5260 let result = h.realize_f32();
5262 assert_eq!(result.len(), seq * d_model);
5263 for &v in &result {
5264 assert!(
5265 v.is_finite(),
5266 "bridge-based LLaMA block output non-finite: {v}"
5267 );
5268 }
5269 }
5270
5271 fn identity_matrix(n: usize) -> Vec<f32> {
5273 let mut out = vec![0.0_f32; n * n];
5274 for i in 0..n {
5275 out[i * n + i] = 1.0;
5276 }
5277 out
5278 }
5279}
5280
5281impl Tensor {
5285 pub fn const_u32_like(
5287 &self,
5288 data: impl Into<Arc<[u32]>>,
5289 shape: impl Into<Shape>,
5290 ) -> std::result::Result<Self, fuel_ir::Error> {
5291 Ok(Self {
5292 inner: self.inner.const_u32_like(data, shape)?,
5293 })
5294 }
5295
5296 pub fn const_placeholder_like(&self, shape: impl Into<Shape>, dtype: fuel_ir::DType) -> Self {
5307 Self {
5308 inner: self.inner.const_placeholder_like(shape, dtype),
5309 }
5310 }
5311
5312 pub fn write_slice(&self, source: &Self, ranges: Vec<(usize, usize)>) -> crate::Result<Self> {
5324 let inner = self.inner.write_slice(&source.inner, ranges)?;
5325 Ok(Self { inner })
5326 }
5327
5328 pub fn write_slice_dyn(
5336 &self,
5337 source: &Self,
5338 ranges: Vec<(usize, usize)>,
5339 dyn_axis: usize,
5340 offset: fuel_ir::DynScalar,
5341 ) -> crate::Result<Self> {
5342 let inner = self
5343 .inner
5344 .write_slice_dyn(&source.inner, ranges, dyn_axis, offset)?;
5345 Ok(Self { inner })
5346 }
5347
5348 pub fn write_slice_rotating(
5363 &self,
5364 source: &Self,
5365 position: &Self,
5366 axis: usize,
5367 modulus: usize,
5368 ranges: Vec<(usize, usize)>,
5369 ) -> crate::Result<Self> {
5370 let inner = self.inner.write_slice_rotating(
5371 &source.inner,
5372 &position.inner,
5373 axis,
5374 modulus,
5375 ranges,
5376 )?;
5377 Ok(Self { inner })
5378 }
5379
5380 pub fn write_slice_doff(
5393 &self,
5394 source: &Self,
5395 offset: &Self,
5396 axis: usize,
5397 ranges: Vec<(usize, usize)>,
5398 ) -> crate::Result<Self> {
5399 let inner = self
5400 .inner
5401 .write_slice_doff(&source.inner, &offset.inner, axis, ranges)?;
5402 Ok(Self { inner })
5403 }
5404
5405 pub fn conv2d(
5416 &self,
5417 weight: &Self,
5418 bias: Option<&Self>,
5419 stride: (usize, usize),
5420 padding: (usize, usize),
5421 groups: usize,
5422 ) -> std::result::Result<Self, fuel_ir::Error> {
5423 if groups < 1 {
5424 return Err(
5425 fuel_ir::Error::Msg(format!("conv2d: groups must be >= 1, got {groups}",)).bt(),
5426 );
5427 }
5428 let x_shape = self.inner.shape();
5429 let x_dims = x_shape.dims();
5430 let w_shape = weight.inner.shape();
5431 let w_dims = w_shape.dims();
5432 if x_dims.len() != 4 {
5433 return Err(fuel_ir::Error::Msg(format!(
5434 "conv2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5435 ))
5436 .bt());
5437 }
5438 if w_dims.len() != 4 {
5439 return Err(fuel_ir::Error::Msg(format!(
5440 "conv2d: weight must be rank 4 [Cout, Cin/groups, Kh, Kw], got {w_dims:?}",
5441 ))
5442 .bt());
5443 }
5444 let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5445 let (cout, cin_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5446 if cin != cin_per_g * groups {
5447 return Err(fuel_ir::Error::Msg(format!(
5448 "conv2d: x has {cin} in-channels but weight expects {} ({cin_per_g}*{groups})",
5449 cin_per_g * groups,
5450 ))
5451 .bt());
5452 }
5453 if cout % groups != 0 {
5454 return Err(fuel_ir::Error::Msg(format!(
5455 "conv2d: Cout={cout} must be divisible by groups={groups}",
5456 ))
5457 .bt());
5458 }
5459 if let Some(b) = bias {
5460 let b_shape = b.inner.shape();
5461 let b_dims = b_shape.dims();
5462 if b_dims != [cout] {
5463 return Err(fuel_ir::Error::Msg(format!(
5464 "conv2d: bias shape {b_dims:?} must match [Cout={cout}]",
5465 ))
5466 .bt());
5467 }
5468 }
5469 let (stride_h, stride_w) = stride;
5470 let (pad_h, pad_w) = padding;
5471 if stride_h < 1 || stride_w < 1 {
5472 return Err(fuel_ir::Error::Msg(format!(
5473 "conv2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5474 ))
5475 .bt());
5476 }
5477 let h_padded = h_in + 2 * pad_h;
5478 let w_padded = w_in + 2 * pad_w;
5479 if h_padded < kh || w_padded < kw {
5480 return Err(fuel_ir::Error::Msg(format!(
5481 "conv2d: padded input ({h_padded}x{w_padded}) smaller than kernel ({kh}x{kw})",
5482 ))
5483 .bt());
5484 }
5485 Ok(Self {
5486 inner: self.inner.conv2d(
5487 &weight.inner,
5488 bias.map(|b| &b.inner),
5489 stride,
5490 padding,
5491 groups,
5492 ),
5493 })
5494 }
5495
5496 pub fn flash_attn(
5506 &self,
5507 k: &Self,
5508 v: &Self,
5509 alibi_slopes: Option<&Self>,
5510 softmax_scale: f32,
5511 causal: bool,
5512 window_size_left: Option<usize>,
5513 window_size_right: Option<usize>,
5514 softcap: Option<f32>,
5515 ) -> std::result::Result<Self, fuel_ir::Error> {
5516 let q_shape = self.inner.shape();
5517 let q_dims = q_shape.dims();
5518 let k_shape = k.inner.shape();
5519 let k_dims = k_shape.dims();
5520 let v_shape = v.inner.shape();
5521 let v_dims = v_shape.dims();
5522 if q_dims.len() != 4 {
5523 return Err(fuel_ir::Error::Msg(format!(
5524 "flash_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5525 ))
5526 .bt());
5527 }
5528 if k_dims.len() != 4 {
5529 return Err(fuel_ir::Error::Msg(format!(
5530 "flash_attn: k must be rank 4 [B, Hkv, Sk, D], got {k_dims:?}",
5531 ))
5532 .bt());
5533 }
5534 if v_dims.len() != 4 {
5535 return Err(fuel_ir::Error::Msg(format!(
5536 "flash_attn: v must be rank 4 [B, Hkv, Sk, D], got {v_dims:?}",
5537 ))
5538 .bt());
5539 }
5540 let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5541 let (bk, hkv, sk, dk) = (k_dims[0], k_dims[1], k_dims[2], k_dims[3]);
5542 let (bv, hkv_v, sk_v, dv) = (v_dims[0], v_dims[1], v_dims[2], v_dims[3]);
5543 if b != bk || b != bv {
5544 return Err(fuel_ir::Error::Msg(
5545 format!("flash_attn: B mismatch q={b} k={bk} v={bv}",),
5546 )
5547 .bt());
5548 }
5549 if hkv != hkv_v {
5550 return Err(fuel_ir::Error::Msg(format!(
5551 "flash_attn: Hkv mismatch k={hkv} vs v={hkv_v}",
5552 ))
5553 .bt());
5554 }
5555 if sk != sk_v {
5556 return Err(fuel_ir::Error::Msg(
5557 format!("flash_attn: Sk mismatch k={sk} vs v={sk_v}",),
5558 )
5559 .bt());
5560 }
5561 if d != dk || d != dv {
5562 return Err(fuel_ir::Error::Msg(format!(
5563 "flash_attn: head_dim mismatch q={d} k={dk} v={dv}",
5564 ))
5565 .bt());
5566 }
5567 if hkv == 0 || hq % hkv != 0 {
5568 return Err(fuel_ir::Error::Msg(format!(
5569 "flash_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5570 ))
5571 .bt());
5572 }
5573 if let Some(a) = alibi_slopes {
5574 let a_shape = a.inner.shape();
5575 let a_dims = a_shape.dims();
5576 if a_dims != [hq] {
5577 return Err(fuel_ir::Error::Msg(format!(
5578 "flash_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5579 ))
5580 .bt());
5581 }
5582 }
5583 Ok(Self {
5584 inner: self.inner.flash_attn(
5585 &k.inner,
5586 &v.inner,
5587 alibi_slopes.map(|t| &t.inner),
5588 softmax_scale,
5589 causal,
5590 window_size_left,
5591 window_size_right,
5592 softcap,
5593 ),
5594 })
5595 }
5596
5597 pub fn paged_attn(
5607 &self,
5608 k_cache: &Self,
5609 v_cache: &Self,
5610 block_table: &Self,
5611 context_lens: &Self,
5612 alibi_slopes: Option<&Self>,
5613 softmax_scale: f32,
5614 block_size: usize,
5615 softcap: Option<f32>,
5616 ) -> std::result::Result<Self, fuel_ir::Error> {
5617 if block_size < 1 {
5618 return Err(fuel_ir::Error::Msg("paged_attn: block_size must be >= 1".into()).bt());
5619 }
5620 let q_shape = self.inner.shape();
5621 let q_dims = q_shape.dims();
5622 let kc_shape = k_cache.inner.shape();
5623 let kc_dims = kc_shape.dims();
5624 let vc_shape = v_cache.inner.shape();
5625 let vc_dims = vc_shape.dims();
5626 let bt_shape = block_table.inner.shape();
5627 let bt_dims = bt_shape.dims();
5628 let cl_shape = context_lens.inner.shape();
5629 let cl_dims = cl_shape.dims();
5630 if q_dims.len() != 4 {
5631 return Err(fuel_ir::Error::Msg(format!(
5632 "paged_attn: q must be rank 4 [B, Hq, Sq, D], got {q_dims:?}",
5633 ))
5634 .bt());
5635 }
5636 if kc_dims.len() != 4 {
5637 return Err(fuel_ir::Error::Msg(format!(
5638 "paged_attn: k_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {kc_dims:?}",
5639 )).bt());
5640 }
5641 if vc_dims.len() != 4 {
5642 return Err(fuel_ir::Error::Msg(format!(
5643 "paged_attn: v_cache must be rank 4 [num_blocks, block_size, Hkv, D], got {vc_dims:?}",
5644 )).bt());
5645 }
5646 if bt_dims.len() != 2 {
5647 return Err(fuel_ir::Error::Msg(format!(
5648 "paged_attn: block_table must be rank 2 [B, max_blocks], got {bt_dims:?}",
5649 ))
5650 .bt());
5651 }
5652 if cl_dims.len() != 1 {
5653 return Err(fuel_ir::Error::Msg(format!(
5654 "paged_attn: context_lens must be rank 1 [B], got {cl_dims:?}",
5655 ))
5656 .bt());
5657 }
5658 let (b, hq, _sq, d) = (q_dims[0], q_dims[1], q_dims[2], q_dims[3]);
5659 if kc_dims[1] != block_size {
5660 return Err(fuel_ir::Error::Msg(format!(
5661 "paged_attn: k_cache block dim {} != block_size {block_size}",
5662 kc_dims[1],
5663 ))
5664 .bt());
5665 }
5666 if vc_dims[1] != block_size {
5667 return Err(fuel_ir::Error::Msg(format!(
5668 "paged_attn: v_cache block dim {} != block_size {block_size}",
5669 vc_dims[1],
5670 ))
5671 .bt());
5672 }
5673 let hkv = kc_dims[2];
5674 if vc_dims[2] != hkv {
5675 return Err(fuel_ir::Error::Msg(format!(
5676 "paged_attn: Hkv mismatch k_cache={hkv} vs v_cache={}",
5677 vc_dims[2],
5678 ))
5679 .bt());
5680 }
5681 if kc_dims[3] != d || vc_dims[3] != d {
5682 return Err(fuel_ir::Error::Msg(format!(
5683 "paged_attn: D mismatch q={d} k={} v={}",
5684 kc_dims[3], vc_dims[3],
5685 ))
5686 .bt());
5687 }
5688 if hkv == 0 || hq % hkv != 0 {
5689 return Err(fuel_ir::Error::Msg(format!(
5690 "paged_attn: Hq={hq} must be a positive multiple of Hkv={hkv}",
5691 ))
5692 .bt());
5693 }
5694 if bt_dims[0] != b {
5695 return Err(fuel_ir::Error::Msg(format!(
5696 "paged_attn: block_table batch dim {} != B={b}",
5697 bt_dims[0],
5698 ))
5699 .bt());
5700 }
5701 if cl_dims[0] != b {
5702 return Err(fuel_ir::Error::Msg(format!(
5703 "paged_attn: context_lens len {} != B={b}",
5704 cl_dims[0],
5705 ))
5706 .bt());
5707 }
5708 if block_table.inner.dtype() != fuel_ir::DType::U32 {
5709 return Err(fuel_ir::Error::Msg(format!(
5710 "paged_attn: block_table must be U32, got {:?}",
5711 block_table.inner.dtype(),
5712 ))
5713 .bt());
5714 }
5715 if context_lens.inner.dtype() != fuel_ir::DType::U32 {
5716 return Err(fuel_ir::Error::Msg(format!(
5717 "paged_attn: context_lens must be U32, got {:?}",
5718 context_lens.inner.dtype(),
5719 ))
5720 .bt());
5721 }
5722 if let Some(a) = alibi_slopes {
5723 let a_shape = a.inner.shape();
5724 let a_dims = a_shape.dims();
5725 if a_dims != [hq] {
5726 return Err(fuel_ir::Error::Msg(format!(
5727 "paged_attn: alibi_slopes must be [Hq={hq}], got {a_dims:?}",
5728 ))
5729 .bt());
5730 }
5731 }
5732 Ok(Self {
5733 inner: self.inner.paged_attn(
5734 &k_cache.inner,
5735 &v_cache.inner,
5736 &block_table.inner,
5737 &context_lens.inner,
5738 alibi_slopes.map(|t| &t.inner),
5739 softmax_scale,
5740 block_size,
5741 softcap,
5742 ),
5743 })
5744 }
5745
5746 pub fn conv_transpose2d(
5756 &self,
5757 weight: &Self,
5758 stride: (usize, usize),
5759 padding: (usize, usize),
5760 output_padding: (usize, usize),
5761 dilation: (usize, usize),
5762 groups: usize,
5763 ) -> std::result::Result<Self, fuel_ir::Error> {
5764 if groups < 1 {
5765 return Err(fuel_ir::Error::Msg(format!(
5766 "conv_transpose2d: groups must be >= 1, got {groups}",
5767 ))
5768 .bt());
5769 }
5770 let x_shape = self.inner.shape();
5771 let x_dims = x_shape.dims();
5772 let w_shape = weight.inner.shape();
5773 let w_dims = w_shape.dims();
5774 if x_dims.len() != 4 {
5775 return Err(fuel_ir::Error::Msg(format!(
5776 "conv_transpose2d: x must be rank 4 [N, Cin, H, W], got {x_dims:?}",
5777 ))
5778 .bt());
5779 }
5780 if w_dims.len() != 4 {
5781 return Err(fuel_ir::Error::Msg(format!(
5782 "conv_transpose2d: weight must be rank 4 [Cin, Cout/groups, Kh, Kw], got {w_dims:?}",
5783 )).bt());
5784 }
5785 let (cin, h_in, w_in) = (x_dims[1], x_dims[2], x_dims[3]);
5786 let (cin_w, cout_per_g, kh, kw) = (w_dims[0], w_dims[1], w_dims[2], w_dims[3]);
5787 if cin != cin_w {
5788 return Err(fuel_ir::Error::Msg(format!(
5789 "conv_transpose2d: x has {cin} in-channels but weight has {cin_w}",
5790 ))
5791 .bt());
5792 }
5793 if cin % groups != 0 {
5794 return Err(fuel_ir::Error::Msg(format!(
5795 "conv_transpose2d: Cin={cin} must be divisible by groups={groups}",
5796 ))
5797 .bt());
5798 }
5799 let (stride_h, stride_w) = stride;
5800 let (pad_h, pad_w) = padding;
5801 let (out_pad_h, out_pad_w) = output_padding;
5802 let (dil_h, dil_w) = dilation;
5803 if stride_h < 1 || stride_w < 1 {
5804 return Err(fuel_ir::Error::Msg(format!(
5805 "conv_transpose2d: stride must be >= 1, got ({stride_h}, {stride_w})",
5806 ))
5807 .bt());
5808 }
5809 if dil_h < 1 || dil_w < 1 {
5810 return Err(fuel_ir::Error::Msg(format!(
5811 "conv_transpose2d: dilation must be >= 1, got ({dil_h}, {dil_w})",
5812 ))
5813 .bt());
5814 }
5815 let h_out = h_in.saturating_sub(1) * stride_h + dil_h * (kh - 1) + out_pad_h + 1;
5816 let w_out = w_in.saturating_sub(1) * stride_w + dil_w * (kw - 1) + out_pad_w + 1;
5817 if h_out <= 2 * pad_h || w_out <= 2 * pad_w {
5818 return Err(fuel_ir::Error::Msg(format!(
5819 "conv_transpose2d: padding ({pad_h}x{pad_w}) is larger than the produced output dims ({h_out}x{w_out})",
5820 )).bt());
5821 }
5822 let _ = cout_per_g;
5823 Ok(Self {
5824 inner: self.inner.conv_transpose2d(
5825 &weight.inner,
5826 stride,
5827 padding,
5828 output_padding,
5829 dilation,
5830 groups,
5831 ),
5832 })
5833 }
5834
5835 pub fn conv_transpose1d(
5849 &self,
5850 weight: &Self,
5851 stride: usize,
5852 padding: usize,
5853 output_padding: usize,
5854 dilation: usize,
5855 groups: usize,
5856 ) -> std::result::Result<Self, fuel_ir::Error> {
5857 if groups < 1 {
5858 return Err(fuel_ir::Error::Msg(format!(
5859 "conv_transpose1d: groups must be >= 1, got {groups}",
5860 ))
5861 .bt());
5862 }
5863 let x_shape = self.inner.shape();
5864 let x_dims = x_shape.dims();
5865 let w_shape = weight.inner.shape();
5866 let w_dims = w_shape.dims();
5867 if x_dims.len() != 3 {
5868 return Err(fuel_ir::Error::Msg(format!(
5869 "conv_transpose1d: x must be rank 3 [N, Cin, Lin], got {x_dims:?}",
5870 ))
5871 .bt());
5872 }
5873 if w_dims.len() != 3 {
5874 return Err(fuel_ir::Error::Msg(format!(
5875 "conv_transpose1d: weight must be rank 3 [Cin, Cout/groups, K], got {w_dims:?}",
5876 ))
5877 .bt());
5878 }
5879 if stride < 1 {
5880 return Err(fuel_ir::Error::Msg(format!(
5881 "conv_transpose1d: stride must be >= 1, got {stride}",
5882 ))
5883 .bt());
5884 }
5885 if dilation < 1 {
5886 return Err(fuel_ir::Error::Msg(format!(
5887 "conv_transpose1d: dilation must be >= 1, got {dilation}",
5888 ))
5889 .bt());
5890 }
5891 let cin = x_dims[1];
5892 let cin_w = w_dims[0];
5893 if cin != cin_w {
5894 return Err(fuel_ir::Error::Msg(format!(
5895 "conv_transpose1d: x has {cin} in-channels but weight has {cin_w}",
5896 ))
5897 .bt());
5898 }
5899 if !cin.is_multiple_of(groups) {
5900 return Err(fuel_ir::Error::Msg(format!(
5901 "conv_transpose1d: Cin={cin} must be divisible by groups={groups}",
5902 ))
5903 .bt());
5904 }
5905 Ok(Self {
5906 inner: self.inner.conv_transpose1d(
5907 &weight.inner,
5908 stride,
5909 padding,
5910 output_padding,
5911 dilation,
5912 groups,
5913 ),
5914 })
5915 }
5916}
5917
5918impl Tensor {
5927 pub fn unsqueeze<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5933 let shape = self.inner.shape();
5934 let dim = dim.to_index_plus_one(&shape, "unsqueeze")?;
5935 Ok(Self {
5936 inner: self.inner.try_unsqueeze(dim)?,
5937 })
5938 }
5939
5940 pub fn triu(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5945 Ok(Self {
5946 inner: self.inner.triu(diagonal)?,
5947 })
5948 }
5949
5950 pub fn tril(&self, diagonal: i64) -> std::result::Result<Self, fuel_ir::Error> {
5953 Ok(Self {
5954 inner: self.inner.tril(diagonal)?,
5955 })
5956 }
5957
5958 pub fn log_softmax_last_dim(&self) -> std::result::Result<Self, fuel_ir::Error> {
5962 Ok(Self {
5963 inner: self.inner.log_softmax_last_dim()?,
5964 })
5965 }
5966
5967 pub fn softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5976 let shape = self.inner.shape();
5977 let axis: usize = dim.to_index(&shape, "softmax")?;
5981 let m = self.max_keepdim(axis)?;
5982 let shifted = self.broadcast_sub(&m)?;
5983 let e = shifted.exp();
5984 let s = e.sum_keepdim(axis)?;
5985 e.broadcast_div(&s)
5986 }
5987
5988 pub fn log_softmax<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
5998 let shape = self.inner.shape();
5999 let axis: usize = dim.to_index(&shape, "log_softmax")?;
6000 let m = self.max_keepdim(axis)?;
6001 let shifted = self.broadcast_sub(&m)?;
6002 let lse = shifted.exp().sum_keepdim(axis)?.log();
6003 shifted.broadcast_sub(&lse)
6004 }
6005
6006 pub fn argmin_dim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6010 let shape = self.inner.shape();
6011 let dim = dim.to_index(&shape, "argmin_dim")?;
6012 Ok(Self {
6013 inner: self.inner.argmin_dim(dim),
6014 })
6015 }
6016
6017 pub fn masked_fill(
6023 &self,
6024 mask: &Self,
6025 value: fuel_ir::Scalar,
6026 ) -> std::result::Result<Self, fuel_ir::Error> {
6027 Ok(Self {
6028 inner: self.inner.masked_fill(&mask.inner, value)?,
6029 })
6030 }
6031
6032 pub fn index_add<D: Dim>(
6038 &self,
6039 dim: D,
6040 indices: &Self,
6041 src: &Self,
6042 ) -> std::result::Result<Self, fuel_ir::Error> {
6043 let shape = self.inner.shape();
6044 let dim = dim.to_index(&shape, "index_add")?;
6045 if indices.inner.dtype() != fuel_ir::DType::U32 {
6046 return Err(fuel_ir::Error::Msg(format!(
6047 "index_add: index must be U32, got {:?}",
6048 indices.inner.dtype(),
6049 ))
6050 .bt());
6051 }
6052 if self.inner.dtype() != src.inner.dtype() {
6053 return Err(fuel_ir::Error::Msg(format!(
6054 "index_add: base and src dtypes must match, got {:?} vs {:?}",
6055 self.inner.dtype(),
6056 src.inner.dtype(),
6057 ))
6058 .bt());
6059 }
6060 let base_dims = shape.dims();
6061 let src_shape = src.inner.shape();
6062 let src_dims = src_shape.dims();
6063 if base_dims.len() != src_dims.len() {
6064 return Err(fuel_ir::Error::Msg(format!(
6065 "index_add: base and src must have the same rank, got {} vs {}",
6066 base_dims.len(),
6067 src_dims.len(),
6068 ))
6069 .bt());
6070 }
6071 let idx_shape = indices.inner.shape();
6072 let idx_dims = idx_shape.dims();
6073 if idx_dims.len() != 1 {
6074 return Err(fuel_ir::Error::Msg(format!(
6075 "index_add: index must be rank 1, got {idx_dims:?}",
6076 ))
6077 .bt());
6078 }
6079 if src_dims[dim] != idx_dims[0] {
6080 return Err(fuel_ir::Error::Msg(format!(
6081 "index_add: src dim {dim} ({}) must match index length ({})",
6082 src_dims[dim], idx_dims[0],
6083 ))
6084 .bt());
6085 }
6086 Ok(Self {
6087 inner: self.inner.index_add(dim, &indices.inner, &src.inner),
6088 })
6089 }
6090
6091 pub fn scatter_add<D: Dim>(
6096 &self,
6097 dim: D,
6098 indices: &Self,
6099 src: &Self,
6100 ) -> std::result::Result<Self, fuel_ir::Error> {
6101 let shape = self.inner.shape();
6102 let dim = dim.to_index(&shape, "scatter_add")?;
6103 if indices.inner.dtype() != fuel_ir::DType::U32 {
6104 return Err(fuel_ir::Error::Msg(format!(
6105 "scatter_add: index must be U32, got {:?}",
6106 indices.inner.dtype(),
6107 ))
6108 .bt());
6109 }
6110 if self.inner.dtype() != src.inner.dtype() {
6111 return Err(fuel_ir::Error::Msg(format!(
6112 "scatter_add: base and src dtypes must match, got {:?} vs {:?}",
6113 self.inner.dtype(),
6114 src.inner.dtype(),
6115 ))
6116 .bt());
6117 }
6118 let idx_shape = indices.inner.shape();
6119 let src_shape = src.inner.shape();
6120 if idx_shape.dims() != src_shape.dims() {
6121 return Err(fuel_ir::Error::Msg(format!(
6122 "scatter_add: index and src must have the same shape, got {:?} vs {:?}",
6123 idx_shape.dims(),
6124 src_shape.dims(),
6125 ))
6126 .bt());
6127 }
6128 Ok(Self {
6129 inner: self.inner.scatter_add(dim, &indices.inner, &src.inner),
6130 })
6131 }
6132
6133 pub fn relu_inplace(&self) -> Self {
6143 Self {
6144 inner: self.inner.relu_inplace(),
6145 }
6146 }
6147
6148 pub fn silu_inplace(&self) -> Self {
6151 Self {
6152 inner: self.inner.silu_inplace(),
6153 }
6154 }
6155
6156 pub fn gelu_tanh_inplace(&self) -> Self {
6159 Self {
6160 inner: self.inner.gelu_tanh_inplace(),
6161 }
6162 }
6163
6164 pub fn tanh_inplace(&self) -> Self {
6167 Self {
6168 inner: self.inner.tanh_inplace(),
6169 }
6170 }
6171
6172 pub fn sigmoid_inplace(&self) -> Self {
6175 Self {
6176 inner: self.inner.sigmoid_inplace(),
6177 }
6178 }
6179
6180 pub fn affine_inplace(&self, mul: f64, add: f64) -> Self {
6183 Self {
6184 inner: self.inner.affine_inplace(mul, add),
6185 }
6186 }
6187
6188 pub fn const_f64_like(
6192 &self,
6193 data: impl Into<Arc<[f64]>>,
6194 shape: impl Into<Shape>,
6195 ) -> std::result::Result<Self, fuel_ir::Error> {
6196 Ok(Self {
6197 inner: self.inner.const_f64_like(data, shape)?,
6198 })
6199 }
6200
6201 pub fn const_i64_like(
6205 &self,
6206 data: impl Into<Arc<[i64]>>,
6207 shape: impl Into<Shape>,
6208 ) -> std::result::Result<Self, fuel_ir::Error> {
6209 Ok(Self {
6210 inner: self.inner.const_i64_like(data, shape)?,
6211 })
6212 }
6213
6214 pub fn on_device(self, device: &Device) -> Self {
6220 Self {
6221 inner: self.inner.on_device(device.location()),
6222 }
6223 }
6224
6225 pub fn release(&self) -> Self {
6229 Self {
6230 inner: self.inner.release(),
6231 }
6232 }
6233
6234 pub fn move_to_device(&self, device: &Device) -> Self {
6237 Self {
6238 inner: self.inner.move_to_device(device.location()),
6239 }
6240 }
6241
6242 pub fn copy_to_device(&self, device: &Device) -> Self {
6245 Self {
6246 inner: self.inner.copy_to_device(device.location()),
6247 }
6248 }
6249
6250 pub fn backward(&self) -> fuel_graph::GradMap {
6257 self.inner.backward()
6258 }
6259}
6260
6261impl Tensor {
6269 pub fn transpose_last_two(&self) -> std::result::Result<Self, fuel_ir::Error> {
6274 self.transpose()
6275 }
6276
6277 pub fn t(&self) -> std::result::Result<Self, fuel_ir::Error> {
6280 self.transpose()
6281 }
6282
6283 pub fn transpose_dims<D1: Dim, D2: Dim>(
6288 &self,
6289 dim1: D1,
6290 dim2: D2,
6291 ) -> std::result::Result<Self, fuel_ir::Error> {
6292 let shape = self.shape();
6293 let dim1 = dim1.to_index(&shape, "transpose_dims")?;
6294 let dim2 = dim2.to_index(&shape, "transpose_dims")?;
6295 if dim1 == dim2 {
6296 return Ok(self.clone());
6297 }
6298 let rank = shape.dims().len();
6299 let mut axes: Vec<usize> = (0..rank).collect();
6300 axes.swap(dim1, dim2);
6301 self.permute(axes.as_slice())
6302 }
6303
6304 pub fn flatten<D1: Dim, D2: Dim>(
6309 &self,
6310 start_dim: D1,
6311 end_dim: D2,
6312 ) -> std::result::Result<Self, fuel_ir::Error> {
6313 let shape = self.shape();
6314 let rank = shape.dims().len();
6315 if rank == 0 {
6316 return Ok(self.clone());
6317 }
6318 let start_dim = start_dim.to_index(&shape, "flatten")?;
6319 let end_dim = end_dim.to_index(&shape, "flatten")?;
6320 if start_dim > end_dim {
6321 return Err(fuel_ir::Error::Msg(format!(
6322 "flatten: start_dim={start_dim} > end_dim={end_dim}",
6323 ))
6324 .bt());
6325 }
6326 let dims = shape.dims();
6327 let merged: usize = dims[start_dim..=end_dim].iter().product();
6328 let mut new_dims: Vec<usize> = Vec::with_capacity(rank - (end_dim - start_dim));
6329 new_dims.extend_from_slice(&dims[..start_dim]);
6330 new_dims.push(merged);
6331 new_dims.extend_from_slice(&dims[end_dim + 1..]);
6332 self.reshape(new_dims)
6333 }
6334
6335 pub fn flatten_to<D: Dim>(&self, end_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6337 self.flatten(0_usize, end_dim)
6338 }
6339
6340 pub fn flatten_from<D: Dim>(&self, start_dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6342 let rank = self.shape().dims().len();
6343 if rank == 0 {
6344 return Ok(self.clone());
6345 }
6346 self.flatten(start_dim, rank - 1)
6347 }
6348
6349 pub fn flatten_all(&self) -> std::result::Result<Self, fuel_ir::Error> {
6351 let rank = self.shape().dims().len();
6352 if rank == 0 {
6353 return Ok(self.clone());
6354 }
6355 self.flatten(0, rank - 1)
6356 }
6357
6358 pub fn stack<D: Dim>(args: &[&Self], dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6363 if args.is_empty() {
6364 return Err(fuel_ir::Error::Msg("stack: requires at least one tensor".into()).bt());
6365 }
6366 let reference_shape = args[0].shape();
6367 let reference_dims = reference_shape.dims().to_vec();
6368 let dim = dim.to_index_plus_one(&reference_shape, "stack")?;
6369 for (idx, t) in args.iter().enumerate().skip(1) {
6370 if t.shape().dims() != reference_dims.as_slice() {
6371 return Err(fuel_ir::Error::Msg(format!(
6372 "stack: tensor {idx} shape {:?} != reference shape {:?}",
6373 t.shape().dims(),
6374 reference_dims,
6375 ))
6376 .bt());
6377 }
6378 }
6379 let mut iter = args.iter();
6381 let first = iter.next().unwrap().unsqueeze(dim)?;
6382 let mut acc = first;
6383 for t in iter {
6384 let u = t.unsqueeze(dim)?;
6385 acc = acc.concat(&u, dim)?;
6386 }
6387 Ok(acc)
6388 }
6389
6390 pub fn sum_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6400 let shape = self.shape();
6401 let dim = dim.to_index(&shape, "sum_keepdim")?;
6402 self.sum_dim(dim).unwrap().unsqueeze(dim)
6403 }
6404
6405 pub fn mean_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6407 let shape = self.shape();
6408 let dim = dim.to_index(&shape, "mean_keepdim")?;
6409 self.mean_dim(dim).unwrap().unsqueeze(dim)
6410 }
6411
6412 pub fn max_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6414 let shape = self.shape();
6415 let dim = dim.to_index(&shape, "max_keepdim")?;
6416 self.max_dim(dim).unwrap().unsqueeze(dim)
6417 }
6418
6419 pub fn min_keepdim<D: Dim>(&self, dim: D) -> std::result::Result<Self, fuel_ir::Error> {
6421 let shape = self.shape();
6422 let dim = dim.to_index(&shape, "min_keepdim")?;
6423 self.min_dim(dim).unwrap().unsqueeze(dim)
6427 }
6428
6429 pub fn var_keepdim<D: Dim>(
6443 &self,
6444 dim: D,
6445 correction: f64,
6446 ) -> std::result::Result<Self, fuel_ir::Error> {
6447 let shape = self.shape();
6448 let dim = dim.to_index(&shape, "var_keepdim")?;
6449 let dims = shape.dims();
6450 let n = dims[dim];
6451 let mean = self.mean_keepdim(dim)?;
6452 let deviation = self.broadcast_sub(&mean).unwrap();
6453 let squares = deviation.sqr();
6454 let summed = squares.sum_keepdim(dim)?;
6456 let divisor = n as f64 - correction;
6457 Ok(summed.mul_scalar(1.0 / divisor))
6458 }
6459
6460 pub fn var<D: Dim>(
6463 &self,
6464 dim: D,
6465 correction: f64,
6466 ) -> std::result::Result<Self, fuel_ir::Error> {
6467 let shape = self.shape();
6468 let dim = dim.to_index(&shape, "var")?;
6469 self.var_keepdim(dim, correction)?.squeeze(dim)
6470 }
6471
6472 pub fn affine(&self, mul: f64, add: f64) -> Self {
6479 self.mul_scalar(mul).add_scalar(add)
6480 }
6481
6482 pub fn scale_and_shift(&self, scale: f64, shift: f64) -> Self {
6486 self.affine(scale, shift)
6487 }
6488
6489 pub fn elu(&self, alpha: f64) -> Self {
6493 let neg_branch = self.exp().affine(alpha, -alpha);
6495 let zero = self
6500 .const_f32_like(vec![0.0; self.elem_count()], self.shape())
6501 .expect("elu: vec![_; self.elem_count()] against self.shape() -- same source");
6502 let mask = self.gt(&zero).unwrap();
6503 mask.where_cond(self, &neg_branch).unwrap()
6504 }
6505
6506 pub fn dot(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6509 let a = self.shape().dims().to_vec();
6510 let b = rhs.shape().dims().to_vec();
6511 if a.len() != 1 || b.len() != 1 {
6512 return Err(fuel_ir::Error::Msg(format!(
6513 "dot: requires rank-1 inputs, got lhs={a:?} rhs={b:?}",
6514 ))
6515 .bt());
6516 }
6517 if a[0] != b[0] {
6518 return Err(fuel_ir::Error::Msg(format!(
6519 "dot: length mismatch lhs={} rhs={}",
6520 a[0], b[0],
6521 ))
6522 .bt());
6523 }
6524 Ok(self.mul(rhs).unwrap().sum_all())
6525 }
6526
6527 pub fn mv(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6531 let a = self.shape().dims().to_vec();
6532 let b = rhs.shape().dims().to_vec();
6533 if a.len() != 2 || b.len() != 1 || a[1] != b[0] {
6534 return Err(fuel_ir::Error::Msg(format!(
6535 "mv: shape mismatch lhs={a:?} rhs={b:?} (need [m,n] · [n])",
6536 ))
6537 .bt());
6538 }
6539 let rhs_col = rhs.unsqueeze(1_usize)?;
6541 let prod = self.matmul(&rhs_col).unwrap();
6542 prod.squeeze(1_usize)
6543 }
6544
6545 pub fn matvec(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6548 self.mv(rhs)
6549 }
6550
6551 pub fn broadcast_matmul(&self, rhs: &Self) -> std::result::Result<Self, fuel_ir::Error> {
6555 self.matmul(rhs)
6556 }
6557
6558 pub fn ones_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6568 let n = self.elem_count();
6569 let shape = self.shape();
6570 match self.dtype() {
6571 DType::F32 => self.const_f32_like(vec![1.0_f32; n], shape),
6572 DType::F64 => self.const_f64_like(vec![1.0_f64; n], shape),
6573 DType::BF16 => self.const_bf16_like(vec![half::bf16::ONE; n], shape),
6574 DType::F16 => self.const_f16_like(vec![half::f16::ONE; n], shape),
6575 DType::U32 => self.const_u32_like(vec![1_u32; n], shape),
6576 DType::I64 => self.const_i64_like(vec![1_i64; n], shape),
6577 other => {
6578 Err(fuel_ir::Error::Msg(format!("ones_like: unsupported dtype {other:?}",)).bt())
6579 }
6580 }
6581 }
6582
6583 pub fn zeros_like(&self) -> std::result::Result<Self, fuel_ir::Error> {
6587 let n = self.elem_count();
6588 let shape = self.shape();
6589 match self.dtype() {
6590 DType::F32 => self.const_f32_like(vec![0.0_f32; n], shape),
6591 DType::F64 => self.const_f64_like(vec![0.0_f64; n], shape),
6592 DType::BF16 => self.const_bf16_like(vec![half::bf16::ZERO; n], shape),
6593 DType::F16 => self.const_f16_like(vec![half::f16::ZERO; n], shape),
6594 DType::U32 => self.const_u32_like(vec![0_u32; n], shape),
6595 DType::I64 => self.const_i64_like(vec![0_i64; n], shape),
6596 other => {
6597 Err(fuel_ir::Error::Msg(format!("zeros_like: unsupported dtype {other:?}",)).bt())
6598 }
6599 }
6600 }
6601
6602 pub fn ones(
6606 shape: impl Into<Shape>,
6607 dtype: DType,
6608 device: &Device,
6609 ) -> std::result::Result<Self, fuel_ir::Error> {
6610 let shape = shape.into();
6611 let n = shape.elem_count();
6612 match dtype {
6613 DType::F32 => Self::from_f32(vec![1.0_f32; n], shape, device),
6614 DType::F64 => Self::from_f64(vec![1.0_f64; n], shape, device),
6615 DType::BF16 => Self::from_bf16(vec![half::bf16::ONE; n], shape, device),
6616 DType::F16 => Self::from_f16(vec![half::f16::ONE; n], shape, device),
6617 DType::U32 => Self::from_u32(vec![1_u32; n], shape, device),
6618 other => Err(fuel_ir::Error::Msg(format!("ones: unsupported dtype {other:?}",)).bt()),
6619 }
6620 }
6621
6622 pub fn zeros(
6632 shape: impl Into<Shape>,
6633 dtype: DType,
6634 device: &Device,
6635 ) -> std::result::Result<Self, fuel_ir::Error> {
6636 let shape = shape.into();
6637 let n = shape.elem_count();
6638 match dtype {
6639 DType::F32 => Self::from_f32(vec![0.0_f32; n], shape, device),
6640 DType::F64 => Self::from_f64(vec![0.0_f64; n], shape, device),
6641 DType::BF16 => Self::from_bf16(vec![half::bf16::ZERO; n], shape, device),
6642 DType::F16 => Self::from_f16(vec![half::f16::ZERO; n], shape, device),
6643 DType::U32 => Self::from_u32(vec![0_u32; n], shape, device),
6644 other => Err(fuel_ir::Error::Msg(format!("zeros: unsupported dtype {other:?}",)).bt()),
6645 }
6646 }
6647
6648 pub fn full(
6657 shape: impl Into<Shape>,
6658 value: fuel_ir::Scalar,
6659 device: &Device,
6660 ) -> std::result::Result<Self, fuel_ir::Error> {
6661 let shape = shape.into();
6662 let n = shape.elem_count();
6663 match value {
6664 fuel_ir::Scalar::F32(v) => Self::from_f32(vec![v; n], shape, device),
6665 fuel_ir::Scalar::F64(v) => Self::from_f64(vec![v; n], shape, device),
6666 fuel_ir::Scalar::BF16(v) => Self::from_bf16(vec![v; n], shape, device),
6667 fuel_ir::Scalar::F16(v) => Self::from_f16(vec![v; n], shape, device),
6668 fuel_ir::Scalar::U32(v) => Self::from_u32(vec![v; n], shape, device),
6669 other => Err(fuel_ir::Error::Msg(format!(
6670 "full: unsupported scalar dtype {:?}",
6671 other.dtype(),
6672 ))
6673 .bt()),
6674 }
6675 }
6676
6677 pub fn eye(n: usize, dtype: DType, device: &Device) -> Self {
6680 let mut data = vec![0.0_f32; n * n];
6681 for i in 0..n {
6682 data[i * n + i] = 1.0;
6683 }
6684 let base = Self::from_f32(data, vec![n, n], device).expect(
6693 "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",
6694 );
6695 if dtype == DType::F32 {
6696 base
6697 } else {
6698 base.to_dtype(dtype).unwrap()
6699 }
6700 }
6701
6702 pub fn split_heads(
6708 &self,
6709 num_heads: usize,
6710 head_dim: usize,
6711 ) -> std::result::Result<Self, fuel_ir::Error> {
6712 let dims = self.inner.shape().dims().to_vec();
6713 debug_assert_eq!(
6714 dims.len(),
6715 3,
6716 "split_heads: input must be rank 3 (B, N, embed), got {dims:?}"
6717 );
6718 debug_assert_eq!(
6719 dims[2],
6720 num_heads * head_dim,
6721 "split_heads: trailing dim ({}) != num_heads * head_dim ({} * {} = {})",
6722 dims[2],
6723 num_heads,
6724 head_dim,
6725 num_heads * head_dim
6726 );
6727 let b = dims[0];
6728 let n = dims[1];
6729 self.reshape(Shape::from_dims(&[b, n, num_heads, head_dim]))?
6730 .permute([0, 2, 1, 3_usize])
6731 }
6732
6733 pub fn merge_heads(&self) -> std::result::Result<Self, fuel_ir::Error> {
6737 let dims = self.inner.shape().dims().to_vec();
6738 debug_assert_eq!(
6739 dims.len(),
6740 4,
6741 "merge_heads: input must be rank 4 (B, heads, N, head_dim), got {dims:?}"
6742 );
6743 let b = dims[0];
6744 let num_heads = dims[1];
6745 let n = dims[2];
6746 let head_dim = dims[3];
6747 self.permute([0, 2, 1, 3_usize])?
6748 .reshape(Shape::from_dims(&[b, n, num_heads * head_dim]))
6749 }
6750
6751 pub fn add_trailing_bias(
6762 &self,
6763 bias: std::sync::Arc<[f32]>,
6764 ) -> std::result::Result<Self, fuel_ir::Error> {
6765 let n = bias.len();
6766 let bias_t = self.const_like_dtype(&bias, Shape::from_dims(&[n]), self.dtype())?;
6772 self.broadcast_add(&bias_t)
6773 }
6774
6775 pub fn embed_tokens(
6787 embed_table: std::sync::Arc<[f32]>,
6788 vocab_size: usize,
6789 hidden: usize,
6790 tokens: &[u32],
6791 device: &crate::Device,
6792 ) -> std::result::Result<Self, fuel_ir::Error> {
6793 let seq = tokens.len();
6794 if seq == 0 {
6795 return Err(fuel_ir::Error::Msg("embed_tokens: tokens must be non-empty".into()).bt());
6796 }
6797 let embed = Self::from_f32(embed_table, Shape::from_dims(&[vocab_size, hidden]), device)?;
6798 let token_ids = embed.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6799 embed
6800 .index_select(0_usize, &token_ids)?
6801 .reshape(Shape::from_dims(&[1, seq, hidden]))
6802 }
6803
6804 pub fn embed_tokens_anchored(
6812 &self,
6813 embed_table: std::sync::Arc<[f32]>,
6814 vocab_size: usize,
6815 hidden: usize,
6816 tokens: &[u32],
6817 ) -> std::result::Result<Self, fuel_ir::Error> {
6818 let seq = tokens.len();
6819 if seq == 0 {
6820 return Err(fuel_ir::Error::Msg(
6821 "embed_tokens_anchored: tokens must be non-empty".into(),
6822 )
6823 .bt());
6824 }
6825 let embed = self.const_f32_like(embed_table, Shape::from_dims(&[vocab_size, hidden]))?;
6826 let token_ids = self.const_u32_like(tokens.to_vec(), Shape::from_dims(&[seq]))?;
6827 embed
6828 .index_select(0_usize, &token_ids)?
6829 .reshape(Shape::from_dims(&[1, seq, hidden]))
6830 }
6831
6832 pub fn rope_tables_const(
6843 &self,
6844 theta: f64,
6845 start_pos: usize,
6846 seq: usize,
6847 head_dim: usize,
6848 ) -> (Self, Self) {
6849 let (cos_data, sin_data) = fuel_graph::build_rope_tables(theta, start_pos, seq, head_dim);
6850 let rope_shape = Shape::from_dims(&[seq, head_dim]);
6851 let rope_cos = self
6857 .const_f32_like(cos_data, rope_shape.clone())
6858 .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6859 let rope_sin = self
6860 .const_f32_like(sin_data, rope_shape)
6861 .expect("rope tables: build_rope_tables must return seq*head_dim elements");
6862 (rope_cos, rope_sin)
6863 }
6864
6865 pub fn rope_delta_tables_const(
6877 &self,
6878 theta: f64,
6879 delta: usize,
6880 rows: usize,
6881 head_dim: usize,
6882 ) -> (Self, Self) {
6883 let (c1, s1) = fuel_graph::build_rope_tables(theta, delta, 1, head_dim);
6884 let mut cos = Vec::with_capacity(rows * head_dim);
6885 let mut sin = Vec::with_capacity(rows * head_dim);
6886 for _ in 0..rows {
6887 cos.extend_from_slice(&c1);
6888 sin.extend_from_slice(&s1);
6889 }
6890 let shape = Shape::from_dims(&[rows, head_dim]);
6891 (
6895 self.const_f32_like(cos, shape.clone()).expect(
6896 "rope tables: rows appends of c1 (local); c1 has head_dim elements (not local)",
6897 ),
6898 self.const_f32_like(sin, shape).expect(
6899 "rope tables: rows appends of s1 (local); s1 has head_dim elements (not local)",
6900 ),
6901 )
6902 }
6903
6904 pub fn rope_delta_rotate_block_f32(
6919 dev: &crate::Device,
6920 k_block: &[f32],
6921 theta: f64,
6922 delta: usize,
6923 block_size: usize,
6924 n_kv_heads: usize,
6925 head_dim: usize,
6926 ) -> Vec<f32> {
6927 let pool_shape = Shape::from_dims(&[block_size, n_kv_heads, head_dim]);
6928 let k = Tensor::from_f32(k_block.to_vec(), pool_shape, dev).expect(
6939 "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)",
6940 );
6941 let k4 = k
6942 .permute([1, 0, 2])
6943 .expect("rope_delta_rotate_block_f32: permute [bs,Hkv,D]->[Hkv,bs,D]")
6944 .reshape(Shape::from_dims(&[1, n_kv_heads, block_size, head_dim]))
6945 .expect("rope_delta_rotate_block_f32: reshape to [1,Hkv,bs,D]");
6946 let (cos, sin) = k4.rope_delta_tables_const(theta, delta, block_size, head_dim);
6947 k4.rope_with_tables_decomposed(&cos, &sin)
6948 .expect("rope_delta_rotate_block_f32: rope_with_tables_decomposed")
6949 .reshape(Shape::from_dims(&[n_kv_heads, block_size, head_dim]))
6950 .expect("rope_delta_rotate_block_f32: reshape back to [Hkv,bs,D]")
6951 .permute([1, 0, 2])
6952 .expect("rope_delta_rotate_block_f32: permute [Hkv,bs,D]->[bs,Hkv,D]")
6953 .realize_f32()
6954 }
6955
6956 pub fn rope_tables_const_batched(
6969 &self,
6970 theta: f64,
6971 positions: &[usize],
6972 head_dim: usize,
6973 ) -> (Self, Self) {
6974 let b = positions.len();
6975 let mut cos_data = Vec::with_capacity(b * head_dim);
6976 let mut sin_data = Vec::with_capacity(b * head_dim);
6977 for &pos in positions {
6978 let (c, s) = fuel_graph::build_rope_tables(theta, pos, 1, head_dim);
6979 cos_data.extend_from_slice(&c);
6980 sin_data.extend_from_slice(&s);
6981 }
6982 let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
6983 let rope_cos = self
6990 .const_f32_like(cos_data, shape.clone())
6991 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
6992 let rope_sin = self
6993 .const_f32_like(sin_data, shape)
6994 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables must yield head_dim per position (not local)");
6995 (rope_cos, rope_sin)
6996 }
6997
6998 pub fn rope_tables_const_with_inv_freq(
7003 &self,
7004 inv_freq: &[f64],
7005 start_pos: usize,
7006 seq: usize,
7007 head_dim: usize,
7008 ) -> (Self, Self) {
7009 let (cos_data, sin_data) =
7010 fuel_graph::build_rope_tables_with_inv_freq(inv_freq, start_pos, seq, head_dim);
7011 let rope_shape = Shape::from_dims(&[seq, head_dim]);
7012 let rope_cos = self.const_f32_like(cos_data, rope_shape.clone()).expect(
7015 "rope tables: build_rope_tables_with_inv_freq must return seq*head_dim elements",
7016 );
7017 let rope_sin = self.const_f32_like(sin_data, rope_shape).expect(
7018 "rope tables: build_rope_tables_with_inv_freq must return seq*head_dim elements",
7019 );
7020 (rope_cos, rope_sin)
7021 }
7022
7023 pub fn rope_tables_const_batched_with_inv_freq(
7026 &self,
7027 inv_freq: &[f64],
7028 positions: &[usize],
7029 head_dim: usize,
7030 ) -> (Self, Self) {
7031 let b = positions.len();
7032 let mut cos_data = Vec::with_capacity(b * head_dim);
7033 let mut sin_data = Vec::with_capacity(b * head_dim);
7034 for &pos in positions {
7035 let (c, s) = fuel_graph::build_rope_tables_with_inv_freq(inv_freq, pos, 1, head_dim);
7036 cos_data.extend_from_slice(&c);
7037 sin_data.extend_from_slice(&s);
7038 }
7039 let shape = Shape::from_dims(&[b, 1, 1, head_dim]);
7040 let rope_cos = self
7043 .const_f32_like(cos_data, shape.clone())
7044 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables_with_inv_freq must yield head_dim per position (not local)");
7045 let rope_sin = self
7046 .const_f32_like(sin_data, shape)
7047 .expect("batched rope tables: b IS positions.len() (local); build_rope_tables_with_inv_freq must yield head_dim per position (not local)");
7048 (rope_cos, rope_sin)
7049 }
7050
7051 pub fn rope_batched(
7071 &self,
7072 cos: &Self,
7073 sin: &Self,
7074 ) -> std::result::Result<Self, fuel_ir::Error> {
7075 let dims: Vec<usize> = self.inner.shape().dims().to_vec();
7076 let rank = dims.len();
7077 if rank < 2 {
7078 return Err(fuel_ir::Error::Msg(format!(
7079 "rope_batched: expected rank >= 2, got {dims:?}",
7080 ))
7081 .bt());
7082 }
7083 let d = dims[rank - 1];
7084 if !d.is_multiple_of(2) {
7085 return Err(
7086 fuel_ir::Error::Msg(format!("rope_batched: head_dim {d} must be even",)).bt(),
7087 );
7088 }
7089 let half = d / 2;
7090 let target = Shape::from_dims(&dims);
7091
7092 let cos_b = cos.broadcast_to(target.clone())?;
7093 let sin_b = sin.broadcast_to(target)?;
7094
7095 let first = self.slice(rank - 1, 0, half)?;
7096 let second = self.slice(rank - 1, half, half)?;
7097 let rotated = second.neg().concat(&first, rank - 1)?;
7098
7099 let left = self.mul(&cos_b)?;
7100 let right = rotated.mul(&sin_b)?;
7101 left.add(&right)
7102 }
7103
7104 pub fn rope_partial(
7115 &self,
7116 rope_cos: &Self,
7117 rope_sin: &Self,
7118 rope_dim: usize,
7119 ) -> std::result::Result<Self, fuel_ir::Error> {
7120 let dims = self.inner.shape();
7121 let dims = dims.dims();
7122 let head_dim = *dims.last().ok_or_else(|| {
7123 fuel_ir::Error::Msg("rope_partial: receiver must have at least one dimension".into())
7124 .bt()
7125 })?;
7126 if rope_dim == head_dim {
7127 return self.rope_with_tables(rope_cos, rope_sin);
7128 }
7129 if rope_dim > head_dim {
7130 return Err(fuel_ir::Error::Msg(format!(
7131 "rope_partial: rope_dim={rope_dim} exceeds head_dim={head_dim}",
7132 ))
7133 .bt());
7134 }
7135 let last = dims.len() - 1;
7136 let pass_dim = head_dim - rope_dim;
7137 let rot = self.slice(last, 0, rope_dim)?;
7138 let pass = self.slice(last, rope_dim, pass_dim)?;
7139 let rot_rotated = rot.rope_with_tables(rope_cos, rope_sin)?;
7140 rot_rotated.concat(&pass, last)
7141 }
7142
7143 pub fn add_optional_trailing_bias(
7148 &self,
7149 bias: Option<&std::sync::Arc<[f32]>>,
7150 ) -> std::result::Result<Self, fuel_ir::Error> {
7151 match bias {
7152 None => Ok(self.clone()),
7153 Some(b) => self.add_trailing_bias(std::sync::Arc::clone(b)),
7154 }
7155 }
7156
7157 pub fn softcap(&self, cap: f64) -> Self {
7163 self.mul_scalar(1.0 / cap).tanh().mul_scalar(cap)
7164 }
7165
7166 pub fn softcap_optional(&self, cap: Option<f64>) -> Self {
7170 match cap {
7171 Some(c) if c > 0.0 => self.softcap(c),
7172 _ => self.clone(),
7173 }
7174 }
7175
7176 pub fn rms_norm_affine_with_offset(
7185 &self,
7186 gain: &[f32],
7187 offset: f32,
7188 eps: f64,
7189 ) -> std::result::Result<Self, fuel_ir::Error> {
7190 let shifted: std::sync::Arc<[f32]> =
7191 std::sync::Arc::from(gain.iter().map(|g| *g + offset).collect::<Vec<_>>());
7192 self.rms_norm_affine(shifted, eps)
7193 }
7194
7195 pub fn rms_norm_affine(
7200 &self,
7201 gain: std::sync::Arc<[f32]>,
7202 eps: f64,
7203 ) -> std::result::Result<Self, fuel_ir::Error> {
7204 let hidden = gain.len();
7205 let normed = self.rms_norm_last_dim(eps)?;
7206 let gain_t = self.const_f32_like(gain, Shape::from_dims(&[hidden]))?;
7207 normed.broadcast_mul(&gain_t)
7208 }
7209
7210 pub fn global_avg_pool_2d(&self) -> std::result::Result<Self, fuel_ir::Error> {
7220 let dims = self.inner.shape().dims().to_vec();
7221 debug_assert_eq!(
7222 dims.len(),
7223 4,
7224 "global_avg_pool_2d: input must be rank 4 (B, C, H, W), got {dims:?}"
7225 );
7226 self.mean_dim(3_usize)?.mean_dim(2_usize)
7228 }
7229
7230 pub fn channel_affine_4d(
7242 &self,
7243 gain: std::sync::Arc<[f32]>,
7244 bias: std::sync::Arc<[f32]>,
7245 ) -> std::result::Result<Self, fuel_ir::Error> {
7246 let dims = self.inner.shape().dims().to_vec();
7247 debug_assert_eq!(
7248 dims.len(),
7249 4,
7250 "channel_affine_4d: input must be rank 4 (B, C, H, W), got {dims:?}"
7251 );
7252 let channels = dims[1];
7253 debug_assert_eq!(
7254 gain.len(),
7255 channels,
7256 "channel_affine_4d: gain len ({}) != C ({})",
7257 gain.len(),
7258 channels
7259 );
7260 debug_assert_eq!(
7261 bias.len(),
7262 channels,
7263 "channel_affine_4d: bias len ({}) != C ({})",
7264 bias.len(),
7265 channels
7266 );
7267 let w_t = self
7268 .const_f32_like(gain, Shape::from_dims(&[channels]))?
7269 .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7270 let b_t = self
7271 .const_f32_like(bias, Shape::from_dims(&[channels]))?
7272 .reshape(Shape::from_dims(&[1, channels, 1, 1]))?;
7273 self.broadcast_mul(&w_t)?.broadcast_add(&b_t)
7274 }
7275
7276 pub fn additive_causal_mask_like(anchor: &Tensor, seq_len: usize) -> Self {
7285 let mut data = vec![0.0_f32; seq_len * seq_len];
7286 for i in 0..seq_len {
7287 for j in (i + 1)..seq_len {
7288 data[i * seq_len + j] = f32::NEG_INFINITY;
7289 }
7290 }
7291 anchor
7295 .const_f32_like(
7296 std::sync::Arc::from(data),
7297 Shape::from_dims(&[seq_len, seq_len]),
7298 )
7299 .expect(
7300 "additive_causal_mask_like: vec![_; seq_len*seq_len] against [seq_len, seq_len]",
7301 )
7302 }
7303
7304 pub fn tril2(n: usize, dtype: DType, device: &Device) -> Self {
7307 let mut data = vec![0.0_f32; n * n];
7308 for i in 0..n {
7309 for j in 0..=i {
7310 data[i * n + j] = 1.0;
7311 }
7312 }
7313 let base = Self::from_f32(data, vec![n, n], device).expect(
7322 "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",
7323 );
7324 if dtype == DType::F32 {
7325 base
7326 } else {
7327 base.to_dtype(dtype).unwrap()
7328 }
7329 }
7330
7331 pub fn triu2(n: usize, dtype: DType, device: &Device) -> Self {
7333 let mut data = vec![0.0_f32; n * n];
7334 for i in 0..n {
7335 for j in i..n {
7336 data[i * n + j] = 1.0;
7337 }
7338 }
7339 let base = Self::from_f32(data, vec![n, n], device).expect(
7348 "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",
7349 );
7350 if dtype == DType::F32 {
7351 base
7352 } else {
7353 base.to_dtype(dtype).unwrap()
7354 }
7355 }
7356
7357 pub fn narrow<D: Dim>(
7364 &self,
7365 dim: D,
7366 start: usize,
7367 len: usize,
7368 ) -> std::result::Result<Self, fuel_ir::Error> {
7369 self.slice(dim, start, len)
7370 }
7371
7372 pub fn chunk<D: Dim>(
7378 &self,
7379 chunks: usize,
7380 dim: D,
7381 ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
7382 let shape = self.shape();
7383 let dim = dim.to_index(&shape, "chunk")?;
7384 if chunks == 0 {
7385 return Err(fuel_ir::Error::Msg("chunk: chunks must be > 0".into()).bt());
7386 }
7387 let dims = shape.dims();
7388 let size = dims[dim];
7389 if size < chunks {
7390 return Ok((0..size).map(|i| self.slice(dim, i, 1).unwrap()).collect());
7391 }
7392 let base = size / chunks;
7393 let extra = size % chunks;
7394 let mut out = Vec::with_capacity(chunks);
7395 let mut start = 0;
7396 for i in 0..chunks {
7397 let len = if i < extra { base + 1 } else { base };
7398 out.push(self.slice(dim, start, len).unwrap());
7399 start += len;
7400 }
7401 Ok(out)
7402 }
7403
7404 pub fn get(&self, i: usize) -> std::result::Result<Self, fuel_ir::Error> {
7407 let dims = self.shape().dims().to_vec();
7408 if dims.is_empty() {
7409 return Ok(self.clone());
7410 }
7411 self.slice(0, i, 1).unwrap().squeeze(0)
7412 }
7413
7414 pub fn get_on_dim<D: Dim>(
7418 &self,
7419 dim: D,
7420 index: usize,
7421 ) -> std::result::Result<Self, fuel_ir::Error> {
7422 let shape = self.shape();
7423 let dim = dim.to_index(&shape, "get_on_dim")?;
7424 self.slice(dim, index, 1).unwrap().squeeze(dim)
7425 }
7426
7427 pub fn sum_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7431 let shape = self.shape();
7432 let mut sorted = dims.to_indexes(&shape, "sum_dims")?;
7433 sorted.sort_by(|a, b| b.cmp(a));
7434 sorted.dedup();
7435 let mut acc = self.clone();
7436 for d in sorted {
7437 acc = acc.sum_dim(d)?;
7438 }
7439 Ok(acc)
7440 }
7441
7442 pub fn mean_dims<D: Dims>(&self, dims: D) -> std::result::Result<Self, fuel_ir::Error> {
7445 let shape = self.shape();
7446 let mut sorted = dims.to_indexes(&shape, "mean_dims")?;
7447 sorted.sort_by(|a, b| b.cmp(a));
7448 sorted.dedup();
7449 let mut acc = self.clone();
7450 for d in sorted {
7451 acc = acc.mean_dim(d)?;
7452 }
7453 Ok(acc)
7454 }
7455
7456 pub fn sum_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7461 let mut sorted: Vec<usize> = dims.to_vec();
7462 sorted.sort();
7463 sorted.dedup();
7464 let mut acc = self.clone();
7465 for d in sorted {
7466 acc = acc.sum_keepdim(d)?;
7467 }
7468 Ok(acc)
7469 }
7470
7471 pub fn mean_dims_keepdim(&self, dims: &[usize]) -> std::result::Result<Self, fuel_ir::Error> {
7473 let mut sorted: Vec<usize> = dims.to_vec();
7474 sorted.sort();
7475 sorted.dedup();
7476 let mut acc = self.clone();
7477 for d in sorted {
7478 acc = acc.mean_keepdim(d)?;
7479 }
7480 Ok(acc)
7481 }
7482
7483 pub fn rand_like(&self, lo: f64, up: f64) -> std::result::Result<Self, fuel_ir::Error> {
7486 Self::rand(self.shape(), lo, up, self.dtype(), &Device::cpu())
7487 }
7488
7489 pub fn randn_like(&self, mean: f64, stdev: f64) -> std::result::Result<Self, fuel_ir::Error> {
7492 Self::randn(self.shape(), mean, stdev, self.dtype(), &Device::cpu())
7493 }
7494
7495 pub fn rand(
7499 shape: impl Into<Shape>,
7500 lo: f64,
7501 up: f64,
7502 dtype: DType,
7503 device: &Device,
7504 ) -> std::result::Result<Self, fuel_ir::Error> {
7505 use rand::RngExt;
7506 let shape = shape.into();
7507 let n = shape.elem_count();
7508 let mut rng = rand::rng();
7509 match dtype {
7510 DType::F32 => {
7511 let data: Vec<f32> = (0..n).map(|_| rng.random_range(lo..up) as f32).collect();
7512 Self::from_f32(data, shape, device)
7513 }
7514 DType::F64 => {
7515 let data: Vec<f64> = (0..n).map(|_| rng.random_range(lo..up)).collect();
7516 Self::from_f64(data, shape, device)
7517 }
7518 DType::BF16 => {
7519 let data: Vec<half::bf16> = (0..n)
7520 .map(|_| half::bf16::from_f64(rng.random_range(lo..up)))
7521 .collect();
7522 Self::from_bf16(data, shape, device)
7523 }
7524 DType::F16 => {
7525 let data: Vec<half::f16> = (0..n)
7526 .map(|_| half::f16::from_f64(rng.random_range(lo..up)))
7527 .collect();
7528 Self::from_f16(data, shape, device)
7529 }
7530 other => {
7531 Err(fuel_ir::Error::Msg(format!("Tensor::rand: unsupported dtype {other:?}",)).bt())
7532 }
7533 }
7534 }
7535
7536 pub fn randn(
7540 shape: impl Into<Shape>,
7541 mean: f64,
7542 stdev: f64,
7543 dtype: DType,
7544 device: &Device,
7545 ) -> std::result::Result<Self, fuel_ir::Error> {
7546 use rand_distr::{Distribution, Normal};
7547 let shape = shape.into();
7548 let n = shape.elem_count();
7549 let normal = Normal::new(mean, stdev).map_err(|e| {
7550 fuel_ir::Error::Msg(format!("Tensor::randn: invalid stdev={stdev}: {e}",)).bt()
7551 })?;
7552 let mut rng = rand::rng();
7553 match dtype {
7554 DType::F32 => {
7555 let data: Vec<f32> = (0..n).map(|_| normal.sample(&mut rng) as f32).collect();
7556 Self::from_f32(data, shape, device)
7557 }
7558 DType::F64 => {
7559 let data: Vec<f64> = (0..n).map(|_| normal.sample(&mut rng)).collect();
7560 Self::from_f64(data, shape, device)
7561 }
7562 DType::BF16 => {
7563 let data: Vec<half::bf16> = (0..n)
7564 .map(|_| half::bf16::from_f64(normal.sample(&mut rng)))
7565 .collect();
7566 Self::from_bf16(data, shape, device)
7567 }
7568 DType::F16 => {
7569 let data: Vec<half::f16> = (0..n)
7570 .map(|_| half::f16::from_f64(normal.sample(&mut rng)))
7571 .collect();
7572 Self::from_f16(data, shape, device)
7573 }
7574 other => Err(fuel_ir::Error::Msg(format!(
7575 "Tensor::randn: unsupported dtype {other:?}",
7576 ))
7577 .bt()),
7578 }
7579 }
7580
7581 pub fn arange(start: f32, end: f32, device: &Device) -> Self {
7584 Self::arange_step(start, end, 1.0, device)
7585 }
7586
7587 pub fn arange_step(start: f32, end: f32, step: f32, device: &Device) -> Self {
7592 assert!(step != 0.0, "arange_step: step must be non-zero");
7593 let mut data = Vec::new();
7594 let mut current = start;
7595 if step > 0.0 {
7596 while current < end {
7597 data.push(current);
7598 current += step;
7599 }
7600 } else {
7601 while current > end {
7602 data.push(current);
7603 current += step;
7604 }
7605 }
7606 let n = data.len();
7607 Self::from_f32(data, vec![n], device).expect(
7614 "arange_step: shape is [n] where n IS data.len(), computed one line above from that same buffer -- the lengths cannot disagree",
7615 )
7616 }
7617
7618 pub fn linspace(start: f32, end: f32, n: usize, device: &Device) -> Self {
7621 assert!(n >= 1, "linspace: n must be >= 1");
7622 if n == 1 {
7623 return Self::from_f32(vec![start], vec![1], device)
7625 .expect("linspace: vec![start] has exactly 1 element and the shape is [1]");
7626 }
7627 let step = (end - start) / ((n - 1) as f32);
7628 let data: Vec<f32> = (0..n).map(|i| start + step * (i as f32)).collect();
7629 Self::from_f32(data, vec![n], device).expect(
7632 "linspace: data is (0..n).map(..).collect() so it has exactly n elements, and the shape is [n] -- both from the same `n`",
7633 )
7634 }
7635
7636 pub fn norm(&self) -> Self {
7638 self.sqr().sum_all().sqrt()
7639 }
7640
7641 pub fn conv1d(
7656 &self,
7657 weight: &Self,
7658 bias: Option<&Self>,
7659 stride: usize,
7660 padding: usize,
7661 groups: usize,
7662 ) -> std::result::Result<Self, fuel_ir::Error> {
7663 let x_dims = self.shape().dims().to_vec();
7664 let w_dims = weight.shape().dims().to_vec();
7665 if x_dims.len() != 3 {
7666 return Err(fuel_ir::Error::Msg(format!(
7667 "conv1d: x must be rank 3 [N, Cin, T], got {x_dims:?}",
7668 ))
7669 .bt());
7670 }
7671 if w_dims.len() != 3 {
7672 return Err(fuel_ir::Error::Msg(format!(
7673 "conv1d: weight must be rank 3 [Cout, Cin/groups, K], got {w_dims:?}",
7674 ))
7675 .bt());
7676 }
7677 if groups < 1 {
7678 return Err(fuel_ir::Error::Msg("conv1d: groups must be ≥ 1".into()).bt());
7679 }
7680 if stride < 1 {
7681 return Err(fuel_ir::Error::Msg("conv1d: stride must be ≥ 1".into()).bt());
7682 }
7683 let x_4d = self.unsqueeze(2_usize)?;
7685 let w_4d = weight.unsqueeze(2_usize)?;
7686 let out_4d = x_4d.conv2d(&w_4d, bias, (1, stride), (0, padding), groups)?;
7687 out_4d.squeeze(2)
7688 }
7689
7690 pub fn conv1d_with_algo<A>(
7695 &self,
7696 weight: &Self,
7697 bias: Option<&Self>,
7698 stride: usize,
7699 padding: usize,
7700 groups: usize,
7701 _algo: A,
7702 ) -> std::result::Result<Self, fuel_ir::Error> {
7703 self.conv1d(weight, bias, stride, padding, groups)
7704 }
7705
7706 pub fn avg_pool2d(
7716 &self,
7717 kernel: (usize, usize),
7718 stride: (usize, usize),
7719 padding: (usize, usize),
7720 count_include_pad: bool,
7721 ) -> std::result::Result<Self, fuel_ir::Error> {
7722 let dims = self.shape().dims().to_vec();
7723 if dims.len() != 4 {
7724 return Err(fuel_ir::Error::Msg(format!(
7725 "avg_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7726 ))
7727 .bt());
7728 }
7729 let (c, h, w) = (dims[1], dims[2], dims[3]);
7730 let (kh, kw) = kernel;
7731 if kh == 0 || kw == 0 {
7732 return Err(
7733 fuel_ir::Error::Msg("avg_pool2d: kernel sizes must be positive".into()).bt(),
7734 );
7735 }
7736
7737 if count_include_pad || padding == (0, 0) {
7742 let inv = 1.0_f32 / ((kh * kw) as f32);
7743 let weight =
7747 self.const_f32_like(vec![inv; c * kh * kw], Shape::from_dims(&[c, 1, kh, kw]))?;
7748 return self.conv2d(&weight, None, stride, padding, c);
7749 }
7750
7751 if let Some((axis, out_idx)) = first_zero_tap_output_index(h, kh, stride.0, padding.0)
7766 .map(|i| ("H", i))
7767 .or_else(|| first_zero_tap_output_index(w, kw, stride.1, padding.1).map(|i| ("W", i)))
7768 {
7769 return Err(fuel_ir::Error::Msg(format!(
7770 "avg_pool2d: count_include_pad=false has no valid (non-padded) \
7771 taps for output position {out_idx} on the {axis} axis \
7772 (kernel={kernel:?}, stride={stride:?}, padding={padding:?}, \
7773 input H×W={h}×{w}) — the average would divide by zero",
7774 ))
7775 .bt());
7776 }
7777
7778 let sum_weight = self.const_f32_like(
7779 vec![1.0_f32; c * kh * kw],
7780 Shape::from_dims(&[c, 1, kh, kw]),
7781 )?;
7782 let summed = self.conv2d(&sum_weight, None, stride, padding, c)?;
7783
7784 let ones_hw = self.const_f32_like(vec![1.0_f32; h * w], Shape::from_dims(&[1, 1, h, w]))?;
7788 let count_weight =
7789 self.const_f32_like(vec![1.0_f32; kh * kw], Shape::from_dims(&[1, 1, kh, kw]))?;
7790 let count = ones_hw.conv2d(&count_weight, None, stride, padding, 1)?;
7791
7792 summed.broadcast_div(&count)
7793 }
7794
7795 pub fn avg_pool2d_with_stride(
7799 &self,
7800 kernel: (usize, usize),
7801 stride: (usize, usize),
7802 ) -> std::result::Result<Self, fuel_ir::Error> {
7803 self.avg_pool2d(kernel, stride, (0, 0), true)
7806 }
7807
7808 pub fn max_pool2d(
7823 &self,
7824 kernel: (usize, usize),
7825 stride: (usize, usize),
7826 padding: (usize, usize),
7827 ) -> std::result::Result<Self, fuel_ir::Error> {
7828 self.max_pool2d_with_pad_value(kernel, stride, padding, f32::NEG_INFINITY)
7836 }
7837
7838 pub fn max_pool2d_with_pad_value(
7844 &self,
7845 kernel: (usize, usize),
7846 stride: (usize, usize),
7847 padding: (usize, usize),
7848 pad_value: f32,
7849 ) -> std::result::Result<Self, fuel_ir::Error> {
7850 let dims = self.shape().dims().to_vec();
7851 if dims.len() != 4 {
7852 return Err(fuel_ir::Error::Msg(format!(
7853 "max_pool2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7854 ))
7855 .bt());
7856 }
7857 let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7858 let (kh, kw) = kernel;
7859 let (sh, sw) = stride;
7860 let (ph, pw) = padding;
7861 if kh == 0 || kw == 0 {
7862 return Err(
7863 fuel_ir::Error::Msg("max_pool2d: kernel sizes must be positive".into()).bt(),
7864 );
7865 }
7866 if sh == 0 || sw == 0 {
7867 return Err(fuel_ir::Error::Msg("max_pool2d: strides must be positive".into()).bt());
7868 }
7869 let h_padded_min = h + 2 * ph;
7870 let w_padded_min = w + 2 * pw;
7871 if h_padded_min < kh || w_padded_min < kw {
7872 return Err(fuel_ir::Error::Msg(format!(
7873 "max_pool2d: padded input ({h_padded_min}×{w_padded_min}) smaller than kernel ({kh}×{kw})",
7874 )).bt());
7875 }
7876 let h_out = (h_padded_min - kh) / sh + 1;
7877 let w_out = (w_padded_min - kw) / sw + 1;
7878 let h_total = h_out * sh + (kh - 1);
7881 let w_total = w_out * sw + (kw - 1);
7882 let extra_h = h_total.saturating_sub(h_padded_min);
7883 let extra_w = w_total.saturating_sub(w_padded_min);
7884 let padded = self
7885 .pad_with_value(2, ph, ph + extra_h, pad_value)?
7886 .pad_with_value(3, pw, pw + extra_w, pad_value)?;
7887 let mut acc: Option<Tensor> = None;
7889 for ky in 0..kh {
7890 let row_slice = padded.slice(2, ky, h_out * sh).unwrap();
7894 let row_reshaped = row_slice.reshape(vec![n, c, h_out, sh, w_total])?;
7897 let row_tap = row_reshaped.slice(3, 0, 1).unwrap().squeeze(3)?;
7898 for kx in 0..kw {
7899 let col_slice = row_tap.slice(3, kx, w_out * sw).unwrap();
7900 let col_reshaped = col_slice.reshape(vec![n, c, h_out, w_out, sw])?;
7901 let win = col_reshaped.slice(4, 0, 1).unwrap().squeeze(4)?;
7902 acc = Some(match acc {
7903 None => win,
7904 Some(a) => a.maximum(&win).unwrap(),
7905 });
7906 }
7907 }
7908 acc.ok_or_else(|| fuel_ir::Error::Msg("max_pool2d: empty kernel".into()).bt())
7909 }
7910
7911 pub fn max_pool2d_with_stride(
7913 &self,
7914 kernel: (usize, usize),
7915 stride: (usize, usize),
7916 ) -> std::result::Result<Self, fuel_ir::Error> {
7917 self.max_pool2d(kernel, stride, (0, 0))
7918 }
7919
7920 pub fn upsample_nearest2d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7930 if scale == 0 {
7931 return Err(
7932 fuel_ir::Error::Msg("upsample_nearest2d: scale must be positive".into()).bt(),
7933 );
7934 }
7935 let dims = self.shape().dims().to_vec();
7936 if dims.len() != 4 {
7937 return Err(fuel_ir::Error::Msg(format!(
7938 "upsample_nearest2d: input must be rank 4 [N, C, H, W], got {dims:?}",
7939 ))
7940 .bt());
7941 }
7942 if scale == 1 {
7943 return Ok(self.clone());
7944 }
7945 let (n, c, h, w) = (dims[0], dims[1], dims[2], dims[3]);
7946 let expanded = self.reshape(vec![n, c, h, 1, w, 1])?;
7948 let h_expanded = (0..scale)
7950 .fold(None, |acc: Option<Tensor>, _| {
7951 Some(match acc {
7952 None => expanded.clone(),
7953 Some(a) => a.concat(&expanded, 3).unwrap(),
7954 })
7955 })
7956 .unwrap();
7957 let h_then_w = (0..scale)
7958 .fold(None, |acc: Option<Tensor>, _| {
7959 Some(match acc {
7960 None => h_expanded.clone(),
7961 Some(a) => a.concat(&h_expanded, 5).unwrap(),
7962 })
7963 })
7964 .unwrap();
7965 h_then_w.reshape(vec![n, c, h * scale, w * scale])
7966 }
7967
7968 pub fn upsample_nearest1d(&self, scale: usize) -> std::result::Result<Self, fuel_ir::Error> {
7972 if scale == 0 {
7973 return Err(
7974 fuel_ir::Error::Msg("upsample_nearest1d: scale must be positive".into()).bt(),
7975 );
7976 }
7977 let dims = self.shape().dims().to_vec();
7978 if dims.len() != 3 {
7979 return Err(fuel_ir::Error::Msg(format!(
7980 "upsample_nearest1d: input must be rank 3 [N, C, T], got {dims:?}",
7981 ))
7982 .bt());
7983 }
7984 if scale == 1 {
7985 return Ok(self.clone());
7986 }
7987 let (n, c, t) = (dims[0], dims[1], dims[2]);
7988 let expanded = self.reshape(vec![n, c, t, 1])?;
7989 let replicated = (0..scale)
7990 .fold(None, |acc: Option<Tensor>, _| {
7991 Some(match acc {
7992 None => expanded.clone(),
7993 Some(a) => a.concat(&expanded, 3).unwrap(),
7994 })
7995 })
7996 .unwrap();
7997 replicated.reshape(vec![n, c, t * scale])
7998 }
7999
8000 pub fn interpolate2d(
8010 &self,
8011 target_h: usize,
8012 target_w: usize,
8013 ) -> std::result::Result<Self, fuel_ir::Error> {
8014 let dims = self.shape().dims().to_vec();
8015 if dims.len() != 4 {
8016 return Err(fuel_ir::Error::Msg(format!(
8017 "interpolate2d: input must be rank 4 [N, C, H, W], got {dims:?}",
8018 ))
8019 .bt());
8020 }
8021 let h = dims[2];
8022 let w = dims[3];
8023 if h == 0 || w == 0 || target_h == 0 || target_w == 0 {
8024 return Err(fuel_ir::Error::Msg(
8025 "interpolate2d: input + target spatial dims must be positive".into(),
8026 )
8027 .bt());
8028 }
8029 if target_h == h && target_w == w {
8031 return Ok(self.clone());
8032 }
8033 if target_h.is_multiple_of(h) && target_w.is_multiple_of(w) && target_h / h == target_w / w
8037 {
8038 return self.upsample_nearest2d(target_h / h);
8039 }
8040 let h_idx: Vec<u32> = (0..target_h)
8044 .map(|oi| ((oi * h) / target_h).min(h - 1) as u32)
8045 .collect();
8046 let w_idx: Vec<u32> = (0..target_w)
8047 .map(|oj| ((oj * w) / target_w).min(w - 1) as u32)
8048 .collect();
8049 let h_idx_tensor = self.const_u32_like(h_idx, fuel_ir::Shape::from_dims(&[target_h]))?;
8050 let w_idx_tensor = self.const_u32_like(w_idx, fuel_ir::Shape::from_dims(&[target_w]))?;
8051 let after_h = self.index_select(2_usize, &h_idx_tensor)?;
8052 after_h.index_select(3_usize, &w_idx_tensor)
8053 }
8054
8055 pub fn interpolate1d(&self, target_t: usize) -> std::result::Result<Self, fuel_ir::Error> {
8059 let dims = self.shape().dims().to_vec();
8060 if dims.len() != 3 {
8061 return Err(fuel_ir::Error::Msg(format!(
8062 "interpolate1d: input must be rank 3 [N, C, T], got {dims:?}",
8063 ))
8064 .bt());
8065 }
8066 let t = dims[2];
8067 if t == 0 {
8068 return Err(
8069 fuel_ir::Error::Msg("interpolate1d: input length must be positive".into()).bt(),
8070 );
8071 }
8072 if !target_t.is_multiple_of(t) {
8073 return Err(fuel_ir::Error::Msg(format!(
8074 "interpolate1d: target {target_t} must be integer multiple of input {t}; non-integer ratios are future work",
8075 )).bt());
8076 }
8077 self.upsample_nearest1d(target_t / t)
8078 }
8079
8080 pub fn pad_with_zeros<D: Dim>(
8084 &self,
8085 dim: D,
8086 left: usize,
8087 right: usize,
8088 ) -> std::result::Result<Self, fuel_ir::Error> {
8089 self.pad_with_value(dim, left, right, 0.0)
8090 }
8091
8092 pub fn pad_with_value<D: Dim>(
8099 &self,
8100 dim: D,
8101 before: usize,
8102 after: usize,
8103 value: f32,
8104 ) -> std::result::Result<Self, fuel_ir::Error> {
8105 let shape = self.shape();
8106 let dim = dim.to_index(&shape, "pad_with_value")?;
8107 let rank = shape.dims().len();
8108 if before == 0 && after == 0 {
8109 return Ok(self.clone());
8110 }
8111 let mut padding: Vec<(usize, usize)> = vec![(0, 0); rank];
8112 padding[dim] = (before, after);
8113 self.pad(padding, fuel_graph::PadMode::Constant, value as f64)
8114 }
8115
8116 pub fn meshgrid(
8129 args: &[&Self],
8130 xy_indexing: bool,
8131 ) -> std::result::Result<Vec<Self>, fuel_ir::Error> {
8132 if args.len() < 2 {
8133 return Err(fuel_ir::Error::Msg(
8134 "meshgrid: requires at least two rank-1 tensors".into(),
8135 )
8136 .bt());
8137 }
8138 let ordered: Vec<&Self> = if xy_indexing {
8139 args.iter().rev().copied().collect()
8140 } else {
8141 args.to_vec()
8142 };
8143 let mut lens = Vec::with_capacity(ordered.len());
8144 for (i, t) in ordered.iter().enumerate() {
8145 let dims = t.shape().dims().to_vec();
8146 if dims.len() != 1 {
8147 return Err(fuel_ir::Error::Msg(format!(
8148 "meshgrid: input {i} must be rank 1, got shape {dims:?}",
8149 ))
8150 .bt());
8151 }
8152 lens.push(dims[0]);
8153 }
8154 let mut grids = Vec::with_capacity(ordered.len());
8155 for (idx, t) in ordered.iter().enumerate() {
8156 let mut shape = vec![1_usize; ordered.len()];
8157 shape[idx] = lens[idx];
8158 let placed = t.reshape(shape)?;
8159 let mut repeats = lens.clone();
8160 repeats[idx] = 1;
8161 let grid = placed.repeat(repeats)?;
8162 grids.push(grid);
8163 }
8164 if xy_indexing {
8165 grids.reverse();
8166 }
8167 Ok(grids)
8168 }
8169
8170 pub fn repeat(&self, repeats: impl Into<Shape>) -> std::result::Result<Self, fuel_ir::Error> {
8174 let repeats = repeats.into();
8175 let repeats: Vec<usize> = repeats.dims().to_vec();
8176 let self_rank = self.shape().dims().len();
8177 let target_rank = repeats.len();
8178 let mut work = if self_rank < target_rank {
8179 let pad_count = target_rank - self_rank;
8180 let mut new_shape: Vec<usize> = vec![1; pad_count];
8181 new_shape.extend_from_slice(self.shape().dims());
8182 self.reshape(new_shape)?
8183 } else if self_rank > target_rank {
8184 return Err(fuel_ir::Error::Msg(format!(
8185 "repeat: repeats rank {target_rank} smaller than tensor rank {self_rank}",
8186 ))
8187 .bt());
8188 } else {
8189 self.clone()
8190 };
8191 for (axis, &n) in repeats.iter().enumerate() {
8192 if n == 0 {
8193 return Err(fuel_ir::Error::Msg(format!(
8194 "repeat: zero repeat count at axis {axis} not supported",
8195 ))
8196 .bt());
8197 }
8198 if n == 1 {
8199 continue;
8200 }
8201 let base = work.clone();
8203 for _ in 1..n {
8204 work = work.concat(&base, axis)?;
8205 }
8206 }
8207 Ok(work)
8208 }
8209}
8210
8211fn first_zero_tap_output_index(
8220 dim: usize,
8221 kernel: usize,
8222 stride: usize,
8223 pad: usize,
8224) -> Option<usize> {
8225 let out_dim = (dim + 2 * pad).saturating_sub(kernel) / stride + 1;
8226 (0..out_dim).find(|&o| {
8227 let start = o as isize * stride as isize - pad as isize;
8228 let end = start + kernel as isize;
8229 let valid_start = start.max(0);
8230 let valid_end = end.min(dim as isize);
8231 valid_end <= valid_start
8232 })
8233}
8234
8235impl Tensor {
8238 pub fn from_safetensors_bytes(
8250 bytes: &[u8],
8251 dtype: safetensors::Dtype,
8252 shape: &[usize],
8253 device: &Device,
8254 ) -> crate::Result<Self> {
8255 use safetensors::Dtype;
8256 let shape_obj = Shape::from_dims(shape);
8257 let elem_count = shape_obj.elem_count();
8258
8259 let check_len = |expected: usize| -> crate::Result<()> {
8260 if bytes.len() != expected {
8261 crate::bail!(
8262 "from_safetensors_bytes: expected {expected} bytes for dtype {dtype:?} \
8263 and shape {shape:?}, got {}",
8264 bytes.len(),
8265 );
8266 }
8267 Ok(())
8268 };
8269
8270 match dtype {
8271 Dtype::F32 => {
8272 check_len(elem_count * 4)?;
8273 let mut data = Vec::with_capacity(elem_count);
8274 for chunk in bytes.as_chunks::<4>().0.iter() {
8275 data.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8276 }
8277 Self::from_f32(data, shape_obj, device)
8278 }
8279 Dtype::F64 => {
8280 check_len(elem_count * 8)?;
8281 let mut data = Vec::with_capacity(elem_count);
8282 for chunk in bytes.as_chunks::<8>().0.iter() {
8283 let arr: [u8; 8] = *chunk;
8284 data.push(f64::from_le_bytes(arr));
8285 }
8286 Self::from_f64(data, shape_obj, device)
8287 }
8288 Dtype::BF16 => {
8289 check_len(elem_count * 2)?;
8290 let mut data = Vec::with_capacity(elem_count);
8291 for chunk in bytes.as_chunks::<2>().0.iter() {
8292 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8293 data.push(half::bf16::from_bits(raw));
8294 }
8295 Self::from_bf16(data, shape_obj, device)
8296 }
8297 Dtype::F16 => {
8298 check_len(elem_count * 2)?;
8299 let mut data = Vec::with_capacity(elem_count);
8300 for chunk in bytes.as_chunks::<2>().0.iter() {
8301 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
8302 data.push(half::f16::from_bits(raw));
8303 }
8304 Self::from_f16(data, shape_obj, device)
8305 }
8306 Dtype::U32 => {
8307 check_len(elem_count * 4)?;
8308 let mut data = Vec::with_capacity(elem_count);
8309 for chunk in bytes.as_chunks::<4>().0.iter() {
8310 data.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
8311 }
8312 Self::from_u32(data, shape_obj, device)
8313 }
8314 other => crate::bail!(
8315 "from_safetensors_bytes: unsupported dtype {other:?} — extend Tensor's \
8316 safetensors loader to handle it",
8317 ),
8318 }
8319 }
8320
8321 pub fn from_safetensors_view(
8325 view: &safetensors::tensor::TensorView<'_>,
8326 device: &Device,
8327 ) -> crate::Result<Self> {
8328 Self::from_safetensors_bytes(view.data(), view.dtype(), view.shape(), device)
8329 }
8330}
8331
8332pub type LayerNormPair = (Arc<[f32]>, Arc<[f32]>);
8336
8337pub type ConvWeightBias = (Arc<[f32]>, Arc<[f32]>);
8341
8342#[derive(Debug, Clone)]
8365pub enum WeightStorage {
8366 F32(Arc<[f32]>),
8367 BF16(Arc<[half::bf16]>),
8368 Q4_0 {
8378 words: Arc<[u32]>,
8379 bytes_len: usize,
8380 in_features: usize,
8381 out_features: usize,
8382 },
8383 WithLoRA {
8394 base: Box<WeightStorage>,
8395 lora_a: Arc<[f32]>,
8397 lora_b: Arc<[f32]>,
8399 rank: usize,
8400 alpha: f32,
8402 in_features: usize,
8403 out_features: usize,
8404 },
8405}
8406
8407impl WeightStorage {
8408 pub fn elem_count(&self) -> usize {
8409 match self {
8410 Self::F32(a) => a.len(),
8411 Self::BF16(a) => a.len(),
8412 Self::Q4_0 {
8414 in_features,
8415 out_features,
8416 ..
8417 } => *in_features * *out_features,
8418 Self::WithLoRA {
8419 in_features,
8420 out_features,
8421 ..
8422 } => *in_features * *out_features,
8423 }
8424 }
8425
8426 pub fn dtype(&self) -> fuel_ir::DType {
8427 match self {
8428 Self::F32(_) => fuel_ir::DType::F32,
8429 Self::BF16(_) => fuel_ir::DType::BF16,
8430 Self::Q4_0 { .. } => fuel_ir::DType::U32,
8434 Self::WithLoRA { base, .. } => base.dtype(),
8437 }
8438 }
8439
8440 pub fn const_like(
8451 &self,
8452 anchor: &Tensor,
8453 shape: Shape,
8454 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8455 match self {
8456 Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8457 Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8458 Self::Q4_0 { words, .. } => {
8459 let _ = shape; anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8462 }
8463 Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8464 "WeightStorage::WithLoRA::const_like is not supported \
8465 — the base + LoRA update must be applied via \
8466 apply_linear to produce the right graph structure."
8467 .into(),
8468 )
8469 .bt()),
8470 }
8471 }
8472
8473 pub fn apply_linear_with_bias(
8482 &self,
8483 x: &Tensor,
8484 in_features: usize,
8485 out_features: usize,
8486 bias: std::sync::Arc<[f32]>,
8487 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8488 if bias.len() != out_features {
8489 return Err(fuel_ir::Error::Msg(format!(
8494 "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8495 bias.len(),
8496 ))
8497 .bt());
8498 }
8499 let projected = self.apply_linear(x, in_features, out_features)?;
8500 let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8501 projected.broadcast_add(&bias_t)
8502 }
8503
8504 pub fn apply_linear(
8522 &self,
8523 x: &Tensor,
8524 in_features: usize,
8525 out_features: usize,
8526 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8527 let x_dims = x.shape().dims().to_vec();
8533 match x_dims.last() {
8534 Some(&k) if k == in_features => {}
8535 _ => {
8536 return Err(fuel_ir::Error::Msg(format!(
8537 "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8538 must have trailing dim {in_features}",
8539 ))
8540 .bt());
8541 }
8542 }
8543 match self {
8544 Self::F32(_) | Self::BF16(_) => {
8545 let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8546 x.matmul(&w)
8547 }
8548 Self::Q4_0 {
8549 in_features: expected_in,
8550 out_features: expected_out,
8551 ..
8552 } => {
8553 if *expected_in != in_features || *expected_out != out_features {
8554 return Err(fuel_ir::Error::Msg(format!(
8555 "WeightStorage::Q4_0 shape mismatch: stored \
8556 [{expected_in}, {expected_out}], requested \
8557 [{in_features}, {out_features}]",
8558 ))
8559 .bt());
8560 }
8561 let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8562 x.qmatmul(
8563 &w_bytes,
8564 fuel_graph::QuantType::Q4_0,
8565 in_features,
8566 out_features,
8567 )
8568 }
8569 Self::WithLoRA {
8570 base,
8571 lora_a,
8572 lora_b,
8573 rank,
8574 alpha,
8575 in_features: expected_in,
8576 out_features: expected_out,
8577 } => {
8578 if *expected_in != in_features || *expected_out != out_features {
8579 return Err(fuel_ir::Error::Msg(format!(
8580 "WeightStorage::WithLoRA shape mismatch: stored \
8581 [{expected_in}, {expected_out}], requested \
8582 [{in_features}, {out_features}]",
8583 ))
8584 .bt());
8585 }
8586 let base_out = base.apply_linear(x, in_features, out_features)?;
8588 let a_t =
8590 x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8591 let b_t =
8592 x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8593 let scale = *alpha as f64 / *rank as f64;
8594 let lora_path = Tensor {
8596 inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8597 };
8598 base_out.add(&lora_path)
8599 }
8600 }
8601 }
8602
8603 pub fn apply_linear_dyn_m(
8616 &self,
8617 x: &Tensor,
8618 in_features: usize,
8619 out_features: usize,
8620 row_count: fuel_ir::DynScalar,
8621 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8622 match self {
8623 Self::F32(_) => {
8624 let w = self
8626 .const_like(x, Shape::from_dims(&[in_features, out_features]))
8627 .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8628 x.matmul_dyn_m(&w, row_count)
8629 }
8630 other => Err(fuel_ir::Error::Msg(format!(
8631 "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8632 weight (cast the expert weights to F32)",
8633 other.dtype(),
8634 ))
8635 .bt()),
8636 }
8637 }
8638
8639 pub fn with_lora(
8644 self,
8645 lora_a: Arc<[f32]>,
8646 lora_b: Arc<[f32]>,
8647 rank: usize,
8648 alpha: f32,
8649 in_features: usize,
8650 out_features: usize,
8651 ) -> Self {
8652 assert_eq!(
8653 lora_a.len(),
8654 in_features * rank,
8655 "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8656 lora_a.len(),
8657 in_features * rank,
8658 );
8659 assert_eq!(
8660 lora_b.len(),
8661 rank * out_features,
8662 "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8663 lora_b.len(),
8664 rank * out_features,
8665 );
8666 assert!(
8667 !matches!(self, Self::WithLoRA { .. }),
8668 "with_lora: base is already WithLoRA (nested adapters unsupported)",
8669 );
8670 Self::WithLoRA {
8671 base: Box::new(self),
8672 lora_a,
8673 lora_b,
8674 rank,
8675 alpha,
8676 in_features,
8677 out_features,
8678 }
8679 }
8680}
8681
8682impl From<Arc<[f32]>> for WeightStorage {
8686 fn from(a: Arc<[f32]>) -> Self {
8687 Self::F32(a)
8688 }
8689}
8690impl From<Vec<f32>> for WeightStorage {
8691 fn from(v: Vec<f32>) -> Self {
8692 Self::F32(Arc::from(v))
8693 }
8694}
8695impl From<Arc<[half::bf16]>> for WeightStorage {
8696 fn from(a: Arc<[half::bf16]>) -> Self {
8697 Self::BF16(a)
8698 }
8699}
8700impl From<Vec<half::bf16>> for WeightStorage {
8701 fn from(v: Vec<half::bf16>) -> Self {
8702 Self::BF16(Arc::from(v))
8703 }
8704}
8705
8706#[derive(Debug, Clone)]
8718pub struct LayerWeights {
8719 pub attn_q: WeightStorage,
8721 pub attn_q_bias: Option<Arc<[f32]>>,
8723 pub attn_k: WeightStorage,
8725 pub attn_k_bias: Option<Arc<[f32]>>,
8727 pub attn_v: WeightStorage,
8729 pub attn_v_bias: Option<Arc<[f32]>>,
8731 pub attn_o: WeightStorage,
8733 pub ffn_gate: WeightStorage,
8735 pub ffn_up: WeightStorage,
8737 pub ffn_down: WeightStorage,
8739 pub attn_norm_gain: Arc<[f32]>,
8742 pub ffn_norm_gain: Arc<[f32]>,
8744}
8745
8746pub fn invalidate_decode_pair_if_stale<C>(
8747 session: &mut Option<crate::inference_context::DecodeSession>,
8748 captured: &mut Option<C>,
8749 ctx: &mut InferenceContext,
8750 seq: usize,
8751 max_seq_len: Option<usize>,
8752 cache_dtype: DType,
8753 n_layers: usize,
8754 shape_key: u64,
8755 kv: &dyn crate::inference_context::KvRebindSource,
8756 drop_session: impl FnOnce(
8757 &mut Option<crate::inference_context::DecodeSession>,
8758 &mut InferenceContext,
8759 ),
8760) -> SessionDisposition {
8761 refresh_decode_session(
8762 session,
8763 ctx,
8764 seq,
8765 max_seq_len,
8766 cache_dtype,
8767 n_layers,
8768 shape_key,
8769 kv,
8770 || *captured = None,
8774 drop_session,
8775 )
8776}
8777
8778#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8780pub enum SessionDisposition {
8781 Absent,
8783 Kept,
8785 Rebound,
8788 Dropped(Option<crate::inference_context::RebindRefusal>),
8792}
8793
8794pub fn refresh_decode_session(
8819 session: &mut Option<crate::inference_context::DecodeSession>,
8820 ctx: &mut InferenceContext,
8821 seq: usize,
8822 max_seq_len: Option<usize>,
8823 cache_dtype: DType,
8824 n_layers: usize,
8825 shape_key: u64,
8826 kv: &dyn crate::inference_context::KvRebindSource,
8827 on_invalidate: impl FnOnce(),
8828 drop_session: impl FnOnce(
8829 &mut Option<crate::inference_context::DecodeSession>,
8830 &mut InferenceContext,
8831 ),
8832) -> SessionDisposition {
8833 use crate::inference_context::PlanValidity;
8834
8835 let verdict = match (session.as_ref(), max_seq_len) {
8836 (None, _) => return SessionDisposition::Absent,
8837 (Some(s), Some(msl)) => {
8838 s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8839 }
8840 (Some(_), None) => PlanValidity::Stale,
8843 };
8844 if verdict == PlanValidity::Valid {
8845 return SessionDisposition::Kept;
8846 }
8847
8848 on_invalidate();
8849
8850 let refusal = if verdict == PlanValidity::AllocationChanged {
8851 match session.as_mut().map(|s| s.rebind_kv(kv)) {
8852 Some(Ok(())) => return SessionDisposition::Rebound,
8853 Some(Err(r)) => Some(r),
8854 None => None,
8858 }
8859 } else {
8860 None
8861 };
8862
8863 drop_session(session, ctx);
8864 SessionDisposition::Dropped(refusal)
8865}
8866
8867pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8868 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8869 for q_idx in 0..seq {
8870 let abs_q = cached_len + q_idx;
8871 for k_idx in (abs_q + 1)..max_seq_len {
8872 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8873 }
8874 }
8875 mask_data
8876}
8877
8878pub fn build_decode_causal_mask_windowed(
8918 cached_len: usize,
8919 seq: usize,
8920 max_seq_len: usize,
8921 window: usize,
8922) -> Vec<f32> {
8923 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8924 for q_idx in 0..seq {
8925 let abs_q = cached_len + q_idx;
8926 for k_idx in 0..max_seq_len {
8927 if k_idx > abs_q || k_idx + window <= abs_q {
8929 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8930 }
8931 }
8932 }
8933 mask_data
8934}
8935
8936#[cfg(test)]
8937mod decode_mask_tests {
8938 use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8939
8940 fn attendable(row: &[f32]) -> Vec<u8> {
8943 row.iter().map(|&v| u8::from(v == 0.0)).collect()
8944 }
8945
8946 #[test]
8953 fn windowed_mask_excludes_positions_older_than_the_window() {
8954 let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8955 assert_eq!(
8956 attendable(&m),
8957 vec![0, 0, 1, 1, 0, 0],
8958 "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8959 );
8960 }
8961
8962 #[test]
8967 fn windowed_mask_offsets_each_row_by_cached_len() {
8968 let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8969 assert_eq!(
8970 attendable(&m[0..5]),
8971 vec![1, 1, 0, 0, 0],
8972 "row 0 = abs pos 1"
8973 );
8974 assert_eq!(
8975 attendable(&m[5..10]),
8976 vec![0, 1, 1, 0, 0],
8977 "row 1 = abs pos 2"
8978 );
8979 }
8980
8981 #[test]
8990 fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
8991 for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
8992 let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
8993 let windowed =
8994 build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
8995 assert_eq!(
8996 dense, windowed,
8997 "window == max_seq_len must equal dense at \
8998 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
8999 );
9000 }
9001 }
9002
9003 #[test]
9012 fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
9013 let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
9014
9015 let mut prefill = vec![0.0_f32; n * n];
9017 for i in 0..n {
9018 for j in 0..n {
9019 if j > i || j + window <= i {
9020 prefill[i * n + j] = f32::NEG_INFINITY;
9021 }
9022 }
9023 }
9024
9025 for p in 0..n {
9026 let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
9027 assert_eq!(
9028 attendable(&decode[0..n]),
9029 attendable(&prefill[p * n..(p + 1) * n]),
9030 "decode at cached_len={p} must match prefill row {p}",
9031 );
9032 assert!(
9033 decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
9034 "the unwritten tail beyond position {n} must stay masked",
9035 );
9036 }
9037 }
9038
9039 }
9080
9081pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9119 match window {
9120 None => (None, None),
9121 Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9122 }
9123}
9124
9125pub fn offer_flash_decode_arm_for_region(
9130 graph: &fuel_graph::SharedGraph,
9131 q: fuel_graph::NodeId,
9132 k: fuel_graph::NodeId,
9133 v: fuel_graph::NodeId,
9134 decomposed_out: fuel_graph::NodeId,
9135 reconverge: fuel_graph::NodeId,
9136 softmax_scale: f32,
9137 attended_len_sym: fuel_ir::SymId,
9138 attn_window: Option<usize>,
9139 softcap: Option<f32>,
9140 cap: fuel_dispatch::decode_flash::FlashArmCapability,
9141) -> crate::Result<Option<fuel_graph::NodeId>> {
9142 use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9143 let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9144 let spec = DecodeFlashSpec {
9145 q,
9146 k,
9147 v,
9148 alibi: None,
9149 softmax_scale,
9150 causal: true,
9151 window_size_left,
9158 window_size_right,
9159 softcap,
9160 k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9161 decomposed_out,
9162 reconverge,
9163 };
9164 let mut g = graph.write().map_err(|_| {
9165 fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9166 })?;
9167 offer_decode_flash_arm(&mut g, &spec, cap)
9168}
9169
9170pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9171 assert_eq!(
9172 gain.len(),
9173 dim,
9174 "apply_affine_rms_norm: gain length must equal dim"
9175 );
9176 let normalized = x.rms_norm_last_dim(eps).unwrap();
9177 let gain_t = x
9183 .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9184 .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9185 normalized.broadcast_mul(&gain_t).unwrap()
9186}
9187
9188pub fn load_tensor_as_f32(
9195 st: &fuel_loaders::safetensors::MmapedSafetensors,
9196 name: &str,
9197) -> crate::Result<Vec<f32>> {
9198 use safetensors::Dtype;
9199 let view = st.get(name)?;
9200 let bytes = view.data();
9201 match view.dtype() {
9202 Dtype::F32 => {
9203 let mut out = Vec::with_capacity(bytes.len() / 4);
9204 for chunk in bytes.as_chunks::<4>().0.iter() {
9205 out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9206 }
9207 Ok(out)
9208 }
9209 Dtype::F64 => {
9210 let mut out = Vec::with_capacity(bytes.len() / 8);
9211 for chunk in bytes.as_chunks::<8>().0.iter() {
9212 let arr: [u8; 8] = *chunk;
9213 out.push(f64::from_le_bytes(arr) as f32);
9214 }
9215 Ok(out)
9216 }
9217 Dtype::BF16 => {
9218 let mut out = Vec::with_capacity(bytes.len() / 2);
9219 for chunk in bytes.as_chunks::<2>().0.iter() {
9220 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9221 out.push(half::bf16::from_bits(raw).to_f32());
9222 }
9223 Ok(out)
9224 }
9225 Dtype::F16 => {
9226 let mut out = Vec::with_capacity(bytes.len() / 2);
9227 for chunk in bytes.as_chunks::<2>().0.iter() {
9228 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9229 out.push(half::f16::from_bits(raw).to_f32());
9230 }
9231 Ok(out)
9232 }
9233 other => {
9234 crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9235 }
9236 }
9237}
9238
9239pub fn load_transposed_matrix(
9246 st: &fuel_loaders::safetensors::MmapedSafetensors,
9247 name: &str,
9248 out_features: usize,
9249 in_features: usize,
9250) -> crate::Result<Vec<f32>> {
9251 let flat = load_tensor_as_f32(st, name)?;
9252 if flat.len() != out_features * in_features {
9253 crate::bail!(
9254 "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9255 flat.len(),
9256 out_features * in_features,
9257 );
9258 }
9259 let mut out = vec![0.0_f32; out_features * in_features];
9263 for i in 0..out_features {
9264 for j in 0..in_features {
9265 out[j * out_features + i] = flat[i * in_features + j];
9266 }
9267 }
9268 Ok(out)
9269}
9270
9271pub fn load_transposed_matrix_preserve_dtype(
9283 st: &fuel_loaders::safetensors::MmapedSafetensors,
9284 name: &str,
9285 out_features: usize,
9286 in_features: usize,
9287) -> crate::Result<WeightStorage> {
9288 use safetensors::Dtype;
9289 let view = st.get(name)?;
9290 let bytes = view.data();
9291 let expected = out_features * in_features;
9292 match view.dtype() {
9293 Dtype::BF16 => {
9294 if bytes.len() != expected * 2 {
9295 crate::bail!(
9296 "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9297 bytes.len(),
9298 expected * 2,
9299 );
9300 }
9301 let mut out = vec![half::bf16::ZERO; expected];
9304 for i in 0..out_features {
9305 for j in 0..in_features {
9306 let src_off = (i * in_features + j) * 2;
9307 let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9308 out[j * out_features + i] = half::bf16::from_bits(bits);
9309 }
9310 }
9311 Ok(WeightStorage::BF16(Arc::from(out)))
9312 }
9313 _ => {
9314 let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9318 Ok(WeightStorage::F32(Arc::from(flat)))
9319 }
9320 }
9321}
9322
9323#[derive(Debug, Clone, Copy, Default)]
9325pub enum SamplingStrategy {
9326 #[default]
9328 Greedy,
9329 Temperature { temp: f32, seed: u64 },
9334}
9335
9336pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9340 match strategy {
9341 SamplingStrategy::Greedy => {
9342 let (i, _) = logits
9343 .iter()
9344 .enumerate()
9345 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9346 .expect("sample_logits: empty logits");
9347 i as u32
9348 }
9349 SamplingStrategy::Temperature { temp, .. } => {
9350 let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9353 let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9354 let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9355 let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9356 let sum: f32 = exp.iter().sum();
9357 let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9358 sample_multinomial(&probs, rng_state)
9359 }
9360 }
9361}
9362
9363pub fn spec_argmax(logits: &[f32]) -> u32 {
9371 let mut best = 0;
9372 let mut best_v = logits[0];
9373 for (i, &v) in logits.iter().enumerate().skip(1) {
9374 if v > best_v {
9375 best_v = v;
9376 best = i;
9377 }
9378 }
9379 best as u32
9380}
9381
9382pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9384 let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9385 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9386 let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9387 let sum: f32 = exp.iter().sum();
9388 exp.iter().map(|&x| x / sum).collect()
9389}
9390
9391pub fn spec_next_u01(state: &mut u64) -> f32 {
9393 *state = state
9394 .wrapping_mul(6364136223846793005)
9395 .wrapping_add(1442695040888963407);
9396 (*state >> 32) as f32 / u32::MAX as f32
9397}
9398
9399pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9401 let u = spec_next_u01(state);
9402 let mut cum = 0.0_f32;
9403 for (i, &p) in probs.iter().enumerate() {
9404 cum += p;
9405 if u <= cum {
9406 return i as u32;
9407 }
9408 }
9409 (probs.len() - 1) as u32
9410}
9411
9412pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9416 *rng_state = rng_state
9418 .wrapping_mul(6364136223846793005)
9419 .wrapping_add(1442695040888963407);
9420 let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9421 let mut cumulative = 0.0_f32;
9422 for (i, &p) in probs.iter().enumerate() {
9423 cumulative += p;
9424 if u <= cumulative {
9425 return i as u32;
9426 }
9427 }
9428 (probs.len() - 1) as u32
9430}
9431
9432#[cfg(test)]
9433mod lora_tests {
9434 use super::*;
9435 use crate::Shape;
9436
9437 #[test]
9438 fn with_lora_matches_manual_base_plus_lora() {
9439 let in_f = 4;
9441 let out_f = 3;
9442 let rank = 2;
9443 let alpha = 8.0_f32;
9444
9445 let anchor =
9446 Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9447 let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9449 let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9450 let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9451
9452 let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9453 Arc::from(lora_a_vec.clone()),
9454 Arc::from(lora_b_vec.clone()),
9455 rank,
9456 alpha,
9457 in_f,
9458 out_f,
9459 );
9460
9461 let batch = 2;
9463 let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9464 let x = anchor
9465 .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9466 .unwrap();
9467 let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9468 let got = y.realize_f32().to_vec();
9469
9470 let mut expected = vec![0.0_f32; batch * out_f];
9472 for b in 0..batch {
9473 for j in 0..out_f {
9474 let mut acc = 0.0_f32;
9475 for k in 0..in_f {
9477 acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9478 }
9479 let scale = alpha as f64 / rank as f64;
9481 for r in 0..rank {
9482 let mut xar = 0.0_f32;
9483 for k in 0..in_f {
9484 xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9485 }
9486 acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9487 }
9488 expected[b * out_f + j] = acc;
9489 }
9490 }
9491
9492 for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9493 let diff = (e - g).abs();
9494 assert!(
9495 diff <= 1e-4,
9496 "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9497 );
9498 }
9499 }
9500
9501 #[test]
9502 #[should_panic(expected = "lora_a length")]
9503 fn with_lora_rejects_mismatched_a_shape() {
9504 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);
9508 }
9509
9510 #[test]
9522 fn apply_linear_errors_on_trailing_dim_mismatch() {
9523 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x = Tensor::from_f32(
9525 vec![0.0_f32; 10],
9526 Shape::from_dims(&[2, 5]), &Device::cpu(),
9528 )
9529 .unwrap();
9530 let err = ws
9531 .apply_linear(&x, 4, 3)
9532 .expect_err("must not panic, must Err");
9533 let msg = err.to_string();
9534 assert!(
9535 msg.contains("trailing dim 4"),
9536 "error should name the expected trailing dim, got: {msg}",
9537 );
9538 }
9539
9540 #[test]
9541 fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9542 let ws = WeightStorage::Q4_0 {
9545 words: Arc::from(vec![0_u32; 8]),
9546 bytes_len: 18, in_features: 4,
9548 out_features: 4,
9549 };
9550 let x =
9551 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9552 let err = ws
9555 .apply_linear(&x, 4, 7)
9556 .expect_err("must not panic, must Err");
9557 let msg = err.to_string();
9558 assert!(
9559 msg.contains("Q4_0 shape mismatch"),
9560 "error should name the stored/requested disagreement, got: {msg}",
9561 );
9562 }
9563
9564 #[test]
9565 fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9566 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x =
9568 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9569 let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); let err = ws
9571 .apply_linear_with_bias(&x, 4, 3, bias)
9572 .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9573 assert!(
9574 err.to_string().contains("bias len"),
9575 "error should name the bias length, got: {err}",
9576 );
9577 }
9578}
9579
9580#[cfg(test)]
9581mod safetensors_bridge_tests {
9582 use super::*;
9583
9584 #[test]
9585 fn from_safetensors_bytes_round_trip_f32() {
9586 let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9590 let mut bytes = Vec::with_capacity(original.len() * 4);
9591 for &v in &original {
9592 bytes.extend_from_slice(&v.to_le_bytes());
9593 }
9594 let t =
9595 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9596 .unwrap();
9597 assert_eq!(t.shape().dims(), &[4]);
9598 assert_eq!(t.dtype(), DType::F32);
9599 assert_eq!(t.realize_f32(), original);
9600 }
9601
9602 #[test]
9603 fn from_safetensors_bytes_round_trip_bf16() {
9604 let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9605 let bf16_vec: Vec<half::bf16> = original_f32
9606 .iter()
9607 .map(|&v| half::bf16::from_f32(v))
9608 .collect();
9609 let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9610 for b in &bf16_vec {
9611 bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9612 }
9613 let t =
9614 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9615 .unwrap();
9616 assert_eq!(t.dtype(), DType::BF16);
9617 let realized = t.realize_bf16();
9620 assert_eq!(realized, bf16_vec);
9621 }
9622
9623 #[test]
9624 fn from_safetensors_bytes_rejects_wrong_byte_count() {
9625 let bad_bytes = vec![0_u8; 4];
9628 let result = Tensor::from_safetensors_bytes(
9629 &bad_bytes,
9630 safetensors::Dtype::F32,
9631 &[3],
9632 &Device::cpu(),
9633 );
9634 assert!(result.is_err());
9635 }
9636}
9637
9638#[cfg(test)]
9647mod phase_a1_wrapper_tests {
9648 use super::*;
9649
9650 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9651 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9652 }
9653
9654 #[test]
9655 fn unsqueeze_adds_size_one_dim() {
9656 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9657 let out = t.unsqueeze(0_usize).unwrap();
9658 assert_eq!(out.shape().dims(), &[1, 2, 2]);
9659 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9660 }
9661
9662 #[test]
9663 fn unsqueeze_errors_out_of_bounds() {
9664 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9665 assert!(t.unsqueeze(0_usize).is_ok());
9667 assert!(t.unsqueeze(1_usize).is_ok());
9668 assert!(t.unsqueeze(2_usize).is_err());
9669 }
9670
9671 #[test]
9672 fn try_reshape_errors_on_size_mismatch() {
9673 let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9674 assert!(t.reshape(vec![3, 2]).is_ok());
9675 assert!(t.reshape(vec![2, 2]).is_err());
9676 }
9677
9678 #[test]
9679 fn try_permute_validates_axes() {
9680 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9681 assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9683 assert!(t.permute([2_usize, 0, 1]).is_ok());
9684 assert!(t.permute([0_usize, 1]).is_err()); assert!(t.permute([0_usize, 0, 1]).is_err()); }
9687
9688 #[test]
9689 fn try_transpose_requires_rank_two_plus() {
9690 let scalar = cpu_f32(vec![1.0], &[1]);
9691 let _ = scalar.transpose();
9693 }
9694
9695 #[test]
9696 fn triu_tril_shape_preserved() {
9697 let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9698 let upper = t.triu(0).unwrap();
9699 let lower = t.tril(0).unwrap();
9700 assert_eq!(upper.shape().dims(), &[3, 3]);
9701 assert_eq!(lower.shape().dims(), &[3, 3]);
9702 assert_eq!(
9704 lower.realize_f32(),
9705 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9706 );
9707 }
9708
9709 #[test]
9710 fn triu_rejects_rank_one() {
9711 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9712 assert!(t.triu(0).is_err());
9713 }
9714
9715 #[test]
9716 fn log_softmax_last_dim_shape_preserved() {
9717 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9718 let out = t.log_softmax_last_dim().unwrap();
9719 assert_eq!(out.shape().dims(), &[2, 2]);
9720 for v in out.realize_f32() {
9722 assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9723 }
9724 }
9725
9726 #[test]
9727 fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9728 let data: Vec<f32> = vec![
9737 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0, ];
9746 let t = cpu_f32(data, &[2, 3, 2]);
9747 let out = t.softmax(1_usize).unwrap();
9748 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9749 let v = out.realize_f32();
9750
9751 let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9753 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9754 let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9755 let s = exps[0] + exps[1] + exps[2];
9756 [exps[0] / s, exps[1] / s, exps[2] / s]
9757 };
9758 let lanes: [[f32; 3]; 4] = [
9760 [-1.0, 0.0, -2.0], [0.0, -3.0, -1.0], [-2.0, -1.0, 0.0], [-1.0, -1.0, 0.0], ];
9765 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9766
9767 for b in 0..2 {
9769 for c in 0..2 {
9770 let lane_ix = b * 2 + c;
9771 for r in 0..3 {
9772 let got = v[b * 6 + r * 2 + c];
9773 let want = refs[lane_ix][r];
9774 assert!(
9775 (got - want).abs() < 1e-6,
9776 "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9777 );
9778 }
9779 let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9781 assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9782 }
9783 }
9784 }
9785
9786 #[test]
9787 fn softmax_last_axis_matches_softmax_last_dim() {
9788 let data: Vec<f32> = vec![
9789 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,
9791 ];
9792 let t = cpu_f32(data, &[2, 2, 4]);
9793 let via_general = t.softmax(2_usize).unwrap();
9794 let via_fused = t.softmax_last_dim().unwrap();
9795 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9796 let g = via_general.realize_f32();
9797 let f = via_fused.realize_f32();
9798 assert_eq!(g.len(), f.len());
9799 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9800 assert!(
9801 (a - b).abs() < 1e-6,
9802 "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9803 );
9804 }
9805 }
9806
9807 #[test]
9808 fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9809 let data: Vec<f32> = vec![
9812 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9813 ];
9814 let t = cpu_f32(data, &[2, 3, 2]);
9815 let out = t.log_softmax(1_usize).unwrap();
9816 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9817 let v = out.realize_f32();
9818
9819 let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9820 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9821 let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9822 let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9823 [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9824 };
9825 let lanes: [[f32; 3]; 4] = [
9826 [-1.0, 0.0, -2.0],
9827 [0.0, -3.0, -1.0],
9828 [-2.0, -1.0, 0.0],
9829 [-1.0, -1.0, 0.0],
9830 ];
9831 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9832
9833 for b in 0..2 {
9834 for c in 0..2 {
9835 let lane_ix = b * 2 + c;
9836 for r in 0..3 {
9837 let got = v[b * 6 + r * 2 + c];
9838 let want = refs[lane_ix][r];
9839 assert!(
9840 (got - want).abs() < 1e-6,
9841 "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9842 );
9843 assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9845 }
9846 }
9847 }
9848 }
9849
9850 #[test]
9851 fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9852 let data: Vec<f32> = vec![
9853 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,
9854 ];
9855 let t = cpu_f32(data, &[2, 2, 4]);
9856 let via_general = t.log_softmax(2_usize).unwrap();
9857 let via_fused = t.log_softmax_last_dim().unwrap();
9858 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9859 let g = via_general.realize_f32();
9860 let f = via_fused.realize_f32();
9861 assert_eq!(g.len(), f.len());
9862 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9863 assert!(
9864 (a - b).abs() < 1e-6,
9865 "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9866 );
9867 }
9868 }
9869
9870 #[test]
9871 fn argmin_dim_drops_reduced_axis() {
9872 let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9873 let out = t.argmin_dim(1_usize).unwrap();
9874 assert_eq!(out.shape().dims(), &[2]);
9875 assert_eq!(out.dtype(), DType::U32);
9876 assert_eq!(out.realize_u32(), vec![1, 0]);
9877 }
9878
9879 #[test]
9880 fn masked_fill_smoke() {
9881 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9882 let probe = t
9885 .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9886 .unwrap();
9887 let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9888 let mask = probe.gt(&threshold).unwrap(); let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9890 assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9891 }
9892
9893 #[test]
9899 fn bool_is_distinguishable_from_u8_end_to_end() {
9900 let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9905 let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9906 let mask = t.gt(&thr).unwrap();
9907 assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9908 assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9909
9910 let u8_vals = t
9917 .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9918 .unwrap()
9919 .to_dtype(DType::U8)
9920 .unwrap();
9921 assert_eq!(u8_vals.dtype(), DType::U8);
9922 assert_eq!(
9923 u8_vals.realize_u8(),
9924 vec![0, 5, 3, 0],
9925 "negative control: the U8 source must really hold 5 and 3, else the \
9926 conversion assertion below cannot discriminate"
9927 );
9928
9929 let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9933 assert_eq!(as_bool.dtype(), DType::Bool);
9934 assert_eq!(
9935 as_bool.realize_u8(),
9936 vec![0, 1, 1, 0],
9937 "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9938 );
9939
9940 assert_eq!(
9942 as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9943 vec![0.0, 1.0, 1.0, 0.0],
9944 "Bool->F32 yields 0.0/1.0"
9945 );
9946 assert_eq!(
9949 mask.to_dtype(DType::U8).unwrap().realize_u8(),
9950 vec![0, 1, 1, 0]
9951 );
9952
9953 let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9958 assert!(
9959 err.is_err(),
9960 "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9961 byte-identical, so accepting it would 'work' and erase the distinction"
9962 );
9963 }
9964
9965 #[test]
9979 fn float_to_bool_lowering_preserves_ieee_semantics() {
9980 let t = cpu_f32(
9981 vec![
9982 0.0,
9983 -0.0,
9984 5.0,
9985 -3.0,
9986 f32::NAN,
9987 f32::INFINITY,
9988 f32::NEG_INFINITY,
9989 ],
9990 &[7],
9991 );
9992 let b = t.to_dtype(DType::Bool).unwrap();
9993 assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
9994 assert_eq!(
9995 b.realize_u8(),
9996 vec![0, 0, 1, 1, 1, 1, 1],
9997 "0 and -0.0 are false; every other value including NaN and both \
9998 infinities is true",
9999 );
10000 }
10001
10002 #[test]
10003 fn index_add_smoke() {
10004 let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
10005 let src = base
10006 .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
10007 .unwrap();
10008 let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
10009 let out = base.index_add(0, &indices, &src).unwrap();
10010 assert_eq!(out.shape().dims(), &[2, 2]);
10011 let v = out.realize_f32();
10013 assert_eq!(v[0], 41.0); assert_eq!(v[1], 61.0); assert_eq!(v[2], 1.0);
10016 assert_eq!(v[3], 1.0);
10017 }
10018
10019 #[test]
10020 fn scatter_add_smoke() {
10021 let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
10022 let src = base
10023 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10024 .unwrap();
10025 let indices = base
10026 .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
10027 .unwrap();
10028 let out = base.scatter_add(0, &indices, &src).unwrap();
10029 assert_eq!(out.shape().dims(), &[2, 2]);
10030 }
10031
10032 #[test]
10033 fn inplace_activations_compile_and_run() {
10034 let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
10035 let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
10037 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
10038 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
10039 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
10040 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10041 let _ = t.affine_inplace(2.0, 1.0);
10042 let v = r.realize_f32();
10044 assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10045 }
10046
10047 #[test]
10048 fn const_f64_like_round_trips() {
10049 let anchor = cpu_f32(vec![0.0], &[1]);
10050 let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10051 assert_eq!(t.shape().dims(), &[3]);
10052 assert_eq!(t.dtype(), DType::F64);
10053 assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10054 }
10055
10056 #[test]
10057 fn const_i64_like_round_trips() {
10058 let anchor = cpu_f32(vec![0.0], &[1]);
10059 let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10060 assert_eq!(t.shape().dims(), &[3]);
10061 assert_eq!(t.dtype(), DType::I64);
10062 }
10063
10064 #[test]
10065 fn on_device_smoke_cpu() {
10066 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10067 let pinned = t.on_device(&Device::cpu());
10068 assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10069 }
10070
10071 #[test]
10072 fn copy_to_device_same_device_round_trips() {
10073 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10074 let copied = t.copy_to_device(&Device::cpu());
10075 assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10076 }
10077}
10078
10079#[cfg(test)]
10083mod phase_a2_composite_tests {
10084 use super::*;
10085
10086 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10087 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10088 }
10089
10090 #[test]
10091 fn transpose_last_two_swaps_last_two_dims() {
10092 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10093 let out = t.transpose_last_two().unwrap();
10094 assert_eq!(out.shape().dims(), &[3, 2]);
10095 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10096 }
10097
10098 #[test]
10099 fn t_is_alias_of_transpose_last_two() {
10100 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10101 assert_eq!(
10102 t.t().unwrap().realize_f32(),
10103 t.transpose_last_two().unwrap().realize_f32()
10104 );
10105 }
10106
10107 #[test]
10108 fn transpose_dims_swaps_arbitrary_axes() {
10109 let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10110 let out = t.transpose_dims(0, 2).unwrap();
10111 assert_eq!(out.shape().dims(), &[4, 3, 2]);
10112 }
10113
10114 #[test]
10115 fn transpose_dims_identity_when_same_axis() {
10116 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10117 let out = t.transpose_dims(0, 0).unwrap();
10118 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10119 }
10120
10121 #[test]
10122 fn transpose_dims_errors_out_of_bounds() {
10123 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10124 assert!(t.transpose_dims(0, 2).is_err());
10125 assert!(t.transpose_dims(5, 0).is_err());
10126 }
10127
10128 #[test]
10129 fn flatten_merges_middle_dims() {
10130 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10131 let out = t.flatten(0, 1).unwrap();
10132 assert_eq!(out.shape().dims(), &[6, 4]);
10133 }
10134
10135 #[test]
10136 fn flatten_to_merges_leading_dims() {
10137 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10138 let out = t.flatten_to(1).unwrap();
10139 assert_eq!(out.shape().dims(), &[6, 4]);
10140 }
10141
10142 #[test]
10143 fn flatten_from_merges_trailing_dims() {
10144 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10145 let out = t.flatten_from(1).unwrap();
10146 assert_eq!(out.shape().dims(), &[2, 12]);
10147 }
10148
10149 #[test]
10150 fn flatten_all_produces_rank_one() {
10151 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10152 let out = t.flatten_all().unwrap();
10153 assert_eq!(out.shape().dims(), &[6]);
10154 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10155 }
10156
10157 #[test]
10167 fn rope_batched_matches_single_position_rope_row_by_row() {
10168 let lcg = |n: usize, seed: u32| -> Vec<f32> {
10169 let mut s = seed;
10170 (0..n)
10171 .map(|_| {
10172 s = s.wrapping_mul(1103515245).wrapping_add(12345);
10173 ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10174 })
10175 .collect()
10176 };
10177 let (b, heads, hd) = (3usize, 4usize, 8usize);
10178 let base = 10000.0f64;
10179 let positions = [0usize, 7, 3];
10180
10181 let x_data = lcg(b * heads * hd, 11);
10182 let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10183 let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10184 let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10185
10186 let per_row = heads * hd;
10187 for (row, &pos) in positions.iter().enumerate() {
10188 let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10189 let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10190 let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10191 let want = xr
10192 .rope_with_tables_decomposed(&c, &s)
10193 .unwrap()
10194 .realize_f32();
10195 assert_eq!(
10196 &got[row * per_row..(row + 1) * per_row],
10197 &want[..],
10198 "row {row} (position {pos}) != decomposed RoPE at that position",
10199 );
10200 }
10201
10202 let shared = [positions[0]; 3];
10207 let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10208 let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10209 for (row, &pos) in positions.iter().enumerate().skip(1) {
10210 assert_ne!(
10211 &wrong[row * per_row..(row + 1) * per_row],
10212 &got[row * per_row..(row + 1) * per_row],
10213 "CONTROL: row {row} at shared position 0 matched its own position \
10214 {pos} — per-row RoPE not applied, or comparison blind to it",
10215 );
10216 }
10217 }
10218
10219 #[test]
10220 fn flatten_errors_on_bad_range() {
10221 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10222 assert!(t.flatten(0, 5).is_err());
10223 assert!(t.flatten(2, 1).is_err()); }
10225
10226 #[test]
10227 fn split_heads_then_merge_heads_round_trip() {
10228 let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10230 let split = x.split_heads(2, 3).unwrap();
10231 assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10232 let merged = split.merge_heads().unwrap();
10233 assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10234 let m = merged.realize_f32();
10235 let original = x.realize_f32();
10236 for (a, b) in m.iter().zip(original.iter()) {
10237 assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10238 }
10239 }
10240
10241 #[test]
10242 fn rope_tables_const_shapes_and_anchoring() {
10243 let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10244 let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10245 assert_eq!(cos.shape().dims(), &[4, 8]);
10246 assert_eq!(sin.shape().dims(), &[4, 8]);
10247 let cv = cos.realize_f32();
10249 let sv = sin.realize_f32();
10250 for i in 0..8 {
10252 assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10253 assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10254 }
10255 }
10256
10257 #[test]
10258 fn embed_tokens_shape_and_lookup() {
10259 let vocab_size = 5;
10262 let hidden = 3;
10263 let table: Vec<f32> = (0..vocab_size)
10264 .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10265 .collect();
10266 let tokens = vec![1_u32, 3, 0];
10267 let out = Tensor::embed_tokens(
10268 std::sync::Arc::from(table),
10269 vocab_size,
10270 hidden,
10271 &tokens,
10272 &crate::Device::cpu(),
10273 )
10274 .unwrap();
10275 assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10276 let v = out.realize_f32();
10277 let want = [
10278 1.0_f32, 1.5, 2.0, 3.0, 3.5, 4.0, 0.0, 0.5, 1.0, ];
10282 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10283 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10284 }
10285 }
10286
10287 #[test]
10288 fn embed_tokens_anchored_lives_on_receiver_graph() {
10289 let vocab_size = 4;
10293 let hidden = 2;
10294 let table: Vec<f32> = (0..vocab_size)
10295 .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10296 .collect();
10297 let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10298 let tokens = vec![2_u32, 1];
10299
10300 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10301 let embedded = anchor
10302 .embed_tokens_anchored(
10303 std::sync::Arc::clone(&table_arc),
10304 vocab_size,
10305 hidden,
10306 &tokens,
10307 )
10308 .unwrap();
10309 assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10310
10311 let one_scaled = anchor
10313 .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10314 .unwrap();
10315 let _ = embedded
10316 .add(
10317 &one_scaled
10318 .reshape(Shape::from_dims(&[1, 1, 1]))
10319 .unwrap()
10320 .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10321 .unwrap(),
10322 )
10323 .unwrap();
10324 let v = embedded.realize_f32();
10325 let want = [
10326 2.0_f32, 4.0, 1.0, 2.0, ];
10329 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10330 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10331 }
10332 }
10333
10334 #[test]
10335 fn embed_tokens_empty_returns_error() {
10336 let r = Tensor::embed_tokens(
10337 std::sync::Arc::from(vec![0.0_f32]),
10338 1,
10339 1,
10340 &[],
10341 &crate::Device::cpu(),
10342 );
10343 assert!(r.is_err());
10344 }
10345
10346 #[test]
10347 fn softcap_matches_tanh_form() {
10348 let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10350 let capped = x.softcap(10.0).realize_f32();
10351 let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10353 for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10354 assert!(
10355 (got - want).abs() < 1e-3,
10356 "softcap[{i}] got={got} want={want}"
10357 );
10358 }
10359 }
10360
10361 #[test]
10362 fn softcap_optional_none_returns_input_unchanged() {
10363 let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10364 let out = x.softcap_optional(None).realize_f32();
10365 let expect = [1.0_f32, -2.0, 30.0];
10366 for (g, w) in out.iter().zip(expect.iter()) {
10367 assert!((g - w).abs() < 1e-6);
10368 }
10369 let out = x.softcap_optional(Some(0.0)).realize_f32();
10371 for (g, w) in out.iter().zip(expect.iter()) {
10372 assert!((g - w).abs() < 1e-6);
10373 }
10374 }
10375
10376 #[test]
10377 fn rope_partial_full_dim_matches_rope_with_tables() {
10378 let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10380 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10381 let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10382 let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10383 let a = via_partial.realize_f32();
10384 let b = via_full.realize_f32();
10385 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10386 assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10387 }
10388 }
10389
10390 #[test]
10391 fn rope_partial_pass_through_suffix_unchanged() {
10392 let qk = cpu_f32(
10396 vec![
10397 1.0_f32, 2.0, 3.0, 4.0,
10399 ],
10400 &[1, 1, 1, 4],
10401 );
10402 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10403 let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10404 for (i, &v) in out.iter().enumerate() {
10405 let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10406 assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10407 }
10408 }
10409
10410 #[test]
10411 fn add_optional_trailing_bias_none_returns_input_unchanged() {
10412 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10413 let original = a.realize_f32();
10414 let out = a.add_optional_trailing_bias(None).unwrap();
10415 assert_eq!(out.realize_f32(), original);
10416 }
10417
10418 #[test]
10419 fn add_optional_trailing_bias_some_applies_add() {
10420 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10421 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10422 let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10423 assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10424 }
10425
10426 #[test]
10427 fn add_trailing_bias_broadcasts_across_leading_dims() {
10428 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10430 let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10431 let out = x.add_trailing_bias(bias).unwrap();
10432 assert_eq!(out.shape().dims(), &[2, 3]);
10433 let v = out.realize_f32();
10434 assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10435 }
10436
10437 #[test]
10438 fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10439 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10440 let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10441 let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10442 let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10443 let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10444 let a = via_offset.realize_f32();
10445 let b = via_plain.realize_f32();
10446 for (x, y) in a.iter().zip(b.iter()) {
10447 assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10448 }
10449 }
10450
10451 #[test]
10452 fn global_avg_pool_2d_averages_spatial_dims() {
10453 let x = cpu_f32(
10457 vec![
10458 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,
10459 ],
10460 &[1, 2, 2, 3],
10461 );
10462 let out = x.global_avg_pool_2d().unwrap();
10463 assert_eq!(out.shape().dims(), &[1, 2]);
10464 let v = out.realize_f32();
10465 assert!((v[0] - 3.5).abs() < 1e-5);
10466 assert!((v[1] - 35.0).abs() < 1e-4);
10467 }
10468
10469 #[test]
10470 fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10471 let x = cpu_f32(
10473 vec![
10474 1.0_f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0, ],
10477 &[1, 2, 2, 2],
10478 );
10479 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10480 let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10481 let out = x.channel_affine_4d(gain, bias).unwrap();
10482 let v = out.realize_f32();
10483 assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10485 assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10487 }
10488
10489 #[test]
10490 fn additive_causal_mask_has_strict_lower_triangle() {
10491 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10492 let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10493 assert_eq!(mask.shape().dims(), &[4, 4]);
10494 let v = mask.realize_f32();
10495 for i in 0..4 {
10501 for j in 0..4 {
10502 let got = v[i * 4 + j];
10503 if j > i {
10504 assert!(
10505 got.is_infinite() && got.is_sign_negative(),
10506 "above-diag (i={i}, j={j}) should be -inf, got {got}"
10507 );
10508 } else {
10509 assert_eq!(
10510 got, 0.0,
10511 "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10512 );
10513 }
10514 }
10515 }
10516 }
10517
10518 #[test]
10519 fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10520 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10521 let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10522 let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10523 let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10524 let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10525 let va = out_affine.realize_f32();
10526 let vp = out_plain.realize_f32();
10527 for (a, b) in va.iter().zip(vp.iter()) {
10528 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10529 }
10530 }
10531
10532 #[test]
10533 fn layer_norm_affine_applies_gain_and_bias() {
10534 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10535 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10536 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10537 let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10538 let v = out.realize_f32();
10539 let mean = 2.0_f32;
10541 let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10542 let den = (var + 1e-5_f32).sqrt();
10543 let expected = [
10544 ((1.0 - mean) / den) * 2.0 + 10.0,
10545 ((2.0 - mean) / den) * 0.5 + (-5.0),
10546 ((3.0 - mean) / den) * 1.5 + 0.0,
10547 ];
10548 for (got, want) in v.iter().zip(expected.iter()) {
10549 assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10550 }
10551 }
10552
10553 #[test]
10554 fn l2_normalize_last_dim_unit_norm_per_row() {
10555 let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10557 let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10558 assert_eq!(out.shape().dims(), &[2, 3]);
10559 let v = out.realize_f32();
10560 let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10561 let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10562 assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10563 assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10564 assert!((v[0] - 0.6).abs() < 1e-5);
10566 assert!((v[1] - 0.8).abs() < 1e-5);
10567 assert!(v[2].abs() < 1e-5);
10568 }
10569
10570 #[test]
10571 fn l2_normalize_eps_zero_works_when_nonzero() {
10572 let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10573 let out = a.l2_normalize(0_usize, 0.0).unwrap();
10574 let v = out.realize_f32();
10575 assert!((v[0] - 1.0).abs() < 1e-6);
10576 assert!(v[1].abs() < 1e-6);
10577 }
10578
10579 #[test]
10580 fn repeat_interleave_last_dim_matches_torch_semantics() {
10581 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10585 let out = a.repeat_interleave(1_usize, 2).unwrap();
10586 assert_eq!(out.shape().dims(), &[2, 6]);
10587 assert_eq!(
10588 out.realize_f32(),
10589 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]
10590 );
10591 }
10592
10593 #[test]
10594 fn repeat_interleave_middle_dim() {
10595 let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10597 let out = a.repeat_interleave(1_usize, 3).unwrap();
10598 assert_eq!(out.shape().dims(), &[2, 6, 2]);
10599 let v = out.realize_f32();
10601 assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10602 assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10603 }
10604
10605 #[test]
10606 fn repeat_interleave_repeats_one_is_noop() {
10607 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10608 let out = a.repeat_interleave(0_usize, 1).unwrap();
10609 assert_eq!(out.shape().dims(), &[3]);
10610 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10611 }
10612
10613 #[test]
10614 fn repeat_interleave_repeats_zero_errors() {
10615 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10616 assert!(a.repeat_interleave(0_usize, 0).is_err());
10617 }
10618
10619 #[test]
10620 fn stack_adds_leading_dim() {
10621 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10622 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10623 let out = Tensor::stack(&[&a, &b], 0).unwrap();
10624 assert_eq!(out.shape().dims(), &[2, 3]);
10625 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10626 }
10627
10628 #[test]
10629 fn stack_adds_trailing_dim() {
10630 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10631 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10632 let out = Tensor::stack(&[&a, &b], 1).unwrap();
10633 assert_eq!(out.shape().dims(), &[3, 2]);
10634 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10635 }
10636
10637 #[test]
10638 fn stack_rejects_mismatched_shapes() {
10639 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10640 let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10641 assert!(Tensor::stack(&[&a, &b], 0).is_err());
10642 }
10643
10644 #[test]
10645 fn stack_rejects_empty_input() {
10646 let result = Tensor::stack(&[], 0);
10647 assert!(result.is_err());
10648 }
10649
10650 #[test]
10651 fn repeat_tiles_along_each_dim() {
10652 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10653 let out = t.repeat(vec![3]).unwrap();
10654 assert_eq!(out.shape().dims(), &[6]);
10655 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10656 }
10657
10658 #[test]
10659 fn repeat_extends_rank_when_needed() {
10660 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10661 let out = t.repeat(vec![3, 2]).unwrap();
10663 assert_eq!(out.shape().dims(), &[3, 4]);
10664 }
10665
10666 #[test]
10667 fn repeat_identity_with_all_ones() {
10668 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10669 let out = t.repeat(vec![1]).unwrap();
10670 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10671 }
10672}
10673
10674#[cfg(test)]
10678mod phase_a3_keepdim_tests {
10679 use super::*;
10680
10681 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10682 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10683 }
10684
10685 #[test]
10686 fn sum_keepdim_preserves_dim_as_one() {
10687 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10688 let out = t.sum_keepdim(1_usize).unwrap();
10689 assert_eq!(out.shape().dims(), &[2, 1]);
10690 assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10691 }
10692
10693 #[test]
10694 fn mean_keepdim_preserves_dim_as_one() {
10695 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10696 let out = t.mean_keepdim(0_usize).unwrap();
10697 assert_eq!(out.shape().dims(), &[1, 2]);
10698 assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10699 }
10700
10701 #[test]
10702 fn max_keepdim_preserves_dim_as_one() {
10703 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10704 let out = t.max_keepdim(1_usize).unwrap();
10705 assert_eq!(out.shape().dims(), &[2, 1]);
10706 assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10707 }
10708
10709 #[test]
10710 fn min_keepdim_preserves_dim_as_one() {
10711 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10712 let out = t.min_keepdim(1_usize).unwrap();
10713 assert_eq!(out.shape().dims(), &[2, 1]);
10714 assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10715 }
10716
10717 #[test]
10718 fn var_matches_unbiased_formula_with_correction_1() {
10719 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10721 let out = t.var(1, 1.0).unwrap();
10722 assert_eq!(out.shape().dims(), &[2]);
10723 let v = out.realize_f32();
10724 assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10725 assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10726 }
10727
10728 #[test]
10733 fn var_population_with_correction_0() {
10734 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10736 let out = t.var(1, 0.0).unwrap();
10737 let v = out.realize_f32();
10738 let want = 2.0 / 3.0;
10739 assert!(
10740 (v[0] - want).abs() < 1e-5,
10741 "population var row 0 = {} != {want}",
10742 v[0]
10743 );
10744 assert!(
10745 (v[1] - want).abs() < 1e-5,
10746 "population var row 1 = {} != {want}",
10747 v[1]
10748 );
10749 }
10750
10751 #[test]
10752 fn var_keepdim_preserves_dim() {
10753 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10754 let out = t.var_keepdim(1, 1.0).unwrap();
10755 assert_eq!(out.shape().dims(), &[2, 1]);
10756 }
10757
10758 #[test]
10759 fn var_errors_out_of_bounds() {
10760 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10761 assert!(t.var(3, 1.0).is_err());
10762 assert!(t.var_keepdim(3, 1.0).is_err());
10763 }
10764}
10765
10766#[cfg(test)]
10770mod phase_a4_composite_tests {
10771 use super::*;
10772
10773 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10774 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10775 }
10776
10777 #[test]
10778 fn affine_applies_mul_then_add() {
10779 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10780 let out = t.affine(2.0, 10.0);
10781 assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10782 }
10783
10784 #[test]
10785 fn scale_and_shift_alias_of_affine() {
10786 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10787 assert_eq!(
10788 t.scale_and_shift(2.0, 10.0).realize_f32(),
10789 t.affine(2.0, 10.0).realize_f32(),
10790 );
10791 }
10792
10793 #[test]
10794 fn elu_matches_reference_values() {
10795 let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10796 let out = t.elu(1.0);
10797 let v = out.realize_f32();
10798 assert!((v[0] - 1.0).abs() < 1e-5);
10801 assert!(v[1].abs() < 1e-5);
10802 assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10803 assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10804 }
10805
10806 #[test]
10807 fn dot_of_rank_one_vectors() {
10808 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10809 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10810 let out = a.dot(&b).unwrap();
10811 assert_eq!(out.shape().elem_count(), 1);
10812 let v = out.realize_f32();
10813 assert_eq!(v[0], 32.0); }
10815
10816 #[test]
10817 fn dot_rejects_non_rank_one() {
10818 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10819 let b = a
10820 .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10821 .unwrap();
10822 assert!(a.dot(&b).is_err());
10823 }
10824
10825 #[test]
10826 fn dot_rejects_length_mismatch() {
10827 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10828 let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10829 assert!(a.dot(&b).is_err());
10830 }
10831
10832 #[test]
10833 fn mv_matrix_times_vector() {
10834 let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10835 let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10836 let out = m.mv(&v).unwrap();
10837 assert_eq!(out.shape().dims(), &[2]);
10838 assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10839 }
10840
10841 #[test]
10842 fn matvec_is_mv_alias() {
10843 let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10844 let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10845 let a = m.mv(&v).unwrap().realize_f32();
10846 let b = m.matvec(&v).unwrap().realize_f32();
10847 assert_eq!(a, b);
10848 }
10849
10850 #[test]
10851 fn mv_rejects_shape_mismatch() {
10852 let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10853 let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10854 assert!(m.mv(&v).is_err());
10855 }
10856
10857 #[test]
10858 fn broadcast_matmul_passes_through_to_matmul() {
10859 let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10860 let b = a
10861 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10862 .unwrap();
10863 let out = a.broadcast_matmul(&b).unwrap();
10864 assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10865 }
10866}
10867
10868#[cfg(test)]
10872mod phase_a5_factory_tests {
10873 use super::*;
10874
10875 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10876 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10877 }
10878
10879 #[test]
10880 fn ones_like_matches_shape_dtype_graph() {
10881 let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10882 let ones = t.ones_like().unwrap();
10883 assert_eq!(ones.shape().dims(), t.shape().dims());
10884 assert_eq!(ones.dtype(), t.dtype());
10885 assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10886 }
10887
10888 #[test]
10889 fn zeros_like_matches_shape_dtype_graph() {
10890 let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10891 let zeros = t.zeros_like().unwrap();
10892 assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10893 }
10894
10895 #[test]
10896 fn static_ones_f32() {
10897 let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10898 assert_eq!(t.shape().dims(), &[2, 3]);
10899 assert_eq!(t.realize_f32(), vec![1.0; 6]);
10900 }
10901
10902 #[test]
10903 fn static_zeros_f64() {
10904 let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10905 assert_eq!(t.dtype(), DType::F64);
10906 assert_eq!(t.realize_f64(), vec![0.0; 4]);
10907 }
10908
10909 #[test]
10910 fn full_with_f32_scalar() {
10911 let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10912 assert_eq!(t.realize_f32(), vec![2.5; 5]);
10913 }
10914
10915 #[test]
10916 fn eye_identity_matrix() {
10917 let t = Tensor::eye(3, DType::F32, &Device::cpu());
10918 assert_eq!(t.shape().dims(), &[3, 3]);
10919 assert_eq!(
10920 t.realize_f32(),
10921 vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10922 );
10923 }
10924
10925 #[test]
10926 fn tril2_lower_triangular_ones() {
10927 let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10928 assert_eq!(
10929 t.realize_f32(),
10930 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10931 );
10932 }
10933
10934 #[test]
10935 fn triu2_upper_triangular_ones() {
10936 let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10937 assert_eq!(
10938 t.realize_f32(),
10939 vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10940 );
10941 }
10942
10943 #[test]
10944 fn meshgrid_ij_indexing_two_inputs() {
10945 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10946 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10947 let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10948 assert_eq!(grids.len(), 2);
10949 assert_eq!(grids[0].shape().dims(), &[3, 2]);
10951 assert_eq!(grids[1].shape().dims(), &[3, 2]);
10952 assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10954 assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10956 }
10957
10958 #[test]
10959 fn meshgrid_xy_indexing_swaps_first_two() {
10960 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10961 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10962 let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10963 assert_eq!(grids[0].shape().dims(), &[2, 3]);
10965 assert_eq!(grids[1].shape().dims(), &[2, 3]);
10966 assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10968 assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10970 }
10971
10972 #[test]
10973 fn meshgrid_rejects_single_input() {
10974 let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
10975 assert!(Tensor::meshgrid(&[&x], false).is_err());
10976 }
10977
10978 #[test]
10979 fn meshgrid_rejects_non_rank_one() {
10980 let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
10981 let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
10982 assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
10983 }
10984
10985 #[test]
10988 fn narrow_is_slice_alias() {
10989 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
10990 let a = t.narrow(0, 1, 3).unwrap().realize_f32();
10991 let b = t.slice(0, 1, 3).unwrap().realize_f32();
10992 assert_eq!(a, b);
10993 assert_eq!(a, vec![2.0, 3.0, 4.0]);
10994 }
10995
10996 #[test]
10997 fn chunk_splits_evenly() {
10998 let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
10999 let parts = t.chunk(3, 0).unwrap();
11000 assert_eq!(parts.len(), 3);
11001 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
11002 assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
11003 assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
11004 }
11005
11006 #[test]
11007 fn chunk_distributes_remainder_to_leading() {
11008 let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
11010 let parts = t.chunk(3, 0).unwrap();
11011 assert_eq!(parts.len(), 3);
11012 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
11013 assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
11014 assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
11015 }
11016
11017 #[test]
11018 fn chunk_returns_singletons_when_size_less_than_chunks() {
11019 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11020 let parts = t.chunk(5, 0).unwrap();
11021 assert_eq!(parts.len(), 2);
11022 assert_eq!(parts[0].realize_f32(), vec![1.0]);
11023 assert_eq!(parts[1].realize_f32(), vec![2.0]);
11024 }
11025
11026 #[test]
11027 fn get_at_first_dim() {
11028 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11029 let row1 = t.get(1).unwrap();
11030 assert_eq!(row1.shape().dims(), &[2]);
11031 assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
11032 }
11033
11034 #[test]
11035 fn get_on_dim_arbitrary_axis() {
11036 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11037 let col0 = t.get_on_dim(1, 0).unwrap();
11038 assert_eq!(col0.shape().dims(), &[3]);
11039 assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
11040 }
11041
11042 #[test]
11043 fn sum_dims_multi_dim_reduces_to_smaller() {
11044 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11046 let s = t.sum_dims([0, 2_usize]).unwrap();
11047 assert_eq!(s.shape().dims(), &[3]);
11048 assert_eq!(s.realize_f32(), vec![8.0; 3]);
11050 }
11051
11052 #[test]
11053 fn mean_dims_multi_dim() {
11054 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11055 let m = t.mean_dims([0, 1_usize]).unwrap();
11056 assert_eq!(m.shape().dims(), &[] as &[usize]);
11057 assert_eq!(m.realize_f32(), vec![2.5]);
11058 }
11059
11060 #[test]
11061 fn sum_dims_keepdim_preserves_rank() {
11062 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11063 let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11064 assert_eq!(s.shape().dims(), &[1, 3, 1]);
11065 }
11066
11067 #[test]
11068 fn mean_dims_keepdim_preserves_rank() {
11069 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11070 let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11071 assert_eq!(m.shape().dims(), &[1, 1]);
11072 assert_eq!(m.realize_f32(), vec![2.5]);
11073 }
11074
11075 #[test]
11076 fn rand_like_matches_shape_dtype() {
11077 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11078 let r = t.rand_like(-1.0, 1.0).unwrap();
11079 assert_eq!(r.shape().dims(), t.shape().dims());
11080 assert_eq!(r.dtype(), t.dtype());
11081 for v in r.realize_f32() {
11083 assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11084 }
11085 }
11086
11087 #[test]
11088 fn randn_like_matches_shape_dtype() {
11089 let t = cpu_f32(vec![0.0; 4], &[4]);
11090 let r = t.randn_like(0.0, 1.0).unwrap();
11091 assert_eq!(r.shape().dims(), &[4]);
11092 assert_eq!(r.dtype(), DType::F32);
11093 for v in r.realize_f32() {
11095 assert!(v.is_finite());
11096 }
11097 }
11098
11099 #[test]
11100 fn static_rand_f32() {
11101 let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11102 let v = t.realize_f32();
11103 let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11105 assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11106 }
11107
11108 #[test]
11109 fn static_randn_f64() {
11110 let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11111 let v = t.realize_f64();
11112 let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11113 assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11115 }
11116
11117 #[test]
11118 fn arange_int_step() {
11119 let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11120 assert_eq!(t.shape().dims(), &[5]);
11121 assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11122 }
11123
11124 #[test]
11125 fn arange_step_fractional() {
11126 let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11127 assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11128 }
11129
11130 #[test]
11131 fn arange_step_negative_descends() {
11132 let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11133 assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11134 }
11135
11136 #[test]
11137 fn linspace_includes_endpoints() {
11138 let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11139 assert_eq!(t.shape().dims(), &[5]);
11140 let v = t.realize_f32();
11141 assert!((v[0] - 0.0).abs() < 1e-6);
11142 assert!((v[4] - 1.0).abs() < 1e-6);
11143 assert!((v[2] - 0.5).abs() < 1e-6);
11144 }
11145
11146 #[test]
11147 fn linspace_n_one_returns_start() {
11148 let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11149 assert_eq!(t.realize_f32(), vec![7.0]);
11150 }
11151
11152 #[test]
11153 fn norm_is_sqrt_sum_sq() {
11154 let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11155 let n = t.norm();
11156 assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11157 }
11158
11159 #[test]
11160 fn pad_with_zeros_left_and_right() {
11161 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11162 let p = t.pad_with_zeros(0, 2, 1).unwrap();
11163 assert_eq!(p.shape().dims(), &[6]);
11164 assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11165 }
11166
11167 #[test]
11168 fn pad_with_zeros_identity_when_both_zero() {
11169 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11170 let p = t.pad_with_zeros(0, 0, 0).unwrap();
11171 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11172 }
11173
11174 #[test]
11175 fn pad_with_zeros_rejects_bad_dim() {
11176 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11177 assert!(t.pad_with_zeros(3, 1, 1).is_err());
11178 }
11179
11180 #[test]
11181 fn pad_with_value_zero_matches_pad_with_zeros() {
11182 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11185 let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11186 let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11187 assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11188 assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11189 assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11190 assert_eq!(
11191 via_value.realize_f32(),
11192 vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11193 );
11194 }
11195
11196 #[test]
11197 fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11198 let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11202 let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11203 assert_eq!(padded.shape().dims(), &[7]);
11204 let v = padded.realize_f32();
11205 assert!(v[0].is_infinite() && v[0].is_sign_negative());
11207 assert!(v[1].is_infinite() && v[1].is_sign_negative());
11208 assert_eq!(v[2], -1.0);
11209 assert_eq!(v[3], -2.0);
11210 assert_eq!(v[4], -3.0);
11211 assert!(v[5].is_infinite() && v[5].is_sign_negative());
11212 assert!(v[6].is_infinite() && v[6].is_sign_negative());
11213 let m = padded.max_all().realize_f32();
11215 assert_eq!(m, vec![-1.0]);
11216 }
11217
11218 #[test]
11219 fn pad_with_value_identity_when_both_zero() {
11220 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11223 let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11224 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11225 }
11226
11227 #[test]
11228 fn pad_with_value_rejects_bad_dim() {
11229 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11230 assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11231 }
11232
11233 #[test]
11234 fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11235 let x = cpu_f32(
11246 vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11247 &[1, 1, 3, 3],
11248 );
11249 let out = x
11250 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11251 .unwrap();
11252 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11253 let v = out.realize_f32();
11254 assert_eq!(v[0], -1.0);
11257 assert_eq!(v[4], -1.0);
11259 assert_eq!(v[8], -5.0);
11262
11263 let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11273 let vd = default_pad.realize_f32();
11274 assert_eq!(
11275 vd[0], -1.0,
11276 "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11277 );
11278 assert_eq!(vd[8], -5.0);
11279
11280 let explicit_zero_pad = x
11281 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11282 .unwrap();
11283 let vz = explicit_zero_pad.realize_f32();
11284 assert_eq!(
11285 vz[0], 0.0,
11286 "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11287 );
11288 assert_eq!(vz[8], 0.0);
11289 }
11290
11291 #[test]
11292 fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11293 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11296 let x = cpu_f32(data, &[1, 1, 4, 4]);
11297 let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11298 let b = x
11299 .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11300 .unwrap();
11301 assert_eq!(a.shape().dims(), b.shape().dims());
11302 assert_eq!(a.realize_f32(), b.realize_f32());
11303 }
11304
11305 #[test]
11308 fn conv1d_identity_kernel_passes_input_through() {
11309 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11311 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11312 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11313 assert_eq!(out.shape().dims(), &[1, 1, 5]);
11314 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11315 }
11316
11317 #[test]
11318 fn conv1d_sum_kernel_two_wide() {
11319 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11321 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11322 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11323 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11324 assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11325 }
11326
11327 #[test]
11328 fn conv1d_with_bias_applies_correctly() {
11329 let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11330 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11331 let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11332 let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11333 assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11334 }
11335
11336 #[test]
11337 fn conv1d_stride_two_halves_output() {
11338 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11340 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11341 let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11342 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11343 assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11344 }
11345
11346 #[test]
11347 fn conv1d_padding_one_preserves_length() {
11348 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11350 let w = x
11351 .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11352 .unwrap();
11353 let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11354 assert_eq!(out.shape().dims(), &[1, 1, 4]);
11355 assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11358 }
11359
11360 #[test]
11361 fn conv1d_multi_channel_output() {
11362 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11364 let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11366 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11367 assert_eq!(out.shape().dims(), &[1, 2, 3]);
11368 assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11370 }
11371
11372 #[test]
11373 fn conv1d_rejects_rank_two_input() {
11374 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11375 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11376 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11377 }
11378
11379 #[test]
11380 fn conv1d_rejects_rank_two_weight() {
11381 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11382 let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11383 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11384 }
11385
11386 #[test]
11387 fn conv1d_rejects_zero_groups_or_stride() {
11388 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11389 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11390 assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11391 assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11392 }
11393
11394 #[test]
11395 fn conv1d_with_algo_ignores_algo_param() {
11396 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11397 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11398 let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11400 let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11401 assert_eq!(a.realize_f32(), b.realize_f32());
11402 }
11403
11404 #[test]
11407 fn avg_pool2d_2x2_stride2() {
11408 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11410 let x = cpu_f32(data, &[1, 1, 4, 4]);
11411 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11412 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11413 let v = out.realize_f32();
11417 assert!((v[0] - 2.5).abs() < 1e-5);
11418 assert!((v[1] - 4.5).abs() < 1e-5);
11419 assert!((v[2] - 10.5).abs() < 1e-5);
11420 assert!((v[3] - 12.5).abs() < 1e-5);
11421 }
11422
11423 #[test]
11424 fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11425 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11426 let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11427 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11428 }
11429
11430 #[test]
11448 fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11449 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11450 let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11451 let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11452 assert_eq!(included.shape().dims(), excluded.shape().dims());
11453
11454 let ve = excluded.realize_f32();
11455 for (i, &v) in ve.iter().enumerate() {
11456 assert!(
11457 (v - 1.0).abs() < 1e-5,
11458 "count_include_pad=false must be exactly 1.0 at every position \
11459 (all real taps are 1.0) — position {i} got {v}",
11460 );
11461 }
11462
11463 let vi = included.realize_f32();
11464 assert!(
11465 (vi[0] - 1.0).abs() > 0.1,
11466 "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11467 real → 4/9, far from 1.0) — got {}, modes should disagree here",
11468 vi[0]
11469 );
11470 }
11471
11472 #[test]
11481 fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11482 let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11483 let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11484 let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11485 let vi = included.realize_f32();
11486 let ve = excluded.realize_f32();
11487 assert!(
11488 (vi[0] - 2.5).abs() < 1e-5,
11489 "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11490 vi[0]
11491 );
11492 assert!(
11493 (ve[0] - 10.0).abs() < 1e-5,
11494 "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11495 ve[0]
11496 );
11497 }
11498
11499 #[test]
11504 fn avg_pool2d_rejects_zero_valid_tap_window() {
11505 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11506 let err = x
11507 .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11508 .expect_err("a window with zero real taps must be rejected, not silently NaN");
11509 let msg = err.to_string();
11510 assert!(
11511 msg.contains("divide by zero") || msg.contains("no valid"),
11512 "error should explain the zero-valid-tap cause, got: {msg}"
11513 );
11514
11515 let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11520 assert!(
11521 ok.is_ok(),
11522 "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11523 );
11524 }
11525
11526 #[test]
11527 fn avg_pool2d_multi_channel() {
11528 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]);
11530 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11531 assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11532 assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11533 }
11534
11535 #[test]
11536 fn max_pool2d_2x2_stride2() {
11537 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11538 let x = cpu_f32(data, &[1, 1, 4, 4]);
11539 let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11540 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11541 assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11543 }
11544
11545 #[test]
11546 fn max_pool2d_3x3_stride1_padding1() {
11547 let x = cpu_f32(
11548 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11549 &[1, 1, 3, 3],
11550 );
11551 let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11552 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11553 let v = out.realize_f32();
11555 assert!((v[4] - 9.0).abs() < 1e-5);
11557 }
11558
11559 #[test]
11560 fn upsample_nearest2d_2x() {
11561 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11562 let out = x.upsample_nearest2d(2).unwrap();
11563 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11564 assert_eq!(
11566 out.realize_f32(),
11567 vec![
11568 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,
11569 ],
11570 );
11571 }
11572
11573 #[test]
11574 fn upsample_nearest2d_3x() {
11575 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11576 let out = x.upsample_nearest2d(3).unwrap();
11577 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11578 assert_eq!(out.realize_f32(), vec![5.0; 9]);
11579 }
11580
11581 #[test]
11582 fn upsample_nearest2d_identity_scale_one() {
11583 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11584 let out = x.upsample_nearest2d(1).unwrap();
11585 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11586 }
11587
11588 #[test]
11589 fn upsample_nearest1d_2x() {
11590 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11591 let out = x.upsample_nearest1d(2).unwrap();
11592 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11593 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11594 }
11595
11596 #[test]
11597 fn interpolate2d_integer_multiple() {
11598 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11599 let out = x.interpolate2d(4, 4).unwrap();
11600 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11601 }
11602
11603 #[test]
11604 fn interpolate2d_accepts_non_integer_ratio() {
11605 let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11610 let out = x.interpolate2d(3, 4).unwrap();
11611 assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11612 }
11613
11614 #[test]
11615 fn interpolate1d_integer_multiple() {
11616 let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11617 let out = x.interpolate1d(6).unwrap();
11618 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11619 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11620 }
11621
11622 #[test]
11623 fn pool_rejects_bad_rank() {
11624 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11625 assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11626 assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11627 }
11628
11629 #[test]
11630 fn pool_rejects_zero_kernel() {
11631 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11632 assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11633 assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11634 }
11635
11636 #[test]
11639 fn numel_matches_elem_count() {
11640 let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11641 assert_eq!(t.numel(), t.elem_count());
11642 assert_eq!(t.numel(), 12);
11643 }
11644
11645 #[test]
11646 fn dim_returns_specific_axis_size() {
11647 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11648 assert_eq!(t.dim(0).unwrap(), 2);
11649 assert_eq!(t.dim(1).unwrap(), 3);
11650 assert_eq!(t.dim(2).unwrap(), 4);
11651 assert!(t.dim(3).is_err());
11652 }
11653
11654 #[test]
11655 fn to_dtype_switches_dtype() {
11656 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11657 let b = t.to_dtype(DType::F64).unwrap();
11658 assert_eq!(b.dtype(), DType::F64);
11659 assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11660 }
11661
11662 #[test]
11663 fn to_dtype_same_dtype_is_noop() {
11664 let t = cpu_f32(vec![1.0_f32], &[1]);
11665 let b = t.to_dtype(DType::F32).unwrap();
11666 assert_eq!(b.dtype(), DType::F32);
11667 }
11668
11669 #[test]
11670 fn detach_is_identity_on_lazy() {
11671 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11672 let d = t.detach();
11673 assert_eq!(d.realize_f32(), t.realize_f32());
11674 }
11675
11676 #[test]
11677 fn track_op_is_true_on_lazy() {
11678 let t = cpu_f32(vec![0.0], &[1]);
11679 assert!(t.track_op());
11680 }
11681
11682 #[test]
11685 fn try_permute_accepts_tuple_syntax() {
11686 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11687 let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11689 assert_eq!(out.shape().dims(), &[4, 2, 3]);
11690 }
11691
11692 #[test]
11693 fn dim_arg_methods_accept_negative_indexing() {
11694 use fuel_ir::D;
11695 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11696 let sum_last = t.shape().dims().to_vec(); assert_eq!(sum_last, vec![2, 3]);
11699 let cumsum_last = t.cumsum(D::Minus1).unwrap();
11702 assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11703 }
11704
11705 #[test]
11706 fn unsqueeze_accepts_dim_trait() {
11707 use fuel_ir::D;
11708 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11709 let out = t.unsqueeze(D::Minus1).unwrap();
11711 assert_eq!(out.shape().dims().len(), 2);
11714 }
11715}
11716pub struct TokenDataHost {
11717 pub token_ids: fuel_ir::HostBuffer,
11718 pub rope_cos: fuel_ir::HostBuffer,
11719 pub rope_sin: fuel_ir::HostBuffer,
11720 pub mask: fuel_ir::HostBuffer,
11721 pub offset: Option<fuel_ir::HostBuffer>,
11722}
11723
11724#[cfg(feature = "cuda")]
11729pub struct TokenDataBytes {
11730 pub token_ids: Vec<u8>,
11731 pub rope_cos: Vec<u8>,
11732 pub rope_sin: Vec<u8>,
11733 pub mask: Vec<u8>,
11734 pub offset: Option<Vec<u8>>,
11735}
11736
11737#[cfg(feature = "cuda")]
11750pub fn captured_output_to_f32(
11751 output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11752) -> fuel_ir::error::Result<Vec<f32>> {
11753 use fuel_memory::BackendStorage;
11754 let guard = output.read().map_err(|_| {
11755 fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11756 })?;
11757 let bytes: Vec<u8> = match &guard.inner {
11758 BackendStorage::Cpu(c) => c.bytes().to_vec(),
11759 BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11760 #[allow(unreachable_patterns)]
11761 other => {
11762 return Err(fuel_ir::Error::Msg(format!(
11763 "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11764 std::mem::discriminant(other),
11765 ))
11766 .bt());
11767 }
11768 };
11769 Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11770}