1use oxicuda_ptx::prelude::*;
16
17use crate::error::{SparseError, SparseResult};
18use crate::ptx_helpers::{
19 emit_warp_reduce_sum, load_float_imm, load_global_float, mul_float, store_global_float,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum EigenTarget {
29 LargestMagnitude,
31 SmallestMagnitude,
33 LargestAlgebraic,
35 SmallestAlgebraic,
37}
38
39pub const KRYLOV_BLOCK_SIZE: u32 = 256;
41
42#[derive(Debug, Clone)]
48pub struct LanczosConfig {
49 pub max_iterations: usize,
51 pub tolerance: f64,
53 pub num_eigenvalues: usize,
55 pub which: EigenTarget,
57}
58
59#[derive(Debug, Clone)]
61pub struct LanczosResult {
62 pub eigenvalues: Vec<f64>,
64 pub alpha: Vec<f64>,
66 pub beta: Vec<f64>,
68 pub iterations: usize,
70 pub converged: bool,
72}
73
74#[derive(Debug)]
85pub struct LanczosPlan {
86 config: LanczosConfig,
87 n: usize,
89}
90
91impl LanczosPlan {
92 pub fn new(config: LanczosConfig, n: usize) -> SparseResult<Self> {
103 if n == 0 {
104 return Err(SparseError::InvalidArgument(
105 "matrix dimension n must be positive".to_string(),
106 ));
107 }
108 if config.num_eigenvalues == 0 {
109 return Err(SparseError::InvalidArgument(
110 "num_eigenvalues must be positive".to_string(),
111 ));
112 }
113 if config.max_iterations < config.num_eigenvalues {
114 return Err(SparseError::InvalidArgument(format!(
115 "max_iterations ({}) must be >= num_eigenvalues ({})",
116 config.max_iterations, config.num_eigenvalues
117 )));
118 }
119 if config.max_iterations > n {
120 return Err(SparseError::InvalidArgument(format!(
121 "max_iterations ({}) must be <= matrix dimension n ({})",
122 config.max_iterations, n
123 )));
124 }
125 if config.tolerance <= 0.0 {
126 return Err(SparseError::InvalidArgument(
127 "tolerance must be positive".to_string(),
128 ));
129 }
130
131 Ok(Self { config, n })
132 }
133
134 #[inline]
136 pub fn config(&self) -> &LanczosConfig {
137 &self.config
138 }
139
140 #[inline]
142 pub fn dimension(&self) -> usize {
143 self.n
144 }
145
146 pub fn workspace_bytes_f64(&self) -> usize {
154 let k = self.config.max_iterations;
155 let n = self.n;
156 let vectors = (k + 2) * n * 8; let scalars = (k + k) * 8; vectors + scalars
159 }
160
161 pub fn workspace_bytes_f32(&self) -> usize {
163 let k = self.config.max_iterations;
164 let n = self.n;
165 let vectors = (k + 2) * n * 4;
166 let scalars = (k + k) * 4;
167 vectors + scalars
168 }
169
170 pub fn generate_lanczos_step_ptx(&self) -> SparseResult<String> {
198 emit_lanczos_step_f64(self.n)
199 }
200
201 pub fn generate_lanczos_step_ptx_f32(&self) -> SparseResult<String> {
206 emit_lanczos_step_f32(self.n)
207 }
208
209 pub fn generate_reorthogonalize_ptx(&self) -> SparseResult<String> {
235 emit_reorthogonalize_f64(self.n)
236 }
237
238 pub fn generate_reorthogonalize_ptx_f32(&self) -> SparseResult<String> {
240 emit_reorthogonalize_f32(self.n)
241 }
242
243 pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
247 emit_dot_product_reduce_f64(self.n)
248 }
249
250 pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
252 emit_dot_product_reduce_f32(self.n)
253 }
254
255 pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
259 emit_norm_sq_reduce_f64(self.n)
260 }
261
262 pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
264 emit_norm_sq_reduce_f32(self.n)
265 }
266}
267
268#[derive(Debug, Clone)]
274pub struct ArnoldiConfig {
275 pub max_iterations: usize,
277 pub tolerance: f64,
279 pub num_eigenvalues: usize,
281 pub which: EigenTarget,
283}
284
285#[derive(Debug, Clone)]
287pub struct ArnoldiResult {
288 pub eigenvalues: Vec<(f64, f64)>,
290 pub hessenberg: Vec<Vec<f64>>,
292 pub iterations: usize,
294 pub converged: bool,
296}
297
298#[derive(Debug)]
308pub struct ArnoldiPlan {
309 config: ArnoldiConfig,
310 n: usize,
312}
313
314impl ArnoldiPlan {
315 pub fn new(config: ArnoldiConfig, n: usize) -> SparseResult<Self> {
326 if n == 0 {
327 return Err(SparseError::InvalidArgument(
328 "matrix dimension n must be positive".to_string(),
329 ));
330 }
331 if config.num_eigenvalues == 0 {
332 return Err(SparseError::InvalidArgument(
333 "num_eigenvalues must be positive".to_string(),
334 ));
335 }
336 if config.max_iterations < config.num_eigenvalues {
337 return Err(SparseError::InvalidArgument(format!(
338 "max_iterations ({}) must be >= num_eigenvalues ({})",
339 config.max_iterations, config.num_eigenvalues
340 )));
341 }
342 if config.max_iterations > n {
343 return Err(SparseError::InvalidArgument(format!(
344 "max_iterations ({}) must be <= matrix dimension n ({})",
345 config.max_iterations, n
346 )));
347 }
348 if config.tolerance <= 0.0 {
349 return Err(SparseError::InvalidArgument(
350 "tolerance must be positive".to_string(),
351 ));
352 }
353
354 Ok(Self { config, n })
355 }
356
357 #[inline]
359 pub fn config(&self) -> &ArnoldiConfig {
360 &self.config
361 }
362
363 #[inline]
365 pub fn dimension(&self) -> usize {
366 self.n
367 }
368
369 pub fn workspace_bytes_f64(&self) -> usize {
376 let k = self.config.max_iterations;
377 let n = self.n;
378 let vectors = (k + 2) * n * 8; let hessenberg = (k + 1) * k * 8; vectors + hessenberg
381 }
382
383 pub fn workspace_bytes_f32(&self) -> usize {
385 let k = self.config.max_iterations;
386 let n = self.n;
387 let vectors = (k + 2) * n * 4;
388 let hessenberg = (k + 1) * k * 4;
389 vectors + hessenberg
390 }
391
392 pub fn generate_arnoldi_step_ptx(&self) -> SparseResult<String> {
419 emit_arnoldi_step_f64(self.n)
420 }
421
422 pub fn generate_arnoldi_step_ptx_f32(&self) -> SparseResult<String> {
424 emit_arnoldi_step_f32(self.n)
425 }
426
427 pub fn generate_gram_schmidt_ptx(&self) -> SparseResult<String> {
445 emit_gram_schmidt_f64(self.n)
446 }
447
448 pub fn generate_gram_schmidt_ptx_f32(&self) -> SparseResult<String> {
450 emit_gram_schmidt_f32(self.n)
451 }
452
453 pub fn generate_dot_product_ptx(&self) -> SparseResult<String> {
457 emit_dot_product_reduce_f64(self.n)
458 }
459
460 pub fn generate_dot_product_ptx_f32(&self) -> SparseResult<String> {
462 emit_dot_product_reduce_f32(self.n)
463 }
464
465 pub fn generate_norm_sq_ptx(&self) -> SparseResult<String> {
469 emit_norm_sq_reduce_f64(self.n)
470 }
471
472 pub fn generate_norm_sq_ptx_f32(&self) -> SparseResult<String> {
474 emit_norm_sq_reduce_f32(self.n)
475 }
476}
477
478fn emit_lanczos_step_f64(n: usize) -> SparseResult<String> {
496 emit_lanczos_step_typed::<f64>(n, "lanczos_step_f64")
497}
498
499fn emit_lanczos_step_f32(n: usize) -> SparseResult<String> {
501 emit_lanczos_step_typed::<f32>(n, "lanczos_step_f32")
502}
503
504fn emit_lanczos_step_typed<T: oxicuda_blas::GpuFloat>(
513 _n: usize,
514 kernel_name: &str,
515) -> SparseResult<String> {
516 let is_f64 = T::SIZE == 8;
517 let elem_bytes = T::size_u32();
518 let mov_suffix = if is_f64 { "f64" } else { "f32" };
519
520 KernelBuilder::new(kernel_name)
521 .target(SmVersion::Sm80)
522 .param("w_ptr", PtxType::U64)
523 .param("v_j_ptr", PtxType::U64)
524 .param("v_jm1_ptr", PtxType::U64)
525 .param("v_jp1_ptr", PtxType::U64)
526 .param("alpha_bits", PtxType::U64)
527 .param("beta_prev_bits", PtxType::U64)
528 .param("beta_j_bits", PtxType::U64)
529 .param("n", PtxType::U32)
530 .body(move |b| {
531 let gid = b.global_thread_id_x();
532 let n_param = b.load_param_u32("n");
533
534 let gid_inner = gid.clone();
535 b.if_lt_u32(gid, n_param, move |b| {
536 let tid = gid_inner;
537 let w_ptr = b.load_param_u64("w_ptr");
538 let v_j_ptr = b.load_param_u64("v_j_ptr");
539 let v_jm1_ptr = b.load_param_u64("v_jm1_ptr");
540 let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
541 let alpha_bits = b.load_param_u64("alpha_bits");
542 let beta_prev_bits = b.load_param_u64("beta_prev_bits");
543 let beta_j_bits = b.load_param_u64("beta_j_bits");
544
545 let alpha = reinterpret_bits::<T>(b, alpha_bits);
546 let beta_prev = reinterpret_bits::<T>(b, beta_prev_bits);
547 let beta_j = reinterpret_bits::<T>(b, beta_j_bits);
548
549 let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
551 let w_val = load_global_float::<T>(b, w_addr.clone());
552
553 let vj_addr = b.byte_offset_addr(v_j_ptr, tid.clone(), elem_bytes);
554 let vj_val = load_global_float::<T>(b, vj_addr);
555
556 let vjm1_addr = b.byte_offset_addr(v_jm1_ptr, tid.clone(), elem_bytes);
557 let vjm1_val = load_global_float::<T>(b, vjm1_addr);
558
559 let alpha_vj = mul_float::<T>(b, alpha, vj_val);
561 let beta_vjm1 = mul_float::<T>(b, beta_prev, vjm1_val);
562 let sub1 = sub_float::<T>(b, w_val, alpha_vj);
563 let w_orth = sub_float::<T>(b, sub1, beta_vjm1);
564
565 store_global_float::<T>(b, w_addr, w_orth.clone());
567
568 let v_jp1_val = div_float::<T>(b, w_orth, beta_j);
570 let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
571 store_global_float::<T>(b, vjp1_addr, v_jp1_val);
572 });
573
574 let _ = mov_suffix;
576
577 b.ret();
578 })
579 .build()
580 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
581}
582
583fn emit_reorthogonalize_f64(n: usize) -> SparseResult<String> {
589 emit_reorthogonalize_typed::<f64>(n, "reorthogonalize_f64")
590}
591
592fn emit_reorthogonalize_f32(n: usize) -> SparseResult<String> {
594 emit_reorthogonalize_typed::<f32>(n, "reorthogonalize_f32")
595}
596
597fn emit_reorthogonalize_typed<T: oxicuda_blas::GpuFloat>(
610 _n: usize,
611 kernel_name: &str,
612) -> SparseResult<String> {
613 let elem_bytes = T::size_u32();
614
615 KernelBuilder::new(kernel_name)
616 .target(SmVersion::Sm80)
617 .param("w_ptr", PtxType::U64)
618 .param("v_i_ptr", PtxType::U64)
619 .param("coeff_bits", PtxType::U64)
620 .param("n", PtxType::U32)
621 .body(move |b| {
622 let gid = b.global_thread_id_x();
623 let n_param = b.load_param_u32("n");
624
625 let gid_inner = gid.clone();
626 b.if_lt_u32(gid, n_param, move |b| {
627 let tid = gid_inner;
628 let w_ptr = b.load_param_u64("w_ptr");
629 let v_i_ptr = b.load_param_u64("v_i_ptr");
630 let coeff_bits = b.load_param_u64("coeff_bits");
631
632 let coeff = reinterpret_bits::<T>(b, coeff_bits);
633
634 let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
636 let w_val = load_global_float::<T>(b, w_addr.clone());
637
638 let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
639 let vi_val = load_global_float::<T>(b, vi_addr);
640
641 let proj = mul_float::<T>(b, coeff, vi_val);
643 let w_new = sub_float::<T>(b, w_val, proj);
644
645 store_global_float::<T>(b, w_addr, w_new);
646 });
647
648 b.ret();
649 })
650 .build()
651 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
652}
653
654fn emit_arnoldi_step_f64(n: usize) -> SparseResult<String> {
660 emit_arnoldi_step_typed::<f64>(n, "arnoldi_step_f64")
661}
662
663fn emit_arnoldi_step_f32(n: usize) -> SparseResult<String> {
665 emit_arnoldi_step_typed::<f32>(n, "arnoldi_step_f32")
666}
667
668fn emit_arnoldi_step_typed<T: oxicuda_blas::GpuFloat>(
680 _n: usize,
681 kernel_name: &str,
682) -> SparseResult<String> {
683 let elem_bytes = T::size_u32();
684
685 KernelBuilder::new(kernel_name)
686 .target(SmVersion::Sm80)
687 .param("w_ptr", PtxType::U64)
688 .param("v_jp1_ptr", PtxType::U64)
689 .param("h_jp1_j_bits", PtxType::U64)
690 .param("n", PtxType::U32)
691 .body(move |b| {
692 let gid = b.global_thread_id_x();
693 let n_param = b.load_param_u32("n");
694
695 let gid_inner = gid.clone();
696 b.if_lt_u32(gid, n_param, move |b| {
697 let tid = gid_inner;
698 let w_ptr = b.load_param_u64("w_ptr");
699 let v_jp1_ptr = b.load_param_u64("v_jp1_ptr");
700 let h_bits = b.load_param_u64("h_jp1_j_bits");
701
702 let h_jp1_j = reinterpret_bits::<T>(b, h_bits);
703
704 let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
706 let w_val = load_global_float::<T>(b, w_addr);
707
708 let v_new = div_float::<T>(b, w_val, h_jp1_j);
710 let vjp1_addr = b.byte_offset_addr(v_jp1_ptr, tid, elem_bytes);
711 store_global_float::<T>(b, vjp1_addr, v_new);
712 });
713
714 b.ret();
715 })
716 .build()
717 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
718}
719
720fn emit_gram_schmidt_f64(n: usize) -> SparseResult<String> {
726 emit_gram_schmidt_typed::<f64>(n, "gram_schmidt_f64")
727}
728
729fn emit_gram_schmidt_f32(n: usize) -> SparseResult<String> {
731 emit_gram_schmidt_typed::<f32>(n, "gram_schmidt_f32")
732}
733
734fn emit_gram_schmidt_typed<T: oxicuda_blas::GpuFloat>(
746 _n: usize,
747 kernel_name: &str,
748) -> SparseResult<String> {
749 let elem_bytes = T::size_u32();
750
751 KernelBuilder::new(kernel_name)
752 .target(SmVersion::Sm80)
753 .param("w_ptr", PtxType::U64)
754 .param("v_i_ptr", PtxType::U64)
755 .param("h_ij_bits", PtxType::U64)
756 .param("n", PtxType::U32)
757 .body(move |b| {
758 let gid = b.global_thread_id_x();
759 let n_param = b.load_param_u32("n");
760
761 let gid_inner = gid.clone();
762 b.if_lt_u32(gid, n_param, move |b| {
763 let tid = gid_inner;
764 let w_ptr = b.load_param_u64("w_ptr");
765 let v_i_ptr = b.load_param_u64("v_i_ptr");
766 let h_bits = b.load_param_u64("h_ij_bits");
767
768 let h_ij = reinterpret_bits::<T>(b, h_bits);
769
770 let w_addr = b.byte_offset_addr(w_ptr, tid.clone(), elem_bytes);
772 let w_val = load_global_float::<T>(b, w_addr.clone());
773
774 let vi_addr = b.byte_offset_addr(v_i_ptr, tid, elem_bytes);
775 let vi_val = load_global_float::<T>(b, vi_addr);
776
777 let proj = mul_float::<T>(b, h_ij, vi_val);
779 let w_new = sub_float::<T>(b, w_val, proj);
780
781 store_global_float::<T>(b, w_addr, w_new);
782 });
783
784 b.ret();
785 })
786 .build()
787 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
788}
789
790fn emit_dot_product_reduce_f64(_n: usize) -> SparseResult<String> {
803 emit_dot_product_reduce_typed::<f64>("dot_product_reduce_f64")
804}
805
806fn emit_dot_product_reduce_f32(_n: usize) -> SparseResult<String> {
807 emit_dot_product_reduce_typed::<f32>("dot_product_reduce_f32")
808}
809
810fn emit_dot_product_reduce_typed<T: oxicuda_blas::GpuFloat>(
811 kernel_name: &str,
812) -> SparseResult<String> {
813 let elem_bytes = T::size_u32();
814
815 KernelBuilder::new(kernel_name)
816 .target(SmVersion::Sm80)
817 .param("a_ptr", PtxType::U64)
818 .param("b_ptr", PtxType::U64)
819 .param("result_ptr", PtxType::U64)
820 .param("n", PtxType::U32)
821 .body(move |b| {
822 let gid = b.global_thread_id_x();
823 let n_param = b.load_param_u32("n");
824
825 let gid_for_lane = gid.clone();
827
828 let prod = load_float_imm::<T>(b, 0.0);
830
831 let gid_inner = gid.clone();
832 let prod_inner = prod.clone();
833 b.if_lt_u32(gid, n_param, move |b| {
834 let tid = gid_inner;
835 let a_ptr = b.load_param_u64("a_ptr");
836 let b_ptr_reg = b.load_param_u64("b_ptr");
837
838 let a_addr = b.byte_offset_addr(a_ptr, tid.clone(), elem_bytes);
839 let a_val = load_global_float::<T>(b, a_addr);
840
841 let b_addr = b.byte_offset_addr(b_ptr_reg, tid, elem_bytes);
842 let b_val = load_global_float::<T>(b, b_addr);
843
844 let p = mul_float::<T>(b, a_val, b_val);
845 let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
846 b.raw_ptx(&format!("mov.{suffix} {prod_inner}, {p};"));
847 });
848
849 let reduced = emit_warp_reduce_sum::<T>(b, prod);
851
852 let lane = b.alloc_reg(PtxType::U32);
854 b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
855
856 let is_lane_0 = b.alloc_reg(PtxType::Pred);
857 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0}, {lane}, 0;"));
858
859 let skip_label = b.fresh_label("dot_skip");
860 b.raw_ptx(&format!("@!{is_lane_0} bra {skip_label};"));
861
862 let result_ptr = b.load_param_u64("result_ptr");
863 crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
864
865 b.label(&skip_label);
866
867 b.ret();
868 })
869 .build()
870 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
871}
872
873fn emit_norm_sq_reduce_f64(_n: usize) -> SparseResult<String> {
885 emit_norm_sq_reduce_typed::<f64>("norm_sq_reduce_f64")
886}
887
888fn emit_norm_sq_reduce_f32(_n: usize) -> SparseResult<String> {
889 emit_norm_sq_reduce_typed::<f32>("norm_sq_reduce_f32")
890}
891
892fn emit_norm_sq_reduce_typed<T: oxicuda_blas::GpuFloat>(kernel_name: &str) -> SparseResult<String> {
893 let elem_bytes = T::size_u32();
894
895 KernelBuilder::new(kernel_name)
896 .target(SmVersion::Sm80)
897 .param("v_ptr", PtxType::U64)
898 .param("result_ptr", PtxType::U64)
899 .param("n", PtxType::U32)
900 .body(move |b| {
901 let gid = b.global_thread_id_x();
902 let n_param = b.load_param_u32("n");
903
904 let gid_for_lane = gid.clone();
906
907 let sq = load_float_imm::<T>(b, 0.0);
908
909 let gid_inner = gid.clone();
910 let sq_inner = sq.clone();
911 b.if_lt_u32(gid, n_param, move |b| {
912 let tid = gid_inner;
913 let v_ptr = b.load_param_u64("v_ptr");
914
915 let v_addr = b.byte_offset_addr(v_ptr, tid, elem_bytes);
916 let v_val = load_global_float::<T>(b, v_addr);
917
918 let p = mul_float::<T>(b, v_val.clone(), v_val);
919 let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
920 b.raw_ptx(&format!("mov.{suffix} {sq_inner}, {p};"));
921 });
922
923 let reduced = emit_warp_reduce_sum::<T>(b, sq);
925
926 let lane = b.alloc_reg(PtxType::U32);
928 b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
929
930 let is_lane_0 = b.alloc_reg(PtxType::Pred);
931 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0}, {lane}, 0;"));
932
933 let skip_label = b.fresh_label("norm_skip");
934 b.raw_ptx(&format!("@!{is_lane_0} bra {skip_label};"));
935
936 let result_ptr = b.load_param_u64("result_ptr");
937 crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
938
939 b.label(&skip_label);
940
941 b.ret();
942 })
943 .build()
944 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
945}
946
947fn reinterpret_bits<T: oxicuda_blas::GpuFloat>(
953 b: &mut BodyBuilder<'_>,
954 bits: Register,
955) -> Register {
956 crate::ptx_helpers::reinterpret_bits_to_float::<T>(b, bits)
957}
958
959fn sub_float<T: oxicuda_blas::GpuFloat>(
961 b: &mut BodyBuilder<'_>,
962 a: Register,
963 bv: Register,
964) -> Register {
965 if T::PTX_TYPE == PtxType::F32 {
966 let dst = b.alloc_reg(PtxType::F32);
967 b.raw_ptx(&format!("sub.rn.f32 {dst}, {a}, {bv};"));
968 dst
969 } else {
970 let dst = b.alloc_reg(PtxType::F64);
971 b.raw_ptx(&format!("sub.rn.f64 {dst}, {a}, {bv};"));
972 dst
973 }
974}
975
976fn div_float<T: oxicuda_blas::GpuFloat>(
978 b: &mut BodyBuilder<'_>,
979 a: Register,
980 bv: Register,
981) -> Register {
982 if T::PTX_TYPE == PtxType::F32 {
983 let dst = b.alloc_reg(PtxType::F32);
984 b.raw_ptx(&format!("div.rn.f32 {dst}, {a}, {bv};"));
985 dst
986 } else {
987 let dst = b.alloc_reg(PtxType::F64);
988 b.raw_ptx(&format!("div.rn.f64 {dst}, {a}, {bv};"));
989 dst
990 }
991}
992
993#[cfg(test)]
998mod tests {
999 use super::*;
1000
1001 #[test]
1004 fn lanczos_new_valid_config() {
1005 let config = LanczosConfig {
1006 max_iterations: 50,
1007 tolerance: 1e-10,
1008 num_eigenvalues: 5,
1009 which: EigenTarget::LargestMagnitude,
1010 };
1011 let plan = LanczosPlan::new(config, 100);
1012 assert!(plan.is_ok());
1013 let plan = plan.expect("test: valid config should succeed");
1014 assert_eq!(plan.dimension(), 100);
1015 }
1016
1017 #[test]
1018 fn lanczos_rejects_zero_dimension() {
1019 let config = LanczosConfig {
1020 max_iterations: 10,
1021 tolerance: 1e-6,
1022 num_eigenvalues: 3,
1023 which: EigenTarget::SmallestMagnitude,
1024 };
1025 let result = LanczosPlan::new(config, 0);
1026 assert!(result.is_err());
1027 match result {
1028 Err(SparseError::InvalidArgument(msg)) => {
1029 assert!(msg.contains("dimension"));
1030 }
1031 other => panic!("expected InvalidArgument, got: {other:?}"),
1032 }
1033 }
1034
1035 #[test]
1036 fn lanczos_rejects_zero_eigenvalues() {
1037 let config = LanczosConfig {
1038 max_iterations: 10,
1039 tolerance: 1e-6,
1040 num_eigenvalues: 0,
1041 which: EigenTarget::LargestAlgebraic,
1042 };
1043 let result = LanczosPlan::new(config, 100);
1044 assert!(result.is_err());
1045 }
1046
1047 #[test]
1048 fn lanczos_rejects_iterations_less_than_eigenvalues() {
1049 let config = LanczosConfig {
1050 max_iterations: 3,
1051 tolerance: 1e-6,
1052 num_eigenvalues: 10,
1053 which: EigenTarget::SmallestAlgebraic,
1054 };
1055 let result = LanczosPlan::new(config, 100);
1056 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1057 }
1058
1059 #[test]
1060 fn lanczos_rejects_iterations_greater_than_n() {
1061 let config = LanczosConfig {
1062 max_iterations: 200,
1063 tolerance: 1e-6,
1064 num_eigenvalues: 5,
1065 which: EigenTarget::LargestMagnitude,
1066 };
1067 let result = LanczosPlan::new(config, 100);
1068 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1069 }
1070
1071 #[test]
1072 fn lanczos_rejects_non_positive_tolerance() {
1073 let config = LanczosConfig {
1074 max_iterations: 50,
1075 tolerance: 0.0,
1076 num_eigenvalues: 5,
1077 which: EigenTarget::LargestMagnitude,
1078 };
1079 let result = LanczosPlan::new(config, 100);
1080 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1081
1082 let config_neg = LanczosConfig {
1083 max_iterations: 50,
1084 tolerance: -1e-6,
1085 num_eigenvalues: 5,
1086 which: EigenTarget::LargestMagnitude,
1087 };
1088 let result_neg = LanczosPlan::new(config_neg, 100);
1089 assert!(matches!(result_neg, Err(SparseError::InvalidArgument(_))));
1090 }
1091
1092 #[test]
1095 fn lanczos_step_ptx_f64_generates() {
1096 let config = LanczosConfig {
1097 max_iterations: 30,
1098 tolerance: 1e-10,
1099 num_eigenvalues: 5,
1100 which: EigenTarget::LargestMagnitude,
1101 };
1102 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1103 let ptx = plan.generate_lanczos_step_ptx();
1104 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1105 let ptx_str = ptx.expect("test: PTX gen should succeed");
1106 assert!(ptx_str.contains(".entry lanczos_step_f64"));
1107 assert!(ptx_str.contains(".target sm_80"));
1108 assert!(ptx_str.contains("w_ptr"));
1110 assert!(ptx_str.contains("v_j_ptr"));
1111 }
1112
1113 #[test]
1114 fn lanczos_step_ptx_f32_generates() {
1115 let config = LanczosConfig {
1116 max_iterations: 20,
1117 tolerance: 1e-6,
1118 num_eigenvalues: 3,
1119 which: EigenTarget::SmallestMagnitude,
1120 };
1121 let plan = LanczosPlan::new(config, 500).expect("test: valid config");
1122 let ptx = plan.generate_lanczos_step_ptx_f32();
1123 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1124 let ptx_str = ptx.expect("test: PTX gen should succeed");
1125 assert!(ptx_str.contains(".entry lanczos_step_f32"));
1126 }
1127
1128 #[test]
1129 fn lanczos_reorthogonalize_ptx_generates() {
1130 let config = LanczosConfig {
1131 max_iterations: 30,
1132 tolerance: 1e-10,
1133 num_eigenvalues: 5,
1134 which: EigenTarget::LargestAlgebraic,
1135 };
1136 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1137 let ptx = plan.generate_reorthogonalize_ptx();
1138 assert!(ptx.is_ok(), "Reorthogonalize PTX failed: {ptx:?}");
1139 let ptx_str = ptx.expect("test: PTX gen should succeed");
1140 assert!(ptx_str.contains(".entry reorthogonalize_f64"));
1141 assert!(ptx_str.contains("w_ptr"));
1142 }
1143
1144 #[test]
1147 fn arnoldi_new_valid_config() {
1148 let config = ArnoldiConfig {
1149 max_iterations: 50,
1150 tolerance: 1e-10,
1151 num_eigenvalues: 5,
1152 which: EigenTarget::LargestMagnitude,
1153 };
1154 let plan = ArnoldiPlan::new(config, 200);
1155 assert!(plan.is_ok());
1156 let plan = plan.expect("test: valid config should succeed");
1157 assert_eq!(plan.dimension(), 200);
1158 }
1159
1160 #[test]
1161 fn arnoldi_rejects_invalid_config() {
1162 let config = ArnoldiConfig {
1164 max_iterations: 10,
1165 tolerance: 1e-6,
1166 num_eigenvalues: 3,
1167 which: EigenTarget::LargestMagnitude,
1168 };
1169 assert!(ArnoldiPlan::new(config, 0).is_err());
1170
1171 let config2 = ArnoldiConfig {
1173 max_iterations: 500,
1174 tolerance: 1e-6,
1175 num_eigenvalues: 3,
1176 which: EigenTarget::SmallestMagnitude,
1177 };
1178 assert!(ArnoldiPlan::new(config2, 100).is_err());
1179
1180 let config3 = ArnoldiConfig {
1182 max_iterations: 5,
1183 tolerance: 1e-6,
1184 num_eigenvalues: 20,
1185 which: EigenTarget::LargestAlgebraic,
1186 };
1187 assert!(ArnoldiPlan::new(config3, 100).is_err());
1188 }
1189
1190 #[test]
1193 fn arnoldi_step_ptx_f64_generates() {
1194 let config = ArnoldiConfig {
1195 max_iterations: 30,
1196 tolerance: 1e-10,
1197 num_eigenvalues: 5,
1198 which: EigenTarget::LargestMagnitude,
1199 };
1200 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1201 let ptx = plan.generate_arnoldi_step_ptx();
1202 assert!(ptx.is_ok(), "Arnoldi PTX failed: {ptx:?}");
1203 let ptx_str = ptx.expect("test: PTX gen should succeed");
1204 assert!(ptx_str.contains(".entry arnoldi_step_f64"));
1205 assert!(ptx_str.contains("w_ptr"));
1206 }
1207
1208 #[test]
1209 fn arnoldi_step_ptx_f32_generates() {
1210 let config = ArnoldiConfig {
1211 max_iterations: 20,
1212 tolerance: 1e-6,
1213 num_eigenvalues: 3,
1214 which: EigenTarget::SmallestAlgebraic,
1215 };
1216 let plan = ArnoldiPlan::new(config, 300).expect("test: valid config");
1217 let ptx = plan.generate_arnoldi_step_ptx_f32();
1218 assert!(ptx.is_ok(), "Arnoldi f32 PTX failed: {ptx:?}");
1219 let ptx_str = ptx.expect("test: PTX gen should succeed");
1220 assert!(ptx_str.contains(".entry arnoldi_step_f32"));
1221 }
1222
1223 #[test]
1224 fn arnoldi_gram_schmidt_ptx_generates() {
1225 let config = ArnoldiConfig {
1226 max_iterations: 30,
1227 tolerance: 1e-10,
1228 num_eigenvalues: 5,
1229 which: EigenTarget::LargestMagnitude,
1230 };
1231 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1232 let ptx = plan.generate_gram_schmidt_ptx();
1233 assert!(ptx.is_ok(), "Gram-Schmidt PTX failed: {ptx:?}");
1234 let ptx_str = ptx.expect("test: PTX gen should succeed");
1235 assert!(ptx_str.contains(".entry gram_schmidt_f64"));
1236 }
1237
1238 #[test]
1241 fn lanczos_workspace_size_f64() {
1242 let config = LanczosConfig {
1243 max_iterations: 50,
1244 tolerance: 1e-10,
1245 num_eigenvalues: 5,
1246 which: EigenTarget::LargestMagnitude,
1247 };
1248 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1249 let ws = plan.workspace_bytes_f64();
1250 assert_eq!(ws, 416_800);
1252 }
1253
1254 #[test]
1255 fn lanczos_workspace_size_f32() {
1256 let config = LanczosConfig {
1257 max_iterations: 50,
1258 tolerance: 1e-10,
1259 num_eigenvalues: 5,
1260 which: EigenTarget::LargestMagnitude,
1261 };
1262 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1263 let ws = plan.workspace_bytes_f32();
1264 assert_eq!(ws, 208_400);
1266 }
1267
1268 #[test]
1269 fn arnoldi_workspace_size_f64() {
1270 let config = ArnoldiConfig {
1271 max_iterations: 30,
1272 tolerance: 1e-10,
1273 num_eigenvalues: 5,
1274 which: EigenTarget::LargestMagnitude,
1275 };
1276 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1277 let ws = plan.workspace_bytes_f64();
1278 assert_eq!(ws, 135_440);
1282 }
1283
1284 #[test]
1287 fn lanczos_result_tridiagonal_structure() {
1288 let result = LanczosResult {
1290 eigenvalues: vec![5.0, 3.0, 1.0],
1291 alpha: vec![4.0, 3.5, 2.0, 1.5, 1.0], beta: vec![1.2, 0.8, 0.5, 0.3], iterations: 5,
1294 converged: true,
1295 };
1296 assert_eq!(result.alpha.len(), 5);
1298 assert_eq!(result.beta.len(), result.alpha.len() - 1);
1299 assert!(result.converged);
1300 assert_eq!(result.iterations, 5);
1301 }
1302
1303 #[test]
1306 #[allow(clippy::needless_range_loop)]
1307 fn arnoldi_result_hessenberg_structure() {
1308 let k = 4;
1310 let mut h = vec![vec![0.0; k]; k + 1]; for j in 0..k {
1313 for i in 0..=j + 1 {
1314 h[i][j] = (i + j + 1) as f64;
1315 }
1316 }
1317 for j in 0..k {
1319 for i in (j + 2)..(k + 1) {
1320 assert!(
1321 (h[i][j]).abs() < 1e-15,
1322 "h[{i}][{j}] should be zero in upper Hessenberg"
1323 );
1324 }
1325 }
1326
1327 let result = ArnoldiResult {
1328 eigenvalues: vec![(3.0, 0.5), (3.0, -0.5), (1.0, 0.0)],
1329 hessenberg: h,
1330 iterations: k,
1331 converged: true,
1332 };
1333 assert_eq!(result.hessenberg.len(), k + 1);
1334 assert_eq!(result.hessenberg[0].len(), k);
1335 assert!(result.converged);
1336 let (r1, i1) = result.eigenvalues[0];
1338 let (r2, i2) = result.eigenvalues[1];
1339 assert!((r1 - r2).abs() < 1e-15, "conjugate pair: same real part");
1340 assert!(
1341 (i1 + i2).abs() < 1e-15,
1342 "conjugate pair: opposite imag part"
1343 );
1344 }
1345
1346 #[test]
1349 fn eigen_target_variants() {
1350 let targets = [
1352 EigenTarget::LargestMagnitude,
1353 EigenTarget::SmallestMagnitude,
1354 EigenTarget::LargestAlgebraic,
1355 EigenTarget::SmallestAlgebraic,
1356 ];
1357 for i in 0..targets.len() {
1358 for j in (i + 1)..targets.len() {
1359 assert_ne!(targets[i], targets[j]);
1360 }
1361 }
1362 }
1363
1364 #[test]
1367 fn dot_product_reduce_ptx_f64_generates() {
1368 let ptx = emit_dot_product_reduce_f64(1000);
1369 assert!(ptx.is_ok(), "dot product PTX failed: {ptx:?}");
1370 let ptx_str = ptx.expect("test: PTX gen should succeed");
1371 assert!(ptx_str.contains(".entry dot_product_reduce_f64"));
1372 }
1373
1374 #[test]
1375 fn dot_product_reduce_ptx_f32_generates() {
1376 let ptx = emit_dot_product_reduce_f32(1000);
1377 assert!(ptx.is_ok());
1378 let ptx_str = ptx.expect("test: PTX gen should succeed");
1379 assert!(ptx_str.contains(".entry dot_product_reduce_f32"));
1380 }
1381
1382 #[test]
1383 fn norm_sq_reduce_ptx_generates() {
1384 let ptx_f64 = emit_norm_sq_reduce_f64(1000);
1385 assert!(ptx_f64.is_ok());
1386 let ptx_str = ptx_f64.expect("test: PTX gen should succeed");
1387 assert!(ptx_str.contains(".entry norm_sq_reduce_f64"));
1388
1389 let ptx_f32 = emit_norm_sq_reduce_f32(1000);
1390 assert!(ptx_f32.is_ok());
1391 let ptx_str_f32 = ptx_f32.expect("test: PTX gen should succeed");
1392 assert!(ptx_str_f32.contains(".entry norm_sq_reduce_f32"));
1393 }
1394
1395 #[test]
1398 fn plan_config_accessors() {
1399 let lanczos_config = LanczosConfig {
1400 max_iterations: 40,
1401 tolerance: 1e-8,
1402 num_eigenvalues: 10,
1403 which: EigenTarget::SmallestAlgebraic,
1404 };
1405 let plan = LanczosPlan::new(lanczos_config.clone(), 200).expect("test: valid config");
1406 assert_eq!(plan.config().max_iterations, 40);
1407 assert_eq!(plan.config().num_eigenvalues, 10);
1408 assert!((plan.config().tolerance - 1e-8).abs() < 1e-15);
1409 assert_eq!(plan.config().which, EigenTarget::SmallestAlgebraic);
1410
1411 let arnoldi_config = ArnoldiConfig {
1412 max_iterations: 25,
1413 tolerance: 1e-12,
1414 num_eigenvalues: 6,
1415 which: EigenTarget::LargestAlgebraic,
1416 };
1417 let aplan = ArnoldiPlan::new(arnoldi_config, 300).expect("test: valid config");
1418 assert_eq!(aplan.config().max_iterations, 25);
1419 assert_eq!(aplan.config().num_eigenvalues, 6);
1420 assert_eq!(aplan.dimension(), 300);
1421 }
1422}