1#![cfg_attr(not(feature = "cpu"), allow(dead_code))]
70
71use std::sync::Arc;
72
73use rlx_ir::{DType, Graph, Node, NodeId, Op, OpExtension, Shape, VjpContext, register_op};
74
75#[cfg(feature = "cpu")]
76use rlx_cpu::op_registry::{CpuKernel, CpuTensorMut, CpuTensorRef, register_cpu_kernel};
77
78pub const SPARSE_LU_SOLVE: &str = "rlx_sparse.lu_solve";
82pub const SPARSE_MAT_VEC: &str = "rlx_sparse.mat_vec";
83pub const SPARSE_CG_SOLVE: &str = "rlx_sparse.cg_solve";
84
85pub const SPARSE_VALUES_GRAD: &str = "rlx_sparse.values_grad";
91
92pub const SPARSE_LU_SOLVE_GENERAL: &str = "rlx_sparse.lu_solve_general";
98
99pub const SPARSE_GMRES_SOLVE: &str = "rlx_sparse.gmres_solve";
104
105pub const SPARSE_TRANSPOSE_VALUES: &str = "rlx_sparse.transpose_values";
114
115pub const SPARSE_PCG_SOLVE: &str = "rlx_sparse.pcg_solve";
122
123pub const SPARSE_BICGSTAB_SOLVE: &str = "rlx_sparse.bicgstab_solve";
128
129pub const SPARSE_ILU_PCG_SOLVE: &str = "rlx_sparse.ilu_pcg_solve";
135
136pub const SPARSE_CHOLESKY_SOLVE: &str = "rlx_sparse.cholesky_solve";
141
142pub const SPARSE_LSQR_SOLVE: &str = "rlx_sparse.lsqr_solve";
148
149pub const SPARSE_SPGEMM: &str = "rlx_sparse.spgemm";
156
157#[cfg(feature = "cpu")]
166mod algos {
167 pub fn lu_solve(
168 values: &[f64],
169 col_idx: &[i32],
170 row_ptr: &[i32],
171 b: &[f64],
172 out: &mut [f64],
173 ) -> Result<(), String> {
174 let n = b.len();
175 if out.len() != n {
176 return Err(format!("sparse_lu: output len {} != b len {n}", out.len()));
177 }
178 if row_ptr.len() != n + 1 {
179 return Err(format!(
180 "sparse_lu: row_ptr len {} != n+1 ({})",
181 row_ptr.len(),
182 n + 1
183 ));
184 }
185 let mut a_dense = vec![0f64; n * n];
186 for r in 0..n {
187 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
188 a_dense[r * n + col_idx[k] as usize] = values[k];
189 }
190 }
191 let mut b_copy = b.to_vec();
192 let info = rlx_cpu::blas::dgesv(&mut a_dense, &mut b_copy, n, 1);
193 if info != 0 {
194 return Err(format!(
195 "sparse_lu: dgesv returned info={info} (>0 → singular)"
196 ));
197 }
198 out.copy_from_slice(&b_copy);
199 Ok(())
200 }
201
202 pub fn mat_vec(
203 values: &[f64],
204 col_idx: &[i32],
205 row_ptr: &[i32],
206 x: &[f64],
207 out: &mut [f64],
208 ) -> Result<(), String> {
209 let n = x.len();
210 if out.len() != n {
211 return Err(format!("mat_vec: output len {} != x len {n}", out.len()));
212 }
213 if row_ptr.len() != n + 1 {
214 return Err(format!(
215 "mat_vec: row_ptr len {} != n+1 ({})",
216 row_ptr.len(),
217 n + 1
218 ));
219 }
220 for r in 0..n {
221 let mut acc = 0f64;
222 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
223 acc += values[k] * x[col_idx[k] as usize];
224 }
225 out[r] = acc;
226 }
227 Ok(())
228 }
229
230 pub fn values_grad(
236 col_idx: &[i32],
237 row_ptr: &[i32],
238 u: &[f64],
239 v: &[f64],
240 out: &mut [f64],
241 ) -> Result<(), String> {
242 let n = u.len();
243 let nnz = col_idx.len();
244 if out.len() != nnz {
245 return Err(format!("values_grad: out len {} != nnz {nnz}", out.len()));
246 }
247 if row_ptr.len() != n + 1 {
248 return Err(format!(
249 "values_grad: row_ptr len {} != n+1 ({})",
250 row_ptr.len(),
251 n + 1
252 ));
253 }
254 let mut row_of_k = vec![0u32; nnz];
256 for r in 0..n {
257 let s = row_ptr[r] as usize;
258 let e = row_ptr[r + 1] as usize;
259 for k in s..e {
260 row_of_k[k] = r as u32;
261 }
262 }
263 for k in 0..nnz {
264 let r = row_of_k[k] as usize;
265 let c = col_idx[k] as usize;
266 if r >= n || c >= v.len() {
267 return Err(format!(
268 "values_grad: k={k} (row={r}, col={c}) out of bounds"
269 ));
270 }
271 out[k] = u[r] * v[c];
272 }
273 Ok(())
274 }
275
276 pub fn gmres_solve(
293 values: &[f64],
294 col_idx: &[i32],
295 row_ptr: &[i32],
296 b: &[f64],
297 out: &mut [f64],
298 max_iter: u32,
299 tol: f64,
300 ) -> Result<(), String> {
301 let n = b.len();
302 if out.len() != n {
303 return Err(format!("gmres_solve: out len {} != n {n}", out.len()));
304 }
305 if row_ptr.len() != n + 1 {
306 return Err(format!(
307 "gmres_solve: row_ptr len {} != n+1 ({})",
308 row_ptr.len(),
309 n + 1
310 ));
311 }
312 let m = max_iter.max(1) as usize;
313
314 let matvec = |x: &[f64], y: &mut [f64]| {
315 for r in 0..n {
316 let mut acc = 0f64;
317 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
318 acc += values[k] * x[col_idx[k] as usize];
319 }
320 y[r] = acc;
321 }
322 };
323
324 let beta_init = b.iter().map(|v| v * v).sum::<f64>().sqrt();
326 if beta_init < tol {
327 for v in out.iter_mut() {
328 *v = 0.0;
329 }
330 return Ok(());
331 }
332
333 let mut v: Vec<Vec<f64>> = Vec::with_capacity(m + 1);
335 v.push(b.iter().map(|x| x / beta_init).collect());
336
337 let mut h: Vec<Vec<f64>> = Vec::with_capacity(m); let mut cs: Vec<f64> = Vec::with_capacity(m);
342 let mut sn: Vec<f64> = Vec::with_capacity(m);
343 let mut g: Vec<f64> = vec![0.0; m + 1];
344 g[0] = beta_init;
345
346 let mut converged_at: Option<usize> = None;
347 let mut w = vec![0f64; n];
348
349 for j in 0..m {
350 matvec(&v[j], &mut w);
351 let mut hcol = vec![0f64; j + 2];
353 for i in 0..=j {
354 hcol[i] = w.iter().zip(&v[i]).map(|(a, b)| a * b).sum();
355 for k in 0..n {
356 w[k] -= hcol[i] * v[i][k];
357 }
358 }
359 hcol[j + 1] = w.iter().map(|x| x * x).sum::<f64>().sqrt();
360 for i in 0..j {
362 let temp = cs[i] * hcol[i] + sn[i] * hcol[i + 1];
363 hcol[i + 1] = -sn[i] * hcol[i] + cs[i] * hcol[i + 1];
364 hcol[i] = temp;
365 }
366 let denom = (hcol[j] * hcol[j] + hcol[j + 1] * hcol[j + 1]).sqrt();
368 if denom == 0.0 {
369 return Err("gmres_solve: breakdown (denom = 0)".into());
370 }
371 let c = hcol[j] / denom;
372 let s = hcol[j + 1] / denom;
373 cs.push(c);
374 sn.push(s);
375 hcol[j] = c * hcol[j] + s * hcol[j + 1];
376 hcol[j + 1] = 0.0;
377 let g_temp = c * g[j] + s * g[j + 1];
379 g[j + 1] = -s * g[j] + c * g[j + 1];
380 g[j] = g_temp;
381 h.push(hcol);
382
383 if g[j + 1].abs() < tol {
385 converged_at = Some(j);
386 break;
387 }
388 if hcol_last_zero_check(&h[j]) {
389 converged_at = Some(j);
391 break;
392 }
393 if j + 1 < m {
394 let inv = 1.0 / hcol_subdiag(&h[j], j + 1).max(f64::MIN_POSITIVE);
395 let _ = inv;
396 let norm_w = w.iter().map(|x| x * x).sum::<f64>().sqrt();
400 if norm_w < f64::MIN_POSITIVE * 64.0 {
401 converged_at = Some(j);
402 break;
403 }
404 v.push(w.iter().map(|x| x / norm_w).collect());
405 }
406 }
407
408 let k = converged_at.map(|j| j + 1).unwrap_or(m);
410 let mut y = vec![0f64; k];
411 for i in (0..k).rev() {
412 let mut s = g[i];
413 for j in (i + 1)..k {
414 s -= h[j][i] * y[j];
415 }
416 y[i] = s / h[i][i];
417 }
418
419 for r in 0..n {
421 out[r] = 0.0;
422 }
423 for j in 0..k {
424 for r in 0..n {
425 out[r] += y[j] * v[j][r];
426 }
427 }
428 Ok(())
429 }
430
431 pub fn transpose_values(
436 values: &[f64],
437 col_idx: &[i32],
438 row_ptr: &[i32],
439 _col_idx_t: &[i32],
440 row_ptr_t: &[i32],
441 out: &mut [f64],
442 ) -> Result<(), String> {
443 let n = row_ptr.len().saturating_sub(1);
444 let nnz = values.len();
445 if out.len() != nnz {
446 return Err(format!(
447 "transpose_values: out len {} != nnz {nnz}",
448 out.len()
449 ));
450 }
451 let mut cursor: Vec<usize> = row_ptr_t.iter().map(|&x| x as usize).collect();
454 for r in 0..n {
455 let s = row_ptr[r] as usize;
456 let e = row_ptr[r + 1] as usize;
457 for k in s..e {
458 let c = col_idx[k] as usize;
459 let pos = cursor[c];
460 if pos >= nnz {
461 return Err(format!(
462 "transpose_values: cursor[{c}]={pos} ≥ nnz={nnz} \
463 (transposed pattern likely inconsistent with input)"
464 ));
465 }
466 out[pos] = values[k];
467 cursor[c] += 1;
468 }
469 }
470 Ok(())
471 }
472
473 pub fn pcg_solve(
480 values: &[f64],
481 col_idx: &[i32],
482 row_ptr: &[i32],
483 b: &[f64],
484 out: &mut [f64],
485 max_iter: u32,
486 tol: f64,
487 ) -> Result<(), String> {
488 let n = b.len();
489 if out.len() != n {
490 return Err(format!("pcg_solve: out len {} != n {n}", out.len()));
491 }
492 if row_ptr.len() != n + 1 {
493 return Err(format!(
494 "pcg_solve: row_ptr len {} != n+1 ({})",
495 row_ptr.len(),
496 n + 1
497 ));
498 }
499
500 let mut diag = vec![1.0f64; n];
506 for r in 0..n {
507 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
508 if col_idx[k] as usize == r {
509 diag[r] = values[k].max(f64::MIN_POSITIVE);
510 break;
511 }
512 }
513 }
514 let inv_diag: Vec<f64> = diag.iter().map(|&d| 1.0 / d).collect();
515
516 let matvec = |x: &[f64], y: &mut [f64]| {
517 for r in 0..n {
518 let mut acc = 0f64;
519 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
520 acc += values[k] * x[col_idx[k] as usize];
521 }
522 y[r] = acc;
523 }
524 };
525
526 let mut x = vec![0f64; n];
528 let mut r = b.to_vec();
529 let mut z: Vec<f64> = r.iter().zip(&inv_diag).map(|(rv, mi)| rv * mi).collect();
530 let mut p = z.clone();
531 let mut ap = vec![0f64; n];
532 let mut rho_old: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
533
534 for _ in 0..max_iter {
535 let r_norm: f64 = r.iter().map(|v| v * v).sum::<f64>().sqrt();
537 if r_norm < tol {
538 break;
539 }
540 matvec(&p, &mut ap);
541 let pap: f64 = p.iter().zip(&ap).map(|(a, b)| a * b).sum();
542 if pap == 0.0 {
543 return Err("pcg_solve: pᵀ·A·p = 0 (A is singular or not SPD)".into());
544 }
545 let alpha = rho_old / pap;
546 for i in 0..n {
547 x[i] += alpha * p[i];
548 }
549 for i in 0..n {
550 r[i] -= alpha * ap[i];
551 }
552 for i in 0..n {
553 z[i] = r[i] * inv_diag[i];
554 }
555 let rho_new: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
556 let beta = rho_new / rho_old;
557 for i in 0..n {
558 p[i] = z[i] + beta * p[i];
559 }
560 rho_old = rho_new;
561 }
562
563 out.copy_from_slice(&x);
564 Ok(())
565 }
566
567 pub fn cholesky_solve(
572 values: &[f64],
573 col_idx: &[i32],
574 row_ptr: &[i32],
575 b: &[f64],
576 out: &mut [f64],
577 ) -> Result<(), String> {
578 let n = b.len();
579 if out.len() != n {
580 return Err(format!("cholesky_solve: out len {} != n {n}", out.len()));
581 }
582 if row_ptr.len() != n + 1 {
583 return Err(format!(
584 "cholesky_solve: row_ptr len {} != n+1",
585 row_ptr.len()
586 ));
587 }
588 let mut a_dense = vec![0f64; n * n];
589 for r in 0..n {
590 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
591 a_dense[r * n + col_idx[k] as usize] = values[k];
592 }
593 }
594 let info = rlx_cpu::blas::dpotrf(&mut a_dense, n, true);
596 if info != 0 {
597 return Err(format!("cholesky_solve: dpotrf info={info} (not SPD?)"));
598 }
599 let mut x = b.to_vec();
601 rlx_cpu::blas::dtrsm_lower_or_upper(
602 &a_dense, &mut x, n, 1, true, false,
603 );
604 rlx_cpu::blas::dtrsm_lower_or_upper(
606 &a_dense, &mut x, n, 1, true, true,
607 );
608 out.copy_from_slice(&x);
609 Ok(())
610 }
611
612 pub fn bicgstab(
616 values: &[f64],
617 col_idx: &[i32],
618 row_ptr: &[i32],
619 b: &[f64],
620 out: &mut [f64],
621 max_iter: u32,
622 tol: f64,
623 transpose_a: bool,
624 ) -> Result<(), String> {
625 let n = b.len();
626 if out.len() != n {
627 return Err(format!("bicgstab: out len {} != n {n}", out.len()));
628 }
629 if row_ptr.len() != n + 1 {
630 return Err(format!(
631 "bicgstab: row_ptr len {} != n+1 ({})",
632 row_ptr.len(),
633 n + 1
634 ));
635 }
636 let matvec = |x: &[f64], y: &mut [f64]| {
637 if !transpose_a {
638 for r in 0..n {
639 let mut acc = 0f64;
640 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
641 acc += values[k] * x[col_idx[k] as usize];
642 }
643 y[r] = acc;
644 }
645 } else {
646 for v in y.iter_mut() {
647 *v = 0.0;
648 }
649 for r in 0..n {
650 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
651 y[col_idx[k] as usize] += values[k] * x[r];
652 }
653 }
654 }
655 };
656 let mut x = vec![0f64; n];
657 let mut r = b.to_vec();
658 let r_hat = r.clone();
659 let mut p = r.clone();
660 let mut v = vec![0f64; n];
661 let mut s = vec![0f64; n];
662 let mut t = vec![0f64; n];
663 let mut rho_old: f64 = r_hat.iter().zip(&r).map(|(a, b)| a * b).sum();
664
665 for _ in 0..max_iter {
666 let r_norm: f64 = r.iter().map(|v| v * v).sum::<f64>().sqrt();
667 if r_norm < tol {
668 break;
669 }
670 matvec(&p, &mut v);
671 let rh_v: f64 = r_hat.iter().zip(&v).map(|(a, b)| a * b).sum();
672 if rh_v == 0.0 {
673 return Err("bicgstab: breakdown r̂·v = 0".into());
674 }
675 let alpha = rho_old / rh_v;
676 for i in 0..n {
677 s[i] = r[i] - alpha * v[i];
678 }
679 let s_norm: f64 = s.iter().map(|v| v * v).sum::<f64>().sqrt();
680 if s_norm < tol {
681 for i in 0..n {
682 x[i] += alpha * p[i];
683 }
684 r[..n].copy_from_slice(&s[..n]);
685 break;
686 }
687 matvec(&s, &mut t);
688 let tt: f64 = t.iter().map(|v| v * v).sum();
689 if tt == 0.0 {
690 return Err("bicgstab: breakdown t·t = 0".into());
691 }
692 let ts: f64 = t.iter().zip(&s).map(|(a, b)| a * b).sum();
693 let omega = ts / tt;
694 for i in 0..n {
695 x[i] += alpha * p[i] + omega * s[i];
696 r[i] = s[i] - omega * t[i];
697 }
698 if omega == 0.0 {
699 return Err("bicgstab: ω = 0 (stagnation)".into());
700 }
701 let rho_new: f64 = r_hat.iter().zip(&r).map(|(a, b)| a * b).sum();
702 if rho_old == 0.0 {
703 return Err("bicgstab: ρ_old = 0".into());
704 }
705 let beta = (rho_new / rho_old) * (alpha / omega);
706 for i in 0..n {
707 p[i] = r[i] + beta * (p[i] - omega * v[i]);
708 }
709 rho_old = rho_new;
710 }
711 out.copy_from_slice(&x);
712 Ok(())
713 }
714
715 pub fn lsqr_solve(
725 values: &[f64],
726 col_idx: &[i32],
727 row_ptr: &[i32],
728 b: &[f64],
729 out: &mut [f64],
730 max_iter: u32,
731 tol: f64,
732 n_cols: usize,
733 ) -> Result<(), String> {
734 let m = b.len();
735 let n = n_cols;
736 if out.len() != n {
737 return Err(format!("lsqr: out len {} != n {n}", out.len()));
738 }
739 if row_ptr.len() != m + 1 {
740 return Err(format!(
741 "lsqr: row_ptr len {} != m+1 ({})",
742 row_ptr.len(),
743 m + 1
744 ));
745 }
746
747 let av = |x: &[f64], y: &mut [f64]| {
749 for r in 0..m {
750 let mut acc = 0f64;
751 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
752 acc += values[k] * x[col_idx[k] as usize];
753 }
754 y[r] = acc;
755 }
756 };
757 let atv = |u: &[f64], y: &mut [f64]| {
759 for v in y.iter_mut() {
760 *v = 0.0;
761 }
762 for r in 0..m {
763 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
764 y[col_idx[k] as usize] += values[k] * u[r];
765 }
766 }
767 };
768
769 let mut u = b.to_vec();
770 let mut beta: f64 = u.iter().map(|v| v * v).sum::<f64>().sqrt();
771 if beta == 0.0 {
772 for v in out.iter_mut() {
773 *v = 0.0;
774 }
775 return Ok(());
776 }
777 for v in u.iter_mut() {
778 *v /= beta;
779 }
780
781 let mut v = vec![0f64; n];
782 atv(&u, &mut v);
783 let mut alpha: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
784 if alpha == 0.0 {
785 for v in out.iter_mut() {
786 *v = 0.0;
787 }
788 return Ok(());
789 }
790 for x in v.iter_mut() {
791 *x /= alpha;
792 }
793
794 let mut x = vec![0f64; n];
795 let mut w = v.clone();
796 let mut phi_bar = beta;
797 let mut rho_bar = alpha;
798
799 let mut tmp_u = vec![0f64; m];
800 let mut tmp_v = vec![0f64; n];
801
802 for _ in 0..max_iter {
803 av(&v, &mut tmp_u);
806 for i in 0..m {
807 tmp_u[i] -= alpha * u[i];
808 }
809 beta = tmp_u.iter().map(|x| x * x).sum::<f64>().sqrt();
810 if beta != 0.0 {
811 for i in 0..m {
812 u[i] = tmp_u[i] / beta;
813 }
814 atv(&u, &mut tmp_v);
816 for i in 0..n {
817 tmp_v[i] -= beta * v[i];
818 }
819 alpha = tmp_v.iter().map(|x| x * x).sum::<f64>().sqrt();
820 if alpha != 0.0 {
821 for i in 0..n {
822 v[i] = tmp_v[i] / alpha;
823 }
824 }
825 }
826
827 let rho = (rho_bar * rho_bar + beta * beta).sqrt();
829 let c = rho_bar / rho;
830 let s = beta / rho;
831 let theta = s * alpha;
832 rho_bar = -c * alpha;
833 let phi = c * phi_bar;
834 phi_bar *= s;
835
836 let phi_over_rho = phi / rho;
838 let theta_over_rho = theta / rho;
839 for i in 0..n {
840 x[i] += phi_over_rho * w[i];
841 w[i] = v[i] - theta_over_rho * w[i];
842 }
843
844 if phi_bar.abs() < tol {
845 break;
846 }
847 if alpha == 0.0 || beta == 0.0 {
848 break;
849 }
850 }
851 out.copy_from_slice(&x);
852 Ok(())
853 }
854
855 pub fn ilu0_factor(
859 values: &[f64],
860 col_idx: &[i32],
861 row_ptr: &[i32],
862 n: usize,
863 out_fact: &mut [f64],
864 ) -> Result<(), String> {
865 if out_fact.len() != values.len() {
866 return Err(format!(
867 "ilu0: out len {} != values len {}",
868 out_fact.len(),
869 values.len()
870 ));
871 }
872 out_fact.copy_from_slice(values);
873 for i in 0..n {
874 let row_i_start = row_ptr[i] as usize;
875 let row_i_end = row_ptr[i + 1] as usize;
876 for k in row_i_start..row_i_end {
877 let j = col_idx[k] as usize;
878 if j >= i {
879 break;
880 }
881 let row_j_start = row_ptr[j] as usize;
883 let row_j_end = row_ptr[j + 1] as usize;
884 let mut a_jj = 0f64;
885 let mut found = false;
886 for kj in row_j_start..row_j_end {
887 if col_idx[kj] as usize == j {
888 a_jj = out_fact[kj];
889 found = true;
890 break;
891 }
892 }
893 if !found || a_jj == 0.0 {
894 return Err(format!("ilu0: zero/missing diag at row {j}"));
895 }
896 out_fact[k] /= a_jj;
897 let lij = out_fact[k];
898 for kk in (k + 1)..row_i_end {
899 let m = col_idx[kk] as usize;
900 for kj in row_j_start..row_j_end {
901 if col_idx[kj] as usize == m {
902 out_fact[kk] -= lij * out_fact[kj];
903 break;
904 }
905 }
906 }
907 }
908 }
909 Ok(())
910 }
911
912 pub fn ilu0_apply(
915 fact: &[f64],
916 col_idx: &[i32],
917 row_ptr: &[i32],
918 n: usize,
919 b: &[f64],
920 out: &mut [f64],
921 ) {
922 for i in 0..n {
924 let mut acc = b[i];
925 for k in row_ptr[i] as usize..row_ptr[i + 1] as usize {
926 let j = col_idx[k] as usize;
927 if j < i {
928 acc -= fact[k] * out[j];
929 } else {
930 break;
931 }
932 }
933 out[i] = acc;
934 }
935 for i in (0..n).rev() {
937 let mut acc = out[i];
938 let mut diag = 1f64;
939 for k in row_ptr[i] as usize..row_ptr[i + 1] as usize {
940 let j = col_idx[k] as usize;
941 if j > i {
942 acc -= fact[k] * out[j];
943 } else if j == i {
944 diag = fact[k];
945 }
946 }
947 out[i] = acc / diag;
948 }
949 }
950
951 pub fn ilu_pcg_solve(
954 values: &[f64],
955 col_idx: &[i32],
956 row_ptr: &[i32],
957 b: &[f64],
958 out: &mut [f64],
959 max_iter: u32,
960 tol: f64,
961 ) -> Result<(), String> {
962 let n = b.len();
963 if out.len() != n {
964 return Err(format!("ilu_pcg: out len {} != n {n}", out.len()));
965 }
966 let mut fact = vec![0f64; values.len()];
967 ilu0_factor(values, col_idx, row_ptr, n, &mut fact)?;
968 let matvec = |x: &[f64], y: &mut [f64]| {
969 for r in 0..n {
970 let mut acc = 0f64;
971 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
972 acc += values[k] * x[col_idx[k] as usize];
973 }
974 y[r] = acc;
975 }
976 };
977 let mut x = vec![0f64; n];
978 let mut r = b.to_vec();
979 let mut z = vec![0f64; n];
980 ilu0_apply(&fact, col_idx, row_ptr, n, &r, &mut z);
981 let mut p = z.clone();
982 let mut ap = vec![0f64; n];
983 let mut rho_old: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
984 for _ in 0..max_iter {
985 let r_norm: f64 = r.iter().map(|v| v * v).sum::<f64>().sqrt();
986 if r_norm < tol {
987 break;
988 }
989 matvec(&p, &mut ap);
990 let pap: f64 = p.iter().zip(&ap).map(|(a, b)| a * b).sum();
991 if pap == 0.0 {
992 return Err("ilu_pcg: pᵀ·A·p = 0".into());
993 }
994 let alpha = rho_old / pap;
995 for i in 0..n {
996 x[i] += alpha * p[i];
997 }
998 for i in 0..n {
999 r[i] -= alpha * ap[i];
1000 }
1001 ilu0_apply(&fact, col_idx, row_ptr, n, &r, &mut z);
1002 let rho_new: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
1003 let beta = rho_new / rho_old;
1004 for i in 0..n {
1005 p[i] = z[i] + beta * p[i];
1006 }
1007 rho_old = rho_new;
1008 }
1009 out.copy_from_slice(&x);
1010 Ok(())
1011 }
1012
1013 pub fn spgemm_csr(
1018 a_values: &[f64],
1019 a_col_idx: &[i32],
1020 a_row_ptr: &[i32],
1021 b_values: &[f64],
1022 b_col_idx: &[i32],
1023 b_row_ptr: &[i32],
1024 m: usize,
1025 k: usize,
1026 n: usize,
1027 ) -> Result<(Vec<f64>, Vec<i32>, Vec<i32>), String> {
1028 if a_row_ptr.len() != m + 1 {
1029 return Err(format!("spgemm: a_row_ptr len {} != m+1", a_row_ptr.len()));
1030 }
1031 if b_row_ptr.len() != k + 1 {
1032 return Err(format!("spgemm: b_row_ptr len {} != k+1", b_row_ptr.len()));
1033 }
1034 let mut c_row_ptr = vec![0i32; m + 1];
1036 let mut c_col_idx: Vec<i32> = Vec::new();
1037 let mut c_values: Vec<f64> = Vec::new();
1038
1039 let mut marker = vec![-1i32; n];
1041 let mut spa_vals = vec![0f64; n];
1042 let mut spa_cols: Vec<usize> = Vec::with_capacity(n);
1043
1044 for i in 0..m {
1045 spa_cols.clear();
1046 for ka in a_row_ptr[i] as usize..a_row_ptr[i + 1] as usize {
1047 let j = a_col_idx[ka] as usize;
1048 let aij = a_values[ka];
1049 for kb in b_row_ptr[j] as usize..b_row_ptr[j + 1] as usize {
1050 let l = b_col_idx[kb] as usize;
1051 let bjl = b_values[kb];
1052 if marker[l] != i as i32 {
1053 marker[l] = i as i32;
1054 spa_vals[l] = aij * bjl;
1055 spa_cols.push(l);
1056 } else {
1057 spa_vals[l] += aij * bjl;
1058 }
1059 }
1060 }
1061 spa_cols.sort_unstable();
1063 for &l in &spa_cols {
1064 c_col_idx.push(l as i32);
1065 c_values.push(spa_vals[l]);
1066 }
1067 c_row_ptr[i + 1] = c_col_idx.len() as i32;
1068 }
1069 Ok((c_values, c_col_idx, c_row_ptr))
1070 }
1071
1072 fn hcol_last_zero_check(hcol: &[f64]) -> bool {
1073 hcol.iter().all(|v| v.abs() < f64::MIN_POSITIVE * 64.0)
1079 }
1080 fn hcol_subdiag(hcol: &[f64], i: usize) -> f64 {
1081 hcol.get(i).copied().unwrap_or(0.0)
1082 }
1083
1084 pub fn cg_solve(
1085 values: &[f64],
1086 col_idx: &[i32],
1087 row_ptr: &[i32],
1088 b: &[f64],
1089 out: &mut [f64],
1090 max_iter: u32,
1091 tol: f64,
1092 ) -> Result<(), String> {
1093 let n = b.len();
1094 if out.len() != n {
1095 return Err(format!("cg_solve: output len {} != b len {n}", out.len()));
1096 }
1097 if row_ptr.len() != n + 1 {
1098 return Err(format!(
1099 "cg_solve: row_ptr len {} != n+1 ({})",
1100 row_ptr.len(),
1101 n + 1
1102 ));
1103 }
1104 let matvec = |x: &[f64], y: &mut [f64]| {
1105 for r in 0..n {
1106 let mut acc = 0f64;
1107 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
1108 acc += values[k] * x[col_idx[k] as usize];
1109 }
1110 y[r] = acc;
1111 }
1112 };
1113 let mut x = vec![0f64; n];
1114 let mut r = b.to_vec();
1115 let mut p = r.clone();
1116 let mut ap = vec![0f64; n];
1117 let mut rs_old: f64 = r.iter().map(|v| v * v).sum();
1118 for _ in 0..max_iter {
1119 if rs_old.sqrt() < tol {
1120 break;
1121 }
1122 matvec(&p, &mut ap);
1123 let pap: f64 = p.iter().zip(&ap).map(|(a, b)| a * b).sum();
1124 if pap == 0.0 {
1125 return Err("cg_solve: pᵀ·A·p = 0 (A is singular or not SPD)".into());
1126 }
1127 let alpha = rs_old / pap;
1128 for i in 0..n {
1129 x[i] += alpha * p[i];
1130 }
1131 for i in 0..n {
1132 r[i] -= alpha * ap[i];
1133 }
1134 let rs_new: f64 = r.iter().map(|v| v * v).sum();
1135 let beta = rs_new / rs_old;
1136 for i in 0..n {
1137 p[i] = r[i] + beta * p[i];
1138 }
1139 rs_old = rs_new;
1140 }
1141 out.copy_from_slice(&x);
1142 Ok(())
1143 }
1144}
1145
1146struct SparseLuExt;
1149
1150impl OpExtension for SparseLuExt {
1151 fn name(&self) -> &str {
1152 SPARSE_LU_SOLVE
1153 }
1154 fn num_inputs(&self) -> usize {
1155 4
1156 }
1157
1158 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1159 let values = inputs[0];
1160 let col_idx = inputs[1];
1161 let row_ptr = inputs[2];
1162 let b = inputs[3];
1163 assert_eq!(values.dtype(), DType::F64, "sparse_lu: values must be F64");
1164 assert_eq!(
1165 col_idx.dtype(),
1166 DType::I32,
1167 "sparse_lu: col_idx must be I32"
1168 );
1169 assert_eq!(
1170 row_ptr.dtype(),
1171 DType::I32,
1172 "sparse_lu: row_ptr must be I32"
1173 );
1174 assert_eq!(b.dtype(), DType::F64, "sparse_lu: b must be F64");
1175 b.clone()
1176 }
1177
1178 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1179 let vals_b = ctx.fwd_map[&node.inputs[0]];
1183 let cidx_b = ctx.fwd_map[&node.inputs[1]];
1184 let rptr_b = ctx.fwd_map[&node.inputs[2]];
1185
1186 let g_b = ctx.bwd.custom_op(
1187 SPARSE_LU_SOLVE,
1188 Vec::new(),
1189 vec![vals_b, cidx_b, rptr_b, ctx.upstream],
1190 );
1191
1192 let y_fwd = ctx.fwd_map[&node.id];
1196 let raw_grad = ctx.bwd.custom_op(
1197 SPARSE_VALUES_GRAD,
1198 Vec::new(),
1199 vec![cidx_b, rptr_b, g_b, y_fwd],
1200 );
1201 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1203 let g_vals = ctx
1204 .bwd
1205 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1206
1207 vec![(0, g_vals), (3, g_b)]
1208 }
1209}
1210
1211#[cfg(feature = "cpu")]
1212struct SparseLuCpu;
1213
1214#[cfg(feature = "cpu")]
1215impl CpuKernel for SparseLuCpu {
1216 fn name(&self) -> &str {
1217 SPARSE_LU_SOLVE
1218 }
1219
1220 fn execute(
1221 &self,
1222 inputs: &[CpuTensorRef<'_>],
1223 output: CpuTensorMut<'_>,
1224 _attrs: &[u8],
1225 ) -> Result<(), String> {
1226 let values = inputs[0].expect_f64("sparse_lu values")?;
1227 let col_idx = inputs[1].expect_i32("sparse_lu col_idx")?;
1228 let row_ptr = inputs[2].expect_i32("sparse_lu row_ptr")?;
1229 let b = inputs[3].expect_f64("sparse_lu b")?;
1230 let out = output.expect_f64_mut("sparse_lu output")?;
1231 algos::lu_solve(values, col_idx, row_ptr, b, out)
1232 }
1233}
1234
1235struct SparseMatVecExt;
1238
1239impl OpExtension for SparseMatVecExt {
1240 fn name(&self) -> &str {
1241 SPARSE_MAT_VEC
1242 }
1243 fn num_inputs(&self) -> usize {
1244 4
1245 }
1246
1247 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1248 inputs[3].clone()
1250 }
1251
1252 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1253 let vals_b = ctx.fwd_map[&node.inputs[0]];
1257 let cidx_b = ctx.fwd_map[&node.inputs[1]];
1258 let rptr_b = ctx.fwd_map[&node.inputs[2]];
1259 let x_bwd = ctx.fwd_map[&node.inputs[3]];
1260
1261 let g_x = ctx.bwd.custom_op(
1262 SPARSE_MAT_VEC,
1263 Vec::new(),
1264 vec![vals_b, cidx_b, rptr_b, ctx.upstream],
1265 );
1266 let g_vals = ctx.bwd.custom_op(
1267 SPARSE_VALUES_GRAD,
1268 Vec::new(),
1269 vec![cidx_b, rptr_b, ctx.upstream, x_bwd],
1270 );
1271 vec![(0, g_vals), (3, g_x)]
1272 }
1273}
1274
1275#[cfg(feature = "cpu")]
1276struct SparseMatVecCpu;
1277
1278#[cfg(feature = "cpu")]
1279impl CpuKernel for SparseMatVecCpu {
1280 fn name(&self) -> &str {
1281 SPARSE_MAT_VEC
1282 }
1283 fn execute(
1284 &self,
1285 inputs: &[CpuTensorRef<'_>],
1286 output: CpuTensorMut<'_>,
1287 _attrs: &[u8],
1288 ) -> Result<(), String> {
1289 let values = inputs[0].expect_f64("mat_vec values")?;
1290 let col_idx = inputs[1].expect_i32("mat_vec col_idx")?;
1291 let row_ptr = inputs[2].expect_i32("mat_vec row_ptr")?;
1292 let x = inputs[3].expect_f64("mat_vec x")?;
1293 let out = output.expect_f64_mut("mat_vec y")?;
1294 algos::mat_vec(values, col_idx, row_ptr, x, out)
1295 }
1296}
1297
1298pub fn encode_cg_attrs(max_iter: u32, tol: f64) -> Vec<u8> {
1303 let mut out = Vec::with_capacity(12);
1304 out.extend_from_slice(&max_iter.to_le_bytes());
1305 out.extend_from_slice(&tol.to_le_bytes());
1306 out
1307}
1308
1309fn decode_cg_attrs(attrs: &[u8]) -> Result<(u32, f64), String> {
1310 if attrs.len() != 12 {
1311 return Err(format!(
1312 "cg_solve: attrs must be 12 bytes (u32 max_iter + f64 tol), got {}",
1313 attrs.len()
1314 ));
1315 }
1316 let max_iter = u32::from_le_bytes(attrs[0..4].try_into().unwrap());
1317 let tol = f64::from_le_bytes(attrs[4..12].try_into().unwrap());
1318 Ok((max_iter, tol))
1319}
1320
1321struct SparseCgExt;
1322
1323impl OpExtension for SparseCgExt {
1324 fn name(&self) -> &str {
1325 SPARSE_CG_SOLVE
1326 }
1327 fn num_inputs(&self) -> usize {
1328 4
1329 }
1330
1331 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1332 inputs[3].clone()
1333 }
1334
1335 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1336 let vals_b = ctx.fwd_map[&node.inputs[0]];
1341 let cidx_b = ctx.fwd_map[&node.inputs[1]];
1342 let rptr_b = ctx.fwd_map[&node.inputs[2]];
1343 let attrs = match &node.op {
1344 Op::Custom { attrs, .. } => attrs.clone(),
1345 _ => Vec::new(),
1346 };
1347 let g_b = ctx.bwd.custom_op(
1348 SPARSE_CG_SOLVE,
1349 attrs,
1350 vec![vals_b, cidx_b, rptr_b, ctx.upstream],
1351 );
1352 let y_fwd = ctx.fwd_map[&node.id];
1353 let raw_grad = ctx.bwd.custom_op(
1354 SPARSE_VALUES_GRAD,
1355 Vec::new(),
1356 vec![cidx_b, rptr_b, g_b, y_fwd],
1357 );
1358 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1359 let g_vals = ctx
1360 .bwd
1361 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1362
1363 vec![(0, g_vals), (3, g_b)]
1364 }
1365}
1366
1367#[cfg(feature = "cpu")]
1368struct SparseCgCpu;
1369
1370#[cfg(feature = "cpu")]
1371impl CpuKernel for SparseCgCpu {
1372 fn name(&self) -> &str {
1373 SPARSE_CG_SOLVE
1374 }
1375
1376 fn execute(
1377 &self,
1378 inputs: &[CpuTensorRef<'_>],
1379 output: CpuTensorMut<'_>,
1380 attrs: &[u8],
1381 ) -> Result<(), String> {
1382 let values = inputs[0].expect_f64("cg_solve values")?;
1383 let col_idx = inputs[1].expect_i32("cg_solve col_idx")?;
1384 let row_ptr = inputs[2].expect_i32("cg_solve row_ptr")?;
1385 let b = inputs[3].expect_f64("cg_solve b")?;
1386 let out = output.expect_f64_mut("cg_solve x")?;
1387 let (max_iter, tol) = decode_cg_attrs(attrs)?;
1388 algos::cg_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
1389 }
1390}
1391
1392struct SparseValuesGradExt;
1395
1396impl OpExtension for SparseValuesGradExt {
1397 fn name(&self) -> &str {
1398 SPARSE_VALUES_GRAD
1399 }
1400 fn num_inputs(&self) -> usize {
1401 4
1402 } fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1405 let col_idx = inputs[0];
1407 assert_eq!(
1408 col_idx.dtype(),
1409 DType::I32,
1410 "values_grad: col_idx must be I32"
1411 );
1412 let nnz = col_idx
1413 .num_elements()
1414 .expect("values_grad: col_idx must have static shape");
1415 Shape::new(&[nnz], DType::F64)
1416 }
1417 }
1420
1421#[cfg(feature = "cpu")]
1422struct SparseValuesGradCpu;
1423#[cfg(feature = "cpu")]
1424impl CpuKernel for SparseValuesGradCpu {
1425 fn name(&self) -> &str {
1426 SPARSE_VALUES_GRAD
1427 }
1428 fn execute(
1429 &self,
1430 inputs: &[CpuTensorRef<'_>],
1431 output: CpuTensorMut<'_>,
1432 _attrs: &[u8],
1433 ) -> Result<(), String> {
1434 let col_idx = inputs[0].expect_i32("values_grad col_idx")?;
1435 let row_ptr = inputs[1].expect_i32("values_grad row_ptr")?;
1436 let u = inputs[2].expect_f64("values_grad u")?;
1437 let v = inputs[3].expect_f64("values_grad v")?;
1438 let out = output.expect_f64_mut("values_grad out")?;
1439 algos::values_grad(col_idx, row_ptr, u, v, out)
1440 }
1441}
1442
1443struct SparseLuGeneralExt;
1453
1454impl OpExtension for SparseLuGeneralExt {
1455 fn name(&self) -> &str {
1456 SPARSE_LU_SOLVE_GENERAL
1457 }
1458 fn num_inputs(&self) -> usize {
1459 7
1460 }
1461 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1463 let b = inputs[3];
1464 b.clone()
1465 }
1466 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1467 let vals_a = ctx.fwd_map[&node.inputs[0]];
1474 let cidx_a = ctx.fwd_map[&node.inputs[1]];
1475 let rptr_a = ctx.fwd_map[&node.inputs[2]];
1476 let vals_at = ctx.fwd_map[&node.inputs[4]];
1477 let cidx_at = ctx.fwd_map[&node.inputs[5]];
1478 let rptr_at = ctx.fwd_map[&node.inputs[6]];
1479
1480 let g_b = ctx.bwd.custom_op(
1481 SPARSE_LU_SOLVE_GENERAL,
1482 Vec::new(),
1483 vec![
1485 vals_at,
1486 cidx_at,
1487 rptr_at,
1488 ctx.upstream,
1489 vals_a,
1490 cidx_a,
1491 rptr_a,
1492 ],
1493 );
1494 let y_fwd = ctx.fwd_map[&node.id];
1495 let raw_grad = ctx.bwd.custom_op(
1496 SPARSE_VALUES_GRAD,
1497 Vec::new(),
1498 vec![cidx_a, rptr_a, g_b, y_fwd],
1499 );
1500 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1501 let g_vals = ctx
1502 .bwd
1503 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1504 vec![(0, g_vals), (3, g_b)]
1505 }
1506}
1507
1508#[cfg(feature = "cpu")]
1509struct SparseLuGeneralCpu;
1510#[cfg(feature = "cpu")]
1511impl CpuKernel for SparseLuGeneralCpu {
1512 fn name(&self) -> &str {
1513 SPARSE_LU_SOLVE_GENERAL
1514 }
1515 fn execute(
1516 &self,
1517 inputs: &[CpuTensorRef<'_>],
1518 output: CpuTensorMut<'_>,
1519 _attrs: &[u8],
1520 ) -> Result<(), String> {
1521 let values = inputs[0].expect_f64("lu_solve_general values")?;
1525 let col_idx = inputs[1].expect_i32("lu_solve_general col_idx")?;
1526 let row_ptr = inputs[2].expect_i32("lu_solve_general row_ptr")?;
1527 let b = inputs[3].expect_f64("lu_solve_general b")?;
1528 let out = output.expect_f64_mut("lu_solve_general out")?;
1529 algos::lu_solve(values, col_idx, row_ptr, b, out)
1530 }
1531}
1532
1533struct SparseGmresExt;
1540
1541impl OpExtension for SparseGmresExt {
1542 fn name(&self) -> &str {
1543 SPARSE_GMRES_SOLVE
1544 }
1545 fn num_inputs(&self) -> usize {
1546 7
1547 }
1548 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1549 inputs[3].clone()
1550 }
1551 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1552 let vals_a = ctx.fwd_map[&node.inputs[0]];
1555 let cidx_a = ctx.fwd_map[&node.inputs[1]];
1556 let rptr_a = ctx.fwd_map[&node.inputs[2]];
1557 let vals_at = ctx.fwd_map[&node.inputs[4]];
1558 let cidx_at = ctx.fwd_map[&node.inputs[5]];
1559 let rptr_at = ctx.fwd_map[&node.inputs[6]];
1560 let attrs = match &node.op {
1561 Op::Custom { attrs, .. } => attrs.clone(),
1562 _ => Vec::new(),
1563 };
1564 let g_b = ctx.bwd.custom_op(
1565 SPARSE_GMRES_SOLVE,
1566 attrs,
1567 vec![
1568 vals_at,
1569 cidx_at,
1570 rptr_at,
1571 ctx.upstream,
1572 vals_a,
1573 cidx_a,
1574 rptr_a,
1575 ],
1576 );
1577 let y_fwd = ctx.fwd_map[&node.id];
1578 let raw_grad = ctx.bwd.custom_op(
1579 SPARSE_VALUES_GRAD,
1580 Vec::new(),
1581 vec![cidx_a, rptr_a, g_b, y_fwd],
1582 );
1583 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1584 let g_vals = ctx
1585 .bwd
1586 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1587 vec![(0, g_vals), (3, g_b)]
1588 }
1589}
1590
1591#[cfg(feature = "cpu")]
1592struct SparseGmresCpu;
1593#[cfg(feature = "cpu")]
1594impl CpuKernel for SparseGmresCpu {
1595 fn name(&self) -> &str {
1596 SPARSE_GMRES_SOLVE
1597 }
1598 fn execute(
1599 &self,
1600 inputs: &[CpuTensorRef<'_>],
1601 output: CpuTensorMut<'_>,
1602 attrs: &[u8],
1603 ) -> Result<(), String> {
1604 let values = inputs[0].expect_f64("gmres values")?;
1605 let col_idx = inputs[1].expect_i32("gmres col_idx")?;
1606 let row_ptr = inputs[2].expect_i32("gmres row_ptr")?;
1607 let b = inputs[3].expect_f64("gmres b")?;
1608 let out = output.expect_f64_mut("gmres out")?;
1609 let (max_iter, tol) = decode_cg_attrs(attrs)?;
1610 algos::gmres_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
1611 }
1612}
1613
1614struct SparseTransposeValuesExt;
1617
1618impl OpExtension for SparseTransposeValuesExt {
1619 fn name(&self) -> &str {
1620 SPARSE_TRANSPOSE_VALUES
1621 }
1622 fn num_inputs(&self) -> usize {
1623 5
1624 } fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1627 inputs[0].clone()
1629 }
1630
1631 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1632 let cidx_a = ctx.fwd_map[&node.inputs[1]];
1636 let rptr_a = ctx.fwd_map[&node.inputs[2]];
1637 let cidx_at = ctx.fwd_map[&node.inputs[3]];
1638 let rptr_at = ctx.fwd_map[&node.inputs[4]];
1639 let g_vals = ctx.bwd.custom_op(
1640 SPARSE_TRANSPOSE_VALUES,
1641 Vec::new(),
1642 vec![ctx.upstream, cidx_at, rptr_at, cidx_a, rptr_a],
1643 );
1644 vec![(0, g_vals)]
1645 }
1646}
1647
1648#[cfg(feature = "cpu")]
1649struct SparseTransposeValuesCpu;
1650#[cfg(feature = "cpu")]
1651impl CpuKernel for SparseTransposeValuesCpu {
1652 fn name(&self) -> &str {
1653 SPARSE_TRANSPOSE_VALUES
1654 }
1655 fn execute(
1656 &self,
1657 inputs: &[CpuTensorRef<'_>],
1658 output: CpuTensorMut<'_>,
1659 _attrs: &[u8],
1660 ) -> Result<(), String> {
1661 let values = inputs[0].expect_f64("transpose_values values")?;
1662 let col_idx = inputs[1].expect_i32("transpose_values col_idx")?;
1663 let row_ptr = inputs[2].expect_i32("transpose_values row_ptr")?;
1664 let col_idx_t = inputs[3].expect_i32("transpose_values col_idx_T")?;
1665 let row_ptr_t = inputs[4].expect_i32("transpose_values row_ptr_T")?;
1666 let out = output.expect_f64_mut("transpose_values out")?;
1667 algos::transpose_values(values, col_idx, row_ptr, col_idx_t, row_ptr_t, out)
1668 }
1669}
1670
1671struct SparsePcgExt;
1674
1675impl OpExtension for SparsePcgExt {
1676 fn name(&self) -> &str {
1677 SPARSE_PCG_SOLVE
1678 }
1679 fn num_inputs(&self) -> usize {
1680 4
1681 } fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1684 inputs[3].clone()
1685 }
1686
1687 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1688 let vals_b = ctx.fwd_map[&node.inputs[0]];
1691 let cidx_b = ctx.fwd_map[&node.inputs[1]];
1692 let rptr_b = ctx.fwd_map[&node.inputs[2]];
1693 let attrs = match &node.op {
1694 rlx_ir::Op::Custom { attrs, .. } => attrs.clone(),
1695 _ => Vec::new(),
1696 };
1697 let g_b = ctx.bwd.custom_op(
1698 SPARSE_PCG_SOLVE,
1699 attrs,
1700 vec![vals_b, cidx_b, rptr_b, ctx.upstream],
1701 );
1702 let y_fwd = ctx.fwd_map[&node.id];
1703 let raw_grad = ctx.bwd.custom_op(
1704 SPARSE_VALUES_GRAD,
1705 Vec::new(),
1706 vec![cidx_b, rptr_b, g_b, y_fwd],
1707 );
1708 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1709 let g_vals = ctx
1710 .bwd
1711 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1712 vec![(0, g_vals), (3, g_b)]
1713 }
1714}
1715
1716#[cfg(feature = "cpu")]
1717struct SparsePcgCpu;
1718#[cfg(feature = "cpu")]
1719impl CpuKernel for SparsePcgCpu {
1720 fn name(&self) -> &str {
1721 SPARSE_PCG_SOLVE
1722 }
1723 fn execute(
1724 &self,
1725 inputs: &[CpuTensorRef<'_>],
1726 output: CpuTensorMut<'_>,
1727 attrs: &[u8],
1728 ) -> Result<(), String> {
1729 let values = inputs[0].expect_f64("pcg values")?;
1730 let col_idx = inputs[1].expect_i32("pcg col_idx")?;
1731 let row_ptr = inputs[2].expect_i32("pcg row_ptr")?;
1732 let b = inputs[3].expect_f64("pcg b")?;
1733 let out = output.expect_f64_mut("pcg x")?;
1734 let (max_iter, tol) = decode_cg_attrs(attrs)?;
1735 algos::pcg_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
1736 }
1737}
1738
1739fn decode_bicgstab_attrs(attrs: &[u8]) -> Result<(u32, f64, bool), String> {
1742 if attrs.len() < 13 {
1743 return Err(format!("bicgstab: attrs len {} < 13", attrs.len()));
1744 }
1745 let max_iter = u32::from_le_bytes(attrs[0..4].try_into().unwrap());
1746 let tol = f64::from_le_bytes(attrs[4..12].try_into().unwrap());
1747 let trans = attrs[12] != 0;
1748 Ok((max_iter, tol, trans))
1749}
1750
1751struct SparseBicgstabExt;
1752
1753impl OpExtension for SparseBicgstabExt {
1754 fn name(&self) -> &str {
1755 SPARSE_BICGSTAB_SOLVE
1756 }
1757 fn num_inputs(&self) -> usize {
1758 4
1759 } fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1761 inputs[3].clone()
1762 }
1763 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1764 let vals = ctx.fwd_map[&node.inputs[0]];
1768 let cidx = ctx.fwd_map[&node.inputs[1]];
1769 let rptr = ctx.fwd_map[&node.inputs[2]];
1770 let attrs = match &node.op {
1771 Op::Custom { attrs, .. } => attrs.clone(),
1772 _ => Vec::new(),
1773 };
1774 let (max_iter, tol, trans) = decode_bicgstab_attrs(&attrs).unwrap_or((1, 1e-9, false));
1775 let mut adj_attrs = Vec::with_capacity(13);
1776 adj_attrs.extend_from_slice(&max_iter.to_le_bytes());
1777 adj_attrs.extend_from_slice(&tol.to_le_bytes());
1778 adj_attrs.push(if !trans { 1 } else { 0 });
1779 let g_b = ctx.bwd.custom_op(
1780 SPARSE_BICGSTAB_SOLVE,
1781 adj_attrs,
1782 vec![vals, cidx, rptr, ctx.upstream],
1783 );
1784 let y_fwd = ctx.fwd_map[&node.id];
1785 let raw_grad =
1786 ctx.bwd
1787 .custom_op(SPARSE_VALUES_GRAD, Vec::new(), vec![cidx, rptr, g_b, y_fwd]);
1788 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1789 let g_vals = ctx
1790 .bwd
1791 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1792 vec![(0, g_vals), (3, g_b)]
1793 }
1794}
1795
1796#[cfg(feature = "cpu")]
1797struct SparseBicgstabCpu;
1798#[cfg(feature = "cpu")]
1799impl CpuKernel for SparseBicgstabCpu {
1800 fn name(&self) -> &str {
1801 SPARSE_BICGSTAB_SOLVE
1802 }
1803 fn execute(
1804 &self,
1805 inputs: &[CpuTensorRef<'_>],
1806 output: CpuTensorMut<'_>,
1807 attrs: &[u8],
1808 ) -> Result<(), String> {
1809 let values = inputs[0].expect_f64("bicgstab values")?;
1810 let col_idx = inputs[1].expect_i32("bicgstab col_idx")?;
1811 let row_ptr = inputs[2].expect_i32("bicgstab row_ptr")?;
1812 let b = inputs[3].expect_f64("bicgstab b")?;
1813 let out = output.expect_f64_mut("bicgstab x")?;
1814 let (max_iter, tol, trans) = decode_bicgstab_attrs(attrs)?;
1815 algos::bicgstab(values, col_idx, row_ptr, b, out, max_iter, tol, trans)
1816 }
1817}
1818
1819struct SparseIluPcgExt;
1822
1823impl OpExtension for SparseIluPcgExt {
1824 fn name(&self) -> &str {
1825 SPARSE_ILU_PCG_SOLVE
1826 }
1827 fn num_inputs(&self) -> usize {
1828 4
1829 }
1830 fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1831 inputs[3].clone()
1832 }
1833 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1834 let vals = ctx.fwd_map[&node.inputs[0]];
1836 let cidx = ctx.fwd_map[&node.inputs[1]];
1837 let rptr = ctx.fwd_map[&node.inputs[2]];
1838 let attrs = match &node.op {
1839 Op::Custom { attrs, .. } => attrs.clone(),
1840 _ => Vec::new(),
1841 };
1842 let g_b = ctx.bwd.custom_op(
1843 SPARSE_ILU_PCG_SOLVE,
1844 attrs,
1845 vec![vals, cidx, rptr, ctx.upstream],
1846 );
1847 let y_fwd = ctx.fwd_map[&node.id];
1848 let raw_grad =
1849 ctx.bwd
1850 .custom_op(SPARSE_VALUES_GRAD, Vec::new(), vec![cidx, rptr, g_b, y_fwd]);
1851 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1852 let g_vals = ctx
1853 .bwd
1854 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1855 vec![(0, g_vals), (3, g_b)]
1856 }
1857}
1858
1859#[cfg(feature = "cpu")]
1860struct SparseIluPcgCpu;
1861#[cfg(feature = "cpu")]
1862impl CpuKernel for SparseIluPcgCpu {
1863 fn name(&self) -> &str {
1864 SPARSE_ILU_PCG_SOLVE
1865 }
1866 fn execute(
1867 &self,
1868 inputs: &[CpuTensorRef<'_>],
1869 output: CpuTensorMut<'_>,
1870 attrs: &[u8],
1871 ) -> Result<(), String> {
1872 let values = inputs[0].expect_f64("ilu_pcg values")?;
1873 let col_idx = inputs[1].expect_i32("ilu_pcg col_idx")?;
1874 let row_ptr = inputs[2].expect_i32("ilu_pcg row_ptr")?;
1875 let b = inputs[3].expect_f64("ilu_pcg b")?;
1876 let out = output.expect_f64_mut("ilu_pcg x")?;
1877 let (max_iter, tol) = decode_cg_attrs(attrs)?;
1878 algos::ilu_pcg_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
1879 }
1880}
1881
1882struct SparseCholeskyExt;
1885
1886impl OpExtension for SparseCholeskyExt {
1887 fn name(&self) -> &str {
1888 SPARSE_CHOLESKY_SOLVE
1889 }
1890 fn num_inputs(&self) -> usize {
1891 4
1892 } fn infer_shape(&self, inputs: &[&Shape], _attrs: &[u8]) -> Shape {
1894 inputs[3].clone()
1895 }
1896 fn vjp(&self, node: &Node, ctx: &mut VjpContext) -> Vec<(usize, NodeId)> {
1897 let vals = ctx.fwd_map[&node.inputs[0]];
1900 let cidx = ctx.fwd_map[&node.inputs[1]];
1901 let rptr = ctx.fwd_map[&node.inputs[2]];
1902 let g_b = ctx.bwd.custom_op(
1903 SPARSE_CHOLESKY_SOLVE,
1904 Vec::new(),
1905 vec![vals, cidx, rptr, ctx.upstream],
1906 );
1907 let y_fwd = ctx.fwd_map[&node.id];
1908 let raw_grad =
1909 ctx.bwd
1910 .custom_op(SPARSE_VALUES_GRAD, Vec::new(), vec![cidx, rptr, g_b, y_fwd]);
1911 let raw_shape = ctx.bwd.node(raw_grad).shape.clone();
1912 let g_vals = ctx
1913 .bwd
1914 .activation(rlx_ir::op::Activation::Neg, raw_grad, raw_shape);
1915 vec![(0, g_vals), (3, g_b)]
1916 }
1917}
1918
1919#[cfg(feature = "cpu")]
1920struct SparseCholeskyCpu;
1921#[cfg(feature = "cpu")]
1922impl CpuKernel for SparseCholeskyCpu {
1923 fn name(&self) -> &str {
1924 SPARSE_CHOLESKY_SOLVE
1925 }
1926 fn execute(
1927 &self,
1928 inputs: &[CpuTensorRef<'_>],
1929 output: CpuTensorMut<'_>,
1930 _attrs: &[u8],
1931 ) -> Result<(), String> {
1932 let values = inputs[0].expect_f64("chol values")?;
1933 let col_idx = inputs[1].expect_i32("chol col_idx")?;
1934 let row_ptr = inputs[2].expect_i32("chol row_ptr")?;
1935 let b = inputs[3].expect_f64("chol b")?;
1936 let out = output.expect_f64_mut("chol x")?;
1937 algos::cholesky_solve(values, col_idx, row_ptr, b, out)
1938 }
1939}
1940
1941fn decode_lsqr_attrs(attrs: &[u8]) -> Result<(u32, f64, u32), String> {
1944 if attrs.len() < 16 {
1945 return Err(format!("lsqr: attrs len {} < 16", attrs.len()));
1946 }
1947 let max_iter = u32::from_le_bytes(attrs[0..4].try_into().unwrap());
1948 let tol = f64::from_le_bytes(attrs[4..12].try_into().unwrap());
1949 let n_cols = u32::from_le_bytes(attrs[12..16].try_into().unwrap());
1950 Ok((max_iter, tol, n_cols))
1951}
1952
1953struct SparseLsqrExt;
1954
1955impl OpExtension for SparseLsqrExt {
1956 fn name(&self) -> &str {
1957 SPARSE_LSQR_SOLVE
1958 }
1959 fn num_inputs(&self) -> usize {
1960 4
1961 } fn infer_shape(&self, inputs: &[&Shape], attrs: &[u8]) -> Shape {
1963 let (_, _, n_cols) =
1964 decode_lsqr_attrs(attrs).expect("lsqr: attrs must encode (max_iter, tol, n_cols)");
1965 Shape::new(&[n_cols as usize], inputs[3].dtype())
1966 }
1967 }
1969
1970#[cfg(feature = "cpu")]
1971struct SparseLsqrCpu;
1972#[cfg(feature = "cpu")]
1973impl CpuKernel for SparseLsqrCpu {
1974 fn name(&self) -> &str {
1975 SPARSE_LSQR_SOLVE
1976 }
1977 fn execute(
1978 &self,
1979 inputs: &[CpuTensorRef<'_>],
1980 output: CpuTensorMut<'_>,
1981 attrs: &[u8],
1982 ) -> Result<(), String> {
1983 let values = inputs[0].expect_f64("lsqr values")?;
1984 let col_idx = inputs[1].expect_i32("lsqr col_idx")?;
1985 let row_ptr = inputs[2].expect_i32("lsqr row_ptr")?;
1986 let b = inputs[3].expect_f64("lsqr b")?;
1987 let out = output.expect_f64_mut("lsqr x")?;
1988 let (max_iter, tol, n_cols) = decode_lsqr_attrs(attrs)?;
1989 algos::lsqr_solve(
1990 values,
1991 col_idx,
1992 row_ptr,
1993 b,
1994 out,
1995 max_iter,
1996 tol,
1997 n_cols as usize,
1998 )
1999 }
2000}
2001
2002#[cfg(feature = "cpu")]
2011pub fn spgemm_csr(
2012 a_values: &[f64],
2013 a_col_idx: &[i32],
2014 a_row_ptr: &[i32],
2015 b_values: &[f64],
2016 b_col_idx: &[i32],
2017 b_row_ptr: &[i32],
2018 m: usize,
2019 k: usize,
2020 n: usize,
2021) -> Result<(Vec<f64>, Vec<i32>, Vec<i32>), String> {
2022 algos::spgemm_csr(
2023 a_values, a_col_idx, a_row_ptr, b_values, b_col_idx, b_row_ptr, m, k, n,
2024 )
2025}
2026
2027pub fn csr_transpose_pattern(
2035 col_idx: &[i32],
2036 row_ptr: &[i32],
2037 n_rows: usize,
2038 n_cols: usize,
2039) -> (Vec<i32>, Vec<i32>) {
2040 let nnz = col_idx.len();
2041 let mut t_count = vec![0i32; n_cols];
2043 for &c in col_idx {
2044 t_count[c as usize] += 1;
2045 }
2046 let mut t_row_ptr = vec![0i32; n_cols + 1];
2047 for r in 0..n_cols {
2048 t_row_ptr[r + 1] = t_row_ptr[r] + t_count[r];
2049 }
2050 let mut t_col_idx = vec![0i32; nnz];
2051 let mut cursor = t_row_ptr.clone();
2052 for r in 0..n_rows {
2053 for k in row_ptr[r] as usize..row_ptr[r + 1] as usize {
2054 let c = col_idx[k] as usize;
2055 let pos = cursor[c] as usize;
2056 t_col_idx[pos] = r as i32;
2057 cursor[c] += 1;
2058 }
2059 }
2060 (t_col_idx, t_row_ptr)
2061}
2062
2063#[derive(Clone, Copy, Debug)]
2072pub struct SparseTensor {
2073 pub values: NodeId,
2075 pub col_idx: NodeId,
2077 pub row_ptr: NodeId,
2079 pub n_rows: usize,
2081 pub n_cols: usize,
2083}
2084
2085impl SparseTensor {
2086 pub fn from_csr(
2090 values: NodeId,
2091 col_idx: NodeId,
2092 row_ptr: NodeId,
2093 n_rows: usize,
2094 n_cols: usize,
2095 ) -> Self {
2096 Self {
2097 values,
2098 col_idx,
2099 row_ptr,
2100 n_rows,
2101 n_cols,
2102 }
2103 }
2104
2105 pub fn mat_vec(&self, g: &mut Graph, x: NodeId) -> NodeId {
2107 g.custom_op(
2108 SPARSE_MAT_VEC,
2109 Vec::new(),
2110 vec![self.values, self.col_idx, self.row_ptr, x],
2111 )
2112 }
2113
2114 pub fn solve(&self, g: &mut Graph, b: NodeId) -> NodeId {
2116 assert_eq!(
2117 self.n_rows, self.n_cols,
2118 "SparseTensor::solve requires a square matrix"
2119 );
2120 g.custom_op(
2121 SPARSE_LU_SOLVE,
2122 Vec::new(),
2123 vec![self.values, self.col_idx, self.row_ptr, b],
2124 )
2125 }
2126
2127 pub fn cg_solve(&self, g: &mut Graph, b: NodeId, max_iter: u32, tol: f64) -> NodeId {
2130 assert_eq!(
2131 self.n_rows, self.n_cols,
2132 "SparseTensor::cg_solve requires a square matrix"
2133 );
2134 g.custom_op(
2135 SPARSE_CG_SOLVE,
2136 encode_cg_attrs(max_iter, tol),
2137 vec![self.values, self.col_idx, self.row_ptr, b],
2138 )
2139 }
2140
2141 pub fn solve_general(&self, g: &mut Graph, b: NodeId, adjoint: &SparseTensor) -> NodeId {
2149 assert_eq!(
2150 self.n_rows, self.n_cols,
2151 "SparseTensor::solve_general requires a square matrix"
2152 );
2153 assert_eq!(
2154 adjoint.n_rows, self.n_cols,
2155 "adjoint shape mismatch: A is {}×{}, Aᵀ should be {}×{}",
2156 self.n_rows, self.n_cols, self.n_cols, self.n_rows
2157 );
2158 g.custom_op(
2159 SPARSE_LU_SOLVE_GENERAL,
2160 Vec::new(),
2161 vec![
2162 self.values,
2163 self.col_idx,
2164 self.row_ptr,
2165 b,
2166 adjoint.values,
2167 adjoint.col_idx,
2168 adjoint.row_ptr,
2169 ],
2170 )
2171 }
2172
2173 pub fn pcg_solve(&self, g: &mut Graph, b: NodeId, max_iter: u32, tol: f64) -> NodeId {
2180 assert_eq!(
2181 self.n_rows, self.n_cols,
2182 "SparseTensor::pcg_solve requires a square matrix"
2183 );
2184 g.custom_op(
2185 SPARSE_PCG_SOLVE,
2186 encode_cg_attrs(max_iter, tol),
2187 vec![self.values, self.col_idx, self.row_ptr, b],
2188 )
2189 }
2190
2191 pub fn transpose_values(&self, g: &mut Graph, col_idx_t: NodeId, row_ptr_t: NodeId) -> NodeId {
2197 g.custom_op(
2198 SPARSE_TRANSPOSE_VALUES,
2199 Vec::new(),
2200 vec![
2201 self.values,
2202 self.col_idx,
2203 self.row_ptr,
2204 col_idx_t,
2205 row_ptr_t,
2206 ],
2207 )
2208 }
2209
2210 pub fn cholesky_solve(&self, g: &mut Graph, b: NodeId) -> NodeId {
2215 assert_eq!(
2216 self.n_rows, self.n_cols,
2217 "SparseTensor::cholesky_solve requires a square matrix"
2218 );
2219 g.custom_op(
2220 SPARSE_CHOLESKY_SOLVE,
2221 Vec::new(),
2222 vec![self.values, self.col_idx, self.row_ptr, b],
2223 )
2224 }
2225
2226 pub fn lsqr_solve(&self, g: &mut Graph, b: NodeId, max_iter: u32, tol: f64) -> NodeId {
2231 let mut attrs = Vec::with_capacity(16);
2232 attrs.extend_from_slice(&max_iter.to_le_bytes());
2233 attrs.extend_from_slice(&tol.to_le_bytes());
2234 attrs.extend_from_slice(&(self.n_cols as u32).to_le_bytes());
2235 g.custom_op(
2236 SPARSE_LSQR_SOLVE,
2237 attrs,
2238 vec![self.values, self.col_idx, self.row_ptr, b],
2239 )
2240 }
2241
2242 pub fn bicgstab_solve(&self, g: &mut Graph, b: NodeId, max_iter: u32, tol: f64) -> NodeId {
2246 assert_eq!(
2247 self.n_rows, self.n_cols,
2248 "SparseTensor::bicgstab_solve requires a square matrix"
2249 );
2250 let mut attrs = Vec::with_capacity(13);
2251 attrs.extend_from_slice(&max_iter.to_le_bytes());
2252 attrs.extend_from_slice(&tol.to_le_bytes());
2253 attrs.push(0); g.custom_op(
2255 SPARSE_BICGSTAB_SOLVE,
2256 attrs,
2257 vec![self.values, self.col_idx, self.row_ptr, b],
2258 )
2259 }
2260
2261 pub fn ilu_pcg_solve(&self, g: &mut Graph, b: NodeId, max_iter: u32, tol: f64) -> NodeId {
2266 assert_eq!(
2267 self.n_rows, self.n_cols,
2268 "SparseTensor::ilu_pcg_solve requires a square matrix"
2269 );
2270 g.custom_op(
2271 SPARSE_ILU_PCG_SOLVE,
2272 encode_cg_attrs(max_iter, tol),
2273 vec![self.values, self.col_idx, self.row_ptr, b],
2274 )
2275 }
2276
2277 pub fn gmres_solve(
2281 &self,
2282 g: &mut Graph,
2283 b: NodeId,
2284 max_iter: u32,
2285 tol: f64,
2286 adjoint: &SparseTensor,
2287 ) -> NodeId {
2288 assert_eq!(
2289 self.n_rows, self.n_cols,
2290 "SparseTensor::gmres_solve requires a square matrix"
2291 );
2292 assert_eq!(adjoint.n_rows, self.n_cols, "adjoint shape mismatch");
2293 g.custom_op(
2294 SPARSE_GMRES_SOLVE,
2295 encode_cg_attrs(max_iter, tol),
2296 vec![
2297 self.values,
2298 self.col_idx,
2299 self.row_ptr,
2300 b,
2301 adjoint.values,
2302 adjoint.col_idx,
2303 adjoint.row_ptr,
2304 ],
2305 )
2306 }
2307}
2308
2309#[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
2321mod metal_kernels {
2322 use super::*;
2323 use rlx_ir::DType;
2324 use rlx_metal::op_registry::MetalKernel;
2325
2326 unsafe fn typed<'a, T: Copy>(
2331 bytes: &'a [u8],
2332 shape: &rlx_ir::Shape,
2333 want: DType,
2334 role: &str,
2335 ) -> Result<&'a [T], String> {
2336 if shape.dtype() != want {
2337 return Err(format!(
2338 "{role}: expected {want:?}, got {:?}",
2339 shape.dtype()
2340 ));
2341 }
2342 let n = shape
2343 .num_elements()
2344 .ok_or_else(|| format!("{role}: dynamic shape not supported"))?;
2345 let need = n * std::mem::size_of::<T>();
2346 if bytes.len() < need {
2347 return Err(format!("{role}: bytes {} < need {need}", bytes.len()));
2348 }
2349 Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, n) })
2350 }
2351
2352 unsafe fn typed_mut<'a, T: Copy>(
2353 bytes: &'a mut [u8],
2354 shape: &rlx_ir::Shape,
2355 want: DType,
2356 role: &str,
2357 ) -> Result<&'a mut [T], String> {
2358 if shape.dtype() != want {
2359 return Err(format!(
2360 "{role}: expected {want:?}, got {:?}",
2361 shape.dtype()
2362 ));
2363 }
2364 let n = shape
2365 .num_elements()
2366 .ok_or_else(|| format!("{role}: dynamic shape not supported"))?;
2367 let need = n * std::mem::size_of::<T>();
2368 if bytes.len() < need {
2369 return Err(format!("{role}: bytes {} < need {need}", bytes.len()));
2370 }
2371 Ok(unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut T, n) })
2372 }
2373
2374 #[derive(Debug)]
2375 pub(super) struct SparseLuMetal;
2376 impl MetalKernel for SparseLuMetal {
2377 fn name(&self) -> &str {
2378 SPARSE_LU_SOLVE
2379 }
2380 fn execute(
2381 &self,
2382 inputs: &[(&[u8], &rlx_ir::Shape)],
2383 output: (&mut [u8], &rlx_ir::Shape),
2384 _attrs: &[u8],
2385 ) -> Result<(), String> {
2386 unsafe {
2387 let values = typed::<f64>(inputs[0].0, inputs[0].1, DType::F64, "values")?;
2388 let col_idx = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "col_idx")?;
2389 let row_ptr = typed::<i32>(inputs[2].0, inputs[2].1, DType::I32, "row_ptr")?;
2390 let b = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "b")?;
2391 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2392 algos::lu_solve(values, col_idx, row_ptr, b, out)
2393 }
2394 }
2395 }
2396
2397 #[derive(Debug)]
2398 pub(super) struct SparseMatVecMetal;
2399 impl MetalKernel for SparseMatVecMetal {
2400 fn name(&self) -> &str {
2401 SPARSE_MAT_VEC
2402 }
2403 fn execute(
2404 &self,
2405 inputs: &[(&[u8], &rlx_ir::Shape)],
2406 output: (&mut [u8], &rlx_ir::Shape),
2407 _attrs: &[u8],
2408 ) -> Result<(), String> {
2409 unsafe {
2410 let values = typed::<f64>(inputs[0].0, inputs[0].1, DType::F64, "values")?;
2411 let col_idx = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "col_idx")?;
2412 let row_ptr = typed::<i32>(inputs[2].0, inputs[2].1, DType::I32, "row_ptr")?;
2413 let x = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "x")?;
2414 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2415 algos::mat_vec(values, col_idx, row_ptr, x, out)
2416 }
2417 }
2418 }
2419
2420 #[derive(Debug)]
2421 pub(super) struct SparseCgMetal;
2422 impl MetalKernel for SparseCgMetal {
2423 fn name(&self) -> &str {
2424 SPARSE_CG_SOLVE
2425 }
2426 fn execute(
2427 &self,
2428 inputs: &[(&[u8], &rlx_ir::Shape)],
2429 output: (&mut [u8], &rlx_ir::Shape),
2430 attrs: &[u8],
2431 ) -> Result<(), String> {
2432 let (max_iter, tol) = decode_cg_attrs(attrs)?;
2433 unsafe {
2434 let values = typed::<f64>(inputs[0].0, inputs[0].1, DType::F64, "values")?;
2435 let col_idx = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "col_idx")?;
2436 let row_ptr = typed::<i32>(inputs[2].0, inputs[2].1, DType::I32, "row_ptr")?;
2437 let b = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "b")?;
2438 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2439 algos::cg_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
2440 }
2441 }
2442 }
2443
2444 #[derive(Debug)]
2445 pub(super) struct SparseValuesGradMetal;
2446 impl MetalKernel for SparseValuesGradMetal {
2447 fn name(&self) -> &str {
2448 SPARSE_VALUES_GRAD
2449 }
2450 fn execute(
2451 &self,
2452 inputs: &[(&[u8], &rlx_ir::Shape)],
2453 output: (&mut [u8], &rlx_ir::Shape),
2454 _attrs: &[u8],
2455 ) -> Result<(), String> {
2456 unsafe {
2457 let col_idx = typed::<i32>(inputs[0].0, inputs[0].1, DType::I32, "col_idx")?;
2458 let row_ptr = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "row_ptr")?;
2459 let u = typed::<f64>(inputs[2].0, inputs[2].1, DType::F64, "u")?;
2460 let v = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "v")?;
2461 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2462 algos::values_grad(col_idx, row_ptr, u, v, out)
2463 }
2464 }
2465 }
2466
2467 #[derive(Debug)]
2468 pub(super) struct SparseLuGeneralMetal;
2469 impl MetalKernel for SparseLuGeneralMetal {
2470 fn name(&self) -> &str {
2471 SPARSE_LU_SOLVE_GENERAL
2472 }
2473 fn execute(
2474 &self,
2475 inputs: &[(&[u8], &rlx_ir::Shape)],
2476 output: (&mut [u8], &rlx_ir::Shape),
2477 _attrs: &[u8],
2478 ) -> Result<(), String> {
2479 unsafe {
2482 let values = typed::<f64>(inputs[0].0, inputs[0].1, DType::F64, "values")?;
2483 let col_idx = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "col_idx")?;
2484 let row_ptr = typed::<i32>(inputs[2].0, inputs[2].1, DType::I32, "row_ptr")?;
2485 let b = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "b")?;
2486 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2487 algos::lu_solve(values, col_idx, row_ptr, b, out)
2488 }
2489 }
2490 }
2491
2492 #[derive(Debug)]
2493 pub(super) struct SparseGmresMetal;
2494 impl MetalKernel for SparseGmresMetal {
2495 fn name(&self) -> &str {
2496 SPARSE_GMRES_SOLVE
2497 }
2498 fn execute(
2499 &self,
2500 inputs: &[(&[u8], &rlx_ir::Shape)],
2501 output: (&mut [u8], &rlx_ir::Shape),
2502 attrs: &[u8],
2503 ) -> Result<(), String> {
2504 let (max_iter, tol) = decode_cg_attrs(attrs)?;
2505 unsafe {
2506 let values = typed::<f64>(inputs[0].0, inputs[0].1, DType::F64, "values")?;
2507 let col_idx = typed::<i32>(inputs[1].0, inputs[1].1, DType::I32, "col_idx")?;
2508 let row_ptr = typed::<i32>(inputs[2].0, inputs[2].1, DType::I32, "row_ptr")?;
2509 let b = typed::<f64>(inputs[3].0, inputs[3].1, DType::F64, "b")?;
2510 let out = typed_mut::<f64>(output.0, output.1, DType::F64, "out")?;
2511 algos::gmres_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
2512 }
2513 }
2514 }
2515}
2516
2517#[cfg(all(feature = "mlx", target_os = "macos"))]
2534mod mlx_kernels {
2535 use super::*;
2536 use rlx_ir::DType;
2537 use rlx_mlx::array::{Array, MlxError};
2538 use rlx_mlx::op_registry::MlxKernel;
2539
2540 fn shape_dims_static(s: &rlx_ir::Shape) -> Result<Vec<usize>, MlxError> {
2541 s.dims()
2542 .iter()
2543 .map(|d| match d {
2544 rlx_ir::Dim::Static(n) => Ok(*n),
2545 _ => Err(MlxError(
2546 "rlx-sparse mlx kernel: dynamic shape not supported".into(),
2547 )),
2548 })
2549 .collect()
2550 }
2551
2552 fn bytes_to_f64(b: &[u8]) -> Vec<f64> {
2554 b.chunks_exact(8)
2555 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
2556 .collect()
2557 }
2558 fn bytes_to_i32(b: &[u8]) -> Vec<i32> {
2559 b.chunks_exact(4)
2560 .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
2561 .collect()
2562 }
2563 fn f64_to_bytes(xs: &[f64]) -> Vec<u8> {
2564 let mut out = Vec::with_capacity(xs.len() * 8);
2565 for x in xs {
2566 out.extend_from_slice(&x.to_le_bytes());
2567 }
2568 out
2569 }
2570
2571 fn run_lu(inputs: &[&Array], output_shape: &rlx_ir::Shape) -> Result<Array, MlxError> {
2572 let values = bytes_to_f64(&inputs[0].to_bytes()?);
2573 let col_idx = bytes_to_i32(&inputs[1].to_bytes()?);
2574 let row_ptr = bytes_to_i32(&inputs[2].to_bytes()?);
2575 let b = bytes_to_f64(&inputs[3].to_bytes()?);
2576 let mut out = vec![0f64; b.len()];
2577 algos::lu_solve(&values, &col_idx, &row_ptr, &b, &mut out).map_err(MlxError)?;
2578 let dims = shape_dims_static(output_shape)?;
2579 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2580 }
2581
2582 fn run_mat_vec(inputs: &[&Array], output_shape: &rlx_ir::Shape) -> Result<Array, MlxError> {
2583 let values = bytes_to_f64(&inputs[0].to_bytes()?);
2584 let col_idx = bytes_to_i32(&inputs[1].to_bytes()?);
2585 let row_ptr = bytes_to_i32(&inputs[2].to_bytes()?);
2586 let x = bytes_to_f64(&inputs[3].to_bytes()?);
2587 let mut out = vec![0f64; x.len()];
2588 algos::mat_vec(&values, &col_idx, &row_ptr, &x, &mut out).map_err(MlxError)?;
2589 let dims = shape_dims_static(output_shape)?;
2590 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2591 }
2592
2593 fn run_cg(
2594 inputs: &[&Array],
2595 output_shape: &rlx_ir::Shape,
2596 attrs: &[u8],
2597 ) -> Result<Array, MlxError> {
2598 let (max_iter, tol) = decode_cg_attrs(attrs).map_err(MlxError)?;
2599 let values = bytes_to_f64(&inputs[0].to_bytes()?);
2600 let col_idx = bytes_to_i32(&inputs[1].to_bytes()?);
2601 let row_ptr = bytes_to_i32(&inputs[2].to_bytes()?);
2602 let b = bytes_to_f64(&inputs[3].to_bytes()?);
2603 let mut out = vec![0f64; b.len()];
2604 algos::cg_solve(&values, &col_idx, &row_ptr, &b, &mut out, max_iter, tol)
2605 .map_err(MlxError)?;
2606 let dims = shape_dims_static(output_shape)?;
2607 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2608 }
2609
2610 pub(super) struct SparseLuMlx;
2611 impl MlxKernel for SparseLuMlx {
2612 fn name(&self) -> &str {
2613 SPARSE_LU_SOLVE
2614 }
2615 fn execute(
2616 &self,
2617 inputs: &[&Array],
2618 out_shape: &rlx_ir::Shape,
2619 _attrs: &[u8],
2620 ) -> Result<Array, MlxError> {
2621 run_lu(inputs, out_shape)
2622 }
2623 }
2624 pub(super) struct SparseMatVecMlx;
2625 impl MlxKernel for SparseMatVecMlx {
2626 fn name(&self) -> &str {
2627 SPARSE_MAT_VEC
2628 }
2629 fn execute(
2630 &self,
2631 inputs: &[&Array],
2632 out_shape: &rlx_ir::Shape,
2633 _attrs: &[u8],
2634 ) -> Result<Array, MlxError> {
2635 run_mat_vec(inputs, out_shape)
2636 }
2637 }
2638 pub(super) struct SparseCgMlx;
2639 impl MlxKernel for SparseCgMlx {
2640 fn name(&self) -> &str {
2641 SPARSE_CG_SOLVE
2642 }
2643 fn execute(
2644 &self,
2645 inputs: &[&Array],
2646 out_shape: &rlx_ir::Shape,
2647 attrs: &[u8],
2648 ) -> Result<Array, MlxError> {
2649 run_cg(inputs, out_shape, attrs)
2650 }
2651 }
2652
2653 fn run_values_grad(inputs: &[&Array], output_shape: &rlx_ir::Shape) -> Result<Array, MlxError> {
2654 let col_idx = bytes_to_i32(&inputs[0].to_bytes()?);
2655 let row_ptr = bytes_to_i32(&inputs[1].to_bytes()?);
2656 let u = bytes_to_f64(&inputs[2].to_bytes()?);
2657 let v = bytes_to_f64(&inputs[3].to_bytes()?);
2658 let mut out = vec![0f64; col_idx.len()];
2659 algos::values_grad(&col_idx, &row_ptr, &u, &v, &mut out).map_err(MlxError)?;
2660 let dims = shape_dims_static(output_shape)?;
2661 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2662 }
2663
2664 fn run_lu_general(inputs: &[&Array], output_shape: &rlx_ir::Shape) -> Result<Array, MlxError> {
2665 let values = bytes_to_f64(&inputs[0].to_bytes()?);
2668 let col_idx = bytes_to_i32(&inputs[1].to_bytes()?);
2669 let row_ptr = bytes_to_i32(&inputs[2].to_bytes()?);
2670 let b = bytes_to_f64(&inputs[3].to_bytes()?);
2671 let mut out = vec![0f64; b.len()];
2672 algos::lu_solve(&values, &col_idx, &row_ptr, &b, &mut out).map_err(MlxError)?;
2673 let dims = shape_dims_static(output_shape)?;
2674 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2675 }
2676
2677 fn run_gmres(
2678 inputs: &[&Array],
2679 output_shape: &rlx_ir::Shape,
2680 attrs: &[u8],
2681 ) -> Result<Array, MlxError> {
2682 let (max_iter, tol) = decode_cg_attrs(attrs).map_err(MlxError)?;
2683 let values = bytes_to_f64(&inputs[0].to_bytes()?);
2684 let col_idx = bytes_to_i32(&inputs[1].to_bytes()?);
2685 let row_ptr = bytes_to_i32(&inputs[2].to_bytes()?);
2686 let b = bytes_to_f64(&inputs[3].to_bytes()?);
2687 let mut out = vec![0f64; b.len()];
2688 algos::gmres_solve(&values, &col_idx, &row_ptr, &b, &mut out, max_iter, tol)
2689 .map_err(MlxError)?;
2690 let dims = shape_dims_static(output_shape)?;
2691 Array::from_bytes(&f64_to_bytes(&out), &dims, DType::F64)
2692 }
2693
2694 pub(super) struct SparseValuesGradMlx;
2695 impl MlxKernel for SparseValuesGradMlx {
2696 fn name(&self) -> &str {
2697 SPARSE_VALUES_GRAD
2698 }
2699 fn execute(
2700 &self,
2701 inputs: &[&Array],
2702 out_shape: &rlx_ir::Shape,
2703 _attrs: &[u8],
2704 ) -> Result<Array, MlxError> {
2705 run_values_grad(inputs, out_shape)
2706 }
2707 }
2708 pub(super) struct SparseLuGeneralMlx;
2709 impl MlxKernel for SparseLuGeneralMlx {
2710 fn name(&self) -> &str {
2711 SPARSE_LU_SOLVE_GENERAL
2712 }
2713 fn execute(
2714 &self,
2715 inputs: &[&Array],
2716 out_shape: &rlx_ir::Shape,
2717 _attrs: &[u8],
2718 ) -> Result<Array, MlxError> {
2719 run_lu_general(inputs, out_shape)
2720 }
2721 }
2722 pub(super) struct SparseGmresMlx;
2723 impl MlxKernel for SparseGmresMlx {
2724 fn name(&self) -> &str {
2725 SPARSE_GMRES_SOLVE
2726 }
2727 fn execute(
2728 &self,
2729 inputs: &[&Array],
2730 out_shape: &rlx_ir::Shape,
2731 attrs: &[u8],
2732 ) -> Result<Array, MlxError> {
2733 run_gmres(inputs, out_shape, attrs)
2734 }
2735 }
2736}
2737
2738pub fn cg_solve(
2746 values: &[f64],
2747 col_idx: &[i32],
2748 row_ptr: &[i32],
2749 b: &[f64],
2750 out: &mut [f64],
2751 max_iter: u32,
2752 tol: f64,
2753) -> Result<(), String> {
2754 algos::cg_solve(values, col_idx, row_ptr, b, out, max_iter, tol)
2755}
2756
2757pub fn register() {
2758 register_op(Arc::new(SparseLuExt));
2759 register_op(Arc::new(SparseMatVecExt));
2760 register_op(Arc::new(SparseCgExt));
2761 register_op(Arc::new(SparseValuesGradExt));
2762 register_op(Arc::new(SparseLuGeneralExt));
2763 register_op(Arc::new(SparseGmresExt));
2764 register_op(Arc::new(SparseTransposeValuesExt));
2765 register_op(Arc::new(SparsePcgExt));
2766 register_op(Arc::new(SparseBicgstabExt));
2767 register_op(Arc::new(SparseIluPcgExt));
2768 register_op(Arc::new(SparseCholeskyExt));
2769 register_op(Arc::new(SparseLsqrExt));
2770
2771 #[cfg(feature = "cpu")]
2772 {
2773 register_cpu_kernel(Arc::new(SparseLuCpu));
2774 register_cpu_kernel(Arc::new(SparseMatVecCpu));
2775 register_cpu_kernel(Arc::new(SparseCgCpu));
2776 register_cpu_kernel(Arc::new(SparseValuesGradCpu));
2777 register_cpu_kernel(Arc::new(SparseLuGeneralCpu));
2778 register_cpu_kernel(Arc::new(SparseGmresCpu));
2779 register_cpu_kernel(Arc::new(SparseTransposeValuesCpu));
2780 register_cpu_kernel(Arc::new(SparsePcgCpu));
2781 register_cpu_kernel(Arc::new(SparseBicgstabCpu));
2782 register_cpu_kernel(Arc::new(SparseIluPcgCpu));
2783 register_cpu_kernel(Arc::new(SparseCholeskyCpu));
2784 register_cpu_kernel(Arc::new(SparseLsqrCpu));
2785 }
2786
2787 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
2788 {
2789 use rlx_metal::op_registry::register_metal_kernel;
2790 register_metal_kernel(Arc::new(metal_kernels::SparseLuMetal));
2791 register_metal_kernel(Arc::new(metal_kernels::SparseMatVecMetal));
2792 register_metal_kernel(Arc::new(metal_kernels::SparseCgMetal));
2793 register_metal_kernel(Arc::new(metal_kernels::SparseValuesGradMetal));
2794 register_metal_kernel(Arc::new(metal_kernels::SparseLuGeneralMetal));
2795 register_metal_kernel(Arc::new(metal_kernels::SparseGmresMetal));
2796 }
2797
2798 #[cfg(all(feature = "mlx", target_os = "macos"))]
2799 {
2800 use rlx_mlx::op_registry::register_mlx_kernel;
2801 register_mlx_kernel(Arc::new(mlx_kernels::SparseLuMlx));
2802 register_mlx_kernel(Arc::new(mlx_kernels::SparseMatVecMlx));
2803 register_mlx_kernel(Arc::new(mlx_kernels::SparseCgMlx));
2804 register_mlx_kernel(Arc::new(mlx_kernels::SparseValuesGradMlx));
2805 register_mlx_kernel(Arc::new(mlx_kernels::SparseLuGeneralMlx));
2806 register_mlx_kernel(Arc::new(mlx_kernels::SparseGmresMlx));
2807 }
2808}