1use std::any::Any;
2use std::hash::Hasher;
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_extension_macros::define_extension_runtime;
7use tenferro_ops::SymDim;
8use tenferro_runtime::extension::{ExtensionExecutionContext, ExtensionOp, HostReference};
9use tenferro_tensor::{
10 DType, DeviceKind, Error, GpuBackendKind, MemoryKind, Placement, Tensor, TensorRead,
11};
12
13use crate::backend::LinalgBackend;
14
15mod gauge;
16#[cfg(all(test, not(feature = "cuda")))]
17mod tests;
18
19pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
20
21pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
22
23pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum SvdGauge {
50 Raw,
52 CanonicalPivot,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum EighGauge {
69 Raw,
71 CanonicalPivot,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum QrGauge {
87 Raw,
89 PositiveDiagonal,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
107pub struct SvdOptions {
108 pub gauge: SvdGauge,
110 pub derivative_eps: f64,
112}
113
114impl Default for SvdOptions {
115 fn default() -> Self {
116 Self {
117 gauge: SvdGauge::Raw,
118 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
119 }
120 }
121}
122
123impl SvdOptions {
124 pub fn gauge(mut self, gauge: SvdGauge) -> Self {
135 self.gauge = gauge;
136 self
137 }
138
139 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
150 self.derivative_eps = derivative_eps;
151 self
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
166pub struct EighOptions {
167 pub gauge: EighGauge,
169 pub derivative_eps: f64,
171}
172
173impl Default for EighOptions {
174 fn default() -> Self {
175 Self {
176 gauge: EighGauge::Raw,
177 derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
178 }
179 }
180}
181
182impl EighOptions {
183 pub fn gauge(mut self, gauge: EighGauge) -> Self {
194 self.gauge = gauge;
195 self
196 }
197
198 pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
209 self.derivative_eps = derivative_eps;
210 self
211 }
212}
213
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub struct QrOptions {
226 pub gauge: QrGauge,
228}
229
230impl Default for QrOptions {
231 fn default() -> Self {
232 Self {
233 gauge: QrGauge::Raw,
234 }
235 }
236}
237
238impl QrOptions {
239 pub fn gauge(mut self, gauge: QrGauge) -> Self {
250 self.gauge = gauge;
251 self
252 }
253}
254
255pub(crate) fn validate_derivative_eps(
256 op: &'static str,
257 derivative_eps: f64,
258) -> tenferro_tensor::Result<()> {
259 if derivative_eps.is_finite() && derivative_eps > 0.0 {
260 Ok(())
261 } else {
262 Err(Error::InvalidConfig {
263 op,
264 message: format!("derivative_eps must be positive and finite, got {derivative_eps}"),
265 })
266 }
267}
268
269#[derive(Clone, Copy, Debug, PartialEq)]
270#[doc(hidden)]
271pub(crate) enum LinalgOp {
272 Cholesky,
273 Lu,
274 LuFactor,
275 LuSolvePrepared {
276 transpose_a: bool,
277 conjugate_a: bool,
278 },
279 FullPivLu,
280 FullPivLuSolve {
281 transpose_a: bool,
282 },
283 Svd {
284 derivative_eps: f64,
285 gauge: SvdGauge,
286 },
287 SvdVals {
288 derivative_eps: f64,
289 },
290 Qr {
291 gauge: QrGauge,
292 },
293 Eigh {
294 derivative_eps: f64,
295 gauge: EighGauge,
296 },
297 EighVals {
298 derivative_eps: f64,
299 },
300 Eig {
301 input_dtype: DType,
302 },
303 EigVals {
304 input_dtype: DType,
305 },
306 TriangularSolve {
307 left_side: bool,
308 lower: bool,
309 transpose_a: bool,
310 unit_diagonal: bool,
311 },
312}
313
314impl LinalgOp {
315 fn output_count(self) -> usize {
316 match self {
317 Self::Cholesky
318 | Self::EighVals { .. }
319 | Self::EigVals { .. }
320 | Self::FullPivLuSolve { .. }
321 | Self::LuSolvePrepared { .. }
322 | Self::SvdVals { .. }
323 | Self::TriangularSolve { .. } => 1,
324 Self::Svd { .. } => 3,
325 Self::Qr { .. } | Self::Eigh { .. } | Self::Eig { .. } => 2,
326 Self::LuFactor => 3,
327 Self::Lu => 4,
328 Self::FullPivLu => 5,
329 }
330 }
331
332 fn input_count(self) -> usize {
333 match self {
334 Self::FullPivLuSolve { .. } | Self::TriangularSolve { .. } => 2,
335 Self::LuSolvePrepared { .. } => 4,
336 _ => 1,
337 }
338 }
339
340 fn tag(self) -> u8 {
341 match self {
342 Self::Cholesky => 0,
343 Self::Lu => 1,
344 Self::FullPivLu => 2,
345 Self::FullPivLuSolve { .. } => 3,
346 Self::Svd { .. } => 4,
347 Self::Qr { .. } => 5,
348 Self::Eigh { .. } => 6,
349 Self::Eig { .. } => 7,
350 Self::TriangularSolve { .. } => 9,
351 Self::LuFactor => 10,
352 Self::LuSolvePrepared { .. } => 11,
353 Self::SvdVals { .. } => 12,
354 Self::EighVals { .. } => 13,
355 Self::EigVals { .. } => 14,
356 }
357 }
358}
359
360#[derive(Clone, Debug, PartialEq)]
361#[doc(hidden)]
362pub(crate) struct LinalgExtensionOp {
363 op: LinalgOp,
364}
365
366impl LinalgExtensionOp {
367 pub(crate) fn new(op: LinalgOp) -> Self {
368 Self { op }
369 }
370
371 pub(crate) fn op(&self) -> LinalgOp {
372 self.op
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
377enum EagerLinalgDevice {
378 Cpu,
379 Cuda(usize),
380}
381
382fn tensor_placement(input: &Tensor) -> &Placement {
383 input.placement()
384}
385
386fn input_eager_device(input: &Tensor) -> tenferro_tensor::Result<EagerLinalgDevice> {
387 let placement = tensor_placement(input);
388 match (&placement.memory_kind, placement.device.as_ref()) {
389 (MemoryKind::Device, Some(device)) => match &device.kind {
390 DeviceKind::Gpu(GpuBackendKind::Cuda) => Ok(EagerLinalgDevice::Cuda(device.ordinal)),
391 DeviceKind::Gpu(kind) => Err(Error::backend_failure(
392 "linalg_host_reference",
393 format!("unsupported GPU backend {kind:?} for eager linalg"),
394 )),
395 kind => Err(Error::backend_failure(
396 "linalg_host_reference",
397 format!("unsupported device kind {kind:?} for eager linalg"),
398 )),
399 },
400 (MemoryKind::Device, None) => Err(Error::backend_failure(
401 "linalg_host_reference",
402 "device tensor is missing placement device metadata",
403 )),
404 _ => Ok(EagerLinalgDevice::Cpu),
405 }
406}
407
408fn eager_linalg_device(inputs: &[&Tensor]) -> tenferro_tensor::Result<EagerLinalgDevice> {
409 let mut selected = None;
410 for input in inputs {
411 let device = input_eager_device(input)?;
412 match (selected, device) {
413 (None, next) => selected = Some(next),
414 (Some(EagerLinalgDevice::Cpu), EagerLinalgDevice::Cpu) => {}
415 (Some(EagerLinalgDevice::Cuda(lhs)), EagerLinalgDevice::Cuda(rhs)) if lhs == rhs => {}
416 (Some(lhs), rhs) => {
417 return Err(Error::backend_failure(
418 "linalg_host_reference",
419 format!("all eager linalg inputs must be on the same device, got {lhs:?} and {rhs:?}"),
420 ));
421 }
422 }
423 }
424 Ok(selected.unwrap_or(EagerLinalgDevice::Cpu))
425}
426
427#[cfg(feature = "cuda")]
428fn execute_cuda_eager_linalg(
429 op: LinalgOp,
430 inputs: &[&Tensor],
431 device_ordinal: usize,
432) -> tenferro_tensor::Result<Vec<Tensor>> {
433 let mut backend = tenferro_gpu::CudaBackend::new(device_ordinal)?;
434 execute_linalg(op, inputs, &mut backend)
435}
436
437#[cfg(not(feature = "cuda"))]
438fn execute_cuda_eager_linalg(
439 _op: LinalgOp,
440 _inputs: &[&Tensor],
441 device_ordinal: usize,
442) -> tenferro_tensor::Result<Vec<Tensor>> {
443 Err(Error::backend_failure(
444 "linalg_host_reference",
445 format!(
446 "received CUDA tensor on cuda:{device_ordinal}, but tenferro-linalg was built \
447 without the cuda feature; enable the cuda feature or download the tensor to CPU \
448 before eager linalg"
449 ),
450 ))
451}
452
453impl ExtensionOp for LinalgExtensionOp {
454 fn family_id(&self) -> &'static str {
455 LINALG_EXTENSION_FAMILY_ID
456 }
457
458 fn payload_hash(&self, hasher: &mut dyn Hasher) {
459 hasher.write_u8(self.op.tag());
460 match self.op {
461 LinalgOp::Svd {
462 derivative_eps,
463 gauge,
464 } => {
465 hasher.write_u64(derivative_eps.to_bits());
466 hash_svd_gauge(hasher, gauge);
467 }
468 LinalgOp::SvdVals { derivative_eps } | LinalgOp::EighVals { derivative_eps } => {
469 hasher.write_u64(derivative_eps.to_bits());
470 }
471 LinalgOp::Qr { gauge } => {
472 hash_qr_gauge(hasher, gauge);
473 }
474 LinalgOp::Eigh {
475 derivative_eps,
476 gauge,
477 } => {
478 hasher.write_u64(derivative_eps.to_bits());
479 hash_eigh_gauge(hasher, gauge);
480 }
481 LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
482 hash_dtype(hasher, input_dtype);
483 }
484 LinalgOp::FullPivLuSolve { transpose_a } => {
485 hasher.write_u8(u8::from(transpose_a));
486 }
487 LinalgOp::LuSolvePrepared {
488 transpose_a,
489 conjugate_a,
490 } => {
491 hasher.write_u8(u8::from(transpose_a));
492 hasher.write_u8(u8::from(conjugate_a));
493 }
494 LinalgOp::TriangularSolve {
495 left_side,
496 lower,
497 transpose_a,
498 unit_diagonal,
499 } => {
500 hasher.write_u8(u8::from(left_side));
501 hasher.write_u8(u8::from(lower));
502 hasher.write_u8(u8::from(transpose_a));
503 hasher.write_u8(u8::from(unit_diagonal));
504 }
505 LinalgOp::Cholesky | LinalgOp::Lu | LinalgOp::LuFactor | LinalgOp::FullPivLu => {}
506 }
507 }
508
509 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
510 other
511 .as_any()
512 .downcast_ref::<Self>()
513 .is_some_and(|that| self == that)
514 }
515
516 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
517 Arc::new(self.clone())
518 }
519
520 fn as_any(&self) -> &dyn Any {
521 self
522 }
523
524 fn input_count(&self) -> usize {
525 self.op.input_count()
526 }
527
528 fn output_count(&self) -> usize {
529 self.op.output_count()
530 }
531
532 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
533 match self.op {
534 LinalgOp::Svd { derivative_eps, .. } if live_outputs == [false, true, false] => {
535 Some(Arc::new(Self::new(LinalgOp::SvdVals { derivative_eps })))
536 }
537 LinalgOp::Eigh { derivative_eps, .. } if live_outputs == [true, false] => {
538 Some(Arc::new(Self::new(LinalgOp::EighVals { derivative_eps })))
539 }
540 LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
541 Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
542 }
543 _ => None,
544 }
545 }
546
547 fn infer_output_meta(
548 &self,
549 input_dtypes: &[DType],
550 input_shapes: &[&[SymDim]],
551 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
552 if input_dtypes.len() != self.input_count() || input_shapes.len() != self.input_count() {
553 return Err(Error::InvalidConfig {
554 op: "tenferro-linalg",
555 message: format!(
556 "expected {} input metadata entries, got dtypes={} shapes={}",
557 self.input_count(),
558 input_dtypes.len(),
559 input_shapes.len()
560 ),
561 });
562 }
563 let metas = match self.op {
564 LinalgOp::Cholesky => {
565 require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
566 vec![(promote_dtypes(input_dtypes), input_shapes[0].to_vec())]
567 }
568 LinalgOp::FullPivLuSolve { .. } => {
569 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
570 require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
571 vec![(promote_dtypes(input_dtypes), input_shapes[1].to_vec())]
572 }
573 LinalgOp::TriangularSolve { .. } => {
574 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
575 require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
576 vec![(promote_dtypes(input_dtypes), input_shapes[1].to_vec())]
577 }
578 LinalgOp::LuSolvePrepared { .. } => {
579 require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
580 require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
581 vec![(
582 promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
583 input_shapes[3].to_vec(),
584 )]
585 }
586 LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
587 LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
588 LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
589 LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
590 LinalgOp::SvdVals { .. } => {
591 vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
592 }
593 LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
594 LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
595 LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
596 LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
597 LinalgOp::EigVals { input_dtype } => {
598 vec![eig_values_meta(input_dtype, input_shapes[0])?]
599 }
600 };
601 Ok(metas)
602 }
603
604 fn host_reference(&self) -> Option<&dyn HostReference> {
605 Some(self)
606 }
607}
608
609impl HostReference for LinalgExtensionOp {
610 fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
611 let expected = self.input_count();
612 if inputs.len() != expected {
613 return Err(Error::InvalidConfig {
614 op: "linalg_host_reference",
615 message: format!(
616 "expected {expected} inputs for {:?}, got {}",
617 self.op,
618 inputs.len()
619 ),
620 });
621 }
622
623 match eager_linalg_device(inputs)? {
624 EagerLinalgDevice::Cpu => {
625 let mut backend = tenferro_cpu::CpuBackend::new();
626 execute_linalg(self.op, inputs, &mut backend)
627 }
628 EagerLinalgDevice::Cuda(device_ordinal) => {
629 execute_cuda_eager_linalg(self.op, inputs, device_ordinal)
630 }
631 }
632 }
633}
634
635fn execute_linalg_extension<B: LinalgBackend + 'static>(
636 op: &LinalgExtensionOp,
637 inputs: &[&Tensor],
638 ctx: &mut ExtensionExecutionContext<'_, B>,
639) -> tenferro_tensor::Result<Vec<Tensor>> {
640 execute_linalg(op.op(), inputs, ctx.backend_mut())
641}
642
643fn execute_linalg_extension_reads<B: LinalgBackend + 'static>(
644 op: &LinalgExtensionOp,
645 inputs: &[TensorRead<'_>],
646 ctx: &mut ExtensionExecutionContext<'_, B>,
647) -> tenferro_tensor::Result<Vec<Tensor>> {
648 let materialized_inputs: Vec<Tensor> = inputs
651 .iter()
652 .map(TensorRead::to_tensor)
653 .collect::<tenferro_tensor::Result<_>>()?;
654 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
655 execute_linalg_extension(op, &input_refs, ctx)
656}
657
658define_extension_runtime! {
659 runtime = LinalgRuntime,
660 family_id = LINALG_EXTENSION_FAMILY_ID,
661 op_type = LinalgExtensionOp,
662 execute = execute_linalg_extension,
663 execute_reads = execute_linalg_extension_reads,
664 register_fn = register_runtime,
665 backend_bound = LinalgBackend,
666}
667
668fn execute_linalg<B: LinalgBackend>(
669 op: LinalgOp,
670 inputs: &[&Tensor],
671 backend: &mut B,
672) -> tenferro_tensor::Result<Vec<Tensor>> {
673 match op {
674 LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
675 LinalgOp::Lu => backend.lu(inputs[0]),
676 LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
677 LinalgOp::LuSolvePrepared {
678 transpose_a,
679 conjugate_a,
680 } => Ok(vec![backend.lu_solve_prepared(
681 inputs[0],
682 inputs[1],
683 inputs[2],
684 inputs[3],
685 transpose_a,
686 conjugate_a,
687 )?]),
688 LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
689 LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
690 inputs[0],
691 inputs[1],
692 transpose_a,
693 )?]),
694 LinalgOp::Svd {
695 derivative_eps,
696 gauge,
697 } => backend.svd_with_options(
698 inputs[0],
699 SvdOptions {
700 derivative_eps,
701 gauge,
702 },
703 ),
704 LinalgOp::SvdVals { .. } => Ok(vec![backend.svd_values(inputs[0])?]),
705 LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
706 LinalgOp::Eigh {
707 derivative_eps,
708 gauge,
709 } => backend.eigh_with_options(
710 inputs[0],
711 EighOptions {
712 derivative_eps,
713 gauge,
714 },
715 ),
716 LinalgOp::EighVals { .. } => Ok(vec![backend.eigh_values(inputs[0])?]),
717 LinalgOp::Eig { .. } => backend.eig(inputs[0]),
718 LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
719 LinalgOp::TriangularSolve {
720 left_side,
721 lower,
722 transpose_a,
723 unit_diagonal,
724 } => Ok(vec![backend.triangular_solve(
725 inputs[0],
726 inputs[1],
727 left_side,
728 lower,
729 transpose_a,
730 unit_diagonal,
731 )?]),
732 }
733}
734
735pub(crate) fn apply_svd_gauge(
736 gauge: SvdGauge,
737 outputs: &mut [Tensor],
738) -> tenferro_tensor::Result<()> {
739 match gauge {
740 SvdGauge::Raw => Ok(()),
741 SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
742 }
743}
744
745fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
746 if outputs.len() != 3 {
747 return Err(Error::InvalidConfig {
748 op: "tenferro-linalg.svd",
749 message: format!(
750 "canonical SVD gauge expected three outputs, got {}",
751 outputs.len()
752 ),
753 });
754 }
755
756 let (u_slice, rest) = outputs.split_at_mut(1);
757 let (singular_slice, vt_slice) = rest.split_at_mut(1);
758 let u = &mut u_slice[0];
759 let singular_values = &singular_slice[0];
760 let vt = &mut vt_slice[0];
761 let u_shape = u.shape().to_vec();
762 let s_shape = singular_values.shape().to_vec();
763 let vt_shape = vt.shape().to_vec();
764 if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
765 return Err(Error::InvalidConfig {
766 op: "tenferro-linalg.svd",
767 message: format!(
768 "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
769 ),
770 });
771 }
772
773 let m = u_shape[0];
774 let k = u_shape[1];
775 let n = vt_shape[1];
776 if s_shape[0] != k
777 || vt_shape[0] != k
778 || u_shape[2..] != vt_shape[2..]
779 || s_shape[1..] != u_shape[2..]
780 {
781 return Err(Error::InvalidConfig {
782 op: "tenferro-linalg.svd",
783 message: format!(
784 "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
785 ),
786 });
787 }
788 let batch_count = u_shape[2..].iter().product::<usize>();
789
790 match (u, vt) {
791 (Tensor::F64(u), Tensor::F64(vt)) => canonicalize_svd_gauge_f64(
792 u.host_data_mut()?,
793 vt.host_data_mut()?,
794 m,
795 k,
796 n,
797 batch_count,
798 ),
799 (Tensor::F32(u), Tensor::F32(vt)) => canonicalize_svd_gauge_f32(
800 u.host_data_mut()?,
801 vt.host_data_mut()?,
802 m,
803 k,
804 n,
805 batch_count,
806 ),
807 (Tensor::C64(u), Tensor::C64(vt)) => canonicalize_svd_gauge_c64(
808 u.host_data_mut()?,
809 vt.host_data_mut()?,
810 m,
811 k,
812 n,
813 batch_count,
814 ),
815 (Tensor::C32(u), Tensor::C32(vt)) => canonicalize_svd_gauge_c32(
816 u.host_data_mut()?,
817 vt.host_data_mut()?,
818 m,
819 k,
820 n,
821 batch_count,
822 ),
823 (u, vt) => Err(Error::DTypeMismatch {
824 op: "tenferro-linalg.svd",
825 lhs: u.dtype(),
826 rhs: vt.dtype(),
827 }),
828 }
829}
830
831fn canonicalize_svd_gauge_f64(
832 u: &mut [f64],
833 vt: &mut [f64],
834 m: usize,
835 k: usize,
836 n: usize,
837 batch_count: usize,
838) -> tenferro_tensor::Result<()> {
839 for batch in 0..batch_count {
840 let u_batch = batch * m * k;
841 let vt_batch = batch * k * n;
842 for col in 0..k {
843 let pivot = max_abs_pivot_f64(u, u_batch, m, col);
844 let pivot_value = u[u_batch + pivot + m * col];
845 if pivot_value < 0.0 {
846 for row in 0..m {
847 let offset = u_batch + row + m * col;
848 u[offset] = -u[offset];
849 }
850 for vt_col in 0..n {
851 let offset = vt_batch + col + k * vt_col;
852 vt[offset] = -vt[offset];
853 }
854 }
855 }
856 }
857 Ok(())
858}
859
860fn canonicalize_svd_gauge_f32(
861 u: &mut [f32],
862 vt: &mut [f32],
863 m: usize,
864 k: usize,
865 n: usize,
866 batch_count: usize,
867) -> tenferro_tensor::Result<()> {
868 for batch in 0..batch_count {
869 let u_batch = batch * m * k;
870 let vt_batch = batch * k * n;
871 for col in 0..k {
872 let pivot = max_abs_pivot_f32(u, u_batch, m, col);
873 let pivot_value = u[u_batch + pivot + m * col];
874 if pivot_value < 0.0 {
875 for row in 0..m {
876 let offset = u_batch + row + m * col;
877 u[offset] = -u[offset];
878 }
879 for vt_col in 0..n {
880 let offset = vt_batch + col + k * vt_col;
881 vt[offset] = -vt[offset];
882 }
883 }
884 }
885 }
886 Ok(())
887}
888
889fn canonicalize_svd_gauge_c64(
890 u: &mut [Complex64],
891 vt: &mut [Complex64],
892 m: usize,
893 k: usize,
894 n: usize,
895 batch_count: usize,
896) -> tenferro_tensor::Result<()> {
897 for batch in 0..batch_count {
898 let u_batch = batch * m * k;
899 let vt_batch = batch * k * n;
900 for col in 0..k {
901 let pivot = max_abs_pivot_c64(u, u_batch, m, col);
902 let pivot_value = u[u_batch + pivot + m * col];
903 let pivot_norm = pivot_value.norm();
904 if pivot_norm == 0.0 {
905 continue;
906 }
907 let phase = pivot_value.conj() / pivot_norm;
908 let vt_phase = phase.conj();
909 for row in 0..m {
910 let offset = u_batch + row + m * col;
911 u[offset] *= phase;
912 }
913 for vt_col in 0..n {
914 let offset = vt_batch + col + k * vt_col;
915 vt[offset] *= vt_phase;
916 }
917 }
918 }
919 Ok(())
920}
921
922fn canonicalize_svd_gauge_c32(
923 u: &mut [Complex32],
924 vt: &mut [Complex32],
925 m: usize,
926 k: usize,
927 n: usize,
928 batch_count: usize,
929) -> tenferro_tensor::Result<()> {
930 for batch in 0..batch_count {
931 let u_batch = batch * m * k;
932 let vt_batch = batch * k * n;
933 for col in 0..k {
934 let pivot = max_abs_pivot_c32(u, u_batch, m, col);
935 let pivot_value = u[u_batch + pivot + m * col];
936 let pivot_norm = pivot_value.norm();
937 if pivot_norm == 0.0 {
938 continue;
939 }
940 let phase = pivot_value.conj() / pivot_norm;
941 let vt_phase = phase.conj();
942 for row in 0..m {
943 let offset = u_batch + row + m * col;
944 u[offset] *= phase;
945 }
946 for vt_col in 0..n {
947 let offset = vt_batch + col + k * vt_col;
948 vt[offset] *= vt_phase;
949 }
950 }
951 }
952 Ok(())
953}
954
955fn max_abs_pivot_f64(u: &[f64], u_batch: usize, m: usize, col: usize) -> usize {
956 let mut pivot = 0;
957 let mut pivot_abs = u[u_batch + m * col].abs();
958 for row in 1..m {
959 let candidate_abs = u[u_batch + row + m * col].abs();
960 if candidate_abs > pivot_abs {
961 pivot = row;
962 pivot_abs = candidate_abs;
963 }
964 }
965 pivot
966}
967
968fn max_abs_pivot_f32(u: &[f32], u_batch: usize, m: usize, col: usize) -> usize {
969 let mut pivot = 0;
970 let mut pivot_abs = u[u_batch + m * col].abs();
971 for row in 1..m {
972 let candidate_abs = u[u_batch + row + m * col].abs();
973 if candidate_abs > pivot_abs {
974 pivot = row;
975 pivot_abs = candidate_abs;
976 }
977 }
978 pivot
979}
980
981fn max_abs_pivot_c64(u: &[Complex64], u_batch: usize, m: usize, col: usize) -> usize {
982 let mut pivot = 0;
983 let mut pivot_abs = u[u_batch + m * col].norm_sqr();
984 for row in 1..m {
985 let candidate_abs = u[u_batch + row + m * col].norm_sqr();
986 if candidate_abs > pivot_abs {
987 pivot = row;
988 pivot_abs = candidate_abs;
989 }
990 }
991 pivot
992}
993
994fn max_abs_pivot_c32(u: &[Complex32], u_batch: usize, m: usize, col: usize) -> usize {
995 let mut pivot = 0;
996 let mut pivot_abs = u[u_batch + m * col].norm_sqr();
997 for row in 1..m {
998 let candidate_abs = u[u_batch + row + m * col].norm_sqr();
999 if candidate_abs > pivot_abs {
1000 pivot = row;
1001 pivot_abs = candidate_abs;
1002 }
1003 }
1004 pivot
1005}
1006
1007fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1008 if shape.len() < 2 {
1009 return Err(Error::RankMismatch {
1010 op,
1011 expected: 2,
1012 actual: shape.len(),
1013 });
1014 }
1015 Ok(())
1016}
1017
1018fn matrix_meta_parts<'a>(
1019 op: &'static str,
1020 shape: &'a [SymDim],
1021) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1022 require_matrix_meta(op, shape)?;
1023 Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1024}
1025
1026fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1027 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1028 let k = m.clone().min(n.clone());
1029 Ok(vec![
1030 (dtype, matrix_shape(m.clone(), m, batch)),
1031 (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1032 (dtype, matrix_shape(k, n, batch)),
1033 (dtype, batch.to_vec()),
1034 ])
1035}
1036
1037fn lu_factor_meta(
1038 dtype: DType,
1039 shape: &[SymDim],
1040) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1041 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1042 let k = m.min(n);
1043 Ok(vec![
1044 (dtype, shape.to_vec()),
1045 (DType::I32, vector_shape(k, batch)),
1046 (dtype, batch.to_vec()),
1047 ])
1048}
1049
1050fn full_piv_lu_meta(
1051 dtype: DType,
1052 shape: &[SymDim],
1053) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1054 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1055 Ok(vec![
1056 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1057 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1058 (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1059 (dtype, matrix_shape(n.clone(), n, batch)),
1060 (singular_values_dtype(dtype), batch.to_vec()),
1061 ])
1062}
1063
1064fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1065 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1066 let k = m.clone().min(n.clone());
1067 Ok(vec![
1068 (dtype, matrix_shape(m, k.clone(), batch)),
1069 (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1070 (dtype, matrix_shape(k, n, batch)),
1071 ])
1072}
1073
1074fn svd_values_meta(
1075 dtype: DType,
1076 shape: &[SymDim],
1077) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1078 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1079 let k = m.min(n);
1080 Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1081}
1082
1083fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1084 let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1085 let k = m.clone().min(n.clone());
1086 Ok(vec![
1087 (dtype, matrix_shape(m, k.clone(), batch)),
1088 (dtype, matrix_shape(k, n, batch)),
1089 ])
1090}
1091
1092fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1093 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
1094 Ok(vec![
1095 (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
1096 (dtype, matrix_shape(n.clone(), n, batch)),
1097 ])
1098}
1099
1100fn eigh_values_meta(
1101 dtype: DType,
1102 shape: &[SymDim],
1103) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1104 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
1105 Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
1106}
1107
1108fn eig_meta(
1109 input_dtype: DType,
1110 shape: &[SymDim],
1111) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1112 let dtype = eig_output_dtype(input_dtype);
1113 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
1114 Ok(vec![
1115 (dtype, vector_shape(n.clone(), batch)),
1116 (dtype, matrix_shape(n.clone(), n, batch)),
1117 ])
1118}
1119
1120fn eig_values_meta(
1121 input_dtype: DType,
1122 shape: &[SymDim],
1123) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1124 let dtype = eig_output_dtype(input_dtype);
1125 let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
1126 Ok((dtype, vector_shape(n, batch)))
1127}
1128
1129fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1130 let mut shape = vec![rows, cols];
1131 shape.extend_from_slice(batch);
1132 shape
1133}
1134
1135fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1136 let mut shape = vec![len];
1137 shape.extend_from_slice(batch);
1138 shape
1139}
1140
1141fn eig_output_dtype(dtype: DType) -> DType {
1142 match dtype {
1143 DType::F64 | DType::C64 => DType::C64,
1144 DType::F32 | DType::C32 => DType::C32,
1145 DType::I32 | DType::I64 | DType::Bool => DType::C64,
1146 }
1147}
1148
1149fn singular_values_dtype(dtype: DType) -> DType {
1150 match dtype {
1151 DType::C64 => DType::F64,
1152 DType::C32 => DType::F32,
1153 other => other,
1154 }
1155}
1156
1157fn promote_dtypes(dtypes: &[DType]) -> DType {
1158 dtypes
1159 .iter()
1160 .copied()
1161 .reduce(tenferro_tensor::validate::promote_dtype)
1162 .unwrap_or(DType::F64)
1163}
1164
1165fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
1166 let tag = match dtype {
1167 DType::F64 => 0,
1168 DType::F32 => 1,
1169 DType::I64 => 2,
1170 DType::C64 => 3,
1171 DType::C32 => 4,
1172 DType::I32 => 5,
1173 DType::Bool => 6,
1174 };
1175 hasher.write_u8(tag);
1176}
1177
1178fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
1179 let tag = match gauge {
1180 SvdGauge::Raw => 0,
1181 SvdGauge::CanonicalPivot => 1,
1182 };
1183 hasher.write_u8(tag);
1184}
1185
1186fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
1187 let tag = match gauge {
1188 EighGauge::Raw => 0,
1189 EighGauge::CanonicalPivot => 1,
1190 };
1191 hasher.write_u8(tag);
1192}
1193
1194fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
1195 let tag = match gauge {
1196 QrGauge::Raw => 0,
1197 QrGauge::PositiveDiagonal => 1,
1198 };
1199 hasher.write_u8(tag);
1200}