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 not_lane_0 = b.alloc_reg(PtxType::Pred);
860 b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
861
862 let skip_label = b.fresh_label("dot_skip");
863 b.branch_if(not_lane_0, &skip_label);
864
865 let result_ptr = b.load_param_u64("result_ptr");
866 crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
867
868 b.label(&skip_label);
869
870 b.ret();
871 })
872 .build()
873 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
874}
875
876fn emit_norm_sq_reduce_f64(_n: usize) -> SparseResult<String> {
888 emit_norm_sq_reduce_typed::<f64>("norm_sq_reduce_f64")
889}
890
891fn emit_norm_sq_reduce_f32(_n: usize) -> SparseResult<String> {
892 emit_norm_sq_reduce_typed::<f32>("norm_sq_reduce_f32")
893}
894
895fn emit_norm_sq_reduce_typed<T: oxicuda_blas::GpuFloat>(kernel_name: &str) -> SparseResult<String> {
896 let elem_bytes = T::size_u32();
897
898 KernelBuilder::new(kernel_name)
899 .target(SmVersion::Sm80)
900 .param("v_ptr", PtxType::U64)
901 .param("result_ptr", PtxType::U64)
902 .param("n", PtxType::U32)
903 .body(move |b| {
904 let gid = b.global_thread_id_x();
905 let n_param = b.load_param_u32("n");
906
907 let gid_for_lane = gid.clone();
909
910 let sq = load_float_imm::<T>(b, 0.0);
911
912 let gid_inner = gid.clone();
913 let sq_inner = sq.clone();
914 b.if_lt_u32(gid, n_param, move |b| {
915 let tid = gid_inner;
916 let v_ptr = b.load_param_u64("v_ptr");
917
918 let v_addr = b.byte_offset_addr(v_ptr, tid, elem_bytes);
919 let v_val = load_global_float::<T>(b, v_addr);
920
921 let p = mul_float::<T>(b, v_val.clone(), v_val);
922 let suffix = if T::SIZE == 8 { "f64" } else { "f32" };
923 b.raw_ptx(&format!("mov.{suffix} {sq_inner}, {p};"));
924 });
925
926 let reduced = emit_warp_reduce_sum::<T>(b, sq);
928
929 let lane = b.alloc_reg(PtxType::U32);
931 b.raw_ptx(&format!("and.b32 {lane}, {gid_for_lane}, 31;"));
932
933 let not_lane_0 = b.alloc_reg(PtxType::Pred);
937 b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
938
939 let skip_label = b.fresh_label("norm_skip");
940 b.branch_if(not_lane_0, &skip_label);
941
942 let result_ptr = b.load_param_u64("result_ptr");
943 crate::ptx_helpers::emit_atomic_add_float::<T>(b, result_ptr, reduced);
944
945 b.label(&skip_label);
946
947 b.ret();
948 })
949 .build()
950 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
951}
952
953fn reinterpret_bits<T: oxicuda_blas::GpuFloat>(
959 b: &mut BodyBuilder<'_>,
960 bits: Register,
961) -> Register {
962 crate::ptx_helpers::reinterpret_bits_to_float::<T>(b, bits)
963}
964
965fn sub_float<T: oxicuda_blas::GpuFloat>(
967 b: &mut BodyBuilder<'_>,
968 a: Register,
969 bv: Register,
970) -> Register {
971 if T::PTX_TYPE == PtxType::F32 {
972 let dst = b.alloc_reg(PtxType::F32);
973 b.raw_ptx(&format!("sub.rn.f32 {dst}, {a}, {bv};"));
974 dst
975 } else {
976 let dst = b.alloc_reg(PtxType::F64);
977 b.raw_ptx(&format!("sub.rn.f64 {dst}, {a}, {bv};"));
978 dst
979 }
980}
981
982fn div_float<T: oxicuda_blas::GpuFloat>(
984 b: &mut BodyBuilder<'_>,
985 a: Register,
986 bv: Register,
987) -> Register {
988 if T::PTX_TYPE == PtxType::F32 {
989 let dst = b.alloc_reg(PtxType::F32);
990 b.raw_ptx(&format!("div.rn.f32 {dst}, {a}, {bv};"));
991 dst
992 } else {
993 let dst = b.alloc_reg(PtxType::F64);
994 b.raw_ptx(&format!("div.rn.f64 {dst}, {a}, {bv};"));
995 dst
996 }
997}
998
999#[cfg(test)]
1004mod tests {
1005 use super::*;
1006 use crate::ptx_helpers::test_support::assert_assembles_and_clean;
1007
1008 #[test]
1012 fn krylov_reductions_f32_f64_assemble_sm86() {
1013 let dot_f32 = emit_dot_product_reduce_f32(1024).expect("dot f32");
1014 assert_assembles_and_clean("krylov_dot_f32", &dot_f32);
1015 let dot_f64 = emit_dot_product_reduce_f64(1024).expect("dot f64");
1016 assert_assembles_and_clean("krylov_dot_f64", &dot_f64);
1017
1018 let norm_f32 = emit_norm_sq_reduce_f32(1024).expect("norm f32");
1019 assert_assembles_and_clean("krylov_norm_f32", &norm_f32);
1020 let norm_f64 = emit_norm_sq_reduce_f64(1024).expect("norm f64");
1021 assert_assembles_and_clean("krylov_norm_f64", &norm_f64);
1022 assert!(
1023 !dot_f64.contains("0F00000000") && !norm_f64.contains("0F00000000"),
1024 "f64 Krylov reduction kernels must not materialize an f32 0.0 immediate"
1025 );
1026 }
1027
1028 #[test]
1031 fn lanczos_new_valid_config() {
1032 let config = LanczosConfig {
1033 max_iterations: 50,
1034 tolerance: 1e-10,
1035 num_eigenvalues: 5,
1036 which: EigenTarget::LargestMagnitude,
1037 };
1038 let plan = LanczosPlan::new(config, 100);
1039 assert!(plan.is_ok());
1040 let plan = plan.expect("test: valid config should succeed");
1041 assert_eq!(plan.dimension(), 100);
1042 }
1043
1044 #[test]
1045 fn lanczos_rejects_zero_dimension() {
1046 let config = LanczosConfig {
1047 max_iterations: 10,
1048 tolerance: 1e-6,
1049 num_eigenvalues: 3,
1050 which: EigenTarget::SmallestMagnitude,
1051 };
1052 let result = LanczosPlan::new(config, 0);
1053 assert!(result.is_err());
1054 match result {
1055 Err(SparseError::InvalidArgument(msg)) => {
1056 assert!(msg.contains("dimension"));
1057 }
1058 other => panic!("expected InvalidArgument, got: {other:?}"),
1059 }
1060 }
1061
1062 #[test]
1063 fn lanczos_rejects_zero_eigenvalues() {
1064 let config = LanczosConfig {
1065 max_iterations: 10,
1066 tolerance: 1e-6,
1067 num_eigenvalues: 0,
1068 which: EigenTarget::LargestAlgebraic,
1069 };
1070 let result = LanczosPlan::new(config, 100);
1071 assert!(result.is_err());
1072 }
1073
1074 #[test]
1075 fn lanczos_rejects_iterations_less_than_eigenvalues() {
1076 let config = LanczosConfig {
1077 max_iterations: 3,
1078 tolerance: 1e-6,
1079 num_eigenvalues: 10,
1080 which: EigenTarget::SmallestAlgebraic,
1081 };
1082 let result = LanczosPlan::new(config, 100);
1083 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1084 }
1085
1086 #[test]
1087 fn lanczos_rejects_iterations_greater_than_n() {
1088 let config = LanczosConfig {
1089 max_iterations: 200,
1090 tolerance: 1e-6,
1091 num_eigenvalues: 5,
1092 which: EigenTarget::LargestMagnitude,
1093 };
1094 let result = LanczosPlan::new(config, 100);
1095 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1096 }
1097
1098 #[test]
1099 fn lanczos_rejects_non_positive_tolerance() {
1100 let config = LanczosConfig {
1101 max_iterations: 50,
1102 tolerance: 0.0,
1103 num_eigenvalues: 5,
1104 which: EigenTarget::LargestMagnitude,
1105 };
1106 let result = LanczosPlan::new(config, 100);
1107 assert!(matches!(result, Err(SparseError::InvalidArgument(_))));
1108
1109 let config_neg = LanczosConfig {
1110 max_iterations: 50,
1111 tolerance: -1e-6,
1112 num_eigenvalues: 5,
1113 which: EigenTarget::LargestMagnitude,
1114 };
1115 let result_neg = LanczosPlan::new(config_neg, 100);
1116 assert!(matches!(result_neg, Err(SparseError::InvalidArgument(_))));
1117 }
1118
1119 #[test]
1122 fn lanczos_step_ptx_f64_generates() {
1123 let config = LanczosConfig {
1124 max_iterations: 30,
1125 tolerance: 1e-10,
1126 num_eigenvalues: 5,
1127 which: EigenTarget::LargestMagnitude,
1128 };
1129 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1130 let ptx = plan.generate_lanczos_step_ptx();
1131 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1132 let ptx_str = ptx.expect("test: PTX gen should succeed");
1133 assert!(ptx_str.contains(".entry lanczos_step_f64"));
1134 assert!(ptx_str.contains(".target sm_80"));
1135 assert!(ptx_str.contains("w_ptr"));
1137 assert!(ptx_str.contains("v_j_ptr"));
1138 }
1139
1140 #[test]
1141 fn lanczos_step_ptx_f32_generates() {
1142 let config = LanczosConfig {
1143 max_iterations: 20,
1144 tolerance: 1e-6,
1145 num_eigenvalues: 3,
1146 which: EigenTarget::SmallestMagnitude,
1147 };
1148 let plan = LanczosPlan::new(config, 500).expect("test: valid config");
1149 let ptx = plan.generate_lanczos_step_ptx_f32();
1150 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1151 let ptx_str = ptx.expect("test: PTX gen should succeed");
1152 assert!(ptx_str.contains(".entry lanczos_step_f32"));
1153 }
1154
1155 #[test]
1156 fn lanczos_reorthogonalize_ptx_generates() {
1157 let config = LanczosConfig {
1158 max_iterations: 30,
1159 tolerance: 1e-10,
1160 num_eigenvalues: 5,
1161 which: EigenTarget::LargestAlgebraic,
1162 };
1163 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1164 let ptx = plan.generate_reorthogonalize_ptx();
1165 assert!(ptx.is_ok(), "Reorthogonalize PTX failed: {ptx:?}");
1166 let ptx_str = ptx.expect("test: PTX gen should succeed");
1167 assert!(ptx_str.contains(".entry reorthogonalize_f64"));
1168 assert!(ptx_str.contains("w_ptr"));
1169 }
1170
1171 #[test]
1174 fn arnoldi_new_valid_config() {
1175 let config = ArnoldiConfig {
1176 max_iterations: 50,
1177 tolerance: 1e-10,
1178 num_eigenvalues: 5,
1179 which: EigenTarget::LargestMagnitude,
1180 };
1181 let plan = ArnoldiPlan::new(config, 200);
1182 assert!(plan.is_ok());
1183 let plan = plan.expect("test: valid config should succeed");
1184 assert_eq!(plan.dimension(), 200);
1185 }
1186
1187 #[test]
1188 fn arnoldi_rejects_invalid_config() {
1189 let config = ArnoldiConfig {
1191 max_iterations: 10,
1192 tolerance: 1e-6,
1193 num_eigenvalues: 3,
1194 which: EigenTarget::LargestMagnitude,
1195 };
1196 assert!(ArnoldiPlan::new(config, 0).is_err());
1197
1198 let config2 = ArnoldiConfig {
1200 max_iterations: 500,
1201 tolerance: 1e-6,
1202 num_eigenvalues: 3,
1203 which: EigenTarget::SmallestMagnitude,
1204 };
1205 assert!(ArnoldiPlan::new(config2, 100).is_err());
1206
1207 let config3 = ArnoldiConfig {
1209 max_iterations: 5,
1210 tolerance: 1e-6,
1211 num_eigenvalues: 20,
1212 which: EigenTarget::LargestAlgebraic,
1213 };
1214 assert!(ArnoldiPlan::new(config3, 100).is_err());
1215 }
1216
1217 #[test]
1220 fn arnoldi_step_ptx_f64_generates() {
1221 let config = ArnoldiConfig {
1222 max_iterations: 30,
1223 tolerance: 1e-10,
1224 num_eigenvalues: 5,
1225 which: EigenTarget::LargestMagnitude,
1226 };
1227 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1228 let ptx = plan.generate_arnoldi_step_ptx();
1229 assert!(ptx.is_ok(), "Arnoldi PTX failed: {ptx:?}");
1230 let ptx_str = ptx.expect("test: PTX gen should succeed");
1231 assert!(ptx_str.contains(".entry arnoldi_step_f64"));
1232 assert!(ptx_str.contains("w_ptr"));
1233 }
1234
1235 #[test]
1236 fn arnoldi_step_ptx_f32_generates() {
1237 let config = ArnoldiConfig {
1238 max_iterations: 20,
1239 tolerance: 1e-6,
1240 num_eigenvalues: 3,
1241 which: EigenTarget::SmallestAlgebraic,
1242 };
1243 let plan = ArnoldiPlan::new(config, 300).expect("test: valid config");
1244 let ptx = plan.generate_arnoldi_step_ptx_f32();
1245 assert!(ptx.is_ok(), "Arnoldi f32 PTX failed: {ptx:?}");
1246 let ptx_str = ptx.expect("test: PTX gen should succeed");
1247 assert!(ptx_str.contains(".entry arnoldi_step_f32"));
1248 }
1249
1250 #[test]
1251 fn arnoldi_gram_schmidt_ptx_generates() {
1252 let config = ArnoldiConfig {
1253 max_iterations: 30,
1254 tolerance: 1e-10,
1255 num_eigenvalues: 5,
1256 which: EigenTarget::LargestMagnitude,
1257 };
1258 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1259 let ptx = plan.generate_gram_schmidt_ptx();
1260 assert!(ptx.is_ok(), "Gram-Schmidt PTX failed: {ptx:?}");
1261 let ptx_str = ptx.expect("test: PTX gen should succeed");
1262 assert!(ptx_str.contains(".entry gram_schmidt_f64"));
1263 }
1264
1265 #[test]
1268 fn lanczos_workspace_size_f64() {
1269 let config = LanczosConfig {
1270 max_iterations: 50,
1271 tolerance: 1e-10,
1272 num_eigenvalues: 5,
1273 which: EigenTarget::LargestMagnitude,
1274 };
1275 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1276 let ws = plan.workspace_bytes_f64();
1277 assert_eq!(ws, 416_800);
1279 }
1280
1281 #[test]
1282 fn lanczos_workspace_size_f32() {
1283 let config = LanczosConfig {
1284 max_iterations: 50,
1285 tolerance: 1e-10,
1286 num_eigenvalues: 5,
1287 which: EigenTarget::LargestMagnitude,
1288 };
1289 let plan = LanczosPlan::new(config, 1000).expect("test: valid config");
1290 let ws = plan.workspace_bytes_f32();
1291 assert_eq!(ws, 208_400);
1293 }
1294
1295 #[test]
1296 fn arnoldi_workspace_size_f64() {
1297 let config = ArnoldiConfig {
1298 max_iterations: 30,
1299 tolerance: 1e-10,
1300 num_eigenvalues: 5,
1301 which: EigenTarget::LargestMagnitude,
1302 };
1303 let plan = ArnoldiPlan::new(config, 500).expect("test: valid config");
1304 let ws = plan.workspace_bytes_f64();
1305 assert_eq!(ws, 135_440);
1309 }
1310
1311 #[test]
1314 fn lanczos_result_tridiagonal_structure() {
1315 let result = LanczosResult {
1317 eigenvalues: vec![5.0, 3.0, 1.0],
1318 alpha: vec![4.0, 3.5, 2.0, 1.5, 1.0], beta: vec![1.2, 0.8, 0.5, 0.3], iterations: 5,
1321 converged: true,
1322 };
1323 assert_eq!(result.alpha.len(), 5);
1325 assert_eq!(result.beta.len(), result.alpha.len() - 1);
1326 assert!(result.converged);
1327 assert_eq!(result.iterations, 5);
1328 }
1329
1330 #[test]
1333 #[allow(clippy::needless_range_loop)]
1334 fn arnoldi_result_hessenberg_structure() {
1335 let k = 4;
1337 let mut h = vec![vec![0.0; k]; k + 1]; for j in 0..k {
1340 for i in 0..=j + 1 {
1341 h[i][j] = (i + j + 1) as f64;
1342 }
1343 }
1344 for j in 0..k {
1346 for i in (j + 2)..(k + 1) {
1347 assert!(
1348 (h[i][j]).abs() < 1e-15,
1349 "h[{i}][{j}] should be zero in upper Hessenberg"
1350 );
1351 }
1352 }
1353
1354 let result = ArnoldiResult {
1355 eigenvalues: vec![(3.0, 0.5), (3.0, -0.5), (1.0, 0.0)],
1356 hessenberg: h,
1357 iterations: k,
1358 converged: true,
1359 };
1360 assert_eq!(result.hessenberg.len(), k + 1);
1361 assert_eq!(result.hessenberg[0].len(), k);
1362 assert!(result.converged);
1363 let (r1, i1) = result.eigenvalues[0];
1365 let (r2, i2) = result.eigenvalues[1];
1366 assert!((r1 - r2).abs() < 1e-15, "conjugate pair: same real part");
1367 assert!(
1368 (i1 + i2).abs() < 1e-15,
1369 "conjugate pair: opposite imag part"
1370 );
1371 }
1372
1373 #[test]
1376 fn eigen_target_variants() {
1377 let targets = [
1379 EigenTarget::LargestMagnitude,
1380 EigenTarget::SmallestMagnitude,
1381 EigenTarget::LargestAlgebraic,
1382 EigenTarget::SmallestAlgebraic,
1383 ];
1384 for i in 0..targets.len() {
1385 for j in (i + 1)..targets.len() {
1386 assert_ne!(targets[i], targets[j]);
1387 }
1388 }
1389 }
1390
1391 #[test]
1394 fn dot_product_reduce_ptx_f64_generates() {
1395 let ptx = emit_dot_product_reduce_f64(1000);
1396 assert!(ptx.is_ok(), "dot product PTX failed: {ptx:?}");
1397 let ptx_str = ptx.expect("test: PTX gen should succeed");
1398 assert!(ptx_str.contains(".entry dot_product_reduce_f64"));
1399 }
1400
1401 #[test]
1402 fn dot_product_reduce_ptx_f32_generates() {
1403 let ptx = emit_dot_product_reduce_f32(1000);
1404 assert!(ptx.is_ok());
1405 let ptx_str = ptx.expect("test: PTX gen should succeed");
1406 assert!(ptx_str.contains(".entry dot_product_reduce_f32"));
1407 }
1408
1409 #[test]
1410 fn norm_sq_reduce_ptx_generates() {
1411 let ptx_f64 = emit_norm_sq_reduce_f64(1000);
1412 assert!(ptx_f64.is_ok());
1413 let ptx_str = ptx_f64.expect("test: PTX gen should succeed");
1414 assert!(ptx_str.contains(".entry norm_sq_reduce_f64"));
1415
1416 let ptx_f32 = emit_norm_sq_reduce_f32(1000);
1417 assert!(ptx_f32.is_ok());
1418 let ptx_str_f32 = ptx_f32.expect("test: PTX gen should succeed");
1419 assert!(ptx_str_f32.contains(".entry norm_sq_reduce_f32"));
1420 }
1421
1422 #[test]
1425 fn plan_config_accessors() {
1426 let lanczos_config = LanczosConfig {
1427 max_iterations: 40,
1428 tolerance: 1e-8,
1429 num_eigenvalues: 10,
1430 which: EigenTarget::SmallestAlgebraic,
1431 };
1432 let plan = LanczosPlan::new(lanczos_config.clone(), 200).expect("test: valid config");
1433 assert_eq!(plan.config().max_iterations, 40);
1434 assert_eq!(plan.config().num_eigenvalues, 10);
1435 assert!((plan.config().tolerance - 1e-8).abs() < 1e-15);
1436 assert_eq!(plan.config().which, EigenTarget::SmallestAlgebraic);
1437
1438 let arnoldi_config = ArnoldiConfig {
1439 max_iterations: 25,
1440 tolerance: 1e-12,
1441 num_eigenvalues: 6,
1442 which: EigenTarget::LargestAlgebraic,
1443 };
1444 let aplan = ArnoldiPlan::new(arnoldi_config, 300).expect("test: valid config");
1445 assert_eq!(aplan.config().max_iterations, 25);
1446 assert_eq!(aplan.config().num_eigenvalues, 6);
1447 assert_eq!(aplan.dimension(), 300);
1448 }
1449}