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 Awq(Arc<crate::awq::AwqHostWeight>),
8388 WithLoRA {
8399 base: Box<WeightStorage>,
8400 lora_a: Arc<[f32]>,
8402 lora_b: Arc<[f32]>,
8404 rank: usize,
8405 alpha: f32,
8407 in_features: usize,
8408 out_features: usize,
8409 },
8410}
8411
8412impl WeightStorage {
8413 pub fn elem_count(&self) -> usize {
8414 match self {
8415 Self::F32(a) => a.len(),
8416 Self::BF16(a) => a.len(),
8417 Self::Q4_0 {
8419 in_features,
8420 out_features,
8421 ..
8422 } => *in_features * *out_features,
8423 Self::Awq(w) => w.ic * w.oc,
8424 Self::WithLoRA {
8425 in_features,
8426 out_features,
8427 ..
8428 } => *in_features * *out_features,
8429 }
8430 }
8431
8432 pub fn dtype(&self) -> fuel_ir::DType {
8433 match self {
8434 Self::F32(_) => fuel_ir::DType::F32,
8435 Self::BF16(_) => fuel_ir::DType::BF16,
8436 Self::Q4_0 { .. } => fuel_ir::DType::U32,
8440 Self::Awq(_) => fuel_ir::DType::U32,
8442 Self::WithLoRA { base, .. } => base.dtype(),
8445 }
8446 }
8447
8448 pub fn const_like(
8459 &self,
8460 anchor: &Tensor,
8461 shape: Shape,
8462 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8463 match self {
8464 Self::F32(a) => anchor.const_f32_like(a.clone(), shape),
8465 Self::BF16(a) => anchor.const_bf16_like(a.clone(), shape),
8466 Self::Q4_0 { words, .. } => {
8467 let _ = shape; anchor.const_u32_like(Arc::clone(words), Shape::from_dims(&[words.len()]))
8470 }
8471 Self::Awq(_) => Err(fuel_ir::Error::Msg(
8472 "WeightStorage::Awq::const_like is not supported — an AWQ weight is three operands \
8473 (weight, zeros, scales), applied via apply_linear."
8474 .into(),
8475 )
8476 .bt()),
8477 Self::WithLoRA { .. } => Err(fuel_ir::Error::Msg(
8478 "WeightStorage::WithLoRA::const_like is not supported \
8479 — the base + LoRA update must be applied via \
8480 apply_linear to produce the right graph structure."
8481 .into(),
8482 )
8483 .bt()),
8484 }
8485 }
8486
8487 pub fn apply_linear_with_bias(
8496 &self,
8497 x: &Tensor,
8498 in_features: usize,
8499 out_features: usize,
8500 bias: std::sync::Arc<[f32]>,
8501 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8502 if bias.len() != out_features {
8503 return Err(fuel_ir::Error::Msg(format!(
8508 "apply_linear_with_bias: bias len ({}) != out_features ({out_features})",
8509 bias.len(),
8510 ))
8511 .bt());
8512 }
8513 let projected = self.apply_linear(x, in_features, out_features)?;
8514 let bias_t = projected.const_f32_like(bias, Shape::from_dims(&[out_features]))?;
8515 projected.broadcast_add(&bias_t)
8516 }
8517
8518 pub fn apply_linear(
8536 &self,
8537 x: &Tensor,
8538 in_features: usize,
8539 out_features: usize,
8540 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8541 let x_dims = x.shape().dims().to_vec();
8547 match x_dims.last() {
8548 Some(&k) if k == in_features => {}
8549 _ => {
8550 return Err(fuel_ir::Error::Msg(format!(
8551 "apply_linear({in_features} -> {out_features}): input shape {x_dims:?} \
8552 must have trailing dim {in_features}",
8553 ))
8554 .bt());
8555 }
8556 }
8557 match self {
8558 Self::F32(_) | Self::BF16(_) => {
8559 let w = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8560 x.matmul(&w)
8561 }
8562 Self::Q4_0 {
8563 in_features: expected_in,
8564 out_features: expected_out,
8565 ..
8566 } => {
8567 if *expected_in != in_features || *expected_out != out_features {
8568 return Err(fuel_ir::Error::Msg(format!(
8569 "WeightStorage::Q4_0 shape mismatch: stored \
8570 [{expected_in}, {expected_out}], requested \
8571 [{in_features}, {out_features}]",
8572 ))
8573 .bt());
8574 }
8575 let w_bytes = self.const_like(x, Shape::from_dims(&[in_features, out_features]))?;
8576 x.qmatmul(
8577 &w_bytes,
8578 fuel_graph::QuantType::Q4_0,
8579 in_features,
8580 out_features,
8581 )
8582 }
8583 Self::Awq(w) => {
8584 if w.ic != in_features || w.oc != out_features {
8585 return Err(fuel_ir::Error::Msg(format!(
8586 "WeightStorage::Awq shape mismatch: stored [{}, {}], requested [{in_features}, {out_features}]",
8587 w.ic, w.oc,
8588 ))
8589 .bt());
8590 }
8591 w.matmul(x, 8)
8594 }
8595 Self::WithLoRA {
8596 base,
8597 lora_a,
8598 lora_b,
8599 rank,
8600 alpha,
8601 in_features: expected_in,
8602 out_features: expected_out,
8603 } => {
8604 if *expected_in != in_features || *expected_out != out_features {
8605 return Err(fuel_ir::Error::Msg(format!(
8606 "WeightStorage::WithLoRA shape mismatch: stored \
8607 [{expected_in}, {expected_out}], requested \
8608 [{in_features}, {out_features}]",
8609 ))
8610 .bt());
8611 }
8612 let base_out = base.apply_linear(x, in_features, out_features)?;
8614 let a_t =
8616 x.const_f32_like(Arc::clone(lora_a), Shape::from_dims(&[in_features, *rank]))?;
8617 let b_t =
8618 x.const_f32_like(Arc::clone(lora_b), Shape::from_dims(&[*rank, out_features]))?;
8619 let scale = *alpha as f64 / *rank as f64;
8620 let lora_path = Tensor {
8622 inner: x.matmul(&a_t)?.matmul(&b_t)?.inner.mul_scalar(scale),
8623 };
8624 base_out.add(&lora_path)
8625 }
8626 }
8627 }
8628
8629 pub fn apply_linear_dyn_m(
8642 &self,
8643 x: &Tensor,
8644 in_features: usize,
8645 out_features: usize,
8646 row_count: fuel_ir::DynScalar,
8647 ) -> std::result::Result<Tensor, fuel_ir::Error> {
8648 match self {
8649 Self::F32(_) => {
8650 let w = self
8652 .const_like(x, Shape::from_dims(&[in_features, out_features]))
8653 .expect("apply_linear_dyn_m F32 arm: const_like cannot fail for F32");
8654 x.matmul_dyn_m(&w, row_count)
8655 }
8656 other => Err(fuel_ir::Error::Msg(format!(
8657 "apply_linear_dyn_m: sparse-MoE dispatch is F32-only today, got {:?} \
8658 weight (cast the expert weights to F32)",
8659 other.dtype(),
8660 ))
8661 .bt()),
8662 }
8663 }
8664
8665 pub fn with_lora(
8670 self,
8671 lora_a: Arc<[f32]>,
8672 lora_b: Arc<[f32]>,
8673 rank: usize,
8674 alpha: f32,
8675 in_features: usize,
8676 out_features: usize,
8677 ) -> Self {
8678 assert_eq!(
8679 lora_a.len(),
8680 in_features * rank,
8681 "lora_a length {} does not match in_features ({in_features}) × rank ({rank}) = {}",
8682 lora_a.len(),
8683 in_features * rank,
8684 );
8685 assert_eq!(
8686 lora_b.len(),
8687 rank * out_features,
8688 "lora_b length {} does not match rank ({rank}) × out_features ({out_features}) = {}",
8689 lora_b.len(),
8690 rank * out_features,
8691 );
8692 assert!(
8693 !matches!(self, Self::WithLoRA { .. }),
8694 "with_lora: base is already WithLoRA (nested adapters unsupported)",
8695 );
8696 Self::WithLoRA {
8697 base: Box::new(self),
8698 lora_a,
8699 lora_b,
8700 rank,
8701 alpha,
8702 in_features,
8703 out_features,
8704 }
8705 }
8706}
8707
8708impl From<Arc<[f32]>> for WeightStorage {
8712 fn from(a: Arc<[f32]>) -> Self {
8713 Self::F32(a)
8714 }
8715}
8716impl From<Vec<f32>> for WeightStorage {
8717 fn from(v: Vec<f32>) -> Self {
8718 Self::F32(Arc::from(v))
8719 }
8720}
8721impl From<Arc<[half::bf16]>> for WeightStorage {
8722 fn from(a: Arc<[half::bf16]>) -> Self {
8723 Self::BF16(a)
8724 }
8725}
8726impl From<Vec<half::bf16>> for WeightStorage {
8727 fn from(v: Vec<half::bf16>) -> Self {
8728 Self::BF16(Arc::from(v))
8729 }
8730}
8731
8732#[derive(Debug, Clone)]
8744pub struct LayerWeights {
8745 pub attn_q: WeightStorage,
8747 pub attn_q_bias: Option<Arc<[f32]>>,
8749 pub attn_k: WeightStorage,
8751 pub attn_k_bias: Option<Arc<[f32]>>,
8753 pub attn_v: WeightStorage,
8755 pub attn_v_bias: Option<Arc<[f32]>>,
8757 pub attn_o: WeightStorage,
8759 pub ffn_gate: WeightStorage,
8761 pub ffn_up: WeightStorage,
8763 pub ffn_down: WeightStorage,
8765 pub attn_norm_gain: Arc<[f32]>,
8768 pub ffn_norm_gain: Arc<[f32]>,
8770}
8771
8772pub fn invalidate_decode_pair_if_stale<C>(
8773 session: &mut Option<crate::inference_context::DecodeSession>,
8774 captured: &mut Option<C>,
8775 ctx: &mut InferenceContext,
8776 seq: usize,
8777 max_seq_len: Option<usize>,
8778 cache_dtype: DType,
8779 n_layers: usize,
8780 shape_key: u64,
8781 kv: &dyn crate::inference_context::KvRebindSource,
8782 drop_session: impl FnOnce(
8783 &mut Option<crate::inference_context::DecodeSession>,
8784 &mut InferenceContext,
8785 ),
8786) -> SessionDisposition {
8787 refresh_decode_session(
8788 session,
8789 ctx,
8790 seq,
8791 max_seq_len,
8792 cache_dtype,
8793 n_layers,
8794 shape_key,
8795 kv,
8796 || *captured = None,
8800 drop_session,
8801 )
8802}
8803
8804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8806pub enum SessionDisposition {
8807 Absent,
8809 Kept,
8811 Rebound,
8814 Dropped(Option<crate::inference_context::RebindRefusal>),
8818}
8819
8820pub fn refresh_decode_session(
8845 session: &mut Option<crate::inference_context::DecodeSession>,
8846 ctx: &mut InferenceContext,
8847 seq: usize,
8848 max_seq_len: Option<usize>,
8849 cache_dtype: DType,
8850 n_layers: usize,
8851 shape_key: u64,
8852 kv: &dyn crate::inference_context::KvRebindSource,
8853 on_invalidate: impl FnOnce(),
8854 drop_session: impl FnOnce(
8855 &mut Option<crate::inference_context::DecodeSession>,
8856 &mut InferenceContext,
8857 ),
8858) -> SessionDisposition {
8859 use crate::inference_context::PlanValidity;
8860
8861 let verdict = match (session.as_ref(), max_seq_len) {
8862 (None, _) => return SessionDisposition::Absent,
8863 (Some(s), Some(msl)) => {
8864 s.validity_for(seq, msl, n_layers, cache_dtype, shape_key, kv.alloc_id())
8865 }
8866 (Some(_), None) => PlanValidity::Stale,
8869 };
8870 if verdict == PlanValidity::Valid {
8871 return SessionDisposition::Kept;
8872 }
8873
8874 on_invalidate();
8875
8876 let refusal = if verdict == PlanValidity::AllocationChanged {
8877 match session.as_mut().map(|s| s.rebind_kv(kv)) {
8878 Some(Ok(())) => return SessionDisposition::Rebound,
8879 Some(Err(r)) => Some(r),
8880 None => None,
8884 }
8885 } else {
8886 None
8887 };
8888
8889 drop_session(session, ctx);
8890 SessionDisposition::Dropped(refusal)
8891}
8892
8893pub fn build_decode_causal_mask(cached_len: usize, seq: usize, max_seq_len: usize) -> Vec<f32> {
8894 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8895 for q_idx in 0..seq {
8896 let abs_q = cached_len + q_idx;
8897 for k_idx in (abs_q + 1)..max_seq_len {
8898 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8899 }
8900 }
8901 mask_data
8902}
8903
8904pub fn build_decode_causal_mask_windowed(
8944 cached_len: usize,
8945 seq: usize,
8946 max_seq_len: usize,
8947 window: usize,
8948) -> Vec<f32> {
8949 let mut mask_data = vec![0.0_f32; seq * max_seq_len];
8950 for q_idx in 0..seq {
8951 let abs_q = cached_len + q_idx;
8952 for k_idx in 0..max_seq_len {
8953 if k_idx > abs_q || k_idx + window <= abs_q {
8955 mask_data[q_idx * max_seq_len + k_idx] = f32::NEG_INFINITY;
8956 }
8957 }
8958 }
8959 mask_data
8960}
8961
8962#[cfg(test)]
8963mod decode_mask_tests {
8964 use super::{build_decode_causal_mask, build_decode_causal_mask_windowed};
8965
8966 fn attendable(row: &[f32]) -> Vec<u8> {
8969 row.iter().map(|&v| u8::from(v == 0.0)).collect()
8970 }
8971
8972 #[test]
8979 fn windowed_mask_excludes_positions_older_than_the_window() {
8980 let m = build_decode_causal_mask_windowed(3, 1, 6, 2);
8981 assert_eq!(
8982 attendable(&m),
8983 vec![0, 0, 1, 1, 0, 0],
8984 "window=2 at absolute position 3 must attend to exactly {{2, 3}}",
8985 );
8986 }
8987
8988 #[test]
8993 fn windowed_mask_offsets_each_row_by_cached_len() {
8994 let m = build_decode_causal_mask_windowed(1, 2, 5, 2);
8995 assert_eq!(
8996 attendable(&m[0..5]),
8997 vec![1, 1, 0, 0, 0],
8998 "row 0 = abs pos 1"
8999 );
9000 assert_eq!(
9001 attendable(&m[5..10]),
9002 vec![0, 1, 1, 0, 0],
9003 "row 1 = abs pos 2"
9004 );
9005 }
9006
9007 #[test]
9016 fn window_wider_than_capacity_is_byte_identical_to_the_dense_mask() {
9017 for (cached_len, seq, max_seq_len) in [(0, 1, 8), (3, 1, 8), (0, 4, 8), (2, 3, 8)] {
9018 let dense = build_decode_causal_mask(cached_len, seq, max_seq_len);
9019 let windowed =
9020 build_decode_causal_mask_windowed(cached_len, seq, max_seq_len, max_seq_len);
9021 assert_eq!(
9022 dense, windowed,
9023 "window == max_seq_len must equal dense at \
9024 (cached_len={cached_len}, seq={seq}, max_seq_len={max_seq_len})",
9025 );
9026 }
9027 }
9028
9029 #[test]
9038 fn decode_row_matches_the_prefill_mask_row_at_the_same_position() {
9039 let (n, window, max_seq_len) = (6_usize, 3_usize, 10_usize);
9040
9041 let mut prefill = vec![0.0_f32; n * n];
9043 for i in 0..n {
9044 for j in 0..n {
9045 if j > i || j + window <= i {
9046 prefill[i * n + j] = f32::NEG_INFINITY;
9047 }
9048 }
9049 }
9050
9051 for p in 0..n {
9052 let decode = build_decode_causal_mask_windowed(p, 1, max_seq_len, window);
9053 assert_eq!(
9054 attendable(&decode[0..n]),
9055 attendable(&prefill[p * n..(p + 1) * n]),
9056 "decode at cached_len={p} must match prefill row {p}",
9057 );
9058 assert!(
9059 decode[n..].iter().all(|&v| v == f32::NEG_INFINITY),
9060 "the unwritten tail beyond position {n} must stay masked",
9061 );
9062 }
9063 }
9064
9065 }
9106
9107pub fn flash_window_bounds(window: Option<usize>) -> (Option<usize>, Option<usize>) {
9145 match window {
9146 None => (None, None),
9147 Some(w) => (Some(w.saturating_sub(1)), Some(0)),
9148 }
9149}
9150
9151pub fn offer_flash_decode_arm_for_region(
9156 graph: &fuel_graph::SharedGraph,
9157 q: fuel_graph::NodeId,
9158 k: fuel_graph::NodeId,
9159 v: fuel_graph::NodeId,
9160 decomposed_out: fuel_graph::NodeId,
9161 reconverge: fuel_graph::NodeId,
9162 softmax_scale: f32,
9163 attended_len_sym: fuel_ir::SymId,
9164 attn_window: Option<usize>,
9165 softcap: Option<f32>,
9166 cap: fuel_dispatch::decode_flash::FlashArmCapability,
9167) -> crate::Result<Option<fuel_graph::NodeId>> {
9168 use fuel_dispatch::decode_flash::{DecodeFlashSpec, offer_decode_flash_arm};
9169 let (window_size_left, window_size_right) = flash_window_bounds(attn_window);
9170 let spec = DecodeFlashSpec {
9171 q,
9172 k,
9173 v,
9174 alibi: None,
9175 softmax_scale,
9176 causal: true,
9177 window_size_left,
9184 window_size_right,
9185 softcap,
9186 k_len: fuel_ir::DynScalar::Sym(attended_len_sym),
9187 decomposed_out,
9188 reconverge,
9189 };
9190 let mut g = graph.write().map_err(|_| {
9191 fuel_ir::Error::Msg("graph lock poisoned during flash-arm offer".into()).bt()
9192 })?;
9193 offer_decode_flash_arm(&mut g, &spec, cap)
9194}
9195
9196pub fn apply_affine_rms_norm(x: &Tensor, gain: &Arc<[f32]>, dim: usize, eps: f64) -> Tensor {
9197 assert_eq!(
9198 gain.len(),
9199 dim,
9200 "apply_affine_rms_norm: gain length must equal dim"
9201 );
9202 let normalized = x.rms_norm_last_dim(eps).unwrap();
9203 let gain_t = x
9209 .const_like_dtype(gain, Shape::from_dims(&[dim]), x.dtype())
9210 .expect("apply_affine_rms_norm: activation dtype must be F32 or BF16");
9211 normalized.broadcast_mul(&gain_t).unwrap()
9212}
9213
9214pub fn load_tensor_as_f32(
9221 st: &fuel_loaders::safetensors::MmapedSafetensors,
9222 name: &str,
9223) -> crate::Result<Vec<f32>> {
9224 use safetensors::Dtype;
9225 let view = st.get(name)?;
9226 let bytes = view.data();
9227 match view.dtype() {
9228 Dtype::F32 => {
9229 let mut out = Vec::with_capacity(bytes.len() / 4);
9230 for chunk in bytes.as_chunks::<4>().0.iter() {
9231 out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
9232 }
9233 Ok(out)
9234 }
9235 Dtype::F64 => {
9236 let mut out = Vec::with_capacity(bytes.len() / 8);
9237 for chunk in bytes.as_chunks::<8>().0.iter() {
9238 let arr: [u8; 8] = *chunk;
9239 out.push(f64::from_le_bytes(arr) as f32);
9240 }
9241 Ok(out)
9242 }
9243 Dtype::BF16 => {
9244 let mut out = Vec::with_capacity(bytes.len() / 2);
9245 for chunk in bytes.as_chunks::<2>().0.iter() {
9246 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9247 out.push(half::bf16::from_bits(raw).to_f32());
9248 }
9249 Ok(out)
9250 }
9251 Dtype::F16 => {
9252 let mut out = Vec::with_capacity(bytes.len() / 2);
9253 for chunk in bytes.as_chunks::<2>().0.iter() {
9254 let raw = u16::from_le_bytes([chunk[0], chunk[1]]);
9255 out.push(half::f16::from_bits(raw).to_f32());
9256 }
9257 Ok(out)
9258 }
9259 other => {
9260 crate::bail!("load_tensor_as_f32: unsupported dtype {other:?} for tensor {name:?}",)
9261 }
9262 }
9263}
9264
9265pub fn load_transposed_matrix(
9272 st: &fuel_loaders::safetensors::MmapedSafetensors,
9273 name: &str,
9274 out_features: usize,
9275 in_features: usize,
9276) -> crate::Result<Vec<f32>> {
9277 let flat = load_tensor_as_f32(st, name)?;
9278 if flat.len() != out_features * in_features {
9279 crate::bail!(
9280 "load_transposed_matrix: tensor {name:?} has {} elements, expected {} ({out_features} × {in_features})",
9281 flat.len(),
9282 out_features * in_features,
9283 );
9284 }
9285 let mut out = vec![0.0_f32; out_features * in_features];
9289 for i in 0..out_features {
9290 for j in 0..in_features {
9291 out[j * out_features + i] = flat[i * in_features + j];
9292 }
9293 }
9294 Ok(out)
9295}
9296
9297pub fn load_transposed_matrix_preserve_dtype(
9309 st: &fuel_loaders::safetensors::MmapedSafetensors,
9310 name: &str,
9311 out_features: usize,
9312 in_features: usize,
9313) -> crate::Result<WeightStorage> {
9314 use safetensors::Dtype;
9315 let view = st.get(name)?;
9316 let bytes = view.data();
9317 let expected = out_features * in_features;
9318 match view.dtype() {
9319 Dtype::BF16 => {
9320 if bytes.len() != expected * 2 {
9321 crate::bail!(
9322 "load_transposed_matrix_preserve_dtype: bf16 tensor {name:?} has {} bytes, expected {}",
9323 bytes.len(),
9324 expected * 2,
9325 );
9326 }
9327 let mut out = vec![half::bf16::ZERO; expected];
9330 for i in 0..out_features {
9331 for j in 0..in_features {
9332 let src_off = (i * in_features + j) * 2;
9333 let bits = u16::from_le_bytes([bytes[src_off], bytes[src_off + 1]]);
9334 out[j * out_features + i] = half::bf16::from_bits(bits);
9335 }
9336 }
9337 Ok(WeightStorage::BF16(Arc::from(out)))
9338 }
9339 _ => {
9340 let flat = load_transposed_matrix(st, name, out_features, in_features)?;
9344 Ok(WeightStorage::F32(Arc::from(flat)))
9345 }
9346 }
9347}
9348
9349#[derive(Debug, Clone, Copy, Default)]
9351pub enum SamplingStrategy {
9352 #[default]
9354 Greedy,
9355 Temperature { temp: f32, seed: u64 },
9360}
9361
9362pub fn sample_logits(logits: &[f32], strategy: SamplingStrategy, rng_state: &mut u64) -> u32 {
9366 match strategy {
9367 SamplingStrategy::Greedy => {
9368 let (i, _) = logits
9369 .iter()
9370 .enumerate()
9371 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
9372 .expect("sample_logits: empty logits");
9373 i as u32
9374 }
9375 SamplingStrategy::Temperature { temp, .. } => {
9376 let inv_temp = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9379 let scaled: Vec<f32> = logits.iter().map(|&x| x * inv_temp).collect();
9380 let max = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9381 let exp: Vec<f32> = scaled.iter().map(|&x| (x - max).exp()).collect();
9382 let sum: f32 = exp.iter().sum();
9383 let probs: Vec<f32> = exp.iter().map(|&x| x / sum).collect();
9384 sample_multinomial(&probs, rng_state)
9385 }
9386 }
9387}
9388
9389pub fn spec_argmax(logits: &[f32]) -> u32 {
9397 let mut best = 0;
9398 let mut best_v = logits[0];
9399 for (i, &v) in logits.iter().enumerate().skip(1) {
9400 if v > best_v {
9401 best_v = v;
9402 best = i;
9403 }
9404 }
9405 best as u32
9406}
9407
9408pub fn spec_softmax_temp(logits: &[f32], temp: f32) -> Vec<f32> {
9410 let inv_t = if temp == 0.0 { 1.0 } else { 1.0 / temp };
9411 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9412 let exp: Vec<f32> = logits.iter().map(|&x| ((x - max) * inv_t).exp()).collect();
9413 let sum: f32 = exp.iter().sum();
9414 exp.iter().map(|&x| x / sum).collect()
9415}
9416
9417pub fn spec_next_u01(state: &mut u64) -> f32 {
9419 *state = state
9420 .wrapping_mul(6364136223846793005)
9421 .wrapping_add(1442695040888963407);
9422 (*state >> 32) as f32 / u32::MAX as f32
9423}
9424
9425pub fn spec_sample_cat(probs: &[f32], state: &mut u64) -> u32 {
9427 let u = spec_next_u01(state);
9428 let mut cum = 0.0_f32;
9429 for (i, &p) in probs.iter().enumerate() {
9430 cum += p;
9431 if u <= cum {
9432 return i as u32;
9433 }
9434 }
9435 (probs.len() - 1) as u32
9436}
9437
9438pub fn sample_multinomial(probs: &[f32], rng_state: &mut u64) -> u32 {
9442 *rng_state = rng_state
9444 .wrapping_mul(6364136223846793005)
9445 .wrapping_add(1442695040888963407);
9446 let u = (*rng_state >> 32) as f32 / u32::MAX as f32;
9447 let mut cumulative = 0.0_f32;
9448 for (i, &p) in probs.iter().enumerate() {
9449 cumulative += p;
9450 if u <= cumulative {
9451 return i as u32;
9452 }
9453 }
9454 (probs.len() - 1) as u32
9456}
9457
9458#[cfg(test)]
9459mod lora_tests {
9460 use super::*;
9461 use crate::Shape;
9462
9463 #[test]
9464 fn with_lora_matches_manual_base_plus_lora() {
9465 let in_f = 4;
9467 let out_f = 3;
9468 let rank = 2;
9469 let alpha = 8.0_f32;
9470
9471 let anchor =
9472 Tensor::from_f32(vec![0.0_f32; 1], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
9473 let base_vec: Vec<f32> = (0..in_f * out_f).map(|i| (i as f32) * 0.1).collect();
9475 let lora_a_vec: Vec<f32> = (0..in_f * rank).map(|i| (i as f32) * 0.05).collect();
9476 let lora_b_vec: Vec<f32> = (0..rank * out_f).map(|i| (i as f32) * 0.02).collect();
9477
9478 let ws = WeightStorage::F32(Arc::from(base_vec.clone())).with_lora(
9479 Arc::from(lora_a_vec.clone()),
9480 Arc::from(lora_b_vec.clone()),
9481 rank,
9482 alpha,
9483 in_f,
9484 out_f,
9485 );
9486
9487 let batch = 2;
9489 let x_data: Vec<f32> = (0..batch * in_f).map(|i| (i as f32) * 0.1 + 0.5).collect();
9490 let x = anchor
9491 .const_f32_like(x_data.clone(), Shape::from_dims(&[batch, in_f]))
9492 .unwrap();
9493 let y = ws.apply_linear(&x, in_f, out_f).unwrap();
9494 let got = y.realize_f32().to_vec();
9495
9496 let mut expected = vec![0.0_f32; batch * out_f];
9498 for b in 0..batch {
9499 for j in 0..out_f {
9500 let mut acc = 0.0_f32;
9501 for k in 0..in_f {
9503 acc += x_data[b * in_f + k] * base_vec[k * out_f + j];
9504 }
9505 let scale = alpha as f64 / rank as f64;
9507 for r in 0..rank {
9508 let mut xar = 0.0_f32;
9509 for k in 0..in_f {
9510 xar += x_data[b * in_f + k] * lora_a_vec[k * rank + r];
9511 }
9512 acc += (xar * lora_b_vec[r * out_f + j]) * scale as f32;
9513 }
9514 expected[b * out_f + j] = acc;
9515 }
9516 }
9517
9518 for (i, (&e, &g)) in expected.iter().zip(got.iter()).enumerate() {
9519 let diff = (e - g).abs();
9520 assert!(
9521 diff <= 1e-4,
9522 "LoRA mismatch at {i}: expected {e}, got {g} (diff {diff})",
9523 );
9524 }
9525 }
9526
9527 #[test]
9528 #[should_panic(expected = "lora_a length")]
9529 fn with_lora_rejects_mismatched_a_shape() {
9530 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);
9534 }
9535
9536 #[test]
9548 fn apply_linear_errors_on_trailing_dim_mismatch() {
9549 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x = Tensor::from_f32(
9551 vec![0.0_f32; 10],
9552 Shape::from_dims(&[2, 5]), &Device::cpu(),
9554 )
9555 .unwrap();
9556 let err = ws
9557 .apply_linear(&x, 4, 3)
9558 .expect_err("must not panic, must Err");
9559 let msg = err.to_string();
9560 assert!(
9561 msg.contains("trailing dim 4"),
9562 "error should name the expected trailing dim, got: {msg}",
9563 );
9564 }
9565
9566 #[test]
9567 fn apply_linear_errors_on_q4_0_stored_shape_mismatch() {
9568 let ws = WeightStorage::Q4_0 {
9571 words: Arc::from(vec![0_u32; 8]),
9572 bytes_len: 18, in_features: 4,
9574 out_features: 4,
9575 };
9576 let x =
9577 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9578 let err = ws
9581 .apply_linear(&x, 4, 7)
9582 .expect_err("must not panic, must Err");
9583 let msg = err.to_string();
9584 assert!(
9585 msg.contains("Q4_0 shape mismatch"),
9586 "error should name the stored/requested disagreement, got: {msg}",
9587 );
9588 }
9589
9590 #[test]
9591 fn apply_linear_with_bias_errors_on_wrong_bias_length() {
9592 let ws = WeightStorage::F32(Arc::from(vec![0.0_f32; 12])); let x =
9594 Tensor::from_f32(vec![0.0_f32; 8], Shape::from_dims(&[2, 4]), &Device::cpu()).unwrap();
9595 let bias: Arc<[f32]> = Arc::from(vec![0.0_f32; 2]); let err = ws
9597 .apply_linear_with_bias(&x, 4, 3, bias)
9598 .expect_err("wrong-length bias must Err in EVERY profile, not just debug");
9599 assert!(
9600 err.to_string().contains("bias len"),
9601 "error should name the bias length, got: {err}",
9602 );
9603 }
9604}
9605
9606#[cfg(test)]
9607mod safetensors_bridge_tests {
9608 use super::*;
9609
9610 #[test]
9611 fn from_safetensors_bytes_round_trip_f32() {
9612 let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
9616 let mut bytes = Vec::with_capacity(original.len() * 4);
9617 for &v in &original {
9618 bytes.extend_from_slice(&v.to_le_bytes());
9619 }
9620 let t =
9621 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::F32, &[4], &Device::cpu())
9622 .unwrap();
9623 assert_eq!(t.shape().dims(), &[4]);
9624 assert_eq!(t.dtype(), DType::F32);
9625 assert_eq!(t.realize_f32(), original);
9626 }
9627
9628 #[test]
9629 fn from_safetensors_bytes_round_trip_bf16() {
9630 let original_f32: Vec<f32> = vec![0.5, -1.0, 2.0, 4.0];
9631 let bf16_vec: Vec<half::bf16> = original_f32
9632 .iter()
9633 .map(|&v| half::bf16::from_f32(v))
9634 .collect();
9635 let mut bytes = Vec::with_capacity(bf16_vec.len() * 2);
9636 for b in &bf16_vec {
9637 bytes.extend_from_slice(&b.to_bits().to_le_bytes());
9638 }
9639 let t =
9640 Tensor::from_safetensors_bytes(&bytes, safetensors::Dtype::BF16, &[4], &Device::cpu())
9641 .unwrap();
9642 assert_eq!(t.dtype(), DType::BF16);
9643 let realized = t.realize_bf16();
9646 assert_eq!(realized, bf16_vec);
9647 }
9648
9649 #[test]
9650 fn from_safetensors_bytes_rejects_wrong_byte_count() {
9651 let bad_bytes = vec![0_u8; 4];
9654 let result = Tensor::from_safetensors_bytes(
9655 &bad_bytes,
9656 safetensors::Dtype::F32,
9657 &[3],
9658 &Device::cpu(),
9659 );
9660 assert!(result.is_err());
9661 }
9662}
9663
9664#[cfg(test)]
9673mod phase_a1_wrapper_tests {
9674 use super::*;
9675
9676 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
9677 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
9678 }
9679
9680 #[test]
9681 fn unsqueeze_adds_size_one_dim() {
9682 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9683 let out = t.unsqueeze(0_usize).unwrap();
9684 assert_eq!(out.shape().dims(), &[1, 2, 2]);
9685 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
9686 }
9687
9688 #[test]
9689 fn unsqueeze_errors_out_of_bounds() {
9690 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9691 assert!(t.unsqueeze(0_usize).is_ok());
9693 assert!(t.unsqueeze(1_usize).is_ok());
9694 assert!(t.unsqueeze(2_usize).is_err());
9695 }
9696
9697 #[test]
9698 fn try_reshape_errors_on_size_mismatch() {
9699 let t = cpu_f32(vec![1.0; 6], &[2, 3]);
9700 assert!(t.reshape(vec![3, 2]).is_ok());
9701 assert!(t.reshape(vec![2, 2]).is_err());
9702 }
9703
9704 #[test]
9705 fn try_permute_validates_axes() {
9706 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
9707 assert!(t.permute((2_usize, 0_usize, 1_usize)).is_ok());
9709 assert!(t.permute([2_usize, 0, 1]).is_ok());
9710 assert!(t.permute([0_usize, 1]).is_err()); assert!(t.permute([0_usize, 0, 1]).is_err()); }
9713
9714 #[test]
9715 fn try_transpose_requires_rank_two_plus() {
9716 let scalar = cpu_f32(vec![1.0], &[1]);
9717 let _ = scalar.transpose();
9719 }
9720
9721 #[test]
9722 fn triu_tril_shape_preserved() {
9723 let t = cpu_f32(vec![1.0; 9], &[3, 3]);
9724 let upper = t.triu(0).unwrap();
9725 let lower = t.tril(0).unwrap();
9726 assert_eq!(upper.shape().dims(), &[3, 3]);
9727 assert_eq!(lower.shape().dims(), &[3, 3]);
9728 assert_eq!(
9730 lower.realize_f32(),
9731 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0]
9732 );
9733 }
9734
9735 #[test]
9736 fn triu_rejects_rank_one() {
9737 let t = cpu_f32(vec![1.0, 2.0], &[2]);
9738 assert!(t.triu(0).is_err());
9739 }
9740
9741 #[test]
9742 fn log_softmax_last_dim_shape_preserved() {
9743 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9744 let out = t.log_softmax_last_dim().unwrap();
9745 assert_eq!(out.shape().dims(), &[2, 2]);
9746 for v in out.realize_f32() {
9748 assert!(v <= 0.0 + 1e-6, "log_softmax produced positive value: {v}");
9749 }
9750 }
9751
9752 #[test]
9753 fn softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9754 let data: Vec<f32> = vec![
9763 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0, ];
9772 let t = cpu_f32(data, &[2, 3, 2]);
9773 let out = t.softmax(1_usize).unwrap();
9774 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9775 let v = out.realize_f32();
9776
9777 let lane_softmax = |xs: [f32; 3]| -> [f32; 3] {
9779 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9780 let exps = [(xs[0] - m).exp(), (xs[1] - m).exp(), (xs[2] - m).exp()];
9781 let s = exps[0] + exps[1] + exps[2];
9782 [exps[0] / s, exps[1] / s, exps[2] / s]
9783 };
9784 let lanes: [[f32; 3]; 4] = [
9786 [-1.0, 0.0, -2.0], [0.0, -3.0, -1.0], [-2.0, -1.0, 0.0], [-1.0, -1.0, 0.0], ];
9791 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_softmax(*l)).collect();
9792
9793 for b in 0..2 {
9795 for c in 0..2 {
9796 let lane_ix = b * 2 + c;
9797 for r in 0..3 {
9798 let got = v[b * 6 + r * 2 + c];
9799 let want = refs[lane_ix][r];
9800 assert!(
9801 (got - want).abs() < 1e-6,
9802 "softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9803 );
9804 }
9805 let sum: f32 = (0..3).map(|r| v[b * 6 + r * 2 + c]).sum();
9807 assert!((sum - 1.0).abs() < 1e-6, "lane (b={b},c={c}) sums to {sum}");
9808 }
9809 }
9810 }
9811
9812 #[test]
9813 fn softmax_last_axis_matches_softmax_last_dim() {
9814 let data: Vec<f32> = vec![
9815 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,
9817 ];
9818 let t = cpu_f32(data, &[2, 2, 4]);
9819 let via_general = t.softmax(2_usize).unwrap();
9820 let via_fused = t.softmax_last_dim().unwrap();
9821 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9822 let g = via_general.realize_f32();
9823 let f = via_fused.realize_f32();
9824 assert_eq!(g.len(), f.len());
9825 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9826 assert!(
9827 (a - b).abs() < 1e-6,
9828 "softmax (general axis=2) vs softmax_last_dim diverge at {i}: {a} vs {b}",
9829 );
9830 }
9831 }
9832
9833 #[test]
9834 fn log_softmax_general_axis_matches_hand_computed_rank3_middle_axis() {
9835 let data: Vec<f32> = vec![
9838 -1.0, 0.0, 0.0, -3.0, -2.0, -1.0, -2.0, -1.0, -1.0, -1.0, 0.0, 0.0,
9839 ];
9840 let t = cpu_f32(data, &[2, 3, 2]);
9841 let out = t.log_softmax(1_usize).unwrap();
9842 assert_eq!(out.shape().dims(), &[2, 3, 2]);
9843 let v = out.realize_f32();
9844
9845 let lane_log_softmax = |xs: [f32; 3]| -> [f32; 3] {
9846 let m = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9847 let shifted = [xs[0] - m, xs[1] - m, xs[2] - m];
9848 let lse = (shifted[0].exp() + shifted[1].exp() + shifted[2].exp()).ln();
9849 [shifted[0] - lse, shifted[1] - lse, shifted[2] - lse]
9850 };
9851 let lanes: [[f32; 3]; 4] = [
9852 [-1.0, 0.0, -2.0],
9853 [0.0, -3.0, -1.0],
9854 [-2.0, -1.0, 0.0],
9855 [-1.0, -1.0, 0.0],
9856 ];
9857 let refs: Vec<[f32; 3]> = lanes.iter().map(|l| lane_log_softmax(*l)).collect();
9858
9859 for b in 0..2 {
9860 for c in 0..2 {
9861 let lane_ix = b * 2 + c;
9862 for r in 0..3 {
9863 let got = v[b * 6 + r * 2 + c];
9864 let want = refs[lane_ix][r];
9865 assert!(
9866 (got - want).abs() < 1e-6,
9867 "log_softmax mismatch at (b={b}, r={r}, c={c}): got {got}, want {want}",
9868 );
9869 assert!(got <= 1e-6, "log_softmax produced positive value: {got}");
9871 }
9872 }
9873 }
9874 }
9875
9876 #[test]
9877 fn log_softmax_last_axis_matches_log_softmax_last_dim() {
9878 let data: Vec<f32> = vec![
9879 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,
9880 ];
9881 let t = cpu_f32(data, &[2, 2, 4]);
9882 let via_general = t.log_softmax(2_usize).unwrap();
9883 let via_fused = t.log_softmax_last_dim().unwrap();
9884 assert_eq!(via_general.shape().dims(), via_fused.shape().dims());
9885 let g = via_general.realize_f32();
9886 let f = via_fused.realize_f32();
9887 assert_eq!(g.len(), f.len());
9888 for (i, (a, b)) in g.iter().zip(f.iter()).enumerate() {
9889 assert!(
9890 (a - b).abs() < 1e-6,
9891 "log_softmax (general axis=2) vs log_softmax_last_dim diverge at {i}: {a} vs {b}",
9892 );
9893 }
9894 }
9895
9896 #[test]
9897 fn argmin_dim_drops_reduced_axis() {
9898 let t = cpu_f32(vec![3.0, 1.0, 2.0, 0.5, 5.0, 4.0], &[2, 3]);
9899 let out = t.argmin_dim(1_usize).unwrap();
9900 assert_eq!(out.shape().dims(), &[2]);
9901 assert_eq!(out.dtype(), DType::U32);
9902 assert_eq!(out.realize_u32(), vec![1, 0]);
9903 }
9904
9905 #[test]
9906 fn masked_fill_smoke() {
9907 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
9908 let probe = t
9911 .const_f32_like(vec![0.0, 1.0, 1.0, 0.0], vec![2, 2])
9912 .unwrap();
9913 let threshold = t.const_f32_like(vec![0.5; 4], vec![2, 2]).unwrap();
9914 let mask = probe.gt(&threshold).unwrap(); let out = t.masked_fill(&mask, fuel_ir::Scalar::F32(-9.0)).unwrap();
9916 assert_eq!(out.realize_f32(), vec![1.0, -9.0, -9.0, 4.0]);
9917 }
9918
9919 #[test]
9925 fn bool_is_distinguishable_from_u8_end_to_end() {
9926 let t = cpu_f32(vec![0.0, 5.0, 3.0, 0.0], &[4]);
9931 let thr = t.const_f32_like(vec![0.5; 4], vec![4]).unwrap();
9932 let mask = t.gt(&thr).unwrap();
9933 assert_eq!(mask.dtype(), DType::Bool, "gt yields Bool, not U8");
9934 assert_eq!(mask.realize_u8(), vec![0, 1, 1, 0], "mask bytes are 0/1");
9935
9936 let u8_vals = t
9943 .const_f32_like(vec![0.0, 5.0, 3.0, 0.0], vec![4])
9944 .unwrap()
9945 .to_dtype(DType::U8)
9946 .unwrap();
9947 assert_eq!(u8_vals.dtype(), DType::U8);
9948 assert_eq!(
9949 u8_vals.realize_u8(),
9950 vec![0, 5, 3, 0],
9951 "negative control: the U8 source must really hold 5 and 3, else the \
9952 conversion assertion below cannot discriminate"
9953 );
9954
9955 let as_bool = u8_vals.to_dtype(DType::Bool).unwrap();
9959 assert_eq!(as_bool.dtype(), DType::Bool);
9960 assert_eq!(
9961 as_bool.realize_u8(),
9962 vec![0, 1, 1, 0],
9963 "U8->Bool must CONVERT (5 -> 1), not reinterpret (5 -> 5)"
9964 );
9965
9966 assert_eq!(
9968 as_bool.to_dtype(DType::F32).unwrap().realize_f32(),
9969 vec![0.0, 1.0, 1.0, 0.0],
9970 "Bool->F32 yields 0.0/1.0"
9971 );
9972 assert_eq!(
9975 mask.to_dtype(DType::U8).unwrap().realize_u8(),
9976 vec![0, 1, 1, 0]
9977 );
9978
9979 let err = t.masked_fill(&u8_vals, fuel_ir::Scalar::F32(-9.0));
9984 assert!(
9985 err.is_err(),
9986 "a U8 mask must be REJECTED by masked_fill — Bool and U8 are \
9987 byte-identical, so accepting it would 'work' and erase the distinction"
9988 );
9989 }
9990
9991 #[test]
10005 fn float_to_bool_lowering_preserves_ieee_semantics() {
10006 let t = cpu_f32(
10007 vec![
10008 0.0,
10009 -0.0,
10010 5.0,
10011 -3.0,
10012 f32::NAN,
10013 f32::INFINITY,
10014 f32::NEG_INFINITY,
10015 ],
10016 &[7],
10017 );
10018 let b = t.to_dtype(DType::Bool).unwrap();
10019 assert_eq!(b.dtype(), DType::Bool, "to_dtype(Bool) still yields Bool");
10020 assert_eq!(
10021 b.realize_u8(),
10022 vec![0, 0, 1, 1, 1, 1, 1],
10023 "0 and -0.0 are false; every other value including NaN and both \
10024 infinities is true",
10025 );
10026 }
10027
10028 #[test]
10029 fn index_add_smoke() {
10030 let base = cpu_f32(vec![1.0, 1.0, 1.0, 1.0], &[2, 2]);
10031 let src = base
10032 .const_f32_like(vec![10.0, 20.0, 30.0, 40.0], vec![2, 2])
10033 .unwrap();
10034 let indices = base.const_u32_like(vec![0_u32, 0_u32], vec![2]).unwrap();
10035 let out = base.index_add(0, &indices, &src).unwrap();
10036 assert_eq!(out.shape().dims(), &[2, 2]);
10037 let v = out.realize_f32();
10039 assert_eq!(v[0], 41.0); assert_eq!(v[1], 61.0); assert_eq!(v[2], 1.0);
10042 assert_eq!(v[3], 1.0);
10043 }
10044
10045 #[test]
10046 fn scatter_add_smoke() {
10047 let base = cpu_f32(vec![0.0, 0.0, 0.0, 0.0], &[2, 2]);
10048 let src = base
10049 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10050 .unwrap();
10051 let indices = base
10052 .const_u32_like(vec![0_u32, 1_u32, 1_u32, 0_u32], vec![2, 2])
10053 .unwrap();
10054 let out = base.scatter_add(0, &indices, &src).unwrap();
10055 assert_eq!(out.shape().dims(), &[2, 2]);
10056 }
10057
10058 #[test]
10059 fn inplace_activations_compile_and_run() {
10060 let t = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]);
10061 let r = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).relu_inplace();
10063 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).silu_inplace();
10064 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).gelu_tanh_inplace();
10065 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).tanh_inplace();
10066 let _ = cpu_f32(vec![-1.0, 0.5, -3.0, 2.0], &[4]).sigmoid_inplace();
10067 let _ = t.affine_inplace(2.0, 1.0);
10068 let v = r.realize_f32();
10070 assert_eq!(v, vec![0.0, 0.5, 0.0, 2.0]);
10071 }
10072
10073 #[test]
10074 fn const_f64_like_round_trips() {
10075 let anchor = cpu_f32(vec![0.0], &[1]);
10076 let t = anchor.const_f64_like(vec![1.5, 2.5, 3.5], vec![3]).unwrap();
10077 assert_eq!(t.shape().dims(), &[3]);
10078 assert_eq!(t.dtype(), DType::F64);
10079 assert_eq!(t.realize_f64(), vec![1.5, 2.5, 3.5]);
10080 }
10081
10082 #[test]
10083 fn const_i64_like_round_trips() {
10084 let anchor = cpu_f32(vec![0.0], &[1]);
10085 let t = anchor.const_i64_like(vec![-1_i64, 2, -3], vec![3]).unwrap();
10086 assert_eq!(t.shape().dims(), &[3]);
10087 assert_eq!(t.dtype(), DType::I64);
10088 }
10089
10090 #[test]
10091 fn on_device_smoke_cpu() {
10092 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10093 let pinned = t.on_device(&Device::cpu());
10094 assert_eq!(pinned.realize_f32(), vec![1.0, 2.0]);
10095 }
10096
10097 #[test]
10098 fn copy_to_device_same_device_round_trips() {
10099 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10100 let copied = t.copy_to_device(&Device::cpu());
10101 assert_eq!(copied.realize_f32(), vec![1.0, 2.0, 3.0]);
10102 }
10103}
10104
10105#[cfg(test)]
10109mod phase_a2_composite_tests {
10110 use super::*;
10111
10112 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10113 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10114 }
10115
10116 #[test]
10117 fn transpose_last_two_swaps_last_two_dims() {
10118 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10119 let out = t.transpose_last_two().unwrap();
10120 assert_eq!(out.shape().dims(), &[3, 2]);
10121 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10122 }
10123
10124 #[test]
10125 fn t_is_alias_of_transpose_last_two() {
10126 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10127 assert_eq!(
10128 t.t().unwrap().realize_f32(),
10129 t.transpose_last_two().unwrap().realize_f32()
10130 );
10131 }
10132
10133 #[test]
10134 fn transpose_dims_swaps_arbitrary_axes() {
10135 let t = cpu_f32((0..24).map(|i| i as f32).collect(), &[2, 3, 4]);
10136 let out = t.transpose_dims(0, 2).unwrap();
10137 assert_eq!(out.shape().dims(), &[4, 3, 2]);
10138 }
10139
10140 #[test]
10141 fn transpose_dims_identity_when_same_axis() {
10142 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10143 let out = t.transpose_dims(0, 0).unwrap();
10144 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
10145 }
10146
10147 #[test]
10148 fn transpose_dims_errors_out_of_bounds() {
10149 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10150 assert!(t.transpose_dims(0, 2).is_err());
10151 assert!(t.transpose_dims(5, 0).is_err());
10152 }
10153
10154 #[test]
10155 fn flatten_merges_middle_dims() {
10156 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10157 let out = t.flatten(0, 1).unwrap();
10158 assert_eq!(out.shape().dims(), &[6, 4]);
10159 }
10160
10161 #[test]
10162 fn flatten_to_merges_leading_dims() {
10163 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10164 let out = t.flatten_to(1).unwrap();
10165 assert_eq!(out.shape().dims(), &[6, 4]);
10166 }
10167
10168 #[test]
10169 fn flatten_from_merges_trailing_dims() {
10170 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
10171 let out = t.flatten_from(1).unwrap();
10172 assert_eq!(out.shape().dims(), &[2, 12]);
10173 }
10174
10175 #[test]
10176 fn flatten_all_produces_rank_one() {
10177 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10178 let out = t.flatten_all().unwrap();
10179 assert_eq!(out.shape().dims(), &[6]);
10180 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10181 }
10182
10183 #[test]
10193 fn rope_batched_matches_single_position_rope_row_by_row() {
10194 let lcg = |n: usize, seed: u32| -> Vec<f32> {
10195 let mut s = seed;
10196 (0..n)
10197 .map(|_| {
10198 s = s.wrapping_mul(1103515245).wrapping_add(12345);
10199 ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
10200 })
10201 .collect()
10202 };
10203 let (b, heads, hd) = (3usize, 4usize, 8usize);
10204 let base = 10000.0f64;
10205 let positions = [0usize, 7, 3];
10206
10207 let x_data = lcg(b * heads * hd, 11);
10208 let x = cpu_f32(x_data.clone(), &[b, heads, 1, hd]);
10209 let (cos, sin) = x.rope_tables_const_batched(base, &positions, hd);
10210 let got = x.rope_batched(&cos, &sin).unwrap().realize_f32();
10211
10212 let per_row = heads * hd;
10213 for (row, &pos) in positions.iter().enumerate() {
10214 let row_data = x_data[row * per_row..(row + 1) * per_row].to_vec();
10215 let xr = cpu_f32(row_data, &[1, heads, 1, hd]);
10216 let (c, s) = xr.rope_tables_const(base, pos, 1, hd);
10217 let want = xr
10218 .rope_with_tables_decomposed(&c, &s)
10219 .unwrap()
10220 .realize_f32();
10221 assert_eq!(
10222 &got[row * per_row..(row + 1) * per_row],
10223 &want[..],
10224 "row {row} (position {pos}) != decomposed RoPE at that position",
10225 );
10226 }
10227
10228 let shared = [positions[0]; 3];
10233 let (c0, s0) = x.rope_tables_const_batched(base, &shared, hd);
10234 let wrong = x.rope_batched(&c0, &s0).unwrap().realize_f32();
10235 for (row, &pos) in positions.iter().enumerate().skip(1) {
10236 assert_ne!(
10237 &wrong[row * per_row..(row + 1) * per_row],
10238 &got[row * per_row..(row + 1) * per_row],
10239 "CONTROL: row {row} at shared position 0 matched its own position \
10240 {pos} — per-row RoPE not applied, or comparison blind to it",
10241 );
10242 }
10243 }
10244
10245 #[test]
10246 fn flatten_errors_on_bad_range() {
10247 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
10248 assert!(t.flatten(0, 5).is_err());
10249 assert!(t.flatten(2, 1).is_err()); }
10251
10252 #[test]
10253 fn split_heads_then_merge_heads_round_trip() {
10254 let x = cpu_f32((0..12).map(|i| i as f32).collect(), &[1, 2, 6]);
10256 let split = x.split_heads(2, 3).unwrap();
10257 assert_eq!(split.shape().dims(), &[1, 2, 2, 3]);
10258 let merged = split.merge_heads().unwrap();
10259 assert_eq!(merged.shape().dims(), &[1, 2, 6]);
10260 let m = merged.realize_f32();
10261 let original = x.realize_f32();
10262 for (a, b) in m.iter().zip(original.iter()) {
10263 assert!((a - b).abs() < 1e-7, "{a} vs {b}");
10264 }
10265 }
10266
10267 #[test]
10268 fn rope_tables_const_shapes_and_anchoring() {
10269 let anchor = cpu_f32(vec![0.0_f32; 8], &[1, 4, 2]);
10270 let (cos, sin) = anchor.rope_tables_const(10_000.0, 0, 4, 8);
10271 assert_eq!(cos.shape().dims(), &[4, 8]);
10272 assert_eq!(sin.shape().dims(), &[4, 8]);
10273 let cv = cos.realize_f32();
10275 let sv = sin.realize_f32();
10276 for i in 0..8 {
10278 assert!((cv[i] - 1.0).abs() < 1e-5, "cos[0,{i}] = {}", cv[i]);
10279 assert!(sv[i].abs() < 1e-5, "sin[0,{i}] = {}", sv[i]);
10280 }
10281 }
10282
10283 #[test]
10284 fn embed_tokens_shape_and_lookup() {
10285 let vocab_size = 5;
10288 let hidden = 3;
10289 let table: Vec<f32> = (0..vocab_size)
10290 .flat_map(|i| vec![i as f32, i as f32 + 0.5, i as f32 + 1.0])
10291 .collect();
10292 let tokens = vec![1_u32, 3, 0];
10293 let out = Tensor::embed_tokens(
10294 std::sync::Arc::from(table),
10295 vocab_size,
10296 hidden,
10297 &tokens,
10298 &crate::Device::cpu(),
10299 )
10300 .unwrap();
10301 assert_eq!(out.shape().dims(), &[1, 3, hidden]);
10302 let v = out.realize_f32();
10303 let want = [
10304 1.0_f32, 1.5, 2.0, 3.0, 3.5, 4.0, 0.0, 0.5, 1.0, ];
10308 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10309 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10310 }
10311 }
10312
10313 #[test]
10314 fn embed_tokens_anchored_lives_on_receiver_graph() {
10315 let vocab_size = 4;
10319 let hidden = 2;
10320 let table: Vec<f32> = (0..vocab_size)
10321 .flat_map(|i| vec![i as f32, i as f32 * 2.0])
10322 .collect();
10323 let table_arc: std::sync::Arc<[f32]> = std::sync::Arc::from(table);
10324 let tokens = vec![2_u32, 1];
10325
10326 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10327 let embedded = anchor
10328 .embed_tokens_anchored(
10329 std::sync::Arc::clone(&table_arc),
10330 vocab_size,
10331 hidden,
10332 &tokens,
10333 )
10334 .unwrap();
10335 assert_eq!(embedded.shape().dims(), &[1, 2, hidden]);
10336
10337 let one_scaled = anchor
10339 .const_f32_like(std::sync::Arc::from(vec![1.0_f32]), Shape::from_dims(&[1]))
10340 .unwrap();
10341 let _ = embedded
10342 .add(
10343 &one_scaled
10344 .reshape(Shape::from_dims(&[1, 1, 1]))
10345 .unwrap()
10346 .broadcast_to(Shape::from_dims(&[1, 2, hidden]))
10347 .unwrap(),
10348 )
10349 .unwrap();
10350 let v = embedded.realize_f32();
10351 let want = [
10352 2.0_f32, 4.0, 1.0, 2.0, ];
10355 for (i, (&got, &exp)) in v.iter().zip(want.iter()).enumerate() {
10356 assert!((got - exp).abs() < 1e-5, "row {i}: got={got} want={exp}");
10357 }
10358 }
10359
10360 #[test]
10361 fn embed_tokens_empty_returns_error() {
10362 let r = Tensor::embed_tokens(
10363 std::sync::Arc::from(vec![0.0_f32]),
10364 1,
10365 1,
10366 &[],
10367 &crate::Device::cpu(),
10368 );
10369 assert!(r.is_err());
10370 }
10371
10372 #[test]
10373 fn softcap_matches_tanh_form() {
10374 let x = cpu_f32(vec![0.0_f32, 5.0, -5.0, 30.0], &[1, 4]);
10376 let capped = x.softcap(10.0).realize_f32();
10377 let expect = [0.0_f32, 4.6212, -4.6212, 9.9505];
10379 for (i, (&got, &want)) in capped.iter().zip(expect.iter()).enumerate() {
10380 assert!(
10381 (got - want).abs() < 1e-3,
10382 "softcap[{i}] got={got} want={want}"
10383 );
10384 }
10385 }
10386
10387 #[test]
10388 fn softcap_optional_none_returns_input_unchanged() {
10389 let x = cpu_f32(vec![1.0_f32, -2.0, 30.0], &[1, 3]);
10390 let out = x.softcap_optional(None).realize_f32();
10391 let expect = [1.0_f32, -2.0, 30.0];
10392 for (g, w) in out.iter().zip(expect.iter()) {
10393 assert!((g - w).abs() < 1e-6);
10394 }
10395 let out = x.softcap_optional(Some(0.0)).realize_f32();
10397 for (g, w) in out.iter().zip(expect.iter()) {
10398 assert!((g - w).abs() < 1e-6);
10399 }
10400 }
10401
10402 #[test]
10403 fn rope_partial_full_dim_matches_rope_with_tables() {
10404 let qk = cpu_f32(vec![1.0_f32; 1 * 1 * 2 * 4], &[1, 1, 2, 4]);
10406 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 2, 4);
10407 let via_partial = qk.rope_partial(&cos, &sin, 4).unwrap();
10408 let via_full = qk.rope_with_tables(&cos, &sin).unwrap();
10409 let a = via_partial.realize_f32();
10410 let b = via_full.realize_f32();
10411 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
10412 assert!((av - bv).abs() < 1e-6, "{i}: {av} vs {bv}");
10413 }
10414 }
10415
10416 #[test]
10417 fn rope_partial_pass_through_suffix_unchanged() {
10418 let qk = cpu_f32(
10422 vec![
10423 1.0_f32, 2.0, 3.0, 4.0,
10425 ],
10426 &[1, 1, 1, 4],
10427 );
10428 let (cos, sin) = qk.rope_tables_const(10_000.0, 0, 1, 2);
10429 let out = qk.rope_partial(&cos, &sin, 2).unwrap().realize_f32();
10430 for (i, &v) in out.iter().enumerate() {
10431 let expect = [1.0_f32, 2.0, 3.0, 4.0][i];
10432 assert!((v - expect).abs() < 1e-6, "{i}: {v} != {expect}");
10433 }
10434 }
10435
10436 #[test]
10437 fn add_optional_trailing_bias_none_returns_input_unchanged() {
10438 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10439 let original = a.realize_f32();
10440 let out = a.add_optional_trailing_bias(None).unwrap();
10441 assert_eq!(out.realize_f32(), original);
10442 }
10443
10444 #[test]
10445 fn add_optional_trailing_bias_some_applies_add() {
10446 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10447 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, 20.0, 30.0]);
10448 let out = a.add_optional_trailing_bias(Some(&bias)).unwrap();
10449 assert_eq!(out.realize_f32(), vec![11.0, 22.0, 33.0]);
10450 }
10451
10452 #[test]
10453 fn add_trailing_bias_broadcasts_across_leading_dims() {
10454 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0], &[2, 3]);
10456 let bias = std::sync::Arc::<[f32]>::from(vec![100.0_f32, 200.0, 300.0]);
10457 let out = x.add_trailing_bias(bias).unwrap();
10458 assert_eq!(out.shape().dims(), &[2, 3]);
10459 let v = out.realize_f32();
10460 assert_eq!(v, vec![101.0, 202.0, 303.0, 110.0, 220.0, 330.0]);
10461 }
10462
10463 #[test]
10464 fn rms_norm_affine_with_offset_adds_offset_to_each_gain() {
10465 let x = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10466 let gain_raw: [f32; 3] = [-0.5, 0.0, 0.5];
10467 let via_offset = x.rms_norm_affine_with_offset(&gain_raw, 1.0, 1e-6).unwrap();
10468 let gain_shifted = std::sync::Arc::<[f32]>::from(vec![0.5_f32, 1.0, 1.5]);
10469 let via_plain = x.rms_norm_affine(gain_shifted, 1e-6).unwrap();
10470 let a = via_offset.realize_f32();
10471 let b = via_plain.realize_f32();
10472 for (x, y) in a.iter().zip(b.iter()) {
10473 assert!((x - y).abs() < 1e-6, "{x} vs {y}");
10474 }
10475 }
10476
10477 #[test]
10478 fn global_avg_pool_2d_averages_spatial_dims() {
10479 let x = cpu_f32(
10483 vec![
10484 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,
10485 ],
10486 &[1, 2, 2, 3],
10487 );
10488 let out = x.global_avg_pool_2d().unwrap();
10489 assert_eq!(out.shape().dims(), &[1, 2]);
10490 let v = out.realize_f32();
10491 assert!((v[0] - 3.5).abs() < 1e-5);
10492 assert!((v[1] - 35.0).abs() < 1e-4);
10493 }
10494
10495 #[test]
10496 fn channel_affine_4d_applies_per_channel_gain_and_bias() {
10497 let x = cpu_f32(
10499 vec![
10500 1.0_f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0, ],
10503 &[1, 2, 2, 2],
10504 );
10505 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5]);
10506 let bias = std::sync::Arc::<[f32]>::from(vec![1.0_f32, -10.0]);
10507 let out = x.channel_affine_4d(gain, bias).unwrap();
10508 let v = out.realize_f32();
10509 assert_eq!(&v[0..4], &[3.0, 5.0, 7.0, 9.0]);
10511 assert_eq!(&v[4..8], &[-5.0, 0.0, 5.0, 10.0]);
10513 }
10514
10515 #[test]
10516 fn additive_causal_mask_has_strict_lower_triangle() {
10517 let anchor = cpu_f32(vec![0.0_f32], &[1]);
10518 let mask = Tensor::additive_causal_mask_like(&anchor, 4);
10519 assert_eq!(mask.shape().dims(), &[4, 4]);
10520 let v = mask.realize_f32();
10521 for i in 0..4 {
10527 for j in 0..4 {
10528 let got = v[i * 4 + j];
10529 if j > i {
10530 assert!(
10531 got.is_infinite() && got.is_sign_negative(),
10532 "above-diag (i={i}, j={j}) should be -inf, got {got}"
10533 );
10534 } else {
10535 assert_eq!(
10536 got, 0.0,
10537 "on/below-diag (i={i}, j={j}) should be 0, got {got}"
10538 );
10539 }
10540 }
10541 }
10542 }
10543
10544 #[test]
10545 fn layer_norm_affine_unit_gain_zero_bias_matches_layer_norm_last_dim() {
10546 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10547 let gain = std::sync::Arc::<[f32]>::from(vec![1.0_f32; 3]);
10548 let bias = std::sync::Arc::<[f32]>::from(vec![0.0_f32; 3]);
10549 let out_affine = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10550 let out_plain = a.layer_norm_last_dim(1e-5).unwrap();
10551 let va = out_affine.realize_f32();
10552 let vp = out_plain.realize_f32();
10553 for (a, b) in va.iter().zip(vp.iter()) {
10554 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
10555 }
10556 }
10557
10558 #[test]
10559 fn layer_norm_affine_applies_gain_and_bias() {
10560 let a = cpu_f32(vec![1.0_f32, 2.0, 3.0], &[1, 3]);
10561 let gain = std::sync::Arc::<[f32]>::from(vec![2.0_f32, 0.5, 1.5]);
10562 let bias = std::sync::Arc::<[f32]>::from(vec![10.0_f32, -5.0, 0.0]);
10563 let out = a.layer_norm_affine(gain, bias, 1e-5).unwrap();
10564 let v = out.realize_f32();
10565 let mean = 2.0_f32;
10567 let var = ((1.0 - mean).powi(2) + (2.0 - mean).powi(2) + (3.0 - mean).powi(2)) / 3.0;
10568 let den = (var + 1e-5_f32).sqrt();
10569 let expected = [
10570 ((1.0 - mean) / den) * 2.0 + 10.0,
10571 ((2.0 - mean) / den) * 0.5 + (-5.0),
10572 ((3.0 - mean) / den) * 1.5 + 0.0,
10573 ];
10574 for (got, want) in v.iter().zip(expected.iter()) {
10575 assert!((got - want).abs() < 1e-4, "{got} vs {want}");
10576 }
10577 }
10578
10579 #[test]
10580 fn l2_normalize_last_dim_unit_norm_per_row() {
10581 let a = cpu_f32(vec![3.0, 4.0, 0.0, 1.0, 2.0, 2.0], &[2, 3]);
10583 let out = a.l2_normalize(1_usize, 1e-12).unwrap();
10584 assert_eq!(out.shape().dims(), &[2, 3]);
10585 let v = out.realize_f32();
10586 let row0_norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
10587 let row1_norm = (v[3] * v[3] + v[4] * v[4] + v[5] * v[5]).sqrt();
10588 assert!((row0_norm - 1.0).abs() < 1e-5, "row 0 norm = {row0_norm}");
10589 assert!((row1_norm - 1.0).abs() < 1e-5, "row 1 norm = {row1_norm}");
10590 assert!((v[0] - 0.6).abs() < 1e-5);
10592 assert!((v[1] - 0.8).abs() < 1e-5);
10593 assert!(v[2].abs() < 1e-5);
10594 }
10595
10596 #[test]
10597 fn l2_normalize_eps_zero_works_when_nonzero() {
10598 let a = cpu_f32(vec![1.0_f32, 0.0], &[2]);
10599 let out = a.l2_normalize(0_usize, 0.0).unwrap();
10600 let v = out.realize_f32();
10601 assert!((v[0] - 1.0).abs() < 1e-6);
10602 assert!(v[1].abs() < 1e-6);
10603 }
10604
10605 #[test]
10606 fn repeat_interleave_last_dim_matches_torch_semantics() {
10607 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10611 let out = a.repeat_interleave(1_usize, 2).unwrap();
10612 assert_eq!(out.shape().dims(), &[2, 6]);
10613 assert_eq!(
10614 out.realize_f32(),
10615 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]
10616 );
10617 }
10618
10619 #[test]
10620 fn repeat_interleave_middle_dim() {
10621 let a = cpu_f32((0..8).map(|i| i as f32).collect(), &[2, 2, 2]);
10623 let out = a.repeat_interleave(1_usize, 3).unwrap();
10624 assert_eq!(out.shape().dims(), &[2, 6, 2]);
10625 let v = out.realize_f32();
10627 assert_eq!(&v[0..6], &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
10628 assert_eq!(&v[6..12], &[2.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
10629 }
10630
10631 #[test]
10632 fn repeat_interleave_repeats_one_is_noop() {
10633 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10634 let out = a.repeat_interleave(0_usize, 1).unwrap();
10635 assert_eq!(out.shape().dims(), &[3]);
10636 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10637 }
10638
10639 #[test]
10640 fn repeat_interleave_repeats_zero_errors() {
10641 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10642 assert!(a.repeat_interleave(0_usize, 0).is_err());
10643 }
10644
10645 #[test]
10646 fn stack_adds_leading_dim() {
10647 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10648 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10649 let out = Tensor::stack(&[&a, &b], 0).unwrap();
10650 assert_eq!(out.shape().dims(), &[2, 3]);
10651 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
10652 }
10653
10654 #[test]
10655 fn stack_adds_trailing_dim() {
10656 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10657 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10658 let out = Tensor::stack(&[&a, &b], 1).unwrap();
10659 assert_eq!(out.shape().dims(), &[3, 2]);
10660 assert_eq!(out.realize_f32(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
10661 }
10662
10663 #[test]
10664 fn stack_rejects_mismatched_shapes() {
10665 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10666 let b = a.const_f32_like(vec![3.0, 4.0, 5.0], vec![3]).unwrap();
10667 assert!(Tensor::stack(&[&a, &b], 0).is_err());
10668 }
10669
10670 #[test]
10671 fn stack_rejects_empty_input() {
10672 let result = Tensor::stack(&[], 0);
10673 assert!(result.is_err());
10674 }
10675
10676 #[test]
10677 fn repeat_tiles_along_each_dim() {
10678 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10679 let out = t.repeat(vec![3]).unwrap();
10680 assert_eq!(out.shape().dims(), &[6]);
10681 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
10682 }
10683
10684 #[test]
10685 fn repeat_extends_rank_when_needed() {
10686 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10687 let out = t.repeat(vec![3, 2]).unwrap();
10689 assert_eq!(out.shape().dims(), &[3, 4]);
10690 }
10691
10692 #[test]
10693 fn repeat_identity_with_all_ones() {
10694 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10695 let out = t.repeat(vec![1]).unwrap();
10696 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0]);
10697 }
10698}
10699
10700#[cfg(test)]
10704mod phase_a3_keepdim_tests {
10705 use super::*;
10706
10707 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10708 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10709 }
10710
10711 #[test]
10712 fn sum_keepdim_preserves_dim_as_one() {
10713 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10714 let out = t.sum_keepdim(1_usize).unwrap();
10715 assert_eq!(out.shape().dims(), &[2, 1]);
10716 assert_eq!(out.realize_f32(), vec![3.0, 7.0]);
10717 }
10718
10719 #[test]
10720 fn mean_keepdim_preserves_dim_as_one() {
10721 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10722 let out = t.mean_keepdim(0_usize).unwrap();
10723 assert_eq!(out.shape().dims(), &[1, 2]);
10724 assert_eq!(out.realize_f32(), vec![2.0, 3.0]);
10725 }
10726
10727 #[test]
10728 fn max_keepdim_preserves_dim_as_one() {
10729 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10730 let out = t.max_keepdim(1_usize).unwrap();
10731 assert_eq!(out.shape().dims(), &[2, 1]);
10732 assert_eq!(out.realize_f32(), vec![3.0, 4.0]);
10733 }
10734
10735 #[test]
10736 fn min_keepdim_preserves_dim_as_one() {
10737 let t = cpu_f32(vec![1.0, 3.0, 2.0, 4.0], &[2, 2]);
10738 let out = t.min_keepdim(1_usize).unwrap();
10739 assert_eq!(out.shape().dims(), &[2, 1]);
10740 assert_eq!(out.realize_f32(), vec![1.0, 2.0]);
10741 }
10742
10743 #[test]
10744 fn var_matches_unbiased_formula_with_correction_1() {
10745 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10747 let out = t.var(1, 1.0).unwrap();
10748 assert_eq!(out.shape().dims(), &[2]);
10749 let v = out.realize_f32();
10750 assert!((v[0] - 1.0).abs() < 1e-5, "var row 0 = {} != 1.0", v[0]);
10751 assert!((v[1] - 1.0).abs() < 1e-5, "var row 1 = {} != 1.0", v[1]);
10752 }
10753
10754 #[test]
10759 fn var_population_with_correction_0() {
10760 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10762 let out = t.var(1, 0.0).unwrap();
10763 let v = out.realize_f32();
10764 let want = 2.0 / 3.0;
10765 assert!(
10766 (v[0] - want).abs() < 1e-5,
10767 "population var row 0 = {} != {want}",
10768 v[0]
10769 );
10770 assert!(
10771 (v[1] - want).abs() < 1e-5,
10772 "population var row 1 = {} != {want}",
10773 v[1]
10774 );
10775 }
10776
10777 #[test]
10778 fn var_keepdim_preserves_dim() {
10779 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10780 let out = t.var_keepdim(1, 1.0).unwrap();
10781 assert_eq!(out.shape().dims(), &[2, 1]);
10782 }
10783
10784 #[test]
10785 fn var_errors_out_of_bounds() {
10786 let t = cpu_f32(vec![1.0, 2.0], &[2]);
10787 assert!(t.var(3, 1.0).is_err());
10788 assert!(t.var_keepdim(3, 1.0).is_err());
10789 }
10790}
10791
10792#[cfg(test)]
10796mod phase_a4_composite_tests {
10797 use super::*;
10798
10799 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10800 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10801 }
10802
10803 #[test]
10804 fn affine_applies_mul_then_add() {
10805 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10806 let out = t.affine(2.0, 10.0);
10807 assert_eq!(out.realize_f32(), vec![12.0, 14.0, 16.0]);
10808 }
10809
10810 #[test]
10811 fn scale_and_shift_alias_of_affine() {
10812 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10813 assert_eq!(
10814 t.scale_and_shift(2.0, 10.0).realize_f32(),
10815 t.affine(2.0, 10.0).realize_f32(),
10816 );
10817 }
10818
10819 #[test]
10820 fn elu_matches_reference_values() {
10821 let t = cpu_f32(vec![1.0, 0.0, -1.0, -2.0], &[4]);
10822 let out = t.elu(1.0);
10823 let v = out.realize_f32();
10824 assert!((v[0] - 1.0).abs() < 1e-5);
10827 assert!(v[1].abs() < 1e-5);
10828 assert!((v[2] - ((-1.0_f32).exp() - 1.0)).abs() < 1e-5);
10829 assert!((v[3] - ((-2.0_f32).exp() - 1.0)).abs() < 1e-5);
10830 }
10831
10832 #[test]
10833 fn dot_of_rank_one_vectors() {
10834 let a = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
10835 let b = a.const_f32_like(vec![4.0, 5.0, 6.0], vec![3]).unwrap();
10836 let out = a.dot(&b).unwrap();
10837 assert_eq!(out.shape().elem_count(), 1);
10838 let v = out.realize_f32();
10839 assert_eq!(v[0], 32.0); }
10841
10842 #[test]
10843 fn dot_rejects_non_rank_one() {
10844 let a = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
10845 let b = a
10846 .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2])
10847 .unwrap();
10848 assert!(a.dot(&b).is_err());
10849 }
10850
10851 #[test]
10852 fn dot_rejects_length_mismatch() {
10853 let a = cpu_f32(vec![1.0, 2.0], &[2]);
10854 let b = a.const_f32_like(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
10855 assert!(a.dot(&b).is_err());
10856 }
10857
10858 #[test]
10859 fn mv_matrix_times_vector() {
10860 let m = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
10861 let v = m.const_f32_like(vec![1.0, 1.0, 1.0], vec![3]).unwrap();
10862 let out = m.mv(&v).unwrap();
10863 assert_eq!(out.shape().dims(), &[2]);
10864 assert_eq!(out.realize_f32(), vec![6.0, 15.0]);
10865 }
10866
10867 #[test]
10868 fn matvec_is_mv_alias() {
10869 let m = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10870 let v = m.const_f32_like(vec![3.0, 4.0], vec![2]).unwrap();
10871 let a = m.mv(&v).unwrap().realize_f32();
10872 let b = m.matvec(&v).unwrap().realize_f32();
10873 assert_eq!(a, b);
10874 }
10875
10876 #[test]
10877 fn mv_rejects_shape_mismatch() {
10878 let m = cpu_f32(vec![1.0; 6], &[2, 3]);
10879 let v = m.const_f32_like(vec![1.0, 1.0], vec![2]).unwrap();
10880 assert!(m.mv(&v).is_err());
10881 }
10882
10883 #[test]
10884 fn broadcast_matmul_passes_through_to_matmul() {
10885 let a = cpu_f32(vec![1.0, 0.0, 0.0, 1.0], &[2, 2]);
10886 let b = a
10887 .const_f32_like(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2])
10888 .unwrap();
10889 let out = a.broadcast_matmul(&b).unwrap();
10890 assert_eq!(out.realize_f32(), vec![5.0, 6.0, 7.0, 8.0]);
10891 }
10892}
10893
10894#[cfg(test)]
10898mod phase_a5_factory_tests {
10899 use super::*;
10900
10901 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
10902 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
10903 }
10904
10905 #[test]
10906 fn ones_like_matches_shape_dtype_graph() {
10907 let t = cpu_f32(vec![5.0, 6.0, 7.0, 8.0], &[2, 2]);
10908 let ones = t.ones_like().unwrap();
10909 assert_eq!(ones.shape().dims(), t.shape().dims());
10910 assert_eq!(ones.dtype(), t.dtype());
10911 assert_eq!(ones.realize_f32(), vec![1.0; 4]);
10912 }
10913
10914 #[test]
10915 fn zeros_like_matches_shape_dtype_graph() {
10916 let t = cpu_f32(vec![5.0, 6.0, 7.0], &[3]);
10917 let zeros = t.zeros_like().unwrap();
10918 assert_eq!(zeros.realize_f32(), vec![0.0; 3]);
10919 }
10920
10921 #[test]
10922 fn static_ones_f32() {
10923 let t = Tensor::ones(vec![2, 3], DType::F32, &Device::cpu()).unwrap();
10924 assert_eq!(t.shape().dims(), &[2, 3]);
10925 assert_eq!(t.realize_f32(), vec![1.0; 6]);
10926 }
10927
10928 #[test]
10929 fn static_zeros_f64() {
10930 let t = Tensor::zeros(vec![4], DType::F64, &Device::cpu()).unwrap();
10931 assert_eq!(t.dtype(), DType::F64);
10932 assert_eq!(t.realize_f64(), vec![0.0; 4]);
10933 }
10934
10935 #[test]
10936 fn full_with_f32_scalar() {
10937 let t = Tensor::full(vec![5], fuel_ir::Scalar::F32(2.5), &Device::cpu()).unwrap();
10938 assert_eq!(t.realize_f32(), vec![2.5; 5]);
10939 }
10940
10941 #[test]
10942 fn eye_identity_matrix() {
10943 let t = Tensor::eye(3, DType::F32, &Device::cpu());
10944 assert_eq!(t.shape().dims(), &[3, 3]);
10945 assert_eq!(
10946 t.realize_f32(),
10947 vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
10948 );
10949 }
10950
10951 #[test]
10952 fn tril2_lower_triangular_ones() {
10953 let t = Tensor::tril2(3, DType::F32, &Device::cpu());
10954 assert_eq!(
10955 t.realize_f32(),
10956 vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
10957 );
10958 }
10959
10960 #[test]
10961 fn triu2_upper_triangular_ones() {
10962 let t = Tensor::triu2(3, DType::F32, &Device::cpu());
10963 assert_eq!(
10964 t.realize_f32(),
10965 vec![1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0],
10966 );
10967 }
10968
10969 #[test]
10970 fn meshgrid_ij_indexing_two_inputs() {
10971 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10972 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10973 let grids = Tensor::meshgrid(&[&x, &y], false).unwrap();
10974 assert_eq!(grids.len(), 2);
10975 assert_eq!(grids[0].shape().dims(), &[3, 2]);
10977 assert_eq!(grids[1].shape().dims(), &[3, 2]);
10978 assert_eq!(grids[0].realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
10980 assert_eq!(grids[1].realize_f32(), vec![4.0, 5.0, 4.0, 5.0, 4.0, 5.0]);
10982 }
10983
10984 #[test]
10985 fn meshgrid_xy_indexing_swaps_first_two() {
10986 let x = Tensor::from_f32(vec![1.0_f32, 2.0, 3.0], vec![3], &Device::cpu()).unwrap();
10987 let y = x.const_f32_like(vec![4.0_f32, 5.0], vec![2]).unwrap();
10988 let grids = Tensor::meshgrid(&[&x, &y], true).unwrap();
10989 assert_eq!(grids[0].shape().dims(), &[2, 3]);
10991 assert_eq!(grids[1].shape().dims(), &[2, 3]);
10992 assert_eq!(grids[0].realize_f32(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
10994 assert_eq!(grids[1].realize_f32(), vec![4.0, 4.0, 4.0, 5.0, 5.0, 5.0]);
10996 }
10997
10998 #[test]
10999 fn meshgrid_rejects_single_input() {
11000 let x = Tensor::from_f32(vec![1.0_f32, 2.0], vec![2], &Device::cpu()).unwrap();
11001 assert!(Tensor::meshgrid(&[&x], false).is_err());
11002 }
11003
11004 #[test]
11005 fn meshgrid_rejects_non_rank_one() {
11006 let x = Tensor::from_f32(vec![1.0; 4], vec![2, 2], &Device::cpu()).unwrap();
11007 let y = x.const_f32_like(vec![1.0, 2.0], vec![2]).unwrap();
11008 assert!(Tensor::meshgrid(&[&x, &y], false).is_err());
11009 }
11010
11011 #[test]
11014 fn narrow_is_slice_alias() {
11015 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[5]);
11016 let a = t.narrow(0, 1, 3).unwrap().realize_f32();
11017 let b = t.slice(0, 1, 3).unwrap().realize_f32();
11018 assert_eq!(a, b);
11019 assert_eq!(a, vec![2.0, 3.0, 4.0]);
11020 }
11021
11022 #[test]
11023 fn chunk_splits_evenly() {
11024 let t = cpu_f32((1..=6).map(|i| i as f32).collect(), &[6]);
11025 let parts = t.chunk(3, 0).unwrap();
11026 assert_eq!(parts.len(), 3);
11027 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0]);
11028 assert_eq!(parts[1].realize_f32(), vec![3.0, 4.0]);
11029 assert_eq!(parts[2].realize_f32(), vec![5.0, 6.0]);
11030 }
11031
11032 #[test]
11033 fn chunk_distributes_remainder_to_leading() {
11034 let t = cpu_f32((1..=7).map(|i| i as f32).collect(), &[7]);
11036 let parts = t.chunk(3, 0).unwrap();
11037 assert_eq!(parts.len(), 3);
11038 assert_eq!(parts[0].realize_f32(), vec![1.0, 2.0, 3.0]);
11039 assert_eq!(parts[1].realize_f32(), vec![4.0, 5.0]);
11040 assert_eq!(parts[2].realize_f32(), vec![6.0, 7.0]);
11041 }
11042
11043 #[test]
11044 fn chunk_returns_singletons_when_size_less_than_chunks() {
11045 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11046 let parts = t.chunk(5, 0).unwrap();
11047 assert_eq!(parts.len(), 2);
11048 assert_eq!(parts[0].realize_f32(), vec![1.0]);
11049 assert_eq!(parts[1].realize_f32(), vec![2.0]);
11050 }
11051
11052 #[test]
11053 fn get_at_first_dim() {
11054 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11055 let row1 = t.get(1).unwrap();
11056 assert_eq!(row1.shape().dims(), &[2]);
11057 assert_eq!(row1.realize_f32(), vec![3.0, 4.0]);
11058 }
11059
11060 #[test]
11061 fn get_on_dim_arbitrary_axis() {
11062 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]);
11063 let col0 = t.get_on_dim(1, 0).unwrap();
11064 assert_eq!(col0.shape().dims(), &[3]);
11065 assert_eq!(col0.realize_f32(), vec![1.0, 3.0, 5.0]);
11066 }
11067
11068 #[test]
11069 fn sum_dims_multi_dim_reduces_to_smaller() {
11070 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11072 let s = t.sum_dims([0, 2_usize]).unwrap();
11073 assert_eq!(s.shape().dims(), &[3]);
11074 assert_eq!(s.realize_f32(), vec![8.0; 3]);
11076 }
11077
11078 #[test]
11079 fn mean_dims_multi_dim() {
11080 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11081 let m = t.mean_dims([0, 1_usize]).unwrap();
11082 assert_eq!(m.shape().dims(), &[] as &[usize]);
11083 assert_eq!(m.realize_f32(), vec![2.5]);
11084 }
11085
11086 #[test]
11087 fn sum_dims_keepdim_preserves_rank() {
11088 let t = cpu_f32(vec![1.0; 24], &[2, 3, 4]);
11089 let s = t.sum_dims_keepdim(&[0, 2]).unwrap();
11090 assert_eq!(s.shape().dims(), &[1, 3, 1]);
11091 }
11092
11093 #[test]
11094 fn mean_dims_keepdim_preserves_rank() {
11095 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11096 let m = t.mean_dims_keepdim(&[0, 1]).unwrap();
11097 assert_eq!(m.shape().dims(), &[1, 1]);
11098 assert_eq!(m.realize_f32(), vec![2.5]);
11099 }
11100
11101 #[test]
11102 fn rand_like_matches_shape_dtype() {
11103 let t = cpu_f32(vec![0.0; 6], &[2, 3]);
11104 let r = t.rand_like(-1.0, 1.0).unwrap();
11105 assert_eq!(r.shape().dims(), t.shape().dims());
11106 assert_eq!(r.dtype(), t.dtype());
11107 for v in r.realize_f32() {
11109 assert!((-1.0..1.0).contains(&v), "sample {v} out of [-1, 1)");
11110 }
11111 }
11112
11113 #[test]
11114 fn randn_like_matches_shape_dtype() {
11115 let t = cpu_f32(vec![0.0; 4], &[4]);
11116 let r = t.randn_like(0.0, 1.0).unwrap();
11117 assert_eq!(r.shape().dims(), &[4]);
11118 assert_eq!(r.dtype(), DType::F32);
11119 for v in r.realize_f32() {
11121 assert!(v.is_finite());
11122 }
11123 }
11124
11125 #[test]
11126 fn static_rand_f32() {
11127 let t = Tensor::rand(vec![100], 0.0, 1.0, DType::F32, &Device::cpu()).unwrap();
11128 let v = t.realize_f32();
11129 let mean: f32 = v.iter().sum::<f32>() / v.len() as f32;
11131 assert!((0.3..0.7).contains(&mean), "mean {mean} too far from 0.5");
11132 }
11133
11134 #[test]
11135 fn static_randn_f64() {
11136 let t = Tensor::randn(vec![1000], 0.0, 1.0, DType::F64, &Device::cpu()).unwrap();
11137 let v = t.realize_f64();
11138 let mean: f64 = v.iter().sum::<f64>() / v.len() as f64;
11139 assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
11141 }
11142
11143 #[test]
11144 fn arange_int_step() {
11145 let t = Tensor::arange(0.0, 5.0, &Device::cpu());
11146 assert_eq!(t.shape().dims(), &[5]);
11147 assert_eq!(t.realize_f32(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
11148 }
11149
11150 #[test]
11151 fn arange_step_fractional() {
11152 let t = Tensor::arange_step(2.0, 4.0, 0.5, &Device::cpu());
11153 assert_eq!(t.realize_f32(), vec![2.0, 2.5, 3.0, 3.5]);
11154 }
11155
11156 #[test]
11157 fn arange_step_negative_descends() {
11158 let t = Tensor::arange_step(5.0, 0.0, -1.0, &Device::cpu());
11159 assert_eq!(t.realize_f32(), vec![5.0, 4.0, 3.0, 2.0, 1.0]);
11160 }
11161
11162 #[test]
11163 fn linspace_includes_endpoints() {
11164 let t = Tensor::linspace(0.0, 1.0, 5, &Device::cpu());
11165 assert_eq!(t.shape().dims(), &[5]);
11166 let v = t.realize_f32();
11167 assert!((v[0] - 0.0).abs() < 1e-6);
11168 assert!((v[4] - 1.0).abs() < 1e-6);
11169 assert!((v[2] - 0.5).abs() < 1e-6);
11170 }
11171
11172 #[test]
11173 fn linspace_n_one_returns_start() {
11174 let t = Tensor::linspace(7.0, 99.0, 1, &Device::cpu());
11175 assert_eq!(t.realize_f32(), vec![7.0]);
11176 }
11177
11178 #[test]
11179 fn norm_is_sqrt_sum_sq() {
11180 let t = Tensor::from_f32(vec![3.0_f32, 4.0], vec![2], &Device::cpu()).unwrap();
11181 let n = t.norm();
11182 assert!((n.realize_f32()[0] - 5.0).abs() < 1e-6);
11183 }
11184
11185 #[test]
11186 fn pad_with_zeros_left_and_right() {
11187 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11188 let p = t.pad_with_zeros(0, 2, 1).unwrap();
11189 assert_eq!(p.shape().dims(), &[6]);
11190 assert_eq!(p.realize_f32(), vec![0.0, 0.0, 1.0, 2.0, 3.0, 0.0]);
11191 }
11192
11193 #[test]
11194 fn pad_with_zeros_identity_when_both_zero() {
11195 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11196 let p = t.pad_with_zeros(0, 0, 0).unwrap();
11197 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11198 }
11199
11200 #[test]
11201 fn pad_with_zeros_rejects_bad_dim() {
11202 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11203 assert!(t.pad_with_zeros(3, 1, 1).is_err());
11204 }
11205
11206 #[test]
11207 fn pad_with_value_zero_matches_pad_with_zeros() {
11208 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11211 let via_zeros = t.pad_with_zeros(0, 1, 2).unwrap();
11212 let via_value = t.pad_with_value(0, 1, 2, 0.0).unwrap();
11213 assert_eq!(via_zeros.shape().dims(), via_value.shape().dims());
11214 assert_eq!(via_zeros.shape().dims(), &[5, 2]);
11215 assert_eq!(via_zeros.realize_f32(), via_value.realize_f32());
11216 assert_eq!(
11217 via_value.realize_f32(),
11218 vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0],
11219 );
11220 }
11221
11222 #[test]
11223 fn pad_with_value_neg_inf_then_max_reduce_ignores_pad() {
11224 let t = cpu_f32(vec![-1.0, -2.0, -3.0], &[3]);
11228 let padded = t.pad_with_value(0, 2, 2, f32::NEG_INFINITY).unwrap();
11229 assert_eq!(padded.shape().dims(), &[7]);
11230 let v = padded.realize_f32();
11231 assert!(v[0].is_infinite() && v[0].is_sign_negative());
11233 assert!(v[1].is_infinite() && v[1].is_sign_negative());
11234 assert_eq!(v[2], -1.0);
11235 assert_eq!(v[3], -2.0);
11236 assert_eq!(v[4], -3.0);
11237 assert!(v[5].is_infinite() && v[5].is_sign_negative());
11238 assert!(v[6].is_infinite() && v[6].is_sign_negative());
11239 let m = padded.max_all().realize_f32();
11241 assert_eq!(m, vec![-1.0]);
11242 }
11243
11244 #[test]
11245 fn pad_with_value_identity_when_both_zero() {
11246 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11249 let p = t.pad_with_value(0, 0, 0, f32::NEG_INFINITY).unwrap();
11250 assert_eq!(p.realize_f32(), vec![1.0, 2.0, 3.0]);
11251 }
11252
11253 #[test]
11254 fn pad_with_value_rejects_bad_dim() {
11255 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11256 assert!(t.pad_with_value(3, 1, 1, 0.0).is_err());
11257 }
11258
11259 #[test]
11260 fn max_pool2d_with_pad_value_neg_inf_on_negative_interior() {
11261 let x = cpu_f32(
11272 vec![-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0],
11273 &[1, 1, 3, 3],
11274 );
11275 let out = x
11276 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), f32::NEG_INFINITY)
11277 .unwrap();
11278 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11279 let v = out.realize_f32();
11280 assert_eq!(v[0], -1.0);
11283 assert_eq!(v[4], -1.0);
11285 assert_eq!(v[8], -5.0);
11288
11289 let default_pad = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11299 let vd = default_pad.realize_f32();
11300 assert_eq!(
11301 vd[0], -1.0,
11302 "max_pool2d's default must now match NEG_INFINITY padding, not 0.0"
11303 );
11304 assert_eq!(vd[8], -5.0);
11305
11306 let explicit_zero_pad = x
11307 .max_pool2d_with_pad_value((3, 3), (1, 1), (1, 1), 0.0)
11308 .unwrap();
11309 let vz = explicit_zero_pad.realize_f32();
11310 assert_eq!(
11311 vz[0], 0.0,
11312 "explicit 0.0 padding must still be reachable via max_pool2d_with_pad_value"
11313 );
11314 assert_eq!(vz[8], 0.0);
11315 }
11316
11317 #[test]
11318 fn max_pool2d_with_pad_value_zero_matches_max_pool2d() {
11319 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11322 let x = cpu_f32(data, &[1, 1, 4, 4]);
11323 let a = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11324 let b = x
11325 .max_pool2d_with_pad_value((2, 2), (2, 2), (0, 0), 0.0)
11326 .unwrap();
11327 assert_eq!(a.shape().dims(), b.shape().dims());
11328 assert_eq!(a.realize_f32(), b.realize_f32());
11329 }
11330
11331 #[test]
11334 fn conv1d_identity_kernel_passes_input_through() {
11335 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0], &[1, 1, 5]);
11337 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11338 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11339 assert_eq!(out.shape().dims(), &[1, 1, 5]);
11340 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
11341 }
11342
11343 #[test]
11344 fn conv1d_sum_kernel_two_wide() {
11345 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11347 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11348 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11349 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11350 assert_eq!(out.realize_f32(), vec![3.0, 5.0, 7.0]);
11351 }
11352
11353 #[test]
11354 fn conv1d_with_bias_applies_correctly() {
11355 let x = cpu_f32(vec![1.0, 1.0, 1.0], &[1, 1, 3]);
11356 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11357 let bias = x.const_f32_like(vec![10.0], vec![1]).unwrap();
11358 let out = x.conv1d(&w, Some(&bias), 1, 0, 1).unwrap();
11359 assert_eq!(out.realize_f32(), vec![11.0, 11.0, 11.0]);
11360 }
11361
11362 #[test]
11363 fn conv1d_stride_two_halves_output() {
11364 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 1, 6]);
11366 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11367 let out = x.conv1d(&w, None, 2, 0, 1).unwrap();
11368 assert_eq!(out.shape().dims(), &[1, 1, 3]);
11369 assert_eq!(out.realize_f32(), vec![3.0, 7.0, 11.0]);
11370 }
11371
11372 #[test]
11373 fn conv1d_padding_one_preserves_length() {
11374 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11376 let w = x
11377 .const_f32_like(vec![1.0, 1.0, 1.0], vec![1, 1, 3])
11378 .unwrap();
11379 let out = x.conv1d(&w, None, 1, 1, 1).unwrap();
11380 assert_eq!(out.shape().dims(), &[1, 1, 4]);
11381 assert_eq!(out.realize_f32(), vec![3.0, 6.0, 9.0, 7.0]);
11384 }
11385
11386 #[test]
11387 fn conv1d_multi_channel_output() {
11388 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11390 let w = x.const_f32_like(vec![2.0, -1.0], vec![2, 1, 1]).unwrap();
11392 let out = x.conv1d(&w, None, 1, 0, 1).unwrap();
11393 assert_eq!(out.shape().dims(), &[1, 2, 3]);
11394 assert_eq!(out.realize_f32(), vec![2.0, 4.0, 6.0, -1.0, -2.0, -3.0]);
11396 }
11397
11398 #[test]
11399 fn conv1d_rejects_rank_two_input() {
11400 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11401 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11402 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11403 }
11404
11405 #[test]
11406 fn conv1d_rejects_rank_two_weight() {
11407 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11408 let w = x.const_f32_like(vec![1.0], vec![1, 1]).unwrap();
11409 assert!(x.conv1d(&w, None, 1, 0, 1).is_err());
11410 }
11411
11412 #[test]
11413 fn conv1d_rejects_zero_groups_or_stride() {
11414 let x = cpu_f32(vec![1.0; 4], &[1, 1, 4]);
11415 let w = x.const_f32_like(vec![1.0], vec![1, 1, 1]).unwrap();
11416 assert!(x.conv1d(&w, None, 0, 0, 1).is_err());
11417 assert!(x.conv1d(&w, None, 1, 0, 0).is_err());
11418 }
11419
11420 #[test]
11421 fn conv1d_with_algo_ignores_algo_param() {
11422 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 4]);
11423 let w = x.const_f32_like(vec![1.0, 1.0], vec![1, 1, 2]).unwrap();
11424 let a = x.conv1d_with_algo(&w, None, 1, 0, 1, "unused").unwrap();
11426 let b = x.conv1d(&w, None, 1, 0, 1).unwrap();
11427 assert_eq!(a.realize_f32(), b.realize_f32());
11428 }
11429
11430 #[test]
11433 fn avg_pool2d_2x2_stride2() {
11434 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11436 let x = cpu_f32(data, &[1, 1, 4, 4]);
11437 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11438 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11439 let v = out.realize_f32();
11443 assert!((v[0] - 2.5).abs() < 1e-5);
11444 assert!((v[1] - 4.5).abs() < 1e-5);
11445 assert!((v[2] - 10.5).abs() < 1e-5);
11446 assert!((v[3] - 12.5).abs() < 1e-5);
11447 }
11448
11449 #[test]
11450 fn avg_pool2d_3x3_stride1_padding1_preserves_size() {
11451 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11452 let out = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11453 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11454 }
11455
11456 #[test]
11474 fn avg_pool2d_excludes_padding_keeps_uniform_data_exactly_at_its_true_value() {
11475 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11476 let included = x.avg_pool2d((3, 3), (1, 1), (1, 1), true).unwrap();
11477 let excluded = x.avg_pool2d((3, 3), (1, 1), (1, 1), false).unwrap();
11478 assert_eq!(included.shape().dims(), excluded.shape().dims());
11479
11480 let ve = excluded.realize_f32();
11481 for (i, &v) in ve.iter().enumerate() {
11482 assert!(
11483 (v - 1.0).abs() < 1e-5,
11484 "count_include_pad=false must be exactly 1.0 at every position \
11485 (all real taps are 1.0) — position {i} got {v}",
11486 );
11487 }
11488
11489 let vi = included.realize_f32();
11490 assert!(
11491 (vi[0] - 1.0).abs() > 0.1,
11492 "count_include_pad=true must NOT match at a corner (4 of 9 taps \
11493 real → 4/9, far from 1.0) — got {}, modes should disagree here",
11494 vi[0]
11495 );
11496 }
11497
11498 #[test]
11507 fn avg_pool2d_excludes_padding_from_divisor_discriminating() {
11508 let x = cpu_f32(vec![10.0, 10.0, 10.0, 10.0], &[1, 1, 2, 2]);
11509 let included = x.avg_pool2d((2, 2), (1, 1), (1, 1), true).unwrap();
11510 let excluded = x.avg_pool2d((2, 2), (1, 1), (1, 1), false).unwrap();
11511 let vi = included.realize_f32();
11512 let ve = excluded.realize_f32();
11513 assert!(
11514 (vi[0] - 2.5).abs() < 1e-5,
11515 "count_include_pad=true: top-left = 10/4 = 2.5, got {}",
11516 vi[0]
11517 );
11518 assert!(
11519 (ve[0] - 10.0).abs() < 1e-5,
11520 "count_include_pad=false: top-left = 10/1 (ONE real tap) = 10.0, got {}",
11521 ve[0]
11522 );
11523 }
11524
11525 #[test]
11530 fn avg_pool2d_rejects_zero_valid_tap_window() {
11531 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11532 let err = x
11533 .avg_pool2d((1, 1), (1, 1), (2, 2), false)
11534 .expect_err("a window with zero real taps must be rejected, not silently NaN");
11535 let msg = err.to_string();
11536 assert!(
11537 msg.contains("divide by zero") || msg.contains("no valid"),
11538 "error should explain the zero-valid-tap cause, got: {msg}"
11539 );
11540
11541 let ok = x.avg_pool2d((1, 1), (1, 1), (2, 2), true);
11546 assert!(
11547 ok.is_ok(),
11548 "count_include_pad=true must still accept this shape (uniform divisor, never zero)"
11549 );
11550 }
11551
11552 #[test]
11553 fn avg_pool2d_multi_channel() {
11554 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]);
11556 let out = x.avg_pool2d((2, 2), (2, 2), (0, 0), true).unwrap();
11557 assert_eq!(out.shape().dims(), &[1, 2, 1, 1]);
11558 assert_eq!(out.realize_f32(), vec![0.0, 1.0]);
11559 }
11560
11561 #[test]
11562 fn max_pool2d_2x2_stride2() {
11563 let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
11564 let x = cpu_f32(data, &[1, 1, 4, 4]);
11565 let out = x.max_pool2d((2, 2), (2, 2), (0, 0)).unwrap();
11566 assert_eq!(out.shape().dims(), &[1, 1, 2, 2]);
11567 assert_eq!(out.realize_f32(), vec![5.0, 7.0, 13.0, 15.0]);
11569 }
11570
11571 #[test]
11572 fn max_pool2d_3x3_stride1_padding1() {
11573 let x = cpu_f32(
11574 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
11575 &[1, 1, 3, 3],
11576 );
11577 let out = x.max_pool2d((3, 3), (1, 1), (1, 1)).unwrap();
11578 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11579 let v = out.realize_f32();
11581 assert!((v[4] - 9.0).abs() < 1e-5);
11583 }
11584
11585 #[test]
11586 fn upsample_nearest2d_2x() {
11587 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11588 let out = x.upsample_nearest2d(2).unwrap();
11589 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11590 assert_eq!(
11592 out.realize_f32(),
11593 vec![
11594 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,
11595 ],
11596 );
11597 }
11598
11599 #[test]
11600 fn upsample_nearest2d_3x() {
11601 let x = cpu_f32(vec![5.0], &[1, 1, 1, 1]);
11602 let out = x.upsample_nearest2d(3).unwrap();
11603 assert_eq!(out.shape().dims(), &[1, 1, 3, 3]);
11604 assert_eq!(out.realize_f32(), vec![5.0; 9]);
11605 }
11606
11607 #[test]
11608 fn upsample_nearest2d_identity_scale_one() {
11609 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11610 let out = x.upsample_nearest2d(1).unwrap();
11611 assert_eq!(out.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
11612 }
11613
11614 #[test]
11615 fn upsample_nearest1d_2x() {
11616 let x = cpu_f32(vec![1.0, 2.0, 3.0], &[1, 1, 3]);
11617 let out = x.upsample_nearest1d(2).unwrap();
11618 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11619 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0]);
11620 }
11621
11622 #[test]
11623 fn interpolate2d_integer_multiple() {
11624 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 1, 2, 2]);
11625 let out = x.interpolate2d(4, 4).unwrap();
11626 assert_eq!(out.shape().dims(), &[1, 1, 4, 4]);
11627 }
11628
11629 #[test]
11630 fn interpolate2d_accepts_non_integer_ratio() {
11631 let x = cpu_f32(vec![1.0; 4], &[1, 1, 2, 2]);
11636 let out = x.interpolate2d(3, 4).unwrap();
11637 assert_eq!(out.shape().dims(), &[1, 1, 3, 4]);
11638 }
11639
11640 #[test]
11641 fn interpolate1d_integer_multiple() {
11642 let x = cpu_f32(vec![1.0, 2.0], &[1, 1, 2]);
11643 let out = x.interpolate1d(6).unwrap();
11644 assert_eq!(out.shape().dims(), &[1, 1, 6]);
11645 assert_eq!(out.realize_f32(), vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0]);
11646 }
11647
11648 #[test]
11649 fn pool_rejects_bad_rank() {
11650 let x = cpu_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
11651 assert!(x.avg_pool2d((2, 2), (2, 2), (0, 0), true).is_err());
11652 assert!(x.max_pool2d((2, 2), (2, 2), (0, 0)).is_err());
11653 }
11654
11655 #[test]
11656 fn pool_rejects_zero_kernel() {
11657 let x = cpu_f32(vec![1.0; 16], &[1, 1, 4, 4]);
11658 assert!(x.avg_pool2d((0, 2), (1, 1), (0, 0), true).is_err());
11659 assert!(x.max_pool2d((2, 0), (1, 1), (0, 0)).is_err());
11660 }
11661
11662 #[test]
11665 fn numel_matches_elem_count() {
11666 let t = cpu_f32(vec![1.0; 12], &[3, 4]);
11667 assert_eq!(t.numel(), t.elem_count());
11668 assert_eq!(t.numel(), 12);
11669 }
11670
11671 #[test]
11672 fn dim_returns_specific_axis_size() {
11673 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11674 assert_eq!(t.dim(0).unwrap(), 2);
11675 assert_eq!(t.dim(1).unwrap(), 3);
11676 assert_eq!(t.dim(2).unwrap(), 4);
11677 assert!(t.dim(3).is_err());
11678 }
11679
11680 #[test]
11681 fn to_dtype_switches_dtype() {
11682 let t = cpu_f32(vec![1.0, 2.0], &[2]);
11683 let b = t.to_dtype(DType::F64).unwrap();
11684 assert_eq!(b.dtype(), DType::F64);
11685 assert_eq!(b.realize_f64(), vec![1.0, 2.0]);
11686 }
11687
11688 #[test]
11689 fn to_dtype_same_dtype_is_noop() {
11690 let t = cpu_f32(vec![1.0_f32], &[1]);
11691 let b = t.to_dtype(DType::F32).unwrap();
11692 assert_eq!(b.dtype(), DType::F32);
11693 }
11694
11695 #[test]
11696 fn detach_is_identity_on_lazy() {
11697 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11698 let d = t.detach();
11699 assert_eq!(d.realize_f32(), t.realize_f32());
11700 }
11701
11702 #[test]
11703 fn track_op_is_true_on_lazy() {
11704 let t = cpu_f32(vec![0.0], &[1]);
11705 assert!(t.track_op());
11706 }
11707
11708 #[test]
11711 fn try_permute_accepts_tuple_syntax() {
11712 let t = cpu_f32(vec![0.0; 24], &[2, 3, 4]);
11713 let out = t.permute((2_usize, 0_usize, 1_usize)).unwrap();
11715 assert_eq!(out.shape().dims(), &[4, 2, 3]);
11716 }
11717
11718 #[test]
11719 fn dim_arg_methods_accept_negative_indexing() {
11720 use fuel_ir::D;
11721 let t = cpu_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
11722 let sum_last = t.shape().dims().to_vec(); assert_eq!(sum_last, vec![2, 3]);
11725 let cumsum_last = t.cumsum(D::Minus1).unwrap();
11728 assert_eq!(cumsum_last.shape().dims(), &[2, 3]);
11729 }
11730
11731 #[test]
11732 fn unsqueeze_accepts_dim_trait() {
11733 use fuel_ir::D;
11734 let t = cpu_f32(vec![1.0, 2.0, 3.0], &[3]);
11735 let out = t.unsqueeze(D::Minus1).unwrap();
11737 assert_eq!(out.shape().dims().len(), 2);
11740 }
11741}
11742pub struct TokenDataHost {
11743 pub token_ids: fuel_ir::HostBuffer,
11744 pub rope_cos: fuel_ir::HostBuffer,
11745 pub rope_sin: fuel_ir::HostBuffer,
11746 pub mask: fuel_ir::HostBuffer,
11747 pub offset: Option<fuel_ir::HostBuffer>,
11748}
11749
11750#[cfg(feature = "cuda")]
11755pub struct TokenDataBytes {
11756 pub token_ids: Vec<u8>,
11757 pub rope_cos: Vec<u8>,
11758 pub rope_sin: Vec<u8>,
11759 pub mask: Vec<u8>,
11760 pub offset: Option<Vec<u8>>,
11761}
11762
11763#[cfg(feature = "cuda")]
11776pub fn captured_output_to_f32(
11777 output: &Arc<std::sync::RwLock<fuel_memory::Storage>>,
11778) -> fuel_ir::error::Result<Vec<f32>> {
11779 use fuel_memory::BackendStorage;
11780 let guard = output.read().map_err(|_| {
11781 fuel_ir::Error::Msg("captured decode output storage lock poisoned".into()).bt()
11782 })?;
11783 let bytes: Vec<u8> = match &guard.inner {
11784 BackendStorage::Cpu(c) => c.bytes().to_vec(),
11785 BackendStorage::Cuda(c) => c.to_cpu_bytes()?,
11786 #[allow(unreachable_patterns)]
11787 other => {
11788 return Err(fuel_ir::Error::Msg(format!(
11789 "captured decode output is BackendStorage::{:?}, expected Cpu or Cuda",
11790 std::mem::discriminant(other),
11791 ))
11792 .bt());
11793 }
11794 };
11795 Ok(bytemuck::cast_slice::<u8, f32>(&bytes).to_vec())
11796}