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;
16use crate::RankRevealingQrOptions;
17
18mod gauge;
19#[cfg(all(test, not(feature = "cuda")))]
20mod tests;
21
22pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
23
24pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
25
26pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum SvdGauge {
53 Raw,
55 CanonicalPivot,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum EighGauge {
72 Raw,
74 CanonicalPivot,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum QrGauge {
90 Raw,
92 PositiveDiagonal,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct SvdOptions {
111 pub gauge: SvdGauge,
113 pub derivative_eps: f64,
115}
116
117impl Default for SvdOptions {
118 fn default() -> Self {
119 Self {
120 gauge: SvdGauge::Raw,
121 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
122 }
123 }
124}
125
126impl SvdOptions {
127 pub fn gauge(mut self, gauge: SvdGauge) -> Self {
138 self.gauge = gauge;
139 self
140 }
141
142 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
153 self.derivative_eps = derivative_eps;
154 self
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq)]
169pub struct EighOptions {
170 pub gauge: EighGauge,
172 pub derivative_eps: f64,
174}
175
176impl Default for EighOptions {
177 fn default() -> Self {
178 Self {
179 gauge: EighGauge::Raw,
180 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
181 }
182 }
183}
184
185impl EighOptions {
186 pub fn gauge(mut self, gauge: EighGauge) -> Self {
197 self.gauge = gauge;
198 self
199 }
200
201 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
212 self.derivative_eps = derivative_eps;
213 self
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub struct QrOptions {
229 pub gauge: QrGauge,
231}
232
233impl Default for QrOptions {
234 fn default() -> Self {
235 Self {
236 gauge: QrGauge::Raw,
237 }
238 }
239}
240
241impl QrOptions {
242 pub fn gauge(mut self, gauge: QrGauge) -> Self {
253 self.gauge = gauge;
254 self
255 }
256}
257
258pub(crate) fn validate_derivative_eps(
259 op: &'static str,
260 derivative_eps: f64,
261) -> tenferro_tensor::Result<()> {
262 if derivative_eps.is_finite() && derivative_eps > 0.0 {
263 Ok(())
264 } else {
265 Err(Error::invalid_argument(
266 op,
267 "derivative_eps",
268 format!("must be positive and finite, got {derivative_eps}"),
269 ))
270 }
271}
272
273#[derive(Clone, Copy, Debug, PartialEq)]
274#[doc(hidden)]
275#[allow(dead_code)]
276pub(crate) enum LinalgOp {
277 Cholesky,
278 Lu,
279 LuFactor,
280 LuSolvePrepared {
281 transpose_a: bool,
282 conjugate_a: bool,
283 },
284 SignDetFromLuFactor,
285 LogAbsDetFromLuFactor,
286 FullPivLu,
287 FullPivLuSolve {
288 transpose_a: bool,
289 },
290 #[cfg_attr(not(feature = "autodiff"), allow(dead_code))]
295 Solve,
296 Svd {
297 derivative_eps: f64,
298 gauge: SvdGauge,
299 },
300 SvdFull,
304 SvdVals {
305 derivative_eps: f64,
306 },
307 Qr {
308 gauge: QrGauge,
309 },
310 RankRevealingQr {
311 gauge: QrGauge,
312 rtol: f64,
313 atol: f64,
314 },
315 HouseholderQrFactor,
316 HouseholderQrFromFactors,
317 HouseholderQrAppend,
318 HouseholderQrR {
319 gauge: QrGauge,
320 },
321 HouseholderQrQColumns {
322 start: usize,
323 end: usize,
324 gauge: QrGauge,
325 },
326 HouseholderQrThinQ {
328 gauge: QrGauge,
329 },
330 HouseholderQrAppendTangent,
332 HouseholderQrSplitTangent {
334 right: bool,
335 },
336 Eigh {
337 derivative_eps: f64,
338 gauge: EighGauge,
339 },
340 EighVals {
341 derivative_eps: f64,
342 },
343 Eig {
344 input_dtype: DType,
345 },
346 EigVals {
347 input_dtype: DType,
348 },
349 TriangularSolve {
350 left_side: bool,
351 lower: bool,
352 transpose_a: bool,
353 unit_diagonal: bool,
354 },
355}
356
357impl LinalgOp {
358 fn output_count(self) -> usize {
359 match self {
360 Self::Cholesky
361 | Self::EighVals { .. }
362 | Self::EigVals { .. }
363 | Self::FullPivLuSolve { .. }
364 | Self::LogAbsDetFromLuFactor
365 | Self::LuSolvePrepared { .. }
366 | Self::SignDetFromLuFactor
367 | Self::Solve
368 | Self::SvdVals { .. }
369 | Self::TriangularSolve { .. } => 1,
370 Self::Svd { .. } | Self::SvdFull => 3,
371 Self::RankRevealingQr { .. } | Self::Lu => 4,
372 Self::Qr { .. }
373 | Self::HouseholderQrFactor
374 | Self::HouseholderQrFromFactors
375 | Self::HouseholderQrAppend
376 | Self::Eigh { .. }
377 | Self::Eig { .. } => 2,
378 Self::HouseholderQrR { .. }
379 | Self::HouseholderQrQColumns { .. }
380 | Self::HouseholderQrThinQ { .. }
381 | Self::HouseholderQrAppendTangent
382 | Self::HouseholderQrSplitTangent { .. } => 1,
383 Self::LuFactor => 3,
384 Self::FullPivLu => 5,
385 }
386 }
387
388 fn input_count(self) -> usize {
389 match self {
390 Self::FullPivLuSolve { .. }
391 | Self::Solve
392 | Self::TriangularSolve { .. }
393 | Self::HouseholderQrFromFactors
394 | Self::HouseholderQrR { .. }
395 | Self::HouseholderQrQColumns { .. }
396 | Self::HouseholderQrThinQ { .. } => 2,
397 Self::LogAbsDetFromLuFactor => 2,
398 Self::SignDetFromLuFactor | Self::HouseholderQrAppend => 3,
399 Self::HouseholderQrAppendTangent => 4,
400 Self::HouseholderQrSplitTangent { .. } => 3,
401 Self::LuSolvePrepared { .. } => 4,
402 _ => 1,
403 }
404 }
405
406 fn tag(self) -> u8 {
407 match self {
408 Self::Cholesky => 0,
409 Self::Lu => 1,
410 Self::FullPivLu => 2,
411 Self::FullPivLuSolve { .. } => 3,
412 Self::Svd { .. } => 4,
413 Self::Qr { .. } => 5,
414 Self::Eigh { .. } => 6,
415 Self::Eig { .. } => 7,
416 Self::TriangularSolve { .. } => 9,
417 Self::LuFactor => 10,
418 Self::LuSolvePrepared { .. } => 11,
419 Self::SvdVals { .. } => 12,
420 Self::EighVals { .. } => 13,
421 Self::EigVals { .. } => 14,
422 Self::SvdFull => 15,
423 Self::LogAbsDetFromLuFactor => 16,
424 Self::SignDetFromLuFactor => 17,
425 Self::Solve => 18,
426 Self::HouseholderQrFactor => 19,
427 Self::HouseholderQrFromFactors => 20,
428 Self::HouseholderQrAppend => 21,
429 Self::HouseholderQrR { .. } => 22,
430 Self::HouseholderQrQColumns { .. } => 23,
431 Self::HouseholderQrThinQ { .. } => 24,
432 Self::HouseholderQrAppendTangent => 25,
433 Self::HouseholderQrSplitTangent { .. } => 26,
434 Self::RankRevealingQr { .. } => 27,
435 }
436 }
437}
438
439#[derive(Clone, Debug, PartialEq)]
440#[doc(hidden)]
441pub(crate) struct LinalgExtensionOp {
442 op: LinalgOp,
443}
444
445impl LinalgExtensionOp {
446 pub(crate) fn new(op: LinalgOp) -> Self {
447 Self { op }
448 }
449
450 pub(crate) fn op(&self) -> LinalgOp {
451 self.op
452 }
453}
454
455impl ExtensionOp for LinalgExtensionOp {
456 fn family_id(&self) -> &'static str {
457 LINALG_EXTENSION_FAMILY_ID
458 }
459
460 fn payload_hash(&self, hasher: &mut dyn Hasher) {
461 hasher.write_u8(self.op.tag());
462 match self.op {
463 LinalgOp::Svd {
464 derivative_eps,
465 gauge,
466 } => {
467 hasher.write_u64(derivative_eps.to_bits());
468 hash_svd_gauge(hasher, gauge);
469 }
470 LinalgOp::SvdVals { derivative_eps } | LinalgOp::EighVals { derivative_eps } => {
471 hasher.write_u64(derivative_eps.to_bits());
472 }
473 LinalgOp::Qr { gauge }
474 | LinalgOp::HouseholderQrR { gauge }
475 | LinalgOp::HouseholderQrThinQ { gauge } => {
476 hash_qr_gauge(hasher, gauge);
477 }
478 LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
479 hash_qr_gauge(hasher, gauge);
480 hasher.write_u64(rtol.to_bits());
481 hasher.write_u64(atol.to_bits());
482 }
483 LinalgOp::HouseholderQrQColumns { start, end, gauge } => {
484 hasher.write_usize(start);
485 hasher.write_usize(end);
486 hash_qr_gauge(hasher, gauge);
487 }
488 LinalgOp::Eigh {
489 derivative_eps,
490 gauge,
491 } => {
492 hasher.write_u64(derivative_eps.to_bits());
493 hash_eigh_gauge(hasher, gauge);
494 }
495 LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
496 hash_dtype(hasher, input_dtype);
497 }
498 LinalgOp::FullPivLuSolve { transpose_a }
499 | LinalgOp::HouseholderQrSplitTangent { right: transpose_a } => {
500 hasher.write_u8(u8::from(transpose_a));
501 }
502 LinalgOp::LuSolvePrepared {
503 transpose_a,
504 conjugate_a,
505 } => {
506 hasher.write_u8(u8::from(transpose_a));
507 hasher.write_u8(u8::from(conjugate_a));
508 }
509 LinalgOp::TriangularSolve {
510 left_side,
511 lower,
512 transpose_a,
513 unit_diagonal,
514 } => {
515 hasher.write_u8(u8::from(left_side));
516 hasher.write_u8(u8::from(lower));
517 hasher.write_u8(u8::from(transpose_a));
518 hasher.write_u8(u8::from(unit_diagonal));
519 }
520 LinalgOp::Cholesky
521 | LinalgOp::Lu
522 | LinalgOp::LuFactor
523 | LinalgOp::LogAbsDetFromLuFactor
524 | LinalgOp::SignDetFromLuFactor
525 | LinalgOp::FullPivLu
526 | LinalgOp::SvdFull
527 | LinalgOp::Solve
528 | LinalgOp::HouseholderQrFactor
529 | LinalgOp::HouseholderQrFromFactors
530 | LinalgOp::HouseholderQrAppend
531 | LinalgOp::HouseholderQrAppendTangent => {}
532 }
533 }
534
535 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
536 other
537 .as_any()
538 .downcast_ref::<Self>()
539 .is_some_and(|that| self == that)
540 }
541
542 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
543 Arc::new(self.clone())
544 }
545
546 fn as_any(&self) -> &dyn Any {
547 self
548 }
549
550 fn input_count(&self) -> usize {
551 self.op.input_count()
552 }
553
554 fn output_count(&self) -> usize {
555 self.op.output_count()
556 }
557
558 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
559 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
560 }
561
562 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
563 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
564 }
565
566 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
567 match self.op {
568 LinalgOp::Svd { derivative_eps, .. } if live_outputs == [false, true, false] => {
569 Some(Arc::new(Self::new(LinalgOp::SvdVals { derivative_eps })))
570 }
571 LinalgOp::Eigh { derivative_eps, .. } if live_outputs == [true, false] => {
572 Some(Arc::new(Self::new(LinalgOp::EighVals { derivative_eps })))
573 }
574 LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
575 Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
576 }
577 _ => None,
578 }
579 }
580
581 fn infer_output_meta(
582 &self,
583 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
584 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
585 let input_dtypes = (0..self.input_count())
586 .map(|input| ctx.input_dtype(input))
587 .collect::<Result<Vec<_>, _>>()?;
588 let input_shapes = (0..self.input_count())
589 .map(|input| ctx.input_shape(input))
590 .collect::<Result<Vec<_>, _>>()?;
591 let metas = match self.op {
592 LinalgOp::Cholesky => {
593 require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
594 vec![(promote_dtypes(&input_dtypes), input_shapes[0].to_vec())]
595 }
596 LinalgOp::FullPivLuSolve { .. } => {
597 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
598 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
599 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
600 }
601 LinalgOp::Solve => {
602 require_matrix_meta("tenferro-linalg.solve", input_shapes[0])?;
603 require_matrix_meta("tenferro-linalg.solve", input_shapes[1])?;
604 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
605 }
606 LinalgOp::TriangularSolve { .. } => {
607 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
608 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
609 vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
610 }
611 LinalgOp::LuSolvePrepared { .. } => {
612 require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
613 require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
614 vec![(
615 promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
616 input_shapes[3].to_vec(),
617 )]
618 }
619 LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
620 LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
621 LinalgOp::SignDetFromLuFactor => {
622 vec![signdet_from_lu_factor_meta(
623 input_dtypes[0],
624 input_shapes[0],
625 input_shapes[1],
626 input_shapes[2],
627 )?]
628 }
629 LinalgOp::LogAbsDetFromLuFactor => {
630 vec![logabsdet_from_lu_factor_meta(
631 input_dtypes[0],
632 input_shapes[0],
633 input_shapes[1],
634 )?]
635 }
636 LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
637 LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
638 LinalgOp::SvdFull => svd_full_meta(input_dtypes[0], input_shapes[0])?,
639 LinalgOp::SvdVals { .. } => {
640 vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
641 }
642 LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
643 LinalgOp::RankRevealingQr { .. } => {
644 rank_revealing_qr_meta(input_dtypes[0], input_shapes[0])?
645 }
646 LinalgOp::HouseholderQrFactor => {
647 householder_qr_factor_meta(input_dtypes[0], input_shapes[0])?
648 }
649 LinalgOp::HouseholderQrFromFactors => {
650 householder_qr_from_factors_meta(&input_dtypes, &input_shapes)?
651 }
652 LinalgOp::HouseholderQrAppend => {
653 householder_qr_append_meta(&input_dtypes, &input_shapes)?
654 }
655 LinalgOp::HouseholderQrR { .. } => vec![householder_qr_r_meta(
656 &input_dtypes,
657 input_shapes[0],
658 input_shapes[1],
659 )?],
660 LinalgOp::HouseholderQrQColumns { start, end, .. } => {
661 vec![householder_qr_q_columns_meta(
662 &input_dtypes,
663 input_shapes[0],
664 input_shapes[1],
665 start,
666 end,
667 )?]
668 }
669 LinalgOp::HouseholderQrThinQ { .. } => {
670 vec![householder_qr_thin_q_meta(
671 &input_dtypes,
672 input_shapes[0],
673 input_shapes[1],
674 )?]
675 }
676 LinalgOp::HouseholderQrAppendTangent => {
677 vec![householder_qr_append_tangent_meta(
678 &input_dtypes,
679 &input_shapes,
680 )?]
681 }
682 LinalgOp::HouseholderQrSplitTangent { right } => {
683 vec![householder_qr_split_tangent_meta(
684 &input_dtypes,
685 &input_shapes,
686 right,
687 )?]
688 }
689 LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
690 LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
691 LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
692 LinalgOp::EigVals { input_dtype } => {
693 vec![eig_values_meta(input_dtype, input_shapes[0])?]
694 }
695 };
696 Ok(metas)
697 }
698}
699
700pub(crate) fn execute_linalg_extension_reads<B: BackendSession + ?Sized>(
701 op: &LinalgExtensionOp,
702 inputs: &[TensorRead<'_>],
703 ctx: &mut ExtensionExecutionContext<'_, B>,
704) -> tenferro_tensor::Result<Vec<Tensor>> {
705 execute_linalg_extension_reads_on_session(op, inputs, ctx.backend_mut())
706}
707
708pub(crate) fn execute_linalg_extension_reads_owner<B: TensorBackend>(
709 op: &LinalgExtensionOp,
710 inputs: &[TensorRead<'_>],
711 ctx: &mut ExtensionExecutionContext<'_, B>,
712) -> tenferro_tensor::Result<Vec<Tensor>> {
713 let (backend, caches) = ctx.parts_mut();
714 backend.with_backend_session(|session| {
715 let mut session_ctx = ExtensionExecutionContext::new(session, caches);
716 execute_linalg_extension_reads(op, inputs, &mut session_ctx)
717 })
718}
719
720fn execute_linalg_extension_reads_on_session<B: BackendSession + ?Sized>(
721 op: &LinalgExtensionOp,
722 inputs: &[TensorRead<'_>],
723 session: &mut B,
724) -> tenferro_tensor::Result<Vec<Tensor>> {
725 if let Some(result) = with_cpu_exec_session(session, |session| {
726 execute_linalg_extension_reads_in_session(op, inputs, session)
727 }) {
728 return result;
729 }
730 #[cfg(feature = "cuda")]
731 if let Some(result) = with_cuda_exec_session(session, |session| {
732 execute_linalg_extension_reads_in_session(op, inputs, session)
733 }) {
734 return result;
735 }
736 Err(Error::unsupported(
737 "linalg_extension",
738 "selected backend session does not expose a linalg execution capability",
739 ))
740}
741
742fn execute_linalg_extension_reads_in_session<S: LinalgBackend>(
743 op: &LinalgExtensionOp,
744 inputs: &[TensorRead<'_>],
745 session: &mut S,
746) -> tenferro_tensor::Result<Vec<Tensor>> {
747 if op.op() == LinalgOp::HouseholderQrAppendTangent {
748 let left = session.to_contiguous_read(inputs[0].clone())?;
749 let right = session.to_contiguous_read(inputs[1].clone())?;
750 return Ok(vec![session.concatenate(&[&left, &right], 1)?]);
751 }
752 if let LinalgOp::HouseholderQrSplitTangent { right } = op.op() {
753 let cotangent_shape = inputs[0].clone().tensor_view().shape().to_vec();
754 let left_shape = inputs[1].clone().tensor_view().shape().to_vec();
755 let right_shape = inputs[2].clone().tensor_view().shape().to_vec();
756 let config =
757 householder_qr_split_config(&cotangent_shape, &left_shape, &right_shape, right)?;
758 let cotangent = session.to_contiguous_read(inputs[0].clone())?;
759 return Ok(vec![session.slice(&cotangent, &config)?]);
760 }
761 if op.op() == LinalgOp::Cholesky {
762 return Ok(vec![session.cholesky_read(inputs[0].clone())?]);
763 }
764 if let LinalgOp::TriangularSolve {
765 left_side,
766 lower,
767 transpose_a,
768 unit_diagonal,
769 } = op.op()
770 {
771 match session.triangular_solve_read(
772 inputs[0].clone(),
773 inputs[1].clone(),
774 left_side,
775 lower,
776 transpose_a,
777 unit_diagonal,
778 ) {
779 Ok(output) => return Ok(vec![output]),
780 Err(error) if error.kind() == ErrorKind::Unsupported => {}
781 Err(error) => return Err(error),
782 }
783 }
784
785 let materialized_inputs = inputs
788 .iter()
789 .cloned()
790 .map(|input| session.to_contiguous_read(input))
791 .collect::<tenferro_tensor::Result<Vec<_>>>()?;
792 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
793 execute_linalg(op.op(), &input_refs, session)
794}
795
796fn linalg_session_supported<B: BackendSession + 'static>(op: &LinalgExtensionOp) -> bool {
797 let type_id = std::any::TypeId::of::<B>();
803 if type_id == std::any::TypeId::of::<tenferro_cpu::CpuBackend>() {
804 return op.op() != LinalgOp::SvdFull;
810 }
811 #[cfg(feature = "cuda")]
812 {
813 if type_id == std::any::TypeId::of::<tenferro_gpu::cuda::CudaBackend>() {
814 return match op.op() {
815 LinalgOp::FullPivLu | LinalgOp::FullPivLuSolve { .. } => false,
817 LinalgOp::Eig { .. } | LinalgOp::EigVals { .. } => false,
818 LinalgOp::Solve => true,
823 LinalgOp::SvdFull => false,
826 LinalgOp::LuSolvePrepared {
828 transpose_a: false,
829 conjugate_a: true,
830 } => false,
831 _ => true,
832 };
833 }
834 }
835 false
836}
837
838fn execute_linalg_extension_in_session(
839 op: &LinalgExtensionOp,
840 session: &mut dyn BackendSession,
841 _extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
842 inputs: &[TensorRead<'_>],
843) -> tenferro_tensor::Result<Vec<Tensor>> {
844 execute_linalg_extension_reads_on_session(op, inputs, session)
848}
849
850define_extension_runtime! {
851 runtime = LinalgRuntime,
852 family_id = LINALG_EXTENSION_FAMILY_ID,
853 op_type = LinalgExtensionOp,
854 execute = execute_linalg_extension_reads_owner,
855 execute_reads = execute_linalg_extension_reads_owner,
856 execute_in_session = execute_linalg_extension_in_session,
857 session_supported = linalg_session_supported,
858 backend_bound = TensorBackend,
859}
860
861fn execute_linalg<B: LinalgBackend>(
862 op: LinalgOp,
863 inputs: &[&Tensor],
864 backend: &mut B,
865) -> tenferro_tensor::Result<Vec<Tensor>> {
866 match op {
867 LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
868 LinalgOp::Lu => backend.lu(inputs[0]),
869 LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
870 LinalgOp::SignDetFromLuFactor => {
871 Ok(vec![signdet_from_lu_factor(inputs[1], inputs[2], backend)?])
872 }
873 LinalgOp::LogAbsDetFromLuFactor => Ok(vec![logabsdet_from_lu_factor(inputs[1], backend)?]),
874 LinalgOp::LuSolvePrepared {
875 transpose_a,
876 conjugate_a,
877 } => Ok(vec![backend.lu_solve_prepared(
878 inputs[0],
879 inputs[1],
880 inputs[2],
881 inputs[3],
882 transpose_a,
883 conjugate_a,
884 )?]),
885 LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
886 LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
887 inputs[0],
888 inputs[1],
889 transpose_a,
890 )?]),
891 LinalgOp::Solve => Ok(vec![backend.solve(inputs[0], inputs[1])?]),
892 LinalgOp::Svd {
893 derivative_eps,
894 gauge,
895 } => backend.svd_with_options(
896 inputs[0],
897 SvdOptions {
898 derivative_eps,
899 gauge,
900 },
901 ),
902 LinalgOp::SvdFull => backend.svd_full(inputs[0]),
903 LinalgOp::SvdVals { .. } => Ok(vec![backend.svd_values(inputs[0])?]),
904 LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
905 LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
906 backend.rank_revealing_qr(inputs[0], RankRevealingQrOptions { gauge, rtol, atol })
907 }
908 LinalgOp::HouseholderQrFactor => {
909 let state = backend.householder_qr(inputs[0])?;
910 Ok(vec![state.packed, state.coeff])
911 }
912 LinalgOp::HouseholderQrFromFactors => {
913 let state = backend.householder_qr_from_factors(inputs[0], inputs[1])?;
914 Ok(vec![state.packed, state.coeff])
915 }
916 LinalgOp::HouseholderQrAppend => {
917 let state = backend.householder_qr_append(inputs[0], inputs[1], inputs[2])?;
918 Ok(vec![state.packed, state.coeff])
919 }
920 LinalgOp::HouseholderQrR { gauge } => Ok(vec![backend.householder_qr_r(
921 inputs[0],
922 inputs[1],
923 QrOptions { gauge },
924 )?]),
925 LinalgOp::HouseholderQrQColumns { start, end, gauge } => Ok(vec![backend
926 .householder_qr_q_columns(inputs[0], inputs[1], start..end, QrOptions { gauge })?]),
927 LinalgOp::HouseholderQrThinQ { gauge } => {
928 let end = inputs[1].shape().first().copied().ok_or_else(|| {
929 Error::rank_mismatch("tenferro-linalg.householder_qr_thin_q", 1, 0)
930 })?;
931 Ok(vec![backend.householder_qr_q_columns(
932 inputs[0],
933 inputs[1],
934 0..end,
935 QrOptions { gauge },
936 )?])
937 }
938 LinalgOp::HouseholderQrAppendTangent => {
939 Ok(vec![backend.concatenate(&[inputs[0], inputs[1]], 1)?])
940 }
941 LinalgOp::HouseholderQrSplitTangent { right } => {
942 let config = householder_qr_split_config(
943 inputs[0].shape(),
944 inputs[1].shape(),
945 inputs[2].shape(),
946 right,
947 )?;
948 Ok(vec![backend.slice(inputs[0], &config)?])
949 }
950 LinalgOp::Eigh {
951 derivative_eps,
952 gauge,
953 } => backend.eigh_with_options(
954 inputs[0],
955 EighOptions {
956 derivative_eps,
957 gauge,
958 },
959 ),
960 LinalgOp::EighVals { .. } => Ok(vec![backend.eigh_values(inputs[0])?]),
961 LinalgOp::Eig { .. } => backend.eig(inputs[0]),
962 LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
963 LinalgOp::TriangularSolve {
964 left_side,
965 lower,
966 transpose_a,
967 unit_diagonal,
968 } => Ok(vec![backend.triangular_solve(
969 inputs[0],
970 inputs[1],
971 left_side,
972 lower,
973 transpose_a,
974 unit_diagonal,
975 )?]),
976 }
977}
978
979fn signdet_from_lu_factor<B: LinalgBackend + ?Sized>(
987 packed_lu: &Tensor,
988 parity: &Tensor,
989 backend: &mut B,
990) -> tenferro_tensor::Result<Tensor> {
991 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
992 let sign_diag = backend.sign_read(TensorRead::from_tensor(&diag))?;
993 let sign_u = backend.reduce_prod_read(TensorRead::from_tensor(&sign_diag), &[0])?;
994 backend.mul_read(
995 TensorRead::from_tensor(parity),
996 TensorRead::from_tensor(&sign_u),
997 )
998}
999
1000fn logabsdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1001 packed_lu: &Tensor,
1002 backend: &mut B,
1003) -> tenferro_tensor::Result<Tensor> {
1004 let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1005 let abs = backend.abs_read(TensorRead::from_tensor(&diag))?;
1006 let log = backend.log_read(TensorRead::from_tensor(&abs))?;
1007 backend.reduce_sum_read(TensorRead::from_tensor(&log), &[0])
1008}
1009
1010pub(crate) fn apply_svd_gauge(
1011 gauge: SvdGauge,
1012 outputs: &mut [Tensor],
1013) -> tenferro_tensor::Result<()> {
1014 match gauge {
1015 SvdGauge::Raw => Ok(()),
1016 SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
1017 }
1018}
1019
1020fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
1021 if outputs.len() != 3 {
1022 return Err(Error::invalid_argument(
1023 "tenferro-linalg.svd",
1024 "outputs",
1025 format!(
1026 "canonical SVD gauge expected three outputs, got {}",
1027 outputs.len()
1028 ),
1029 ));
1030 }
1031
1032 let (u_slice, rest) = outputs.split_at_mut(1);
1033 let (singular_slice, vt_slice) = rest.split_at_mut(1);
1034 let u = &mut u_slice[0];
1035 let singular_values = &singular_slice[0];
1036 let vt = &mut vt_slice[0];
1037 let u_shape = u.shape().to_vec();
1038 let s_shape = singular_values.shape().to_vec();
1039 let vt_shape = vt.shape().to_vec();
1040 if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
1041 return Err(Error::invalid_argument(
1042 "tenferro-linalg.svd",
1043 "outputs",
1044 format!(
1045 "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1046 ),
1047 ));
1048 }
1049
1050 let m = u_shape[0];
1051 let k = u_shape[1];
1052 let n = vt_shape[1];
1053 if s_shape[0] != k
1054 || vt_shape[0] != k
1055 || u_shape[2..] != vt_shape[2..]
1056 || s_shape[1..] != u_shape[2..]
1057 {
1058 return Err(Error::invalid_argument(
1059 "tenferro-linalg.svd",
1060 "outputs",
1061 format!(
1062 "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1063 ),
1064 ));
1065 }
1066 let layout = canonical_svd_gauge_layout(m, k, n, &u_shape[2..])?;
1067
1068 match (u, vt) {
1069 (Tensor::F64(u), Tensor::F64(vt)) => {
1070 canonicalize_svd_gauge_f64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1071 }
1072 (Tensor::F32(u), Tensor::F32(vt)) => {
1073 canonicalize_svd_gauge_f32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1074 }
1075 (Tensor::C64(u), Tensor::C64(vt)) => {
1076 canonicalize_svd_gauge_c64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1077 }
1078 (Tensor::C32(u), Tensor::C32(vt)) => {
1079 canonicalize_svd_gauge_c32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1080 }
1081 (u, vt) => Err(Error::dtype_mismatch(
1082 "tenferro-linalg.svd",
1083 u.dtype(),
1084 vt.dtype(),
1085 )),
1086 }
1087}
1088
1089#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1090struct CanonicalSvdGaugeLayout {
1091 m: usize,
1092 k: usize,
1093 batch_count: usize,
1094 u_batch_len: usize,
1095 vt_batch_len: usize,
1096 u_len: usize,
1097 vt_len: usize,
1098}
1099
1100impl CanonicalSvdGaugeLayout {
1101 fn validate_storage(self, u_len: usize, vt_len: usize) -> tenferro_tensor::Result<()> {
1102 if u_len != self.u_len {
1103 return Err(Error::invalid_argument(
1104 "tenferro-linalg.svd",
1105 "U storage",
1106 format!(
1107 "canonical SVD gauge expected U storage length {}, got {u_len}",
1108 self.u_len
1109 ),
1110 ));
1111 }
1112 if vt_len != self.vt_len {
1113 return Err(Error::invalid_argument(
1114 "tenferro-linalg.svd",
1115 "VT storage",
1116 format!(
1117 "canonical SVD gauge expected VT storage length {}, got {vt_len}",
1118 self.vt_len
1119 ),
1120 ));
1121 }
1122 Ok(())
1123 }
1124}
1125
1126fn canonical_svd_gauge_layout(
1127 m: usize,
1128 k: usize,
1129 n: usize,
1130 batch_shape: &[usize],
1131) -> tenferro_tensor::Result<CanonicalSvdGaugeLayout> {
1132 let batch_count = tenferro_tensor::validate::checked_shape_product(
1133 "tenferro-linalg.svd",
1134 "canonical SVD batch",
1135 batch_shape,
1136 )?;
1137 let u_batch_len = tenferro_tensor::validate::checked_shape_product(
1138 "tenferro-linalg.svd",
1139 "canonical SVD U batch",
1140 &[m, k],
1141 )?;
1142 let vt_batch_len = tenferro_tensor::validate::checked_shape_product(
1143 "tenferro-linalg.svd",
1144 "canonical SVD VT batch",
1145 &[k, n],
1146 )?;
1147 let u_len = tenferro_tensor::validate::checked_shape_product(
1148 "tenferro-linalg.svd",
1149 "canonical SVD U storage",
1150 &[u_batch_len, batch_count],
1151 )?;
1152 let vt_len = tenferro_tensor::validate::checked_shape_product(
1153 "tenferro-linalg.svd",
1154 "canonical SVD VT storage",
1155 &[vt_batch_len, batch_count],
1156 )?;
1157 Ok(CanonicalSvdGaugeLayout {
1158 m,
1159 k,
1160 batch_count,
1161 u_batch_len,
1162 vt_batch_len,
1163 u_len,
1164 vt_len,
1165 })
1166}
1167
1168fn canonicalize_svd_gauge_f64(
1169 u: &mut [f64],
1170 vt: &mut [f64],
1171 layout: CanonicalSvdGaugeLayout,
1172) -> tenferro_tensor::Result<()> {
1173 layout.validate_storage(u.len(), vt.len())?;
1174 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1175 return Ok(());
1176 }
1177 for (u_batch, vt_batch) in u
1178 .chunks_exact_mut(layout.u_batch_len)
1179 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1180 {
1181 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1182 let pivot = max_abs_pivot_f64(u_column);
1183 let pivot_value = u_column[pivot];
1184 if pivot_value < 0.0 {
1185 for value in u_column {
1186 *value = -*value;
1187 }
1188 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1189 vt_column[col] = -vt_column[col];
1190 }
1191 }
1192 }
1193 }
1194 Ok(())
1195}
1196
1197fn canonicalize_svd_gauge_f32(
1198 u: &mut [f32],
1199 vt: &mut [f32],
1200 layout: CanonicalSvdGaugeLayout,
1201) -> tenferro_tensor::Result<()> {
1202 layout.validate_storage(u.len(), vt.len())?;
1203 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1204 return Ok(());
1205 }
1206 for (u_batch, vt_batch) in u
1207 .chunks_exact_mut(layout.u_batch_len)
1208 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1209 {
1210 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1211 let pivot = max_abs_pivot_f32(u_column);
1212 let pivot_value = u_column[pivot];
1213 if pivot_value < 0.0 {
1214 for value in u_column {
1215 *value = -*value;
1216 }
1217 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1218 vt_column[col] = -vt_column[col];
1219 }
1220 }
1221 }
1222 }
1223 Ok(())
1224}
1225
1226fn canonicalize_svd_gauge_c64(
1227 u: &mut [Complex64],
1228 vt: &mut [Complex64],
1229 layout: CanonicalSvdGaugeLayout,
1230) -> tenferro_tensor::Result<()> {
1231 layout.validate_storage(u.len(), vt.len())?;
1232 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1233 return Ok(());
1234 }
1235 for (u_batch, vt_batch) in u
1236 .chunks_exact_mut(layout.u_batch_len)
1237 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1238 {
1239 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1240 let pivot = max_abs_pivot_c64(u_column);
1241 let pivot_value = u_column[pivot];
1242 let pivot_norm = pivot_value.norm();
1243 if pivot_norm == 0.0 {
1244 continue;
1245 }
1246 let phase = pivot_value.conj() / pivot_norm;
1247 let vt_phase = phase.conj();
1248 for value in u_column {
1249 *value *= phase;
1250 }
1251 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1252 vt_column[col] *= vt_phase;
1253 }
1254 }
1255 }
1256 Ok(())
1257}
1258
1259fn canonicalize_svd_gauge_c32(
1260 u: &mut [Complex32],
1261 vt: &mut [Complex32],
1262 layout: CanonicalSvdGaugeLayout,
1263) -> tenferro_tensor::Result<()> {
1264 layout.validate_storage(u.len(), vt.len())?;
1265 if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1266 return Ok(());
1267 }
1268 for (u_batch, vt_batch) in u
1269 .chunks_exact_mut(layout.u_batch_len)
1270 .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1271 {
1272 for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1273 let pivot = max_abs_pivot_c32(u_column);
1274 let pivot_value = u_column[pivot];
1275 let pivot_norm = pivot_value.norm();
1276 if pivot_norm == 0.0 {
1277 continue;
1278 }
1279 let phase = pivot_value.conj() / pivot_norm;
1280 let vt_phase = phase.conj();
1281 for value in u_column {
1282 *value *= phase;
1283 }
1284 for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1285 vt_column[col] *= vt_phase;
1286 }
1287 }
1288 }
1289 Ok(())
1290}
1291
1292fn max_abs_pivot_f64(u_column: &[f64]) -> usize {
1293 let mut pivot = 0;
1294 let mut pivot_abs = u_column[0].abs();
1295 for (row, value) in u_column.iter().enumerate().skip(1) {
1296 let candidate_abs = value.abs();
1297 if candidate_abs > pivot_abs {
1298 pivot = row;
1299 pivot_abs = candidate_abs;
1300 }
1301 }
1302 pivot
1303}
1304
1305fn max_abs_pivot_f32(u_column: &[f32]) -> usize {
1306 let mut pivot = 0;
1307 let mut pivot_abs = u_column[0].abs();
1308 for (row, value) in u_column.iter().enumerate().skip(1) {
1309 let candidate_abs = value.abs();
1310 if candidate_abs > pivot_abs {
1311 pivot = row;
1312 pivot_abs = candidate_abs;
1313 }
1314 }
1315 pivot
1316}
1317
1318fn max_abs_pivot_c64(u_column: &[Complex64]) -> usize {
1319 let mut pivot = 0;
1320 let mut pivot_abs = u_column[0].norm_sqr();
1321 for (row, value) in u_column.iter().enumerate().skip(1) {
1322 let candidate_abs = value.norm_sqr();
1323 if candidate_abs > pivot_abs {
1324 pivot = row;
1325 pivot_abs = candidate_abs;
1326 }
1327 }
1328 pivot
1329}
1330
1331fn max_abs_pivot_c32(u_column: &[Complex32]) -> usize {
1332 let mut pivot = 0;
1333 let mut pivot_abs = u_column[0].norm_sqr();
1334 for (row, value) in u_column.iter().enumerate().skip(1) {
1335 let candidate_abs = value.norm_sqr();
1336 if candidate_abs > pivot_abs {
1337 pivot = row;
1338 pivot_abs = candidate_abs;
1339 }
1340 }
1341 pivot
1342}
1343
1344fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1345 if shape.len() < 2 {
1346 return Err(Error::rank_mismatch(op, 2, shape.len()));
1347 }
1348 Ok(())
1349}
1350
1351fn matrix_meta_parts<'a>(
1352 op: &'static str,
1353 shape: &'a [SymDim],
1354) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1355 require_matrix_meta(op, shape)?;
1356 Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1357}
1358
1359fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1360 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1361 let k = m.clone().min(n.clone());
1362 Ok(vec![
1363 (dtype, matrix_shape(m.clone(), m, batch)),
1364 (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1365 (dtype, matrix_shape(k, n, batch)),
1366 (dtype, batch.to_vec()),
1367 ])
1368}
1369
1370fn lu_factor_meta(
1371 dtype: DType,
1372 shape: &[SymDim],
1373) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1374 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1375 let k = m.min(n);
1376 Ok(vec![
1377 (dtype, shape.to_vec()),
1378 (DType::I32, vector_shape(k, batch)),
1379 (dtype, batch.to_vec()),
1380 ])
1381}
1382
1383fn signdet_from_lu_factor_meta(
1384 input_dtype: DType,
1385 input_shape: &[SymDim],
1386 packed_shape: &[SymDim],
1387 parity_shape: &[SymDim],
1388) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1389 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.signdet_from_lu_factor", input_shape)?;
1390 require_matrix_meta(
1391 "tenferro-linalg.signdet_from_lu_factor_packed",
1392 packed_shape,
1393 )?;
1394 if parity_shape.len() != batch.len() {
1395 return Err(Error::rank_mismatch(
1396 "tenferro-linalg.signdet_from_lu_factor_parity",
1397 batch.len(),
1398 parity_shape.len(),
1399 ));
1400 }
1401 Ok((input_dtype, batch.to_vec()))
1402}
1403
1404fn logabsdet_from_lu_factor_meta(
1405 input_dtype: DType,
1406 input_shape: &[SymDim],
1407 packed_shape: &[SymDim],
1408) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1409 let (_, _, batch) = matrix_meta_parts("tenferro-linalg.logabsdet_from_lu_factor", input_shape)?;
1410 require_matrix_meta(
1411 "tenferro-linalg.logabsdet_from_lu_factor_packed",
1412 packed_shape,
1413 )?;
1414 Ok((singular_values_dtype(input_dtype), batch.to_vec()))
1415}
1416
1417fn full_piv_lu_meta(
1418 dtype: DType,
1419 shape: &[SymDim],
1420) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1421 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1422 Ok(vec![
1423 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1424 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1425 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1426 (dtype, matrix_shape(n.clone(), n, batch)),
1427 (singular_values_dtype(dtype), batch.to_vec()),
1428 ])
1429}
1430
1431fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1432 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1433 let k = m.clone().min(n.clone());
1434 Ok(vec![
1435 (dtype, matrix_shape(m, k.clone(), batch)),
1436 (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1437 (dtype, matrix_shape(k, n, batch)),
1438 ])
1439}
1440
1441fn svd_full_meta(
1442 dtype: DType,
1443 shape: &[SymDim],
1444) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1445 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_full", shape)?;
1446 let k = m.clone().min(n.clone());
1447 Ok(vec![
1448 (dtype, matrix_shape(m.clone(), m, batch)),
1449 (singular_values_dtype(dtype), vector_shape(k, batch)),
1450 (dtype, matrix_shape(n.clone(), n, batch)),
1451 ])
1452}
1453
1454fn svd_values_meta(
1455 dtype: DType,
1456 shape: &[SymDim],
1457) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1458 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1459 let k = m.min(n);
1460 Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1461}
1462
1463fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1464 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1465 let k = m.clone().min(n.clone());
1466 Ok(vec![
1467 (dtype, matrix_shape(m, k.clone(), batch)),
1468 (dtype, matrix_shape(k, n, batch)),
1469 ])
1470}
1471
1472fn rank_revealing_qr_meta(
1473 dtype: DType,
1474 shape: &[SymDim],
1475) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1476 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.rank_revealing_qr", shape)?;
1477 let k = m.clone().min(n.clone());
1478 Ok(vec![
1479 (dtype, matrix_shape(m, k.clone(), batch)),
1480 (dtype, matrix_shape(k, n.clone(), batch)),
1481 (DType::I64, vector_shape(n, batch)),
1482 (DType::I64, batch.to_vec()),
1483 ])
1484}
1485
1486fn require_householder_rank2(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1487 if shape.len() != 2 {
1488 return Err(Error::rank_mismatch(op, 2, shape.len()));
1489 }
1490 Ok(())
1491}
1492
1493fn householder_qr_factor_meta(
1494 dtype: DType,
1495 shape: &[SymDim],
1496) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1497 require_householder_rank2("tenferro-linalg.householder_qr", shape)?;
1498 let k = shape[0].clone().min(shape[1].clone());
1499 Ok(vec![(dtype, shape.to_vec()), (dtype, vec![k])])
1500}
1501
1502fn householder_qr_from_factors_meta(
1503 dtypes: &[DType],
1504 shapes: &[&[SymDim]],
1505) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1506 const OP: &str = "tenferro-linalg.householder_qr_from_factors";
1507 require_householder_rank2(OP, shapes[0])?;
1508 require_householder_rank2(OP, shapes[1])?;
1509 if dtypes[0] != dtypes[1] {
1510 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1511 }
1512 require_static_extent_equal(OP, "q.cols/r.rows", &shapes[0][1], &shapes[1][0])?;
1513 if let (Some(q_cols), Some(q_rows), Some(r_cols)) = (
1514 shapes[0][1].constant_value(),
1515 shapes[0][0].constant_value(),
1516 shapes[1][1].constant_value(),
1517 ) {
1518 if q_cols > q_rows.min(r_cols) {
1519 return Err(Error::invalid_argument(
1520 OP,
1521 "shape",
1522 "Q column count exceeds min(Q rows, R columns)",
1523 ));
1524 }
1525 }
1526 let m = shapes[0][0].clone();
1527 let n = shapes[1][1].clone();
1528 let k = m.clone().min(n.clone());
1529 Ok(vec![(dtypes[0], vec![m, n]), (dtypes[0], vec![k])])
1530}
1531
1532fn householder_qr_append_meta(
1533 dtypes: &[DType],
1534 shapes: &[&[SymDim]],
1535) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1536 const OP: &str = "tenferro-linalg.householder_qr_append";
1537 require_householder_state_meta(OP, dtypes, shapes[0], shapes[1])?;
1538 require_householder_rank2(OP, shapes[2])?;
1539 if dtypes[0] != dtypes[2] {
1540 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[2]));
1541 }
1542 require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[2][0])?;
1543 let m = shapes[0][0].clone();
1544 let width = shapes[0][1].clone() + shapes[2][1].clone();
1545 let k = m.clone().min(width.clone());
1546 Ok(vec![(dtypes[0], vec![m, width]), (dtypes[0], vec![k])])
1547}
1548
1549fn require_static_extent_equal(
1550 op: &'static str,
1551 field: &'static str,
1552 lhs: &SymDim,
1553 rhs: &SymDim,
1554) -> tenferro_tensor::Result<()> {
1555 if let (Some(lhs), Some(rhs)) = (lhs.constant_value(), rhs.constant_value()) {
1556 if lhs != rhs {
1557 return Err(Error::invalid_argument(
1558 op,
1559 field,
1560 format!("expected equal extents, got {lhs} and {rhs}"),
1561 ));
1562 }
1563 }
1564 Ok(())
1565}
1566
1567fn require_householder_state_meta(
1568 op: &'static str,
1569 dtypes: &[DType],
1570 packed: &[SymDim],
1571 coeff: &[SymDim],
1572) -> tenferro_tensor::Result<()> {
1573 require_householder_rank2(op, packed)?;
1574 if coeff.len() != 1 {
1575 return Err(Error::rank_mismatch(op, 1, coeff.len()));
1576 }
1577 if dtypes[0] != dtypes[1] {
1578 return Err(Error::dtype_mismatch(op, dtypes[0], dtypes[1]));
1579 }
1580 let expected = packed[0].clone().min(packed[1].clone());
1581 require_static_extent_equal(op, "coeff", &coeff[0], &expected)
1582}
1583
1584fn householder_qr_r_meta(
1585 dtypes: &[DType],
1586 packed: &[SymDim],
1587 coeff: &[SymDim],
1588) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1589 require_householder_state_meta("tenferro-linalg.householder_qr_r", dtypes, packed, coeff)?;
1590 Ok((dtypes[0], vec![coeff[0].clone(), packed[1].clone()]))
1591}
1592
1593fn householder_qr_q_columns_meta(
1594 dtypes: &[DType],
1595 packed: &[SymDim],
1596 coeff: &[SymDim],
1597 start: usize,
1598 end: usize,
1599) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1600 require_householder_state_meta(
1601 "tenferro-linalg.householder_qr_q_columns",
1602 dtypes,
1603 packed,
1604 coeff,
1605 )?;
1606 if start > end {
1607 return Err(Error::invalid_argument(
1608 "tenferro-linalg.householder_qr_q_columns",
1609 "range",
1610 format!("invalid Q-column range {start}..{end}"),
1611 ));
1612 }
1613 if coeff[0].constant_value().is_some_and(|k| end > k) {
1614 return Err(Error::invalid_argument(
1615 "tenferro-linalg.householder_qr_q_columns",
1616 "range",
1617 format!("Q-column range {start}..{end} exceeds thin-Q width"),
1618 ));
1619 }
1620 Ok((
1621 dtypes[0],
1622 vec![packed[0].clone(), SymDim::from(end - start)],
1623 ))
1624}
1625
1626fn householder_qr_thin_q_meta(
1627 dtypes: &[DType],
1628 packed: &[SymDim],
1629 coeff: &[SymDim],
1630) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1631 require_householder_state_meta(
1632 "tenferro-linalg.householder_qr_thin_q",
1633 dtypes,
1634 packed,
1635 coeff,
1636 )?;
1637 Ok((dtypes[0], vec![packed[0].clone(), coeff[0].clone()]))
1638}
1639
1640fn householder_qr_split_config(
1641 cotangent: &[usize],
1642 left: &[usize],
1643 right_shape: &[usize],
1644 take_right: bool,
1645) -> tenferro_tensor::Result<tenferro_tensor::SliceConfig> {
1646 const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1647 for shape in [cotangent, left, right_shape] {
1648 if shape.len() != 2 {
1649 return Err(Error::rank_mismatch(OP, 2, shape.len()));
1650 }
1651 }
1652 let total_width = left[1]
1653 .checked_add(right_shape[1])
1654 .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1655 if cotangent[0] != left[0] || cotangent[0] != right_shape[0] || cotangent[1] != total_width {
1656 return Err(Error::invalid_argument(
1657 OP,
1658 "shape",
1659 "cotangent shape does not match appended factors",
1660 ));
1661 }
1662 let selected = if take_right { right_shape } else { left };
1663 let start = if take_right { left[1] } else { 0 };
1664 let end = start
1665 .checked_add(selected[1])
1666 .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1667 Ok(tenferro_tensor::SliceConfig {
1668 starts: vec![0, start],
1669 limits: vec![selected[0], end],
1670 strides: vec![1, 1],
1671 })
1672}
1673
1674fn householder_qr_append_tangent_meta(
1675 dtypes: &[DType],
1676 shapes: &[&[SymDim]],
1677) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1678 const OP: &str = "tenferro-linalg.householder_qr_append_tangent";
1679 for shape in shapes {
1680 require_householder_rank2(OP, shape)?;
1681 }
1682 if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1683 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1684 }
1685 require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[1][0])?;
1686 require_static_extent_equal(OP, "left tangent", &shapes[0][0], &shapes[2][0])?;
1687 require_static_extent_equal(OP, "right tangent", &shapes[1][0], &shapes[3][0])?;
1688 require_static_extent_equal(OP, "anchor rows", &shapes[2][0], &shapes[3][0])?;
1689 Ok((
1690 dtypes[0],
1691 vec![
1692 shapes[2][0].clone(),
1693 shapes[2][1].clone() + shapes[3][1].clone(),
1694 ],
1695 ))
1696}
1697
1698fn householder_qr_split_tangent_meta(
1699 dtypes: &[DType],
1700 shapes: &[&[SymDim]],
1701 right: bool,
1702) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1703 const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1704 for shape in shapes {
1705 require_householder_rank2(OP, shape)?;
1706 }
1707 if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1708 return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1709 }
1710 require_static_extent_equal(OP, "left rows", &shapes[0][0], &shapes[1][0])?;
1711 require_static_extent_equal(OP, "right rows", &shapes[0][0], &shapes[2][0])?;
1712 let expected_width = shapes[1][1].clone() + shapes[2][1].clone();
1713 require_static_extent_equal(OP, "width", &shapes[0][1], &expected_width)?;
1714 let selected = if right { shapes[2] } else { shapes[1] };
1715 Ok((dtypes[0], selected.to_vec()))
1716}
1717
1718fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1719 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
1720 Ok(vec![
1721 (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
1722 (dtype, matrix_shape(n.clone(), n, batch)),
1723 ])
1724}
1725
1726fn eigh_values_meta(
1727 dtype: DType,
1728 shape: &[SymDim],
1729) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1730 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
1731 Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
1732}
1733
1734fn eig_meta(
1735 input_dtype: DType,
1736 shape: &[SymDim],
1737) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1738 let dtype = eig_output_dtype(input_dtype);
1739 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
1740 Ok(vec![
1741 (dtype, vector_shape(n.clone(), batch)),
1742 (dtype, matrix_shape(n.clone(), n, batch)),
1743 ])
1744}
1745
1746fn eig_values_meta(
1747 input_dtype: DType,
1748 shape: &[SymDim],
1749) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1750 let dtype = eig_output_dtype(input_dtype);
1751 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
1752 Ok((dtype, vector_shape(n, batch)))
1753}
1754
1755fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1756 let mut shape = vec![rows, cols];
1757 shape.extend_from_slice(batch);
1758 shape
1759}
1760
1761fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1762 let mut shape = vec![len];
1763 shape.extend_from_slice(batch);
1764 shape
1765}
1766
1767fn eig_output_dtype(dtype: DType) -> DType {
1768 match dtype {
1769 DType::F64 | DType::C64 => DType::C64,
1770 DType::F32 | DType::C32 => DType::C32,
1771 DType::I32 | DType::I64 | DType::Bool => DType::C64,
1772 }
1773}
1774
1775fn singular_values_dtype(dtype: DType) -> DType {
1776 match dtype {
1777 DType::C64 => DType::F64,
1778 DType::C32 => DType::F32,
1779 other => other,
1780 }
1781}
1782
1783fn promote_dtypes(dtypes: &[DType]) -> DType {
1784 dtypes
1785 .iter()
1786 .copied()
1787 .reduce(tenferro_tensor::validate::promote_dtype)
1788 .unwrap_or(DType::F64)
1789}
1790
1791fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
1792 let tag = match dtype {
1793 DType::F64 => 0,
1794 DType::F32 => 1,
1795 DType::I64 => 2,
1796 DType::C64 => 3,
1797 DType::C32 => 4,
1798 DType::I32 => 5,
1799 DType::Bool => 6,
1800 };
1801 hasher.write_u8(tag);
1802}
1803
1804fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
1805 let tag = match gauge {
1806 SvdGauge::Raw => 0,
1807 SvdGauge::CanonicalPivot => 1,
1808 };
1809 hasher.write_u8(tag);
1810}
1811
1812fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
1813 let tag = match gauge {
1814 EighGauge::Raw => 0,
1815 EighGauge::CanonicalPivot => 1,
1816 };
1817 hasher.write_u8(tag);
1818}
1819
1820fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
1821 let tag = match gauge {
1822 QrGauge::Raw => 0,
1823 QrGauge::PositiveDiagonal => 1,
1824 };
1825 hasher.write_u8(tag);
1826}