1use std::any::Any;
2use std::hash::{Hash, Hasher};
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_cpu::with_cpu_engine;
7use tenferro_extension_macros::define_extension_runtime;
8use tenferro_ops::SymDim;
9use tenferro_runtime::extension::ExtensionOp;
10use tenferro_tensor::{DType, Engine, Error, ErrorKind, Tensor, TensorRead};
11
12#[cfg(feature = "cuda")]
13use tenferro_gpu::cuda::raw::with_cuda_engine;
14
15use crate::backend::LinalgBackend;
16use crate::RankRevealingQrOptions;
17
18#[cfg(all(test, feature = "cuda"))]
19#[path = "extension/cuda_tests.rs"]
20mod cuda_tests;
21mod gauge;
22#[cfg(all(test, not(feature = "cuda")))]
23mod tests;
24
25pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
26
27pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
28
29pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum SvdGauge {
56 Raw,
58 CanonicalPivot,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum EighGauge {
75 Raw,
77 CanonicalPivot,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum QrGauge {
93 Raw,
95 PositiveDiagonal,
97}
98
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
117pub enum SvdDriver {
118 #[default]
125 Auto,
126 Gesvdj,
128 Gesvd,
130 Xgesvdp,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
173pub struct SvdOptions {
174 pub gauge: SvdGauge,
176 pub derivative_eps: f64,
178 pub driver: SvdDriver,
180}
181
182impl Default for SvdOptions {
183 fn default() -> Self {
184 Self {
185 gauge: SvdGauge::Raw,
186 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
187 driver: SvdDriver::Auto,
188 }
189 }
190}
191
192impl SvdOptions {
193 pub fn gauge(mut self, gauge: SvdGauge) -> Self {
204 self.gauge = gauge;
205 self
206 }
207
208 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
219 self.derivative_eps = derivative_eps;
220 self
221 }
222
223 pub fn driver(mut self, driver: SvdDriver) -> Self {
234 self.driver = driver;
235 self
236 }
237}
238
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
257pub enum EighDriver {
258 #[default]
267 Auto,
268 Syevd,
271 Syevj,
279}
280
281#[derive(Clone, Copy, Debug, PartialEq)]
295pub struct EighOptions {
296 pub gauge: EighGauge,
298 pub derivative_eps: f64,
300 pub driver: EighDriver,
302}
303
304impl Default for EighOptions {
305 fn default() -> Self {
306 Self {
307 gauge: EighGauge::Raw,
308 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
309 driver: EighDriver::Auto,
310 }
311 }
312}
313
314impl EighOptions {
315 pub fn gauge(mut self, gauge: EighGauge) -> Self {
326 self.gauge = gauge;
327 self
328 }
329
330 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
341 self.derivative_eps = derivative_eps;
342 self
343 }
344
345 pub fn driver(mut self, driver: EighDriver) -> Self {
356 self.driver = driver;
357 self
358 }
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq)]
372pub struct QrOptions {
373 pub gauge: QrGauge,
375}
376
377impl Default for QrOptions {
378 fn default() -> Self {
379 Self {
380 gauge: QrGauge::Raw,
381 }
382 }
383}
384
385impl QrOptions {
386 pub fn gauge(mut self, gauge: QrGauge) -> Self {
397 self.gauge = gauge;
398 self
399 }
400}
401
402pub(crate) fn validate_derivative_eps(
403 op: &'static str,
404 derivative_eps: f64,
405) -> tenferro_tensor::Result<()> {
406 if derivative_eps.is_finite() && derivative_eps > 0.0 {
407 Ok(())
408 } else {
409 Err(Error::invalid_argument(
410 op,
411 "derivative_eps",
412 format!("must be positive and finite, got {derivative_eps}"),
413 ))
414 }
415}
416
417#[derive(Clone, Copy, Debug, PartialEq)]
418#[doc(hidden)]
419#[allow(dead_code)]
420pub(crate) enum LinalgOp {
421 Cholesky,
422 Lu,
423 LuFactor,
424 LuSolvePrepared {
425 transpose_a: bool,
426 conjugate_a: bool,
427 },
428 SignDetFromLuFactor,
429 LogAbsDetFromLuFactor,
430 FullPivLu,
431 FullPivLuSolve {
432 transpose_a: bool,
433 },
434 Solve,
438 LuFactorSolve,
448 Svd {
449 derivative_eps: f64,
450 gauge: SvdGauge,
451 driver: SvdDriver,
452 },
453 SvdFull,
457 SvdVals {
459 derivative_eps: f64,
460 driver: SvdDriver,
461 },
462 Qr {
463 gauge: QrGauge,
464 },
465 RankRevealingQr {
466 gauge: QrGauge,
467 rtol: f64,
468 atol: f64,
469 },
470 HouseholderQrFactor,
471 HouseholderQrFromFactors,
472 HouseholderQrAppend,
473 HouseholderQrR {
474 gauge: QrGauge,
475 },
476 HouseholderQrQColumns {
477 start: usize,
478 end: usize,
479 gauge: QrGauge,
480 },
481 HouseholderQrThinQ {
483 gauge: QrGauge,
484 },
485 HouseholderQrAppendTangent,
487 HouseholderQrSplitTangent {
489 right: bool,
490 },
491 Eigh {
492 derivative_eps: f64,
493 gauge: EighGauge,
494 driver: EighDriver,
495 },
496 EighVals {
498 derivative_eps: f64,
499 driver: EighDriver,
500 },
501 Eig {
502 input_dtype: DType,
503 },
504 EigVals {
505 input_dtype: DType,
506 },
507 TriangularSolve {
508 left_side: bool,
509 lower: bool,
510 transpose_a: bool,
511 unit_diagonal: bool,
512 },
513}
514
515impl LinalgOp {
516 fn output_count(self) -> usize {
517 match self {
518 Self::Cholesky
519 | Self::EighVals { .. }
520 | Self::EigVals { .. }
521 | Self::FullPivLuSolve { .. }
522 | Self::LogAbsDetFromLuFactor
523 | Self::LuSolvePrepared { .. }
524 | Self::SignDetFromLuFactor
525 | Self::Solve
526 | Self::SvdVals { .. }
527 | Self::TriangularSolve { .. } => 1,
528 Self::Svd { .. } | Self::SvdFull | Self::LuFactorSolve => 3,
529 Self::RankRevealingQr { .. } | Self::Lu => 4,
530 Self::Qr { .. }
531 | Self::HouseholderQrFactor
532 | Self::HouseholderQrFromFactors
533 | Self::HouseholderQrAppend
534 | Self::Eigh { .. }
535 | Self::Eig { .. } => 2,
536 Self::HouseholderQrR { .. }
537 | Self::HouseholderQrQColumns { .. }
538 | Self::HouseholderQrThinQ { .. }
539 | Self::HouseholderQrAppendTangent
540 | Self::HouseholderQrSplitTangent { .. } => 1,
541 Self::LuFactor => 3,
542 Self::FullPivLu => 5,
543 }
544 }
545
546 fn input_count(self) -> usize {
547 match self {
548 Self::FullPivLuSolve { .. }
549 | Self::Solve
550 | Self::LuFactorSolve
551 | Self::TriangularSolve { .. }
552 | Self::HouseholderQrFromFactors
553 | Self::HouseholderQrR { .. }
554 | Self::HouseholderQrQColumns { .. }
555 | Self::HouseholderQrThinQ { .. } => 2,
556 Self::LogAbsDetFromLuFactor => 2,
557 Self::SignDetFromLuFactor | Self::HouseholderQrAppend => 3,
558 Self::HouseholderQrAppendTangent => 4,
559 Self::HouseholderQrSplitTangent { .. } => 3,
560 Self::LuSolvePrepared { .. } => 4,
561 _ => 1,
562 }
563 }
564
565 fn tag(self) -> u8 {
566 match self {
567 Self::Cholesky => 0,
568 Self::Lu => 1,
569 Self::FullPivLu => 2,
570 Self::FullPivLuSolve { .. } => 3,
571 Self::Svd { .. } => 4,
572 Self::Qr { .. } => 5,
573 Self::Eigh { .. } => 6,
574 Self::Eig { .. } => 7,
575 Self::TriangularSolve { .. } => 9,
576 Self::LuFactor => 10,
577 Self::LuSolvePrepared { .. } => 11,
578 Self::SvdVals { .. } => 12,
579 Self::EighVals { .. } => 13,
580 Self::EigVals { .. } => 14,
581 Self::SvdFull => 15,
582 Self::LogAbsDetFromLuFactor => 16,
583 Self::SignDetFromLuFactor => 17,
584 Self::Solve => 18,
585 Self::HouseholderQrFactor => 19,
586 Self::HouseholderQrFromFactors => 20,
587 Self::HouseholderQrAppend => 21,
588 Self::HouseholderQrR { .. } => 22,
589 Self::HouseholderQrQColumns { .. } => 23,
590 Self::HouseholderQrThinQ { .. } => 24,
591 Self::HouseholderQrAppendTangent => 25,
592 Self::HouseholderQrSplitTangent { .. } => 26,
593 Self::RankRevealingQr { .. } => 27,
594 Self::LuFactorSolve => 28,
595 }
596 }
597}
598
599#[derive(Clone, Debug, PartialEq)]
600#[doc(hidden)]
601pub(crate) struct LinalgExtensionOp {
602 op: LinalgOp,
603}
604
605impl LinalgExtensionOp {
606 pub(crate) fn new(op: LinalgOp) -> Self {
607 Self { op }
608 }
609
610 pub(crate) fn op(&self) -> LinalgOp {
611 self.op
612 }
613}
614
615impl ExtensionOp for LinalgExtensionOp {
616 fn family_id(&self) -> &'static str {
617 LINALG_EXTENSION_FAMILY_ID
618 }
619
620 fn payload_hash(&self, hasher: &mut dyn Hasher) {
621 hasher.write_u8(self.op.tag());
622 match self.op {
623 LinalgOp::Svd {
624 derivative_eps,
625 gauge,
626 driver,
627 } => {
628 hasher.write_u64(derivative_eps.to_bits());
629 hash_svd_gauge(hasher, gauge);
630 hash_svd_driver(hasher, driver);
631 }
632 LinalgOp::SvdVals {
633 derivative_eps,
634 driver,
635 } => {
636 hasher.write_u64(derivative_eps.to_bits());
637 hash_svd_driver(hasher, driver);
638 }
639 LinalgOp::EighVals {
640 derivative_eps,
641 driver,
642 } => {
643 hasher.write_u64(derivative_eps.to_bits());
644 hash_eigh_driver(hasher, driver);
645 }
646 LinalgOp::Qr { gauge }
647 | LinalgOp::HouseholderQrR { gauge }
648 | LinalgOp::HouseholderQrThinQ { gauge } => {
649 hash_qr_gauge(hasher, gauge);
650 }
651 LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
652 hash_qr_gauge(hasher, gauge);
653 hasher.write_u64(rtol.to_bits());
654 hasher.write_u64(atol.to_bits());
655 }
656 LinalgOp::HouseholderQrQColumns { start, end, gauge } => {
657 hasher.write_usize(start);
658 hasher.write_usize(end);
659 hash_qr_gauge(hasher, gauge);
660 }
661 LinalgOp::Eigh {
662 derivative_eps,
663 gauge,
664 driver,
665 } => {
666 hasher.write_u64(derivative_eps.to_bits());
667 hash_eigh_gauge(hasher, gauge);
668 hash_eigh_driver(hasher, driver);
669 }
670 LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
671 hash_dtype(hasher, input_dtype);
672 }
673 LinalgOp::FullPivLuSolve { transpose_a }
674 | LinalgOp::HouseholderQrSplitTangent { right: transpose_a } => {
675 hasher.write_u8(u8::from(transpose_a));
676 }
677 LinalgOp::LuSolvePrepared {
678 transpose_a,
679 conjugate_a,
680 } => {
681 hasher.write_u8(u8::from(transpose_a));
682 hasher.write_u8(u8::from(conjugate_a));
683 }
684 LinalgOp::TriangularSolve {
685 left_side,
686 lower,
687 transpose_a,
688 unit_diagonal,
689 } => {
690 hasher.write_u8(u8::from(left_side));
691 hasher.write_u8(u8::from(lower));
692 hasher.write_u8(u8::from(transpose_a));
693 hasher.write_u8(u8::from(unit_diagonal));
694 }
695 LinalgOp::Cholesky
696 | LinalgOp::Lu
697 | LinalgOp::LuFactor
698 | LinalgOp::LogAbsDetFromLuFactor
699 | LinalgOp::SignDetFromLuFactor
700 | LinalgOp::FullPivLu
701 | LinalgOp::SvdFull
702 | LinalgOp::Solve
703 | LinalgOp::LuFactorSolve
704 | LinalgOp::HouseholderQrFactor
705 | LinalgOp::HouseholderQrFromFactors
706 | LinalgOp::HouseholderQrAppend
707 | LinalgOp::HouseholderQrAppendTangent => {}
708 }
709 }
710
711 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
712 other
713 .as_any()
714 .downcast_ref::<Self>()
715 .is_some_and(|that| self == that)
716 }
717
718 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
719 Arc::new(self.clone())
720 }
721
722 fn as_any(&self) -> &dyn Any {
723 self
724 }
725
726 fn input_count(&self) -> usize {
727 self.op.input_count()
728 }
729
730 fn output_count(&self) -> usize {
731 self.op.output_count()
732 }
733
734 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
735 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
736 }
737
738 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
739 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
740 }
741
742 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
743 match self.op {
744 LinalgOp::Svd {
745 derivative_eps,
746 driver,
747 ..
748 } if live_outputs == [false, true, false] => {
749 Some(Arc::new(Self::new(LinalgOp::SvdVals {
750 derivative_eps,
751 driver,
752 })))
753 }
754 LinalgOp::Eigh {
755 derivative_eps,
756 driver,
757 ..
758 } if live_outputs == [true, false] => Some(Arc::new(Self::new(LinalgOp::EighVals {
759 derivative_eps,
760 driver,
761 }))),
762 LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
763 Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
764 }
765 _ => None,
773 }
774 }
775
776 fn infer_output_meta(
777 &self,
778 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
779 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
780 let input_dtypes = (0..self.input_count())
781 .map(|input| ctx.input_dtype(input))
782 .collect::<Result<Vec<_>, _>>()?;
783 let input_shapes = (0..self.input_count())
784 .map(|input| ctx.input_shape(input))
785 .collect::<Result<Vec<_>, _>>()?;
786 let metas = match self.op {
787 LinalgOp::Cholesky => {
788 require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
789 vec![(promote_dtypes(&input_dtypes), input_shapes[0].to_vec())]
790 }
791 LinalgOp::FullPivLuSolve { .. } => {
792 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
793 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
794 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
795 }
796 LinalgOp::Solve => {
797 require_matrix_meta("tenferro-linalg.solve", input_shapes[0])?;
798 require_matrix_meta("tenferro-linalg.solve", input_shapes[1])?;
799 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
800 }
801 LinalgOp::LuFactorSolve => {
802 require_matrix_meta("tenferro-linalg.lu_factor_solve", input_shapes[1])?;
803 let mut factors = lu_factor_meta(input_dtypes[0], input_shapes[0])?.into_iter();
804 let (Some(packed_lu), Some(pivots)) = (factors.next(), factors.next()) else {
805 return Err(Error::Internal(
806 "lu_factor_solve: lu_factor metadata returned fewer than two outputs"
807 .into(),
808 ));
809 };
810 vec![
811 (promote_dtypes(&input_dtypes), input_shapes[1].to_vec()),
812 packed_lu,
813 pivots,
814 ]
815 }
816 LinalgOp::TriangularSolve { .. } => {
817 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
818 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
819 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
820 }
821 LinalgOp::LuSolvePrepared { .. } => {
822 require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
823 require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
824 vec![(
825 promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
826 input_shapes[3].to_vec(),
827 )]
828 }
829 LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
830 LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
831 LinalgOp::SignDetFromLuFactor => {
832 vec![signdet_from_lu_factor_meta(
833 input_dtypes[0],
834 input_shapes[0],
835 input_shapes[1],
836 input_shapes[2],
837 )?]
838 }
839 LinalgOp::LogAbsDetFromLuFactor => {
840 vec![logabsdet_from_lu_factor_meta(
841 input_dtypes[0],
842 input_shapes[0],
843 input_shapes[1],
844 )?]
845 }
846 LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
847 LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
848 LinalgOp::SvdFull => svd_full_meta(input_dtypes[0], input_shapes[0])?,
849 LinalgOp::SvdVals { .. } => {
850 vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
851 }
852 LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
853 LinalgOp::RankRevealingQr { .. } => {
854 rank_revealing_qr_meta(input_dtypes[0], input_shapes[0])?
855 }
856 LinalgOp::HouseholderQrFactor => {
857 householder_qr_factor_meta(input_dtypes[0], input_shapes[0])?
858 }
859 LinalgOp::HouseholderQrFromFactors => {
860 householder_qr_from_factors_meta(&input_dtypes, &input_shapes)?
861 }
862 LinalgOp::HouseholderQrAppend => {
863 householder_qr_append_meta(&input_dtypes, &input_shapes)?
864 }
865 LinalgOp::HouseholderQrR { .. } => vec![householder_qr_r_meta(
866 &input_dtypes,
867 input_shapes[0],
868 input_shapes[1],
869 )?],
870 LinalgOp::HouseholderQrQColumns { start, end, .. } => {
871 vec![householder_qr_q_columns_meta(
872 &input_dtypes,
873 input_shapes[0],
874 input_shapes[1],
875 start,
876 end,
877 )?]
878 }
879 LinalgOp::HouseholderQrThinQ { .. } => {
880 vec![householder_qr_thin_q_meta(
881 &input_dtypes,
882 input_shapes[0],
883 input_shapes[1],
884 )?]
885 }
886 LinalgOp::HouseholderQrAppendTangent => {
887 vec![householder_qr_append_tangent_meta(
888 &input_dtypes,
889 &input_shapes,
890 )?]
891 }
892 LinalgOp::HouseholderQrSplitTangent { right } => {
893 vec![householder_qr_split_tangent_meta(
894 &input_dtypes,
895 &input_shapes,
896 right,
897 )?]
898 }
899 LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
900 LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
901 LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
902 LinalgOp::EigVals { input_dtype } => {
903 vec![eig_values_meta(input_dtype, input_shapes[0])?]
904 }
905 };
906 Ok(metas)
907 }
908}
909
910fn execute_linalg_extension_reads_on_session<B: Engine + ?Sized>(
911 op: &LinalgExtensionOp,
912 inputs: &[TensorRead<'_>],
913 session: &mut B,
914) -> tenferro_tensor::Result<Vec<Tensor>> {
915 if let Some(result) = with_cpu_engine(session, |session| {
916 execute_linalg_extension_reads_in_session(op, inputs, session)
917 }) {
918 return result;
919 }
920 #[cfg(feature = "cuda")]
921 if let Some(result) = with_cuda_engine(session, |session| {
922 execute_linalg_extension_reads_in_session(op, inputs, session)
923 }) {
924 return result;
925 }
926 Err(Error::unsupported(
927 "linalg_extension",
928 "selected backend session does not expose a linalg execution capability",
929 ))
930}
931
932fn execute_linalg_extension_reads_in_session<S: LinalgBackend>(
933 op: &LinalgExtensionOp,
934 inputs: &[TensorRead<'_>],
935 session: &mut S,
936) -> tenferro_tensor::Result<Vec<Tensor>> {
937 if op.op() == LinalgOp::HouseholderQrAppendTangent {
938 let left = session.to_contiguous_read(inputs[0].clone())?;
939 let right = session.to_contiguous_read(inputs[1].clone())?;
940 return Ok(vec![session.concatenate(&[&left, &right], 1)?]);
941 }
942 if let LinalgOp::HouseholderQrSplitTangent { right } = op.op() {
943 let cotangent_shape = inputs[0].clone().tensor_view().shape().to_vec();
944 let left_shape = inputs[1].clone().tensor_view().shape().to_vec();
945 let right_shape = inputs[2].clone().tensor_view().shape().to_vec();
946 let config =
947 householder_qr_split_config(&cotangent_shape, &left_shape, &right_shape, right)?;
948 let cotangent = session.to_contiguous_read(inputs[0].clone())?;
949 return Ok(vec![session.slice(&cotangent, &config)?]);
950 }
951 match op.op() {
955 LinalgOp::Cholesky => return Ok(vec![session.cholesky_read(inputs[0].clone())?]),
956 LinalgOp::Lu => return session.lu_read(inputs[0].clone()),
957 LinalgOp::FullPivLu => return session.full_piv_lu_read(inputs[0].clone()),
958 LinalgOp::Svd {
959 derivative_eps,
960 gauge,
961 driver,
962 } => {
963 return session.svd_with_options_read(
964 inputs[0].clone(),
965 SvdOptions {
966 derivative_eps,
967 gauge,
968 driver,
969 },
970 );
971 }
972 LinalgOp::SvdFull => return session.svd_full_read(inputs[0].clone()),
973 LinalgOp::SvdVals { driver, .. } => {
974 return Ok(vec![
975 session.svd_values_with_driver_read(inputs[0].clone(), driver)?
976 ]);
977 }
978 LinalgOp::Qr { gauge } => {
979 return session.qr_with_options_read(inputs[0].clone(), QrOptions { gauge });
980 }
981 LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
982 return session.rank_revealing_qr_read(
983 inputs[0].clone(),
984 RankRevealingQrOptions { gauge, rtol, atol },
985 );
986 }
987 LinalgOp::Eigh {
988 derivative_eps,
989 gauge,
990 driver,
991 } => {
992 return session.eigh_with_options_read(
993 inputs[0].clone(),
994 EighOptions {
995 derivative_eps,
996 gauge,
997 driver,
998 },
999 );
1000 }
1001 LinalgOp::EighVals { driver, .. } => {
1002 return Ok(vec![
1003 session.eigh_values_with_driver_read(inputs[0].clone(), driver)?
1004 ]);
1005 }
1006 LinalgOp::Eig { .. } => return session.eig_read(inputs[0].clone()),
1007 LinalgOp::EigVals { .. } => return Ok(vec![session.eig_values_read(inputs[0].clone())?]),
1008 LinalgOp::Solve => match session.solve_read(inputs[0].clone(), inputs[1].clone()) {
1009 Ok(output) => return Ok(vec![output]),
1010 Err(error) if error.kind() == ErrorKind::Unsupported => {}
1011 Err(error) => return Err(error),
1012 },
1013 _ => {}
1014 }
1015 if let LinalgOp::TriangularSolve {
1016 left_side,
1017 lower,
1018 transpose_a,
1019 unit_diagonal,
1020 } = op.op()
1021 {
1022 match session.triangular_solve_read(
1023 inputs[0].clone(),
1024 inputs[1].clone(),
1025 left_side,
1026 lower,
1027 transpose_a,
1028 unit_diagonal,
1029 ) {
1030 Ok(output) => return Ok(vec![output]),
1031 Err(error) if error.kind() == ErrorKind::Unsupported => {}
1032 Err(error) => return Err(error),
1033 }
1034 }
1035
1036 let materialized_inputs = inputs
1039 .iter()
1040 .filter(|input| input.as_tensor().is_none())
1041 .cloned()
1042 .map(|input| session.to_contiguous_read(input))
1043 .collect::<tenferro_tensor::Result<Vec<_>>>()?;
1044 let mut views = materialized_inputs.iter();
1045 let input_refs: Vec<&Tensor> = inputs
1048 .iter()
1049 .filter_map(|input| input.as_tensor().or_else(|| views.next()))
1050 .collect();
1051 execute_linalg(op.op(), &input_refs, session)
1052}
1053
1054fn linalg_session_supported<B: Engine + 'static>(
1055 #[cfg_attr(not(feature = "cuda"), allow(unused_variables))] op: &LinalgExtensionOp,
1056) -> bool {
1057 let type_id = std::any::TypeId::of::<B>();
1063 if type_id == std::any::TypeId::of::<tenferro_cpu::CpuEngine>() {
1064 return true;
1068 }
1069 #[cfg(feature = "cuda")]
1070 {
1071 if type_id == std::any::TypeId::of::<tenferro_gpu::cuda::CudaEngine>() {
1072 return match op.op() {
1073 LinalgOp::FullPivLu | LinalgOp::FullPivLuSolve { .. } => false,
1075 LinalgOp::Eig { .. } | LinalgOp::EigVals { .. } => false,
1076 LinalgOp::Solve => true,
1081 LinalgOp::LuFactorSolve => true,
1084 LinalgOp::LuSolvePrepared {
1086 transpose_a: false,
1087 conjugate_a: true,
1088 } => false,
1089 _ => true,
1090 };
1091 }
1092 }
1093 false
1094}
1095
1096fn execute_linalg_extension_in_session(
1097 op: &LinalgExtensionOp,
1098 session: &mut dyn Engine,
1099 _extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
1100 inputs: &[TensorRead<'_>],
1101) -> tenferro_tensor::Result<Vec<Tensor>> {
1102 execute_linalg_extension_reads_on_session(op, inputs, session)
1106}
1107
1108define_extension_runtime! {
1109 runtime = LinalgRuntime,
1110 family_id = LINALG_EXTENSION_FAMILY_ID,
1111 op_type = LinalgExtensionOp,
1112 execute_in_session = execute_linalg_extension_in_session,
1113 session_supported = linalg_session_supported,
1114 backend_bound = Engine,
1115}
1116
1117fn execute_linalg<B: LinalgBackend>(
1118 op: LinalgOp,
1119 inputs: &[&Tensor],
1120 backend: &mut B,
1121) -> tenferro_tensor::Result<Vec<Tensor>> {
1122 match op {
1123 LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
1124 LinalgOp::Lu => backend.lu(inputs[0]),
1125 LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
1126 LinalgOp::SignDetFromLuFactor => {
1127 Ok(vec![signdet_from_lu_factor(inputs[1], inputs[2], backend)?])
1128 }
1129 LinalgOp::LogAbsDetFromLuFactor => Ok(vec![logabsdet_from_lu_factor(inputs[1], backend)?]),
1130 LinalgOp::LuSolvePrepared {
1131 transpose_a,
1132 conjugate_a,
1133 } => Ok(vec![backend.lu_solve_prepared(
1134 inputs[0],
1135 inputs[1],
1136 inputs[2],
1137 inputs[3],
1138 transpose_a,
1139 conjugate_a,
1140 )?]),
1141 LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
1142 LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
1143 inputs[0],
1144 inputs[1],
1145 transpose_a,
1146 )?]),
1147 LinalgOp::Solve => Ok(vec![backend.solve(inputs[0], inputs[1])?]),
1148 LinalgOp::LuFactorSolve => backend.lu_factor_solve(inputs[0], inputs[1]),
1149 LinalgOp::Svd {
1150 derivative_eps,
1151 gauge,
1152 driver,
1153 } => backend.svd_with_options(
1154 inputs[0],
1155 SvdOptions {
1156 derivative_eps,
1157 gauge,
1158 driver,
1159 },
1160 ),
1161 LinalgOp::SvdFull => backend.svd_full(inputs[0]),
1162 LinalgOp::SvdVals { driver, .. } => {
1163 Ok(vec![backend.svd_values_with_driver(inputs[0], driver)?])
1164 }
1165 LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
1166 LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
1167 backend.rank_revealing_qr(inputs[0], RankRevealingQrOptions { gauge, rtol, atol })
1168 }
1169 LinalgOp::HouseholderQrFactor => {
1170 let state = backend.householder_qr(inputs[0])?;
1171 Ok(vec![state.packed, state.coeff])
1172 }
1173 LinalgOp::HouseholderQrFromFactors => {
1174 let state = backend.householder_qr_from_factors(inputs[0], inputs[1])?;
1175 Ok(vec![state.packed, state.coeff])
1176 }
1177 LinalgOp::HouseholderQrAppend => {
1178 let state = backend.householder_qr_append(inputs[0], inputs[1], inputs[2])?;
1179 Ok(vec![state.packed, state.coeff])
1180 }
1181 LinalgOp::HouseholderQrR { gauge } => Ok(vec![backend.householder_qr_r(
1182 inputs[0],
1183 inputs[1],
1184 QrOptions { gauge },
1185 )?]),
1186 LinalgOp::HouseholderQrQColumns { start, end, gauge } => Ok(vec![backend
1187 .householder_qr_q_columns(inputs[0], inputs[1], start..end, QrOptions { gauge })?]),
1188 LinalgOp::HouseholderQrThinQ { gauge } => {
1189 let end = inputs[1].shape().first().copied().ok_or_else(|| {
1190 Error::rank_mismatch("tenferro-linalg.householder_qr_thin_q", 1, 0)
1191 })?;
1192 Ok(vec![backend.householder_qr_q_columns(
1193 inputs[0],
1194 inputs[1],
1195 0..end,
1196 QrOptions { gauge },
1197 )?])
1198 }
1199 LinalgOp::HouseholderQrAppendTangent => {
1200 Ok(vec![backend.concatenate(&[inputs[0], inputs[1]], 1)?])
1201 }
1202 LinalgOp::HouseholderQrSplitTangent { right } => {
1203 let config = householder_qr_split_config(
1204 inputs[0].shape(),
1205 inputs[1].shape(),
1206 inputs[2].shape(),
1207 right,
1208 )?;
1209 Ok(vec![backend.slice(inputs[0], &config)?])
1210 }
1211 LinalgOp::Eigh {
1212 derivative_eps,
1213 gauge,
1214 driver,
1215 } => backend.eigh_with_options(
1216 inputs[0],
1217 EighOptions {
1218 derivative_eps,
1219 gauge,
1220 driver,
1221 },
1222 ),
1223 LinalgOp::EighVals { driver, .. } => {
1224 Ok(vec![backend.eigh_values_with_driver(inputs[0], driver)?])
1225 }
1226 LinalgOp::Eig { .. } => backend.eig(inputs[0]),
1227 LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
1228 LinalgOp::TriangularSolve {
1229 left_side,
1230 lower,
1231 transpose_a,
1232 unit_diagonal,
1233 } => Ok(vec![backend.triangular_solve(
1234 inputs[0],
1235 inputs[1],
1236 left_side,
1237 lower,
1238 transpose_a,
1239 unit_diagonal,
1240 )?]),
1241 }
1242}
1243
1244fn signdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1252 packed_lu: &Tensor,
1253 parity: &Tensor,
1254 backend: &mut B,
1255) -> tenferro_tensor::Result<Tensor> {
1256 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1257 let sign_diag = backend.sign_read(TensorRead::from_tensor(&diag))?;
1258 let sign_u = backend.reduce_prod_read(TensorRead::from_tensor(&sign_diag), &[0])?;
1259 backend.mul_read(
1260 TensorRead::from_tensor(parity),
1261 TensorRead::from_tensor(&sign_u),
1262 )
1263}
1264
1265fn logabsdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1266 packed_lu: &Tensor,
1267 backend: &mut B,
1268) -> tenferro_tensor::Result<Tensor> {
1269 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1270 let abs = backend.abs_read(TensorRead::from_tensor(&diag))?;
1271 let log = backend.log_read(TensorRead::from_tensor(&abs))?;
1272 backend.reduce_sum_read(TensorRead::from_tensor(&log), &[0])
1273}
1274
1275pub(crate) fn apply_svd_gauge(
1276 gauge: SvdGauge,
1277 outputs: &mut [Tensor],
1278) -> tenferro_tensor::Result<()> {
1279 match gauge {
1280 SvdGauge::Raw => Ok(()),
1281 SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
1282 }
1283}
1284
1285fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
1286 if outputs.len() != 3 {
1287 return Err(Error::invalid_argument(
1288 "tenferro-linalg.svd",
1289 "outputs",
1290 format!(
1291 "canonical SVD gauge expected three outputs, got {}",
1292 outputs.len()
1293 ),
1294 ));
1295 }
1296
1297 let (u_slice, rest) = outputs.split_at_mut(1);
1298 let (singular_slice, vt_slice) = rest.split_at_mut(1);
1299 let u = &mut u_slice[0];
1300 let singular_values = &singular_slice[0];
1301 let vt = &mut vt_slice[0];
1302 let u_shape = u.shape().to_vec();
1303 let s_shape = singular_values.shape().to_vec();
1304 let vt_shape = vt.shape().to_vec();
1305 if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
1306 return Err(Error::invalid_argument(
1307 "tenferro-linalg.svd",
1308 "outputs",
1309 format!(
1310 "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1311 ),
1312 ));
1313 }
1314
1315 let m = u_shape[0];
1316 let k = u_shape[1];
1317 let n = vt_shape[1];
1318 if s_shape[0] != k
1319 || vt_shape[0] != k
1320 || u_shape[2..] != vt_shape[2..]
1321 || s_shape[1..] != u_shape[2..]
1322 {
1323 return Err(Error::invalid_argument(
1324 "tenferro-linalg.svd",
1325 "outputs",
1326 format!(
1327 "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1328 ),
1329 ));
1330 }
1331 let layout = canonical_svd_gauge_layout(m, k, n, &u_shape[2..])?;
1332
1333 match (u.dtype(), vt.dtype()) {
1334 (tenferro_tensor::DType::F64, tenferro_tensor::DType::F64) => {
1335 let (u, vt) = svd_gauge_pair_mut::<f64>(u, vt)?;
1336 canonicalize_svd_gauge_f64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1337 }
1338 (tenferro_tensor::DType::F32, tenferro_tensor::DType::F32) => {
1339 let (u, vt) = svd_gauge_pair_mut::<f32>(u, vt)?;
1340 canonicalize_svd_gauge_f32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1341 }
1342 (tenferro_tensor::DType::C64, tenferro_tensor::DType::C64) => {
1343 let (u, vt) = svd_gauge_pair_mut::<Complex64>(u, vt)?;
1344 canonicalize_svd_gauge_c64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1345 }
1346 (tenferro_tensor::DType::C32, tenferro_tensor::DType::C32) => {
1347 let (u, vt) = svd_gauge_pair_mut::<Complex32>(u, vt)?;
1348 canonicalize_svd_gauge_c32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1349 }
1350 (u_dtype, vt_dtype) => Err(Error::dtype_mismatch(
1351 "tenferro-linalg.svd",
1352 u_dtype,
1353 vt_dtype,
1354 )),
1355 }
1356}
1357
1358fn svd_gauge_pair_mut<'a, T: tenferro_tensor::TensorScalar>(
1360 u: &'a mut Tensor,
1361 vt: &'a mut Tensor,
1362) -> tenferro_tensor::Result<(
1363 &'a mut tenferro_tensor::TypedTensor<T>,
1364 &'a mut tenferro_tensor::TypedTensor<T>,
1365)> {
1366 let (u_dtype, vt_dtype) = (u.dtype(), vt.dtype());
1367 let u_t = u
1368 .as_typed_mut::<T>()
1369 .ok_or_else(|| Error::dtype_mismatch("tenferro-linalg.svd", u_dtype, vt_dtype))?;
1370 let vt_t = vt
1371 .as_typed_mut::<T>()
1372 .ok_or_else(|| Error::dtype_mismatch("tenferro-linalg.svd", u_dtype, vt_dtype))?;
1373 Ok((u_t, vt_t))
1374}
1375
1376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1377struct CanonicalSvdGaugeLayout {
1378 m: usize,
1379 k: usize,
1380 batch_count: usize,
1381 u_batch_len: usize,
1382 vt_batch_len: usize,
1383 u_len: usize,
1384 vt_len: usize,
1385}
1386
1387impl CanonicalSvdGaugeLayout {
1388 fn validate_storage(self, u_len: usize, vt_len: usize) -> tenferro_tensor::Result<()> {
1389 if u_len != self.u_len {
1390 return Err(Error::invalid_argument(
1391 "tenferro-linalg.svd",
1392 "U storage",
1393 format!(
1394 "canonical SVD gauge expected U storage length {}, got {u_len}",
1395 self.u_len
1396 ),
1397 ));
1398 }
1399 if vt_len != self.vt_len {
1400 return Err(Error::invalid_argument(
1401 "tenferro-linalg.svd",
1402 "VT storage",
1403 format!(
1404 "canonical SVD gauge expected VT storage length {}, got {vt_len}",
1405 self.vt_len
1406 ),
1407 ));
1408 }
1409 Ok(())
1410 }
1411}
1412
1413fn canonical_svd_gauge_layout(
1414 m: usize,
1415 k: usize,
1416 n: usize,
1417 batch_shape: &[usize],
1418) -> tenferro_tensor::Result<CanonicalSvdGaugeLayout> {
1419 let batch_count = tenferro_tensor::validate::checked_shape_product(
1420 "tenferro-linalg.svd",
1421 "canonical SVD batch",
1422 batch_shape,
1423 )?;
1424 let u_batch_len = tenferro_tensor::validate::checked_shape_product(
1425 "tenferro-linalg.svd",
1426 "canonical SVD U batch",
1427 &[m, k],
1428 )?;
1429 let vt_batch_len = tenferro_tensor::validate::checked_shape_product(
1430 "tenferro-linalg.svd",
1431 "canonical SVD VT batch",
1432 &[k, n],
1433 )?;
1434 let u_len = tenferro_tensor::validate::checked_shape_product(
1435 "tenferro-linalg.svd",
1436 "canonical SVD U storage",
1437 &[u_batch_len, batch_count],
1438 )?;
1439 let vt_len = tenferro_tensor::validate::checked_shape_product(
1440 "tenferro-linalg.svd",
1441 "canonical SVD VT storage",
1442 &[vt_batch_len, batch_count],
1443 )?;
1444 Ok(CanonicalSvdGaugeLayout {
1445 m,
1446 k,
1447 batch_count,
1448 u_batch_len,
1449 vt_batch_len,
1450 u_len,
1451 vt_len,
1452 })
1453}
1454
1455fn canonicalize_svd_gauge_f64(
1456 u: &mut [f64],
1457 vt: &mut [f64],
1458 layout: CanonicalSvdGaugeLayout,
1459) -> tenferro_tensor::Result<()> {
1460 layout.validate_storage(u.len(), vt.len())?;
1461 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1462 return Ok(());
1463 }
1464 for (u_batch, vt_batch) in u
1465 .chunks_exact_mut(layout.u_batch_len)
1466 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1467 {
1468 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1469 let pivot = max_abs_pivot_f64(u_column);
1470 let pivot_value = u_column[pivot];
1471 if pivot_value < 0.0 {
1472 for value in u_column {
1473 *value = -*value;
1474 }
1475 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1476 vt_column[col] = -vt_column[col];
1477 }
1478 }
1479 }
1480 }
1481 Ok(())
1482}
1483
1484fn canonicalize_svd_gauge_f32(
1485 u: &mut [f32],
1486 vt: &mut [f32],
1487 layout: CanonicalSvdGaugeLayout,
1488) -> tenferro_tensor::Result<()> {
1489 layout.validate_storage(u.len(), vt.len())?;
1490 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1491 return Ok(());
1492 }
1493 for (u_batch, vt_batch) in u
1494 .chunks_exact_mut(layout.u_batch_len)
1495 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1496 {
1497 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1498 let pivot = max_abs_pivot_f32(u_column);
1499 let pivot_value = u_column[pivot];
1500 if pivot_value < 0.0 {
1501 for value in u_column {
1502 *value = -*value;
1503 }
1504 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1505 vt_column[col] = -vt_column[col];
1506 }
1507 }
1508 }
1509 }
1510 Ok(())
1511}
1512
1513fn canonicalize_svd_gauge_c64(
1514 u: &mut [Complex64],
1515 vt: &mut [Complex64],
1516 layout: CanonicalSvdGaugeLayout,
1517) -> tenferro_tensor::Result<()> {
1518 layout.validate_storage(u.len(), vt.len())?;
1519 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1520 return Ok(());
1521 }
1522 for (u_batch, vt_batch) in u
1523 .chunks_exact_mut(layout.u_batch_len)
1524 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1525 {
1526 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1527 let pivot = max_abs_pivot_c64(u_column);
1528 let pivot_value = u_column[pivot];
1529 let pivot_norm = pivot_value.norm();
1530 if pivot_norm == 0.0 {
1531 continue;
1532 }
1533 let phase = pivot_value.conj() / pivot_norm;
1534 let vt_phase = phase.conj();
1535 for value in u_column {
1536 *value *= phase;
1537 }
1538 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1539 vt_column[col] *= vt_phase;
1540 }
1541 }
1542 }
1543 Ok(())
1544}
1545
1546fn canonicalize_svd_gauge_c32(
1547 u: &mut [Complex32],
1548 vt: &mut [Complex32],
1549 layout: CanonicalSvdGaugeLayout,
1550) -> tenferro_tensor::Result<()> {
1551 layout.validate_storage(u.len(), vt.len())?;
1552 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1553 return Ok(());
1554 }
1555 for (u_batch, vt_batch) in u
1556 .chunks_exact_mut(layout.u_batch_len)
1557 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1558 {
1559 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1560 let pivot = max_abs_pivot_c32(u_column);
1561 let pivot_value = u_column[pivot];
1562 let pivot_norm = pivot_value.norm();
1563 if pivot_norm == 0.0 {
1564 continue;
1565 }
1566 let phase = pivot_value.conj() / pivot_norm;
1567 let vt_phase = phase.conj();
1568 for value in u_column {
1569 *value *= phase;
1570 }
1571 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1572 vt_column[col] *= vt_phase;
1573 }
1574 }
1575 }
1576 Ok(())
1577}
1578
1579fn max_abs_pivot_f64(u_column: &[f64]) -> usize {
1580 let mut pivot = 0;
1581 let mut pivot_abs = u_column[0].abs();
1582 for (row, value) in u_column.iter().enumerate().skip(1) {
1583 let candidate_abs = value.abs();
1584 if candidate_abs > pivot_abs {
1585 pivot = row;
1586 pivot_abs = candidate_abs;
1587 }
1588 }
1589 pivot
1590}
1591
1592fn max_abs_pivot_f32(u_column: &[f32]) -> usize {
1593 let mut pivot = 0;
1594 let mut pivot_abs = u_column[0].abs();
1595 for (row, value) in u_column.iter().enumerate().skip(1) {
1596 let candidate_abs = value.abs();
1597 if candidate_abs > pivot_abs {
1598 pivot = row;
1599 pivot_abs = candidate_abs;
1600 }
1601 }
1602 pivot
1603}
1604
1605fn max_abs_pivot_c64(u_column: &[Complex64]) -> usize {
1606 let mut pivot = 0;
1607 let mut pivot_abs = u_column[0].norm_sqr();
1608 for (row, value) in u_column.iter().enumerate().skip(1) {
1609 let candidate_abs = value.norm_sqr();
1610 if candidate_abs > pivot_abs {
1611 pivot = row;
1612 pivot_abs = candidate_abs;
1613 }
1614 }
1615 pivot
1616}
1617
1618fn max_abs_pivot_c32(u_column: &[Complex32]) -> usize {
1619 let mut pivot = 0;
1620 let mut pivot_abs = u_column[0].norm_sqr();
1621 for (row, value) in u_column.iter().enumerate().skip(1) {
1622 let candidate_abs = value.norm_sqr();
1623 if candidate_abs > pivot_abs {
1624 pivot = row;
1625 pivot_abs = candidate_abs;
1626 }
1627 }
1628 pivot
1629}
1630
1631fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1632 if shape.len() < 2 {
1633 return Err(Error::rank_mismatch(op, 2, shape.len()));
1634 }
1635 Ok(())
1636}
1637
1638fn matrix_meta_parts<'a>(
1639 op: &'static str,
1640 shape: &'a [SymDim],
1641) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1642 require_matrix_meta(op, shape)?;
1643 Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1644}
1645
1646fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1647 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1648 let k = m.clone().min(n.clone());
1649 Ok(vec![
1650 (dtype, matrix_shape(m.clone(), m, batch)),
1651 (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1652 (dtype, matrix_shape(k, n, batch)),
1653 (dtype, batch.to_vec()),
1654 ])
1655}
1656
1657fn lu_factor_meta(
1658 dtype: DType,
1659 shape: &[SymDim],
1660) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1661 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1662 let k = m.min(n);
1663 Ok(vec![
1664 (dtype, shape.to_vec()),
1665 (DType::I32, vector_shape(k, batch)),
1666 (dtype, batch.to_vec()),
1667 ])
1668}
1669
1670fn signdet_from_lu_factor_meta(
1671 input_dtype: DType,
1672 input_shape: &[SymDim],
1673 packed_shape: &[SymDim],
1674 parity_shape: &[SymDim],
1675) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1676 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.signdet_from_lu_factor", input_shape)?;
1677 require_matrix_meta(
1678 "tenferro-linalg.signdet_from_lu_factor_packed",
1679 packed_shape,
1680 )?;
1681 if parity_shape.len() != batch.len() {
1682 return Err(Error::rank_mismatch(
1683 "tenferro-linalg.signdet_from_lu_factor_parity",
1684 batch.len(),
1685 parity_shape.len(),
1686 ));
1687 }
1688 Ok((input_dtype, batch.to_vec()))
1689}
1690
1691fn logabsdet_from_lu_factor_meta(
1692 input_dtype: DType,
1693 input_shape: &[SymDim],
1694 packed_shape: &[SymDim],
1695) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1696 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.logabsdet_from_lu_factor", input_shape)?;
1697 require_matrix_meta(
1698 "tenferro-linalg.logabsdet_from_lu_factor_packed",
1699 packed_shape,
1700 )?;
1701 Ok((singular_values_dtype(input_dtype), batch.to_vec()))
1702}
1703
1704fn full_piv_lu_meta(
1705 dtype: DType,
1706 shape: &[SymDim],
1707) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1708 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1709 Ok(vec![
1710 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1711 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1712 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1713 (dtype, matrix_shape(n.clone(), n, batch)),
1714 (singular_values_dtype(dtype), batch.to_vec()),
1715 ])
1716}
1717
1718fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1719 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1720 let k = m.clone().min(n.clone());
1721 Ok(vec![
1722 (dtype, matrix_shape(m, k.clone(), batch)),
1723 (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1724 (dtype, matrix_shape(k, n, batch)),
1725 ])
1726}
1727
1728fn svd_full_meta(
1729 dtype: DType,
1730 shape: &[SymDim],
1731) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1732 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_full", shape)?;
1733 let k = m.clone().min(n.clone());
1734 Ok(vec![
1735 (dtype, matrix_shape(m.clone(), m, batch)),
1736 (singular_values_dtype(dtype), vector_shape(k, batch)),
1737 (dtype, matrix_shape(n.clone(), n, batch)),
1738 ])
1739}
1740
1741fn svd_values_meta(
1742 dtype: DType,
1743 shape: &[SymDim],
1744) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1745 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1746 let k = m.min(n);
1747 Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1748}
1749
1750fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1751 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1752 let k = m.clone().min(n.clone());
1753 Ok(vec![
1754 (dtype, matrix_shape(m, k.clone(), batch)),
1755 (dtype, matrix_shape(k, n, batch)),
1756 ])
1757}
1758
1759fn rank_revealing_qr_meta(
1760 dtype: DType,
1761 shape: &[SymDim],
1762) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1763 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.rank_revealing_qr", shape)?;
1764 let k = m.clone().min(n.clone());
1765 Ok(vec![
1766 (dtype, matrix_shape(m, k.clone(), batch)),
1767 (dtype, matrix_shape(k, n.clone(), batch)),
1768 (DType::I64, vector_shape(n, batch)),
1769 (DType::I64, batch.to_vec()),
1770 ])
1771}
1772
1773fn require_householder_rank2(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1774 if shape.len() != 2 {
1775 return Err(Error::rank_mismatch(op, 2, shape.len()));
1776 }
1777 Ok(())
1778}
1779
1780fn householder_qr_factor_meta(
1781 dtype: DType,
1782 shape: &[SymDim],
1783) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1784 require_householder_rank2("tenferro-linalg.householder_qr", shape)?;
1785 let k = shape[0].clone().min(shape[1].clone());
1786 Ok(vec![(dtype, shape.to_vec()), (dtype, vec![k])])
1787}
1788
1789fn householder_qr_from_factors_meta(
1790 dtypes: &[DType],
1791 shapes: &[&[SymDim]],
1792) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1793 const OP: &str = "tenferro-linalg.householder_qr_from_factors";
1794 require_householder_rank2(OP, shapes[0])?;
1795 require_householder_rank2(OP, shapes[1])?;
1796 if dtypes[0] != dtypes[1] {
1797 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1798 }
1799 require_static_extent_equal(OP, "q.cols/r.rows", &shapes[0][1], &shapes[1][0])?;
1800 if let (Some(q_cols), Some(q_rows), Some(r_cols)) = (
1801 shapes[0][1].constant_value(),
1802 shapes[0][0].constant_value(),
1803 shapes[1][1].constant_value(),
1804 ) && q_cols > q_rows.min(r_cols)
1805 {
1806 return Err(Error::invalid_argument(
1807 OP,
1808 "shape",
1809 "Q column count exceeds min(Q rows, R columns)",
1810 ));
1811 }
1812 let m = shapes[0][0].clone();
1813 let n = shapes[1][1].clone();
1814 let k = m.clone().min(n.clone());
1815 Ok(vec![(dtypes[0], vec![m, n]), (dtypes[0], vec![k])])
1816}
1817
1818fn householder_qr_append_meta(
1819 dtypes: &[DType],
1820 shapes: &[&[SymDim]],
1821) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1822 const OP: &str = "tenferro-linalg.householder_qr_append";
1823 require_householder_state_meta(OP, dtypes, shapes[0], shapes[1])?;
1824 require_householder_rank2(OP, shapes[2])?;
1825 if dtypes[0] != dtypes[2] {
1826 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[2]));
1827 }
1828 require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[2][0])?;
1829 let m = shapes[0][0].clone();
1830 let width = shapes[0][1].clone() + shapes[2][1].clone();
1831 let k = m.clone().min(width.clone());
1832 Ok(vec![(dtypes[0], vec![m, width]), (dtypes[0], vec![k])])
1833}
1834
1835fn require_static_extent_equal(
1836 op: &'static str,
1837 field: &'static str,
1838 lhs: &SymDim,
1839 rhs: &SymDim,
1840) -> tenferro_tensor::Result<()> {
1841 if let (Some(lhs), Some(rhs)) = (lhs.constant_value(), rhs.constant_value())
1842 && lhs != rhs
1843 {
1844 return Err(Error::invalid_argument(
1845 op,
1846 field,
1847 format!("expected equal extents, got {lhs} and {rhs}"),
1848 ));
1849 }
1850 Ok(())
1851}
1852
1853fn require_householder_state_meta(
1854 op: &'static str,
1855 dtypes: &[DType],
1856 packed: &[SymDim],
1857 coeff: &[SymDim],
1858) -> tenferro_tensor::Result<()> {
1859 require_householder_rank2(op, packed)?;
1860 if coeff.len() != 1 {
1861 return Err(Error::rank_mismatch(op, 1, coeff.len()));
1862 }
1863 if dtypes[0] != dtypes[1] {
1864 return Err(Error::dtype_mismatch(op, dtypes[0], dtypes[1]));
1865 }
1866 let expected = packed[0].clone().min(packed[1].clone());
1867 require_static_extent_equal(op, "coeff", &coeff[0], &expected)
1868}
1869
1870fn householder_qr_r_meta(
1871 dtypes: &[DType],
1872 packed: &[SymDim],
1873 coeff: &[SymDim],
1874) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1875 require_householder_state_meta("tenferro-linalg.householder_qr_r", dtypes, packed, coeff)?;
1876 Ok((dtypes[0], vec![coeff[0].clone(), packed[1].clone()]))
1877}
1878
1879fn householder_qr_q_columns_meta(
1880 dtypes: &[DType],
1881 packed: &[SymDim],
1882 coeff: &[SymDim],
1883 start: usize,
1884 end: usize,
1885) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1886 require_householder_state_meta(
1887 "tenferro-linalg.householder_qr_q_columns",
1888 dtypes,
1889 packed,
1890 coeff,
1891 )?;
1892 if start > end {
1893 return Err(Error::invalid_argument(
1894 "tenferro-linalg.householder_qr_q_columns",
1895 "range",
1896 format!("invalid Q-column range {start}..{end}"),
1897 ));
1898 }
1899 if packed[0].constant_value().is_some_and(|rows| end > rows) {
1902 return Err(Error::invalid_argument(
1903 "tenferro-linalg.householder_qr_q_columns",
1904 "range",
1905 format!("Q-column range {start}..{end} exceeds full-Q width"),
1906 ));
1907 }
1908 Ok((
1909 dtypes[0],
1910 vec![packed[0].clone(), SymDim::from(end - start)],
1911 ))
1912}
1913
1914fn householder_qr_thin_q_meta(
1915 dtypes: &[DType],
1916 packed: &[SymDim],
1917 coeff: &[SymDim],
1918) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1919 require_householder_state_meta(
1920 "tenferro-linalg.householder_qr_thin_q",
1921 dtypes,
1922 packed,
1923 coeff,
1924 )?;
1925 Ok((dtypes[0], vec![packed[0].clone(), coeff[0].clone()]))
1926}
1927
1928fn householder_qr_split_config(
1929 cotangent: &[usize],
1930 left: &[usize],
1931 right_shape: &[usize],
1932 take_right: bool,
1933) -> tenferro_tensor::Result<tenferro_tensor::SliceConfig> {
1934 const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1935 for shape in [cotangent, left, right_shape] {
1936 if shape.len() != 2 {
1937 return Err(Error::rank_mismatch(OP, 2, shape.len()));
1938 }
1939 }
1940 let total_width = left[1]
1941 .checked_add(right_shape[1])
1942 .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1943 if cotangent[0] != left[0] || cotangent[0] != right_shape[0] || cotangent[1] != total_width {
1944 return Err(Error::invalid_argument(
1945 OP,
1946 "shape",
1947 "cotangent shape does not match appended factors",
1948 ));
1949 }
1950 let selected = if take_right { right_shape } else { left };
1951 let start = if take_right { left[1] } else { 0 };
1952 let end = start
1953 .checked_add(selected[1])
1954 .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1955 Ok(tenferro_tensor::SliceConfig {
1956 starts: vec![0, start],
1957 limits: vec![selected[0], end],
1958 strides: vec![1, 1],
1959 })
1960}
1961
1962fn householder_qr_append_tangent_meta(
1963 dtypes: &[DType],
1964 shapes: &[&[SymDim]],
1965) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1966 const OP: &str = "tenferro-linalg.householder_qr_append_tangent";
1967 for shape in shapes {
1968 require_householder_rank2(OP, shape)?;
1969 }
1970 if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1971 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1972 }
1973 require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[1][0])?;
1974 require_static_extent_equal(OP, "left tangent", &shapes[0][0], &shapes[2][0])?;
1975 require_static_extent_equal(OP, "right tangent", &shapes[1][0], &shapes[3][0])?;
1976 require_static_extent_equal(OP, "anchor rows", &shapes[2][0], &shapes[3][0])?;
1977 Ok((
1978 dtypes[0],
1979 vec![
1980 shapes[2][0].clone(),
1981 shapes[2][1].clone() + shapes[3][1].clone(),
1982 ],
1983 ))
1984}
1985
1986fn householder_qr_split_tangent_meta(
1987 dtypes: &[DType],
1988 shapes: &[&[SymDim]],
1989 right: bool,
1990) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1991 const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1992 for shape in shapes {
1993 require_householder_rank2(OP, shape)?;
1994 }
1995 if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1996 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1997 }
1998 require_static_extent_equal(OP, "left rows", &shapes[0][0], &shapes[1][0])?;
1999 require_static_extent_equal(OP, "right rows", &shapes[0][0], &shapes[2][0])?;
2000 let expected_width = shapes[1][1].clone() + shapes[2][1].clone();
2001 require_static_extent_equal(OP, "width", &shapes[0][1], &expected_width)?;
2002 let selected = if right { shapes[2] } else { shapes[1] };
2003 Ok((dtypes[0], selected.to_vec()))
2004}
2005
2006fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2007 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
2008 Ok(vec![
2009 (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
2010 (dtype, matrix_shape(n.clone(), n, batch)),
2011 ])
2012}
2013
2014fn eigh_values_meta(
2015 dtype: DType,
2016 shape: &[SymDim],
2017) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
2018 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
2019 Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
2020}
2021
2022fn eig_meta(
2023 input_dtype: DType,
2024 shape: &[SymDim],
2025) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2026 let dtype = eig_output_dtype(input_dtype);
2027 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
2028 Ok(vec![
2029 (dtype, vector_shape(n.clone(), batch)),
2030 (dtype, matrix_shape(n.clone(), n, batch)),
2031 ])
2032}
2033
2034fn eig_values_meta(
2035 input_dtype: DType,
2036 shape: &[SymDim],
2037) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
2038 let dtype = eig_output_dtype(input_dtype);
2039 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
2040 Ok((dtype, vector_shape(n, batch)))
2041}
2042
2043fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
2044 let mut shape = vec![rows, cols];
2045 shape.extend_from_slice(batch);
2046 shape
2047}
2048
2049fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
2050 let mut shape = vec![len];
2051 shape.extend_from_slice(batch);
2052 shape
2053}
2054
2055fn eig_output_dtype(dtype: DType) -> DType {
2056 match dtype {
2057 DType::F64 | DType::C64 => DType::C64,
2058 DType::F32 | DType::C32 => DType::C32,
2059 DType::I32 | DType::I64 | DType::Bool => DType::C64,
2060 DType::External(_) => unreachable!("linalg validates its input dtype first"),
2063 }
2064}
2065
2066fn singular_values_dtype(dtype: DType) -> DType {
2067 match dtype {
2068 DType::C64 => DType::F64,
2069 DType::C32 => DType::F32,
2070 DType::External(id) => {
2071 DType::External(id)
2074 }
2075 other => other,
2076 }
2077}
2078
2079fn promote_dtypes(dtypes: &[DType]) -> DType {
2080 dtypes
2081 .iter()
2082 .copied()
2083 .reduce(tenferro_tensor::validate::promote_dtype)
2084 .unwrap_or(DType::F64)
2085}
2086
2087fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
2088 let tag = match dtype {
2089 DType::F64 => 0,
2090 DType::F32 => 1,
2091 DType::I64 => 2,
2092 DType::C64 => 3,
2093 DType::C32 => 4,
2094 DType::I32 => 5,
2095 DType::Bool => 6,
2096 DType::External(id) => {
2097 let mut identity = std::collections::hash_map::DefaultHasher::new();
2103 Hash::hash(&id, &mut identity);
2104 hasher.write_u8(7);
2105 hasher.write_u64(identity.finish());
2106 return;
2107 }
2108 };
2109 hasher.write_u8(tag);
2110}
2111
2112fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
2113 let tag = match gauge {
2114 SvdGauge::Raw => 0,
2115 SvdGauge::CanonicalPivot => 1,
2116 };
2117 hasher.write_u8(tag);
2118}
2119
2120fn hash_svd_driver(hasher: &mut dyn Hasher, driver: SvdDriver) {
2121 let tag = match driver {
2122 SvdDriver::Auto => 0,
2123 SvdDriver::Gesvdj => 1,
2124 SvdDriver::Gesvd => 2,
2125 SvdDriver::Xgesvdp => 3,
2126 };
2127 hasher.write_u8(tag);
2128}
2129
2130fn hash_eigh_driver(hasher: &mut dyn Hasher, driver: EighDriver) {
2131 let tag = match driver {
2132 EighDriver::Auto => 0,
2133 EighDriver::Syevd => 1,
2134 EighDriver::Syevj => 2,
2135 };
2136 hasher.write_u8(tag);
2137}
2138
2139fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
2140 let tag = match gauge {
2141 EighGauge::Raw => 0,
2142 EighGauge::CanonicalPivot => 1,
2143 };
2144 hasher.write_u8(tag);
2145}
2146
2147fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
2148 let tag = match gauge {
2149 QrGauge::Raw => 0,
2150 QrGauge::PositiveDiagonal => 1,
2151 };
2152 hasher.write_u8(tag);
2153}