1use std::any::Any;
2use std::hash::Hasher;
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_cpu::with_cpu_exec_session;
7use tenferro_extension_macros::define_extension_runtime;
8use tenferro_ops::SymDim;
9use tenferro_runtime::extension::{ExtensionExecutionContext, ExtensionOp};
10use tenferro_tensor::{BackendSession, DType, Error, ErrorKind, Tensor, TensorBackend, TensorRead};
11
12#[cfg(feature = "cuda")]
13use tenferro_gpu::cuda::with_cuda_exec_session;
14
15use crate::backend::LinalgBackend;
16
17mod gauge;
18#[cfg(all(test, not(feature = "cuda")))]
19mod tests;
20
21pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
22
23pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
24
25pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum SvdGauge {
52 Raw,
54 CanonicalPivot,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum EighGauge {
71 Raw,
73 CanonicalPivot,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum QrGauge {
89 Raw,
91 PositiveDiagonal,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct SvdOptions {
110 pub gauge: SvdGauge,
112 pub derivative_eps: f64,
114}
115
116impl Default for SvdOptions {
117 fn default() -> Self {
118 Self {
119 gauge: SvdGauge::Raw,
120 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
121 }
122 }
123}
124
125impl SvdOptions {
126 pub fn gauge(mut self, gauge: SvdGauge) -> Self {
137 self.gauge = gauge;
138 self
139 }
140
141 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
152 self.derivative_eps = derivative_eps;
153 self
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct EighOptions {
169 pub gauge: EighGauge,
171 pub derivative_eps: f64,
173}
174
175impl Default for EighOptions {
176 fn default() -> Self {
177 Self {
178 gauge: EighGauge::Raw,
179 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
180 }
181 }
182}
183
184impl EighOptions {
185 pub fn gauge(mut self, gauge: EighGauge) -> Self {
196 self.gauge = gauge;
197 self
198 }
199
200 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
211 self.derivative_eps = derivative_eps;
212 self
213 }
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
227pub struct QrOptions {
228 pub gauge: QrGauge,
230}
231
232impl Default for QrOptions {
233 fn default() -> Self {
234 Self {
235 gauge: QrGauge::Raw,
236 }
237 }
238}
239
240impl QrOptions {
241 pub fn gauge(mut self, gauge: QrGauge) -> Self {
252 self.gauge = gauge;
253 self
254 }
255}
256
257pub(crate) fn validate_derivative_eps(
258 op: &'static str,
259 derivative_eps: f64,
260) -> tenferro_tensor::Result<()> {
261 if derivative_eps.is_finite() && derivative_eps > 0.0 {
262 Ok(())
263 } else {
264 Err(Error::invalid_argument(
265 op,
266 "derivative_eps",
267 format!("must be positive and finite, got {derivative_eps}"),
268 ))
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq)]
273#[doc(hidden)]
274pub(crate) enum LinalgOp {
275 Cholesky,
276 Lu,
277 LuFactor,
278 LuSolvePrepared {
279 transpose_a: bool,
280 conjugate_a: bool,
281 },
282 SignDetFromLuFactor,
283 LogAbsDetFromLuFactor,
284 FullPivLu,
285 FullPivLuSolve {
286 transpose_a: bool,
287 },
288 #[cfg_attr(not(feature = "autodiff"), allow(dead_code))]
293 Solve,
294 Svd {
295 derivative_eps: f64,
296 gauge: SvdGauge,
297 },
298 SvdFull,
302 SvdVals {
303 derivative_eps: f64,
304 },
305 Qr {
306 gauge: QrGauge,
307 },
308 Eigh {
309 derivative_eps: f64,
310 gauge: EighGauge,
311 },
312 EighVals {
313 derivative_eps: f64,
314 },
315 Eig {
316 input_dtype: DType,
317 },
318 EigVals {
319 input_dtype: DType,
320 },
321 TriangularSolve {
322 left_side: bool,
323 lower: bool,
324 transpose_a: bool,
325 unit_diagonal: bool,
326 },
327}
328
329impl LinalgOp {
330 fn output_count(self) -> usize {
331 match self {
332 Self::Cholesky
333 | Self::EighVals { .. }
334 | Self::EigVals { .. }
335 | Self::FullPivLuSolve { .. }
336 | Self::LogAbsDetFromLuFactor
337 | Self::LuSolvePrepared { .. }
338 | Self::SignDetFromLuFactor
339 | Self::Solve
340 | Self::SvdVals { .. }
341 | Self::TriangularSolve { .. } => 1,
342 Self::Svd { .. } | Self::SvdFull => 3,
343 Self::Qr { .. } | Self::Eigh { .. } | Self::Eig { .. } => 2,
344 Self::LuFactor => 3,
345 Self::Lu => 4,
346 Self::FullPivLu => 5,
347 }
348 }
349
350 fn input_count(self) -> usize {
351 match self {
352 Self::FullPivLuSolve { .. } | Self::Solve | Self::TriangularSolve { .. } => 2,
353 Self::LogAbsDetFromLuFactor => 2,
354 Self::SignDetFromLuFactor => 3,
355 Self::LuSolvePrepared { .. } => 4,
356 _ => 1,
357 }
358 }
359
360 fn tag(self) -> u8 {
361 match self {
362 Self::Cholesky => 0,
363 Self::Lu => 1,
364 Self::FullPivLu => 2,
365 Self::FullPivLuSolve { .. } => 3,
366 Self::Svd { .. } => 4,
367 Self::Qr { .. } => 5,
368 Self::Eigh { .. } => 6,
369 Self::Eig { .. } => 7,
370 Self::TriangularSolve { .. } => 9,
371 Self::LuFactor => 10,
372 Self::LuSolvePrepared { .. } => 11,
373 Self::SvdVals { .. } => 12,
374 Self::EighVals { .. } => 13,
375 Self::EigVals { .. } => 14,
376 Self::SvdFull => 15,
377 Self::LogAbsDetFromLuFactor => 16,
378 Self::SignDetFromLuFactor => 17,
379 Self::Solve => 18,
380 }
381 }
382}
383
384#[derive(Clone, Debug, PartialEq)]
385#[doc(hidden)]
386pub(crate) struct LinalgExtensionOp {
387 op: LinalgOp,
388}
389
390impl LinalgExtensionOp {
391 pub(crate) fn new(op: LinalgOp) -> Self {
392 Self { op }
393 }
394
395 pub(crate) fn op(&self) -> LinalgOp {
396 self.op
397 }
398}
399
400impl ExtensionOp for LinalgExtensionOp {
401 fn family_id(&self) -> &'static str {
402 LINALG_EXTENSION_FAMILY_ID
403 }
404
405 fn payload_hash(&self, hasher: &mut dyn Hasher) {
406 hasher.write_u8(self.op.tag());
407 match self.op {
408 LinalgOp::Svd {
409 derivative_eps,
410 gauge,
411 } => {
412 hasher.write_u64(derivative_eps.to_bits());
413 hash_svd_gauge(hasher, gauge);
414 }
415 LinalgOp::SvdVals { derivative_eps } | LinalgOp::EighVals { derivative_eps } => {
416 hasher.write_u64(derivative_eps.to_bits());
417 }
418 LinalgOp::Qr { gauge } => {
419 hash_qr_gauge(hasher, gauge);
420 }
421 LinalgOp::Eigh {
422 derivative_eps,
423 gauge,
424 } => {
425 hasher.write_u64(derivative_eps.to_bits());
426 hash_eigh_gauge(hasher, gauge);
427 }
428 LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
429 hash_dtype(hasher, input_dtype);
430 }
431 LinalgOp::FullPivLuSolve { transpose_a } => {
432 hasher.write_u8(u8::from(transpose_a));
433 }
434 LinalgOp::LuSolvePrepared {
435 transpose_a,
436 conjugate_a,
437 } => {
438 hasher.write_u8(u8::from(transpose_a));
439 hasher.write_u8(u8::from(conjugate_a));
440 }
441 LinalgOp::TriangularSolve {
442 left_side,
443 lower,
444 transpose_a,
445 unit_diagonal,
446 } => {
447 hasher.write_u8(u8::from(left_side));
448 hasher.write_u8(u8::from(lower));
449 hasher.write_u8(u8::from(transpose_a));
450 hasher.write_u8(u8::from(unit_diagonal));
451 }
452 LinalgOp::Cholesky
453 | LinalgOp::Lu
454 | LinalgOp::LuFactor
455 | LinalgOp::LogAbsDetFromLuFactor
456 | LinalgOp::SignDetFromLuFactor
457 | LinalgOp::FullPivLu
458 | LinalgOp::SvdFull
459 | LinalgOp::Solve => {}
460 }
461 }
462
463 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
464 other
465 .as_any()
466 .downcast_ref::<Self>()
467 .is_some_and(|that| self == that)
468 }
469
470 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
471 Arc::new(self.clone())
472 }
473
474 fn as_any(&self) -> &dyn Any {
475 self
476 }
477
478 fn input_count(&self) -> usize {
479 self.op.input_count()
480 }
481
482 fn output_count(&self) -> usize {
483 self.op.output_count()
484 }
485
486 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
487 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
488 }
489
490 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
491 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
492 }
493
494 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
495 match self.op {
496 LinalgOp::Svd { derivative_eps, .. } if live_outputs == [false, true, false] => {
497 Some(Arc::new(Self::new(LinalgOp::SvdVals { derivative_eps })))
498 }
499 LinalgOp::Eigh { derivative_eps, .. } if live_outputs == [true, false] => {
500 Some(Arc::new(Self::new(LinalgOp::EighVals { derivative_eps })))
501 }
502 LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
503 Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
504 }
505 _ => None,
506 }
507 }
508
509 fn infer_output_meta(
510 &self,
511 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
512 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
513 let input_dtypes = (0..self.input_count())
514 .map(|input| ctx.input_dtype(input))
515 .collect::<Result<Vec<_>, _>>()?;
516 let input_shapes = (0..self.input_count())
517 .map(|input| ctx.input_shape(input))
518 .collect::<Result<Vec<_>, _>>()?;
519 let metas = match self.op {
520 LinalgOp::Cholesky => {
521 require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
522 vec![(promote_dtypes(&input_dtypes), input_shapes[0].to_vec())]
523 }
524 LinalgOp::FullPivLuSolve { .. } => {
525 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
526 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
527 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
528 }
529 LinalgOp::Solve => {
530 require_matrix_meta("tenferro-linalg.solve", input_shapes[0])?;
531 require_matrix_meta("tenferro-linalg.solve", input_shapes[1])?;
532 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
533 }
534 LinalgOp::TriangularSolve { .. } => {
535 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
536 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
537 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
538 }
539 LinalgOp::LuSolvePrepared { .. } => {
540 require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
541 require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
542 vec![(
543 promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
544 input_shapes[3].to_vec(),
545 )]
546 }
547 LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
548 LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
549 LinalgOp::SignDetFromLuFactor => {
550 vec![signdet_from_lu_factor_meta(
551 input_dtypes[0],
552 input_shapes[0],
553 input_shapes[1],
554 input_shapes[2],
555 )?]
556 }
557 LinalgOp::LogAbsDetFromLuFactor => {
558 vec![logabsdet_from_lu_factor_meta(
559 input_dtypes[0],
560 input_shapes[0],
561 input_shapes[1],
562 )?]
563 }
564 LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
565 LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
566 LinalgOp::SvdFull => svd_full_meta(input_dtypes[0], input_shapes[0])?,
567 LinalgOp::SvdVals { .. } => {
568 vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
569 }
570 LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
571 LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
572 LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
573 LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
574 LinalgOp::EigVals { input_dtype } => {
575 vec![eig_values_meta(input_dtype, input_shapes[0])?]
576 }
577 };
578 Ok(metas)
579 }
580}
581
582pub(crate) fn execute_linalg_extension_reads<B: BackendSession + ?Sized>(
583 op: &LinalgExtensionOp,
584 inputs: &[TensorRead<'_>],
585 ctx: &mut ExtensionExecutionContext<'_, B>,
586) -> tenferro_tensor::Result<Vec<Tensor>> {
587 execute_linalg_extension_reads_on_session(op, inputs, ctx.backend_mut())
588}
589
590pub(crate) fn execute_linalg_extension_reads_owner<B: TensorBackend>(
591 op: &LinalgExtensionOp,
592 inputs: &[TensorRead<'_>],
593 ctx: &mut ExtensionExecutionContext<'_, B>,
594) -> tenferro_tensor::Result<Vec<Tensor>> {
595 let (backend, caches) = ctx.parts_mut();
596 backend.with_backend_session(|session| {
597 let mut session_ctx = ExtensionExecutionContext::new(session, caches);
598 execute_linalg_extension_reads(op, inputs, &mut session_ctx)
599 })
600}
601
602fn execute_linalg_extension_reads_on_session<B: BackendSession + ?Sized>(
603 op: &LinalgExtensionOp,
604 inputs: &[TensorRead<'_>],
605 session: &mut B,
606) -> tenferro_tensor::Result<Vec<Tensor>> {
607 if let Some(result) = with_cpu_exec_session(session, |session| {
608 execute_linalg_extension_reads_in_session(op, inputs, session)
609 }) {
610 return result;
611 }
612 #[cfg(feature = "cuda")]
613 if let Some(result) = with_cuda_exec_session(session, |session| {
614 execute_linalg_extension_reads_in_session(op, inputs, session)
615 }) {
616 return result;
617 }
618 Err(Error::unsupported(
619 "linalg_extension",
620 "selected backend session does not expose a linalg execution capability",
621 ))
622}
623
624fn execute_linalg_extension_reads_in_session<S: LinalgBackend>(
625 op: &LinalgExtensionOp,
626 inputs: &[TensorRead<'_>],
627 session: &mut S,
628) -> tenferro_tensor::Result<Vec<Tensor>> {
629 if op.op() == LinalgOp::Cholesky {
630 return Ok(vec![session.cholesky_read(inputs[0].clone())?]);
631 }
632 if let LinalgOp::TriangularSolve {
633 left_side,
634 lower,
635 transpose_a,
636 unit_diagonal,
637 } = op.op()
638 {
639 match session.triangular_solve_read(
640 inputs[0].clone(),
641 inputs[1].clone(),
642 left_side,
643 lower,
644 transpose_a,
645 unit_diagonal,
646 ) {
647 Ok(output) => return Ok(vec![output]),
648 Err(error) if error.kind() == ErrorKind::Unsupported => {}
649 Err(error) => return Err(error),
650 }
651 }
652
653 let materialized_inputs = inputs
656 .iter()
657 .cloned()
658 .map(|input| session.to_contiguous_read(input))
659 .collect::<tenferro_tensor::Result<Vec<_>>>()?;
660 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
661 execute_linalg(op.op(), &input_refs, session)
662}
663
664fn linalg_session_supported<B: BackendSession + 'static>(op: &LinalgExtensionOp) -> bool {
665 let type_id = std::any::TypeId::of::<B>();
671 if type_id == std::any::TypeId::of::<tenferro_cpu::CpuBackend>() {
672 return op.op() != LinalgOp::SvdFull;
678 }
679 #[cfg(feature = "cuda")]
680 {
681 if type_id == std::any::TypeId::of::<tenferro_gpu::cuda::CudaBackend>() {
682 return match op.op() {
683 LinalgOp::FullPivLu | LinalgOp::FullPivLuSolve { .. } => false,
685 LinalgOp::Eig { .. } | LinalgOp::EigVals { .. } => false,
686 LinalgOp::Solve => true,
691 LinalgOp::SvdFull => false,
694 LinalgOp::LuSolvePrepared {
696 transpose_a: false,
697 conjugate_a: true,
698 } => false,
699 _ => true,
700 };
701 }
702 }
703 false
704}
705
706fn execute_linalg_extension_in_session(
707 op: &LinalgExtensionOp,
708 session: &mut dyn BackendSession,
709 _extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
710 inputs: &[TensorRead<'_>],
711) -> tenferro_tensor::Result<Vec<Tensor>> {
712 execute_linalg_extension_reads_on_session(op, inputs, session)
716}
717
718define_extension_runtime! {
719 runtime = LinalgRuntime,
720 family_id = LINALG_EXTENSION_FAMILY_ID,
721 op_type = LinalgExtensionOp,
722 execute = execute_linalg_extension_reads_owner,
723 execute_reads = execute_linalg_extension_reads_owner,
724 execute_in_session = execute_linalg_extension_in_session,
725 session_supported = linalg_session_supported,
726 backend_bound = TensorBackend,
727}
728
729fn execute_linalg<B: LinalgBackend>(
730 op: LinalgOp,
731 inputs: &[&Tensor],
732 backend: &mut B,
733) -> tenferro_tensor::Result<Vec<Tensor>> {
734 match op {
735 LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
736 LinalgOp::Lu => backend.lu(inputs[0]),
737 LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
738 LinalgOp::SignDetFromLuFactor => Ok(vec![signdet_from_lu_factor(
739 inputs[0].dtype(),
740 inputs[1],
741 inputs[2],
742 backend,
743 )?]),
744 LinalgOp::LogAbsDetFromLuFactor => Ok(vec![logabsdet_from_lu_factor(inputs[1], backend)?]),
745 LinalgOp::LuSolvePrepared {
746 transpose_a,
747 conjugate_a,
748 } => Ok(vec![backend.lu_solve_prepared(
749 inputs[0],
750 inputs[1],
751 inputs[2],
752 inputs[3],
753 transpose_a,
754 conjugate_a,
755 )?]),
756 LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
757 LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
758 inputs[0],
759 inputs[1],
760 transpose_a,
761 )?]),
762 LinalgOp::Solve => Ok(vec![backend.solve(inputs[0], inputs[1])?]),
763 LinalgOp::Svd {
764 derivative_eps,
765 gauge,
766 } => backend.svd_with_options(
767 inputs[0],
768 SvdOptions {
769 derivative_eps,
770 gauge,
771 },
772 ),
773 LinalgOp::SvdFull => backend.svd_full(inputs[0]),
774 LinalgOp::SvdVals { .. } => Ok(vec![backend.svd_values(inputs[0])?]),
775 LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
776 LinalgOp::Eigh {
777 derivative_eps,
778 gauge,
779 } => backend.eigh_with_options(
780 inputs[0],
781 EighOptions {
782 derivative_eps,
783 gauge,
784 },
785 ),
786 LinalgOp::EighVals { .. } => Ok(vec![backend.eigh_values(inputs[0])?]),
787 LinalgOp::Eig { .. } => backend.eig(inputs[0]),
788 LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
789 LinalgOp::TriangularSolve {
790 left_side,
791 lower,
792 transpose_a,
793 unit_diagonal,
794 } => Ok(vec![backend.triangular_solve(
795 inputs[0],
796 inputs[1],
797 left_side,
798 lower,
799 transpose_a,
800 unit_diagonal,
801 )?]),
802 }
803}
804
805fn signdet_from_lu_factor<B: LinalgBackend + ?Sized>(
806 input_dtype: DType,
807 packed_lu: &Tensor,
808 parity: &Tensor,
809 backend: &mut B,
810) -> tenferro_tensor::Result<Tensor> {
811 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
812 let det_u = backend.reduce_prod_read(TensorRead::from_tensor(&diag), &[0])?;
813 let det = backend.mul_read(
814 TensorRead::from_tensor(parity),
815 TensorRead::from_tensor(&det_u),
816 )?;
817 if matches!(input_dtype, DType::C32 | DType::C64) {
818 let abs = backend.abs_read(TensorRead::from_tensor(&det))?;
819 let abs = backend.convert(&abs, input_dtype)?;
820 backend.div_read(TensorRead::from_tensor(&det), TensorRead::from_tensor(&abs))
821 } else {
822 backend.sign_read(TensorRead::from_tensor(&det))
823 }
824}
825
826fn logabsdet_from_lu_factor<B: LinalgBackend + ?Sized>(
827 packed_lu: &Tensor,
828 backend: &mut B,
829) -> tenferro_tensor::Result<Tensor> {
830 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
831 let abs = backend.abs_read(TensorRead::from_tensor(&diag))?;
832 let log = backend.log_read(TensorRead::from_tensor(&abs))?;
833 backend.reduce_sum_read(TensorRead::from_tensor(&log), &[0])
834}
835
836pub(crate) fn apply_svd_gauge(
837 gauge: SvdGauge,
838 outputs: &mut [Tensor],
839) -> tenferro_tensor::Result<()> {
840 match gauge {
841 SvdGauge::Raw => Ok(()),
842 SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
843 }
844}
845
846fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
847 if outputs.len() != 3 {
848 return Err(Error::invalid_argument(
849 "tenferro-linalg.svd",
850 "outputs",
851 format!(
852 "canonical SVD gauge expected three outputs, got {}",
853 outputs.len()
854 ),
855 ));
856 }
857
858 let (u_slice, rest) = outputs.split_at_mut(1);
859 let (singular_slice, vt_slice) = rest.split_at_mut(1);
860 let u = &mut u_slice[0];
861 let singular_values = &singular_slice[0];
862 let vt = &mut vt_slice[0];
863 let u_shape = u.shape().to_vec();
864 let s_shape = singular_values.shape().to_vec();
865 let vt_shape = vt.shape().to_vec();
866 if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
867 return Err(Error::invalid_argument(
868 "tenferro-linalg.svd",
869 "outputs",
870 format!(
871 "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
872 ),
873 ));
874 }
875
876 let m = u_shape[0];
877 let k = u_shape[1];
878 let n = vt_shape[1];
879 if s_shape[0] != k
880 || vt_shape[0] != k
881 || u_shape[2..] != vt_shape[2..]
882 || s_shape[1..] != u_shape[2..]
883 {
884 return Err(Error::invalid_argument(
885 "tenferro-linalg.svd",
886 "outputs",
887 format!(
888 "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
889 ),
890 ));
891 }
892 let layout = canonical_svd_gauge_layout(m, k, n, &u_shape[2..])?;
893
894 match (u, vt) {
895 (Tensor::F64(u), Tensor::F64(vt)) => {
896 canonicalize_svd_gauge_f64(u.host_data_mut()?, vt.host_data_mut()?, layout)
897 }
898 (Tensor::F32(u), Tensor::F32(vt)) => {
899 canonicalize_svd_gauge_f32(u.host_data_mut()?, vt.host_data_mut()?, layout)
900 }
901 (Tensor::C64(u), Tensor::C64(vt)) => {
902 canonicalize_svd_gauge_c64(u.host_data_mut()?, vt.host_data_mut()?, layout)
903 }
904 (Tensor::C32(u), Tensor::C32(vt)) => {
905 canonicalize_svd_gauge_c32(u.host_data_mut()?, vt.host_data_mut()?, layout)
906 }
907 (u, vt) => Err(Error::dtype_mismatch(
908 "tenferro-linalg.svd",
909 u.dtype(),
910 vt.dtype(),
911 )),
912 }
913}
914
915#[derive(Clone, Copy, Debug, PartialEq, Eq)]
916struct CanonicalSvdGaugeLayout {
917 m: usize,
918 k: usize,
919 batch_count: usize,
920 u_batch_len: usize,
921 vt_batch_len: usize,
922 u_len: usize,
923 vt_len: usize,
924}
925
926impl CanonicalSvdGaugeLayout {
927 fn validate_storage(self, u_len: usize, vt_len: usize) -> tenferro_tensor::Result<()> {
928 if u_len != self.u_len {
929 return Err(Error::invalid_argument(
930 "tenferro-linalg.svd",
931 "U storage",
932 format!(
933 "canonical SVD gauge expected U storage length {}, got {u_len}",
934 self.u_len
935 ),
936 ));
937 }
938 if vt_len != self.vt_len {
939 return Err(Error::invalid_argument(
940 "tenferro-linalg.svd",
941 "VT storage",
942 format!(
943 "canonical SVD gauge expected VT storage length {}, got {vt_len}",
944 self.vt_len
945 ),
946 ));
947 }
948 Ok(())
949 }
950}
951
952fn canonical_svd_gauge_layout(
953 m: usize,
954 k: usize,
955 n: usize,
956 batch_shape: &[usize],
957) -> tenferro_tensor::Result<CanonicalSvdGaugeLayout> {
958 let batch_count = tenferro_tensor::validate::checked_shape_product(
959 "tenferro-linalg.svd",
960 "canonical SVD batch",
961 batch_shape,
962 )?;
963 let u_batch_len = tenferro_tensor::validate::checked_shape_product(
964 "tenferro-linalg.svd",
965 "canonical SVD U batch",
966 &[m, k],
967 )?;
968 let vt_batch_len = tenferro_tensor::validate::checked_shape_product(
969 "tenferro-linalg.svd",
970 "canonical SVD VT batch",
971 &[k, n],
972 )?;
973 let u_len = tenferro_tensor::validate::checked_shape_product(
974 "tenferro-linalg.svd",
975 "canonical SVD U storage",
976 &[u_batch_len, batch_count],
977 )?;
978 let vt_len = tenferro_tensor::validate::checked_shape_product(
979 "tenferro-linalg.svd",
980 "canonical SVD VT storage",
981 &[vt_batch_len, batch_count],
982 )?;
983 Ok(CanonicalSvdGaugeLayout {
984 m,
985 k,
986 batch_count,
987 u_batch_len,
988 vt_batch_len,
989 u_len,
990 vt_len,
991 })
992}
993
994fn canonicalize_svd_gauge_f64(
995 u: &mut [f64],
996 vt: &mut [f64],
997 layout: CanonicalSvdGaugeLayout,
998) -> tenferro_tensor::Result<()> {
999 layout.validate_storage(u.len(), vt.len())?;
1000 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1001 return Ok(());
1002 }
1003 for (u_batch, vt_batch) in u
1004 .chunks_exact_mut(layout.u_batch_len)
1005 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1006 {
1007 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1008 let pivot = max_abs_pivot_f64(u_column);
1009 let pivot_value = u_column[pivot];
1010 if pivot_value < 0.0 {
1011 for value in u_column {
1012 *value = -*value;
1013 }
1014 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1015 vt_column[col] = -vt_column[col];
1016 }
1017 }
1018 }
1019 }
1020 Ok(())
1021}
1022
1023fn canonicalize_svd_gauge_f32(
1024 u: &mut [f32],
1025 vt: &mut [f32],
1026 layout: CanonicalSvdGaugeLayout,
1027) -> tenferro_tensor::Result<()> {
1028 layout.validate_storage(u.len(), vt.len())?;
1029 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1030 return Ok(());
1031 }
1032 for (u_batch, vt_batch) in u
1033 .chunks_exact_mut(layout.u_batch_len)
1034 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1035 {
1036 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1037 let pivot = max_abs_pivot_f32(u_column);
1038 let pivot_value = u_column[pivot];
1039 if pivot_value < 0.0 {
1040 for value in u_column {
1041 *value = -*value;
1042 }
1043 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1044 vt_column[col] = -vt_column[col];
1045 }
1046 }
1047 }
1048 }
1049 Ok(())
1050}
1051
1052fn canonicalize_svd_gauge_c64(
1053 u: &mut [Complex64],
1054 vt: &mut [Complex64],
1055 layout: CanonicalSvdGaugeLayout,
1056) -> tenferro_tensor::Result<()> {
1057 layout.validate_storage(u.len(), vt.len())?;
1058 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1059 return Ok(());
1060 }
1061 for (u_batch, vt_batch) in u
1062 .chunks_exact_mut(layout.u_batch_len)
1063 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1064 {
1065 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1066 let pivot = max_abs_pivot_c64(u_column);
1067 let pivot_value = u_column[pivot];
1068 let pivot_norm = pivot_value.norm();
1069 if pivot_norm == 0.0 {
1070 continue;
1071 }
1072 let phase = pivot_value.conj() / pivot_norm;
1073 let vt_phase = phase.conj();
1074 for value in u_column {
1075 *value *= phase;
1076 }
1077 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1078 vt_column[col] *= vt_phase;
1079 }
1080 }
1081 }
1082 Ok(())
1083}
1084
1085fn canonicalize_svd_gauge_c32(
1086 u: &mut [Complex32],
1087 vt: &mut [Complex32],
1088 layout: CanonicalSvdGaugeLayout,
1089) -> tenferro_tensor::Result<()> {
1090 layout.validate_storage(u.len(), vt.len())?;
1091 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1092 return Ok(());
1093 }
1094 for (u_batch, vt_batch) in u
1095 .chunks_exact_mut(layout.u_batch_len)
1096 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1097 {
1098 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1099 let pivot = max_abs_pivot_c32(u_column);
1100 let pivot_value = u_column[pivot];
1101 let pivot_norm = pivot_value.norm();
1102 if pivot_norm == 0.0 {
1103 continue;
1104 }
1105 let phase = pivot_value.conj() / pivot_norm;
1106 let vt_phase = phase.conj();
1107 for value in u_column {
1108 *value *= phase;
1109 }
1110 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1111 vt_column[col] *= vt_phase;
1112 }
1113 }
1114 }
1115 Ok(())
1116}
1117
1118fn max_abs_pivot_f64(u_column: &[f64]) -> usize {
1119 let mut pivot = 0;
1120 let mut pivot_abs = u_column[0].abs();
1121 for (row, value) in u_column.iter().enumerate().skip(1) {
1122 let candidate_abs = value.abs();
1123 if candidate_abs > pivot_abs {
1124 pivot = row;
1125 pivot_abs = candidate_abs;
1126 }
1127 }
1128 pivot
1129}
1130
1131fn max_abs_pivot_f32(u_column: &[f32]) -> usize {
1132 let mut pivot = 0;
1133 let mut pivot_abs = u_column[0].abs();
1134 for (row, value) in u_column.iter().enumerate().skip(1) {
1135 let candidate_abs = value.abs();
1136 if candidate_abs > pivot_abs {
1137 pivot = row;
1138 pivot_abs = candidate_abs;
1139 }
1140 }
1141 pivot
1142}
1143
1144fn max_abs_pivot_c64(u_column: &[Complex64]) -> usize {
1145 let mut pivot = 0;
1146 let mut pivot_abs = u_column[0].norm_sqr();
1147 for (row, value) in u_column.iter().enumerate().skip(1) {
1148 let candidate_abs = value.norm_sqr();
1149 if candidate_abs > pivot_abs {
1150 pivot = row;
1151 pivot_abs = candidate_abs;
1152 }
1153 }
1154 pivot
1155}
1156
1157fn max_abs_pivot_c32(u_column: &[Complex32]) -> usize {
1158 let mut pivot = 0;
1159 let mut pivot_abs = u_column[0].norm_sqr();
1160 for (row, value) in u_column.iter().enumerate().skip(1) {
1161 let candidate_abs = value.norm_sqr();
1162 if candidate_abs > pivot_abs {
1163 pivot = row;
1164 pivot_abs = candidate_abs;
1165 }
1166 }
1167 pivot
1168}
1169
1170fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1171 if shape.len() < 2 {
1172 return Err(Error::rank_mismatch(op, 2, shape.len()));
1173 }
1174 Ok(())
1175}
1176
1177fn matrix_meta_parts<'a>(
1178 op: &'static str,
1179 shape: &'a [SymDim],
1180) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1181 require_matrix_meta(op, shape)?;
1182 Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1183}
1184
1185fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1186 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1187 let k = m.clone().min(n.clone());
1188 Ok(vec![
1189 (dtype, matrix_shape(m.clone(), m, batch)),
1190 (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1191 (dtype, matrix_shape(k, n, batch)),
1192 (dtype, batch.to_vec()),
1193 ])
1194}
1195
1196fn lu_factor_meta(
1197 dtype: DType,
1198 shape: &[SymDim],
1199) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1200 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1201 let k = m.min(n);
1202 Ok(vec![
1203 (dtype, shape.to_vec()),
1204 (DType::I32, vector_shape(k, batch)),
1205 (dtype, batch.to_vec()),
1206 ])
1207}
1208
1209fn signdet_from_lu_factor_meta(
1210 input_dtype: DType,
1211 input_shape: &[SymDim],
1212 packed_shape: &[SymDim],
1213 parity_shape: &[SymDim],
1214) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1215 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.signdet_from_lu_factor", input_shape)?;
1216 require_matrix_meta(
1217 "tenferro-linalg.signdet_from_lu_factor_packed",
1218 packed_shape,
1219 )?;
1220 if parity_shape.len() != batch.len() {
1221 return Err(Error::rank_mismatch(
1222 "tenferro-linalg.signdet_from_lu_factor_parity",
1223 batch.len(),
1224 parity_shape.len(),
1225 ));
1226 }
1227 Ok((input_dtype, batch.to_vec()))
1228}
1229
1230fn logabsdet_from_lu_factor_meta(
1231 input_dtype: DType,
1232 input_shape: &[SymDim],
1233 packed_shape: &[SymDim],
1234) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1235 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.logabsdet_from_lu_factor", input_shape)?;
1236 require_matrix_meta(
1237 "tenferro-linalg.logabsdet_from_lu_factor_packed",
1238 packed_shape,
1239 )?;
1240 Ok((singular_values_dtype(input_dtype), batch.to_vec()))
1241}
1242
1243fn full_piv_lu_meta(
1244 dtype: DType,
1245 shape: &[SymDim],
1246) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1247 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1248 Ok(vec![
1249 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1250 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1251 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1252 (dtype, matrix_shape(n.clone(), n, batch)),
1253 (singular_values_dtype(dtype), batch.to_vec()),
1254 ])
1255}
1256
1257fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1258 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1259 let k = m.clone().min(n.clone());
1260 Ok(vec![
1261 (dtype, matrix_shape(m, k.clone(), batch)),
1262 (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1263 (dtype, matrix_shape(k, n, batch)),
1264 ])
1265}
1266
1267fn svd_full_meta(
1268 dtype: DType,
1269 shape: &[SymDim],
1270) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1271 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_full", shape)?;
1272 let k = m.clone().min(n.clone());
1273 Ok(vec![
1274 (dtype, matrix_shape(m.clone(), m, batch)),
1275 (singular_values_dtype(dtype), vector_shape(k, batch)),
1276 (dtype, matrix_shape(n.clone(), n, batch)),
1277 ])
1278}
1279
1280fn svd_values_meta(
1281 dtype: DType,
1282 shape: &[SymDim],
1283) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1284 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1285 let k = m.min(n);
1286 Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1287}
1288
1289fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1290 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1291 let k = m.clone().min(n.clone());
1292 Ok(vec![
1293 (dtype, matrix_shape(m, k.clone(), batch)),
1294 (dtype, matrix_shape(k, n, batch)),
1295 ])
1296}
1297
1298fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1299 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
1300 Ok(vec![
1301 (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
1302 (dtype, matrix_shape(n.clone(), n, batch)),
1303 ])
1304}
1305
1306fn eigh_values_meta(
1307 dtype: DType,
1308 shape: &[SymDim],
1309) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1310 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
1311 Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
1312}
1313
1314fn eig_meta(
1315 input_dtype: DType,
1316 shape: &[SymDim],
1317) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1318 let dtype = eig_output_dtype(input_dtype);
1319 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
1320 Ok(vec![
1321 (dtype, vector_shape(n.clone(), batch)),
1322 (dtype, matrix_shape(n.clone(), n, batch)),
1323 ])
1324}
1325
1326fn eig_values_meta(
1327 input_dtype: DType,
1328 shape: &[SymDim],
1329) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1330 let dtype = eig_output_dtype(input_dtype);
1331 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
1332 Ok((dtype, vector_shape(n, batch)))
1333}
1334
1335fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1336 let mut shape = vec![rows, cols];
1337 shape.extend_from_slice(batch);
1338 shape
1339}
1340
1341fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1342 let mut shape = vec![len];
1343 shape.extend_from_slice(batch);
1344 shape
1345}
1346
1347fn eig_output_dtype(dtype: DType) -> DType {
1348 match dtype {
1349 DType::F64 | DType::C64 => DType::C64,
1350 DType::F32 | DType::C32 => DType::C32,
1351 DType::I32 | DType::I64 | DType::Bool => DType::C64,
1352 }
1353}
1354
1355fn singular_values_dtype(dtype: DType) -> DType {
1356 match dtype {
1357 DType::C64 => DType::F64,
1358 DType::C32 => DType::F32,
1359 other => other,
1360 }
1361}
1362
1363fn promote_dtypes(dtypes: &[DType]) -> DType {
1364 dtypes
1365 .iter()
1366 .copied()
1367 .reduce(tenferro_tensor::validate::promote_dtype)
1368 .unwrap_or(DType::F64)
1369}
1370
1371fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
1372 let tag = match dtype {
1373 DType::F64 => 0,
1374 DType::F32 => 1,
1375 DType::I64 => 2,
1376 DType::C64 => 3,
1377 DType::C32 => 4,
1378 DType::I32 => 5,
1379 DType::Bool => 6,
1380 };
1381 hasher.write_u8(tag);
1382}
1383
1384fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
1385 let tag = match gauge {
1386 SvdGauge::Raw => 0,
1387 SvdGauge::CanonicalPivot => 1,
1388 };
1389 hasher.write_u8(tag);
1390}
1391
1392fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
1393 let tag = match gauge {
1394 EighGauge::Raw => 0,
1395 EighGauge::CanonicalPivot => 1,
1396 };
1397 hasher.write_u8(tag);
1398}
1399
1400fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
1401 let tag = match gauge {
1402 QrGauge::Raw => 0,
1403 QrGauge::PositiveDiagonal => 1,
1404 };
1405 hasher.write_u8(tag);
1406}